Skip to main content

quantrs2_core/
shannon.rs

1//! Quantum Shannon decomposition for arbitrary unitaries
2//!
3//! This module implements the quantum Shannon decomposition algorithm,
4//! which decomposes any n-qubit unitary into a sequence of single-qubit
5//! and CNOT gates with asymptotically optimal gate count.
6
7use crate::{
8    cartan::OptimizedCartanDecomposer,
9    controlled::make_controlled,
10    error::{QuantRS2Error, QuantRS2Result},
11    gate::{single::*, GateOp},
12    matrix_ops::{DenseMatrix, QuantumMatrix},
13    qubit::QubitId,
14    synthesis::{decompose_single_qubit_zyz, SingleQubitDecomposition},
15};
16use rustc_hash::FxHashMap;
17use scirs2_core::ndarray::{s, Array2};
18use scirs2_core::Complex;
19use std::f64::consts::PI;
20
21/// Shannon decomposition result for an n-qubit unitary
22#[derive(Debug, Clone)]
23pub struct ShannonDecomposition {
24    /// The decomposed gate sequence
25    pub gates: Vec<Box<dyn GateOp>>,
26    /// Number of CNOT gates used
27    pub cnot_count: usize,
28    /// Number of single-qubit gates used
29    pub single_qubit_count: usize,
30    /// Total circuit depth
31    pub depth: usize,
32}
33
34/// Shannon decomposer for quantum circuits
35pub struct ShannonDecomposer {
36    /// Tolerance for numerical comparisons
37    tolerance: f64,
38    /// Cache for small unitaries
39    cache: FxHashMap<u64, ShannonDecomposition>,
40    /// Maximum recursion depth
41    max_depth: usize,
42}
43
44impl ShannonDecomposer {
45    /// Create a new Shannon decomposer
46    pub fn new() -> Self {
47        Self {
48            tolerance: 1e-10,
49            cache: FxHashMap::default(),
50            max_depth: 20,
51        }
52    }
53
54    /// Create with custom tolerance
55    pub fn with_tolerance(tolerance: f64) -> Self {
56        Self {
57            tolerance,
58            cache: FxHashMap::default(),
59            max_depth: 20,
60        }
61    }
62
63    /// Decompose an n-qubit unitary matrix
64    pub fn decompose(
65        &mut self,
66        unitary: &Array2<Complex<f64>>,
67        qubit_ids: &[QubitId],
68    ) -> QuantRS2Result<ShannonDecomposition> {
69        let n = qubit_ids.len();
70        let size = 1 << n;
71
72        // Validate input
73        if unitary.shape() != [size, size] {
74            return Err(QuantRS2Error::InvalidInput(format!(
75                "Unitary size {} doesn't match {} qubits",
76                unitary.shape()[0],
77                n
78            )));
79        }
80
81        // Check unitarity
82        let mat = DenseMatrix::new(unitary.clone())?;
83        if !mat.is_unitary(self.tolerance)? {
84            return Err(QuantRS2Error::InvalidInput(
85                "Matrix is not unitary".to_string(),
86            ));
87        }
88
89        // Base cases
90        if n == 0 {
91            return Ok(ShannonDecomposition {
92                gates: vec![],
93                cnot_count: 0,
94                single_qubit_count: 0,
95                depth: 0,
96            });
97        }
98
99        if n == 1 {
100            // Single-qubit gate
101            let decomp = decompose_single_qubit_zyz(&unitary.view())?;
102            let gates = self.single_qubit_to_gates(&decomp, qubit_ids[0]);
103            let count = gates.len();
104
105            return Ok(ShannonDecomposition {
106                gates,
107                cnot_count: 0,
108                single_qubit_count: count,
109                depth: count,
110            });
111        }
112
113        if n == 2 {
114            // Use specialized two-qubit decomposition
115            return self.decompose_two_qubit(unitary, qubit_ids);
116        }
117
118        // For n > 2, use recursive Shannon decomposition
119        self.decompose_recursive(unitary, qubit_ids, 0)
120    }
121
122    /// Recursive Shannon decomposition for n > 2 qubits
123    fn decompose_recursive(
124        &mut self,
125        unitary: &Array2<Complex<f64>>,
126        qubit_ids: &[QubitId],
127        depth: usize,
128    ) -> QuantRS2Result<ShannonDecomposition> {
129        if depth > self.max_depth {
130            return Err(QuantRS2Error::InvalidInput(
131                "Maximum recursion depth exceeded".to_string(),
132            ));
133        }
134
135        let n = qubit_ids.len();
136        let half_size = 1 << (n - 1);
137
138        // Split the unitary into blocks based on the first qubit
139        // U = [A B]
140        //     [C D]
141        let a = unitary.slice(s![..half_size, ..half_size]).to_owned();
142        let b = unitary.slice(s![..half_size, half_size..]).to_owned();
143        let c = unitary.slice(s![half_size.., ..half_size]).to_owned();
144        let d = unitary.slice(s![half_size.., half_size..]).to_owned();
145
146        // Use block decomposition to find V, W such that:
147        // U = (I ⊗ V) · Controlled-U_d · (I ⊗ W)
148        // where U_d is diagonal in the computational basis
149        let (v, w, u_diag) = self.block_diagonalize(&a, &b, &c, &d)?;
150
151        let mut gates: Vec<Box<dyn GateOp>> = Vec::new();
152        let mut cnot_count = 0;
153        let mut single_qubit_count = 0;
154
155        // Apply W to the lower qubits
156        if !self.is_identity(&w) {
157            let w_decomp = self.decompose_recursive(&w, &qubit_ids[1..], depth + 1)?;
158            gates.extend(w_decomp.gates);
159            cnot_count += w_decomp.cnot_count;
160            single_qubit_count += w_decomp.single_qubit_count;
161        }
162
163        // Apply controlled diagonal gates
164        let diag_gates = self.decompose_controlled_diagonal(&u_diag, qubit_ids)?;
165        cnot_count += diag_gates.1;
166        single_qubit_count += diag_gates.2;
167        gates.extend(diag_gates.0);
168
169        // Apply V† to the lower qubits
170        if !self.is_identity(&v) {
171            let v_dag = v.mapv(|z| z.conj()).t().to_owned();
172            let v_decomp = self.decompose_recursive(&v_dag, &qubit_ids[1..], depth + 1)?;
173            gates.extend(v_decomp.gates);
174            cnot_count += v_decomp.cnot_count;
175            single_qubit_count += v_decomp.single_qubit_count;
176        }
177
178        // Calculate depth (approximate)
179        let depth = gates.len();
180
181        Ok(ShannonDecomposition {
182            gates,
183            cnot_count,
184            single_qubit_count,
185            depth,
186        })
187    }
188
189    /// Block-diagonalise a 2×2 block matrix `[[A,B],[C,D]]` for the Shannon recursion
190    /// `U = (I ⊗ V) · U_d · (I ⊗ W)` with `U_d` block diagonal.
191    ///
192    /// Correct, exact case: when the off-diagonal blocks already vanish
193    /// (`‖B‖, ‖C‖ ≈ 0`) the matrix is `diag(A, D)` and the factorisation is trivial
194    /// with `V = W = I` and `U_d = diag(A, D)`. This branch is exact and is preserved.
195    ///
196    /// General case: a faithful demultiplexing requires the full cosine-sine
197    /// decomposition together with uniformly-controlled-rotation synthesis, which this
198    /// recursion does not yet provide. Rather than silently returning identity factors
199    /// and the *unfactored* matrix (which would misrepresent the state — the former
200    /// behaviour), an honest [`QuantRS2Error::UnsupportedOperation`] is returned.
201    fn block_diagonalize(
202        &self,
203        a: &Array2<Complex<f64>>,
204        b: &Array2<Complex<f64>>,
205        c: &Array2<Complex<f64>>,
206        d: &Array2<Complex<f64>>,
207    ) -> QuantRS2Result<(
208        Array2<Complex<f64>>,
209        Array2<Complex<f64>>,
210        Array2<Complex<f64>>,
211    )> {
212        let size = a.shape()[0];
213
214        // Already block diagonal: U = diag(A, D); exact trivial factorisation.
215        let b_norm = b.iter().map(|z| z.norm_sqr()).sum::<f64>().sqrt();
216        let c_norm = c.iter().map(|z| z.norm_sqr()).sum::<f64>().sqrt();
217
218        if b_norm < self.tolerance && c_norm < self.tolerance {
219            let identity = Array2::eye(size);
220            let combined = self.combine_blocks(a, b, c, d);
221            return Ok((identity.clone(), identity, combined));
222        }
223
224        // General (genuinely block-coupled) case is not yet supported here.
225        Err(QuantRS2Error::UnsupportedOperation(
226            "quantum Shannon block-diagonalization for matrices with non-zero off-diagonal \
227             blocks requires cosine-sine decomposition and is not yet implemented; \
228             use the multi-qubit KAK decomposer (CSD/block-diagonal paths) instead"
229                .to_string(),
230        ))
231    }
232
233    /// Combine 2x2 blocks into a single matrix
234    fn combine_blocks(
235        &self,
236        a: &Array2<Complex<f64>>,
237        b: &Array2<Complex<f64>>,
238        c: &Array2<Complex<f64>>,
239        d: &Array2<Complex<f64>>,
240    ) -> Array2<Complex<f64>> {
241        let size = a.shape()[0];
242        let total_size = 2 * size;
243        let mut result = Array2::zeros((total_size, total_size));
244
245        result.slice_mut(s![..size, ..size]).assign(a);
246        result.slice_mut(s![..size, size..]).assign(b);
247        result.slice_mut(s![size.., ..size]).assign(c);
248        result.slice_mut(s![size.., size..]).assign(d);
249
250        result
251    }
252
253    /// Decompose controlled diagonal gates
254    fn decompose_controlled_diagonal(
255        &self,
256        diagonal: &Array2<Complex<f64>>,
257        qubit_ids: &[QubitId],
258    ) -> QuantRS2Result<(Vec<Box<dyn GateOp>>, usize, usize)> {
259        let mut gates: Vec<Box<dyn GateOp>> = Vec::new();
260        let mut cnot_count = 0;
261        let mut single_qubit_count = 0;
262
263        // Extract diagonal elements
264        let n = diagonal.shape()[0];
265        let mut phases = Vec::with_capacity(n);
266
267        for i in 0..n {
268            let phase = diagonal[[i, i]].arg();
269            phases.push(phase);
270        }
271
272        // Decompose into controlled phase gates
273        // This is a simplified version - optimal decomposition would use Gray codes
274        let control = qubit_ids[0];
275
276        for (i, &phase) in phases.iter().enumerate() {
277            if phase.abs() > self.tolerance {
278                if i == 0 {
279                    // Global phase on |0⟩ state
280                    let gate: Box<dyn GateOp> = Box::new(RotationZ {
281                        target: control,
282                        theta: phase,
283                    });
284                    gates.push(gate);
285                    single_qubit_count += 1;
286                } else {
287                    // Controlled phase
288                    // For now, use simple controlled-RZ
289                    // Optimal would use multi-controlled decomposition
290                    let base_gate = Box::new(RotationZ {
291                        target: qubit_ids[1],
292                        theta: phase,
293                    });
294
295                    let controlled = Box::new(make_controlled(vec![control], *base_gate));
296                    gates.push(controlled);
297                    cnot_count += 2; // Controlled-RZ uses 2 CNOTs
298                    single_qubit_count += 3; // And 3 single-qubit gates
299                }
300            }
301        }
302
303        Ok((gates, cnot_count, single_qubit_count))
304    }
305
306    /// Specialized two-qubit decomposition
307    fn decompose_two_qubit(
308        &self,
309        unitary: &Array2<Complex<f64>>,
310        qubit_ids: &[QubitId],
311    ) -> QuantRS2Result<ShannonDecomposition> {
312        // Check for identity matrix first
313        if self.is_identity(unitary) {
314            return Ok(ShannonDecomposition {
315                gates: vec![],
316                cnot_count: 0,
317                single_qubit_count: 0,
318                depth: 0,
319            });
320        }
321
322        // Use Cartan (KAK) decomposition for optimal two-qubit decomposition
323        let mut cartan_decomposer = OptimizedCartanDecomposer::new();
324        let cartan_decomp = cartan_decomposer.decompose(unitary)?;
325        let gates = cartan_decomposer.base.to_gates(&cartan_decomp, qubit_ids)?;
326
327        // Count gates
328        let mut cnot_count = 0;
329        let mut single_qubit_count = 0;
330
331        for gate in &gates {
332            match gate.name() {
333                "CNOT" => cnot_count += 1,
334                _ => single_qubit_count += 1,
335            }
336        }
337
338        let depth = gates.len();
339
340        Ok(ShannonDecomposition {
341            gates,
342            cnot_count,
343            single_qubit_count,
344            depth,
345        })
346    }
347
348    /// Convert single-qubit decomposition to gates
349    fn single_qubit_to_gates(
350        &self,
351        decomp: &SingleQubitDecomposition,
352        qubit: QubitId,
353    ) -> Vec<Box<dyn GateOp>> {
354        let mut gates = Vec::new();
355
356        // First RZ rotation
357        if decomp.theta1.abs() > self.tolerance {
358            gates.push(Box::new(RotationZ {
359                target: qubit,
360                theta: decomp.theta1,
361            }) as Box<dyn GateOp>);
362        }
363
364        // RY rotation
365        if decomp.phi.abs() > self.tolerance {
366            gates.push(Box::new(RotationY {
367                target: qubit,
368                theta: decomp.phi,
369            }) as Box<dyn GateOp>);
370        }
371
372        // Second RZ rotation
373        if decomp.theta2.abs() > self.tolerance {
374            gates.push(Box::new(RotationZ {
375                target: qubit,
376                theta: decomp.theta2,
377            }) as Box<dyn GateOp>);
378        }
379
380        // Global phase is ignored in gate sequence
381
382        gates
383    }
384
385    /// Check if a matrix is approximately the identity
386    fn is_identity(&self, matrix: &Array2<Complex<f64>>) -> bool {
387        let n = matrix.shape()[0];
388
389        for i in 0..n {
390            for j in 0..n {
391                let expected = if i == j {
392                    Complex::new(1.0, 0.0)
393                } else {
394                    Complex::new(0.0, 0.0)
395                };
396                if (matrix[[i, j]] - expected).norm() > self.tolerance {
397                    return false;
398                }
399            }
400        }
401
402        true
403    }
404}
405
406/// Optimized Shannon decomposition with gate count reduction
407pub struct OptimizedShannonDecomposer {
408    base: ShannonDecomposer,
409    /// Enable peephole optimization
410    peephole: bool,
411    /// Enable commutation-based optimization
412    commutation: bool,
413}
414
415impl OptimizedShannonDecomposer {
416    /// Create a new optimized decomposer
417    pub fn new() -> Self {
418        Self {
419            base: ShannonDecomposer::new(),
420            peephole: true,
421            commutation: true,
422        }
423    }
424
425    /// Decompose with optimization
426    pub fn decompose(
427        &mut self,
428        unitary: &Array2<Complex<f64>>,
429        qubit_ids: &[QubitId],
430    ) -> QuantRS2Result<ShannonDecomposition> {
431        // Get base decomposition
432        let mut decomp = self.base.decompose(unitary, qubit_ids)?;
433
434        if self.peephole {
435            decomp = self.apply_peephole_optimization(decomp)?;
436        }
437
438        if self.commutation {
439            decomp = self.apply_commutation_optimization(decomp)?;
440        }
441
442        Ok(decomp)
443    }
444
445    /// Apply peephole optimization to reduce gate count
446    fn apply_peephole_optimization(
447        &self,
448        mut decomp: ShannonDecomposition,
449    ) -> QuantRS2Result<ShannonDecomposition> {
450        // Look for patterns like:
451        // - Adjacent inverse gates
452        // - Mergeable rotations
453        // - CNOT-CNOT = Identity
454
455        let mut optimized_gates = Vec::new();
456        let mut i = 0;
457
458        while i < decomp.gates.len() {
459            if i + 1 < decomp.gates.len() {
460                // Check for cancellations
461                if self.gates_cancel(&decomp.gates[i], &decomp.gates[i + 1]) {
462                    // Skip both gates
463                    i += 2;
464                    decomp.cnot_count =
465                        decomp
466                            .cnot_count
467                            .saturating_sub(if decomp.gates[i - 2].name() == "CNOT" {
468                                2
469                            } else {
470                                0
471                            });
472                    decomp.single_qubit_count = decomp.single_qubit_count.saturating_sub(
473                        if decomp.gates[i - 2].name() == "CNOT" {
474                            0
475                        } else {
476                            2
477                        },
478                    );
479                    continue;
480                }
481
482                // Check for mergeable rotations
483                if let Some(merged) =
484                    self.try_merge_rotations(&decomp.gates[i], &decomp.gates[i + 1])
485                {
486                    optimized_gates.push(merged);
487                    i += 2;
488                    decomp.single_qubit_count = decomp.single_qubit_count.saturating_sub(1);
489                    continue;
490                }
491            }
492
493            optimized_gates.push(decomp.gates[i].clone());
494            i += 1;
495        }
496
497        decomp.gates = optimized_gates;
498        decomp.depth = decomp.gates.len();
499
500        Ok(decomp)
501    }
502
503    /// Apply commutation-based optimization
504    const fn apply_commutation_optimization(
505        &self,
506        decomp: ShannonDecomposition,
507    ) -> QuantRS2Result<ShannonDecomposition> {
508        // Move commuting gates to reduce circuit depth
509        // This is a simplified version - full implementation would use
510        // a dependency graph and topological sorting
511
512        Ok(decomp)
513    }
514
515    /// Check if two gates cancel each other
516    fn gates_cancel(&self, gate1: &Box<dyn GateOp>, gate2: &Box<dyn GateOp>) -> bool {
517        // Same gate on same qubits
518        if gate1.name() == gate2.name() && gate1.qubits() == gate2.qubits() {
519            match gate1.name() {
520                "X" | "Y" | "Z" | "H" | "CNOT" | "SWAP" => true,
521                _ => false,
522            }
523        } else {
524            false
525        }
526    }
527
528    /// Try to merge two rotation gates
529    fn try_merge_rotations(
530        &self,
531        gate1: &Box<dyn GateOp>,
532        gate2: &Box<dyn GateOp>,
533    ) -> Option<Box<dyn GateOp>> {
534        // Check if both are rotations on the same qubit and axis
535        if gate1.qubits() != gate2.qubits() || gate1.qubits().len() != 1 {
536            return None;
537        }
538
539        let qubit = gate1.qubits()[0];
540
541        match (gate1.name(), gate2.name()) {
542            ("RZ", "RZ") => {
543                let theta1 = gate1.as_any().downcast_ref::<RotationZ>()?.theta;
544                let theta2 = gate2.as_any().downcast_ref::<RotationZ>()?.theta;
545                Some(Box::new(RotationZ {
546                    target: qubit,
547                    theta: theta1 + theta2,
548                }))
549            }
550            ("RX", "RX") => {
551                let theta1 = gate1.as_any().downcast_ref::<RotationX>()?.theta;
552                let theta2 = gate2.as_any().downcast_ref::<RotationX>()?.theta;
553                Some(Box::new(RotationX {
554                    target: qubit,
555                    theta: theta1 + theta2,
556                }))
557            }
558            ("RY", "RY") => {
559                let theta1 = gate1.as_any().downcast_ref::<RotationY>()?.theta;
560                let theta2 = gate2.as_any().downcast_ref::<RotationY>()?.theta;
561                Some(Box::new(RotationY {
562                    target: qubit,
563                    theta: theta1 + theta2,
564                }))
565            }
566            _ => None,
567        }
568    }
569}
570
571/// Utility function for quick Shannon decomposition
572pub fn shannon_decompose(
573    unitary: &Array2<Complex<f64>>,
574    qubit_ids: &[QubitId],
575) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
576    let mut decomposer = ShannonDecomposer::new();
577    let decomp = decomposer.decompose(unitary, qubit_ids)?;
578    Ok(decomp.gates)
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584    use scirs2_core::ndarray::Array2;
585    use scirs2_core::Complex;
586
587    #[test]
588    fn test_shannon_single_qubit() {
589        let mut decomposer = ShannonDecomposer::new();
590
591        // Hadamard matrix
592        let h = Array2::from_shape_vec(
593            (2, 2),
594            vec![
595                Complex::new(1.0, 0.0),
596                Complex::new(1.0, 0.0),
597                Complex::new(1.0, 0.0),
598                Complex::new(-1.0, 0.0),
599            ],
600        )
601        .expect("Failed to create Hadamard matrix")
602            / Complex::new(2.0_f64.sqrt(), 0.0);
603
604        let qubit_ids = vec![QubitId(0)];
605        let decomp = decomposer
606            .decompose(&h, &qubit_ids)
607            .expect("Failed to decompose Hadamard gate");
608
609        // Should decompose into at most 3 single-qubit gates
610        assert!(decomp.single_qubit_count <= 3);
611        assert_eq!(decomp.cnot_count, 0);
612    }
613
614    #[test]
615    fn test_shannon_two_qubit() {
616        let mut decomposer = ShannonDecomposer::new();
617
618        // CNOT matrix
619        let cnot = Array2::from_shape_vec(
620            (4, 4),
621            vec![
622                Complex::new(1.0, 0.0),
623                Complex::new(0.0, 0.0),
624                Complex::new(0.0, 0.0),
625                Complex::new(0.0, 0.0),
626                Complex::new(0.0, 0.0),
627                Complex::new(1.0, 0.0),
628                Complex::new(0.0, 0.0),
629                Complex::new(0.0, 0.0),
630                Complex::new(0.0, 0.0),
631                Complex::new(0.0, 0.0),
632                Complex::new(0.0, 0.0),
633                Complex::new(1.0, 0.0),
634                Complex::new(0.0, 0.0),
635                Complex::new(0.0, 0.0),
636                Complex::new(1.0, 0.0),
637                Complex::new(0.0, 0.0),
638            ],
639        )
640        .expect("Failed to create CNOT matrix");
641
642        let qubit_ids = vec![QubitId(0), QubitId(1)];
643        let decomp = decomposer
644            .decompose(&cnot, &qubit_ids)
645            .expect("Failed to decompose CNOT gate");
646
647        // Should use at most 3 CNOTs for arbitrary two-qubit gate
648        assert!(decomp.cnot_count <= 3);
649    }
650
651    #[test]
652    fn test_optimized_decomposer() {
653        let mut decomposer = OptimizedShannonDecomposer::new();
654
655        // Identity matrix should result in empty circuit
656        let identity = Array2::eye(4);
657        let identity_complex = identity.mapv(|x| Complex::new(x, 0.0));
658
659        let qubit_ids = vec![QubitId(0), QubitId(1)];
660        let decomp = decomposer
661            .decompose(&identity_complex, &qubit_ids)
662            .expect("Failed to decompose identity matrix");
663
664        // Optimizations should eliminate all gates for identity
665        assert_eq!(decomp.gates.len(), 0);
666    }
667
668    #[test]
669    fn test_merge_rz_rotations() {
670        let decomposer = OptimizedShannonDecomposer::new();
671        let qubit = QubitId(0);
672        let g1 = Box::new(RotationZ {
673            target: qubit,
674            theta: 0.3,
675        }) as Box<dyn GateOp>;
676        let g2 = Box::new(RotationZ {
677            target: qubit,
678            theta: 0.4,
679        }) as Box<dyn GateOp>;
680        let merged = decomposer
681            .try_merge_rotations(&g1, &g2)
682            .expect("should merge RZ+RZ");
683        let rz = merged
684            .as_any()
685            .downcast_ref::<RotationZ>()
686            .expect("merged gate must be RotationZ");
687        assert!(
688            (rz.theta - 0.7).abs() < 1e-10,
689            "merged theta should be 0.7, got {}",
690            rz.theta
691        );
692    }
693
694    #[test]
695    fn test_merge_rx_rotations() {
696        let decomposer = OptimizedShannonDecomposer::new();
697        let qubit = QubitId(0);
698        let g1 = Box::new(RotationX {
699            target: qubit,
700            theta: 0.5,
701        }) as Box<dyn GateOp>;
702        let g2 = Box::new(RotationX {
703            target: qubit,
704            theta: 0.3,
705        }) as Box<dyn GateOp>;
706        let merged = decomposer
707            .try_merge_rotations(&g1, &g2)
708            .expect("should merge RX+RX");
709        let rx = merged
710            .as_any()
711            .downcast_ref::<RotationX>()
712            .expect("merged gate must be RotationX");
713        assert!(
714            (rx.theta - 0.8).abs() < 1e-10,
715            "merged theta should be 0.8, got {}",
716            rx.theta
717        );
718    }
719
720    /// Site-4: the block-diagonal branch of `block_diagonalize` is exact — for
721    /// `U = diag(A, D)` it returns `V = W = I` and `U_d = diag(A, D)`, which recomposes
722    /// to the input.
723    #[test]
724    fn test_block_diagonalize_exact_block_diagonal() {
725        let decomposer = ShannonDecomposer::new();
726        // A and D are 2x2 unitaries; off-diagonal blocks are zero.
727        let a = Array2::from_shape_vec(
728            (2, 2),
729            vec![
730                Complex::new(0.0, 0.0),
731                Complex::new(1.0, 0.0),
732                Complex::new(1.0, 0.0),
733                Complex::new(0.0, 0.0),
734            ],
735        )
736        .expect("A");
737        let d = Array2::from_shape_vec(
738            (2, 2),
739            vec![
740                Complex::new(0.0, 0.0),
741                Complex::new(0.0, -1.0),
742                Complex::new(0.0, 1.0),
743                Complex::new(0.0, 0.0),
744            ],
745        )
746        .expect("D");
747        let zero = Array2::<Complex<f64>>::zeros((2, 2));
748
749        let (v, w, u_diag) = decomposer
750            .block_diagonalize(&a, &zero, &zero, &d)
751            .expect("block-diagonal case must succeed");
752
753        // V and W are identity.
754        let id = Array2::<Complex<f64>>::eye(2);
755        let v_err = v
756            .iter()
757            .zip(id.iter())
758            .map(|(x, y)| (x - y).norm_sqr())
759            .sum::<f64>()
760            .sqrt();
761        let w_err = w
762            .iter()
763            .zip(id.iter())
764            .map(|(x, y)| (x - y).norm_sqr())
765            .sum::<f64>()
766            .sqrt();
767        assert!(v_err < 1e-12 && w_err < 1e-12, "V, W should be identity");
768
769        // u_diag == diag(A, D), i.e. (I⊗V)·u_diag·(I⊗W) == original block matrix.
770        let original = decomposer.combine_blocks(&a, &zero, &zero, &d);
771        let err = u_diag
772            .iter()
773            .zip(original.iter())
774            .map(|(x, y)| (x - y).norm_sqr())
775            .sum::<f64>()
776            .sqrt();
777        assert!(err < 1e-12, "u_diag must equal diag(A, D), err {err}");
778    }
779
780    /// Site-4: the general (block-coupled) case returns an honest error instead of the
781    /// previous fabricated identity-factor / full-matrix result.
782    #[test]
783    fn test_block_diagonalize_general_honest_error() {
784        let decomposer = ShannonDecomposer::new();
785        let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
786        // A genuinely block-coupled unitary (a 4x4 rotation mixing the blocks): use the
787        // Hadamard-like structure so B and C are non-zero.
788        let a = Array2::from_shape_vec(
789            (2, 2),
790            vec![
791                Complex::new(inv_sqrt2, 0.0),
792                Complex::new(0.0, 0.0),
793                Complex::new(0.0, 0.0),
794                Complex::new(inv_sqrt2, 0.0),
795            ],
796        )
797        .expect("A");
798        let b = a.clone();
799        let c = a.clone();
800        let d = a.mapv(|z| -z);
801
802        let result = decomposer.block_diagonalize(&a, &b, &c, &d);
803        assert!(matches!(
804            result,
805            Err(QuantRS2Error::UnsupportedOperation(_))
806        ));
807    }
808
809    #[test]
810    fn test_no_merge_different_axes() {
811        let decomposer = OptimizedShannonDecomposer::new();
812        let qubit = QubitId(0);
813        let g1 = Box::new(RotationZ {
814            target: qubit,
815            theta: 0.3,
816        }) as Box<dyn GateOp>;
817        let g2 = Box::new(RotationX {
818            target: qubit,
819            theta: 0.4,
820        }) as Box<dyn GateOp>;
821        assert!(
822            decomposer.try_merge_rotations(&g1, &g2).is_none(),
823            "RZ and RX should not merge"
824        );
825    }
826}