Skip to main content

cqlib_core/compile/transform/
native_quality.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.
6// You may 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//! Native-loop-specific quality policy and exact scope diagnostics.
14
15use crate::circuit::{Instruction, Operation, Qubit, ValueInstruction, ValueOperation};
16use crate::compile::device_planning::cost::{
17    DevicePhysicalCost, MetricAvailability, NativePlanLeaf, RobustDurationKey, RobustErrorKey,
18};
19use smallvec::SmallVec;
20use std::cmp::Ordering;
21use std::collections::HashMap;
22
23/// Tolerance for harmless floating-point accumulation differences in predicted
24/// log error. This is deliberately an absolute tolerance on the additive log
25/// domain, rather than a percentage that would permit larger regressions on
26/// noisier circuits.
27const ERROR_LOG_TOLERANCE: f64 = 1e-12;
28
29/// Candidate acceptance and ranking policy used by [`super::NativeOptimizer`].
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
31pub enum NativeQualityPolicy {
32    /// Preserve the historical global device-cost ordering.
33    #[default]
34    EntanglerFirst,
35    /// Protect entangler count/depth and total depth before ranking structural quality.
36    BalancedDepth,
37}
38
39#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
40pub(crate) struct NativeCriticalPathQuality {
41    /// Largest one-qubit population among all unit-duration critical paths.
42    pub(crate) one_qubit_ops: u32,
43    /// Longest consecutive one-qubit segment on any unit-duration critical path.
44    pub(crate) longest_one_qubit_run: u32,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq)]
48pub(crate) struct NativeQualityVector {
49    pub(crate) physical: DevicePhysicalCost,
50    pub(crate) critical_path: NativeCriticalPathQuality,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub(crate) enum NativeQualityViolation {
55    ScopeShape,
56    TwoQubitOps,
57    TwoQubitDepth,
58    TotalDepth,
59    Error,
60    Makespan,
61    Rank,
62}
63
64impl NativeQualityVector {
65    pub(crate) fn for_operations(physical: DevicePhysicalCost, operations: &[Operation]) -> Self {
66        let mut path = CriticalPathBuilder::default();
67        for operation in operations {
68            if matches!(
69                operation.instruction,
70                Instruction::Standard(_) | Instruction::McGate(_)
71            ) {
72                path.add_gate(&operation.qubits);
73            }
74        }
75        Self {
76            physical,
77            critical_path: path.finish(),
78        }
79    }
80
81    pub(crate) fn for_value_operations(
82        physical: DevicePhysicalCost,
83        operations: &[ValueOperation],
84    ) -> Self {
85        let mut path = CriticalPathBuilder::default();
86        for operation in operations {
87            if matches!(operation.instruction, ValueInstruction::Instruction(_)) {
88                path.add_gate(&operation.qubits);
89            }
90        }
91        Self {
92            physical,
93            critical_path: path.finish(),
94        }
95    }
96
97    /// Builds structural metrics from the exact native leaves selected by the
98    /// device planner, rather than from a higher-level synthesis candidate that
99    /// may expand into several physical one-qubit gates during legalization.
100    pub(crate) fn for_native_plan_leaves(
101        physical: DevicePhysicalCost,
102        leaves: &[NativePlanLeaf],
103    ) -> Self {
104        let mut path = CriticalPathBuilder::default();
105        for leaf in leaves {
106            let qubits = leaf
107                .ordered_qargs
108                .iter()
109                .copied()
110                .map(Qubit::from)
111                .collect::<SmallVec<[Qubit; 2]>>();
112            path.add_gate(&qubits);
113        }
114        Self {
115            physical,
116            critical_path: path.finish(),
117        }
118    }
119
120    /// Returns whether the candidate remains inside the immutable entry envelope.
121    pub(crate) fn admissible_against(self, entry: Self, policy: NativeQualityPolicy) -> bool {
122        self.admissibility_violation_against(entry, policy)
123            .is_none()
124    }
125
126    /// Returns the first immutable-entry protection violated by the candidate.
127    pub(crate) fn admissibility_violation_against(
128        self,
129        entry: Self,
130        policy: NativeQualityPolicy,
131    ) -> Option<NativeQualityViolation> {
132        match policy {
133            NativeQualityPolicy::EntanglerFirst => None,
134            NativeQualityPolicy::BalancedDepth => {
135                if self.physical.native_two_qubit_ops > entry.physical.native_two_qubit_ops {
136                    return Some(NativeQualityViolation::TwoQubitOps);
137                }
138                if self.physical.native_two_qubit_depth > entry.physical.native_two_qubit_depth {
139                    return Some(NativeQualityViolation::TwoQubitDepth);
140                }
141                if self.physical.total_native_depth > entry.physical.total_native_depth {
142                    return Some(NativeQualityViolation::TotalDepth);
143                }
144                if !error_is_no_worse(self.physical.error, entry.physical.error) {
145                    return Some(NativeQualityViolation::Error);
146                }
147                if !makespan_is_no_worse(self.physical.makespan, entry.physical.makespan) {
148                    return Some(NativeQualityViolation::Makespan);
149                }
150                None
151            }
152        }
153    }
154
155    /// Orders candidates under one Native-only policy. Lower is better.
156    pub(crate) fn compare(self, other: Self, policy: NativeQualityPolicy) -> Ordering {
157        match policy {
158            NativeQualityPolicy::EntanglerFirst => self.physical.compare(other.physical),
159            NativeQualityPolicy::BalancedDepth => self
160                .physical
161                .total_native_depth
162                .cmp(&other.physical.total_native_depth)
163                .then_with(|| {
164                    self.physical
165                        .makespan
166                        .compare_by(other.physical.makespan, |left, right| {
167                            left.total_cmp(&right)
168                        })
169                })
170                .then_with(|| {
171                    self.critical_path
172                        .one_qubit_ops
173                        .cmp(&other.critical_path.one_qubit_ops)
174                })
175                .then_with(|| {
176                    self.critical_path
177                        .longest_one_qubit_run
178                        .cmp(&other.critical_path.longest_one_qubit_run)
179                })
180                .then_with(|| {
181                    self.physical
182                        .native_two_qubit_depth
183                        .cmp(&other.physical.native_two_qubit_depth)
184                })
185                .then_with(|| {
186                    self.physical
187                        .native_two_qubit_ops
188                        .cmp(&other.physical.native_two_qubit_ops)
189                })
190                .then_with(|| {
191                    self.physical
192                        .native_total_ops
193                        .cmp(&other.physical.native_total_ops)
194                })
195                .then_with(|| {
196                    self.physical
197                        .error
198                        .compare_by(other.physical.error, |left, right| left.compare(right))
199                })
200                .then_with(|| {
201                    self.physical
202                        .duration
203                        .compare_by(other.physical.duration, |left, right| left.compare(right))
204                }),
205        }
206    }
207
208    /// Returns Pareto dominance against one exact control-flow scope.
209    ///
210    /// `Some(false)` means equality across every protected component,
211    /// `Some(true)` means no component is worse and at least one is strictly
212    /// better, and `None` means the vectors are incomparable or `self`
213    /// regresses at least one component. Optional metric availability must
214    /// match exactly so missing calibration data can never masquerade as an
215    /// improvement.
216    pub(crate) fn exact_pareto_dominance(self, other: Self) -> Option<bool> {
217        let mut strict = false;
218        dominance_component(
219            self.physical.native_two_qubit_ops,
220            other.physical.native_two_qubit_ops,
221            &mut strict,
222        )?;
223        dominance_component(
224            self.physical.native_two_qubit_depth,
225            other.physical.native_two_qubit_depth,
226            &mut strict,
227        )?;
228        dominance_component(
229            self.physical.total_native_depth,
230            other.physical.total_native_depth,
231            &mut strict,
232        )?;
233        dominance_component(
234            self.physical.native_total_ops,
235            other.physical.native_total_ops,
236            &mut strict,
237        )?;
238        strict |= error_dominance(self.physical.error, other.physical.error)?;
239        strict |= duration_dominance(self.physical.duration, other.physical.duration)?;
240        strict |= scalar_availability_dominance(self.physical.makespan, other.physical.makespan)?;
241        dominance_component(
242            self.critical_path.one_qubit_ops,
243            other.critical_path.one_qubit_ops,
244            &mut strict,
245        )?;
246        dominance_component(
247            self.critical_path.longest_one_qubit_run,
248            other.critical_path.longest_one_qubit_run,
249            &mut strict,
250        )?;
251        Some(strict)
252    }
253}
254
255fn dominance_component<T: Ord>(candidate: T, incumbent: T, strict: &mut bool) -> Option<()> {
256    match candidate.cmp(&incumbent) {
257        Ordering::Less => *strict = true,
258        Ordering::Equal => {}
259        Ordering::Greater => return None,
260    }
261    Some(())
262}
263
264fn error_dominance(
265    candidate: MetricAvailability<RobustErrorKey>,
266    incumbent: MetricAvailability<RobustErrorKey>,
267) -> Option<bool> {
268    match (candidate, incumbent) {
269        (MetricAvailability::Disabled, MetricAvailability::Disabled)
270        | (MetricAvailability::Inconsistent, MetricAvailability::Inconsistent) => Some(false),
271        (MetricAvailability::Available(candidate), MetricAvailability::Available(incumbent)) => {
272            if !error_is_no_worse(
273                MetricAvailability::Available(candidate),
274                MetricAvailability::Available(incumbent),
275            ) {
276                return None;
277            }
278            Some(
279                candidate.unavailable_count < incumbent.unavailable_count
280                    || candidate.imputed_count < incumbent.imputed_count
281                    || candidate.log_error + ERROR_LOG_TOLERANCE < incumbent.log_error,
282            )
283        }
284        _ => None,
285    }
286}
287
288fn duration_dominance(
289    candidate: MetricAvailability<RobustDurationKey>,
290    incumbent: MetricAvailability<RobustDurationKey>,
291) -> Option<bool> {
292    match (candidate, incumbent) {
293        (MetricAvailability::Disabled, MetricAvailability::Disabled)
294        | (MetricAvailability::Inconsistent, MetricAvailability::Inconsistent) => Some(false),
295        (MetricAvailability::Available(candidate), MetricAvailability::Available(incumbent)) => {
296            if candidate.unavailable_count > incumbent.unavailable_count
297                || candidate.imputed_count > incumbent.imputed_count
298                || candidate
299                    .duration_work
300                    .total_cmp(&incumbent.duration_work)
301                    .is_gt()
302            {
303                return None;
304            }
305            Some(
306                candidate.unavailable_count < incumbent.unavailable_count
307                    || candidate.imputed_count < incumbent.imputed_count
308                    || candidate
309                        .duration_work
310                        .total_cmp(&incumbent.duration_work)
311                        .is_lt(),
312            )
313        }
314        _ => None,
315    }
316}
317
318fn scalar_availability_dominance(
319    candidate: MetricAvailability<f64>,
320    incumbent: MetricAvailability<f64>,
321) -> Option<bool> {
322    match (candidate, incumbent) {
323        (MetricAvailability::Disabled, MetricAvailability::Disabled)
324        | (MetricAvailability::Inconsistent, MetricAvailability::Inconsistent) => Some(false),
325        (MetricAvailability::Available(candidate), MetricAvailability::Available(incumbent)) => {
326            match candidate.total_cmp(&incumbent) {
327                Ordering::Less => Some(true),
328                Ordering::Equal => Some(false),
329                Ordering::Greater => None,
330            }
331        }
332        _ => None,
333    }
334}
335
336fn error_is_no_worse(
337    candidate: MetricAvailability<RobustErrorKey>,
338    entry: MetricAvailability<RobustErrorKey>,
339) -> bool {
340    match (candidate, entry) {
341        (MetricAvailability::Disabled, MetricAvailability::Disabled)
342        | (MetricAvailability::Inconsistent, MetricAvailability::Inconsistent) => true,
343        (MetricAvailability::Available(candidate), MetricAvailability::Available(entry)) => {
344            candidate.unavailable_count <= entry.unavailable_count
345                && candidate.imputed_count <= entry.imputed_count
346                && (candidate.log_error <= entry.log_error
347                    || candidate.log_error - entry.log_error <= ERROR_LOG_TOLERANCE)
348        }
349        _ => false,
350    }
351}
352
353fn makespan_is_no_worse(
354    candidate: MetricAvailability<f64>,
355    entry: MetricAvailability<f64>,
356) -> bool {
357    match (candidate, entry) {
358        (MetricAvailability::Disabled, MetricAvailability::Disabled)
359        | (MetricAvailability::Inconsistent, MetricAvailability::Inconsistent) => true,
360        (MetricAvailability::Available(candidate), MetricAvailability::Available(entry)) => {
361            candidate <= entry
362        }
363        _ => false,
364    }
365}
366
367#[derive(Debug)]
368struct PathNode {
369    one_qubit: bool,
370    predecessors: SmallVec<[usize; 3]>,
371    successors: SmallVec<[usize; 3]>,
372    forward_depth: u32,
373    backward_depth: u32,
374}
375
376#[derive(Default)]
377pub(super) struct CriticalPathBuilder {
378    nodes: Vec<PathNode>,
379    last_on_qubit: HashMap<Qubit, usize>,
380}
381
382impl CriticalPathBuilder {
383    pub(super) fn add_gate(&mut self, qubits: &[Qubit]) {
384        if qubits.is_empty() {
385            return;
386        }
387        let mut predecessors = SmallVec::<[usize; 3]>::new();
388        for qubit in qubits {
389            if let Some(predecessor) = self.last_on_qubit.get(qubit).copied()
390                && !predecessors.contains(&predecessor)
391            {
392                predecessors.push(predecessor);
393            }
394        }
395        let forward_depth = predecessors
396            .iter()
397            .map(|predecessor| self.nodes[*predecessor].forward_depth)
398            .max()
399            .unwrap_or(0)
400            .saturating_add(1);
401        let index = self.nodes.len();
402        for predecessor in &predecessors {
403            self.nodes[*predecessor].successors.push(index);
404        }
405        self.nodes.push(PathNode {
406            one_qubit: qubits.len() == 1,
407            predecessors,
408            successors: SmallVec::new(),
409            forward_depth,
410            backward_depth: 1,
411        });
412        for qubit in qubits {
413            self.last_on_qubit.insert(*qubit, index);
414        }
415    }
416
417    pub(super) fn finish(mut self) -> NativeCriticalPathQuality {
418        let total_depth = self
419            .nodes
420            .iter()
421            .map(|node| node.forward_depth)
422            .max()
423            .unwrap_or(0);
424        for index in (0..self.nodes.len()).rev() {
425            self.nodes[index].backward_depth = self.nodes[index]
426                .successors
427                .iter()
428                .map(|successor| self.nodes[*successor].backward_depth)
429                .max()
430                .unwrap_or(0)
431                .saturating_add(1);
432        }
433
434        let mut one_qubit_counts = vec![0_u32; self.nodes.len()];
435        let mut current_one_qubit_runs = vec![0_u32; self.nodes.len()];
436        let mut longest_one_qubit_run = 0_u32;
437        for index in 0..self.nodes.len() {
438            if !is_critical(&self.nodes[index], total_depth) {
439                continue;
440            }
441            let tight_predecessors =
442                self.nodes[index]
443                    .predecessors
444                    .iter()
445                    .copied()
446                    .filter(|predecessor| {
447                        is_critical(&self.nodes[*predecessor], total_depth)
448                            && self.nodes[*predecessor].forward_depth.saturating_add(1)
449                                == self.nodes[index].forward_depth
450                    });
451            let mut best_count = 0_u32;
452            let mut best_run = 0_u32;
453            for predecessor in tight_predecessors {
454                best_count = best_count.max(one_qubit_counts[predecessor]);
455                if self.nodes[predecessor].one_qubit {
456                    best_run = best_run.max(current_one_qubit_runs[predecessor]);
457                }
458            }
459            if self.nodes[index].one_qubit {
460                one_qubit_counts[index] = best_count.saturating_add(1);
461                current_one_qubit_runs[index] = best_run.saturating_add(1);
462                longest_one_qubit_run = longest_one_qubit_run.max(current_one_qubit_runs[index]);
463            } else {
464                one_qubit_counts[index] = best_count;
465            }
466        }
467
468        let one_qubit_ops = self
469            .nodes
470            .iter()
471            .enumerate()
472            .filter(|(_, node)| node.forward_depth == total_depth)
473            .map(|(index, _)| one_qubit_counts[index])
474            .max()
475            .unwrap_or(0);
476        NativeCriticalPathQuality {
477            one_qubit_ops,
478            longest_one_qubit_run,
479        }
480    }
481}
482
483fn is_critical(node: &PathNode, total_depth: u32) -> bool {
484    node.forward_depth
485        .saturating_add(node.backward_depth)
486        .saturating_sub(1)
487        == total_depth
488}