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#[derive(Debug, Clone)]
27#[cfg_attr(feature = "ibm", derive(serde::Deserialize))]
28pub struct IBMBackend {
29 pub id: String,
31 pub name: String,
33 pub simulator: bool,
35 pub n_qubits: usize,
37 pub status: String,
39 pub description: String,
41 pub version: String,
43}
44
45#[derive(Debug, Clone)]
47#[cfg_attr(feature = "ibm", derive(Serialize))]
48pub struct IBMCircuitConfig {
49 pub name: String,
51 pub qasm: String,
53 pub shots: usize,
55 pub optimization_level: Option<usize>,
57 pub initial_layout: Option<std::collections::HashMap<String, usize>>,
59}
60
61#[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#[cfg(feature = "ibm")]
87#[derive(Debug, Deserialize)]
88pub struct IBMJobResponse {
89 pub id: String,
91 pub status: IBMJobStatus,
93 pub shots: usize,
95 pub backend: IBMBackend,
97}
98
99#[cfg(not(feature = "ibm"))]
100#[derive(Debug)]
101pub struct IBMJobResponse {
102 pub id: String,
104 pub status: IBMJobStatus,
106 pub shots: usize,
108}
109
110#[cfg(feature = "ibm")]
112#[derive(Debug, Deserialize)]
113pub struct IBMJobResult {
114 pub counts: HashMap<String, usize>,
116 pub shots: usize,
118 pub status: IBMJobStatus,
120 pub error: Option<String>,
122}
123
124#[cfg(not(feature = "ibm"))]
125#[derive(Debug)]
126pub struct IBMJobResult {
127 pub counts: std::collections::HashMap<String, usize>,
129 pub shots: usize,
131 pub status: IBMJobStatus,
133 pub error: Option<String>,
135}
136
137#[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#[cfg(feature = "ibm")]
161#[derive(Clone)]
162pub struct IBMQuantumClient {
163 client: Client,
165 api_url: String,
167 token: String,
169}
170
171#[cfg(not(feature = "ibm"))]
172#[derive(Clone)]
173pub struct IBMQuantumClient;
174
175#[cfg(feature = "ibm")]
176impl IBMQuantumClient {
177 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 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 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 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 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 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 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; 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 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 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 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 let mut qasm = String::from("OPENQASM 2.0;\ninclude \"qelib1.inc\";\n\n");
457
458 qasm.push_str(&format!("qreg q[{}];\n", N));
460 qasm.push_str(&format!("creg c[{}];\n\n", N));
461
462 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}