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}