Skip to main content

quantrs2_sim/
mps_basic.rs

1//! Basic MPS simulator implementation without external linear algebra dependencies
2//!
3//! This provides a simplified MPS implementation that doesn't require ndarray-linalg
4
5use quantrs2_circuit::builder::{Circuit, Simulator};
6use quantrs2_core::{
7    error::{QuantRS2Error, QuantRS2Result},
8    gate::GateOp,
9    register::Register,
10};
11use scirs2_core::ndarray::{array, s, Array1, Array2, Array3};
12use scirs2_core::random::prelude::*;
13use scirs2_core::random::{thread_rng, Rng};
14use scirs2_core::Complex64;
15use std::f64::consts::SQRT_2;
16
17/// Configuration for basic MPS simulator
18#[derive(Debug, Clone)]
19pub struct BasicMPSConfig {
20    /// Maximum allowed bond dimension
21    pub max_bond_dim: usize,
22    /// SVD truncation threshold
23    pub svd_threshold: f64,
24}
25
26impl Default for BasicMPSConfig {
27    fn default() -> Self {
28        Self {
29            max_bond_dim: 64,
30            svd_threshold: 1e-10,
31        }
32    }
33}
34
35/// MPS tensor for a single qubit
36#[derive(Debug, Clone)]
37struct MPSTensor {
38    /// The tensor data: `left_bond` x physical x `right_bond`
39    data: Array3<Complex64>,
40}
41
42impl MPSTensor {
43    /// Create initial tensor for |0> state
44    fn zero_state(position: usize, num_qubits: usize) -> Self {
45        let is_first = position == 0;
46        let is_last = position == num_qubits - 1;
47
48        let data = if is_first && is_last {
49            // Single qubit: 1x2x1 tensor
50            let mut tensor = Array3::zeros((1, 2, 1));
51            tensor[[0, 0, 0]] = Complex64::new(1.0, 0.0);
52            tensor
53        } else if is_first {
54            // First qubit: 1x2x2 tensor
55            let mut tensor = Array3::zeros((1, 2, 2));
56            tensor[[0, 0, 0]] = Complex64::new(1.0, 0.0);
57            tensor
58        } else if is_last {
59            // Last qubit: 2x2x1 tensor
60            let mut tensor = Array3::zeros((2, 2, 1));
61            tensor[[0, 0, 0]] = Complex64::new(1.0, 0.0);
62            tensor
63        } else {
64            // Middle qubit: 2x2x2 tensor
65            let mut tensor = Array3::zeros((2, 2, 2));
66            tensor[[0, 0, 0]] = Complex64::new(1.0, 0.0);
67            tensor
68        };
69        Self { data }
70    }
71}
72
73/// Basic Matrix Product State representation
74pub struct BasicMPS {
75    /// MPS tensors for each qubit
76    tensors: Vec<MPSTensor>,
77    /// Number of qubits
78    num_qubits: usize,
79    /// Configuration
80    config: BasicMPSConfig,
81}
82
83impl BasicMPS {
84    /// Create a new MPS in the |0...0> state
85    #[must_use]
86    pub fn new(num_qubits: usize, config: BasicMPSConfig) -> Self {
87        let tensors = (0..num_qubits)
88            .map(|i| MPSTensor::zero_state(i, num_qubits))
89            .collect();
90
91        Self {
92            tensors,
93            num_qubits,
94            config,
95        }
96    }
97
98    /// Apply a single-qubit gate
99    pub fn apply_single_qubit_gate(
100        &mut self,
101        gate_matrix: &Array2<Complex64>,
102        qubit: usize,
103    ) -> QuantRS2Result<()> {
104        if qubit >= self.num_qubits {
105            return Err(QuantRS2Error::InvalidQubitId(qubit as u32));
106        }
107
108        let tensor = &mut self.tensors[qubit];
109        let shape = tensor.data.shape();
110        let (left_dim, _, right_dim) = (shape[0], shape[1], shape[2]);
111
112        let mut new_data = Array3::zeros((left_dim, 2, right_dim));
113
114        // Apply gate to physical index
115        for l in 0..left_dim {
116            for r in 0..right_dim {
117                for new_phys in 0..2 {
118                    for old_phys in 0..2 {
119                        new_data[[l, new_phys, r]] +=
120                            gate_matrix[[new_phys, old_phys]] * tensor.data[[l, old_phys, r]];
121                    }
122                }
123            }
124        }
125
126        tensor.data = new_data;
127        Ok(())
128    }
129
130    /// Apply a two-qubit gate to adjacent qubits
131    pub fn apply_two_qubit_gate(
132        &mut self,
133        gate_matrix: &Array2<Complex64>,
134        qubit1: usize,
135        qubit2: usize,
136    ) -> QuantRS2Result<()> {
137        if (qubit1 as i32 - qubit2 as i32).abs() != 1 {
138            return Err(QuantRS2Error::InvalidInput(
139                "MPS requires adjacent qubits for two-qubit gates".to_string(),
140            ));
141        }
142
143        let (left_q, right_q) = if qubit1 < qubit2 {
144            (qubit1, qubit2)
145        } else {
146            (qubit2, qubit1)
147        };
148
149        // Simple implementation: contract and re-decompose
150        // This is not optimal but works for demonstration
151
152        let left_shape = self.tensors[left_q].data.shape().to_vec();
153        let right_shape = self.tensors[right_q].data.shape().to_vec();
154
155        // Contract the two tensors
156        let mut combined = Array3::<Complex64>::zeros((left_shape[0], 4, right_shape[2]));
157
158        for l in 0..left_shape[0] {
159            for r in 0..right_shape[2] {
160                for i in 0..2 {
161                    for j in 0..2 {
162                        for m in 0..left_shape[2] {
163                            combined[[l, i * 2 + j, r]] += self.tensors[left_q].data[[l, i, m]]
164                                * self.tensors[right_q].data[[m, j, r]];
165                        }
166                    }
167                }
168            }
169        }
170
171        // Apply gate
172        let mut result = Array3::<Complex64>::zeros((left_shape[0], 4, right_shape[2]));
173        for l in 0..left_shape[0] {
174            for r in 0..right_shape[2] {
175                for out_idx in 0..4 {
176                    for in_idx in 0..4 {
177                        result[[l, out_idx, r]] +=
178                            gate_matrix[[out_idx, in_idx]] * combined[[l, in_idx, r]];
179                    }
180                }
181            }
182        }
183
184        // Split the gated two-site tensor back into two MPS tensors using a genuine SVD.
185        // Reshape the (left, 4, right) tensor into a matrix M[(left, s_left), (s_right, right)]
186        // where the physical indices are split between the two sites, then truncate to the
187        // largest singular values. Anything less than a real SVD silently destroys the
188        // entanglement the gate just created.
189        let left_dim = left_shape[0];
190        let right_dim = right_shape[2];
191        let mut matrix = Array2::<Complex64>::zeros((left_dim * 2, 2 * right_dim));
192        for l in 0..left_dim {
193            for r in 0..right_dim {
194                for i in 0..2 {
195                    for j in 0..2 {
196                        // combined physical index i*2 + j -> row (l, i), col (j, r)
197                        matrix[[l * 2 + i, j * right_dim + r]] = result[[l, i * 2 + j, r]];
198                    }
199                }
200            }
201        }
202
203        let (u, singular_values, vt) =
204            truncated_svd(&matrix, self.config.max_bond_dim, self.config.svd_threshold)?;
205        let new_bond = singular_values.len();
206
207        // Left tensor receives U reshaped to (left_dim, 2, new_bond).
208        let mut left_new = Array3::<Complex64>::zeros((left_dim, 2, new_bond));
209        for l in 0..left_dim {
210            for i in 0..2 {
211                for b in 0..new_bond {
212                    left_new[[l, i, b]] = u[[l * 2 + i, b]];
213                }
214            }
215        }
216
217        // Right tensor receives diag(S) · Vt reshaped to (new_bond, 2, right_dim), folding
218        // the singular values into the right tensor so the product reconstructs the state.
219        let mut right_new = Array3::<Complex64>::zeros((new_bond, 2, right_dim));
220        for b in 0..new_bond {
221            let scale = Complex64::new(singular_values[b], 0.0);
222            for j in 0..2 {
223                for r in 0..right_dim {
224                    right_new[[b, j, r]] = scale * vt[[b, j * right_dim + r]];
225                }
226            }
227        }
228
229        self.tensors[left_q].data = left_new;
230        self.tensors[right_q].data = right_new;
231
232        Ok(())
233    }
234
235    /// Get amplitude of a computational basis state
236    pub fn get_amplitude(&self, bitstring: &[bool]) -> QuantRS2Result<Complex64> {
237        if bitstring.len() != self.num_qubits {
238            return Err(QuantRS2Error::InvalidInput(format!(
239                "Bitstring length {} doesn't match qubit count {}",
240                bitstring.len(),
241                self.num_qubits
242            )));
243        }
244
245        // Contract MPS from left to right
246        let mut result = Array2::from_elem((1, 1), Complex64::new(1.0, 0.0));
247
248        for (i, &bit) in bitstring.iter().enumerate() {
249            let tensor = &self.tensors[i];
250            let physical_idx = i32::from(bit);
251
252            // Extract matrix for this physical index
253            let matrix = tensor.data.slice(s![.., physical_idx, ..]);
254
255            // Contract with accumulated result
256            result = result.dot(&matrix);
257        }
258
259        Ok(result[[0, 0]])
260    }
261
262    /// Sample a measurement outcome
263    #[must_use]
264    pub fn sample(&self) -> Vec<bool> {
265        let mut rng = thread_rng();
266        let mut result = vec![false; self.num_qubits];
267        let mut accumulated = Array2::from_elem((1, 1), Complex64::new(1.0, 0.0));
268
269        for (i, tensor) in self.tensors.iter().enumerate() {
270            // Compute probabilities for this qubit
271            let matrix0 = tensor.data.slice(s![.., 0, ..]);
272            let matrix1 = tensor.data.slice(s![.., 1, ..]);
273
274            let branch0: Array2<Complex64> = accumulated.dot(&matrix0);
275            let branch1: Array2<Complex64> = accumulated.dot(&matrix1);
276
277            // Compute norms (simplified - doesn't contract remaining qubits)
278            let norm0_sq: f64 = branch0.iter().map(scirs2_core::Complex::norm_sqr).sum();
279            let norm1_sq: f64 = branch1.iter().map(scirs2_core::Complex::norm_sqr).sum();
280
281            let total = norm0_sq + norm1_sq;
282            let prob0 = norm0_sq / total;
283
284            if rng.random::<f64>() < prob0 {
285                result[i] = false;
286                accumulated = branch0;
287            } else {
288                result[i] = true;
289                accumulated = branch1;
290            }
291
292            // Renormalize
293            let norm_sq: f64 = accumulated.iter().map(scirs2_core::Complex::norm_sqr).sum();
294            if norm_sq > 0.0 {
295                accumulated /= Complex64::new(norm_sq.sqrt(), 0.0);
296            }
297        }
298
299        result
300    }
301
302    /// Contract the MPS into a dense state vector of `2^n` complex amplitudes.
303    ///
304    /// Amplitudes follow the little-endian convention `amplitude[index]`, where bit `q` of
305    /// `index` holds the computational-basis value of qubit `q`. Each amplitude is computed
306    /// as the genuine chain of matrix products selected by those bits, so an entangled MPS
307    /// (e.g. a Bell pair) yields the correct entangled amplitudes.
308    fn to_statevector(&self) -> QuantRS2Result<Vec<Complex64>> {
309        let dim = 1usize << self.num_qubits;
310        let mut amplitudes = vec![Complex64::new(0.0, 0.0); dim];
311
312        for (index, amplitude) in amplitudes.iter_mut().enumerate() {
313            let mut accumulated = Array2::from_elem((1, 1), Complex64::new(1.0, 0.0));
314            for qubit in 0..self.num_qubits {
315                let bit = (index >> qubit) & 1;
316                let slice = self.tensors[qubit].data.slice(s![.., bit as i32, ..]);
317                accumulated = accumulated.dot(&slice);
318            }
319            *amplitude = accumulated[[0, 0]];
320        }
321
322        Ok(amplitudes)
323    }
324}
325
326/// Truncated complex singular value decomposition `A = U · diag(S) · Vt`.
327///
328/// Delegates the full factorization to the robust one-sided Jacobi SVD in
329/// [`crate::mps_simulator::complex_jacobi_svd`], then keeps the largest singular values
330/// (those above `threshold`, capped at `max_bond`), returning `(U, S, Vt)` with the bond
331/// dimension truncated to the number of retained singular values. At least one singular
332/// value is always retained so the resulting tensors remain well-formed.
333fn truncated_svd(
334    matrix: &Array2<Complex64>,
335    max_bond: usize,
336    threshold: f64,
337) -> QuantRS2Result<(Array2<Complex64>, Array1<f64>, Array2<Complex64>)> {
338    let (full_u, full_s, full_vt) = crate::mps_simulator::complex_jacobi_svd(matrix)?;
339    let full_rank = full_s.len();
340
341    let mut kept = full_s.iter().filter(|&&value| value > threshold).count();
342    kept = kept.min(max_bond).min(full_rank);
343    if kept == 0 {
344        kept = full_rank.max(1);
345    }
346
347    let u: Array2<Complex64> = full_u.slice(s![.., ..kept]).to_owned();
348    let truncated_s: Array1<f64> = full_s.slice(s![..kept]).to_owned();
349    let vt: Array2<Complex64> = full_vt.slice(s![..kept, ..]).to_owned();
350
351    Ok((u, truncated_s, vt))
352}
353
354/// Basic MPS quantum simulator
355pub struct BasicMPSSimulator {
356    config: BasicMPSConfig,
357}
358
359impl BasicMPSSimulator {
360    /// Create a new basic MPS simulator
361    #[must_use]
362    pub const fn new(config: BasicMPSConfig) -> Self {
363        Self { config }
364    }
365
366    /// Create with default configuration
367    #[must_use]
368    pub fn default() -> Self {
369        Self::new(BasicMPSConfig::default())
370    }
371}
372
373impl<const N: usize> Simulator<N> for BasicMPSSimulator {
374    fn run(&self, circuit: &Circuit<N>) -> QuantRS2Result<Register<N>> {
375        // Create initial MPS state
376        let mut mps = BasicMPS::new(N, self.config.clone());
377
378        // Apply gates from circuit
379        for gate in circuit.gates() {
380            match gate.name() {
381                "H" => {
382                    let h_matrix = {
383                        let h = 1.0 / SQRT_2;
384                        array![
385                            [Complex64::new(h, 0.), Complex64::new(h, 0.)],
386                            [Complex64::new(h, 0.), Complex64::new(-h, 0.)]
387                        ]
388                    };
389                    if let Some(&qubit) = gate.qubits().first() {
390                        mps.apply_single_qubit_gate(&h_matrix, qubit.id() as usize)?;
391                    }
392                }
393                "X" => {
394                    let x_matrix = array![
395                        [Complex64::new(0., 0.), Complex64::new(1., 0.)],
396                        [Complex64::new(1., 0.), Complex64::new(0., 0.)]
397                    ];
398                    if let Some(&qubit) = gate.qubits().first() {
399                        mps.apply_single_qubit_gate(&x_matrix, qubit.id() as usize)?;
400                    }
401                }
402                "CNOT" | "CX" => {
403                    let cnot_matrix = array![
404                        [
405                            Complex64::new(1., 0.),
406                            Complex64::new(0., 0.),
407                            Complex64::new(0., 0.),
408                            Complex64::new(0., 0.)
409                        ],
410                        [
411                            Complex64::new(0., 0.),
412                            Complex64::new(1., 0.),
413                            Complex64::new(0., 0.),
414                            Complex64::new(0., 0.)
415                        ],
416                        [
417                            Complex64::new(0., 0.),
418                            Complex64::new(0., 0.),
419                            Complex64::new(0., 0.),
420                            Complex64::new(1., 0.)
421                        ],
422                        [
423                            Complex64::new(0., 0.),
424                            Complex64::new(0., 0.),
425                            Complex64::new(1., 0.),
426                            Complex64::new(0., 0.)
427                        ],
428                    ];
429                    let qubits = gate.qubits();
430                    if qubits.len() == 2 {
431                        mps.apply_two_qubit_gate(
432                            &cnot_matrix,
433                            qubits[0].id() as usize,
434                            qubits[1].id() as usize,
435                        )?;
436                    }
437                }
438                _ => {
439                    // Gate not supported in basic implementation
440                }
441            }
442        }
443
444        // Contract the MPS into a dense state vector and build the register from it.
445        let amplitudes = mps.to_statevector()?;
446        Register::<N>::with_amplitudes(amplitudes)
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    #[test]
455    fn test_basic_mps_initialization() {
456        let mps = BasicMPS::new(4, BasicMPSConfig::default());
457
458        // Check |0000> state
459        let amp = mps
460            .get_amplitude(&[false, false, false, false])
461            .expect("Failed to get amplitude for |0000>");
462        assert!((amp.norm() - 1.0).abs() < 1e-10);
463
464        let amp = mps
465            .get_amplitude(&[true, false, false, false])
466            .expect("Failed to get amplitude for |1000>");
467        assert!(amp.norm() < 1e-10);
468    }
469
470    #[test]
471    fn test_single_qubit_gate() {
472        let mut mps = BasicMPS::new(3, BasicMPSConfig::default());
473
474        // Apply X to first qubit
475        let x_matrix = array![
476            [Complex64::new(0., 0.), Complex64::new(1., 0.)],
477            [Complex64::new(1., 0.), Complex64::new(0., 0.)]
478        ];
479        mps.apply_single_qubit_gate(&x_matrix, 0)
480            .expect("Failed to apply X gate");
481
482        // Check |100> state
483        let amp = mps
484            .get_amplitude(&[true, false, false])
485            .expect("Failed to get amplitude for |100>");
486        assert!((amp.norm() - 1.0).abs() < 1e-10);
487    }
488
489    #[test]
490    fn test_truncated_svd_reconstructs_matrix() {
491        let matrix = Array2::from_shape_vec(
492            (2, 2),
493            vec![
494                Complex64::new(1.0, 1.0),
495                Complex64::new(2.0, -1.0),
496                Complex64::new(-1.0, 0.5),
497                Complex64::new(0.0, 2.0),
498            ],
499        )
500        .expect("failed to build test matrix");
501
502        let (u, s, vt) =
503            truncated_svd(&matrix, 16, 1e-14).expect("real SVD decomposition should succeed");
504
505        let k = s.len();
506        let mut max_err = 0.0_f64;
507        for i in 0..2 {
508            for j in 0..2 {
509                let mut acc = Complex64::new(0.0, 0.0);
510                for r in 0..k {
511                    acc += u[[i, r]] * Complex64::new(s[r], 0.0) * vt[[r, j]];
512                }
513                max_err = max_err.max((acc - matrix[[i, j]]).norm());
514            }
515        }
516        assert!(
517            max_err < 1e-8,
518            "SVD reconstruction error too large: {max_err}"
519        );
520        // Not the fabricated identity-like decomposition.
521        assert!(
522            s.iter().any(|&value| (value - 1.0).abs() > 1e-6),
523            "singular values are all ~1 (identity fabrication)"
524        );
525    }
526
527    #[test]
528    fn test_two_qubit_gate_bell_contraction() {
529        // Prepare a Bell state via H on qubit 0 followed by CNOT(0, 1) using real SVD.
530        let mut mps = BasicMPS::new(2, BasicMPSConfig::default());
531
532        let h = 1.0 / SQRT_2;
533        let h_matrix = array![
534            [Complex64::new(h, 0.), Complex64::new(h, 0.)],
535            [Complex64::new(h, 0.), Complex64::new(-h, 0.)]
536        ];
537        mps.apply_single_qubit_gate(&h_matrix, 0)
538            .expect("failed to apply H");
539
540        let cnot = array![
541            [
542                Complex64::new(1., 0.),
543                Complex64::new(0., 0.),
544                Complex64::new(0., 0.),
545                Complex64::new(0., 0.)
546            ],
547            [
548                Complex64::new(0., 0.),
549                Complex64::new(1., 0.),
550                Complex64::new(0., 0.),
551                Complex64::new(0., 0.)
552            ],
553            [
554                Complex64::new(0., 0.),
555                Complex64::new(0., 0.),
556                Complex64::new(0., 0.),
557                Complex64::new(1., 0.)
558            ],
559            [
560                Complex64::new(0., 0.),
561                Complex64::new(0., 0.),
562                Complex64::new(1., 0.),
563                Complex64::new(0., 0.)
564            ],
565        ];
566        mps.apply_two_qubit_gate(&cnot, 0, 1)
567            .expect("failed to apply CNOT");
568
569        let state = mps.to_statevector().expect("contraction should succeed");
570
571        let inv_sqrt2 = 1.0 / SQRT_2;
572        assert!((state[0].re - inv_sqrt2).abs() < 1e-10, "amp(|00>) wrong");
573        assert!(state[1].norm() < 1e-10, "amp(|01>) should vanish");
574        assert!(state[2].norm() < 1e-10, "amp(|10>) should vanish");
575        assert!((state[3].re - inv_sqrt2).abs() < 1e-10, "amp(|11>) wrong");
576
577        // Honest check: genuinely entangled, not the empty/|00> placeholder register.
578        assert!(
579            state[3].norm() > 1e-3,
580            "two-qubit gate fabricated a non-entangled state"
581        );
582    }
583}