1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38#[non_exhaustive]
39pub enum CloudPlatform {
40 IBM,
42 AWS,
44 Google,
46 Azure,
48 Rigetti,
50 IonQ,
52}
53
54impl CloudPlatform {
55 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 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 pub const fn supports_qubits(&self, num_qubits: usize) -> bool {
81 match self {
82 Self::IBM => num_qubits <= 127, Self::AWS => num_qubits <= 34, Self::Google => num_qubits <= 72, Self::Azure => num_qubits <= 40, Self::Rigetti => num_qubits <= 80, Self::IonQ => num_qubits <= 32, }
89 }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum DeviceType {
95 QPU,
97 Simulator,
99 TensorNetworkSimulator,
101 NoisySimulator,
103}
104
105#[derive(Debug, Clone)]
107pub struct DeviceInfo {
108 pub platform: CloudPlatform,
110 pub name: String,
112 pub device_type: DeviceType,
114 pub num_qubits: usize,
116 pub connectivity: Vec<(usize, usize)>,
118 pub gate_set: Vec<String>,
120 pub gate_fidelities: HashMap<String, f64>,
122 pub t1_times: Vec<f64>,
124 pub t2_times: Vec<f64>,
126 pub readout_fidelity: Vec<f64>,
128 pub is_available: bool,
130 pub queue_depth: usize,
132 pub cost_per_shot: f64,
134}
135
136impl DeviceInfo {
137 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 }
155 }
156
157 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 }
175 }
176
177 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198#[non_exhaustive]
199pub enum JobStatus {
200 Queued,
202 Running,
204 Completed,
206 Failed,
208 Cancelled,
210}
211
212#[derive(Debug, Clone)]
214pub struct QuantumJob {
215 pub job_id: String,
217 pub platform: CloudPlatform,
219 pub device_name: String,
221 pub status: JobStatus,
223 pub shots: usize,
225 pub submitted_at: SystemTime,
227 pub completed_at: Option<SystemTime>,
229 pub result: Option<JobResult>,
231 pub error_message: Option<String>,
233 pub estimated_cost: f64,
235}
236
237impl QuantumJob {
238 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 pub const fn is_finished(&self) -> bool {
246 matches!(
247 self.status,
248 JobStatus::Completed | JobStatus::Failed | JobStatus::Cancelled
249 )
250 }
251}
252
253#[derive(Debug, Clone)]
255pub struct JobResult {
256 pub counts: HashMap<String, usize>,
258 pub expectation_values: Option<Vec<f64>>,
260 pub state_vector: Option<Array1<Complex>>,
262 pub density_matrix: Option<Array2<Complex>>,
264 pub raw_data: Vec<Vec<usize>>,
266 pub metadata: HashMap<String, String>,
268}
269
270impl JobResult {
271 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 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 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#[derive(Debug, Clone)]
305pub struct CloudConfig {
306 pub platform: CloudPlatform,
308 pub api_token: String,
310 pub endpoint: Option<String>,
312 pub default_shots: usize,
314 pub timeout: u64,
316 pub auto_optimize: bool,
318 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
336pub struct CloudClient {
338 config: CloudConfig,
339 devices: Vec<DeviceInfo>,
340}
341
342impl CloudClient {
343 pub const fn new(config: CloudConfig) -> Self {
345 Self {
346 config,
347 devices: Vec::new(),
348 }
349 }
350
351 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 self.devices = self.load_devices()?;
366
367 Ok(())
368 }
369
370 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 pub fn list_devices(&self) -> &[DeviceInfo] {
387 &self.devices
388 }
389
390 pub fn get_device(&self, name: &str) -> Option<&DeviceInfo> {
392 self.devices.iter().find(|d| d.name == name)
393 }
394
395 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 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 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 let estimated_cost = shots as f64 * device.cost_per_shot;
433
434 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 pub const fn check_job_status(&self, job_id: &str) -> QuantRS2Result<JobStatus> {
455 Ok(JobStatus::Queued)
457 }
458
459 pub fn wait_for_job(
461 &self,
462 job_id: &str,
463 timeout: Option<Duration>,
464 ) -> QuantRS2Result<QuantumJob> {
465 Err(QuantRS2Error::UnsupportedOperation(
467 "Job waiting not implemented in this simplified version".to_string(),
468 ))
469 }
470
471 pub fn get_job_result(&self, job_id: &str) -> QuantRS2Result<JobResult> {
473 Err(QuantRS2Error::UnsupportedOperation(
475 "Job result retrieval not implemented in this simplified version".to_string(),
476 ))
477 }
478
479 pub const fn cancel_job(&self, job_id: &str) -> QuantRS2Result<()> {
481 Ok(())
483 }
484
485 pub const fn list_jobs(&self, limit: Option<usize>) -> QuantRS2Result<Vec<QuantumJob>> {
487 Ok(Vec::new())
489 }
490}
491
492#[derive(Debug, Clone)]
494pub struct QuantumCircuit {
495 pub num_qubits: usize,
497 pub gates: Vec<Box<dyn GateOp>>,
499 pub measurements: Vec<usize>,
501}
502
503impl QuantumCircuit {
504 pub fn new(num_qubits: usize) -> Self {
506 Self {
507 num_qubits,
508 gates: Vec::new(),
509 measurements: Vec::new(),
510 }
511 }
512
513 pub fn add_gate(&mut self, gate: Box<dyn GateOp>) {
515 self.gates.push(gate);
516 }
517
518 pub fn measure(&mut self, qubit: usize) {
520 if qubit < self.num_qubits {
521 self.measurements.push(qubit);
522 }
523 }
524
525 pub fn measure_all(&mut self) {
527 self.measurements = (0..self.num_qubits).collect();
528 }
529
530 pub fn depth(&self) -> usize {
532 self.gates.len()
534 }
535
536 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 assert!(matches!(result, Err(QuantRS2Error::InvalidInput(_))));
621 }
622
623 #[test]
624 fn test_connect_returns_honest_error_no_network_backend() {
625 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 assert!(client.list_devices().is_empty());
649 }
650}