Skip to main content

oxicuda_quantum/tensor/
contraction.rs

1//! Tensor-network contraction back-end for shallow, wide quantum circuits.
2//!
3//! For circuits that are *shallow* (low depth) but *wide* (many qubits), the
4//! full `2^n` state vector is intractable, yet individual output amplitudes
5//! `⟨x| U |0…0⟩` can be evaluated by contracting the circuit's tensor network.
6//! Each single-qubit gate is a rank-2 tensor, each two-qubit gate a rank-4
7//! tensor, and the contraction follows the wire connectivity. This file provides
8//!
9//! * [`Tensor`] — a dense complex tensor with named [`Index`] legs and a general
10//!   pairwise [`Tensor::contract`] (a `tensordot` over all shared legs), plus an
11//!   outer product and a scalar extractor;
12//! * [`TensorNetwork`] — an ordered collection of tensors contracted greedily;
13//! * [`amplitude`] — builds the network for a gate list and returns
14//!   `⟨bitstring| U |0…0⟩`.
15//!
16//! The contraction is *exact*: for any circuit it returns the same amplitude as
17//! the dense state-vector simulator, but the per-amplitude cost scales with the
18//! treewidth of the circuit graph rather than `2^n`, which is the advantage for
19//! shallow wide circuits where only a few amplitudes (or marginals) are needed.
20
21use num_complex::Complex;
22
23use crate::error::{QuantumError, QuantumResult};
24
25type Complex32 = Complex<f32>;
26
27/// A unique label for a tensor leg (a qubit "wire" at a given time slice).
28///
29/// Two tensors are contracted over every [`Index`] they share.
30pub type Index = u32;
31
32/// A dense complex tensor with named legs.
33///
34/// `legs[d]` is the [`Index`] of dimension `d`; every leg has extent 2 (a qubit
35/// wire). Data is stored row-major in leg order: the flat offset of a multi-index
36/// `(i_0, …, i_{r-1})` is `Σ_d i_d · 2^{r-1-d}`.
37#[derive(Debug, Clone)]
38pub struct Tensor {
39    /// Names of the legs, in storage order.
40    pub legs: Vec<Index>,
41    /// Dense amplitudes, length `2^legs.len()`.
42    pub data: Vec<Complex32>,
43}
44
45impl Tensor {
46    /// Build a tensor from explicit legs and data, validating the length.
47    ///
48    /// # Errors
49    /// Returns [`QuantumError::DimensionMismatch`] if `data.len() != 2^legs.len()`.
50    pub fn new(legs: Vec<Index>, data: Vec<Complex32>) -> QuantumResult<Self> {
51        let expected = 1usize << legs.len();
52        if data.len() != expected {
53            return Err(QuantumError::DimensionMismatch {
54                expected,
55                got: data.len(),
56            });
57        }
58        Ok(Self { legs, data })
59    }
60
61    /// Rank-0 scalar tensor.
62    #[must_use]
63    pub fn scalar(value: Complex32) -> Self {
64        Self {
65            legs: Vec::new(),
66            data: vec![value],
67        }
68    }
69
70    /// The single-qubit `|0⟩` state vector as a rank-1 tensor on leg `wire`.
71    #[must_use]
72    pub fn ket_zero(wire: Index) -> Self {
73        Self {
74            legs: vec![wire],
75            data: vec![Complex32::new(1.0, 0.0), Complex32::new(0.0, 0.0)],
76        }
77    }
78
79    /// The single-qubit basis bra `⟨b|` as a rank-1 tensor on leg `wire`.
80    #[must_use]
81    pub fn bra_basis(wire: Index, bit: bool) -> Self {
82        let data = if bit {
83            vec![Complex32::new(0.0, 0.0), Complex32::new(1.0, 0.0)]
84        } else {
85            vec![Complex32::new(1.0, 0.0), Complex32::new(0.0, 0.0)]
86        };
87        Self {
88            legs: vec![wire],
89            data,
90        }
91    }
92
93    /// A single-qubit gate tensor `out·in` with legs `[out_wire, in_wire]`.
94    ///
95    /// `m[r][c]` is the matrix entry `⟨r|G|c⟩`; the `out_wire` leg carries the
96    /// row index, the `in_wire` leg the column index.
97    #[must_use]
98    pub fn gate_1q(out_wire: Index, in_wire: Index, m: &[[Complex32; 2]; 2]) -> Self {
99        // Storage order [out, in] → offset = out*2 + in.
100        let data = vec![m[0][0], m[0][1], m[1][0], m[1][1]];
101        Self {
102            legs: vec![out_wire, in_wire],
103            data,
104        }
105    }
106
107    /// A two-qubit gate tensor with legs `[out0, out1, in0, in1]`.
108    ///
109    /// `m` is the `4 × 4` matrix in row-major form using the convention that the
110    /// composite index is `2*q0 + q1` (qubit 0 most significant), i.e. the same
111    /// ordering used by the dense two-qubit application path.
112    #[must_use]
113    pub fn gate_2q(
114        out0: Index,
115        out1: Index,
116        in0: Index,
117        in1: Index,
118        m: &[[Complex32; 4]; 4],
119    ) -> Self {
120        // Legs [out0,out1,in0,in1]; flat offset = ((out0*2+out1)*2+in0)*2+in1.
121        // For two qubits the composite index already equals out0*2+out1 (and
122        // in0*2+in1), so the leg layout coincides with the matrix layout and the
123        // matrix can be copied row-major directly.
124        let mut data = vec![Complex32::new(0.0, 0.0); 16];
125        for (o, row) in m.iter().enumerate() {
126            for (i, &entry) in row.iter().enumerate() {
127                data[o * 4 + i] = entry;
128            }
129        }
130        Self {
131            legs: vec![out0, out1, in0, in1],
132            data,
133        }
134    }
135
136    /// Rank of the tensor (number of legs).
137    #[must_use]
138    pub fn rank(&self) -> usize {
139        self.legs.len()
140    }
141
142    /// Extract the scalar value of a rank-0 tensor.
143    ///
144    /// # Errors
145    /// Returns [`QuantumError::Internal`] if the tensor is not rank-0.
146    pub fn as_scalar(&self) -> QuantumResult<Complex32> {
147        if self.legs.is_empty() && self.data.len() == 1 {
148            Ok(self.data[0])
149        } else {
150            Err(QuantumError::Internal {
151                msg: format!("tensor is rank {}, not a scalar", self.rank()),
152            })
153        }
154    }
155
156    /// Contract `self` with `other` over **all shared legs** (a `tensordot`).
157    ///
158    /// The resulting tensor's legs are `self`'s remaining legs followed by
159    /// `other`'s remaining legs, both in their original order. Shared legs are
160    /// summed over. With no shared legs this reduces to an outer product.
161    ///
162    /// # Errors
163    /// Currently infallible for well-formed inputs; returns a `Result` for
164    /// interface uniformity and future fallibility.
165    pub fn contract(&self, other: &Self) -> QuantumResult<Self> {
166        // Partition legs into shared / free for each operand.
167        let mut shared: Vec<Index> = Vec::new();
168        for &l in &self.legs {
169            if other.legs.contains(&l) {
170                shared.push(l);
171            }
172        }
173        let a_free: Vec<Index> = self
174            .legs
175            .iter()
176            .copied()
177            .filter(|l| !shared.contains(l))
178            .collect();
179        let b_free: Vec<Index> = other
180            .legs
181            .iter()
182            .copied()
183            .filter(|l| !shared.contains(l))
184            .collect();
185
186        let s = shared.len();
187        let af = a_free.len();
188        let bf = b_free.len();
189
190        // Precompute, for each operand, the bit position (in its flat layout) of
191        // every shared and free leg, so we can assemble flat offsets quickly.
192        let a_pos = leg_positions(&self.legs);
193        let b_pos = leg_positions(&other.legs);
194
195        let mut out_data = vec![Complex32::new(0.0, 0.0); 1usize << (af + bf)];
196
197        // Iterate over all combinations of (free_a, free_b, shared) bit patterns.
198        let n_af = 1usize << af;
199        let n_bf = 1usize << bf;
200        let n_s = 1usize << s;
201
202        for fa in 0..n_af {
203            for fb in 0..n_bf {
204                let mut acc = Complex32::new(0.0, 0.0);
205                for sv in 0..n_s {
206                    // Build A's flat index.
207                    let mut a_idx = 0usize;
208                    for (d, leg) in a_free.iter().enumerate() {
209                        let bit = (fa >> (af - 1 - d)) & 1;
210                        a_idx |= bit << (self.legs.len() - 1 - a_pos[leg]);
211                    }
212                    for (d, leg) in shared.iter().enumerate() {
213                        let bit = (sv >> (s - 1 - d)) & 1;
214                        a_idx |= bit << (self.legs.len() - 1 - a_pos[leg]);
215                    }
216                    // Build B's flat index.
217                    let mut b_idx = 0usize;
218                    for (d, leg) in b_free.iter().enumerate() {
219                        let bit = (fb >> (bf - 1 - d)) & 1;
220                        b_idx |= bit << (other.legs.len() - 1 - b_pos[leg]);
221                    }
222                    for (d, leg) in shared.iter().enumerate() {
223                        let bit = (sv >> (s - 1 - d)) & 1;
224                        b_idx |= bit << (other.legs.len() - 1 - b_pos[leg]);
225                    }
226                    acc += self.data[a_idx] * other.data[b_idx];
227                }
228                let out_idx = (fa << bf) | fb;
229                out_data[out_idx] = acc;
230            }
231        }
232
233        let mut out_legs = a_free;
234        out_legs.extend(b_free);
235        Ok(Self {
236            legs: out_legs,
237            data: out_data,
238        })
239    }
240}
241
242/// Map each leg name to its position in a leg list.
243fn leg_positions(legs: &[Index]) -> std::collections::HashMap<Index, usize> {
244    legs.iter().enumerate().map(|(i, &l)| (l, i)).collect()
245}
246
247/// An unordered bag of tensors to be contracted into a scalar.
248#[derive(Debug, Clone, Default)]
249pub struct TensorNetwork {
250    pub tensors: Vec<Tensor>,
251}
252
253impl TensorNetwork {
254    /// Empty network.
255    #[must_use]
256    pub fn new() -> Self {
257        Self {
258            tensors: Vec::new(),
259        }
260    }
261
262    /// Add a tensor to the network.
263    pub fn push(&mut self, t: Tensor) {
264        self.tensors.push(t);
265    }
266
267    /// Greedily contract the whole network and return the resulting tensor.
268    ///
269    /// The contraction order is a simple greedy heuristic: repeatedly contract
270    /// the pair of tensors sharing the most legs (falling back to the first pair
271    /// when none share a leg, producing an outer product). For shallow circuits
272    /// this keeps intermediate ranks small.
273    ///
274    /// # Errors
275    /// Returns [`QuantumError::EmptyInput`] if the network is empty.
276    pub fn contract_all(&self) -> QuantumResult<Tensor> {
277        if self.tensors.is_empty() {
278            return Err(QuantumError::EmptyInput);
279        }
280        let mut work: Vec<Tensor> = self.tensors.clone();
281        while work.len() > 1 {
282            // Find the pair with the most shared legs.
283            let mut best = (0usize, 1usize);
284            let mut best_shared = -1i64;
285            for i in 0..work.len() {
286                for j in (i + 1)..work.len() {
287                    let shared = work[i]
288                        .legs
289                        .iter()
290                        .filter(|l| work[j].legs.contains(l))
291                        .count() as i64;
292                    if shared > best_shared {
293                        best_shared = shared;
294                        best = (i, j);
295                    }
296                }
297            }
298            let (i, j) = best;
299            // Remove j first (higher index) then i to keep indices valid.
300            let tj = work.remove(j);
301            let ti = work.remove(i);
302            let merged = ti.contract(&tj)?;
303            work.push(merged);
304        }
305        Ok(work.into_iter().next().unwrap_or_else(|| {
306            // Unreachable: loop guarantees exactly one element, but avoid panic.
307            Tensor::scalar(Complex32::new(0.0, 0.0))
308        }))
309    }
310
311    /// Contract to a scalar (the full amplitude).
312    ///
313    /// # Errors
314    /// Returns an error if the final tensor is not rank-0.
315    pub fn contract_to_scalar(&self) -> QuantumResult<Complex32> {
316        self.contract_all()?.as_scalar()
317    }
318}
319
320/// A circuit gate for the tensor-network amplitude evaluator.
321#[derive(Debug, Clone)]
322pub enum TnGate {
323    /// Single-qubit gate `m` on `qubit`.
324    OneQ {
325        qubit: usize,
326        m: [[Complex32; 2]; 2],
327    },
328    /// Two-qubit gate `m` (4×4, composite index `2*q0+q1`) on `(q0, q1)`.
329    TwoQ {
330        q0: usize,
331        q1: usize,
332        m: [[Complex32; 4]; 4],
333    },
334}
335
336/// Compute the amplitude `⟨bitstring| U |0…0⟩` for an `n_qubits` circuit by
337/// tensor-network contraction.
338///
339/// `bitstring[q]` is the desired measured value of qubit `q` (little-endian,
340/// matching the state-vector index convention). Wires are time-sliced: each gate
341/// allocates fresh output [`Index`] legs so the network is a DAG with no leg
342/// reuse, then `ket_zero` caps the inputs and `bra_basis` caps the outputs.
343///
344/// # Errors
345/// Returns an error for an invalid qubit count, a `bitstring` of the wrong
346/// length, or an out-of-range qubit in any gate.
347pub fn amplitude(
348    n_qubits: usize,
349    gates: &[TnGate],
350    bitstring: &[bool],
351) -> QuantumResult<Complex32> {
352    if n_qubits == 0 || n_qubits > 30 {
353        return Err(QuantumError::InvalidQubitCount { n: n_qubits });
354    }
355    if bitstring.len() != n_qubits {
356        return Err(QuantumError::DimensionMismatch {
357            expected: n_qubits,
358            got: bitstring.len(),
359        });
360    }
361
362    let mut net = TensorNetwork::new();
363    // Fresh-leg allocator.
364    let mut next_index: Index = 0;
365    let mut fresh = || {
366        let i = next_index;
367        next_index += 1;
368        i
369    };
370
371    // current_wire[q] = the leg currently dangling on qubit q's timeline.
372    let mut current_wire: Vec<Index> = (0..n_qubits).map(|_| fresh()).collect();
373    // Cap each qubit's input with |0⟩.
374    for &w in &current_wire {
375        net.push(Tensor::ket_zero(w));
376    }
377
378    for g in gates {
379        match g {
380            TnGate::OneQ { qubit, m } => {
381                if *qubit >= n_qubits {
382                    return Err(QuantumError::QubitIndexOutOfRange {
383                        index: *qubit,
384                        n_qubits,
385                    });
386                }
387                let in_w = current_wire[*qubit];
388                let out_w = fresh();
389                net.push(Tensor::gate_1q(out_w, in_w, m));
390                current_wire[*qubit] = out_w;
391            }
392            TnGate::TwoQ { q0, q1, m } => {
393                if *q0 >= n_qubits || *q1 >= n_qubits || q0 == q1 {
394                    return Err(QuantumError::InvalidParameter {
395                        name: "two-qubit gate indices".into(),
396                    });
397                }
398                let in0 = current_wire[*q0];
399                let in1 = current_wire[*q1];
400                let out0 = fresh();
401                let out1 = fresh();
402                net.push(Tensor::gate_2q(out0, out1, in0, in1, m));
403                current_wire[*q0] = out0;
404                current_wire[*q1] = out1;
405            }
406        }
407    }
408
409    // Cap each qubit's output with ⟨bitstring[q]|.
410    for (q, &w) in current_wire.iter().enumerate() {
411        net.push(Tensor::bra_basis(w, bitstring[q]));
412    }
413
414    net.contract_to_scalar()
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420    use crate::gates::controlled::apply_cnot;
421    use crate::gates::hadamard::gate_h;
422    use crate::gates::pauli::gate_x;
423    use crate::handle::LcgRng;
424    use crate::statevec::apply_1q::apply_1q_inplace;
425    use crate::statevec::state::StateVector;
426
427    fn h_mat() -> [[Complex32; 2]; 2] {
428        gate_h()
429    }
430
431    fn cnot_mat() -> [[Complex32; 4]; 4] {
432        // CNOT with q0 as control, q1 as target, composite index 2*q0+q1.
433        let z = Complex32::new(0.0, 0.0);
434        let o = Complex32::new(1.0, 0.0);
435        [[o, z, z, z], [z, o, z, z], [z, z, z, o], [z, z, o, z]]
436    }
437
438    #[test]
439    fn scalar_contract_inner_product() {
440        // ⟨0|0⟩ = 1.
441        let ket = Tensor::ket_zero(0);
442        let bra = Tensor::bra_basis(0, false);
443        let s = bra
444            .contract(&ket)
445            .expect("contract")
446            .as_scalar()
447            .expect("scalar");
448        assert!((s.re - 1.0).abs() < 1e-6 && s.im.abs() < 1e-6);
449        // ⟨1|0⟩ = 0.
450        let bra1 = Tensor::bra_basis(0, true);
451        let s2 = bra1
452            .contract(&Tensor::ket_zero(0))
453            .expect("c")
454            .as_scalar()
455            .expect("s");
456        assert!(s2.norm() < 1e-6);
457    }
458
459    #[test]
460    fn single_hadamard_amplitudes() {
461        // H|0⟩ = (|0⟩ + |1⟩)/√2.
462        let gates = vec![TnGate::OneQ {
463            qubit: 0,
464            m: h_mat(),
465        }];
466        let a0 = amplitude(1, &gates, &[false]).expect("amp0");
467        let a1 = amplitude(1, &gates, &[true]).expect("amp1");
468        let inv_sqrt2 = std::f32::consts::FRAC_1_SQRT_2;
469        assert!((a0.re - inv_sqrt2).abs() < 1e-5, "a0={a0:?}");
470        assert!((a1.re - inv_sqrt2).abs() < 1e-5, "a1={a1:?}");
471    }
472
473    #[test]
474    fn bell_state_amplitudes_match_statevector() {
475        // H on q0, CNOT(0→1): Bell state. Compare all 4 amplitudes to statevec.
476        let gates = vec![
477            TnGate::OneQ {
478                qubit: 0,
479                m: h_mat(),
480            },
481            TnGate::TwoQ {
482                q0: 0,
483                q1: 1,
484                m: cnot_mat(),
485            },
486        ];
487
488        // Reference state vector.
489        let mut sv = StateVector::new_zero_state(2).expect("2q");
490        apply_1q_inplace(&mut sv, 0, &gate_h()).expect("H");
491        apply_cnot(&mut sv, 0, 1).expect("cnot");
492
493        for idx in 0..4usize {
494            let b0 = (idx & 1) != 0; // qubit 0 is bit 0 (little-endian)
495            let b1 = (idx & 2) != 0;
496            let amp = amplitude(2, &gates, &[b0, b1]).expect("amp");
497            let reference = sv.amps[idx];
498            assert!(
499                (amp.re - reference.re).abs() < 1e-5 && (amp.im - reference.im).abs() < 1e-5,
500                "idx={idx}: tn={amp:?} sv={reference:?}"
501            );
502        }
503    }
504
505    #[test]
506    fn deep_random_circuit_matches_statevector() {
507        // 3-qubit moderately deep circuit; check a few amplitudes exactly.
508        let mut rng = LcgRng::new(123);
509        let mut gates: Vec<TnGate> = Vec::new();
510        let mut sv = StateVector::new_zero_state(3).expect("3q");
511
512        for _ in 0..6 {
513            // Random single-qubit RY-like gates on each qubit.
514            for q in 0..3usize {
515                let theta = rng.next_f32() * std::f32::consts::TAU;
516                let c = (theta * 0.5).cos();
517                let s = (theta * 0.5).sin();
518                let m = [
519                    [Complex32::new(c, 0.0), Complex32::new(-s, 0.0)],
520                    [Complex32::new(s, 0.0), Complex32::new(c, 0.0)],
521                ];
522                gates.push(TnGate::OneQ { qubit: q, m });
523                apply_1q_inplace(&mut sv, q, &m).expect("apply ry");
524            }
525            // A CNOT ladder.
526            gates.push(TnGate::TwoQ {
527                q0: 0,
528                q1: 1,
529                m: cnot_mat(),
530            });
531            apply_cnot(&mut sv, 0, 1).expect("cnot01");
532            gates.push(TnGate::TwoQ {
533                q0: 1,
534                q1: 2,
535                m: cnot_mat(),
536            });
537            apply_cnot(&mut sv, 1, 2).expect("cnot12");
538        }
539
540        for idx in 0..8usize {
541            let b0 = (idx & 1) != 0;
542            let b1 = (idx & 2) != 0;
543            let b2 = (idx & 4) != 0;
544            let amp = amplitude(3, &gates, &[b0, b1, b2]).expect("amp");
545            let reference = sv.amps[idx];
546            assert!(
547                (amp.re - reference.re).abs() < 1e-3 && (amp.im - reference.im).abs() < 1e-3,
548                "idx={idx}: tn={amp:?} sv={reference:?}"
549            );
550        }
551    }
552
553    #[test]
554    fn amplitude_probabilities_sum_to_one() {
555        // Random shallow wide-ish circuit: total probability over all bitstrings
556        // must be 1 (unitarity), verified via tensor-network amplitudes.
557        let mut rng = LcgRng::new(77);
558        let n = 4usize;
559        let mut gates: Vec<TnGate> = Vec::new();
560        for q in 0..n {
561            let theta = rng.next_f32() * std::f32::consts::TAU;
562            let c = (theta * 0.5).cos();
563            let s = (theta * 0.5).sin();
564            let m = [
565                [Complex32::new(c, 0.0), Complex32::new(-s, 0.0)],
566                [Complex32::new(s, 0.0), Complex32::new(c, 0.0)],
567            ];
568            gates.push(TnGate::OneQ { qubit: q, m });
569        }
570        gates.push(TnGate::TwoQ {
571            q0: 0,
572            q1: 1,
573            m: cnot_mat(),
574        });
575        gates.push(TnGate::TwoQ {
576            q0: 2,
577            q1: 3,
578            m: cnot_mat(),
579        });
580
581        let mut total = 0.0_f32;
582        for idx in 0..(1usize << n) {
583            let bits: Vec<bool> = (0..n).map(|q| (idx >> q) & 1 != 0).collect();
584            let amp = amplitude(n, &gates, &bits).expect("amp");
585            total += amp.norm_sqr();
586        }
587        assert!((total - 1.0).abs() < 1e-4, "total prob={total}");
588    }
589
590    #[test]
591    fn x_gate_flips_amplitude() {
592        let gates = vec![TnGate::OneQ {
593            qubit: 0,
594            m: gate_x(),
595        }];
596        let a1 = amplitude(1, &gates, &[true]).expect("a1");
597        let a0 = amplitude(1, &gates, &[false]).expect("a0");
598        assert!((a1.re - 1.0).abs() < 1e-6);
599        assert!(a0.norm() < 1e-6);
600    }
601
602    #[test]
603    fn empty_network_errors() {
604        let net = TensorNetwork::new();
605        assert!(net.contract_all().is_err());
606    }
607}