Skip to main content

quantrs2_core/
kak_multiqubit.rs

1//! KAK decomposition for multi-qubit unitaries
2//!
3//! This module extends the Cartan (KAK) decomposition to handle arbitrary
4//! n-qubit unitaries through recursive application and generalized
5//! decomposition techniques.
6
7use crate::{
8    cartan::{CartanDecomposer, CartanDecomposition},
9    error::{QuantRS2Error, QuantRS2Result},
10    gate::{multi::*, single::*, GateOp},
11    matrix_ops::{DenseMatrix, QuantumMatrix},
12    qubit::QubitId,
13    shannon::ShannonDecomposer,
14    synthesis::{decompose_single_qubit_zyz, SingleQubitDecomposition},
15};
16use rustc_hash::FxHashMap;
17use scirs2_core::ndarray::{s, Array2};
18use scirs2_core::Complex;
19
20/// Complex one-sided Jacobi SVD of a square matrix `m`.
21///
22/// Returns `(U, s, Vᴴ)` with `m = U · diag(s) · Vᴴ`, `U` and `Vᴴ` unitary and `s` the
23/// singular values in non-increasing order. One-sided Jacobi orthogonalises the columns
24/// of `m` by unitary plane rotations; the resulting column norms are the singular values
25/// and the accumulated rotations form `V`. This is used because the SciRS2 LAPACK SVD
26/// currently exposes only a real-valued path, whereas the CSD operates on complex blocks.
27fn complex_svd(
28    m: &Array2<Complex<f64>>,
29) -> QuantRS2Result<(Array2<Complex<f64>>, Vec<f64>, Array2<Complex<f64>>)> {
30    let (rows, cols) = (m.nrows(), m.ncols());
31    if rows != cols {
32        return Err(QuantRS2Error::InvalidInput(
33            "complex_svd helper expects a square matrix".to_string(),
34        ));
35    }
36    let n = rows;
37
38    let mut a = m.clone(); // columns orthogonalised in place -> U·diag(s)
39    let mut v = Array2::<Complex<f64>>::eye(n); // accumulates right rotations
40
41    let eps = 1e-15;
42    let max_sweeps = 60;
43    for _sweep in 0..max_sweeps {
44        let mut off = 0.0f64;
45        for i in 0..n {
46            for j in (i + 1)..n {
47                let mut alpha = 0.0f64;
48                let mut beta = 0.0f64;
49                let mut gamma = Complex::new(0.0, 0.0);
50                for r in 0..n {
51                    let ai = a[[r, i]];
52                    let aj = a[[r, j]];
53                    alpha += ai.norm_sqr();
54                    beta += aj.norm_sqr();
55                    gamma += ai.conj() * aj;
56                }
57                let gamma_abs = gamma.norm();
58                off += gamma_abs;
59                if gamma_abs <= eps * (alpha.sqrt() * beta.sqrt()).max(eps) {
60                    continue;
61                }
62                let phase = gamma / gamma_abs;
63                let zeta = (beta - alpha) / (2.0 * gamma_abs);
64                let t = zeta.signum() / (zeta.abs() + (1.0 + zeta * zeta).sqrt());
65                let cval = 1.0 / (1.0 + t * t).sqrt();
66                let s_ij = phase * Complex::new(cval * t, 0.0);
67                for r in 0..n {
68                    let ai = a[[r, i]];
69                    let aj = a[[r, j]];
70                    a[[r, i]] = Complex::new(cval, 0.0) * ai - s_ij.conj() * aj;
71                    a[[r, j]] = s_ij * ai + Complex::new(cval, 0.0) * aj;
72                }
73                for r in 0..n {
74                    let vi = v[[r, i]];
75                    let vj = v[[r, j]];
76                    v[[r, i]] = Complex::new(cval, 0.0) * vi - s_ij.conj() * vj;
77                    v[[r, j]] = s_ij * vi + Complex::new(cval, 0.0) * vj;
78                }
79            }
80        }
81        if off <= eps {
82            break;
83        }
84    }
85
86    // Column norms are the singular values; sort non-increasing.
87    let mut order: Vec<(f64, usize)> = (0..n)
88        .map(|j| {
89            let norm = (0..n).map(|r| a[[r, j]].norm_sqr()).sum::<f64>().sqrt();
90            (norm, j)
91        })
92        .collect();
93    order.sort_by(|x, y| y.0.total_cmp(&x.0));
94
95    let mut u = Array2::<Complex<f64>>::zeros((n, n));
96    let mut s_vec = vec![0.0f64; n];
97    let mut v_sorted = Array2::<Complex<f64>>::zeros((n, n));
98    let mut zero_cols: Vec<usize> = Vec::new();
99    for (new_idx, &(norm, old_idx)) in order.iter().enumerate() {
100        s_vec[new_idx] = norm;
101        if norm > 1e-300 {
102            for r in 0..n {
103                u[[r, new_idx]] = a[[r, old_idx]] / Complex::new(norm, 0.0);
104            }
105        } else {
106            zero_cols.push(new_idx);
107        }
108        for r in 0..n {
109            v_sorted[[r, new_idx]] = v[[r, old_idx]];
110        }
111    }
112    // Fill any zero singular-value columns of U with an orthonormal completion so U
113    // stays unitary.
114    if !zero_cols.is_empty() {
115        complete_orthonormal_columns(&mut u, &zero_cols, 1e-12);
116    }
117
118    let vh = v_sorted.mapv(|z| z.conj()).t().to_owned();
119    Ok((u, s_vec, vh))
120}
121
122/// Fill the specified columns of `mat` (an `n×n` matrix whose *other* columns are
123/// already orthonormal) with vectors that extend the set to a full orthonormal basis.
124///
125/// Uses modified Gram–Schmidt against the existing columns and the candidates built so
126/// far, seeding from standard basis vectors.
127fn complete_orthonormal_columns(mat: &mut Array2<Complex<f64>>, columns: &[usize], tol: f64) {
128    let n = mat.nrows();
129    let fixed: std::collections::HashSet<usize> = columns.iter().copied().collect();
130
131    // Collect the already-fixed (good) columns as the starting orthonormal set.
132    let mut basis: Vec<Vec<Complex<f64>>> = Vec::new();
133    for j in 0..n {
134        if !fixed.contains(&j) {
135            basis.push((0..n).map(|r| mat[[r, j]]).collect());
136        }
137    }
138
139    let mut seed = 0usize;
140    for &col in columns {
141        // Find a standard basis vector e_seed that is not (numerically) in the span,
142        // orthogonalise it against the current basis, and normalise.
143        let mut placed = false;
144        while seed < n && !placed {
145            let mut v = vec![Complex::new(0.0, 0.0); n];
146            v[seed] = Complex::new(1.0, 0.0);
147            for b in &basis {
148                let proj: Complex<f64> =
149                    b.iter().zip(v.iter()).map(|(bk, vk)| bk.conj() * vk).sum();
150                for r in 0..n {
151                    v[r] -= proj * b[r];
152                }
153            }
154            let norm = v.iter().map(|z| z.norm_sqr()).sum::<f64>().sqrt();
155            if norm > tol {
156                for r in 0..n {
157                    v[r] /= Complex::new(norm, 0.0);
158                }
159                for r in 0..n {
160                    mat[[r, col]] = v[r];
161                }
162                basis.push(v);
163                placed = true;
164            }
165            seed += 1;
166        }
167        if !placed {
168            // Fallback: leave a unit vector (should not happen for a valid completion).
169            mat[[col % n, col]] = Complex::new(1.0, 0.0);
170        }
171    }
172}
173
174/// Result of multi-qubit KAK decomposition
175#[derive(Debug, Clone)]
176pub struct MultiQubitKAK {
177    /// The decomposed gate sequence
178    pub gates: Vec<Box<dyn GateOp>>,
179    /// Decomposition tree structure
180    pub tree: DecompositionTree,
181    /// Total CNOT count
182    pub cnot_count: usize,
183    /// Total single-qubit gate count
184    pub single_qubit_count: usize,
185    /// Circuit depth
186    pub depth: usize,
187}
188
189/// Tree structure representing the hierarchical decomposition
190#[derive(Debug, Clone)]
191pub enum DecompositionTree {
192    /// Leaf node - single or two-qubit gate
193    Leaf {
194        qubits: Vec<QubitId>,
195        gate_type: LeafType,
196    },
197    /// Internal node - recursive decomposition
198    Node {
199        qubits: Vec<QubitId>,
200        method: DecompositionMethod,
201        children: Vec<Self>,
202    },
203}
204
205/// Type of leaf decomposition
206#[derive(Debug, Clone)]
207pub enum LeafType {
208    SingleQubit(SingleQubitDecomposition),
209    TwoQubit(CartanDecomposition),
210}
211
212/// Method used for decomposition at this level
213#[derive(Debug, Clone)]
214pub enum DecompositionMethod {
215    /// Cosine-Sine Decomposition
216    CSD { pivot: usize },
217    /// Quantum Shannon Decomposition
218    Shannon { partition: usize },
219    /// Block diagonalization
220    BlockDiagonal { block_size: usize },
221    /// Direct Cartan for 2 qubits
222    Cartan,
223}
224
225/// Multi-qubit KAK decomposer
226pub struct MultiQubitKAKDecomposer {
227    /// Tolerance for numerical comparisons
228    tolerance: f64,
229    /// Maximum recursion depth
230    max_depth: usize,
231    /// Cache for decompositions
232    #[allow(dead_code)]
233    cache: FxHashMap<u64, MultiQubitKAK>,
234    /// Use optimized methods
235    use_optimization: bool,
236    /// Cartan decomposer for two-qubit blocks
237    cartan: CartanDecomposer,
238}
239
240impl MultiQubitKAKDecomposer {
241    /// Create a new multi-qubit KAK decomposer
242    pub fn new() -> Self {
243        Self {
244            tolerance: 1e-10,
245            max_depth: 20,
246            cache: FxHashMap::default(),
247            use_optimization: true,
248            cartan: CartanDecomposer::new(),
249        }
250    }
251
252    /// Create with custom tolerance
253    pub fn with_tolerance(tolerance: f64) -> Self {
254        Self {
255            tolerance,
256            max_depth: 20,
257            cache: FxHashMap::default(),
258            use_optimization: true,
259            cartan: CartanDecomposer::with_tolerance(tolerance),
260        }
261    }
262
263    /// Decompose an n-qubit unitary
264    pub fn decompose(
265        &mut self,
266        unitary: &Array2<Complex<f64>>,
267        qubit_ids: &[QubitId],
268    ) -> QuantRS2Result<MultiQubitKAK> {
269        let n = qubit_ids.len();
270        let size = 1 << n;
271
272        // Validate input
273        if unitary.shape() != [size, size] {
274            return Err(QuantRS2Error::InvalidInput(format!(
275                "Unitary size {} doesn't match {} qubits",
276                unitary.shape()[0],
277                n
278            )));
279        }
280
281        // Check unitarity
282        let mat = DenseMatrix::new(unitary.clone())?;
283        if !mat.is_unitary(self.tolerance)? {
284            return Err(QuantRS2Error::InvalidInput(
285                "Matrix is not unitary".to_string(),
286            ));
287        }
288
289        // Check cache
290        if let Some(cached) = self.check_cache(unitary) {
291            return Ok(cached.clone());
292        }
293
294        // Perform decomposition
295        let (tree, gates) = self.decompose_recursive(unitary, qubit_ids, 0)?;
296
297        // Count gates
298        let mut cnot_count = 0;
299        let mut single_qubit_count = 0;
300
301        for gate in &gates {
302            match gate.name() {
303                "CNOT" | "CZ" | "SWAP" => cnot_count += self.count_cnots(gate.name()),
304                _ => single_qubit_count += 1,
305            }
306        }
307
308        // Calculate actual circuit depth using critical path through DAG.
309        // For each gate in topological order:
310        //   depth[gate] = 1 + max(depth[prev_gate]) over all preceding gates sharing a qubit.
311        let depth = Self::calculate_circuit_depth(&gates);
312
313        let result = MultiQubitKAK {
314            gates,
315            tree,
316            cnot_count,
317            single_qubit_count,
318            depth,
319        };
320
321        // Cache result
322        self.cache_result(unitary, &result);
323
324        Ok(result)
325    }
326
327    /// Recursive decomposition algorithm
328    fn decompose_recursive(
329        &mut self,
330        unitary: &Array2<Complex<f64>>,
331        qubit_ids: &[QubitId],
332        depth: usize,
333    ) -> QuantRS2Result<(DecompositionTree, Vec<Box<dyn GateOp>>)> {
334        if depth > self.max_depth {
335            return Err(QuantRS2Error::InvalidInput(
336                "Maximum recursion depth exceeded".to_string(),
337            ));
338        }
339
340        let n = qubit_ids.len();
341
342        // Base cases
343        match n {
344            0 => {
345                let tree = DecompositionTree::Leaf {
346                    qubits: vec![],
347                    gate_type: LeafType::SingleQubit(SingleQubitDecomposition {
348                        global_phase: 0.0,
349                        theta1: 0.0,
350                        phi: 0.0,
351                        theta2: 0.0,
352                        basis: "ZYZ".to_string(),
353                    }),
354                };
355                Ok((tree, vec![]))
356            }
357            1 => {
358                let decomp = decompose_single_qubit_zyz(&unitary.view())?;
359                let gates = self.single_qubit_to_gates(&decomp, qubit_ids[0]);
360                let tree = DecompositionTree::Leaf {
361                    qubits: qubit_ids.to_vec(),
362                    gate_type: LeafType::SingleQubit(decomp),
363                };
364                Ok((tree, gates))
365            }
366            2 => {
367                let decomp = self.cartan.decompose(unitary)?;
368                let gates = self.cartan.to_gates(&decomp, qubit_ids)?;
369                let tree = DecompositionTree::Leaf {
370                    qubits: qubit_ids.to_vec(),
371                    gate_type: LeafType::TwoQubit(decomp),
372                };
373                Ok((tree, gates))
374            }
375            _ => {
376                // For n > 2, choose decomposition method
377                let method = self.choose_decomposition_method(unitary, n);
378
379                match method {
380                    DecompositionMethod::CSD { pivot } => {
381                        self.decompose_csd(unitary, qubit_ids, pivot, depth)
382                    }
383                    DecompositionMethod::Shannon { partition } => {
384                        self.decompose_shannon(unitary, qubit_ids, partition, depth)
385                    }
386                    DecompositionMethod::BlockDiagonal { block_size } => {
387                        self.decompose_block_diagonal(unitary, qubit_ids, block_size, depth)
388                    }
389                    DecompositionMethod::Cartan => unreachable!("Invalid method for n > 2"),
390                }
391            }
392        }
393    }
394
395    /// Choose optimal decomposition method based on matrix structure
396    fn choose_decomposition_method(
397        &self,
398        unitary: &Array2<Complex<f64>>,
399        n: usize,
400    ) -> DecompositionMethod {
401        if self.use_optimization {
402            // Analyze matrix structure to choose optimal method
403            if self.has_block_structure(unitary, n) {
404                DecompositionMethod::BlockDiagonal { block_size: n / 2 }
405            } else if n % 2 == 0 {
406                // Even number of qubits - use CSD at midpoint
407                DecompositionMethod::CSD { pivot: n / 2 }
408            } else {
409                // Odd number - use Shannon decomposition
410                DecompositionMethod::Shannon { partition: n / 2 }
411            }
412        } else {
413            // Default to CSD
414            DecompositionMethod::CSD { pivot: n / 2 }
415        }
416    }
417
418    /// Decompose using Cosine-Sine Decomposition
419    fn decompose_csd(
420        &mut self,
421        unitary: &Array2<Complex<f64>>,
422        qubit_ids: &[QubitId],
423        pivot: usize,
424        depth: usize,
425    ) -> QuantRS2Result<(DecompositionTree, Vec<Box<dyn GateOp>>)> {
426        let n = qubit_ids.len();
427        // let _size = 1 << n;
428        let pivot_size = 1 << pivot;
429
430        // Split unitary into blocks based on pivot
431        // U = [A B]
432        //     [C D]
433        let a = unitary.slice(s![..pivot_size, ..pivot_size]).to_owned();
434        let b = unitary.slice(s![..pivot_size, pivot_size..]).to_owned();
435        let c = unitary.slice(s![pivot_size.., ..pivot_size]).to_owned();
436        let d = unitary.slice(s![pivot_size.., pivot_size..]).to_owned();
437
438        // Apply CSD to find:
439        // U = (U1 ⊗ V1) · Σ · (U2 ⊗ V2)
440        // where Σ is diagonal in the CSD basis
441
442        // This is a simplified version - full CSD would use SVD
443        let (u1, v1, sigma, u2, v2) = self.compute_csd(&a, &b, &c, &d)?;
444
445        let mut gates = Vec::new();
446        let mut children = Vec::new();
447
448        // Decompose U2 and V2 (right multiplications)
449        let left_qubits = &qubit_ids[..pivot];
450        let right_qubits = &qubit_ids[pivot..];
451
452        let (u2_tree, u2_gates) = self.decompose_recursive(&u2, left_qubits, depth + 1)?;
453        let (v2_tree, v2_gates) = self.decompose_recursive(&v2, right_qubits, depth + 1)?;
454
455        gates.extend(u2_gates);
456        gates.extend(v2_gates);
457        children.push(u2_tree);
458        children.push(v2_tree);
459
460        // Apply diagonal gates (controlled rotations)
461        let diag_gates = self.diagonal_to_gates(&sigma, qubit_ids)?;
462        gates.extend(diag_gates);
463
464        // Decompose U1 and V1 (left multiplications)
465        let (u1_tree, u1_gates) = self.decompose_recursive(&u1, left_qubits, depth + 1)?;
466        let (v1_tree, v1_gates) = self.decompose_recursive(&v1, right_qubits, depth + 1)?;
467
468        gates.extend(u1_gates);
469        gates.extend(v1_gates);
470        children.push(u1_tree);
471        children.push(v1_tree);
472
473        let tree = DecompositionTree::Node {
474            qubits: qubit_ids.to_vec(),
475            method: DecompositionMethod::CSD { pivot },
476            children,
477        };
478
479        Ok((tree, gates))
480    }
481
482    /// Decompose using Shannon decomposition
483    fn decompose_shannon(
484        &self,
485        unitary: &Array2<Complex<f64>>,
486        qubit_ids: &[QubitId],
487        partition: usize,
488        _depth: usize,
489    ) -> QuantRS2Result<(DecompositionTree, Vec<Box<dyn GateOp>>)> {
490        // Use the Shannon decomposer for this
491        let mut shannon = ShannonDecomposer::new();
492        let decomp = shannon.decompose(unitary, qubit_ids)?;
493
494        // Build tree structure
495        let tree = DecompositionTree::Node {
496            qubits: qubit_ids.to_vec(),
497            method: DecompositionMethod::Shannon { partition },
498            children: vec![], // Shannon decomposer doesn't provide tree structure
499        };
500
501        Ok((tree, decomp.gates))
502    }
503
504    /// Decompose block diagonal matrix
505    fn decompose_block_diagonal(
506        &mut self,
507        unitary: &Array2<Complex<f64>>,
508        qubit_ids: &[QubitId],
509        block_size: usize,
510        depth: usize,
511    ) -> QuantRS2Result<(DecompositionTree, Vec<Box<dyn GateOp>>)> {
512        let n = qubit_ids.len();
513        let num_blocks = n / block_size;
514
515        let mut gates = Vec::new();
516        let mut children = Vec::new();
517
518        // Decompose each block independently
519        for i in 0..num_blocks {
520            let start = i * block_size;
521            let end = (i + 1) * block_size;
522            let block_qubits = &qubit_ids[start..end];
523
524            // Extract block from unitary
525            let block = self.extract_block(unitary, i, block_size)?;
526
527            let (block_tree, block_gates) =
528                self.decompose_recursive(&block, block_qubits, depth + 1)?;
529            gates.extend(block_gates);
530            children.push(block_tree);
531        }
532
533        let tree = DecompositionTree::Node {
534            qubits: qubit_ids.to_vec(),
535            method: DecompositionMethod::BlockDiagonal { block_size },
536            children,
537        };
538
539        Ok((tree, gates))
540    }
541
542    /// Compute the Cosine-Sine Decomposition (CSD) of the unitary block matrix
543    /// `W = [[A, B], [C, D]]`.
544    ///
545    /// Returns `(U1, V1, Σ, U2, V2)` such that
546    ///
547    /// ```text
548    /// W = diag(U1, U2) · Σ · diag(V1, V2)†
549    /// ```
550    ///
551    /// where `U1, U2, V1, V2` are `n×n` unitaries and `Σ` is the `2n×2n` CS matrix
552    /// `[[C', S'], [-S', C']]` with diagonal cosine/sine blocks `C' = diag(cos θ_k)`,
553    /// `S' = diag(sin θ_k)` (so `Σ` is itself orthogonal).
554    ///
555    /// Algorithm (Stewart-style, driven by an SVD of the `A` block):
556    /// 1. `A = U1 · C' · V1†` via SVD; the singular values are the cosines `cos θ_k`.
557    /// 2. The columns of `C · V1` are mutually orthogonal with norms `sin θ_k`; this
558    ///    fixes `S'` and `U2 = (C V1) S'^{-1}` (with a safe fallback for `sin θ_k ≈ 0`).
559    /// 3. `V2` is recovered from `D = U2 · C' · V2†` (or `B = -U1 · S' · V2†` when a
560    ///    cosine vanishes) so that the full block identity holds.
561    ///
562    /// The result is verified against `W`; if a numerically consistent CSD cannot be
563    /// produced (degenerate edge cases the simple driver does not cover), an honest
564    /// [`QuantRS2Error::UnsupportedOperation`] is returned rather than a fabricated
565    /// identity.
566    fn compute_csd(
567        &self,
568        a: &Array2<Complex<f64>>,
569        b: &Array2<Complex<f64>>,
570        c: &Array2<Complex<f64>>,
571        d: &Array2<Complex<f64>>,
572    ) -> QuantRS2Result<(
573        Array2<Complex<f64>>, // U1
574        Array2<Complex<f64>>, // V1
575        Array2<Complex<f64>>, // Sigma (2n x 2n)
576        Array2<Complex<f64>>, // U2
577        Array2<Complex<f64>>, // V2
578    )> {
579        let n = a.shape()[0];
580        let tol = self.tolerance.max(1e-12);
581
582        // Step 1: SVD of A -> A = U1 · diag(cos) · V1†.
583        let (u1, cos_vals, v1h) = complex_svd(a)?;
584        let v1 = v1h.mapv(|z| z.conj()).t().to_owned(); // V1 (n x n)
585
586        // Cosines, clamped to [0, 1] for numerical safety.
587        let cos: Vec<f64> = cos_vals.iter().map(|&s| s.clamp(0.0, 1.0)).collect();
588
589        // Step 2: with Σ = [[C', S'], [-S', C']] the block identity gives
590        // C = -U2 · S' · V1†, hence M = C·V1 = -U2·S'. Its columns are orthogonal with
591        // norm sin θ_k, and U2 = -M · S'^{-1}.
592        let m = c.dot(&v1);
593        let mut sin = vec![0.0f64; n];
594        for k in 0..n {
595            let col_norm = (0..n).map(|r| m[[r, k]].norm_sqr()).sum::<f64>().sqrt();
596            // Prefer the value consistent with cos²+sin²=1 when the column norm is
597            // well defined; otherwise derive sin from cos.
598            sin[k] = if col_norm > tol {
599                col_norm
600            } else {
601                (1.0 - cos[k] * cos[k]).max(0.0).sqrt()
602            };
603        }
604
605        // U2 columns: where sin θ_k is non-negligible, U2[:,k] = -M[:,k] / sin θ_k.
606        // Columns with sin θ_k ≈ 0 are filled afterwards by orthonormal completion.
607        let mut u2 = Array2::<Complex<f64>>::zeros((n, n));
608        let mut needs_completion: Vec<usize> = Vec::new();
609        for k in 0..n {
610            if sin[k] > tol {
611                for r in 0..n {
612                    u2[[r, k]] = -m[[r, k]] / Complex::new(sin[k], 0.0);
613                }
614            } else {
615                needs_completion.push(k);
616            }
617        }
618        if !needs_completion.is_empty() {
619            complete_orthonormal_columns(&mut u2, &needs_completion, tol);
620        }
621
622        // Step 3: recover V2. From D = U2 · C' · V2†  ==>  V2† = C'^{-1} U2† D, falling
623        // back to B = U1 · S' · V2†  ==>  V2† = S'^{-1} U1† B for rows where cos θ_k ≈ 0.
624        let u2h = u2.mapv(|z| z.conj()).t().to_owned();
625        let u1h = u1.mapv(|z| z.conj()).t().to_owned();
626        let u2h_d = u2h.dot(d); // (n x n)
627        let u1h_b = u1h.dot(b); // (n x n)
628        let mut v2h = Array2::<Complex<f64>>::zeros((n, n));
629        for k in 0..n {
630            if cos[k] > tol {
631                for col in 0..n {
632                    v2h[[k, col]] = u2h_d[[k, col]] / Complex::new(cos[k], 0.0);
633                }
634            } else if sin[k] > tol {
635                for col in 0..n {
636                    v2h[[k, col]] = u1h_b[[k, col]] / Complex::new(sin[k], 0.0);
637                }
638            } else {
639                // cos = sin = 0 is impossible for a unitary; bail out honestly.
640                return Err(QuantRS2Error::UnsupportedOperation(
641                    "cosine-sine decomposition encountered a degenerate angle (cos=sin=0)"
642                        .to_string(),
643                ));
644            }
645        }
646        let v2 = v2h.mapv(|z| z.conj()).t().to_owned();
647
648        // Assemble Σ = [[C', S'], [-S', C']].
649        let mut sigma = Array2::<Complex<f64>>::zeros((2 * n, 2 * n));
650        for k in 0..n {
651            sigma[[k, k]] = Complex::new(cos[k], 0.0);
652            sigma[[k, n + k]] = Complex::new(sin[k], 0.0);
653            sigma[[n + k, k]] = Complex::new(-sin[k], 0.0);
654            sigma[[n + k, n + k]] = Complex::new(cos[k], 0.0);
655        }
656
657        // Verify: W ?= diag(U1, U2) · Σ · diag(V1, V2)†.
658        let recon = Self::assemble_from_csd(&u1, &u2, &sigma, &v1, &v2);
659        let mut max_err = 0.0f64;
660        for (i, j, expected) in [(0usize, 0usize, a), (0, 1, b), (1, 0, c), (1, 1, d)]
661            .iter()
662            .flat_map(|&(bi, bj, blk)| {
663                (0..n).flat_map(move |r| {
664                    (0..n).map(move |col| (bi * n + r, bj * n + col, blk[[r, col]]))
665                })
666            })
667        {
668            max_err = max_err.max((recon[[i, j]] - expected).norm());
669        }
670
671        if max_err > 1e-7 {
672            return Err(QuantRS2Error::UnsupportedOperation(format!(
673                "cosine-sine decomposition for this matrix is not yet supported \
674                 (reconstruction error {max_err:.3e})"
675            )));
676        }
677
678        Ok((u1, v1, sigma, u2, v2))
679    }
680
681    /// Reassemble `diag(U1, U2) · Σ · diag(V1, V2)†` into a `2n×2n` matrix.
682    fn assemble_from_csd(
683        u1: &Array2<Complex<f64>>,
684        u2: &Array2<Complex<f64>>,
685        sigma: &Array2<Complex<f64>>,
686        v1: &Array2<Complex<f64>>,
687        v2: &Array2<Complex<f64>>,
688    ) -> Array2<Complex<f64>> {
689        let n = u1.nrows();
690        let mut left = Array2::<Complex<f64>>::zeros((2 * n, 2 * n));
691        left.slice_mut(s![..n, ..n]).assign(u1);
692        left.slice_mut(s![n.., n..]).assign(u2);
693
694        let mut right_dag = Array2::<Complex<f64>>::zeros((2 * n, 2 * n));
695        let v1h = v1.mapv(|z| z.conj()).t().to_owned();
696        let v2h = v2.mapv(|z| z.conj()).t().to_owned();
697        right_dag.slice_mut(s![..n, ..n]).assign(&v1h);
698        right_dag.slice_mut(s![n.., n..]).assign(&v2h);
699
700        left.dot(sigma).dot(&right_dag)
701    }
702
703    /// Convert diagonal matrix to controlled rotation gates
704    fn diagonal_to_gates(
705        &self,
706        diagonal: &Array2<Complex<f64>>,
707        qubit_ids: &[QubitId],
708    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
709        let mut gates = Vec::new();
710
711        // Extract diagonal elements
712        let n = diagonal.shape()[0];
713        for i in 0..n {
714            let phase = diagonal[[i, i]].arg();
715            if phase.abs() > self.tolerance {
716                // Determine which qubits are in state |1⟩ for this diagonal element
717                let mut control_pattern = Vec::new();
718                let mut temp = i;
719                for j in 0..qubit_ids.len() {
720                    if temp & 1 == 1 {
721                        control_pattern.push(j);
722                    }
723                    temp >>= 1;
724                }
725
726                // Create multi-controlled phase gate
727                if control_pattern.is_empty() {
728                    // Global phase - can be ignored
729                } else if control_pattern.len() == 1 {
730                    // Single-qubit phase
731                    gates.push(Box::new(RotationZ {
732                        target: qubit_ids[control_pattern[0]],
733                        theta: phase,
734                    }) as Box<dyn GateOp>);
735                } else {
736                    // Multi-controlled phase - decompose further
737                    // For now, use simple decomposition
738                    // Note: control_pattern.len() >= 2 at this point, so pop is safe
739                    let target_idx = control_pattern.pop().unwrap_or(0);
740                    let target = qubit_ids[target_idx];
741                    for &control_idx in &control_pattern {
742                        gates.push(Box::new(CNOT {
743                            control: qubit_ids[control_idx],
744                            target,
745                        }));
746                    }
747
748                    gates.push(Box::new(RotationZ {
749                        target,
750                        theta: phase,
751                    }) as Box<dyn GateOp>);
752
753                    // Uncompute CNOTs
754                    for &control_idx in control_pattern.iter().rev() {
755                        gates.push(Box::new(CNOT {
756                            control: qubit_ids[control_idx],
757                            target,
758                        }));
759                    }
760                }
761            }
762        }
763
764        Ok(gates)
765    }
766
767    /// Check if matrix has block diagonal structure
768    fn has_block_structure(&self, unitary: &Array2<Complex<f64>>, _n: usize) -> bool {
769        // Simple check - look for zeros in off-diagonal blocks
770        let size = unitary.shape()[0];
771        let block_size = size / 2;
772
773        let mut off_diagonal_norm = 0.0;
774
775        // Check upper-right block
776        for i in 0..block_size {
777            for j in block_size..size {
778                off_diagonal_norm += unitary[[i, j]].norm_sqr();
779            }
780        }
781
782        // Check lower-left block
783        for i in block_size..size {
784            for j in 0..block_size {
785                off_diagonal_norm += unitary[[i, j]].norm_sqr();
786            }
787        }
788
789        off_diagonal_norm.sqrt() < self.tolerance
790    }
791
792    /// Extract a block from block-diagonal matrix
793    fn extract_block(
794        &self,
795        unitary: &Array2<Complex<f64>>,
796        block_idx: usize,
797        block_size: usize,
798    ) -> QuantRS2Result<Array2<Complex<f64>>> {
799        let size = 1 << block_size;
800        let start = block_idx * size;
801        let end = (block_idx + 1) * size;
802
803        Ok(unitary.slice(s![start..end, start..end]).to_owned())
804    }
805
806    /// Convert single-qubit decomposition to gates
807    fn single_qubit_to_gates(
808        &self,
809        decomp: &SingleQubitDecomposition,
810        qubit: QubitId,
811    ) -> Vec<Box<dyn GateOp>> {
812        let mut gates = Vec::new();
813
814        if decomp.theta1.abs() > self.tolerance {
815            gates.push(Box::new(RotationZ {
816                target: qubit,
817                theta: decomp.theta1,
818            }) as Box<dyn GateOp>);
819        }
820
821        if decomp.phi.abs() > self.tolerance {
822            gates.push(Box::new(RotationY {
823                target: qubit,
824                theta: decomp.phi,
825            }) as Box<dyn GateOp>);
826        }
827
828        if decomp.theta2.abs() > self.tolerance {
829            gates.push(Box::new(RotationZ {
830                target: qubit,
831                theta: decomp.theta2,
832            }) as Box<dyn GateOp>);
833        }
834
835        gates
836    }
837
838    /// Count CNOTs for different gate types
839    fn count_cnots(&self, gate_name: &str) -> usize {
840        match gate_name {
841            "CNOT" | "CZ" => 1, // CZ = H·CNOT·H
842            "SWAP" => 3,        // SWAP uses 3 CNOTs
843            _ => 0,
844        }
845    }
846
847    /// Check cache for existing decomposition
848    /// Calculate circuit depth as the length of the critical path through the DAG.
849    ///
850    /// For each gate in topological order (gates are already ordered):
851    ///   `depth[i] = 1 + max(depth[j])` for all j < i that share at least one qubit with gate i.
852    ///
853    /// Uses a BFS/forward-pass approach since gates are given in topological order.
854    fn calculate_circuit_depth(gates: &[Box<dyn GateOp>]) -> usize {
855        if gates.is_empty() {
856            return 0;
857        }
858
859        // depth_at[i] = the depth level at which gate i completes (1-based)
860        let mut depth_at: Vec<usize> = vec![0; gates.len()];
861        // last_qubit_depth maps qubit id -> (gate_index, depth) of the last gate on that qubit
862        let mut last_qubit_finish: FxHashMap<u32, usize> = FxHashMap::default();
863
864        for (i, gate) in gates.iter().enumerate() {
865            let qubits = gate.qubits();
866            // Find the maximum finish depth among all preceding gates on shared qubits
867            let predecessor_max_depth = qubits
868                .iter()
869                .filter_map(|q| last_qubit_finish.get(&q.0).copied())
870                .max()
871                .unwrap_or(0);
872
873            depth_at[i] = predecessor_max_depth + 1;
874
875            // Update last finish depth for each qubit this gate touches
876            for q in &qubits {
877                last_qubit_finish.insert(q.0, depth_at[i]);
878            }
879        }
880
881        depth_at.into_iter().max().unwrap_or(0)
882    }
883
884    const fn check_cache(&self, _unitary: &Array2<Complex<f64>>) -> Option<&MultiQubitKAK> {
885        // Simple hash based on first few elements
886        // Real implementation would use better hashing
887        None
888    }
889
890    /// Cache decomposition result
891    const fn cache_result(&self, _unitary: &Array2<Complex<f64>>, _result: &MultiQubitKAK) {
892        // Cache implementation
893    }
894}
895
896impl Default for MultiQubitKAKDecomposer {
897    fn default() -> Self {
898        Self::new()
899    }
900}
901
902/// Analyze decomposition tree structure
903pub struct KAKTreeAnalyzer {
904    /// Track statistics
905    stats: DecompositionStats,
906}
907
908#[derive(Debug, Default, Clone)]
909pub struct DecompositionStats {
910    pub total_nodes: usize,
911    pub leaf_nodes: usize,
912    pub max_depth: usize,
913    pub method_counts: FxHashMap<String, usize>,
914    pub cnot_distribution: FxHashMap<usize, usize>,
915}
916
917impl KAKTreeAnalyzer {
918    /// Create new analyzer
919    pub fn new() -> Self {
920        Self {
921            stats: DecompositionStats::default(),
922        }
923    }
924
925    /// Analyze decomposition tree
926    pub fn analyze(&mut self, tree: &DecompositionTree) -> DecompositionStats {
927        self.stats = DecompositionStats::default();
928        self.analyze_recursive(tree, 0);
929        self.stats.clone()
930    }
931
932    fn analyze_recursive(&mut self, tree: &DecompositionTree, depth: usize) {
933        self.stats.total_nodes += 1;
934        self.stats.max_depth = self.stats.max_depth.max(depth);
935
936        match tree {
937            DecompositionTree::Leaf {
938                qubits: _qubits,
939                gate_type,
940            } => {
941                self.stats.leaf_nodes += 1;
942
943                match gate_type {
944                    LeafType::SingleQubit(_) => {
945                        *self
946                            .stats
947                            .method_counts
948                            .entry("single_qubit".to_string())
949                            .or_insert(0) += 1;
950                    }
951                    LeafType::TwoQubit(cartan) => {
952                        *self
953                            .stats
954                            .method_counts
955                            .entry("two_qubit".to_string())
956                            .or_insert(0) += 1;
957                        let cnots = cartan.interaction.cnot_count(1e-10);
958                        *self.stats.cnot_distribution.entry(cnots).or_insert(0) += 1;
959                    }
960                }
961            }
962            DecompositionTree::Node {
963                method, children, ..
964            } => {
965                let method_name = match method {
966                    DecompositionMethod::CSD { .. } => "csd",
967                    DecompositionMethod::Shannon { .. } => "shannon",
968                    DecompositionMethod::BlockDiagonal { .. } => "block_diagonal",
969                    DecompositionMethod::Cartan => "cartan",
970                };
971                *self
972                    .stats
973                    .method_counts
974                    .entry(method_name.to_string())
975                    .or_insert(0) += 1;
976
977                for child in children {
978                    self.analyze_recursive(child, depth + 1);
979                }
980            }
981        }
982    }
983}
984
985/// Utility function for quick multi-qubit KAK decomposition
986pub fn kak_decompose_multiqubit(
987    unitary: &Array2<Complex<f64>>,
988    qubit_ids: &[QubitId],
989) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
990    let mut decomposer = MultiQubitKAKDecomposer::new();
991    let decomp = decomposer.decompose(unitary, qubit_ids)?;
992    Ok(decomp.gates)
993}
994
995#[cfg(test)]
996mod tests {
997    use super::*;
998    use scirs2_core::ndarray::Array2;
999    use scirs2_core::Complex;
1000
1001    #[test]
1002    fn test_multiqubit_kak_single() {
1003        let mut decomposer = MultiQubitKAKDecomposer::new();
1004
1005        // Hadamard matrix
1006        let h = Array2::from_shape_vec(
1007            (2, 2),
1008            vec![
1009                Complex::new(1.0, 0.0),
1010                Complex::new(1.0, 0.0),
1011                Complex::new(1.0, 0.0),
1012                Complex::new(-1.0, 0.0),
1013            ],
1014        )
1015        .expect("Failed to create Hadamard matrix")
1016            / Complex::new(2.0_f64.sqrt(), 0.0);
1017
1018        let qubit_ids = vec![QubitId(0)];
1019        let decomp = decomposer
1020            .decompose(&h, &qubit_ids)
1021            .expect("Single-qubit KAK decomposition failed");
1022
1023        assert!(decomp.single_qubit_count <= 3);
1024        assert_eq!(decomp.cnot_count, 0);
1025
1026        // Check tree structure
1027        match &decomp.tree {
1028            DecompositionTree::Leaf {
1029                gate_type: LeafType::SingleQubit(_),
1030                ..
1031            } => {}
1032            _ => panic!("Expected single-qubit leaf"),
1033        }
1034    }
1035
1036    #[test]
1037    fn test_multiqubit_kak_two() {
1038        let mut decomposer = MultiQubitKAKDecomposer::new();
1039
1040        // CNOT matrix
1041        let cnot = Array2::from_shape_vec(
1042            (4, 4),
1043            vec![
1044                Complex::new(1.0, 0.0),
1045                Complex::new(0.0, 0.0),
1046                Complex::new(0.0, 0.0),
1047                Complex::new(0.0, 0.0),
1048                Complex::new(0.0, 0.0),
1049                Complex::new(1.0, 0.0),
1050                Complex::new(0.0, 0.0),
1051                Complex::new(0.0, 0.0),
1052                Complex::new(0.0, 0.0),
1053                Complex::new(0.0, 0.0),
1054                Complex::new(0.0, 0.0),
1055                Complex::new(1.0, 0.0),
1056                Complex::new(0.0, 0.0),
1057                Complex::new(0.0, 0.0),
1058                Complex::new(1.0, 0.0),
1059                Complex::new(0.0, 0.0),
1060            ],
1061        )
1062        .expect("Failed to create CNOT matrix");
1063
1064        let qubit_ids = vec![QubitId(0), QubitId(1)];
1065        let decomp = decomposer
1066            .decompose(&cnot, &qubit_ids)
1067            .expect("Two-qubit KAK decomposition failed");
1068
1069        assert!(decomp.cnot_count <= 1);
1070
1071        // Check tree structure
1072        match &decomp.tree {
1073            DecompositionTree::Leaf {
1074                gate_type: LeafType::TwoQubit(_),
1075                ..
1076            } => {}
1077            _ => panic!("Expected two-qubit leaf"),
1078        }
1079    }
1080
1081    #[test]
1082    fn test_multiqubit_kak_three() {
1083        let mut decomposer = MultiQubitKAKDecomposer::new();
1084
1085        // 3-qubit identity
1086        let identity = Array2::eye(8);
1087        let identity_complex = identity.mapv(|x| Complex::new(x, 0.0));
1088
1089        let qubit_ids = vec![QubitId(0), QubitId(1), QubitId(2)];
1090        let decomp = decomposer
1091            .decompose(&identity_complex, &qubit_ids)
1092            .expect("Three-qubit KAK decomposition failed");
1093
1094        // Identity should result in empty circuit
1095        assert_eq!(decomp.gates.len(), 0);
1096        assert_eq!(decomp.cnot_count, 0);
1097        assert_eq!(decomp.single_qubit_count, 0);
1098    }
1099
1100    #[test]
1101    fn test_tree_analyzer() {
1102        let mut analyzer = KAKTreeAnalyzer::new();
1103
1104        // Create a simple tree
1105        let tree = DecompositionTree::Node {
1106            qubits: vec![QubitId(0), QubitId(1), QubitId(2)],
1107            method: DecompositionMethod::CSD { pivot: 2 },
1108            children: vec![
1109                DecompositionTree::Leaf {
1110                    qubits: vec![QubitId(0), QubitId(1)],
1111                    gate_type: LeafType::TwoQubit(CartanDecomposition {
1112                        left_gates: (
1113                            SingleQubitDecomposition {
1114                                global_phase: 0.0,
1115                                theta1: 0.0,
1116                                phi: 0.0,
1117                                theta2: 0.0,
1118                                basis: "ZYZ".to_string(),
1119                            },
1120                            SingleQubitDecomposition {
1121                                global_phase: 0.0,
1122                                theta1: 0.0,
1123                                phi: 0.0,
1124                                theta2: 0.0,
1125                                basis: "ZYZ".to_string(),
1126                            },
1127                        ),
1128                        right_gates: (
1129                            SingleQubitDecomposition {
1130                                global_phase: 0.0,
1131                                theta1: 0.0,
1132                                phi: 0.0,
1133                                theta2: 0.0,
1134                                basis: "ZYZ".to_string(),
1135                            },
1136                            SingleQubitDecomposition {
1137                                global_phase: 0.0,
1138                                theta1: 0.0,
1139                                phi: 0.0,
1140                                theta2: 0.0,
1141                                basis: "ZYZ".to_string(),
1142                            },
1143                        ),
1144                        interaction: crate::prelude::CartanCoefficients::new(0.0, 0.0, 0.0),
1145                        global_phase: 0.0,
1146                    }),
1147                },
1148                DecompositionTree::Leaf {
1149                    qubits: vec![QubitId(2)],
1150                    gate_type: LeafType::SingleQubit(SingleQubitDecomposition {
1151                        global_phase: 0.0,
1152                        theta1: 0.0,
1153                        phi: 0.0,
1154                        theta2: 0.0,
1155                        basis: "ZYZ".to_string(),
1156                    }),
1157                },
1158            ],
1159        };
1160
1161        let stats = analyzer.analyze(&tree);
1162
1163        assert_eq!(stats.total_nodes, 3);
1164        assert_eq!(stats.leaf_nodes, 2);
1165        assert_eq!(stats.max_depth, 1);
1166        assert_eq!(stats.method_counts.get("csd"), Some(&1));
1167    }
1168
1169    /// Gram–Schmidt orthonormalisation of the columns of a square complex matrix,
1170    /// producing a unitary (used to manufacture test unitaries).
1171    fn orthonormalize(mut m: Array2<Complex<f64>>) -> Array2<Complex<f64>> {
1172        let n = m.nrows();
1173        for j in 0..n {
1174            for prev in 0..j {
1175                let proj: Complex<f64> = (0..n).map(|r| m[[r, prev]].conj() * m[[r, j]]).sum();
1176                for r in 0..n {
1177                    let sub = proj * m[[r, prev]];
1178                    m[[r, j]] -= sub;
1179                }
1180            }
1181            let norm = (0..n).map(|r| m[[r, j]].norm_sqr()).sum::<f64>().sqrt();
1182            for r in 0..n {
1183                m[[r, j]] /= Complex::new(norm, 0.0);
1184            }
1185        }
1186        m
1187    }
1188
1189    /// Site-3 proof: the CSD returned by `compute_csd` recomposes to the original
1190    /// unitary block matrix `W = [[A,B],[C,D]]` within 1e-8, and the factors are the
1191    /// promised unitaries / CS structure (no fabricated identities).
1192    #[test]
1193    fn test_compute_csd_recomposes() {
1194        let decomposer = MultiQubitKAKDecomposer::new();
1195
1196        // Manufacture a 4x4 unitary W (n = 2 blocks) from a fixed complex matrix.
1197        let raw = Array2::from_shape_vec(
1198            (4, 4),
1199            vec![
1200                Complex::new(0.5, 0.2),
1201                Complex::new(-0.3, 0.4),
1202                Complex::new(0.1, -0.2),
1203                Complex::new(0.6, 0.0),
1204                Complex::new(0.2, -0.1),
1205                Complex::new(0.5, 0.3),
1206                Complex::new(-0.4, 0.1),
1207                Complex::new(0.0, 0.2),
1208                Complex::new(-0.3, 0.2),
1209                Complex::new(0.1, 0.5),
1210                Complex::new(0.6, -0.1),
1211                Complex::new(0.2, 0.3),
1212                Complex::new(0.4, 0.0),
1213                Complex::new(-0.2, 0.3),
1214                Complex::new(0.1, 0.4),
1215                Complex::new(0.5, -0.2),
1216            ],
1217        )
1218        .expect("raw matrix");
1219        let w = orthonormalize(raw);
1220
1221        // Confirm W is unitary.
1222        let wh = w.mapv(|z| z.conj()).t().to_owned();
1223        let prod = wh.dot(&w);
1224        for i in 0..4 {
1225            for j in 0..4 {
1226                let exp = if i == j { 1.0 } else { 0.0 };
1227                assert!(
1228                    (prod[[i, j]] - Complex::new(exp, 0.0)).norm() < 1e-9,
1229                    "manufactured W is not unitary"
1230                );
1231            }
1232        }
1233
1234        let a = w.slice(s![..2, ..2]).to_owned();
1235        let b = w.slice(s![..2, 2..]).to_owned();
1236        let c = w.slice(s![2.., ..2]).to_owned();
1237        let d = w.slice(s![2.., 2..]).to_owned();
1238
1239        let (u1, v1, sigma, u2, v2) = decomposer
1240            .compute_csd(&a, &b, &c, &d)
1241            .expect("CSD should succeed for a generic 4x4 unitary");
1242
1243        // U1, U2, V1, V2 must be unitary (not identity-fabrications unless genuinely so).
1244        for (name, mat) in [("U1", &u1), ("U2", &u2), ("V1", &v1), ("V2", &v2)] {
1245            let mh = mat.mapv(|z| z.conj()).t().to_owned();
1246            let pr = mh.dot(mat);
1247            for i in 0..2 {
1248                for j in 0..2 {
1249                    let exp = if i == j { 1.0 } else { 0.0 };
1250                    assert!(
1251                        (pr[[i, j]] - Complex::new(exp, 0.0)).norm() < 1e-7,
1252                        "{name} is not unitary"
1253                    );
1254                }
1255            }
1256        }
1257
1258        // Sigma must have the CS structure: Σ = [[C', S'], [-S', C']] and be orthogonal.
1259        let sh = sigma.mapv(|z| z.conj()).t().to_owned();
1260        let sps = sh.dot(&sigma);
1261        for i in 0..4 {
1262            for j in 0..4 {
1263                let exp = if i == j { 1.0 } else { 0.0 };
1264                assert!(
1265                    (sps[[i, j]] - Complex::new(exp, 0.0)).norm() < 1e-7,
1266                    "Sigma (CS matrix) is not orthogonal"
1267                );
1268            }
1269        }
1270
1271        // Recompose and compare to W.
1272        let recon = MultiQubitKAKDecomposer::assemble_from_csd(&u1, &u2, &sigma, &v1, &v2);
1273        let mut max_err = 0.0f64;
1274        for i in 0..4 {
1275            for j in 0..4 {
1276                max_err = max_err.max((recon[[i, j]] - w[[i, j]]).norm());
1277            }
1278        }
1279        assert!(
1280            max_err < 1e-8,
1281            "CSD recomposition error {max_err} exceeds 1e-8"
1282        );
1283    }
1284
1285    #[test]
1286    fn test_complex_svd_helper_roundtrip() {
1287        // Independent check of the complex SVD used by the CSD.
1288        let m = Array2::from_shape_vec(
1289            (3, 3),
1290            vec![
1291                Complex::new(1.0, 0.2),
1292                Complex::new(0.3, -0.4),
1293                Complex::new(-0.1, 0.5),
1294                Complex::new(0.2, 0.1),
1295                Complex::new(-0.5, 0.3),
1296                Complex::new(0.4, 0.0),
1297                Complex::new(0.0, -0.3),
1298                Complex::new(0.6, 0.1),
1299                Complex::new(-0.2, 0.2),
1300            ],
1301        )
1302        .expect("matrix");
1303        let (u, s, vh) = complex_svd(&m).expect("svd");
1304        let mut s_mat = Array2::<Complex<f64>>::zeros((3, 3));
1305        for i in 0..3 {
1306            s_mat[[i, i]] = Complex::new(s[i], 0.0);
1307        }
1308        let recon = u.dot(&s_mat).dot(&vh);
1309        let mut err = 0.0f64;
1310        for i in 0..3 {
1311            for j in 0..3 {
1312                err = err.max((recon[[i, j]] - m[[i, j]]).norm());
1313            }
1314        }
1315        assert!(err < 1e-9, "complex_svd roundtrip error {err}");
1316    }
1317
1318    #[test]
1319    fn test_block_structure_detection() {
1320        let decomposer = MultiQubitKAKDecomposer::new();
1321
1322        // Create block diagonal matrix
1323        let mut block_diag = Array2::zeros((4, 4));
1324        block_diag[[0, 0]] = Complex::new(1.0, 0.0);
1325        block_diag[[1, 1]] = Complex::new(1.0, 0.0);
1326        block_diag[[2, 2]] = Complex::new(1.0, 0.0);
1327        block_diag[[3, 3]] = Complex::new(1.0, 0.0);
1328
1329        assert!(decomposer.has_block_structure(&block_diag, 2));
1330
1331        // Non-block diagonal
1332        block_diag[[0, 2]] = Complex::new(1.0, 0.0);
1333        assert!(!decomposer.has_block_structure(&block_diag, 2));
1334    }
1335}