Coverage for src / tracekit / utils / windowing.py: 100%
62 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
1"""Window function support for spectral analysis.
3This module provides standard window functions for FFT and spectral
4analysis, implementing the requirements for windowed spectral estimation.
7Example:
8 >>> from tracekit.utils.windowing import get_window, WINDOW_FUNCTIONS
9 >>> window = get_window("hann", 1024)
10 >>> print(f"Available windows: {list(WINDOW_FUNCTIONS.keys())}")
12References:
13 Harris, F. J. (1978). "On the use of windows for harmonic analysis
14 with the discrete Fourier transform." Proceedings of the IEEE, 66(1).
15"""
17from __future__ import annotations
19from collections.abc import Callable
20from typing import TYPE_CHECKING, Any, Literal
22import numpy as np
24if TYPE_CHECKING:
25 from numpy.typing import NDArray
27# Type alias for window function (using string annotation for TYPE_CHECKING compatibility)
28WindowFunction = Callable[[int], "NDArray[np.float64]"]
31def rectangular(n: int) -> NDArray[np.float64]:
32 """Rectangular (boxcar) window.
34 No tapering applied - all samples weighted equally.
36 Args:
37 n: Window length in samples.
39 Returns:
40 Window coefficients (all ones).
42 Example:
43 >>> w = rectangular(64)
44 >>> assert np.all(w == 1.0)
45 """
46 return np.ones(n, dtype=np.float64)
49def hann(n: int) -> NDArray[np.float64]:
50 """Hann (raised cosine) window.
52 Also known as Hanning window. Provides good frequency resolution
53 with moderate sidelobe suppression.
55 Args:
56 n: Window length in samples.
58 Returns:
59 Window coefficients.
61 Example:
62 >>> w = hann(64)
63 >>> assert w[0] == w[-1] # Symmetric
65 References:
66 IEEE Std 1057-2017 Section 4.4.2
67 """
68 return np.hanning(n).astype(np.float64)
71def hamming(n: int) -> NDArray[np.float64]:
72 """Hamming window.
74 Similar to Hann but with reduced first sidelobe at cost of
75 slower rolloff.
77 Args:
78 n: Window length in samples.
80 Returns:
81 Window coefficients.
83 Example:
84 >>> w = hamming(64)
85 >>> assert w[32] > w[0] # Peak in center
86 """
87 return np.hamming(n).astype(np.float64)
90def blackman(n: int) -> NDArray[np.float64]:
91 """Blackman window.
93 Three-term cosine window with excellent sidelobe suppression
94 (-58 dB first sidelobe).
96 Args:
97 n: Window length in samples.
99 Returns:
100 Window coefficients.
102 Example:
103 >>> w = blackman(64)
104 >>> assert w[32] > w[0]
105 """
106 return np.blackman(n).astype(np.float64)
109def kaiser(n: int, beta: float = 8.6) -> NDArray[np.float64]:
110 """Kaiser window with configurable shape parameter.
112 Provides adjustable tradeoff between main lobe width and
113 sidelobe attenuation.
115 Args:
116 n: Window length in samples.
117 beta: Shape parameter (default 8.6 for ~60 dB sidelobe attenuation).
118 - beta=0: Rectangular
119 - beta=5: ~30 dB sidelobe attenuation
120 - beta=8.6: ~60 dB sidelobe attenuation
121 - beta=14: ~90 dB sidelobe attenuation
123 Returns:
124 Window coefficients.
126 Example:
127 >>> w = kaiser(64, beta=10)
128 >>> assert 0 < w[0] < w[32]
129 """
130 return np.kaiser(n, beta).astype(np.float64)
133def flattop(n: int) -> NDArray[np.float64]:
134 """Flat-top window for accurate amplitude measurements.
136 Provides minimal scalloping loss (<0.01 dB) at cost of
137 wider main lobe. Best for amplitude accuracy when frequency
138 resolution is not critical.
140 Args:
141 n: Window length in samples.
143 Returns:
144 Window coefficients.
146 Example:
147 >>> w = flattop(64)
148 >>> # Flat-top has characteristic near-zero values at edges
150 References:
151 D'Antona, G. & Ferrero, A. (2006). "Digital Signal Processing
152 for Measurement Systems."
153 """
154 # Flat-top coefficients per HP/Agilent standard
155 a0 = 0.21557895
156 a1 = 0.41663158
157 a2 = 0.277263158
158 a3 = 0.083578947
159 a4 = 0.006947368
161 k = np.arange(n, dtype=np.float64)
162 w = (
163 a0
164 - a1 * np.cos(2 * np.pi * k / (n - 1))
165 + a2 * np.cos(4 * np.pi * k / (n - 1))
166 - a3 * np.cos(6 * np.pi * k / (n - 1))
167 + a4 * np.cos(8 * np.pi * k / (n - 1))
168 )
169 return np.asarray(w, dtype=np.float64)
172def bartlett(n: int) -> NDArray[np.float64]:
173 """Bartlett (triangular) window.
175 Linear taper from zero at edges to maximum at center.
177 Args:
178 n: Window length in samples.
180 Returns:
181 Window coefficients.
183 Example:
184 >>> w = bartlett(64)
185 >>> assert w[32] == 1.0 # Maximum at center
186 """
187 return np.bartlett(n).astype(np.float64)
190def blackman_harris(n: int) -> NDArray[np.float64]:
191 """Blackman-Harris window (4-term).
193 Four-term cosine window with excellent sidelobe suppression
194 (-92 dB first sidelobe).
196 Args:
197 n: Window length in samples.
199 Returns:
200 Window coefficients.
202 Example:
203 >>> w = blackman_harris(64)
204 >>> assert w[32] > w[0]
205 """
206 a0 = 0.35875
207 a1 = 0.48829
208 a2 = 0.14128
209 a3 = 0.01168
211 k = np.arange(n, dtype=np.float64)
212 w = (
213 a0
214 - a1 * np.cos(2 * np.pi * k / (n - 1))
215 + a2 * np.cos(4 * np.pi * k / (n - 1))
216 - a3 * np.cos(6 * np.pi * k / (n - 1))
217 )
218 return np.asarray(w, dtype=np.float64)
221# Window function registry
222WINDOW_FUNCTIONS: dict[str, WindowFunction] = {
223 "rectangular": rectangular,
224 "boxcar": rectangular,
225 "rect": rectangular,
226 "hann": hann,
227 "hanning": hann,
228 "hamming": hamming,
229 "blackman": blackman,
230 "kaiser": lambda n: kaiser(n, beta=8.6),
231 "flattop": flattop,
232 "flat_top": flattop,
233 "bartlett": bartlett,
234 "triangular": bartlett,
235 "blackman_harris": blackman_harris,
236 "blackmanharris": blackman_harris,
237}
240# Type for window names
241WindowName = Literal[
242 "rectangular",
243 "boxcar",
244 "rect",
245 "hann",
246 "hanning",
247 "hamming",
248 "blackman",
249 "kaiser",
250 "flattop",
251 "flat_top",
252 "bartlett",
253 "triangular",
254 "blackman_harris",
255 "blackmanharris",
256]
259def get_window(
260 window: str | WindowFunction | NDArray[np.floating[Any]],
261 n: int,
262 *,
263 beta: float | None = None,
264) -> NDArray[np.float64]:
265 """Get window coefficients by name or callable.
267 Args:
268 window: Window specification. Can be:
269 - A string name from WINDOW_FUNCTIONS
270 - A callable that takes length and returns coefficients
271 - A pre-computed array of coefficients
272 n: Window length in samples.
273 beta: Optional beta parameter for Kaiser window.
275 Returns:
276 Window coefficients array of length n.
278 Raises:
279 ValueError: If window name is unknown.
281 Example:
282 >>> w = get_window("hann", 1024)
283 >>> w = get_window("kaiser", 1024, beta=10)
284 >>> w = get_window(np.hamming, 1024)
285 """
286 if isinstance(window, np.ndarray):
287 if len(window) != n:
288 raise ValueError(f"Window array length {len(window)} != requested {n}")
289 return window.astype(np.float64)
291 if callable(window) and not isinstance(window, str):
292 return np.asarray(window(n), dtype=np.float64)
294 window_name = window.lower()
296 if window_name == "kaiser" and beta is not None:
297 return kaiser(n, beta)
299 if window_name not in WINDOW_FUNCTIONS:
300 available = ", ".join(sorted(set(WINDOW_FUNCTIONS.keys())))
301 raise ValueError(f"Unknown window: {window}. Available: {available}")
303 return WINDOW_FUNCTIONS[window_name](n)
306def window_properties(window: str | NDArray[np.floating[Any]], n: int = 1024) -> dict[str, Any]:
307 """Compute window properties for analysis.
309 Args:
310 window: Window name or coefficients.
311 n: Window length for named windows.
313 Returns:
314 Dictionary with window properties:
315 - coherent_gain: Sum of window / length
316 - noise_bandwidth: Normalized equivalent noise bandwidth
317 - scalloping_loss: Peak amplitude error in dB
319 Example:
320 >>> props = window_properties("hann")
321 >>> print(f"ENBW: {props['noise_bandwidth']:.3f}")
322 """
323 if isinstance(window, str):
324 w = get_window(window, n)
325 else:
326 w = np.asarray(window, dtype=np.float64)
327 n = len(w)
329 # Coherent gain (DC gain)
330 coherent_gain = np.sum(w) / n
332 # Noise equivalent bandwidth
333 # ENBW = N * sum(w^2) / (sum(w))^2
334 noise_bandwidth = n * np.sum(w**2) / np.sum(w) ** 2
336 # Scalloping loss (worst-case amplitude error at bin edge)
337 # Approximate by evaluating window at half-bin offset
338 k = np.arange(n)
339 w_shifted = w * np.exp(2j * np.pi * 0.5 * k / n)
340 scalloping_loss = 20 * np.log10(np.abs(np.sum(w_shifted)) / np.abs(np.sum(w)))
342 return {
343 "coherent_gain": float(coherent_gain),
344 "noise_bandwidth": float(noise_bandwidth),
345 "scalloping_loss": float(scalloping_loss),
346 "length": n,
347 }
350__all__ = [
351 "WINDOW_FUNCTIONS",
352 "bartlett",
353 "blackman",
354 "blackman_harris",
355 "flattop",
356 "get_window",
357 "hamming",
358 "hann",
359 "kaiser",
360 "rectangular",
361 "window_properties",
362]