Skip to main content

cqlib_core/compile/transform/layout/
objective.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//! Layout scoring objective shared by layout algorithms.
14//!
15//! A layout objective assigns lower scores to better candidate mappings. The
16//! score combines topology distance, directed-coupling mismatch, optional
17//! two-qubit error rates, and optional readout error rates. Algorithms may use
18//! this objective either as a final ranking function or as a local tie-breaker
19//! while constructing a candidate.
20
21use super::analysis::CircuitLayoutAnalysis;
22use super::scoring::score_adjacent_interaction;
23use crate::compile::CompilerError;
24use crate::compile::physical_target::PhysicalLayoutGraph;
25use crate::device::Layout;
26use std::collections::BTreeMap;
27
28/// Weighted scoring objective used to rank candidate layouts.
29///
30/// All weights must be finite and non-negative. Setting a weight to `0.0`
31/// removes that component from the total score. The raw components are still
32/// reported in [`LayoutScore`] when they can be computed.
33#[derive(Debug, Clone, PartialEq)]
34pub struct LayoutObjective {
35    /// Weight for weighted logical interaction distance.
36    ///
37    /// Each interaction contributes `interaction.weight * physical_distance`.
38    pub distance_weight: f64,
39    /// Weight for directly adjacent interactions whose observed operation
40    /// direction is unsupported by the directed coupling graph.
41    pub direction_weight: f64,
42    /// Weight for effective two-qubit fidelity cost.
43    ///
44    /// Known gate-specific calibrations contribute their error probability.
45    /// A gate that is native elsewhere on the device but unsupported or
46    /// uncalibrated on the candidate edge contributes the conservative upper
47    /// bound `1.0`. This is a placement-ranking penalty, not a measured error
48    /// rate or circuit-success estimate.
49    pub two_qubit_error_weight: f64,
50    /// Weight for known readout error rates.
51    ///
52    /// Readout error is scaled by logical activity. Interaction-free circuits
53    /// assign activity `1.0` to each logical qubit so idle layouts can still be
54    /// ranked by readout fidelity.
55    pub readout_error_weight: f64,
56}
57
58impl LayoutObjective {
59    /// Creates a topology-only objective.
60    ///
61    /// This ranks by interaction distance and directed-coupling mismatch only,
62    /// ignoring all calibration data even if the target device provides it.
63    pub fn topology_only() -> Self {
64        Self {
65            distance_weight: 1.0,
66            direction_weight: 1.0,
67            two_qubit_error_weight: 0.0,
68            readout_error_weight: 0.0,
69        }
70    }
71
72    /// Creates a fidelity-aware objective when useful calibration data exists.
73    ///
74    /// If the physical graph has no usable error-rate data, this falls back to
75    /// [`LayoutObjective::topology_only`] instead of failing.
76    pub fn auto_from_physical(physical: &PhysicalLayoutGraph) -> Self {
77        let mut objective = Self::topology_only();
78        if physical.has_two_qubit_error_data() {
79            objective.two_qubit_error_weight = 10.0;
80        }
81        if physical.has_readout_error_data() {
82            objective.readout_error_weight = 1.0;
83        }
84        objective
85    }
86
87    /// Creates a fidelity-aware objective and requires calibration data.
88    ///
89    /// Use this for workflows where silently falling back to topology-only
90    /// scoring would hide a device-data configuration problem.
91    pub fn fidelity_required(physical: &PhysicalLayoutGraph) -> Result<Self, CompilerError> {
92        if physical.has_fidelity_data() {
93            Ok(Self::auto_from_physical(physical))
94        } else {
95            Err(CompilerError::InvalidInput(
96                "layout fidelity scoring was requested but the device has no usable fidelity data"
97                    .to_string(),
98            ))
99        }
100    }
101
102    /// Creates the default fidelity-aware objective.
103    ///
104    /// The default weights keep topology distance and direction mismatch active
105    /// while making known two-qubit error rates a stronger tie-breaker than
106    /// readout error.
107    pub fn fidelity_aware() -> Self {
108        Self {
109            distance_weight: 1.0,
110            direction_weight: 1.0,
111            two_qubit_error_weight: 10.0,
112            readout_error_weight: 1.0,
113        }
114    }
115
116    /// Returns whether any fidelity/error term can affect scoring.
117    pub fn uses_fidelity(&self) -> bool {
118        self.two_qubit_error_weight != 0.0 || self.readout_error_weight != 0.0
119    }
120
121    /// Scores a candidate layout against circuit and physical-graph analysis.
122    ///
123    /// The layout must map every logical qubit in `analysis`. Interactions
124    /// whose mapped physical qubits are disconnected are rejected because the
125    /// resulting mapping cannot be routed within the usable topology.
126    ///
127    /// # Errors
128    ///
129    /// Returns [`CompilerError::InvalidInput`] for invalid objective weights or
130    /// disconnected mapped interactions. Returns
131    /// [`CompilerError::InvariantViolation`] if the layout omits a logical
132    /// qubit present in the analysis.
133    pub fn score_layout(
134        &self,
135        analysis: &CircuitLayoutAnalysis,
136        physical: &PhysicalLayoutGraph,
137        layout: &Layout,
138    ) -> Result<LayoutScore, CompilerError> {
139        validate_objective_weight(self.distance_weight, "distance_weight")?;
140        validate_objective_weight(self.direction_weight, "direction_weight")?;
141        validate_objective_weight(self.two_qubit_error_weight, "two_qubit_error_weight")?;
142        validate_objective_weight(self.readout_error_weight, "readout_error_weight")?;
143
144        let mut distance = 0.0;
145        let mut direction = 0.0;
146        let mut two_qubit_error = 0.0;
147
148        for (interaction_index, interaction) in
149            analysis.interactions.interactions().iter().enumerate()
150        {
151            let left_physical = layout.get_physical(interaction.left).ok_or_else(|| {
152                CompilerError::InvariantViolation(format!(
153                    "layout does not map logical qubit {}",
154                    interaction.left
155                ))
156            })?;
157            let right_physical = layout.get_physical(interaction.right).ok_or_else(|| {
158                CompilerError::InvariantViolation(format!(
159                    "layout does not map logical qubit {}",
160                    interaction.right
161                ))
162            })?;
163            let Some(pair_distance) = physical.distance(left_physical, right_physical) else {
164                return Err(CompilerError::InvalidInput(format!(
165                    "physical qubits {} and {} are disconnected in the usable topology",
166                    left_physical, right_physical
167                )));
168            };
169
170            distance += interaction.weight * f64::from(pair_distance);
171
172            if pair_distance == 1 {
173                // Direction and two-qubit error are meaningful only for a
174                // concrete adjacent hardware edge. Non-adjacent interactions
175                // are handled through the distance term and later routing.
176                let left_index = physical.physical_index(left_physical).ok_or_else(|| {
177                    CompilerError::InvariantViolation(format!(
178                        "layout mapped {} outside the physical target",
179                        left_physical
180                    ))
181                })?;
182                let right_index = physical.physical_index(right_physical).ok_or_else(|| {
183                    CompilerError::InvariantViolation(format!(
184                        "layout mapped {} outside the physical target",
185                        right_physical
186                    ))
187                })?;
188                let adjacent = score_adjacent_interaction(
189                    analysis.interactions.gate_contributions(interaction_index),
190                    left_index,
191                    right_index,
192                    physical,
193                );
194                direction += adjacent.direction;
195                if self.two_qubit_error_weight != 0.0 {
196                    two_qubit_error += adjacent.effective_two_qubit_error;
197                }
198            }
199        }
200
201        let readout_error = if self.readout_error_weight == 0.0 {
202            0.0
203        } else {
204            score_readout_error(analysis, physical, layout)?
205        };
206
207        let total = self.distance_weight * distance
208            + self.direction_weight * direction
209            + self.two_qubit_error_weight * two_qubit_error
210            + self.readout_error_weight * readout_error;
211
212        Ok(LayoutScore {
213            total,
214            distance,
215            direction,
216            two_qubit_error,
217            readout_error,
218            used_fidelity: self.uses_fidelity(),
219        })
220    }
221}
222
223impl Default for LayoutObjective {
224    fn default() -> Self {
225        Self::topology_only()
226    }
227}
228
229/// Breakdown of one layout score.
230///
231/// Component fields are unweighted by the objective, but include the logical
232/// interaction weights already present in the circuit analysis. The
233/// [`LayoutScore::total`] field applies the objective weights.
234#[derive(Debug, Clone, PartialEq)]
235pub struct LayoutScore {
236    /// Weighted sum according to [`LayoutObjective`].
237    pub total: f64,
238    /// Raw weighted-distance component.
239    pub distance: f64,
240    /// Raw direction-mismatch component.
241    pub direction: f64,
242    /// Unweighted effective two-qubit placement cost.
243    ///
244    /// In addition to known gate-specific error probabilities, this includes a
245    /// conservative `1.0` cost for native gates that are unsupported or lack
246    /// calibration on the selected physical edge. The value is accumulated
247    /// over weighted logical interactions and may exceed `1.0`; it is not a
248    /// measured error rate and must not be used for calibration reporting or
249    /// circuit-success estimation.
250    pub two_qubit_error: f64,
251    /// Raw readout error component.
252    pub readout_error: f64,
253    /// Whether the objective was configured to use fidelity terms.
254    pub used_fidelity: bool,
255}
256
257/// Computes the raw readout-error component for a complete layout.
258///
259/// Logical qubit activity scales the contribution of each mapped physical
260/// readout error. Interaction-free circuits use one unit of activity per
261/// logical qubit so readout data can still rank idle mappings.
262fn score_readout_error(
263    analysis: &CircuitLayoutAnalysis,
264    physical: &PhysicalLayoutGraph,
265    layout: &Layout,
266) -> Result<f64, CompilerError> {
267    let mut activity = analysis.interactions.logical_activity();
268    if activity.is_empty() {
269        // Without two-qubit interactions there is no natural activity signal.
270        // Assigning one unit to each logical qubit lets readout calibration
271        // still rank otherwise equivalent interaction-free layouts.
272        activity = analysis
273            .logical_qubits
274            .iter()
275            .copied()
276            .map(|logical| (logical, 1.0))
277            .collect::<BTreeMap<_, _>>();
278    }
279
280    let mut readout_error = 0.0;
281    for logical in &analysis.logical_qubits {
282        let physical_qubit = layout.get_physical(*logical).ok_or_else(|| {
283            CompilerError::InvariantViolation(format!(
284                "layout does not map logical qubit {logical}"
285            ))
286        })?;
287        if let Some(error) = physical.readout_error(physical_qubit) {
288            readout_error += activity.get(logical).copied().unwrap_or(1.0) * error;
289        }
290    }
291    Ok(readout_error)
292}
293
294/// Validates that one objective weight can participate in total ordering.
295fn validate_objective_weight(value: f64, name: &str) -> Result<(), CompilerError> {
296    if value.is_finite() && value >= 0.0 {
297        Ok(())
298    } else {
299        Err(CompilerError::InvalidInput(format!(
300            "layout objective {name} must be finite and non-negative"
301        )))
302    }
303}