quantrs2_core/
neutral_atom.rs

1//! Neutral Atom Quantum Computing Platform
2//!
3//! This module implements neutral atom quantum computing using ultracold atoms
4//! trapped in optical tweezers. It includes:
5//! - Rydberg atom interactions for two-qubit gates
6//! - Optical tweezer array management
7//! - Laser pulse sequences for gate operations
8//! - Atom loading and arrangement protocols
9//! - Error models for neutral atom platforms
10
11use crate::{
12    error::{QuantRS2Error, QuantRS2Result},
13    qubit::QubitId,
14};
15use ndarray::{Array1, Array2};
16use num_complex::Complex64;
17use rustc_hash::FxHashMap;
18use std::collections::HashMap;
19use std::f64::consts::PI;
20
21/// Types of neutral atoms used for quantum computing
22#[derive(Debug, Clone, PartialEq)]
23pub enum AtomSpecies {
24    /// Rubidium-87 (commonly used)
25    Rb87,
26    /// Cesium-133
27    Cs133,
28    /// Strontium-88
29    Sr88,
30    /// Ytterbium-171
31    Yb171,
32    /// Custom atom with specified properties
33    Custom {
34        mass: f64,             // atomic mass in u
35        ground_state: String,  // ground state configuration
36        rydberg_state: String, // Rydberg state used
37    },
38}
39
40impl AtomSpecies {
41    /// Get atomic mass in atomic mass units
42    pub fn mass(&self) -> f64 {
43        match self {
44            AtomSpecies::Rb87 => 86.909183,
45            AtomSpecies::Cs133 => 132.905447,
46            AtomSpecies::Sr88 => 87.905614,
47            AtomSpecies::Yb171 => 170.936426,
48            AtomSpecies::Custom { mass, .. } => *mass,
49        }
50    }
51
52    /// Get typical trap depth in μK
53    pub fn typical_trap_depth(&self) -> f64 {
54        match self {
55            AtomSpecies::Rb87 => 1000.0,
56            AtomSpecies::Cs133 => 800.0,
57            AtomSpecies::Sr88 => 1200.0,
58            AtomSpecies::Yb171 => 900.0,
59            AtomSpecies::Custom { .. } => 1000.0,
60        }
61    }
62
63    /// Get Rydberg state energy in GHz
64    pub fn rydberg_energy(&self) -> f64 {
65        match self {
66            AtomSpecies::Rb87 => 100.0, // Example value
67            AtomSpecies::Cs133 => 95.0,
68            AtomSpecies::Sr88 => 110.0,
69            AtomSpecies::Yb171 => 105.0,
70            AtomSpecies::Custom { .. } => 100.0,
71        }
72    }
73}
74
75/// Position in 3D space (in micrometers)
76#[derive(Debug, Clone, Copy, PartialEq)]
77pub struct Position3D {
78    pub x: f64,
79    pub y: f64,
80    pub z: f64,
81}
82
83impl Position3D {
84    pub fn new(x: f64, y: f64, z: f64) -> Self {
85        Self { x, y, z }
86    }
87
88    /// Calculate distance to another position
89    pub fn distance_to(&self, other: &Position3D) -> f64 {
90        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2) + (self.z - other.z).powi(2))
91            .sqrt()
92    }
93
94    /// Calculate Rydberg interaction strength at this distance
95    pub fn rydberg_interaction(&self, other: &Position3D, c6: f64) -> f64 {
96        let distance = self.distance_to(other);
97        if distance == 0.0 {
98            f64::INFINITY
99        } else {
100            c6 / distance.powi(6) // van der Waals interaction
101        }
102    }
103}
104
105/// State of a neutral atom
106#[derive(Debug, Clone, Copy, PartialEq)]
107pub enum AtomState {
108    /// Ground state |g⟩
109    Ground,
110    /// Rydberg state |r⟩
111    Rydberg,
112    /// Intermediate state during laser transitions
113    Intermediate,
114    /// Atom is missing (loading failure)
115    Missing,
116}
117
118/// Individual neutral atom in an optical tweezer
119#[derive(Debug, Clone)]
120pub struct NeutralAtom {
121    /// Atom species
122    pub species: AtomSpecies,
123    /// Position in the tweezer array
124    pub position: Position3D,
125    /// Current quantum state
126    pub state: AtomState,
127    /// Loading probability (success rate)
128    pub loading_probability: f64,
129    /// Lifetime in trap (seconds)
130    pub lifetime: f64,
131    /// Coherence time (seconds)
132    pub coherence_time: f64,
133}
134
135impl NeutralAtom {
136    /// Create a new neutral atom
137    pub fn new(species: AtomSpecies, position: Position3D) -> Self {
138        Self {
139            species,
140            position,
141            state: AtomState::Ground,
142            loading_probability: 0.9, // 90% loading success
143            lifetime: 100.0,          // 100 seconds
144            coherence_time: 1.0,      // 1 second
145        }
146    }
147
148    /// Check if atom is successfully loaded
149    pub fn is_loaded(&self) -> bool {
150        self.state != AtomState::Missing
151    }
152
153    /// Calculate interaction strength with another atom
154    pub fn interaction_with(&self, other: &NeutralAtom) -> f64 {
155        // C6 coefficient for Rydberg interactions (MHz·μm^6)
156        let c6 = match (&self.species, &other.species) {
157            (AtomSpecies::Rb87, AtomSpecies::Rb87) => 5000.0,
158            (AtomSpecies::Cs133, AtomSpecies::Cs133) => 6000.0,
159            (AtomSpecies::Sr88, AtomSpecies::Sr88) => 4500.0,
160            (AtomSpecies::Yb171, AtomSpecies::Yb171) => 4800.0,
161            _ => 5000.0, // Default for mixed species
162        };
163
164        self.position.rydberg_interaction(&other.position, c6)
165    }
166}
167
168/// Optical tweezer for trapping individual atoms
169#[derive(Debug, Clone)]
170pub struct OpticalTweezer {
171    /// Position of the tweezer focus
172    pub position: Position3D,
173    /// Laser power (mW)
174    pub power: f64,
175    /// Wavelength (nm)
176    pub wavelength: f64,
177    /// Numerical aperture of focusing objective
178    pub numerical_aperture: f64,
179    /// Whether tweezer is active
180    pub active: bool,
181    /// Trapped atom (if any)
182    pub atom: Option<NeutralAtom>,
183}
184
185impl OpticalTweezer {
186    /// Create a new optical tweezer
187    pub fn new(position: Position3D, power: f64, wavelength: f64, numerical_aperture: f64) -> Self {
188        Self {
189            position,
190            power,
191            wavelength,
192            numerical_aperture,
193            active: true,
194            atom: None,
195        }
196    }
197
198    /// Calculate trap depth in μK
199    pub fn trap_depth(&self) -> f64 {
200        if !self.active {
201            return 0.0;
202        }
203
204        // Simplified calculation: depth ∝ power / (wavelength * beam_waist²)
205        let beam_waist = self.wavelength / (PI * self.numerical_aperture); // Rough approximation
206        (self.power * 1000.0) / (self.wavelength * beam_waist.powi(2))
207    }
208
209    /// Load an atom into the tweezer
210    pub fn load_atom(&mut self, mut atom: NeutralAtom) -> bool {
211        if !self.active || self.atom.is_some() {
212            return false;
213        }
214
215        // Simulate probabilistic loading
216        use rand::Rng;
217        let mut rng = rand::rng();
218        let success = rng.random::<f64>() < atom.loading_probability;
219
220        if success {
221            atom.position = self.position;
222            self.atom = Some(atom);
223            true
224        } else {
225            false
226        }
227    }
228
229    /// Remove atom from tweezer
230    pub fn remove_atom(&mut self) -> Option<NeutralAtom> {
231        self.atom.take()
232    }
233
234    /// Check if tweezer has an atom
235    pub fn has_atom(&self) -> bool {
236        self.atom.is_some()
237    }
238}
239
240/// Laser system for controlling neutral atoms
241#[derive(Debug, Clone)]
242pub struct LaserSystem {
243    /// Wavelength in nm
244    pub wavelength: f64,
245    /// Power in mW
246    pub power: f64,
247    /// Beam waist in μm
248    pub beam_waist: f64,
249    /// Detuning from atomic transition in MHz
250    pub detuning: f64,
251    /// Laser linewidth in kHz
252    pub linewidth: f64,
253    /// Whether laser is currently on
254    pub active: bool,
255}
256
257impl LaserSystem {
258    /// Create a new laser system
259    pub fn new(wavelength: f64, power: f64, beam_waist: f64, detuning: f64) -> Self {
260        Self {
261            wavelength,
262            power,
263            beam_waist,
264            detuning,
265            linewidth: 1.0, // 1 kHz linewidth
266            active: false,
267        }
268    }
269
270    /// Calculate Rabi frequency in MHz
271    pub fn rabi_frequency(&self) -> f64 {
272        if !self.active {
273            return 0.0;
274        }
275
276        // Simplified: Ω ∝ √(power) / beam_waist
277        (self.power.sqrt() * 10.0) / self.beam_waist
278    }
279
280    /// Set laser detuning
281    pub fn set_detuning(&mut self, detuning: f64) {
282        self.detuning = detuning;
283    }
284
285    /// Turn laser on/off
286    pub fn set_active(&mut self, active: bool) {
287        self.active = active;
288    }
289}
290
291/// Neutral atom quantum computer platform
292#[derive(Debug)]
293pub struct NeutralAtomQC {
294    /// Array of optical tweezers
295    pub tweezers: Vec<OpticalTweezer>,
296    /// Map from qubit ID to tweezer index
297    pub qubit_map: FxHashMap<QubitId, usize>,
298    /// Laser systems for different transitions
299    pub lasers: HashMap<String, LaserSystem>,
300    /// Current quantum state (simplified representation)
301    pub state: Array1<Complex64>,
302    /// Number of qubits
303    pub num_qubits: usize,
304    /// Global phase accumulation
305    pub global_phase: f64,
306    /// Error model parameters
307    pub error_model: NeutralAtomErrorModel,
308}
309
310impl NeutralAtomQC {
311    /// Create a new neutral atom quantum computer
312    pub fn new(num_qubits: usize) -> Self {
313        let mut tweezers = Vec::new();
314        let mut qubit_map = FxHashMap::default();
315
316        // Create a linear array of tweezers
317        for i in 0..num_qubits {
318            let position = Position3D::new(i as f64 * 5.0, 0.0, 0.0); // 5 μm spacing
319            let tweezer = OpticalTweezer::new(
320                position, 1.0,    // 1 mW power
321                1064.0, // 1064 nm wavelength
322                0.75,   // NA = 0.75
323            );
324            tweezers.push(tweezer);
325            qubit_map.insert(QubitId(i as u32), i);
326        }
327
328        // Setup laser systems
329        let mut lasers = HashMap::new();
330
331        // Cooling laser (for Rb87)
332        lasers.insert(
333            "cooling".to_string(),
334            LaserSystem::new(780.0, 10.0, 100.0, -10.0),
335        );
336
337        // Rydberg excitation laser
338        lasers.insert(
339            "rydberg".to_string(),
340            LaserSystem::new(480.0, 1.0, 2.0, 0.0),
341        );
342
343        // Two-photon Raman laser
344        lasers.insert("raman".to_string(), LaserSystem::new(795.0, 5.0, 10.0, 0.0));
345
346        // Initialize quantum state to |00...0⟩
347        let dim = 1 << num_qubits;
348        let mut state = Array1::zeros(dim);
349        state[0] = Complex64::new(1.0, 0.0);
350
351        Self {
352            tweezers,
353            qubit_map,
354            lasers,
355            state,
356            num_qubits,
357            global_phase: 0.0,
358            error_model: NeutralAtomErrorModel::default(),
359        }
360    }
361
362    /// Load atoms into the tweezer array
363    pub fn load_atoms(&mut self, species: AtomSpecies) -> QuantRS2Result<usize> {
364        let mut loaded_count = 0;
365
366        for tweezer in &mut self.tweezers {
367            let atom = NeutralAtom::new(species.clone(), tweezer.position);
368            if tweezer.load_atom(atom) {
369                loaded_count += 1;
370            }
371        }
372
373        Ok(loaded_count)
374    }
375
376    /// Perform atom rearrangement to fill gaps
377    pub fn rearrange_atoms(&mut self) -> QuantRS2Result<()> {
378        // Collect all loaded atoms
379        let mut atoms = Vec::new();
380        for tweezer in &mut self.tweezers {
381            if let Some(atom) = tweezer.remove_atom() {
382                atoms.push(atom);
383            }
384        }
385
386        // Redistribute atoms to fill tweezers from the beginning
387        for (i, atom) in atoms.into_iter().enumerate() {
388            if i < self.tweezers.len() {
389                self.tweezers[i].load_atom(atom);
390            }
391        }
392
393        Ok(())
394    }
395
396    /// Apply single-qubit rotation gate
397    pub fn apply_single_qubit_gate(
398        &mut self,
399        qubit: QubitId,
400        gate_matrix: &Array2<Complex64>,
401    ) -> QuantRS2Result<()> {
402        let tweezer_idx = *self
403            .qubit_map
404            .get(&qubit)
405            .ok_or_else(|| QuantRS2Error::InvalidInput(format!("Qubit {:?} not found", qubit)))?;
406
407        // Check if atom is present
408        if !self.tweezers[tweezer_idx].has_atom() {
409            return Err(QuantRS2Error::InvalidOperation(
410                "No atom in tweezer".to_string(),
411            ));
412        }
413
414        // Apply gate using tensor product
415        let qubit_index = qubit.0 as usize;
416        let new_state = self.apply_single_qubit_tensor(qubit_index, gate_matrix)?;
417        self.state = new_state;
418
419        // Apply errors
420        self.apply_single_qubit_errors(qubit_index)?;
421
422        Ok(())
423    }
424
425    /// Apply two-qubit Rydberg gate
426    pub fn apply_rydberg_gate(&mut self, control: QubitId, target: QubitId) -> QuantRS2Result<()> {
427        let control_idx = *self.qubit_map.get(&control).ok_or_else(|| {
428            QuantRS2Error::InvalidInput(format!("Control qubit {:?} not found", control))
429        })?;
430        let target_idx = *self.qubit_map.get(&target).ok_or_else(|| {
431            QuantRS2Error::InvalidInput(format!("Target qubit {:?} not found", target))
432        })?;
433
434        // Check if both atoms are present
435        if !self.tweezers[control_idx].has_atom() || !self.tweezers[target_idx].has_atom() {
436            return Err(QuantRS2Error::InvalidOperation(
437                "Missing atoms for two-qubit gate".to_string(),
438            ));
439        }
440
441        // Check interaction strength
442        let control_atom = self.tweezers[control_idx].atom.as_ref().unwrap();
443        let target_atom = self.tweezers[target_idx].atom.as_ref().unwrap();
444        let interaction = control_atom.interaction_with(target_atom);
445
446        if interaction < 1.0 {
447            // Minimum interaction strength in MHz
448            return Err(QuantRS2Error::InvalidOperation(
449                "Insufficient Rydberg interaction".to_string(),
450            ));
451        }
452
453        // Apply Rydberg CZ gate (simplified)
454        let cz_matrix = self.create_rydberg_cz_matrix()?;
455        let new_state =
456            self.apply_two_qubit_tensor(control.0 as usize, target.0 as usize, &cz_matrix)?;
457        self.state = new_state;
458
459        // Apply errors
460        self.apply_two_qubit_errors(control.0 as usize, target.0 as usize)?;
461
462        Ok(())
463    }
464
465    /// Create Rydberg CZ gate matrix
466    fn create_rydberg_cz_matrix(&self) -> QuantRS2Result<Array2<Complex64>> {
467        let mut cz = Array2::eye(4);
468        cz[[3, 3]] = Complex64::new(-1.0, 0.0); // |11⟩ → -|11⟩
469        Ok(cz)
470    }
471
472    /// Apply single-qubit gate using tensor product
473    fn apply_single_qubit_tensor(
474        &self,
475        qubit_idx: usize,
476        gate: &Array2<Complex64>,
477    ) -> QuantRS2Result<Array1<Complex64>> {
478        let dim = 1 << self.num_qubits;
479        let mut new_state = Array1::zeros(dim);
480
481        for state in 0..dim {
482            let qubit_bit = (state >> qubit_idx) & 1;
483            let other_bits = state & !(1 << qubit_idx);
484
485            for new_qubit_bit in 0..2 {
486                let new_state_idx = other_bits | (new_qubit_bit << qubit_idx);
487                let gate_element = gate[[new_qubit_bit, qubit_bit]];
488                new_state[new_state_idx] += gate_element * self.state[state];
489            }
490        }
491
492        Ok(new_state)
493    }
494
495    /// Apply two-qubit gate using tensor product
496    fn apply_two_qubit_tensor(
497        &self,
498        qubit1: usize,
499        qubit2: usize,
500        gate: &Array2<Complex64>,
501    ) -> QuantRS2Result<Array1<Complex64>> {
502        let dim = 1 << self.num_qubits;
503        let mut new_state = Array1::zeros(dim);
504
505        for state in 0..dim {
506            let bit1 = (state >> qubit1) & 1;
507            let bit2 = (state >> qubit2) & 1;
508            let other_bits = state & !((1 << qubit1) | (1 << qubit2));
509            let two_qubit_state = (bit1 << 1) | bit2;
510
511            for new_two_qubit_state in 0..4 {
512                let new_bit1 = (new_two_qubit_state >> 1) & 1;
513                let new_bit2 = new_two_qubit_state & 1;
514                let new_state_idx = other_bits | (new_bit1 << qubit1) | (new_bit2 << qubit2);
515
516                let gate_element = gate[[new_two_qubit_state, two_qubit_state]];
517                new_state[new_state_idx] += gate_element * self.state[state];
518            }
519        }
520
521        Ok(new_state)
522    }
523
524    /// Apply single-qubit error models
525    fn apply_single_qubit_errors(&mut self, _qubit: usize) -> QuantRS2Result<()> {
526        // Simplified error model - in practice would include:
527        // - Laser intensity fluctuations
528        // - Decoherence from atom motion
529        // - Spontaneous emission
530        // - AC Stark shifts
531
532        use rand::Rng;
533        let mut rng = rand::rng();
534
535        // Add small random phase error
536        let phase_error = rng.random_range(-0.01..0.01);
537        self.global_phase += phase_error;
538
539        Ok(())
540    }
541
542    /// Apply two-qubit error models
543    fn apply_two_qubit_errors(&mut self, _qubit1: usize, _qubit2: usize) -> QuantRS2Result<()> {
544        // Simplified error model for Rydberg gates
545        use rand::Rng;
546        let mut rng = rand::rng();
547
548        // Rydberg gate errors are typically higher
549        let phase_error = rng.random_range(-0.05..0.05);
550        self.global_phase += phase_error;
551
552        Ok(())
553    }
554
555    /// Measure a qubit
556    pub fn measure_qubit(&mut self, qubit: QubitId) -> QuantRS2Result<u8> {
557        let qubit_idx = qubit.0 as usize;
558        if qubit_idx >= self.num_qubits {
559            return Err(QuantRS2Error::InvalidInput(
560                "Qubit index out of range".to_string(),
561            ));
562        }
563
564        // Calculate probabilities
565        let mut prob_0 = 0.0;
566        let mut prob_1 = 0.0;
567
568        for state in 0..(1 << self.num_qubits) {
569            let amplitude_sq = self.state[state].norm_sqr();
570            if (state >> qubit_idx) & 1 == 0 {
571                prob_0 += amplitude_sq;
572            } else {
573                prob_1 += amplitude_sq;
574            }
575        }
576
577        // Sample measurement result
578        use rand::Rng;
579        let mut rng = rand::rng();
580        let result: usize = if rng.random::<f64>() < prob_0 / (prob_0 + prob_1) {
581            0
582        } else {
583            1
584        };
585
586        // Collapse state
587        let mut new_state = Array1::zeros(1 << self.num_qubits);
588        let normalization = if result == 0 {
589            prob_0.sqrt()
590        } else {
591            prob_1.sqrt()
592        };
593
594        for state in 0..(1 << self.num_qubits) {
595            if ((state >> qubit_idx) & 1) == result {
596                new_state[state] = self.state[state] / normalization;
597            }
598        }
599
600        self.state = new_state;
601        Ok(result as u8)
602    }
603
604    /// Get number of loaded atoms
605    pub fn loaded_atom_count(&self) -> usize {
606        self.tweezers.iter().filter(|t| t.has_atom()).count()
607    }
608
609    /// Get quantum state probabilities
610    pub fn get_probabilities(&self) -> Vec<f64> {
611        self.state.iter().map(|c| c.norm_sqr()).collect()
612    }
613
614    /// Reset to ground state
615    pub fn reset(&mut self) {
616        let dim = 1 << self.num_qubits;
617        self.state = Array1::zeros(dim);
618        self.state[0] = Complex64::new(1.0, 0.0);
619        self.global_phase = 0.0;
620    }
621
622    /// Get atom positions for visualization
623    pub fn get_atom_positions(&self) -> Vec<(QubitId, Position3D, bool)> {
624        self.tweezers
625            .iter()
626            .enumerate()
627            .filter_map(|(i, tweezer)| {
628                self.qubit_map
629                    .iter()
630                    .find(|(_, &idx)| idx == i)
631                    .map(|(&qubit_id, _)| (qubit_id, tweezer.position, tweezer.has_atom()))
632            })
633            .collect()
634    }
635}
636
637/// Error model for neutral atom quantum computing
638#[derive(Debug, Clone)]
639pub struct NeutralAtomErrorModel {
640    /// Loading fidelity (probability of successful atom loading)
641    pub loading_fidelity: f64,
642    /// Single-qubit gate fidelity
643    pub single_qubit_fidelity: f64,
644    /// Two-qubit gate fidelity  
645    pub two_qubit_fidelity: f64,
646    /// Measurement fidelity
647    pub measurement_fidelity: f64,
648    /// Coherence time in seconds
649    pub coherence_time: f64,
650    /// Rydberg blockade radius in μm
651    pub blockade_radius: f64,
652}
653
654impl Default for NeutralAtomErrorModel {
655    fn default() -> Self {
656        Self {
657            loading_fidelity: 0.95,
658            single_qubit_fidelity: 0.999,
659            two_qubit_fidelity: 0.98,
660            measurement_fidelity: 0.99,
661            coherence_time: 1.0,
662            blockade_radius: 10.0,
663        }
664    }
665}
666
667/// Neutral atom gate library
668pub struct NeutralAtomGates;
669
670impl NeutralAtomGates {
671    /// X gate (π rotation around X-axis)
672    pub fn x_gate() -> Array2<Complex64> {
673        let mut x = Array2::zeros((2, 2));
674        x[[0, 1]] = Complex64::new(1.0, 0.0);
675        x[[1, 0]] = Complex64::new(1.0, 0.0);
676        x
677    }
678
679    /// Y gate (π rotation around Y-axis)
680    pub fn y_gate() -> Array2<Complex64> {
681        let mut y = Array2::zeros((2, 2));
682        y[[0, 1]] = Complex64::new(0.0, -1.0);
683        y[[1, 0]] = Complex64::new(0.0, 1.0);
684        y
685    }
686
687    /// Z gate (π rotation around Z-axis)
688    pub fn z_gate() -> Array2<Complex64> {
689        let mut z = Array2::zeros((2, 2));
690        z[[0, 0]] = Complex64::new(1.0, 0.0);
691        z[[1, 1]] = Complex64::new(-1.0, 0.0);
692        z
693    }
694
695    /// Hadamard gate
696    pub fn h_gate() -> Array2<Complex64> {
697        let h_val = 1.0 / 2.0_f64.sqrt();
698        let mut h = Array2::zeros((2, 2));
699        h[[0, 0]] = Complex64::new(h_val, 0.0);
700        h[[0, 1]] = Complex64::new(h_val, 0.0);
701        h[[1, 0]] = Complex64::new(h_val, 0.0);
702        h[[1, 1]] = Complex64::new(-h_val, 0.0);
703        h
704    }
705
706    /// Rotation around X-axis
707    pub fn rx_gate(theta: f64) -> Array2<Complex64> {
708        let cos_half = (theta / 2.0).cos();
709        let sin_half = (theta / 2.0).sin();
710
711        let mut rx = Array2::zeros((2, 2));
712        rx[[0, 0]] = Complex64::new(cos_half, 0.0);
713        rx[[0, 1]] = Complex64::new(0.0, -sin_half);
714        rx[[1, 0]] = Complex64::new(0.0, -sin_half);
715        rx[[1, 1]] = Complex64::new(cos_half, 0.0);
716        rx
717    }
718
719    /// Rotation around Y-axis
720    pub fn ry_gate(theta: f64) -> Array2<Complex64> {
721        let cos_half = (theta / 2.0).cos();
722        let sin_half = (theta / 2.0).sin();
723
724        let mut ry = Array2::zeros((2, 2));
725        ry[[0, 0]] = Complex64::new(cos_half, 0.0);
726        ry[[0, 1]] = Complex64::new(-sin_half, 0.0);
727        ry[[1, 0]] = Complex64::new(sin_half, 0.0);
728        ry[[1, 1]] = Complex64::new(cos_half, 0.0);
729        ry
730    }
731
732    /// Rotation around Z-axis
733    pub fn rz_gate(theta: f64) -> Array2<Complex64> {
734        let exp_neg = Complex64::new(0.0, -theta / 2.0).exp();
735        let exp_pos = Complex64::new(0.0, theta / 2.0).exp();
736
737        let mut rz = Array2::zeros((2, 2));
738        rz[[0, 0]] = exp_neg;
739        rz[[1, 1]] = exp_pos;
740        rz
741    }
742}
743
744#[cfg(test)]
745mod tests {
746    use super::*;
747
748    #[test]
749    fn test_atom_species_properties() {
750        let rb87 = AtomSpecies::Rb87;
751        assert!((rb87.mass() - 86.909183).abs() < 1e-6);
752        assert!(rb87.typical_trap_depth() > 0.0);
753        assert!(rb87.rydberg_energy() > 0.0);
754
755        let custom = AtomSpecies::Custom {
756            mass: 100.0,
757            ground_state: "5s".to_string(),
758            rydberg_state: "50s".to_string(),
759        };
760        assert_eq!(custom.mass(), 100.0);
761    }
762
763    #[test]
764    fn test_position_calculations() {
765        let pos1 = Position3D::new(0.0, 0.0, 0.0);
766        let pos2 = Position3D::new(3.0, 4.0, 0.0);
767
768        assert_eq!(pos1.distance_to(&pos2), 5.0);
769
770        let interaction = pos1.rydberg_interaction(&pos2, 1000.0);
771        assert!(interaction > 0.0);
772        assert!(interaction.is_finite());
773    }
774
775    #[test]
776    fn test_optical_tweezer() {
777        let position = Position3D::new(0.0, 0.0, 0.0);
778        let mut tweezer = OpticalTweezer::new(position, 1.0, 1064.0, 0.75);
779
780        assert!(tweezer.active);
781        assert!(!tweezer.has_atom());
782        assert!(tweezer.trap_depth() > 0.0);
783
784        let atom = NeutralAtom::new(AtomSpecies::Rb87, position);
785        let loaded = tweezer.load_atom(atom);
786        // Loading is probabilistic, so we can't guarantee success
787        // Test that loading returns a boolean value
788        assert!(loaded == true || loaded == false);
789    }
790
791    #[test]
792    fn test_laser_system() {
793        let mut laser = LaserSystem::new(780.0, 10.0, 100.0, -10.0);
794
795        assert!(!laser.active);
796        assert_eq!(laser.rabi_frequency(), 0.0);
797
798        laser.set_active(true);
799        assert!(laser.rabi_frequency() > 0.0);
800
801        laser.set_detuning(5.0);
802        assert_eq!(laser.detuning, 5.0);
803    }
804
805    #[test]
806    fn test_neutral_atom_creation() {
807        let position = Position3D::new(1.0, 2.0, 3.0);
808        let atom = NeutralAtom::new(AtomSpecies::Rb87, position);
809
810        assert_eq!(atom.species, AtomSpecies::Rb87);
811        assert_eq!(atom.position, position);
812        assert_eq!(atom.state, AtomState::Ground);
813        assert!(atom.is_loaded());
814    }
815
816    #[test]
817    fn test_neutral_atom_qc_initialization() {
818        let qc = NeutralAtomQC::new(3);
819
820        assert_eq!(qc.num_qubits, 3);
821        assert_eq!(qc.tweezers.len(), 3);
822        assert_eq!(qc.qubit_map.len(), 3);
823        assert_eq!(qc.state.len(), 8); // 2^3 = 8
824
825        // Initial state should be |000⟩
826        assert!((qc.state[0].norm_sqr() - 1.0).abs() < 1e-10);
827        for i in 1..8 {
828            assert!(qc.state[i].norm_sqr() < 1e-10);
829        }
830    }
831
832    #[test]
833    fn test_atom_loading() {
834        let mut qc = NeutralAtomQC::new(2);
835        let loaded = qc.load_atoms(AtomSpecies::Rb87).unwrap();
836
837        // Should attempt to load into both tweezers
838        assert!(loaded <= 2);
839        assert!(qc.loaded_atom_count() <= 2);
840    }
841
842    #[test]
843    fn test_single_qubit_gates() {
844        let mut qc = NeutralAtomQC::new(1);
845        qc.load_atoms(AtomSpecies::Rb87).unwrap();
846
847        if qc.loaded_atom_count() > 0 {
848            let x_gate = NeutralAtomGates::x_gate();
849            let result = qc.apply_single_qubit_gate(QubitId(0), &x_gate);
850
851            // If atom is loaded, gate should succeed
852            assert!(result.is_ok());
853        }
854    }
855
856    #[test]
857    fn test_two_qubit_rydberg_gate() {
858        let mut qc = NeutralAtomQC::new(2);
859        qc.load_atoms(AtomSpecies::Rb87).unwrap();
860
861        if qc.loaded_atom_count() == 2 {
862            let result = qc.apply_rydberg_gate(QubitId(0), QubitId(1));
863            // Should succeed if both atoms are loaded and close enough
864            assert!(result.is_ok() || result.is_err()); // Test interface
865        }
866    }
867
868    #[test]
869    fn test_measurement() {
870        let mut qc = NeutralAtomQC::new(1);
871        qc.load_atoms(AtomSpecies::Rb87).unwrap();
872
873        if qc.loaded_atom_count() > 0 {
874            let result = qc.measure_qubit(QubitId(0));
875            if result.is_ok() {
876                let measurement = result.unwrap();
877                assert!(measurement == 0 || measurement == 1);
878            }
879        }
880    }
881
882    #[test]
883    fn test_gate_matrices() {
884        let x = NeutralAtomGates::x_gate();
885        let y = NeutralAtomGates::y_gate();
886        let z = NeutralAtomGates::z_gate();
887        let h = NeutralAtomGates::h_gate();
888
889        // Test basic properties
890        assert_eq!(x.dim(), (2, 2));
891        assert_eq!(y.dim(), (2, 2));
892        assert_eq!(z.dim(), (2, 2));
893        assert_eq!(h.dim(), (2, 2));
894
895        // Test Pauli X matrix elements
896        assert_eq!(x[[0, 1]], Complex64::new(1.0, 0.0));
897        assert_eq!(x[[1, 0]], Complex64::new(1.0, 0.0));
898        assert_eq!(x[[0, 0]], Complex64::new(0.0, 0.0));
899        assert_eq!(x[[1, 1]], Complex64::new(0.0, 0.0));
900    }
901
902    #[test]
903    fn test_rotation_gates() {
904        let rx_pi = NeutralAtomGates::rx_gate(PI);
905        let _ry_pi = NeutralAtomGates::ry_gate(PI);
906        let _rz_pi = NeutralAtomGates::rz_gate(PI);
907
908        // RX(π) should be approximately -iX
909        let x = NeutralAtomGates::x_gate();
910        let expected_rx = x.mapv(|x| Complex64::new(0.0, -1.0) * x);
911
912        for i in 0..2 {
913            for j in 0..2 {
914                assert!((rx_pi[[i, j]] - expected_rx[[i, j]]).norm() < 1e-10);
915            }
916        }
917    }
918
919    #[test]
920    fn test_atom_rearrangement() {
921        let mut qc = NeutralAtomQC::new(3);
922
923        // Manually add some atoms to non-consecutive tweezers with 100% loading probability
924        let mut atom1 = NeutralAtom::new(AtomSpecies::Rb87, qc.tweezers[0].position);
925        let mut atom2 = NeutralAtom::new(AtomSpecies::Rb87, qc.tweezers[2].position);
926        atom1.loading_probability = 1.0; // Ensure 100% loading success for test
927        atom2.loading_probability = 1.0; // Ensure 100% loading success for test
928        qc.tweezers[0].atom = Some(atom1);
929        qc.tweezers[2].atom = Some(atom2);
930
931        assert_eq!(qc.loaded_atom_count(), 2);
932
933        // Rearrange should succeed
934        assert!(qc.rearrange_atoms().is_ok());
935
936        // After rearrangement, atoms should be in consecutive tweezers
937        assert!(qc.tweezers[0].has_atom());
938        assert!(qc.tweezers[1].has_atom());
939        assert!(!qc.tweezers[2].has_atom());
940    }
941
942    #[test]
943    fn test_error_model_defaults() {
944        let error_model = NeutralAtomErrorModel::default();
945
946        assert!(error_model.loading_fidelity > 0.0 && error_model.loading_fidelity <= 1.0);
947        assert!(
948            error_model.single_qubit_fidelity > 0.0 && error_model.single_qubit_fidelity <= 1.0
949        );
950        assert!(error_model.two_qubit_fidelity > 0.0 && error_model.two_qubit_fidelity <= 1.0);
951        assert!(error_model.measurement_fidelity > 0.0 && error_model.measurement_fidelity <= 1.0);
952        assert!(error_model.coherence_time > 0.0);
953        assert!(error_model.blockade_radius > 0.0);
954    }
955
956    #[test]
957    fn test_quantum_state_probabilities() {
958        let qc = NeutralAtomQC::new(2);
959        let probs = qc.get_probabilities();
960
961        assert_eq!(probs.len(), 4); // 2^2 = 4 states
962
963        // Probabilities should sum to 1
964        let total: f64 = probs.iter().sum();
965        assert!((total - 1.0).abs() < 1e-10);
966
967        // Initial state |00⟩ should have probability 1
968        assert!((probs[0] - 1.0).abs() < 1e-10);
969    }
970
971    #[test]
972    fn test_atom_position_retrieval() {
973        let qc = NeutralAtomQC::new(2);
974        let positions = qc.get_atom_positions();
975
976        assert_eq!(positions.len(), 2);
977
978        // Check that positions are returned correctly
979        for (qubit_id, position, has_atom) in positions {
980            assert!(qubit_id.0 < 2);
981            assert!(position.x >= 0.0); // Should be at positive x coordinates
982                                        // has_atom depends on loading success, so we just test the interface
983                                        // Test that has_atom returns a boolean value
984            assert!(has_atom == true || has_atom == false);
985        }
986    }
987}