yagi/nco/
osc.rs

1
2use num_complex::Complex;
3use std::f32::consts::PI;
4
5use crate::error::{Error, Result};
6
7use super::nco::Nco;
8use super::vco::Vco;
9
10const PLL_BANDWIDTH_DEFAULT: f32 = 0.1;
11
12
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum OscScheme {
15    Nco,
16    Vco,
17}
18
19#[derive(Debug, Clone)]
20enum OscData {
21    Nco(Nco),
22    Vco(Vco),
23}
24
25// Main NCO structure
26#[derive(Debug, Clone)]
27pub struct Osc {
28    theta: u32,
29    d_theta: u32,
30    alpha: f32,
31    beta: f32,
32    osc: OscData,
33}
34
35impl Osc {
36    // Create a new NCO
37    pub fn new(osc_scheme: OscScheme) -> Self {
38        let data = match osc_scheme {
39            OscScheme::Nco => OscData::Nco(Nco::new()),
40            OscScheme::Vco => OscData::Vco(Vco::new()),
41        };
42        let mut nco = Osc { 
43            theta: 0,
44            d_theta: 0,
45            alpha: 0.0,
46            beta: 0.0,
47            osc: data,
48        };
49
50        // Set default PLL bandwidth
51        nco.pll_set_bandwidth(PLL_BANDWIDTH_DEFAULT);
52
53        // Reset object
54        nco.reset();
55
56        nco
57    }
58
59    // Reset internal state
60    pub fn reset(&mut self) {
61        self.theta = 0;
62        self.d_theta = 0;
63    }
64
65    // Set frequency
66    pub fn set_frequency(&mut self, dtheta: f32) {
67        self.d_theta = Self::constrain(dtheta);
68    }
69
70    // Adjust frequency
71    pub fn adjust_frequency(&mut self, df: f32) {
72        self.d_theta = self.d_theta.wrapping_add(Self::constrain(df));
73    }
74
75    // Set phase
76    pub fn set_phase(&mut self, phi: f32) {
77        self.theta = Self::constrain(phi);
78    }
79
80    // Adjust phase
81    pub fn adjust_phase(&mut self, dphi: f32) {
82        self.theta = self.theta.wrapping_add(Self::constrain(dphi));
83    }
84
85    // Increment internal phase
86    pub fn step(&mut self) {
87        self.theta = self.theta.wrapping_add(self.d_theta);
88    }
89
90    // Get phase
91    pub fn get_phase(&self) -> f32 {
92        2.0 * PI * self.theta as f32 / ((1u64 << 32) as f32)
93    }
94
95    // Get frequency
96    pub fn get_frequency(&self) -> f32 {
97        let d_theta = 2.0 * PI * self.d_theta as f32 / (1u64 << 32) as f32;
98        if d_theta > PI {
99            d_theta - 2.0 * PI
100        } else {
101            d_theta
102        }
103    }
104
105    // Compute sine of internal phase
106    pub fn sin(&self) -> f32 {
107        match self.osc {
108            OscData::Nco(ref nco) => nco.sin(self.theta),
109            OscData::Vco(ref vco) => vco.sin(self.theta),
110        }
111    }
112
113    // Compute cosine of internal phase
114    pub fn cos(&self) -> f32 {
115        match self.osc {
116            OscData::Nco(ref nco) => nco.cos(self.theta),
117            OscData::Vco(ref vco) => vco.cos(self.theta),
118        }
119    }
120
121    // Compute sine and cosine of internal phase
122    pub fn sin_cos(&self) -> (f32, f32) {
123        match self.osc {
124            OscData::Nco(ref nco) => nco.sin_cos(self.theta),
125            OscData::Vco(ref vco) => vco.sin_cos(self.theta),
126        }
127    }
128
129    // Compute complex exponential of internal phase
130    pub fn cexp(&self) -> Complex<f32> {
131        let (sin, cos) = self.sin_cos();
132        Complex::new(cos, sin)
133    }
134
135    // PLL methods
136
137    // Set PLL bandwidth
138    pub fn pll_set_bandwidth(&mut self, bw: f32) {
139        if bw < 0.0 {
140            panic!("Bandwidth must be positive");
141        }
142        self.alpha = bw;
143        self.beta = bw.sqrt();
144    }
145
146    // Advance PLL phase
147    pub fn pll_step(&mut self, dphi: f32) {
148        self.adjust_frequency(dphi * self.alpha);
149        self.adjust_phase(dphi * self.beta);
150    }
151
152    // Mixing methods
153
154    // Mix up
155    pub fn mix_up(&self, input: Complex<f32>) -> Complex<f32> {
156        let (sin, cos) = self.sin_cos();
157        input * Complex::new(cos, sin)
158    }
159
160    // Mix block up
161    pub fn mix_block_up(&mut self, input: &[Complex<f32>], output: &mut [Complex<f32>]) -> Result<()> {
162        if input.len() != output.len() {
163            return Err(Error::Range("Input and output slices must have the same length".to_owned()));
164        }
165        for (x, y) in input.iter().zip(output.iter_mut()) {
166            *y = self.mix_up(*x);
167            self.step();
168        }
169        Ok(())
170    }
171
172    // Mix down
173    pub fn mix_down(&self, input: Complex<f32>) -> Complex<f32> {
174        let (sin, cos) = self.sin_cos();
175        input * Complex::new(cos, sin).conj()
176    }
177
178    // Mix block down
179    pub fn mix_block_down(&mut self, input: &[Complex<f32>], output: &mut [Complex<f32>]) -> Result<()> {
180        if input.len() != output.len() {
181            return Err(Error::Range("Input and output slices must have the same length".to_owned()));
182        }
183        for (x, y) in input.iter().zip(output.iter_mut()) {
184            *y = self.mix_down(*x);
185            self.step();
186        }
187        Ok(())
188    }
189
190    // Helper functions
191    fn constrain(theta: f32) -> u32 {
192        let mut theta = theta;
193        while theta >= 2.0 * PI {
194            theta -= 2.0 * PI;
195        }
196        while theta < 0.0 {
197            theta += 2.0 * PI;
198        }
199        ((theta / (2.0 * PI)) * (u32::MAX as f32)) as u32
200    }
201}
202
203// Additional implementations...
204
205#[cfg(test)]
206mod tests {
207    use crate::nco::{Osc, OscScheme};
208    use lazy_static::lazy_static;
209    use num_complex::Complex;
210    use std::f32::consts::PI;
211    use test_macro::autotest_annotate;
212    use crate::utility::test_helpers::{PsdRegion, validate_psd_spgramcf};
213    use crate::fft::spgram::Spgram;
214    use crate::math::windows::{hann, WindowType};
215
216    // Helper function to calculate phase/frequency error
217    fn pll_error(a: f32, b: f32) -> f32 {
218        let mut error = a - b;
219        while error >= 2.0 * PI {
220            error -= 2.0 * PI;
221        }
222        while error <= -2.0 * PI {
223            error += 2.0 * PI;
224        }
225        error
226    }
227
228    // Test phase-locked loop
229    fn nco_crcf_pll_test(scheme: OscScheme, phase_offset: f32, freq_offset: f32, pll_bandwidth: f32, num_iterations: usize, tol: f32) {
230        // Create NCO objects
231        let mut nco_tx = Osc::new(scheme);
232        let mut nco_rx = Osc::new(scheme);
233
234        // Initialize objects
235        nco_tx.set_phase(phase_offset);
236        nco_tx.set_frequency(freq_offset);
237        nco_rx.pll_set_bandwidth(pll_bandwidth);
238
239        // Run loop
240        for _ in 0..num_iterations {
241            // Received complex signal
242            let r = nco_tx.cexp();
243            let v = nco_rx.cexp();
244
245            // Error estimation
246            let phase_error = (r * v.conj()).arg();
247
248            // Update PLL
249            nco_rx.pll_step(phase_error);
250
251            // Update NCO objects
252            nco_tx.step();
253            nco_rx.step();
254        }
255
256        // Ensure phase of oscillators is locked
257        let phase_error = pll_error(nco_tx.get_phase(), nco_rx.get_phase());
258        assert!((phase_error).abs() < tol, "Phase error: {}", phase_error);
259
260        // Ensure frequency of oscillators is locked
261        let freq_error = pll_error(nco_tx.get_frequency(), nco_rx.get_frequency());
262        assert!((freq_error).abs() < tol, "Frequency error: {}", freq_error);
263
264        println!(
265            "nco[bw:{:.4},n={}], phase:{:.6},e={:.4e}, freq:{:.6},e={:.4e}",
266            pll_bandwidth, num_iterations, phase_offset, phase_error, freq_offset, freq_error
267        );
268    }
269
270    #[test]
271    #[autotest_annotate(autotest_nco_crcf_pll_phase)]
272    fn test_nco_crcf_pll_phase() {
273        let bandwidths = [0.1, 0.01, 0.001, 0.0001];
274        let tol = 1e-2;
275
276        for &bw in &bandwidths {
277            // Adjust number of steps according to loop bandwidth
278            let num_steps = (32.0 / bw) as usize;
279
280            // Test various phase offsets
281            nco_crcf_pll_test(OscScheme::Nco, -PI / 1.1, 0.0, bw, num_steps, tol);
282            nco_crcf_pll_test(OscScheme::Nco, -PI / 2.0, 0.0, bw, num_steps, tol);
283            nco_crcf_pll_test(OscScheme::Nco, -PI / 4.0, 0.0, bw, num_steps, tol);
284            nco_crcf_pll_test(OscScheme::Nco, -PI / 8.0, 0.0, bw, num_steps, tol);
285            nco_crcf_pll_test(OscScheme::Nco, PI / 8.0, 0.0, bw, num_steps, tol);
286            nco_crcf_pll_test(OscScheme::Nco, PI / 4.0, 0.0, bw, num_steps, tol);
287            nco_crcf_pll_test(OscScheme::Nco, PI / 2.0, 0.0, bw, num_steps, tol);
288            nco_crcf_pll_test(OscScheme::Nco, PI / 1.1, 0.0, bw, num_steps, tol);
289        }
290    }
291
292    #[test]
293    #[autotest_annotate(autotest_nco_crcf_pll_freq)]
294    fn test_nco_crcf_pll_freq() {
295        let bandwidths = [0.1, 0.05, 0.02, 0.01];
296        let tol = 1e-2;
297
298        for &bw in &bandwidths {
299            // Adjust number of steps according to loop bandwidth
300            let num_steps = (32.0 / bw) as usize;
301
302            // Test various frequency offsets
303            nco_crcf_pll_test(OscScheme::Nco, 0.0, -0.8, bw, num_steps, tol);
304            nco_crcf_pll_test(OscScheme::Nco, 0.0, -0.4, bw, num_steps, tol);
305            nco_crcf_pll_test(OscScheme::Nco, 0.0, -0.2, bw, num_steps, tol);
306            nco_crcf_pll_test(OscScheme::Nco, 0.0, -0.1, bw, num_steps, tol);
307            nco_crcf_pll_test(OscScheme::Nco, 0.0, 0.1, bw, num_steps, tol);
308            nco_crcf_pll_test(OscScheme::Nco, 0.0, 0.2, bw, num_steps, tol);
309            nco_crcf_pll_test(OscScheme::Nco, 0.0, 0.4, bw, num_steps, tol);
310            nco_crcf_pll_test(OscScheme::Nco, 0.0, 0.8, bw, num_steps, tol);
311        }
312    }
313
314    // Autotest helper function
315    fn nco_crcf_phase_test(scheme: OscScheme, theta: f32, expected_cos: f32, expected_sin: f32, tol: f32) {
316        // Create object
317        let mut nco = Osc::new(scheme);
318
319        // Set phase
320        nco.set_phase(theta);
321
322        // Compute cosine and sine outputs
323        let c = nco.cos();
324        let s = nco.sin();
325
326        println!(
327            "cos({:8.5}) = {:8.5} ({:8.5}) e:{:8.5}, sin({:8.5}) = {:8.5} ({:8.5}) e:{:8.5}",
328            theta,
329            expected_cos,
330            c,
331            expected_cos - c,
332            theta,
333            expected_sin,
334            s,
335            expected_sin - s
336        );
337
338        // Run tests
339        assert!((c - expected_cos).abs() < tol, "Cosine error: expected {}, got {}", expected_cos, c);
340        assert!((s - expected_sin).abs() < tol, "Sine error: expected {}, got {}", expected_sin, s);
341    }
342
343    #[test]
344    #[autotest_annotate(autotest_nco_crcf_phase)]
345    fn test_nco_crcf_phase() {
346        // Error tolerance (higher for NCO)
347        let tol = 0.02;
348
349        nco_crcf_phase_test(OscScheme::Nco, -6.283185307, 1.000000000, 0.000000000, tol);
350        nco_crcf_phase_test(OscScheme::Nco, -6.195739393, 0.996179042, 0.087334510, tol);
351        nco_crcf_phase_test(OscScheme::Nco, -5.951041106, 0.945345356, 0.326070787, tol);
352        nco_crcf_phase_test(OscScheme::Nco, -5.131745978, 0.407173250, 0.913350943, tol);
353        nco_crcf_phase_test(OscScheme::Nco, -4.748043551, 0.035647016, 0.999364443, tol);
354        nco_crcf_phase_test(OscScheme::Nco, -3.041191113, -0.994963998, -0.100232943, tol);
355        nco_crcf_phase_test(OscScheme::Nco, -1.947799864, -0.368136099, -0.929771914, tol);
356        nco_crcf_phase_test(OscScheme::Nco, -1.143752030, 0.414182352, -0.910193924, tol);
357        nco_crcf_phase_test(OscScheme::Nco, -1.029377689, 0.515352252, -0.856978446, tol);
358        nco_crcf_phase_test(OscScheme::Nco, -0.174356887, 0.984838307, -0.173474811, tol);
359        nco_crcf_phase_test(OscScheme::Nco, -0.114520496, 0.993449692, -0.114270338, tol);
360        nco_crcf_phase_test(OscScheme::Nco, 0.000000000, 1.000000000, 0.000000000, tol);
361        nco_crcf_phase_test(OscScheme::Nco, 1.436080000, 0.134309213, 0.990939471, tol);
362        nco_crcf_phase_test(OscScheme::Nco, 2.016119855, -0.430749878, 0.902471353, tol);
363        nco_crcf_phase_test(OscScheme::Nco, 2.996498473, -0.989492293, 0.144585621, tol);
364        nco_crcf_phase_test(OscScheme::Nco, 3.403689755, -0.965848729, -0.259106603, tol);
365        nco_crcf_phase_test(OscScheme::Nco, 3.591162483, -0.900634128, -0.434578148, tol);
366        nco_crcf_phase_test(OscScheme::Nco, 5.111428476, 0.388533479, -0.921434607, tol);
367        nco_crcf_phase_test(OscScheme::Nco, 5.727585681, 0.849584319, -0.527452828, tol);
368        nco_crcf_phase_test(OscScheme::Nco, 6.283185307, 1.000000000, -0.000000000, tol);
369    }
370
371    #[test]
372    #[autotest_annotate(autotest_nco_basic)]
373    fn test_nco_basic() {
374        let mut nco = Osc::new(OscScheme::Nco);
375
376        let tol = 1e-4; // Error tolerance
377        let f = 2.0 * PI / 64.0; // Frequency to test
378
379        nco.set_phase(0.0);
380        assert!((nco.cos() - 1.0).abs() < tol, "Cosine at phase 0 error");
381        assert!(nco.sin().abs() < tol, "Sine at phase 0 error");
382
383        let (s, c) = nco.sin_cos();
384        assert!(s.abs() < tol, "Sine at phase 0 error (sin_cos)");
385        assert!((c - 1.0).abs() < tol, "Cosine at phase 0 error (sin_cos)");
386
387        nco.set_phase(PI / 2.0);
388        assert!(nco.cos().abs() < tol, "Cosine at phase PI/2 error");
389        assert!((nco.sin() - 1.0).abs() < tol, "Sine at phase PI/2 error");
390
391        let (s, c) = nco.sin_cos();
392        assert!((s - 1.0).abs() < tol, "Sine at phase PI/2 error (sin_cos)");
393        assert!(c.abs() < tol, "Cosine at phase PI/2 error (sin_cos)");
394
395        // Cycle through one full period in 64 steps
396        nco.set_phase(0.0);
397        nco.set_frequency(f);
398        for i in 0..128 {
399            let (s, c) = nco.sin_cos();
400            assert!((s - (i as f32 * f).sin()).abs() < tol, "Sine error at step {}", i);
401            assert!((c - (i as f32 * f).cos()).abs() < tol, "Cosine error at step {}", i);
402            nco.step();
403        }
404
405        // Double frequency: cycle through one full period in 32 steps
406        nco.set_phase(0.0);
407        nco.set_frequency(2.0 * f);
408        for i in 0..128 {
409            let (s, c) = nco.sin_cos();
410            assert!((s - (i as f32 * 2.0 * f).sin()).abs() < tol, "Sine error at step {} (double frequency)", i);
411            assert!((c - (i as f32 * 2.0 * f).cos()).abs() < tol, "Cosine error at step {} (double frequency)", i);
412            nco.step();
413        }
414    }
415
416    #[test]
417    #[autotest_annotate(autotest_nco_mixing)]
418    fn test_nco_mixing() {
419        // frequency, phase
420        let f = 0.1;
421        let phi = PI;
422
423        // error tolerance (high for NCO)
424        let tol = 0.05;
425
426        // initialize nco object
427        let mut nco = Osc::new(OscScheme::Nco);
428        nco.set_frequency(f);
429        nco.set_phase(phi);
430
431        for _ in 0..64 {
432            // generate sin/cos
433            let (nco_q, nco_i) = nco.sin_cos();
434
435            // mix back to zero phase
436            let nco_cplx_in = Complex::new(nco_i, nco_q);
437            let nco_cplx_out = nco.mix_down(nco_cplx_in);
438
439            // assert mixer output is correct
440            assert!((nco_cplx_out.re - 1.0).abs() < tol, "Real part mixing error");
441            assert!(nco_cplx_out.im.abs() < tol, "Imaginary part mixing error");
442
443            // step nco
444            nco.step();
445        }
446    }
447
448    #[test]
449    #[autotest_annotate(autotest_nco_block_mixing)]
450    fn test_nco_block_mixing() {
451        // frequency, phase
452        let f = 0.1;
453        let phi = PI;
454
455        // error tolerance (high for NCO)
456        let tol = 0.05;
457
458        // number of samples
459        const NUM_SAMPLES: usize = 1024;
460
461        // store samples
462        let mut x = [Complex::new(0.0, 0.0); NUM_SAMPLES];
463        let mut y = [Complex::new(0.0, 0.0); NUM_SAMPLES];
464
465        // generate complex sin/cos
466        for i in 0..NUM_SAMPLES {
467            x[i] = Complex::new(0.0, f * i as f32 + phi).exp();
468        }
469
470        // initialize nco object
471        let mut nco = Osc::new(OscScheme::Nco);
472        nco.set_frequency(f);
473        nco.set_phase(phi);
474
475        // mix signal back to zero phase (in pieces)
476        let mut i = 0;
477        while i < NUM_SAMPLES {
478            let n = std::cmp::min(7, NUM_SAMPLES - i);
479            nco.mix_block_down(&x[i..i + n], &mut y[i..i + n]).unwrap();
480            i += n;
481        }
482
483        // assert mixer output is correct
484        for i in 0..NUM_SAMPLES {
485            assert!((y[i].re - 1.0).abs() < tol, "Real part mixing error at index {}", i);
486            assert!(y[i].im.abs() < tol, "Imaginary part mixing error at index {}", i);
487        }
488    }
489
490    fn testbench_nco_crcf_mix(scheme: OscScheme, phase: f32, frequency: f32) {
491        use rand::Rng;
492        // options
493        let buf_len = 1200;
494        let tol = 1e-2;
495
496        // create and initialize object
497        let mut nco = Osc::new(scheme);
498        nco.set_phase(phase);
499        nco.set_frequency(frequency);
500
501        // generate signal (pseudo-random)
502        let mut rng = rand::thread_rng();
503        let buf_0: Vec<Complex<f32>> = (0..buf_len).map(|_| Complex::new(0.0, 2.0 * PI * rng.gen::<f32>()).exp()).collect();
504
505        // mix signal
506        let mut buf_1 = vec![Complex::new(0.0, 0.0); buf_len];
507        nco.mix_block_up(&buf_0, &mut buf_1).unwrap();
508
509        // compare result to expected
510        let mut theta = phase;
511        for i in 0..buf_len {
512            let v = buf_0[i] * Complex::new(0.0, theta).exp();
513            assert!((buf_1[i].re - v.re).abs() < tol, "Real part mixing error at index {}", i);
514            assert!((buf_1[i].im - v.im).abs() < tol, "Imaginary part mixing error at index {}", i);
515
516            // update and constrain phase
517            theta += frequency;
518            while theta > PI {
519                theta -= 2.0 * PI;
520            }
521            while theta < -PI {
522                theta += 2.0 * PI;
523            }
524        }
525    }
526
527    // test NCO mixing
528    #[test]
529    #[autotest_annotate(autotest_nco_crcf_mix_nco_0)]
530    fn test_nco_crcf_mix_nco_0() {
531        testbench_nco_crcf_mix(OscScheme::Nco, 0.000, 0.000);
532    }
533
534    #[test]
535    #[autotest_annotate(autotest_nco_crcf_mix_nco_1)]
536    fn test_nco_crcf_mix_nco_1() {
537        testbench_nco_crcf_mix(OscScheme::Nco, 1.234, 0.000);
538    }
539
540    #[test]
541    #[autotest_annotate(autotest_nco_crcf_mix_nco_2)]
542    fn test_nco_crcf_mix_nco_2() {
543        testbench_nco_crcf_mix(OscScheme::Nco, -1.234, 0.000);
544    }
545
546    #[test]
547    #[autotest_annotate(autotest_nco_crcf_mix_nco_3)]
548    fn test_nco_crcf_mix_nco_3() {
549        testbench_nco_crcf_mix(OscScheme::Nco, 99.000, 0.000);
550    }
551
552    #[test]
553    #[autotest_annotate(autotest_nco_crcf_mix_nco_4)]
554    fn test_nco_crcf_mix_nco_4() {
555        testbench_nco_crcf_mix(OscScheme::Nco, PI, 0.000);
556    }
557
558    #[test]
559    #[autotest_annotate(autotest_nco_crcf_mix_nco_5)]
560    fn test_nco_crcf_mix_nco_5() {
561        testbench_nco_crcf_mix(OscScheme::Nco, 0.000, PI);
562    }
563
564    #[test]
565    #[autotest_annotate(autotest_nco_crcf_mix_nco_6)]
566    fn test_nco_crcf_mix_nco_6() {
567        testbench_nco_crcf_mix(OscScheme::Nco, 0.000, -PI);
568    }
569
570    #[test]
571    #[autotest_annotate(autotest_nco_crcf_mix_nco_7)]
572    fn test_nco_crcf_mix_nco_7() {
573        testbench_nco_crcf_mix(OscScheme::Nco, 0.000, 0.123);
574    }
575
576    #[test]
577    #[autotest_annotate(autotest_nco_crcf_mix_nco_8)]
578    fn test_nco_crcf_mix_nco_8() {
579        testbench_nco_crcf_mix(OscScheme::Nco, 0.000, -0.123);
580    }
581
582    #[test]
583    #[autotest_annotate(autotest_nco_crcf_mix_nco_9)]
584    fn test_nco_crcf_mix_nco_9() {
585        testbench_nco_crcf_mix(OscScheme::Nco, 0.000, 1e-5);
586    }
587
588    #[test]
589    #[autotest_annotate(autotest_nco_crcf_mix_vco_0)]
590    fn test_nco_crcf_mix_vco_0() {
591        testbench_nco_crcf_mix(OscScheme::Vco, 0.000, 0.000);
592    }
593
594    #[test]
595    #[autotest_annotate(autotest_nco_crcf_mix_vco_1)]
596    fn test_nco_crcf_mix_vco_1() {
597        testbench_nco_crcf_mix(OscScheme::Vco, 1.234, 0.000);
598    }
599
600    #[test]
601    #[autotest_annotate(autotest_nco_crcf_mix_vco_2)]
602    fn test_nco_crcf_mix_vco_2() {
603        testbench_nco_crcf_mix(OscScheme::Vco, -1.234, 0.000);
604    }
605
606    #[test]
607    #[autotest_annotate(autotest_nco_crcf_mix_vco_3)]
608    fn test_nco_crcf_mix_vco_3() {
609        testbench_nco_crcf_mix(OscScheme::Vco, 99.000, 0.000);
610    }
611
612    #[test]
613    #[autotest_annotate(autotest_nco_crcf_mix_vco_4)]
614    fn test_nco_crcf_mix_vco_4() {
615        testbench_nco_crcf_mix(OscScheme::Vco, PI, 0.000);
616    }
617
618    #[test]
619    #[autotest_annotate(autotest_nco_crcf_mix_vco_5)]
620    fn test_nco_crcf_mix_vco_5() {
621        testbench_nco_crcf_mix(OscScheme::Vco, 0.000, PI);
622    }
623
624    #[test]
625    #[autotest_annotate(autotest_nco_crcf_mix_vco_6)]
626    fn test_nco_crcf_mix_vco_6() {
627        testbench_nco_crcf_mix(OscScheme::Vco, 0.000, -PI);
628    }
629
630    #[test]
631    #[autotest_annotate(autotest_nco_crcf_mix_vco_7)]
632    fn test_nco_crcf_mix_vco_7() {
633        testbench_nco_crcf_mix(OscScheme::Vco, 0.000, 0.123);
634    }
635
636    #[test]
637    #[autotest_annotate(autotest_nco_crcf_mix_vco_8)]
638    fn test_nco_crcf_mix_vco_8() {
639        testbench_nco_crcf_mix(OscScheme::Vco, 0.000, -0.123);
640    }
641
642    #[test]
643    #[autotest_annotate(autotest_nco_crcf_mix_vco_9)]
644    fn test_nco_crcf_mix_vco_9() {
645        testbench_nco_crcf_mix(OscScheme::Vco, 0.000, 1e-5);
646    }
647
648    fn nco_crcf_spectrum_test(scheme: OscScheme, freq: f32) {
649        let num_samples = 1 << 16;
650        let nfft: usize = 9600;
651
652        let mut nco = Osc::new(scheme);
653        nco.set_frequency(2.0 * PI * freq);
654
655        let buf_len = 3 * nfft;
656        let mut buf_0 = vec![Complex::new(0.0, 0.0); buf_len];
657        let mut buf_1 = vec![Complex::new(0.0, 0.0); buf_len];
658        for i in 0..buf_len {
659            buf_0[i] = Complex::new(1.0 / (nfft as f32).sqrt(), 0.0);
660        }
661
662        let mut psd = Spgram::new(nfft, WindowType::BlackmanHarris, nfft, nfft / 2).unwrap();
663
664        while psd.get_num_samples_total() < num_samples {
665            nco.mix_block_up(&buf_0, &mut buf_1).unwrap();
666            if psd.get_num_samples_total() == 0 {
667                for i in 0..buf_len {
668                    buf_1[i] *= hann(i, 2 * buf_len).unwrap();
669                }
670            }
671            psd.write(&buf_1);
672        }
673
674        #[rustfmt::skip]
675        let regions = [
676            PsdRegion { fmin:         -0.5, fmax: freq - 0.002, pmin: 0.0, pmax: -60.0, test_lo: false, test_hi: true },
677            PsdRegion { fmin: freq - 0.002, fmax: freq + 0.002, pmin: 0.0, pmax:   0.0, test_lo: false, test_hi: true },
678            PsdRegion { fmin: freq + 0.002, fmax: 0.5,          pmin: 0.0, pmax: -60.0, test_lo: false, test_hi: true },
679        ];
680
681        let result = validate_psd_spgramcf(&psd, &regions).unwrap();
682        assert!(result, "PSD test failed");
683    }
684
685    #[test]
686    #[autotest_annotate(autotest_nco_crcf_spectrum_nco_f00)]
687    fn test_nco_crcf_spectrum_nco_0() {
688        nco_crcf_spectrum_test(OscScheme::Nco, 0.000);
689    }
690
691    #[test]
692    #[autotest_annotate(autotest_nco_crcf_spectrum_nco_f01)]
693    fn test_nco_crcf_spectrum_nco_1() {
694        nco_crcf_spectrum_test(OscScheme::Nco, 0.1234);
695    }
696
697    #[test]
698    #[autotest_annotate(autotest_nco_crcf_spectrum_nco_f02)]
699    fn test_nco_crcf_spectrum_nco_2() {
700        nco_crcf_spectrum_test(OscScheme::Nco, -0.1234);
701    }
702
703    #[test]
704    #[autotest_annotate(autotest_nco_crcf_spectrum_nco_f03)]
705    fn test_nco_crcf_spectrum_nco_3() {
706        nco_crcf_spectrum_test(OscScheme::Nco, 0.25);
707    }
708
709    #[test]
710    #[autotest_annotate(autotest_nco_crcf_spectrum_nco_f04)]
711    fn test_nco_crcf_spectrum_nco_4() {
712        nco_crcf_spectrum_test(OscScheme::Nco, 0.1);
713    }
714    #[test]
715    #[autotest_annotate(autotest_nco_crcf_spectrum_vco_f00)]
716    fn test_nco_crcf_spectrum_vco_0() {
717        nco_crcf_spectrum_test(OscScheme::Vco, 0.0);
718    }
719
720    #[test]
721    #[autotest_annotate(autotest_nco_crcf_spectrum_vco_f01)]
722    fn test_nco_crcf_spectrum_vco_1() {
723        nco_crcf_spectrum_test(OscScheme::Vco, 0.1234);
724    }
725
726    #[test]
727    #[autotest_annotate(autotest_nco_crcf_spectrum_vco_f02)]
728    fn test_nco_crcf_spectrum_vco_2() {
729        nco_crcf_spectrum_test(OscScheme::Vco, -0.1234);
730    }
731
732    #[test]
733    #[autotest_annotate(autotest_nco_crcf_spectrum_vco_f03)]
734    fn test_nco_crcf_spectrum_vco_3() {
735        nco_crcf_spectrum_test(OscScheme::Vco, 0.25);
736    }
737
738    #[test]
739    #[autotest_annotate(autotest_nco_crcf_spectrum_vco_f04)]
740    fn test_nco_crcf_spectrum_vco_4() {
741        nco_crcf_spectrum_test(OscScheme::Vco, 0.1);
742    }
743
744    // autotest helper function
745    fn nco_crcf_frequency_test(scheme: OscScheme, phase: f32, frequency: f32, sincos: &[Complex<f32>], num_samples: usize, tol: f32) {
746        // create object
747        let mut nco = Osc::new(scheme);
748
749        // set phase and frequency
750        nco.set_phase(phase);
751        nco.set_frequency(frequency);
752
753        // run trials
754        for i in 0..num_samples {
755            // compute complex output
756            let y_test = nco.cexp();
757
758            // compare to expected output
759            let y = sincos[i];
760
761            // run tests
762            assert!((y_test.re - y.re).abs() < tol, "Real part error at index {}: expected {}, got {}", i, y.re, y_test.re);
763            assert!((y_test.im - y.im).abs() < tol, "Imaginary part error at index {}: expected {}, got {}", i, y.im, y_test.im);
764
765            // step oscillator
766            nco.step();
767        }
768    }
769
770    #[test]
771    #[autotest_annotate(autotest_nco_crcf_frequency)]
772    fn test_nco_crcf_frequency() {
773        // error tolerance (higher for NCO)
774        let tol = 0.04;
775
776        // test frequencies with irrational values
777        nco_crcf_frequency_test(OscScheme::Nco, 0.0, 1.0 / 2.0_f32.sqrt(), &NCO_SINCOS_FSQRT1_2, 256, tol); // 1/sqrt(2)
778        nco_crcf_frequency_test(OscScheme::Nco, 0.0, 1.0 / 3.0_f32.sqrt(), &NCO_SINCOS_FSQRT1_3, 256, tol); // 1/sqrt(3)
779        nco_crcf_frequency_test(OscScheme::Nco, 0.0, 1.0 / 5.0_f32.sqrt(), &NCO_SINCOS_FSQRT1_5, 256, tol); // 1/sqrt(5)
780        nco_crcf_frequency_test(OscScheme::Nco, 0.0, 1.0 / 7.0_f32.sqrt(), &NCO_SINCOS_FSQRT1_7, 256, tol); // 1/sqrt(7)
781    }
782
783    pub fn generate_sincos(frequency: f32, num_samples: usize) -> Vec<Complex<f32>> {
784        (0..num_samples)
785            .map(|i| {
786                let phase = i as f32 * frequency;
787                Complex::new(phase.cos(), phase.sin())
788            })
789            .collect()
790    }
791
792    // note these stand in for liquid's nco_sincos_fsqrt1_2, etc.
793    lazy_static! {
794        pub static ref NCO_SINCOS_FSQRT1_2: Vec<Complex<f32>> = generate_sincos(1.0 / 2.0_f32.sqrt(), 256);
795        pub static ref NCO_SINCOS_FSQRT1_3: Vec<Complex<f32>> = generate_sincos(1.0 / 3.0_f32.sqrt(), 256);
796        pub static ref NCO_SINCOS_FSQRT1_5: Vec<Complex<f32>> = generate_sincos(1.0 / 5.0_f32.sqrt(), 256);
797        pub static ref NCO_SINCOS_FSQRT1_7: Vec<Complex<f32>> = generate_sincos(1.0 / 7.0_f32.sqrt(), 256);
798    }
799}