Skip to main content

quantrs2_core/
tensor_network.rs

1//! Tensor Network representations for quantum circuits
2//!
3//! This module provides tensor network representations and operations for quantum circuits,
4//! leveraging SciRS2 for efficient tensor manipulations and contractions.
5
6use crate::{
7    error::{QuantRS2Error, QuantRS2Result},
8    gate::GateOp,
9    linalg_stubs::svd,
10    register::Register,
11};
12use scirs2_core::ndarray::{Array, Array2, ArrayD, IxDyn};
13use scirs2_core::Complex;
14// use scirs2_linalg::svd;
15use std::collections::{HashMap, HashSet};
16
17/// Type alias for complex numbers
18type Complex64 = Complex<f64>;
19
20/// A tensor in the network
21#[derive(Debug, Clone)]
22pub struct Tensor {
23    /// Unique identifier for the tensor
24    pub id: usize,
25    /// The tensor data
26    pub data: ArrayD<Complex64>,
27    /// Labels for each index of the tensor
28    pub indices: Vec<String>,
29    /// Shape of the tensor
30    pub shape: Vec<usize>,
31}
32
33impl Tensor {
34    /// Create a new tensor
35    pub fn new(id: usize, data: ArrayD<Complex64>, indices: Vec<String>) -> Self {
36        let shape = data.shape().to_vec();
37        Self {
38            id,
39            data,
40            indices,
41            shape,
42        }
43    }
44
45    /// Create a tensor from a 2D array (matrix)
46    pub fn from_matrix(
47        id: usize,
48        matrix: Array2<Complex64>,
49        in_idx: String,
50        out_idx: String,
51    ) -> Self {
52        let shape = matrix.shape().to_vec();
53        let data = matrix.into_dyn();
54        Self {
55            id,
56            data,
57            indices: vec![in_idx, out_idx],
58            shape,
59        }
60    }
61
62    /// Create a qubit tensor in |0⟩ state
63    pub fn qubit_zero(id: usize, idx: String) -> Self {
64        let mut data = Array::zeros(IxDyn(&[2]));
65        data[[0]] = Complex64::new(1.0, 0.0);
66        Self {
67            id,
68            data,
69            indices: vec![idx],
70            shape: vec![2],
71        }
72    }
73
74    /// Create a qubit tensor in |1⟩ state
75    pub fn qubit_one(id: usize, idx: String) -> Self {
76        let mut data = Array::zeros(IxDyn(&[2]));
77        data[[1]] = Complex64::new(1.0, 0.0);
78        Self {
79            id,
80            data,
81            indices: vec![idx],
82            shape: vec![2],
83        }
84    }
85
86    /// Create a tensor from an ndarray with specified indices
87    pub fn from_array<D>(
88        array: scirs2_core::ndarray::ArrayBase<scirs2_core::ndarray::OwnedRepr<Complex64>, D>,
89        indices: Vec<usize>,
90    ) -> Self
91    where
92        D: scirs2_core::ndarray::Dimension,
93    {
94        let shape = array.shape().to_vec();
95        let data = array.into_dyn();
96        let index_labels: Vec<String> = indices.iter().map(|i| format!("idx_{i}")).collect();
97        Self {
98            id: 0, // Default ID
99            data,
100            indices: index_labels,
101            shape,
102        }
103    }
104
105    /// Get the rank (number of indices) of the tensor
106    pub fn rank(&self) -> usize {
107        self.indices.len()
108    }
109
110    /// Get a reference to the tensor data
111    pub const fn tensor(&self) -> &ArrayD<Complex64> {
112        &self.data
113    }
114
115    /// Get the number of dimensions
116    pub fn ndim(&self) -> usize {
117        self.data.ndim()
118    }
119
120    /// Contract this tensor with another over specified indices
121    pub fn contract(&self, other: &Self, self_idx: &str, other_idx: &str) -> QuantRS2Result<Self> {
122        // Find the positions of the indices to contract
123        let self_pos = self
124            .indices
125            .iter()
126            .position(|s| s == self_idx)
127            .ok_or_else(|| {
128                QuantRS2Error::InvalidInput(format!("Index {self_idx} not found in tensor"))
129            })?;
130        let other_pos = other
131            .indices
132            .iter()
133            .position(|s| s == other_idx)
134            .ok_or_else(|| {
135                QuantRS2Error::InvalidInput(format!("Index {other_idx} not found in tensor"))
136            })?;
137
138        // Check dimensions match
139        if self.shape[self_pos] != other.shape[other_pos] {
140            return Err(QuantRS2Error::InvalidInput(format!(
141                "Cannot contract indices with different dimensions: {} vs {}",
142                self.shape[self_pos], other.shape[other_pos]
143            )));
144        }
145
146        // Perform tensor contraction using einsum-like operation
147        let contracted = self.contract_indices(&other, self_pos, other_pos)?;
148
149        // Build new index list
150        let mut new_indices = Vec::new();
151        for (i, idx) in self.indices.iter().enumerate() {
152            if i != self_pos {
153                new_indices.push(idx.clone());
154            }
155        }
156        for (i, idx) in other.indices.iter().enumerate() {
157            if i != other_pos {
158                new_indices.push(idx.clone());
159            }
160        }
161
162        Ok(Self::new(
163            self.id.max(other.id) + 1,
164            contracted,
165            new_indices,
166        ))
167    }
168
169    /// Perform the actual index contraction
170    fn contract_indices(
171        &self,
172        other: &Self,
173        self_idx: usize,
174        other_idx: usize,
175    ) -> QuantRS2Result<ArrayD<Complex64>> {
176        // Reshape tensors for matrix multiplication
177        let self_shape = self.data.shape();
178        let other_shape = other.data.shape();
179
180        // Calculate dimensions for reshaping
181        let mut self_left_dims = 1;
182        let mut self_right_dims = 1;
183        for i in 0..self_idx {
184            self_left_dims *= self_shape[i];
185        }
186        for i in (self_idx + 1)..self_shape.len() {
187            self_right_dims *= self_shape[i];
188        }
189
190        let mut other_left_dims = 1;
191        let mut other_right_dims = 1;
192        for i in 0..other_idx {
193            other_left_dims *= other_shape[i];
194        }
195        for i in (other_idx + 1)..other_shape.len() {
196            other_right_dims *= other_shape[i];
197        }
198
199        let contract_dim = self_shape[self_idx];
200
201        // Reshape to matrices
202        let self_mat = self
203            .data
204            .view()
205            .into_shape_with_order((self_left_dims, contract_dim * self_right_dims))
206            .map_err(|e| QuantRS2Error::InvalidInput(format!("Shape error: {e}")))?
207            .to_owned();
208        let other_mat = other
209            .data
210            .view()
211            .into_shape_with_order((other_left_dims * contract_dim, other_right_dims))
212            .map_err(|e| QuantRS2Error::InvalidInput(format!("Shape error: {e}")))?
213            .to_owned();
214
215        // Perform contraction via matrix multiplication
216        let _result_mat: Array2<Complex64> = Array2::zeros((
217            self_left_dims * self_right_dims,
218            other_left_dims * other_right_dims,
219        ));
220
221        // This is a simplified contraction - a full implementation would be more efficient
222        let mut result_vec = Vec::new();
223        for i in 0..self_left_dims {
224            for j in 0..self_right_dims {
225                for k in 0..other_left_dims {
226                    for l in 0..other_right_dims {
227                        let mut sum = Complex64::new(0.0, 0.0);
228                        for c in 0..contract_dim {
229                            // Commented out - index calculations unused
230                            // let _ = i * contract_dim * self_right_dims + c * self_right_dims + j;
231                            // let _ = k * contract_dim * other_right_dims + c * other_right_dims + l;
232                            sum += self_mat[[i, c * self_right_dims + j]]
233                                * other_mat[[k * contract_dim + c, l]];
234                        }
235                        result_vec.push(sum);
236                    }
237                }
238            }
239        }
240
241        // Build result shape
242        let mut result_shape = Vec::new();
243        for i in 0..self_idx {
244            result_shape.push(self_shape[i]);
245        }
246        for i in (self_idx + 1)..self_shape.len() {
247            result_shape.push(self_shape[i]);
248        }
249        for i in 0..other_idx {
250            result_shape.push(other_shape[i]);
251        }
252        for i in (other_idx + 1)..other_shape.len() {
253            result_shape.push(other_shape[i]);
254        }
255
256        ArrayD::from_shape_vec(IxDyn(&result_shape), result_vec)
257            .map_err(|e| QuantRS2Error::InvalidInput(format!("Shape error: {e}")))
258    }
259
260    /// Apply SVD decomposition to split tensor along specified index
261    pub fn svd_decompose(
262        &self,
263        idx: usize,
264        max_rank: Option<usize>,
265    ) -> QuantRS2Result<(Self, Self)> {
266        if idx >= self.rank() {
267            return Err(QuantRS2Error::InvalidInput(format!(
268                "Index {} out of bounds for tensor with rank {}",
269                idx,
270                self.rank()
271            )));
272        }
273
274        // Reshape tensor into matrix
275        let shape = self.data.shape();
276        let mut left_dim = 1;
277        let mut right_dim = 1;
278
279        for i in 0..=idx {
280            left_dim *= shape[i];
281        }
282        for i in (idx + 1)..shape.len() {
283            right_dim *= shape[i];
284        }
285
286        // Convert to matrix
287        let matrix = self
288            .data
289            .view()
290            .into_shape_with_order((left_dim, right_dim))
291            .map_err(|e| QuantRS2Error::InvalidInput(format!("Shape error: {e}")))?
292            .to_owned();
293
294        // Perform SVD using SciRS2
295        let real_matrix = matrix.mapv(|c| c.re);
296        let (u, s, vt) = svd(&real_matrix.view(), false, None)
297            .map_err(|e| QuantRS2Error::ComputationError(format!("SVD failed: {e:?}")))?;
298
299        // Determine rank to keep
300        let rank = if let Some(max_r) = max_rank {
301            max_r.min(s.len())
302        } else {
303            s.len()
304        };
305
306        // Truncate based on rank
307        let u_trunc = u.slice(scirs2_core::ndarray::s![.., ..rank]).to_owned();
308        let s_trunc = s.slice(scirs2_core::ndarray::s![..rank]).to_owned();
309        let vt_trunc = vt.slice(scirs2_core::ndarray::s![..rank, ..]).to_owned();
310
311        // Create S matrix
312        let mut s_mat = Array2::zeros((rank, rank));
313        for i in 0..rank {
314            s_mat[[i, i]] = Complex64::new(s_trunc[i].sqrt(), 0.0);
315        }
316
317        // Multiply U * sqrt(S) and sqrt(S) * V^T
318        let left_data = u_trunc.mapv(|x| Complex64::new(x, 0.0)).dot(&s_mat);
319        let right_data = s_mat.dot(&vt_trunc.mapv(|x| Complex64::new(x, 0.0)));
320
321        // Create new tensors with appropriate shapes and indices
322        let mut left_indices = self.indices[..=idx].to_vec();
323        left_indices.push(format!("bond_{}", self.id));
324
325        let mut right_indices = vec![format!("bond_{}", self.id)];
326        right_indices.extend_from_slice(&self.indices[(idx + 1)..]);
327
328        let left_tensor = Self::new(self.id * 2, left_data.into_dyn(), left_indices);
329
330        let right_tensor = Self::new(self.id * 2 + 1, right_data.into_dyn(), right_indices);
331
332        Ok((left_tensor, right_tensor))
333    }
334}
335
336/// Edge in the tensor network
337#[derive(Debug, Clone, PartialEq, Eq, Hash)]
338pub struct TensorEdge {
339    /// First tensor ID
340    pub tensor1: usize,
341    /// Index on first tensor
342    pub index1: String,
343    /// Second tensor ID
344    pub tensor2: usize,
345    /// Index on second tensor
346    pub index2: String,
347}
348
349/// Tensor network representation
350#[derive(Debug)]
351pub struct TensorNetwork {
352    /// Tensors in the network
353    pub tensors: HashMap<usize, Tensor>,
354    /// Edges connecting tensors
355    pub edges: Vec<TensorEdge>,
356    /// Open indices (not connected to other tensors)
357    pub open_indices: HashMap<usize, Vec<String>>,
358    /// Next available tensor ID
359    next_id: usize,
360}
361
362impl TensorNetwork {
363    /// Create a new empty tensor network
364    pub fn new() -> Self {
365        Self {
366            tensors: HashMap::new(),
367            edges: Vec::new(),
368            open_indices: HashMap::new(),
369            next_id: 0,
370        }
371    }
372
373    /// Add a tensor to the network
374    pub fn add_tensor(&mut self, tensor: Tensor) -> usize {
375        let id = tensor.id;
376        self.open_indices.insert(id, tensor.indices.clone());
377        self.tensors.insert(id, tensor);
378        self.next_id = self.next_id.max(id + 1);
379        id
380    }
381
382    /// Connect two tensor indices
383    pub fn connect(
384        &mut self,
385        tensor1: usize,
386        index1: String,
387        tensor2: usize,
388        index2: String,
389    ) -> QuantRS2Result<()> {
390        // Verify tensors exist
391        if !self.tensors.contains_key(&tensor1) {
392            return Err(QuantRS2Error::InvalidInput(format!(
393                "Tensor {tensor1} not found"
394            )));
395        }
396        if !self.tensors.contains_key(&tensor2) {
397            return Err(QuantRS2Error::InvalidInput(format!(
398                "Tensor {tensor2} not found"
399            )));
400        }
401
402        // Verify indices exist and match dimensions
403        let t1 = &self.tensors[&tensor1];
404        let t2 = &self.tensors[&tensor2];
405
406        let idx1_pos = t1
407            .indices
408            .iter()
409            .position(|s| s == &index1)
410            .ok_or_else(|| {
411                QuantRS2Error::InvalidInput(format!("Index {index1} not found in tensor {tensor1}"))
412            })?;
413        let idx2_pos = t2
414            .indices
415            .iter()
416            .position(|s| s == &index2)
417            .ok_or_else(|| {
418                QuantRS2Error::InvalidInput(format!("Index {index2} not found in tensor {tensor2}"))
419            })?;
420
421        if t1.shape[idx1_pos] != t2.shape[idx2_pos] {
422            return Err(QuantRS2Error::InvalidInput(format!(
423                "Connected indices must have same dimension: {} vs {}",
424                t1.shape[idx1_pos], t2.shape[idx2_pos]
425            )));
426        }
427
428        // Add edge
429        self.edges.push(TensorEdge {
430            tensor1,
431            index1: index1.clone(),
432            tensor2,
433            index2: index2.clone(),
434        });
435
436        // Remove from open indices
437        if let Some(indices) = self.open_indices.get_mut(&tensor1) {
438            indices.retain(|s| s != &index1);
439        }
440        if let Some(indices) = self.open_indices.get_mut(&tensor2) {
441            indices.retain(|s| s != &index2);
442        }
443
444        Ok(())
445    }
446
447    /// Find optimal contraction order using greedy algorithm
448    pub fn find_contraction_order(&self) -> Vec<(usize, usize)> {
449        // Simple greedy algorithm: contract pairs that minimize intermediate tensor size
450        let mut remaining_tensors: HashSet<_> = self.tensors.keys().copied().collect();
451        let mut order = Vec::new();
452
453        // Build adjacency list
454        let mut adjacency: HashMap<usize, Vec<usize>> = HashMap::new();
455        for edge in &self.edges {
456            adjacency
457                .entry(edge.tensor1)
458                .or_insert_with(Vec::new)
459                .push(edge.tensor2);
460            adjacency
461                .entry(edge.tensor2)
462                .or_insert_with(Vec::new)
463                .push(edge.tensor1);
464        }
465
466        while remaining_tensors.len() > 1 {
467            let mut best_pair = None;
468            let mut min_cost = usize::MAX;
469
470            // Consider all pairs of connected tensors
471            for &t1 in &remaining_tensors {
472                if let Some(neighbors) = adjacency.get(&t1) {
473                    for &t2 in neighbors {
474                        if t2 > t1 && remaining_tensors.contains(&t2) {
475                            // Estimate cost as product of remaining dimensions
476                            let cost = self.estimate_contraction_cost(t1, t2);
477                            if cost < min_cost {
478                                min_cost = cost;
479                                best_pair = Some((t1, t2));
480                            }
481                        }
482                    }
483                }
484            }
485
486            if let Some((t1, t2)) = best_pair {
487                order.push((t1, t2));
488                remaining_tensors.remove(&t1);
489                remaining_tensors.remove(&t2);
490
491                // Add a virtual tensor representing the contraction result
492                let virtual_id = self.next_id + order.len();
493                remaining_tensors.insert(virtual_id);
494
495                // Update adjacency for virtual tensor
496                let mut virtual_neighbors = HashSet::new();
497                if let Some(n1) = adjacency.get(&t1) {
498                    virtual_neighbors.extend(
499                        n1.iter()
500                            .filter(|&&n| n != t2 && remaining_tensors.contains(&n)),
501                    );
502                }
503                if let Some(n2) = adjacency.get(&t2) {
504                    virtual_neighbors.extend(
505                        n2.iter()
506                            .filter(|&&n| n != t1 && remaining_tensors.contains(&n)),
507                    );
508                }
509                adjacency.insert(virtual_id, virtual_neighbors.into_iter().collect());
510            } else {
511                break;
512            }
513        }
514
515        order
516    }
517
518    /// Estimate the computational cost of contracting two tensors
519    const fn estimate_contraction_cost(&self, _t1: usize, _t2: usize) -> usize {
520        // Cost is roughly the product of all dimensions in the result
521        // This is a simplified estimate
522        1000 // Placeholder
523    }
524
525    /// Contract the entire network to a single tensor
526    pub fn contract_all(&mut self) -> QuantRS2Result<Tensor> {
527        if self.tensors.is_empty() {
528            return Err(QuantRS2Error::InvalidInput(
529                "Cannot contract empty tensor network".into(),
530            ));
531        }
532
533        if self.tensors.len() == 1 {
534            return self
535                .tensors
536                .values()
537                .next()
538                .map(|t| t.clone())
539                .ok_or_else(|| {
540                    QuantRS2Error::InvalidInput("Single tensor expected but not found".into())
541                });
542        }
543
544        // Find contraction order
545        let order = self.find_contraction_order();
546
547        // Execute contractions
548        let mut tensor_map = self.tensors.clone();
549        let mut next_id = self.next_id;
550
551        for (t1_id, t2_id) in order {
552            // Find the edge connecting these tensors
553            let edge = self
554                .edges
555                .iter()
556                .find(|e| {
557                    (e.tensor1 == t1_id && e.tensor2 == t2_id)
558                        || (e.tensor1 == t2_id && e.tensor2 == t1_id)
559                })
560                .ok_or_else(|| QuantRS2Error::InvalidInput("Tensors not connected".into()))?;
561
562            let t1 = tensor_map
563                .remove(&t1_id)
564                .ok_or_else(|| QuantRS2Error::InvalidInput("Tensor not found".into()))?;
565            let t2 = tensor_map
566                .remove(&t2_id)
567                .ok_or_else(|| QuantRS2Error::InvalidInput("Tensor not found".into()))?;
568
569            // Contract tensors
570            let contracted = if edge.tensor1 == t1_id {
571                t1.contract(&t2, &edge.index1, &edge.index2)?
572            } else {
573                t1.contract(&t2, &edge.index2, &edge.index1)?
574            };
575
576            // Add result back
577            let mut new_tensor = contracted;
578            new_tensor.id = next_id;
579            tensor_map.insert(next_id, new_tensor);
580            next_id += 1;
581        }
582
583        // Return the final tensor
584        tensor_map
585            .into_values()
586            .next()
587            .ok_or_else(|| QuantRS2Error::InvalidInput("Contraction failed".into()))
588    }
589
590    /// Decompose the (contracted) network into a Matrix Product State (MPS).
591    ///
592    /// The network is first contracted to a single tensor whose open indices are the
593    /// physical legs (each assumed dimension 2). A left-to-right sweep of singular-value
594    /// decompositions then factors that tensor into a chain of rank-3 site tensors
595    /// `A[0], …, A[n-1]` with bond indices between neighbours. Singular values are kept
596    /// up to `max_bond_dim` (when supplied), giving an exact MPS when the bond
597    /// dimension is unrestricted and an optimal truncation otherwise.
598    ///
599    /// The returned tensors carry indices `["phys_k", "bond_{k-1}", "bond_k"]` (the
600    /// boundary bonds are dimension 1), so contracting the chain reproduces the
601    /// original full tensor (up to the truncation error).
602    ///
603    /// Uses a complex one-sided Jacobi SVD (see [`Self::complex_svd`]) since the
604    /// SciRS2 LAPACK SVD currently exposes only the real-valued path.
605    ///
606    /// Note: the network must contract to a single tensor whose open legs are the
607    /// physical sites in order. Disconnected networks (e.g. an un-entangled product of
608    /// independent qubit lines) are limited by [`Self::contract_all`], which returns a
609    /// single connected component; build the network with entangling links between the
610    /// sites to be represented (as a real circuit does) for a faithful MPS.
611    pub fn to_mps(&self, max_bond_dim: Option<usize>) -> QuantRS2Result<Vec<Tensor>> {
612        // Contract the network to a single tensor (operate on a clone: to_mps is &self).
613        let mut work = TensorNetwork {
614            tensors: self.tensors.clone(),
615            edges: self.edges.clone(),
616            open_indices: self.open_indices.clone(),
617            next_id: self.next_id,
618        };
619        let full = work.contract_all()?;
620
621        // Flatten the full tensor into a vector using the *same* extraction as
622        // `to_statevector` (`into_raw_vec`), so the MPS represents exactly the state
623        // that `to_statevector` exposes. We treat the flattened amplitudes as a chain
624        // of qubits (physical dimension 2); the contracted tensor's reported axis
625        // layout may merge legs, so we derive the site count from the amplitude count.
626        let total: usize = full.shape.iter().product();
627        let flat: Vec<Complex64> = full.data.clone().into_raw_vec_and_offset().0;
628        if flat.len() != total {
629            return Err(QuantRS2Error::ComputationError(format!(
630                "contracted tensor buffer length {} does not match element count {total}",
631                flat.len()
632            )));
633        }
634
635        // Determine the number of qubit sites: total must be a power of two.
636        if total == 0 || (total & (total - 1)) != 0 {
637            return Err(QuantRS2Error::UnsupportedOperation(format!(
638                "MPS construction expects a qubit state (2^n amplitudes); got {total}"
639            )));
640        }
641        let n_sites = total.trailing_zeros() as usize;
642        if n_sites == 0 {
643            return Err(QuantRS2Error::InvalidInput(
644                "cannot build an MPS from a scalar (rank-0) tensor".into(),
645            ));
646        }
647        let phys_dims: Vec<usize> = vec![2usize; n_sites];
648
649        let mut mps = Vec::with_capacity(n_sites);
650
651        // `psi` holds the remaining (left_bond * rest) matrix as a flat row-major
652        // buffer with `left_bond` rows; initially left_bond = 1.
653        let mut left_bond = 1usize;
654        let mut psi = flat;
655        let mut remaining = total; // = product of physical dims not yet split off
656
657        for site in 0..n_sites {
658            let d = phys_dims[site];
659            remaining /= d;
660            // Reshape psi (left_bond x (d*remaining)) into a matrix M of shape
661            // (left_bond*d, remaining) so the SVD separates this site from the rest.
662            let rows = left_bond * d;
663            let cols = remaining;
664            let mut m = Array2::<Complex64>::zeros((rows, cols));
665            for lb in 0..left_bond {
666                for phys in 0..d {
667                    for rc in 0..cols {
668                        // psi index: ((lb)*d + phys)*cols + rc  (row-major over [lb, phys, rc])
669                        let src = (lb * d + phys) * cols + rc;
670                        m[[lb * d + phys, rc]] = psi[src];
671                    }
672                }
673            }
674
675            if site == n_sites - 1 {
676                // Last site: no further splitting; the whole matrix is the final
677                // tensor with right bond dimension 1.
678                let right_bond = 1usize;
679                // rows = left_bond * d, cols should be 1 here.
680                let data = Array::from_shape_vec(
681                    IxDyn(&[left_bond, d, right_bond]),
682                    (0..left_bond * d * right_bond)
683                        .map(|idx| {
684                            let lb = idx / d;
685                            let phys = idx % d;
686                            m[[lb * d + phys, 0]]
687                        })
688                        .collect(),
689                )
690                .map_err(|e| QuantRS2Error::InvalidInput(format!("Shape error: {e}")))?;
691                mps.push(Tensor::new(
692                    site,
693                    data,
694                    vec![
695                        format!("bond_{site}"),
696                        format!("phys_{site}"),
697                        format!("bond_{}", site + 1),
698                    ],
699                ));
700                break;
701            }
702
703            // SVD: M = U S V^H.
704            let (u, s, vh) = Self::complex_svd(&m)?;
705
706            // Determine kept rank (truncate tiny singular values and cap at max_bond_dim).
707            let mut rank = s.len();
708            let tol = 1e-12 * s.first().copied().unwrap_or(0.0).max(1.0);
709            while rank > 1 && s[rank - 1] <= tol {
710                rank -= 1;
711            }
712            if let Some(max_b) = max_bond_dim {
713                rank = rank.min(max_b.max(1));
714            }
715            rank = rank.max(1);
716
717            // Site tensor A[site] = U[:, :rank] reshaped to (left_bond, d, rank).
718            let mut a_data = Array::zeros(IxDyn(&[left_bond, d, rank]));
719            for lb in 0..left_bond {
720                for phys in 0..d {
721                    for r in 0..rank {
722                        a_data[[lb, phys, r]] = u[[lb * d + phys, r]];
723                    }
724                }
725            }
726            mps.push(Tensor::new(
727                site,
728                a_data,
729                vec![
730                    format!("bond_{site}"),
731                    format!("phys_{site}"),
732                    format!("bond_{}", site + 1),
733                ],
734            ));
735
736            // Form the remainder S[:rank] * V^H[:rank, :] as the new psi
737            // (shape rank x cols), which becomes the next iteration's left part.
738            let mut new_psi = vec![Complex64::new(0.0, 0.0); rank * cols];
739            for r in 0..rank {
740                let sigma = Complex64::new(s[r], 0.0);
741                for c in 0..cols {
742                    new_psi[r * cols + c] = sigma * vh[[r, c]];
743                }
744            }
745            psi = new_psi;
746            left_bond = rank;
747        }
748
749        Ok(mps)
750    }
751
752    /// Apply a Matrix Product Operator (MPO) to the specified physical qubits.
753    ///
754    /// Honest status: this `TensorNetwork` stores a general tensor network, not an MPS
755    /// state, so there is no canonical MPS chain for an MPO to act on in place. Applying
756    /// an MPO correctly requires first bringing the state into MPS form (see
757    /// [`Self::to_mps`]) and contracting the operator legs site-by-site. Rather than
758    /// silently doing nothing (the previous behaviour), this returns an explicit error.
759    pub fn apply_mpo(&mut self, _mpo: &[Tensor], _qubits: &[usize]) -> QuantRS2Result<()> {
760        Err(QuantRS2Error::UnsupportedOperation(
761            "MPO application requires MPS form; call to_mps first".into(),
762        ))
763    }
764
765    /// Complex one-sided Jacobi SVD: returns `(U, s, Vᴴ)` with `M = U·diag(s)·Vᴴ`,
766    /// `U` (m×k) and `Vᴴ` (k×n) having orthonormal rows/columns and `s` the singular
767    /// values in non-increasing order (`k = min(m, n)`).
768    ///
769    /// One-sided Jacobi rotates pairs of columns of `M` until they are mutually
770    /// orthogonal; the column norms are then the singular values and the accumulated
771    /// rotations form `V`. The method is numerically robust, handles repeated/zero
772    /// singular values gracefully, and works directly on complex data (unlike the
773    /// real-only LAPACK path currently exposed by SciRS2).
774    fn complex_svd(
775        m: &Array2<Complex64>,
776    ) -> QuantRS2Result<(Array2<Complex64>, Vec<f64>, Array2<Complex64>)> {
777        let (rows, cols) = (m.nrows(), m.ncols());
778
779        // Work on whichever orientation has at least as many rows as columns so that
780        // the column-orthogonalisation has full column rank handling; transpose back
781        // afterwards if needed.
782        let transposed = rows < cols;
783        let a0 = if transposed {
784            m.mapv(|z| z.conj()).t().to_owned() // (cols x rows)
785        } else {
786            m.clone()
787        };
788        let (p, q) = (a0.nrows(), a0.ncols()); // p >= q
789
790        let mut a = a0; // columns will be orthogonalised in place
791        let mut v = Array2::<Complex64>::eye(q); // accumulates right rotations
792
793        let max_sweeps = 60;
794        let eps = 1e-15;
795        for _sweep in 0..max_sweeps {
796            let mut off = 0.0_f64;
797            for i in 0..q {
798                for j in (i + 1)..q {
799                    // Compute the 2x2 Hermitian block of A^H A restricted to cols i, j.
800                    let mut alpha = 0.0_f64; // <a_i, a_i>
801                    let mut beta = 0.0_f64; // <a_j, a_j>
802                    let mut gamma = Complex64::new(0.0, 0.0); // <a_i, a_j>
803                    for r in 0..p {
804                        let ai = a[[r, i]];
805                        let aj = a[[r, j]];
806                        alpha += ai.norm_sqr();
807                        beta += aj.norm_sqr();
808                        gamma += ai.conj() * aj;
809                    }
810                    let gamma_abs = gamma.norm();
811                    off += gamma_abs;
812                    if gamma_abs <= eps * (alpha.sqrt() * beta.sqrt()).max(eps) {
813                        continue;
814                    }
815
816                    // Jacobi rotation that diagonalises [[alpha, gamma],[gamma*, beta]].
817                    // Phase factor to make the off-diagonal real-positive.
818                    let phase = gamma / gamma_abs;
819                    let zeta = (beta - alpha) / (2.0 * gamma_abs);
820                    let t = zeta.signum() / (zeta.abs() + (1.0 + zeta * zeta).sqrt());
821                    let c = 1.0 / (1.0 + t * t).sqrt();
822                    let sgn = c * t; // real sine magnitude
823                    let s_ij = phase * Complex64::new(sgn, 0.0);
824
825                    // Apply rotation to columns i, j of A:
826                    //   a_i' =  c·a_i - conj(s)·a_j
827                    //   a_j' =  s·a_i +      c·a_j
828                    for r in 0..p {
829                        let ai = a[[r, i]];
830                        let aj = a[[r, j]];
831                        a[[r, i]] = Complex64::new(c, 0.0) * ai - s_ij.conj() * aj;
832                        a[[r, j]] = s_ij * ai + Complex64::new(c, 0.0) * aj;
833                    }
834                    // Accumulate into V (same rotation on its columns).
835                    for r in 0..q {
836                        let vi = v[[r, i]];
837                        let vj = v[[r, j]];
838                        v[[r, i]] = Complex64::new(c, 0.0) * vi - s_ij.conj() * vj;
839                        v[[r, j]] = s_ij * vi + Complex64::new(c, 0.0) * vj;
840                    }
841                }
842            }
843            if off <= eps {
844                break;
845            }
846        }
847
848        // Singular values are the column norms of the orthogonalised A; U columns are
849        // the normalised columns.
850        let mut sigma: Vec<(f64, usize)> = (0..q)
851            .map(|j| {
852                let norm = (0..p).map(|r| a[[r, j]].norm_sqr()).sum::<f64>().sqrt();
853                (norm, j)
854            })
855            .collect();
856        // Sort singular values in non-increasing order.
857        sigma.sort_by(|x, y| y.0.total_cmp(&x.0));
858
859        let k = q; // number of singular values for the (p x q), p>=q orientation
860        let mut u_mat = Array2::<Complex64>::zeros((p, k));
861        let mut s_vec = vec![0.0_f64; k];
862        let mut v_sorted = Array2::<Complex64>::zeros((q, k));
863        for (new_idx, &(norm, old_idx)) in sigma.iter().enumerate() {
864            s_vec[new_idx] = norm;
865            if norm > 1e-300 {
866                for r in 0..p {
867                    u_mat[[r, new_idx]] = a[[r, old_idx]] / Complex64::new(norm, 0.0);
868                }
869            } else {
870                // Degenerate/zero column: leave U column zero (its singular value is 0).
871                u_mat[[0.min(p - 1), new_idx]] = Complex64::new(0.0, 0.0);
872            }
873            for r in 0..q {
874                v_sorted[[r, new_idx]] = v[[r, old_idx]];
875            }
876        }
877
878        // Reassemble in the original orientation.
879        if transposed {
880            // Original M = (a0)^H. With a0 = U_a S V_a^H we get
881            // M = V_a S U_a^H, i.e. U_M = V_a, V_M^H = U_a^H.
882            let u_m = v_sorted; // (q x k) = (rows? ) ; careful with shapes below
883            let vh_m = u_mat.mapv(|z| z.conj()).t().to_owned(); // (k x p)
884            Ok((u_m, s_vec, vh_m))
885        } else {
886            let vh_m = v_sorted.mapv(|z| z.conj()).t().to_owned(); // (k x q)
887            Ok((u_mat, s_vec, vh_m))
888        }
889    }
890
891    /// Get a reference to the tensors in the network
892    pub fn tensors(&self) -> Vec<&Tensor> {
893        self.tensors.values().collect()
894    }
895
896    /// Get a reference to a tensor by ID
897    pub fn tensor(&self, id: usize) -> Option<&Tensor> {
898        self.tensors.get(&id)
899    }
900}
901
902/// Builder for quantum circuits as tensor networks
903pub struct TensorNetworkBuilder {
904    network: TensorNetwork,
905    qubit_indices: HashMap<usize, String>,
906    current_indices: HashMap<usize, String>,
907}
908
909impl TensorNetworkBuilder {
910    /// Create a new tensor network builder for n qubits
911    pub fn new(num_qubits: usize) -> Self {
912        let mut network = TensorNetwork::new();
913        let mut qubit_indices = HashMap::new();
914        let mut current_indices = HashMap::new();
915
916        // Initialize qubits in |0⟩ state
917        for i in 0..num_qubits {
918            let idx = format!("q{i}_0");
919            let tensor = Tensor::qubit_zero(i, idx.clone());
920            network.add_tensor(tensor);
921            qubit_indices.insert(i, idx.clone());
922            current_indices.insert(i, idx);
923        }
924
925        Self {
926            network,
927            qubit_indices,
928            current_indices,
929        }
930    }
931
932    /// Apply a single-qubit gate
933    pub fn apply_single_qubit_gate(
934        &mut self,
935        gate: &dyn GateOp,
936        qubit: usize,
937    ) -> QuantRS2Result<()> {
938        let matrix_vec = gate.matrix()?;
939        let matrix = Array2::from_shape_vec((2, 2), matrix_vec)
940            .map_err(|e| QuantRS2Error::InvalidInput(format!("Shape error: {e}")))?;
941
942        // Create gate tensor
943        let in_idx = self.current_indices[&qubit].clone();
944        let out_idx = format!("q{}_{}", qubit, self.network.next_id);
945        let gate_tensor = Tensor::from_matrix(
946            self.network.next_id,
947            matrix,
948            in_idx.clone(),
949            out_idx.clone(),
950        );
951
952        // Add to network
953        let gate_id = self.network.add_tensor(gate_tensor);
954
955        // Connect to previous tensor on this qubit
956        if let Some(prev_tensor) = self.find_tensor_with_index(&in_idx) {
957            self.network
958                .connect(prev_tensor, in_idx.clone(), gate_id, in_idx)?;
959        }
960
961        // Update current index
962        self.current_indices.insert(qubit, out_idx);
963
964        Ok(())
965    }
966
967    /// Apply a two-qubit gate
968    pub fn apply_two_qubit_gate(
969        &mut self,
970        gate: &dyn GateOp,
971        qubit1: usize,
972        qubit2: usize,
973    ) -> QuantRS2Result<()> {
974        let matrix_vec = gate.matrix()?;
975        let matrix = Array2::from_shape_vec((4, 4), matrix_vec)
976            .map_err(|e| QuantRS2Error::InvalidInput(format!("Shape error: {e}")))?;
977
978        // Reshape to rank-4 tensor
979        let tensor_data = matrix
980            .into_shape_with_order((2, 2, 2, 2))
981            .map_err(|e| QuantRS2Error::InvalidInput(format!("Shape error: {e}")))?
982            .into_dyn();
983
984        // Create indices
985        let in1_idx = self.current_indices[&qubit1].clone();
986        let in2_idx = self.current_indices[&qubit2].clone();
987        let out1_idx = format!("q{}_{}", qubit1, self.network.next_id);
988        let out2_idx = format!("q{}_{}", qubit2, self.network.next_id);
989
990        let gate_tensor = Tensor::new(
991            self.network.next_id,
992            tensor_data,
993            vec![
994                in1_idx.clone(),
995                in2_idx.clone(),
996                out1_idx.clone(),
997                out2_idx.clone(),
998            ],
999        );
1000
1001        // Add to network
1002        let gate_id = self.network.add_tensor(gate_tensor);
1003
1004        // Connect to previous tensors
1005        if let Some(prev1) = self.find_tensor_with_index(&in1_idx) {
1006            self.network
1007                .connect(prev1, in1_idx.clone(), gate_id, in1_idx)?;
1008        }
1009        if let Some(prev2) = self.find_tensor_with_index(&in2_idx) {
1010            self.network
1011                .connect(prev2, in2_idx.clone(), gate_id, in2_idx)?;
1012        }
1013
1014        // Update current indices
1015        self.current_indices.insert(qubit1, out1_idx);
1016        self.current_indices.insert(qubit2, out2_idx);
1017
1018        Ok(())
1019    }
1020
1021    /// Find tensor that has the given index as output
1022    fn find_tensor_with_index(&self, index: &str) -> Option<usize> {
1023        for (id, tensor) in &self.network.tensors {
1024            if tensor.indices.iter().any(|idx| idx == index) {
1025                return Some(*id);
1026            }
1027        }
1028        None
1029    }
1030
1031    /// Build the final tensor network
1032    pub fn build(self) -> TensorNetwork {
1033        self.network
1034    }
1035
1036    /// Contract the network and return the quantum state
1037    #[must_use]
1038    pub fn to_statevector(&mut self) -> QuantRS2Result<Vec<Complex64>> {
1039        let final_tensor = self.network.contract_all()?;
1040        Ok(final_tensor.data.into_raw_vec_and_offset().0)
1041    }
1042}
1043
1044/// Quantum circuit simulation using tensor networks
1045pub struct TensorNetworkSimulator {
1046    /// Maximum bond dimension for MPS
1047    max_bond_dim: usize,
1048    /// Use SVD compression
1049    use_compression: bool,
1050    /// Parallelization threshold
1051    parallel_threshold: usize,
1052}
1053
1054impl TensorNetworkSimulator {
1055    /// Create a new tensor network simulator
1056    pub const fn new() -> Self {
1057        Self {
1058            max_bond_dim: 64,
1059            use_compression: true,
1060            parallel_threshold: 1000,
1061        }
1062    }
1063
1064    /// Set maximum bond dimension
1065    #[must_use]
1066    pub const fn with_max_bond_dim(mut self, dim: usize) -> Self {
1067        self.max_bond_dim = dim;
1068        self
1069    }
1070
1071    /// Enable or disable compression
1072    #[must_use]
1073    pub const fn with_compression(mut self, compress: bool) -> Self {
1074        self.use_compression = compress;
1075        self
1076    }
1077
1078    /// Simulate a quantum circuit
1079    pub fn simulate<const N: usize>(
1080        &self,
1081        gates: &[Box<dyn GateOp>],
1082    ) -> QuantRS2Result<Register<N>> {
1083        let mut builder = TensorNetworkBuilder::new(N);
1084
1085        // Apply gates
1086        for gate in gates {
1087            let qubits = gate.qubits();
1088            match qubits.len() {
1089                1 => builder.apply_single_qubit_gate(gate.as_ref(), qubits[0].0 as usize)?,
1090                2 => builder.apply_two_qubit_gate(
1091                    gate.as_ref(),
1092                    qubits[0].0 as usize,
1093                    qubits[1].0 as usize,
1094                )?,
1095                _ => {
1096                    return Err(QuantRS2Error::UnsupportedOperation(format!(
1097                        "Gates with {} qubits not supported in tensor network",
1098                        qubits.len()
1099                    )))
1100                }
1101            }
1102        }
1103
1104        // Contract to get statevector
1105        let amplitudes = builder.to_statevector()?;
1106        Register::with_amplitudes(amplitudes)
1107    }
1108}
1109
1110/// Optimized contraction strategies
1111pub mod contraction_optimization {
1112    use super::*;
1113
1114    /// Dynamic programming algorithm for optimal contraction order
1115    pub struct DynamicProgrammingOptimizer {
1116        memo: HashMap<Vec<usize>, (usize, Vec<(usize, usize)>)>,
1117    }
1118
1119    impl DynamicProgrammingOptimizer {
1120        pub fn new() -> Self {
1121            Self {
1122                memo: HashMap::new(),
1123            }
1124        }
1125
1126        /// Find optimal contraction order using dynamic programming
1127        pub fn optimize(&mut self, network: &TensorNetwork) -> Vec<(usize, usize)> {
1128            let tensor_ids: Vec<_> = network.tensors.keys().copied().collect();
1129            self.find_optimal_order(&tensor_ids, network).1
1130        }
1131
1132        fn find_optimal_order(
1133            &mut self,
1134            tensors: &[usize],
1135            network: &TensorNetwork,
1136        ) -> (usize, Vec<(usize, usize)>) {
1137            if tensors.len() <= 1 {
1138                return (0, vec![]);
1139            }
1140
1141            let key = tensors.to_vec();
1142            if let Some(result) = self.memo.get(&key) {
1143                return result.clone();
1144            }
1145
1146            let mut best_cost = usize::MAX;
1147            let mut best_order = vec![];
1148
1149            // Try all possible pairings
1150            for i in 0..tensors.len() {
1151                for j in (i + 1)..tensors.len() {
1152                    // Check if tensors are connected
1153                    if self.are_connected(tensors[i], tensors[j], network) {
1154                        let cost = network.estimate_contraction_cost(tensors[i], tensors[j]);
1155
1156                        // Remaining tensors after contraction
1157                        let mut remaining = vec![];
1158                        for (k, &t) in tensors.iter().enumerate() {
1159                            if k != i && k != j {
1160                                remaining.push(t);
1161                            }
1162                        }
1163                        remaining.push(network.next_id + remaining.len()); // Virtual tensor
1164
1165                        let (sub_cost, sub_order) = self.find_optimal_order(&remaining, network);
1166                        let total_cost = cost + sub_cost;
1167
1168                        if total_cost < best_cost {
1169                            best_cost = total_cost;
1170                            best_order = vec![(tensors[i], tensors[j])];
1171                            best_order.extend(sub_order);
1172                        }
1173                    }
1174                }
1175            }
1176
1177            self.memo.insert(key, (best_cost, best_order.clone()));
1178            (best_cost, best_order)
1179        }
1180
1181        fn are_connected(&self, t1: usize, t2: usize, network: &TensorNetwork) -> bool {
1182            network.edges.iter().any(|e| {
1183                (e.tensor1 == t1 && e.tensor2 == t2) || (e.tensor1 == t2 && e.tensor2 == t1)
1184            })
1185        }
1186    }
1187}
1188
1189#[cfg(test)]
1190mod tests {
1191    use super::*;
1192
1193    #[test]
1194    fn test_tensor_creation() {
1195        let data = ArrayD::zeros(IxDyn(&[2, 2]));
1196        let tensor = Tensor::new(0, data, vec!["in".to_string(), "out".to_string()]);
1197        assert_eq!(tensor.rank(), 2);
1198        assert_eq!(tensor.shape, vec![2, 2]);
1199    }
1200
1201    #[test]
1202    fn test_qubit_tensors() {
1203        let t0 = Tensor::qubit_zero(0, "q0".to_string());
1204        assert_eq!(t0.data[[0]], Complex64::new(1.0, 0.0));
1205        assert_eq!(t0.data[[1]], Complex64::new(0.0, 0.0));
1206
1207        let t1 = Tensor::qubit_one(1, "q1".to_string());
1208        assert_eq!(t1.data[[0]], Complex64::new(0.0, 0.0));
1209        assert_eq!(t1.data[[1]], Complex64::new(1.0, 0.0));
1210    }
1211
1212    #[test]
1213    fn test_tensor_network_builder() {
1214        let builder = TensorNetworkBuilder::new(2);
1215        assert_eq!(builder.network.tensors.len(), 2);
1216    }
1217
1218    /// Contract a returned MPS chain back into the full dense tensor (row-major flat
1219    /// vector over the physical indices). Each site tensor has indices
1220    /// `[bond_left, phys, bond_right]` with boundary bonds of dimension 1.
1221    fn contract_mps(mps: &[Tensor]) -> Vec<Complex64> {
1222        // psi is a flat (left_bond x phys_so_far) buffer; start with left_bond = 1 and
1223        // a single scalar 1.0.
1224        let mut acc: Vec<Complex64> = vec![Complex64::new(1.0, 0.0)];
1225        let mut left_bond = 1usize;
1226        for t in mps {
1227            let lb = t.shape[0];
1228            let d = t.shape[1];
1229            let rb = t.shape[2];
1230            assert_eq!(lb, left_bond, "bond mismatch while contracting MPS");
1231            let cols = acc.len() / left_bond; // physical entries accumulated so far
1232                                              // new_acc has shape (rb x (cols*d)) flattened row-major over [rb, cols, d].
1233            let mut new_acc = vec![Complex64::new(0.0, 0.0); rb * cols * d];
1234            for r in 0..rb {
1235                for cidx in 0..cols {
1236                    for phys in 0..d {
1237                        let mut sum = Complex64::new(0.0, 0.0);
1238                        for l in 0..lb {
1239                            // acc indexed row-major over [l, cidx]
1240                            let a_val = acc[l * cols + cidx];
1241                            let t_val = t.data[[l, phys, r]];
1242                            sum += a_val * t_val;
1243                        }
1244                        new_acc[(r * cols + cidx) * d + phys] = sum;
1245                    }
1246                }
1247            }
1248            acc = new_acc;
1249            left_bond = rb;
1250        }
1251        acc
1252    }
1253
1254    /// Build a single-tensor network wrapping a known statevector `amps` over
1255    /// `n` qubits (shape `[2; n]`). `contract_all` returns such a single tensor
1256    /// verbatim (deterministically), so this isolates the MPS decomposition from the
1257    /// network contraction engine.
1258    fn single_tensor_network(amps: Vec<Complex64>, n: usize) -> TensorNetwork {
1259        let shape: Vec<usize> = vec![2usize; n];
1260        let data = Array::from_shape_vec(IxDyn(&shape), amps).expect("state tensor");
1261        let indices: Vec<String> = (0..n).map(|i| format!("phys_{i}")).collect();
1262        let mut net = TensorNetwork::new();
1263        net.add_tensor(Tensor::new(0, data, indices));
1264        net
1265    }
1266
1267    #[test]
1268    fn test_to_mps_reconstructs_bell_state() {
1269        // Site-6 proof: to_mps reconstructs an entangled (bond-dim-2) Bell state.
1270        let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
1271        let bell = vec![
1272            Complex64::new(inv_sqrt2, 0.0),
1273            Complex64::new(0.0, 0.0),
1274            Complex64::new(0.0, 0.0),
1275            Complex64::new(inv_sqrt2, 0.0),
1276        ];
1277        let net = single_tensor_network(bell.clone(), 2);
1278        let mps = net.to_mps(None).expect("to_mps");
1279        assert_eq!(mps.len(), 2, "one MPS tensor per qubit");
1280        // Genuine entanglement => inner bond dimension 2.
1281        assert_eq!(mps[0].shape[2], 2, "Bell state needs bond dimension 2");
1282
1283        let recon = contract_mps(&mps);
1284        let err: f64 = recon
1285            .iter()
1286            .zip(bell.iter())
1287            .map(|(a, b)| (a - b).norm_sqr())
1288            .sum::<f64>()
1289            .sqrt();
1290        assert!(err < 1e-9, "Bell MPS reconstruction error {err}");
1291    }
1292
1293    #[test]
1294    fn test_to_mps_reconstructs_ghz_state() {
1295        // 3-qubit GHZ = (|000> + |111>)/sqrt2.
1296        let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
1297        let mut ghz = vec![Complex64::new(0.0, 0.0); 8];
1298        ghz[0] = Complex64::new(inv_sqrt2, 0.0);
1299        ghz[7] = Complex64::new(inv_sqrt2, 0.0);
1300        let net = single_tensor_network(ghz.clone(), 3);
1301        let mps = net.to_mps(None).expect("to_mps");
1302        assert_eq!(mps.len(), 3, "one MPS tensor per qubit");
1303
1304        let recon = contract_mps(&mps);
1305        let err: f64 = recon
1306            .iter()
1307            .zip(ghz.iter())
1308            .map(|(a, b)| (a - b).norm_sqr())
1309            .sum::<f64>()
1310            .sqrt();
1311        assert!(err < 1e-9, "GHZ MPS reconstruction error {err}");
1312    }
1313
1314    #[test]
1315    fn test_to_mps_reconstructs_generic_state() {
1316        // A generic normalised 2-qubit complex state (no special structure).
1317        let raw = [
1318            Complex64::new(0.3, 0.1),
1319            Complex64::new(-0.2, 0.4),
1320            Complex64::new(0.5, -0.25),
1321            Complex64::new(0.1, 0.35),
1322        ];
1323        let norm = raw.iter().map(|z| z.norm_sqr()).sum::<f64>().sqrt();
1324        let state: Vec<Complex64> = raw.iter().map(|z| z / norm).collect();
1325        let net = single_tensor_network(state.clone(), 2);
1326        let mps = net.to_mps(None).expect("to_mps");
1327        let recon = contract_mps(&mps);
1328        let err: f64 = recon
1329            .iter()
1330            .zip(state.iter())
1331            .map(|(a, b)| (a - b).norm_sqr())
1332            .sum::<f64>()
1333            .sqrt();
1334        assert!(err < 1e-9, "generic MPS reconstruction error {err}");
1335    }
1336
1337    #[test]
1338    fn test_to_mps_truncation_keeps_bond_dim() {
1339        // With max_bond_dim = 1 the Bell state cannot be represented exactly, but the
1340        // call must still succeed and cap every bond dimension at 1 (lossy truncation),
1341        // proving the truncation path is real (not ignored).
1342        let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
1343        let bell = vec![
1344            Complex64::new(inv_sqrt2, 0.0),
1345            Complex64::new(0.0, 0.0),
1346            Complex64::new(0.0, 0.0),
1347            Complex64::new(inv_sqrt2, 0.0),
1348        ];
1349        let net = single_tensor_network(bell, 2);
1350        let mps = net.to_mps(Some(1)).expect("to_mps truncated");
1351        for t in &mps {
1352            assert!(
1353                t.shape[0] <= 1 && t.shape[2] <= 1,
1354                "bond dimension exceeded max_bond_dim=1: {:?}",
1355                t.shape
1356            );
1357        }
1358    }
1359
1360    #[test]
1361    fn test_apply_mpo_honest_error() {
1362        // Site-6: apply_mpo must report an honest error rather than silently no-op.
1363        let mut network = TensorNetwork::new();
1364        let result = network.apply_mpo(&[], &[0]);
1365        assert!(matches!(
1366            result,
1367            Err(QuantRS2Error::UnsupportedOperation(_))
1368        ));
1369    }
1370
1371    #[test]
1372    fn test_complex_svd_roundtrip() {
1373        // Validate the complex Jacobi SVD: M ≈ U diag(s) V^H with orthonormal factors.
1374        let m = Array2::from_shape_vec(
1375            (3, 2),
1376            vec![
1377                Complex64::new(1.0, 0.5),
1378                Complex64::new(-0.3, 0.2),
1379                Complex64::new(0.4, -0.1),
1380                Complex64::new(0.7, 0.0),
1381                Complex64::new(-0.2, 0.9),
1382                Complex64::new(0.1, 0.1),
1383            ],
1384        )
1385        .expect("matrix");
1386        let (u, s, vh) = TensorNetwork::complex_svd(&m).expect("svd");
1387        // Reconstruct.
1388        let k = s.len();
1389        let mut s_mat = Array2::<Complex64>::zeros((k, k));
1390        for i in 0..k {
1391            s_mat[[i, i]] = Complex64::new(s[i], 0.0);
1392        }
1393        let recon = u.dot(&s_mat).dot(&vh);
1394        let err: f64 = recon
1395            .iter()
1396            .zip(m.iter())
1397            .map(|(a, b)| (a - b).norm_sqr())
1398            .sum::<f64>()
1399            .sqrt();
1400        assert!(err < 1e-9, "complex SVD reconstruction error {err}");
1401        // Singular values non-increasing and non-negative.
1402        for i in 1..s.len() {
1403            assert!(s[i] <= s[i - 1] + 1e-12);
1404            assert!(s[i] >= -1e-12);
1405        }
1406    }
1407
1408    #[test]
1409    fn test_network_connection() {
1410        let mut network = TensorNetwork::new();
1411
1412        let t1 = Tensor::qubit_zero(0, "q0".to_string());
1413        let t2 = Tensor::qubit_zero(1, "q1".to_string());
1414
1415        let id1 = network.add_tensor(t1);
1416        let id2 = network.add_tensor(t2);
1417
1418        // Should fail - indices don't exist on these tensors
1419        assert!(network
1420            .connect(id1, "bond".to_string(), id2, "bond".to_string())
1421            .is_err());
1422    }
1423}