Skip to main content

quantrs2_device/
ibm.rs

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