scirs2_fft/
window_extended.rs

1//! Extended window functions and analysis tools
2//!
3//! This module provides additional window functions and analysis capabilities
4//! to complement the basic window module, matching SciPy's comprehensive catalog.
5
6use scirs2_core::ndarray::{Array1, Array2};
7use std::f64::consts::PI;
8
9use crate::error::{FFTError, FFTResult};
10use crate::fft::fft;
11use crate::helper::fftfreq;
12
13/// Extended window types not covered in the basic window module
14#[derive(Debug, Clone, PartialEq)]
15pub enum ExtendedWindow {
16    /// Chebyshev window (equiripple)
17    Chebyshev { attenuation_db: f64 },
18    /// Slepian (DPSS) window
19    Slepian { width: f64 },
20    /// Lanczos window (sinc window)
21    Lanczos,
22    /// Planck-taper window
23    PlanckTaper { epsilon: f64 },
24    /// Dolph-Chebyshev window
25    DolphChebyshev { attenuation_db: f64 },
26    /// Poisson window
27    Poisson { alpha: f64 },
28    /// Hann-Poisson window
29    HannPoisson { alpha: f64 },
30    /// Cauchy window
31    Cauchy { alpha: f64 },
32    /// Advancedspherical window
33    Advancedspherical { mu: f64, x0: f64 },
34    /// Taylor window
35    Taylor {
36        n_sidelobes: usize,
37        sidelobe_level_db: f64,
38    },
39}
40
41/// Generate an extended window function
42#[allow(dead_code)]
43pub fn get_extended_window(window: ExtendedWindow, n: usize) -> FFTResult<Array1<f64>> {
44    let mut w = Array1::zeros(n);
45
46    match window {
47        ExtendedWindow::Chebyshev { attenuation_db } => {
48            generate_chebyshev_window(&mut w, attenuation_db)?;
49        }
50        ExtendedWindow::Slepian { width } => {
51            generate_slepian_window(&mut w, width)?;
52        }
53        ExtendedWindow::Lanczos => {
54            generate_lanczos_window(&mut w);
55        }
56        ExtendedWindow::PlanckTaper { epsilon } => {
57            generate_planck_taper_window(&mut w, epsilon)?;
58        }
59        ExtendedWindow::DolphChebyshev { attenuation_db } => {
60            generate_dolph_chebyshev_window(&mut w, attenuation_db)?;
61        }
62        ExtendedWindow::Poisson { alpha } => {
63            generate_poisson_window(&mut w, alpha);
64        }
65        ExtendedWindow::HannPoisson { alpha } => {
66            generate_hann_poisson_window(&mut w, alpha);
67        }
68        ExtendedWindow::Cauchy { alpha } => {
69            generate_cauchy_window(&mut w, alpha);
70        }
71        ExtendedWindow::Advancedspherical { mu, x0 } => {
72            generate_advancedspherical_window(&mut w, mu, x0)?;
73        }
74        ExtendedWindow::Taylor {
75            n_sidelobes,
76            sidelobe_level_db,
77        } => {
78            generate_taylor_window(&mut w, n_sidelobes, sidelobe_level_db)?;
79        }
80    }
81
82    Ok(w)
83}
84
85/// Generate Chebyshev window
86#[allow(dead_code)]
87fn generate_chebyshev_window(w: &mut Array1<f64>, attenuationdb: f64) -> FFTResult<()> {
88    let n = w.len();
89    if attenuationdb <= 0.0 {
90        return Err(FFTError::ValueError(
91            "Attenuation must be positive".to_string(),
92        ));
93    }
94
95    // Simplified Chebyshev window implementation
96    let r = 10.0_f64.powf(attenuationdb / 20.0);
97    let beta = (r + (r * r - 1.0).sqrt()).ln() / n as f64;
98
99    for i in 0..n {
100        let x = 2.0 * i as f64 / (n - 1) as f64 - 1.0;
101        w[i] = ((n as f64 - 1.0) * beta * (1.0 - x * x).sqrt().acos()).cosh() / r.cosh();
102    }
103
104    Ok(())
105}
106
107/// Generate Slepian (DPSS) window
108#[allow(dead_code)]
109fn generate_slepian_window(w: &mut Array1<f64>, width: f64) -> FFTResult<()> {
110    let n = w.len();
111    if width <= 0.0 || width >= 0.5 {
112        return Err(FFTError::ValueError(
113            "Width must be between 0 and 0.5".to_string(),
114        ));
115    }
116
117    // Simplified DPSS window (this is an approximation)
118    for i in 0..n {
119        let t = 2.0 * i as f64 / (n - 1) as f64 - 1.0;
120        w[i] = (PI * width * n as f64 * t).sin() / (PI * t);
121        if t == 0.0 {
122            w[i] = width * n as f64;
123        }
124    }
125
126    // Normalize
127    let sum: f64 = w.sum();
128    w.mapv_inplace(|x| x / sum * n as f64);
129
130    Ok(())
131}
132
133/// Generate Lanczos window
134#[allow(dead_code)]
135fn generate_lanczos_window(w: &mut Array1<f64>) {
136    let n = w.len();
137    for i in 0..n {
138        let x = 2.0 * i as f64 / (n - 1) as f64 - 1.0;
139        w[i] = if x == 0.0 {
140            1.0
141        } else {
142            (PI * x).sin() / (PI * x)
143        };
144    }
145}
146
147/// Generate Planck-taper window
148#[allow(dead_code)]
149fn generate_planck_taper_window(w: &mut Array1<f64>, epsilon: f64) -> FFTResult<()> {
150    let n = w.len();
151    if epsilon <= 0.0 || epsilon >= 0.5 {
152        return Err(FFTError::ValueError(
153            "Epsilon must be between 0 and 0.5".to_string(),
154        ));
155    }
156
157    for i in 0..n {
158        let t = i as f64 / (n - 1) as f64;
159        w[i] = if t < epsilon {
160            1.0 / ((-epsilon / (t * (epsilon - t))).exp() + 1.0)
161        } else if t > 1.0 - epsilon {
162            1.0 / ((-epsilon / ((1.0 - t) * (t - 1.0 + epsilon))).exp() + 1.0)
163        } else {
164            1.0
165        };
166    }
167
168    Ok(())
169}
170
171/// Generate Dolph-Chebyshev window
172#[allow(dead_code)]
173fn generate_dolph_chebyshev_window(w: &mut Array1<f64>, attenuationdb: f64) -> FFTResult<()> {
174    // This is similar to Chebyshev but with different normalization
175    generate_chebyshev_window(w, attenuationdb)?;
176
177    // Normalize to unit sum
178    let sum: f64 = w.sum();
179    let n = w.len() as f64;
180    w.mapv_inplace(|x| x / sum * n);
181
182    Ok(())
183}
184
185/// Generate Poisson window
186#[allow(dead_code)]
187fn generate_poisson_window(w: &mut Array1<f64>, alpha: f64) {
188    let n = w.len();
189    let half_n = n as f64 / 2.0;
190
191    for i in 0..n {
192        let t = (i as f64 - half_n).abs() / half_n;
193        w[i] = (-alpha * t).exp();
194    }
195}
196
197/// Generate Hann-Poisson window
198#[allow(dead_code)]
199fn generate_hann_poisson_window(w: &mut Array1<f64>, alpha: f64) {
200    let n = w.len();
201
202    for i in 0..n {
203        let hann_part = 0.5 * (1.0 - (2.0 * PI * i as f64 / (n - 1) as f64).cos());
204        let poisson_part = (-alpha * (n as f64 / 2.0 - i as f64).abs() / (n as f64 / 2.0)).exp();
205        w[i] = hann_part * poisson_part;
206    }
207}
208
209/// Generate Cauchy window
210#[allow(dead_code)]
211fn generate_cauchy_window(w: &mut Array1<f64>, alpha: f64) {
212    let n = w.len();
213    let center = (n - 1) as f64 / 2.0;
214
215    for i in 0..n {
216        let t = (i as f64 - center) / center;
217        w[i] = 1.0 / (1.0 + (alpha * t).powi(2));
218    }
219}
220
221/// Generate Advancedspherical window
222#[allow(dead_code)]
223fn generate_advancedspherical_window(w: &mut Array1<f64>, mu: f64, x0: f64) -> FFTResult<()> {
224    let n = w.len();
225    if x0 <= 0.0 || x0 >= 1.0 {
226        return Err(FFTError::ValueError(
227            "x0 must be between 0 and 1".to_string(),
228        ));
229    }
230
231    // Simplified advancedspherical window
232    for i in 0..n {
233        let x = 2.0 * i as f64 / (n - 1) as f64 - 1.0;
234        if x.abs() < x0 {
235            w[i] = (1.0 - (x / x0).powi(2)).powf(mu);
236        } else {
237            w[i] = 0.0;
238        }
239    }
240
241    Ok(())
242}
243
244/// Generate Taylor window
245#[allow(dead_code)]
246fn generate_taylor_window(
247    w: &mut Array1<f64>,
248    n_sidelobes: usize,
249    sidelobe_level_db: f64,
250) -> FFTResult<()> {
251    let n = w.len();
252    if n_sidelobes == 0 {
253        return Err(FFTError::ValueError(
254            "Number of _sidelobes must be positive".to_string(),
255        ));
256    }
257
258    // Simplified Taylor window implementation
259    let a = sidelobe_level_db.abs() / 20.0;
260    let r = 10.0_f64.powf(a);
261
262    for i in 0..n {
263        let mut sum = 0.0;
264        for k in 1..=n_sidelobes {
265            let x = 2.0 * PI * k as f64 * (i as f64 / (n - 1) as f64 - 0.5);
266            sum += (-1.0_f64).powi(k as i32 + 1) * x.cos() / k as f64;
267        }
268        w[i] = 1.0 + 2.0 * r * sum;
269    }
270
271    // Normalize
272    let max_val = w.iter().cloned().fold(0.0, f64::max);
273    w.mapv_inplace(|x| x / max_val);
274
275    Ok(())
276}
277
278/// Analyze window properties
279#[derive(Debug, Clone)]
280pub struct WindowProperties {
281    /// Main lobe width (in bins)
282    pub main_lobe_width: f64,
283    /// Sidelobe level (in dB)
284    pub sidelobe_level_db: f64,
285    /// Coherent gain
286    pub coherent_gain: f64,
287    /// Processing gain
288    pub processing_gain: f64,
289    /// Equivalent noise bandwidth
290    pub enbw: f64,
291    /// Scalloping loss (in dB)
292    pub scalloping_loss_db: f64,
293    /// Worst-case processing loss (in dB)
294    pub worst_case_loss_db: f64,
295}
296
297/// Analyze properties of a window function
298#[allow(dead_code)]
299pub fn analyze_window(
300    window: &Array1<f64>,
301    sample_rate: Option<f64>,
302) -> FFTResult<WindowProperties> {
303    let n = window.len();
304    let fs = sample_rate.unwrap_or(1.0);
305
306    // Coherent gain (DC gain)
307    let coherent_gain = window.sum() / n as f64;
308
309    // Processing gain
310    let sum_squared: f64 = window.mapv(|x| x * x).sum();
311    let processing_gain = window.sum().powi(2) / (n as f64 * sum_squared);
312
313    // ENBW (Equivalent Noise Bandwidth)
314    let enbw = n as f64 * sum_squared / window.sum().powi(2);
315
316    // Compute window spectrum via FFT
317    let n_fft = 8 * n; // Use larger FFT for better resolution
318    let mut padded = Array1::zeros(n_fft);
319    for i in 0..n {
320        padded[i] = window[i];
321    }
322
323    let fft_result = fft(
324        &padded
325            .mapv(|x| scirs2_core::numeric::Complex64::new(x, 0.0))
326            .to_vec(),
327        None,
328    )?;
329    let magnitude: Vec<f64> = fft_result.iter().map(|c| c.norm()).collect();
330
331    // Convert to dB
332    let max_mag = magnitude.iter().cloned().fold(0.0, f64::max);
333    let mag_db: Vec<f64> = magnitude
334        .iter()
335        .map(|&m| 20.0 * (m / max_mag).log10())
336        .collect();
337
338    // Find main lobe width (3dB down from peak)
339    let mut main_lobe_width = 0.0;
340    for (i, &val) in mag_db.iter().enumerate().take(n_fft / 2).skip(1) {
341        if val < -3.0 {
342            main_lobe_width = 2.0 * i as f64 * fs / n_fft as f64;
343            break;
344        }
345    }
346
347    // Find sidelobe level
348    let mut sidelobe_level_db = -1000.0;
349    let main_lobe_end = (main_lobe_width * n_fft as f64 / fs).ceil() as usize;
350    for &val in mag_db.iter().take(n_fft / 2).skip(main_lobe_end) {
351        if val > sidelobe_level_db {
352            sidelobe_level_db = val;
353        }
354    }
355
356    // Scalloping loss (loss at bin edge)
357    let bin_edge_response = magnitude[n_fft / (2 * n)];
358    let scalloping_loss_db = -20.0 * (bin_edge_response / max_mag).log10();
359
360    // Worst-case processing loss
361    let worst_case_loss_db = scalloping_loss_db + 10.0 * processing_gain.log10();
362
363    Ok(WindowProperties {
364        main_lobe_width,
365        sidelobe_level_db,
366        coherent_gain,
367        processing_gain,
368        enbw,
369        scalloping_loss_db,
370        worst_case_loss_db,
371    })
372}
373
374/// Create a window visualization plot data
375#[allow(dead_code)]
376pub fn visualize_window(
377    window: &Array1<f64>,
378) -> FFTResult<(Array1<f64>, Array1<f64>, Array2<f64>)> {
379    let n = window.len();
380
381    // Time domain
382    let time = Array1::range(0.0, n as f64, 1.0);
383
384    // Frequency response
385    let n_fft = 8 * n;
386    let mut padded = vec![scirs2_core::numeric::Complex64::new(0.0, 0.0); n_fft];
387    for i in 0..n {
388        padded[i] = scirs2_core::numeric::Complex64::new(window[i], 0.0);
389    }
390
391    let fft_result = fft(&padded, None).unwrap_or(padded);
392    let freq = fftfreq(n_fft, 1.0)?;
393    let magnitude: Vec<f64> = fft_result.iter().map(|c| c.norm()).collect();
394
395    // Convert to dB
396    let max_mag = magnitude.iter().cloned().fold(0.0, f64::max);
397    let mag_db: Vec<f64> = magnitude
398        .iter()
399        .map(|&m| 20.0 * (m / max_mag).max(1e-100).log10())
400        .collect();
401
402    // Create frequency response matrix (frequency, magnitude_db)
403    let mut freq_response = Array2::zeros((n_fft, 2));
404    for i in 0..n_fft {
405        freq_response[[i, 0]] = freq[i];
406        freq_response[[i, 1]] = mag_db[i];
407    }
408
409    Ok((time, window.clone(), freq_response))
410}
411
412/// Compare multiple windows
413#[allow(dead_code)]
414pub fn compare_windows(
415    windows: &[(String, Array1<f64>)],
416) -> FFTResult<Vec<(String, WindowProperties)>> {
417    let mut results = Vec::new();
418
419    for (name, window) in windows {
420        let props = analyze_window(window, None)?;
421        results.push((name.clone(), props));
422    }
423
424    Ok(results)
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430    use crate::Window;
431
432    #[test]
433    fn test_extended_windows() {
434        let n = 64;
435
436        // Test Chebyshev window
437        let cheby = get_extended_window(
438            ExtendedWindow::Chebyshev {
439                attenuation_db: 60.0,
440            },
441            n,
442        )
443        .unwrap();
444        assert_eq!(cheby.len(), n);
445
446        // Test Lanczos window
447        let lanczos = get_extended_window(ExtendedWindow::Lanczos, n).unwrap();
448        assert_eq!(lanczos.len(), n);
449
450        // Test Poisson window
451        let poisson = get_extended_window(ExtendedWindow::Poisson { alpha: 2.0 }, n).unwrap();
452        assert_eq!(poisson.len(), n);
453    }
454
455    #[test]
456    fn test_window_analysis() {
457        let n = 64;
458        let window = crate::window::get_window(Window::Hann, n, true).unwrap();
459
460        let props = analyze_window(&window, Some(1000.0)).unwrap();
461
462        // Check basic properties
463        assert!(props.coherent_gain > 0.0);
464        assert!(props.processing_gain > 0.0);
465        assert!(props.enbw > 0.0);
466        assert!(props.sidelobe_level_db < 0.0);
467    }
468
469    #[test]
470    fn test_window_comparison() {
471        let n = 64;
472        let windows = vec![
473            (
474                "Hann".to_string(),
475                crate::window::get_window(Window::Hann, n, true).unwrap(),
476            ),
477            (
478                "Hamming".to_string(),
479                crate::window::get_window(Window::Hamming, n, true).unwrap(),
480            ),
481        ];
482
483        let comparison = compare_windows(&windows).unwrap();
484        assert_eq!(comparison.len(), 2);
485    }
486}