Skip to main content

quantrs2_anneal/
coherent_ising_machine.rs

1//! Coherent Ising Machine simulation for photonic quantum annealing
2//!
3//! This module implements coherent Ising machines (CIMs) based on optical parametric
4//! oscillators (OPOs) and photonic networks. CIMs use optical pulses to represent
5//! spins and leverage optical interference and parametric amplification to solve
6//! optimization problems through a completely different physical mechanism than
7//! traditional quantum annealing.
8
9use scirs2_core::random::prelude::*;
10use scirs2_core::random::ChaCha8Rng;
11use scirs2_core::random::{Rng, SeedableRng};
12use scirs2_core::RngExt;
13use std::collections::HashMap;
14use std::time::{Duration, Instant};
15use thiserror::Error;
16
17use crate::ising::{IsingError, IsingModel};
18
19/// Errors that can occur in coherent Ising machine simulation
20#[derive(Error, Debug)]
21pub enum CimError {
22    /// Ising model error
23    #[error("Ising error: {0}")]
24    IsingError(#[from] IsingError),
25
26    /// Invalid optical parameters
27    #[error("Invalid optical parameters: {0}")]
28    InvalidOpticalParameters(String),
29
30    /// Simulation error
31    #[error("Simulation error: {0}")]
32    SimulationError(String),
33
34    /// Network topology error
35    #[error("Network topology error: {0}")]
36    TopologyError(String),
37
38    /// Convergence error
39    #[error("Convergence error: {0}")]
40    ConvergenceError(String),
41}
42
43/// Result type for CIM operations
44pub type CimResult<T> = Result<T, CimError>;
45
46/// Optical parametric oscillator (OPO) representation
47#[derive(Debug, Clone)]
48pub struct OpticalParametricOscillator {
49    /// Oscillator index
50    pub index: usize,
51
52    /// Complex amplitude (amplitude and phase)
53    pub amplitude: Complex,
54
55    /// Parametric gain
56    pub gain: f64,
57
58    /// Linear loss rate
59    pub loss: f64,
60
61    /// Nonlinear saturation parameter
62    pub saturation: f64,
63
64    /// External injection field
65    pub injection: Complex,
66
67    /// Oscillation threshold
68    pub threshold: f64,
69}
70
71/// Complex number representation for optical fields
72#[derive(Debug, Clone, Copy, PartialEq)]
73pub struct Complex {
74    /// Real part
75    pub re: f64,
76    /// Imaginary part
77    pub im: f64,
78}
79
80impl Complex {
81    /// Create a new complex number
82    #[must_use]
83    pub const fn new(re: f64, im: f64) -> Self {
84        Self { re, im }
85    }
86
87    /// Create from polar coordinates
88    #[must_use]
89    pub fn from_polar(magnitude: f64, phase: f64) -> Self {
90        Self {
91            re: magnitude * phase.cos(),
92            im: magnitude * phase.sin(),
93        }
94    }
95
96    /// Get magnitude squared
97    #[must_use]
98    pub fn magnitude_squared(&self) -> f64 {
99        self.re.mul_add(self.re, self.im * self.im)
100    }
101
102    /// Get magnitude
103    #[must_use]
104    pub fn magnitude(&self) -> f64 {
105        self.magnitude_squared().sqrt()
106    }
107
108    /// Get phase angle
109    #[must_use]
110    pub fn phase(&self) -> f64 {
111        self.im.atan2(self.re)
112    }
113
114    /// Complex conjugate
115    #[must_use]
116    pub fn conjugate(&self) -> Self {
117        Self {
118            re: self.re,
119            im: -self.im,
120        }
121    }
122}
123
124impl std::ops::Add for Complex {
125    type Output = Self;
126
127    fn add(self, other: Self) -> Self {
128        Self {
129            re: self.re + other.re,
130            im: self.im + other.im,
131        }
132    }
133}
134
135impl std::ops::Sub for Complex {
136    type Output = Self;
137
138    fn sub(self, other: Self) -> Self {
139        Self {
140            re: self.re - other.re,
141            im: self.im - other.im,
142        }
143    }
144}
145
146impl std::ops::Mul for Complex {
147    type Output = Self;
148
149    fn mul(self, other: Self) -> Self {
150        Self {
151            re: self.re.mul_add(other.re, -(self.im * other.im)),
152            im: self.re.mul_add(other.im, self.im * other.re),
153        }
154    }
155}
156
157impl std::ops::Mul<f64> for Complex {
158    type Output = Self;
159
160    fn mul(self, scalar: f64) -> Self {
161        Self {
162            re: self.re * scalar,
163            im: self.im * scalar,
164        }
165    }
166}
167
168/// Optical coupling between oscillators
169#[derive(Debug, Clone)]
170pub struct OpticalCoupling {
171    /// Source oscillator index
172    pub from: usize,
173
174    /// Target oscillator index
175    pub to: usize,
176
177    /// Coupling strength (can be complex for phase shifts)
178    pub strength: Complex,
179
180    /// Time delay (for large networks)
181    pub delay: f64,
182}
183
184/// Coherent Ising machine configuration
185#[derive(Debug, Clone)]
186pub struct CimConfig {
187    /// Number of optical oscillators
188    pub num_oscillators: usize,
189
190    /// Simulation time step
191    pub dt: f64,
192
193    /// Total simulation time
194    pub total_time: f64,
195
196    /// Pump power ramping schedule
197    pub pump_schedule: PumpSchedule,
198
199    /// Optical network topology
200    pub topology: NetworkTopology,
201
202    /// Noise parameters
203    pub noise_config: NoiseConfig,
204
205    /// Measurement and detection parameters
206    pub measurement_config: MeasurementConfig,
207
208    /// Convergence criteria
209    pub convergence_config: ConvergenceConfig,
210
211    /// Random seed
212    pub seed: Option<u64>,
213
214    /// Enable detailed logging
215    pub detailed_logging: bool,
216
217    /// Output file for optical state evolution
218    pub output_file: Option<String>,
219}
220
221impl Default for CimConfig {
222    fn default() -> Self {
223        Self {
224            num_oscillators: 4,
225            dt: 0.001,
226            total_time: 10.0,
227            pump_schedule: PumpSchedule::Linear {
228                initial_power: 0.5,
229                final_power: 2.0,
230            },
231            topology: NetworkTopology::FullyConnected,
232            noise_config: NoiseConfig::default(),
233            measurement_config: MeasurementConfig::default(),
234            convergence_config: ConvergenceConfig::default(),
235            seed: None,
236            detailed_logging: false,
237            output_file: None,
238        }
239    }
240}
241
242/// Pump power ramping schedules
243pub enum PumpSchedule {
244    /// Linear increase in pump power
245    Linear {
246        initial_power: f64,
247        final_power: f64,
248    },
249
250    /// Exponential approach to final power
251    Exponential {
252        initial_power: f64,
253        final_power: f64,
254        time_constant: f64,
255    },
256
257    /// S-curve (sigmoid) ramping
258    Sigmoid {
259        initial_power: f64,
260        final_power: f64,
261        steepness: f64,
262        midpoint: f64,
263    },
264
265    /// Custom time-dependent function
266    Custom {
267        power_function: Box<dyn Fn(f64) -> f64 + Send + Sync>,
268    },
269}
270
271impl std::fmt::Debug for PumpSchedule {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        match self {
274            Self::Linear {
275                initial_power,
276                final_power,
277            } => f
278                .debug_struct("Linear")
279                .field("initial_power", initial_power)
280                .field("final_power", final_power)
281                .finish(),
282            Self::Exponential {
283                initial_power,
284                final_power,
285                time_constant,
286            } => f
287                .debug_struct("Exponential")
288                .field("initial_power", initial_power)
289                .field("final_power", final_power)
290                .field("time_constant", time_constant)
291                .finish(),
292            Self::Sigmoid {
293                initial_power,
294                final_power,
295                steepness,
296                midpoint,
297            } => f
298                .debug_struct("Sigmoid")
299                .field("initial_power", initial_power)
300                .field("final_power", final_power)
301                .field("steepness", steepness)
302                .field("midpoint", midpoint)
303                .finish(),
304            Self::Custom { .. } => f
305                .debug_struct("Custom")
306                .field("power_function", &"<function>")
307                .finish(),
308        }
309    }
310}
311
312impl Clone for PumpSchedule {
313    fn clone(&self) -> Self {
314        match self {
315            Self::Linear {
316                initial_power,
317                final_power,
318            } => Self::Linear {
319                initial_power: *initial_power,
320                final_power: *final_power,
321            },
322            Self::Exponential {
323                initial_power,
324                final_power,
325                time_constant,
326            } => Self::Exponential {
327                initial_power: *initial_power,
328                final_power: *final_power,
329                time_constant: *time_constant,
330            },
331            Self::Sigmoid {
332                initial_power,
333                final_power,
334                steepness,
335                midpoint,
336            } => Self::Sigmoid {
337                initial_power: *initial_power,
338                final_power: *final_power,
339                steepness: *steepness,
340                midpoint: *midpoint,
341            },
342            Self::Custom { .. } => {
343                // Cannot clone function pointers, use default linear schedule
344                Self::Linear {
345                    initial_power: 0.0,
346                    final_power: 1.0,
347                }
348            }
349        }
350    }
351}
352
353/// Network topology configurations
354#[derive(Debug, Clone)]
355pub enum NetworkTopology {
356    /// Fully connected network (all-to-all coupling)
357    FullyConnected,
358
359    /// Ring topology
360    Ring,
361
362    /// Two-dimensional lattice
363    Lattice2D { width: usize, height: usize },
364
365    /// Random network with specified connectivity
366    Random { connectivity: f64 },
367
368    /// Small-world network
369    SmallWorld {
370        ring_connectivity: usize,
371        rewiring_probability: f64,
372    },
373
374    /// Custom topology from coupling matrix
375    Custom { couplings: Vec<OpticalCoupling> },
376}
377
378/// Noise configuration for realistic simulation
379#[derive(Debug, Clone)]
380pub struct NoiseConfig {
381    /// Quantum noise strength
382    pub quantum_noise: f64,
383
384    /// Classical phase noise
385    pub phase_noise: f64,
386
387    /// Amplitude noise
388    pub amplitude_noise: f64,
389
390    /// Thermal noise temperature
391    pub temperature: f64,
392
393    /// Environmental decoherence rate
394    pub decoherence_rate: f64,
395}
396
397impl Default for NoiseConfig {
398    fn default() -> Self {
399        Self {
400            quantum_noise: 0.01,
401            phase_noise: 0.001,
402            amplitude_noise: 0.001,
403            temperature: 0.01,
404            decoherence_rate: 0.001,
405        }
406    }
407}
408
409/// Measurement and detection configuration
410#[derive(Debug, Clone)]
411pub struct MeasurementConfig {
412    /// Homodyne detection efficiency
413    pub detection_efficiency: f64,
414
415    /// Measurement time window
416    pub measurement_window: f64,
417
418    /// Phase reference for homodyne detection
419    pub reference_phase: f64,
420
421    /// Number of measurement repetitions
422    pub measurement_repetitions: usize,
423
424    /// Threshold for binary decision
425    pub decision_threshold: f64,
426}
427
428impl Default for MeasurementConfig {
429    fn default() -> Self {
430        Self {
431            detection_efficiency: 0.95,
432            measurement_window: 1.0,
433            reference_phase: 0.0,
434            measurement_repetitions: 100,
435            decision_threshold: 0.0,
436        }
437    }
438}
439
440/// Convergence criteria configuration
441#[derive(Debug, Clone)]
442pub struct ConvergenceConfig {
443    /// Energy tolerance for convergence
444    pub energy_tolerance: f64,
445
446    /// Maximum time without improvement
447    pub stagnation_time: f64,
448
449    /// Minimum oscillation threshold
450    pub oscillation_threshold: f64,
451
452    /// Phase stability requirement
453    pub phase_stability: f64,
454}
455
456impl Default for ConvergenceConfig {
457    fn default() -> Self {
458        Self {
459            energy_tolerance: 1e-6,
460            stagnation_time: 1.0,
461            oscillation_threshold: 0.1,
462            phase_stability: 0.01,
463        }
464    }
465}
466
467/// Results from coherent Ising machine simulation
468#[derive(Debug, Clone)]
469pub struct CimResults {
470    /// Best solution found
471    pub best_solution: Vec<i8>,
472
473    /// Best energy achieved
474    pub best_energy: f64,
475
476    /// Final optical state
477    pub final_optical_state: Vec<Complex>,
478
479    /// Energy evolution over time
480    pub energy_history: Vec<f64>,
481
482    /// Optical state evolution (sampled)
483    pub optical_evolution: Vec<Vec<Complex>>,
484
485    /// Time points for evolution data
486    pub time_points: Vec<f64>,
487
488    /// Convergence achieved
489    pub converged: bool,
490
491    /// Convergence time
492    pub convergence_time: f64,
493
494    /// Total simulation time
495    pub total_simulation_time: Duration,
496
497    /// Optical statistics
498    pub optical_stats: OpticalStatistics,
499
500    /// Performance metrics
501    pub performance_metrics: CimPerformanceMetrics,
502}
503
504/// Optical system statistics
505#[derive(Debug, Clone)]
506pub struct OpticalStatistics {
507    /// Average optical power over time
508    pub average_power: f64,
509
510    /// Power variance
511    pub power_variance: f64,
512
513    /// Phase coherence measures
514    pub phase_coherence: Vec<f64>,
515
516    /// Cross-correlations between oscillators
517    pub cross_correlations: Vec<Vec<f64>>,
518
519    /// Oscillation frequencies
520    pub oscillation_frequencies: Vec<f64>,
521
522    /// Pump efficiency
523    pub pump_efficiency: f64,
524}
525
526/// Performance metrics for CIM
527#[derive(Debug, Clone)]
528pub struct CimPerformanceMetrics {
529    /// Solution quality (energy gap from best known)
530    pub solution_quality: f64,
531
532    /// Time to convergence
533    pub time_to_convergence: f64,
534
535    /// Number of phase transitions
536    pub phase_transitions: usize,
537
538    /// Average optical power efficiency
539    pub power_efficiency: f64,
540
541    /// Noise resilience score
542    pub noise_resilience: f64,
543}
544
545/// Coherent Ising machine simulator
546pub struct CoherentIsingMachine {
547    /// Configuration
548    config: CimConfig,
549
550    /// Optical oscillators
551    oscillators: Vec<OpticalParametricOscillator>,
552
553    /// Optical couplings
554    couplings: Vec<OpticalCoupling>,
555
556    /// Random number generator
557    rng: ChaCha8Rng,
558
559    /// Current simulation time
560    current_time: f64,
561
562    /// Evolution history (for analysis)
563    evolution_history: Vec<(f64, Vec<Complex>)>,
564
565    /// Energy history
566    energy_history: Vec<f64>,
567}
568
569impl CoherentIsingMachine {
570    /// Create a new coherent Ising machine
571    pub fn new(config: CimConfig) -> CimResult<Self> {
572        let rng = match config.seed {
573            Some(seed) => ChaCha8Rng::seed_from_u64(seed),
574            None => {
575                // Generate a random seed without using rand::thread_rng.
576                // We derive entropy from the current system time (nanoseconds).
577                let seed = std::time::SystemTime::now()
578                    .duration_since(std::time::UNIX_EPOCH)
579                    .map(|d| {
580                        d.subsec_nanos() as u64 ^ (d.as_secs().wrapping_mul(0x9e37_79b9_7f4a_7c15))
581                    })
582                    .unwrap_or(0x1234_5678_9abc_def0);
583                ChaCha8Rng::seed_from_u64(seed)
584            }
585        };
586
587        let mut cim = Self {
588            oscillators: Vec::new(),
589            couplings: Vec::new(),
590            rng,
591            current_time: 0.0,
592            evolution_history: Vec::new(),
593            energy_history: Vec::new(),
594            config,
595        };
596
597        cim.initialize_system()?;
598        Ok(cim)
599    }
600
601    /// Initialize the optical system
602    fn initialize_system(&mut self) -> CimResult<()> {
603        // Initialize oscillators
604        for i in 0..self.config.num_oscillators {
605            let osc = OpticalParametricOscillator {
606                index: i,
607                amplitude: Complex::new(
608                    self.rng.random_range(-0.1..0.1),
609                    self.rng.random_range(-0.1..0.1),
610                ),
611                gain: 1.0,
612                loss: 0.1,
613                saturation: 1.0,
614                injection: Complex::new(0.0, 0.0),
615                threshold: 1.0,
616            };
617            self.oscillators.push(osc);
618        }
619
620        // Initialize couplings based on topology
621        self.initialize_topology()?;
622
623        Ok(())
624    }
625
626    /// Initialize network topology
627    fn initialize_topology(&mut self) -> CimResult<()> {
628        self.couplings.clear();
629
630        let topology = self.config.topology.clone();
631        match topology {
632            NetworkTopology::FullyConnected => {
633                for i in 0..self.config.num_oscillators {
634                    for j in (i + 1)..self.config.num_oscillators {
635                        self.couplings.push(OpticalCoupling {
636                            from: i,
637                            to: j,
638                            strength: Complex::new(0.1, 0.0),
639                            delay: 0.0,
640                        });
641                        self.couplings.push(OpticalCoupling {
642                            from: j,
643                            to: i,
644                            strength: Complex::new(0.1, 0.0),
645                            delay: 0.0,
646                        });
647                    }
648                }
649            }
650
651            NetworkTopology::Ring => {
652                for i in 0..self.config.num_oscillators {
653                    let j = (i + 1) % self.config.num_oscillators;
654                    self.couplings.push(OpticalCoupling {
655                        from: i,
656                        to: j,
657                        strength: Complex::new(0.2, 0.0),
658                        delay: 0.0,
659                    });
660                    self.couplings.push(OpticalCoupling {
661                        from: j,
662                        to: i,
663                        strength: Complex::new(0.2, 0.0),
664                        delay: 0.0,
665                    });
666                }
667            }
668
669            NetworkTopology::Lattice2D { width, height } => {
670                if width * height != self.config.num_oscillators {
671                    return Err(CimError::TopologyError(
672                        "Lattice dimensions don't match number of oscillators".to_string(),
673                    ));
674                }
675
676                // Nearest neighbor couplings
677                for i in 0..width {
678                    for j in 0..height {
679                        let idx = i * height + j;
680
681                        // Right neighbor
682                        if i + 1 < width {
683                            let neighbor = (i + 1) * height + j;
684                            self.add_bidirectional_coupling(idx, neighbor, 0.15);
685                        }
686
687                        // Bottom neighbor
688                        if j + 1 < height {
689                            let neighbor = i * height + (j + 1);
690                            self.add_bidirectional_coupling(idx, neighbor, 0.15);
691                        }
692                    }
693                }
694            }
695
696            NetworkTopology::Random { connectivity } => {
697                let num_possible_edges =
698                    self.config.num_oscillators * (self.config.num_oscillators - 1) / 2;
699                let num_edges = (num_possible_edges as f64 * connectivity) as usize;
700
701                let mut added_edges = std::collections::HashSet::new();
702
703                while added_edges.len() < num_edges {
704                    let i = self.rng.random_range(0..self.config.num_oscillators);
705                    let j = self.rng.random_range(0..self.config.num_oscillators);
706
707                    if i != j {
708                        let edge = if i < j { (i, j) } else { (j, i) };
709                        if added_edges.insert(edge) {
710                            let strength = self.rng.random_range(0.05..0.25);
711                            self.add_bidirectional_coupling(edge.0, edge.1, strength);
712                        }
713                    }
714                }
715            }
716
717            NetworkTopology::SmallWorld {
718                ring_connectivity,
719                rewiring_probability,
720            } => {
721                // Start with ring lattice
722                for i in 0..self.config.num_oscillators {
723                    for k in 1..=ring_connectivity {
724                        let j = (i + k) % self.config.num_oscillators;
725
726                        // Rewire with probability
727                        if self.rng.random_bool(rewiring_probability) {
728                            let new_target = self.rng.random_range(0..self.config.num_oscillators);
729                            if new_target != i {
730                                self.add_bidirectional_coupling(i, new_target, 0.1);
731                            }
732                        } else {
733                            self.add_bidirectional_coupling(i, j, 0.1);
734                        }
735                    }
736                }
737            }
738
739            NetworkTopology::Custom { couplings } => {
740                self.couplings = couplings;
741            }
742        }
743
744        Ok(())
745    }
746
747    /// Add bidirectional coupling between two oscillators
748    fn add_bidirectional_coupling(&mut self, i: usize, j: usize, strength: f64) {
749        self.couplings.push(OpticalCoupling {
750            from: i,
751            to: j,
752            strength: Complex::new(strength, 0.0),
753            delay: 0.0,
754        });
755        self.couplings.push(OpticalCoupling {
756            from: j,
757            to: i,
758            strength: Complex::new(strength, 0.0),
759            delay: 0.0,
760        });
761    }
762
763    /// Solve an Ising problem using the coherent Ising machine
764    pub fn solve(&mut self, problem: &IsingModel) -> CimResult<CimResults> {
765        if problem.num_qubits != self.config.num_oscillators {
766            return Err(CimError::SimulationError(format!(
767                "Problem size {} doesn't match CIM size {}",
768                problem.num_qubits, self.config.num_oscillators
769            )));
770        }
771
772        println!("Starting coherent Ising machine simulation...");
773        let start_time = Instant::now();
774
775        // Map Ising problem to optical couplings
776        self.map_ising_to_optical(problem)?;
777
778        // Reset simulation state
779        self.current_time = 0.0;
780        self.evolution_history.clear();
781        self.energy_history.clear();
782
783        // Main simulation loop
784        let num_steps = (self.config.total_time / self.config.dt) as usize;
785        let mut best_energy = f64::INFINITY;
786        let mut best_solution = vec![0; problem.num_qubits];
787        let mut converged = false;
788        let mut convergence_time = self.config.total_time;
789
790        for step in 0..num_steps {
791            self.current_time = step as f64 * self.config.dt;
792
793            // Update pump power according to schedule
794            let pump_power = self.get_pump_power(self.current_time);
795            self.update_pump_power(pump_power);
796
797            // Evolve optical system
798            self.evolve_system()?;
799
800            // Add noise
801            self.add_noise();
802
803            // Record state
804            if step % 100 == 0 || step == num_steps - 1 {
805                self.record_state();
806            }
807
808            // Evaluate current solution
809            let current_solution = self.measure_solution()?;
810            let current_energy = self.evaluate_energy(problem, &current_solution)?;
811            self.energy_history.push(current_energy);
812
813            // Update best solution
814            if current_energy < best_energy {
815                best_energy = current_energy;
816                best_solution = current_solution;
817            }
818
819            // Check convergence
820            if !converged && self.check_convergence()? {
821                converged = true;
822                convergence_time = self.current_time;
823                if self.config.detailed_logging {
824                    println!("Converged at time {convergence_time:.3}");
825                }
826            }
827
828            // Logging
829            if step % 1000 == 0 && self.config.detailed_logging {
830                println!(
831                    "Step {}: Time = {:.3}, Energy = {:.6}, Power = {:.3}",
832                    step, self.current_time, current_energy, pump_power
833                );
834            }
835        }
836
837        let total_simulation_time = start_time.elapsed();
838
839        // Calculate final statistics
840        let optical_stats = self.calculate_optical_statistics();
841        let performance_metrics =
842            self.calculate_performance_metrics(problem, best_energy, convergence_time);
843
844        // Prepare results
845        let results = CimResults {
846            best_solution,
847            best_energy,
848            final_optical_state: self.oscillators.iter().map(|osc| osc.amplitude).collect(),
849            energy_history: self.energy_history.clone(),
850            optical_evolution: self
851                .evolution_history
852                .iter()
853                .map(|(_, state)| state.clone())
854                .collect(),
855            time_points: self.evolution_history.iter().map(|(t, _)| *t).collect(),
856            converged,
857            convergence_time,
858            total_simulation_time,
859            optical_stats,
860            performance_metrics,
861        };
862
863        println!("CIM simulation completed in {total_simulation_time:.2?}");
864        println!("Best energy: {best_energy:.6}, Converged: {converged}");
865
866        Ok(results)
867    }
868
869    /// Map Ising problem to optical couplings
870    fn map_ising_to_optical(&mut self, problem: &IsingModel) -> CimResult<()> {
871        // Set injection fields based on biases
872        for i in 0..problem.num_qubits {
873            let bias = problem.get_bias(i).unwrap_or(0.0);
874            self.oscillators[i].injection = Complex::new(bias * 0.1, 0.0);
875        }
876
877        // Update coupling strengths based on Ising couplings
878        for coupling in &mut self.couplings {
879            if let Ok(ising_coupling) = problem.get_coupling(coupling.from, coupling.to) {
880                if ising_coupling != 0.0 {
881                    // Map Ising coupling to optical coupling
882                    let optical_strength = ising_coupling * 0.1;
883                    coupling.strength = Complex::new(optical_strength, 0.0);
884                }
885            }
886        }
887
888        Ok(())
889    }
890
891    /// Get pump power according to schedule
892    fn get_pump_power(&self, time: f64) -> f64 {
893        let normalized_time = time / self.config.total_time;
894
895        match &self.config.pump_schedule {
896            PumpSchedule::Linear {
897                initial_power,
898                final_power,
899            } => initial_power + (final_power - initial_power) * normalized_time,
900
901            PumpSchedule::Exponential {
902                initial_power,
903                final_power,
904                time_constant,
905            } => {
906                initial_power
907                    + (final_power - initial_power) * (1.0 - (-time / time_constant).exp())
908            }
909
910            PumpSchedule::Sigmoid {
911                initial_power,
912                final_power,
913                steepness,
914                midpoint,
915            } => {
916                let sigmoid = 1.0 / (1.0 + (-(normalized_time - midpoint) * steepness).exp());
917                initial_power + (final_power - initial_power) * sigmoid
918            }
919
920            PumpSchedule::Custom { power_function } => power_function(time),
921        }
922    }
923
924    /// Update pump power for all oscillators
925    fn update_pump_power(&mut self, pump_power: f64) {
926        for osc in &mut self.oscillators {
927            osc.gain = pump_power;
928        }
929    }
930
931    /// Evolve the optical system by one time step
932    fn evolve_system(&mut self) -> CimResult<()> {
933        let dt = self.config.dt;
934        let mut new_amplitudes = Vec::new();
935
936        // Calculate evolution for each oscillator
937        for i in 0..self.oscillators.len() {
938            let osc = &self.oscillators[i];
939            let mut derivative = Complex::new(0.0, 0.0);
940
941            // Parametric gain term (above threshold)
942            let power = osc.amplitude.magnitude_squared();
943            if osc.gain > osc.threshold {
944                let net_gain = osc.gain - osc.threshold - osc.loss;
945                derivative = derivative + osc.amplitude * net_gain * (1.0 - power / osc.saturation);
946            } else {
947                // Below threshold: only loss
948                derivative = derivative + osc.amplitude * (-osc.loss);
949            }
950
951            // Injection field
952            derivative = derivative + osc.injection;
953
954            // Coupling terms
955            for coupling in &self.couplings {
956                if coupling.to == i {
957                    let source_amplitude = self.oscillators[coupling.from].amplitude;
958                    derivative = derivative + source_amplitude * coupling.strength;
959                }
960            }
961
962            // Integrate using Euler method
963            let new_amplitude = osc.amplitude + derivative * dt;
964            new_amplitudes.push(new_amplitude);
965        }
966
967        // Update amplitudes
968        for (i, new_amplitude) in new_amplitudes.into_iter().enumerate() {
969            self.oscillators[i].amplitude = new_amplitude;
970        }
971
972        Ok(())
973    }
974
975    /// Add noise to the optical system
976    fn add_noise(&mut self) {
977        let noise_config = &self.config.noise_config;
978
979        for osc in &mut self.oscillators {
980            // Quantum noise (white noise)
981            let quantum_noise_re = self.rng.random_range(-1.0..1.0) * noise_config.quantum_noise;
982            let quantum_noise_im = self.rng.random_range(-1.0..1.0) * noise_config.quantum_noise;
983
984            // Phase noise
985            let phase_noise = self.rng.random_range(-1.0..1.0) * noise_config.phase_noise;
986            let current_phase = osc.amplitude.phase();
987            let new_phase = current_phase + phase_noise;
988
989            // Amplitude noise
990            let amplitude_noise = self
991                .rng
992                .random_range(-1.0_f64..1.0)
993                .mul_add(noise_config.amplitude_noise, 1.0);
994            let current_magnitude = osc.amplitude.magnitude();
995            let new_magnitude = current_magnitude * amplitude_noise;
996
997            // Apply noise
998            osc.amplitude = Complex::from_polar(new_magnitude, new_phase)
999                + Complex::new(quantum_noise_re, quantum_noise_im);
1000
1001            // Decoherence (amplitude damping)
1002            osc.amplitude =
1003                osc.amplitude * noise_config.decoherence_rate.mul_add(-self.config.dt, 1.0);
1004        }
1005    }
1006
1007    /// Record current state for analysis
1008    fn record_state(&mut self) {
1009        let state: Vec<Complex> = self.oscillators.iter().map(|osc| osc.amplitude).collect();
1010        self.evolution_history.push((self.current_time, state));
1011    }
1012
1013    /// Measure solution from optical state
1014    fn measure_solution(&self) -> CimResult<Vec<i8>> {
1015        let mut solution = Vec::new();
1016
1017        for osc in &self.oscillators {
1018            // Homodyne detection: measure real part (in-phase quadrature)
1019            let measurement =
1020                osc.amplitude.re * self.config.measurement_config.detection_efficiency;
1021
1022            // Binary decision based on threshold
1023            let spin = if measurement > self.config.measurement_config.decision_threshold {
1024                1
1025            } else {
1026                -1
1027            };
1028
1029            solution.push(spin);
1030        }
1031
1032        Ok(solution)
1033    }
1034
1035    /// Evaluate energy of a solution
1036    fn evaluate_energy(&self, problem: &IsingModel, solution: &[i8]) -> CimResult<f64> {
1037        let mut energy = 0.0;
1038
1039        // Bias terms
1040        for i in 0..solution.len() {
1041            energy += problem.get_bias(i).unwrap_or(0.0) * f64::from(solution[i]);
1042        }
1043
1044        // Coupling terms
1045        for i in 0..solution.len() {
1046            for j in (i + 1)..solution.len() {
1047                energy += problem.get_coupling(i, j).unwrap_or(0.0)
1048                    * f64::from(solution[i])
1049                    * f64::from(solution[j]);
1050            }
1051        }
1052
1053        Ok(energy)
1054    }
1055
1056    /// Check convergence criteria
1057    fn check_convergence(&self) -> CimResult<bool> {
1058        if self.energy_history.len() < 100 {
1059            return Ok(false);
1060        }
1061
1062        // Check energy stability
1063        let recent_window = 50;
1064        let recent_energies = &self.energy_history[self.energy_history.len() - recent_window..];
1065        let energy_variance = {
1066            let mean = recent_energies.iter().sum::<f64>() / recent_energies.len() as f64;
1067            recent_energies
1068                .iter()
1069                .map(|&e| (e - mean).powi(2))
1070                .sum::<f64>()
1071                / recent_energies.len() as f64
1072        };
1073
1074        let energy_stable = energy_variance < self.config.convergence_config.energy_tolerance;
1075
1076        // Check oscillation stability
1077        let oscillation_stable = self.oscillators.iter().all(|osc| {
1078            osc.amplitude.magnitude() > self.config.convergence_config.oscillation_threshold
1079        });
1080
1081        Ok(energy_stable && oscillation_stable)
1082    }
1083
1084    /// Calculate optical system statistics
1085    fn calculate_optical_statistics(&self) -> OpticalStatistics {
1086        let num_oscillators = self.oscillators.len();
1087
1088        // Average power
1089        let total_power: f64 = self
1090            .oscillators
1091            .iter()
1092            .map(|osc| osc.amplitude.magnitude_squared())
1093            .sum();
1094        let average_power = total_power / num_oscillators as f64;
1095
1096        // Power variance
1097        let power_variance = {
1098            let powers: Vec<f64> = self
1099                .oscillators
1100                .iter()
1101                .map(|osc| osc.amplitude.magnitude_squared())
1102                .collect();
1103            let mean = powers.iter().sum::<f64>() / powers.len() as f64;
1104            powers.iter().map(|&p| (p - mean).powi(2)).sum::<f64>() / powers.len() as f64
1105        };
1106
1107        // Phase coherence (simplified)
1108        let phase_coherence: Vec<f64> = self
1109            .oscillators
1110            .iter()
1111            .map(|osc| osc.amplitude.magnitude().min(1.0))
1112            .collect();
1113
1114        // Cross-correlations (simplified)
1115        let mut cross_correlations = vec![vec![0.0; num_oscillators]; num_oscillators];
1116        for i in 0..num_oscillators {
1117            for j in 0..num_oscillators {
1118                if i == j {
1119                    cross_correlations[i][j] = 1.0;
1120                } else {
1121                    // Simplified correlation based on phase difference
1122                    let phase_diff = (self.oscillators[i].amplitude.phase()
1123                        - self.oscillators[j].amplitude.phase())
1124                    .abs();
1125                    cross_correlations[i][j] = (phase_diff / std::f64::consts::PI).cos().abs();
1126                }
1127            }
1128        }
1129
1130        // Oscillation frequencies (estimated from evolution if available)
1131        let oscillation_frequencies = vec![1.0; num_oscillators]; // Placeholder
1132
1133        OpticalStatistics {
1134            average_power,
1135            power_variance,
1136            phase_coherence,
1137            cross_correlations,
1138            oscillation_frequencies,
1139            pump_efficiency: 0.8, // Placeholder
1140        }
1141    }
1142
1143    /// Calculate performance metrics.
1144    ///
1145    /// `solution_quality` is computed relative to the run's own observed
1146    /// energy history rather than a fixed constant: it measures how much of
1147    /// the *possible* improvement (from the worst energy actually seen down
1148    /// to a real, always-computable naive lower bound on the true ground
1149    /// state) was captured by `best_energy`. The naive lower bound sums the
1150    /// most negative possible contribution of every bias and coupling term
1151    /// independently (`-sum(|h_i|) - sum(|J_ij|)`) -- a valid (if usually
1152    /// loose) lower bound on any Ising ground-state energy, derived from
1153    /// `problem` itself with no external ground-truth required. A run that
1154    /// makes no progress at all scores near `0`; one that reaches (or beats,
1155    /// in the degenerate case the bound is tight) the naive bound scores `1`.
1156    fn calculate_performance_metrics(
1157        &self,
1158        problem: &IsingModel,
1159        best_energy: f64,
1160        convergence_time: f64,
1161    ) -> CimPerformanceMetrics {
1162        let naive_lower_bound: f64 = -problem
1163            .biases()
1164            .iter()
1165            .map(|(_, bias)| bias.abs())
1166            .sum::<f64>()
1167            - problem
1168                .couplings()
1169                .iter()
1170                .map(|c| c.strength.abs())
1171                .sum::<f64>();
1172
1173        let worst_energy = self
1174            .energy_history
1175            .iter()
1176            .copied()
1177            .fold(f64::NEG_INFINITY, f64::max);
1178
1179        let solution_quality =
1180            if !worst_energy.is_finite() || (worst_energy - naive_lower_bound).abs() < 1e-12 {
1181                // No history recorded, or the bound coincides with the worst
1182                // energy seen (degenerate/trivial Hamiltonian): no meaningful
1183                // improvement range to measure against.
1184                0.0
1185            } else {
1186                ((worst_energy - best_energy) / (worst_energy - naive_lower_bound)).clamp(0.0, 1.0)
1187            };
1188
1189        // Time to convergence
1190        let time_to_convergence = convergence_time / self.config.total_time;
1191
1192        // Power efficiency
1193        let average_power = self
1194            .oscillators
1195            .iter()
1196            .map(|osc| osc.amplitude.magnitude_squared())
1197            .sum::<f64>()
1198            / self.oscillators.len() as f64;
1199        let power_efficiency = 1.0 / (1.0 + average_power);
1200
1201        // Noise resilience: real, derived from the run's own configured
1202        // noise levels -- lower configured noise implies the run is more
1203        // resilient to noise-induced degradation (there was less of it to
1204        // begin with), rather than a fixed constant regardless of `self.config`.
1205        let noise_level = self.config.noise_config.quantum_noise
1206            + self.config.noise_config.phase_noise
1207            + self.config.noise_config.amplitude_noise;
1208        let noise_resilience = 1.0 / (1.0 + noise_level);
1209
1210        CimPerformanceMetrics {
1211            solution_quality,
1212            time_to_convergence,
1213            phase_transitions: 0, // Would need to track phase transitions
1214            power_efficiency,
1215            noise_resilience,
1216        }
1217    }
1218}
1219
1220/// Helper functions for creating CIM configurations
1221
1222/// Create a standard CIM configuration for small problems
1223#[must_use]
1224pub fn create_standard_cim_config(num_oscillators: usize, simulation_time: f64) -> CimConfig {
1225    CimConfig {
1226        num_oscillators,
1227        dt: 0.001,
1228        total_time: simulation_time,
1229        pump_schedule: PumpSchedule::Linear {
1230            initial_power: 0.5,
1231            final_power: 1.5,
1232        },
1233        topology: NetworkTopology::FullyConnected,
1234        ..Default::default()
1235    }
1236}
1237
1238/// Create a CIM configuration with low noise for testing
1239#[must_use]
1240pub fn create_low_noise_cim_config(num_oscillators: usize) -> CimConfig {
1241    let mut config = create_standard_cim_config(num_oscillators, 5.0);
1242    config.noise_config = NoiseConfig {
1243        quantum_noise: 0.001,
1244        phase_noise: 0.0001,
1245        amplitude_noise: 0.0001,
1246        temperature: 0.001,
1247        decoherence_rate: 0.0001,
1248    };
1249    config
1250}
1251
1252/// Create a CIM configuration with realistic noise
1253#[must_use]
1254pub fn create_realistic_cim_config(num_oscillators: usize) -> CimConfig {
1255    let mut config = create_standard_cim_config(num_oscillators, 10.0);
1256    config.noise_config = NoiseConfig {
1257        quantum_noise: 0.05,
1258        phase_noise: 0.01,
1259        amplitude_noise: 0.01,
1260        temperature: 0.1,
1261        decoherence_rate: 0.01,
1262    };
1263    config.detailed_logging = true;
1264    config
1265}
1266
1267#[cfg(test)]
1268mod tests {
1269    use super::*;
1270
1271    #[test]
1272    fn test_complex_operations() {
1273        let c1 = Complex::new(3.0, 4.0);
1274        let c2 = Complex::new(1.0, 2.0);
1275
1276        assert_eq!(c1.magnitude(), 5.0);
1277        assert!((c1.phase() - 4.0_f64.atan2(3.0)).abs() < 1e-10);
1278
1279        let sum = c1 + c2;
1280        assert_eq!(sum.re, 4.0);
1281        assert_eq!(sum.im, 6.0);
1282
1283        let product = c1 * c2;
1284        assert_eq!(product.re, -5.0); // 3*1 - 4*2
1285        assert_eq!(product.im, 10.0); // 3*2 + 4*1
1286    }
1287
1288    #[test]
1289    fn test_cim_config_creation() {
1290        let config = create_standard_cim_config(4, 5.0);
1291        assert_eq!(config.num_oscillators, 4);
1292        assert_eq!(config.total_time, 5.0);
1293
1294        match config.topology {
1295            NetworkTopology::FullyConnected => {}
1296            _ => panic!("Expected fully connected topology"),
1297        }
1298    }
1299
1300    #[test]
1301    fn test_optical_oscillator() {
1302        let osc = OpticalParametricOscillator {
1303            index: 0,
1304            amplitude: Complex::new(1.0, 0.0),
1305            gain: 1.5,
1306            loss: 0.1,
1307            saturation: 1.0,
1308            injection: Complex::new(0.1, 0.0),
1309            threshold: 1.0,
1310        };
1311
1312        assert_eq!(osc.amplitude.magnitude(), 1.0);
1313        assert_eq!(osc.gain, 1.5);
1314        assert!(osc.gain > osc.threshold);
1315    }
1316
1317    #[test]
1318    fn solution_quality_is_real_and_data_dependent_not_a_fixed_one() {
1319        let config = create_standard_cim_config(2, 1.0);
1320        let mut cim = CoherentIsingMachine::new(config).expect("CIM creation should succeed");
1321
1322        let mut problem = IsingModel::new(2);
1323        problem.set_bias(0, 1.0).expect("set_bias should succeed");
1324        problem.set_bias(1, -1.0).expect("set_bias should succeed");
1325        problem
1326            .set_coupling(0, 1, 0.5)
1327            .expect("set_coupling should succeed");
1328        // naive_lower_bound = -(|1.0| + |-1.0| + |0.5|) = -2.5
1329
1330        cim.energy_history = vec![-2.5, -2.0, -1.0, 0.5]; // worst = 0.5
1331        let metrics = cim.calculate_performance_metrics(&problem, -2.0, 0.5);
1332
1333        // quality = (worst - best) / (worst - lower_bound) = (0.5 - (-2.0)) / (0.5 - (-2.5)) = 2.5/3.0
1334        assert!(
1335            (metrics.solution_quality - (2.5 / 3.0)).abs() < 1e-9,
1336            "expected a real data-derived quality, got {}",
1337            metrics.solution_quality
1338        );
1339
1340        // A run that made no progress at all (best == worst) must score near 0,
1341        // not the old fixed 1.0 that claimed "perfect" regardless of outcome.
1342        cim.energy_history = vec![0.5, 0.5, 0.5];
1343        let no_progress_metrics = cim.calculate_performance_metrics(&problem, 0.5, 0.5);
1344        assert!(no_progress_metrics.solution_quality < 0.1);
1345    }
1346
1347    #[test]
1348    fn noise_resilience_reflects_the_real_configured_noise_level() {
1349        let low_noise_config = create_low_noise_cim_config(2);
1350        let low_noise_cim =
1351            CoherentIsingMachine::new(low_noise_config).expect("CIM creation should succeed");
1352
1353        let mut high_noise_config = create_standard_cim_config(2, 1.0);
1354        high_noise_config.noise_config = NoiseConfig {
1355            quantum_noise: 0.5,
1356            phase_noise: 0.5,
1357            amplitude_noise: 0.5,
1358            temperature: 1.0,
1359            decoherence_rate: 0.1,
1360        };
1361        let high_noise_cim =
1362            CoherentIsingMachine::new(high_noise_config).expect("CIM creation should succeed");
1363
1364        let problem = IsingModel::new(2);
1365        let low = low_noise_cim.calculate_performance_metrics(&problem, 0.0, 0.5);
1366        let high = high_noise_cim.calculate_performance_metrics(&problem, 0.0, 0.5);
1367
1368        assert!(
1369            low.noise_resilience > high.noise_resilience,
1370            "a lower-noise configuration must score higher noise resilience than a higher-noise one \
1371             (low={}, high={})",
1372            low.noise_resilience,
1373            high.noise_resilience
1374        );
1375    }
1376}