Skip to main content

cqlib_core/compile/transform/decompose/
rule.rs

1// This code is part of Cqlib.
2//
3// (C) Copyright China Telecom Quantum Group 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//! Runtime decomposition rules.
14//!
15//! This module provides pass-local templates for algorithmic decomposition
16//! results. These rules are generated from synthesized value operations and
17//! replayed by role, not by concrete qubit id. They deliberately stay separate
18//! from the static knowledge-rule library because decomposition templates may
19//! depend on resource signatures such as clean or dirty ancillary usage.
20
21use crate::circuit::{
22    Instruction, Parameter, ParameterValue, Qubit, StandardGate, ValueInstruction, ValueOperation,
23};
24use crate::compile::CompilerError;
25use crate::compile::transform::decompose::unitary::TwoQubitSynthesisTarget;
26use crate::compile::transform::target_basis::TargetBasisSignature;
27use ndarray::Array2;
28use num_complex::Complex64;
29use smallvec::SmallVec;
30use std::collections::HashMap;
31
32/// Pass-local cache for runtime decomposition rules.
33#[derive(Debug, Clone, Default)]
34pub struct DecompositionRuleCache {
35    mc_gate_rules: HashMap<McGateRuleKey, DecompositionRule>,
36    numeric_unitary_rules: HashMap<NumericUnitaryRuleKey, NumericUnitaryRule>,
37    stats: DecompositionRuleStats,
38}
39
40/// Basic cache accounting used by tests and diagnostics.
41#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
42pub struct DecompositionRuleStats {
43    pub hits: usize,
44    pub misses: usize,
45    pub inserts: usize,
46}
47
48impl DecompositionRuleCache {
49    /// Instantiates a cached multi-controlled-gate decomposition rule.
50    ///
51    /// Returns `Ok(None)` when the cache has no matching rule. The supplied
52    /// qubit slices provide the concrete role bindings for a cached template.
53    pub fn instantiate_mc_gate(
54        &mut self,
55        request: McGateRuleRequest<'_>,
56        controls: &[Qubit],
57        targets: &[Qubit],
58        ancillas: &[Qubit],
59    ) -> Result<Option<Vec<ValueOperation>>, CompilerError> {
60        let key = McGateRuleKey::from_request(request);
61        if let Some(rule) = self.mc_gate_rules.get(&key) {
62            self.stats.hits += 1;
63            return Ok(Some(rule.instantiate(controls, targets, ancillas)?));
64        }
65        self.stats.misses += 1;
66        Ok(None)
67    }
68
69    /// Records a synthesized multi-controlled-gate decomposition as a runtime rule.
70    ///
71    /// `operations` must use only the supplied controls, targets, and ancillas.
72    /// The generated rule is independent of the concrete qubit ids and can be
73    /// instantiated later with any qubits that have the same role counts.
74    pub fn insert_mc_gate(
75        &mut self,
76        request: McGateRuleRequest<'_>,
77        controls: &[Qubit],
78        targets: &[Qubit],
79        ancillas: &[Qubit],
80        operations: &[ValueOperation],
81    ) -> Result<(), CompilerError> {
82        let key = McGateRuleKey::from_request(request);
83        let rule = DecompositionRule::from_operations(controls, targets, ancillas, operations)?;
84        self.mc_gate_rules.insert(key, rule);
85        self.stats.inserts += 1;
86        Ok(())
87    }
88
89    /// Returns cache hit/miss/insert counters.
90    pub const fn stats(&self) -> DecompositionRuleStats {
91        self.stats
92    }
93
94    /// Instantiates a cached numeric unitary decomposition rule.
95    ///
96    /// Returns `Ok(None)` when no bit-exact matrix and basis match exists.
97    pub fn instantiate_numeric_unitary(
98        &mut self,
99        request: NumericUnitaryRuleRequest<'_>,
100        qubits: &[Qubit],
101    ) -> Result<Option<(Vec<ValueOperation>, f64)>, CompilerError> {
102        let key = NumericUnitaryRuleKey::from_request(request);
103        if let Some(rule) = self.numeric_unitary_rules.get(&key) {
104            self.stats.hits += 1;
105            return Ok(Some((
106                rule.rule.instantiate(&[], qubits, &[])?,
107                rule.phase_delta,
108            )));
109        }
110        self.stats.misses += 1;
111        Ok(None)
112    }
113
114    /// Records a synthesized numeric unitary decomposition as a runtime rule.
115    ///
116    /// The numeric matrix key is bit-exact: mathematically equivalent matrices
117    /// with different floating-point representations are intentionally distinct.
118    pub fn insert_numeric_unitary(
119        &mut self,
120        request: NumericUnitaryRuleRequest<'_>,
121        qubits: &[Qubit],
122        operations: &[ValueOperation],
123        phase_delta: f64,
124    ) -> Result<(), CompilerError> {
125        let key = NumericUnitaryRuleKey::from_request(request);
126        let rule = DecompositionRule::from_operations(&[], qubits, &[], operations)?;
127        self.numeric_unitary_rules
128            .insert(key, NumericUnitaryRule { rule, phase_delta });
129        self.stats.inserts += 1;
130        Ok(())
131    }
132}
133
134/// Runtime cache request for one multi-controlled-gate synthesis.
135#[derive(Debug, Clone, Copy)]
136pub struct McGateRuleRequest<'a> {
137    pub gate: StandardGate,
138    pub control_count: usize,
139    pub target_count: usize,
140    pub params: &'a [ParameterValue],
141    pub resource: ResourceSignature,
142}
143
144/// Runtime cache request for one numeric unitary synthesis.
145#[derive(Debug, Clone)]
146pub struct NumericUnitaryRuleRequest<'a> {
147    pub num_qubits: u16,
148    pub matrix: &'a Array2<Complex64>,
149    pub synthesis_key: NumericUnitarySynthesisKey,
150}
151
152/// Semantic context that determines numeric-unitary synthesis output.
153///
154/// This key intentionally includes the canonical target-basis identity and
155/// exactness mode, so a cached template is never reused under a different
156/// lowering policy.
157#[derive(Debug, Clone, PartialEq, Eq, Hash)]
158pub enum NumericUnitarySynthesisKey {
159    OneQubit,
160    TwoQubit {
161        target_basis: Option<TargetBasisSignature>,
162        fallback_pauli: bool,
163    },
164}
165
166impl NumericUnitarySynthesisKey {
167    pub const fn one_qubit() -> Self {
168        Self::OneQubit
169    }
170
171    pub(crate) fn two_qubit(target: &TwoQubitSynthesisTarget) -> Self {
172        Self::TwoQubit {
173            target_basis: target.cache_signature(),
174            fallback_pauli: target.fallback_pauli(),
175        }
176    }
177}
178
179#[derive(Debug, Clone)]
180struct NumericUnitaryRule {
181    rule: DecompositionRule,
182    phase_delta: f64,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Hash)]
186struct NumericUnitaryRuleKey {
187    num_qubits: u16,
188    rows: usize,
189    cols: usize,
190    matrix: Vec<(u64, u64)>,
191    synthesis_key: NumericUnitarySynthesisKey,
192}
193
194impl NumericUnitaryRuleKey {
195    fn from_request(request: NumericUnitaryRuleRequest<'_>) -> Self {
196        Self {
197            num_qubits: request.num_qubits,
198            rows: request.matrix.nrows(),
199            cols: request.matrix.ncols(),
200            matrix: request
201                .matrix
202                .iter()
203                .map(|value| (value.re.to_bits(), value.im.to_bits()))
204                .collect(),
205            synthesis_key: request.synthesis_key,
206        }
207    }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Hash)]
211struct McGateRuleKey {
212    gate: StandardGate,
213    control_count: usize,
214    target_count: usize,
215    params: Vec<ParameterValueKey>,
216    resource: ResourceSignature,
217}
218
219impl McGateRuleKey {
220    fn from_request(request: McGateRuleRequest<'_>) -> Self {
221        Self {
222            gate: request.gate,
223            control_count: request.control_count,
224            target_count: request.target_count,
225            params: request.params.iter().map(ParameterValueKey::from).collect(),
226            resource: request.resource,
227        }
228    }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Hash)]
232enum ParameterValueKey {
233    Fixed(u64),
234    Param(Parameter),
235}
236
237impl From<&ParameterValue> for ParameterValueKey {
238    fn from(value: &ParameterValue) -> Self {
239        match value {
240            ParameterValue::Fixed(value) => Self::Fixed(value.to_bits()),
241            ParameterValue::Param(parameter) => Self::Param(parameter.clone()),
242        }
243    }
244}
245
246/// Resource-dependent synthesis signature for a runtime rule.
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
248pub struct ResourceSignature {
249    pub algorithm: DecompositionAlgorithm,
250    pub ancilla_kind: RuntimeAncillaKind,
251    pub ancilla_count: usize,
252}
253
254impl ResourceSignature {
255    /// Creates a signature for an ancillary-free decomposition.
256    pub const fn no_aux(algorithm: DecompositionAlgorithm) -> Self {
257        Self {
258            algorithm,
259            ancilla_kind: RuntimeAncillaKind::None,
260            ancilla_count: 0,
261        }
262    }
263
264    /// Creates a signature for a clean-ancilla decomposition.
265    pub const fn clean(algorithm: DecompositionAlgorithm, count: usize) -> Self {
266        Self {
267            algorithm,
268            ancilla_kind: RuntimeAncillaKind::CleanZero,
269            ancilla_count: count,
270        }
271    }
272
273    /// Creates a signature for a dirty-ancilla decomposition.
274    pub const fn dirty(algorithm: DecompositionAlgorithm, count: usize) -> Self {
275        Self {
276            algorithm,
277            ancilla_kind: RuntimeAncillaKind::Dirty,
278            ancilla_count: count,
279        }
280    }
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
284pub enum RuntimeAncillaKind {
285    None,
286    CleanZero,
287    Dirty,
288}
289
290/// Algorithm identity used to prevent incompatible template reuse.
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
292pub enum DecompositionAlgorithm {
293    PauliSmall,
294    PauliTwoClean,
295    PauliOneCleanKg24,
296    PauliManyClean,
297    PauliManyDirty,
298    PauliTwoDirty,
299    PauliOneDirty,
300    PauliOneCleanB95,
301    PauliNoAux,
302    CleanAccumulator,
303    NoAux,
304}
305
306#[derive(Debug, Clone)]
307pub struct DecompositionRule {
308    operations: Vec<TemplateOperation>,
309}
310
311impl DecompositionRule {
312    /// Builds a runtime rule from concrete value operations.
313    ///
314    /// The resulting rule abstracts all qubits into control, target, and
315    /// ancilla roles. The operation sequence may later be instantiated with
316    /// different concrete qubits that preserve the same role counts.
317    pub fn from_operations(
318        controls: &[Qubit],
319        targets: &[Qubit],
320        ancillas: &[Qubit],
321        operations: &[ValueOperation],
322    ) -> Result<Self, CompilerError> {
323        let mut template_operations = Vec::with_capacity(operations.len());
324        for operation in operations {
325            let ValueInstruction::Instruction(instruction) = &operation.instruction else {
326                return Err(CompilerError::InvariantViolation(
327                    "runtime decomposition rules support only gate-like value instructions"
328                        .to_string(),
329                ));
330            };
331            let qubits = operation
332                .qubits
333                .iter()
334                .map(|qubit| template_qubit(*qubit, controls, targets, ancillas))
335                .collect::<Result<SmallVec<[_; 3]>, _>>()?;
336            template_operations.push(TemplateOperation {
337                instruction: instruction.clone(),
338                qubits,
339                params: operation.params.clone(),
340                label: operation.label.clone(),
341            });
342        }
343        Ok(Self {
344            operations: template_operations,
345        })
346    }
347
348    /// Instantiates this rule with concrete control, target, and ancilla qubits.
349    pub fn instantiate(
350        &self,
351        controls: &[Qubit],
352        targets: &[Qubit],
353        ancillas: &[Qubit],
354    ) -> Result<Vec<ValueOperation>, CompilerError> {
355        self.operations
356            .iter()
357            .map(|operation| operation.instantiate(controls, targets, ancillas))
358            .collect()
359    }
360}
361
362#[derive(Debug, Clone)]
363struct TemplateOperation {
364    instruction: Instruction,
365    qubits: SmallVec<[TemplateQubit; 3]>,
366    params: SmallVec<[ParameterValue; 1]>,
367    label: Option<Box<str>>,
368}
369
370impl TemplateOperation {
371    fn instantiate(
372        &self,
373        controls: &[Qubit],
374        targets: &[Qubit],
375        ancillas: &[Qubit],
376    ) -> Result<ValueOperation, CompilerError> {
377        let qubits = self
378            .qubits
379            .iter()
380            .map(|qubit| qubit.instantiate(controls, targets, ancillas))
381            .collect::<Result<SmallVec<[_; 3]>, _>>()?;
382        Ok(ValueOperation {
383            instruction: ValueInstruction::from_instruction(self.instruction.clone()),
384            qubits,
385            params: self.params.clone(),
386            label: self.label.clone(),
387        })
388    }
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
392enum TemplateQubit {
393    Control(usize),
394    Target(usize),
395    Ancilla(usize),
396}
397
398impl TemplateQubit {
399    fn instantiate(
400        self,
401        controls: &[Qubit],
402        targets: &[Qubit],
403        ancillas: &[Qubit],
404    ) -> Result<Qubit, CompilerError> {
405        match self {
406            Self::Control(index) => controls.get(index).copied(),
407            Self::Target(index) => targets.get(index).copied(),
408            Self::Ancilla(index) => ancillas.get(index).copied(),
409        }
410        .ok_or_else(|| {
411            CompilerError::InvariantViolation(format!(
412                "runtime decomposition rule references missing template qubit {self:?}"
413            ))
414        })
415    }
416}
417
418fn template_qubit(
419    qubit: Qubit,
420    controls: &[Qubit],
421    targets: &[Qubit],
422    ancillas: &[Qubit],
423) -> Result<TemplateQubit, CompilerError> {
424    if let Some(index) = controls.iter().position(|control| *control == qubit) {
425        return Ok(TemplateQubit::Control(index));
426    }
427    if let Some(index) = targets.iter().position(|target| *target == qubit) {
428        return Ok(TemplateQubit::Target(index));
429    }
430    if let Some(index) = ancillas.iter().position(|ancilla| *ancilla == qubit) {
431        return Ok(TemplateQubit::Ancilla(index));
432    }
433    Err(CompilerError::InvariantViolation(format!(
434        "runtime decomposition rule output uses qubit {qubit} outside source operands and ancillas"
435    )))
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441    use crate::circuit::{Instruction, Parameter, Qubit, StandardGate, ValueOperation};
442    use crate::compile::transform::decompose::unitary::TwoQubitSynthesisTarget;
443    use ndarray::Array2;
444    use num_complex::Complex64;
445    use smallvec::smallvec;
446
447    fn request<'a>(
448        params: &'a [ParameterValue],
449        resource: ResourceSignature,
450    ) -> McGateRuleRequest<'a> {
451        McGateRuleRequest {
452            gate: StandardGate::RX,
453            control_count: 2,
454            target_count: 1,
455            params,
456            resource,
457        }
458    }
459
460    #[test]
461    fn cached_rule_instantiates_on_different_qubits() {
462        let params = [ParameterValue::Param(Parameter::symbol("theta"))];
463        let resource = ResourceSignature::clean(DecompositionAlgorithm::CleanAccumulator, 1);
464        let mut cache = DecompositionRuleCache::default();
465        let operations = vec![
466            ValueOperation {
467                instruction: ValueInstruction::from_instruction(Instruction::Standard(
468                    StandardGate::CX,
469                )),
470                qubits: smallvec![Qubit::new(0), Qubit::new(3)],
471                params: smallvec![],
472                label: None,
473            },
474            ValueOperation {
475                instruction: ValueInstruction::from_instruction(Instruction::Standard(
476                    StandardGate::RX,
477                )),
478                qubits: smallvec![Qubit::new(2)],
479                params: smallvec![params[0].clone()],
480                label: None,
481            },
482        ];
483
484        assert!(
485            cache
486                .instantiate_mc_gate(
487                    request(&params, resource),
488                    &[Qubit::new(0), Qubit::new(1)],
489                    &[Qubit::new(2)],
490                    &[Qubit::new(3)]
491                )
492                .unwrap()
493                .is_none()
494        );
495        cache
496            .insert_mc_gate(
497                request(&params, resource),
498                &[Qubit::new(0), Qubit::new(1)],
499                &[Qubit::new(2)],
500                &[Qubit::new(3)],
501                &operations,
502            )
503            .unwrap();
504
505        let instantiated = cache
506            .instantiate_mc_gate(
507                request(&params, resource),
508                &[Qubit::new(5), Qubit::new(6)],
509                &[Qubit::new(7)],
510                &[Qubit::new(8)],
511            )
512            .unwrap()
513            .unwrap();
514
515        assert_eq!(
516            instantiated[0].qubits.as_slice(),
517            &[Qubit::new(5), Qubit::new(8)]
518        );
519        assert_eq!(instantiated[1].qubits.as_slice(), &[Qubit::new(7)]);
520        assert_eq!(
521            cache.stats(),
522            DecompositionRuleStats {
523                hits: 1,
524                misses: 1,
525                inserts: 1,
526            }
527        );
528    }
529
530    #[test]
531    fn resource_signature_separates_otherwise_equal_rules() {
532        let params = [ParameterValue::Fixed(0.25)];
533        let mut cache = DecompositionRuleCache::default();
534        let operations = vec![ValueOperation::from_standard(
535            StandardGate::RX,
536            [Qubit::new(2)],
537            [params[0].clone()],
538        )];
539        cache
540            .insert_mc_gate(
541                request(
542                    &params,
543                    ResourceSignature::no_aux(DecompositionAlgorithm::NoAux),
544                ),
545                &[Qubit::new(0), Qubit::new(1)],
546                &[Qubit::new(2)],
547                &[],
548                &operations,
549            )
550            .unwrap();
551
552        assert!(
553            cache
554                .instantiate_mc_gate(
555                    request(
556                        &params,
557                        ResourceSignature::clean(DecompositionAlgorithm::CleanAccumulator, 1),
558                    ),
559                    &[Qubit::new(0), Qubit::new(1)],
560                    &[Qubit::new(2)],
561                    &[Qubit::new(3)],
562                )
563                .unwrap()
564                .is_none()
565        );
566    }
567
568    #[test]
569    fn numeric_two_qubit_cache_separates_target_signatures() {
570        let matrix = Array2::<Complex64>::eye(4);
571        let cx_target = TwoQubitSynthesisTarget::from_standard_gates(
572            vec![StandardGate::U, StandardGate::H],
573            vec![StandardGate::CX],
574            true,
575        )
576        .unwrap();
577        let cz_target = TwoQubitSynthesisTarget::from_standard_gates(
578            vec![StandardGate::U, StandardGate::H],
579            vec![StandardGate::CZ],
580            true,
581        )
582        .unwrap();
583        let request = NumericUnitaryRuleRequest {
584            num_qubits: 2,
585            matrix: &matrix,
586            synthesis_key: NumericUnitarySynthesisKey::two_qubit(&cx_target),
587        };
588        let distinct_target_request = NumericUnitaryRuleRequest {
589            num_qubits: 2,
590            matrix: &matrix,
591            synthesis_key: NumericUnitarySynthesisKey::two_qubit(&cz_target),
592        };
593        let qubits = [Qubit::new(0), Qubit::new(1)];
594        let operations = vec![ValueOperation::from_standard(StandardGate::CX, qubits, [])];
595        let mut cache = DecompositionRuleCache::default();
596
597        assert!(
598            cache
599                .instantiate_numeric_unitary(request.clone(), &qubits)
600                .unwrap()
601                .is_none()
602        );
603        cache
604            .insert_numeric_unitary(request.clone(), &qubits, &operations, 0.0)
605            .unwrap();
606        assert!(
607            cache
608                .instantiate_numeric_unitary(request, &[Qubit::new(2), Qubit::new(3)])
609                .unwrap()
610                .is_some()
611        );
612        assert!(
613            cache
614                .instantiate_numeric_unitary(distinct_target_request, &qubits)
615                .unwrap()
616                .is_none()
617        );
618        assert_eq!(
619            cache.stats(),
620            DecompositionRuleStats {
621                hits: 1,
622                misses: 2,
623                inserts: 1,
624            }
625        );
626    }
627}