quantrs2_device/
azure.rs

1use quantrs2_circuit::prelude::Circuit;
2use std::collections::HashMap;
3#[cfg(feature = "azure")]
4use std::sync::Arc;
5#[cfg(feature = "azure")]
6use std::thread::sleep;
7#[cfg(feature = "azure")]
8use std::time::Duration;
9
10#[cfg(feature = "azure")]
11use reqwest::{header, Client};
12#[cfg(feature = "azure")]
13use serde::{Deserialize, Serialize};
14#[cfg(feature = "azure")]
15use serde_json;
16use thiserror::Error;
17
18use crate::DeviceError;
19use crate::DeviceResult;
20
21#[cfg(feature = "azure")]
22const AZURE_QUANTUM_API_URL: &str = "https://eastus.quantum.azure.com";
23#[cfg(feature = "azure")]
24const DEFAULT_TIMEOUT_SECS: u64 = 90;
25
26/// Represents the available providers on Azure Quantum
27#[derive(Debug, Clone)]
28#[cfg_attr(feature = "azure", derive(serde::Deserialize))]
29pub struct AzureProvider {
30    /// Unique identifier for the provider
31    pub id: String,
32    /// Name of the provider (e.g., "ionq", "microsoft", "quantinuum")
33    pub name: String,
34    /// Provider-specific capabilities and settings
35    pub capabilities: HashMap<String, String>,
36}
37
38/// Represents the available target devices on Azure Quantum
39#[derive(Debug, Clone)]
40#[cfg_attr(feature = "azure", derive(serde::Deserialize))]
41pub struct AzureTarget {
42    /// Target ID
43    pub id: String,
44    /// Display name of the target
45    pub name: String,
46    /// Provider ID
47    pub provider_id: String,
48    /// Whether the target is a simulator or real quantum hardware
49    pub is_simulator: bool,
50    /// Number of qubits on the target
51    pub num_qubits: usize,
52    /// Status of the target (e.g., "Available", "Offline")
53    pub status: String,
54    /// Target-specific capabilities and properties
55    #[cfg(feature = "azure")]
56    pub properties: HashMap<String, serde_json::Value>,
57    #[cfg(not(feature = "azure"))]
58    pub properties: HashMap<String, String>,
59}
60
61/// Configuration for a quantum circuit to be submitted to Azure Quantum
62#[derive(Debug, Clone)]
63#[cfg_attr(feature = "azure", derive(Serialize))]
64pub struct AzureCircuitConfig {
65    /// Name of the job
66    pub name: String,
67    /// Circuit representation (varies by provider)
68    pub circuit: String,
69    /// Number of shots to run
70    pub shots: usize,
71    /// Provider-specific parameters
72    #[cfg(feature = "azure")]
73    pub provider_parameters: HashMap<String, serde_json::Value>,
74    #[cfg(not(feature = "azure"))]
75    pub provider_parameters: HashMap<String, String>,
76}
77
78/// Status of a job in Azure Quantum
79#[derive(Debug, Clone, PartialEq, Eq)]
80#[cfg_attr(feature = "azure", derive(Deserialize))]
81pub enum AzureJobStatus {
82    #[cfg_attr(feature = "azure", serde(rename = "Waiting"))]
83    Waiting,
84    #[cfg_attr(feature = "azure", serde(rename = "Executing"))]
85    Executing,
86    #[cfg_attr(feature = "azure", serde(rename = "Succeeded"))]
87    Succeeded,
88    #[cfg_attr(feature = "azure", serde(rename = "Failed"))]
89    Failed,
90    #[cfg_attr(feature = "azure", serde(rename = "Cancelled"))]
91    Cancelled,
92}
93
94/// Response from submitting a job to Azure Quantum
95#[cfg(feature = "azure")]
96#[derive(Debug, Deserialize)]
97pub struct AzureJobResponse {
98    /// Job ID
99    pub id: String,
100    /// Name of the job
101    pub name: String,
102    /// Job status
103    pub status: AzureJobStatus,
104    /// Provider ID
105    pub provider: String,
106    /// Target ID
107    pub target: String,
108    /// Creation timestamp
109    pub creation_time: String,
110    /// Execution time (if completed)
111    pub execution_time: Option<String>,
112}
113
114#[cfg(not(feature = "azure"))]
115#[derive(Debug)]
116pub struct AzureJobResponse {
117    /// Job ID
118    pub id: String,
119    /// Name of the job
120    pub name: String,
121    /// Job status
122    pub status: AzureJobStatus,
123}
124
125/// Results from a completed job
126#[cfg(feature = "azure")]
127#[derive(Debug, Deserialize)]
128pub struct AzureJobResult {
129    /// Counts of each basis state
130    pub histogram: HashMap<String, f64>,
131    /// Total number of shots executed
132    pub shots: usize,
133    /// Job status
134    pub status: AzureJobStatus,
135    /// Error message, if any
136    pub error: Option<String>,
137    /// Additional metadata
138    pub metadata: HashMap<String, serde_json::Value>,
139}
140
141#[cfg(not(feature = "azure"))]
142#[derive(Debug)]
143pub struct AzureJobResult {
144    /// Counts of each basis state (as probabilities)
145    pub histogram: HashMap<String, f64>,
146    /// Total number of shots executed
147    pub shots: usize,
148    /// Job status
149    pub status: AzureJobStatus,
150    /// Error message, if any
151    pub error: Option<String>,
152}
153
154/// Errors specific to Azure Quantum
155#[derive(Error, Debug)]
156pub enum AzureQuantumError {
157    #[error("Authentication error: {0}")]
158    Authentication(String),
159
160    #[error("API error: {0}")]
161    API(String),
162
163    #[error("Target not available: {0}")]
164    TargetUnavailable(String),
165
166    #[error("Circuit conversion error: {0}")]
167    CircuitConversion(String),
168
169    #[error("Job submission error: {0}")]
170    JobSubmission(String),
171
172    #[error("Timeout waiting for job completion")]
173    Timeout,
174}
175
176/// Client for interacting with Azure Quantum
177#[cfg(feature = "azure")]
178#[derive(Clone)]
179pub struct AzureQuantumClient {
180    /// HTTP client for making API requests
181    client: Client,
182    /// Base URL for the Azure Quantum API
183    api_url: String,
184    /// Workspace name
185    workspace: String,
186    /// Subscription ID
187    subscription_id: String,
188    /// Resource group
189    resource_group: String,
190    /// Authentication token
191    token: String,
192}
193
194#[cfg(not(feature = "azure"))]
195#[derive(Clone)]
196pub struct AzureQuantumClient;
197
198#[cfg(feature = "azure")]
199impl AzureQuantumClient {
200    /// Create a new Azure Quantum client with the given token and workspace details
201    pub fn new(
202        token: &str,
203        subscription_id: &str,
204        resource_group: &str,
205        workspace: &str,
206        region: Option<&str>,
207    ) -> DeviceResult<Self> {
208        let mut headers = header::HeaderMap::new();
209        headers.insert(
210            header::CONTENT_TYPE,
211            header::HeaderValue::from_static("application/json"),
212        );
213
214        let client = Client::builder()
215            .default_headers(headers)
216            .timeout(Duration::from_secs(30))
217            .build()
218            .map_err(|e| DeviceError::Connection(e.to_string()))?;
219
220        let api_url = match region {
221            Some(region) => format!("https://{}.quantum.azure.com", region),
222            None => AZURE_QUANTUM_API_URL.to_string(),
223        };
224
225        Ok(Self {
226            client,
227            api_url,
228            workspace: workspace.to_string(),
229            subscription_id: subscription_id.to_string(),
230            resource_group: resource_group.to_string(),
231            token: token.to_string(),
232        })
233    }
234
235    /// Get API base path for this workspace
236    fn get_api_base_path(&self) -> String {
237        format!(
238            "/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Quantum/Workspaces/{}",
239            self.subscription_id, self.resource_group, self.workspace
240        )
241    }
242
243    /// List all available providers
244    pub async fn list_providers(&self) -> DeviceResult<Vec<AzureProvider>> {
245        let base_path = self.get_api_base_path();
246        let url = format!("{}{}/providers", self.api_url, base_path);
247
248        let response = self
249            .client
250            .get(&url)
251            .header("Authorization", format!("Bearer {}", self.token))
252            .send()
253            .await
254            .map_err(|e| DeviceError::Connection(e.to_string()))?;
255
256        if !response.status().is_success() {
257            let error_msg = response
258                .text()
259                .await
260                .unwrap_or_else(|_| "Unknown error".to_string());
261            return Err(DeviceError::APIError(error_msg));
262        }
263
264        let providers: Vec<AzureProvider> = response
265            .json()
266            .await
267            .map_err(|e| DeviceError::Deserialization(e.to_string()))?;
268
269        Ok(providers)
270    }
271
272    /// List all available targets (devices and simulators)
273    pub async fn list_targets(&self) -> DeviceResult<Vec<AzureTarget>> {
274        let base_path = self.get_api_base_path();
275        let url = format!("{}{}/targets", self.api_url, base_path);
276
277        let response = self
278            .client
279            .get(&url)
280            .header("Authorization", format!("Bearer {}", self.token))
281            .send()
282            .await
283            .map_err(|e| DeviceError::Connection(e.to_string()))?;
284
285        if !response.status().is_success() {
286            let error_msg = response
287                .text()
288                .await
289                .unwrap_or_else(|_| "Unknown error".to_string());
290            return Err(DeviceError::APIError(error_msg));
291        }
292
293        let targets: Vec<AzureTarget> = response
294            .json()
295            .await
296            .map_err(|e| DeviceError::Deserialization(e.to_string()))?;
297
298        Ok(targets)
299    }
300
301    /// Get details about a specific target
302    pub async fn get_target(&self, target_id: &str) -> DeviceResult<AzureTarget> {
303        let base_path = self.get_api_base_path();
304        let url = format!("{}{}/targets/{}", self.api_url, base_path, target_id);
305
306        let response = self
307            .client
308            .get(&url)
309            .header("Authorization", format!("Bearer {}", self.token))
310            .send()
311            .await
312            .map_err(|e| DeviceError::Connection(e.to_string()))?;
313
314        if !response.status().is_success() {
315            let error_msg = response
316                .text()
317                .await
318                .unwrap_or_else(|_| "Unknown error".to_string());
319            return Err(DeviceError::APIError(error_msg));
320        }
321
322        let target: AzureTarget = response
323            .json()
324            .await
325            .map_err(|e| DeviceError::Deserialization(e.to_string()))?;
326
327        Ok(target)
328    }
329
330    /// Submit a circuit to be executed on an Azure Quantum target
331    pub async fn submit_circuit(
332        &self,
333        target_id: &str,
334        provider_id: &str,
335        config: AzureCircuitConfig,
336    ) -> DeviceResult<String> {
337        let base_path = self.get_api_base_path();
338        let url = format!("{}{}/jobs", self.api_url, base_path);
339
340        use serde_json::json;
341
342        let payload = json!({
343            "name": config.name,
344            "providerId": provider_id,
345            "target": target_id,
346            "input": config.circuit,
347            "inputDataFormat": "qir", // Default to QIR, change based on provider
348            "outputDataFormat": "microsoft.quantum-results.v1",
349            "metadata": {
350                "shots": config.shots
351            },
352            "params": config.provider_parameters
353        });
354
355        let response = self
356            .client
357            .post(&url)
358            .header("Authorization", format!("Bearer {}", self.token))
359            .json(&payload)
360            .send()
361            .await
362            .map_err(|e| DeviceError::Connection(e.to_string()))?;
363
364        if !response.status().is_success() {
365            let error_msg = response
366                .text()
367                .await
368                .unwrap_or_else(|_| "Unknown error".to_string());
369            return Err(DeviceError::JobSubmission(error_msg));
370        }
371
372        let job_response: AzureJobResponse = response
373            .json()
374            .await
375            .map_err(|e| DeviceError::Deserialization(e.to_string()))?;
376
377        Ok(job_response.id)
378    }
379
380    /// Get the status of a job
381    pub async fn get_job_status(&self, job_id: &str) -> DeviceResult<AzureJobStatus> {
382        let base_path = self.get_api_base_path();
383        let url = format!("{}{}/jobs/{}", self.api_url, base_path, job_id);
384
385        let response = self
386            .client
387            .get(&url)
388            .header("Authorization", format!("Bearer {}", self.token))
389            .send()
390            .await
391            .map_err(|e| DeviceError::Connection(e.to_string()))?;
392
393        if !response.status().is_success() {
394            let error_msg = response
395                .text()
396                .await
397                .unwrap_or_else(|_| "Unknown error".to_string());
398            return Err(DeviceError::APIError(error_msg));
399        }
400
401        let job: AzureJobResponse = response
402            .json()
403            .await
404            .map_err(|e| DeviceError::Deserialization(e.to_string()))?;
405
406        Ok(job.status)
407    }
408
409    /// Get the results of a completed job
410    pub async fn get_job_result(&self, job_id: &str) -> DeviceResult<AzureJobResult> {
411        let base_path = self.get_api_base_path();
412        let url = format!("{}{}/jobs/{}/results", self.api_url, base_path, job_id);
413
414        let response = self
415            .client
416            .get(&url)
417            .header("Authorization", format!("Bearer {}", self.token))
418            .send()
419            .await
420            .map_err(|e| DeviceError::Connection(e.to_string()))?;
421
422        if !response.status().is_success() {
423            let error_msg = response
424                .text()
425                .await
426                .unwrap_or_else(|_| "Unknown error".to_string());
427            return Err(DeviceError::APIError(error_msg));
428        }
429
430        let result: AzureJobResult = response
431            .json()
432            .await
433            .map_err(|e| DeviceError::Deserialization(e.to_string()))?;
434
435        Ok(result)
436    }
437
438    /// Wait for a job to complete with timeout
439    pub async fn wait_for_job(
440        &self,
441        job_id: &str,
442        timeout_secs: Option<u64>,
443    ) -> DeviceResult<AzureJobResult> {
444        let timeout = timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS);
445        let mut elapsed = 0;
446        let interval = 5; // Check status every 5 seconds
447
448        while elapsed < timeout {
449            let status = self.get_job_status(job_id).await?;
450
451            match status {
452                AzureJobStatus::Succeeded => {
453                    return self.get_job_result(job_id).await;
454                }
455                AzureJobStatus::Failed => {
456                    return Err(DeviceError::JobExecution(format!(
457                        "Job {} encountered an error",
458                        job_id
459                    )));
460                }
461                AzureJobStatus::Cancelled => {
462                    return Err(DeviceError::JobExecution(format!(
463                        "Job {} was cancelled",
464                        job_id
465                    )));
466                }
467                _ => {
468                    // Still in progress, wait and check again
469                    sleep(Duration::from_secs(interval));
470                    elapsed += interval;
471                }
472            }
473        }
474
475        Err(DeviceError::Timeout(format!(
476            "Timed out waiting for job {} to complete",
477            job_id
478        )))
479    }
480
481    /// Submit multiple circuits in parallel
482    pub async fn submit_circuits_parallel(
483        &self,
484        target_id: &str,
485        provider_id: &str,
486        configs: Vec<AzureCircuitConfig>,
487    ) -> DeviceResult<Vec<String>> {
488        let client = Arc::new(self.clone());
489
490        let mut handles = vec![];
491
492        for config in configs {
493            let client_clone = client.clone();
494            let target_id = target_id.to_string();
495            let provider_id = provider_id.to_string();
496
497            let handle = tokio::task::spawn(async move {
498                client_clone
499                    .submit_circuit(&target_id, &provider_id, config)
500                    .await
501            });
502
503            handles.push(handle);
504        }
505
506        let mut job_ids = vec![];
507
508        for handle in handles {
509            match handle.await {
510                Ok(result) => match result {
511                    Ok(job_id) => job_ids.push(job_id),
512                    Err(e) => return Err(e),
513                },
514                Err(e) => {
515                    return Err(DeviceError::JobSubmission(format!(
516                        "Failed to join task: {}",
517                        e
518                    )));
519                }
520            }
521        }
522
523        Ok(job_ids)
524    }
525
526    /// Convert a Quantrs circuit to a provider-specific format
527    pub fn circuit_to_provider_format<const N: usize>(
528        circuit: &Circuit<N>,
529        provider_id: &str,
530    ) -> DeviceResult<String> {
531        // Different format conversions based on provider
532        match provider_id {
533            "ionq" => Self::circuit_to_ionq_format(circuit),
534            "microsoft" => Self::circuit_to_qir_format(circuit),
535            "quantinuum" => Self::circuit_to_qasm_format(circuit),
536            _ => Err(DeviceError::CircuitConversion(format!(
537                "Unsupported provider: {}",
538                provider_id
539            ))),
540        }
541    }
542
543    // IonQ specific circuit format conversion
544    fn circuit_to_ionq_format<const N: usize>(_circuit: &Circuit<N>) -> DeviceResult<String> {
545        // IonQ uses a JSON circuit format
546        use serde_json::json;
547
548        // This is a placeholder for the actual conversion logic
549        #[allow(unused_variables)]
550        let gates: Vec<serde_json::Value> = vec![]; // Convert gates to IonQ format
551
552        let ionq_circuit = json!({
553            "qubits": N,
554            "circuit": gates,
555        });
556
557        Ok(ionq_circuit.to_string())
558    }
559
560    // Microsoft QIR format conversion
561    fn circuit_to_qir_format<const N: usize>(_circuit: &Circuit<N>) -> DeviceResult<String> {
562        // QIR is a LLVM IR based format
563        // For now, this is just a placeholder
564        Err(DeviceError::CircuitConversion(
565            "QIR conversion not yet implemented".to_string(),
566        ))
567    }
568
569    // QASM format conversion for Quantinuum
570    fn circuit_to_qasm_format<const N: usize>(_circuit: &Circuit<N>) -> DeviceResult<String> {
571        // Similar to IBM's QASM format
572        let mut qasm = String::from("OPENQASM 2.0;\ninclude \"qelib1.inc\";\n\n");
573
574        // Define the quantum and classical registers
575        qasm.push_str(&format!("qreg q[{}];\n", N));
576        qasm.push_str(&format!("creg c[{}];\n\n", N));
577
578        // Implement conversion of gates to QASM here
579        // For now, just return placeholder QASM
580        Ok(qasm)
581    }
582}
583
584#[cfg(not(feature = "azure"))]
585impl AzureQuantumClient {
586    pub fn new(
587        _token: &str,
588        _subscription_id: &str,
589        _resource_group: &str,
590        _workspace: &str,
591        _region: Option<&str>,
592    ) -> DeviceResult<Self> {
593        Err(DeviceError::UnsupportedDevice(
594            "Azure Quantum support not enabled. Recompile with the 'azure' feature.".to_string(),
595        ))
596    }
597
598    pub async fn list_providers(&self) -> DeviceResult<Vec<AzureProvider>> {
599        Err(DeviceError::UnsupportedDevice(
600            "Azure Quantum support not enabled".to_string(),
601        ))
602    }
603
604    pub async fn list_targets(&self) -> DeviceResult<Vec<AzureTarget>> {
605        Err(DeviceError::UnsupportedDevice(
606            "Azure Quantum support not enabled".to_string(),
607        ))
608    }
609
610    pub async fn get_target(&self, _target_id: &str) -> DeviceResult<AzureTarget> {
611        Err(DeviceError::UnsupportedDevice(
612            "Azure Quantum support not enabled".to_string(),
613        ))
614    }
615
616    pub async fn submit_circuit(
617        &self,
618        _target_id: &str,
619        _provider_id: &str,
620        _config: AzureCircuitConfig,
621    ) -> DeviceResult<String> {
622        Err(DeviceError::UnsupportedDevice(
623            "Azure Quantum support not enabled".to_string(),
624        ))
625    }
626
627    pub async fn get_job_status(&self, _job_id: &str) -> DeviceResult<AzureJobStatus> {
628        Err(DeviceError::UnsupportedDevice(
629            "Azure Quantum support not enabled".to_string(),
630        ))
631    }
632
633    pub async fn get_job_result(&self, _job_id: &str) -> DeviceResult<AzureJobResult> {
634        Err(DeviceError::UnsupportedDevice(
635            "Azure Quantum support not enabled".to_string(),
636        ))
637    }
638
639    pub async fn wait_for_job(
640        &self,
641        _job_id: &str,
642        _timeout_secs: Option<u64>,
643    ) -> DeviceResult<AzureJobResult> {
644        Err(DeviceError::UnsupportedDevice(
645            "Azure Quantum support not enabled".to_string(),
646        ))
647    }
648
649    pub async fn submit_circuits_parallel(
650        &self,
651        _target_id: &str,
652        _provider_id: &str,
653        _configs: Vec<AzureCircuitConfig>,
654    ) -> DeviceResult<Vec<String>> {
655        Err(DeviceError::UnsupportedDevice(
656            "Azure Quantum support not enabled".to_string(),
657        ))
658    }
659
660    pub fn circuit_to_provider_format<const N: usize>(
661        _circuit: &Circuit<N>,
662        _provider_id: &str,
663    ) -> DeviceResult<String> {
664        Err(DeviceError::UnsupportedDevice(
665            "Azure Quantum support not enabled".to_string(),
666        ))
667    }
668}