Skip to main content

quantrs2_circuit/optimization/passes/
basic_passes.rs

1//! Basic optimization passes: gate cancellation, commutation, merging, and rotation merging.
2
3use crate::optimization::cost_model::CostModel;
4use crate::optimization::gate_properties::CommutationTable;
5use quantrs2_core::error::QuantRS2Result;
6use quantrs2_core::gate::{
7    multi,
8    single::{self, RotationX, RotationY, RotationZ},
9    GateOp,
10};
11use quantrs2_core::qubit::QubitId;
12use scirs2_core::Complex64;
13use std::collections::HashSet;
14use std::f64::consts::PI;
15
16use super::OptimizationPass;
17
18/// Gate cancellation pass - removes redundant gates
19pub struct GateCancellation {
20    aggressive: bool,
21}
22
23impl GateCancellation {
24    #[must_use]
25    pub const fn new(aggressive: bool) -> Self {
26        Self { aggressive }
27    }
28
29    /// Multiply two 2x2 matrices stored row-major as 4-element slices.
30    fn mul_2x2(a: &[Complex64; 4], b: &[Complex64; 4]) -> [Complex64; 4] {
31        [
32            a[0] * b[0] + a[1] * b[2],
33            a[0] * b[1] + a[1] * b[3],
34            a[2] * b[0] + a[3] * b[2],
35            a[2] * b[1] + a[3] * b[3],
36        ]
37    }
38
39    /// Extract a single-qubit gate's matrix as a fixed 2x2 array.
40    ///
41    /// Returns `None` if the gate cannot produce a 2x2 matrix (e.g. its matrix
42    /// computation fails or it is not a single-qubit gate), so callers fall back
43    /// to keeping the gates untouched.
44    fn single_qubit_matrix(gate: &dyn GateOp) -> Option<[Complex64; 4]> {
45        let data = gate.matrix().ok()?;
46        if data.len() != 4 {
47            return None;
48        }
49        Some([data[0], data[1], data[2], data[3]])
50    }
51
52    /// Check whether four single-qubit gates, applied in order
53    /// `g1` then `g2` then `g3` then `g4`, compose to the identity up to an
54    /// unobservable global phase.
55    ///
56    /// The combined unitary is `U = M4 · M3 · M2 · M1` (gates apply left-to-right
57    /// in circuit order, so later gates left-multiply). `U` equals identity up to
58    /// global phase iff it is diagonal with both diagonal entries equal and of
59    /// unit modulus. Removing such a block is always physically correct.
60    fn four_gate_block_is_identity(
61        g1: &dyn GateOp,
62        g2: &dyn GateOp,
63        g3: &dyn GateOp,
64        g4: &dyn GateOp,
65    ) -> bool {
66        let (Some(m1), Some(m2), Some(m3), Some(m4)) = (
67            Self::single_qubit_matrix(g1),
68            Self::single_qubit_matrix(g2),
69            Self::single_qubit_matrix(g3),
70            Self::single_qubit_matrix(g4),
71        ) else {
72            return false;
73        };
74
75        // Compose in application order: U = M4 * (M3 * (M2 * M1)).
76        let u = Self::mul_2x2(&m2, &m1);
77        let u = Self::mul_2x2(&m3, &u);
78        let u = Self::mul_2x2(&m4, &u);
79
80        const TOL: f64 = 1e-10;
81
82        // Off-diagonals must vanish.
83        if u[1].norm() > TOL || u[2].norm() > TOL {
84            return false;
85        }
86        // Diagonal entries must be equal (a single global phase) ...
87        if (u[0] - u[3]).norm() > TOL {
88            return false;
89        }
90        // ... and that phase must have unit modulus (genuinely a phase).
91        (u[0].norm() - 1.0).abs() <= TOL
92    }
93}
94
95impl OptimizationPass for GateCancellation {
96    fn name(&self) -> &'static str {
97        "Gate Cancellation"
98    }
99
100    fn apply_to_gates(
101        &self,
102        gates: Vec<Box<dyn GateOp>>,
103        _cost_model: &dyn CostModel,
104    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
105        let mut optimized = Vec::new();
106        let mut i = 0;
107
108        while i < gates.len() {
109            if i + 1 < gates.len() {
110                let gate1 = &gates[i];
111                let gate2 = &gates[i + 1];
112
113                // Check if gates act on the same qubits
114                if gate1.qubits() == gate2.qubits() && gate1.name() == gate2.name() {
115                    // Check for self-inverse gates (H, X, Y, Z)
116                    match gate1.name() {
117                        "H" | "X" | "Y" | "Z" => {
118                            // These gates cancel when applied twice - skip both
119                            i += 2;
120                            continue;
121                        }
122                        "RX" | "RY" | "RZ" => {
123                            // Check if rotations cancel
124                            if let (Some(rx1), Some(rx2)) = (
125                                gate1.as_any().downcast_ref::<single::RotationX>(),
126                                gate2.as_any().downcast_ref::<single::RotationX>(),
127                            ) {
128                                let combined_angle = rx1.theta + rx2.theta;
129                                // Check if the combined rotation is effectively zero
130                                if (combined_angle % (2.0 * PI)).abs() < 1e-10 {
131                                    i += 2;
132                                    continue;
133                                }
134                            } else if let (Some(ry1), Some(ry2)) = (
135                                gate1.as_any().downcast_ref::<single::RotationY>(),
136                                gate2.as_any().downcast_ref::<single::RotationY>(),
137                            ) {
138                                let combined_angle = ry1.theta + ry2.theta;
139                                if (combined_angle % (2.0 * PI)).abs() < 1e-10 {
140                                    i += 2;
141                                    continue;
142                                }
143                            } else if let (Some(rz1), Some(rz2)) = (
144                                gate1.as_any().downcast_ref::<single::RotationZ>(),
145                                gate2.as_any().downcast_ref::<single::RotationZ>(),
146                            ) {
147                                let combined_angle = rz1.theta + rz2.theta;
148                                if (combined_angle % (2.0 * PI)).abs() < 1e-10 {
149                                    i += 2;
150                                    continue;
151                                }
152                            }
153                        }
154                        "CNOT" => {
155                            // CNOT is self-inverse
156                            if let (Some(cnot1), Some(cnot2)) = (
157                                gate1.as_any().downcast_ref::<multi::CNOT>(),
158                                gate2.as_any().downcast_ref::<multi::CNOT>(),
159                            ) {
160                                if cnot1.control == cnot2.control && cnot1.target == cnot2.target {
161                                    i += 2;
162                                    continue;
163                                }
164                            }
165                        }
166                        _ => {}
167                    }
168                }
169
170                // Aggressive mode: cancel any block of four consecutive
171                // single-qubit gates on the *same* qubit whose combined unitary
172                // is the identity (up to an unobservable global phase). Unlike a
173                // name-pattern heuristic, this is verified by actually
174                // multiplying the gate matrices, so it can never remove gates
175                // that do not truly cancel.
176                if self.aggressive && i + 3 < gates.len() {
177                    let gate3 = &gates[i + 2];
178                    let gate4 = &gates[i + 3];
179
180                    let same_single_qubit = gate1.qubits() == gate2.qubits()
181                        && gate2.qubits() == gate3.qubits()
182                        && gate3.qubits() == gate4.qubits()
183                        && gate1.qubits().len() == 1;
184
185                    if same_single_qubit
186                        && Self::four_gate_block_is_identity(
187                            gate1.as_ref(),
188                            gate2.as_ref(),
189                            gate3.as_ref(),
190                            gate4.as_ref(),
191                        )
192                    {
193                        // The four gates compose to identity: drop all of them.
194                        i += 4;
195                        continue;
196                    }
197                }
198            }
199
200            // If we didn't skip, add the gate to optimized list
201            optimized.push(gates[i].clone());
202            i += 1;
203        }
204
205        Ok(optimized)
206    }
207}
208
209/// Gate commutation pass - reorders gates to enable other optimizations
210pub struct GateCommutation {
211    max_lookahead: usize,
212    commutation_table: CommutationTable,
213}
214
215impl GateCommutation {
216    #[must_use]
217    pub fn new(max_lookahead: usize) -> Self {
218        Self {
219            max_lookahead,
220            commutation_table: CommutationTable::new(),
221        }
222    }
223}
224
225impl GateCommutation {
226    /// Check if two gates commute based on commutation rules
227    fn gates_commute(&self, gate1: &dyn GateOp, gate2: &dyn GateOp) -> bool {
228        // Use commutation table if available
229        if self.commutation_table.commutes(gate1.name(), gate2.name()) {
230            return true;
231        }
232
233        // Additional commutation rules
234        match (gate1.name(), gate2.name()) {
235            // Pauli gates commutation
236            ("X", "X") | ("Y", "Y") | ("Z", "Z") => true,
237            ("I", _) | (_, "I") => true,
238
239            // Phase/T gates commute with Z
240            ("S" | "T", "Z") | ("Z", "S" | "T") => true,
241
242            // Same-axis rotations commute
243            ("RX", "RX") | ("RY", "RY") | ("RZ", "RZ") => true,
244
245            // RZ commutes with Z-like gates
246            ("RZ", "Z" | "S" | "T") | ("Z" | "S" | "T", "RZ") => true,
247
248            _ => false,
249        }
250    }
251
252    /// Check if swapping gates at position i would enable optimizations
253    fn would_benefit_from_swap(&self, gates: &[Box<dyn GateOp>], i: usize) -> bool {
254        if i + 2 >= gates.len() {
255            return false;
256        }
257
258        let gate1 = &gates[i];
259        let gate2 = &gates[i + 1];
260        let gate3 = &gates[i + 2];
261
262        // Check if swapping would create cancellation opportunities
263        if gate1.name() == gate3.name() && gate1.qubits() == gate3.qubits() {
264            // After swap, gate2 and gate3 (originally gate1) would be adjacent
265            match gate3.name() {
266                "H" | "X" | "Y" | "Z" => return true,
267                _ => {}
268            }
269        }
270
271        // Check if swapping would enable rotation merging
272        if gate2.name() == gate3.name() && gate2.qubits() == gate3.qubits() {
273            match gate2.name() {
274                "RX" | "RY" | "RZ" => return true,
275                _ => {}
276            }
277        }
278
279        false
280    }
281}
282
283impl OptimizationPass for GateCommutation {
284    fn name(&self) -> &'static str {
285        "Gate Commutation"
286    }
287
288    fn apply_to_gates(
289        &self,
290        gates: Vec<Box<dyn GateOp>>,
291        _cost_model: &dyn CostModel,
292    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
293        if gates.len() < 2 {
294            return Ok(gates);
295        }
296
297        let mut optimized = gates;
298        // Bound the number of outer iterations to prevent oscillation.
299        // Each pass does at most one forward scan; repeated passes let reordering
300        // propagate, but the bound ensures we always terminate.
301        let max_outer = self.max_lookahead * 2 + 1;
302        let mut outer_iter = 0;
303        let mut changed = true;
304
305        // Keep trying to commute gates until no more changes or the iteration
306        // bound is reached.
307        while changed && outer_iter < max_outer {
308            changed = false;
309            outer_iter += 1;
310            let mut i = 0;
311
312            while i < optimized.len().saturating_sub(1) {
313                let can_swap = {
314                    let gate1 = &optimized[i];
315                    let gate2 = &optimized[i + 1];
316
317                    // Check if gates act on different qubits (always commute)
318                    let qubits1: HashSet<_> = gate1.qubits().into_iter().collect();
319                    let qubits2: HashSet<_> = gate2.qubits().into_iter().collect();
320
321                    if qubits1.is_disjoint(&qubits2) {
322                        // Gates on disjoint qubits: only swap when it would enable
323                        // further optimisations (not just because they commute).
324                        self.would_benefit_from_swap(&optimized, i)
325                    } else if qubits1 == qubits2 {
326                        // Same qubit set: only swap when a downstream gate of the
327                        // same type exists that could later cancel or merge.
328                        // Swapping two identical same-qubit gates is always a no-op,
329                        // so guard against that first.
330                        if gate1.name() == gate2.name() {
331                            // Identical gate names on same qubits: swapping achieves
332                            // nothing useful — skip to avoid oscillation.
333                            false
334                        } else {
335                            self.gates_commute(gate1.as_ref(), gate2.as_ref())
336                        }
337                    } else {
338                        // Overlapping but not identical qubit sets
339                        false
340                    }
341                };
342
343                if can_swap {
344                    optimized.swap(i, i + 1);
345                    changed = true;
346                }
347                // Always advance forward to avoid cycling on the same pair.
348                i += 1;
349
350                // Limit lookahead to prevent excessive computation
351                if i >= self.max_lookahead {
352                    break;
353                }
354            }
355        }
356
357        Ok(optimized)
358    }
359}
360
361/// Gate merging pass - combines adjacent gates
362pub struct GateMerging {
363    merge_rotations: bool,
364    merge_threshold: f64,
365}
366
367impl GateMerging {
368    #[must_use]
369    pub const fn new(merge_rotations: bool, merge_threshold: f64) -> Self {
370        Self {
371            merge_rotations,
372            merge_threshold,
373        }
374    }
375}
376
377impl OptimizationPass for GateMerging {
378    fn name(&self) -> &'static str {
379        "Gate Merging"
380    }
381
382    fn apply_to_gates(
383        &self,
384        gates: Vec<Box<dyn GateOp>>,
385        _cost_model: &dyn CostModel,
386    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
387        let mut optimized = Vec::new();
388        let mut i = 0;
389
390        while i < gates.len() {
391            if i + 1 < gates.len() && self.merge_rotations {
392                let gate1 = &gates[i];
393                let gate2 = &gates[i + 1];
394
395                // Try to merge rotation gates
396                if gate1.qubits() == gate2.qubits() {
397                    let merged = match (gate1.name(), gate2.name()) {
398                        // Same-axis rotations can be directly merged
399                        ("RX", "RX") | ("RY", "RY") | ("RZ", "RZ") => {
400                            // Already handled by RotationMerging pass, skip here
401                            None
402                        }
403                        // Different axis rotations might be mergeable using Euler decomposition
404                        ("RZ" | "RY", "RX") | ("RX" | "RY", "RZ") | ("RX" | "RZ", "RY")
405                            if self.merge_threshold > 0.0 =>
406                        {
407                            // Complex merging would require matrix multiplication
408                            // For now, skip this advanced optimization
409                            None
410                        }
411                        // Phase gates (S, T) can sometimes be merged with RZ
412                        ("S" | "T", "RZ") | ("RZ", "S" | "T") => {
413                            // S = RZ(π/2), T = RZ(π/4)
414                            // These could be merged but need special handling
415                            None
416                        }
417                        _ => None,
418                    };
419
420                    if let Some(merged_gate) = merged {
421                        optimized.push(merged_gate);
422                        i += 2;
423                        continue;
424                    }
425                }
426            }
427
428            // Check for special merging patterns
429            if i + 1 < gates.len() {
430                let gate1 = &gates[i];
431                let gate2 = &gates[i + 1];
432
433                // H-Z-H = X, H-X-H = Z (basis change)
434                if i + 2 < gates.len() {
435                    let gate3 = &gates[i + 2];
436                    if gate1.name() == "H"
437                        && gate3.name() == "H"
438                        && gate1.qubits() == gate2.qubits()
439                        && gate2.qubits() == gate3.qubits()
440                    {
441                        match gate2.name() {
442                            "Z" => {
443                                // H-Z-H = X
444                                optimized.push(Box::new(single::PauliX {
445                                    target: gate1.qubits()[0],
446                                })
447                                    as Box<dyn GateOp>);
448                                i += 3;
449                                continue;
450                            }
451                            "X" => {
452                                // H-X-H = Z
453                                optimized.push(Box::new(single::PauliZ {
454                                    target: gate1.qubits()[0],
455                                })
456                                    as Box<dyn GateOp>);
457                                i += 3;
458                                continue;
459                            }
460                            _ => {}
461                        }
462                    }
463                }
464            }
465
466            // If no merging happened, keep the original gate
467            optimized.push(gates[i].clone());
468            i += 1;
469        }
470
471        Ok(optimized)
472    }
473}
474
475/// Rotation merging pass - specifically merges rotation gates
476pub struct RotationMerging {
477    tolerance: f64,
478}
479
480impl RotationMerging {
481    #[must_use]
482    pub const fn new(tolerance: f64) -> Self {
483        Self { tolerance }
484    }
485
486    /// Check if angle is effectively zero (or 2π multiple)
487    fn is_zero_rotation(&self, angle: f64) -> bool {
488        let normalized = angle % (2.0 * PI);
489        normalized.abs() < self.tolerance || 2.0f64.mul_add(-PI, normalized).abs() < self.tolerance
490    }
491
492    /// Merge two rotation angles
493    fn merge_angles(&self, angle1: f64, angle2: f64) -> f64 {
494        let merged = angle1 + angle2;
495        let normalized = merged % (2.0 * PI);
496        if normalized > PI {
497            2.0f64.mul_add(-PI, normalized)
498        } else if normalized < -PI {
499            2.0f64.mul_add(PI, normalized)
500        } else {
501            normalized
502        }
503    }
504}
505
506impl OptimizationPass for RotationMerging {
507    fn name(&self) -> &'static str {
508        "Rotation Merging"
509    }
510
511    fn apply_to_gates(
512        &self,
513        gates: Vec<Box<dyn GateOp>>,
514        _cost_model: &dyn CostModel,
515    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
516        let mut optimized = Vec::new();
517        let mut i = 0;
518
519        while i < gates.len() {
520            if i + 1 < gates.len() {
521                let gate1 = &gates[i];
522                let gate2 = &gates[i + 1];
523
524                // Check if both gates are rotations on the same qubit and axis
525                if gate1.qubits() == gate2.qubits() && gate1.name() == gate2.name() {
526                    match gate1.name() {
527                        "RX" => {
528                            if let (Some(rx1), Some(rx2)) = (
529                                gate1.as_any().downcast_ref::<single::RotationX>(),
530                                gate2.as_any().downcast_ref::<single::RotationX>(),
531                            ) {
532                                let merged_angle = self.merge_angles(rx1.theta, rx2.theta);
533                                if self.is_zero_rotation(merged_angle) {
534                                    // Skip both gates if the merged rotation is effectively zero
535                                    i += 2;
536                                    continue;
537                                }
538                                // Create a new merged rotation gate
539                                optimized.push(Box::new(single::RotationX {
540                                    target: rx1.target,
541                                    theta: merged_angle,
542                                })
543                                    as Box<dyn GateOp>);
544                                i += 2;
545                                continue;
546                            }
547                        }
548                        "RY" => {
549                            if let (Some(ry1), Some(ry2)) = (
550                                gate1.as_any().downcast_ref::<single::RotationY>(),
551                                gate2.as_any().downcast_ref::<single::RotationY>(),
552                            ) {
553                                let merged_angle = self.merge_angles(ry1.theta, ry2.theta);
554                                if self.is_zero_rotation(merged_angle) {
555                                    i += 2;
556                                    continue;
557                                }
558                                optimized.push(Box::new(single::RotationY {
559                                    target: ry1.target,
560                                    theta: merged_angle,
561                                })
562                                    as Box<dyn GateOp>);
563                                i += 2;
564                                continue;
565                            }
566                        }
567                        "RZ" => {
568                            if let (Some(rz1), Some(rz2)) = (
569                                gate1.as_any().downcast_ref::<single::RotationZ>(),
570                                gate2.as_any().downcast_ref::<single::RotationZ>(),
571                            ) {
572                                let merged_angle = self.merge_angles(rz1.theta, rz2.theta);
573                                if self.is_zero_rotation(merged_angle) {
574                                    i += 2;
575                                    continue;
576                                }
577                                optimized.push(Box::new(single::RotationZ {
578                                    target: rz1.target,
579                                    theta: merged_angle,
580                                })
581                                    as Box<dyn GateOp>);
582                                i += 2;
583                                continue;
584                            }
585                        }
586                        _ => {}
587                    }
588                }
589            }
590
591            // If we didn't merge, keep the original gate
592            optimized.push(gates[i].clone());
593            i += 1;
594        }
595
596        Ok(optimized)
597    }
598}
599
600#[cfg(test)]
601mod basic_passes_tests {
602    use super::*;
603    use crate::optimization::cost_model::{AbstractCostModel, CostWeights};
604    use quantrs2_core::gate::single::{Hadamard, PauliX, PauliY, PauliZ};
605
606    fn cost() -> AbstractCostModel {
607        AbstractCostModel::new(CostWeights::default())
608    }
609
610    #[test]
611    fn test_aggressive_cancels_xyxy_block() {
612        // X-Y-X-Y on the same qubit composes to -I (identity up to global
613        // phase), so all four must be removed. Pairwise cancellation does NOT
614        // catch this (adjacent gates differ), exercising the matrix-based block
615        // check.
616        let pass = GateCancellation::new(true);
617        let gates: Vec<Box<dyn GateOp>> = vec![
618            Box::new(PauliX { target: QubitId(0) }),
619            Box::new(PauliY { target: QubitId(0) }),
620            Box::new(PauliX { target: QubitId(0) }),
621            Box::new(PauliY { target: QubitId(0) }),
622        ];
623
624        let result = pass
625            .apply_to_gates(gates, &cost())
626            .expect("cancellation pass must succeed");
627        assert!(
628            result.is_empty(),
629            "XYXY block should fully cancel, got {} gates",
630            result.len()
631        );
632    }
633
634    #[test]
635    fn test_aggressive_preserves_non_cancelling_block() {
636        // H-X-Y-Z does NOT compose to identity, so every gate must be kept.
637        let pass = GateCancellation::new(true);
638        let gates: Vec<Box<dyn GateOp>> = vec![
639            Box::new(Hadamard { target: QubitId(0) }),
640            Box::new(PauliX { target: QubitId(0) }),
641            Box::new(PauliY { target: QubitId(0) }),
642            Box::new(PauliZ { target: QubitId(0) }),
643        ];
644
645        let result = pass
646            .apply_to_gates(gates, &cost())
647            .expect("cancellation pass must succeed");
648        assert_eq!(
649            result.len(),
650            4,
651            "non-cancelling block must be preserved entirely"
652        );
653    }
654
655    #[test]
656    fn test_block_identity_check_direct() {
657        // Direct unit check of the matrix-based predicate.
658        let x = PauliX { target: QubitId(0) };
659        let y = PauliY { target: QubitId(0) };
660        let z = PauliZ { target: QubitId(0) };
661        let h = Hadamard { target: QubitId(0) };
662
663        // XYXY = -I (cancels).
664        assert!(GateCancellation::four_gate_block_is_identity(
665            &x, &y, &x, &y
666        ));
667        // HXYZ is not a global phase times identity.
668        assert!(!GateCancellation::four_gate_block_is_identity(
669            &h, &x, &y, &z
670        ));
671    }
672}