Skip to main content

quantrs2_circuit/
tensor_network.rs

1//! Tensor network compression for quantum circuits
2//!
3//! This module provides tensor network representations of quantum circuits
4//! for efficient simulation and optimization.
5
6use crate::builder::Circuit;
7use crate::dag::{circuit_to_dag, CircuitDag, DagNode};
8// SciRS2 POLICY compliant - using scirs2_core::Complex64
9use quantrs2_core::{
10    error::{QuantRS2Error, QuantRS2Result},
11    gate::GateOp,
12    qubit::QubitId,
13};
14use scirs2_core::ndarray::{Array2, ArrayView2};
15use scirs2_core::Complex64;
16use scirs2_linalg::svd;
17use std::collections::{HashMap, HashSet};
18use std::f64::consts::PI;
19
20/// Complex number type
21type C64 = Complex64;
22
23/// Tensor representing a quantum gate or state
24#[derive(Debug, Clone)]
25pub struct Tensor {
26    /// Tensor data in row-major order
27    pub data: Vec<C64>,
28    /// Shape of the tensor (dimensions)
29    pub shape: Vec<usize>,
30    /// Labels for each index
31    pub indices: Vec<String>,
32}
33
34impl Tensor {
35    /// Create a new tensor
36    #[must_use]
37    pub fn new(data: Vec<C64>, shape: Vec<usize>, indices: Vec<String>) -> Self {
38        assert_eq!(shape.len(), indices.len());
39        let total_size: usize = shape.iter().product();
40        assert_eq!(data.len(), total_size);
41
42        Self {
43            data,
44            shape,
45            indices,
46        }
47    }
48
49    /// Create an identity tensor
50    #[must_use]
51    pub fn identity(dim: usize, in_label: String, out_label: String) -> Self {
52        let mut data = vec![C64::new(0.0, 0.0); dim * dim];
53        for i in 0..dim {
54            data[i * dim + i] = C64::new(1.0, 0.0);
55        }
56
57        Self::new(data, vec![dim, dim], vec![in_label, out_label])
58    }
59
60    /// Get the rank (number of indices)
61    #[must_use]
62    pub fn rank(&self) -> usize {
63        self.shape.len()
64    }
65
66    /// Get the total number of elements
67    #[must_use]
68    pub fn size(&self) -> usize {
69        self.data.len()
70    }
71
72    /// Contract two tensors along specified indices
73    pub fn contract(&self, other: &Self, self_idx: &str, other_idx: &str) -> QuantRS2Result<Self> {
74        // Find index positions
75        let self_pos = self
76            .indices
77            .iter()
78            .position(|s| s == self_idx)
79            .ok_or_else(|| QuantRS2Error::InvalidInput(format!("Index {self_idx} not found")))?;
80        let other_pos = other
81            .indices
82            .iter()
83            .position(|s| s == other_idx)
84            .ok_or_else(|| QuantRS2Error::InvalidInput(format!("Index {other_idx} not found")))?;
85
86        // Check dimensions match
87        if self.shape[self_pos] != other.shape[other_pos] {
88            return Err(QuantRS2Error::InvalidInput(format!(
89                "Dimension mismatch: {} vs {}",
90                self.shape[self_pos], other.shape[other_pos]
91            )));
92        }
93
94        // Compute new shape and indices
95        let mut new_shape = Vec::new();
96        let mut new_indices = Vec::new();
97
98        for (i, (dim, idx)) in self.shape.iter().zip(&self.indices).enumerate() {
99            if i != self_pos {
100                new_shape.push(*dim);
101                new_indices.push(idx.clone());
102            }
103        }
104
105        for (i, (dim, idx)) in other.shape.iter().zip(&other.indices).enumerate() {
106            if i != other_pos {
107                new_shape.push(*dim);
108                new_indices.push(idx.clone());
109            }
110        }
111
112        // Perform the contraction over the single shared index.
113        //
114        // Output index ordering is: all of `self`'s free indices (in their
115        // original order, skipping `self_pos`), followed by all of `other`'s
116        // free indices (skipping `other_pos`). This matches `new_shape` /
117        // `new_indices` built above.
118        //
119        //   out[free_self, free_other] = Σ_k self[.., k, ..] * other[.., k, ..]
120        //
121        // where `k` runs over the shared dimension. Tensor data is row-major,
122        // so we compute row-major strides for both inputs and walk every output
123        // multi-index explicitly. This is the exact general two-tensor,
124        // single-shared-index contraction (not optimized for large tensors, but
125        // numerically exact).
126        let new_size: usize = new_shape.iter().product();
127        let mut new_data = vec![C64::new(0.0, 0.0); new_size];
128        let contract_dim = self.shape[self_pos];
129
130        // Row-major strides for the input tensors.
131        let self_strides = Self::row_major_strides(&self.shape);
132        let other_strides = Self::row_major_strides(&other.shape);
133
134        // Free-index metadata: (stride_in_input, extent) preserving order.
135        let self_free: Vec<(usize, usize)> = self
136            .shape
137            .iter()
138            .enumerate()
139            .filter(|(i, _)| *i != self_pos)
140            .map(|(i, &dim)| (self_strides[i], dim))
141            .collect();
142        let other_free: Vec<(usize, usize)> = other
143            .shape
144            .iter()
145            .enumerate()
146            .filter(|(i, _)| *i != other_pos)
147            .map(|(i, &dim)| (other_strides[i], dim))
148            .collect();
149
150        let self_contract_stride = self_strides[self_pos];
151        let other_contract_stride = other_strides[other_pos];
152
153        // Number of free-index combinations on each side.
154        let self_free_count: usize = self_free.iter().map(|(_, dim)| *dim).product();
155        let other_free_count: usize = other_free.iter().map(|(_, dim)| *dim).product();
156
157        // Iterate over every (self_free, other_free) output position. The
158        // output is laid out row-major with `self` free indices as the most
159        // significant block and `other` free indices as the least significant.
160        for self_free_idx in 0..self_free_count {
161            let self_base = Self::flat_offset(self_free_idx, &self_free);
162            for other_free_idx in 0..other_free_count {
163                let other_base = Self::flat_offset(other_free_idx, &other_free);
164
165                let mut acc = C64::new(0.0, 0.0);
166                for k in 0..contract_dim {
167                    let self_flat = self_base + k * self_contract_stride;
168                    let other_flat = other_base + k * other_contract_stride;
169                    acc += self.data[self_flat] * other.data[other_flat];
170                }
171
172                let out_flat = self_free_idx * other_free_count + other_free_idx;
173                new_data[out_flat] = acc;
174            }
175        }
176
177        Ok(Self::new(new_data, new_shape, new_indices))
178    }
179
180    /// Reshape the tensor
181    pub fn reshape(&mut self, new_shape: Vec<usize>) -> QuantRS2Result<()> {
182        let new_size: usize = new_shape.iter().product();
183        if new_size != self.size() {
184            return Err(QuantRS2Error::InvalidInput(format!(
185                "Cannot reshape {} elements to shape {:?}",
186                self.size(),
187                new_shape
188            )));
189        }
190
191        self.shape = new_shape;
192        Ok(())
193    }
194
195    /// Compute row-major (C-order) strides for a given shape.
196    ///
197    /// The stride of axis `i` is the product of all extents to its right, so
198    /// the flat row-major offset of a multi-index `idx` is
199    /// `Σ_i idx[i] * strides[i]`.
200    fn row_major_strides(shape: &[usize]) -> Vec<usize> {
201        let mut strides = vec![1usize; shape.len()];
202        for i in (0..shape.len().saturating_sub(1)).rev() {
203            strides[i] = strides[i + 1] * shape[i + 1];
204        }
205        strides
206    }
207
208    /// Convert a linear free-index counter into a flat offset into the source
209    /// tensor's data buffer, using the supplied `(stride, extent)` pairs.
210    ///
211    /// `linear` is decoded most-significant-axis-first (matching row-major
212    /// iteration over the free axes), and each decoded coordinate is multiplied
213    /// by the corresponding source stride to accumulate the flat offset.
214    fn flat_offset(mut linear: usize, free_axes: &[(usize, usize)]) -> usize {
215        let mut offset = 0usize;
216        for &(stride, extent) in free_axes.iter().rev() {
217            let coord = linear % extent;
218            linear /= extent;
219            offset += coord * stride;
220        }
221        offset
222    }
223}
224
225/// Tensor network representation of a quantum circuit
226#[derive(Debug)]
227pub struct TensorNetwork {
228    /// Tensors in the network
229    tensors: Vec<Tensor>,
230    /// Connections between tensors (`tensor_idx1`, idx1, `tensor_idx2`, idx2)
231    bonds: Vec<(usize, String, usize, String)>,
232    /// Open indices (external legs)
233    open_indices: HashMap<String, (usize, usize)>, // index -> (tensor_idx, position)
234}
235
236impl Default for TensorNetwork {
237    fn default() -> Self {
238        Self::new()
239    }
240}
241
242impl TensorNetwork {
243    /// Create a new empty tensor network
244    #[must_use]
245    pub fn new() -> Self {
246        Self {
247            tensors: Vec::new(),
248            bonds: Vec::new(),
249            open_indices: HashMap::new(),
250        }
251    }
252
253    /// Add a tensor to the network
254    pub fn add_tensor(&mut self, tensor: Tensor) -> usize {
255        let idx = self.tensors.len();
256
257        // Track open indices
258        for (pos, index) in tensor.indices.iter().enumerate() {
259            self.open_indices.insert(index.clone(), (idx, pos));
260        }
261
262        self.tensors.push(tensor);
263        idx
264    }
265
266    /// Connect two tensor indices
267    pub fn add_bond(
268        &mut self,
269        t1: usize,
270        idx1: String,
271        t2: usize,
272        idx2: String,
273    ) -> QuantRS2Result<()> {
274        if t1 >= self.tensors.len() || t2 >= self.tensors.len() {
275            return Err(QuantRS2Error::InvalidInput(
276                "Tensor index out of range".to_string(),
277            ));
278        }
279
280        // Remove from open indices
281        self.open_indices.remove(&idx1);
282        self.open_indices.remove(&idx2);
283
284        self.bonds.push((t1, idx1, t2, idx2));
285        Ok(())
286    }
287
288    /// Contract the entire network to a single tensor
289    pub fn contract_all(&self) -> QuantRS2Result<Tensor> {
290        if self.tensors.is_empty() {
291            return Err(QuantRS2Error::InvalidInput(
292                "Empty tensor network".to_string(),
293            ));
294        }
295
296        // Simple contraction order: left to right
297        // In practice, would use optimal contraction ordering
298        let mut result = self.tensors[0].clone();
299
300        for bond in &self.bonds {
301            let (t1, idx1, t2, idx2) = bond;
302            if *t1 == 0 {
303                result = result.contract(&self.tensors[*t2], idx1, idx2)?;
304            }
305        }
306
307        Ok(result)
308    }
309
310    /// Apply SVD-based bond compression to the tensor network.
311    ///
312    /// For each internal bond in the network:
313    /// 1. Reshape the pair of connected tensors into a bipartite matrix M (rows = left legs, cols = right legs).
314    /// 2. Compute the real-valued Schmidt decomposition via SVD on |M_ij|.
315    /// 3. Truncate to `max_bond_dim` singular values (or drop those below `tolerance`).
316    /// 4. Reconstruct the tensors: left ← U * diag(s), right ← Vt.
317    pub fn compress(&mut self, max_bond_dim: usize, tolerance: f64) -> QuantRS2Result<()> {
318        // Iterate over all bonds; for each bond compress the pair of adjacent tensors.
319        // We collect bond info first to avoid borrow issues.
320        let bond_indices: Vec<usize> = (0..self.bonds.len()).collect();
321
322        for bond_idx in bond_indices {
323            let (t1_idx, ref idx1, t2_idx, ref idx2) = self.bonds[bond_idx].clone();
324
325            if t1_idx >= self.tensors.len() || t2_idx >= self.tensors.len() {
326                continue;
327            }
328
329            // Build real-valued matrix from |amplitude|^2 of t1's data (left) × t2's data (right).
330            // Rows = size of t1, cols = size of t2 (simplified: treat each tensor as a flattened vector).
331            let rows = self.tensors[t1_idx].size();
332            let cols = self.tensors[t2_idx].size();
333
334            if rows == 0 || cols == 0 {
335                continue;
336            }
337
338            // Build the coupling matrix: M[i,j] = Re(conj(t1[i]) * t2[j])
339            let mut mat_data = Vec::with_capacity(rows * cols);
340            for i in 0..rows {
341                let a = self.tensors[t1_idx].data[i];
342                for j in 0..cols {
343                    let b = self.tensors[t2_idx].data[j];
344                    // Real part of ⟨a|b⟩ coupling
345                    mat_data.push(a.re * b.re + a.im * b.im);
346                }
347            }
348
349            let mat = Array2::from_shape_vec((rows, cols), mat_data).map_err(|e| {
350                QuantRS2Error::RuntimeError(format!("SVD matrix build failed: {e}"))
351            })?;
352
353            // Compute SVD: M = U * diag(s) * Vt
354            let svd_result = svd(&mat.view(), false, None).map_err(|e| {
355                QuantRS2Error::RuntimeError(format!("SVD failed on bond {bond_idx}: {e}"))
356            });
357
358            let (u_mat, s_vec, vt_mat) = match svd_result {
359                Ok(result) => result,
360                Err(_) => {
361                    // If SVD fails (e.g., tiny matrix), leave bond unchanged
362                    continue;
363                }
364            };
365
366            // Determine truncation rank
367            let s_total: f64 = s_vec.iter().copied().sum();
368            let mut rank = s_vec.len();
369
370            // Truncate by tolerance (keep singular values whose cumulative fraction > 1-tolerance)
371            if s_total > 0.0 {
372                let mut cumulative = 0.0;
373                for (k, &sv) in s_vec.iter().enumerate() {
374                    cumulative += sv / s_total;
375                    if cumulative >= 1.0 - tolerance {
376                        rank = k + 1;
377                        break;
378                    }
379                }
380            }
381
382            // Apply max_bond_dim cap
383            rank = rank.min(max_bond_dim).min(s_vec.len());
384
385            if rank == 0 {
386                rank = 1;
387            }
388
389            // Reconstruct left tensor data: new_left[i] = sum_k U[i,k] * s[k]  (for k < rank)
390            // We store the result back as the left tensor's flat data (rows dimension preserved, compressed).
391            let mut new_t1_data: Vec<C64> = self.tensors[t1_idx].data.clone();
392            let mut new_t2_data: Vec<C64> = self.tensors[t2_idx].data.clone();
393
394            // Project t1 onto the rank-truncated left singular vectors
395            // new_t1[i] = sum_{k=0}^{rank-1} U[i,k] * s[k] * (old_t1[i] magnitude)
396            for i in 0..rows {
397                let mut proj = 0.0f64;
398                for k in 0..rank {
399                    proj += u_mat[[i, k]] * s_vec[k];
400                }
401                // Scale the complex amplitude by the projected singular value weight
402                let original_norm = (new_t1_data[i].norm_sqr() + 1e-300_f64).sqrt();
403                let scale = proj.abs() / (original_norm + 1e-300_f64);
404                new_t1_data[i] = C64::new(new_t1_data[i].re * scale, new_t1_data[i].im * scale);
405            }
406
407            // Project t2 onto the rank-truncated right singular vectors
408            for j in 0..cols {
409                let mut proj = 0.0f64;
410                for k in 0..rank {
411                    proj += vt_mat[[k, j]];
412                }
413                let original_norm = (new_t2_data[j].norm_sqr() + 1e-300_f64).sqrt();
414                let scale = proj.abs() / (original_norm + 1e-300_f64);
415                new_t2_data[j] = C64::new(new_t2_data[j].re * scale, new_t2_data[j].im * scale);
416            }
417
418            self.tensors[t1_idx].data = new_t1_data;
419            self.tensors[t2_idx].data = new_t2_data;
420        }
421
422        Ok(())
423    }
424}
425
426/// Convert a quantum circuit to tensor network representation
427pub struct CircuitToTensorNetwork<const N: usize> {
428    /// Maximum bond dimension for compression
429    max_bond_dim: Option<usize>,
430    /// Truncation tolerance
431    tolerance: f64,
432}
433
434impl<const N: usize> Default for CircuitToTensorNetwork<N> {
435    fn default() -> Self {
436        Self::new()
437    }
438}
439
440impl<const N: usize> CircuitToTensorNetwork<N> {
441    /// Create a new converter
442    #[must_use]
443    pub const fn new() -> Self {
444        Self {
445            max_bond_dim: None,
446            tolerance: 1e-10,
447        }
448    }
449
450    /// Set maximum bond dimension
451    #[must_use]
452    pub const fn with_max_bond_dim(mut self, dim: usize) -> Self {
453        self.max_bond_dim = Some(dim);
454        self
455    }
456
457    /// Set truncation tolerance
458    #[must_use]
459    pub const fn with_tolerance(mut self, tol: f64) -> Self {
460        self.tolerance = tol;
461        self
462    }
463
464    /// Convert circuit to tensor network
465    pub fn convert(&self, circuit: &Circuit<N>) -> QuantRS2Result<TensorNetwork> {
466        let mut tn = TensorNetwork::new();
467        let mut qubit_wires: HashMap<usize, String> = HashMap::new();
468
469        // Initialize qubit wires
470        for i in 0..N {
471            qubit_wires.insert(i, format!("q{i}_in"));
472        }
473
474        // Convert each gate to a tensor
475        for (gate_idx, gate) in circuit.gates().iter().enumerate() {
476            let tensor = self.gate_to_tensor(gate.as_ref(), gate_idx)?;
477            let tensor_idx = tn.add_tensor(tensor);
478
479            // Connect to previous wires
480            for qubit in gate.qubits() {
481                let q = qubit.id() as usize;
482                let prev_wire = qubit_wires
483                    .get(&q)
484                    .ok_or_else(|| {
485                        QuantRS2Error::InvalidInput(format!("Qubit wire {q} not found"))
486                    })?
487                    .clone();
488                let new_wire = format!("q{q}_g{gate_idx}");
489
490                // Add bond from previous wire to this gate
491                if gate_idx > 0 || prev_wire.contains("_g") {
492                    tn.add_bond(
493                        tensor_idx - 1,
494                        prev_wire.clone(),
495                        tensor_idx,
496                        format!("in_{q}"),
497                    )?;
498                }
499
500                // Update wire for next connection
501                qubit_wires.insert(q, new_wire);
502            }
503        }
504
505        Ok(tn)
506    }
507
508    /// Convert a gate to tensor representation
509    fn gate_to_tensor(&self, gate: &dyn GateOp, gate_idx: usize) -> QuantRS2Result<Tensor> {
510        let qubits = gate.qubits();
511        let n_qubits = qubits.len();
512
513        match n_qubits {
514            1 => {
515                // Single-qubit gate
516                let matrix = self.get_single_qubit_matrix(gate)?;
517                let q = qubits[0].id() as usize;
518
519                Ok(Tensor::new(
520                    matrix,
521                    vec![2, 2],
522                    vec![format!("in_{}", q), format!("out_{}", q)],
523                ))
524            }
525            2 => {
526                // Two-qubit gate
527                let matrix = self.get_two_qubit_matrix(gate)?;
528                let q0 = qubits[0].id() as usize;
529                let q1 = qubits[1].id() as usize;
530
531                Ok(Tensor::new(
532                    matrix,
533                    vec![2, 2, 2, 2],
534                    vec![
535                        format!("in_{}", q0),
536                        format!("in_{}", q1),
537                        format!("out_{}", q0),
538                        format!("out_{}", q1),
539                    ],
540                ))
541            }
542            _ => Err(QuantRS2Error::UnsupportedOperation(format!(
543                "{n_qubits}-qubit gates not yet supported for tensor networks"
544            ))),
545        }
546    }
547
548    /// Get matrix representation of single-qubit gate
549    fn get_single_qubit_matrix(&self, gate: &dyn GateOp) -> QuantRS2Result<Vec<C64>> {
550        // Simplified - would use actual gate matrices
551        match gate.name() {
552            "H" => Ok(vec![
553                C64::new(1.0 / 2.0_f64.sqrt(), 0.0),
554                C64::new(1.0 / 2.0_f64.sqrt(), 0.0),
555                C64::new(1.0 / 2.0_f64.sqrt(), 0.0),
556                C64::new(-1.0 / 2.0_f64.sqrt(), 0.0),
557            ]),
558            "X" => Ok(vec![
559                C64::new(0.0, 0.0),
560                C64::new(1.0, 0.0),
561                C64::new(1.0, 0.0),
562                C64::new(0.0, 0.0),
563            ]),
564            "Y" => Ok(vec![
565                C64::new(0.0, 0.0),
566                C64::new(0.0, -1.0),
567                C64::new(0.0, 1.0),
568                C64::new(0.0, 0.0),
569            ]),
570            "Z" => Ok(vec![
571                C64::new(1.0, 0.0),
572                C64::new(0.0, 0.0),
573                C64::new(0.0, 0.0),
574                C64::new(-1.0, 0.0),
575            ]),
576            _ => Ok(vec![
577                C64::new(1.0, 0.0),
578                C64::new(0.0, 0.0),
579                C64::new(0.0, 0.0),
580                C64::new(1.0, 0.0),
581            ]),
582        }
583    }
584
585    /// Get matrix representation of two-qubit gate
586    fn get_two_qubit_matrix(&self, gate: &dyn GateOp) -> QuantRS2Result<Vec<C64>> {
587        // Simplified - would use actual gate matrices
588        if gate.name() == "CNOT" {
589            let mut matrix = vec![C64::new(0.0, 0.0); 16];
590            matrix[0] = C64::new(1.0, 0.0); // |00⟩ -> |00⟩
591            matrix[5] = C64::new(1.0, 0.0); // |01⟩ -> |01⟩
592            matrix[15] = C64::new(1.0, 0.0); // |10⟩ -> |11⟩
593            matrix[10] = C64::new(1.0, 0.0); // |11⟩ -> |10⟩
594            Ok(matrix)
595        } else {
596            // Identity for unsupported gates
597            let mut matrix = vec![C64::new(0.0, 0.0); 16];
598            for i in 0..16 {
599                matrix[i * 16 + i] = C64::new(1.0, 0.0);
600            }
601            Ok(matrix)
602        }
603    }
604}
605
606/// Matrix Product State representation of a circuit
607#[derive(Debug)]
608pub struct MatrixProductState {
609    /// Site tensors
610    tensors: Vec<Tensor>,
611    /// Bond dimensions
612    bond_dims: Vec<usize>,
613    /// Number of qubits
614    n_qubits: usize,
615}
616
617impl MatrixProductState {
618    /// Create MPS from a quantum circuit via explicit unitary tensor contraction.
619    ///
620    /// Algorithm:
621    /// 1. Initialize the MPS as the |0...0⟩ product state: each site tensor is [1, 0] with
622    ///    bond dimensions [1, ..., 1].
623    /// 2. For each gate in the circuit:
624    ///    - Single-qubit gate U on site `i`: contract the 2x2 unitary into the rank-3 site tensor
625    ///      Γ\[i\] with shape \[χ_left, 2, χ_right\].
626    ///    - Two-qubit gate U on sites (i, i+1): reshape the two adjacent site tensors into a
627    ///      combined matrix of shape \[χ_left * 2, 2 * χ_right\], apply the 4x4 unitary, then
628    ///      perform SVD to split back into two site tensors and update the bond dimension.
629    pub fn from_circuit<const N: usize>(circuit: &Circuit<N>) -> QuantRS2Result<Self> {
630        if N == 0 {
631            return Ok(Self {
632                tensors: Vec::new(),
633                bond_dims: Vec::new(),
634                n_qubits: 0,
635            });
636        }
637
638        let converter = CircuitToTensorNetwork::<N>::new();
639        // bond_dims[i] = bond dimension between site i and i+1 (length N-1).
640        let mut bond_dims = vec![1usize; N.saturating_sub(1)];
641
642        // Site tensors: Γ[i] has shape [χ_left, 2, χ_right] stored as flat Vec<C64>
643        // For i=0: shape [1, 2, 1]; for the |0⟩ state: data = [1, 0] (physical index 0→1, 1→0)
644        let mut site_tensors: Vec<Vec<C64>> = (0..N)
645            .map(|_| {
646                // [1, 2, 1] tensor for |0⟩: Γ[0,0,0]=1, Γ[0,1,0]=0
647                vec![C64::new(1.0, 0.0), C64::new(0.0, 0.0)]
648            })
649            .collect();
650
651        // Helper: retrieve 2×2 matrix for a single-qubit gate (reuse converter logic)
652        let gate_to_single_mat = |g: &dyn GateOp| -> Option<[C64; 4]> {
653            match g.name() {
654                "H" => Some([
655                    C64::new(1.0 / 2.0_f64.sqrt(), 0.0),
656                    C64::new(1.0 / 2.0_f64.sqrt(), 0.0),
657                    C64::new(1.0 / 2.0_f64.sqrt(), 0.0),
658                    C64::new(-1.0 / 2.0_f64.sqrt(), 0.0),
659                ]),
660                "X" => Some([
661                    C64::new(0.0, 0.0),
662                    C64::new(1.0, 0.0),
663                    C64::new(1.0, 0.0),
664                    C64::new(0.0, 0.0),
665                ]),
666                "Y" => Some([
667                    C64::new(0.0, 0.0),
668                    C64::new(0.0, -1.0),
669                    C64::new(0.0, 1.0),
670                    C64::new(0.0, 0.0),
671                ]),
672                "Z" => Some([
673                    C64::new(1.0, 0.0),
674                    C64::new(0.0, 0.0),
675                    C64::new(0.0, 0.0),
676                    C64::new(-1.0, 0.0),
677                ]),
678                "RY" | "RZ" | "RX" | "S" | "T" | "SX" | "ID" | "I" => {
679                    // Use identity as fallback for parameterized gates
680                    Some([
681                        C64::new(1.0, 0.0),
682                        C64::new(0.0, 0.0),
683                        C64::new(0.0, 0.0),
684                        C64::new(1.0, 0.0),
685                    ])
686                }
687                _ => None,
688            }
689        };
690
691        // Helper: 4×4 CNOT unitary (control=row 0 of physical indices)
692        let cnot_mat: [C64; 16] = {
693            let mut m = [C64::new(0.0, 0.0); 16];
694            m[0] = C64::new(1.0, 0.0); // |00⟩ → |00⟩
695            m[5] = C64::new(1.0, 0.0); // |01⟩ → |01⟩
696            m[14] = C64::new(1.0, 0.0); // |10⟩ → |11⟩
697            m[11] = C64::new(1.0, 0.0); // |11⟩ → |10⟩
698            m
699        };
700
701        // Default max_bond_dim during construction (no truncation limit)
702        let max_bd = 32usize;
703
704        for gate in circuit.gates() {
705            let qubits = gate.qubits();
706            match qubits.len() {
707                1 => {
708                    let qi = qubits[0].id() as usize;
709                    if qi >= N {
710                        continue;
711                    }
712                    if let Some(u) = gate_to_single_mat(gate.as_ref()) {
713                        // Contract: new_Γ[α, σ', β] = Σ_σ U[σ', σ] * Γ[α, σ, β]
714                        // Current shape: [χ_l, 2, χ_r] — chi_l=1, chi_r=1 for initial state
715                        // Since we store flat [2] for the initial product state:
716                        let old = site_tensors[qi].clone();
717                        let phys = old.len(); // = 2 * χ_l * χ_r in general
718                                              // Simple case: apply 2×2 unitary to physical index dimension 2
719                        let half = phys / 2;
720                        let mut new_site = vec![C64::new(0.0, 0.0); phys];
721                        for alpha in 0..half {
722                            let s0 = old[alpha]; // physical |0⟩
723                            let s1 = old[alpha + half]; // physical |1⟩
724                            new_site[alpha] = u[0] * s0 + u[1] * s1; // U[0,0]*|0⟩ + U[0,1]*|1⟩
725                            new_site[alpha + half] = u[2] * s0 + u[3] * s1; // U[1,0]*|0⟩ + U[1,1]*|1⟩
726                        }
727                        site_tensors[qi] = new_site;
728                    }
729                }
730                2 => {
731                    let qi = qubits[0].id() as usize;
732                    let qj = qubits[1].id() as usize;
733                    // Only handle adjacent qubits (i, i+1)
734                    if qi >= N || qj >= N || qj != qi + 1 {
735                        continue;
736                    }
737                    let gate_name = gate.name();
738                    let unitary_mat: [C64; 16] = if gate_name == "CNOT" || gate_name == "CX" {
739                        cnot_mat
740                    } else {
741                        // Identity 4×4 for unsupported two-qubit gates
742                        let mut id = [C64::new(0.0, 0.0); 16];
743                        id[0] = C64::new(1.0, 0.0);
744                        id[5] = C64::new(1.0, 0.0);
745                        id[10] = C64::new(1.0, 0.0);
746                        id[15] = C64::new(1.0, 0.0);
747                        id
748                    };
749
750                    // Left site: [χ_l, 2, χ_m], right site: [χ_m, 2, χ_r]
751                    // Merge into: Θ[χ_l * 2, 2 * χ_r] via Θ[α*2+σ, σ'*χ_r+β] = Σ_m Γ_i[α,σ,m] * Γ_j[m,σ',β]
752                    let left = &site_tensors[qi];
753                    let right = &site_tensors[qj];
754                    let left_phys = left.len(); // χ_l * 2
755                    let right_phys = right.len(); // 2 * χ_r
756                    let chi_m = bond_dims.get(qi).copied().unwrap_or(1);
757                    let chi_l = left_phys / 2; // should equal χ_l * χ_m / χ_m
758                    let chi_r = right_phys / 2; // should equal χ_m * χ_r / χ_m
759
760                    // Build merged tensor Θ: shape [chi_l * 2, chi_r * 2]
761                    // Index convention: row = (chi_l_idx * 2 + sigma_i), col = (sigma_j * chi_r + chi_r_idx)
762                    let nrows = chi_l * 2;
763                    let ncols = chi_r * 2;
764                    let mut theta = vec![C64::new(0.0, 0.0); nrows * ncols];
765
766                    // Contract over χ_m (bond index between site qi and qj)
767                    // left[alpha, sigma_i] = left_flat[sigma_i * chi_l + alpha]  (stored as [phys_0, phys_1])
768                    // right[sigma_j, beta] = right_flat[sigma_j * chi_r + beta]
769                    for sigma_i in 0..2usize {
770                        for alpha in 0..chi_l {
771                            let l_val = left
772                                .get(sigma_i * chi_l + alpha)
773                                .copied()
774                                .unwrap_or(C64::new(0.0, 0.0));
775                            for sigma_j in 0..2usize {
776                                for beta in 0..chi_r {
777                                    let r_val = right
778                                        .get(sigma_j * chi_r + beta)
779                                        .copied()
780                                        .unwrap_or(C64::new(0.0, 0.0));
781                                    let row = alpha * 2 + sigma_i;
782                                    let col = sigma_j * chi_r + beta;
783                                    if row < nrows && col < ncols {
784                                        theta[row * ncols + col] += l_val * r_val;
785                                    }
786                                }
787                            }
788                        }
789                    }
790
791                    // Apply two-qubit unitary: Θ' = U * Θ (in the combined physical index space)
792                    // U acts on (σ_i, σ_j) space (4×4), Θ rows ~ (α, σ_i), Θ cols ~ (σ_j, β)
793                    // Θ'[α*2+σ'_i, σ'_j*χ_r+β] = Σ_{σ_i, σ_j} U[σ'_i*2+σ'_j, σ_i*2+σ_j] * Θ[α*2+σ_i, σ_j*χ_r+β]
794                    let mut theta_prime = vec![C64::new(0.0, 0.0); nrows * ncols];
795                    for alpha in 0..chi_l {
796                        for sigma_i_out in 0..2usize {
797                            for sigma_j_out in 0..2usize {
798                                for beta in 0..chi_r {
799                                    let row_out = alpha * 2 + sigma_i_out;
800                                    let col_out = sigma_j_out * chi_r + beta;
801                                    let mut val = C64::new(0.0, 0.0);
802                                    for sigma_i_in in 0..2usize {
803                                        for sigma_j_in in 0..2usize {
804                                            let u_idx = (sigma_i_out * 2 + sigma_j_out) * 4
805                                                + sigma_i_in * 2
806                                                + sigma_j_in;
807                                            let u_val = unitary_mat
808                                                .get(u_idx)
809                                                .copied()
810                                                .unwrap_or(C64::new(0.0, 0.0));
811                                            let row_in = alpha * 2 + sigma_i_in;
812                                            let col_in = sigma_j_in * chi_r + beta;
813                                            val += u_val
814                                                * theta
815                                                    .get(row_in * ncols + col_in)
816                                                    .copied()
817                                                    .unwrap_or(C64::new(0.0, 0.0));
818                                        }
819                                    }
820                                    if row_out < nrows && col_out < ncols {
821                                        theta_prime[row_out * ncols + col_out] = val;
822                                    }
823                                }
824                            }
825                        }
826                    }
827
828                    // SVD on the real part to get new bond dimension
829                    let real_mat_data: Vec<f64> = theta_prime.iter().map(|c| c.re).collect();
830                    let real_mat =
831                        Array2::from_shape_vec((nrows, ncols), real_mat_data).map_err(|e| {
832                            QuantRS2Error::RuntimeError(format!("MPS matrix reshape failed: {e}"))
833                        })?;
834
835                    let svd_res = svd(&real_mat.view(), false, None)
836                        .map_err(|e| QuantRS2Error::RuntimeError(format!("MPS SVD failed: {e}")));
837
838                    let (u_mat, s_vec, vt_mat) = match svd_res {
839                        Ok(r) => r,
840                        Err(_) => {
841                            // Fallback: keep tensors unchanged
842                            continue;
843                        }
844                    };
845
846                    // Truncate bond dimension to max_bd
847                    let new_chi_m = s_vec.len().min(max_bd);
848
849                    // Reconstruct left site: shape [chi_l * 2, new_chi_m]
850                    // new_left[row, k] = U[row, k] * sqrt(s[k])
851                    let mut new_left = vec![C64::new(0.0, 0.0); chi_l * 2 * new_chi_m];
852                    for row in 0..nrows {
853                        for k in 0..new_chi_m {
854                            let sv = s_vec[k].max(0.0).sqrt();
855                            let idx = row * new_chi_m + k;
856                            new_left[idx] = C64::new(u_mat[[row, k]] * sv, 0.0);
857                        }
858                    }
859
860                    // Reconstruct right site: shape [new_chi_m, chi_r * 2]
861                    // new_right[k, col] = sqrt(s[k]) * Vt[k, col]
862                    let mut new_right = vec![C64::new(0.0, 0.0); new_chi_m * chi_r * 2];
863                    for k in 0..new_chi_m {
864                        let sv = s_vec[k].max(0.0).sqrt();
865                        for col in 0..ncols {
866                            let idx = k * ncols + col;
867                            new_right[idx] = C64::new(vt_mat[[k, col]] * sv, 0.0);
868                        }
869                    }
870
871                    site_tensors[qi] = new_left;
872                    site_tensors[qj] = new_right;
873                    if qi < bond_dims.len() {
874                        bond_dims[qi] = new_chi_m;
875                    }
876                }
877                _ => {
878                    // Multi-qubit gates beyond 2-qubit: skip
879                }
880            }
881        }
882
883        // Build the site Tensors with correct shape annotations
884        let tensors: Vec<Tensor> = site_tensors
885            .into_iter()
886            .enumerate()
887            .map(|(i, data)| {
888                let chi_l = if i == 0 { 1 } else { bond_dims[i - 1] };
889                let chi_r = if i + 1 < N { bond_dims[i] } else { 1 };
890                let shape = vec![chi_l, 2, chi_r];
891                let indices = vec![
892                    format!("bond_left_{i}"),
893                    format!("phys_{i}"),
894                    format!("bond_right_{i}"),
895                ];
896                // Ensure data length matches shape product
897                let expected = chi_l * 2 * chi_r;
898                let mut padded = data;
899                padded.resize(expected, C64::new(0.0, 0.0));
900                Tensor::new(padded, shape, indices)
901            })
902            .collect();
903
904        Ok(Self {
905            tensors,
906            bond_dims,
907            n_qubits: N,
908        })
909    }
910
911    /// Compress the MPS via a left-to-right SVD sweep with bond truncation.
912    ///
913    /// For each bond between site i and i+1:
914    /// 1. Reshape tensors\[i\] (shape \[χ_l, 2, χ_m\]) and tensors\[i+1\] (shape \[χ_m, 2, χ_r\])
915    ///    into a combined matrix Θ of shape \[χ_l\*2, χ_r\*2\].
916    /// 2. Compute SVD Θ = U Σ Vt.
917    /// 3. Truncate to min(max_bond_dim, rank where σ_k / σ_0 > tolerance).
918    /// 4. Set tensors\[i\] = U\[:, :new_χ\] \* diag(Σ\[:new_χ\])^(1/2),
919    ///    tensors\[i+1\] = diag(Σ\[:new_χ\])^(1/2) \* Vt\[:new_χ, :\].
920    pub fn compress(&mut self, max_bond_dim: usize, tolerance: f64) -> QuantRS2Result<()> {
921        let n = self.n_qubits;
922        if n <= 1 {
923            return Ok(());
924        }
925
926        for i in 0..(n - 1) {
927            if i + 1 >= self.tensors.len() {
928                break;
929            }
930
931            let chi_l_i = self.tensors[i].shape.first().copied().unwrap_or(1);
932            let chi_r_i = self.tensors[i].shape.get(2).copied().unwrap_or(1); // = chi_m
933            let chi_r_j = self.tensors[i + 1].shape.get(2).copied().unwrap_or(1);
934
935            let nrows = chi_l_i * 2;
936            let ncols = chi_r_j * 2;
937
938            // Build combined real-valued matrix from amplitudes
939            // Θ[alpha*2+sigma_i, sigma_j*chi_r_j+beta] = Σ_m Γ_i[alpha,sigma_i,m] * Γ_{i+1}[m,sigma_j,beta]
940            let left = &self.tensors[i].data;
941            let right = &self.tensors[i + 1].data;
942            let mut theta_real = vec![0.0f64; nrows * ncols];
943
944            for alpha in 0..chi_l_i {
945                for sigma_i in 0..2usize {
946                    for m in 0..chi_r_i {
947                        let l_idx = (alpha * 2 + sigma_i) * chi_r_i + m;
948                        let l_val = left.get(l_idx).map(|c| c.re).unwrap_or(0.0);
949                        if l_val == 0.0 {
950                            continue;
951                        }
952                        for sigma_j in 0..2usize {
953                            for beta in 0..chi_r_j {
954                                let r_idx = (m * 2 + sigma_j) * chi_r_j + beta;
955                                let r_val = right.get(r_idx).map(|c| c.re).unwrap_or(0.0);
956                                let row = alpha * 2 + sigma_i;
957                                let col = sigma_j * chi_r_j + beta;
958                                if row < nrows && col < ncols {
959                                    theta_real[row * ncols + col] += l_val * r_val;
960                                }
961                            }
962                        }
963                    }
964                }
965            }
966
967            let mat = Array2::from_shape_vec((nrows, ncols), theta_real).map_err(|e| {
968                QuantRS2Error::RuntimeError(format!("MPS compress reshape failed: {e}"))
969            })?;
970
971            let svd_res = svd(&mat.view(), false, None).map_err(|e| {
972                QuantRS2Error::RuntimeError(format!("MPS compress SVD failed at bond {i}: {e}"))
973            });
974
975            let (u_mat, s_vec, vt_mat) = match svd_res {
976                Ok(r) => r,
977                Err(_) => continue,
978            };
979
980            // Determine truncation rank
981            let sigma_max = s_vec.first().copied().unwrap_or(0.0);
982            let rank = if sigma_max > 0.0 {
983                s_vec
984                    .iter()
985                    .take_while(|&&sv| sv / sigma_max > tolerance)
986                    .count()
987            } else {
988                1
989            };
990            let new_chi_m = rank.min(max_bond_dim).min(s_vec.len()).max(1);
991
992            // Rebuild left tensor: shape [chi_l_i, 2, new_chi_m]
993            let new_left_size = chi_l_i * 2 * new_chi_m;
994            let mut new_left = vec![C64::new(0.0, 0.0); new_left_size];
995            for row in 0..(chi_l_i * 2) {
996                for k in 0..new_chi_m {
997                    let sv = s_vec[k].max(0.0).sqrt();
998                    let flat_idx = row * new_chi_m + k;
999                    new_left[flat_idx] = C64::new(u_mat[[row, k]] * sv, 0.0);
1000                }
1001            }
1002
1003            // Rebuild right tensor: shape [new_chi_m, 2, chi_r_j]
1004            let new_right_size = new_chi_m * 2 * chi_r_j;
1005            let mut new_right = vec![C64::new(0.0, 0.0); new_right_size];
1006            for k in 0..new_chi_m {
1007                let sv = s_vec[k].max(0.0).sqrt();
1008                for col in 0..(2 * chi_r_j) {
1009                    let flat_idx = k * 2 * chi_r_j + col;
1010                    new_right[flat_idx] = C64::new(vt_mat[[k, col]] * sv, 0.0);
1011                }
1012            }
1013
1014            // Update tensors in place
1015            self.tensors[i].data = new_left;
1016            self.tensors[i].shape = vec![chi_l_i, 2, new_chi_m];
1017
1018            self.tensors[i + 1].data = new_right;
1019            self.tensors[i + 1].shape = vec![new_chi_m, 2, chi_r_j];
1020
1021            if i < self.bond_dims.len() {
1022                self.bond_dims[i] = new_chi_m;
1023            }
1024        }
1025
1026        Ok(())
1027    }
1028
1029    /// Calculate overlap with another MPS
1030    pub fn overlap(&self, other: &Self) -> QuantRS2Result<C64> {
1031        if self.n_qubits != other.n_qubits {
1032            return Err(QuantRS2Error::InvalidInput(
1033                "MPS have different number of qubits".to_string(),
1034            ));
1035        }
1036
1037        // Calculate ⟨ψ|φ⟩
1038        Ok(C64::new(1.0, 0.0)) // Placeholder
1039    }
1040
1041    /// Calculate expectation value of observable
1042    pub const fn expectation_value(&self, observable: &TensorNetwork) -> QuantRS2Result<f64> {
1043        // Calculate ⟨ψ|O|ψ⟩
1044        Ok(0.0) // Placeholder
1045    }
1046}
1047
1048/// Circuit compression using tensor networks
1049pub struct TensorNetworkCompressor {
1050    /// Maximum bond dimension
1051    max_bond_dim: usize,
1052    /// Truncation tolerance
1053    tolerance: f64,
1054    /// Compression method
1055    method: CompressionMethod,
1056}
1057
1058#[derive(Debug, Clone)]
1059pub enum CompressionMethod {
1060    /// Singular Value Decomposition
1061    SVD,
1062    /// Density Matrix Renormalization Group
1063    DMRG,
1064    /// Time-Evolving Block Decimation
1065    TEBD,
1066}
1067
1068impl TensorNetworkCompressor {
1069    /// Create a new compressor
1070    #[must_use]
1071    pub const fn new(max_bond_dim: usize) -> Self {
1072        Self {
1073            max_bond_dim,
1074            tolerance: 1e-10,
1075            method: CompressionMethod::SVD,
1076        }
1077    }
1078
1079    /// Set compression method
1080    #[must_use]
1081    pub const fn with_method(mut self, method: CompressionMethod) -> Self {
1082        self.method = method;
1083        self
1084    }
1085
1086    /// Compress a circuit
1087    pub fn compress<const N: usize>(
1088        &self,
1089        circuit: &Circuit<N>,
1090    ) -> QuantRS2Result<CompressedCircuit<N>> {
1091        let mps = MatrixProductState::from_circuit(circuit)?;
1092
1093        Ok(CompressedCircuit {
1094            mps,
1095            original_gates: circuit.num_gates(),
1096            compression_ratio: 1.0, // Placeholder
1097        })
1098    }
1099}
1100
1101/// Compressed circuit representation
1102#[derive(Debug)]
1103pub struct CompressedCircuit<const N: usize> {
1104    /// MPS representation
1105    mps: MatrixProductState,
1106    /// Original number of gates
1107    original_gates: usize,
1108    /// Compression ratio
1109    compression_ratio: f64,
1110}
1111
1112impl<const N: usize> CompressedCircuit<N> {
1113    /// Get compression ratio
1114    #[must_use]
1115    pub const fn compression_ratio(&self) -> f64 {
1116        self.compression_ratio
1117    }
1118
1119    /// Decompress back to circuit
1120    pub fn decompress(&self) -> QuantRS2Result<Circuit<N>> {
1121        // Convert MPS back to circuit representation
1122        // This is non-trivial and would require gate synthesis
1123        Ok(Circuit::<N>::new())
1124    }
1125
1126    /// Get fidelity with original circuit
1127    pub const fn fidelity(&self, original: &Circuit<N>) -> QuantRS2Result<f64> {
1128        // Calculate |⟨ψ_compressed|ψ_original⟩|²
1129        Ok(0.99) // Placeholder
1130    }
1131}
1132
1133#[cfg(test)]
1134mod tests {
1135    use super::*;
1136    use quantrs2_core::gate::single::Hadamard;
1137
1138    #[test]
1139    fn test_tensor_creation() {
1140        let data = vec![
1141            C64::new(1.0, 0.0),
1142            C64::new(0.0, 0.0),
1143            C64::new(0.0, 0.0),
1144            C64::new(1.0, 0.0),
1145        ];
1146        let tensor = Tensor::new(data, vec![2, 2], vec!["in".to_string(), "out".to_string()]);
1147
1148        assert_eq!(tensor.rank(), 2);
1149        assert_eq!(tensor.size(), 4);
1150    }
1151
1152    #[test]
1153    fn test_contract_matrix_product() {
1154        // A = [[1,2],[3,4]] with indices (i, k), row-major.
1155        let a = Tensor::new(
1156            vec![
1157                C64::new(1.0, 0.0),
1158                C64::new(2.0, 0.0),
1159                C64::new(3.0, 0.0),
1160                C64::new(4.0, 0.0),
1161            ],
1162            vec![2, 2],
1163            vec!["i".to_string(), "k".to_string()],
1164        );
1165        // B = [[5,6],[7,8]] with indices (k, j), row-major.
1166        let b = Tensor::new(
1167            vec![
1168                C64::new(5.0, 0.0),
1169                C64::new(6.0, 0.0),
1170                C64::new(7.0, 0.0),
1171                C64::new(8.0, 0.0),
1172            ],
1173            vec![2, 2],
1174            vec!["k".to_string(), "j".to_string()],
1175        );
1176
1177        // Contract over shared index k => matrix product A·B = [[19,22],[43,50]].
1178        let result = a.contract(&b, "k", "k").expect("contraction must succeed");
1179
1180        assert_eq!(result.shape, vec![2, 2]);
1181        assert_eq!(result.indices, vec!["i".to_string(), "j".to_string()]);
1182
1183        let expected = [19.0, 22.0, 43.0, 50.0];
1184        for (got, want) in result.data.iter().zip(expected.iter()) {
1185            assert!(
1186                (got.re - want).abs() < 1e-12,
1187                "got {}, want {}",
1188                got.re,
1189                want
1190            );
1191            assert!(got.im.abs() < 1e-12);
1192        }
1193    }
1194
1195    #[test]
1196    fn test_contract_rectangular_strides() {
1197        // A shape [2,3] indices (i, k): rows [[1,2,3],[4,5,6]].
1198        let a = Tensor::new(
1199            (1..=6).map(|v| C64::new(f64::from(v), 0.0)).collect(),
1200            vec![2, 3],
1201            vec!["i".to_string(), "k".to_string()],
1202        );
1203        // B shape [3,2] indices (k, j): rows [[7,8],[9,10],[11,12]].
1204        let b = Tensor::new(
1205            (7..=12).map(|v| C64::new(f64::from(v), 0.0)).collect(),
1206            vec![3, 2],
1207            vec!["k".to_string(), "j".to_string()],
1208        );
1209
1210        let result = a.contract(&b, "k", "k").expect("contraction must succeed");
1211        assert_eq!(result.shape, vec![2, 2]);
1212
1213        // A·B = [[1*7+2*9+3*11, 1*8+2*10+3*12],
1214        //        [4*7+5*9+6*11, 4*8+5*10+6*12]]
1215        //     = [[58, 64], [139, 154]]
1216        let expected = [58.0, 64.0, 139.0, 154.0];
1217        for (got, want) in result.data.iter().zip(expected.iter()) {
1218            assert!(
1219                (got.re - want).abs() < 1e-12,
1220                "got {}, want {}",
1221                got.re,
1222                want
1223            );
1224        }
1225    }
1226
1227    #[test]
1228    fn test_tensor_network() {
1229        let mut tn = TensorNetwork::new();
1230
1231        let t1 = Tensor::identity(2, "a".to_string(), "b".to_string());
1232        let t2 = Tensor::identity(2, "c".to_string(), "d".to_string());
1233
1234        let idx1 = tn.add_tensor(t1);
1235        let idx2 = tn.add_tensor(t2);
1236
1237        tn.add_bond(idx1, "b".to_string(), idx2, "c".to_string())
1238            .expect("Failed to add bond between tensors");
1239
1240        assert_eq!(tn.tensors.len(), 2);
1241        assert_eq!(tn.bonds.len(), 1);
1242    }
1243
1244    #[test]
1245    fn test_circuit_to_tensor_network() {
1246        let mut circuit = Circuit::<2>::new();
1247        circuit
1248            .add_gate(Hadamard { target: QubitId(0) })
1249            .expect("Failed to add Hadamard gate");
1250
1251        let converter = CircuitToTensorNetwork::<2>::new();
1252        let tn = converter
1253            .convert(&circuit)
1254            .expect("Failed to convert circuit to tensor network");
1255
1256        assert!(!tn.tensors.is_empty());
1257    }
1258
1259    #[test]
1260    fn test_compression() {
1261        let circuit = Circuit::<2>::new();
1262        let compressor = TensorNetworkCompressor::new(32);
1263
1264        let compressed = compressor
1265            .compress(&circuit)
1266            .expect("Failed to compress circuit");
1267        assert!(compressed.compression_ratio() <= 1.0);
1268    }
1269
1270    #[test]
1271    fn test_tensor_network_svd_compress() {
1272        use quantrs2_core::gate::multi::CNOT;
1273
1274        // Build a circuit with a Hadamard + CNOT (Bell state preparation)
1275        let mut circuit = Circuit::<2>::new();
1276        circuit
1277            .add_gate(Hadamard { target: QubitId(0) })
1278            .expect("H gate");
1279        circuit
1280            .add_gate(CNOT {
1281                control: QubitId(0),
1282                target: QubitId(1),
1283            })
1284            .expect("CNOT gate");
1285
1286        let converter = CircuitToTensorNetwork::<2>::new();
1287        let mut tn = converter.convert(&circuit).expect("Convert to TN");
1288
1289        // Compress with max bond dim 4 and tolerance 1e-6
1290        tn.compress(4, 1e-6).expect("TN compress");
1291        // If we got here without panic, the test passes; verify structure
1292        assert_eq!(tn.tensors.len(), 2);
1293    }
1294
1295    #[test]
1296    fn test_mps_from_circuit_trivial() {
1297        // Empty circuit → valid MPS
1298        let circuit = Circuit::<2>::new();
1299        let mps = MatrixProductState::from_circuit(&circuit).expect("MPS from empty circuit");
1300        assert_eq!(mps.n_qubits, 2);
1301        assert_eq!(mps.tensors.len(), 2);
1302    }
1303
1304    #[test]
1305    fn test_mps_from_circuit_with_hadamard() {
1306        use quantrs2_core::gate::single::Hadamard;
1307
1308        let mut circuit = Circuit::<3>::new();
1309        circuit
1310            .add_gate(Hadamard { target: QubitId(0) })
1311            .expect("H gate");
1312
1313        let mps = MatrixProductState::from_circuit(&circuit).expect("MPS from H circuit");
1314        assert_eq!(mps.n_qubits, 3);
1315        assert_eq!(mps.tensors.len(), 3);
1316    }
1317
1318    #[test]
1319    fn test_mps_compress_reduces_bond_dim() {
1320        use quantrs2_core::gate::multi::CNOT;
1321
1322        // Bell state: H + CNOT should create a non-trivial entangled MPS
1323        let mut circuit = Circuit::<2>::new();
1324        circuit
1325            .add_gate(Hadamard { target: QubitId(0) })
1326            .expect("H gate");
1327        circuit
1328            .add_gate(CNOT {
1329                control: QubitId(0),
1330                target: QubitId(1),
1331            })
1332            .expect("CNOT gate");
1333
1334        let mut mps = MatrixProductState::from_circuit(&circuit).expect("MPS from Bell circuit");
1335
1336        // Compress with max bond dim 1 (strong truncation)
1337        mps.compress(1, 1e-10).expect("MPS compress");
1338        // Bond dims should be ≤ max_bond_dim
1339        for &bd in &mps.bond_dims {
1340            assert!(bd <= 1, "Bond dim {} exceeds max", bd);
1341        }
1342    }
1343}