Skip to main content

cqlib_core/device/
device_impl.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//! Quantum Device Hardware Characteristics and Topology.
14//!
15//! This module defines the core data structures used to represent a target quantum backend
16//! within the CQLib compiler. These structures (`Device`, `QubitProp`, `EdgeProp`, `InstructionProp`)
17//! encapsulate all the physical constraints and fidelity data necessary for noise-aware compilation,
18//! mapping, routing, and circuit scheduling.
19
20use crate::circuit::{
21    Circuit, ClassicalControlOp, Directive, Instruction, Operation, Qubit, StandardGate,
22    ValueClassicalControlOp, ValueInstruction, ValueOperation,
23};
24use crate::device::topology::Topology;
25use crate::device::{DeviceError, DeviceValidationError, PhysicalQubit};
26use std::collections::{BTreeSet, HashMap, HashSet};
27use std::ops::RangeInclusive;
28use time::OffsetDateTime;
29
30/// Checks that an instruction can be represented at a native-capability location.
31///
32/// The current device model resolves only atomic standard gates and represents
33/// physical placement only for individual qubits and directed two-qubit edges.
34fn validate_native_gate_arity(
35    instruction: &Instruction,
36    expected: RangeInclusive<usize>,
37) -> Result<(), DeviceError> {
38    let Instruction::Standard(gate) = instruction else {
39        return Err(DeviceError::NonStandardNativeInstruction {
40            instruction: instruction.to_string(),
41        });
42    };
43    let actual = gate.num_qubits();
44    if expected.contains(&actual) {
45        Ok(())
46    } else {
47        Err(DeviceError::InvalidNativeInstructionArity {
48            instruction: instruction.to_string(),
49            expected,
50            actual,
51        })
52    }
53}
54
55/// Represents the physical properties and execution characteristics of a quantum instruction (gate)
56/// when applied to specific qubit(s).
57///
58/// This structure stores crucial calibration data such as the error rate (fidelity) and the
59/// execution duration of the gate. This information is vital for noise-aware compilation,
60/// gate scheduling, and duration-based dynamical decoupling.
61#[derive(Debug, Clone)]
62pub struct InstructionProp {
63    /// The instruction (gate) being characterized.
64    instruction: Instruction,
65    /// Error rate for this instruction on the specific qubit(s), range [0.0, 1.0].
66    error_rate: f64,
67    /// Gate duration in nanoseconds.
68    length: Option<f64>,
69}
70
71impl InstructionProp {
72    /// Creates a new `InstructionProp`.
73    pub fn new(instruction: Instruction, error_rate: f64) -> Self {
74        Self {
75            instruction,
76            error_rate,
77            length: None,
78        }
79    }
80
81    /// Sets the gate duration (in nanoseconds) using the builder pattern.
82    pub fn with_length(mut self, length: f64) -> Self {
83        self.length = Some(length);
84        self
85    }
86
87    pub fn set_length(&mut self, length: f64) {
88        self.length = Some(length);
89    }
90    pub fn with_instruction(mut self, instruction: Instruction) -> Self {
91        self.instruction = instruction;
92        self
93    }
94
95    pub fn set_instruction(&mut self, instruction: Instruction) {
96        self.instruction = instruction;
97    }
98
99    pub fn with_error_rate(mut self, error_rate: f64) -> Self {
100        self.error_rate = error_rate;
101        self
102    }
103
104    pub fn set_error_rate(&mut self, error_rate: f64) {
105        self.error_rate = error_rate;
106    }
107
108    /// Gets a reference to the underlying instruction.
109    pub fn instruction(&self) -> &Instruction {
110        &self.instruction
111    }
112
113    /// Gets the error rate of this instruction.
114    pub fn error_rate(&self) -> f64 {
115        self.error_rate
116    }
117
118    /// Gets the duration of this instruction in nanoseconds, if defined.
119    pub fn length(&self) -> Option<f64> {
120        self.length
121    }
122
123    fn validate(&self) -> Result<(), DeviceError> {
124        if !(self.error_rate.is_finite() && (0.0..=1.0).contains(&self.error_rate)) {
125            return Err(DeviceError::InvalidNativeInstructionErrorRate {
126                instruction: self.instruction.to_string(),
127                value: format!("{:?}", self.error_rate),
128            });
129        }
130        if let Some(length) = self.length
131            && !(length.is_finite() && length >= 0.0)
132        {
133            return Err(DeviceError::InvalidNativeInstructionDuration {
134                instruction: self.instruction.to_string(),
135                value: format!("{length:?}"),
136            });
137        }
138        Ok(())
139    }
140}
141
142/// Represents the physical and operational properties of a single quantum qubit.
143///
144/// This includes coherence metrics (T1 relaxation time, T2 dephasing time), operational frequency,
145/// and measurement error rates. It also maintains a list of `InstructionProp` which defines
146/// the specific native single-qubit instructions supported by this qubit along with their
147/// calibrated fidelities and durations.
148#[derive(Debug, Clone)]
149pub struct QubitProp {
150    /// Readout error rate, range [0.0, 1.0].
151    readout_error: f64,
152    /// Prob of measuring 0 given state was prepared in 1 (p0|1)
153    prob_meas0_prep1: Option<f64>,
154    /// Prob of measuring 1 given state was prepared in 0 (p1|0)
155    prob_meas1_prep0: Option<f64>,
156
157    /// T1 relaxation time in microseconds.
158    t1: Option<f64>,
159    /// T2 dephasing time in microseconds.
160    t2: Option<f64>,
161    /// Qubit frequency in GHz.
162    frequency: Option<f64>,
163    /// Native instructions supported on this qubit.
164    ///
165    /// An empty list inherits matching one-qubit instructions from the device
166    /// defaults. A non-empty list completely overrides those defaults for this
167    /// qubit.
168    native_instructions: Vec<InstructionProp>,
169}
170
171impl QubitProp {
172    /// Creates a new `QubitProp` with the specified readout error rate.
173    pub fn new(readout_error: f64) -> Self {
174        Self {
175            readout_error,
176            prob_meas0_prep1: None,
177            prob_meas1_prep0: None,
178            t1: None,
179            t2: None,
180            frequency: None,
181            native_instructions: Vec::new(),
182        }
183    }
184
185    /// Sets the probability of measuring 0 given the state was prepared in 1.
186    pub fn with_prob_meas0_prep1(mut self, prob: f64) -> Self {
187        self.prob_meas0_prep1 = Some(prob);
188        self
189    }
190    pub fn set_prob_meas0_prep1(&mut self, prob: f64) {
191        self.prob_meas0_prep1 = Some(prob);
192    }
193
194    /// Sets the probability of measuring 1 given the state was prepared in 0.
195    pub fn with_prob_meas1_prep0(mut self, prob: f64) -> Self {
196        self.prob_meas1_prep0 = Some(prob);
197        self
198    }
199    pub fn set_prob_meas1_prep0(&mut self, prob: f64) {
200        self.prob_meas1_prep0 = Some(prob);
201    }
202
203    /// Sets the T1 relaxation time in microseconds.
204    pub fn with_t1(mut self, t1: f64) -> Self {
205        self.t1 = Some(t1);
206        self
207    }
208
209    pub fn set_t1(&mut self, t1: f64) {
210        self.t1 = Some(t1);
211    }
212
213    /// Sets the T2 dephasing time in microseconds.
214    pub fn with_t2(mut self, t2: f64) -> Self {
215        self.t2 = Some(t2);
216        self
217    }
218    pub fn set_t2(&mut self, t2: f64) {
219        self.t2 = Some(t2);
220    }
221
222    /// Sets the qubit frequency in GHz.
223    pub fn with_frequency(mut self, frequency: f64) -> Self {
224        self.frequency = Some(frequency);
225        self
226    }
227
228    pub fn set_frequency(&mut self, frequency: f64) {
229        self.frequency = Some(frequency);
230    }
231
232    /// Adds a single-qubit standard gate to this qubit's native capabilities.
233    ///
234    /// # Errors
235    ///
236    /// Returns [`DeviceError`] if `prop` does not describe a single-qubit
237    /// standard gate.
238    pub fn with_native_instruction(mut self, prop: InstructionProp) -> Result<Self, DeviceError> {
239        self.set_native_instruction(prop)?;
240        Ok(self)
241    }
242    /// Appends a single-qubit standard gate, preserving the list on error.
243    pub fn set_native_instruction(&mut self, prop: InstructionProp) -> Result<(), DeviceError> {
244        validate_native_gate_arity(prop.instruction(), 1..=1)?;
245        prop.validate()?;
246        self.native_instructions.push(prop);
247        Ok(())
248    }
249
250    /// Gets the readout error rate.
251    pub fn readout_error(&self) -> f64 {
252        self.readout_error
253    }
254
255    /// Gets the probability of measuring 0 given the state was prepared in 1 (p0|1).
256    pub fn prob_meas0_prep1(&self) -> Option<f64> {
257        self.prob_meas0_prep1
258    }
259
260    /// Gets the probability of measuring 1 given the state was prepared in 0 (p1|0).
261    pub fn prob_meas1_prep0(&self) -> Option<f64> {
262        self.prob_meas1_prep0
263    }
264
265    /// Gets the T1 relaxation time in microseconds, if defined.
266    pub fn t1(&self) -> Option<f64> {
267        self.t1
268    }
269
270    /// Gets the T2 dephasing time in microseconds, if defined.
271    pub fn t2(&self) -> Option<f64> {
272        self.t2
273    }
274
275    /// Gets the qubit frequency in GHz, if defined.
276    pub fn frequency(&self) -> Option<f64> {
277        self.frequency
278    }
279
280    /// Gets a slice of the native instructions supported on this qubit.
281    pub fn native_instructions(&self) -> &[InstructionProp] {
282        &self.native_instructions
283    }
284}
285
286/// Represents the physical properties of a coupling edge between two qubits in the device topology.
287///
288/// This structure primarily tracks the native multi-qubit instructions (e.g., CX, CZ)
289/// supported across this specific physical connection, including their directional
290/// error rates and execution times.
291#[derive(Debug, Clone)]
292pub struct EdgeProp {
293    /// Native instructions supported on this directed edge (typically 2-qubit gates).
294    ///
295    /// An empty list inherits matching two-qubit instructions from the device
296    /// defaults. A non-empty list completely overrides those defaults for this
297    /// ordered edge and does not grant support in the reverse direction.
298    native_instructions: Vec<InstructionProp>,
299}
300
301/// Result of resolving one instruction against a local override and device defaults.
302enum NativeInstructionSupport<'a> {
303    /// The instruction is explicitly supported and has local calibration.
304    Explicit(&'a InstructionProp),
305    /// The instruction is inherited from the device-wide defaults.
306    Inherited,
307    /// Neither the local override nor the defaults support the instruction.
308    Unsupported,
309}
310
311/// Optional calibration attached to one exact native instruction capability.
312#[derive(Debug, Clone, Copy, Default, PartialEq)]
313pub(crate) struct NativeInstructionCalibration {
314    pub(crate) error_rate: Option<f64>,
315    pub(crate) duration: Option<f64>,
316}
317
318impl EdgeProp {
319    /// Creates a new empty edge property.
320    pub fn new() -> Self {
321        Self {
322            native_instructions: Vec::new(),
323        }
324    }
325
326    /// Adds a two-qubit standard gate to this edge's native capabilities.
327    ///
328    /// # Errors
329    ///
330    /// Returns [`DeviceError`] if `prop` does not describe a two-qubit
331    /// standard gate.
332    pub fn with_native_instruction(mut self, prop: InstructionProp) -> Result<Self, DeviceError> {
333        self.set_native_instruction(prop)?;
334        Ok(self)
335    }
336
337    /// Appends a two-qubit standard gate, preserving the list on error.
338    pub fn set_native_instruction(&mut self, prop: InstructionProp) -> Result<(), DeviceError> {
339        validate_native_gate_arity(prop.instruction(), 2..=2)?;
340        prop.validate()?;
341        self.native_instructions.push(prop);
342        Ok(())
343    }
344
345    /// Gets a slice of the native instructions supported on this edge.
346    pub fn native_instructions(&self) -> &[InstructionProp] {
347        &self.native_instructions
348    }
349}
350
351impl Default for EdgeProp {
352    fn default() -> Self {
353        Self::new()
354    }
355}
356
357/// Represents a quantum device's hardware characteristics and topology.
358///
359/// The `Device` struct is a fundamental component for compiler optimization, mapping,
360/// routing, and noise-aware scheduling. It encapsulates:
361/// - The physical connectivity (`Topology`) between qubits.
362/// - Available and faulty qubits.
363/// - Device-wide default parameters (e.g., T1, T2, gate error rates).
364/// - Specific physical properties and supported instructions for individual qubits
365///   and coupling edges.
366///
367/// This structure provides the necessary physical constraints and fidelity data
368/// required to simulate noise models or compile quantum circuits onto realistic
369/// backend hardware.
370///
371/// # Example
372///
373/// ```rust
374/// use std::collections::HashSet;
375/// use cqlib_core::device::{Device, PhysicalQubit, QubitProp, Topology};
376///
377/// // Create a 2-qubit topology
378/// let q0 = PhysicalQubit::new(0);
379/// let q1 = PhysicalQubit::new(1);
380/// let topo = Topology::new(vec![q0, q1], vec![(q0, q1, "G1".to_string())]).unwrap();
381///
382/// // Initialize a device with defaults
383/// let mut device = Device::new("mock_device", HashSet::from_iter([q0, q1]), topo).unwrap()
384///     .with_default_t1(50.0)
385///     .with_default_t2(25.0)
386///     .with_default_readout_error(0.01);
387///
388/// // Add specific properties for Qubit 0
389/// let q0_prop = QubitProp::new(0.05).with_t1(40.0);
390/// device.add_qubit_properties(q0, q0_prop).unwrap();
391///
392/// // Query T1, using specific properties if available, else fallback to defaults
393/// assert_eq!(device.get_t1(q0), Some(40.0));
394/// assert_eq!(device.get_t1(q1), Some(50.0));
395/// ```
396#[derive(Debug, Clone)]
397pub struct Device {
398    name: String,
399    /// Physical qubits registered with the device.
400    qubits: BTreeSet<PhysicalQubit>,
401    /// Offline or faulty qubits.
402    invalid_qubits: BTreeSet<PhysicalQubit>,
403    /// Connectivity topology.
404    topology: Topology,
405    /// Device-wide default native gates.
406    ///
407    /// Defaults apply by instruction arity when a qubit or directed edge has
408    /// no non-empty local native-instruction override. Local capabilities are
409    /// not synchronized into this list.
410    native_gates: Vec<Instruction>,
411
412    /// System calibration timestamp.
413    calibration_time: Option<OffsetDateTime>,
414    /// Default T1 time (μs) for qubits without specific data.
415    default_t1: Option<f64>,
416    /// Default T2 time (μs) for qubits without specific data.
417    default_t2: Option<f64>,
418    /// Default readout error for qubits without specific data.
419    default_readout_error: Option<f64>,
420    /// Default single-qubit gate error.
421    default_single_qubit_error: Option<f64>,
422    /// Default two-qubit gate error.
423    default_two_qubit_error: Option<f64>,
424
425    /// Per-qubit properties (T1, T2, readout error, native gates).
426    qubit_properties: HashMap<PhysicalQubit, QubitProp>,
427    /// Per-edge properties (gate fidelities on specific couplings).
428    edge_properties: HashMap<(PhysicalQubit, PhysicalQubit), EdgeProp>,
429}
430
431impl Device {
432    /// Creates a new `Device` with the specified name and topology.
433    pub fn new(
434        name: impl Into<String>,
435        qubits: HashSet<PhysicalQubit>,
436        topology: Topology,
437    ) -> Result<Self, DeviceError> {
438        for q in topology.qubits() {
439            if !qubits.contains(&q) {
440                return Err(DeviceError::InvalidOnlineQubit(q));
441            }
442        }
443
444        Ok(Self {
445            name: name.into(),
446            qubits: qubits.into_iter().collect(),
447            invalid_qubits: BTreeSet::new(),
448            topology,
449            native_gates: Vec::new(),
450            calibration_time: None,
451            default_t1: None,
452            default_t2: None,
453            default_readout_error: None,
454            default_single_qubit_error: None,
455            default_two_qubit_error: None,
456            qubit_properties: HashMap::new(),
457            edge_properties: HashMap::new(),
458        })
459    }
460
461    /// Creates a device with physical qubits connected as a directed line.
462    ///
463    /// The device contains physical qubits `0..num_qubits`, all of which are
464    /// online. Native gates and calibration data are left unset and may be
465    /// configured with the builder-style setters.
466    pub fn line(name: impl Into<String>, num_qubits: u32) -> Result<Self, DeviceError> {
467        let physical_qubits = (0..num_qubits).map(PhysicalQubit::new).collect::<Vec<_>>();
468        Self::line_from_qubits(name, physical_qubits)
469    }
470
471    /// Creates a device with the supplied physical qubits connected as a directed line.
472    ///
473    /// The device contains every supplied physical qubit, all of which are
474    /// online. Couplings follow the supplied order: `qubits[i] -> qubits[i + 1]`.
475    pub fn line_from_qubits(
476        name: impl Into<String>,
477        physical_qubits: Vec<PhysicalQubit>,
478    ) -> Result<Self, DeviceError> {
479        let qubits = physical_qubits.iter().copied().collect::<HashSet<_>>();
480        let topology = Topology::line(physical_qubits).map_err(DeviceError::InvalidTopology)?;
481        Self::new(name, qubits, topology)
482    }
483
484    /// Creates a device with physical qubits connected as a bidirectional line.
485    ///
486    /// The device contains physical qubits `0..num_qubits`, all of which are
487    /// online. Adjacent qubits are connected in both directions.
488    pub fn bidirectional_line(
489        name: impl Into<String>,
490        num_qubits: u32,
491    ) -> Result<Self, DeviceError> {
492        let edges = (0..num_qubits.saturating_sub(1))
493            .flat_map(|index| [(index, index + 1), (index + 1, index)])
494            .collect::<Vec<_>>();
495        Self::from_u32_edges(name, num_qubits, &edges)
496    }
497
498    /// Creates a device with physical qubits connected as a bidirectional ring.
499    ///
500    /// The device contains physical qubits `0..num_qubits`, all of which are
501    /// online. For two or more qubits, each qubit is connected to its successor
502    /// modulo `num_qubits` in both directions.
503    pub fn ring(name: impl Into<String>, num_qubits: u32) -> Result<Self, DeviceError> {
504        let mut edges = BTreeSet::new();
505        if num_qubits >= 2 {
506            for index in 0..num_qubits {
507                let next = (index + 1) % num_qubits;
508                edges.insert((index, next));
509                edges.insert((next, index));
510            }
511        }
512        let edges = edges.into_iter().collect::<Vec<_>>();
513        Self::from_u32_edges(name, num_qubits, &edges)
514    }
515
516    /// Creates a device with physical qubits connected as a bidirectional star.
517    ///
518    /// The device contains physical qubits `0..num_qubits`, all of which are
519    /// online. Every non-center qubit is connected to `center` in both
520    /// directions.
521    pub fn star(
522        name: impl Into<String>,
523        num_qubits: u32,
524        center: u32,
525    ) -> Result<Self, DeviceError> {
526        let edges = (0..num_qubits)
527            .filter(|&index| index != center)
528            .flat_map(|index| [(center, index), (index, center)])
529            .collect::<Vec<_>>();
530        Self::from_u32_edges(name, num_qubits, &edges)
531    }
532
533    /// Creates a device with physical qubits connected as a bidirectional grid.
534    ///
535    /// Qubit IDs are assigned in row-major order. Horizontal and vertical
536    /// nearest-neighbor couplings are added in both directions.
537    pub fn grid(name: impl Into<String>, rows: u32, cols: u32) -> Result<Self, DeviceError> {
538        let num_qubits = rows.saturating_mul(cols);
539        let mut edges = Vec::new();
540        for row in 0..rows {
541            for col in 0..cols {
542                let current = row * cols + col;
543                if col + 1 < cols {
544                    edges.push((current, current + 1));
545                    edges.push((current + 1, current));
546                }
547                if row + 1 < rows {
548                    edges.push((current, current + cols));
549                    edges.push((current + cols, current));
550                }
551            }
552        }
553        Self::from_u32_edges(name, num_qubits, &edges)
554    }
555
556    /// Creates a device with physical qubits `0..num_qubits` and explicit directed edges.
557    ///
558    /// Each `(control, target)` pair in `edges` becomes one directed coupling.
559    pub fn from_edges(
560        name: impl Into<String>,
561        num_qubits: u32,
562        edges: &[(u32, u32)],
563    ) -> Result<Self, DeviceError> {
564        Self::from_u32_edges(name, num_qubits, edges)
565    }
566
567    fn from_u32_edges(
568        name: impl Into<String>,
569        num_qubits: u32,
570        edges: &[(u32, u32)],
571    ) -> Result<Self, DeviceError> {
572        let physical_qubits = (0..num_qubits).map(PhysicalQubit::new).collect::<Vec<_>>();
573        let qubits = physical_qubits.iter().copied().collect::<HashSet<_>>();
574        let coupling_map = edges
575            .iter()
576            .enumerate()
577            .map(|(index, &(control, target))| {
578                (
579                    PhysicalQubit::new(control),
580                    PhysicalQubit::new(target),
581                    format!("e{index}"),
582                )
583            })
584            .collect::<Vec<_>>();
585        let topology =
586            Topology::new(physical_qubits, coupling_map).map_err(DeviceError::InvalidTopology)?;
587        Self::new(name, qubits, topology)
588    }
589
590    /// Sets the offline or faulty physical qubits using the builder pattern.
591    ///
592    /// # Errors
593    ///
594    /// Returns [`DeviceError::QubitNotInDevice`] if any invalid qubit is not
595    /// registered with the device.
596    pub fn with_invalid_qubits(
597        mut self,
598        invalid_qubits: HashSet<PhysicalQubit>,
599    ) -> Result<Self, DeviceError> {
600        self.set_invalid_qubits(invalid_qubits)?;
601        Ok(self)
602    }
603
604    /// Sets the offline or faulty physical qubits.
605    ///
606    /// # Errors
607    ///
608    /// Returns [`DeviceError::QubitNotInDevice`] if any invalid qubit is not
609    /// registered with the device. The existing set is preserved on error.
610    pub fn set_invalid_qubits(
611        &mut self,
612        invalid_qubits: HashSet<PhysicalQubit>,
613    ) -> Result<(), DeviceError> {
614        for &qubit in &invalid_qubits {
615            if !self.qubits.contains(&qubit) {
616                return Err(DeviceError::QubitNotInDevice(qubit));
617            }
618        }
619        self.invalid_qubits = invalid_qubits.into_iter().collect();
620        Ok(())
621    }
622
623    /// Sets the device-wide default native gates.
624    ///
625    /// This does not add, remove, or otherwise synchronize native instructions
626    /// stored on individual qubits or directed edges.
627    ///
628    /// # Errors
629    ///
630    /// Returns [`DeviceError`] if any entry is not a standard gate or acts on
631    /// more than two qubits.
632    pub fn with_native_gates(mut self, gates: Vec<Instruction>) -> Result<Self, DeviceError> {
633        self.set_native_gates(gates)?;
634        Ok(self)
635    }
636
637    /// Replaces the device-wide native defaults after validating the full list.
638    ///
639    /// The existing defaults are preserved when any entry is invalid.
640    ///
641    /// # Errors
642    ///
643    /// Returns [`DeviceError`] if any entry is not a standard gate or acts on
644    /// more than two qubits.
645    pub fn set_native_gates(&mut self, gates: Vec<Instruction>) -> Result<(), DeviceError> {
646        for gate in &gates {
647            validate_native_gate_arity(gate, 0..=2)?;
648        }
649        self.native_gates = gates;
650        Ok(())
651    }
652
653    /// Sets the system calibration timestamp.
654    pub fn with_calibration_time(mut self, time: OffsetDateTime) -> Self {
655        self.calibration_time = Some(time);
656        self
657    }
658
659    pub fn set_calibration_time(&mut self, time: OffsetDateTime) {
660        self.calibration_time = Some(time);
661    }
662
663    /// Sets the default T1 time (μs).
664    pub fn with_default_t1(mut self, t1: f64) -> Self {
665        self.default_t1 = Some(t1);
666        self
667    }
668
669    pub fn set_default_t1(&mut self, t1: f64) {
670        self.default_t1 = Some(t1);
671    }
672
673    /// Sets the default T2 time (μs).
674    pub fn with_default_t2(mut self, t2: f64) -> Self {
675        self.default_t2 = Some(t2);
676        self
677    }
678
679    pub fn set_default_t2(&mut self, t2: f64) {
680        self.default_t2 = Some(t2);
681    }
682
683    /// Sets the default readout error rate.
684    pub fn with_default_readout_error(mut self, error: f64) -> Self {
685        self.default_readout_error = Some(error);
686        self
687    }
688
689    pub fn set_default_readout_error(&mut self, error: f64) {
690        self.default_readout_error = Some(error);
691    }
692
693    /// Sets the default single-qubit gate error rate.
694    pub fn with_default_single_qubit_error(mut self, error: f64) -> Self {
695        self.default_single_qubit_error = Some(error);
696        self
697    }
698
699    pub fn set_default_single_qubit_error(&mut self, error: f64) {
700        self.default_single_qubit_error = Some(error);
701    }
702
703    /// Sets the default two-qubit gate error rate.
704    pub fn with_default_two_qubit_error(mut self, error: f64) -> Self {
705        self.default_two_qubit_error = Some(error);
706        self
707    }
708
709    pub fn set_default_two_qubit_error(&mut self, error: f64) {
710        self.default_two_qubit_error = Some(error);
711    }
712
713    /// Adds properties for a specific qubit.
714    ///
715    /// # Errors
716    ///
717    /// Returns [`DeviceError::QubitNotInDevice`] if the qubit is not registered
718    /// with the device, or [`DeviceError::QubitNotInTopology`] if it is
719    /// registered but absent from the topology. Properties may be retained for
720    /// a registered qubit while it is marked invalid/offline.
721    pub fn add_qubit_properties(
722        &mut self,
723        qubit: PhysicalQubit,
724        props: QubitProp,
725    ) -> Result<(), DeviceError> {
726        if !self.qubits.contains(&qubit) {
727            return Err(DeviceError::QubitNotInDevice(qubit));
728        }
729        if !self.topology.contains_qubit(&qubit) {
730            return Err(DeviceError::QubitNotInTopology(qubit));
731        }
732        self.qubit_properties.insert(qubit, props);
733        Ok(())
734    }
735
736    /// Adds properties for a specific coupling edge.
737    ///
738    /// # Errors
739    ///
740    /// Returns `DeviceError::EdgeNotInTopology` if the edge is not in the device's topology.
741    pub fn add_edge_properties(
742        &mut self,
743        control: PhysicalQubit,
744        target: PhysicalQubit,
745        props: EdgeProp,
746    ) -> Result<(), DeviceError> {
747        if !self.topology.supports_directed_coupling(control, target) {
748            return Err(DeviceError::EdgeNotInTopology(control, target));
749        }
750        self.edge_properties.insert((control, target), props);
751        Ok(())
752    }
753
754    /// Compares instruction identities supported by the current device model.
755    ///
756    /// Device-native capabilities are currently limited to standard gates;
757    /// composite and runtime instructions are handled by circuit validation.
758    fn instruction_matches(stored: &Instruction, requested: &Instruction) -> bool {
759        match (stored, requested) {
760            (Instruction::Standard(stored), Instruction::Standard(requested)) => {
761                stored == requested
762            }
763            _ => false,
764        }
765    }
766
767    /// Resolves the shared local-override/default-inheritance capability contract.
768    ///
769    /// A non-empty local list is authoritative. An absent or empty local list
770    /// inherits the device defaults. Calibration consumers retain the explicit
771    /// property through the result instead of repeating capability resolution.
772    fn resolve_native_instruction<'a>(
773        &'a self,
774        local: Option<&'a [InstructionProp]>,
775        instruction: &Instruction,
776    ) -> NativeInstructionSupport<'a> {
777        if let Some(local) = local.filter(|instructions| !instructions.is_empty()) {
778            return local
779                .iter()
780                .find(|prop| Self::instruction_matches(prop.instruction(), instruction))
781                .map_or(
782                    NativeInstructionSupport::Unsupported,
783                    NativeInstructionSupport::Explicit,
784                );
785        }
786
787        if self
788            .native_gates
789            .iter()
790            .any(|stored| Self::instruction_matches(stored, instruction))
791        {
792            NativeInstructionSupport::Inherited
793        } else {
794            NativeInstructionSupport::Unsupported
795        }
796    }
797
798    /// Gets the name of the device.
799    pub fn name(&self) -> &str {
800        &self.name
801    }
802
803    /// Gets an iterator over the physical qubits registered with the device.
804    pub fn qubits(&self) -> impl Iterator<Item = PhysicalQubit> + '_ {
805        self.qubits.iter().copied()
806    }
807
808    /// Gets an iterator over the invalid (offline/faulty) qubits.
809    pub fn invalid_qubits(&self) -> impl Iterator<Item = PhysicalQubit> + '_ {
810        self.invalid_qubits.iter().copied()
811    }
812
813    /// Returns whether a physical qubit is registered and not marked invalid.
814    pub fn is_usable_qubit(&self, qubit: PhysicalQubit) -> bool {
815        self.qubits.contains(&qubit) && !self.invalid_qubits.contains(&qubit)
816    }
817
818    /// Gets an iterator over registered physical qubits that are not invalid.
819    pub fn usable_qubits(&self) -> impl Iterator<Item = PhysicalQubit> + '_ {
820        self.qubits.difference(&self.invalid_qubits).copied()
821    }
822
823    /// Returns the number of registered physical qubits that are not invalid.
824    pub fn num_usable_qubits(&self) -> usize {
825        self.qubits.len() - self.invalid_qubits.len()
826    }
827
828    /// Gets a reference to the device's connectivity topology.
829    pub fn topology(&self) -> &Topology {
830        &self.topology
831    }
832
833    /// Gets the default native gates supported by the device.
834    pub fn native_gates(&self) -> &[Instruction] {
835        &self.native_gates
836    }
837
838    /// Gets the properties of a specific qubit.
839    pub fn qubit_properties(&self, qubit: PhysicalQubit) -> Option<&QubitProp> {
840        self.qubit_properties.get(&qubit)
841    }
842
843    /// Gets the properties of a specific coupling edge.
844    pub fn edge_properties(
845        &self,
846        control: PhysicalQubit,
847        target: PhysicalQubit,
848    ) -> Option<&EdgeProp> {
849        self.edge_properties.get(&(control, target))
850    }
851
852    /// Returns whether a native hardware instruction can execute on the exact
853    /// ordered physical qargs.
854    ///
855    /// This query evaluates capability only; calibration error rates and
856    /// durations do not affect the result. Standard one- and two-qubit gates
857    /// follow the device default/local-override contract, and two-qubit qargs
858    /// are directional. Structured control flow, runtime operations, and
859    /// composite gates are not atomic device capabilities; validate them with
860    /// [`Self::validate_operation`] or [`Self::validate_circuit`] instead.
861    pub fn supports_native_instruction(
862        &self,
863        instruction: &Instruction,
864        qargs: &[PhysicalQubit],
865    ) -> bool {
866        if qargs.iter().any(|qubit| !self.is_usable_qubit(*qubit)) {
867            return false;
868        }
869
870        match instruction {
871            Instruction::Standard(StandardGate::GPhase) if qargs.is_empty() => !matches!(
872                self.resolve_native_instruction(None, instruction),
873                NativeInstructionSupport::Unsupported
874            ),
875            Instruction::Standard(gate) if gate.num_qubits() == qargs.len() => match qargs {
876                [qubit] => !matches!(
877                    self.resolve_native_instruction(
878                        self.qubit_properties
879                            .get(qubit)
880                            .map(QubitProp::native_instructions),
881                        instruction,
882                    ),
883                    NativeInstructionSupport::Unsupported
884                ),
885                [control, target]
886                    if self.topology.supports_directed_coupling(*control, *target) =>
887                {
888                    !matches!(
889                        self.resolve_native_instruction(
890                            self.edge_properties(*control, *target)
891                                .map(EdgeProp::native_instructions),
892                            instruction,
893                        ),
894                        NativeInstructionSupport::Unsupported
895                    )
896                }
897                _ => false,
898            },
899            Instruction::Standard(_)
900            | Instruction::McGate(_)
901            | Instruction::UnitaryGate(_)
902            | Instruction::CircuitGate(_)
903            | Instruction::Directive(_)
904            | Instruction::ClassicalData(_)
905            | Instruction::ClassicalControl(_)
906            | Instruction::Delay => false,
907        }
908    }
909
910    /// Validates one operation interpreted in the physical-qubit ID space.
911    ///
912    /// Structured control-flow bodies are checked recursively. This method is
913    /// read-only and never performs layout, routing, lowering, or direction
914    /// correction.
915    pub fn validate_operation(&self, operation: &Operation) -> Result<(), DeviceValidationError> {
916        let qargs = self.validate_qargs(&operation.qubits)?;
917        match &operation.instruction {
918            Instruction::ClassicalControl(control) => match control {
919                ClassicalControlOp::If(op) => {
920                    self.validate_control_body(op.then_body().operations())?;
921                    if let Some(body) = op.else_body() {
922                        self.validate_control_body(body.operations())?;
923                    }
924                }
925                ClassicalControlOp::While(op) => {
926                    self.validate_control_body(op.body().operations())?
927                }
928                ClassicalControlOp::For(op) => {
929                    self.validate_control_body(op.body().operations())?
930                }
931                ClassicalControlOp::Switch(op) => {
932                    for case in op.cases() {
933                        self.validate_control_body(case.body().operations())?;
934                    }
935                    if let Some(body) = op.default() {
936                        self.validate_control_body(body.operations())?;
937                    }
938                }
939                ClassicalControlOp::Break | ClassicalControlOp::Continue => {}
940            },
941            instruction => self.validate_atomic_instruction(instruction, qargs)?,
942        }
943        Ok(())
944    }
945
946    /// Validates one value-level operation interpreted in the physical-qubit ID space.
947    ///
948    /// This is the construction-IR counterpart of [`Self::validate_operation`].
949    /// Structured control-flow bodies are checked recursively without first
950    /// converting the operation into a [`Circuit`], so circuit-owned classical
951    /// variables and values are not required solely for device validation.
952    pub fn validate_value_operation(
953        &self,
954        operation: &ValueOperation,
955    ) -> Result<(), DeviceValidationError> {
956        let qargs = self.validate_qargs(&operation.qubits)?;
957        match &operation.instruction {
958            ValueInstruction::Instruction(instruction) => {
959                self.validate_atomic_instruction(instruction, qargs)?
960            }
961            ValueInstruction::ClassicalControl(control) => match control {
962                ValueClassicalControlOp::If {
963                    then_body,
964                    else_body,
965                    ..
966                } => {
967                    self.validate_value_control_body(then_body.operations())?;
968                    if let Some(body) = else_body {
969                        self.validate_value_control_body(body.operations())?;
970                    }
971                }
972                ValueClassicalControlOp::While { body, .. }
973                | ValueClassicalControlOp::For { body, .. } => {
974                    self.validate_value_control_body(body.operations())?
975                }
976                ValueClassicalControlOp::Switch { cases, default, .. } => {
977                    for case in cases {
978                        self.validate_value_control_body(case.body.operations())?;
979                    }
980                    if let Some(body) = default {
981                        self.validate_value_control_body(body.operations())?;
982                    }
983                }
984                ValueClassicalControlOp::Break | ValueClassicalControlOp::Continue => {}
985            },
986        }
987        Ok(())
988    }
989
990    fn validate_qargs(
991        &self,
992        qubits: &[Qubit],
993    ) -> Result<Vec<PhysicalQubit>, DeviceValidationError> {
994        let qargs = qubits
995            .iter()
996            .copied()
997            .map(PhysicalQubit::from_qubit)
998            .collect::<Vec<_>>();
999        // Diagnose unusable qargs before the coarser capability query so
1000        // callers receive the concrete physical-qubit failure.
1001        if let Some(qubit) = qargs
1002            .iter()
1003            .copied()
1004            .find(|qubit| !self.is_usable_qubit(*qubit))
1005        {
1006            return Err(DeviceValidationError::UnusablePhysicalQubit {
1007                device: self.name.clone(),
1008                qubit,
1009            });
1010        }
1011        Ok(qargs)
1012    }
1013
1014    fn validate_atomic_instruction(
1015        &self,
1016        instruction: &Instruction,
1017        qargs: Vec<PhysicalQubit>,
1018    ) -> Result<(), DeviceValidationError> {
1019        match instruction {
1020            Instruction::Directive(Directive::Measure | Directive::Reset) if qargs.len() == 1 => {}
1021            Instruction::Directive(Directive::Barrier) => {}
1022            Instruction::ClassicalData(op) => match op {
1023                crate::circuit::ClassicalDataOp::Store { .. } if qargs.is_empty() => {}
1024                crate::circuit::ClassicalDataOp::MeasureBit { .. } if qargs.len() == 1 => {}
1025                crate::circuit::ClassicalDataOp::MeasureBits { .. } if !qargs.is_empty() => {}
1026                _ => return Err(self.unsupported_instruction(instruction, qargs)),
1027            },
1028            Instruction::Delay if qargs.len() == 1 => {}
1029            Instruction::McGate(_) | Instruction::UnitaryGate(_) | Instruction::CircuitGate(_) => {
1030                return Err(DeviceValidationError::UndecomposedInstruction {
1031                    device: self.name.clone(),
1032                    instruction: instruction.to_string(),
1033                    qargs,
1034                });
1035            }
1036            Instruction::Standard(gate) if gate.num_qubits() > 2 => {
1037                return Err(DeviceValidationError::UndecomposedInstruction {
1038                    device: self.name.clone(),
1039                    instruction: instruction.to_string(),
1040                    qargs,
1041                });
1042            }
1043            Instruction::Standard(gate) if gate.num_qubits() == 2 => {
1044                let [control, target] = qargs.as_slice() else {
1045                    return Err(self.unsupported_instruction(instruction, qargs));
1046                };
1047                if !self.topology.supports_directed_coupling(*control, *target) {
1048                    return Err(DeviceValidationError::MissingDirectedCoupling {
1049                        device: self.name.clone(),
1050                        instruction: instruction.to_string(),
1051                        control: *control,
1052                        target: *target,
1053                    });
1054                }
1055                if !self.supports_native_instruction(instruction, &qargs) {
1056                    return Err(self.unsupported_instruction(instruction, qargs));
1057                }
1058            }
1059            Instruction::Standard(_) if self.supports_native_instruction(instruction, &qargs) => {}
1060            _ => return Err(self.unsupported_instruction(instruction, qargs)),
1061        }
1062        Ok(())
1063    }
1064
1065    /// Validates a circuit interpreted in the physical-qubit ID space.
1066    ///
1067    /// Validation stops at the first unsupported operation. The circuit is not
1068    /// modified and must already have completed logical-to-physical mapping and
1069    /// hardware lowering.
1070    pub fn validate_circuit(&self, circuit: &Circuit) -> Result<(), DeviceValidationError> {
1071        self.validate_operations(circuit.operations())
1072    }
1073
1074    fn validate_operations(&self, operations: &[Operation]) -> Result<(), DeviceValidationError> {
1075        // Preserve source order and stop at the first failure so diagnostics
1076        // are deterministic for top-level and nested control-flow bodies.
1077        for operation in operations {
1078            self.validate_operation(operation)?;
1079        }
1080        Ok(())
1081    }
1082
1083    /// Validates one canonical control-flow body.
1084    ///
1085    /// Until control bodies carry their own phase metadata, canonicalization
1086    /// represents a body-local global phase as one leading zero-qubit GPhase
1087    /// marker. It is semantic IR metadata rather than a hardware instruction,
1088    /// so it does not require a device capability. No other GPhase position is
1089    /// granted this exception.
1090    fn validate_control_body(&self, operations: &[Operation]) -> Result<(), DeviceValidationError> {
1091        let operations = if operations.first().is_some_and(|operation| {
1092            matches!(
1093                operation.instruction,
1094                Instruction::Standard(StandardGate::GPhase)
1095            ) && operation.qubits.is_empty()
1096        }) {
1097            &operations[1..]
1098        } else {
1099            operations
1100        };
1101        self.validate_operations(operations)
1102    }
1103
1104    fn validate_value_control_body(
1105        &self,
1106        operations: &[ValueOperation],
1107    ) -> Result<(), DeviceValidationError> {
1108        let operations = if operations.first().is_some_and(|operation| {
1109            matches!(
1110                operation.instruction,
1111                ValueInstruction::Instruction(Instruction::Standard(StandardGate::GPhase))
1112            ) && operation.qubits.is_empty()
1113        }) {
1114            &operations[1..]
1115        } else {
1116            operations
1117        };
1118        for operation in operations {
1119            self.validate_value_operation(operation)?;
1120        }
1121        Ok(())
1122    }
1123
1124    /// Builds the common diagnostic for a well-formed but unsupported atomic instruction.
1125    fn unsupported_instruction(
1126        &self,
1127        instruction: &Instruction,
1128        qargs: Vec<PhysicalQubit>,
1129    ) -> DeviceValidationError {
1130        DeviceValidationError::UnsupportedInstruction {
1131            device: self.name.clone(),
1132            instruction: instruction.to_string(),
1133            qargs,
1134        }
1135    }
1136
1137    /// Gets the error rate for `instruction` on a single physical qubit.
1138    ///
1139    /// Returns `None` if the qubit is not usable or does not support the
1140    /// instruction. A non-empty local native-instruction list is a complete
1141    /// capability override; otherwise device-wide one-qubit defaults apply.
1142    /// The default error rate is used only for an inherited supported gate.
1143    pub fn single_qubit_error(
1144        &self,
1145        qubit: PhysicalQubit,
1146        instruction: &Instruction,
1147    ) -> Option<f64> {
1148        if !self.is_usable_qubit(qubit) {
1149            return None;
1150        }
1151
1152        if !matches!(instruction, Instruction::Standard(gate) if gate.num_qubits() == 1) {
1153            return None;
1154        }
1155        match self.resolve_native_instruction(
1156            self.qubit_properties
1157                .get(&qubit)
1158                .map(QubitProp::native_instructions),
1159            instruction,
1160        ) {
1161            NativeInstructionSupport::Explicit(prop) => Some(prop.error_rate()),
1162            NativeInstructionSupport::Inherited => self.default_single_qubit_error,
1163            NativeInstructionSupport::Unsupported => None,
1164        }
1165    }
1166
1167    /// Gets the error rate for `instruction` on a directed coupling.
1168    ///
1169    /// Returns `None` if either endpoint is not usable, the exact directed
1170    /// coupling does not exist, or that edge does not support the instruction.
1171    /// A non-empty local native-instruction list is a complete capability
1172    /// override; otherwise device-wide two-qubit defaults apply. The default
1173    /// error rate is used only for an inherited supported gate.
1174    pub fn two_qubit_error(
1175        &self,
1176        control: PhysicalQubit,
1177        target: PhysicalQubit,
1178        instruction: &Instruction,
1179    ) -> Option<f64> {
1180        if !self.is_usable_qubit(control)
1181            || !self.is_usable_qubit(target)
1182            || !self.topology.supports_directed_coupling(control, target)
1183        {
1184            return None;
1185        }
1186
1187        if !matches!(instruction, Instruction::Standard(gate) if gate.num_qubits() == 2) {
1188            return None;
1189        }
1190        match self.resolve_native_instruction(
1191            self.edge_properties(control, target)
1192                .map(EdgeProp::native_instructions),
1193            instruction,
1194        ) {
1195            NativeInstructionSupport::Explicit(prop) => Some(prop.error_rate()),
1196            NativeInstructionSupport::Inherited => self.default_two_qubit_error,
1197            NativeInstructionSupport::Unsupported => None,
1198        }
1199    }
1200
1201    /// Returns calibration for a supported instruction on exact ordered qargs.
1202    ///
1203    /// Capability and calibration are deliberately separate: `None` means the
1204    /// instruction is unsupported, while `Some(default)` means it is supported
1205    /// but has no explicit or inherited calibration data.
1206    pub(crate) fn native_instruction_calibration(
1207        &self,
1208        instruction: &Instruction,
1209        qargs: &[PhysicalQubit],
1210    ) -> Option<NativeInstructionCalibration> {
1211        if !self.supports_native_instruction(instruction, qargs) {
1212            return None;
1213        }
1214
1215        let support = match qargs {
1216            [qubit] => self.resolve_native_instruction(
1217                self.qubit_properties
1218                    .get(qubit)
1219                    .map(QubitProp::native_instructions),
1220                instruction,
1221            ),
1222            [control, target] => self.resolve_native_instruction(
1223                self.edge_properties(*control, *target)
1224                    .map(EdgeProp::native_instructions),
1225                instruction,
1226            ),
1227            _ => NativeInstructionSupport::Inherited,
1228        };
1229
1230        match support {
1231            NativeInstructionSupport::Explicit(prop) => Some(NativeInstructionCalibration {
1232                error_rate: Some(prop.error_rate()),
1233                duration: prop.length(),
1234            }),
1235            NativeInstructionSupport::Inherited => Some(NativeInstructionCalibration {
1236                error_rate: match qargs.len() {
1237                    1 => self.default_single_qubit_error,
1238                    2 => self.default_two_qubit_error,
1239                    _ => None,
1240                },
1241                duration: None,
1242            }),
1243            NativeInstructionSupport::Unsupported => None,
1244        }
1245    }
1246
1247    /// Gets a direction-specific coupling error suitable for routing costs.
1248    ///
1249    /// Returns the best available native two-qubit instruction error on the
1250    /// edge, or the default two-qubit error if no per-edge calibration exists.
1251    ///
1252    /// This instruction-agnostic calibration query does not prove that any
1253    /// two-qubit instruction is supported, even when it returns `Some`. It
1254    /// must not be used as a capability predicate. Use
1255    /// [`Self::supports_native_instruction`] or [`Self::two_qubit_error`] when
1256    /// a concrete instruction capability matters.
1257    pub fn edge_error(&self, control: PhysicalQubit, target: PhysicalQubit) -> Option<f64> {
1258        if !self.is_usable_qubit(control)
1259            || !self.is_usable_qubit(target)
1260            || !self.topology.supports_directed_coupling(control, target)
1261        {
1262            return None;
1263        }
1264
1265        self.edge_properties(control, target)
1266            .and_then(|props| {
1267                props
1268                    .native_instructions()
1269                    .iter()
1270                    .map(InstructionProp::error_rate)
1271                    .min_by(|a, b| a.total_cmp(b))
1272            })
1273            .or(self.default_two_qubit_error)
1274    }
1275
1276    /// Gets the T1 relaxation time for a qubit (μs).
1277    ///
1278    /// Falls back to the default T1 time if not specified for the qubit.
1279    pub fn get_t1(&self, qubit: PhysicalQubit) -> Option<f64> {
1280        self.qubit_properties
1281            .get(&qubit)
1282            .and_then(|p| p.t1)
1283            .or(self.default_t1)
1284    }
1285
1286    /// Gets the T2 dephasing time for a qubit (μs).
1287    ///
1288    /// Falls back to the default T2 time if not specified for the qubit.
1289    pub fn get_t2(&self, qubit: PhysicalQubit) -> Option<f64> {
1290        self.qubit_properties
1291            .get(&qubit)
1292            .and_then(|p| p.t2)
1293            .or(self.default_t2)
1294    }
1295
1296    /// Gets the readout error rate for a qubit.
1297    ///
1298    /// Falls back to the default readout error if not specified for the qubit.
1299    pub fn get_readout_error(&self, qubit: PhysicalQubit) -> Option<f64> {
1300        self.qubit_properties
1301            .get(&qubit)
1302            .map(|p| p.readout_error)
1303            .or(self.default_readout_error)
1304    }
1305
1306    /// Gets the default single-qubit gate error rate.
1307    pub fn default_single_qubit_error(&self) -> Option<f64> {
1308        self.default_single_qubit_error
1309    }
1310
1311    /// Gets the default two-qubit gate error rate.
1312    pub fn default_two_qubit_error(&self) -> Option<f64> {
1313        self.default_two_qubit_error
1314    }
1315
1316    /// Gets the default T1 relaxation time.
1317    pub fn default_t1(&self) -> Option<f64> {
1318        self.default_t1
1319    }
1320
1321    /// Gets the default T2 dephasing time.
1322    pub fn default_t2(&self) -> Option<f64> {
1323        self.default_t2
1324    }
1325
1326    /// Gets the default readout error rate.
1327    pub fn default_readout_error(&self) -> Option<f64> {
1328        self.default_readout_error
1329    }
1330
1331    /// Gets the system calibration timestamp.
1332    pub fn calibration_time(&self) -> Option<OffsetDateTime> {
1333        self.calibration_time
1334    }
1335}
1336
1337#[cfg(test)]
1338#[path = "./device_test.rs"]
1339mod device_test;