gonum.org/v1/gonum@v0.14.0/dsp/window/cmd/leakage/leakage.go (about)

     1  // Copyright ©2021 The Gonum Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // The leakage program provides summary characteristics and a plot
     6  // of spectral response for window functions or csv input. It is intended
     7  // to be used to verify window behaviour against foreign implementations.
     8  // For example, the behavior of a NumPy window can be captured using this
     9  // python code:
    10  //
    11  //	import matplotlib.pyplot as plt
    12  //	import numpy as np
    13  //	from numpy.fft import fft
    14  //
    15  //	window = np.blackman(20)
    16  //	print("# beta = %f" % np.mean(window))
    17  //
    18  //	plt.figure()
    19  //
    20  //	A = fft(window, 1000)
    21  //	mag = np.abs(A)
    22  //	with np.errstate(divide='ignore', invalid='ignore'):
    23  //	    mag = 20 * np.log10(mag)
    24  //	mag -= max(mag)
    25  //	freq = np.linspace(0, len(window)/2, len(A)/2)
    26  //
    27  //	for m in mag[:len(mag)/2]:
    28  //		print(m)
    29  //
    30  //	plt.plot(freq, mag[:len(mag)/2])
    31  //	plt.title("Spectral leakage")
    32  //	plt.ylabel("Amplitude (dB)")
    33  //	plt.xlabel("DFT bin")
    34  //
    35  //	plt.show()
    36  //
    37  // and then be exported to leakage and compared with the Gonum
    38  // implementation.
    39  package main
    40  
    41  import (
    42  	"bufio"
    43  	"flag"
    44  	"fmt"
    45  	"image/color"
    46  	"io"
    47  	"log"
    48  	"math"
    49  	"math/cmplx"
    50  	"os"
    51  	"strconv"
    52  	"strings"
    53  
    54  	"gonum.org/v1/gonum/dsp/fourier"
    55  	"gonum.org/v1/gonum/dsp/window"
    56  	"gonum.org/v1/gonum/floats"
    57  	"gonum.org/v1/gonum/stat"
    58  	"gonum.org/v1/plot"
    59  	"gonum.org/v1/plot/plotter"
    60  	"gonum.org/v1/plot/vg"
    61  )
    62  
    63  var windows = map[string]*builtin{
    64  	"rectangular": {
    65  		name: func(_ float64) string { return "Rectangular" },
    66  		fn:   func(_ float64) func([]float64) []float64 { return window.Rectangular },
    67  		ok:   func(_ float64) bool { return true },
    68  	},
    69  	"sine": {
    70  		name: func(_ float64) string { return "Sine" },
    71  		fn:   func(_ float64) func([]float64) []float64 { return window.Sine },
    72  		ok:   func(_ float64) bool { return true },
    73  	},
    74  	"lanczos": {
    75  		name: func(_ float64) string { return "Lanczos" },
    76  		fn:   func(_ float64) func([]float64) []float64 { return window.Lanczos },
    77  		ok:   func(_ float64) bool { return true },
    78  	},
    79  	"triangular": {
    80  		name: func(_ float64) string { return "Triangular" },
    81  		fn:   func(_ float64) func([]float64) []float64 { return window.Triangular },
    82  		ok:   func(_ float64) bool { return true },
    83  	},
    84  	"hann": {
    85  		name: func(_ float64) string { return "Hann" },
    86  		fn:   func(_ float64) func([]float64) []float64 { return window.Hann },
    87  		ok:   func(_ float64) bool { return true },
    88  	},
    89  	"bartletthann": {
    90  		name: func(_ float64) string { return "BartlettHann" },
    91  		fn:   func(_ float64) func([]float64) []float64 { return window.BartlettHann },
    92  		ok:   func(_ float64) bool { return true },
    93  	},
    94  	"hamming": {
    95  		name: func(_ float64) string { return "Hamming" },
    96  		fn:   func(_ float64) func([]float64) []float64 { return window.Hamming },
    97  		ok:   func(_ float64) bool { return true },
    98  	},
    99  	"blackman": {
   100  		name: func(_ float64) string { return "Blackman" },
   101  		fn:   func(_ float64) func([]float64) []float64 { return window.Blackman },
   102  		ok:   func(_ float64) bool { return true },
   103  	},
   104  	"blackmanharris": {
   105  		name: func(_ float64) string { return "BlackmanHarris" },
   106  		fn:   func(_ float64) func([]float64) []float64 { return window.BlackmanHarris },
   107  		ok:   func(_ float64) bool { return true },
   108  	},
   109  	"nuttall": {
   110  		name: func(_ float64) string { return "Nuttall" },
   111  		fn:   func(_ float64) func([]float64) []float64 { return window.Nuttall },
   112  		ok:   func(_ float64) bool { return true },
   113  	},
   114  	"blackmannuttall": {
   115  		name: func(_ float64) string { return "BlackmanNuttall" },
   116  		fn:   func(_ float64) func([]float64) []float64 { return window.BlackmanNuttall },
   117  		ok:   func(_ float64) bool { return true },
   118  	},
   119  	"flattop": {
   120  		name: func(_ float64) string { return "FlatTop" },
   121  		fn:   func(_ float64) func([]float64) []float64 { return window.FlatTop },
   122  		ok:   func(_ float64) bool { return true },
   123  	},
   124  	"gaussian": {
   125  		name: func(p float64) string { return fmt.Sprintf("Gaussian σ=%v", p) },
   126  		fn:   func(p float64) func([]float64) []float64 { return window.Gaussian{Sigma: p}.Transform },
   127  		ok:   func(p float64) bool { return !math.IsNaN(p) },
   128  	},
   129  	"tukey": {
   130  		name: func(p float64) string { return fmt.Sprintf("Tukey α=%v", p) },
   131  		fn:   func(p float64) func([]float64) []float64 { return window.Tukey{Alpha: p}.Transform },
   132  		ok:   func(p float64) bool { return !math.IsNaN(p) },
   133  	},
   134  }
   135  
   136  type builtin struct {
   137  	name func(float64) string
   138  	fn   func(float64) func([]float64) []float64
   139  	ok   func(float64) bool
   140  }
   141  
   142  func main() {
   143  	name := flag.String("window", "", "specify a built-in window name (required if csv not given)")
   144  	param := flag.Float64("param", math.NaN(), "specify parameter for parametric windows")
   145  	symm := flag.Bool("symm", true, "specify whether the window is symmetric")
   146  	n := flag.Int("n", 20, "specify window length")
   147  	m := flag.Int("m", 1000, "specify sample length (must be greater than n)")
   148  	csv := flag.String("csv", "", "specify an input file of dB transformed frequency amplitudes (required if window not given)")
   149  	out := flag.String("o", "", "specify output file for plot (required, formats eps, jpg, jpeg, pdf, png, svg, tex or tif)")
   150  	width := flag.Float64("width", 16, "specify plot width (cm)")
   151  	height := flag.Float64("height", 8, "specify plot height (cm)")
   152  	comment := flag.Bool("comment", false, "output a comment line for the window (only for window function)")
   153  	flag.Parse()
   154  
   155  	win := windows[strings.ToLower(*name)]
   156  	if win == nil && *csv == "" {
   157  		fmt.Fprintln(os.Stderr, "missing function name or csv input")
   158  		flag.Usage()
   159  		os.Exit(2)
   160  	}
   161  	if *csv == "" && !win.ok(*param) {
   162  		fmt.Fprintln(os.Stderr, "missing parameter")
   163  		flag.Usage()
   164  		os.Exit(2)
   165  	}
   166  	if *out == "" {
   167  		fmt.Fprintln(os.Stderr, "missing output filename")
   168  		flag.Usage()
   169  		os.Exit(2)
   170  	}
   171  
   172  	p := plot.New()
   173  	p.X.Label.Text = "DFT bin"
   174  	p.Y.Label.Text = "Amplitude [dB]"
   175  	p.Add(plotter.NewGrid())
   176  
   177  	var (
   178  		c        *characteristics
   179  		spectrum plotter.XYs
   180  		min      float64
   181  		err      error
   182  	)
   183  	if win != nil {
   184  		symmetry := "(symmetric)"
   185  		if !*symm {
   186  			symmetry = "(periodic)"
   187  		}
   188  		p.Title.Text = fmt.Sprintf("Spectral Leakage for %s %s", win.name(*param), symmetry)
   189  		c, spectrum, min, err = funcCharacteristics(win.fn(*param), *n, *m, *symm)
   190  		if err != nil {
   191  			log.Fatal(err)
   192  		}
   193  		if *comment {
   194  			fmt.Printf("// Spectral leakage parameters: ΔF_0 = %2f, ΔF_0.5 = %.2f, K = %.2f, ɣ_max = %2f, β = %2f.\n",
   195  				c.deltaF0, c.deltaFhalf, c.k(), c.gammaMax, c.beta)
   196  		}
   197  	} else {
   198  		f, err := os.Open(*csv)
   199  		if err != nil {
   200  			log.Fatal(err)
   201  		}
   202  		defer f.Close()
   203  		p.Title.Text = fmt.Sprintf("Spectral Leakage for %s", *csv)
   204  		c, spectrum, min, err = csvCharacteristics(f, *n, *m)
   205  		if err != nil {
   206  			log.Fatal(err)
   207  		}
   208  	}
   209  
   210  	spectrumLines, err := plotter.NewLine(spectrum)
   211  	if err != nil {
   212  		log.Fatalf("spectrum: %v", err)
   213  	}
   214  
   215  	gammaLine, err := plotter.NewLine(plotter.XYs{
   216  		{X: 0, Y: c.gammaMax}, {X: float64(*n) / 2, Y: c.gammaMax},
   217  	})
   218  	if err != nil {
   219  		log.Fatalf("ɣ_max: %v", err)
   220  	}
   221  	gammaLine.Color = color.RGBA{R: 0x40, G: 0x80, B: 0xff, A: 0xff}
   222  
   223  	deltaF0Line, err := plotter.NewLine(plotter.XYs{
   224  		{X: c.deltaF0 / 2, Y: 0}, {X: c.deltaF0 / 2, Y: min},
   225  	})
   226  	if err != nil {
   227  		log.Fatalf("ΔF_0: %v", err)
   228  	}
   229  	deltaF0Line.Color = color.RGBA{R: 0xff, A: 0xff}
   230  
   231  	deltaFhalfLine, err := plotter.NewLine(plotter.XYs{
   232  		{X: c.deltaFhalf / 2, Y: 0}, {X: c.deltaFhalf / 2, Y: min},
   233  	})
   234  	if err != nil {
   235  		log.Fatalf("ΔF_0.5: %v", err)
   236  	}
   237  	deltaFhalfLine.Color = color.RGBA{G: 0xff, A: 0xff}
   238  
   239  	var blank plotter.Line
   240  
   241  	p.Add(
   242  		gammaLine,
   243  		deltaF0Line,
   244  		deltaFhalfLine,
   245  		spectrumLines,
   246  	)
   247  	p.Legend.Add(fmt.Sprintf("ΔF_0 = %.3v", c.deltaF0), deltaF0Line)
   248  	p.Legend.Add(fmt.Sprintf("ΔF_0.5 = %.3v", c.deltaFhalf), deltaFhalfLine)
   249  	p.Legend.Add(fmt.Sprintf("K = %.3v", c.k()), &blank)
   250  	p.Legend.Add(fmt.Sprintf("ɣ_max = %.3v", c.gammaMax), gammaLine)
   251  	p.Legend.Add(fmt.Sprintf("β = %.3v", c.beta), &blank)
   252  	p.Legend.Top = true
   253  	p.Legend.XOffs = -10
   254  	p.Legend.YOffs = -10
   255  
   256  	err = p.Save(vg.Length(*width)*vg.Centimeter, vg.Length(*height)*vg.Centimeter, *out)
   257  	if err != nil {
   258  		log.Fatal(err)
   259  	}
   260  }
   261  
   262  // characteristics hold DFT window characteristic statistics.
   263  // See http://www.dsplib.ru/content/win/win.html for details.
   264  type characteristics struct {
   265  	deltaF0    float64
   266  	deltaFhalf float64
   267  	gammaMax   float64
   268  	beta       float64
   269  }
   270  
   271  // k returns the K window parameter which is the ratio of the window's
   272  // ΔF_0 to the ΔF_0 of the rectangular window.
   273  func (c *characteristics) k() float64 {
   274  	return c.deltaF0 / 2
   275  }
   276  
   277  func funcCharacteristics(fn func([]float64) []float64, n, m int, symm bool) (c *characteristics, xy plotter.XYs, min float64, err error) {
   278  	if m < n {
   279  		return nil, nil, 0, fmt.Errorf("window: sequence too short for window: %d < %d", m, n)
   280  	}
   281  
   282  	var w []float64
   283  	t := make([]float64, m)
   284  	if symm {
   285  		w = window.NewValues(fn, n)
   286  	} else {
   287  		w = window.NewValues(fn, n+1)[:n]
   288  	}
   289  
   290  	copy(t, w)
   291  
   292  	var max float64
   293  	xy = make(plotter.XYs, m/2+1)
   294  	fft := fourier.NewFFT(len(t))
   295  	for i, c := range fft.Coefficients(nil, t) {
   296  		a := db(cmplx.Abs(c))
   297  		t[i] = a
   298  		if !math.IsInf(a, -1) && a < min {
   299  			min = a
   300  		}
   301  		if i == 0 {
   302  			max = a
   303  		}
   304  	}
   305  	for i, a := range t[:m/2+1] {
   306  		if math.IsInf(a, -1) {
   307  			a = min
   308  		}
   309  		xy[i] = plotter.XY{X: float64(i) * float64(n) / float64(m), Y: a - max}
   310  	}
   311  
   312  	c = &characteristics{beta: db(stat.Mean(w, nil))}
   313  	c.deltaF0, c.deltaFhalf, c.gammaMax = parameters(t, n, m)
   314  
   315  	return c, xy, min - max, nil
   316  }
   317  
   318  func csvCharacteristics(r io.Reader, n, m int) (c *characteristics, xy plotter.XYs, min float64, err error) {
   319  	if m < n {
   320  		return nil, nil, 0, fmt.Errorf("window: sequence too short for window: %d < %d", m, n)
   321  	}
   322  	sc := bufio.NewScanner(r)
   323  	max := math.Inf(-1)
   324  	var t []float64
   325  	for sc.Scan() {
   326  		text := sc.Text()
   327  		if strings.HasPrefix(text, "#") {
   328  			continue
   329  		}
   330  		v, err := strconv.ParseFloat(text, 64)
   331  		if err != nil {
   332  			log.Fatal(err)
   333  		}
   334  		if v > max {
   335  			max = v
   336  		}
   337  		t = append(t, v)
   338  	}
   339  
   340  	xy = make(plotter.XYs, len(t))
   341  	for i, a := range t {
   342  		if math.IsInf(a, -1) {
   343  			a = min
   344  		} else if a < min {
   345  			min = a
   346  		}
   347  		if i == 0 {
   348  			max = a
   349  		}
   350  		xy[i] = plotter.XY{X: float64(i) * float64(n) / float64(m), Y: a - max}
   351  	}
   352  	err = sc.Err()
   353  	if err != nil {
   354  		return nil, nil, 0, err
   355  	}
   356  
   357  	c = &characteristics{beta: math.NaN()}
   358  	c.deltaF0, c.deltaFhalf, c.gammaMax = parameters(t, n, m)
   359  
   360  	return c, xy, min - max, nil
   361  }
   362  
   363  func parameters(spectrum []float64, n, m int) (deltaF0, deltaFhalf, gammaMax float64) {
   364  	max := spectrum[0]
   365  	var peaks []float64
   366  	for i, r := range spectrum {
   367  		if i > 1 {
   368  			if spectrum[i-1] < r && deltaF0 == 0 {
   369  				deltaF0 = 2 * float64((i-1)*n) / float64(m)
   370  			}
   371  			if thresh := max - 3; thresh < spectrum[i-1] && r <= thresh {
   372  				deltaFhalf = 2 * float64((i-1)*n) / float64(m)
   373  			}
   374  		}
   375  		if i > 2 && spectrum[i-2] <= spectrum[i-1] && spectrum[i-1] > r {
   376  			peaks = append(peaks, spectrum[i-1])
   377  		}
   378  	}
   379  
   380  	if len(peaks) == 0 {
   381  		gammaMax = math.NaN()
   382  	} else {
   383  		gammaMax = floats.Max(peaks) - max
   384  	}
   385  
   386  	return deltaF0, deltaFhalf, gammaMax
   387  }
   388  
   389  func db(m float64) float64 {
   390  	return 20 * math.Log10(m)
   391  }