quantrs2_device/continuous_variable/
heterodyne.rs

1//! Heterodyne detection for continuous variable quantum systems
2//!
3//! This module implements heterodyne detection, which simultaneously measures both
4//! quadratures of the quantum field using two local oscillators in quadrature.
5
6use super::{CVDeviceConfig, Complex, GaussianState};
7use crate::{DeviceError, DeviceResult};
8use scirs2_core::random::prelude::*;
9use scirs2_core::random::{Distribution, RandNormal};
10// Alias for backward compatibility
11type Normal<T> = RandNormal<T>;
12use serde::{Deserialize, Serialize};
13use std::f64::consts::PI;
14
15/// Heterodyne detector configuration
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct HeterodyneDetectorConfig {
18    /// Local oscillator power (mW)
19    pub lo_power_mw: f64,
20    /// Detection efficiency
21    pub efficiency: f64,
22    /// Electronic noise (V²/Hz)
23    pub electronic_noise: f64,
24    /// Detector bandwidth (Hz)
25    pub bandwidth_hz: f64,
26    /// Intermediate frequency (Hz)
27    pub intermediate_frequency_hz: f64,
28    /// Phase lock loop configurations for both LOs
29    pub pll_x_config: PLLConfig,
30    pub pll_p_config: PLLConfig,
31    /// Photodiode specifications
32    pub photodiode_config: PhotodiodeConfig,
33    /// IQ demodulator config
34    pub iq_demod_config: IQDemodulatorConfig,
35}
36
37/// Phase-locked loop configuration
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct PLLConfig {
40    /// Loop bandwidth (Hz)
41    pub loop_bandwidth_hz: f64,
42    /// Phase noise (rad²/Hz)
43    pub phase_noise_density: f64,
44    /// Lock range (rad)
45    pub lock_range: f64,
46    /// Acquisition time (ms)
47    pub acquisition_time_ms: f64,
48}
49
50/// Photodiode configuration
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct PhotodiodeConfig {
53    /// Responsivity (A/W)
54    pub responsivity: f64,
55    /// Dark current (nA)
56    pub dark_current_na: f64,
57    /// NEP (noise equivalent power) (W/√Hz)
58    pub nep: f64,
59    /// Active area (mm²)
60    pub active_area_mm2: f64,
61}
62
63/// IQ demodulator configuration
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct IQDemodulatorConfig {
66    /// IQ imbalance (amplitude)
67    pub amplitude_imbalance: f64,
68    /// IQ imbalance (phase, rad)
69    pub phase_imbalance: f64,
70    /// DC offset I channel (V)
71    pub dc_offset_i: f64,
72    /// DC offset Q channel (V)
73    pub dc_offset_q: f64,
74    /// Low-pass filter cutoff (Hz)
75    pub lpf_cutoff_hz: f64,
76}
77
78impl Default for HeterodyneDetectorConfig {
79    fn default() -> Self {
80        Self {
81            lo_power_mw: 10.0,
82            efficiency: 0.95,
83            electronic_noise: 1e-12, // V²/Hz
84            bandwidth_hz: 10e6,
85            intermediate_frequency_hz: 100e6, // 100 MHz IF
86            pll_x_config: PLLConfig::default(),
87            pll_p_config: PLLConfig::default(),
88            photodiode_config: PhotodiodeConfig::default(),
89            iq_demod_config: IQDemodulatorConfig::default(),
90        }
91    }
92}
93
94impl Default for PLLConfig {
95    fn default() -> Self {
96        Self {
97            loop_bandwidth_hz: 1000.0,
98            phase_noise_density: 1e-8, // rad²/Hz at 1 kHz
99            lock_range: PI,
100            acquisition_time_ms: 10.0,
101        }
102    }
103}
104
105impl Default for PhotodiodeConfig {
106    fn default() -> Self {
107        Self {
108            responsivity: 0.8, // A/W at 1550 nm
109            dark_current_na: 10.0,
110            nep: 1e-14, // W/√Hz
111            active_area_mm2: 1.0,
112        }
113    }
114}
115
116impl Default for IQDemodulatorConfig {
117    fn default() -> Self {
118        Self {
119            amplitude_imbalance: 0.01, // 1%
120            phase_imbalance: 0.02,     // ~1.1 degrees
121            dc_offset_i: 0.001,        // 1 mV
122            dc_offset_q: 0.001,        // 1 mV
123            lpf_cutoff_hz: 1e6,        // 1 MHz
124        }
125    }
126}
127
128/// Heterodyne detection result with detailed statistics
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct HeterodyneResult {
131    /// Measured complex amplitude
132    pub complex_amplitude: Complex,
133    /// I and Q quadrature values
134    pub i_quadrature: f64,
135    pub q_quadrature: f64,
136    /// Shot noise level (each quadrature)
137    pub shot_noise_level: f64,
138    /// Electronic noise contribution
139    pub electronic_noise_level: f64,
140    /// Signal-to-noise ratio (dB)
141    pub snr_db: f64,
142    /// Phase measurement uncertainty (rad)
143    pub phase_uncertainty: f64,
144    /// Amplitude measurement uncertainty
145    pub amplitude_uncertainty: f64,
146    /// IQ imbalance correction applied
147    pub iq_correction: IQCorrection,
148    /// Measurement fidelity
149    pub fidelity: f64,
150    /// Raw detector data
151    pub detector_data: HeterodyneDetectorData,
152}
153
154/// IQ imbalance correction data
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct IQCorrection {
157    /// Amplitude correction factor
158    pub amplitude_correction: f64,
159    /// Phase correction (rad)
160    pub phase_correction: f64,
161    /// DC offset correction I
162    pub dc_offset_i: f64,
163    /// DC offset correction Q
164    pub dc_offset_q: f64,
165}
166
167/// Raw detector data
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct HeterodyneDetectorData {
170    /// IF signal amplitude (V)
171    pub if_amplitude: f64,
172    /// IF signal phase (rad)
173    pub if_phase: f64,
174    /// Raw I signal (V)
175    pub raw_i_signal: f64,
176    /// Raw Q signal (V)
177    pub raw_q_signal: f64,
178    /// Local oscillator powers (mW)
179    pub lo_powers: (f64, f64),
180}
181
182/// Heterodyne detector system
183pub struct HeterodyneDetector {
184    /// Detector configuration
185    config: HeterodyneDetectorConfig,
186    /// Local oscillator phases (x and p LOs)
187    lo_phases: (f64, f64),
188    /// Phase lock status for both PLLs
189    phase_lock_status: (bool, bool),
190    /// Calibration data
191    calibration: HeterodyneCalibration,
192    /// Measurement history
193    measurement_history: Vec<HeterodyneResult>,
194}
195
196/// Calibration data for heterodyne detector
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct HeterodyneCalibration {
199    /// IQ calibration matrix [[a, b], [c, d]]
200    pub iq_matrix: [[f64; 2]; 2],
201    /// DC offset corrections
202    pub dc_offsets: (f64, f64),
203    /// Phase calibration between LOs
204    pub relative_phase_offset: f64,
205    /// Amplitude calibration factors
206    pub amplitude_factors: (f64, f64),
207    /// Common mode rejection ratio (dB)
208    pub cmrr_db: f64,
209}
210
211impl Default for HeterodyneCalibration {
212    fn default() -> Self {
213        Self {
214            iq_matrix: [[1.0, 0.0], [0.0, 1.0]], // Identity matrix
215            dc_offsets: (0.0, 0.0),
216            relative_phase_offset: 0.0,
217            amplitude_factors: (1.0, 1.0),
218            cmrr_db: 60.0,
219        }
220    }
221}
222
223impl HeterodyneDetector {
224    /// Create a new heterodyne detector
225    pub fn new(config: HeterodyneDetectorConfig) -> Self {
226        Self {
227            config,
228            lo_phases: (0.0, PI / 2.0), // X and P quadratures
229            phase_lock_status: (false, false),
230            calibration: HeterodyneCalibration::default(),
231            measurement_history: Vec::new(),
232        }
233    }
234
235    /// Initialize and calibrate the detector
236    pub async fn initialize(&mut self) -> DeviceResult<()> {
237        println!("Initializing heterodyne detector...");
238
239        // Simulate initialization process
240        tokio::time::sleep(std::time::Duration::from_millis(150)).await;
241
242        // Calibrate the system
243        self.calibrate().await?;
244
245        // Lock both local oscillators
246        self.acquire_phase_locks().await?;
247
248        println!("Heterodyne detector initialized successfully");
249        Ok(())
250    }
251
252    /// Calibrate the heterodyne detector
253    async fn calibrate(&mut self) -> DeviceResult<()> {
254        println!("Calibrating heterodyne detector...");
255
256        // Simulate calibration measurements
257        tokio::time::sleep(std::time::Duration::from_millis(300)).await;
258
259        // Measure IQ imbalance with known coherent states
260        let amplitude_imbalance = self.config.iq_demod_config.amplitude_imbalance;
261        let phase_imbalance = self.config.iq_demod_config.phase_imbalance;
262
263        // Build correction matrix
264        let cos_phi = phase_imbalance.cos();
265        let sin_phi = phase_imbalance.sin();
266        let alpha = 1.0 + amplitude_imbalance;
267
268        // Inverse of imbalance matrix
269        let det = alpha * cos_phi;
270        self.calibration.iq_matrix = [[cos_phi / det, -sin_phi / det], [0.0, 1.0 / det]];
271
272        // Measure DC offsets
273        self.calibration.dc_offsets = (
274            0.0005f64.mul_add(
275                thread_rng().gen::<f64>() - 0.5,
276                self.config.iq_demod_config.dc_offset_i,
277            ),
278            0.0005f64.mul_add(
279                thread_rng().gen::<f64>() - 0.5,
280                self.config.iq_demod_config.dc_offset_q,
281            ),
282        );
283
284        // Measure relative phase between LOs
285        self.calibration.relative_phase_offset = 0.02 * (thread_rng().gen::<f64>() - 0.5);
286
287        // Measure amplitude factors
288        self.calibration.amplitude_factors = (
289            0.01f64.mul_add(thread_rng().gen::<f64>() - 0.5, 1.0),
290            0.01f64.mul_add(thread_rng().gen::<f64>() - 0.5, 1.0),
291        );
292
293        println!("Calibration complete");
294        Ok(())
295    }
296
297    /// Acquire phase lock for both local oscillators
298    async fn acquire_phase_locks(&mut self) -> DeviceResult<()> {
299        println!("Acquiring phase locks for both LOs...");
300
301        // Lock X quadrature LO
302        let x_acquisition_time =
303            std::time::Duration::from_millis(self.config.pll_x_config.acquisition_time_ms as u64);
304        tokio::time::sleep(x_acquisition_time).await;
305        self.phase_lock_status.0 = true;
306        println!("X quadrature LO locked");
307
308        // Lock P quadrature LO (in quadrature)
309        let p_acquisition_time =
310            std::time::Duration::from_millis(self.config.pll_p_config.acquisition_time_ms as u64);
311        tokio::time::sleep(p_acquisition_time).await;
312        self.phase_lock_status.1 = true;
313        println!("P quadrature LO locked");
314
315        Ok(())
316    }
317
318    /// Check if both phase locks are acquired
319    pub const fn is_phase_locked(&self) -> bool {
320        self.phase_lock_status.0 && self.phase_lock_status.1
321    }
322
323    /// Perform heterodyne measurement
324    pub async fn measure(
325        &mut self,
326        state: &mut GaussianState,
327        mode: usize,
328    ) -> DeviceResult<HeterodyneResult> {
329        if !self.is_phase_locked() {
330            return Err(DeviceError::DeviceNotInitialized(
331                "Phase locks not acquired".to_string(),
332            ));
333        }
334
335        if mode >= state.num_modes {
336            return Err(DeviceError::InvalidInput(format!(
337                "Mode {mode} exceeds available modes"
338            )));
339        }
340
341        // Get state parameters with phase corrections
342        let mean_x = state.mean_vector[2 * mode];
343        let mean_p = state.mean_vector[2 * mode + 1];
344        let var_x = state.covariancematrix[2 * mode][2 * mode];
345        let var_p = state.covariancematrix[2 * mode + 1][2 * mode + 1];
346        let cov_xp = state.covariancematrix[2 * mode][2 * mode + 1];
347
348        // Calculate noise contributions
349        let shot_noise = self.calculate_shot_noise_level();
350        let electronic_noise = self.calculate_electronic_noise_level();
351        let phase_noise_x =
352            self.calculate_phase_noise_contribution(mean_x, &self.config.pll_x_config);
353        let phase_noise_p =
354            self.calculate_phase_noise_contribution(mean_p, &self.config.pll_p_config);
355
356        // Total noise variances for each quadrature
357        let noise_var_x =
358            var_x / self.config.efficiency + shot_noise + electronic_noise + phase_noise_x;
359        let noise_var_p =
360            var_p / self.config.efficiency + shot_noise + electronic_noise + phase_noise_p;
361
362        // Sample both quadratures
363        let dist_x = Normal::new(mean_x, noise_var_x.sqrt())
364            .map_err(|e| DeviceError::InvalidInput(format!("Distribution error: {e}")))?;
365        let dist_p = Normal::new(mean_p, noise_var_p.sqrt())
366            .map_err(|e| DeviceError::InvalidInput(format!("Distribution error: {e}")))?;
367
368        let mut rng = StdRng::seed_from_u64(thread_rng().gen::<u64>());
369        let raw_i = dist_x.sample(&mut rng);
370        let raw_q = dist_p.sample(&mut rng);
371
372        // Apply IQ corrections
373        let iq_correction = self.apply_iq_correction(raw_i, raw_q);
374        let corrected_i = iq_correction.corrected_i;
375        let corrected_q = iq_correction.corrected_q;
376
377        // Convert to complex amplitude
378        let complex_amplitude = Complex::new(
379            corrected_q.mul_add(Complex::i().real, corrected_i) / (2.0_f64).sqrt(),
380            corrected_i.mul_add(-Complex::i().imag, corrected_q) / (2.0_f64).sqrt(),
381        );
382
383        // Calculate uncertainties
384        let amplitude_uncertainty = (noise_var_x + noise_var_p).sqrt() / 2.0;
385        let phase_uncertainty = if complex_amplitude.magnitude() > 0.0 {
386            amplitude_uncertainty / complex_amplitude.magnitude()
387        } else {
388            PI // Maximum uncertainty for zero amplitude
389        };
390
391        // Calculate SNR
392        let signal_power = complex_amplitude.magnitude().powi(2);
393        let noise_power = f64::midpoint(noise_var_x, noise_var_p);
394        let snr_db = 10.0 * (signal_power / noise_power).log10();
395
396        // Generate detector data
397        let detector_data = self.generate_detector_data(raw_i, raw_q, complex_amplitude);
398
399        // Calculate measurement fidelity
400        let fidelity =
401            self.calculate_measurement_fidelity(signal_power, noise_power, phase_uncertainty);
402
403        let result = HeterodyneResult {
404            complex_amplitude,
405            i_quadrature: corrected_i,
406            q_quadrature: corrected_q,
407            shot_noise_level: shot_noise,
408            electronic_noise_level: electronic_noise,
409            snr_db,
410            phase_uncertainty,
411            amplitude_uncertainty,
412            iq_correction: iq_correction.correction_data,
413            fidelity,
414            detector_data,
415        };
416
417        // Update state (heterodyne destroys the mode)
418        state.condition_on_heterodyne_measurement(mode, complex_amplitude)?;
419
420        self.measurement_history.push(result.clone());
421        Ok(result)
422    }
423
424    /// Apply IQ correction
425    fn apply_iq_correction(&self, raw_i: f64, raw_q: f64) -> IQCorrectionResult {
426        // Remove DC offsets
427        let dc_corrected_i = raw_i - self.calibration.dc_offsets.0;
428        let dc_corrected_q = raw_q - self.calibration.dc_offsets.1;
429
430        // Apply calibration matrix
431        let matrix = &self.calibration.iq_matrix;
432        let corrected_i = matrix[0][0].mul_add(dc_corrected_i, matrix[0][1] * dc_corrected_q);
433        let corrected_q = matrix[1][0].mul_add(dc_corrected_i, matrix[1][1] * dc_corrected_q);
434
435        // Apply amplitude corrections
436        let final_i = corrected_i * self.calibration.amplitude_factors.0;
437        let final_q = corrected_q * self.calibration.amplitude_factors.1;
438
439        IQCorrectionResult {
440            corrected_i: final_i,
441            corrected_q: final_q,
442            correction_data: IQCorrection {
443                amplitude_correction: f64::midpoint(
444                    self.calibration.amplitude_factors.0,
445                    self.calibration.amplitude_factors.1,
446                ),
447                phase_correction: self.calibration.relative_phase_offset,
448                dc_offset_i: self.calibration.dc_offsets.0,
449                dc_offset_q: self.calibration.dc_offsets.1,
450            },
451        }
452    }
453
454    /// Calculate shot noise level (heterodyne has 3dB penalty)
455    fn calculate_shot_noise_level(&self) -> f64 {
456        // Heterodyne has 3dB penalty compared to homodyne: 2ℏω per quadrature
457        2.0 / self.config.efficiency
458    }
459
460    /// Calculate electronic noise level
461    fn calculate_electronic_noise_level(&self) -> f64 {
462        // Similar to homodyne but accounting for IF processing
463        let current_noise = (2.0
464            * 1.602e-19
465            * self.config.photodiode_config.dark_current_na
466            * 1e-9
467            * self.config.bandwidth_hz)
468            .sqrt();
469
470        let thermal_noise = (4.0 * 1.381e-23 * 300.0 * self.config.bandwidth_hz / 50.0).sqrt();
471
472        // Additional noise from IQ demodulation
473        let iq_noise = self.config.electronic_noise.sqrt();
474
475        let total_electronic_noise = iq_noise
476            .mul_add(
477                iq_noise,
478                thermal_noise.powi(2).mul_add(1.0, current_noise.powi(2)),
479            )
480            .sqrt();
481
482        // Convert to quadrature variance units
483        total_electronic_noise
484            / (self.config.photodiode_config.responsivity * (self.config.lo_power_mw * 1e-3).sqrt())
485    }
486
487    /// Calculate phase noise contribution
488    fn calculate_phase_noise_contribution(
489        &self,
490        signal_amplitude: f64,
491        pll_config: &PLLConfig,
492    ) -> f64 {
493        let phase_noise_variance = pll_config.phase_noise_density * self.config.bandwidth_hz;
494        signal_amplitude.powi(2) * phase_noise_variance
495    }
496
497    /// Generate detector data
498    fn generate_detector_data(
499        &self,
500        raw_i: f64,
501        raw_q: f64,
502        complex_amplitude: Complex,
503    ) -> HeterodyneDetectorData {
504        let if_amplitude = raw_i.hypot(raw_q);
505        let if_phase = raw_q.atan2(raw_i);
506
507        HeterodyneDetectorData {
508            if_amplitude,
509            if_phase,
510            raw_i_signal: raw_i,
511            raw_q_signal: raw_q,
512            lo_powers: (self.config.lo_power_mw, self.config.lo_power_mw),
513        }
514    }
515
516    /// Calculate measurement fidelity
517    fn calculate_measurement_fidelity(
518        &self,
519        signal_power: f64,
520        noise_power: f64,
521        phase_uncertainty: f64,
522    ) -> f64 {
523        let snr = signal_power / noise_power;
524        let phase_penalty = 1.0 / phase_uncertainty.mul_add(phase_uncertainty, 1.0);
525        let efficiency_penalty = self.config.efficiency;
526
527        // Additional penalty for heterodyne 3dB penalty
528        let heterodyne_penalty = 0.5;
529
530        let fidelity =
531            (snr / (1.0 + snr)) * phase_penalty * efficiency_penalty * heterodyne_penalty;
532        fidelity.clamp(0.0, 1.0)
533    }
534
535    /// Get measurement statistics
536    pub fn get_measurement_statistics(&self) -> HeterodyneStatistics {
537        if self.measurement_history.is_empty() {
538            return HeterodyneStatistics::default();
539        }
540
541        let total_measurements = self.measurement_history.len();
542
543        let avg_snr = self
544            .measurement_history
545            .iter()
546            .map(|r| r.snr_db)
547            .sum::<f64>()
548            / total_measurements as f64;
549
550        let avg_amplitude_uncertainty = self
551            .measurement_history
552            .iter()
553            .map(|r| r.amplitude_uncertainty)
554            .sum::<f64>()
555            / total_measurements as f64;
556
557        let avg_phase_uncertainty = self
558            .measurement_history
559            .iter()
560            .map(|r| r.phase_uncertainty)
561            .sum::<f64>()
562            / total_measurements as f64;
563
564        let avg_fidelity = self
565            .measurement_history
566            .iter()
567            .map(|r| r.fidelity)
568            .sum::<f64>()
569            / total_measurements as f64;
570
571        HeterodyneStatistics {
572            total_measurements,
573            average_snr_db: avg_snr,
574            average_amplitude_uncertainty: avg_amplitude_uncertainty,
575            average_phase_uncertainty: avg_phase_uncertainty,
576            average_fidelity: avg_fidelity,
577            phase_lock_status: self.phase_lock_status,
578            detector_efficiency: self.config.efficiency,
579        }
580    }
581
582    /// Clear measurement history
583    pub fn clear_history(&mut self) {
584        self.measurement_history.clear();
585    }
586
587    /// Get calibration data
588    pub const fn get_calibration(&self) -> &HeterodyneCalibration {
589        &self.calibration
590    }
591}
592
593/// Result of IQ correction
594struct IQCorrectionResult {
595    corrected_i: f64,
596    corrected_q: f64,
597    correction_data: IQCorrection,
598}
599
600/// Statistics for heterodyne detector performance
601#[derive(Debug, Clone, Serialize, Deserialize)]
602pub struct HeterodyneStatistics {
603    pub total_measurements: usize,
604    pub average_snr_db: f64,
605    pub average_amplitude_uncertainty: f64,
606    pub average_phase_uncertainty: f64,
607    pub average_fidelity: f64,
608    pub phase_lock_status: (bool, bool),
609    pub detector_efficiency: f64,
610}
611
612impl Default for HeterodyneStatistics {
613    fn default() -> Self {
614        Self {
615            total_measurements: 0,
616            average_snr_db: 0.0,
617            average_amplitude_uncertainty: 0.0,
618            average_phase_uncertainty: 0.0,
619            average_fidelity: 0.0,
620            phase_lock_status: (false, false),
621            detector_efficiency: 0.0,
622        }
623    }
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629
630    #[tokio::test]
631    async fn test_heterodyne_detector_creation() {
632        let config = HeterodyneDetectorConfig::default();
633        let detector = HeterodyneDetector::new(config);
634        assert!(!detector.is_phase_locked());
635        assert_eq!(detector.measurement_history.len(), 0);
636    }
637
638    #[tokio::test]
639    async fn test_detector_initialization() {
640        let config = HeterodyneDetectorConfig::default();
641        let mut detector = HeterodyneDetector::new(config);
642
643        detector
644            .initialize()
645            .await
646            .expect("Detector initialization should succeed");
647        assert!(detector.is_phase_locked());
648    }
649
650    #[tokio::test]
651    async fn test_heterodyne_measurement() {
652        let config = HeterodyneDetectorConfig::default();
653        let mut detector = HeterodyneDetector::new(config);
654        detector
655            .initialize()
656            .await
657            .expect("Detector initialization should succeed");
658
659        let mut state = GaussianState::coherent_state(1, vec![Complex::new(2.0, 1.0)])
660            .expect("Coherent state creation should succeed");
661
662        let result = detector
663            .measure(&mut state, 0)
664            .await
665            .expect("Heterodyne measurement should succeed");
666
667        assert!(result.complex_amplitude.magnitude() > 0.0);
668        assert!(result.fidelity > 0.0);
669        assert!(result.snr_db.is_finite());
670        assert_eq!(detector.measurement_history.len(), 1);
671    }
672
673    #[tokio::test]
674    async fn test_vacuum_measurement() {
675        let config = HeterodyneDetectorConfig::default();
676        let mut detector = HeterodyneDetector::new(config);
677        detector
678            .initialize()
679            .await
680            .expect("Detector initialization should succeed");
681
682        let mut state = GaussianState::vacuum_state(1);
683
684        let result = detector
685            .measure(&mut state, 0)
686            .await
687            .expect("Heterodyne measurement should succeed");
688
689        // Vacuum should have small but finite amplitude due to noise
690        assert!(result.complex_amplitude.magnitude() >= 0.0);
691        assert!(result.amplitude_uncertainty > 0.0);
692        assert!(result.phase_uncertainty > 0.0);
693    }
694
695    #[test]
696    fn test_iq_correction() {
697        let config = HeterodyneDetectorConfig::default();
698        let detector = HeterodyneDetector::new(config);
699
700        let correction_result = detector.apply_iq_correction(1.0, 0.5);
701        assert!(correction_result.corrected_i.is_finite());
702        assert!(correction_result.corrected_q.is_finite());
703    }
704
705    #[test]
706    fn test_noise_calculations() {
707        let config = HeterodyneDetectorConfig::default();
708        let efficiency = config.efficiency;
709        let detector = HeterodyneDetector::new(config);
710
711        let shot_noise = detector.calculate_shot_noise_level();
712        assert!(shot_noise > 0.0);
713        // Heterodyne should have higher shot noise than homodyne
714        assert!(shot_noise >= 2.0 / efficiency);
715
716        let electronic_noise = detector.calculate_electronic_noise_level();
717        assert!(electronic_noise > 0.0);
718    }
719
720    #[test]
721    fn test_detector_data_generation() {
722        let config = HeterodyneDetectorConfig::default();
723        let detector = HeterodyneDetector::new(config);
724
725        let complex_amp = Complex::new(1.0, 0.5);
726        let data = detector.generate_detector_data(1.0, 0.5, complex_amp);
727
728        assert!(data.if_amplitude > 0.0);
729        assert!(data.if_phase.is_finite());
730        assert_eq!(data.raw_i_signal, 1.0);
731        assert_eq!(data.raw_q_signal, 0.5);
732    }
733
734    #[test]
735    fn test_statistics() {
736        let config = HeterodyneDetectorConfig::default();
737        let detector = HeterodyneDetector::new(config);
738
739        let stats = detector.get_measurement_statistics();
740        assert_eq!(stats.total_measurements, 0);
741        assert_eq!(stats.phase_lock_status, (false, false));
742    }
743}