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}