1use crate::builder::Circuit;
8use crate::scirs2_integration::{AnalyzerConfig, SciRS2CircuitAnalyzer};
9use quantrs2_core::{
10 error::{QuantRS2Error, QuantRS2Result},
11 gate::{
12 multi::{CRX, CRY, CRZ},
13 single::{RotationX, RotationY, RotationZ},
14 GateOp,
15 },
16 qubit::QubitId,
17};
18use scirs2_core::ndarray::{array, Array2, ArrayView2};
19use scirs2_core::Complex64;
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22
23const DEFAULT_TOLERANCE: f64 = 1e-10;
25
26const SCIRS2_DEFAULT_TOLERANCE: f64 = 1e-12;
28
29const COMPLEX_TOLERANCE: f64 = 1e-14;
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct EquivalenceResult {
35 pub equivalent: bool,
37 pub check_type: EquivalenceType,
39 pub max_difference: Option<f64>,
41 pub details: String,
43 pub numerical_analysis: Option<NumericalAnalysis>,
45 pub confidence_score: f64,
47 pub statistical_significance: Option<f64>,
49 pub error_bounds: Option<ErrorBounds>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct NumericalAnalysis {
56 pub condition_number: Option<f64>,
58 pub numerical_rank: Option<usize>,
60 pub frobenius_norm: f64,
62 pub spectral_norm: Option<f64>,
64 pub adaptive_tolerance: f64,
66 pub stability_indicator: f64,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct ErrorBounds {
73 pub lower_bound: f64,
75 pub upper_bound: f64,
77 pub confidence_level: f64,
79 pub standard_deviation: Option<f64>,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
85pub enum EquivalenceType {
86 UnitaryEquivalence,
88 StateVectorEquivalence,
90 ProbabilisticEquivalence,
92 StructuralEquivalence,
94 GlobalPhaseEquivalence,
96 SciRS2NumericalEquivalence,
98 SciRS2StatisticalEquivalence,
100 SciRS2GraphEquivalence,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct EquivalenceOptions {
107 pub tolerance: f64,
109 pub ignore_global_phase: bool,
111 pub check_all_states: bool,
113 pub max_unitary_qubits: usize,
115 pub enable_adaptive_tolerance: bool,
117 pub enable_statistical_analysis: bool,
119 pub enable_stability_analysis: bool,
121 pub enable_graph_comparison: bool,
123 pub confidence_level: f64,
125 pub max_condition_number: f64,
127 pub scirs2_config: Option<AnalyzerConfig>,
129 pub complex_tolerance: f64,
131 pub enable_parallel_computation: bool,
133}
134
135impl Default for EquivalenceOptions {
136 fn default() -> Self {
137 Self {
138 tolerance: DEFAULT_TOLERANCE,
139 ignore_global_phase: true,
140 check_all_states: true,
141 max_unitary_qubits: 10,
142 enable_adaptive_tolerance: true,
143 enable_statistical_analysis: true,
144 enable_stability_analysis: true,
145 enable_graph_comparison: false, confidence_level: 0.95,
147 max_condition_number: 1e12,
148 scirs2_config: None, complex_tolerance: COMPLEX_TOLERANCE,
150 enable_parallel_computation: true,
151 }
152 }
153}
154
155pub struct EquivalenceChecker {
157 options: EquivalenceOptions,
158 scirs2_analyzer: Option<SciRS2CircuitAnalyzer>,
159 numerical_cache: HashMap<String, NumericalAnalysis>,
160}
161
162impl EquivalenceChecker {
163 #[must_use]
165 pub fn new(options: EquivalenceOptions) -> Self {
166 let scirs2_analyzer = if options.enable_graph_comparison
167 || options.enable_statistical_analysis
168 || options.enable_stability_analysis
169 {
170 Some(SciRS2CircuitAnalyzer::new())
171 } else {
172 None
173 };
174
175 Self {
176 options,
177 scirs2_analyzer,
178 numerical_cache: HashMap::new(),
179 }
180 }
181
182 #[must_use]
184 pub fn default() -> Self {
185 Self::new(EquivalenceOptions::default())
186 }
187
188 #[must_use]
190 pub fn with_scirs2_config(config: AnalyzerConfig) -> Self {
191 let scirs2_analyzer = Some(SciRS2CircuitAnalyzer::with_config(config.clone()));
192
193 Self {
194 options: EquivalenceOptions {
195 scirs2_config: Some(config),
196 enable_graph_comparison: true,
197 ..Default::default()
198 },
199 scirs2_analyzer,
200 numerical_cache: HashMap::new(),
201 }
202 }
203
204 pub fn check_equivalence<const N: usize>(
206 &mut self,
207 circuit1: &Circuit<N>,
208 circuit2: &Circuit<N>,
209 ) -> QuantRS2Result<EquivalenceResult> {
210 if self.options.enable_graph_comparison {
212 if let Ok(result) = self.check_scirs2_graph_equivalence(circuit1, circuit2) {
213 if result.equivalent {
214 return Ok(result);
215 }
216 }
217 }
218
219 if let Ok(result) = self.check_structural_equivalence(circuit1, circuit2) {
221 if result.equivalent {
222 return Ok(result);
223 }
224 }
225
226 if (self.options.enable_adaptive_tolerance || self.options.enable_statistical_analysis)
228 && N <= self.options.max_unitary_qubits
229 {
230 return self.check_scirs2_numerical_equivalence(circuit1, circuit2);
231 }
232
233 if N <= self.options.max_unitary_qubits {
235 return self.check_unitary_equivalence(circuit1, circuit2);
236 }
237
238 self.check_state_vector_equivalence(circuit1, circuit2)
240 }
241
242 pub fn check_scirs2_numerical_equivalence<const N: usize>(
244 &mut self,
245 circuit1: &Circuit<N>,
246 circuit2: &Circuit<N>,
247 ) -> QuantRS2Result<EquivalenceResult> {
248 if N > self.options.max_unitary_qubits {
249 return Err(QuantRS2Error::InvalidInput(format!(
250 "Circuit too large for SciRS2 numerical analysis: {} qubits (max: {})",
251 N, self.options.max_unitary_qubits
252 )));
253 }
254
255 let unitary1 = self.get_circuit_unitary(circuit1)?;
257 let unitary2 = self.get_circuit_unitary(circuit2)?;
258
259 let numerical_analysis = self.perform_scirs2_numerical_analysis(&unitary1, &unitary2)?;
261
262 let adaptive_tolerance = self.calculate_adaptive_tolerance::<N>(N, &numerical_analysis);
264
265 let (equivalent, max_diff, confidence_score, error_bounds) =
267 self.scirs2_unitaries_equal(&unitary1, &unitary2, adaptive_tolerance)?;
268
269 let statistical_significance = if self.options.enable_statistical_analysis {
271 Some(self.calculate_statistical_significance(&unitary1, &unitary2, max_diff)?)
272 } else {
273 None
274 };
275
276 Ok(EquivalenceResult {
277 equivalent,
278 check_type: EquivalenceType::SciRS2NumericalEquivalence,
279 max_difference: Some(max_diff),
280 details: format!(
281 "SciRS2 numerical analysis: tolerance={:.2e}, confidence={:.3}, condition_number={:.2e}",
282 adaptive_tolerance,
283 confidence_score,
284 numerical_analysis.condition_number.unwrap_or(0.0)
285 ),
286 numerical_analysis: Some(numerical_analysis),
287 confidence_score,
288 statistical_significance,
289 error_bounds: Some(error_bounds),
290 })
291 }
292
293 pub fn check_scirs2_graph_equivalence<const N: usize>(
295 &mut self,
296 circuit1: &Circuit<N>,
297 circuit2: &Circuit<N>,
298 ) -> QuantRS2Result<EquivalenceResult> {
299 let analyzer = self.scirs2_analyzer.as_mut().ok_or_else(|| {
300 QuantRS2Error::InvalidInput("SciRS2 analyzer not initialized".to_string())
301 })?;
302
303 let graph1 = analyzer.circuit_to_scirs2_graph(circuit1)?;
305 let graph2 = analyzer.circuit_to_scirs2_graph(circuit2)?;
306
307 let (equivalent, similarity_score, graph_details) =
309 self.compare_scirs2_graphs(&graph1, &graph2)?;
310
311 Ok(EquivalenceResult {
312 equivalent,
313 check_type: EquivalenceType::SciRS2GraphEquivalence,
314 max_difference: Some(1.0 - similarity_score),
315 details: graph_details,
316 numerical_analysis: None,
317 confidence_score: similarity_score,
318 statistical_significance: None,
319 error_bounds: None,
320 })
321 }
322
323 pub fn check_structural_equivalence<const N: usize>(
325 &self,
326 circuit1: &Circuit<N>,
327 circuit2: &Circuit<N>,
328 ) -> QuantRS2Result<EquivalenceResult> {
329 if circuit1.num_gates() != circuit2.num_gates() {
330 return Ok(EquivalenceResult {
331 equivalent: false,
332 check_type: EquivalenceType::StructuralEquivalence,
333 max_difference: None,
334 details: format!(
335 "Different number of gates: {} vs {}",
336 circuit1.num_gates(),
337 circuit2.num_gates()
338 ),
339 numerical_analysis: None,
340 confidence_score: 0.0,
341 statistical_significance: None,
342 error_bounds: None,
343 });
344 }
345
346 let gates1 = circuit1.gates();
347 let gates2 = circuit2.gates();
348
349 for (i, (gate1, gate2)) in gates1.iter().zip(gates2.iter()).enumerate() {
350 if !self.gates_equal(gate1.as_ref(), gate2.as_ref()) {
351 return Ok(EquivalenceResult {
352 equivalent: false,
353 check_type: EquivalenceType::StructuralEquivalence,
354 max_difference: None,
355 details: format!(
356 "Gates differ at position {}: {} vs {}",
357 i,
358 gate1.name(),
359 gate2.name()
360 ),
361 numerical_analysis: None,
362 confidence_score: 0.0,
363 statistical_significance: None,
364 error_bounds: None,
365 });
366 }
367 }
368
369 Ok(EquivalenceResult {
370 equivalent: true,
371 check_type: EquivalenceType::StructuralEquivalence,
372 max_difference: Some(0.0),
373 details: "Circuits are structurally identical".to_string(),
374 numerical_analysis: None,
375 confidence_score: 1.0,
376 statistical_significance: None,
377 error_bounds: None,
378 })
379 }
380
381 fn gates_equal(&self, gate1: &dyn GateOp, gate2: &dyn GateOp) -> bool {
386 if gate1.name() != gate2.name() {
388 return false;
389 }
390
391 let qubits1 = gate1.qubits();
393 let qubits2 = gate2.qubits();
394
395 if qubits1.len() != qubits2.len() {
396 return false;
397 }
398
399 for (q1, q2) in qubits1.iter().zip(qubits2.iter()) {
400 if q1 != q2 {
401 return false;
402 }
403 }
404
405 if !self.check_gate_parameters(gate1, gate2) {
407 return false;
408 }
409
410 true
411 }
412
413 fn check_gate_parameters(&self, gate1: &dyn GateOp, gate2: &dyn GateOp) -> bool {
415 if let Some(rx1) = gate1.as_any().downcast_ref::<RotationX>() {
418 if let Some(rx2) = gate2.as_any().downcast_ref::<RotationX>() {
419 return (rx1.theta - rx2.theta).abs() < self.options.tolerance;
420 }
421 }
422
423 if let Some(ry1) = gate1.as_any().downcast_ref::<RotationY>() {
424 if let Some(ry2) = gate2.as_any().downcast_ref::<RotationY>() {
425 return (ry1.theta - ry2.theta).abs() < self.options.tolerance;
426 }
427 }
428
429 if let Some(rz1) = gate1.as_any().downcast_ref::<RotationZ>() {
430 if let Some(rz2) = gate2.as_any().downcast_ref::<RotationZ>() {
431 return (rz1.theta - rz2.theta).abs() < self.options.tolerance;
432 }
433 }
434
435 if let Some(crx1) = gate1.as_any().downcast_ref::<CRX>() {
437 if let Some(crx2) = gate2.as_any().downcast_ref::<CRX>() {
438 return (crx1.theta - crx2.theta).abs() < self.options.tolerance;
439 }
440 }
441
442 if let Some(cry1) = gate1.as_any().downcast_ref::<CRY>() {
443 if let Some(cry2) = gate2.as_any().downcast_ref::<CRY>() {
444 return (cry1.theta - cry2.theta).abs() < self.options.tolerance;
445 }
446 }
447
448 if let Some(crz1) = gate1.as_any().downcast_ref::<CRZ>() {
449 if let Some(crz2) = gate2.as_any().downcast_ref::<CRZ>() {
450 return (crz1.theta - crz2.theta).abs() < self.options.tolerance;
451 }
452 }
453
454 true
457 }
458
459 pub fn check_unitary_equivalence<const N: usize>(
461 &self,
462 circuit1: &Circuit<N>,
463 circuit2: &Circuit<N>,
464 ) -> QuantRS2Result<EquivalenceResult> {
465 if N > self.options.max_unitary_qubits {
466 return Err(QuantRS2Error::InvalidInput(format!(
467 "Circuit too large for unitary construction: {} qubits (max: {})",
468 N, self.options.max_unitary_qubits
469 )));
470 }
471
472 let unitary1 = self.get_circuit_unitary(circuit1)?;
474 let unitary2 = self.get_circuit_unitary(circuit2)?;
475
476 let (equivalent, max_diff) = if self.options.ignore_global_phase {
478 self.unitaries_equal_up_to_phase(&unitary1, &unitary2)
479 } else {
480 self.unitaries_equal(&unitary1, &unitary2)
481 };
482
483 Ok(EquivalenceResult {
484 equivalent,
485 check_type: if self.options.ignore_global_phase {
486 EquivalenceType::GlobalPhaseEquivalence
487 } else {
488 EquivalenceType::UnitaryEquivalence
489 },
490 max_difference: Some(max_diff),
491 details: if equivalent {
492 "Unitaries are equivalent".to_string()
493 } else {
494 format!("Maximum unitary difference: {max_diff:.2e}")
495 },
496 numerical_analysis: None,
497 confidence_score: if equivalent {
498 1.0 - (max_diff / self.options.tolerance)
499 } else {
500 0.0
501 },
502 statistical_significance: None,
503 error_bounds: None,
504 })
505 }
506
507 fn get_circuit_unitary<const N: usize>(
509 &self,
510 circuit: &Circuit<N>,
511 ) -> QuantRS2Result<Array2<Complex64>> {
512 let dim = 1 << N;
513 let mut unitary = Array2::eye(dim);
514
515 for gate in circuit.gates() {
517 self.apply_gate_to_unitary(&mut unitary, gate.as_ref(), N)?;
518 }
519
520 Ok(unitary)
521 }
522
523 fn apply_gate_to_unitary(
525 &self,
526 unitary: &mut Array2<Complex64>,
527 gate: &dyn GateOp,
528 num_qubits: usize,
529 ) -> QuantRS2Result<()> {
530 let gate_matrix = self.get_gate_matrix(gate)?;
531 let qubits = gate.qubits();
532
533 match qubits.len() {
535 1 => {
536 let qubit_idx = qubits[0].id() as usize;
537 self.apply_single_qubit_gate(unitary, &gate_matrix, qubit_idx, num_qubits)?;
538 }
539 2 => {
540 let control_idx = qubits[0].id() as usize;
541 let target_idx = qubits[1].id() as usize;
542 self.apply_two_qubit_gate(
543 unitary,
544 &gate_matrix,
545 control_idx,
546 target_idx,
547 num_qubits,
548 )?;
549 }
550 _ => {
551 return Err(QuantRS2Error::UnsupportedOperation(format!(
552 "Gates with {} qubits not yet supported",
553 qubits.len()
554 )));
555 }
556 }
557
558 Ok(())
559 }
560
561 fn get_gate_matrix(&self, gate: &dyn GateOp) -> QuantRS2Result<Array2<Complex64>> {
563 let c0 = Complex64::new(0.0, 0.0);
564 let c1 = Complex64::new(1.0, 0.0);
565 let ci = Complex64::new(0.0, 1.0);
566
567 match gate.name() {
568 "H" => {
569 let sqrt2_inv = 1.0 / std::f64::consts::SQRT_2;
570 Ok(array![
571 [c1 * sqrt2_inv, c1 * sqrt2_inv],
572 [c1 * sqrt2_inv, -c1 * sqrt2_inv]
573 ])
574 }
575 "X" => Ok(array![[c0, c1], [c1, c0]]),
576 "Y" => Ok(array![[c0, -ci], [ci, c0]]),
577 "Z" => Ok(array![[c1, c0], [c0, -c1]]),
578 "S" => Ok(array![[c1, c0], [c0, ci]]),
579 "T" => Ok(array![
580 [c1, c0],
581 [
582 c0,
583 Complex64::new(
584 1.0 / std::f64::consts::SQRT_2,
585 1.0 / std::f64::consts::SQRT_2
586 )
587 ]
588 ]),
589 "CNOT" | "CX" => Ok(array![
590 [c1, c0, c0, c0],
591 [c0, c1, c0, c0],
592 [c0, c0, c0, c1],
593 [c0, c0, c1, c0]
594 ]),
595 "CZ" => Ok(array![
596 [c1, c0, c0, c0],
597 [c0, c1, c0, c0],
598 [c0, c0, c1, c0],
599 [c0, c0, c0, -c1]
600 ]),
601 "SWAP" => Ok(array![
602 [c1, c0, c0, c0],
603 [c0, c0, c1, c0],
604 [c0, c1, c0, c0],
605 [c0, c0, c0, c1]
606 ]),
607 _ => Err(QuantRS2Error::UnsupportedOperation(format!(
608 "Gate '{}' matrix not yet implemented",
609 gate.name()
610 ))),
611 }
612 }
613
614 fn apply_single_qubit_gate(
616 &self,
617 unitary: &mut Array2<Complex64>,
618 gate_matrix: &Array2<Complex64>,
619 qubit_idx: usize,
620 num_qubits: usize,
621 ) -> QuantRS2Result<()> {
622 let dim = 1 << num_qubits;
623 let mut new_unitary = Array2::zeros((dim, dim));
624
625 for col in 0..dim {
627 for row in 0..dim {
628 let mut sum = Complex64::new(0.0, 0.0);
629
630 let row_bit = (row >> qubit_idx) & 1;
632 let col_bit = (col >> qubit_idx) & 1;
633
634 for k in 0..dim {
635 let k_bit = (k >> qubit_idx) & 1;
636
637 if (row ^ k) == ((row_bit ^ k_bit) << qubit_idx) {
639 sum += gate_matrix[[row_bit, k_bit]] * unitary[[k, col]];
640 }
641 }
642
643 new_unitary[[row, col]] = sum;
644 }
645 }
646
647 *unitary = new_unitary;
648 Ok(())
649 }
650
651 fn apply_two_qubit_gate(
653 &self,
654 unitary: &mut Array2<Complex64>,
655 gate_matrix: &Array2<Complex64>,
656 qubit1_idx: usize,
657 qubit2_idx: usize,
658 num_qubits: usize,
659 ) -> QuantRS2Result<()> {
660 let dim = 1 << num_qubits;
661 let mut new_unitary = Array2::zeros((dim, dim));
662
663 for col in 0..dim {
664 for row in 0..dim {
665 let mut sum = Complex64::new(0.0, 0.0);
666
667 let row_q1 = (row >> qubit1_idx) & 1;
669 let row_q2 = (row >> qubit2_idx) & 1;
670 let row_gate_idx = (row_q1 << 1) | row_q2;
671
672 let col_q1 = (col >> qubit1_idx) & 1;
673 let col_q2 = (col >> qubit2_idx) & 1;
674
675 for k in 0..dim {
676 let k_q1 = (k >> qubit1_idx) & 1;
677 let k_q2 = (k >> qubit2_idx) & 1;
678 let k_gate_idx = (k_q1 << 1) | k_q2;
679
680 let diff = row ^ k;
682 let expected_diff =
683 ((row_q1 ^ k_q1) << qubit1_idx) | ((row_q2 ^ k_q2) << qubit2_idx);
684
685 if diff == expected_diff {
686 sum += gate_matrix[[row_gate_idx, k_gate_idx]] * unitary[[k, col]];
687 }
688 }
689
690 new_unitary[[row, col]] = sum;
691 }
692 }
693
694 *unitary = new_unitary;
695 Ok(())
696 }
697
698 fn unitaries_equal(&self, u1: &Array2<Complex64>, u2: &Array2<Complex64>) -> (bool, f64) {
700 if u1.shape() != u2.shape() {
701 return (false, f64::INFINITY);
702 }
703
704 let mut max_diff = 0.0;
705 for (a, b) in u1.iter().zip(u2.iter()) {
706 let diff = (a - b).norm();
707 if diff > max_diff {
708 max_diff = diff;
709 }
710 if diff > self.options.tolerance {
711 return (false, max_diff);
712 }
713 }
714
715 (true, max_diff)
716 }
717
718 fn unitaries_equal_up_to_phase(
720 &self,
721 u1: &Array2<Complex64>,
722 u2: &Array2<Complex64>,
723 ) -> (bool, f64) {
724 if u1.shape() != u2.shape() {
725 return (false, f64::INFINITY);
726 }
727
728 let mut phase = None;
730 for (a, b) in u1.iter().zip(u2.iter()) {
731 if a.norm() > self.options.tolerance && b.norm() > self.options.tolerance {
732 phase = Some(b / a);
733 break;
734 }
735 }
736
737 let phase = match phase {
738 Some(p) => p,
739 None => return (false, f64::INFINITY),
740 };
741
742 let mut max_diff = 0.0;
744 for (a, b) in u1.iter().zip(u2.iter()) {
745 let adjusted = a * phase;
746 let diff = (adjusted - b).norm();
747 if diff > max_diff {
748 max_diff = diff;
749 }
750 if diff > self.options.tolerance {
751 return (false, max_diff);
752 }
753 }
754
755 (true, max_diff)
756 }
757
758 pub fn check_state_vector_equivalence<const N: usize>(
760 &self,
761 circuit1: &Circuit<N>,
762 circuit2: &Circuit<N>,
763 ) -> QuantRS2Result<EquivalenceResult> {
764 let mut max_diff = 0.0;
765 let num_states = if self.options.check_all_states {
766 1 << N
767 } else {
768 std::cmp::min(1 << N, 100)
770 };
771
772 for state_idx in 0..num_states {
773 let state1 = self.apply_circuit_to_state(circuit1, state_idx, N)?;
774 let state2 = self.apply_circuit_to_state(circuit2, state_idx, N)?;
775
776 let (equal, diff) = if self.options.ignore_global_phase {
777 self.states_equal_up_to_phase(&state1, &state2)
778 } else {
779 self.states_equal(&state1, &state2)
780 };
781
782 if diff > max_diff {
783 max_diff = diff;
784 }
785
786 if !equal {
787 return Ok(EquivalenceResult {
788 equivalent: false,
789 check_type: EquivalenceType::StateVectorEquivalence,
790 max_difference: Some(max_diff),
791 details: format!(
792 "States differ for input |{state_idx:0b}>: max difference {max_diff:.2e}"
793 ),
794 numerical_analysis: None,
795 confidence_score: 0.0,
796 statistical_significance: None,
797 error_bounds: None,
798 });
799 }
800 }
801
802 Ok(EquivalenceResult {
803 equivalent: true,
804 check_type: EquivalenceType::StateVectorEquivalence,
805 max_difference: Some(max_diff),
806 details: format!("Checked {num_states} computational basis states"),
807 numerical_analysis: None,
808 confidence_score: 1.0 - (max_diff / self.options.tolerance).min(1.0),
809 statistical_significance: None,
810 error_bounds: None,
811 })
812 }
813
814 fn apply_circuit_to_state<const N: usize>(
816 &self,
817 circuit: &Circuit<N>,
818 state_idx: usize,
819 num_qubits: usize,
820 ) -> QuantRS2Result<Vec<Complex64>> {
821 let dim = 1 << num_qubits;
822 let mut state = vec![Complex64::new(0.0, 0.0); dim];
823 state[state_idx] = Complex64::new(1.0, 0.0);
824
825 for gate in circuit.gates() {
827 self.apply_gate_to_state(&mut state, gate.as_ref(), num_qubits)?;
828 }
829
830 Ok(state)
831 }
832
833 fn apply_gate_to_state(
835 &self,
836 state: &mut Vec<Complex64>,
837 gate: &dyn GateOp,
838 num_qubits: usize,
839 ) -> QuantRS2Result<()> {
840 let gate_matrix = self.get_gate_matrix(gate)?;
841 let qubits = gate.qubits();
842
843 match qubits.len() {
844 1 => {
845 let qubit_idx = qubits[0].id() as usize;
846 self.apply_single_qubit_gate_to_state(state, &gate_matrix, qubit_idx, num_qubits)?;
847 }
848 2 => {
849 let control_idx = qubits[0].id() as usize;
850 let target_idx = qubits[1].id() as usize;
851 self.apply_two_qubit_gate_to_state(
852 state,
853 &gate_matrix,
854 control_idx,
855 target_idx,
856 num_qubits,
857 )?;
858 }
859 _ => {
860 return Err(QuantRS2Error::UnsupportedOperation(format!(
861 "Gates with {} qubits not yet supported",
862 qubits.len()
863 )));
864 }
865 }
866
867 Ok(())
868 }
869
870 fn apply_single_qubit_gate_to_state(
872 &self,
873 state: &mut Vec<Complex64>,
874 gate_matrix: &Array2<Complex64>,
875 qubit_idx: usize,
876 num_qubits: usize,
877 ) -> QuantRS2Result<()> {
878 let dim = 1 << num_qubits;
879 let mut new_state = vec![Complex64::new(0.0, 0.0); dim];
880
881 for i in 0..dim {
882 let bit = (i >> qubit_idx) & 1;
883
884 for j in 0..2 {
885 let other_idx = i ^ ((bit ^ j) << qubit_idx);
886 new_state[i] += gate_matrix[[bit, j]] * state[other_idx];
887 }
888 }
889
890 *state = new_state;
891 Ok(())
892 }
893
894 fn apply_two_qubit_gate_to_state(
896 &self,
897 state: &mut Vec<Complex64>,
898 gate_matrix: &Array2<Complex64>,
899 qubit1_idx: usize,
900 qubit2_idx: usize,
901 num_qubits: usize,
902 ) -> QuantRS2Result<()> {
903 let dim = 1 << num_qubits;
904 let mut new_state = vec![Complex64::new(0.0, 0.0); dim];
905
906 for i in 0..dim {
907 let bit1 = (i >> qubit1_idx) & 1;
908 let bit2 = (i >> qubit2_idx) & 1;
909 let gate_row = (bit1 << 1) | bit2;
910
911 for gate_col in 0..4 {
912 let new_bit1 = (gate_col >> 1) & 1;
913 let new_bit2 = gate_col & 1;
914
915 let j = i ^ ((bit1 ^ new_bit1) << qubit1_idx) ^ ((bit2 ^ new_bit2) << qubit2_idx);
916 new_state[i] += gate_matrix[[gate_row, gate_col]] * state[j];
917 }
918 }
919
920 *state = new_state;
921 Ok(())
922 }
923
924 fn states_equal(&self, s1: &[Complex64], s2: &[Complex64]) -> (bool, f64) {
926 if s1.len() != s2.len() {
927 return (false, f64::INFINITY);
928 }
929
930 let mut max_diff = 0.0;
931 for (a, b) in s1.iter().zip(s2.iter()) {
932 let diff = (a - b).norm();
933 if diff > max_diff {
934 max_diff = diff;
935 }
936 if diff > self.options.tolerance {
937 return (false, max_diff);
938 }
939 }
940
941 (true, max_diff)
942 }
943
944 fn states_equal_up_to_phase(&self, s1: &[Complex64], s2: &[Complex64]) -> (bool, f64) {
946 if s1.len() != s2.len() {
947 return (false, f64::INFINITY);
948 }
949
950 let mut phase = None;
952 for (a, b) in s1.iter().zip(s2.iter()) {
953 if a.norm() > self.options.tolerance && b.norm() > self.options.tolerance {
954 phase = Some(b / a);
955 break;
956 }
957 }
958
959 let phase = match phase {
960 Some(p) => p,
961 None => return (false, f64::INFINITY),
962 };
963
964 let mut max_diff = 0.0;
966 for (a, b) in s1.iter().zip(s2.iter()) {
967 let adjusted = a * phase;
968 let diff = (adjusted - b).norm();
969 if diff > max_diff {
970 max_diff = diff;
971 }
972 if diff > self.options.tolerance {
973 return (false, max_diff);
974 }
975 }
976
977 (true, max_diff)
978 }
979
980 pub fn check_probabilistic_equivalence<const N: usize>(
982 &self,
983 circuit1: &Circuit<N>,
984 circuit2: &Circuit<N>,
985 ) -> QuantRS2Result<EquivalenceResult> {
986 let mut max_diff = 0.0;
988
989 for state_idx in 0..(1 << N) {
990 let probs1 = self.get_measurement_probabilities(circuit1, state_idx, N)?;
991 let probs2 = self.get_measurement_probabilities(circuit2, state_idx, N)?;
992
993 for (p1, p2) in probs1.iter().zip(probs2.iter()) {
994 let diff = (p1 - p2).abs();
995 if diff > max_diff {
996 max_diff = diff;
997 }
998 if diff > self.options.tolerance {
999 return Ok(EquivalenceResult {
1000 equivalent: false,
1001 check_type: EquivalenceType::ProbabilisticEquivalence,
1002 max_difference: Some(max_diff),
1003 details: format!(
1004 "Measurement probabilities differ for input |{state_idx:0b}>"
1005 ),
1006 numerical_analysis: None,
1007 confidence_score: 0.0,
1008 statistical_significance: None,
1009 error_bounds: None,
1010 });
1011 }
1012 }
1013 }
1014
1015 Ok(EquivalenceResult {
1016 equivalent: true,
1017 check_type: EquivalenceType::ProbabilisticEquivalence,
1018 max_difference: Some(max_diff),
1019 details: "Measurement probabilities match for all inputs".to_string(),
1020 numerical_analysis: None,
1021 confidence_score: 1.0 - (max_diff / self.options.tolerance).min(1.0),
1022 statistical_significance: None,
1023 error_bounds: None,
1024 })
1025 }
1026
1027 fn get_measurement_probabilities<const N: usize>(
1029 &self,
1030 circuit: &Circuit<N>,
1031 state_idx: usize,
1032 num_qubits: usize,
1033 ) -> QuantRS2Result<Vec<f64>> {
1034 let final_state = self.apply_circuit_to_state(circuit, state_idx, num_qubits)?;
1036
1037 let probs: Vec<f64> = final_state
1039 .iter()
1040 .map(scirs2_core::Complex::norm_sqr)
1041 .collect();
1042
1043 Ok(probs)
1044 }
1045
1046 fn perform_scirs2_numerical_analysis(
1050 &self,
1051 unitary1: &Array2<Complex64>,
1052 unitary2: &Array2<Complex64>,
1053 ) -> QuantRS2Result<NumericalAnalysis> {
1054 let diff_matrix = unitary1 - unitary2;
1056
1057 let frobenius_norm = diff_matrix
1059 .iter()
1060 .map(scirs2_core::Complex::norm_sqr)
1061 .sum::<f64>()
1062 .sqrt();
1063
1064 let condition_number = if self.options.enable_stability_analysis {
1066 Some(self.estimate_condition_number(unitary1)?)
1067 } else {
1068 None
1069 };
1070
1071 let spectral_norm = if self.options.enable_stability_analysis {
1073 Some(self.calculate_spectral_norm(&diff_matrix)?)
1074 } else {
1075 None
1076 };
1077
1078 let numerical_rank = self.estimate_numerical_rank(&diff_matrix);
1080
1081 let stability_indicator = if let Some(cond_num) = condition_number {
1083 1.0 / (1.0 + (cond_num / self.options.max_condition_number).log10())
1084 } else {
1085 1.0
1086 };
1087
1088 let adaptive_tolerance = self.calculate_adaptive_tolerance_internal(
1090 unitary1.nrows(),
1091 frobenius_norm,
1092 condition_number.unwrap_or(1.0),
1093 );
1094
1095 Ok(NumericalAnalysis {
1096 condition_number,
1097 numerical_rank: Some(numerical_rank),
1098 frobenius_norm,
1099 spectral_norm,
1100 adaptive_tolerance,
1101 stability_indicator,
1102 })
1103 }
1104
1105 fn calculate_adaptive_tolerance<const N: usize>(
1107 &self,
1108 num_qubits: usize,
1109 analysis: &NumericalAnalysis,
1110 ) -> f64 {
1111 let base_tolerance = if self.options.enable_adaptive_tolerance {
1112 SCIRS2_DEFAULT_TOLERANCE
1113 } else {
1114 self.options.tolerance
1115 };
1116
1117 let size_factor = (num_qubits as f64).powf(1.5).mul_add(1e-15, 1.0);
1119
1120 let condition_factor = if let Some(cond_num) = analysis.condition_number {
1122 (cond_num / 1e12).log10().max(0.0).mul_add(1e-2, 1.0)
1123 } else {
1124 1.0
1125 };
1126
1127 let norm_factor = analysis.frobenius_norm.mul_add(1e-3, 1.0);
1129
1130 base_tolerance * size_factor * condition_factor * norm_factor
1131 }
1132
1133 fn calculate_adaptive_tolerance_internal(
1135 &self,
1136 matrix_size: usize,
1137 frobenius_norm: f64,
1138 condition_number: f64,
1139 ) -> f64 {
1140 let base_tolerance = SCIRS2_DEFAULT_TOLERANCE;
1141 let size_factor = (matrix_size as f64).sqrt().mul_add(1e-15, 1.0);
1142 let condition_factor = (condition_number / 1e12)
1143 .log10()
1144 .max(0.0)
1145 .mul_add(1e-2, 1.0);
1146 let norm_factor = frobenius_norm.mul_add(1e-3, 1.0);
1147
1148 base_tolerance * size_factor * condition_factor * norm_factor
1149 }
1150
1151 fn scirs2_unitaries_equal(
1153 &self,
1154 u1: &Array2<Complex64>,
1155 u2: &Array2<Complex64>,
1156 adaptive_tolerance: f64,
1157 ) -> QuantRS2Result<(bool, f64, f64, ErrorBounds)> {
1158 if u1.shape() != u2.shape() {
1159 return Ok((
1160 false,
1161 f64::INFINITY,
1162 0.0,
1163 ErrorBounds {
1164 lower_bound: f64::INFINITY,
1165 upper_bound: f64::INFINITY,
1166 confidence_level: 0.0,
1167 standard_deviation: None,
1168 },
1169 ));
1170 }
1171
1172 let mut max_diff = 0.0;
1173 let mut differences = Vec::new();
1174
1175 for (a, b) in u1.iter().zip(u2.iter()) {
1177 let diff = if self.options.ignore_global_phase {
1178 let phase = if a.norm() > adaptive_tolerance && b.norm() > adaptive_tolerance {
1180 b / a
1181 } else {
1182 Complex64::new(1.0, 0.0)
1183 };
1184 (a * phase - b).norm()
1185 } else {
1186 (a - b).norm()
1187 };
1188
1189 differences.push(diff);
1190 if diff > max_diff {
1191 max_diff = diff;
1192 }
1193 }
1194
1195 let mean_diff = differences.iter().sum::<f64>() / differences.len() as f64;
1197 let variance = differences
1198 .iter()
1199 .map(|d| (d - mean_diff).powi(2))
1200 .sum::<f64>()
1201 / differences.len() as f64;
1202 let std_dev = variance.sqrt();
1203
1204 let confidence_score = if max_diff <= adaptive_tolerance {
1206 1.0 - (max_diff / adaptive_tolerance).min(1.0)
1207 } else {
1208 0.0
1209 };
1210
1211 let error_bounds = ErrorBounds {
1213 lower_bound: 2.0f64.mul_add(-std_dev, mean_diff).max(0.0),
1214 upper_bound: 2.0f64.mul_add(std_dev, mean_diff),
1215 confidence_level: self.options.confidence_level,
1216 standard_deviation: Some(std_dev),
1217 };
1218
1219 let equivalent = max_diff <= adaptive_tolerance;
1220
1221 Ok((equivalent, max_diff, confidence_score, error_bounds))
1222 }
1223
1224 fn compare_scirs2_graphs(
1226 &self,
1227 graph1: &crate::scirs2_integration::SciRS2CircuitGraph,
1228 graph2: &crate::scirs2_integration::SciRS2CircuitGraph,
1229 ) -> QuantRS2Result<(bool, f64, String)> {
1230 if graph1.nodes.len() != graph2.nodes.len() {
1232 return Ok((
1233 false,
1234 0.0,
1235 format!(
1236 "Different number of nodes: {} vs {}",
1237 graph1.nodes.len(),
1238 graph2.nodes.len()
1239 ),
1240 ));
1241 }
1242
1243 if graph1.edges.len() != graph2.edges.len() {
1244 return Ok((
1245 false,
1246 0.0,
1247 format!(
1248 "Different number of edges: {} vs {}",
1249 graph1.edges.len(),
1250 graph2.edges.len()
1251 ),
1252 ));
1253 }
1254
1255 let node_similarity = self.calculate_node_similarity(graph1, graph2);
1257 let edge_similarity = self.calculate_edge_similarity(graph1, graph2);
1258 let topology_similarity = self.calculate_topology_similarity(graph1, graph2);
1259
1260 let overall_similarity = (node_similarity + edge_similarity + topology_similarity) / 3.0;
1262
1263 let equivalent = overall_similarity > 0.95; let details = format!(
1266 "Graph similarity analysis: nodes={node_similarity:.3}, edges={edge_similarity:.3}, topology={topology_similarity:.3}, overall={overall_similarity:.3}"
1267 );
1268
1269 Ok((equivalent, overall_similarity, details))
1270 }
1271
1272 fn calculate_node_similarity(
1274 &self,
1275 graph1: &crate::scirs2_integration::SciRS2CircuitGraph,
1276 graph2: &crate::scirs2_integration::SciRS2CircuitGraph,
1277 ) -> f64 {
1278 if graph1.nodes.is_empty() && graph2.nodes.is_empty() {
1279 return 1.0;
1280 }
1281
1282 let total_nodes = graph1.nodes.len().max(graph2.nodes.len());
1283 let mut matching_nodes = 0;
1284
1285 for node1 in graph1.nodes.values() {
1287 for node2 in graph2.nodes.values() {
1288 if node1.node_type == node2.node_type {
1289 matching_nodes += 1;
1290 break;
1291 }
1292 }
1293 }
1294
1295 f64::from(matching_nodes) / total_nodes as f64
1296 }
1297
1298 fn calculate_edge_similarity(
1300 &self,
1301 graph1: &crate::scirs2_integration::SciRS2CircuitGraph,
1302 graph2: &crate::scirs2_integration::SciRS2CircuitGraph,
1303 ) -> f64 {
1304 if graph1.edges.is_empty() && graph2.edges.is_empty() {
1305 return 1.0;
1306 }
1307
1308 let total_edges = graph1.edges.len().max(graph2.edges.len());
1309 let mut matching_edges = 0;
1310
1311 for edge1 in graph1.edges.values() {
1313 for edge2 in graph2.edges.values() {
1314 if edge1.edge_type == edge2.edge_type {
1315 matching_edges += 1;
1316 break;
1317 }
1318 }
1319 }
1320
1321 f64::from(matching_edges) / total_edges as f64
1322 }
1323
1324 fn calculate_topology_similarity(
1326 &self,
1327 graph1: &crate::scirs2_integration::SciRS2CircuitGraph,
1328 graph2: &crate::scirs2_integration::SciRS2CircuitGraph,
1329 ) -> f64 {
1330 if graph1.adjacency_matrix.len() != graph2.adjacency_matrix.len() {
1331 return 0.0;
1332 }
1333
1334 let mut total_elements = 0;
1335 let mut matching_elements = 0;
1336
1337 for (row1, row2) in graph1
1338 .adjacency_matrix
1339 .iter()
1340 .zip(graph2.adjacency_matrix.iter())
1341 {
1342 if row1.len() != row2.len() {
1343 return 0.0;
1344 }
1345
1346 for (elem1, elem2) in row1.iter().zip(row2.iter()) {
1347 total_elements += 1;
1348 if elem1 == elem2 {
1349 matching_elements += 1;
1350 }
1351 }
1352 }
1353
1354 if total_elements == 0 {
1355 1.0
1356 } else {
1357 f64::from(matching_elements) / f64::from(total_elements)
1358 }
1359 }
1360
1361 fn estimate_condition_number(&self, matrix: &Array2<Complex64>) -> QuantRS2Result<f64> {
1363 let n = matrix.nrows();
1366 if n == 0 {
1367 return Ok(1.0);
1368 }
1369
1370 let mut v = vec![Complex64::new(1.0, 0.0); n];
1372 for _ in 0..10 {
1373 let mut new_v = vec![Complex64::new(0.0, 0.0); n];
1375 for i in 0..n {
1376 for j in 0..n {
1377 for k in 0..n {
1378 new_v[i] += matrix[[k, i]].conj() * matrix[[k, j]] * v[j];
1379 }
1380 }
1381 }
1382
1383 let norm = new_v
1385 .iter()
1386 .map(scirs2_core::Complex::norm_sqr)
1387 .sum::<f64>()
1388 .sqrt();
1389 if norm > 0.0 {
1390 for x in &mut new_v {
1391 *x /= norm;
1392 }
1393 }
1394 v = new_v;
1395 }
1396
1397 let estimated_largest_sv = v.iter().map(|x| x.norm()).sum::<f64>() / n as f64;
1399 let estimated_smallest_sv = 1.0 / estimated_largest_sv; Ok((estimated_largest_sv / estimated_smallest_sv.max(1e-16)).min(1e16))
1402 }
1403
1404 fn calculate_spectral_norm(&self, matrix: &Array2<Complex64>) -> QuantRS2Result<f64> {
1406 Ok(matrix.iter().map(|x| x.norm()).fold(0.0, f64::max))
1409 }
1410
1411 fn estimate_numerical_rank(&self, matrix: &Array2<Complex64>) -> usize {
1413 let tolerance = self.options.tolerance;
1414 let mut rank = 0;
1415
1416 for row in matrix.rows() {
1417 let row_norm = row
1418 .iter()
1419 .map(scirs2_core::Complex::norm_sqr)
1420 .sum::<f64>()
1421 .sqrt();
1422 if row_norm > tolerance {
1423 rank += 1;
1424 }
1425 }
1426
1427 rank
1428 }
1429
1430 fn calculate_statistical_significance(
1432 &self,
1433 u1: &Array2<Complex64>,
1434 u2: &Array2<Complex64>,
1435 max_difference: f64,
1436 ) -> QuantRS2Result<f64> {
1437 let n = u1.len();
1440 let degrees_of_freedom = n - 1;
1441
1442 let differences: Vec<f64> = u1
1444 .iter()
1445 .zip(u2.iter())
1446 .map(|(a, b)| (a - b).norm())
1447 .collect();
1448
1449 let mean_diff = differences.iter().sum::<f64>() / n as f64;
1450 let variance = differences
1451 .iter()
1452 .map(|d| (d - mean_diff).powi(2))
1453 .sum::<f64>()
1454 / degrees_of_freedom as f64;
1455 let std_error = (variance / n as f64).sqrt();
1456
1457 let t_stat = if std_error > 0.0 {
1458 mean_diff / std_error
1459 } else {
1460 0.0
1461 };
1462
1463 let p_value = 2.0 * (1.0 - (t_stat.abs() / (1.0 + t_stat.abs())));
1465
1466 Ok(p_value.clamp(0.0, 1.0))
1467 }
1468}
1469
1470#[must_use]
1472pub fn circuits_structurally_equal<const N: usize>(
1473 circuit1: &Circuit<N>,
1474 circuit2: &Circuit<N>,
1475) -> bool {
1476 let checker = EquivalenceChecker::default();
1477 checker
1478 .check_structural_equivalence(circuit1, circuit2)
1479 .map(|result| result.equivalent)
1480 .unwrap_or(false)
1481}
1482
1483pub fn circuits_equivalent<const N: usize>(
1485 circuit1: &Circuit<N>,
1486 circuit2: &Circuit<N>,
1487) -> QuantRS2Result<bool> {
1488 let mut checker = EquivalenceChecker::default();
1489 Ok(checker.check_equivalence(circuit1, circuit2)?.equivalent)
1490}
1491
1492pub fn circuits_scirs2_equivalent<const N: usize>(
1494 circuit1: &Circuit<N>,
1495 circuit2: &Circuit<N>,
1496 options: EquivalenceOptions,
1497) -> QuantRS2Result<EquivalenceResult> {
1498 let mut checker = EquivalenceChecker::new(options);
1499 checker.check_equivalence(circuit1, circuit2)
1500}
1501
1502pub fn circuits_scirs2_numerical_equivalent<const N: usize>(
1504 circuit1: &Circuit<N>,
1505 circuit2: &Circuit<N>,
1506) -> QuantRS2Result<EquivalenceResult> {
1507 let options = EquivalenceOptions {
1508 enable_adaptive_tolerance: true,
1509 enable_statistical_analysis: true,
1510 enable_stability_analysis: true,
1511 ..Default::default()
1512 };
1513
1514 let mut checker = EquivalenceChecker::new(options);
1515 checker.check_scirs2_numerical_equivalence(circuit1, circuit2)
1516}
1517
1518#[cfg(test)]
1519mod tests {
1520 use super::*;
1521 use quantrs2_core::gate::multi::CNOT;
1522 use quantrs2_core::gate::single::{Hadamard, PauliX, PauliZ};
1523
1524 #[test]
1525 fn test_structural_equivalence() {
1526 let mut circuit1 = Circuit::<2>::new();
1527 circuit1
1528 .add_gate(Hadamard { target: QubitId(0) })
1529 .expect("Failed to add Hadamard gate to circuit1");
1530 circuit1
1531 .add_gate(CNOT {
1532 control: QubitId(0),
1533 target: QubitId(1),
1534 })
1535 .expect("Failed to add CNOT gate to circuit1");
1536
1537 let mut circuit2 = Circuit::<2>::new();
1538 circuit2
1539 .add_gate(Hadamard { target: QubitId(0) })
1540 .expect("Failed to add Hadamard gate to circuit2");
1541 circuit2
1542 .add_gate(CNOT {
1543 control: QubitId(0),
1544 target: QubitId(1),
1545 })
1546 .expect("Failed to add CNOT gate to circuit2");
1547
1548 let checker = EquivalenceChecker::default();
1549 let result = checker
1550 .check_structural_equivalence(&circuit1, &circuit2)
1551 .expect("Structural equivalence check failed");
1552 assert!(result.equivalent);
1553 }
1554
1555 #[test]
1556 fn test_structural_non_equivalence() {
1557 let mut circuit1 = Circuit::<2>::new();
1558 circuit1
1559 .add_gate(Hadamard { target: QubitId(0) })
1560 .expect("Failed to add Hadamard gate to circuit1");
1561 circuit1
1562 .add_gate(CNOT {
1563 control: QubitId(0),
1564 target: QubitId(1),
1565 })
1566 .expect("Failed to add CNOT gate to circuit1");
1567
1568 let mut circuit2 = Circuit::<2>::new();
1569 circuit2
1570 .add_gate(PauliX { target: QubitId(0) })
1571 .expect("Failed to add PauliX gate to circuit2");
1572 circuit2
1573 .add_gate(CNOT {
1574 control: QubitId(0),
1575 target: QubitId(1),
1576 })
1577 .expect("Failed to add CNOT gate to circuit2");
1578
1579 let checker = EquivalenceChecker::default();
1580 let result = checker
1581 .check_structural_equivalence(&circuit1, &circuit2)
1582 .expect("Structural equivalence check failed");
1583 assert!(!result.equivalent);
1584 }
1585
1586 #[test]
1587 fn test_different_gate_count() {
1588 let mut circuit1 = Circuit::<2>::new();
1589 circuit1
1590 .add_gate(Hadamard { target: QubitId(0) })
1591 .expect("Failed to add Hadamard gate to circuit1");
1592
1593 let mut circuit2 = Circuit::<2>::new();
1594 circuit2
1595 .add_gate(Hadamard { target: QubitId(0) })
1596 .expect("Failed to add Hadamard gate to circuit2");
1597 circuit2
1598 .add_gate(PauliZ { target: QubitId(0) })
1599 .expect("Failed to add PauliZ gate to circuit2");
1600
1601 let checker = EquivalenceChecker::default();
1602 let result = checker
1603 .check_structural_equivalence(&circuit1, &circuit2)
1604 .expect("Structural equivalence check failed");
1605 assert!(!result.equivalent);
1606 assert!(result.details.contains("Different number of gates"));
1607 }
1608
1609 #[test]
1610 fn test_parametric_gate_equivalence_equal() {
1611 let mut circuit1 = Circuit::<1>::new();
1613 circuit1
1614 .add_gate(RotationX {
1615 target: QubitId(0),
1616 theta: std::f64::consts::PI / 4.0,
1617 })
1618 .expect("Failed to add RotationX gate to circuit1");
1619
1620 let mut circuit2 = Circuit::<1>::new();
1621 circuit2
1622 .add_gate(RotationX {
1623 target: QubitId(0),
1624 theta: std::f64::consts::PI / 4.0,
1625 })
1626 .expect("Failed to add RotationX gate to circuit2");
1627
1628 let checker = EquivalenceChecker::default();
1629 let result = checker
1630 .check_structural_equivalence(&circuit1, &circuit2)
1631 .expect("Structural equivalence check failed");
1632 assert!(result.equivalent);
1633 }
1634
1635 #[test]
1636 fn test_parametric_gate_equivalence_different_params() {
1637 let mut circuit1 = Circuit::<1>::new();
1639 circuit1
1640 .add_gate(RotationX {
1641 target: QubitId(0),
1642 theta: std::f64::consts::PI / 4.0,
1643 })
1644 .expect("Failed to add RotationX gate to circuit1");
1645
1646 let mut circuit2 = Circuit::<1>::new();
1647 circuit2
1648 .add_gate(RotationX {
1649 target: QubitId(0),
1650 theta: std::f64::consts::PI / 2.0,
1651 })
1652 .expect("Failed to add RotationX gate to circuit2");
1653
1654 let checker = EquivalenceChecker::default();
1655 let result = checker
1656 .check_structural_equivalence(&circuit1, &circuit2)
1657 .expect("Structural equivalence check failed");
1658 assert!(!result.equivalent);
1659 }
1660
1661 #[test]
1662 fn test_parametric_gate_numerical_tolerance() {
1663 let mut circuit1 = Circuit::<1>::new();
1665 circuit1
1666 .add_gate(RotationY {
1667 target: QubitId(0),
1668 theta: 1.0,
1669 })
1670 .expect("Failed to add RotationY gate to circuit1");
1671
1672 let mut circuit2 = Circuit::<1>::new();
1673 circuit2
1674 .add_gate(RotationY {
1675 target: QubitId(0),
1676 theta: 1.0 + 1e-12, })
1678 .expect("Failed to add RotationY gate to circuit2");
1679
1680 let checker = EquivalenceChecker::default();
1681 let result = checker
1682 .check_structural_equivalence(&circuit1, &circuit2)
1683 .expect("Structural equivalence check failed");
1684 assert!(result.equivalent);
1685 }
1686
1687 #[test]
1688 fn test_controlled_rotation_equivalence() {
1689 let mut circuit1 = Circuit::<2>::new();
1691 circuit1
1692 .add_gate(CRZ {
1693 control: QubitId(0),
1694 target: QubitId(1),
1695 theta: std::f64::consts::PI,
1696 })
1697 .expect("Failed to add CRZ gate to circuit1");
1698
1699 let mut circuit2 = Circuit::<2>::new();
1700 circuit2
1701 .add_gate(CRZ {
1702 control: QubitId(0),
1703 target: QubitId(1),
1704 theta: std::f64::consts::PI,
1705 })
1706 .expect("Failed to add CRZ gate to circuit2");
1707
1708 let checker = EquivalenceChecker::default();
1709 let result = checker
1710 .check_structural_equivalence(&circuit1, &circuit2)
1711 .expect("Structural equivalence check failed");
1712 assert!(result.equivalent);
1713 }
1714}