quantrs2_device/
ibm.rs

1use quantrs2_circuit::prelude::Circuit;
2#[cfg(feature = "ibm")]
3use std::collections::HashMap;
4#[cfg(feature = "ibm")]
5use std::sync::Arc;
6#[cfg(feature = "ibm")]
7use std::thread::sleep;
8#[cfg(feature = "ibm")]
9use std::time::{Duration, Instant, SystemTime};
10#[cfg(feature = "ibm")]
11use tokio::sync::RwLock;
12
13#[cfg(feature = "ibm")]
14use reqwest::{header, Client};
15#[cfg(feature = "ibm")]
16use serde::{Deserialize, Serialize};
17use thiserror::Error;
18
19use crate::DeviceError;
20use crate::DeviceResult;
21
22#[cfg(feature = "ibm")]
23const IBM_QUANTUM_API_URL: &str = "https://api.quantum-computing.ibm.com/api";
24#[cfg(feature = "ibm")]
25const IBM_AUTH_URL: &str = "https://auth.quantum-computing.ibm.com/api";
26#[cfg(feature = "ibm")]
27const DEFAULT_TIMEOUT_SECS: u64 = 90;
28/// Token validity buffer in seconds (refresh 5 minutes before expiry)
29#[cfg(feature = "ibm")]
30const TOKEN_REFRESH_BUFFER_SECS: u64 = 300;
31/// Default token validity period in seconds (1 hour)
32#[cfg(feature = "ibm")]
33const DEFAULT_TOKEN_VALIDITY_SECS: u64 = 3600;
34/// Default maximum retry attempts
35#[cfg(feature = "ibm")]
36const DEFAULT_MAX_RETRIES: u32 = 3;
37/// Default initial retry delay in milliseconds
38#[cfg(feature = "ibm")]
39const DEFAULT_INITIAL_RETRY_DELAY_MS: u64 = 100;
40/// Default maximum retry delay in milliseconds
41#[cfg(feature = "ibm")]
42const DEFAULT_MAX_RETRY_DELAY_MS: u64 = 30000;
43/// Default backoff multiplier
44#[cfg(feature = "ibm")]
45const DEFAULT_BACKOFF_MULTIPLIER: f64 = 2.0;
46
47/// Retry configuration for IBM Quantum API calls
48#[cfg(feature = "ibm")]
49#[derive(Debug, Clone)]
50pub struct IBMRetryConfig {
51    /// Maximum number of retry attempts
52    pub max_attempts: u32,
53    /// Initial delay between retries
54    pub initial_delay: Duration,
55    /// Maximum delay between retries
56    pub max_delay: Duration,
57    /// Backoff multiplier for exponential backoff
58    pub backoff_multiplier: f64,
59    /// Jitter factor (0.0 to 1.0) to randomize delays
60    pub jitter_factor: f64,
61}
62
63#[cfg(feature = "ibm")]
64impl Default for IBMRetryConfig {
65    fn default() -> Self {
66        Self {
67            max_attempts: DEFAULT_MAX_RETRIES,
68            initial_delay: Duration::from_millis(DEFAULT_INITIAL_RETRY_DELAY_MS),
69            max_delay: Duration::from_millis(DEFAULT_MAX_RETRY_DELAY_MS),
70            backoff_multiplier: DEFAULT_BACKOFF_MULTIPLIER,
71            jitter_factor: 0.1,
72        }
73    }
74}
75
76#[cfg(feature = "ibm")]
77impl IBMRetryConfig {
78    /// Create a configuration for aggressive retries (good for transient network errors)
79    pub const fn aggressive() -> Self {
80        Self {
81            max_attempts: 5,
82            initial_delay: Duration::from_millis(50),
83            max_delay: Duration::from_secs(10),
84            backoff_multiplier: 2.0,
85            jitter_factor: 0.2,
86        }
87    }
88
89    /// Create a configuration for patient retries (good for rate limiting)
90    pub const fn patient() -> Self {
91        Self {
92            max_attempts: 3,
93            initial_delay: Duration::from_secs(1),
94            max_delay: Duration::from_secs(60),
95            backoff_multiplier: 3.0,
96            jitter_factor: 0.3,
97        }
98    }
99}
100
101/// Token information including expiration tracking
102#[cfg(feature = "ibm")]
103#[derive(Debug, Clone)]
104pub struct TokenInfo {
105    /// The access token
106    pub access_token: String,
107    /// When the token was obtained
108    pub obtained_at: Instant,
109    /// Token validity period in seconds
110    pub valid_for_secs: u64,
111}
112
113#[cfg(feature = "ibm")]
114impl TokenInfo {
115    /// Check if the token is expired or about to expire
116    pub fn is_expired(&self) -> bool {
117        let elapsed = self.obtained_at.elapsed().as_secs();
118        elapsed + TOKEN_REFRESH_BUFFER_SECS >= self.valid_for_secs
119    }
120
121    /// Get remaining validity time in seconds
122    pub fn remaining_secs(&self) -> u64 {
123        let elapsed = self.obtained_at.elapsed().as_secs();
124        self.valid_for_secs.saturating_sub(elapsed)
125    }
126}
127
128/// Response from IBM Quantum authentication endpoint
129#[cfg(feature = "ibm")]
130#[derive(Debug, Deserialize)]
131struct AuthResponse {
132    /// The access token
133    id: String,
134    /// Token TTL in seconds (if provided)
135    ttl: Option<u64>,
136}
137
138/// Authentication configuration for IBM Quantum
139#[cfg(feature = "ibm")]
140#[derive(Debug, Clone)]
141pub struct IBMAuthConfig {
142    /// The API key (used to obtain access tokens)
143    pub api_key: String,
144    /// Whether to automatically refresh expired tokens
145    pub auto_refresh: bool,
146    /// Custom token validity period (if known)
147    pub token_validity_secs: Option<u64>,
148}
149
150/// Represents the available backends on IBM Quantum
151#[derive(Debug, Clone)]
152#[cfg_attr(feature = "ibm", derive(serde::Deserialize))]
153pub struct IBMBackend {
154    /// Unique identifier for the backend
155    pub id: String,
156    /// Name of the backend
157    pub name: String,
158    /// Whether the backend is a simulator or real quantum hardware
159    pub simulator: bool,
160    /// Number of qubits on the backend
161    pub n_qubits: usize,
162    /// Status of the backend (e.g., "active", "maintenance")
163    pub status: String,
164    /// Description of the backend
165    pub description: String,
166    /// Version of the backend
167    pub version: String,
168}
169
170/// Configuration for a quantum circuit to be submitted to IBM Quantum
171#[derive(Debug, Clone)]
172#[cfg_attr(feature = "ibm", derive(Serialize))]
173pub struct IBMCircuitConfig {
174    /// Name of the circuit
175    pub name: String,
176    /// QASM representation of the circuit
177    pub qasm: String,
178    /// Number of shots to run
179    pub shots: usize,
180    /// Optional optimization level (0-3)
181    pub optimization_level: Option<usize>,
182    /// Optional initial layout mapping
183    pub initial_layout: Option<std::collections::HashMap<String, usize>>,
184}
185
186/// Status of a job in IBM Quantum
187#[derive(Debug, Clone, PartialEq, Eq)]
188#[cfg_attr(feature = "ibm", derive(Deserialize))]
189pub enum IBMJobStatus {
190    #[cfg_attr(feature = "ibm", serde(rename = "CREATING"))]
191    Creating,
192    #[cfg_attr(feature = "ibm", serde(rename = "CREATED"))]
193    Created,
194    #[cfg_attr(feature = "ibm", serde(rename = "VALIDATING"))]
195    Validating,
196    #[cfg_attr(feature = "ibm", serde(rename = "VALIDATED"))]
197    Validated,
198    #[cfg_attr(feature = "ibm", serde(rename = "QUEUED"))]
199    Queued,
200    #[cfg_attr(feature = "ibm", serde(rename = "RUNNING"))]
201    Running,
202    #[cfg_attr(feature = "ibm", serde(rename = "COMPLETED"))]
203    Completed,
204    #[cfg_attr(feature = "ibm", serde(rename = "CANCELLED"))]
205    Cancelled,
206    #[cfg_attr(feature = "ibm", serde(rename = "ERROR"))]
207    Error,
208}
209
210/// Response from submitting a job to IBM Quantum
211#[cfg(feature = "ibm")]
212#[derive(Debug, Deserialize)]
213pub struct IBMJobResponse {
214    /// Job ID
215    pub id: String,
216    /// Status of the job
217    pub status: IBMJobStatus,
218    /// Number of shots
219    pub shots: usize,
220    /// Backend used for the job
221    pub backend: IBMBackend,
222}
223
224#[cfg(not(feature = "ibm"))]
225#[derive(Debug)]
226pub struct IBMJobResponse {
227    /// Job ID
228    pub id: String,
229    /// Status of the job
230    pub status: IBMJobStatus,
231    /// Number of shots
232    pub shots: usize,
233}
234
235/// Results from a completed job
236#[cfg(feature = "ibm")]
237#[derive(Debug, Deserialize)]
238pub struct IBMJobResult {
239    /// Counts of each basis state
240    pub counts: HashMap<String, usize>,
241    /// Total number of shots executed
242    pub shots: usize,
243    /// Status of the job
244    pub status: IBMJobStatus,
245    /// Error message, if any
246    pub error: Option<String>,
247}
248
249#[cfg(not(feature = "ibm"))]
250#[derive(Debug)]
251pub struct IBMJobResult {
252    /// Counts of each basis state
253    pub counts: std::collections::HashMap<String, usize>,
254    /// Total number of shots executed
255    pub shots: usize,
256    /// Status of the job
257    pub status: IBMJobStatus,
258    /// Error message, if any
259    pub error: Option<String>,
260}
261
262/// Errors specific to IBM Quantum
263#[derive(Error, Debug)]
264pub enum IBMQuantumError {
265    #[error("Authentication error: {0}")]
266    Authentication(String),
267
268    #[error("API error: {0}")]
269    API(String),
270
271    #[error("Backend not available: {0}")]
272    BackendUnavailable(String),
273
274    #[error("QASM conversion error: {0}")]
275    QasmConversion(String),
276
277    #[error("Job submission error: {0}")]
278    JobSubmission(String),
279
280    #[error("Timeout waiting for job completion")]
281    Timeout,
282}
283
284/// Client for interacting with IBM Quantum
285#[cfg(feature = "ibm")]
286pub struct IBMQuantumClient {
287    /// HTTP client for making API requests
288    client: Client,
289    /// Base URL for the IBM Quantum API
290    api_url: String,
291    /// Authentication URL
292    auth_url: String,
293    /// Current token information (protected by RwLock for thread-safe refresh)
294    token_info: Arc<RwLock<TokenInfo>>,
295    /// Authentication configuration
296    auth_config: IBMAuthConfig,
297    /// Retry configuration for API calls
298    retry_config: IBMRetryConfig,
299}
300
301#[cfg(feature = "ibm")]
302impl Clone for IBMQuantumClient {
303    fn clone(&self) -> Self {
304        Self {
305            client: self.client.clone(),
306            api_url: self.api_url.clone(),
307            auth_url: self.auth_url.clone(),
308            token_info: Arc::clone(&self.token_info),
309            auth_config: self.auth_config.clone(),
310            retry_config: self.retry_config.clone(),
311        }
312    }
313}
314
315#[cfg(not(feature = "ibm"))]
316#[derive(Clone)]
317pub struct IBMQuantumClient;
318
319#[cfg(feature = "ibm")]
320impl IBMQuantumClient {
321    /// Create a new IBM Quantum client with the given access token (legacy method)
322    ///
323    /// Note: This method does not support automatic token refresh.
324    /// For production use, prefer `new_with_api_key` which supports auto-refresh.
325    pub fn new(token: &str) -> DeviceResult<Self> {
326        let mut headers = header::HeaderMap::new();
327        headers.insert(
328            header::CONTENT_TYPE,
329            header::HeaderValue::from_static("application/json"),
330        );
331
332        let client = Client::builder()
333            .default_headers(headers)
334            .timeout(Duration::from_secs(30))
335            .build()
336            .map_err(|e| DeviceError::Connection(e.to_string()))?;
337
338        let token_info = TokenInfo {
339            access_token: token.to_string(),
340            obtained_at: Instant::now(),
341            valid_for_secs: DEFAULT_TOKEN_VALIDITY_SECS,
342        };
343
344        Ok(Self {
345            client,
346            api_url: IBM_QUANTUM_API_URL.to_string(),
347            auth_url: IBM_AUTH_URL.to_string(),
348            token_info: Arc::new(RwLock::new(token_info)),
349            auth_config: IBMAuthConfig {
350                api_key: String::new(), // No API key for legacy token-based auth
351                auto_refresh: false,
352                token_validity_secs: None,
353            },
354            retry_config: IBMRetryConfig::default(),
355        })
356    }
357
358    /// Create a new IBM Quantum client with an API key
359    ///
360    /// This method exchanges the API key for an access token and supports
361    /// automatic token refresh when the token expires.
362    pub async fn new_with_api_key(api_key: &str) -> DeviceResult<Self> {
363        Self::new_with_config(IBMAuthConfig {
364            api_key: api_key.to_string(),
365            auto_refresh: true,
366            token_validity_secs: None,
367        })
368        .await
369    }
370
371    /// Create a new IBM Quantum client with full authentication configuration
372    pub async fn new_with_config(config: IBMAuthConfig) -> DeviceResult<Self> {
373        Self::new_with_config_and_retry(config, IBMRetryConfig::default()).await
374    }
375
376    /// Create a new IBM Quantum client with authentication and retry configuration
377    pub async fn new_with_config_and_retry(
378        config: IBMAuthConfig,
379        retry_config: IBMRetryConfig,
380    ) -> DeviceResult<Self> {
381        let mut headers = header::HeaderMap::new();
382        headers.insert(
383            header::CONTENT_TYPE,
384            header::HeaderValue::from_static("application/json"),
385        );
386
387        let client = Client::builder()
388            .default_headers(headers)
389            .timeout(Duration::from_secs(30))
390            .build()
391            .map_err(|e| DeviceError::Connection(e.to_string()))?;
392
393        // Exchange API key for access token
394        let token_info = Self::exchange_api_key_for_token(&client, &config.api_key).await?;
395
396        Ok(Self {
397            client,
398            api_url: IBM_QUANTUM_API_URL.to_string(),
399            auth_url: IBM_AUTH_URL.to_string(),
400            token_info: Arc::new(RwLock::new(token_info)),
401            auth_config: config,
402            retry_config,
403        })
404    }
405
406    /// Set retry configuration
407    pub const fn set_retry_config(&mut self, config: IBMRetryConfig) {
408        self.retry_config = config;
409    }
410
411    /// Get current retry configuration
412    pub const fn retry_config(&self) -> &IBMRetryConfig {
413        &self.retry_config
414    }
415
416    /// Execute an async operation with exponential backoff retry
417    async fn with_retry<F, Fut, T>(&self, operation: F) -> DeviceResult<T>
418    where
419        F: Fn() -> Fut,
420        Fut: std::future::Future<Output = DeviceResult<T>>,
421    {
422        use scirs2_core::random::prelude::*;
423
424        let mut attempt = 0;
425        let mut delay = self.retry_config.initial_delay;
426
427        loop {
428            match operation().await {
429                Ok(result) => return Ok(result),
430                Err(err) => {
431                    attempt += 1;
432
433                    // Check if error is retryable
434                    let is_retryable = match &err {
435                        DeviceError::Connection(_) | DeviceError::Timeout(_) => true,
436                        DeviceError::APIError(msg) => {
437                            msg.contains("rate") || msg.contains('5') || msg.contains("503")
438                        }
439                        _ => false,
440                    };
441
442                    if !is_retryable || attempt >= self.retry_config.max_attempts {
443                        return Err(err);
444                    }
445
446                    // Calculate delay with jitter
447                    let jitter = if self.retry_config.jitter_factor > 0.0 {
448                        let mut rng = thread_rng();
449                        let jitter_range =
450                            delay.as_millis() as f64 * self.retry_config.jitter_factor;
451                        Duration::from_millis((rng.gen::<f64>() * jitter_range) as u64)
452                    } else {
453                        Duration::ZERO
454                    };
455
456                    let actual_delay = delay + jitter;
457                    tokio::time::sleep(actual_delay).await;
458
459                    // Calculate next delay with exponential backoff
460                    delay = Duration::from_millis(
461                        (delay.as_millis() as f64 * self.retry_config.backoff_multiplier) as u64,
462                    )
463                    .min(self.retry_config.max_delay);
464                }
465            }
466        }
467    }
468
469    /// Exchange an API key for an access token
470    async fn exchange_api_key_for_token(client: &Client, api_key: &str) -> DeviceResult<TokenInfo> {
471        let response = client
472            .post(format!("{IBM_AUTH_URL}/users/loginWithToken"))
473            .json(&serde_json::json!({ "apiToken": api_key }))
474            .send()
475            .await
476            .map_err(|e| DeviceError::Connection(format!("Authentication request failed: {e}")))?;
477
478        if !response.status().is_success() {
479            let error_msg = response
480                .text()
481                .await
482                .unwrap_or_else(|_| "Unknown authentication error".to_string());
483            return Err(DeviceError::Authentication(error_msg));
484        }
485
486        let auth_response: AuthResponse = response.json().await.map_err(|e| {
487            DeviceError::Deserialization(format!("Failed to parse auth response: {e}"))
488        })?;
489
490        let valid_for_secs = auth_response.ttl.unwrap_or(DEFAULT_TOKEN_VALIDITY_SECS);
491
492        Ok(TokenInfo {
493            access_token: auth_response.id,
494            obtained_at: Instant::now(),
495            valid_for_secs,
496        })
497    }
498
499    /// Refresh the access token if it's expired or about to expire
500    pub async fn refresh_token(&self) -> DeviceResult<()> {
501        if self.auth_config.api_key.is_empty() {
502            return Err(DeviceError::Authentication(
503                "Cannot refresh token: no API key configured. Use new_with_api_key() for auto-refresh support.".to_string()
504            ));
505        }
506
507        let new_token_info =
508            Self::exchange_api_key_for_token(&self.client, &self.auth_config.api_key).await?;
509
510        let mut token_guard = self.token_info.write().await;
511        *token_guard = new_token_info;
512
513        Ok(())
514    }
515
516    /// Get a valid access token, refreshing if necessary
517    async fn get_valid_token(&self) -> DeviceResult<String> {
518        // First check if refresh is needed
519        let needs_refresh = {
520            let token_guard = self.token_info.read().await;
521            token_guard.is_expired()
522        };
523
524        if needs_refresh && self.auth_config.auto_refresh {
525            self.refresh_token().await?;
526        }
527
528        let token_guard = self.token_info.read().await;
529
530        // If still expired after refresh attempt (or auto_refresh disabled), warn but continue
531        if token_guard.is_expired() && !self.auth_config.auto_refresh {
532            // Token is expired but auto-refresh is disabled
533            // Let the API call fail and return appropriate error
534        }
535
536        Ok(token_guard.access_token.clone())
537    }
538
539    /// Check if the current token is valid
540    pub async fn is_token_valid(&self) -> bool {
541        let token_guard = self.token_info.read().await;
542        !token_guard.is_expired()
543    }
544
545    /// Get token expiration information
546    pub async fn token_info(&self) -> TokenInfo {
547        let token_guard = self.token_info.read().await;
548        token_guard.clone()
549    }
550
551    /// List all available backends with automatic retry
552    pub async fn list_backends_with_retry(&self) -> DeviceResult<Vec<IBMBackend>> {
553        self.with_retry(|| async { self.list_backends().await })
554            .await
555    }
556
557    /// List all available backends
558    pub async fn list_backends(&self) -> DeviceResult<Vec<IBMBackend>> {
559        let token = self.get_valid_token().await?;
560
561        let response = self
562            .client
563            .get(format!("{}/backends", self.api_url))
564            .header("Authorization", format!("Bearer {token}"))
565            .send()
566            .await
567            .map_err(|e| DeviceError::Connection(e.to_string()))?;
568
569        if !response.status().is_success() {
570            let error_msg = response
571                .text()
572                .await
573                .unwrap_or_else(|_| "Unknown error".to_string());
574            return Err(DeviceError::APIError(error_msg));
575        }
576
577        let backends: Vec<IBMBackend> = response
578            .json()
579            .await
580            .map_err(|e| DeviceError::Deserialization(e.to_string()))?;
581
582        Ok(backends)
583    }
584
585    /// Get details about a specific backend
586    pub async fn get_backend(&self, backend_name: &str) -> DeviceResult<IBMBackend> {
587        let token = self.get_valid_token().await?;
588
589        let response = self
590            .client
591            .get(format!("{}/backends/{}", self.api_url, backend_name))
592            .header("Authorization", format!("Bearer {token}"))
593            .send()
594            .await
595            .map_err(|e| DeviceError::Connection(e.to_string()))?;
596
597        if !response.status().is_success() {
598            let error_msg = response
599                .text()
600                .await
601                .unwrap_or_else(|_| "Unknown error".to_string());
602            return Err(DeviceError::APIError(error_msg));
603        }
604
605        let backend: IBMBackend = response
606            .json()
607            .await
608            .map_err(|e| DeviceError::Deserialization(e.to_string()))?;
609
610        Ok(backend)
611    }
612
613    /// Submit a circuit to be executed on an IBM Quantum backend
614    pub async fn submit_circuit(
615        &self,
616        backend_name: &str,
617        config: IBMCircuitConfig,
618    ) -> DeviceResult<String> {
619        #[cfg(feature = "ibm")]
620        {
621            use serde_json::json;
622
623            let token = self.get_valid_token().await?;
624
625            let payload = json!({
626                "backend": backend_name,
627                "name": config.name,
628                "qasm": config.qasm,
629                "shots": config.shots,
630                "optimization_level": config.optimization_level.unwrap_or(1),
631                "initial_layout": config.initial_layout.unwrap_or_default(),
632            });
633
634            let response = self
635                .client
636                .post(format!("{}/jobs", self.api_url))
637                .header("Authorization", format!("Bearer {token}"))
638                .json(&payload)
639                .send()
640                .await
641                .map_err(|e| DeviceError::Connection(e.to_string()))?;
642
643            if !response.status().is_success() {
644                let error_msg = response
645                    .text()
646                    .await
647                    .unwrap_or_else(|_| "Unknown error".to_string());
648                return Err(DeviceError::JobSubmission(error_msg));
649            }
650
651            let job_response: IBMJobResponse = response
652                .json()
653                .await
654                .map_err(|e| DeviceError::Deserialization(e.to_string()))?;
655
656            Ok(job_response.id)
657        }
658
659        #[cfg(not(feature = "ibm"))]
660        Err(DeviceError::UnsupportedDevice(
661            "IBM Quantum support not enabled".to_string(),
662        ))
663    }
664
665    /// Get the status of a job
666    pub async fn get_job_status(&self, job_id: &str) -> DeviceResult<IBMJobStatus> {
667        let token = self.get_valid_token().await?;
668
669        let response = self
670            .client
671            .get(format!("{}/jobs/{}", self.api_url, job_id))
672            .header("Authorization", format!("Bearer {token}"))
673            .send()
674            .await
675            .map_err(|e| DeviceError::Connection(e.to_string()))?;
676
677        if !response.status().is_success() {
678            let error_msg = response
679                .text()
680                .await
681                .unwrap_or_else(|_| "Unknown error".to_string());
682            return Err(DeviceError::APIError(error_msg));
683        }
684
685        let job: IBMJobResponse = response
686            .json()
687            .await
688            .map_err(|e| DeviceError::Deserialization(e.to_string()))?;
689
690        Ok(job.status)
691    }
692
693    /// Get the results of a completed job
694    pub async fn get_job_result(&self, job_id: &str) -> DeviceResult<IBMJobResult> {
695        let token = self.get_valid_token().await?;
696
697        let response = self
698            .client
699            .get(format!("{}/jobs/{}/result", self.api_url, job_id))
700            .header("Authorization", format!("Bearer {token}"))
701            .send()
702            .await
703            .map_err(|e| DeviceError::Connection(e.to_string()))?;
704
705        if !response.status().is_success() {
706            let error_msg = response
707                .text()
708                .await
709                .unwrap_or_else(|_| "Unknown error".to_string());
710            return Err(DeviceError::APIError(error_msg));
711        }
712
713        let result: IBMJobResult = response
714            .json()
715            .await
716            .map_err(|e| DeviceError::Deserialization(e.to_string()))?;
717
718        Ok(result)
719    }
720
721    /// Wait for a job to complete with timeout
722    pub async fn wait_for_job(
723        &self,
724        job_id: &str,
725        timeout_secs: Option<u64>,
726    ) -> DeviceResult<IBMJobResult> {
727        let timeout = timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS);
728        let mut elapsed = 0;
729        let interval = 5; // Check status every 5 seconds
730
731        while elapsed < timeout {
732            let status = self.get_job_status(job_id).await?;
733
734            match status {
735                IBMJobStatus::Completed => {
736                    return self.get_job_result(job_id).await;
737                }
738                IBMJobStatus::Error => {
739                    return Err(DeviceError::JobExecution(format!(
740                        "Job {job_id} encountered an error"
741                    )));
742                }
743                IBMJobStatus::Cancelled => {
744                    return Err(DeviceError::JobExecution(format!(
745                        "Job {job_id} was cancelled"
746                    )));
747                }
748                _ => {
749                    // Still in progress, wait and check again
750                    sleep(Duration::from_secs(interval));
751                    elapsed += interval;
752                }
753            }
754        }
755
756        Err(DeviceError::Timeout(format!(
757            "Timed out waiting for job {job_id} to complete"
758        )))
759    }
760
761    /// Submit multiple circuits in parallel
762    pub async fn submit_circuits_parallel(
763        &self,
764        backend_name: &str,
765        configs: Vec<IBMCircuitConfig>,
766    ) -> DeviceResult<Vec<String>> {
767        #[cfg(feature = "ibm")]
768        {
769            use tokio::task;
770
771            let client = Arc::new(self.clone());
772
773            let mut handles = vec![];
774
775            for config in configs {
776                let client_clone = client.clone();
777                let backend_name = backend_name.to_string();
778
779                let handle =
780                    task::spawn(
781                        async move { client_clone.submit_circuit(&backend_name, config).await },
782                    );
783
784                handles.push(handle);
785            }
786
787            let mut job_ids = vec![];
788
789            for handle in handles {
790                match handle.await {
791                    Ok(result) => match result {
792                        Ok(job_id) => job_ids.push(job_id),
793                        Err(e) => return Err(e),
794                    },
795                    Err(e) => {
796                        return Err(DeviceError::JobSubmission(format!(
797                            "Failed to join task: {e}"
798                        )));
799                    }
800                }
801            }
802
803            Ok(job_ids)
804        }
805
806        #[cfg(not(feature = "ibm"))]
807        Err(DeviceError::UnsupportedDevice(
808            "IBM Quantum support not enabled".to_string(),
809        ))
810    }
811
812    /// Convert a Quantrs circuit to QASM
813    pub fn circuit_to_qasm<const N: usize>(
814        _circuit: &Circuit<N>,
815        _initial_layout: Option<std::collections::HashMap<String, usize>>,
816    ) -> DeviceResult<String> {
817        // This is a placeholder for the actual conversion logic
818        // In a complete implementation, this would translate our circuit representation
819        // to OpenQASM format compatible with IBM Quantum
820
821        let mut qasm = String::from("OPENQASM 2.0;\ninclude \"qelib1.inc\";\n\n");
822
823        // Define the quantum and classical registers
824        use std::fmt::Write;
825        writeln!(qasm, "qreg q[{N}];")
826            .map_err(|e| DeviceError::CircuitConversion(format!("Failed to write QASM: {e}")))?;
827        writeln!(qasm, "creg c[{N}];")
828            .map_err(|e| DeviceError::CircuitConversion(format!("Failed to write QASM: {e}")))?;
829
830        // Implement conversion of gates to QASM here
831        // For example:
832        // - X gate: x q[i];
833        // - H gate: h q[i];
834        // - CNOT gate: cx q[i], q[j];
835
836        // For now, just return placeholder QASM
837        Ok(qasm)
838    }
839}
840
841#[cfg(not(feature = "ibm"))]
842impl IBMQuantumClient {
843    pub fn new(_token: &str) -> DeviceResult<Self> {
844        Err(DeviceError::UnsupportedDevice(
845            "IBM Quantum support not enabled. Recompile with the 'ibm' feature.".to_string(),
846        ))
847    }
848
849    pub async fn list_backends(&self) -> DeviceResult<Vec<IBMBackend>> {
850        Err(DeviceError::UnsupportedDevice(
851            "IBM Quantum support not enabled".to_string(),
852        ))
853    }
854
855    pub async fn get_backend(&self, _backend_name: &str) -> DeviceResult<IBMBackend> {
856        Err(DeviceError::UnsupportedDevice(
857            "IBM Quantum support not enabled".to_string(),
858        ))
859    }
860
861    pub async fn submit_circuit(
862        &self,
863        _backend_name: &str,
864        _config: IBMCircuitConfig,
865    ) -> DeviceResult<String> {
866        Err(DeviceError::UnsupportedDevice(
867            "IBM Quantum support not enabled".to_string(),
868        ))
869    }
870
871    pub async fn get_job_status(&self, _job_id: &str) -> DeviceResult<IBMJobStatus> {
872        Err(DeviceError::UnsupportedDevice(
873            "IBM Quantum support not enabled".to_string(),
874        ))
875    }
876
877    pub async fn get_job_result(&self, _job_id: &str) -> DeviceResult<IBMJobResult> {
878        Err(DeviceError::UnsupportedDevice(
879            "IBM Quantum support not enabled".to_string(),
880        ))
881    }
882
883    pub async fn wait_for_job(
884        &self,
885        _job_id: &str,
886        _timeout_secs: Option<u64>,
887    ) -> DeviceResult<IBMJobResult> {
888        Err(DeviceError::UnsupportedDevice(
889            "IBM Quantum support not enabled".to_string(),
890        ))
891    }
892
893    pub async fn submit_circuits_parallel(
894        &self,
895        _backend_name: &str,
896        _configs: Vec<IBMCircuitConfig>,
897    ) -> DeviceResult<Vec<String>> {
898        Err(DeviceError::UnsupportedDevice(
899            "IBM Quantum support not enabled".to_string(),
900        ))
901    }
902
903    pub fn circuit_to_qasm<const N: usize>(
904        _circuit: &Circuit<N>,
905        _initial_layout: Option<std::collections::HashMap<String, usize>>,
906    ) -> DeviceResult<String> {
907        Err(DeviceError::UnsupportedDevice(
908            "IBM Quantum support not enabled".to_string(),
909        ))
910    }
911}