quantrs2_device/
pulse_scirs2_signal.rs

1//! Enhanced Pulse Control with SciRS2 Signal Processing
2//!
3//! This module extends the basic pulse control with advanced signal processing
4//! capabilities from SciRS2, including:
5//! - Frequency domain analysis via FFT
6//! - Pulse shape optimization
7//! - Spectral filtering
8//! - Noise characterization
9//! - Signal quality metrics
10
11use crate::{
12    pulse::{ChannelType, PulseCalibration, PulseInstruction, PulseSchedule, PulseShape},
13    DeviceError, DeviceResult,
14};
15use scirs2_core::{
16    ndarray::{Array1, Array2},
17    Complex64,
18};
19use scirs2_fft::{fft, ifft};
20use std::collections::HashMap;
21use std::f64::consts::PI;
22
23/// Signal processing configuration for pulse control
24#[derive(Debug, Clone)]
25pub struct SignalProcessingConfig {
26    /// Enable FFT-based pulse optimization
27    pub enable_fft_optimization: bool,
28    /// Enable spectral filtering
29    pub enable_filtering: bool,
30    /// Sampling rate (Hz)
31    pub sample_rate: f64,
32    /// Filter cutoff frequency (Hz)
33    pub filter_cutoff: f64,
34    /// Filter order
35    pub filter_order: usize,
36    /// Window function for spectral analysis
37    pub window_function: WindowType,
38    /// FFT size (power of 2)
39    pub fft_size: usize,
40}
41
42impl Default for SignalProcessingConfig {
43    fn default() -> Self {
44        Self {
45            enable_fft_optimization: true,
46            enable_filtering: true,
47            sample_rate: 1e9,     // 1 GHz default
48            filter_cutoff: 100e6, // 100 MHz
49            filter_order: 4,
50            window_function: WindowType::Hamming,
51            fft_size: 1024,
52        }
53    }
54}
55
56/// Window function types for signal processing
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum WindowType {
59    /// Rectangular window (no windowing)
60    Rectangular,
61    /// Hamming window
62    Hamming,
63    /// Hanning window
64    Hanning,
65    /// Blackman window
66    Blackman,
67    /// Kaiser window
68    Kaiser,
69}
70
71impl WindowType {
72    /// Apply window function to signal
73    pub fn apply(&self, signal: &mut Array1<Complex64>) {
74        let n = signal.len();
75        match self {
76            WindowType::Rectangular => {
77                // No windowing
78            }
79            WindowType::Hamming => {
80                for i in 0..n {
81                    let w = 0.54 - 0.46 * (2.0 * PI * i as f64 / (n - 1) as f64).cos();
82                    signal[i] *= w;
83                }
84            }
85            WindowType::Hanning => {
86                for i in 0..n {
87                    let w = 0.5 * (1.0 - (2.0 * PI * i as f64 / (n - 1) as f64).cos());
88                    signal[i] *= w;
89                }
90            }
91            WindowType::Blackman => {
92                for i in 0..n {
93                    let w = 0.42 - 0.5 * (2.0 * PI * i as f64 / (n - 1) as f64).cos()
94                        + 0.08 * (4.0 * PI * i as f64 / (n - 1) as f64).cos();
95                    signal[i] *= w;
96                }
97            }
98            WindowType::Kaiser => {
99                // Simplified Kaiser window (beta=5)
100                for i in 0..n {
101                    let x = 2.0 * i as f64 / (n - 1) as f64 - 1.0;
102                    let w = if x.abs() < 1.0 {
103                        (1.0 - x * x).sqrt()
104                    } else {
105                        0.0
106                    };
107                    signal[i] *= w;
108                }
109            }
110        }
111    }
112}
113
114/// Pulse signal quality metrics
115#[derive(Debug, Clone)]
116pub struct PulseQualityMetrics {
117    /// Signal-to-noise ratio (dB)
118    pub snr: f64,
119    /// Peak signal power
120    pub peak_power: f64,
121    /// Average signal power
122    pub average_power: f64,
123    /// Bandwidth (Hz)
124    pub bandwidth: f64,
125    /// Center frequency (Hz)
126    pub center_frequency: f64,
127    /// Spectral purity (fraction of power in main lobe)
128    pub spectral_purity: f64,
129    /// Total harmonic distortion (%)
130    pub thd: f64,
131}
132
133/// Spectral analysis result
134#[derive(Debug, Clone)]
135pub struct SpectralAnalysisResult {
136    /// Frequency bins (Hz)
137    pub frequencies: Vec<f64>,
138    /// Power spectral density
139    pub psd: Vec<f64>,
140    /// Dominant frequency components
141    pub peaks: Vec<(f64, f64)>, // (frequency, power)
142    /// Total signal power
143    pub total_power: f64,
144}
145
146/// Enhanced pulse controller with SciRS2 signal processing
147pub struct SciRS2PulseController {
148    config: SignalProcessingConfig,
149    calibration: Option<PulseCalibration>,
150}
151
152impl SciRS2PulseController {
153    /// Create a new SciRS2 pulse controller
154    pub fn new(config: SignalProcessingConfig) -> Self {
155        Self {
156            config,
157            calibration: None,
158        }
159    }
160
161    /// Set calibration data
162    pub fn set_calibration(&mut self, calibration: PulseCalibration) {
163        self.calibration = Some(calibration);
164    }
165
166    /// Optimize pulse shape using FFT analysis
167    pub fn optimize_pulse_shape(
168        &self,
169        pulse: &PulseShape,
170        sample_rate: f64,
171    ) -> DeviceResult<PulseShape> {
172        // Convert pulse to time-domain samples
173        let samples = self.pulse_to_samples(pulse, sample_rate)?;
174
175        // Apply FFT for frequency domain analysis
176        let fft_result = self.compute_fft(&samples)?;
177
178        // Analyze spectrum and filter unwanted components
179        let filtered_fft = self.apply_spectral_filter(&fft_result)?;
180
181        // Convert back to time domain
182        let optimized_samples = self.compute_ifft(&filtered_fft)?;
183
184        // Create optimized pulse
185        Ok(PulseShape::Arbitrary {
186            samples: optimized_samples,
187            sample_rate,
188        })
189    }
190
191    /// Convert pulse shape to time-domain samples
192    fn pulse_to_samples(
193        &self,
194        pulse: &PulseShape,
195        sample_rate: f64,
196    ) -> DeviceResult<Vec<Complex64>> {
197        match pulse {
198            PulseShape::Gaussian {
199                duration,
200                sigma,
201                amplitude,
202            } => {
203                let n_samples = (duration * sample_rate) as usize;
204                let mut samples = Vec::with_capacity(n_samples);
205                let t_center = duration / 2.0;
206
207                for i in 0..n_samples {
208                    let t = i as f64 / sample_rate;
209                    let gaussian = (-(t - t_center).powi(2) / (2.0 * sigma.powi(2))).exp();
210                    samples.push(*amplitude * gaussian);
211                }
212
213                Ok(samples)
214            }
215            PulseShape::GaussianDrag {
216                duration,
217                sigma,
218                amplitude,
219                beta,
220            } => {
221                let n_samples = (duration * sample_rate) as usize;
222                let mut samples = Vec::with_capacity(n_samples);
223                let t_center = duration / 2.0;
224
225                for i in 0..n_samples {
226                    let t = i as f64 / sample_rate;
227                    let t_shifted = t - t_center;
228                    let gaussian = (-(t_shifted).powi(2) / (2.0 * sigma.powi(2))).exp();
229                    let derivative = -t_shifted / sigma.powi(2) * gaussian;
230                    let drag = gaussian + Complex64::i() * beta * derivative;
231                    samples.push(*amplitude * drag);
232                }
233
234                Ok(samples)
235            }
236            PulseShape::Square {
237                duration,
238                amplitude,
239            } => {
240                let n_samples = (duration * sample_rate) as usize;
241                Ok(vec![*amplitude; n_samples])
242            }
243            PulseShape::CosineTapered {
244                duration,
245                amplitude,
246                rise_time,
247            } => {
248                let n_samples = (duration * sample_rate) as usize;
249                let n_rise = (rise_time * sample_rate) as usize;
250                let mut samples = Vec::with_capacity(n_samples);
251
252                for i in 0..n_samples {
253                    let t = i as f64 / sample_rate;
254                    let envelope = if t < *rise_time {
255                        0.5 * (1.0 - (PI * (rise_time - t) / rise_time).cos())
256                    } else if t > *duration - *rise_time {
257                        0.5 * (1.0 - (PI * (t - (*duration - *rise_time)) / rise_time).cos())
258                    } else {
259                        1.0
260                    };
261                    samples.push(*amplitude * envelope);
262                }
263
264                Ok(samples)
265            }
266            PulseShape::Arbitrary {
267                samples,
268                sample_rate: _,
269            } => Ok(samples.clone()),
270        }
271    }
272
273    /// Compute FFT of signal
274    fn compute_fft(&self, samples: &[Complex64]) -> DeviceResult<Array1<Complex64>> {
275        let mut signal = Array1::from(samples.to_vec());
276
277        // Apply window function
278        self.config.window_function.apply(&mut signal);
279
280        // Pad to FFT size
281        let mut padded_vec = vec![Complex64::new(0.0, 0.0); self.config.fft_size];
282        let copy_len = samples.len().min(self.config.fft_size);
283        for i in 0..copy_len {
284            padded_vec[i] = signal[i];
285        }
286
287        // Perform FFT using scirs2-fft high-level API
288        let fft_result = fft(&padded_vec, None)
289            .map_err(|e| DeviceError::InvalidInput(format!("FFT failed: {}", e)))?;
290
291        Ok(Array1::from(fft_result))
292    }
293
294    /// Compute inverse FFT
295    fn compute_ifft(&self, spectrum: &Array1<Complex64>) -> DeviceResult<Vec<Complex64>> {
296        // Convert Array1 to Vec for scirs2-fft API
297        let spectrum_vec = spectrum.to_vec();
298
299        // Perform IFFT using scirs2-fft high-level API
300        let ifft_result = ifft(&spectrum_vec, None)
301            .map_err(|e| DeviceError::InvalidInput(format!("IFFT failed: {}", e)))?;
302
303        Ok(ifft_result)
304    }
305
306    /// Apply spectral filter to remove unwanted frequency components
307    fn apply_spectral_filter(
308        &self,
309        spectrum: &Array1<Complex64>,
310    ) -> DeviceResult<Array1<Complex64>> {
311        if !self.config.enable_filtering {
312            return Ok(spectrum.clone());
313        }
314
315        let mut filtered = spectrum.clone();
316        let cutoff_bin = (self.config.filter_cutoff * self.config.fft_size as f64
317            / self.config.sample_rate) as usize;
318
319        // Apply low-pass filter (simple frequency domain cutoff)
320        for i in cutoff_bin..filtered.len() {
321            filtered[i] = Complex64::new(0.0, 0.0);
322        }
323
324        Ok(filtered)
325    }
326
327    /// Analyze pulse spectrum
328    pub fn analyze_spectrum(
329        &self,
330        pulse: &PulseShape,
331        sample_rate: f64,
332    ) -> DeviceResult<SpectralAnalysisResult> {
333        let samples = self.pulse_to_samples(pulse, sample_rate)?;
334        let fft_result = self.compute_fft(&samples)?;
335
336        // Compute power spectral density
337        let psd: Vec<f64> = fft_result.iter().map(|c| c.norm_sqr()).collect();
338
339        // Frequency bins
340        let df = sample_rate / self.config.fft_size as f64;
341        let frequencies: Vec<f64> = (0..psd.len()).map(|i| i as f64 * df).collect();
342
343        // Find peaks (local maxima)
344        let mut peaks = Vec::new();
345        let max_psd = psd.iter().cloned().fold(0.0f64, f64::max);
346        let threshold = 0.001 * max_psd; // Lower threshold (0.1% of max)
347
348        for i in 1..psd.len() - 1 {
349            if psd[i] > psd[i - 1] && psd[i] > psd[i + 1] && psd[i] > threshold {
350                peaks.push((frequencies[i], psd[i]));
351            }
352        }
353
354        // If no peaks found, add the maximum value as a peak
355        if peaks.is_empty() {
356            if let Some(max_idx) = psd
357                .iter()
358                .enumerate()
359                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
360                .map(|(idx, _)| idx)
361            {
362                peaks.push((frequencies[max_idx], psd[max_idx]));
363            }
364        }
365
366        // Sort peaks by power
367        peaks.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
368        peaks.truncate(10); // Keep top 10 peaks
369
370        // Total power
371        let total_power: f64 = psd.iter().sum();
372
373        Ok(SpectralAnalysisResult {
374            frequencies,
375            psd,
376            peaks,
377            total_power,
378        })
379    }
380
381    /// Compute pulse quality metrics
382    pub fn compute_quality_metrics(
383        &self,
384        pulse: &PulseShape,
385        sample_rate: f64,
386    ) -> DeviceResult<PulseQualityMetrics> {
387        let samples = self.pulse_to_samples(pulse, sample_rate)?;
388        let spectrum = self.analyze_spectrum(pulse, sample_rate)?;
389
390        // Signal power
391        let signal_power: f64 =
392            samples.iter().map(|s| s.norm_sqr()).sum::<f64>() / samples.len() as f64;
393        let peak_power = samples.iter().map(|s| s.norm_sqr()).fold(0.0f64, f64::max);
394
395        // Estimate SNR (simplified)
396        let noise_floor = spectrum.psd.iter().cloned().fold(f64::INFINITY, f64::min);
397        let snr = 10.0 * (signal_power / noise_floor.max(1e-10)).log10();
398
399        // Bandwidth (3dB bandwidth)
400        let max_psd = spectrum.psd.iter().cloned().fold(0.0f64, f64::max);
401        let threshold = max_psd / 2.0; // -3dB point
402        let bandwidth = spectrum
403            .frequencies
404            .iter()
405            .zip(&spectrum.psd)
406            .filter(|(_, &p)| p > threshold)
407            .count() as f64
408            * (sample_rate / self.config.fft_size as f64);
409
410        // Center frequency (weighted average)
411        let center_frequency = if spectrum.total_power > 0.0 {
412            spectrum
413                .frequencies
414                .iter()
415                .zip(&spectrum.psd)
416                .map(|(f, p)| f * p)
417                .sum::<f64>()
418                / spectrum.total_power
419        } else {
420            0.0
421        };
422
423        // Spectral purity (power in main peak vs total)
424        let main_peak_power = spectrum.peaks.first().map(|(_, p)| *p).unwrap_or(0.0);
425        let spectral_purity = if spectrum.total_power > 0.0 {
426            main_peak_power / spectrum.total_power
427        } else {
428            0.0
429        };
430
431        // Total harmonic distortion (simplified)
432        let fundamental = spectrum.peaks.first().map(|(_, p)| *p).unwrap_or(0.0);
433        let harmonics: f64 = spectrum.peaks.iter().skip(1).take(5).map(|(_, p)| p).sum();
434        let thd = if fundamental > 0.0 {
435            100.0 * (harmonics / fundamental).sqrt()
436        } else {
437            0.0
438        };
439
440        Ok(PulseQualityMetrics {
441            snr,
442            peak_power,
443            average_power: signal_power,
444            bandwidth,
445            center_frequency,
446            spectral_purity,
447            thd,
448        })
449    }
450
451    /// Optimize pulse schedule using signal processing
452    pub fn optimize_schedule(&self, schedule: &PulseSchedule) -> DeviceResult<PulseSchedule> {
453        let mut optimized = schedule.clone();
454
455        if self.config.enable_fft_optimization {
456            // Optimize each pulse in the schedule
457            for instruction in &mut optimized.instructions {
458                let sample_rate = self
459                    .calibration
460                    .as_ref()
461                    .map(|c| 1.0 / c.dt)
462                    .unwrap_or(self.config.sample_rate);
463
464                instruction.pulse = self.optimize_pulse_shape(&instruction.pulse, sample_rate)?;
465            }
466        }
467
468        Ok(optimized)
469    }
470
471    /// Generate pulse quality report
472    pub fn generate_quality_report(
473        &self,
474        pulse: &PulseShape,
475        sample_rate: f64,
476    ) -> DeviceResult<String> {
477        let metrics = self.compute_quality_metrics(pulse, sample_rate)?;
478        let spectrum = self.analyze_spectrum(pulse, sample_rate)?;
479
480        let mut report = String::from("=== Pulse Quality Analysis Report ===\n\n");
481        report.push_str("Signal Quality Metrics:\n");
482        report.push_str(&format!("  SNR: {:.2} dB\n", metrics.snr));
483        report.push_str(&format!("  Peak Power: {:.4}\n", metrics.peak_power));
484        report.push_str(&format!("  Average Power: {:.4}\n", metrics.average_power));
485        report.push_str(&format!(
486            "  Bandwidth: {:.2} MHz\n",
487            metrics.bandwidth / 1e6
488        ));
489        report.push_str(&format!(
490            "  Center Frequency: {:.2} MHz\n",
491            metrics.center_frequency / 1e6
492        ));
493        report.push_str(&format!(
494            "  Spectral Purity: {:.1}%\n",
495            metrics.spectral_purity * 100.0
496        ));
497        report.push_str(&format!("  THD: {:.2}%\n\n", metrics.thd));
498
499        report.push_str("Spectral Analysis:\n");
500        report.push_str(&format!("  Total Power: {:.4}\n", spectrum.total_power));
501        report.push_str(&format!("  Number of Peaks: {}\n", spectrum.peaks.len()));
502        report.push_str("  Top Frequency Components:\n");
503        for (i, (freq, power)) in spectrum.peaks.iter().take(5).enumerate() {
504            report.push_str(&format!(
505                "    {}: {:.2} MHz ({:.2}% of total)\n",
506                i + 1,
507                freq / 1e6,
508                100.0 * power / spectrum.total_power
509            ));
510        }
511
512        Ok(report)
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519
520    #[test]
521    fn test_controller_creation() {
522        let config = SignalProcessingConfig::default();
523        let controller = SciRS2PulseController::new(config);
524        assert!(controller.calibration.is_none());
525    }
526
527    #[test]
528    fn test_pulse_to_samples_gaussian() {
529        let config = SignalProcessingConfig::default();
530        let controller = SciRS2PulseController::new(config);
531
532        let pulse = PulseShape::Gaussian {
533            duration: 100e-9, // 100 ns
534            sigma: 20e-9,     // 20 ns
535            amplitude: Complex64::new(1.0, 0.0),
536        };
537
538        let samples = controller
539            .pulse_to_samples(&pulse, 1e9)
540            .expect("Failed to convert pulse to samples");
541
542        assert_eq!(samples.len(), 100);
543        assert!(samples.iter().all(|s| s.norm() <= 1.0));
544    }
545
546    #[test]
547    fn test_pulse_to_samples_square() {
548        let config = SignalProcessingConfig::default();
549        let controller = SciRS2PulseController::new(config);
550
551        let pulse = PulseShape::Square {
552            duration: 50e-9,
553            amplitude: Complex64::new(0.5, 0.0),
554        };
555
556        let samples = controller
557            .pulse_to_samples(&pulse, 1e9)
558            .expect("Failed to convert pulse to samples");
559
560        assert_eq!(samples.len(), 50);
561        assert!(samples.iter().all(|s| (s.norm() - 0.5).abs() < 1e-10));
562    }
563
564    #[test]
565    fn test_window_functions() {
566        let mut signal = Array1::from(vec![
567            Complex64::new(1.0, 0.0),
568            Complex64::new(1.0, 0.0),
569            Complex64::new(1.0, 0.0),
570            Complex64::new(1.0, 0.0),
571        ]);
572
573        // Test Hamming window
574        WindowType::Hamming.apply(&mut signal);
575        assert!(signal[0].re < 1.0); // Windowed values should be < 1
576        assert!(signal[signal.len() - 1].re < 1.0);
577
578        // Test Hanning window
579        let mut signal2 = Array1::from(vec![Complex64::new(1.0, 0.0); 4]);
580        WindowType::Hanning.apply(&mut signal2);
581        assert!(signal2[0].re < 1.0);
582    }
583
584    #[test]
585    fn test_spectrum_analysis() {
586        let config = SignalProcessingConfig {
587            fft_size: 256,
588            ..Default::default()
589        };
590        let controller = SciRS2PulseController::new(config);
591
592        let pulse = PulseShape::Gaussian {
593            duration: 100e-9,
594            sigma: 20e-9,
595            amplitude: Complex64::new(1.0, 0.0),
596        };
597
598        let spectrum = controller
599            .analyze_spectrum(&pulse, 1e9)
600            .expect("Failed to analyze spectrum");
601
602        assert_eq!(spectrum.frequencies.len(), spectrum.psd.len());
603        assert!(spectrum.total_power > 0.0);
604        assert!(!spectrum.peaks.is_empty());
605    }
606
607    #[test]
608    fn test_quality_metrics() {
609        let config = SignalProcessingConfig {
610            fft_size: 256,
611            ..Default::default()
612        };
613        let controller = SciRS2PulseController::new(config);
614
615        let pulse = PulseShape::Gaussian {
616            duration: 100e-9,
617            sigma: 20e-9,
618            amplitude: Complex64::new(1.0, 0.0),
619        };
620
621        let metrics = controller
622            .compute_quality_metrics(&pulse, 1e9)
623            .expect("Failed to compute metrics");
624
625        assert!(metrics.peak_power > 0.0);
626        assert!(metrics.average_power > 0.0);
627        assert!(metrics.bandwidth > 0.0);
628        assert!(metrics.spectral_purity >= 0.0 && metrics.spectral_purity <= 1.0);
629    }
630
631    #[test]
632    fn test_quality_report_generation() {
633        let config = SignalProcessingConfig {
634            fft_size: 256,
635            ..Default::default()
636        };
637        let controller = SciRS2PulseController::new(config);
638
639        let pulse = PulseShape::Gaussian {
640            duration: 100e-9,
641            sigma: 20e-9,
642            amplitude: Complex64::new(1.0, 0.0),
643        };
644
645        let report = controller
646            .generate_quality_report(&pulse, 1e9)
647            .expect("Failed to generate report");
648
649        assert!(report.contains("SNR"));
650        assert!(report.contains("Bandwidth"));
651        assert!(report.contains("Spectral Analysis"));
652    }
653
654    #[test]
655    fn test_pulse_optimization() {
656        let config = SignalProcessingConfig {
657            fft_size: 128,
658            enable_fft_optimization: true,
659            ..Default::default()
660        };
661        let controller = SciRS2PulseController::new(config);
662
663        let pulse = PulseShape::Square {
664            duration: 50e-9,
665            amplitude: Complex64::new(1.0, 0.0),
666        };
667
668        let optimized = controller
669            .optimize_pulse_shape(&pulse, 1e9)
670            .expect("Failed to optimize pulse");
671
672        // Optimized pulse should be Arbitrary type
673        match optimized {
674            PulseShape::Arbitrary { samples, .. } => {
675                assert!(!samples.is_empty());
676            }
677            _ => panic!("Expected Arbitrary pulse shape"),
678        }
679    }
680}