Skip to main content

quantrs2_core/
cloud_platforms.rs

1//! Quantum Cloud Platform Integration
2//!
3//! This module provides unified interfaces for interacting with major quantum computing
4//! cloud platforms including IBM Quantum, AWS Braket, and Google Quantum AI.
5//!
6//! ## Supported Platforms
7//!
8//! - **IBM Quantum**: Access to IBM's quantum processors and simulators
9//! - **AWS Braket**: Amazon's quantum computing service
10//! - **Google Quantum AI**: Google's quantum processors
11//! - **Azure Quantum**: Microsoft's quantum computing platform
12//!
13//! ## Features
14//!
15//! - Unified API across all platforms
16//! - Job submission and monitoring
17//! - Result retrieval and analysis
18//! - Device capability querying
19//! - Circuit transpilation for platform-specific requirements
20//! - Cost estimation and optimization
21
22use crate::{
23    error::{QuantRS2Error, QuantRS2Result},
24    gate::GateOp,
25    qubit::QubitId,
26};
27use scirs2_core::ndarray::{Array1, Array2};
28use scirs2_core::Complex64 as Complex;
29use std::collections::HashMap;
30use std::time::{Duration, SystemTime};
31
32// ================================================================================================
33// Cloud Platform Types
34// ================================================================================================
35
36/// Supported quantum cloud platforms
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38#[non_exhaustive]
39pub enum CloudPlatform {
40    /// IBM Quantum
41    IBM,
42    /// AWS Braket
43    AWS,
44    /// Google Quantum AI
45    Google,
46    /// Microsoft Azure Quantum
47    Azure,
48    /// Rigetti Quantum Cloud Services
49    Rigetti,
50    /// IonQ Cloud
51    IonQ,
52}
53
54impl CloudPlatform {
55    /// Get platform name
56    pub const fn name(&self) -> &'static str {
57        match self {
58            Self::IBM => "IBM Quantum",
59            Self::AWS => "AWS Braket",
60            Self::Google => "Google Quantum AI",
61            Self::Azure => "Azure Quantum",
62            Self::Rigetti => "Rigetti QCS",
63            Self::IonQ => "IonQ Cloud",
64        }
65    }
66
67    /// Get default API endpoint
68    pub const fn endpoint(&self) -> &'static str {
69        match self {
70            Self::IBM => "https://auth.quantum-computing.ibm.com/api",
71            Self::AWS => "https://braket.us-east-1.amazonaws.com",
72            Self::Google => "https://quantumengine.googleapis.com",
73            Self::Azure => "https://quantum.azure.com",
74            Self::Rigetti => "https://api.rigetti.com",
75            Self::IonQ => "https://api.ionq.com",
76        }
77    }
78
79    /// Check if platform supports specific qubit count
80    pub const fn supports_qubits(&self, num_qubits: usize) -> bool {
81        match self {
82            Self::IBM => num_qubits <= 127,    // IBM Quantum Eagle
83            Self::AWS => num_qubits <= 34,     // AWS Braket max
84            Self::Google => num_qubits <= 72,  // Google Sycamore
85            Self::Azure => num_qubits <= 40,   // Azure various backends
86            Self::Rigetti => num_qubits <= 80, // Rigetti Aspen-M
87            Self::IonQ => num_qubits <= 32,    // IonQ Aria
88        }
89    }
90}
91
92/// Device type (hardware or simulator)
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum DeviceType {
95    /// Quantum processing unit (real hardware)
96    QPU,
97    /// State vector simulator
98    Simulator,
99    /// Tensor network simulator
100    TensorNetworkSimulator,
101    /// Noisy simulator with error models
102    NoisySimulator,
103}
104
105/// Backend device information
106#[derive(Debug, Clone)]
107pub struct DeviceInfo {
108    /// Platform the device belongs to
109    pub platform: CloudPlatform,
110    /// Device name
111    pub name: String,
112    /// Device type (QPU or simulator)
113    pub device_type: DeviceType,
114    /// Number of qubits
115    pub num_qubits: usize,
116    /// Connectivity graph (which qubits are connected)
117    pub connectivity: Vec<(usize, usize)>,
118    /// Gate set supported by the device
119    pub gate_set: Vec<String>,
120    /// Average gate fidelities
121    pub gate_fidelities: HashMap<String, f64>,
122    /// Qubit coherence times T1 (microseconds)
123    pub t1_times: Vec<f64>,
124    /// Qubit coherence times T2 (microseconds)
125    pub t2_times: Vec<f64>,
126    /// Readout fidelity per qubit
127    pub readout_fidelity: Vec<f64>,
128    /// Whether device is currently available
129    pub is_available: bool,
130    /// Queue depth
131    pub queue_depth: usize,
132    /// Estimated cost per shot (in credits or USD)
133    pub cost_per_shot: f64,
134}
135
136impl DeviceInfo {
137    /// Get average single-qubit gate fidelity
138    pub fn avg_single_qubit_fidelity(&self) -> f64 {
139        let single_qubit_gates = vec!["X", "Y", "Z", "H", "RX", "RY", "RZ"];
140        let mut sum = 0.0;
141        let mut count = 0;
142
143        for gate in single_qubit_gates {
144            if let Some(&fidelity) = self.gate_fidelities.get(gate) {
145                sum += fidelity;
146                count += 1;
147            }
148        }
149
150        if count > 0 {
151            sum / count as f64
152        } else {
153            0.99 // Default
154        }
155    }
156
157    /// Get average two-qubit gate fidelity
158    pub fn avg_two_qubit_fidelity(&self) -> f64 {
159        let two_qubit_gates = vec!["CNOT", "CZ", "SWAP", "iSWAP"];
160        let mut sum = 0.0;
161        let mut count = 0;
162
163        for gate in two_qubit_gates {
164            if let Some(&fidelity) = self.gate_fidelities.get(gate) {
165                sum += fidelity;
166                count += 1;
167            }
168        }
169
170        if count > 0 {
171            sum / count as f64
172        } else {
173            0.95 // Default
174        }
175    }
176
177    /// Calculate quality score for ranking devices
178    pub fn quality_score(&self) -> f64 {
179        let gate_score = f64::midpoint(
180            self.avg_single_qubit_fidelity(),
181            self.avg_two_qubit_fidelity(),
182        );
183        let readout_score =
184            self.readout_fidelity.iter().sum::<f64>() / self.readout_fidelity.len() as f64;
185        let availability_score = if self.is_available { 1.0 } else { 0.5 };
186        let queue_score = 1.0 / (1.0 + self.queue_depth as f64 / 10.0);
187
188        gate_score.mul_add(0.4, readout_score * 0.3) + availability_score * 0.2 + queue_score * 0.1
189    }
190}
191
192// ================================================================================================
193// Quantum Job Management
194// ================================================================================================
195
196/// Job status
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198#[non_exhaustive]
199pub enum JobStatus {
200    /// Job is queued
201    Queued,
202    /// Job is running
203    Running,
204    /// Job completed successfully
205    Completed,
206    /// Job failed with error
207    Failed,
208    /// Job was cancelled
209    Cancelled,
210}
211
212/// Quantum job submitted to cloud platform
213#[derive(Debug, Clone)]
214pub struct QuantumJob {
215    /// Unique job ID
216    pub job_id: String,
217    /// Platform where job is running
218    pub platform: CloudPlatform,
219    /// Device name
220    pub device_name: String,
221    /// Job status
222    pub status: JobStatus,
223    /// Number of shots requested
224    pub shots: usize,
225    /// Submission time
226    pub submitted_at: SystemTime,
227    /// Completion time (if completed)
228    pub completed_at: Option<SystemTime>,
229    /// Result data (if completed)
230    pub result: Option<JobResult>,
231    /// Error message (if failed)
232    pub error_message: Option<String>,
233    /// Estimated cost
234    pub estimated_cost: f64,
235}
236
237impl QuantumJob {
238    /// Get execution time
239    pub fn execution_time(&self) -> Option<Duration> {
240        self.completed_at
241            .and_then(|completed| completed.duration_since(self.submitted_at).ok())
242    }
243
244    /// Check if job is finished (completed, failed, or cancelled)
245    pub const fn is_finished(&self) -> bool {
246        matches!(
247            self.status,
248            JobStatus::Completed | JobStatus::Failed | JobStatus::Cancelled
249        )
250    }
251}
252
253/// Job execution result
254#[derive(Debug, Clone)]
255pub struct JobResult {
256    /// Measurement counts (bitstring -> count)
257    pub counts: HashMap<String, usize>,
258    /// Measured expectation values (if applicable)
259    pub expectation_values: Option<Vec<f64>>,
260    /// State vector (if using simulator)
261    pub state_vector: Option<Array1<Complex>>,
262    /// Density matrix (if using noisy simulator)
263    pub density_matrix: Option<Array2<Complex>>,
264    /// Raw measurement data
265    pub raw_data: Vec<Vec<usize>>,
266    /// Job metadata
267    pub metadata: HashMap<String, String>,
268}
269
270impl JobResult {
271    /// Get probability distribution from counts
272    pub fn probabilities(&self) -> HashMap<String, f64> {
273        let total: usize = self.counts.values().sum();
274        self.counts
275            .iter()
276            .map(|(k, v)| (k.clone(), *v as f64 / total as f64))
277            .collect()
278    }
279
280    /// Get most probable measurement outcome
281    pub fn most_probable_outcome(&self) -> Option<String> {
282        self.counts
283            .iter()
284            .max_by_key(|(_, count)| *count)
285            .map(|(outcome, _)| outcome.clone())
286    }
287
288    /// Calculate measurement entropy
289    pub fn entropy(&self) -> f64 {
290        let probs = self.probabilities();
291        -probs
292            .values()
293            .filter(|&&p| p > 0.0)
294            .map(|&p| p * p.log2())
295            .sum::<f64>()
296    }
297}
298
299// ================================================================================================
300// Cloud Platform Client
301// ================================================================================================
302
303/// Configuration for cloud platform connection
304#[derive(Debug, Clone)]
305pub struct CloudConfig {
306    /// Platform to connect to
307    pub platform: CloudPlatform,
308    /// API token/key for authentication
309    pub api_token: String,
310    /// API endpoint (optional, uses default if not specified)
311    pub endpoint: Option<String>,
312    /// Default number of shots
313    pub default_shots: usize,
314    /// Timeout for API requests (seconds)
315    pub timeout: u64,
316    /// Enable automatic circuit optimization
317    pub auto_optimize: bool,
318    /// Maximum qubits to use
319    pub max_qubits: Option<usize>,
320}
321
322impl Default for CloudConfig {
323    fn default() -> Self {
324        Self {
325            platform: CloudPlatform::IBM,
326            api_token: String::new(),
327            endpoint: None,
328            default_shots: 1000,
329            timeout: 300,
330            auto_optimize: true,
331            max_qubits: None,
332        }
333    }
334}
335
336/// Cloud platform client for job submission and management
337pub struct CloudClient {
338    config: CloudConfig,
339    devices: Vec<DeviceInfo>,
340}
341
342impl CloudClient {
343    /// Create a new cloud client
344    pub const fn new(config: CloudConfig) -> Self {
345        Self {
346            config,
347            devices: Vec::new(),
348        }
349    }
350
351    /// Connect to the cloud platform and authenticate.
352    ///
353    /// Validates that an API token is present, then queries the platform for its available
354    /// devices. Live device enumeration requires a network backend which is not linked into
355    /// this crate, so [`Self::load_devices`] currently returns an honest error rather than
356    /// fabricating a hardcoded device list.
357    pub fn connect(&mut self) -> QuantRS2Result<()> {
358        if self.config.api_token.is_empty() {
359            return Err(QuantRS2Error::InvalidInput(
360                "API token is required".to_string(),
361            ));
362        }
363
364        // Load available devices from the live platform.
365        self.devices = self.load_devices()?;
366
367        Ok(())
368    }
369
370    /// Load available devices from the platform.
371    ///
372    /// This performs live device enumeration against the configured cloud provider, which
373    /// requires an HTTP/network backend. No such backend is linked into `quantrs2-core`, so
374    /// rather than fabricating a hardcoded list of devices that would be presented as real
375    /// hardware, this returns an honest [`QuantRS2Error::UnsupportedOperation`]. A
376    /// network-enabled device crate must provide the real implementation.
377    fn load_devices(&self) -> QuantRS2Result<Vec<DeviceInfo>> {
378        Err(QuantRS2Error::UnsupportedOperation(format!(
379            "live device enumeration requires a network backend which is not linked; \
380             cannot query real {} devices",
381            self.config.platform.name()
382        )))
383    }
384
385    /// Get list of available devices
386    pub fn list_devices(&self) -> &[DeviceInfo] {
387        &self.devices
388    }
389
390    /// Get device by name
391    pub fn get_device(&self, name: &str) -> Option<&DeviceInfo> {
392        self.devices.iter().find(|d| d.name == name)
393    }
394
395    /// Get best available device based on requirements
396    pub fn select_best_device(&self, min_qubits: usize, prefer_qpu: bool) -> Option<&DeviceInfo> {
397        self.devices
398            .iter()
399            .filter(|d| {
400                d.num_qubits >= min_qubits
401                    && (!prefer_qpu || matches!(d.device_type, DeviceType::QPU))
402            })
403            .max_by(|a, b| {
404                a.quality_score()
405                    .partial_cmp(&b.quality_score())
406                    .unwrap_or(std::cmp::Ordering::Equal)
407            })
408    }
409
410    /// Submit a quantum job
411    pub fn submit_job(
412        &self,
413        device_name: &str,
414        circuit: &QuantumCircuit,
415        shots: Option<usize>,
416    ) -> QuantRS2Result<QuantumJob> {
417        let device = self.get_device(device_name).ok_or_else(|| {
418            QuantRS2Error::InvalidInput(format!("Device {device_name} not found"))
419        })?;
420
421        let shots = shots.unwrap_or(self.config.default_shots);
422
423        // Validate circuit
424        if circuit.num_qubits > device.num_qubits {
425            return Err(QuantRS2Error::InvalidInput(format!(
426                "Circuit requires {} qubits, device only has {}",
427                circuit.num_qubits, device.num_qubits
428            )));
429        }
430
431        // Calculate estimated cost
432        let estimated_cost = shots as f64 * device.cost_per_shot;
433
434        // Create job (simplified - in production would make API call)
435        let timestamp = SystemTime::now()
436            .duration_since(SystemTime::UNIX_EPOCH)
437            .unwrap_or(Duration::ZERO)
438            .as_millis();
439        Ok(QuantumJob {
440            job_id: format!("job_{}", timestamp),
441            platform: self.config.platform,
442            device_name: device_name.to_string(),
443            status: JobStatus::Queued,
444            shots,
445            submitted_at: SystemTime::now(),
446            completed_at: None,
447            result: None,
448            error_message: None,
449            estimated_cost,
450        })
451    }
452
453    /// Check job status
454    pub const fn check_job_status(&self, job_id: &str) -> QuantRS2Result<JobStatus> {
455        // Simplified: in production would make API call
456        Ok(JobStatus::Queued)
457    }
458
459    /// Wait for job completion
460    pub fn wait_for_job(
461        &self,
462        job_id: &str,
463        timeout: Option<Duration>,
464    ) -> QuantRS2Result<QuantumJob> {
465        // Simplified: in production would poll API until job completes
466        Err(QuantRS2Error::UnsupportedOperation(
467            "Job waiting not implemented in this simplified version".to_string(),
468        ))
469    }
470
471    /// Get job result
472    pub fn get_job_result(&self, job_id: &str) -> QuantRS2Result<JobResult> {
473        // Simplified: in production would fetch from API
474        Err(QuantRS2Error::UnsupportedOperation(
475            "Job result retrieval not implemented in this simplified version".to_string(),
476        ))
477    }
478
479    /// Cancel a job
480    pub const fn cancel_job(&self, job_id: &str) -> QuantRS2Result<()> {
481        // Simplified: in production would make API call
482        Ok(())
483    }
484
485    /// List user's jobs
486    pub const fn list_jobs(&self, limit: Option<usize>) -> QuantRS2Result<Vec<QuantumJob>> {
487        // Simplified: in production would fetch from API
488        Ok(Vec::new())
489    }
490}
491
492/// Quantum circuit representation for cloud submission
493#[derive(Debug, Clone)]
494pub struct QuantumCircuit {
495    /// Number of qubits
496    pub num_qubits: usize,
497    /// Circuit gates
498    pub gates: Vec<Box<dyn GateOp>>,
499    /// Measurements to perform
500    pub measurements: Vec<usize>,
501}
502
503impl QuantumCircuit {
504    /// Create a new quantum circuit
505    pub fn new(num_qubits: usize) -> Self {
506        Self {
507            num_qubits,
508            gates: Vec::new(),
509            measurements: Vec::new(),
510        }
511    }
512
513    /// Add a gate to the circuit
514    pub fn add_gate(&mut self, gate: Box<dyn GateOp>) {
515        self.gates.push(gate);
516    }
517
518    /// Add measurement
519    pub fn measure(&mut self, qubit: usize) {
520        if qubit < self.num_qubits {
521            self.measurements.push(qubit);
522        }
523    }
524
525    /// Measure all qubits
526    pub fn measure_all(&mut self) {
527        self.measurements = (0..self.num_qubits).collect();
528    }
529
530    /// Get circuit depth
531    pub fn depth(&self) -> usize {
532        // Simplified: actual implementation would compute proper depth
533        self.gates.len()
534    }
535
536    /// Count gates by type
537    pub fn gate_counts(&self) -> HashMap<String, usize> {
538        let mut counts = HashMap::new();
539        for gate in &self.gates {
540            *counts.entry(gate.name().to_string()).or_insert(0) += 1;
541        }
542        counts
543    }
544}
545
546#[cfg(test)]
547mod tests {
548    use super::*;
549
550    #[test]
551    fn test_cloud_platform_names() {
552        assert_eq!(CloudPlatform::IBM.name(), "IBM Quantum");
553        assert_eq!(CloudPlatform::AWS.name(), "AWS Braket");
554        assert_eq!(CloudPlatform::Google.name(), "Google Quantum AI");
555    }
556
557    #[test]
558    fn test_device_quality_score() {
559        let device = DeviceInfo {
560            platform: CloudPlatform::IBM,
561            name: "test_device".to_string(),
562            device_type: DeviceType::QPU,
563            num_qubits: 5,
564            connectivity: vec![],
565            gate_set: vec![],
566            gate_fidelities: HashMap::from([("X".to_string(), 0.999), ("CNOT".to_string(), 0.99)]),
567            t1_times: vec![],
568            t2_times: vec![],
569            readout_fidelity: vec![0.95, 0.96, 0.97, 0.98, 0.99],
570            is_available: true,
571            queue_depth: 5,
572            cost_per_shot: 0.001,
573        };
574
575        let score = device.quality_score();
576        assert!(score > 0.8 && score < 1.0);
577    }
578
579    #[test]
580    fn test_job_result_probabilities() {
581        let result = JobResult {
582            counts: HashMap::from([
583                ("00".to_string(), 500),
584                ("01".to_string(), 250),
585                ("10".to_string(), 150),
586                ("11".to_string(), 100),
587            ]),
588            expectation_values: None,
589            state_vector: None,
590            density_matrix: None,
591            raw_data: vec![],
592            metadata: HashMap::new(),
593        };
594
595        let probs = result.probabilities();
596        assert_eq!(probs.get("00"), Some(&0.5));
597        assert_eq!(probs.get("01"), Some(&0.25));
598
599        let most_probable = result
600            .most_probable_outcome()
601            .expect("should have most probable outcome");
602        assert_eq!(most_probable, "00");
603    }
604
605    #[test]
606    fn test_quantum_circuit() {
607        let mut circuit = QuantumCircuit::new(2);
608        assert_eq!(circuit.num_qubits, 2);
609        assert_eq!(circuit.gates.len(), 0);
610
611        circuit.measure_all();
612        assert_eq!(circuit.measurements.len(), 2);
613    }
614
615    #[test]
616    fn test_connect_requires_api_token() {
617        let mut client = CloudClient::new(CloudConfig::default());
618        let result = client.connect();
619        // Empty token: must fail on the token check before reaching device loading.
620        assert!(matches!(result, Err(QuantRS2Error::InvalidInput(_))));
621    }
622
623    #[test]
624    fn test_connect_returns_honest_error_no_network_backend() {
625        // With a token present, `connect` reaches `load_devices`, which must return an
626        // honest "no network backend" error instead of fabricating mock devices.
627        let config = CloudConfig {
628            platform: CloudPlatform::IBM,
629            api_token: "dummy-token".to_string(),
630            ..CloudConfig::default()
631        };
632        let mut client = CloudClient::new(config);
633        let result = client.connect();
634        match result {
635            Err(QuantRS2Error::UnsupportedOperation(msg)) => {
636                assert!(
637                    msg.contains("network backend"),
638                    "error should explain the missing network backend, got: {msg}"
639                );
640                assert!(
641                    msg.contains("IBM Quantum"),
642                    "error should name the platform, got: {msg}"
643                );
644            }
645            other => panic!("expected honest UnsupportedOperation error, got {other:?}"),
646        }
647        // No devices must have been fabricated into the client state.
648        assert!(client.list_devices().is_empty());
649    }
650}