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;
10
11#[cfg(feature = "ibm")]
12use reqwest::{header, Client};
13#[cfg(feature = "ibm")]
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16
17use crate::DeviceError;
18use crate::DeviceResult;
19
20#[cfg(feature = "ibm")]
21const IBM_QUANTUM_API_URL: &str = "https://api.quantum-computing.ibm.com/api";
22#[cfg(feature = "ibm")]
23const DEFAULT_TIMEOUT_SECS: u64 = 90;
24
25/// Represents the available backends on IBM Quantum
26#[derive(Debug, Clone)]
27#[cfg_attr(feature = "ibm", derive(serde::Deserialize))]
28pub struct IBMBackend {
29    /// Unique identifier for the backend
30    pub id: String,
31    /// Name of the backend
32    pub name: String,
33    /// Whether the backend is a simulator or real quantum hardware
34    pub simulator: bool,
35    /// Number of qubits on the backend
36    pub n_qubits: usize,
37    /// Status of the backend (e.g., "active", "maintenance")
38    pub status: String,
39    /// Description of the backend
40    pub description: String,
41    /// Version of the backend
42    pub version: String,
43}
44
45/// Configuration for a quantum circuit to be submitted to IBM Quantum
46#[derive(Debug, Clone)]
47#[cfg_attr(feature = "ibm", derive(Serialize))]
48pub struct IBMCircuitConfig {
49    /// Name of the circuit
50    pub name: String,
51    /// QASM representation of the circuit
52    pub qasm: String,
53    /// Number of shots to run
54    pub shots: usize,
55    /// Optional optimization level (0-3)
56    pub optimization_level: Option<usize>,
57    /// Optional initial layout mapping
58    pub initial_layout: Option<std::collections::HashMap<String, usize>>,
59}
60
61/// Status of a job in IBM Quantum
62#[derive(Debug, Clone, PartialEq)]
63#[cfg_attr(feature = "ibm", derive(Deserialize))]
64pub enum IBMJobStatus {
65    #[cfg_attr(feature = "ibm", serde(rename = "CREATING"))]
66    Creating,
67    #[cfg_attr(feature = "ibm", serde(rename = "CREATED"))]
68    Created,
69    #[cfg_attr(feature = "ibm", serde(rename = "VALIDATING"))]
70    Validating,
71    #[cfg_attr(feature = "ibm", serde(rename = "VALIDATED"))]
72    Validated,
73    #[cfg_attr(feature = "ibm", serde(rename = "QUEUED"))]
74    Queued,
75    #[cfg_attr(feature = "ibm", serde(rename = "RUNNING"))]
76    Running,
77    #[cfg_attr(feature = "ibm", serde(rename = "COMPLETED"))]
78    Completed,
79    #[cfg_attr(feature = "ibm", serde(rename = "CANCELLED"))]
80    Cancelled,
81    #[cfg_attr(feature = "ibm", serde(rename = "ERROR"))]
82    Error,
83}
84
85/// Response from submitting a job to IBM Quantum
86#[cfg(feature = "ibm")]
87#[derive(Debug, Deserialize)]
88pub struct IBMJobResponse {
89    /// Job ID
90    pub id: String,
91    /// Status of the job
92    pub status: IBMJobStatus,
93    /// Number of shots
94    pub shots: usize,
95    /// Backend used for the job
96    pub backend: IBMBackend,
97}
98
99#[cfg(not(feature = "ibm"))]
100#[derive(Debug)]
101pub struct IBMJobResponse {
102    /// Job ID
103    pub id: String,
104    /// Status of the job
105    pub status: IBMJobStatus,
106    /// Number of shots
107    pub shots: usize,
108}
109
110/// Results from a completed job
111#[cfg(feature = "ibm")]
112#[derive(Debug, Deserialize)]
113pub struct IBMJobResult {
114    /// Counts of each basis state
115    pub counts: HashMap<String, usize>,
116    /// Total number of shots executed
117    pub shots: usize,
118    /// Status of the job
119    pub status: IBMJobStatus,
120    /// Error message, if any
121    pub error: Option<String>,
122}
123
124#[cfg(not(feature = "ibm"))]
125#[derive(Debug)]
126pub struct IBMJobResult {
127    /// Counts of each basis state
128    pub counts: std::collections::HashMap<String, usize>,
129    /// Total number of shots executed
130    pub shots: usize,
131    /// Status of the job
132    pub status: IBMJobStatus,
133    /// Error message, if any
134    pub error: Option<String>,
135}
136
137/// Errors specific to IBM Quantum
138#[derive(Error, Debug)]
139pub enum IBMQuantumError {
140    #[error("Authentication error: {0}")]
141    Authentication(String),
142
143    #[error("API error: {0}")]
144    API(String),
145
146    #[error("Backend not available: {0}")]
147    BackendUnavailable(String),
148
149    #[error("QASM conversion error: {0}")]
150    QasmConversion(String),
151
152    #[error("Job submission error: {0}")]
153    JobSubmission(String),
154
155    #[error("Timeout waiting for job completion")]
156    Timeout,
157}
158
159/// Client for interacting with IBM Quantum
160#[cfg(feature = "ibm")]
161#[derive(Clone)]
162pub struct IBMQuantumClient {
163    /// HTTP client for making API requests
164    client: Client,
165    /// Base URL for the IBM Quantum API
166    api_url: String,
167    /// Authentication token
168    token: String,
169}
170
171#[cfg(not(feature = "ibm"))]
172#[derive(Clone)]
173pub struct IBMQuantumClient;
174
175#[cfg(feature = "ibm")]
176impl IBMQuantumClient {
177    /// Create a new IBM Quantum client with the given token
178    pub fn new(token: &str) -> DeviceResult<Self> {
179        let mut headers = header::HeaderMap::new();
180        headers.insert(
181            header::CONTENT_TYPE,
182            header::HeaderValue::from_static("application/json"),
183        );
184
185        let client = Client::builder()
186            .default_headers(headers)
187            .timeout(Duration::from_secs(30))
188            .build()
189            .map_err(|e| DeviceError::Connection(e.to_string()))?;
190
191        Ok(Self {
192            client,
193            api_url: IBM_QUANTUM_API_URL.to_string(),
194            token: token.to_string(),
195        })
196    }
197
198    /// List all available backends
199    pub async fn list_backends(&self) -> DeviceResult<Vec<IBMBackend>> {
200        let response = self
201            .client
202            .get(&format!("{}/backends", self.api_url))
203            .header("Authorization", format!("Bearer {}", self.token))
204            .send()
205            .await
206            .map_err(|e| DeviceError::Connection(e.to_string()))?;
207
208        if !response.status().is_success() {
209            let error_msg = response
210                .text()
211                .await
212                .unwrap_or_else(|_| "Unknown error".to_string());
213            return Err(DeviceError::APIError(error_msg));
214        }
215
216        let backends: Vec<IBMBackend> = response
217            .json()
218            .await
219            .map_err(|e| DeviceError::Deserialization(e.to_string()))?;
220
221        Ok(backends)
222    }
223
224    /// Get details about a specific backend
225    pub async fn get_backend(&self, backend_name: &str) -> DeviceResult<IBMBackend> {
226        let response = self
227            .client
228            .get(&format!("{}/backends/{}", self.api_url, backend_name))
229            .header("Authorization", format!("Bearer {}", self.token))
230            .send()
231            .await
232            .map_err(|e| DeviceError::Connection(e.to_string()))?;
233
234        if !response.status().is_success() {
235            let error_msg = response
236                .text()
237                .await
238                .unwrap_or_else(|_| "Unknown error".to_string());
239            return Err(DeviceError::APIError(error_msg));
240        }
241
242        let backend: IBMBackend = response
243            .json()
244            .await
245            .map_err(|e| DeviceError::Deserialization(e.to_string()))?;
246
247        Ok(backend)
248    }
249
250    /// Submit a circuit to be executed on an IBM Quantum backend
251    pub async fn submit_circuit(
252        &self,
253        backend_name: &str,
254        config: IBMCircuitConfig,
255    ) -> DeviceResult<String> {
256        #[cfg(feature = "ibm")]
257        {
258            use serde_json::json;
259
260            let payload = json!({
261                "backend": backend_name,
262                "name": config.name,
263                "qasm": config.qasm,
264                "shots": config.shots,
265                "optimization_level": config.optimization_level.unwrap_or(1),
266                "initial_layout": config.initial_layout.unwrap_or_default(),
267            });
268
269            let response = self
270                .client
271                .post(&format!("{}/jobs", self.api_url))
272                .header("Authorization", format!("Bearer {}", self.token))
273                .json(&payload)
274                .send()
275                .await
276                .map_err(|e| DeviceError::Connection(e.to_string()))?;
277
278            if !response.status().is_success() {
279                let error_msg = response
280                    .text()
281                    .await
282                    .unwrap_or_else(|_| "Unknown error".to_string());
283                return Err(DeviceError::JobSubmission(error_msg));
284            }
285
286            let job_response: IBMJobResponse = response
287                .json()
288                .await
289                .map_err(|e| DeviceError::Deserialization(e.to_string()))?;
290
291            Ok(job_response.id)
292        }
293
294        #[cfg(not(feature = "ibm"))]
295        Err(DeviceError::UnsupportedDevice(
296            "IBM Quantum support not enabled".to_string(),
297        ))
298    }
299
300    /// Get the status of a job
301    pub async fn get_job_status(&self, job_id: &str) -> DeviceResult<IBMJobStatus> {
302        let response = self
303            .client
304            .get(&format!("{}/jobs/{}", self.api_url, job_id))
305            .header("Authorization", format!("Bearer {}", self.token))
306            .send()
307            .await
308            .map_err(|e| DeviceError::Connection(e.to_string()))?;
309
310        if !response.status().is_success() {
311            let error_msg = response
312                .text()
313                .await
314                .unwrap_or_else(|_| "Unknown error".to_string());
315            return Err(DeviceError::APIError(error_msg));
316        }
317
318        let job: IBMJobResponse = response
319            .json()
320            .await
321            .map_err(|e| DeviceError::Deserialization(e.to_string()))?;
322
323        Ok(job.status)
324    }
325
326    /// Get the results of a completed job
327    pub async fn get_job_result(&self, job_id: &str) -> DeviceResult<IBMJobResult> {
328        let response = self
329            .client
330            .get(&format!("{}/jobs/{}/result", self.api_url, job_id))
331            .header("Authorization", format!("Bearer {}", self.token))
332            .send()
333            .await
334            .map_err(|e| DeviceError::Connection(e.to_string()))?;
335
336        if !response.status().is_success() {
337            let error_msg = response
338                .text()
339                .await
340                .unwrap_or_else(|_| "Unknown error".to_string());
341            return Err(DeviceError::APIError(error_msg));
342        }
343
344        let result: IBMJobResult = response
345            .json()
346            .await
347            .map_err(|e| DeviceError::Deserialization(e.to_string()))?;
348
349        Ok(result)
350    }
351
352    /// Wait for a job to complete with timeout
353    pub async fn wait_for_job(
354        &self,
355        job_id: &str,
356        timeout_secs: Option<u64>,
357    ) -> DeviceResult<IBMJobResult> {
358        let timeout = timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS);
359        let mut elapsed = 0;
360        let interval = 5; // Check status every 5 seconds
361
362        while elapsed < timeout {
363            let status = self.get_job_status(job_id).await?;
364
365            match status {
366                IBMJobStatus::Completed => {
367                    return self.get_job_result(job_id).await;
368                }
369                IBMJobStatus::Error => {
370                    return Err(DeviceError::JobExecution(format!(
371                        "Job {} encountered an error",
372                        job_id
373                    )));
374                }
375                IBMJobStatus::Cancelled => {
376                    return Err(DeviceError::JobExecution(format!(
377                        "Job {} was cancelled",
378                        job_id
379                    )));
380                }
381                _ => {
382                    // Still in progress, wait and check again
383                    sleep(Duration::from_secs(interval));
384                    elapsed += interval;
385                }
386            }
387        }
388
389        Err(DeviceError::Timeout(format!(
390            "Timed out waiting for job {} to complete",
391            job_id
392        )))
393    }
394
395    /// Submit multiple circuits in parallel
396    pub async fn submit_circuits_parallel(
397        &self,
398        backend_name: &str,
399        configs: Vec<IBMCircuitConfig>,
400    ) -> DeviceResult<Vec<String>> {
401        #[cfg(feature = "ibm")]
402        {
403            use tokio::task;
404
405            let client = Arc::new(self.clone());
406
407            let mut handles = vec![];
408
409            for config in configs {
410                let client_clone = client.clone();
411                let backend_name = backend_name.to_string();
412
413                let handle =
414                    task::spawn(
415                        async move { client_clone.submit_circuit(&backend_name, config).await },
416                    );
417
418                handles.push(handle);
419            }
420
421            let mut job_ids = vec![];
422
423            for handle in handles {
424                match handle.await {
425                    Ok(result) => match result {
426                        Ok(job_id) => job_ids.push(job_id),
427                        Err(e) => return Err(e),
428                    },
429                    Err(e) => {
430                        return Err(DeviceError::JobSubmission(format!(
431                            "Failed to join task: {}",
432                            e
433                        )));
434                    }
435                }
436            }
437
438            Ok(job_ids)
439        }
440
441        #[cfg(not(feature = "ibm"))]
442        Err(DeviceError::UnsupportedDevice(
443            "IBM Quantum support not enabled".to_string(),
444        ))
445    }
446
447    /// Convert a Quantrs circuit to QASM
448    pub fn circuit_to_qasm<const N: usize>(
449        _circuit: &Circuit<N>,
450        _initial_layout: Option<std::collections::HashMap<String, usize>>,
451    ) -> DeviceResult<String> {
452        // This is a placeholder for the actual conversion logic
453        // In a complete implementation, this would translate our circuit representation
454        // to OpenQASM format compatible with IBM Quantum
455
456        let mut qasm = String::from("OPENQASM 2.0;\ninclude \"qelib1.inc\";\n\n");
457
458        // Define the quantum and classical registers
459        qasm.push_str(&format!("qreg q[{}];\n", N));
460        qasm.push_str(&format!("creg c[{}];\n\n", N));
461
462        // Implement conversion of gates to QASM here
463        // For example:
464        // - X gate: x q[i];
465        // - H gate: h q[i];
466        // - CNOT gate: cx q[i], q[j];
467
468        // For now, just return placeholder QASM
469        Ok(qasm)
470    }
471}
472
473#[cfg(not(feature = "ibm"))]
474impl IBMQuantumClient {
475    pub fn new(_token: &str) -> DeviceResult<Self> {
476        Err(DeviceError::UnsupportedDevice(
477            "IBM Quantum support not enabled. Recompile with the 'ibm' feature.".to_string(),
478        ))
479    }
480
481    pub async fn list_backends(&self) -> DeviceResult<Vec<IBMBackend>> {
482        Err(DeviceError::UnsupportedDevice(
483            "IBM Quantum support not enabled".to_string(),
484        ))
485    }
486
487    pub async fn get_backend(&self, _backend_name: &str) -> DeviceResult<IBMBackend> {
488        Err(DeviceError::UnsupportedDevice(
489            "IBM Quantum support not enabled".to_string(),
490        ))
491    }
492
493    pub async fn submit_circuit(
494        &self,
495        _backend_name: &str,
496        _config: IBMCircuitConfig,
497    ) -> DeviceResult<String> {
498        Err(DeviceError::UnsupportedDevice(
499            "IBM Quantum support not enabled".to_string(),
500        ))
501    }
502
503    pub async fn get_job_status(&self, _job_id: &str) -> DeviceResult<IBMJobStatus> {
504        Err(DeviceError::UnsupportedDevice(
505            "IBM Quantum support not enabled".to_string(),
506        ))
507    }
508
509    pub async fn get_job_result(&self, _job_id: &str) -> DeviceResult<IBMJobResult> {
510        Err(DeviceError::UnsupportedDevice(
511            "IBM Quantum support not enabled".to_string(),
512        ))
513    }
514
515    pub async fn wait_for_job(
516        &self,
517        _job_id: &str,
518        _timeout_secs: Option<u64>,
519    ) -> DeviceResult<IBMJobResult> {
520        Err(DeviceError::UnsupportedDevice(
521            "IBM Quantum support not enabled".to_string(),
522        ))
523    }
524
525    pub async fn submit_circuits_parallel(
526        &self,
527        _backend_name: &str,
528        _configs: Vec<IBMCircuitConfig>,
529    ) -> DeviceResult<Vec<String>> {
530        Err(DeviceError::UnsupportedDevice(
531            "IBM Quantum support not enabled".to_string(),
532        ))
533    }
534
535    pub fn circuit_to_qasm<const N: usize>(
536        _circuit: &Circuit<N>,
537        _initial_layout: Option<std::collections::HashMap<String, usize>>,
538    ) -> DeviceResult<String> {
539        Err(DeviceError::UnsupportedDevice(
540            "IBM Quantum support not enabled".to_string(),
541        ))
542    }
543}