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 }