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#[derive(Debug, Clone)]
28#[cfg_attr(feature = "azure", derive(serde::Deserialize))]
29pub struct AzureProvider {
30 pub id: String,
32 pub name: String,
34 pub capabilities: HashMap<String, String>,
36}
37
38#[derive(Debug, Clone)]
40#[cfg_attr(feature = "azure", derive(serde::Deserialize))]
41pub struct AzureTarget {
42 pub id: String,
44 pub name: String,
46 pub provider_id: String,
48 pub is_simulator: bool,
50 pub num_qubits: usize,
52 pub status: String,
54 #[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#[derive(Debug, Clone)]
63#[cfg_attr(feature = "azure", derive(Serialize))]
64pub struct AzureCircuitConfig {
65 pub name: String,
67 pub circuit: String,
69 pub shots: usize,
71 #[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#[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#[cfg(feature = "azure")]
96#[derive(Debug, Deserialize)]
97pub struct AzureJobResponse {
98 pub id: String,
100 pub name: String,
102 pub status: AzureJobStatus,
104 pub provider: String,
106 pub target: String,
108 pub creation_time: String,
110 pub execution_time: Option<String>,
112}
113
114#[cfg(not(feature = "azure"))]
115#[derive(Debug)]
116pub struct AzureJobResponse {
117 pub id: String,
119 pub name: String,
121 pub status: AzureJobStatus,
123}
124
125#[cfg(feature = "azure")]
127#[derive(Debug, Deserialize)]
128pub struct AzureJobResult {
129 pub histogram: HashMap<String, f64>,
131 pub shots: usize,
133 pub status: AzureJobStatus,
135 pub error: Option<String>,
137 pub metadata: HashMap<String, serde_json::Value>,
139}
140
141#[cfg(not(feature = "azure"))]
142#[derive(Debug)]
143pub struct AzureJobResult {
144 pub histogram: HashMap<String, f64>,
146 pub shots: usize,
148 pub status: AzureJobStatus,
150 pub error: Option<String>,
152}
153
154#[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#[cfg(feature = "azure")]
178#[derive(Clone)]
179pub struct AzureQuantumClient {
180 client: Client,
182 api_url: String,
184 workspace: String,
186 subscription_id: String,
188 resource_group: String,
190 token: String,
192}
193
194#[cfg(not(feature = "azure"))]
195#[derive(Clone)]
196pub struct AzureQuantumClient;
197
198#[cfg(feature = "azure")]
199impl AzureQuantumClient {
200 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 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 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 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 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 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", "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 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 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 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; 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 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 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 pub fn circuit_to_provider_format<const N: usize>(
528 circuit: &Circuit<N>,
529 provider_id: &str,
530 ) -> DeviceResult<String> {
531 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 fn circuit_to_ionq_format<const N: usize>(_circuit: &Circuit<N>) -> DeviceResult<String> {
545 use serde_json::json;
547
548 #[allow(unused_variables)]
550 let gates: Vec<serde_json::Value> = vec![]; let ionq_circuit = json!({
553 "qubits": N,
554 "circuit": gates,
555 });
556
557 Ok(ionq_circuit.to_string())
558 }
559
560 fn circuit_to_qir_format<const N: usize>(_circuit: &Circuit<N>) -> DeviceResult<String> {
562 Err(DeviceError::CircuitConversion(
565 "QIR conversion not yet implemented".to_string(),
566 ))
567 }
568
569 fn circuit_to_qasm_format<const N: usize>(_circuit: &Circuit<N>) -> DeviceResult<String> {
571 let mut qasm = String::from("OPENQASM 2.0;\ninclude \"qelib1.inc\";\n\n");
573
574 qasm.push_str(&format!("qreg q[{}];\n", N));
576 qasm.push_str(&format!("creg c[{}];\n\n", N));
577
578 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}