Skip to main content

cqlib_core/compile/commutation/
rules.rs

1// This code is part of Cqlib.
2//
3// (C) Copyright China Telecom Quantum Group 2025-2026
4//
5// This code is licensed under the Apache License, Version 2.0. You may
6// obtain a copy of this license in the LICENSE.txt file in the root directory
7// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8//
9// Any modifications or derivative works of this code must retain this
10// copyright notice, and modified files need to carry a notice indicating
11// that they have been altered from the originals.
12
13//! Rule-based commutation oracle.
14//!
15//! This module extracts exact two-operation commutation rules from the compiler
16//! knowledge library.  A rule is accepted by this oracle only when its source
17//! and target are the same two operations in swapped order, i.e. an explicit
18//! `A; B -> B; A` proof.  Matching is structural: instruction identity, a
19//! one-to-one qubit binding, symbolic parameter bindings, and rule conditions
20//! must all agree.
21//!
22//! # Examples
23//!
24//! ```rust
25//! use cqlib_core::circuit::{Instruction, Parameter, Qubit, StandardGate};
26//! use cqlib_core::compile::commutation::{Commutation, CommutationChecker};
27//!
28//! let checker = CommutationChecker::builtin();
29//! let result = checker.check(
30//!     &Instruction::Standard(StandardGate::CX),
31//!     &[Qubit::new(0), Qubit::new(1)],
32//!     &[],
33//!     &Instruction::Standard(StandardGate::RZ),
34//!     &[Qubit::new(0)],
35//!     &[Parameter::symbol("theta")],
36//! );
37//!
38//! assert_eq!(result, Some(Commutation::Exact));
39//! ```
40
41use crate::circuit::{Instruction, Parameter, ParameterValue, Qubit};
42use crate::compile::PARAMETER_EQ_TOLERANCE;
43use crate::compile::commutation::checker::{Commutation, CommutationResult};
44use crate::compile::knowledge::library::{RuleKind, RuleLibrary};
45use crate::compile::knowledge::matcher::KnowledgeInstructionKey;
46use crate::compile::knowledge::rule::{Condition, Rule, RuleItem};
47use smallvec::SmallVec;
48use std::collections::HashMap;
49
50/// Compiled commutation rules loaded from the compiler knowledge library.
51#[derive(Debug, Clone, Default)]
52pub struct RuleCommutationOracle {
53    /// Validated two-operation swap rules.
54    rules: Vec<Rule>,
55    /// Candidate rules indexed by the concrete ordered instruction pair.
56    rule_index:
57        HashMap<(KnowledgeInstructionKey, KnowledgeInstructionKey), SmallVec<[IndexedRule; 4]>>,
58}
59
60/// One rule candidate and the operand order used to match it.
61#[derive(Debug, Clone, Copy)]
62struct IndexedRule {
63    /// Position of the rule in [`RuleCommutationOracle::rules`].
64    rule_index: usize,
65    /// Whether the concrete operands match the rule's source order or its reverse.
66    direction: MatchDirection,
67}
68
69/// Operand direction recorded while building the instruction-pair index.
70#[derive(Debug, Clone, Copy)]
71enum MatchDirection {
72    Forward,
73    Reverse,
74}
75
76/// Bindings accumulated while matching one commutation rule instance.
77///
78/// Qubits are tracked in both directions so one rule-local qubit cannot bind to
79/// multiple concrete qubits and one concrete qubit cannot satisfy two distinct
80/// rule-local labels.
81#[derive(Debug, Clone, Default)]
82struct MatchState {
83    /// Mapping from rule-local qubit labels to concrete circuit qubits.
84    qubits: HashMap<u32, Qubit>,
85    /// Reverse mapping used to enforce one-to-one qubit bindings.
86    reverse_qubits: HashMap<Qubit, u32>,
87    /// Symbolic parameter bindings captured from the rule pattern.
88    params: HashMap<String, Parameter>,
89}
90
91impl RuleCommutationOracle {
92    /// Builds an oracle from the builtin compiler knowledge library.
93    pub fn builtin() -> Self {
94        let library =
95            RuleLibrary::builtin_rules().expect("builtin compiler knowledge rules must load");
96        Self::from_library(library)
97    }
98
99    /// Builds an oracle from the `RuleKind::Commute` rules in `library`.
100    pub fn from_library(library: &RuleLibrary) -> Self {
101        let rules = library
102            .rules_by_kind(RuleKind::Commute)
103            .iter()
104            .filter_map(|id| library.get(*id))
105            .cloned()
106            .collect::<Vec<_>>();
107
108        Self::from_rules(&rules)
109    }
110
111    /// Checks whether the oracle has an exact swap proof for the operation pair.
112    ///
113    /// Candidate rules are selected by ordered instruction pair and retain both
114    /// operand directions, so callers do not need to canonicalize `lhs` and
115    /// `rhs` before querying.
116    pub fn check(
117        &self,
118        lhs_inst: &Instruction,
119        lhs_qubits: &[Qubit],
120        lhs_params: &[Parameter],
121        rhs_inst: &Instruction,
122        rhs_qubits: &[Qubit],
123        rhs_params: &[Parameter],
124    ) -> CommutationResult {
125        let lhs_key = KnowledgeInstructionKey::from_instruction(lhs_inst)?;
126        let rhs_key = KnowledgeInstructionKey::from_instruction(rhs_inst)?;
127        let candidates = self.rule_index.get(&(lhs_key, rhs_key))?;
128
129        for candidate in candidates {
130            let rule = &self.rules[candidate.rule_index];
131            let matched = match candidate.direction {
132                MatchDirection::Forward => rule_matches(
133                    rule, lhs_inst, lhs_qubits, lhs_params, rhs_inst, rhs_qubits, rhs_params,
134                ),
135                MatchDirection::Reverse => rule_matches(
136                    rule, rhs_inst, rhs_qubits, rhs_params, lhs_inst, lhs_qubits, lhs_params,
137                ),
138            };
139            if matched {
140                return Some(Commutation::Exact);
141            }
142        }
143        None
144    }
145
146    /// Filters raw rules down to exact two-operation swaps accepted by this oracle.
147    fn from_rules(rules: &[Rule]) -> Self {
148        let rules = rules
149            .iter()
150            .filter(|rule| {
151                if rule.operations.len() != 2 || rule.target.len() != 2 {
152                    return false;
153                }
154
155                rule.operations[0].equivalent_to(&rule.target[1])
156                    && rule.operations[1].equivalent_to(&rule.target[0])
157            })
158            .cloned()
159            .collect::<Vec<_>>();
160        let mut rule_index: HashMap<
161            (KnowledgeInstructionKey, KnowledgeInstructionKey),
162            SmallVec<[IndexedRule; 4]>,
163        > = HashMap::new();
164
165        for (index, rule) in rules.iter().enumerate() {
166            let Some(lhs_key) =
167                KnowledgeInstructionKey::from_instruction(&rule.operations[0].instruction)
168            else {
169                continue;
170            };
171            let Some(rhs_key) =
172                KnowledgeInstructionKey::from_instruction(&rule.operations[1].instruction)
173            else {
174                continue;
175            };
176
177            rule_index
178                .entry((lhs_key.clone(), rhs_key.clone()))
179                .or_default()
180                .push(IndexedRule {
181                    rule_index: index,
182                    direction: MatchDirection::Forward,
183                });
184            rule_index
185                .entry((rhs_key, lhs_key))
186                .or_default()
187                .push(IndexedRule {
188                    rule_index: index,
189                    direction: MatchDirection::Reverse,
190                });
191        }
192
193        Self { rules, rule_index }
194    }
195}
196
197/// Matches one accepted commutation rule against an ordered operation pair.
198fn rule_matches(
199    rule: &Rule,
200    lhs_inst: &Instruction,
201    lhs_qubits: &[Qubit],
202    lhs_params: &[Parameter],
203    rhs_inst: &Instruction,
204    rhs_qubits: &[Qubit],
205    rhs_params: &[Parameter],
206) -> bool {
207    let mut state = MatchState::default();
208    match_rule_item(
209        &rule.operations[0],
210        lhs_inst,
211        lhs_qubits,
212        lhs_params,
213        &mut state,
214    ) && match_rule_item(
215        &rule.operations[1],
216        rhs_inst,
217        rhs_qubits,
218        rhs_params,
219        &mut state,
220    ) && conditions_hold(rule.conditions.as_deref().unwrap_or(&[]), &state.params)
221}
222
223/// Matches one rule item against one concrete operation and updates bindings.
224///
225/// This matcher is intentionally small and exact because commutation rules are
226/// used as proofs.  Any instruction mismatch, arity mismatch, inconsistent
227/// qubit binding, or unprovable parameter relation rejects the rule.
228fn match_rule_item(
229    item: &RuleItem,
230    instruction: &Instruction,
231    qubits: &[Qubit],
232    params: &[Parameter],
233    state: &mut MatchState,
234) -> bool {
235    if !matches!(
236        (&item.instruction, instruction),
237        (Instruction::Standard(lhs), Instruction::Standard(rhs)) if lhs == rhs
238    ) && !matches!(
239        (&item.instruction, instruction),
240        (Instruction::McGate(lhs), Instruction::McGate(rhs)) if lhs == rhs
241    ) {
242        return false;
243    }
244    if item.qubits.len() != qubits.len() {
245        return false;
246    }
247
248    for (&rule_qubit, &actual_qubit) in item.qubits.iter().zip(qubits) {
249        if let Some(bound) = state.qubits.get(&rule_qubit) {
250            if *bound != actual_qubit {
251                return false;
252            }
253        } else if let Some(other_rule_qubit) = state.reverse_qubits.get(&actual_qubit) {
254            if *other_rule_qubit != rule_qubit {
255                return false;
256            }
257        } else {
258            state.qubits.insert(rule_qubit, actual_qubit);
259            state.reverse_qubits.insert(actual_qubit, rule_qubit);
260        }
261    }
262
263    let rule_params = item.params.as_deref().unwrap_or(&[]);
264    if rule_params.len() != params.len() {
265        return false;
266    }
267
268    rule_params
269        .iter()
270        .zip(params)
271        .all(|(rule_param, actual)| match_parameter(rule_param, actual, &mut state.params))
272}
273
274/// Matches or binds one parameter pattern against a concrete parameter value.
275///
276/// A bare symbol creates or checks a binding.  A compound expression is first
277/// substituted with known bindings and must then reduce to a provable equality.
278fn match_parameter(
279    rule_param: &ParameterValue,
280    actual: &Parameter,
281    bindings: &mut HashMap<String, Parameter>,
282) -> bool {
283    match rule_param {
284        ParameterValue::Fixed(value) => {
285            Parameter::from(*value).provably_equal(actual, PARAMETER_EQ_TOLERANCE)
286        }
287        ParameterValue::Param(pattern) => {
288            if let Some(symbol) = pattern.as_symbol() {
289                if let Some(bound) = bindings.get(&symbol) {
290                    return bound.provably_equal(actual, PARAMETER_EQ_TOLERANCE);
291                }
292                bindings.insert(symbol, actual.clone());
293                return true;
294            }
295
296            let substituted = pattern.substitute_many(bindings);
297            substituted.get_symbols().is_empty()
298                && substituted.provably_equal(actual, PARAMETER_EQ_TOLERANCE)
299        }
300    }
301}
302
303/// Returns whether every rule condition holds under current parameter bindings.
304fn conditions_hold(conditions: &[Condition], bindings: &HashMap<String, Parameter>) -> bool {
305    conditions.iter().all(|condition| match condition {
306        Condition::Eq(lhs, rhs) => {
307            let lhs = lhs.substitute_many(bindings);
308            let rhs = rhs.substitute_many(bindings);
309            lhs.provably_equal(&rhs, PARAMETER_EQ_TOLERANCE)
310        }
311        Condition::EqMod(lhs, rhs, modulus) => {
312            let lhs = lhs.substitute_many(bindings);
313            let rhs = rhs.substitute_many(bindings);
314            let modulus = modulus.substitute_many(bindings);
315            lhs.provably_equal_modulo(&rhs, &modulus, PARAMETER_EQ_TOLERANCE)
316        }
317    })
318}
319
320#[cfg(test)]
321#[path = "./rules_test.rs"]
322mod rules_test;