1use crate::circuit::error::{CircuitError, ParameterError};
21use crate::circuit::symbolic_matrix::matrix::simplify_matrix;
22use crate::circuit::symbolic_matrix::{
23 SymbolicMatrix, circuit_to_symbolic_matrix, evaluate_symbolic_matrix,
24 symbolic_matrices_equivalent,
25};
26use crate::circuit::{Circuit, Instruction, Parameter, ParameterValue, Qubit, StandardGate};
27use crate::compile::knowledge::rule::{Condition, Rule, RuleItem};
28use crate::compile::{NUMERIC_ZERO_TOLERANCE, UNIT_PHASE_NORM_TOLERANCE};
29use ndarray::parallel::prelude::*;
30use num_complex::Complex64;
31use rand::Rng;
32use smallvec::SmallVec;
33use std::collections::HashMap;
34
35const DETERMINISTIC_NUMERIC_TOLERANCE: f64 = 1e-12;
36const RULE_CONDITION_TOLERANCE: f64 = 1e-8;
37const DEFAULT_VERIFY_NUM_BINDINGS: usize = 16;
38const DEFAULT_VERIFY_SAMPLING_TOLERANCE: f64 = 1e-8;
39const ENV_VERIFY_BINDINGS: &str = "CQLIB_RULE_VERIFY_BINDINGS";
40const ENV_VERIFY_TOLERANCE: &str = "CQLIB_RULE_VERIFY_TOLERANCE";
41
42pub fn verify_sampling_bindings() -> usize {
46 std::env::var(ENV_VERIFY_BINDINGS)
47 .ok()
48 .and_then(|value| value.parse().ok())
49 .filter(|count| *count > 0)
50 .unwrap_or(DEFAULT_VERIFY_NUM_BINDINGS)
51}
52
53pub fn verify_sampling_tolerance() -> f64 {
57 std::env::var(ENV_VERIFY_TOLERANCE)
58 .ok()
59 .and_then(|value| value.parse().ok())
60 .filter(|tolerance: &f64| *tolerance > 0.0 && tolerance.is_finite())
61 .unwrap_or(DEFAULT_VERIFY_SAMPLING_TOLERANCE)
62}
63
64#[derive(Debug)]
79pub enum VerifyResult {
80 Equivalent,
82 SampledEqual { num_bindings: usize },
85 NotEquivalent,
87 Inconclusive { reason: String },
89}
90
91impl VerifyResult {
92 pub fn is_verified(&self) -> bool {
94 matches!(self, Self::Equivalent | Self::SampledEqual { .. })
95 }
96}
97
98#[derive(Debug, thiserror::Error)]
100pub enum VerifyError {
101 #[error("unsupported instruction pattern: {0}")]
102 UnsupportedPattern(String),
103 #[error("parameter error: {0}")]
104 Parameter(#[from] ParameterError),
105 #[error("circuit error: {0}")]
106 Circuit(#[from] CircuitError),
107}
108
109impl Rule {
110 pub fn verify(&self) -> Result<VerifyResult, VerifyError> {
116 let (lhs, rhs) = build_simplified_matrices(self)?;
117 layered_verify(
118 self,
119 &lhs,
120 &rhs,
121 verify_sampling_bindings(),
122 verify_sampling_tolerance(),
123 )
124 }
125
126 pub fn verify_by_sampling(
128 &self,
129 num_bindings: usize,
130 tolerance: f64,
131 ) -> Result<VerifyResult, VerifyError> {
132 let (lhs, rhs) = build_simplified_matrices(self)?;
133 layered_verify(self, &lhs, &rhs, num_bindings, tolerance)
134 }
135
136 pub fn needs_sampling_fallback(&self) -> bool {
139 !self.collect_free_symbols().is_empty() || self.conditions.is_some()
140 }
141}
142
143fn layered_verify(
144 rule: &Rule,
145 lhs: &SymbolicMatrix,
146 rhs: &SymbolicMatrix,
147 num_bindings: usize,
148 tolerance: f64,
149) -> Result<VerifyResult, VerifyError> {
150 if symbolic_matrices_equivalent(lhs, rhs)? {
151 return Ok(VerifyResult::Equivalent);
152 }
153
154 if !rule.needs_sampling_fallback() {
155 let (lhs_num, rhs_num) = rayon::join(
156 || evaluate_symbolic_matrix(lhs, &None),
157 || evaluate_symbolic_matrix(rhs, &None),
158 );
159 let lhs_num = lhs_num?;
160 let rhs_num = rhs_num?;
161 if max_diff_up_to_global_phase(&lhs_num, &rhs_num) < DETERMINISTIC_NUMERIC_TOLERANCE {
162 return Ok(VerifyResult::Equivalent);
163 }
164 return Ok(VerifyResult::NotEquivalent);
165 }
166
167 verify_by_sampling(rule, lhs, rhs, num_bindings, tolerance)
168}
169
170fn build_simplified_matrices(rule: &Rule) -> Result<(SymbolicMatrix, SymbolicMatrix), VerifyError> {
171 let num_qubits = rule.num_qubits();
172
173 let (lhs, rhs) = rayon::join(
174 || rule_items_to_circuit(&rule.operations, num_qubits),
175 || rule_items_to_circuit(&rule.target, num_qubits),
176 );
177 let lhs = lhs?;
178 let rhs = rhs?;
179
180 let (lhs, rhs) = rayon::join(
181 || circuit_to_symbolic_matrix(&lhs, None),
182 || circuit_to_symbolic_matrix(&rhs, None),
183 );
184 let lhs = lhs?;
185 let rhs = rhs?;
186
187 let (lhs, rhs) = rayon::join(|| simplify_matrix(&lhs), || simplify_matrix(&rhs));
188 let lhs = lhs?;
189 let rhs = rhs?;
190
191 Ok((lhs, rhs))
192}
193
194fn verify_by_sampling(
195 rule: &Rule,
196 lhs: &SymbolicMatrix,
197 rhs: &SymbolicMatrix,
198 num_bindings: usize,
199 tolerance: f64,
200) -> Result<VerifyResult, VerifyError> {
201 let free_symbols = rule.collect_free_symbols();
202 if free_symbols.is_empty() {
203 let (lhs_num, rhs_num) = rayon::join(
204 || evaluate_symbolic_matrix(lhs, &None),
205 || evaluate_symbolic_matrix(rhs, &None),
206 );
207 let lhs_num = lhs_num?;
208 let rhs_num = rhs_num?;
209 let diff = max_diff_up_to_global_phase(&lhs_num, &rhs_num);
210 if diff < tolerance {
211 return Ok(VerifyResult::SampledEqual { num_bindings: 1 });
212 }
213 return Ok(VerifyResult::NotEquivalent);
214 }
215
216 let symbol_names: Vec<&str> = free_symbols.iter().map(|s| s.as_str()).collect();
217 let bindings =
218 generate_satisfying_bindings(&symbol_names, rule.conditions.as_deref(), num_bindings);
219
220 if bindings.is_empty() {
221 return Ok(VerifyResult::Inconclusive {
222 reason: "could not generate parameter bindings satisfying conditions".to_string(),
223 });
224 }
225
226 for binding in &bindings {
227 let bindings_ref: Option<HashMap<&str, f64>> =
228 Some(binding.iter().map(|(k, &v)| (k.as_str(), v)).collect());
229 let (lhs_num, rhs_num) = rayon::join(
230 || evaluate_symbolic_matrix(lhs, &bindings_ref),
231 || evaluate_symbolic_matrix(rhs, &bindings_ref),
232 );
233 let lhs_num = lhs_num?;
234 let rhs_num = rhs_num?;
235 let diff = max_diff_up_to_global_phase(&lhs_num, &rhs_num);
236 if diff >= tolerance {
237 return Ok(VerifyResult::NotEquivalent);
238 }
239 }
240
241 Ok(VerifyResult::SampledEqual {
242 num_bindings: bindings.len(),
243 })
244}
245
246fn rule_items_to_circuit(ops: &[RuleItem], num_qubits: usize) -> Result<Circuit, VerifyError> {
247 let mut circuit = Circuit::new(num_qubits);
248
249 for op in ops {
250 let instruction = match &op.instruction {
251 Instruction::Standard(gate) => Instruction::Standard(*gate),
252 Instruction::McGate(gate) => Instruction::McGate(gate.clone()),
253 other => {
254 return Err(VerifyError::UnsupportedPattern(other.to_string()));
255 }
256 };
257
258 if matches!(&instruction, Instruction::Standard(StandardGate::GPhase)) {
259 let actual = op.params.as_ref().map_or(0, SmallVec::len);
260 if actual != 1 {
261 return Err(CircuitError::ParameterCountMismatch {
262 expected: 1,
263 actual,
264 }
265 .into());
266 }
267 let theta = op
268 .params
269 .as_ref()
270 .and_then(|params| params.first())
271 .expect("GPhase parameter count was checked");
272 let phase = match theta {
273 ParameterValue::Param(param) => param.clone(),
274 ParameterValue::Fixed(value) => Parameter::from(*value),
275 };
276 circuit.set_global_phase(circuit.global_phase() + phase);
277 continue;
278 }
279
280 let qubits: SmallVec<[Qubit; 3]> =
281 op.qubits.iter().map(|&qubit| Qubit::new(qubit)).collect();
282 let params = op.params.clone().unwrap_or_default();
283 circuit.append(instruction, qubits, params, None)?;
284 }
285
286 Ok(circuit)
287}
288
289fn max_diff_strict(lhs: &ndarray::Array2<Complex64>, rhs: &ndarray::Array2<Complex64>) -> f64 {
291 (0..lhs.len())
292 .into_par_iter()
293 .map(|i| {
294 let l = lhs.as_slice().unwrap()[i];
295 let r = rhs.as_slice().unwrap()[i];
296 (l - r).norm()
297 })
298 .fold(|| 0.0_f64, f64::max)
299 .reduce(|| 0.0_f64, f64::max)
300}
301
302fn max_diff_up_to_global_phase(
304 lhs: &ndarray::Array2<Complex64>,
305 rhs: &ndarray::Array2<Complex64>,
306) -> f64 {
307 let mut phase_ratio = None;
308
309 for (&l, &r) in lhs.iter().zip(rhs.iter()) {
310 let l_zero = l.norm() <= NUMERIC_ZERO_TOLERANCE;
311 let r_zero = r.norm() <= NUMERIC_ZERO_TOLERANCE;
312
313 if l_zero != r_zero {
314 return max_diff_strict(lhs, rhs);
315 }
316
317 if !l_zero {
318 let ratio = r / l;
319 let ratio_norm = ratio.norm();
320
321 if !ratio_norm.is_finite() || (ratio_norm - 1.0).abs() > UNIT_PHASE_NORM_TOLERANCE {
322 return max_diff_strict(lhs, rhs);
323 }
324
325 phase_ratio = Some(ratio);
326 break;
327 }
328 }
329
330 let Some(phase_ratio) = phase_ratio else {
331 return max_diff_strict(lhs, rhs);
332 };
333
334 let lhs_slice = lhs.as_slice().expect("lhs matrix must be contiguous");
335 let rhs_slice = rhs.as_slice().expect("rhs matrix must be contiguous");
336
337 (0..lhs_slice.len())
338 .into_par_iter()
339 .map(|i| {
340 let l = lhs_slice[i];
341 let r = rhs_slice[i];
342
343 let normalized_r = r / phase_ratio;
345 (l - normalized_r).norm()
346 })
347 .fold(|| 0.0_f64, f64::max)
348 .reduce(|| 0.0_f64, f64::max)
349}
350
351fn generate_satisfying_bindings(
353 symbols: &[&str],
354 conditions: Option<&[Condition]>,
355 num_bindings: usize,
356) -> Vec<HashMap<String, f64>> {
357 let mut result = Vec::new();
358 let mut rng = rand::rng();
359 let two_pi = 2.0 * std::f64::consts::PI;
360
361 for _ in 0..num_bindings {
362 let mut bindings: HashMap<String, f64> = HashMap::new();
363 for &sym in symbols {
364 let val = rng.random_range(-two_pi..two_pi);
365 bindings.insert(sym.to_string(), val);
366 }
367
368 if let Some(conds) = conditions {
369 for cond in conds {
370 adjust_binding_for_condition(&mut bindings, cond, symbols);
371 }
372 }
373
374 if let Some(conds) = conditions {
375 let bindings_ref: Option<HashMap<&str, f64>> =
376 Some(bindings.iter().map(|(k, &v)| (k.as_str(), v)).collect());
377 let all_satisfied = conds.iter().all(|c| match c {
378 Condition::Eq(a, b) => {
379 if let (Ok(va), Ok(vb)) = (a.evaluate(&bindings_ref), b.evaluate(&bindings_ref))
380 {
381 (va - vb).abs() < RULE_CONDITION_TOLERANCE
382 } else {
383 false
384 }
385 }
386 Condition::EqMod(a, b, m) => {
387 if let (Ok(va), Ok(vb), Ok(vm)) = (
388 a.evaluate(&bindings_ref),
389 b.evaluate(&bindings_ref),
390 m.evaluate(&bindings_ref),
391 ) {
392 if vm.abs() < NUMERIC_ZERO_TOLERANCE {
393 false
394 } else {
395 let remainder = (va - vb) % vm;
396 remainder.abs() < RULE_CONDITION_TOLERANCE
397 || (vm - remainder).abs() < RULE_CONDITION_TOLERANCE
398 }
399 } else {
400 false
401 }
402 }
403 });
404 if !all_satisfied {
405 continue;
406 }
407 }
408
409 result.push(bindings);
410 }
411
412 result
413}
414
415fn adjust_binding_for_condition(
417 bindings: &mut HashMap<String, f64>,
418 condition: &Condition,
419 symbols: &[&str],
420) {
421 match condition {
422 Condition::Eq(lhs, rhs) => {
423 adjust_for_equality(bindings, lhs, rhs, None, symbols);
424 }
425 Condition::EqMod(lhs, rhs, modulus) => {
426 if let Ok(mod_val) = modulus.evaluate(&Some(
427 bindings.iter().map(|(k, &v)| (k.as_str(), v)).collect(),
428 )) && mod_val.abs() > NUMERIC_ZERO_TOLERANCE
429 {
430 adjust_for_equality(bindings, lhs, rhs, Some(mod_val), symbols);
431 }
432 }
433 }
434}
435
436fn adjust_for_equality(
438 bindings: &mut HashMap<String, f64>,
439 lhs: &Parameter,
440 rhs: &Parameter,
441 modulus: Option<f64>,
442 symbols: &[&str],
443) {
444 if try_adjust_symbol(bindings, rhs, lhs, modulus, symbols) {
445 return;
446 }
447 try_adjust_symbol(bindings, lhs, rhs, modulus, symbols);
448}
449
450fn try_adjust_symbol(
452 bindings: &mut HashMap<String, f64>,
453 expr: &Parameter,
454 target: &Parameter,
455 modulus: Option<f64>,
456 symbols: &[&str],
457) -> bool {
458 let expr_syms = expr.get_symbols();
459 let target_sym = symbols.iter().find(|&&s| expr_syms.contains(s));
460
461 let Some(&sym) = target_sym else {
462 return false;
463 };
464
465 let bindings_ref: Option<HashMap<&str, f64>> =
466 Some(bindings.iter().map(|(k, &v)| (k.as_str(), v)).collect());
467
468 let Ok(target_val) = target.evaluate(&bindings_ref) else {
469 return false;
470 };
471
472 let bindings_without: Option<HashMap<&str, f64>> = Some(
473 bindings
474 .iter()
475 .map(|(k, &v)| {
476 if k == sym {
477 (k.as_str(), 0.0)
478 } else {
479 (k.as_str(), v)
480 }
481 })
482 .collect(),
483 );
484 let bindings_one: Option<HashMap<&str, f64>> = Some(
485 bindings
486 .iter()
487 .map(|(k, &v)| {
488 if k == sym {
489 (k.as_str(), 1.0)
490 } else {
491 (k.as_str(), v)
492 }
493 })
494 .collect(),
495 );
496 let (Ok(expr_zero), Ok(expr_one)) = (
497 expr.evaluate(&bindings_without),
498 expr.evaluate(&bindings_one),
499 ) else {
500 return false;
501 };
502
503 let coeff = expr_one - expr_zero;
504 if coeff.abs() < NUMERIC_ZERO_TOLERANCE {
505 return false;
506 }
507
508 let current_sym_val = bindings.get(sym).copied().unwrap_or(0.0);
509
510 if let Some(mod_val) = modulus {
511 let sym_for_k0 = (target_val - expr_zero) / coeff;
512 let k = ((current_sym_val - sym_for_k0) / mod_val).round();
513 let adjusted = sym_for_k0 + k * mod_val;
514 bindings.insert(sym.to_string(), adjusted);
515 } else {
516 let new_val = (target_val - expr_zero) / coeff;
517 bindings.insert(sym.to_string(), new_val);
518 }
519
520 true
521}
522
523#[cfg(test)]
524#[path = "rule_equivalence_test.rs"]
525mod rule_equivalence_test;