Skip to main content

ruqu_core/
mixed_precision.rs

1//! Mixed-precision (f32) quantum state vector.
2//!
3//! Provides a float32 complex type and state vector that uses half the memory
4//! of the standard f64 state, enabling simulation of approximately one
5//! additional qubit at each memory threshold.
6//!
7//! | Qubits | f64 memory | f32 memory |
8//! |--------|-----------|-----------|
9//! | 25     | 512 MiB   | 256 MiB   |
10//! | 30     | 16 GiB    | 8 GiB     |
11//! | 32     | 64 GiB    | 32 GiB    |
12//! | 33     | 128 GiB   | 64 GiB    |
13
14use crate::error::{QuantumError, Result};
15use crate::gate::Gate;
16use crate::types::{Complex, MeasurementOutcome, QubitIndex};
17
18use rand::rngs::StdRng;
19use rand::{Rng, SeedableRng};
20use std::fmt;
21use std::ops::{Add, AddAssign, Mul, Neg, Sub};
22
23// ---------------------------------------------------------------------------
24// Complex32
25// ---------------------------------------------------------------------------
26
27/// Complex number using f32 precision (8 bytes vs 16 bytes for f64).
28///
29/// This is the building block for `QuantumStateF32`. Each amplitude occupies
30/// half the memory of the standard `Complex` (f64) type, doubling the number
31/// of amplitudes that fit in a given memory budget and thus enabling roughly
32/// one additional qubit of simulation capacity.
33#[derive(Clone, Copy, PartialEq)]
34pub struct Complex32 {
35    /// Real component.
36    pub re: f32,
37    /// Imaginary component.
38    pub im: f32,
39}
40
41impl Complex32 {
42    /// The additive identity, 0 + 0i.
43    pub const ZERO: Self = Self { re: 0.0, im: 0.0 };
44
45    /// The multiplicative identity, 1 + 0i.
46    pub const ONE: Self = Self { re: 1.0, im: 0.0 };
47
48    /// The imaginary unit, 0 + 1i.
49    pub const I: Self = Self { re: 0.0, im: 1.0 };
50
51    /// Create a new complex number from real and imaginary parts.
52    #[inline]
53    pub fn new(re: f32, im: f32) -> Self {
54        Self { re, im }
55    }
56
57    /// Squared magnitude: |z|^2 = re^2 + im^2.
58    #[inline]
59    pub fn norm_sq(&self) -> f32 {
60        self.re * self.re + self.im * self.im
61    }
62
63    /// Magnitude: |z|.
64    #[inline]
65    pub fn norm(&self) -> f32 {
66        self.norm_sq().sqrt()
67    }
68
69    /// Complex conjugate: conj(a + bi) = a - bi.
70    #[inline]
71    pub fn conj(&self) -> Self {
72        Self {
73            re: self.re,
74            im: -self.im,
75        }
76    }
77
78    /// Convert from an f64 `Complex` by narrowing each component to f32.
79    #[inline]
80    pub fn from_f64(c: &Complex) -> Self {
81        Self {
82            re: c.re as f32,
83            im: c.im as f32,
84        }
85    }
86
87    /// Convert to an f64 `Complex` by widening each component to f64.
88    #[inline]
89    pub fn to_f64(&self) -> Complex {
90        Complex {
91            re: self.re as f64,
92            im: self.im as f64,
93        }
94    }
95}
96
97// ---------------------------------------------------------------------------
98// Arithmetic trait implementations for Complex32
99// ---------------------------------------------------------------------------
100
101impl Add for Complex32 {
102    type Output = Self;
103    #[inline]
104    fn add(self, rhs: Self) -> Self {
105        Self {
106            re: self.re + rhs.re,
107            im: self.im + rhs.im,
108        }
109    }
110}
111
112impl Sub for Complex32 {
113    type Output = Self;
114    #[inline]
115    fn sub(self, rhs: Self) -> Self {
116        Self {
117            re: self.re - rhs.re,
118            im: self.im - rhs.im,
119        }
120    }
121}
122
123impl Mul for Complex32 {
124    type Output = Self;
125    #[inline]
126    fn mul(self, rhs: Self) -> Self {
127        Self {
128            re: self.re * rhs.re - self.im * rhs.im,
129            im: self.re * rhs.im + self.im * rhs.re,
130        }
131    }
132}
133
134impl Neg for Complex32 {
135    type Output = Self;
136    #[inline]
137    fn neg(self) -> Self {
138        Self {
139            re: -self.re,
140            im: -self.im,
141        }
142    }
143}
144
145impl AddAssign for Complex32 {
146    #[inline]
147    fn add_assign(&mut self, rhs: Self) {
148        self.re += rhs.re;
149        self.im += rhs.im;
150    }
151}
152
153impl Mul<f32> for Complex32 {
154    type Output = Self;
155    #[inline]
156    fn mul(self, rhs: f32) -> Self {
157        Self {
158            re: self.re * rhs,
159            im: self.im * rhs,
160        }
161    }
162}
163
164impl fmt::Debug for Complex32 {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        write!(f, "({}, {})", self.re, self.im)
167    }
168}
169
170impl fmt::Display for Complex32 {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        if self.im >= 0.0 {
173            write!(f, "{}+{}i", self.re, self.im)
174        } else {
175            write!(f, "{}{}i", self.re, self.im)
176        }
177    }
178}
179
180// ---------------------------------------------------------------------------
181// QuantumStateF32
182// ---------------------------------------------------------------------------
183
184/// Maximum qubits for f32 state vector (1 more than f64 due to halved memory).
185pub const MAX_QUBITS_F32: u32 = 33;
186
187/// Quantum state using f32 precision for reduced memory usage.
188///
189/// Uses 8 bytes per amplitude instead of 16, enabling simulation of
190/// approximately one additional qubit at each memory boundary. This is
191/// intended for warm/exploratory runs; final verification can upcast to
192/// the full `QuantumState` (f64) via [`QuantumStateF32::to_f64`].
193pub struct QuantumStateF32 {
194    amplitudes: Vec<Complex32>,
195    num_qubits: u32,
196    rng: StdRng,
197    measurement_record: Vec<MeasurementOutcome>,
198    /// Running count of gate applications, used for error bound estimation.
199    gate_count: u64,
200}
201
202// ---------------------------------------------------------------------------
203// Construction
204// ---------------------------------------------------------------------------
205
206impl QuantumStateF32 {
207    /// Create the |00...0> state for `num_qubits` qubits using f32 precision.
208    pub fn new(num_qubits: u32) -> Result<Self> {
209        if num_qubits == 0 {
210            return Err(QuantumError::CircuitError(
211                "cannot create quantum state with 0 qubits".into(),
212            ));
213        }
214        if num_qubits > MAX_QUBITS_F32 {
215            return Err(QuantumError::QubitLimitExceeded {
216                requested: num_qubits,
217                maximum: MAX_QUBITS_F32,
218            });
219        }
220        let n = 1usize << num_qubits;
221        let mut amplitudes = vec![Complex32::ZERO; n];
222        amplitudes[0] = Complex32::ONE;
223        Ok(Self {
224            amplitudes,
225            num_qubits,
226            rng: StdRng::from_entropy(),
227            measurement_record: Vec::new(),
228            gate_count: 0,
229        })
230    }
231
232    /// Create the |00...0> state with a deterministic seed for reproducibility.
233    pub fn new_with_seed(num_qubits: u32, seed: u64) -> Result<Self> {
234        if num_qubits == 0 {
235            return Err(QuantumError::CircuitError(
236                "cannot create quantum state with 0 qubits".into(),
237            ));
238        }
239        if num_qubits > MAX_QUBITS_F32 {
240            return Err(QuantumError::QubitLimitExceeded {
241                requested: num_qubits,
242                maximum: MAX_QUBITS_F32,
243            });
244        }
245        let n = 1usize << num_qubits;
246        let mut amplitudes = vec![Complex32::ZERO; n];
247        amplitudes[0] = Complex32::ONE;
248        Ok(Self {
249            amplitudes,
250            num_qubits,
251            rng: StdRng::seed_from_u64(seed),
252            measurement_record: Vec::new(),
253            gate_count: 0,
254        })
255    }
256
257    /// Downcast from an f64 `QuantumState`, narrowing each amplitude to f32.
258    ///
259    /// The measurement record is cloned from the source state.
260    pub fn from_f64(state: &crate::state::QuantumState) -> Self {
261        let amplitudes: Vec<Complex32> = state
262            .state_vector()
263            .iter()
264            .map(|c| Complex32::from_f64(c))
265            .collect();
266        Self {
267            num_qubits: state.num_qubits(),
268            amplitudes,
269            rng: StdRng::from_entropy(),
270            measurement_record: state.measurement_record().to_vec(),
271            gate_count: 0,
272        }
273    }
274
275    /// Upcast to an f64 `QuantumState` for high-precision verification.
276    ///
277    /// Each f32 amplitude is widened to f64. The measurement record is
278    /// **not** transferred since the f64 state is typically used for fresh
279    /// verification runs.
280    pub fn to_f64(&self) -> Result<crate::state::QuantumState> {
281        let amps: Vec<Complex> = self.amplitudes.iter().map(|c| c.to_f64()).collect();
282        crate::state::QuantumState::from_amplitudes(amps, self.num_qubits)
283    }
284
285    // -------------------------------------------------------------------
286    // Accessors
287    // -------------------------------------------------------------------
288
289    /// Number of qubits in this state.
290    pub fn num_qubits(&self) -> u32 {
291        self.num_qubits
292    }
293
294    /// Number of amplitudes (2^num_qubits).
295    pub fn num_amplitudes(&self) -> usize {
296        self.amplitudes.len()
297    }
298
299    /// Compute |amplitude|^2 for each basis state.
300    ///
301    /// Probabilities are returned as f64 for downstream accuracy: the f32
302    /// norm-squared values are widened before being returned.
303    pub fn probabilities(&self) -> Vec<f64> {
304        self.amplitudes
305            .iter()
306            .map(|a| a.norm_sq() as f64)
307            .collect()
308    }
309
310    /// Estimated memory in bytes for an f32 state of `num_qubits` qubits.
311    ///
312    /// Each amplitude is 8 bytes (two f32 values).
313    pub fn estimate_memory(num_qubits: u32) -> usize {
314        (1usize << num_qubits) * std::mem::size_of::<Complex32>()
315    }
316
317    /// Returns the record of measurements performed on this state.
318    pub fn measurement_record(&self) -> &[MeasurementOutcome] {
319        &self.measurement_record
320    }
321
322    /// Rough upper-bound estimate of accumulated floating-point error from
323    /// using f32 instead of f64.
324    ///
325    /// Each gate application introduces approximately `f32::EPSILON` (~1.2e-7)
326    /// of relative error per amplitude. Over `g` gates this compounds to
327    /// roughly `g * eps`. This is a conservative, heuristic bound.
328    pub fn precision_error_bound(&self) -> f64 {
329        (self.gate_count as f64) * (f32::EPSILON as f64)
330    }
331
332    // -------------------------------------------------------------------
333    // Gate dispatch
334    // -------------------------------------------------------------------
335
336    /// Apply a gate to the state, returning any measurement outcomes.
337    ///
338    /// The gate's f64 matrices are converted to f32 before application.
339    pub fn apply_gate(&mut self, gate: &Gate) -> Result<Vec<MeasurementOutcome>> {
340        // Validate qubit indices.
341        for &q in gate.qubits().iter() {
342            self.validate_qubit(q)?;
343        }
344
345        match gate {
346            Gate::Barrier => Ok(vec![]),
347
348            Gate::Measure(q) => {
349                let outcome = self.measure(*q)?;
350                Ok(vec![outcome])
351            }
352
353            Gate::Reset(q) => {
354                self.reset_qubit(*q)?;
355                Ok(vec![])
356            }
357
358            // Two-qubit gates
359            Gate::CNOT(q1, q2)
360            | Gate::CZ(q1, q2)
361            | Gate::SWAP(q1, q2)
362            | Gate::Rzz(q1, q2, _) => {
363                if q1 == q2 {
364                    return Err(QuantumError::CircuitError(format!(
365                        "two-qubit gate requires distinct qubits, got {} and {}",
366                        q1, q2
367                    )));
368                }
369                let matrix_f64 = gate.matrix_2q().unwrap();
370                let matrix = convert_matrix_2q(&matrix_f64);
371                self.apply_two_qubit_gate(*q1, *q2, &matrix);
372                self.gate_count += 1;
373                Ok(vec![])
374            }
375
376            // Everything else must be a single-qubit unitary.
377            other => {
378                if let Some(matrix_f64) = other.matrix_1q() {
379                    let q = other.qubits()[0];
380                    let matrix = convert_matrix_1q(&matrix_f64);
381                    self.apply_single_qubit_gate(q, &matrix);
382                    self.gate_count += 1;
383                    Ok(vec![])
384                } else {
385                    Err(QuantumError::CircuitError(format!(
386                        "unsupported gate: {:?}",
387                        other
388                    )))
389                }
390            }
391        }
392    }
393
394    // -------------------------------------------------------------------
395    // Single-qubit gate kernel
396    // -------------------------------------------------------------------
397
398    /// Apply a 2x2 unitary matrix to the given qubit.
399    ///
400    /// For each pair of amplitudes where the qubit bit is 0 (index `i`)
401    /// versus 1 (index `j = i + step`), the matrix transformation is applied.
402    pub fn apply_single_qubit_gate(
403        &mut self,
404        qubit: QubitIndex,
405        matrix: &[[Complex32; 2]; 2],
406    ) {
407        let step = 1usize << qubit;
408        let n = self.amplitudes.len();
409
410        let mut block_start = 0;
411        while block_start < n {
412            for i in block_start..block_start + step {
413                let j = i + step;
414                let a = self.amplitudes[i]; // qubit = 0
415                let b = self.amplitudes[j]; // qubit = 1
416                self.amplitudes[i] = matrix[0][0] * a + matrix[0][1] * b;
417                self.amplitudes[j] = matrix[1][0] * a + matrix[1][1] * b;
418            }
419            block_start += step << 1;
420        }
421    }
422
423    // -------------------------------------------------------------------
424    // Two-qubit gate kernel
425    // -------------------------------------------------------------------
426
427    /// Apply a 4x4 unitary matrix to qubits `q1` and `q2`.
428    ///
429    /// Matrix row/column index = q1_bit * 2 + q2_bit.
430    pub fn apply_two_qubit_gate(
431        &mut self,
432        q1: QubitIndex,
433        q2: QubitIndex,
434        matrix: &[[Complex32; 4]; 4],
435    ) {
436        let q1_bit = 1usize << q1;
437        let q2_bit = 1usize << q2;
438        let n = self.amplitudes.len();
439
440        for base in 0..n {
441            // Process each group of 4 amplitudes exactly once: when both
442            // target bits in the index are zero.
443            if base & q1_bit != 0 || base & q2_bit != 0 {
444                continue;
445            }
446
447            let idxs = [
448                base,                   // q1=0, q2=0
449                base | q2_bit,          // q1=0, q2=1
450                base | q1_bit,          // q1=1, q2=0
451                base | q1_bit | q2_bit, // q1=1, q2=1
452            ];
453
454            let vals = [
455                self.amplitudes[idxs[0]],
456                self.amplitudes[idxs[1]],
457                self.amplitudes[idxs[2]],
458                self.amplitudes[idxs[3]],
459            ];
460
461            for r in 0..4 {
462                self.amplitudes[idxs[r]] = matrix[r][0] * vals[0]
463                    + matrix[r][1] * vals[1]
464                    + matrix[r][2] * vals[2]
465                    + matrix[r][3] * vals[3];
466            }
467        }
468    }
469
470    // -------------------------------------------------------------------
471    // Measurement
472    // -------------------------------------------------------------------
473
474    /// Measure a single qubit projectively.
475    ///
476    /// 1. Compute P(qubit = 0) using f32 arithmetic.
477    /// 2. Sample the outcome.
478    /// 3. Collapse the state vector (zero out the other branch).
479    /// 4. Renormalise.
480    ///
481    /// The probability stored in the returned `MeasurementOutcome` is widened
482    /// to f64 for compatibility with the rest of the engine.
483    pub fn measure(&mut self, qubit: QubitIndex) -> Result<MeasurementOutcome> {
484        self.validate_qubit(qubit)?;
485
486        let qubit_bit = 1usize << qubit;
487        let n = self.amplitudes.len();
488
489        // Probability of measuring |0> (accumulated in f32).
490        let mut p0: f32 = 0.0;
491        for i in 0..n {
492            if i & qubit_bit == 0 {
493                p0 += self.amplitudes[i].norm_sq();
494            }
495        }
496
497        let random: f64 = self.rng.gen();
498        let result = random >= p0 as f64; // true => measured |1>
499        let prob_f32 = if result { 1.0_f32 - p0 } else { p0 };
500
501        // Guard against division by zero (degenerate state).
502        let norm_factor = if prob_f32 > 0.0 {
503            1.0_f32 / prob_f32.sqrt()
504        } else {
505            0.0_f32
506        };
507
508        // Collapse + renormalise.
509        for i in 0..n {
510            let bit_is_one = i & qubit_bit != 0;
511            if bit_is_one == result {
512                self.amplitudes[i] = self.amplitudes[i] * norm_factor;
513            } else {
514                self.amplitudes[i] = Complex32::ZERO;
515            }
516        }
517
518        let outcome = MeasurementOutcome {
519            qubit,
520            result,
521            probability: prob_f32 as f64,
522        };
523        self.measurement_record.push(outcome.clone());
524        Ok(outcome)
525    }
526
527    // -------------------------------------------------------------------
528    // Reset
529    // -------------------------------------------------------------------
530
531    /// Reset a qubit to |0>.
532    ///
533    /// Implemented as "measure, then flip if result was |1>".
534    fn reset_qubit(&mut self, qubit: QubitIndex) -> Result<()> {
535        let outcome = self.measure(qubit)?;
536        if outcome.result {
537            // Qubit collapsed to |1>; apply X to bring it back to |0>.
538            let x_matrix_f64 = Gate::X(qubit).matrix_1q().unwrap();
539            let x_matrix = convert_matrix_1q(&x_matrix_f64);
540            self.apply_single_qubit_gate(qubit, &x_matrix);
541        }
542        Ok(())
543    }
544
545    // -------------------------------------------------------------------
546    // Internal helpers
547    // -------------------------------------------------------------------
548
549    /// Validate that a qubit index is within range.
550    fn validate_qubit(&self, qubit: QubitIndex) -> Result<()> {
551        if qubit >= self.num_qubits {
552            return Err(QuantumError::InvalidQubitIndex {
553                index: qubit,
554                num_qubits: self.num_qubits,
555            });
556        }
557        Ok(())
558    }
559}
560
561// ---------------------------------------------------------------------------
562// Matrix conversion helpers (f64 -> f32)
563// ---------------------------------------------------------------------------
564
565/// Convert a 2x2 f64 gate matrix to f32.
566fn convert_matrix_1q(m: &[[Complex; 2]; 2]) -> [[Complex32; 2]; 2] {
567    [
568        [Complex32::from_f64(&m[0][0]), Complex32::from_f64(&m[0][1])],
569        [Complex32::from_f64(&m[1][0]), Complex32::from_f64(&m[1][1])],
570    ]
571}
572
573/// Convert a 4x4 f64 gate matrix to f32.
574fn convert_matrix_2q(m: &[[Complex; 4]; 4]) -> [[Complex32; 4]; 4] {
575    [
576        [
577            Complex32::from_f64(&m[0][0]),
578            Complex32::from_f64(&m[0][1]),
579            Complex32::from_f64(&m[0][2]),
580            Complex32::from_f64(&m[0][3]),
581        ],
582        [
583            Complex32::from_f64(&m[1][0]),
584            Complex32::from_f64(&m[1][1]),
585            Complex32::from_f64(&m[1][2]),
586            Complex32::from_f64(&m[1][3]),
587        ],
588        [
589            Complex32::from_f64(&m[2][0]),
590            Complex32::from_f64(&m[2][1]),
591            Complex32::from_f64(&m[2][2]),
592            Complex32::from_f64(&m[2][3]),
593        ],
594        [
595            Complex32::from_f64(&m[3][0]),
596            Complex32::from_f64(&m[3][1]),
597            Complex32::from_f64(&m[3][2]),
598            Complex32::from_f64(&m[3][3]),
599        ],
600    ]
601}
602
603// ---------------------------------------------------------------------------
604// Tests
605// ---------------------------------------------------------------------------
606
607#[cfg(test)]
608mod tests {
609    use super::*;
610
611    const EPS: f32 = 1e-6;
612
613    fn approx_eq_f32(a: f32, b: f32) -> bool {
614        (a - b).abs() < EPS
615    }
616
617    #[test]
618    fn complex32_arithmetic() {
619        let a = Complex32::new(1.0, 2.0);
620        let b = Complex32::new(3.0, -1.0);
621
622        let sum = a + b;
623        assert!(approx_eq_f32(sum.re, 4.0));
624        assert!(approx_eq_f32(sum.im, 1.0));
625
626        let diff = a - b;
627        assert!(approx_eq_f32(diff.re, -2.0));
628        assert!(approx_eq_f32(diff.im, 3.0));
629
630        // (1+2i)*(3-i) = 3 - i + 6i - 2i^2 = 3 + 5i + 2 = 5 + 5i
631        let prod = a * b;
632        assert!(approx_eq_f32(prod.re, 5.0));
633        assert!(approx_eq_f32(prod.im, 5.0));
634
635        let neg = -a;
636        assert!(approx_eq_f32(neg.re, -1.0));
637        assert!(approx_eq_f32(neg.im, -2.0));
638
639        assert!(approx_eq_f32(a.norm_sq(), 5.0));
640        assert!(approx_eq_f32(a.conj().im, -2.0));
641    }
642
643    #[test]
644    fn complex32_f64_conversion() {
645        let c64 = Complex::new(1.5, -2.5);
646        let c32 = Complex32::from_f64(&c64);
647        assert!(approx_eq_f32(c32.re, 1.5));
648        assert!(approx_eq_f32(c32.im, -2.5));
649
650        let back = c32.to_f64();
651        assert!((back.re - 1.5).abs() < 1e-6);
652        assert!((back.im - (-2.5)).abs() < 1e-6);
653    }
654
655    #[test]
656    fn state_f32_creation() {
657        let state = QuantumStateF32::new(3).unwrap();
658        assert_eq!(state.num_qubits(), 3);
659        assert_eq!(state.num_amplitudes(), 8);
660
661        let probs = state.probabilities();
662        assert!((probs[0] - 1.0).abs() < 1e-6);
663        for &p in &probs[1..] {
664            assert!(p.abs() < 1e-6);
665        }
666    }
667
668    #[test]
669    fn state_f32_zero_qubits_error() {
670        assert!(QuantumStateF32::new(0).is_err());
671    }
672
673    #[test]
674    fn state_f32_memory_estimate() {
675        // 3 qubits -> 8 amplitudes * 8 bytes = 64 bytes
676        assert_eq!(QuantumStateF32::estimate_memory(3), 64);
677        // 10 qubits -> 1024 amplitudes * 8 bytes = 8192 bytes
678        assert_eq!(QuantumStateF32::estimate_memory(10), 8192);
679    }
680
681    #[test]
682    fn state_f32_h_gate() {
683        let mut state = QuantumStateF32::new_with_seed(1, 42).unwrap();
684        state.apply_gate(&Gate::H(0)).unwrap();
685
686        let probs = state.probabilities();
687        assert!((probs[0] - 0.5).abs() < 1e-5);
688        assert!((probs[1] - 0.5).abs() < 1e-5);
689    }
690
691    #[test]
692    fn state_f32_bell_state() {
693        let mut state = QuantumStateF32::new_with_seed(2, 42).unwrap();
694        state.apply_gate(&Gate::H(0)).unwrap();
695        state.apply_gate(&Gate::CNOT(0, 1)).unwrap();
696
697        let probs = state.probabilities();
698        // Bell state: |00> + |11>, each with probability 0.5
699        assert!((probs[0] - 0.5).abs() < 1e-5);
700        assert!(probs[1].abs() < 1e-5);
701        assert!(probs[2].abs() < 1e-5);
702        assert!((probs[3] - 0.5).abs() < 1e-5);
703    }
704
705    #[test]
706    fn state_f32_measurement() {
707        let mut state = QuantumStateF32::new_with_seed(1, 42).unwrap();
708        state.apply_gate(&Gate::X(0)).unwrap();
709
710        let outcome = state.measure(0).unwrap();
711        assert!(outcome.result); // Must be |1> with certainty
712        assert!((outcome.probability - 1.0).abs() < 1e-5);
713        assert_eq!(state.measurement_record().len(), 1);
714    }
715
716    #[test]
717    fn state_f32_from_f64_roundtrip() {
718        let f64_state = crate::state::QuantumState::new_with_seed(3, 99).unwrap();
719        let f32_state = QuantumStateF32::from_f64(&f64_state);
720        assert_eq!(f32_state.num_qubits(), 3);
721        assert_eq!(f32_state.num_amplitudes(), 8);
722
723        // Upcast back and check probabilities are close.
724        let back = f32_state.to_f64().unwrap();
725        let p_orig = f64_state.probabilities();
726        let p_back = back.probabilities();
727        for (a, b) in p_orig.iter().zip(p_back.iter()) {
728            assert!((a - b).abs() < 1e-6);
729        }
730    }
731
732    #[test]
733    fn state_f32_precision_error_bound() {
734        let mut state = QuantumStateF32::new_with_seed(2, 42).unwrap();
735        assert_eq!(state.precision_error_bound(), 0.0);
736
737        state.apply_gate(&Gate::H(0)).unwrap();
738        state.apply_gate(&Gate::CNOT(0, 1)).unwrap();
739        // 2 gates applied
740        let bound = state.precision_error_bound();
741        assert!(bound > 0.0);
742        assert!(bound < 1e-5); // Should be very small for 2 gates
743    }
744
745    #[test]
746    fn state_f32_invalid_qubit() {
747        let mut state = QuantumStateF32::new(2).unwrap();
748        assert!(state.apply_gate(&Gate::H(5)).is_err());
749    }
750
751    #[test]
752    fn state_f32_distinct_qubits_check() {
753        let mut state = QuantumStateF32::new(2).unwrap();
754        assert!(state.apply_gate(&Gate::CNOT(0, 0)).is_err());
755    }
756}