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    ///
476    /// # Errors
477    ///
478    /// Returns [`DeviceError::EmptyDevice`] if `physical_qubits` is empty.
479    pub fn line_from_qubits(
480        name: impl Into<String>,
481        physical_qubits: Vec<PhysicalQubit>,
482    ) -> Result<Self, DeviceError> {
483        if physical_qubits.is_empty() {
484            return Err(DeviceError::EmptyDevice);
485        }
486        let qubits = physical_qubits.iter().copied().collect::<HashSet<_>>();
487        let topology = Topology::line(physical_qubits).map_err(DeviceError::InvalidTopology)?;
488        Self::new(name, qubits, topology)
489    }
490
491    /// Creates a device with physical qubits connected as a bidirectional line.
492    ///
493    /// The device contains physical qubits `0..num_qubits`, all of which are
494    /// online. Adjacent qubits are connected in both directions.
495    pub fn bidirectional_line(
496        name: impl Into<String>,
497        num_qubits: u32,
498    ) -> Result<Self, DeviceError> {
499        let edges = (0..num_qubits.saturating_sub(1))
500            .flat_map(|index| [(index, index + 1), (index + 1, index)])
501            .collect::<Vec<_>>();
502        Self::from_u32_edges(name, num_qubits, &edges)
503    }
504
505    /// Creates a device with physical qubits connected as a bidirectional ring.
506    ///
507    /// The device contains physical qubits `0..num_qubits`, all of which are
508    /// online. For two or more qubits, each qubit is connected to its successor
509    /// modulo `num_qubits` in both directions.
510    pub fn ring(name: impl Into<String>, num_qubits: u32) -> Result<Self, DeviceError> {
511        let mut edges = BTreeSet::new();
512        if num_qubits >= 2 {
513            for index in 0..num_qubits {
514                let next = (index + 1) % num_qubits;
515                edges.insert((index, next));
516                edges.insert((next, index));
517            }
518        }
519        let edges = edges.into_iter().collect::<Vec<_>>();
520        Self::from_u32_edges(name, num_qubits, &edges)
521    }
522
523    /// Creates a device with physical qubits connected as a bidirectional star.
524    ///
525    /// The device contains physical qubits `0..num_qubits`, all of which are
526    /// online. Every non-center qubit is connected to `center` in both
527    /// directions.
528    pub fn star(
529        name: impl Into<String>,
530        num_qubits: u32,
531        center: u32,
532    ) -> Result<Self, DeviceError> {
533        let edges = (0..num_qubits)
534            .filter(|&index| index != center)
535            .flat_map(|index| [(center, index), (index, center)])
536            .collect::<Vec<_>>();
537        Self::from_u32_edges(name, num_qubits, &edges)
538    }
539
540    /// Creates a device with physical qubits connected as a bidirectional grid.
541    ///
542    /// Qubit IDs are assigned in row-major order. Horizontal and vertical
543    /// nearest-neighbor couplings are added in both directions.
544    pub fn grid(name: impl Into<String>, rows: u32, cols: u32) -> Result<Self, DeviceError> {
545        let num_qubits = rows.saturating_mul(cols);
546        let mut edges = Vec::new();
547        for row in 0..rows {
548            for col in 0..cols {
549                let current = row * cols + col;
550                if col + 1 < cols {
551                    edges.push((current, current + 1));
552                    edges.push((current + 1, current));
553                }
554                if row + 1 < rows {
555                    edges.push((current, current + cols));
556                    edges.push((current + cols, current));
557                }
558            }
559        }
560        Self::from_u32_edges(name, num_qubits, &edges)
561    }
562
563    /// Creates a device with physical qubits `0..num_qubits` and explicit directed edges.
564    ///
565    /// Each `(control, target)` pair in `edges` becomes one directed coupling.
566    pub fn from_edges(
567        name: impl Into<String>,
568        num_qubits: u32,
569        edges: &[(u32, u32)],
570    ) -> Result<Self, DeviceError> {
571        Self::from_u32_edges(name, num_qubits, edges)
572    }
573
574    fn from_u32_edges(
575        name: impl Into<String>,
576        num_qubits: u32,
577        edges: &[(u32, u32)],
578    ) -> Result<Self, DeviceError> {
579        if num_qubits == 0 {
580            return Err(DeviceError::EmptyDevice);
581        }
582        let physical_qubits = (0..num_qubits).map(PhysicalQubit::new).collect::<Vec<_>>();
583        let qubits = physical_qubits.iter().copied().collect::<HashSet<_>>();
584        let coupling_map = edges
585            .iter()
586            .enumerate()
587            .map(|(index, &(control, target))| {
588                (
589                    PhysicalQubit::new(control),
590                    PhysicalQubit::new(target),
591                    format!("e{index}"),
592                )
593            })
594            .collect::<Vec<_>>();
595        let topology =
596            Topology::new(physical_qubits, coupling_map).map_err(DeviceError::InvalidTopology)?;
597        Self::new(name, qubits, topology)
598    }
599
600    /// Sets the offline or faulty physical qubits using the builder pattern.
601    ///
602    /// # Errors
603    ///
604    /// Returns [`DeviceError::QubitNotInDevice`] if any invalid qubit is not
605    /// registered with the device.
606    pub fn with_invalid_qubits(
607        mut self,
608        invalid_qubits: HashSet<PhysicalQubit>,
609    ) -> Result<Self, DeviceError> {
610        self.set_invalid_qubits(invalid_qubits)?;
611        Ok(self)
612    }
613
614    /// Sets the offline or faulty physical qubits.
615    ///
616    /// # Errors
617    ///
618    /// Returns [`DeviceError::QubitNotInDevice`] if any invalid qubit is not
619    /// registered with the device. The existing set is preserved on error.
620    pub fn set_invalid_qubits(
621        &mut self,
622        invalid_qubits: HashSet<PhysicalQubit>,
623    ) -> Result<(), DeviceError> {
624        for &qubit in &invalid_qubits {
625            if !self.qubits.contains(&qubit) {
626                return Err(DeviceError::QubitNotInDevice(qubit));
627            }
628        }
629        self.invalid_qubits = invalid_qubits.into_iter().collect();
630        Ok(())
631    }
632
633    /// Sets the device-wide default native gates.
634    ///
635    /// This does not add, remove, or otherwise synchronize native instructions
636    /// stored on individual qubits or directed edges.
637    ///
638    /// # Errors
639    ///
640    /// Returns [`DeviceError`] if any entry is not a standard gate or acts on
641    /// more than two qubits.
642    pub fn with_native_gates(mut self, gates: Vec<Instruction>) -> Result<Self, DeviceError> {
643        self.set_native_gates(gates)?;
644        Ok(self)
645    }
646
647    /// Replaces the device-wide native defaults after validating the full list.
648    ///
649    /// The existing defaults are preserved when any entry is invalid.
650    ///
651    /// # Errors
652    ///
653    /// Returns [`DeviceError`] if any entry is not a standard gate or acts on
654    /// more than two qubits.
655    pub fn set_native_gates(&mut self, gates: Vec<Instruction>) -> Result<(), DeviceError> {
656        for gate in &gates {
657            validate_native_gate_arity(gate, 0..=2)?;
658        }
659        self.native_gates = gates;
660        Ok(())
661    }
662
663    /// Sets the system calibration timestamp.
664    pub fn with_calibration_time(mut self, time: OffsetDateTime) -> Self {
665        self.calibration_time = Some(time);
666        self
667    }
668
669    pub fn set_calibration_time(&mut self, time: OffsetDateTime) {
670        self.calibration_time = Some(time);
671    }
672
673    /// Sets the default T1 time (μs).
674    pub fn with_default_t1(mut self, t1: f64) -> Self {
675        self.default_t1 = Some(t1);
676        self
677    }
678
679    pub fn set_default_t1(&mut self, t1: f64) {
680        self.default_t1 = Some(t1);
681    }
682
683    /// Sets the default T2 time (μs).
684    pub fn with_default_t2(mut self, t2: f64) -> Self {
685        self.default_t2 = Some(t2);
686        self
687    }
688
689    pub fn set_default_t2(&mut self, t2: f64) {
690        self.default_t2 = Some(t2);
691    }
692
693    /// Sets the default readout error rate.
694    pub fn with_default_readout_error(mut self, error: f64) -> Self {
695        self.default_readout_error = Some(error);
696        self
697    }
698
699    pub fn set_default_readout_error(&mut self, error: f64) {
700        self.default_readout_error = Some(error);
701    }
702
703    /// Sets the default single-qubit gate error rate.
704    pub fn with_default_single_qubit_error(mut self, error: f64) -> Self {
705        self.default_single_qubit_error = Some(error);
706        self
707    }
708
709    pub fn set_default_single_qubit_error(&mut self, error: f64) {
710        self.default_single_qubit_error = Some(error);
711    }
712
713    /// Sets the default two-qubit gate error rate.
714    pub fn with_default_two_qubit_error(mut self, error: f64) -> Self {
715        self.default_two_qubit_error = Some(error);
716        self
717    }
718
719    pub fn set_default_two_qubit_error(&mut self, error: f64) {
720        self.default_two_qubit_error = Some(error);
721    }
722
723    /// Adds properties for a specific qubit.
724    ///
725    /// # Errors
726    ///
727    /// Returns [`DeviceError::QubitNotInDevice`] if the qubit is not registered
728    /// with the device, or [`DeviceError::QubitNotInTopology`] if it is
729    /// registered but absent from the topology. Properties may be retained for
730    /// a registered qubit while it is marked invalid/offline.
731    pub fn add_qubit_properties(
732        &mut self,
733        qubit: PhysicalQubit,
734        props: QubitProp,
735    ) -> Result<(), DeviceError> {
736        if !self.qubits.contains(&qubit) {
737            return Err(DeviceError::QubitNotInDevice(qubit));
738        }
739        if !self.topology.contains_qubit(&qubit) {
740            return Err(DeviceError::QubitNotInTopology(qubit));
741        }
742        self.qubit_properties.insert(qubit, props);
743        Ok(())
744    }
745
746    /// Adds properties for a specific coupling edge.
747    ///
748    /// # Errors
749    ///
750    /// Returns `DeviceError::EdgeNotInTopology` if the edge is not in the device's topology.
751    pub fn add_edge_properties(
752        &mut self,
753        control: PhysicalQubit,
754        target: PhysicalQubit,
755        props: EdgeProp,
756    ) -> Result<(), DeviceError> {
757        if !self.topology.supports_directed_coupling(control, target) {
758            return Err(DeviceError::EdgeNotInTopology(control, target));
759        }
760        self.edge_properties.insert((control, target), props);
761        Ok(())
762    }
763
764    /// Compares instruction identities supported by the current device model.
765    ///
766    /// Device-native capabilities are currently limited to standard gates;
767    /// composite and runtime instructions are handled by circuit validation.
768    fn instruction_matches(stored: &Instruction, requested: &Instruction) -> bool {
769        match (stored, requested) {
770            (Instruction::Standard(stored), Instruction::Standard(requested)) => {
771                stored == requested
772            }
773            _ => false,
774        }
775    }
776
777    /// Resolves the shared local-override/default-inheritance capability contract.
778    ///
779    /// A non-empty local list is authoritative. An absent or empty local list
780    /// inherits the device defaults. Calibration consumers retain the explicit
781    /// property through the result instead of repeating capability resolution.
782    fn resolve_native_instruction<'a>(
783        &'a self,
784        local: Option<&'a [InstructionProp]>,
785        instruction: &Instruction,
786    ) -> NativeInstructionSupport<'a> {
787        if let Some(local) = local.filter(|instructions| !instructions.is_empty()) {
788            return local
789                .iter()
790                .find(|prop| Self::instruction_matches(prop.instruction(), instruction))
791                .map_or(
792                    NativeInstructionSupport::Unsupported,
793                    NativeInstructionSupport::Explicit,
794                );
795        }
796
797        if self
798            .native_gates
799            .iter()
800            .any(|stored| Self::instruction_matches(stored, instruction))
801        {
802            NativeInstructionSupport::Inherited
803        } else {
804            NativeInstructionSupport::Unsupported
805        }
806    }
807
808    /// Gets the name of the device.
809    pub fn name(&self) -> &str {
810        &self.name
811    }
812
813    /// Gets an iterator over the physical qubits registered with the device.
814    pub fn qubits(&self) -> impl Iterator<Item = PhysicalQubit> + '_ {
815        self.qubits.iter().copied()
816    }
817
818    /// Gets an iterator over the invalid (offline/faulty) qubits.
819    pub fn invalid_qubits(&self) -> impl Iterator<Item = PhysicalQubit> + '_ {
820        self.invalid_qubits.iter().copied()
821    }
822
823    /// Returns whether a physical qubit is registered and not marked invalid.
824    pub fn is_usable_qubit(&self, qubit: PhysicalQubit) -> bool {
825        self.qubits.contains(&qubit) && !self.invalid_qubits.contains(&qubit)
826    }
827
828    /// Gets an iterator over registered physical qubits that are not invalid.
829    pub fn usable_qubits(&self) -> impl Iterator<Item = PhysicalQubit> + '_ {
830        self.qubits.difference(&self.invalid_qubits).copied()
831    }
832
833    /// Returns the number of registered physical qubits that are not invalid.
834    pub fn num_usable_qubits(&self) -> usize {
835        self.qubits.len() - self.invalid_qubits.len()
836    }
837
838    /// Gets a reference to the device's connectivity topology.
839    pub fn topology(&self) -> &Topology {
840        &self.topology
841    }
842
843    /// Gets the default native gates supported by the device.
844    pub fn native_gates(&self) -> &[Instruction] {
845        &self.native_gates
846    }
847
848    /// Gets the properties of a specific qubit.
849    pub fn qubit_properties(&self, qubit: PhysicalQubit) -> Option<&QubitProp> {
850        self.qubit_properties.get(&qubit)
851    }
852
853    /// Gets the properties of a specific coupling edge.
854    pub fn edge_properties(
855        &self,
856        control: PhysicalQubit,
857        target: PhysicalQubit,
858    ) -> Option<&EdgeProp> {
859        self.edge_properties.get(&(control, target))
860    }
861
862    /// Returns whether a native hardware instruction can execute on the exact
863    /// ordered physical qargs.
864    ///
865    /// This query evaluates capability only; calibration error rates and
866    /// durations do not affect the result. Standard one- and two-qubit gates
867    /// follow the device default/local-override contract, and two-qubit qargs
868    /// are directional. Structured control flow, runtime operations, and
869    /// composite gates are not atomic device capabilities; validate them with
870    /// [`Self::validate_operation`] or [`Self::validate_circuit`] instead.
871    pub fn supports_native_instruction(
872        &self,
873        instruction: &Instruction,
874        qargs: &[PhysicalQubit],
875    ) -> bool {
876        if qargs.iter().any(|qubit| !self.is_usable_qubit(*qubit)) {
877            return false;
878        }
879
880        match instruction {
881            Instruction::Standard(StandardGate::GPhase) if qargs.is_empty() => !matches!(
882                self.resolve_native_instruction(None, instruction),
883                NativeInstructionSupport::Unsupported
884            ),
885            Instruction::Standard(gate) if gate.num_qubits() == qargs.len() => match qargs {
886                [qubit] => !matches!(
887                    self.resolve_native_instruction(
888                        self.qubit_properties
889                            .get(qubit)
890                            .map(QubitProp::native_instructions),
891                        instruction,
892                    ),
893                    NativeInstructionSupport::Unsupported
894                ),
895                [control, target]
896                    if self.topology.supports_directed_coupling(*control, *target) =>
897                {
898                    !matches!(
899                        self.resolve_native_instruction(
900                            self.edge_properties(*control, *target)
901                                .map(EdgeProp::native_instructions),
902                            instruction,
903                        ),
904                        NativeInstructionSupport::Unsupported
905                    )
906                }
907                _ => false,
908            },
909            Instruction::Standard(_)
910            | Instruction::McGate(_)
911            | Instruction::UnitaryGate(_)
912            | Instruction::CircuitGate(_)
913            | Instruction::Directive(_)
914            | Instruction::ClassicalData(_)
915            | Instruction::ClassicalControl(_)
916            | Instruction::Delay => false,
917        }
918    }
919
920    /// Validates one operation interpreted in the physical-qubit ID space.
921    ///
922    /// Structured control-flow bodies are checked recursively. This method is
923    /// read-only and never performs layout, routing, lowering, or direction
924    /// correction.
925    pub fn validate_operation(&self, operation: &Operation) -> Result<(), DeviceValidationError> {
926        let qargs = self.validate_qargs(&operation.qubits)?;
927        match &operation.instruction {
928            Instruction::ClassicalControl(control) => match control {
929                ClassicalControlOp::If(op) => {
930                    self.validate_control_body(op.then_body().operations())?;
931                    if let Some(body) = op.else_body() {
932                        self.validate_control_body(body.operations())?;
933                    }
934                }
935                ClassicalControlOp::While(op) => {
936                    self.validate_control_body(op.body().operations())?
937                }
938                ClassicalControlOp::For(op) => {
939                    self.validate_control_body(op.body().operations())?
940                }
941                ClassicalControlOp::Switch(op) => {
942                    for case in op.cases() {
943                        self.validate_control_body(case.body().operations())?;
944                    }
945                    if let Some(body) = op.default() {
946                        self.validate_control_body(body.operations())?;
947                    }
948                }
949                ClassicalControlOp::Break | ClassicalControlOp::Continue => {}
950            },
951            instruction => self.validate_atomic_instruction(instruction, qargs)?,
952        }
953        Ok(())
954    }
955
956    /// Validates one value-level operation interpreted in the physical-qubit ID space.
957    ///
958    /// This is the construction-IR counterpart of [`Self::validate_operation`].
959    /// Structured control-flow bodies are checked recursively without first
960    /// converting the operation into a [`Circuit`], so circuit-owned classical
961    /// variables and values are not required solely for device validation.
962    pub fn validate_value_operation(
963        &self,
964        operation: &ValueOperation,
965    ) -> Result<(), DeviceValidationError> {
966        let qargs = self.validate_qargs(&operation.qubits)?;
967        match &operation.instruction {
968            ValueInstruction::Instruction(instruction) => {
969                self.validate_atomic_instruction(instruction, qargs)?
970            }
971            ValueInstruction::ClassicalControl(control) => match control {
972                ValueClassicalControlOp::If {
973                    then_body,
974                    else_body,
975                    ..
976                } => {
977                    self.validate_value_control_body(then_body.operations())?;
978                    if let Some(body) = else_body {
979                        self.validate_value_control_body(body.operations())?;
980                    }
981                }
982                ValueClassicalControlOp::While { body, .. }
983                | ValueClassicalControlOp::For { body, .. } => {
984                    self.validate_value_control_body(body.operations())?
985                }
986                ValueClassicalControlOp::Switch { cases, default, .. } => {
987                    for case in cases {
988                        self.validate_value_control_body(case.body.operations())?;
989                    }
990                    if let Some(body) = default {
991                        self.validate_value_control_body(body.operations())?;
992                    }
993                }
994                ValueClassicalControlOp::Break | ValueClassicalControlOp::Continue => {}
995            },
996        }
997        Ok(())
998    }
999
1000    fn validate_qargs(
1001        &self,
1002        qubits: &[Qubit],
1003    ) -> Result<Vec<PhysicalQubit>, DeviceValidationError> {
1004        let qargs = qubits
1005            .iter()
1006            .copied()
1007            .map(PhysicalQubit::from_qubit)
1008            .collect::<Vec<_>>();
1009        // Diagnose unusable qargs before the coarser capability query so
1010        // callers receive the concrete physical-qubit failure.
1011        if let Some(qubit) = qargs
1012            .iter()
1013            .copied()
1014            .find(|qubit| !self.is_usable_qubit(*qubit))
1015        {
1016            return Err(DeviceValidationError::UnusablePhysicalQubit {
1017                device: self.name.clone(),
1018                qubit,
1019            });
1020        }
1021        Ok(qargs)
1022    }
1023
1024    fn validate_atomic_instruction(
1025        &self,
1026        instruction: &Instruction,
1027        qargs: Vec<PhysicalQubit>,
1028    ) -> Result<(), DeviceValidationError> {
1029        match instruction {
1030            Instruction::Directive(Directive::Measure | Directive::Reset) if qargs.len() == 1 => {}
1031            Instruction::Directive(Directive::Barrier) => {}
1032            Instruction::ClassicalData(op) => match op {
1033                crate::circuit::ClassicalDataOp::Store { .. } if qargs.is_empty() => {}
1034                crate::circuit::ClassicalDataOp::MeasureBit { .. } if qargs.len() == 1 => {}
1035                crate::circuit::ClassicalDataOp::MeasureBits { .. } if !qargs.is_empty() => {}
1036                _ => return Err(self.unsupported_instruction(instruction, qargs)),
1037            },
1038            Instruction::Delay if qargs.len() == 1 => {}
1039            Instruction::McGate(_) | Instruction::UnitaryGate(_) | Instruction::CircuitGate(_) => {
1040                return Err(DeviceValidationError::UndecomposedInstruction {
1041                    device: self.name.clone(),
1042                    instruction: instruction.to_string(),
1043                    qargs,
1044                });
1045            }
1046            Instruction::Standard(gate) if gate.num_qubits() > 2 => {
1047                return Err(DeviceValidationError::UndecomposedInstruction {
1048                    device: self.name.clone(),
1049                    instruction: instruction.to_string(),
1050                    qargs,
1051                });
1052            }
1053            Instruction::Standard(gate) if gate.num_qubits() == 2 => {
1054                let [control, target] = qargs.as_slice() else {
1055                    return Err(self.unsupported_instruction(instruction, qargs));
1056                };
1057                if !self.topology.supports_directed_coupling(*control, *target) {
1058                    return Err(DeviceValidationError::MissingDirectedCoupling {
1059                        device: self.name.clone(),
1060                        instruction: instruction.to_string(),
1061                        control: *control,
1062                        target: *target,
1063                    });
1064                }
1065                if !self.supports_native_instruction(instruction, &qargs) {
1066                    return Err(self.unsupported_instruction(instruction, qargs));
1067                }
1068            }
1069            Instruction::Standard(_) if self.supports_native_instruction(instruction, &qargs) => {}
1070            _ => return Err(self.unsupported_instruction(instruction, qargs)),
1071        }
1072        Ok(())
1073    }
1074
1075    /// Validates a circuit interpreted in the physical-qubit ID space.
1076    ///
1077    /// Validation stops at the first unsupported operation. The circuit is not
1078    /// modified and must already have completed logical-to-physical mapping and
1079    /// hardware lowering.
1080    pub fn validate_circuit(&self, circuit: &Circuit) -> Result<(), DeviceValidationError> {
1081        self.validate_operations(circuit.operations())
1082    }
1083
1084    fn validate_operations(&self, operations: &[Operation]) -> Result<(), DeviceValidationError> {
1085        // Preserve source order and stop at the first failure so diagnostics
1086        // are deterministic for top-level and nested control-flow bodies.
1087        for operation in operations {
1088            self.validate_operation(operation)?;
1089        }
1090        Ok(())
1091    }
1092
1093    /// Validates one canonical control-flow body.
1094    ///
1095    /// Until control bodies carry their own phase metadata, canonicalization
1096    /// represents a body-local global phase as one leading zero-qubit GPhase
1097    /// marker. It is semantic IR metadata rather than a hardware instruction,
1098    /// so it does not require a device capability. No other GPhase position is
1099    /// granted this exception.
1100    fn validate_control_body(&self, operations: &[Operation]) -> Result<(), DeviceValidationError> {
1101        let operations = if operations.first().is_some_and(|operation| {
1102            matches!(
1103                operation.instruction,
1104                Instruction::Standard(StandardGate::GPhase)
1105            ) && operation.qubits.is_empty()
1106        }) {
1107            &operations[1..]
1108        } else {
1109            operations
1110        };
1111        self.validate_operations(operations)
1112    }
1113
1114    fn validate_value_control_body(
1115        &self,
1116        operations: &[ValueOperation],
1117    ) -> Result<(), DeviceValidationError> {
1118        let operations = if operations.first().is_some_and(|operation| {
1119            matches!(
1120                operation.instruction,
1121                ValueInstruction::Instruction(Instruction::Standard(StandardGate::GPhase))
1122            ) && operation.qubits.is_empty()
1123        }) {
1124            &operations[1..]
1125        } else {
1126            operations
1127        };
1128        for operation in operations {
1129            self.validate_value_operation(operation)?;
1130        }
1131        Ok(())
1132    }
1133
1134    /// Builds the common diagnostic for a well-formed but unsupported atomic instruction.
1135    fn unsupported_instruction(
1136        &self,
1137        instruction: &Instruction,
1138        qargs: Vec<PhysicalQubit>,
1139    ) -> DeviceValidationError {
1140        DeviceValidationError::UnsupportedInstruction {
1141            device: self.name.clone(),
1142            instruction: instruction.to_string(),
1143            qargs,
1144        }
1145    }
1146
1147    /// Gets the error rate for `instruction` on a single physical qubit.
1148    ///
1149    /// Returns `None` if the qubit is not usable or does not support the
1150    /// instruction. A non-empty local native-instruction list is a complete
1151    /// capability override; otherwise device-wide one-qubit defaults apply.
1152    /// The default error rate is used only for an inherited supported gate.
1153    pub fn single_qubit_error(
1154        &self,
1155        qubit: PhysicalQubit,
1156        instruction: &Instruction,
1157    ) -> Option<f64> {
1158        if !self.is_usable_qubit(qubit) {
1159            return None;
1160        }
1161
1162        if !matches!(instruction, Instruction::Standard(gate) if gate.num_qubits() == 1) {
1163            return None;
1164        }
1165        match self.resolve_native_instruction(
1166            self.qubit_properties
1167                .get(&qubit)
1168                .map(QubitProp::native_instructions),
1169            instruction,
1170        ) {
1171            NativeInstructionSupport::Explicit(prop) => Some(prop.error_rate()),
1172            NativeInstructionSupport::Inherited => self.default_single_qubit_error,
1173            NativeInstructionSupport::Unsupported => None,
1174        }
1175    }
1176
1177    /// Gets the error rate for `instruction` on a directed coupling.
1178    ///
1179    /// Returns `None` if either endpoint is not usable, the exact directed
1180    /// coupling does not exist, or that edge does not support the instruction.
1181    /// A non-empty local native-instruction list is a complete capability
1182    /// override; otherwise device-wide two-qubit defaults apply. The default
1183    /// error rate is used only for an inherited supported gate.
1184    pub fn two_qubit_error(
1185        &self,
1186        control: PhysicalQubit,
1187        target: PhysicalQubit,
1188        instruction: &Instruction,
1189    ) -> Option<f64> {
1190        if !self.is_usable_qubit(control)
1191            || !self.is_usable_qubit(target)
1192            || !self.topology.supports_directed_coupling(control, target)
1193        {
1194            return None;
1195        }
1196
1197        if !matches!(instruction, Instruction::Standard(gate) if gate.num_qubits() == 2) {
1198            return None;
1199        }
1200        match self.resolve_native_instruction(
1201            self.edge_properties(control, target)
1202                .map(EdgeProp::native_instructions),
1203            instruction,
1204        ) {
1205            NativeInstructionSupport::Explicit(prop) => Some(prop.error_rate()),
1206            NativeInstructionSupport::Inherited => self.default_two_qubit_error,
1207            NativeInstructionSupport::Unsupported => None,
1208        }
1209    }
1210
1211    /// Returns calibration for a supported instruction on exact ordered qargs.
1212    ///
1213    /// Capability and calibration are deliberately separate: `None` means the
1214    /// instruction is unsupported, while `Some(default)` means it is supported
1215    /// but has no explicit or inherited calibration data.
1216    pub(crate) fn native_instruction_calibration(
1217        &self,
1218        instruction: &Instruction,
1219        qargs: &[PhysicalQubit],
1220    ) -> Option<NativeInstructionCalibration> {
1221        if !self.supports_native_instruction(instruction, qargs) {
1222            return None;
1223        }
1224
1225        let support = match qargs {
1226            [qubit] => self.resolve_native_instruction(
1227                self.qubit_properties
1228                    .get(qubit)
1229                    .map(QubitProp::native_instructions),
1230                instruction,
1231            ),
1232            [control, target] => self.resolve_native_instruction(
1233                self.edge_properties(*control, *target)
1234                    .map(EdgeProp::native_instructions),
1235                instruction,
1236            ),
1237            _ => NativeInstructionSupport::Inherited,
1238        };
1239
1240        match support {
1241            NativeInstructionSupport::Explicit(prop) => Some(NativeInstructionCalibration {
1242                error_rate: Some(prop.error_rate()),
1243                duration: prop.length(),
1244            }),
1245            NativeInstructionSupport::Inherited => Some(NativeInstructionCalibration {
1246                error_rate: match qargs.len() {
1247                    1 => self.default_single_qubit_error,
1248                    2 => self.default_two_qubit_error,
1249                    _ => None,
1250                },
1251                duration: None,
1252            }),
1253            NativeInstructionSupport::Unsupported => None,
1254        }
1255    }
1256
1257    /// Gets a direction-specific coupling error suitable for routing costs.
1258    ///
1259    /// Returns the best available native two-qubit instruction error on the
1260    /// edge, or the default two-qubit error if no per-edge calibration exists.
1261    ///
1262    /// This instruction-agnostic calibration query does not prove that any
1263    /// two-qubit instruction is supported, even when it returns `Some`. It
1264    /// must not be used as a capability predicate. Use
1265    /// [`Self::supports_native_instruction`] or [`Self::two_qubit_error`] when
1266    /// a concrete instruction capability matters.
1267    pub fn edge_error(&self, control: PhysicalQubit, target: PhysicalQubit) -> Option<f64> {
1268        if !self.is_usable_qubit(control)
1269            || !self.is_usable_qubit(target)
1270            || !self.topology.supports_directed_coupling(control, target)
1271        {
1272            return None;
1273        }
1274
1275        self.edge_properties(control, target)
1276            .and_then(|props| {
1277                props
1278                    .native_instructions()
1279                    .iter()
1280                    .map(InstructionProp::error_rate)
1281                    .min_by(|a, b| a.total_cmp(b))
1282            })
1283            .or(self.default_two_qubit_error)
1284    }
1285
1286    /// Gets the T1 relaxation time for a qubit (μs).
1287    ///
1288    /// Falls back to the default T1 time if not specified for the qubit.
1289    pub fn get_t1(&self, qubit: PhysicalQubit) -> Option<f64> {
1290        self.qubit_properties
1291            .get(&qubit)
1292            .and_then(|p| p.t1)
1293            .or(self.default_t1)
1294    }
1295
1296    /// Gets the T2 dephasing time for a qubit (μs).
1297    ///
1298    /// Falls back to the default T2 time if not specified for the qubit.
1299    pub fn get_t2(&self, qubit: PhysicalQubit) -> Option<f64> {
1300        self.qubit_properties
1301            .get(&qubit)
1302            .and_then(|p| p.t2)
1303            .or(self.default_t2)
1304    }
1305
1306    /// Gets the readout error rate for a qubit.
1307    ///
1308    /// Falls back to the default readout error if not specified for the qubit.
1309    pub fn get_readout_error(&self, qubit: PhysicalQubit) -> Option<f64> {
1310        self.qubit_properties
1311            .get(&qubit)
1312            .map(|p| p.readout_error)
1313            .or(self.default_readout_error)
1314    }
1315
1316    /// Gets the default single-qubit gate error rate.
1317    pub fn default_single_qubit_error(&self) -> Option<f64> {
1318        self.default_single_qubit_error
1319    }
1320
1321    /// Gets the default two-qubit gate error rate.
1322    pub fn default_two_qubit_error(&self) -> Option<f64> {
1323        self.default_two_qubit_error
1324    }
1325
1326    /// Gets the default T1 relaxation time.
1327    pub fn default_t1(&self) -> Option<f64> {
1328        self.default_t1
1329    }
1330
1331    /// Gets the default T2 dephasing time.
1332    pub fn default_t2(&self) -> Option<f64> {
1333        self.default_t2
1334    }
1335
1336    /// Gets the default readout error rate.
1337    pub fn default_readout_error(&self) -> Option<f64> {
1338        self.default_readout_error
1339    }
1340
1341    /// Gets the system calibration timestamp.
1342    pub fn calibration_time(&self) -> Option<OffsetDateTime> {
1343        self.calibration_time
1344    }
1345}
1346
1347#[cfg(test)]
1348#[path = "./device_test.rs"]
1349mod device_test;