Skip to main content

cqlib_core/compile/
physical_target.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//! Physical topology view used by layout and routing preparation.
14//!
15//! This module converts a [`Device`] into the compiler-local view needed by
16//! layout scoring and SABRE routing. The graph contains only usable physical
17//! qubits. Distances and adjacency are undirected because routing can insert
18//! SWAPs along either orientation of a connected edge, while directed coupling
19//! information is retained separately for target-aware scoring and diagnostics.
20//!
21//! Calibration data is optional. Missing readout or two-qubit error data does
22//! not make the graph invalid; callers decide whether topology-only scoring is
23//! acceptable or whether fidelity data is required.
24
25use crate::circuit::{Instruction, StandardGate};
26use crate::compile::CompilerError;
27use crate::device::{Device, PhysicalQubit, Topology};
28use std::collections::{BTreeMap, BTreeSet, VecDeque};
29
30type DirectedQubitPair = (PhysicalQubit, PhysicalQubit);
31type TwoQubitGateMap = BTreeMap<DirectedQubitPair, Vec<TwoQubitGateStatus>>;
32
33/// Resolved native capability and calibration for one gate on one directed edge.
34#[derive(Debug, Clone, Copy, Default, PartialEq)]
35pub(super) enum TwoQubitGateStatus {
36    #[default]
37    Unsupported,
38    Uncalibrated,
39    Calibrated(f64),
40}
41
42impl TwoQubitGateStatus {
43    pub(super) fn is_supported(self) -> bool {
44        !matches!(self, Self::Unsupported)
45    }
46
47    pub(super) fn error(self) -> Option<f64> {
48        match self {
49            Self::Calibrated(error) => Some(error),
50            Self::Unsupported | Self::Uncalibrated => None,
51        }
52    }
53}
54
55/// Compiler-local physical graph with usable qubits, distances, and calibration data.
56///
57/// The graph intentionally separates undirected routing connectivity from
58/// directed coupling support. Use [`Self::distance`] or
59/// [`Self::is_adjacent_undirected`] for routing reachability, and
60/// [`Self::supports_directed_coupling`] when a target-specific direction
61/// matters.
62#[derive(Debug, Clone, PartialEq)]
63pub struct PhysicalLayoutGraph {
64    physical_qubits: Vec<PhysicalQubit>,
65    physical_index: BTreeMap<PhysicalQubit, usize>,
66    adjacency: Vec<Vec<usize>>,
67    distances: DistanceTable,
68    directed_couplings: BTreeSet<DirectedQubitPair>,
69    readout_errors: BTreeMap<PhysicalQubit, f64>,
70    two_qubit_gates: TwoQubitGateMap,
71    native_two_qubit_gates: Vec<bool>,
72    has_readout_error_data: bool,
73    has_native_two_qubit_capabilities: bool,
74    has_two_qubit_error_data: bool,
75}
76
77impl PhysicalLayoutGraph {
78    /// Builds a layout physical graph from a device, excluding unusable qubits.
79    ///
80    /// The resulting graph includes all usable qubits even when the topology is
81    /// disconnected. Disconnected pairs have no finite distance and are rejected
82    /// later by layout or routing checks when an interaction needs them.
83    pub fn from_device(device: &Device) -> Result<Self, CompilerError> {
84        let physical_qubits: Vec<_> = device.usable_qubits().collect();
85        if physical_qubits.is_empty() {
86            return Err(CompilerError::InvalidInput(
87                "layout requires at least one usable physical qubit".to_string(),
88            ));
89        }
90
91        let usable: BTreeSet<_> = physical_qubits.iter().copied().collect();
92        let physical_index = build_physical_index(&physical_qubits);
93        let adjacency = build_undirected_adjacency(device.topology(), &physical_qubits, &usable);
94        let distances =
95            DistanceTable::from_adjacency(&physical_qubits, &physical_index, &adjacency);
96        let directed_couplings = collect_directed_couplings(device.topology(), &usable);
97        let (readout_errors, has_readout_error_data) =
98            collect_readout_errors(device, &physical_qubits)?;
99        let two_qubit_data = collect_two_qubit_data(device, &usable)?;
100
101        Ok(Self {
102            physical_qubits,
103            physical_index,
104            adjacency,
105            distances,
106            directed_couplings,
107            readout_errors,
108            two_qubit_gates: two_qubit_data.gates,
109            native_two_qubit_gates: two_qubit_data.native_gates,
110            has_readout_error_data,
111            has_native_two_qubit_capabilities: two_qubit_data.has_native_capabilities,
112            has_two_qubit_error_data: two_qubit_data.has_error_data,
113        })
114    }
115
116    /// Returns usable physical qubits in deterministic order.
117    pub fn physical_qubits(&self) -> &[PhysicalQubit] {
118        &self.physical_qubits
119    }
120
121    /// Returns all-pairs undirected shortest-path distances over usable qubits.
122    pub fn distances(&self) -> &DistanceTable {
123        &self.distances
124    }
125
126    pub(super) fn physical_index(&self, qubit: PhysicalQubit) -> Option<usize> {
127        self.physical_index.get(&qubit).copied()
128    }
129
130    pub(super) fn physical_at(&self, index: usize) -> Option<PhysicalQubit> {
131        self.physical_qubits.get(index).copied()
132    }
133
134    pub(super) fn distance_by_index(&self, a: usize, b: usize) -> Option<u32> {
135        self.distances.distance_by_index(a, b)
136    }
137
138    /// Returns the undirected shortest-path distance between two usable qubits.
139    ///
140    /// Returns `None` when either qubit is unusable or when the two usable
141    /// qubits are disconnected.
142    pub fn distance(&self, a: PhysicalQubit, b: PhysicalQubit) -> Option<u32> {
143        let a_index = self.physical_index(a)?;
144        let b_index = self.physical_index(b)?;
145        self.distance_by_index(a_index, b_index)
146    }
147
148    /// Returns whether two usable physical qubits are adjacent in either direction.
149    pub fn is_adjacent_undirected(&self, a: PhysicalQubit, b: PhysicalQubit) -> bool {
150        let Some(a_index) = self.physical_index(a) else {
151            return false;
152        };
153        let Some(b_index) = self.physical_index(b) else {
154            return false;
155        };
156        self.adjacency[a_index].contains(&b_index)
157    }
158
159    /// Returns every usable undirected topology edge exactly once as dense
160    /// physical indices.
161    pub(super) fn undirected_edges_by_index(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
162        self.adjacency
163            .iter()
164            .enumerate()
165            .flat_map(|(left, neighbors)| {
166                neighbors
167                    .iter()
168                    .copied()
169                    .filter(move |right| left < *right)
170                    .map(move |right| (left, right))
171            })
172    }
173
174    /// Returns the readout error for a physical qubit, if known.
175    pub fn readout_error(&self, qubit: PhysicalQubit) -> Option<f64> {
176        self.readout_errors.get(&qubit).copied()
177    }
178
179    pub(super) fn readout_error_by_index(&self, index: usize) -> Option<f64> {
180        self.physical_at(index)
181            .and_then(|qubit| self.readout_error(qubit))
182    }
183
184    /// Returns whether `gate` is a native capability on the directed edge.
185    pub fn supports_two_qubit_gate_directed(
186        &self,
187        control: PhysicalQubit,
188        target: PhysicalQubit,
189        gate: StandardGate,
190    ) -> bool {
191        self.two_qubit_gate_status(control, target, gate)
192            .is_supported()
193    }
194
195    /// Returns the calibrated error for `gate` on the directed edge.
196    ///
197    /// `None` means either that the gate is unsupported or that it is supported
198    /// without a known calibration. Use
199    /// [`Self::supports_two_qubit_gate_directed`] to distinguish those states.
200    pub fn two_qubit_gate_error_directed(
201        &self,
202        control: PhysicalQubit,
203        target: PhysicalQubit,
204        gate: StandardGate,
205    ) -> Option<f64> {
206        self.two_qubit_gate_status(control, target, gate).error()
207    }
208
209    pub(super) fn two_qubit_gate_status(
210        &self,
211        control: PhysicalQubit,
212        target: PhysicalQubit,
213        gate: StandardGate,
214    ) -> TwoQubitGateStatus {
215        self.two_qubit_gates
216            .get(&(control, target))
217            .and_then(|gates| gates.get(gate as usize))
218            .copied()
219            .unwrap_or_default()
220    }
221
222    pub(super) fn two_qubit_gate_status_by_index(
223        &self,
224        control: usize,
225        target: usize,
226        gate: StandardGate,
227    ) -> TwoQubitGateStatus {
228        match (self.physical_at(control), self.physical_at(target)) {
229            (Some(control), Some(target)) => self.two_qubit_gate_status(control, target, gate),
230            _ => TwoQubitGateStatus::Unsupported,
231        }
232    }
233
234    pub(super) fn is_two_qubit_gate_native_anywhere(&self, gate: StandardGate) -> bool {
235        self.native_two_qubit_gates
236            .get(gate as usize)
237            .copied()
238            .unwrap_or(false)
239    }
240
241    /// Returns whether there is a directed coupling from `control` to `target`.
242    pub fn supports_directed_coupling(
243        &self,
244        control: PhysicalQubit,
245        target: PhysicalQubit,
246    ) -> bool {
247        self.directed_couplings.contains(&(control, target))
248    }
249
250    pub(super) fn supports_directed_coupling_by_index(
251        &self,
252        control: usize,
253        target: usize,
254    ) -> bool {
255        match (self.physical_at(control), self.physical_at(target)) {
256            (Some(control), Some(target)) => self.supports_directed_coupling(control, target),
257            _ => false,
258        }
259    }
260
261    /// Returns whether any readout or two-qubit calibration data is available.
262    pub fn has_fidelity_data(&self) -> bool {
263        self.has_readout_error_data || self.has_two_qubit_error_data
264    }
265
266    /// Returns whether readout-error data is available.
267    pub fn has_readout_error_data(&self) -> bool {
268        self.has_readout_error_data
269    }
270
271    /// Returns whether any usable directed edge has native two-qubit capabilities.
272    pub(crate) fn has_native_two_qubit_capabilities(&self) -> bool {
273        self.has_native_two_qubit_capabilities
274    }
275
276    /// Returns whether two-qubit error data is available.
277    pub fn has_two_qubit_error_data(&self) -> bool {
278        self.has_two_qubit_error_data
279    }
280}
281
282/// All-pairs undirected shortest-path distances over usable physical qubits.
283///
284/// A missing distance means the pair is disconnected in the usable undirected
285/// topology or at least one queried qubit is outside this table.
286#[derive(Debug, Clone, PartialEq)]
287pub struct DistanceTable {
288    qubits: Vec<PhysicalQubit>,
289    index: BTreeMap<PhysicalQubit, usize>,
290    distances: Vec<Vec<Option<u32>>>,
291}
292
293impl DistanceTable {
294    /// Builds all-pairs distances by BFS from every usable physical qubit.
295    fn from_adjacency(
296        qubits: &[PhysicalQubit],
297        index: &BTreeMap<PhysicalQubit, usize>,
298        adjacency: &[Vec<usize>],
299    ) -> Self {
300        let mut distances = vec![vec![None; qubits.len()]; qubits.len()];
301
302        for (start_index, item) in distances.iter_mut().enumerate().take(qubits.len()) {
303            let mut queue = VecDeque::new();
304            item[start_index] = Some(0);
305            queue.push_back(start_index);
306
307            while let Some(current_index) = queue.pop_front() {
308                let current_distance =
309                    item[current_index].expect("queued nodes have assigned distances");
310
311                for neighbor_index in adjacency[current_index].iter().copied() {
312                    if item[neighbor_index].is_none() {
313                        item[neighbor_index] = Some(current_distance + 1);
314                        queue.push_back(neighbor_index);
315                    }
316                }
317            }
318        }
319
320        Self {
321            qubits: qubits.to_vec(),
322            index: index.clone(),
323            distances,
324        }
325    }
326
327    /// Returns the physical qubits covered by this table.
328    pub fn qubits(&self) -> &[PhysicalQubit] {
329        &self.qubits
330    }
331
332    /// Returns the shortest-path distance between two physical qubits.
333    ///
334    /// Returns `None` when either qubit is absent from the table or the pair is
335    /// disconnected.
336    pub fn distance(&self, a: PhysicalQubit, b: PhysicalQubit) -> Option<u32> {
337        let a_index = self.index.get(&a)?;
338        let b_index = self.index.get(&b)?;
339        self.distance_by_index(*a_index, *b_index)
340    }
341
342    pub(super) fn distance_by_index(&self, a: usize, b: usize) -> Option<u32> {
343        self.distances.get(a)?.get(b).copied().flatten()
344    }
345}
346
347fn build_physical_index(physical_qubits: &[PhysicalQubit]) -> BTreeMap<PhysicalQubit, usize> {
348    physical_qubits
349        .iter()
350        .copied()
351        .enumerate()
352        .map(|(index, qubit)| (qubit, index))
353        .collect()
354}
355
356fn build_undirected_adjacency(
357    topology: &Topology,
358    physical_qubits: &[PhysicalQubit],
359    usable: &BTreeSet<PhysicalQubit>,
360) -> Vec<Vec<usize>> {
361    let index = build_physical_index(physical_qubits);
362    let mut adjacency = vec![Vec::new(); physical_qubits.len()];
363
364    for qubit in physical_qubits {
365        let qubit_index = index[qubit];
366        for neighbor in topology.successors(*qubit) {
367            if usable.contains(&neighbor) {
368                adjacency[qubit_index].push(index[&neighbor]);
369            }
370        }
371        for neighbor in topology.predecessors(*qubit) {
372            if usable.contains(&neighbor) {
373                adjacency[qubit_index].push(index[&neighbor]);
374            }
375        }
376    }
377
378    for neighbors in &mut adjacency {
379        neighbors.sort_unstable_by_key(|index| physical_qubits[*index]);
380        neighbors.dedup();
381    }
382
383    adjacency
384}
385
386fn collect_readout_errors(
387    device: &Device,
388    physical_qubits: &[PhysicalQubit],
389) -> Result<(BTreeMap<PhysicalQubit, f64>, bool), CompilerError> {
390    let mut errors = BTreeMap::new();
391    for qubit in physical_qubits {
392        if let Some(error) = device.get_readout_error(*qubit) {
393            validate_probability(error, "readout error")?;
394            errors.insert(*qubit, error);
395        }
396    }
397    let has_data = !errors.is_empty();
398    Ok((errors, has_data))
399}
400
401fn collect_directed_couplings(
402    topology: &Topology,
403    usable: &BTreeSet<PhysicalQubit>,
404) -> BTreeSet<DirectedQubitPair> {
405    let mut couplings = BTreeSet::new();
406    for control in usable {
407        for target in topology.successors(*control) {
408            if usable.contains(&target) {
409                couplings.insert((*control, target));
410            }
411        }
412    }
413    couplings
414}
415
416struct TwoQubitData {
417    gates: TwoQubitGateMap,
418    native_gates: Vec<bool>,
419    has_native_capabilities: bool,
420    has_error_data: bool,
421}
422
423fn collect_two_qubit_data(
424    device: &Device,
425    usable: &BTreeSet<PhysicalQubit>,
426) -> Result<TwoQubitData, CompilerError> {
427    if let Some(error) = device.default_two_qubit_error() {
428        validate_probability(error, "default two-qubit error")?;
429    }
430
431    let mut gates = BTreeMap::new();
432    let mut native_gates = vec![false; StandardGate::all().len()];
433    let mut has_native_capabilities = false;
434    let mut has_error_data = false;
435    for control in usable {
436        for target in device.topology().successors(*control) {
437            if !usable.contains(&target) {
438                continue;
439            }
440
441            let edge = device.edge_properties(*control, target);
442            if let Some(edge) = edge {
443                for property in edge.native_instructions() {
444                    validate_probability(property.error_rate(), "edge two-qubit error")?;
445                }
446            }
447
448            let mut edge_gates = None;
449            for gate in StandardGate::all()
450                .iter()
451                .copied()
452                .filter(|gate| gate.num_qubits() == 2)
453            {
454                let instruction = Instruction::Standard(gate);
455                if !device.supports_native_instruction(&instruction, &[*control, target]) {
456                    continue;
457                }
458
459                has_native_capabilities = true;
460                native_gates[gate as usize] = true;
461                let status = match device.two_qubit_error(*control, target, &instruction) {
462                    Some(error) => {
463                        validate_probability(error, "gate-specific two-qubit error")?;
464                        has_error_data = true;
465                        TwoQubitGateStatus::Calibrated(error)
466                    }
467                    None => TwoQubitGateStatus::Uncalibrated,
468                };
469                edge_gates.get_or_insert_with(|| {
470                    vec![TwoQubitGateStatus::Unsupported; StandardGate::all().len()]
471                })[gate as usize] = status;
472            }
473            if let Some(edge_gates) = edge_gates {
474                gates.insert((*control, target), edge_gates);
475            }
476        }
477    }
478
479    Ok(TwoQubitData {
480        gates,
481        native_gates,
482        has_native_capabilities,
483        has_error_data,
484    })
485}
486
487fn validate_probability(value: f64, name: &str) -> Result<(), CompilerError> {
488    if value.is_finite() && (0.0..=1.0).contains(&value) {
489        Ok(())
490    } else {
491        Err(CompilerError::InvalidInput(format!(
492            "device {} must be a finite probability in [0, 1], got {}",
493            name, value
494        )))
495    }
496}