Skip to main content

scirs2_core/quantum/
qubits.rs

1//! Qubit state representation and multi-qubit register operations.
2//!
3//! This module provides the fundamental quantum state types used throughout
4//! the quantum simulation library:
5//!
6//! - [`Qubit`]: a single-qubit pure state stored as a pair of complex amplitudes.
7//! - [`QubitRegister`]: an n-qubit register stored as a statevector of 2^n amplitudes.
8//!
9//! # Conventions
10//!
11//! States are stored in the **computational basis** |0⟩, |1⟩, …, |2^n-1⟩ ordered
12//! from least-significant qubit to most-significant qubit, i.e. qubit 0 is the
13//! rightmost (fastest-varying) index.
14//!
15//! For a 2-qubit system the basis order is:
16//! ```text
17//! index 0 → |q1=0, q0=0⟩ = |00⟩
18//! index 1 → |q1=0, q0=1⟩ = |01⟩
19//! index 2 → |q1=1, q0=0⟩ = |10⟩
20//! index 3 → |q1=1, q0=1⟩ = |11⟩
21//! ```
22//!
23//! # Examples
24//!
25//! ```rust
26//! use scirs2_core::quantum::qubits::{Qubit, QubitRegister};
27//!
28//! let q0 = Qubit::new_zero();
29//! let q1 = Qubit::new_one();
30//!
31//! // Build a 2-qubit register |0⟩ ⊗ |1⟩ = |01⟩
32//! let r0 = QubitRegister::from_qubit(&q0);
33//! let r1 = QubitRegister::from_qubit(&q1);
34//! let reg = QubitRegister::tensor_product(&r0, &r1);
35//! assert_eq!(reg.n_qubits(), 2);
36//! assert_eq!(reg.dim(), 4);
37//! ```
38
39use num_complex::Complex;
40use rand::{Rng, RngExt};
41use std::f64::consts::PI;
42
43use super::error::{QuantumError, QuantumResult};
44
45/// A single-qubit pure state: |ψ⟩ = α|0⟩ + β|1⟩.
46///
47/// Invariant: |α|² + |β|² = 1 (up to floating-point tolerance).
48#[derive(Debug, Clone, PartialEq)]
49pub struct Qubit {
50    /// Amplitude for |0⟩.
51    pub(crate) alpha: Complex<f64>,
52    /// Amplitude for |1⟩.
53    pub(crate) beta: Complex<f64>,
54}
55
56impl Qubit {
57    /// Construct a qubit from raw amplitudes, normalising automatically.
58    ///
59    /// Returns an error if the norm is zero (unphysical).
60    pub fn new(alpha: Complex<f64>, beta: Complex<f64>) -> QuantumResult<Self> {
61        let norm_sq = alpha.norm_sqr() + beta.norm_sqr();
62        if norm_sq < 1e-15 {
63            return Err(QuantumError::ZeroStateVector);
64        }
65        let norm = norm_sq.sqrt();
66        Ok(Self {
67            alpha: alpha / norm,
68            beta: beta / norm,
69        })
70    }
71
72    /// |0⟩ state.
73    pub fn new_zero() -> Self {
74        Self {
75            alpha: Complex::new(1.0, 0.0),
76            beta: Complex::new(0.0, 0.0),
77        }
78    }
79
80    /// |1⟩ state.
81    pub fn new_one() -> Self {
82        Self {
83            alpha: Complex::new(0.0, 0.0),
84            beta: Complex::new(1.0, 0.0),
85        }
86    }
87
88    /// Bloch-sphere parametrisation: |ψ⟩ = cos(θ/2)|0⟩ + e^{iφ} sin(θ/2)|1⟩.
89    ///
90    /// - `theta` ∈ [0, π]: polar angle.
91    /// - `phi`   ∈ [0, 2π): azimuthal angle.
92    pub fn new_superposition(theta: f64, phi: f64) -> Self {
93        let half = theta / 2.0;
94        Self {
95            alpha: Complex::new(half.cos(), 0.0),
96            beta: Complex::from_polar(half.sin(), phi),
97        }
98    }
99
100    /// Equal superposition: (|0⟩ + |1⟩) / √2.
101    pub fn new_plus() -> Self {
102        let s = 1.0 / 2.0_f64.sqrt();
103        Self {
104            alpha: Complex::new(s, 0.0),
105            beta: Complex::new(s, 0.0),
106        }
107    }
108
109    /// Equal superposition: (|0⟩ − |1⟩) / √2.
110    pub fn new_minus() -> Self {
111        let s = 1.0 / 2.0_f64.sqrt();
112        Self {
113            alpha: Complex::new(s, 0.0),
114            beta: Complex::new(-s, 0.0),
115        }
116    }
117
118    /// Amplitude for |0⟩.
119    pub fn alpha(&self) -> Complex<f64> {
120        self.alpha
121    }
122
123    /// Amplitude for |1⟩.
124    pub fn beta(&self) -> Complex<f64> {
125        self.beta
126    }
127
128    /// Probability of measuring |0⟩.
129    pub fn prob_zero(&self) -> f64 {
130        self.alpha.norm_sqr()
131    }
132
133    /// Probability of measuring |1⟩.
134    pub fn prob_one(&self) -> f64 {
135        self.beta.norm_sqr()
136    }
137
138    /// Check normalisation; returns `true` if within `tol` of 1.
139    pub fn is_normalised(&self, tol: f64) -> bool {
140        ((self.alpha.norm_sqr() + self.beta.norm_sqr()) - 1.0).abs() < tol
141    }
142
143    /// Perform a projective measurement in the computational basis.
144    ///
145    /// Returns `(outcome, post_measurement_state)` where `outcome` is 0 or 1
146    /// and the post-measurement state has collapsed to the corresponding basis state.
147    ///
148    /// Uses `rng` to sample the Born-rule distribution.
149    pub fn measure<R: Rng>(&self, rng: &mut R) -> (u8, Qubit) {
150        let p0 = self.prob_zero();
151        let sample: f64 = rng.random();
152        if sample < p0 {
153            (0, Qubit::new_zero())
154        } else {
155            (1, Qubit::new_one())
156        }
157    }
158
159    /// Bloch-sphere angles (theta, phi) for this qubit.
160    ///
161    /// Returns `(theta, phi)` where θ ∈ [0, π] and φ ∈ [0, 2π).
162    pub fn bloch_angles(&self) -> (f64, f64) {
163        let theta = 2.0 * self.alpha.norm().acos().min(PI);
164        let phi = {
165            let raw = self.beta.arg() - self.alpha.arg();
166            raw.rem_euclid(2.0 * PI)
167        };
168        (theta, phi)
169    }
170
171    /// Convert this single-qubit state into a [`QubitRegister`].
172    pub fn to_register(&self) -> QubitRegister {
173        QubitRegister {
174            amplitudes: vec![self.alpha, self.beta],
175            n_qubits: 1,
176        }
177    }
178}
179
180impl Default for Qubit {
181    fn default() -> Self {
182        Self::new_zero()
183    }
184}
185
186impl std::fmt::Display for Qubit {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        write!(
189            f,
190            "({:.6} + {:.6}i)|0⟩ + ({:.6} + {:.6}i)|1⟩",
191            self.alpha.re, self.alpha.im, self.beta.re, self.beta.im
192        )
193    }
194}
195
196// ─────────────────────────────────────────────────────────────────────────────
197// QubitRegister
198// ─────────────────────────────────────────────────────────────────────────────
199
200/// An n-qubit quantum register stored as a 2^n-dimensional statevector.
201///
202/// The state is always kept normalised.  Amplitudes are ordered by the binary
203/// representation of the basis index with qubit 0 as the least-significant bit.
204#[derive(Debug, Clone, PartialEq)]
205pub struct QubitRegister {
206    /// Statevector amplitudes — length must equal 2^n_qubits.
207    pub(crate) amplitudes: Vec<Complex<f64>>,
208    /// Number of qubits.
209    pub(crate) n_qubits: usize,
210}
211
212impl QubitRegister {
213    // ── Constructors ─────────────────────────────────────────────────────────
214
215    /// Create a register from a raw amplitude vector.
216    ///
217    /// `n_qubits` must satisfy `amplitudes.len() == 2^n_qubits`.
218    /// The vector is automatically re-normalised.
219    pub fn new(n_qubits: usize, amplitudes: Vec<Complex<f64>>) -> QuantumResult<Self> {
220        let expected_dim = 1usize
221            .checked_shl(n_qubits as u32)
222            .ok_or(QuantumError::TooManyQubits(n_qubits))?;
223        if amplitudes.len() != expected_dim {
224            return Err(QuantumError::DimensionMismatch {
225                expected: expected_dim,
226                actual: amplitudes.len(),
227            });
228        }
229        let norm_sq: f64 = amplitudes.iter().map(|a| a.norm_sqr()).sum();
230        if norm_sq < 1e-15 {
231            return Err(QuantumError::ZeroStateVector);
232        }
233        let norm = norm_sq.sqrt();
234        let normalised = amplitudes.iter().map(|a| a / norm).collect();
235        Ok(Self {
236            amplitudes: normalised,
237            n_qubits,
238        })
239    }
240
241    /// Wrap a single `Qubit` as a 1-qubit register.
242    pub fn from_qubit(q: &Qubit) -> Self {
243        Self {
244            amplitudes: vec![q.alpha, q.beta],
245            n_qubits: 1,
246        }
247    }
248
249    /// All-zeros state |0…0⟩.
250    pub fn new_zero_state(n_qubits: usize) -> QuantumResult<Self> {
251        if n_qubits == 0 {
252            return Err(QuantumError::InvalidQubitCount(n_qubits));
253        }
254        let dim = 1usize
255            .checked_shl(n_qubits as u32)
256            .ok_or(QuantumError::TooManyQubits(n_qubits))?;
257        let mut amps = vec![Complex::new(0.0, 0.0); dim];
258        amps[0] = Complex::new(1.0, 0.0);
259        Ok(Self {
260            amplitudes: amps,
261            n_qubits,
262        })
263    }
264
265    /// Equal superposition (Hadamard applied to all qubits of |0…0⟩).
266    ///
267    /// Each amplitude has magnitude 1/√(2^n).
268    pub fn new_uniform_superposition(n_qubits: usize) -> QuantumResult<Self> {
269        if n_qubits == 0 {
270            return Err(QuantumError::InvalidQubitCount(n_qubits));
271        }
272        let dim = 1usize
273            .checked_shl(n_qubits as u32)
274            .ok_or(QuantumError::TooManyQubits(n_qubits))?;
275        let amp = Complex::new(1.0 / (dim as f64).sqrt(), 0.0);
276        Ok(Self {
277            amplitudes: vec![amp; dim],
278            n_qubits,
279        })
280    }
281
282    /// Computational basis state |k⟩ for a given integer `k` < 2^n.
283    pub fn new_basis_state(n_qubits: usize, k: usize) -> QuantumResult<Self> {
284        if n_qubits == 0 {
285            return Err(QuantumError::InvalidQubitCount(n_qubits));
286        }
287        let dim = 1usize
288            .checked_shl(n_qubits as u32)
289            .ok_or(QuantumError::TooManyQubits(n_qubits))?;
290        if k >= dim {
291            return Err(QuantumError::BasisIndexOutOfRange { index: k, dim });
292        }
293        let mut amps = vec![Complex::new(0.0, 0.0); dim];
294        amps[k] = Complex::new(1.0, 0.0);
295        Ok(Self {
296            amplitudes: amps,
297            n_qubits,
298        })
299    }
300
301    // ── Accessors ─────────────────────────────────────────────────────────────
302
303    /// Number of qubits in this register.
304    pub fn n_qubits(&self) -> usize {
305        self.n_qubits
306    }
307
308    /// Hilbert-space dimension: 2^n.
309    pub fn dim(&self) -> usize {
310        self.amplitudes.len()
311    }
312
313    /// Slice over all amplitudes.
314    pub fn amplitudes(&self) -> &[Complex<f64>] {
315        &self.amplitudes
316    }
317
318    /// Mutable slice over all amplitudes (use with care — normalisation is not
319    /// automatically maintained).
320    pub fn amplitudes_mut(&mut self) -> &mut Vec<Complex<f64>> {
321        &mut self.amplitudes
322    }
323
324    /// Amplitude for basis state `k`.
325    pub fn amplitude(&self, k: usize) -> QuantumResult<Complex<f64>> {
326        self.amplitudes
327            .get(k)
328            .copied()
329            .ok_or(QuantumError::BasisIndexOutOfRange {
330                index: k,
331                dim: self.dim(),
332            })
333    }
334
335    /// Probability of measuring basis state `k`.
336    pub fn probability(&self, k: usize) -> QuantumResult<f64> {
337        Ok(self.amplitude(k)?.norm_sqr())
338    }
339
340    /// All measurement probabilities |ψ_k|² in basis order.
341    pub fn probabilities(&self) -> Vec<f64> {
342        self.amplitudes.iter().map(|a| a.norm_sqr()).collect()
343    }
344
345    /// Re-normalise the statevector in place.
346    pub fn normalise(&mut self) -> QuantumResult<()> {
347        let norm_sq: f64 = self.amplitudes.iter().map(|a| a.norm_sqr()).sum();
348        if norm_sq < 1e-15 {
349            return Err(QuantumError::ZeroStateVector);
350        }
351        let norm = norm_sq.sqrt();
352        for a in &mut self.amplitudes {
353            *a /= norm;
354        }
355        Ok(())
356    }
357
358    /// Check that the statevector is normalised within `tol`.
359    pub fn is_normalised(&self, tol: f64) -> bool {
360        let norm_sq: f64 = self.amplitudes.iter().map(|a| a.norm_sqr()).sum();
361        (norm_sq - 1.0).abs() < tol
362    }
363
364    // ── Operations ───────────────────────────────────────────────────────────
365
366    /// Tensor product (Kronecker product) of two registers: result = a ⊗ b.
367    ///
368    /// If `a` has n₁ qubits and `b` has n₂ qubits, the result has n₁+n₂ qubits.
369    /// Qubit ordering: qubits of `a` are the *more* significant bits.
370    pub fn tensor_product(a: &QubitRegister, b: &QubitRegister) -> QubitRegister {
371        let n = a.n_qubits + b.n_qubits;
372        let mut amps = Vec::with_capacity(a.dim() * b.dim());
373        for &amp_a in &a.amplitudes {
374            for &amp_b in &b.amplitudes {
375                amps.push(amp_a * amp_b);
376            }
377        }
378        QubitRegister {
379            amplitudes: amps,
380            n_qubits: n,
381        }
382    }
383
384    /// Measure a single qubit at index `qubit_idx` and return `(outcome, collapsed_state)`.
385    ///
386    /// The returned register has the same number of qubits; amplitudes inconsistent
387    /// with the measurement outcome are zeroed and the result is renormalised.
388    pub fn measure_qubit<R: Rng>(
389        &self,
390        qubit_idx: usize,
391        rng: &mut R,
392    ) -> QuantumResult<(u8, QubitRegister)> {
393        if qubit_idx >= self.n_qubits {
394            return Err(QuantumError::QubitIndexOutOfRange {
395                index: qubit_idx,
396                n_qubits: self.n_qubits,
397            });
398        }
399
400        // Compute probability of measuring |1⟩ on this qubit.
401        let mut prob_one: f64 = 0.0;
402        for (k, amp) in self.amplitudes.iter().enumerate() {
403            if (k >> qubit_idx) & 1 == 1 {
404                prob_one += amp.norm_sqr();
405            }
406        }
407        let sample: f64 = rng.random();
408        let outcome: u8 = if sample < prob_one { 1 } else { 0 };
409
410        // Project and renormalise.
411        let mut new_amps = self.amplitudes.clone();
412        for (k, amp) in new_amps.iter_mut().enumerate() {
413            let bit = ((k >> qubit_idx) & 1) as u8;
414            if bit != outcome {
415                *amp = Complex::new(0.0, 0.0);
416            }
417        }
418        let mut collapsed = QubitRegister {
419            amplitudes: new_amps,
420            n_qubits: self.n_qubits,
421        };
422        collapsed.normalise()?;
423        Ok((outcome, collapsed))
424    }
425
426    /// Measure all qubits and return a bit-string outcome (qubit 0 first).
427    ///
428    /// Samples once from the Born-rule distribution.
429    pub fn measure_all<R: Rng>(&self, rng: &mut R) -> Vec<u8> {
430        // Build CDF and sample.
431        let probs = self.probabilities();
432        let sample: f64 = rng.random();
433        let mut cumulative = 0.0;
434        let mut outcome_index = probs.len().saturating_sub(1);
435        for (i, &p) in probs.iter().enumerate() {
436            cumulative += p;
437            if sample < cumulative {
438                outcome_index = i;
439                break;
440            }
441        }
442        // Decode basis index into bits (qubit 0 = LSB).
443        (0..self.n_qubits)
444            .map(|q| ((outcome_index >> q) & 1) as u8)
445            .collect()
446    }
447
448    /// Inner product ⟨other|self⟩.
449    pub fn inner_product(&self, other: &QubitRegister) -> QuantumResult<Complex<f64>> {
450        if self.n_qubits != other.n_qubits {
451            return Err(QuantumError::DimensionMismatch {
452                expected: self.dim(),
453                actual: other.dim(),
454            });
455        }
456        let ip = self
457            .amplitudes
458            .iter()
459            .zip(other.amplitudes.iter())
460            .map(|(a, b)| b.conj() * a)
461            .sum();
462        Ok(ip)
463    }
464
465    /// Fidelity |⟨other|self⟩|² with another register.
466    pub fn fidelity(&self, other: &QubitRegister) -> QuantumResult<f64> {
467        let ip = self.inner_product(other)?;
468        Ok(ip.norm_sqr())
469    }
470
471    /// Von-Neumann entropy (in nats) of the full pure state (always 0 for pure states).
472    /// Provided for completeness; useful as a sanity-check (should return 0.0).
473    pub fn entropy(&self) -> f64 {
474        let probs = self.probabilities();
475        -probs
476            .iter()
477            .filter(|&&p| p > 1e-15)
478            .map(|&p| p * p.ln())
479            .sum::<f64>()
480    }
481}
482
483impl Default for QubitRegister {
484    fn default() -> Self {
485        // 1-qubit |0⟩ state
486        Self {
487            amplitudes: vec![Complex::new(1.0, 0.0), Complex::new(0.0, 0.0)],
488            n_qubits: 1,
489        }
490    }
491}
492
493impl std::fmt::Display for QubitRegister {
494    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
495        write!(f, "QubitRegister({} qubits) [", self.n_qubits)?;
496        for (i, amp) in self.amplitudes.iter().enumerate() {
497            if i > 0 {
498                write!(f, ", ")?;
499            }
500            if amp.im >= 0.0 {
501                write!(
502                    f,
503                    "|{:0>width$b}⟩: {:.4}+{:.4}i",
504                    i,
505                    amp.re,
506                    amp.im,
507                    width = self.n_qubits
508                )?;
509            } else {
510                write!(
511                    f,
512                    "|{:0>width$b}⟩: {:.4}{:.4}i",
513                    i,
514                    amp.re,
515                    amp.im,
516                    width = self.n_qubits
517                )?;
518            }
519        }
520        write!(f, "]")
521    }
522}
523
524// ─────────────────────────────────────────────────────────────────────────────
525// Tests
526// ─────────────────────────────────────────────────────────────────────────────
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531    use rand::SeedableRng;
532    use rand_chacha::ChaCha20Rng;
533
534    const TOL: f64 = 1e-12;
535
536    #[test]
537    fn test_qubit_zero_normalised() {
538        let q = Qubit::new_zero();
539        assert!(q.is_normalised(TOL));
540        assert!((q.prob_zero() - 1.0).abs() < TOL);
541        assert!(q.prob_one().abs() < TOL);
542    }
543
544    #[test]
545    fn test_qubit_one_normalised() {
546        let q = Qubit::new_one();
547        assert!(q.is_normalised(TOL));
548        assert!(q.prob_zero().abs() < TOL);
549        assert!((q.prob_one() - 1.0).abs() < TOL);
550    }
551
552    #[test]
553    fn test_qubit_superposition() {
554        let q = Qubit::new_plus();
555        assert!(q.is_normalised(TOL));
556        assert!((q.prob_zero() - 0.5).abs() < TOL);
557        assert!((q.prob_one() - 0.5).abs() < TOL);
558    }
559
560    #[test]
561    fn test_bloch_sphere_zero() {
562        let q = Qubit::new_superposition(0.0, 0.0);
563        // theta=0 → |0⟩
564        assert!((q.prob_zero() - 1.0).abs() < TOL);
565    }
566
567    #[test]
568    fn test_bloch_sphere_one() {
569        let q = Qubit::new_superposition(std::f64::consts::PI, 0.0);
570        // theta=π → |1⟩
571        assert!((q.prob_one() - 1.0).abs() < 1e-10);
572    }
573
574    #[test]
575    fn test_qubit_measure_deterministic_zero() {
576        let q = Qubit::new_zero();
577        let mut rng = ChaCha20Rng::seed_from_u64(0);
578        let (outcome, post) = q.measure(&mut rng);
579        assert_eq!(outcome, 0);
580        assert!(post.is_normalised(TOL));
581        assert!((post.prob_zero() - 1.0).abs() < TOL);
582    }
583
584    #[test]
585    fn test_qubit_measure_deterministic_one() {
586        let q = Qubit::new_one();
587        let mut rng = ChaCha20Rng::seed_from_u64(0);
588        let (outcome, post) = q.measure(&mut rng);
589        assert_eq!(outcome, 1);
590        assert!(post.is_normalised(TOL));
591    }
592
593    #[test]
594    fn test_register_zero_state() {
595        let reg = QubitRegister::new_zero_state(3).expect("valid");
596        assert_eq!(reg.n_qubits(), 3);
597        assert_eq!(reg.dim(), 8);
598        assert!((reg.probability(0).expect("ok") - 1.0).abs() < TOL);
599    }
600
601    #[test]
602    fn test_register_uniform_superposition() {
603        let reg = QubitRegister::new_uniform_superposition(2).expect("valid");
604        let p = reg.probability(0).expect("ok");
605        assert!((p - 0.25).abs() < TOL);
606    }
607
608    #[test]
609    fn test_tensor_product_dims() {
610        let r1 = QubitRegister::new_zero_state(2).expect("valid");
611        let r2 = QubitRegister::new_zero_state(3).expect("valid");
612        let combined = QubitRegister::tensor_product(&r1, &r2);
613        assert_eq!(combined.n_qubits(), 5);
614        assert_eq!(combined.dim(), 32);
615    }
616
617    #[test]
618    fn test_measure_all_basis_state() {
619        let reg = QubitRegister::new_basis_state(3, 5).expect("valid");
620        let mut rng = ChaCha20Rng::seed_from_u64(42);
621        let bits = reg.measure_all(&mut rng);
622        // basis index 5 = 101 in binary → q0=1, q1=0, q2=1
623        assert_eq!(bits, vec![1, 0, 1]);
624    }
625
626    #[test]
627    fn test_fidelity_same_state() {
628        let r = QubitRegister::new_zero_state(2).expect("valid");
629        let f = r.fidelity(&r).expect("ok");
630        assert!((f - 1.0).abs() < TOL);
631    }
632
633    #[test]
634    fn test_fidelity_orthogonal() {
635        let r0 = QubitRegister::new_basis_state(1, 0).expect("valid");
636        let r1 = QubitRegister::new_basis_state(1, 1).expect("valid");
637        let f = r0.fidelity(&r1).expect("ok");
638        assert!(f.abs() < TOL);
639    }
640}