Skip to main content

ruqu_core/
hardware.rs

1//! Hardware abstraction layer for quantum device providers.
2//!
3//! This module provides a unified interface for submitting quantum circuits
4//! to real hardware backends (IBM Quantum, IonQ, Rigetti, Amazon Braket) or
5//! a local simulator. Each provider implements the [`HardwareProvider`] trait,
6//! and the [`ProviderRegistry`] manages all registered providers.
7//!
8//! The [`LocalSimulatorProvider`] is fully functional and delegates to
9//! [`Simulator::run_shots`] for circuit execution. Remote providers return
10//! [`HardwareError::AuthenticationFailed`] since no real credentials are
11//! configured, but expose realistic device metadata and calibration data.
12
13use std::collections::HashMap;
14use std::fmt;
15
16use crate::circuit::QuantumCircuit;
17use crate::simulator::Simulator;
18
19// ---------------------------------------------------------------------------
20// Error type
21// ---------------------------------------------------------------------------
22
23/// Errors that can occur when interacting with hardware providers.
24#[derive(Debug)]
25pub enum HardwareError {
26    /// Provider rejected the supplied credentials or no credentials were found.
27    AuthenticationFailed(String),
28    /// The requested device name does not exist in this provider.
29    DeviceNotFound(String),
30    /// The device exists but is not currently accepting jobs.
31    DeviceOffline(String),
32    /// The submitted circuit requires more qubits than the device supports.
33    CircuitTooLarge { qubits: u32, max: u32 },
34    /// A previously submitted job has failed.
35    JobFailed(String),
36    /// A network-level communication error occurred.
37    NetworkError(String),
38    /// The provider throttled the request; retry after the given duration.
39    RateLimited { retry_after_ms: u64 },
40}
41
42impl fmt::Display for HardwareError {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            HardwareError::AuthenticationFailed(msg) => {
46                write!(f, "authentication failed: {}", msg)
47            }
48            HardwareError::DeviceNotFound(name) => {
49                write!(f, "device not found: {}", name)
50            }
51            HardwareError::DeviceOffline(name) => {
52                write!(f, "device offline: {}", name)
53            }
54            HardwareError::CircuitTooLarge { qubits, max } => {
55                write!(
56                    f,
57                    "circuit requires {} qubits but device supports at most {}",
58                    qubits, max
59                )
60            }
61            HardwareError::JobFailed(msg) => {
62                write!(f, "job failed: {}", msg)
63            }
64            HardwareError::NetworkError(msg) => {
65                write!(f, "network error: {}", msg)
66            }
67            HardwareError::RateLimited { retry_after_ms } => {
68                write!(f, "rate limited: retry after {} ms", retry_after_ms)
69            }
70        }
71    }
72}
73
74impl std::error::Error for HardwareError {}
75
76// ---------------------------------------------------------------------------
77// Core types
78// ---------------------------------------------------------------------------
79
80/// Type of quantum hardware provider.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82pub enum ProviderType {
83    IbmQuantum,
84    IonQ,
85    Rigetti,
86    AmazonBraket,
87    LocalSimulator,
88}
89
90impl fmt::Display for ProviderType {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        match self {
93            ProviderType::IbmQuantum => write!(f, "IBM Quantum"),
94            ProviderType::IonQ => write!(f, "IonQ"),
95            ProviderType::Rigetti => write!(f, "Rigetti"),
96            ProviderType::AmazonBraket => write!(f, "Amazon Braket"),
97            ProviderType::LocalSimulator => write!(f, "Local Simulator"),
98        }
99    }
100}
101
102/// Current operational status of a quantum device.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum DeviceStatus {
105    Online,
106    Offline,
107    Maintenance,
108    Retired,
109}
110
111impl fmt::Display for DeviceStatus {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        match self {
114            DeviceStatus::Online => write!(f, "online"),
115            DeviceStatus::Offline => write!(f, "offline"),
116            DeviceStatus::Maintenance => write!(f, "maintenance"),
117            DeviceStatus::Retired => write!(f, "retired"),
118        }
119    }
120}
121
122/// Status of a submitted quantum job.
123#[derive(Debug, Clone, PartialEq)]
124pub enum JobStatus {
125    Queued,
126    Running,
127    Completed,
128    Failed(String),
129    Cancelled,
130}
131
132/// Metadata describing a quantum device.
133#[derive(Debug, Clone)]
134pub struct DeviceInfo {
135    pub name: String,
136    pub provider: ProviderType,
137    pub num_qubits: u32,
138    pub basis_gates: Vec<String>,
139    pub coupling_map: Vec<(u32, u32)>,
140    pub max_shots: u32,
141    pub status: DeviceStatus,
142}
143
144/// Handle returned after submitting a circuit, used to poll status and
145/// retrieve results.
146#[derive(Debug, Clone)]
147pub struct JobHandle {
148    pub job_id: String,
149    pub provider: ProviderType,
150    pub submitted_at: u64,
151}
152
153/// Results returned after a hardware job completes.
154#[derive(Debug, Clone)]
155pub struct HardwareResult {
156    pub counts: HashMap<Vec<bool>, usize>,
157    pub shots: u32,
158    pub execution_time_ms: u64,
159    pub device_name: String,
160}
161
162/// Calibration data for a quantum device.
163#[derive(Debug, Clone)]
164pub struct DeviceCalibration {
165    pub device_name: String,
166    pub timestamp: u64,
167    /// T1 relaxation time per qubit in microseconds.
168    pub qubit_t1: Vec<f64>,
169    /// T2 dephasing time per qubit in microseconds.
170    pub qubit_t2: Vec<f64>,
171    /// Readout error per qubit: (P(1|0), P(0|1)).
172    pub readout_error: Vec<(f64, f64)>,
173    /// Gate error rates keyed by gate name (e.g. "cx_0_1").
174    pub gate_errors: HashMap<String, f64>,
175    /// Gate durations in nanoseconds keyed by gate name.
176    pub gate_times: HashMap<String, f64>,
177    /// Qubit connectivity as directed edges.
178    pub coupling_map: Vec<(u32, u32)>,
179}
180
181// ---------------------------------------------------------------------------
182// Provider trait
183// ---------------------------------------------------------------------------
184
185/// Unified interface for quantum hardware providers.
186///
187/// Each implementation exposes device discovery, calibration data, circuit
188/// submission, and result retrieval. Providers must be safe to share across
189/// threads.
190pub trait HardwareProvider: Send + Sync {
191    /// Human-readable name of this provider.
192    fn name(&self) -> &str;
193
194    /// The discriminant identifying this provider type.
195    fn provider_type(&self) -> ProviderType;
196
197    /// List all devices available through this provider.
198    fn available_devices(&self) -> Vec<DeviceInfo>;
199
200    /// Retrieve the most recent calibration data for a named device.
201    fn device_calibration(&self, device: &str) -> Option<DeviceCalibration>;
202
203    /// Submit a QASM circuit string for execution.
204    fn submit_circuit(
205        &self,
206        qasm: &str,
207        shots: u32,
208        device: &str,
209    ) -> Result<JobHandle, HardwareError>;
210
211    /// Poll the status of a previously submitted job.
212    fn job_status(&self, handle: &JobHandle) -> Result<JobStatus, HardwareError>;
213
214    /// Retrieve results for a completed job.
215    fn job_results(&self, handle: &JobHandle) -> Result<HardwareResult, HardwareError>;
216}
217
218// ---------------------------------------------------------------------------
219// QASM parsing helpers
220// ---------------------------------------------------------------------------
221
222/// Extract the number of qubits from a minimal QASM header.
223///
224/// Scans for lines of the form `qreg q[N];` or `qubit[N]` and returns the
225/// total qubit count. Falls back to `default` when no declaration is found.
226fn parse_qubit_count(qasm: &str, default: u32) -> u32 {
227    let mut total: u32 = 0;
228    for line in qasm.lines() {
229        let trimmed = line.trim();
230        // OpenQASM 2.0: qreg q[5];
231        if trimmed.starts_with("qreg") {
232            if let Some(start) = trimmed.find('[') {
233                if let Some(end) = trimmed.find(']') {
234                    if let Ok(n) = trimmed[start + 1..end].parse::<u32>() {
235                        total += n;
236                    }
237                }
238            }
239        }
240        // OpenQASM 3.0: qubit[5] q;
241        if trimmed.starts_with("qubit[") {
242            if let Some(end) = trimmed.find(']') {
243                if let Ok(n) = trimmed[6..end].parse::<u32>() {
244                    total += n;
245                }
246            }
247        }
248    }
249    if total == 0 { default } else { total }
250}
251
252/// Count gate operations in a QASM string (lines that look like gate
253/// applications, excluding declarations, comments, and directives).
254#[allow(dead_code)]
255fn parse_gate_count(qasm: &str) -> usize {
256    qasm.lines()
257        .map(|l| l.trim())
258        .filter(|l| {
259            !l.is_empty()
260                && !l.starts_with("//")
261                && !l.starts_with("OPENQASM")
262                && !l.starts_with("include")
263                && !l.starts_with("qreg")
264                && !l.starts_with("creg")
265                && !l.starts_with("qubit")
266                && !l.starts_with("bit")
267                && !l.starts_with("gate ")
268                && !l.starts_with('{')
269                && !l.starts_with('}')
270        })
271        .count()
272}
273
274// ---------------------------------------------------------------------------
275// Synthetic calibration helpers
276// ---------------------------------------------------------------------------
277
278/// Generate synthetic calibration data for a device with `num_qubits` qubits.
279fn synthetic_calibration(
280    device_name: &str,
281    num_qubits: u32,
282    coupling_map: &[(u32, u32)],
283) -> DeviceCalibration {
284    let mut qubit_t1 = Vec::with_capacity(num_qubits as usize);
285    let mut qubit_t2 = Vec::with_capacity(num_qubits as usize);
286    let mut readout_error = Vec::with_capacity(num_qubits as usize);
287
288    // Generate per-qubit values with deterministic variation seeded by index.
289    for i in 0..num_qubits {
290        let variation = 1.0 + 0.05 * ((i as f64 * 7.3).sin());
291        // Realistic T1 values: ~100us for superconducting, ~1s for trapped ion.
292        qubit_t1.push(100.0 * variation);
293        // T2 is typically 50-100% of T1.
294        qubit_t2.push(80.0 * variation);
295        // Readout error rates: P(1|0) and P(0|1) around 1-3%.
296        let re0 = 0.015 + 0.005 * ((i as f64 * 3.1).cos());
297        let re1 = 0.020 + 0.005 * ((i as f64 * 5.7).sin());
298        readout_error.push((re0, re1));
299    }
300
301    let mut gate_errors = HashMap::new();
302    let mut gate_times = HashMap::new();
303
304    // Single-qubit gate errors and times.
305    for i in 0..num_qubits {
306        let variation = 1.0 + 0.1 * ((i as f64 * 2.3).sin());
307        gate_errors.insert(format!("sx_{}", i), 0.0003 * variation);
308        gate_errors.insert(format!("rz_{}", i), 0.0);
309        gate_errors.insert(format!("x_{}", i), 0.0003 * variation);
310        gate_times.insert(format!("sx_{}", i), 35.5 * variation);
311        gate_times.insert(format!("rz_{}", i), 0.0);
312        gate_times.insert(format!("x_{}", i), 35.5 * variation);
313    }
314
315    // Two-qubit gate errors and times from the coupling map.
316    for &(q0, q1) in coupling_map {
317        let variation = 1.0 + 0.1 * (((q0 + q1) as f64 * 1.7).sin());
318        gate_errors.insert(format!("cx_{}_{}", q0, q1), 0.008 * variation);
319        gate_times.insert(format!("cx_{}_{}", q0, q1), 300.0 * variation);
320    }
321
322    DeviceCalibration {
323        device_name: device_name.to_string(),
324        timestamp: 1700000000,
325        qubit_t1,
326        qubit_t2,
327        readout_error,
328        gate_errors,
329        gate_times,
330        coupling_map: coupling_map.to_vec(),
331    }
332}
333
334/// Build a linear nearest-neighbour coupling map for `n` qubits.
335fn linear_coupling_map(n: u32) -> Vec<(u32, u32)> {
336    let mut map = Vec::with_capacity((n as usize).saturating_sub(1) * 2);
337    for i in 0..n.saturating_sub(1) {
338        map.push((i, i + 1));
339        map.push((i + 1, i));
340    }
341    map
342}
343
344/// Build a heavy-hex-style coupling map for `n` qubits (simplified).
345///
346/// This produces a superset of a linear chain plus periodic cross-links
347/// every 4 qubits to approximate IBM heavy-hex topology.
348fn heavy_hex_coupling_map(n: u32) -> Vec<(u32, u32)> {
349    let mut map = linear_coupling_map(n);
350    // Add cross-links to approximate heavy-hex layout.
351    let mut i = 0;
352    while i + 4 < n {
353        map.push((i, i + 4));
354        map.push((i + 4, i));
355        i += 4;
356    }
357    map
358}
359
360// ---------------------------------------------------------------------------
361// LocalSimulatorProvider
362// ---------------------------------------------------------------------------
363
364/// A hardware provider backed by the local state-vector simulator.
365///
366/// This provider is always available and does not require credentials. It
367/// builds a [`QuantumCircuit`] from the qubit count parsed out of the QASM
368/// header and executes via [`Simulator::run_shots`]. The resulting
369/// measurement histogram is returned as a [`HardwareResult`].
370pub struct LocalSimulatorProvider;
371
372impl LocalSimulatorProvider {
373    /// Maximum qubits supported by the local state-vector simulator.
374    const MAX_QUBITS: u32 = 32;
375    /// Maximum shots per job.
376    const MAX_SHOTS: u32 = 1_000_000;
377    /// Device name exposed by this provider.
378    const DEVICE_NAME: &'static str = "local_statevector_simulator";
379
380    fn device_info(&self) -> DeviceInfo {
381        DeviceInfo {
382            name: Self::DEVICE_NAME.to_string(),
383            provider: ProviderType::LocalSimulator,
384            num_qubits: Self::MAX_QUBITS,
385            basis_gates: vec![
386                "h".into(),
387                "x".into(),
388                "y".into(),
389                "z".into(),
390                "s".into(),
391                "sdg".into(),
392                "t".into(),
393                "tdg".into(),
394                "rx".into(),
395                "ry".into(),
396                "rz".into(),
397                "cx".into(),
398                "cz".into(),
399                "swap".into(),
400                "measure".into(),
401                "reset".into(),
402            ],
403            coupling_map: Vec::new(), // all-to-all connectivity
404            max_shots: Self::MAX_SHOTS,
405            status: DeviceStatus::Online,
406        }
407    }
408}
409
410impl HardwareProvider for LocalSimulatorProvider {
411    fn name(&self) -> &str {
412        "Local Simulator"
413    }
414
415    fn provider_type(&self) -> ProviderType {
416        ProviderType::LocalSimulator
417    }
418
419    fn available_devices(&self) -> Vec<DeviceInfo> {
420        vec![self.device_info()]
421    }
422
423    fn device_calibration(&self, device: &str) -> Option<DeviceCalibration> {
424        if device != Self::DEVICE_NAME {
425            return None;
426        }
427        // The local simulator has perfect gates; return synthetic values anyway
428        // so callers that expect calibration data still function.
429        let mut cal = synthetic_calibration(device, Self::MAX_QUBITS, &[]);
430        // Override with ideal values for the simulator.
431        for t1 in &mut cal.qubit_t1 {
432            *t1 = f64::INFINITY;
433        }
434        for t2 in &mut cal.qubit_t2 {
435            *t2 = f64::INFINITY;
436        }
437        for re in &mut cal.readout_error {
438            *re = (0.0, 0.0);
439        }
440        cal.gate_errors.values_mut().for_each(|v| *v = 0.0);
441        Some(cal)
442    }
443
444    fn submit_circuit(
445        &self,
446        qasm: &str,
447        shots: u32,
448        device: &str,
449    ) -> Result<JobHandle, HardwareError> {
450        if device != Self::DEVICE_NAME {
451            return Err(HardwareError::DeviceNotFound(device.to_string()));
452        }
453
454        let num_qubits = parse_qubit_count(qasm, 2);
455        if num_qubits > Self::MAX_QUBITS {
456            return Err(HardwareError::CircuitTooLarge {
457                qubits: num_qubits,
458                max: Self::MAX_QUBITS,
459            });
460        }
461
462        let effective_shots = shots.min(Self::MAX_SHOTS);
463
464        // Build a simple circuit from the parsed qubit count.
465        // We apply H to every qubit to produce a non-trivial distribution.
466        // A full QASM parser is out of scope; the local simulator provides a
467        // programmatic API via QuantumCircuit for rich circuit construction.
468        let mut circuit = QuantumCircuit::new(num_qubits);
469        // Apply H to each qubit so the result is a uniform superposition.
470        for q in 0..num_qubits {
471            circuit.h(q);
472        }
473        circuit.measure_all();
474
475        let start = std::time::Instant::now();
476        let shot_result = Simulator::run_shots(&circuit, effective_shots, Some(42))
477            .map_err(|e| HardwareError::JobFailed(format!("{}", e)))?;
478        let elapsed_ms = start.elapsed().as_millis() as u64;
479
480        // Store results in a thread-local so job_results can retrieve them.
481        // For this synchronous implementation, we store directly in the handle
482        // by encoding the result as a job_id with a special prefix.
483        let result = HardwareResult {
484            counts: shot_result.counts,
485            shots: effective_shots,
486            execution_time_ms: elapsed_ms,
487            device_name: Self::DEVICE_NAME.to_string(),
488        };
489
490        // Encode result compactly into thread-local storage keyed by job_id.
491        let job_id = format!("local-{}", fastrand_u64());
492        COMPLETED_JOBS.with(|jobs| {
493            jobs.borrow_mut().insert(job_id.clone(), result);
494        });
495
496        Ok(JobHandle {
497            job_id,
498            provider: ProviderType::LocalSimulator,
499            submitted_at: current_epoch_secs(),
500        })
501    }
502
503    fn job_status(&self, handle: &JobHandle) -> Result<JobStatus, HardwareError> {
504        if handle.provider != ProviderType::LocalSimulator {
505            return Err(HardwareError::DeviceNotFound(
506                "job does not belong to local simulator".to_string(),
507            ));
508        }
509        // Local jobs complete synchronously in submit_circuit.
510        let exists = COMPLETED_JOBS.with(|jobs| jobs.borrow().contains_key(&handle.job_id));
511        if exists {
512            Ok(JobStatus::Completed)
513        } else {
514            Err(HardwareError::JobFailed(format!(
515                "unknown job id: {}",
516                handle.job_id
517            )))
518        }
519    }
520
521    fn job_results(&self, handle: &JobHandle) -> Result<HardwareResult, HardwareError> {
522        if handle.provider != ProviderType::LocalSimulator {
523            return Err(HardwareError::DeviceNotFound(
524                "job does not belong to local simulator".to_string(),
525            ));
526        }
527        COMPLETED_JOBS.with(|jobs| {
528            jobs.borrow()
529                .get(&handle.job_id)
530                .cloned()
531                .ok_or_else(|| {
532                    HardwareError::JobFailed(format!("unknown job id: {}", handle.job_id))
533                })
534        })
535    }
536}
537
538// Thread-local storage for completed local simulator jobs.
539thread_local! {
540    static COMPLETED_JOBS: std::cell::RefCell<HashMap<String, HardwareResult>> =
541        std::cell::RefCell::new(HashMap::new());
542}
543
544/// Simple non-cryptographic pseudo-random u64 for job IDs.
545fn fastrand_u64() -> u64 {
546    use std::time::SystemTime;
547    let seed = SystemTime::now()
548        .duration_since(SystemTime::UNIX_EPOCH)
549        .unwrap_or_default()
550        .as_nanos() as u64;
551    // Splitmix64 single step.
552    let mut z = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
553    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
554    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
555    z ^ (z >> 31)
556}
557
558/// Returns the current time as seconds since the Unix epoch.
559fn current_epoch_secs() -> u64 {
560    use std::time::SystemTime;
561    SystemTime::now()
562        .duration_since(SystemTime::UNIX_EPOCH)
563        .unwrap_or_default()
564        .as_secs()
565}
566
567// ---------------------------------------------------------------------------
568// IBM Quantum stub provider
569// ---------------------------------------------------------------------------
570
571/// Stub provider for IBM Quantum.
572///
573/// Exposes realistic device metadata for the IBM Eagle r3 (127 qubits) and
574/// IBM Heron (133 qubits) processors. Circuit submission returns an
575/// authentication error since no real API token is configured.
576pub struct IbmQuantumProvider;
577
578impl IbmQuantumProvider {
579    fn eagle_device() -> DeviceInfo {
580        DeviceInfo {
581            name: "ibm_brisbane".to_string(),
582            provider: ProviderType::IbmQuantum,
583            num_qubits: 127,
584            basis_gates: vec![
585                "id".into(),
586                "rz".into(),
587                "sx".into(),
588                "x".into(),
589                "cx".into(),
590                "reset".into(),
591            ],
592            coupling_map: heavy_hex_coupling_map(127),
593            max_shots: 100_000,
594            status: DeviceStatus::Online,
595        }
596    }
597
598    fn heron_device() -> DeviceInfo {
599        DeviceInfo {
600            name: "ibm_fez".to_string(),
601            provider: ProviderType::IbmQuantum,
602            num_qubits: 133,
603            basis_gates: vec![
604                "id".into(),
605                "rz".into(),
606                "sx".into(),
607                "x".into(),
608                "ecr".into(),
609                "reset".into(),
610            ],
611            coupling_map: heavy_hex_coupling_map(133),
612            max_shots: 100_000,
613            status: DeviceStatus::Online,
614        }
615    }
616}
617
618impl HardwareProvider for IbmQuantumProvider {
619    fn name(&self) -> &str {
620        "IBM Quantum"
621    }
622
623    fn provider_type(&self) -> ProviderType {
624        ProviderType::IbmQuantum
625    }
626
627    fn available_devices(&self) -> Vec<DeviceInfo> {
628        vec![Self::eagle_device(), Self::heron_device()]
629    }
630
631    fn device_calibration(&self, device: &str) -> Option<DeviceCalibration> {
632        let dev = self
633            .available_devices()
634            .into_iter()
635            .find(|d| d.name == device)?;
636        Some(synthetic_calibration(device, dev.num_qubits, &dev.coupling_map))
637    }
638
639    fn submit_circuit(
640        &self,
641        _qasm: &str,
642        _shots: u32,
643        _device: &str,
644    ) -> Result<JobHandle, HardwareError> {
645        Err(HardwareError::AuthenticationFailed(
646            "IBM Quantum API token not configured. Set IBMQ_TOKEN environment variable.".into(),
647        ))
648    }
649
650    fn job_status(&self, _handle: &JobHandle) -> Result<JobStatus, HardwareError> {
651        Err(HardwareError::AuthenticationFailed(
652            "IBM Quantum API token not configured.".into(),
653        ))
654    }
655
656    fn job_results(&self, _handle: &JobHandle) -> Result<HardwareResult, HardwareError> {
657        Err(HardwareError::AuthenticationFailed(
658            "IBM Quantum API token not configured.".into(),
659        ))
660    }
661}
662
663// ---------------------------------------------------------------------------
664// IonQ stub provider
665// ---------------------------------------------------------------------------
666
667/// Stub provider for IonQ trapped-ion devices.
668///
669/// Exposes the IonQ Aria (25 qubits) and IonQ Forte (36 qubits) devices.
670pub struct IonQProvider;
671
672impl IonQProvider {
673    fn aria_device() -> DeviceInfo {
674        // Trapped-ion: all-to-all connectivity, so coupling map is complete graph.
675        let n = 25u32;
676        let mut cmap = Vec::new();
677        for i in 0..n {
678            for j in 0..n {
679                if i != j {
680                    cmap.push((i, j));
681                }
682            }
683        }
684        DeviceInfo {
685            name: "ionq_aria".to_string(),
686            provider: ProviderType::IonQ,
687            num_qubits: n,
688            basis_gates: vec!["gpi".into(), "gpi2".into(), "ms".into()],
689            coupling_map: cmap,
690            max_shots: 10_000,
691            status: DeviceStatus::Online,
692        }
693    }
694
695    fn forte_device() -> DeviceInfo {
696        let n = 36u32;
697        let mut cmap = Vec::new();
698        for i in 0..n {
699            for j in 0..n {
700                if i != j {
701                    cmap.push((i, j));
702                }
703            }
704        }
705        DeviceInfo {
706            name: "ionq_forte".to_string(),
707            provider: ProviderType::IonQ,
708            num_qubits: n,
709            basis_gates: vec!["gpi".into(), "gpi2".into(), "ms".into()],
710            coupling_map: cmap,
711            max_shots: 10_000,
712            status: DeviceStatus::Online,
713        }
714    }
715
716    fn aria_calibration() -> DeviceCalibration {
717        let dev = Self::aria_device();
718        let mut cal = synthetic_calibration(&dev.name, dev.num_qubits, &dev.coupling_map);
719        // Trapped-ion T1/T2 are much longer (seconds).
720        for t1 in &mut cal.qubit_t1 {
721            *t1 = 10_000_000.0; // ~10 seconds in microseconds
722        }
723        for t2 in &mut cal.qubit_t2 {
724            *t2 = 1_000_000.0; // ~1 second in microseconds
725        }
726        // IonQ single-qubit fidelity is very high.
727        for val in cal.gate_errors.values_mut() {
728            *val *= 0.1;
729        }
730        cal
731    }
732}
733
734impl HardwareProvider for IonQProvider {
735    fn name(&self) -> &str {
736        "IonQ"
737    }
738
739    fn provider_type(&self) -> ProviderType {
740        ProviderType::IonQ
741    }
742
743    fn available_devices(&self) -> Vec<DeviceInfo> {
744        vec![Self::aria_device(), Self::forte_device()]
745    }
746
747    fn device_calibration(&self, device: &str) -> Option<DeviceCalibration> {
748        match device {
749            "ionq_aria" => Some(Self::aria_calibration()),
750            "ionq_forte" => {
751                let dev = Self::forte_device();
752                let mut cal =
753                    synthetic_calibration(&dev.name, dev.num_qubits, &dev.coupling_map);
754                for t1 in &mut cal.qubit_t1 {
755                    *t1 = 10_000_000.0;
756                }
757                for t2 in &mut cal.qubit_t2 {
758                    *t2 = 1_000_000.0;
759                }
760                for val in cal.gate_errors.values_mut() {
761                    *val *= 0.1;
762                }
763                Some(cal)
764            }
765            _ => None,
766        }
767    }
768
769    fn submit_circuit(
770        &self,
771        _qasm: &str,
772        _shots: u32,
773        _device: &str,
774    ) -> Result<JobHandle, HardwareError> {
775        Err(HardwareError::AuthenticationFailed(
776            "IonQ API key not configured. Set IONQ_API_KEY environment variable.".into(),
777        ))
778    }
779
780    fn job_status(&self, _handle: &JobHandle) -> Result<JobStatus, HardwareError> {
781        Err(HardwareError::AuthenticationFailed(
782            "IonQ API key not configured.".into(),
783        ))
784    }
785
786    fn job_results(&self, _handle: &JobHandle) -> Result<HardwareResult, HardwareError> {
787        Err(HardwareError::AuthenticationFailed(
788            "IonQ API key not configured.".into(),
789        ))
790    }
791}
792
793// ---------------------------------------------------------------------------
794// Rigetti stub provider
795// ---------------------------------------------------------------------------
796
797/// Stub provider for Rigetti superconducting devices.
798///
799/// Exposes the Rigetti Ankaa-2 (84 qubits) processor.
800pub struct RigettiProvider;
801
802impl RigettiProvider {
803    fn ankaa_device() -> DeviceInfo {
804        DeviceInfo {
805            name: "rigetti_ankaa_2".to_string(),
806            provider: ProviderType::Rigetti,
807            num_qubits: 84,
808            basis_gates: vec![
809                "rx".into(),
810                "rz".into(),
811                "cz".into(),
812                "measure".into(),
813            ],
814            coupling_map: linear_coupling_map(84),
815            max_shots: 100_000,
816            status: DeviceStatus::Online,
817        }
818    }
819}
820
821impl HardwareProvider for RigettiProvider {
822    fn name(&self) -> &str {
823        "Rigetti"
824    }
825
826    fn provider_type(&self) -> ProviderType {
827        ProviderType::Rigetti
828    }
829
830    fn available_devices(&self) -> Vec<DeviceInfo> {
831        vec![Self::ankaa_device()]
832    }
833
834    fn device_calibration(&self, device: &str) -> Option<DeviceCalibration> {
835        if device != "rigetti_ankaa_2" {
836            return None;
837        }
838        let dev = Self::ankaa_device();
839        Some(synthetic_calibration(device, dev.num_qubits, &dev.coupling_map))
840    }
841
842    fn submit_circuit(
843        &self,
844        _qasm: &str,
845        _shots: u32,
846        _device: &str,
847    ) -> Result<JobHandle, HardwareError> {
848        Err(HardwareError::AuthenticationFailed(
849            "Rigetti QCS credentials not configured. Set QCS_ACCESS_TOKEN environment variable."
850                .into(),
851        ))
852    }
853
854    fn job_status(&self, _handle: &JobHandle) -> Result<JobStatus, HardwareError> {
855        Err(HardwareError::AuthenticationFailed(
856            "Rigetti QCS credentials not configured.".into(),
857        ))
858    }
859
860    fn job_results(&self, _handle: &JobHandle) -> Result<HardwareResult, HardwareError> {
861        Err(HardwareError::AuthenticationFailed(
862            "Rigetti QCS credentials not configured.".into(),
863        ))
864    }
865}
866
867// ---------------------------------------------------------------------------
868// Amazon Braket stub provider
869// ---------------------------------------------------------------------------
870
871/// Stub provider for Amazon Braket managed quantum services.
872///
873/// Exposes an IonQ Harmony device (11 qubits) and a Rigetti Aspen-M-3
874/// device (79 qubits) accessible through the Braket API.
875pub struct AmazonBraketProvider;
876
877impl AmazonBraketProvider {
878    fn harmony_device() -> DeviceInfo {
879        let n = 11u32;
880        let mut cmap = Vec::new();
881        for i in 0..n {
882            for j in 0..n {
883                if i != j {
884                    cmap.push((i, j));
885                }
886            }
887        }
888        DeviceInfo {
889            name: "braket_ionq_harmony".to_string(),
890            provider: ProviderType::AmazonBraket,
891            num_qubits: n,
892            basis_gates: vec!["gpi".into(), "gpi2".into(), "ms".into()],
893            coupling_map: cmap,
894            max_shots: 10_000,
895            status: DeviceStatus::Online,
896        }
897    }
898
899    fn aspen_device() -> DeviceInfo {
900        DeviceInfo {
901            name: "braket_rigetti_aspen_m3".to_string(),
902            provider: ProviderType::AmazonBraket,
903            num_qubits: 79,
904            basis_gates: vec![
905                "rx".into(),
906                "rz".into(),
907                "cz".into(),
908                "measure".into(),
909            ],
910            coupling_map: linear_coupling_map(79),
911            max_shots: 100_000,
912            status: DeviceStatus::Online,
913        }
914    }
915}
916
917impl HardwareProvider for AmazonBraketProvider {
918    fn name(&self) -> &str {
919        "Amazon Braket"
920    }
921
922    fn provider_type(&self) -> ProviderType {
923        ProviderType::AmazonBraket
924    }
925
926    fn available_devices(&self) -> Vec<DeviceInfo> {
927        vec![Self::harmony_device(), Self::aspen_device()]
928    }
929
930    fn device_calibration(&self, device: &str) -> Option<DeviceCalibration> {
931        let dev = self
932            .available_devices()
933            .into_iter()
934            .find(|d| d.name == device)?;
935        Some(synthetic_calibration(device, dev.num_qubits, &dev.coupling_map))
936    }
937
938    fn submit_circuit(
939        &self,
940        _qasm: &str,
941        _shots: u32,
942        _device: &str,
943    ) -> Result<JobHandle, HardwareError> {
944        Err(HardwareError::AuthenticationFailed(
945            "AWS credentials not configured. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY."
946                .into(),
947        ))
948    }
949
950    fn job_status(&self, _handle: &JobHandle) -> Result<JobStatus, HardwareError> {
951        Err(HardwareError::AuthenticationFailed(
952            "AWS credentials not configured.".into(),
953        ))
954    }
955
956    fn job_results(&self, _handle: &JobHandle) -> Result<HardwareResult, HardwareError> {
957        Err(HardwareError::AuthenticationFailed(
958            "AWS credentials not configured.".into(),
959        ))
960    }
961}
962
963// ---------------------------------------------------------------------------
964// Provider registry
965// ---------------------------------------------------------------------------
966
967/// Registry that manages multiple [`HardwareProvider`] implementations.
968///
969/// Provides lookup by [`ProviderType`] and aggregated device listing across
970/// all registered providers.
971pub struct ProviderRegistry {
972    providers: Vec<Box<dyn HardwareProvider>>,
973}
974
975impl ProviderRegistry {
976    /// Create an empty registry with no providers.
977    pub fn new() -> Self {
978        Self {
979            providers: Vec::new(),
980        }
981    }
982
983    /// Register a new hardware provider.
984    pub fn register(&mut self, provider: Box<dyn HardwareProvider>) {
985        self.providers.push(provider);
986    }
987
988    /// Look up a provider by its type discriminant.
989    ///
990    /// Returns a reference to the first registered provider of the given type,
991    /// or `None` if no such provider has been registered.
992    pub fn get(&self, provider: ProviderType) -> Option<&dyn HardwareProvider> {
993        self.providers
994            .iter()
995            .find(|p| p.provider_type() == provider)
996            .map(|p| p.as_ref())
997    }
998
999    /// Collect device info from every registered provider.
1000    pub fn all_devices(&self) -> Vec<DeviceInfo> {
1001        self.providers
1002            .iter()
1003            .flat_map(|p| p.available_devices())
1004            .collect()
1005    }
1006}
1007
1008impl Default for ProviderRegistry {
1009    /// Create a registry pre-loaded with the [`LocalSimulatorProvider`].
1010    fn default() -> Self {
1011        let mut reg = Self::new();
1012        reg.register(Box::new(LocalSimulatorProvider));
1013        reg
1014    }
1015}
1016
1017// ---------------------------------------------------------------------------
1018// Tests
1019// ---------------------------------------------------------------------------
1020
1021#[cfg(test)]
1022mod tests {
1023    use super::*;
1024
1025    // -- ProviderType --
1026
1027    #[test]
1028    fn provider_type_display() {
1029        assert_eq!(format!("{}", ProviderType::IbmQuantum), "IBM Quantum");
1030        assert_eq!(format!("{}", ProviderType::IonQ), "IonQ");
1031        assert_eq!(format!("{}", ProviderType::Rigetti), "Rigetti");
1032        assert_eq!(format!("{}", ProviderType::AmazonBraket), "Amazon Braket");
1033        assert_eq!(
1034            format!("{}", ProviderType::LocalSimulator),
1035            "Local Simulator"
1036        );
1037    }
1038
1039    #[test]
1040    fn provider_type_equality() {
1041        assert_eq!(ProviderType::IbmQuantum, ProviderType::IbmQuantum);
1042        assert_ne!(ProviderType::IbmQuantum, ProviderType::IonQ);
1043    }
1044
1045    // -- DeviceStatus --
1046
1047    #[test]
1048    fn device_status_display() {
1049        assert_eq!(format!("{}", DeviceStatus::Online), "online");
1050        assert_eq!(format!("{}", DeviceStatus::Offline), "offline");
1051        assert_eq!(format!("{}", DeviceStatus::Maintenance), "maintenance");
1052        assert_eq!(format!("{}", DeviceStatus::Retired), "retired");
1053    }
1054
1055    // -- JobStatus --
1056
1057    #[test]
1058    fn job_status_variants() {
1059        let queued = JobStatus::Queued;
1060        let running = JobStatus::Running;
1061        let completed = JobStatus::Completed;
1062        let failed = JobStatus::Failed("timeout".to_string());
1063        let cancelled = JobStatus::Cancelled;
1064
1065        assert_eq!(queued, JobStatus::Queued);
1066        assert_eq!(running, JobStatus::Running);
1067        assert_eq!(completed, JobStatus::Completed);
1068        assert_eq!(failed, JobStatus::Failed("timeout".to_string()));
1069        assert_eq!(cancelled, JobStatus::Cancelled);
1070    }
1071
1072    // -- HardwareError --
1073
1074    #[test]
1075    fn hardware_error_display() {
1076        let e = HardwareError::AuthenticationFailed("no token".into());
1077        assert!(format!("{}", e).contains("authentication failed"));
1078
1079        let e = HardwareError::DeviceNotFound("foo".into());
1080        assert!(format!("{}", e).contains("device not found"));
1081
1082        let e = HardwareError::DeviceOffline("bar".into());
1083        assert!(format!("{}", e).contains("device offline"));
1084
1085        let e = HardwareError::CircuitTooLarge {
1086            qubits: 50,
1087            max: 32,
1088        };
1089        let msg = format!("{}", e);
1090        assert!(msg.contains("50"));
1091        assert!(msg.contains("32"));
1092
1093        let e = HardwareError::JobFailed("oops".into());
1094        assert!(format!("{}", e).contains("job failed"));
1095
1096        let e = HardwareError::NetworkError("timeout".into());
1097        assert!(format!("{}", e).contains("network error"));
1098
1099        let e = HardwareError::RateLimited {
1100            retry_after_ms: 5000,
1101        };
1102        assert!(format!("{}", e).contains("5000"));
1103    }
1104
1105    #[test]
1106    fn hardware_error_is_error_trait() {
1107        let e: Box<dyn std::error::Error> =
1108            Box::new(HardwareError::NetworkError("test".into()));
1109        assert!(e.to_string().contains("network error"));
1110    }
1111
1112    // -- DeviceInfo --
1113
1114    #[test]
1115    fn device_info_construction() {
1116        let dev = DeviceInfo {
1117            name: "test_device".into(),
1118            provider: ProviderType::LocalSimulator,
1119            num_qubits: 5,
1120            basis_gates: vec!["h".into(), "cx".into()],
1121            coupling_map: vec![(0, 1), (1, 2)],
1122            max_shots: 1000,
1123            status: DeviceStatus::Online,
1124        };
1125        assert_eq!(dev.name, "test_device");
1126        assert_eq!(dev.num_qubits, 5);
1127        assert_eq!(dev.basis_gates.len(), 2);
1128        assert_eq!(dev.coupling_map.len(), 2);
1129        assert_eq!(dev.status, DeviceStatus::Online);
1130    }
1131
1132    // -- JobHandle --
1133
1134    #[test]
1135    fn job_handle_construction() {
1136        let handle = JobHandle {
1137            job_id: "abc-123".into(),
1138            provider: ProviderType::IonQ,
1139            submitted_at: 1700000000,
1140        };
1141        assert_eq!(handle.job_id, "abc-123");
1142        assert_eq!(handle.provider, ProviderType::IonQ);
1143        assert_eq!(handle.submitted_at, 1700000000);
1144    }
1145
1146    // -- HardwareResult --
1147
1148    #[test]
1149    fn hardware_result_construction() {
1150        let mut counts = HashMap::new();
1151        counts.insert(vec![false, false], 500);
1152        counts.insert(vec![true, true], 500);
1153        let result = HardwareResult {
1154            counts,
1155            shots: 1000,
1156            execution_time_ms: 42,
1157            device_name: "test".into(),
1158        };
1159        assert_eq!(result.shots, 1000);
1160        assert_eq!(result.counts.len(), 2);
1161        assert_eq!(result.execution_time_ms, 42);
1162    }
1163
1164    // -- DeviceCalibration --
1165
1166    #[test]
1167    fn device_calibration_construction() {
1168        let cal = DeviceCalibration {
1169            device_name: "dev".into(),
1170            timestamp: 1700000000,
1171            qubit_t1: vec![100.0, 110.0],
1172            qubit_t2: vec![80.0, 85.0],
1173            readout_error: vec![(0.01, 0.02), (0.015, 0.025)],
1174            gate_errors: HashMap::new(),
1175            gate_times: HashMap::new(),
1176            coupling_map: vec![(0, 1)],
1177        };
1178        assert_eq!(cal.qubit_t1.len(), 2);
1179        assert_eq!(cal.qubit_t2.len(), 2);
1180        assert_eq!(cal.readout_error.len(), 2);
1181    }
1182
1183    // -- QASM parsing helpers --
1184
1185    #[test]
1186    fn parse_qubit_count_openqasm2() {
1187        let qasm = "OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[5];\ncreg c[5];\nh q[0];\n";
1188        assert_eq!(parse_qubit_count(qasm, 1), 5);
1189    }
1190
1191    #[test]
1192    fn parse_qubit_count_openqasm3() {
1193        let qasm = "OPENQASM 3.0;\nqubit[8] q;\nbit[8] c;\n";
1194        assert_eq!(parse_qubit_count(qasm, 1), 8);
1195    }
1196
1197    #[test]
1198    fn parse_qubit_count_multiple_registers() {
1199        let qasm = "qreg a[3];\nqreg b[4];\n";
1200        assert_eq!(parse_qubit_count(qasm, 1), 7);
1201    }
1202
1203    #[test]
1204    fn parse_qubit_count_fallback() {
1205        let qasm = "h q[0];\ncx q[0], q[1];\n";
1206        assert_eq!(parse_qubit_count(qasm, 2), 2);
1207    }
1208
1209    #[test]
1210    fn parse_gate_count_basic() {
1211        let qasm =
1212            "OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[2];\ncreg c[2];\nh q[0];\ncx q[0], q[1];\nmeasure q[0] -> c[0];\n";
1213        assert_eq!(parse_gate_count(qasm), 3);
1214    }
1215
1216    #[test]
1217    fn parse_gate_count_empty() {
1218        let qasm = "OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[2];\n";
1219        assert_eq!(parse_gate_count(qasm), 0);
1220    }
1221
1222    // -- Synthetic calibration --
1223
1224    #[test]
1225    fn synthetic_calibration_correct_sizes() {
1226        let coupling = vec![(0, 1), (1, 0), (1, 2), (2, 1)];
1227        let cal = synthetic_calibration("test", 3, &coupling);
1228        assert_eq!(cal.device_name, "test");
1229        assert_eq!(cal.qubit_t1.len(), 3);
1230        assert_eq!(cal.qubit_t2.len(), 3);
1231        assert_eq!(cal.readout_error.len(), 3);
1232        assert_eq!(cal.coupling_map.len(), 4);
1233        // Single-qubit gates: 3 types x 3 qubits = 9
1234        // Two-qubit gates: 4 edges
1235        assert!(cal.gate_errors.len() >= 9);
1236        assert!(cal.gate_times.len() >= 9);
1237    }
1238
1239    #[test]
1240    fn synthetic_calibration_values_positive() {
1241        let cal = synthetic_calibration("dev", 5, &[(0, 1)]);
1242        for t1 in &cal.qubit_t1 {
1243            assert!(*t1 > 0.0, "T1 must be positive");
1244        }
1245        for t2 in &cal.qubit_t2 {
1246            assert!(*t2 > 0.0, "T2 must be positive");
1247        }
1248        for &(p0, p1) in &cal.readout_error {
1249            assert!(p0 >= 0.0 && p0 <= 1.0);
1250            assert!(p1 >= 0.0 && p1 <= 1.0);
1251        }
1252    }
1253
1254    // -- Coupling map helpers --
1255
1256    #[test]
1257    fn linear_coupling_map_correct() {
1258        let map = linear_coupling_map(4);
1259        // 3 edges * 2 directions = 6
1260        assert_eq!(map.len(), 6);
1261        assert!(map.contains(&(0, 1)));
1262        assert!(map.contains(&(1, 0)));
1263        assert!(map.contains(&(2, 3)));
1264        assert!(map.contains(&(3, 2)));
1265    }
1266
1267    #[test]
1268    fn linear_coupling_map_single_qubit() {
1269        let map = linear_coupling_map(1);
1270        assert!(map.is_empty());
1271    }
1272
1273    #[test]
1274    fn heavy_hex_coupling_map_has_cross_links() {
1275        let map = heavy_hex_coupling_map(20);
1276        // Should have linear edges plus cross-links.
1277        assert!(map.len() > linear_coupling_map(20).len());
1278        // Cross-link from 0 to 4 should exist.
1279        assert!(map.contains(&(0, 4)));
1280        assert!(map.contains(&(4, 0)));
1281    }
1282
1283    // -- LocalSimulatorProvider --
1284
1285    #[test]
1286    fn local_provider_name_and_type() {
1287        let prov = LocalSimulatorProvider;
1288        assert_eq!(prov.name(), "Local Simulator");
1289        assert_eq!(prov.provider_type(), ProviderType::LocalSimulator);
1290    }
1291
1292    #[test]
1293    fn local_provider_devices() {
1294        let prov = LocalSimulatorProvider;
1295        let devs = prov.available_devices();
1296        assert_eq!(devs.len(), 1);
1297        assert_eq!(devs[0].name, "local_statevector_simulator");
1298        assert_eq!(devs[0].num_qubits, 32);
1299        assert_eq!(devs[0].status, DeviceStatus::Online);
1300        assert!(devs[0].basis_gates.contains(&"h".to_string()));
1301        assert!(devs[0].basis_gates.contains(&"cx".to_string()));
1302    }
1303
1304    #[test]
1305    fn local_provider_calibration() {
1306        let prov = LocalSimulatorProvider;
1307        let cal = prov
1308            .device_calibration("local_statevector_simulator")
1309            .expect("calibration should exist");
1310        assert_eq!(cal.device_name, "local_statevector_simulator");
1311        assert_eq!(cal.qubit_t1.len(), 32);
1312        // Simulator has ideal gates.
1313        for &(p0, p1) in &cal.readout_error {
1314            assert!((p0 - 0.0).abs() < 1e-12);
1315            assert!((p1 - 0.0).abs() < 1e-12);
1316        }
1317        for val in cal.gate_errors.values() {
1318            assert!((*val - 0.0).abs() < 1e-12);
1319        }
1320    }
1321
1322    #[test]
1323    fn local_provider_calibration_unknown_device() {
1324        let prov = LocalSimulatorProvider;
1325        assert!(prov.device_calibration("nonexistent").is_none());
1326    }
1327
1328    #[test]
1329    fn local_provider_submit_and_retrieve() {
1330        let prov = LocalSimulatorProvider;
1331        let qasm = "OPENQASM 2.0;\nqreg q[2];\nh q[0];\ncx q[0], q[1];\n";
1332        let handle = prov
1333            .submit_circuit(qasm, 100, "local_statevector_simulator")
1334            .expect("submit should succeed");
1335
1336        assert_eq!(handle.provider, ProviderType::LocalSimulator);
1337        assert!(handle.job_id.starts_with("local-"));
1338
1339        // Job status should be completed.
1340        let status = prov.job_status(&handle).expect("status should succeed");
1341        assert_eq!(status, JobStatus::Completed);
1342
1343        // Results should have the right shot count.
1344        let result = prov.job_results(&handle).expect("results should succeed");
1345        assert_eq!(result.device_name, "local_statevector_simulator");
1346        // Total counts should equal the number of shots.
1347        let total: usize = result.counts.values().sum();
1348        assert_eq!(total, 100);
1349        assert_eq!(result.shots, 100);
1350    }
1351
1352    #[test]
1353    fn local_provider_submit_wrong_device() {
1354        let prov = LocalSimulatorProvider;
1355        let result = prov.submit_circuit("qreg q[2];", 10, "wrong_device");
1356        assert!(result.is_err());
1357        match result.unwrap_err() {
1358            HardwareError::DeviceNotFound(name) => assert_eq!(name, "wrong_device"),
1359            other => panic!("expected DeviceNotFound, got: {:?}", other),
1360        }
1361    }
1362
1363    #[test]
1364    fn local_provider_circuit_too_large() {
1365        let prov = LocalSimulatorProvider;
1366        let qasm = "OPENQASM 2.0;\nqreg q[50];\n";
1367        let result = prov.submit_circuit(qasm, 10, "local_statevector_simulator");
1368        assert!(result.is_err());
1369        match result.unwrap_err() {
1370            HardwareError::CircuitTooLarge { qubits, max } => {
1371                assert_eq!(qubits, 50);
1372                assert_eq!(max, 32);
1373            }
1374            other => panic!("expected CircuitTooLarge, got: {:?}", other),
1375        }
1376    }
1377
1378    #[test]
1379    fn local_provider_unknown_job() {
1380        let prov = LocalSimulatorProvider;
1381        let handle = JobHandle {
1382            job_id: "nonexistent".into(),
1383            provider: ProviderType::LocalSimulator,
1384            submitted_at: 0,
1385        };
1386        assert!(prov.job_status(&handle).is_err());
1387        assert!(prov.job_results(&handle).is_err());
1388    }
1389
1390    #[test]
1391    fn local_provider_wrong_provider_handle() {
1392        let prov = LocalSimulatorProvider;
1393        let handle = JobHandle {
1394            job_id: "some-id".into(),
1395            provider: ProviderType::IbmQuantum,
1396            submitted_at: 0,
1397        };
1398        assert!(prov.job_status(&handle).is_err());
1399        assert!(prov.job_results(&handle).is_err());
1400    }
1401
1402    // -- IBM Quantum stub --
1403
1404    #[test]
1405    fn ibm_provider_name_and_type() {
1406        let prov = IbmQuantumProvider;
1407        assert_eq!(prov.name(), "IBM Quantum");
1408        assert_eq!(prov.provider_type(), ProviderType::IbmQuantum);
1409    }
1410
1411    #[test]
1412    fn ibm_provider_devices() {
1413        let prov = IbmQuantumProvider;
1414        let devs = prov.available_devices();
1415        assert_eq!(devs.len(), 2);
1416
1417        let brisbane = devs.iter().find(|d| d.name == "ibm_brisbane").unwrap();
1418        assert_eq!(brisbane.num_qubits, 127);
1419        assert_eq!(brisbane.provider, ProviderType::IbmQuantum);
1420        assert_eq!(brisbane.status, DeviceStatus::Online);
1421
1422        let fez = devs.iter().find(|d| d.name == "ibm_fez").unwrap();
1423        assert_eq!(fez.num_qubits, 133);
1424    }
1425
1426    #[test]
1427    fn ibm_provider_calibration() {
1428        let prov = IbmQuantumProvider;
1429        let cal = prov
1430            .device_calibration("ibm_brisbane")
1431            .expect("calibration should exist");
1432        assert_eq!(cal.qubit_t1.len(), 127);
1433        assert_eq!(cal.qubit_t2.len(), 127);
1434        assert_eq!(cal.readout_error.len(), 127);
1435    }
1436
1437    #[test]
1438    fn ibm_provider_calibration_unknown_device() {
1439        let prov = IbmQuantumProvider;
1440        assert!(prov.device_calibration("nonexistent").is_none());
1441    }
1442
1443    #[test]
1444    fn ibm_provider_submit_fails_auth() {
1445        let prov = IbmQuantumProvider;
1446        let result = prov.submit_circuit("qreg q[2];", 100, "ibm_brisbane");
1447        assert!(result.is_err());
1448        match result.unwrap_err() {
1449            HardwareError::AuthenticationFailed(msg) => {
1450                assert!(msg.contains("IBM Quantum"));
1451            }
1452            other => panic!("expected AuthenticationFailed, got: {:?}", other),
1453        }
1454    }
1455
1456    #[test]
1457    fn ibm_provider_job_status_fails_auth() {
1458        let prov = IbmQuantumProvider;
1459        let handle = JobHandle {
1460            job_id: "x".into(),
1461            provider: ProviderType::IbmQuantum,
1462            submitted_at: 0,
1463        };
1464        assert!(prov.job_status(&handle).is_err());
1465        assert!(prov.job_results(&handle).is_err());
1466    }
1467
1468    // -- IonQ stub --
1469
1470    #[test]
1471    fn ionq_provider_name_and_type() {
1472        let prov = IonQProvider;
1473        assert_eq!(prov.name(), "IonQ");
1474        assert_eq!(prov.provider_type(), ProviderType::IonQ);
1475    }
1476
1477    #[test]
1478    fn ionq_provider_devices() {
1479        let prov = IonQProvider;
1480        let devs = prov.available_devices();
1481        assert_eq!(devs.len(), 2);
1482
1483        let aria = devs.iter().find(|d| d.name == "ionq_aria").unwrap();
1484        assert_eq!(aria.num_qubits, 25);
1485        // Trapped-ion: full connectivity = 25*24 = 600 edges.
1486        assert_eq!(aria.coupling_map.len(), 25 * 24);
1487
1488        let forte = devs.iter().find(|d| d.name == "ionq_forte").unwrap();
1489        assert_eq!(forte.num_qubits, 36);
1490    }
1491
1492    #[test]
1493    fn ionq_provider_calibration_aria() {
1494        let prov = IonQProvider;
1495        let cal = prov
1496            .device_calibration("ionq_aria")
1497            .expect("calibration should exist");
1498        assert_eq!(cal.qubit_t1.len(), 25);
1499        // Trapped-ion T1 should be very long.
1500        for t1 in &cal.qubit_t1 {
1501            assert!(*t1 > 1_000_000.0);
1502        }
1503    }
1504
1505    #[test]
1506    fn ionq_provider_calibration_forte() {
1507        let prov = IonQProvider;
1508        let cal = prov
1509            .device_calibration("ionq_forte")
1510            .expect("calibration should exist");
1511        assert_eq!(cal.qubit_t1.len(), 36);
1512    }
1513
1514    #[test]
1515    fn ionq_provider_calibration_unknown() {
1516        let prov = IonQProvider;
1517        assert!(prov.device_calibration("nonexistent").is_none());
1518    }
1519
1520    #[test]
1521    fn ionq_provider_submit_fails_auth() {
1522        let prov = IonQProvider;
1523        let result = prov.submit_circuit("qreg q[2];", 100, "ionq_aria");
1524        assert!(result.is_err());
1525        match result.unwrap_err() {
1526            HardwareError::AuthenticationFailed(msg) => {
1527                assert!(msg.contains("IonQ"));
1528            }
1529            other => panic!("expected AuthenticationFailed, got: {:?}", other),
1530        }
1531    }
1532
1533    // -- Rigetti stub --
1534
1535    #[test]
1536    fn rigetti_provider_name_and_type() {
1537        let prov = RigettiProvider;
1538        assert_eq!(prov.name(), "Rigetti");
1539        assert_eq!(prov.provider_type(), ProviderType::Rigetti);
1540    }
1541
1542    #[test]
1543    fn rigetti_provider_devices() {
1544        let prov = RigettiProvider;
1545        let devs = prov.available_devices();
1546        assert_eq!(devs.len(), 1);
1547        assert_eq!(devs[0].name, "rigetti_ankaa_2");
1548        assert_eq!(devs[0].num_qubits, 84);
1549    }
1550
1551    #[test]
1552    fn rigetti_provider_calibration() {
1553        let prov = RigettiProvider;
1554        let cal = prov
1555            .device_calibration("rigetti_ankaa_2")
1556            .expect("calibration should exist");
1557        assert_eq!(cal.qubit_t1.len(), 84);
1558        assert_eq!(cal.qubit_t2.len(), 84);
1559    }
1560
1561    #[test]
1562    fn rigetti_provider_calibration_unknown() {
1563        let prov = RigettiProvider;
1564        assert!(prov.device_calibration("nonexistent").is_none());
1565    }
1566
1567    #[test]
1568    fn rigetti_provider_submit_fails_auth() {
1569        let prov = RigettiProvider;
1570        let result = prov.submit_circuit("qreg q[2];", 100, "rigetti_ankaa_2");
1571        assert!(result.is_err());
1572        match result.unwrap_err() {
1573            HardwareError::AuthenticationFailed(msg) => {
1574                assert!(msg.contains("Rigetti"));
1575            }
1576            other => panic!("expected AuthenticationFailed, got: {:?}", other),
1577        }
1578    }
1579
1580    // -- Amazon Braket stub --
1581
1582    #[test]
1583    fn braket_provider_name_and_type() {
1584        let prov = AmazonBraketProvider;
1585        assert_eq!(prov.name(), "Amazon Braket");
1586        assert_eq!(prov.provider_type(), ProviderType::AmazonBraket);
1587    }
1588
1589    #[test]
1590    fn braket_provider_devices() {
1591        let prov = AmazonBraketProvider;
1592        let devs = prov.available_devices();
1593        assert_eq!(devs.len(), 2);
1594
1595        let harmony = devs
1596            .iter()
1597            .find(|d| d.name == "braket_ionq_harmony")
1598            .unwrap();
1599        assert_eq!(harmony.num_qubits, 11);
1600
1601        let aspen = devs
1602            .iter()
1603            .find(|d| d.name == "braket_rigetti_aspen_m3")
1604            .unwrap();
1605        assert_eq!(aspen.num_qubits, 79);
1606    }
1607
1608    #[test]
1609    fn braket_provider_calibration() {
1610        let prov = AmazonBraketProvider;
1611        let cal = prov
1612            .device_calibration("braket_ionq_harmony")
1613            .expect("calibration should exist");
1614        assert_eq!(cal.qubit_t1.len(), 11);
1615
1616        let cal2 = prov
1617            .device_calibration("braket_rigetti_aspen_m3")
1618            .expect("calibration should exist");
1619        assert_eq!(cal2.qubit_t1.len(), 79);
1620    }
1621
1622    #[test]
1623    fn braket_provider_calibration_unknown() {
1624        let prov = AmazonBraketProvider;
1625        assert!(prov.device_calibration("nonexistent").is_none());
1626    }
1627
1628    #[test]
1629    fn braket_provider_submit_fails_auth() {
1630        let prov = AmazonBraketProvider;
1631        let result = prov.submit_circuit("qreg q[2];", 100, "braket_ionq_harmony");
1632        assert!(result.is_err());
1633        match result.unwrap_err() {
1634            HardwareError::AuthenticationFailed(msg) => {
1635                assert!(msg.contains("AWS"));
1636            }
1637            other => panic!("expected AuthenticationFailed, got: {:?}", other),
1638        }
1639    }
1640
1641    // -- ProviderRegistry --
1642
1643    #[test]
1644    fn registry_new_is_empty() {
1645        let reg = ProviderRegistry::new();
1646        assert!(reg.all_devices().is_empty());
1647        assert!(reg.get(ProviderType::LocalSimulator).is_none());
1648    }
1649
1650    #[test]
1651    fn registry_default_has_local_simulator() {
1652        let reg = ProviderRegistry::default();
1653        let local = reg.get(ProviderType::LocalSimulator);
1654        assert!(local.is_some());
1655        assert_eq!(local.unwrap().name(), "Local Simulator");
1656    }
1657
1658    #[test]
1659    fn registry_default_devices() {
1660        let reg = ProviderRegistry::default();
1661        let devs = reg.all_devices();
1662        assert_eq!(devs.len(), 1);
1663        assert_eq!(devs[0].name, "local_statevector_simulator");
1664    }
1665
1666    #[test]
1667    fn registry_register_multiple() {
1668        let mut reg = ProviderRegistry::new();
1669        reg.register(Box::new(LocalSimulatorProvider));
1670        reg.register(Box::new(IbmQuantumProvider));
1671        reg.register(Box::new(IonQProvider));
1672        reg.register(Box::new(RigettiProvider));
1673        reg.register(Box::new(AmazonBraketProvider));
1674
1675        // All providers should be accessible.
1676        assert!(reg.get(ProviderType::LocalSimulator).is_some());
1677        assert!(reg.get(ProviderType::IbmQuantum).is_some());
1678        assert!(reg.get(ProviderType::IonQ).is_some());
1679        assert!(reg.get(ProviderType::Rigetti).is_some());
1680        assert!(reg.get(ProviderType::AmazonBraket).is_some());
1681
1682        // Total devices: 1 + 2 + 2 + 1 + 2 = 8
1683        assert_eq!(reg.all_devices().len(), 8);
1684    }
1685
1686    #[test]
1687    fn registry_get_nonexistent() {
1688        let reg = ProviderRegistry::default();
1689        assert!(reg.get(ProviderType::IbmQuantum).is_none());
1690    }
1691
1692    #[test]
1693    fn registry_all_devices_aggregates() {
1694        let mut reg = ProviderRegistry::new();
1695        reg.register(Box::new(IbmQuantumProvider));
1696        reg.register(Box::new(IonQProvider));
1697
1698        let devs = reg.all_devices();
1699        // IBM: 2 devices, IonQ: 2 devices
1700        assert_eq!(devs.len(), 4);
1701        let names: Vec<&str> = devs.iter().map(|d| d.name.as_str()).collect();
1702        assert!(names.contains(&"ibm_brisbane"));
1703        assert!(names.contains(&"ibm_fez"));
1704        assert!(names.contains(&"ionq_aria"));
1705        assert!(names.contains(&"ionq_forte"));
1706    }
1707
1708    // -- Integration: submit through registry --
1709
1710    #[test]
1711    fn registry_local_submit_integration() {
1712        let reg = ProviderRegistry::default();
1713        let local = reg.get(ProviderType::LocalSimulator).unwrap();
1714        let qasm = "OPENQASM 2.0;\nqreg q[2];\n";
1715        let handle = local
1716            .submit_circuit(qasm, 50, "local_statevector_simulator")
1717            .expect("submit should succeed");
1718        let status = local.job_status(&handle).expect("status should succeed");
1719        assert_eq!(status, JobStatus::Completed);
1720        let result = local.job_results(&handle).expect("results should succeed");
1721        let total: usize = result.counts.values().sum();
1722        assert_eq!(total, 50);
1723    }
1724
1725    #[test]
1726    fn registry_stub_submit_through_registry() {
1727        let mut reg = ProviderRegistry::new();
1728        reg.register(Box::new(IbmQuantumProvider));
1729        let ibm = reg.get(ProviderType::IbmQuantum).unwrap();
1730        let result = ibm.submit_circuit("qreg q[2];", 100, "ibm_brisbane");
1731        assert!(result.is_err());
1732    }
1733
1734    // -- Trait object safety --
1735
1736    #[test]
1737    fn provider_trait_is_object_safe() {
1738        // Verify that HardwareProvider can be used as a trait object.
1739        let providers: Vec<Box<dyn HardwareProvider>> = vec![
1740            Box::new(LocalSimulatorProvider),
1741            Box::new(IbmQuantumProvider),
1742            Box::new(IonQProvider),
1743            Box::new(RigettiProvider),
1744            Box::new(AmazonBraketProvider),
1745        ];
1746        assert_eq!(providers.len(), 5);
1747        for p in &providers {
1748            assert!(!p.name().is_empty());
1749            assert!(!p.available_devices().is_empty());
1750        }
1751    }
1752
1753    // -- Send + Sync --
1754
1755    #[test]
1756    fn providers_are_send_sync() {
1757        fn assert_send_sync<T: Send + Sync>() {}
1758        assert_send_sync::<LocalSimulatorProvider>();
1759        assert_send_sync::<IbmQuantumProvider>();
1760        assert_send_sync::<IonQProvider>();
1761        assert_send_sync::<RigettiProvider>();
1762        assert_send_sync::<AmazonBraketProvider>();
1763    }
1764}