Skip to main content

quantrs2_sim/
fusion.rs

1//! Gate fusion optimization for quantum circuit simulation.
2//!
3//! This module implements gate fusion techniques to optimize quantum circuits
4//! by combining consecutive gates that act on the same qubits into single
5//! multi-qubit gates, reducing the number of matrix multiplications needed.
6
7use scirs2_core::ndarray::Array2;
8use scirs2_core::Complex64;
9use std::collections::{HashMap, HashSet};
10
11use crate::error::{Result, SimulatorError};
12use crate::sparse::{CSRMatrix, SparseMatrixBuilder};
13use quantrs2_core::gate::GateOp;
14use quantrs2_core::qubit::QubitId;
15
16// SciRS2 stub types (would be replaced with actual SciRS2 imports)
17#[derive(Debug)]
18struct SciRS2MatrixMultiplier;
19
20impl SciRS2MatrixMultiplier {
21    fn multiply_sparse(a: &CSRMatrix, b: &CSRMatrix) -> Result<CSRMatrix> {
22        // Stub implementation for SciRS2 sparse matrix multiplication
23        if a.num_cols != b.num_rows {
24            return Err(SimulatorError::DimensionMismatch(format!(
25                "Cannot multiply {}x{} with {}x{}",
26                a.num_rows, a.num_cols, b.num_rows, b.num_cols
27            )));
28        }
29
30        let mut builder = SparseMatrixBuilder::new(a.num_rows, b.num_cols);
31
32        // Simple sparse matrix multiplication
33        for i in 0..a.num_rows {
34            for k in a.row_ptr[i]..a.row_ptr[i + 1] {
35                let a_val = a.values[k];
36                let a_col = a.col_indices[k];
37
38                for j_idx in b.row_ptr[a_col]..b.row_ptr[a_col + 1] {
39                    let b_val = b.values[j_idx];
40                    let b_col = b.col_indices[j_idx];
41
42                    builder.add(i, b_col, a_val * b_val);
43                }
44            }
45        }
46
47        Ok(builder.build())
48    }
49
50    #[must_use]
51    fn multiply_dense(a: &Array2<Complex64>, b: &Array2<Complex64>) -> Result<Array2<Complex64>> {
52        // Stub implementation for SciRS2 dense matrix multiplication
53        if a.ncols() != b.nrows() {
54            return Err(SimulatorError::DimensionMismatch(format!(
55                "Cannot multiply {}x{} with {}x{}",
56                a.nrows(),
57                a.ncols(),
58                b.nrows(),
59                b.ncols()
60            )));
61        }
62
63        Ok(a.dot(b))
64    }
65}
66
67/// Gate fusion strategy
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum FusionStrategy {
70    /// Fuse all consecutive gates on same qubits
71    Aggressive,
72    /// Only fuse if it reduces gate count
73    Conservative,
74    /// Fuse based on gate depth reduction
75    DepthOptimized,
76    /// Custom fusion with cost function
77    Custom,
78}
79
80/// Fusable gate group
81#[derive(Debug, Clone)]
82pub struct GateGroup {
83    /// Indices of gates in this group
84    pub gate_indices: Vec<usize>,
85    /// Qubits this group acts on
86    pub qubits: Vec<QubitId>,
87    /// Whether this group can be fused
88    pub fusable: bool,
89    /// Estimated cost of fusion
90    pub fusion_cost: f64,
91}
92
93/// Gate fusion optimizer
94pub struct GateFusion {
95    /// Fusion strategy
96    strategy: FusionStrategy,
97    /// Maximum qubits to fuse (to limit matrix size)
98    max_fusion_qubits: usize,
99    /// Minimum gates to consider fusion
100    min_fusion_gates: usize,
101    /// Cost threshold for fusion
102    cost_threshold: f64,
103}
104
105impl GateFusion {
106    /// Create a new gate fusion optimizer
107    #[must_use]
108    pub const fn new(strategy: FusionStrategy) -> Self {
109        Self {
110            strategy,
111            max_fusion_qubits: 4,
112            min_fusion_gates: 2,
113            cost_threshold: 0.8,
114        }
115    }
116
117    /// Configure fusion parameters
118    #[must_use]
119    pub const fn with_params(
120        mut self,
121        max_qubits: usize,
122        min_gates: usize,
123        threshold: f64,
124    ) -> Self {
125        self.max_fusion_qubits = max_qubits;
126        self.min_fusion_gates = min_gates;
127        self.cost_threshold = threshold;
128        self
129    }
130
131    /// Analyze circuit for fusion opportunities
132    pub fn analyze_circuit(&self, gates: &[Box<dyn GateOp>]) -> Result<Vec<GateGroup>> {
133        let mut groups = Vec::new();
134        let mut processed = vec![false; gates.len()];
135
136        for i in 0..gates.len() {
137            if processed[i] {
138                continue;
139            }
140
141            // Start a new group
142            let mut group = GateGroup {
143                gate_indices: vec![i],
144                qubits: gates[i].qubits().clone(),
145                fusable: false,
146                fusion_cost: 0.0,
147            };
148
149            // Find consecutive gates that can be fused
150            for j in i + 1..gates.len() {
151                if processed[j] {
152                    continue;
153                }
154
155                // Check if gate j can be added to the group
156                if self.can_fuse_with_group(&group, gates[j].as_ref()) {
157                    group.gate_indices.push(j);
158
159                    // Update qubit set
160                    for qubit in gates[j].qubits() {
161                        if !group.qubits.contains(&qubit) {
162                            group.qubits.push(qubit);
163                        }
164                    }
165
166                    // Check if we've reached the limit
167                    if group.qubits.len() > self.max_fusion_qubits {
168                        group.gate_indices.pop();
169                        break;
170                    }
171                } else if self.blocks_fusion(&group, gates[j].as_ref()) {
172                    // This gate blocks further fusion
173                    break;
174                }
175            }
176
177            // Evaluate if this group should be fused
178            if group.gate_indices.len() >= self.min_fusion_gates {
179                group.fusion_cost = self.compute_fusion_cost(&group, gates)?;
180                group.fusable = self.should_fuse(&group);
181
182                // Mark gates as processed if we're fusing
183                if group.fusable {
184                    for &idx in &group.gate_indices {
185                        processed[idx] = true;
186                    }
187                }
188            }
189
190            groups.push(group);
191        }
192
193        Ok(groups)
194    }
195
196    /// Check if a gate can be fused with a group
197    fn can_fuse_with_group(&self, group: &GateGroup, gate: &dyn GateOp) -> bool {
198        // Gate must share at least one qubit with the group
199        let gate_qubits: HashSet<_> = gate.qubits().iter().copied().collect();
200        let group_qubits: HashSet<_> = group.qubits.iter().copied().collect();
201
202        match self.strategy {
203            FusionStrategy::Aggressive => {
204                // Fuse if there's any qubit overlap
205                !gate_qubits.is_disjoint(&group_qubits)
206            }
207            FusionStrategy::Conservative => {
208                // Only fuse if all qubits are in the group
209                gate_qubits.is_subset(&group_qubits) || group_qubits.is_subset(&gate_qubits)
210            }
211            FusionStrategy::DepthOptimized => {
212                // Fuse if it doesn't increase qubit count too much
213                let combined_qubits: HashSet<_> =
214                    gate_qubits.union(&group_qubits).copied().collect();
215                combined_qubits.len() <= self.max_fusion_qubits
216            }
217            FusionStrategy::Custom => {
218                // Custom logic (simplified here)
219                !gate_qubits.is_disjoint(&group_qubits)
220            }
221        }
222    }
223
224    /// Check if a gate blocks fusion
225    fn blocks_fusion(&self, group: &GateGroup, gate: &dyn GateOp) -> bool {
226        // A gate blocks fusion if it acts on some but not all qubits of the group
227        let gate_qubits: HashSet<_> = gate.qubits().iter().copied().collect();
228        let group_qubits: HashSet<_> = group.qubits.iter().copied().collect();
229
230        let intersection = gate_qubits.intersection(&group_qubits).count();
231        intersection > 0 && intersection < group_qubits.len()
232    }
233
234    /// Compute the cost of fusing a group
235    fn compute_fusion_cost(&self, group: &GateGroup, gates: &[Box<dyn GateOp>]) -> Result<f64> {
236        let num_qubits = group.qubits.len();
237        let num_gates = group.gate_indices.len();
238
239        // Cost factors:
240        // 1. Matrix size (2^n x 2^n for n qubits)
241        let matrix_size_cost = f64::from(1 << num_qubits);
242
243        // 2. Number of operations saved
244        let ops_saved = (num_gates - 1) as f64;
245
246        // 3. Memory requirements
247        let memory_cost = matrix_size_cost * matrix_size_cost * 16.0; // Complex64 size
248
249        // Combined cost (lower is better)
250        let cost = matrix_size_cost / (ops_saved + 1.0) + memory_cost / 1e9;
251
252        Ok(cost)
253    }
254
255    /// Decide if a group should be fused
256    fn should_fuse(&self, group: &GateGroup) -> bool {
257        match self.strategy {
258            FusionStrategy::Aggressive => true,
259            FusionStrategy::Conservative => group.fusion_cost < self.cost_threshold,
260            FusionStrategy::DepthOptimized => group.gate_indices.len() > 2,
261            FusionStrategy::Custom => group.fusion_cost < self.cost_threshold,
262        }
263    }
264
265    /// Fuse a group of gates into a single gate
266    pub fn fuse_group(
267        &self,
268        group: &GateGroup,
269        gates: &[Box<dyn GateOp>],
270        num_qubits: usize,
271    ) -> Result<FusedGate> {
272        let group_qubits = &group.qubits;
273        let group_size = group_qubits.len();
274
275        // Create identity matrix for the fused gate
276        let dim = 1 << group_size;
277        let mut fused_matrix = Array2::eye(dim);
278
279        // Apply each gate in sequence
280        for &gate_idx in &group.gate_indices {
281            let gate = &gates[gate_idx];
282            let gate_matrix = self.get_gate_matrix(gate.as_ref())?;
283
284            // Map gate qubits to group qubits
285            let gate_qubits = gate.qubits();
286            let qubit_map: HashMap<QubitId, usize> = group_qubits
287                .iter()
288                .enumerate()
289                .map(|(i, &q)| (q, i))
290                .collect();
291
292            // Expand gate matrix to group dimension
293            let expanded =
294                self.expand_gate_matrix(&gate_matrix, &gate_qubits, &qubit_map, group_size)?;
295
296            // Multiply using SciRS2
297            fused_matrix = SciRS2MatrixMultiplier::multiply_dense(&expanded, &fused_matrix)?;
298        }
299
300        Ok(FusedGate {
301            matrix: fused_matrix,
302            qubits: group_qubits.clone(),
303            original_gates: group.gate_indices.clone(),
304        })
305    }
306
307    /// Get matrix representation of a gate
308    ///
309    /// Uses the gate's own real [`GateOp::matrix`] implementation (already
310    /// implemented for every gate type in `quantrs2_core::gate`), so
311    /// rotation gates (RX/RY/RZ), phase gates (S/T), SWAP, Toffoli, and any
312    /// custom/parameterized gate all fuse correctly -- there is no
313    /// gate-name special case and no identity fallback for "unknown"
314    /// gates.
315    fn get_gate_matrix(&self, gate: &dyn GateOp) -> Result<Array2<Complex64>> {
316        let n = gate.qubits().len();
317        let dim = 1 << n;
318        let flat = gate.matrix().map_err(|e| {
319            SimulatorError::InvalidInput(format!(
320                "failed to get matrix for gate '{}': {e}",
321                gate.name()
322            ))
323        })?;
324        Array2::from_shape_vec((dim, dim), flat).map_err(|e| {
325            SimulatorError::InvalidInput(format!(
326                "gate '{}' returned a matrix with the wrong shape for {n} qubit(s): {e}",
327                gate.name()
328            ))
329        })
330    }
331
332    /// Expand a gate matrix to act on a larger qubit space
333    fn expand_gate_matrix(
334        &self,
335        gate_matrix: &Array2<Complex64>,
336        gate_qubits: &[QubitId],
337        qubit_map: &HashMap<QubitId, usize>,
338        total_qubits: usize,
339    ) -> Result<Array2<Complex64>> {
340        let dim = 1 << total_qubits;
341        let mut expanded = Array2::zeros((dim, dim));
342
343        // Map gate qubits to their positions in the expanded space
344        let gate_positions: Vec<usize> = gate_qubits
345            .iter()
346            .map(|q| qubit_map.get(q).copied().unwrap_or(0))
347            .collect();
348
349        // Fill the expanded matrix
350        for i in 0..dim {
351            for j in 0..dim {
352                // Extract the relevant bits for the gate qubits
353                let mut gate_i = 0;
354                let mut gate_j = 0;
355                let mut other_bits_match = true;
356
357                for (k, &pos) in gate_positions.iter().enumerate() {
358                    if (i >> pos) & 1 == 1 {
359                        gate_i |= 1 << k;
360                    }
361                    if (j >> pos) & 1 == 1 {
362                        gate_j |= 1 << k;
363                    }
364                }
365
366                // Check that non-gate qubits match
367                for k in 0..total_qubits {
368                    if !gate_positions.contains(&k) && ((i >> k) & 1) != ((j >> k) & 1) {
369                        other_bits_match = false;
370                        break;
371                    }
372                }
373
374                if other_bits_match {
375                    expanded[[i, j]] = gate_matrix[[gate_i, gate_j]];
376                }
377            }
378        }
379
380        Ok(expanded)
381    }
382
383    /// Apply fusion to a circuit
384    pub fn optimize_circuit(
385        &self,
386        gates: Vec<Box<dyn GateOp>>,
387        num_qubits: usize,
388    ) -> Result<OptimizedCircuit> {
389        let groups = self.analyze_circuit(&gates)?;
390        let mut optimized_gates = Vec::new();
391        let mut fusion_map = HashMap::new();
392
393        let mut processed = vec![false; gates.len()];
394
395        for group in &groups {
396            if group.fusable && group.gate_indices.len() > 1 {
397                // Fuse this group
398                let fused = self.fuse_group(group, &gates, num_qubits)?;
399                let fused_idx = optimized_gates.len();
400                optimized_gates.push(OptimizedGate::Fused(fused));
401
402                // Record fusion mapping
403                for &gate_idx in &group.gate_indices {
404                    fusion_map.insert(gate_idx, fused_idx);
405                    processed[gate_idx] = true;
406                }
407            } else {
408                // Keep gates unfused
409                for &gate_idx in &group.gate_indices {
410                    if !processed[gate_idx] {
411                        optimized_gates.push(OptimizedGate::Original(gate_idx));
412                        processed[gate_idx] = true;
413                    }
414                }
415            }
416        }
417
418        // Add any remaining unfused gates
419        for (i, &p) in processed.iter().enumerate() {
420            if !p {
421                optimized_gates.push(OptimizedGate::Original(i));
422            }
423        }
424
425        Ok(OptimizedCircuit {
426            gates: optimized_gates,
427            original_gates: gates,
428            fusion_map,
429            stats: self.compute_stats(&groups),
430        })
431    }
432
433    /// Compute fusion statistics
434    fn compute_stats(&self, groups: &[GateGroup]) -> FusionStats {
435        let total_groups = groups.len();
436        let fused_groups = groups.iter().filter(|g| g.fusable).count();
437        let total_gates: usize = groups.iter().map(|g| g.gate_indices.len()).sum();
438        let fused_gates: usize = groups
439            .iter()
440            .filter(|g| g.fusable)
441            .map(|g| g.gate_indices.len())
442            .sum();
443
444        FusionStats {
445            total_gates,
446            fused_gates,
447            fusion_ratio: fused_gates as f64 / total_gates.max(1) as f64,
448            groups_analyzed: total_groups,
449            groups_fused: fused_groups,
450        }
451    }
452}
453
454/// A fused gate combining multiple gates
455#[derive(Debug)]
456pub struct FusedGate {
457    /// Combined matrix representation
458    pub matrix: Array2<Complex64>,
459    /// Qubits this gate acts on
460    pub qubits: Vec<QubitId>,
461    /// Original gate indices that were fused
462    pub original_gates: Vec<usize>,
463}
464
465impl FusedGate {
466    /// Convert to sparse representation
467    pub fn to_sparse(&self) -> Result<CSRMatrix> {
468        let mut builder = SparseMatrixBuilder::new(self.matrix.nrows(), self.matrix.ncols());
469
470        for ((i, j), &val) in self.matrix.indexed_iter() {
471            if val.norm() > 1e-12 {
472                builder.set_value(i, j, val);
473            }
474        }
475
476        Ok(builder.build())
477    }
478
479    /// Get the dimension of the gate
480    #[must_use]
481    pub fn dimension(&self) -> usize {
482        self.matrix.nrows()
483    }
484}
485
486/// Optimized gate representation
487#[derive(Debug)]
488pub enum OptimizedGate {
489    /// Original unfused gate (index into original gates)
490    Original(usize),
491    /// Fused gate combining multiple gates
492    Fused(FusedGate),
493}
494
495/// Optimized circuit after fusion
496#[derive(Debug)]
497pub struct OptimizedCircuit {
498    /// Optimized gate sequence
499    pub gates: Vec<OptimizedGate>,
500    /// Original gates (for reference)
501    pub original_gates: Vec<Box<dyn GateOp>>,
502    /// Mapping from original gate index to fused gate index
503    pub fusion_map: HashMap<usize, usize>,
504    /// Fusion statistics
505    pub stats: FusionStats,
506}
507
508impl OptimizedCircuit {
509    /// Get the effective gate count after fusion
510    #[must_use]
511    pub fn gate_count(&self) -> usize {
512        self.gates.len()
513    }
514
515    /// Get memory usage estimate
516    #[must_use]
517    pub fn memory_usage(&self) -> usize {
518        self.gates
519            .iter()
520            .map(|g| match g {
521                OptimizedGate::Original(_) => 64, // Approximate
522                OptimizedGate::Fused(f) => f.dimension() * f.dimension() * 16,
523            })
524            .sum()
525    }
526}
527
528/// Fusion statistics
529#[derive(Debug)]
530pub struct FusionStats {
531    /// Total number of gates before fusion
532    pub total_gates: usize,
533    /// Number of gates that were fused
534    pub fused_gates: usize,
535    /// Ratio of fused gates
536    pub fusion_ratio: f64,
537    /// Number of groups analyzed
538    pub groups_analyzed: usize,
539    /// Number of groups that were fused
540    pub groups_fused: usize,
541}
542
543/// Benchmark different fusion strategies
544pub fn benchmark_fusion_strategies(gates: Vec<Box<dyn GateOp>>, num_qubits: usize) -> Result<()> {
545    println!("\nGate Fusion Benchmark");
546    println!("Original circuit: {} gates", gates.len());
547    println!("{:-<60}", "");
548
549    for strategy in [
550        FusionStrategy::Conservative,
551        FusionStrategy::Aggressive,
552        FusionStrategy::DepthOptimized,
553    ] {
554        let fusion = GateFusion::new(strategy);
555        let start = std::time::Instant::now();
556
557        let optimized = fusion.optimize_circuit(gates.clone(), num_qubits)?;
558        let elapsed = start.elapsed();
559
560        println!("\n{strategy:?} Strategy:");
561        println!("  Gates after fusion: {}", optimized.gate_count());
562        println!(
563            "  Fusion ratio: {:.2}%",
564            optimized.stats.fusion_ratio * 100.0
565        );
566        println!(
567            "  Groups fused: {}/{}",
568            optimized.stats.groups_fused, optimized.stats.groups_analyzed
569        );
570        println!(
571            "  Memory usage: {:.2} MB",
572            optimized.memory_usage() as f64 / 1e6
573        );
574        println!("  Optimization time: {elapsed:?}");
575    }
576
577    Ok(())
578}
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583    use quantrs2_core::gate::multi::CNOT;
584    use quantrs2_core::gate::single::{Hadamard, PauliX, RotationX};
585
586    #[test]
587    fn test_gate_group_creation() {
588        let group = GateGroup {
589            gate_indices: vec![0, 1, 2],
590            qubits: vec![QubitId::new(0), QubitId::new(1)],
591            fusable: true,
592            fusion_cost: 0.5,
593        };
594
595        assert_eq!(group.gate_indices.len(), 3);
596        assert_eq!(group.qubits.len(), 2);
597    }
598
599    #[test]
600    fn test_fusion_strategy() {
601        let fusion = GateFusion::new(FusionStrategy::Conservative);
602        assert_eq!(fusion.max_fusion_qubits, 4);
603        assert_eq!(fusion.min_fusion_gates, 2);
604    }
605
606    #[test]
607    fn test_sparse_matrix_multiplication() {
608        let mut builder1 = SparseMatrixBuilder::new(2, 2);
609        builder1.set_value(0, 0, Complex64::new(1.0, 0.0));
610        builder1.set_value(1, 1, Complex64::new(1.0, 0.0));
611        let m1 = builder1.build();
612
613        let mut builder2 = SparseMatrixBuilder::new(2, 2);
614        builder2.set_value(0, 1, Complex64::new(1.0, 0.0));
615        builder2.set_value(1, 0, Complex64::new(1.0, 0.0));
616        let m2 = builder2.build();
617
618        let result = SciRS2MatrixMultiplier::multiply_sparse(&m1, &m2)
619            .expect("sparse matrix multiplication should succeed");
620        assert_eq!(result.num_rows, 2);
621        assert_eq!(result.num_cols, 2);
622    }
623
624    #[test]
625    fn test_fused_gate() {
626        let matrix = Array2::eye(4);
627        let fused = FusedGate {
628            matrix,
629            qubits: vec![QubitId::new(0), QubitId::new(1)],
630            original_gates: vec![0, 1],
631        };
632
633        assert_eq!(fused.dimension(), 4);
634        let sparse = fused
635            .to_sparse()
636            .expect("conversion to sparse should succeed");
637        assert_eq!(sparse.num_rows, 4);
638    }
639
640    #[test]
641    fn test_fusion_cost() {
642        let fusion = GateFusion::new(FusionStrategy::Conservative);
643        let group = GateGroup {
644            gate_indices: vec![0, 1],
645            qubits: vec![QubitId::new(0), QubitId::new(1)],
646            fusable: false,
647            fusion_cost: 0.0,
648        };
649
650        let gates: Vec<Box<dyn GateOp>> = vec![
651            Box::new(Hadamard {
652                target: QubitId::new(0),
653            }),
654            Box::new(CNOT {
655                control: QubitId::new(0),
656                target: QubitId::new(1),
657            }),
658        ];
659
660        let cost = fusion
661            .compute_fusion_cost(&group, &gates)
662            .expect("fusion cost computation should succeed");
663        assert!(cost > 0.0);
664    }
665
666    /// Regression test for the P1 finding: `get_gate_matrix` used to
667    /// silently default any gate that wasn't literally named "Hadamard",
668    /// "PauliX", or "CNOT" to an identity matrix -- so fusing a rotation
669    /// gate would drop its effect entirely. RX(pi) is NOT the identity;
670    /// fusing a single-gate group containing only it must produce the
671    /// real RX(pi) matrix.
672    #[test]
673    fn test_fuse_group_uses_real_rotation_gate_matrix() {
674        let fusion = GateFusion::new(FusionStrategy::Aggressive);
675        let theta = std::f64::consts::PI;
676        let rx_gate = RotationX {
677            target: QubitId::new(0),
678            theta,
679        };
680        let expected_matrix = rx_gate
681            .matrix()
682            .expect("RX matrix computation should succeed");
683
684        let group = GateGroup {
685            gate_indices: vec![0],
686            qubits: vec![QubitId::new(0)],
687            fusable: true,
688            fusion_cost: 0.0,
689        };
690        let gates: Vec<Box<dyn GateOp>> = vec![Box::new(rx_gate)];
691
692        let fused = fusion
693            .fuse_group(&group, &gates, 1)
694            .expect("fusing a single rotation gate should succeed");
695
696        // Before the fix, this would have been the 2x2 identity (from the
697        // hardcoded gate-name fallback) instead of the real RX(pi) matrix.
698        assert!(
699            (fused.matrix[[0, 0]] - expected_matrix[0]).norm() < 1e-10,
700            "fused matrix[0,0] should match RX(pi)[0,0], got {:?} vs {:?}",
701            fused.matrix[[0, 0]],
702            expected_matrix[0]
703        );
704        assert!(
705            (fused.matrix[[0, 1]] - expected_matrix[1]).norm() < 1e-10,
706            "fused matrix[0,1] should match RX(pi)[0,1], got {:?} vs {:?}",
707            fused.matrix[[0, 1]],
708            expected_matrix[1]
709        );
710        // RX(pi) is not the identity: off-diagonal terms must be non-zero.
711        assert!(
712            fused.matrix[[0, 1]].norm() > 0.9,
713            "RX(pi) off-diagonal should be near -i, got {:?}",
714            fused.matrix[[0, 1]]
715        );
716    }
717}