Skip to main content

quantrs2_circuit/optimization/passes/
rewriting_passes.rs

1//! Rewriting optimization passes: peephole, template matching, circuit rewriting, and parallelization.
2
3use crate::optimization::cost_model::CostModel;
4use quantrs2_core::error::{QuantRS2Error, QuantRS2Result};
5use quantrs2_core::gate::{
6    multi,
7    single::{self},
8    GateOp,
9};
10use quantrs2_core::qubit::QubitId;
11use std::collections::{HashMap, HashSet};
12use std::f64::consts::PI;
13
14use super::OptimizationPass;
15
16/// Peephole optimization - looks at small windows of gates for local optimizations
17pub struct PeepholeOptimization {
18    window_size: usize,
19    patterns: Vec<PeepholePattern>,
20}
21
22#[derive(Clone)]
23pub struct PeepholePattern {
24    name: String,
25    window_size: usize,
26    matcher: fn(&[Box<dyn GateOp>]) -> Option<Vec<Box<dyn GateOp>>>,
27}
28
29impl PeepholeOptimization {
30    #[must_use]
31    pub fn new(window_size: usize) -> Self {
32        let patterns = vec![
33            // Pattern: X-Y-X = -Y
34            PeepholePattern {
35                name: "X-Y-X to -Y".to_string(),
36                window_size: 3,
37                matcher: |gates| {
38                    if gates.len() >= 3 {
39                        let g0 = &gates[0];
40                        let g1 = &gates[1];
41                        let g2 = &gates[2];
42
43                        if g0.name() == "X"
44                            && g2.name() == "X"
45                            && g1.name() == "Y"
46                            && g0.qubits() == g1.qubits()
47                            && g1.qubits() == g2.qubits()
48                        {
49                            // X-Y-X = -Y, we can return Y with a phase
50                            return Some(vec![g1.clone()]);
51                        }
52                    }
53                    None
54                },
55            },
56            // Pattern: H-S-H = X·RZ(π/2)·X
57            PeepholePattern {
58                name: "H-S-H simplification".to_string(),
59                window_size: 3,
60                matcher: |gates| {
61                    if gates.len() >= 3 {
62                        let g0 = &gates[0];
63                        let g1 = &gates[1];
64                        let g2 = &gates[2];
65
66                        if g0.name() == "H"
67                            && g2.name() == "H"
68                            && g1.name() == "S"
69                            && g0.qubits() == g1.qubits()
70                            && g1.qubits() == g2.qubits()
71                        {
72                            let target = g0.qubits()[0];
73                            return Some(vec![
74                                Box::new(single::PauliX { target }) as Box<dyn GateOp>,
75                                Box::new(single::RotationZ {
76                                    target,
77                                    theta: PI / 2.0,
78                                }) as Box<dyn GateOp>,
79                                Box::new(single::PauliX { target }) as Box<dyn GateOp>,
80                            ]);
81                        }
82                    }
83                    None
84                },
85            },
86            // Pattern: RZ-RX-RZ (Euler decomposition check)
87            PeepholePattern {
88                name: "Euler angle optimization".to_string(),
89                window_size: 3,
90                matcher: |gates| {
91                    if gates.len() >= 3 {
92                        let g0 = &gates[0];
93                        let g1 = &gates[1];
94                        let g2 = &gates[2];
95
96                        if g0.name() == "RZ"
97                            && g1.name() == "RX"
98                            && g2.name() == "RZ"
99                            && g0.qubits() == g1.qubits()
100                            && g1.qubits() == g2.qubits()
101                        {
102                            if let (Some(rz1), Some(rx), Some(rz2)) = (
103                                g0.as_any().downcast_ref::<single::RotationZ>(),
104                                g1.as_any().downcast_ref::<single::RotationX>(),
105                                g2.as_any().downcast_ref::<single::RotationZ>(),
106                            ) {
107                                // If middle rotation is small, might be numerical error
108                                if rx.theta.abs() < 1e-10 {
109                                    let combined_angle = rz1.theta + rz2.theta;
110                                    if combined_angle.abs() < 1e-10 {
111                                        return Some(vec![]); // Identity
112                                    }
113                                    return Some(vec![Box::new(single::RotationZ {
114                                        target: rz1.target,
115                                        theta: combined_angle,
116                                    })
117                                        as Box<dyn GateOp>]);
118                                }
119                            }
120                        }
121                    }
122                    None
123                },
124            },
125            // Pattern: CNOT-RZ-CNOT (phase gadget)
126            PeepholePattern {
127                name: "Phase gadget optimization".to_string(),
128                window_size: 3,
129                matcher: |gates| {
130                    if gates.len() >= 3 {
131                        let g0 = &gates[0];
132                        let g1 = &gates[1];
133                        let g2 = &gates[2];
134
135                        if g0.name() == "CNOT" && g2.name() == "CNOT" && g1.name() == "RZ" {
136                            if let (Some(cnot1), Some(rz), Some(cnot2)) = (
137                                g0.as_any().downcast_ref::<multi::CNOT>(),
138                                g1.as_any().downcast_ref::<single::RotationZ>(),
139                                g2.as_any().downcast_ref::<multi::CNOT>(),
140                            ) {
141                                if cnot1.control == cnot2.control
142                                    && cnot1.target == cnot2.target
143                                    && rz.target == cnot1.target
144                                {
145                                    // This is a controlled-RZ, keep as is for now
146                                    return None;
147                                }
148                            }
149                        }
150                    }
151                    None
152                },
153            },
154            // Pattern: Hadamard ladder reduction
155            PeepholePattern {
156                name: "Hadamard ladder".to_string(),
157                window_size: 4,
158                matcher: |gates| {
159                    if gates.len() >= 4 {
160                        // H-CNOT-H-CNOT pattern
161                        if gates[0].name() == "H"
162                            && gates[1].name() == "CNOT"
163                            && gates[2].name() == "H"
164                            && gates[3].name() == "CNOT"
165                        {
166                            let h1_target = gates[0].qubits()[0];
167                            let h2_target = gates[2].qubits()[0];
168
169                            if let (Some(cnot1), Some(cnot2)) = (
170                                gates[1].as_any().downcast_ref::<multi::CNOT>(),
171                                gates[3].as_any().downcast_ref::<multi::CNOT>(),
172                            ) {
173                                if h1_target == cnot1.control
174                                    && h2_target == cnot2.control
175                                    && cnot1.target == cnot2.target
176                                {
177                                    return None; // Keep for now, needs deeper analysis
178                                }
179                            }
180                        }
181                    }
182                    None
183                },
184            },
185        ];
186
187        Self {
188            window_size,
189            patterns,
190        }
191    }
192
193    /// Apply patterns to a window of gates
194    fn apply_patterns(&self, window: &[Box<dyn GateOp>]) -> Option<Vec<Box<dyn GateOp>>> {
195        for pattern in &self.patterns {
196            if window.len() >= pattern.window_size {
197                if let Some(replacement) = (pattern.matcher)(window) {
198                    return Some(replacement);
199                }
200            }
201        }
202        None
203    }
204}
205
206impl OptimizationPass for PeepholeOptimization {
207    fn name(&self) -> &'static str {
208        "Peephole Optimization"
209    }
210
211    fn apply_to_gates(
212        &self,
213        gates: Vec<Box<dyn GateOp>>,
214        _cost_model: &dyn CostModel,
215    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
216        let mut optimized = Vec::new();
217        let mut i = 0;
218
219        while i < gates.len() {
220            let mut matched = false;
221
222            for window_size in (2..=self.window_size).rev() {
223                if i + window_size <= gates.len() {
224                    let window = &gates[i..i + window_size];
225
226                    if let Some(replacement) = self.apply_patterns(window) {
227                        optimized.extend(replacement);
228                        i += window_size;
229                        matched = true;
230                        break;
231                    }
232                }
233            }
234
235            if !matched {
236                optimized.push(gates[i].clone());
237                i += 1;
238            }
239        }
240
241        Ok(optimized)
242    }
243}
244
245/// Template matching optimization
246pub struct TemplateMatching {
247    templates: Vec<CircuitTemplate>,
248}
249
250#[derive(Clone)]
251pub struct CircuitTemplate {
252    name: String,
253    pattern: Vec<String>,
254    replacement: Vec<String>,
255    cost_reduction: f64,
256}
257
258impl TemplateMatching {
259    #[must_use]
260    pub fn new() -> Self {
261        let templates = vec![
262            CircuitTemplate {
263                name: "H-Z-H to X".to_string(),
264                pattern: vec!["H".to_string(), "Z".to_string(), "H".to_string()],
265                replacement: vec!["X".to_string()],
266                cost_reduction: 2.0,
267            },
268            CircuitTemplate {
269                name: "H-X-H to Z".to_string(),
270                pattern: vec!["H".to_string(), "X".to_string(), "H".to_string()],
271                replacement: vec!["Z".to_string()],
272                cost_reduction: 2.0,
273            },
274            CircuitTemplate {
275                name: "CNOT-H-CNOT to CZ".to_string(),
276                pattern: vec!["CNOT".to_string(), "H".to_string(), "CNOT".to_string()],
277                replacement: vec!["CZ".to_string()],
278                cost_reduction: 1.5,
279            },
280            CircuitTemplate {
281                name: "Double CNOT elimination".to_string(),
282                pattern: vec!["CNOT".to_string(), "CNOT".to_string()],
283                replacement: vec![],
284                cost_reduction: 2.0,
285            },
286            CircuitTemplate {
287                name: "S-S to Z".to_string(),
288                pattern: vec!["S".to_string(), "S".to_string()],
289                replacement: vec!["Z".to_string()],
290                cost_reduction: 1.0,
291            },
292        ];
293
294        Self { templates }
295    }
296
297    #[must_use]
298    pub const fn with_templates(templates: Vec<CircuitTemplate>) -> Self {
299        Self { templates }
300    }
301
302    /// Create an advanced template matcher with more sophisticated patterns
303    #[must_use]
304    pub fn with_advanced_templates() -> Self {
305        let templates = vec![
306            CircuitTemplate {
307                name: "H-Z-H to X".to_string(),
308                pattern: vec!["H".to_string(), "Z".to_string(), "H".to_string()],
309                replacement: vec!["X".to_string()],
310                cost_reduction: 2.0,
311            },
312            CircuitTemplate {
313                name: "H-X-H to Z".to_string(),
314                pattern: vec!["H".to_string(), "X".to_string(), "H".to_string()],
315                replacement: vec!["Z".to_string()],
316                cost_reduction: 2.0,
317            },
318            CircuitTemplate {
319                name: "CNOT-CNOT elimination".to_string(),
320                pattern: vec!["CNOT".to_string(), "CNOT".to_string()],
321                replacement: vec![],
322                cost_reduction: 2.0,
323            },
324            CircuitTemplate {
325                name: "S-S to Z".to_string(),
326                pattern: vec!["S".to_string(), "S".to_string()],
327                replacement: vec!["Z".to_string()],
328                cost_reduction: 1.0,
329            },
330            CircuitTemplate {
331                name: "T-T-T-T to Identity".to_string(),
332                pattern: vec![
333                    "T".to_string(),
334                    "T".to_string(),
335                    "T".to_string(),
336                    "T".to_string(),
337                ],
338                replacement: vec![],
339                cost_reduction: 4.0,
340            },
341            CircuitTemplate {
342                name: "CNOT-H-CNOT to CZ".to_string(),
343                pattern: vec!["CNOT".to_string(), "H".to_string(), "CNOT".to_string()],
344                replacement: vec!["CZ".to_string()],
345                cost_reduction: 1.0,
346            },
347            CircuitTemplate {
348                name: "SWAP via 3 CNOTs".to_string(),
349                pattern: vec!["CNOT".to_string(), "CNOT".to_string(), "CNOT".to_string()],
350                replacement: vec!["SWAP".to_string()],
351                cost_reduction: 0.5,
352            },
353        ];
354
355        Self { templates }
356    }
357
358    /// Create a template matcher for specific hardware
359    #[must_use]
360    pub fn for_hardware(hardware: &str) -> Self {
361        let templates = match hardware {
362            "ibm" => vec![
363                CircuitTemplate {
364                    name: "H-Z-H to X".to_string(),
365                    pattern: vec!["H".to_string(), "Z".to_string(), "H".to_string()],
366                    replacement: vec!["X".to_string()],
367                    cost_reduction: 2.0,
368                },
369                CircuitTemplate {
370                    name: "CNOT-CNOT elimination".to_string(),
371                    pattern: vec!["CNOT".to_string(), "CNOT".to_string()],
372                    replacement: vec![],
373                    cost_reduction: 2.0,
374                },
375            ],
376            "google" => vec![CircuitTemplate {
377                name: "CNOT to CZ with Hadamards".to_string(),
378                pattern: vec!["CNOT".to_string()],
379                replacement: vec!["H".to_string(), "CZ".to_string(), "H".to_string()],
380                cost_reduction: -0.5,
381            }],
382            _ => Self::new().templates,
383        };
384
385        Self { templates }
386    }
387}
388
389impl OptimizationPass for TemplateMatching {
390    fn name(&self) -> &'static str {
391        "Template Matching"
392    }
393
394    fn apply_to_gates(
395        &self,
396        gates: Vec<Box<dyn GateOp>>,
397        cost_model: &dyn CostModel,
398    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
399        let mut optimized = gates;
400        let mut changed = true;
401
402        while changed {
403            changed = false;
404            let original_cost = cost_model.gates_cost(&optimized);
405
406            for template in &self.templates {
407                let result = self.apply_template(template, optimized.clone())?;
408                let new_cost = cost_model.gates_cost(&result);
409
410                if new_cost < original_cost {
411                    optimized = result;
412                    changed = true;
413                    break;
414                }
415            }
416        }
417
418        Ok(optimized)
419    }
420}
421
422impl TemplateMatching {
423    fn apply_template(
424        &self,
425        template: &CircuitTemplate,
426        gates: Vec<Box<dyn GateOp>>,
427    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
428        let mut result = Vec::new();
429        let mut i = 0;
430
431        while i < gates.len() {
432            if let Some(replacement) = self.match_pattern_at_position(template, &gates, i)? {
433                result.extend(replacement);
434                i += template.pattern.len();
435            } else {
436                result.push(gates[i].clone());
437                i += 1;
438            }
439        }
440
441        Ok(result)
442    }
443
444    fn match_pattern_at_position(
445        &self,
446        template: &CircuitTemplate,
447        gates: &[Box<dyn GateOp>],
448        start: usize,
449    ) -> QuantRS2Result<Option<Vec<Box<dyn GateOp>>>> {
450        if start + template.pattern.len() > gates.len() {
451            return Ok(None);
452        }
453
454        let mut qubit_mapping = HashMap::new();
455        let mut all_qubits = Vec::new();
456        let mut is_match = true;
457
458        for (i, pattern_gate) in template.pattern.iter().enumerate() {
459            let gate = &gates[start + i];
460
461            if !self.gate_matches_pattern(gate.as_ref(), pattern_gate, &qubit_mapping) {
462                is_match = false;
463                break;
464            }
465
466            for qubit in gate.qubits() {
467                if !all_qubits.contains(&qubit) {
468                    all_qubits.push(qubit);
469                }
470            }
471        }
472
473        if !is_match {
474            return Ok(None);
475        }
476
477        if template
478            .pattern
479            .iter()
480            .all(|p| p == "H" || p == "X" || p == "Y" || p == "Z" || p == "S" || p == "T")
481        {
482            let first_qubit = gates[start].qubits();
483            if first_qubit.len() != 1 {
484                return Ok(None);
485            }
486
487            for i in 1..template.pattern.len() {
488                let gate_qubits = gates[start + i].qubits();
489                if gate_qubits != first_qubit {
490                    return Ok(None);
491                }
492            }
493        }
494
495        qubit_mapping.insert("qubits".to_string(), all_qubits);
496        self.generate_replacement_gates(template, &qubit_mapping)
497    }
498
499    fn gate_matches_pattern(
500        &self,
501        gate: &dyn GateOp,
502        pattern: &str,
503        _qubit_mapping: &HashMap<String, Vec<QubitId>>,
504    ) -> bool {
505        gate.name() == pattern
506    }
507
508    fn generate_replacement_gates(
509        &self,
510        template: &CircuitTemplate,
511        qubit_mapping: &HashMap<String, Vec<QubitId>>,
512    ) -> QuantRS2Result<Option<Vec<Box<dyn GateOp>>>> {
513        let mut replacement_gates = Vec::new();
514
515        let qubits: Vec<QubitId> = qubit_mapping
516            .values()
517            .flat_map(|v| v.iter().copied())
518            .collect();
519        let mut unique_qubits: Vec<QubitId> = Vec::new();
520        for qubit in qubits {
521            if !unique_qubits.contains(&qubit) {
522                unique_qubits.push(qubit);
523            }
524        }
525
526        for replacement_pattern in &template.replacement {
527            if let Some(gate) = self.create_simple_gate(replacement_pattern, &unique_qubits)? {
528                replacement_gates.push(gate);
529            }
530        }
531
532        Ok(Some(replacement_gates))
533    }
534
535    fn create_simple_gate(
536        &self,
537        pattern: &str,
538        qubits: &[QubitId],
539    ) -> QuantRS2Result<Option<Box<dyn GateOp>>> {
540        if qubits.is_empty() {
541            return Ok(None);
542        }
543
544        match pattern {
545            "H" => Ok(Some(Box::new(single::Hadamard { target: qubits[0] }))),
546            "X" => Ok(Some(Box::new(single::PauliX { target: qubits[0] }))),
547            "Y" => Ok(Some(Box::new(single::PauliY { target: qubits[0] }))),
548            "Z" => Ok(Some(Box::new(single::PauliZ { target: qubits[0] }))),
549            "S" => Ok(Some(Box::new(single::Phase { target: qubits[0] }))),
550            "T" => Ok(Some(Box::new(single::T { target: qubits[0] }))),
551            "CNOT" if qubits.len() >= 2 => Ok(Some(Box::new(multi::CNOT {
552                control: qubits[0],
553                target: qubits[1],
554            }))),
555            "CZ" if qubits.len() >= 2 => Ok(Some(Box::new(multi::CZ {
556                control: qubits[0],
557                target: qubits[1],
558            }))),
559            "SWAP" if qubits.len() >= 2 => Ok(Some(Box::new(multi::SWAP {
560                qubit1: qubits[0],
561                qubit2: qubits[1],
562            }))),
563            _ => Ok(None),
564        }
565    }
566
567    /// Create a gate instance from name and qubits
568    fn create_gate(&self, gate_name: &str, qubits: &[QubitId]) -> QuantRS2Result<Box<dyn GateOp>> {
569        match (gate_name, qubits.len()) {
570            ("H", 1) => Ok(Box::new(single::Hadamard { target: qubits[0] })),
571            ("X", 1) => Ok(Box::new(single::PauliX { target: qubits[0] })),
572            ("Y", 1) => Ok(Box::new(single::PauliY { target: qubits[0] })),
573            ("Z", 1) => Ok(Box::new(single::PauliZ { target: qubits[0] })),
574            ("S", 1) => Ok(Box::new(single::Phase { target: qubits[0] })),
575            ("T", 1) => Ok(Box::new(single::T { target: qubits[0] })),
576            ("CNOT", 2) => Ok(Box::new(multi::CNOT {
577                control: qubits[0],
578                target: qubits[1],
579            })),
580            ("CZ", 2) => Ok(Box::new(multi::CZ {
581                control: qubits[0],
582                target: qubits[1],
583            })),
584            ("SWAP", 2) => Ok(Box::new(multi::SWAP {
585                qubit1: qubits[0],
586                qubit2: qubits[1],
587            })),
588            _ => Err(QuantRS2Error::UnsupportedOperation(format!(
589                "Cannot create gate {} with {} qubits",
590                gate_name,
591                qubits.len()
592            ))),
593        }
594    }
595}
596
597impl Default for TemplateMatching {
598    fn default() -> Self {
599        Self::new()
600    }
601}
602
603// ─── helpers shared by CircuitRewriting rule closures ───────────────────────
604
605/// Return the single qubit targeted by a gate, or `None` if it is multi-qubit.
606pub fn single_qubit_of(g: &dyn GateOp) -> Option<QubitId> {
607    let qs = g.qubits();
608    if qs.len() == 1 {
609        Some(qs[0])
610    } else {
611        None
612    }
613}
614
615/// `true` when every gate in the slice targets exactly the same single qubit.
616pub fn all_same_single_qubit(gates: &[Box<dyn GateOp>]) -> bool {
617    let first = match single_qubit_of(gates[0].as_ref()) {
618        Some(q) => q,
619        None => return false,
620    };
621    gates[1..]
622        .iter()
623        .all(|g| single_qubit_of(g.as_ref()) == Some(first))
624}
625
626/// Extract the rotation angle from an RZ gate.
627///
628/// `RotationZ::matrix()` follows the IBM/Qiskit/OpenQASM-3 convention
629/// `Rz(θ) = diag(e^{-iθ/2}, e^{+iθ/2})`, so the angle cannot be read off a
630/// single diagonal entry's phase (`arg(m[0]) == -θ/2`, which silently negates
631/// θ). Instead take the phase difference between the two diagonal entries,
632/// which is `θ` and is invariant to any overall global phase the matrix may
633/// carry.
634pub fn extract_rz_angle(g: &dyn GateOp) -> Option<f64> {
635    if g.name() != "RZ" {
636        return None;
637    }
638    g.matrix().ok().map(|m| (m[3] * m[0].conj()).arg())
639}
640
641/// Extract the rotation angle from an RX gate.
642pub fn extract_rx_angle(g: &dyn GateOp) -> Option<f64> {
643    if g.name() != "RX" {
644        return None;
645    }
646    g.matrix().ok().map(|m| {
647        let sin_half = -m[1].im;
648        let cos_half = m[0].re;
649        2.0 * sin_half.atan2(cos_half)
650    })
651}
652
653/// Extract the rotation angle from an RY gate.
654pub fn extract_ry_angle(g: &dyn GateOp) -> Option<f64> {
655    if g.name() != "RY" {
656        return None;
657    }
658    g.matrix().ok().map(|m| {
659        let sin_half = -m[1].re;
660        let cos_half = m[0].re;
661        2.0 * sin_half.atan2(cos_half)
662    })
663}
664
665/// Normalise an angle into `(-π, π]`.
666pub fn normalise_angle(theta: f64) -> f64 {
667    use std::f64::consts::TAU;
668    let t = theta % TAU;
669    if t > PI {
670        t - TAU
671    } else if t <= -PI {
672        t + TAU
673    } else {
674        t
675    }
676}
677
678/// `true` when `normalise_angle(theta)` is within `eps` of zero.
679pub fn is_identity_angle(theta: f64, eps: f64) -> bool {
680    normalise_angle(theta).abs() < eps
681}
682
683fn default_rewrite_rules() -> Vec<RewriteRule> {
684    // 3-gate rules — listed first so they fire before their 2-gate sub-patterns
685
686    // H X H → Z
687    let hxh_to_z = RewriteRule {
688        name: "H-X-H to Z".to_string(),
689        window_size: 3,
690        condition: |w| {
691            w[0].name() == "H"
692                && w[1].name() == "X"
693                && w[2].name() == "H"
694                && all_same_single_qubit(w)
695        },
696        rewrite: |w| {
697            let t = single_qubit_of(w[0].as_ref()).unwrap_or_else(|| w[0].qubits()[0]);
698            vec![Box::new(single::PauliZ { target: t }) as Box<dyn GateOp>]
699        },
700    };
701
702    // H Z H → X
703    let hzh_to_x = RewriteRule {
704        name: "H-Z-H to X".to_string(),
705        window_size: 3,
706        condition: |w| {
707            w[0].name() == "H"
708                && w[1].name() == "Z"
709                && w[2].name() == "H"
710                && all_same_single_qubit(w)
711        },
712        rewrite: |w| {
713            let t = single_qubit_of(w[0].as_ref()).unwrap_or_else(|| w[0].qubits()[0]);
714            vec![Box::new(single::PauliX { target: t }) as Box<dyn GateOp>]
715        },
716    };
717
718    // H Y H → Y  (H Y H = −Y; global phase dropped → Y)
719    let hyh_to_y = RewriteRule {
720        name: "H-Y-H to Y (global phase)".to_string(),
721        window_size: 3,
722        condition: |w| {
723            w[0].name() == "H"
724                && w[1].name() == "Y"
725                && w[2].name() == "H"
726                && all_same_single_qubit(w)
727        },
728        rewrite: |w| {
729            let t = single_qubit_of(w[1].as_ref()).unwrap_or_else(|| w[1].qubits()[0]);
730            vec![Box::new(single::PauliY { target: t }) as Box<dyn GateOp>]
731        },
732    };
733
734    // X Z X → Z  (X Z X = −Z; global phase dropped → Z)
735    let xzx_to_z = RewriteRule {
736        name: "X-Z-X to Z (global phase)".to_string(),
737        window_size: 3,
738        condition: |w| {
739            w[0].name() == "X"
740                && w[1].name() == "Z"
741                && w[2].name() == "X"
742                && all_same_single_qubit(w)
743        },
744        rewrite: |w| {
745            let t = single_qubit_of(w[1].as_ref()).unwrap_or_else(|| w[1].qubits()[0]);
746            vec![Box::new(single::PauliZ { target: t }) as Box<dyn GateOp>]
747        },
748    };
749
750    // Z X Z → X  (Z X Z = −X; global phase dropped → X)
751    let zxz_to_x = RewriteRule {
752        name: "Z-X-Z to X (global phase)".to_string(),
753        window_size: 3,
754        condition: |w| {
755            w[0].name() == "Z"
756                && w[1].name() == "X"
757                && w[2].name() == "Z"
758                && all_same_single_qubit(w)
759        },
760        rewrite: |w| {
761            let t = single_qubit_of(w[1].as_ref()).unwrap_or_else(|| w[1].qubits()[0]);
762            vec![Box::new(single::PauliX { target: t }) as Box<dyn GateOp>]
763        },
764    };
765
766    // 2-gate Pauli cancellation rules
767    let hh = RewriteRule {
768        name: "H-H cancel".to_string(),
769        window_size: 2,
770        condition: |w| w[0].name() == "H" && w[1].name() == "H" && all_same_single_qubit(w),
771        rewrite: |_w| vec![],
772    };
773    let xx = RewriteRule {
774        name: "X-X cancel".to_string(),
775        window_size: 2,
776        condition: |w| w[0].name() == "X" && w[1].name() == "X" && all_same_single_qubit(w),
777        rewrite: |_w| vec![],
778    };
779    let yy = RewriteRule {
780        name: "Y-Y cancel".to_string(),
781        window_size: 2,
782        condition: |w| w[0].name() == "Y" && w[1].name() == "Y" && all_same_single_qubit(w),
783        rewrite: |_w| vec![],
784    };
785    let zz = RewriteRule {
786        name: "Z-Z cancel".to_string(),
787        window_size: 2,
788        condition: |w| w[0].name() == "Z" && w[1].name() == "Z" && all_same_single_qubit(w),
789        rewrite: |_w| vec![],
790    };
791
792    // S² = Z
793    let ss_to_z = RewriteRule {
794        name: "S-S to Z".to_string(),
795        window_size: 2,
796        condition: |w| w[0].name() == "S" && w[1].name() == "S" && all_same_single_qubit(w),
797        rewrite: |w| {
798            let t = single_qubit_of(w[0].as_ref()).unwrap_or_else(|| w[0].qubits()[0]);
799            vec![Box::new(single::PauliZ { target: t }) as Box<dyn GateOp>]
800        },
801    };
802
803    // T² = S
804    let tt_to_s = RewriteRule {
805        name: "T-T to S".to_string(),
806        window_size: 2,
807        condition: |w| w[0].name() == "T" && w[1].name() == "T" && all_same_single_qubit(w),
808        rewrite: |w| {
809            let t = single_qubit_of(w[0].as_ref()).unwrap_or_else(|| w[0].qubits()[0]);
810            vec![Box::new(single::Phase { target: t }) as Box<dyn GateOp>]
811        },
812    };
813
814    // CNOT CNOT (same control+target) → identity
815    let cnot_cnot = RewriteRule {
816        name: "CNOT-CNOT cancel".to_string(),
817        window_size: 2,
818        condition: |w| {
819            if w[0].name() != "CNOT" || w[1].name() != "CNOT" {
820                return false;
821            }
822            match (
823                w[0].as_any().downcast_ref::<multi::CNOT>(),
824                w[1].as_any().downcast_ref::<multi::CNOT>(),
825            ) {
826                (Some(c1), Some(c2)) => c1.control == c2.control && c1.target == c2.target,
827                _ => false,
828            }
829        },
830        rewrite: |_w| vec![],
831    };
832
833    // CZ CZ → identity (CZ is self-inverse and symmetric)
834    let cz_cz = RewriteRule {
835        name: "CZ-CZ cancel".to_string(),
836        window_size: 2,
837        condition: |w| {
838            if w[0].name() != "CZ" || w[1].name() != "CZ" {
839                return false;
840            }
841            let q0 = w[0].qubits();
842            let q1 = w[1].qubits();
843            if q0.len() != 2 || q1.len() != 2 {
844                return false;
845            }
846            (q0[0] == q1[0] && q0[1] == q1[1]) || (q0[0] == q1[1] && q0[1] == q1[0])
847        },
848        rewrite: |_w| vec![],
849    };
850
851    // Rotation merging rules
852    let rx_merge = RewriteRule {
853        name: "RX-RX merge".to_string(),
854        window_size: 2,
855        condition: |w| {
856            w[0].name() == "RX"
857                && w[1].name() == "RX"
858                && all_same_single_qubit(w)
859                && extract_rx_angle(w[0].as_ref()).is_some()
860                && extract_rx_angle(w[1].as_ref()).is_some()
861        },
862        rewrite: |w| {
863            let t = single_qubit_of(w[0].as_ref()).unwrap_or_else(|| w[0].qubits()[0]);
864            let a = extract_rx_angle(w[0].as_ref()).unwrap_or(0.0);
865            let b = extract_rx_angle(w[1].as_ref()).unwrap_or(0.0);
866            let sum = normalise_angle(a + b);
867            if is_identity_angle(sum, 1e-10) {
868                vec![]
869            } else {
870                vec![Box::new(single::RotationX {
871                    target: t,
872                    theta: sum,
873                }) as Box<dyn GateOp>]
874            }
875        },
876    };
877
878    let ry_merge = RewriteRule {
879        name: "RY-RY merge".to_string(),
880        window_size: 2,
881        condition: |w| {
882            w[0].name() == "RY"
883                && w[1].name() == "RY"
884                && all_same_single_qubit(w)
885                && extract_ry_angle(w[0].as_ref()).is_some()
886                && extract_ry_angle(w[1].as_ref()).is_some()
887        },
888        rewrite: |w| {
889            let t = single_qubit_of(w[0].as_ref()).unwrap_or_else(|| w[0].qubits()[0]);
890            let a = extract_ry_angle(w[0].as_ref()).unwrap_or(0.0);
891            let b = extract_ry_angle(w[1].as_ref()).unwrap_or(0.0);
892            let sum = normalise_angle(a + b);
893            if is_identity_angle(sum, 1e-10) {
894                vec![]
895            } else {
896                vec![Box::new(single::RotationY {
897                    target: t,
898                    theta: sum,
899                }) as Box<dyn GateOp>]
900            }
901        },
902    };
903
904    let rz_merge = RewriteRule {
905        name: "RZ-RZ merge".to_string(),
906        window_size: 2,
907        condition: |w| {
908            w[0].name() == "RZ"
909                && w[1].name() == "RZ"
910                && all_same_single_qubit(w)
911                && extract_rz_angle(w[0].as_ref()).is_some()
912                && extract_rz_angle(w[1].as_ref()).is_some()
913        },
914        rewrite: |w| {
915            let t = single_qubit_of(w[0].as_ref()).unwrap_or_else(|| w[0].qubits()[0]);
916            let a = extract_rz_angle(w[0].as_ref()).unwrap_or(0.0);
917            let b = extract_rz_angle(w[1].as_ref()).unwrap_or(0.0);
918            let sum = normalise_angle(a + b);
919            if is_identity_angle(sum, 1e-10) {
920                vec![]
921            } else {
922                vec![Box::new(single::RotationZ {
923                    target: t,
924                    theta: sum,
925                }) as Box<dyn GateOp>]
926            }
927        },
928    };
929
930    vec![
931        hxh_to_z, hzh_to_x, hyh_to_y, xzx_to_z, zxz_to_x, hh, xx, yy, zz, ss_to_z, tt_to_s,
932        cnot_cnot, cz_cz, rx_merge, ry_merge, rz_merge,
933    ]
934}
935
936/// Circuit rewriting using equivalence rules
937pub struct CircuitRewriting {
938    rules: Vec<RewriteRule>,
939    max_rewrites: usize,
940}
941
942/// A single rewrite rule: a window size, a pattern condition, and a replacement producer.
943#[derive(Clone)]
944pub struct RewriteRule {
945    /// Human-readable label for debugging
946    name: String,
947    /// Number of consecutive gates this rule inspects
948    window_size: usize,
949    /// Returns `true` when the window matches this rule's LHS pattern
950    condition: fn(&[Box<dyn GateOp>]) -> bool,
951    /// Returns the replacement gates (may be empty for cancellation rules)
952    rewrite: fn(&[Box<dyn GateOp>]) -> Vec<Box<dyn GateOp>>,
953}
954
955impl CircuitRewriting {
956    /// Create a `CircuitRewriting` pass with the default rule set.
957    #[must_use]
958    pub fn new(max_rewrites: usize) -> Self {
959        Self {
960            rules: default_rewrite_rules(),
961            max_rewrites,
962        }
963    }
964
965    /// Create a `CircuitRewriting` pass with a caller-supplied rule set.
966    #[must_use]
967    pub fn with_rules(rules: Vec<RewriteRule>, max_rewrites: usize) -> Self {
968        Self {
969            rules,
970            max_rewrites,
971        }
972    }
973
974    /// Try every rule at position `pos` in `gates`.
975    fn try_apply_at(
976        &self,
977        gates: &[Box<dyn GateOp>],
978        pos: usize,
979    ) -> Option<(usize, Vec<Box<dyn GateOp>>)> {
980        for rule in &self.rules {
981            let end = pos + rule.window_size;
982            if end > gates.len() {
983                continue;
984            }
985            let window = &gates[pos..end];
986            if (rule.condition)(window) {
987                return Some((rule.window_size, (rule.rewrite)(window)));
988            }
989        }
990        None
991    }
992
993    /// One left-to-right scan. Returns `(new_gates, did_fire)`.
994    fn scan_once(&self, gates: Vec<Box<dyn GateOp>>) -> (Vec<Box<dyn GateOp>>, bool) {
995        let mut out: Vec<Box<dyn GateOp>> = Vec::with_capacity(gates.len());
996        let mut i = 0;
997        let mut fired = false;
998
999        while i < gates.len() {
1000            if let Some((consumed, replacement)) = self.try_apply_at(&gates, i) {
1001                out.extend(replacement);
1002                i += consumed;
1003                fired = true;
1004            } else {
1005                out.push(gates[i].clone());
1006                i += 1;
1007            }
1008        }
1009
1010        (out, fired)
1011    }
1012}
1013
1014impl OptimizationPass for CircuitRewriting {
1015    fn name(&self) -> &'static str {
1016        "Circuit Rewriting"
1017    }
1018
1019    fn apply_to_gates(
1020        &self,
1021        gates: Vec<Box<dyn GateOp>>,
1022        _cost_model: &dyn CostModel,
1023    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
1024        let mut current = gates;
1025        let mut passes_done = 0;
1026
1027        loop {
1028            if passes_done >= self.max_rewrites {
1029                break;
1030            }
1031            let (next, fired) = self.scan_once(current);
1032            current = next;
1033            if !fired {
1034                break;
1035            }
1036            passes_done += 1;
1037        }
1038
1039        Ok(current)
1040    }
1041}
1042
1043/// ASAP gate scheduling pass — reorders gates to minimise circuit depth.
1044///
1045/// Uses Kahn's topological-sort algorithm. Two gates are dependent if they
1046/// share at least one qubit. Gates in each layer are emitted in original
1047/// program order for determinism.
1048pub struct ParallelizationPass;
1049
1050impl ParallelizationPass {
1051    /// Create a new `ParallelizationPass`.
1052    #[must_use]
1053    pub const fn new() -> Self {
1054        Self
1055    }
1056}
1057
1058impl Default for ParallelizationPass {
1059    fn default() -> Self {
1060        Self::new()
1061    }
1062}
1063
1064impl OptimizationPass for ParallelizationPass {
1065    fn name(&self) -> &'static str {
1066        "Parallelization (ASAP)"
1067    }
1068
1069    fn apply_to_gates(
1070        &self,
1071        gates: Vec<Box<dyn GateOp>>,
1072        _cost_model: &dyn CostModel,
1073    ) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
1074        Ok(parallelize_gates(gates))
1075    }
1076}
1077
1078/// Reorder `gates` with ASAP scheduling so independent gates appear as early
1079/// as possible, minimising circuit depth.
1080///
1081/// Gate *j* depends on gate *i* (i < j) if and only if they share at least one
1082/// qubit. Kahn's algorithm is used to emit gates in topological order layer by
1083/// layer; within each layer the original index order is preserved.
1084#[must_use]
1085pub fn parallelize_gates(gates: Vec<Box<dyn GateOp>>) -> Vec<Box<dyn GateOp>> {
1086    let n = gates.len();
1087    if n == 0 {
1088        return gates;
1089    }
1090
1091    // Build the set of qubit ids for every gate.
1092    let qubit_sets: Vec<HashSet<u32>> = gates
1093        .iter()
1094        .map(|g| g.qubits().into_iter().map(|q| q.id()).collect())
1095        .collect();
1096
1097    let mut in_degree = vec![0usize; n];
1098    let mut predecessors: Vec<HashSet<usize>> = vec![HashSet::new(); n];
1099    let mut successors: Vec<Vec<usize>> = vec![Vec::new(); n];
1100
1101    let mut last_gate_on_qubit: HashMap<u32, usize> = HashMap::new();
1102
1103    for j in 0..n {
1104        for &qid in &qubit_sets[j] {
1105            if let Some(&i) = last_gate_on_qubit.get(&qid) {
1106                if predecessors[j].insert(i) {
1107                    successors[i].push(j);
1108                    in_degree[j] += 1;
1109                }
1110            }
1111        }
1112        for &qid in &qubit_sets[j] {
1113            last_gate_on_qubit.insert(qid, j);
1114        }
1115    }
1116
1117    // Kahn's algorithm
1118    let mut result: Vec<Box<dyn GateOp>> = Vec::with_capacity(n);
1119    let mut ready: Vec<usize> = (0..n).filter(|&i| in_degree[i] == 0).collect();
1120
1121    while !ready.is_empty() {
1122        ready.sort_unstable();
1123        let layer = std::mem::take(&mut ready);
1124        for &idx in &layer {
1125            result.push(gates[idx].clone());
1126            for &succ in &successors[idx] {
1127                in_degree[succ] -= 1;
1128                if in_degree[succ] == 0 {
1129                    ready.push(succ);
1130                }
1131            }
1132        }
1133    }
1134
1135    // If the graph had a cycle (impossible for sequential gate list, but be safe)
1136    // fall back to original order.
1137    if result.len() != n {
1138        gates
1139    } else {
1140        result
1141    }
1142}
1143
1144/// Helper functions for optimization passes
1145pub mod utils {
1146    use super::{GateOp, HashMap};
1147    use crate::optimization::gate_properties::get_gate_properties;
1148
1149    /// Check if two gates cancel each other
1150    pub fn gates_cancel(gate1: &dyn GateOp, gate2: &dyn GateOp) -> bool {
1151        if gate1.name() != gate2.name() || gate1.qubits() != gate2.qubits() {
1152            return false;
1153        }
1154
1155        let props = get_gate_properties(gate1);
1156        props.is_self_inverse
1157    }
1158
1159    /// Check if a gate is effectively identity
1160    pub fn is_identity_gate(gate: &dyn GateOp, tolerance: f64) -> bool {
1161        match gate.name() {
1162            "RX" | "RY" | "RZ" => {
1163                if let Ok(matrix) = gate.matrix() {
1164                    (matrix[0].re - 1.0).abs() < tolerance && matrix[0].im.abs() < tolerance
1165                } else {
1166                    false
1167                }
1168            }
1169            _ => false,
1170        }
1171    }
1172
1173    /// Calculate circuit depth
1174    #[must_use]
1175    pub fn calculate_depth(gates: &[Box<dyn GateOp>]) -> usize {
1176        let mut qubit_depths: HashMap<u32, usize> = HashMap::new();
1177        let mut max_depth = 0;
1178
1179        for gate in gates {
1180            let gate_qubits = gate.qubits();
1181            let current_depth = gate_qubits
1182                .iter()
1183                .map(|q| qubit_depths.get(&q.id()).copied().unwrap_or(0))
1184                .max()
1185                .unwrap_or(0);
1186
1187            let new_depth = current_depth + 1;
1188            for qubit in gate_qubits {
1189                qubit_depths.insert(qubit.id(), new_depth);
1190            }
1191
1192            max_depth = max_depth.max(new_depth);
1193        }
1194
1195        max_depth
1196    }
1197}
1198
1199// ─── unit tests ──────────────────────────────────────────────────────────────
1200
1201#[cfg(test)]
1202mod rewriting_tests {
1203    use super::*;
1204    use quantrs2_core::gate::single::{
1205        Hadamard, PauliX, PauliY, PauliZ, Phase, RotationX, RotationY, RotationZ, T,
1206    };
1207    use quantrs2_core::qubit::QubitId;
1208
1209    fn q(id: u32) -> QubitId {
1210        QubitId::new(id)
1211    }
1212
1213    fn cost() -> crate::optimization::cost_model::AbstractCostModel {
1214        crate::optimization::cost_model::AbstractCostModel::new(
1215            crate::optimization::cost_model::CostWeights::default(),
1216        )
1217    }
1218
1219    #[test]
1220    fn test_hh_cancels() {
1221        let pass = CircuitRewriting::new(10);
1222        let q0 = q(0);
1223        let gates: Vec<Box<dyn GateOp>> = vec![
1224            Box::new(Hadamard { target: q0 }),
1225            Box::new(Hadamard { target: q0 }),
1226        ];
1227        let result = pass.apply_to_gates(gates, &cost()).expect("apply failed");
1228        assert!(result.is_empty(), "H-H should cancel to identity");
1229    }
1230
1231    #[test]
1232    fn test_hh_different_qubits_no_cancel() {
1233        let pass = CircuitRewriting::new(10);
1234        let gates: Vec<Box<dyn GateOp>> = vec![
1235            Box::new(Hadamard { target: q(0) }),
1236            Box::new(Hadamard { target: q(1) }),
1237        ];
1238        let result = pass.apply_to_gates(gates, &cost()).expect("apply failed");
1239        assert_eq!(result.len(), 2, "H on different qubits must not cancel");
1240    }
1241
1242    #[test]
1243    fn test_ss_to_z() {
1244        let pass = CircuitRewriting::new(10);
1245        let q0 = q(0);
1246        let gates: Vec<Box<dyn GateOp>> = vec![
1247            Box::new(Phase { target: q0 }),
1248            Box::new(Phase { target: q0 }),
1249        ];
1250        let result = pass.apply_to_gates(gates, &cost()).expect("apply failed");
1251        assert_eq!(result.len(), 1, "S-S should produce one gate");
1252        assert_eq!(result[0].name(), "Z", "S-S should produce Z");
1253    }
1254
1255    #[test]
1256    fn test_tt_to_s() {
1257        let pass = CircuitRewriting::new(10);
1258        let q0 = q(0);
1259        let gates: Vec<Box<dyn GateOp>> =
1260            vec![Box::new(T { target: q0 }), Box::new(T { target: q0 })];
1261        let result = pass.apply_to_gates(gates, &cost()).expect("apply failed");
1262        assert_eq!(result.len(), 1, "T-T should produce one gate");
1263        assert_eq!(result[0].name(), "S", "T-T should produce S");
1264    }
1265
1266    #[test]
1267    fn test_rz_rz_merge() {
1268        let pass = CircuitRewriting::new(10);
1269        let q0 = q(0);
1270        let theta1 = PI / 4.0;
1271        let theta2 = PI / 4.0;
1272        let gates: Vec<Box<dyn GateOp>> = vec![
1273            Box::new(RotationZ {
1274                target: q0,
1275                theta: theta1,
1276            }),
1277            Box::new(RotationZ {
1278                target: q0,
1279                theta: theta2,
1280            }),
1281        ];
1282        let result = pass.apply_to_gates(gates, &cost()).expect("apply failed");
1283        assert_eq!(result.len(), 1, "RZ-RZ should merge to one gate");
1284        assert_eq!(result[0].name(), "RZ");
1285        let merged = result[0]
1286            .as_any()
1287            .downcast_ref::<RotationZ>()
1288            .expect("should be RotationZ");
1289        let expected = normalise_angle(theta1 + theta2);
1290        assert!(
1291            (merged.theta - expected).abs() < 1e-9,
1292            "merged angle {:.6} != expected {:.6}",
1293            merged.theta,
1294            expected
1295        );
1296    }
1297
1298    #[test]
1299    fn test_rz_rz_cancel() {
1300        let pass = CircuitRewriting::new(10);
1301        let q0 = q(0);
1302        let gates: Vec<Box<dyn GateOp>> = vec![
1303            Box::new(RotationZ {
1304                target: q0,
1305                theta: PI,
1306            }),
1307            Box::new(RotationZ {
1308                target: q0,
1309                theta: PI,
1310            }),
1311        ];
1312        let result = pass.apply_to_gates(gates, &cost()).expect("apply failed");
1313        assert!(result.is_empty(), "RZ(π)+RZ(π) should cancel");
1314    }
1315
1316    #[test]
1317    fn test_rx_rx_merge() {
1318        let pass = CircuitRewriting::new(10);
1319        let q0 = q(0);
1320        let gates: Vec<Box<dyn GateOp>> = vec![
1321            Box::new(RotationX {
1322                target: q0,
1323                theta: PI / 3.0,
1324            }),
1325            Box::new(RotationX {
1326                target: q0,
1327                theta: PI / 6.0,
1328            }),
1329        ];
1330        let result = pass.apply_to_gates(gates, &cost()).expect("apply failed");
1331        assert_eq!(result.len(), 1, "RX-RX should merge");
1332        assert_eq!(result[0].name(), "RX");
1333        let merged = result[0]
1334            .as_any()
1335            .downcast_ref::<RotationX>()
1336            .expect("RotationX");
1337        let expected = normalise_angle(PI / 3.0 + PI / 6.0);
1338        assert!((merged.theta - expected).abs() < 1e-9);
1339    }
1340
1341    #[test]
1342    fn test_ry_ry_cancel() {
1343        let pass = CircuitRewriting::new(10);
1344        let q0 = q(0);
1345        let gates: Vec<Box<dyn GateOp>> = vec![
1346            Box::new(RotationY {
1347                target: q0,
1348                theta: PI / 2.0,
1349            }),
1350            Box::new(RotationY {
1351                target: q0,
1352                theta: -PI / 2.0,
1353            }),
1354        ];
1355        let result = pass.apply_to_gates(gates, &cost()).expect("apply failed");
1356        assert!(result.is_empty(), "RY(π/2)+RY(-π/2) should cancel");
1357    }
1358
1359    #[test]
1360    fn test_hxh_to_z() {
1361        let pass = CircuitRewriting::new(10);
1362        let q0 = q(0);
1363        let gates: Vec<Box<dyn GateOp>> = vec![
1364            Box::new(Hadamard { target: q0 }),
1365            Box::new(PauliX { target: q0 }),
1366            Box::new(Hadamard { target: q0 }),
1367        ];
1368        let result = pass.apply_to_gates(gates, &cost()).expect("apply failed");
1369        assert_eq!(result.len(), 1);
1370        assert_eq!(result[0].name(), "Z", "H-X-H → Z");
1371    }
1372
1373    #[test]
1374    fn test_hzh_to_x() {
1375        let pass = CircuitRewriting::new(10);
1376        let q0 = q(0);
1377        let gates: Vec<Box<dyn GateOp>> = vec![
1378            Box::new(Hadamard { target: q0 }),
1379            Box::new(PauliZ { target: q0 }),
1380            Box::new(Hadamard { target: q0 }),
1381        ];
1382        let result = pass.apply_to_gates(gates, &cost()).expect("apply failed");
1383        assert_eq!(result.len(), 1);
1384        assert_eq!(result[0].name(), "X", "H-Z-H → X");
1385    }
1386
1387    #[test]
1388    fn test_hyh_to_y() {
1389        let pass = CircuitRewriting::new(10);
1390        let q0 = q(0);
1391        let gates: Vec<Box<dyn GateOp>> = vec![
1392            Box::new(Hadamard { target: q0 }),
1393            Box::new(PauliY { target: q0 }),
1394            Box::new(Hadamard { target: q0 }),
1395        ];
1396        let result = pass.apply_to_gates(gates, &cost()).expect("apply failed");
1397        assert_eq!(result.len(), 1);
1398        assert_eq!(result[0].name(), "Y", "H-Y-H → Y");
1399    }
1400
1401    #[test]
1402    fn test_hhh_converges_to_h() {
1403        let pass = CircuitRewriting::new(10);
1404        let q0 = q(0);
1405        let gates: Vec<Box<dyn GateOp>> = vec![
1406            Box::new(Hadamard { target: q0 }),
1407            Box::new(Hadamard { target: q0 }),
1408            Box::new(Hadamard { target: q0 }),
1409        ];
1410        let result = pass.apply_to_gates(gates, &cost()).expect("apply failed");
1411        assert_eq!(result.len(), 1, "H-H-H should converge to one H");
1412        assert_eq!(result[0].name(), "H");
1413    }
1414
1415    #[test]
1416    fn test_xxyy_cancel() {
1417        let pass = CircuitRewriting::new(10);
1418        let q0 = q(0);
1419        let gates: Vec<Box<dyn GateOp>> = vec![
1420            Box::new(PauliX { target: q0 }),
1421            Box::new(PauliX { target: q0 }),
1422            Box::new(PauliY { target: q0 }),
1423            Box::new(PauliY { target: q0 }),
1424        ];
1425        let result = pass.apply_to_gates(gates, &cost()).expect("apply failed");
1426        assert!(result.is_empty(), "X-X-Y-Y should cancel");
1427    }
1428}