use quantrs2_circuit::prelude::Circuit;
#[cfg(feature = "ibm")]
use std::collections::HashMap;
#[cfg(feature = "ibm")]
use std::sync::Arc;
#[cfg(feature = "ibm")]
use std::thread::sleep;
#[cfg(feature = "ibm")]
use std::time::{Duration, Instant, SystemTime};
#[cfg(feature = "ibm")]
use tokio::sync::RwLock;
#[cfg(feature = "ibm")]
use reqwest::{header, Client};
#[cfg(feature = "ibm")]
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::DeviceError;
use crate::DeviceResult;
#[cfg(feature = "ibm")]
const IBM_QUANTUM_API_URL: &str = "https://api.quantum-computing.ibm.com/api";
#[cfg(feature = "ibm")]
const IBM_AUTH_URL: &str = "https://auth.quantum-computing.ibm.com/api";
#[cfg(feature = "ibm")]
const DEFAULT_TIMEOUT_SECS: u64 = 90;
#[cfg(feature = "ibm")]
const TOKEN_REFRESH_BUFFER_SECS: u64 = 300;
#[cfg(feature = "ibm")]
const DEFAULT_TOKEN_VALIDITY_SECS: u64 = 3600;
#[cfg(feature = "ibm")]
const DEFAULT_MAX_RETRIES: u32 = 3;
#[cfg(feature = "ibm")]
const DEFAULT_INITIAL_RETRY_DELAY_MS: u64 = 100;
#[cfg(feature = "ibm")]
const DEFAULT_MAX_RETRY_DELAY_MS: u64 = 30000;
#[cfg(feature = "ibm")]
const DEFAULT_BACKOFF_MULTIPLIER: f64 = 2.0;
#[cfg(feature = "ibm")]
#[derive(Debug, Clone)]
pub struct IBMRetryConfig {
pub max_attempts: u32,
pub initial_delay: Duration,
pub max_delay: Duration,
pub backoff_multiplier: f64,
pub jitter_factor: f64,
}
#[cfg(feature = "ibm")]
impl Default for IBMRetryConfig {
fn default() -> Self {
Self {
max_attempts: DEFAULT_MAX_RETRIES,
initial_delay: Duration::from_millis(DEFAULT_INITIAL_RETRY_DELAY_MS),
max_delay: Duration::from_millis(DEFAULT_MAX_RETRY_DELAY_MS),
backoff_multiplier: DEFAULT_BACKOFF_MULTIPLIER,
jitter_factor: 0.1,
}
}
}
#[cfg(feature = "ibm")]
impl IBMRetryConfig {
pub const fn aggressive() -> Self {
Self {
max_attempts: 5,
initial_delay: Duration::from_millis(50),
max_delay: Duration::from_secs(10),
backoff_multiplier: 2.0,
jitter_factor: 0.2,
}
}
pub const fn patient() -> Self {
Self {
max_attempts: 3,
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
backoff_multiplier: 3.0,
jitter_factor: 0.3,
}
}
}
#[cfg(feature = "ibm")]
#[derive(Debug, Clone)]
pub struct TokenInfo {
pub access_token: String,
pub obtained_at: Instant,
pub valid_for_secs: u64,
}
#[cfg(feature = "ibm")]
impl TokenInfo {
pub fn is_expired(&self) -> bool {
let elapsed = self.obtained_at.elapsed().as_secs();
elapsed + TOKEN_REFRESH_BUFFER_SECS >= self.valid_for_secs
}
pub fn remaining_secs(&self) -> u64 {
let elapsed = self.obtained_at.elapsed().as_secs();
self.valid_for_secs.saturating_sub(elapsed)
}
}
#[cfg(feature = "ibm")]
#[derive(Debug, Deserialize)]
struct AuthResponse {
id: String,
ttl: Option<u64>,
}
#[cfg(feature = "ibm")]
#[derive(Debug, Clone)]
pub struct IBMAuthConfig {
pub api_key: String,
pub auto_refresh: bool,
pub token_validity_secs: Option<u64>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "ibm", derive(serde::Deserialize))]
pub struct IBMBackend {
pub id: String,
pub name: String,
pub simulator: bool,
pub n_qubits: usize,
pub status: String,
pub description: String,
pub version: String,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "ibm", derive(Serialize))]
pub struct IBMCircuitConfig {
pub name: String,
pub qasm: String,
pub shots: usize,
pub optimization_level: Option<usize>,
pub initial_layout: Option<std::collections::HashMap<String, usize>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "ibm", derive(Deserialize))]
pub enum IBMJobStatus {
#[cfg_attr(feature = "ibm", serde(rename = "CREATING"))]
Creating,
#[cfg_attr(feature = "ibm", serde(rename = "CREATED"))]
Created,
#[cfg_attr(feature = "ibm", serde(rename = "VALIDATING"))]
Validating,
#[cfg_attr(feature = "ibm", serde(rename = "VALIDATED"))]
Validated,
#[cfg_attr(feature = "ibm", serde(rename = "QUEUED"))]
Queued,
#[cfg_attr(feature = "ibm", serde(rename = "RUNNING"))]
Running,
#[cfg_attr(feature = "ibm", serde(rename = "COMPLETED"))]
Completed,
#[cfg_attr(feature = "ibm", serde(rename = "CANCELLED"))]
Cancelled,
#[cfg_attr(feature = "ibm", serde(rename = "ERROR"))]
Error,
}
#[cfg(feature = "ibm")]
#[derive(Debug, Deserialize)]
pub struct IBMJobResponse {
pub id: String,
pub status: IBMJobStatus,
pub shots: usize,
pub backend: IBMBackend,
}
#[cfg(not(feature = "ibm"))]
#[derive(Debug)]
pub struct IBMJobResponse {
pub id: String,
pub status: IBMJobStatus,
pub shots: usize,
}
#[cfg(feature = "ibm")]
#[derive(Debug, Deserialize)]
pub struct IBMJobResult {
pub counts: HashMap<String, usize>,
pub shots: usize,
pub status: IBMJobStatus,
pub error: Option<String>,
}
#[cfg(not(feature = "ibm"))]
#[derive(Debug)]
pub struct IBMJobResult {
pub counts: std::collections::HashMap<String, usize>,
pub shots: usize,
pub status: IBMJobStatus,
pub error: Option<String>,
}
#[derive(Error, Debug)]
pub enum IBMQuantumError {
#[error("Authentication error: {0}")]
Authentication(String),
#[error("API error: {0}")]
API(String),
#[error("Backend not available: {0}")]
BackendUnavailable(String),
#[error("QASM conversion error: {0}")]
QasmConversion(String),
#[error("Job submission error: {0}")]
JobSubmission(String),
#[error("Timeout waiting for job completion")]
Timeout,
}
#[cfg(feature = "ibm")]
pub struct IBMQuantumClient {
client: Client,
api_url: String,
auth_url: String,
token_info: Arc<RwLock<TokenInfo>>,
auth_config: IBMAuthConfig,
retry_config: IBMRetryConfig,
}
#[cfg(feature = "ibm")]
impl Clone for IBMQuantumClient {
fn clone(&self) -> Self {
Self {
client: self.client.clone(),
api_url: self.api_url.clone(),
auth_url: self.auth_url.clone(),
token_info: Arc::clone(&self.token_info),
auth_config: self.auth_config.clone(),
retry_config: self.retry_config.clone(),
}
}
}
#[cfg(not(feature = "ibm"))]
#[derive(Clone)]
pub struct IBMQuantumClient;
#[cfg(feature = "ibm")]
impl IBMQuantumClient {
pub fn new(token: &str) -> DeviceResult<Self> {
let mut headers = header::HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
);
let client = Client::builder()
.default_headers(headers)
.timeout(Duration::from_secs(30))
.build()
.map_err(|e| DeviceError::Connection(e.to_string()))?;
let token_info = TokenInfo {
access_token: token.to_string(),
obtained_at: Instant::now(),
valid_for_secs: DEFAULT_TOKEN_VALIDITY_SECS,
};
Ok(Self {
client,
api_url: IBM_QUANTUM_API_URL.to_string(),
auth_url: IBM_AUTH_URL.to_string(),
token_info: Arc::new(RwLock::new(token_info)),
auth_config: IBMAuthConfig {
api_key: String::new(), auto_refresh: false,
token_validity_secs: None,
},
retry_config: IBMRetryConfig::default(),
})
}
pub async fn new_with_api_key(api_key: &str) -> DeviceResult<Self> {
Self::new_with_config(IBMAuthConfig {
api_key: api_key.to_string(),
auto_refresh: true,
token_validity_secs: None,
})
.await
}
pub async fn new_with_config(config: IBMAuthConfig) -> DeviceResult<Self> {
Self::new_with_config_and_retry(config, IBMRetryConfig::default()).await
}
pub async fn new_with_config_and_retry(
config: IBMAuthConfig,
retry_config: IBMRetryConfig,
) -> DeviceResult<Self> {
let mut headers = header::HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
);
let client = Client::builder()
.default_headers(headers)
.timeout(Duration::from_secs(30))
.build()
.map_err(|e| DeviceError::Connection(e.to_string()))?;
let token_info = Self::exchange_api_key_for_token(&client, &config.api_key).await?;
Ok(Self {
client,
api_url: IBM_QUANTUM_API_URL.to_string(),
auth_url: IBM_AUTH_URL.to_string(),
token_info: Arc::new(RwLock::new(token_info)),
auth_config: config,
retry_config,
})
}
pub const fn set_retry_config(&mut self, config: IBMRetryConfig) {
self.retry_config = config;
}
pub const fn retry_config(&self) -> &IBMRetryConfig {
&self.retry_config
}
async fn with_retry<F, Fut, T>(&self, operation: F) -> DeviceResult<T>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = DeviceResult<T>>,
{
use scirs2_core::random::prelude::*;
let mut attempt = 0;
let mut delay = self.retry_config.initial_delay;
loop {
match operation().await {
Ok(result) => return Ok(result),
Err(err) => {
attempt += 1;
let is_retryable = match &err {
DeviceError::Connection(_) | DeviceError::Timeout(_) => true,
DeviceError::APIError(msg) => {
msg.contains("rate") || msg.contains('5') || msg.contains("503")
}
_ => false,
};
if !is_retryable || attempt >= self.retry_config.max_attempts {
return Err(err);
}
let jitter = if self.retry_config.jitter_factor > 0.0 {
let mut rng = thread_rng();
let jitter_range =
delay.as_millis() as f64 * self.retry_config.jitter_factor;
Duration::from_millis((rng.random::<f64>() * jitter_range) as u64)
} else {
Duration::ZERO
};
let actual_delay = delay + jitter;
tokio::time::sleep(actual_delay).await;
delay = Duration::from_millis(
(delay.as_millis() as f64 * self.retry_config.backoff_multiplier) as u64,
)
.min(self.retry_config.max_delay);
}
}
}
}
async fn exchange_api_key_for_token(client: &Client, api_key: &str) -> DeviceResult<TokenInfo> {
let response = client
.post(format!("{IBM_AUTH_URL}/users/loginWithToken"))
.json(&serde_json::json!({ "apiToken": api_key }))
.send()
.await
.map_err(|e| DeviceError::Connection(format!("Authentication request failed: {e}")))?;
if !response.status().is_success() {
let error_msg = response
.text()
.await
.unwrap_or_else(|_| "Unknown authentication error".to_string());
return Err(DeviceError::Authentication(error_msg));
}
let auth_response: AuthResponse = response.json().await.map_err(|e| {
DeviceError::Deserialization(format!("Failed to parse auth response: {e}"))
})?;
let valid_for_secs = auth_response.ttl.unwrap_or(DEFAULT_TOKEN_VALIDITY_SECS);
Ok(TokenInfo {
access_token: auth_response.id,
obtained_at: Instant::now(),
valid_for_secs,
})
}
pub async fn refresh_token(&self) -> DeviceResult<()> {
if self.auth_config.api_key.is_empty() {
return Err(DeviceError::Authentication(
"Cannot refresh token: no API key configured. Use new_with_api_key() for auto-refresh support.".to_string()
));
}
let new_token_info =
Self::exchange_api_key_for_token(&self.client, &self.auth_config.api_key).await?;
let mut token_guard = self.token_info.write().await;
*token_guard = new_token_info;
Ok(())
}
async fn get_valid_token(&self) -> DeviceResult<String> {
let needs_refresh = {
let token_guard = self.token_info.read().await;
token_guard.is_expired()
};
if needs_refresh && self.auth_config.auto_refresh {
self.refresh_token().await?;
}
let token_guard = self.token_info.read().await;
if token_guard.is_expired() && !self.auth_config.auto_refresh {
}
Ok(token_guard.access_token.clone())
}
pub async fn is_token_valid(&self) -> bool {
let token_guard = self.token_info.read().await;
!token_guard.is_expired()
}
pub async fn token_info(&self) -> TokenInfo {
let token_guard = self.token_info.read().await;
token_guard.clone()
}
pub async fn list_backends_with_retry(&self) -> DeviceResult<Vec<IBMBackend>> {
self.with_retry(|| async { self.list_backends().await })
.await
}
pub async fn list_backends(&self) -> DeviceResult<Vec<IBMBackend>> {
let token = self.get_valid_token().await?;
let response = self
.client
.get(format!("{}/backends", self.api_url))
.header("Authorization", format!("Bearer {token}"))
.send()
.await
.map_err(|e| DeviceError::Connection(e.to_string()))?;
if !response.status().is_success() {
let error_msg = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(DeviceError::APIError(error_msg));
}
let backends: Vec<IBMBackend> = response
.json()
.await
.map_err(|e| DeviceError::Deserialization(e.to_string()))?;
Ok(backends)
}
pub async fn get_backend(&self, backend_name: &str) -> DeviceResult<IBMBackend> {
let token = self.get_valid_token().await?;
let response = self
.client
.get(format!("{}/backends/{}", self.api_url, backend_name))
.header("Authorization", format!("Bearer {token}"))
.send()
.await
.map_err(|e| DeviceError::Connection(e.to_string()))?;
if !response.status().is_success() {
let error_msg = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(DeviceError::APIError(error_msg));
}
let backend: IBMBackend = response
.json()
.await
.map_err(|e| DeviceError::Deserialization(e.to_string()))?;
Ok(backend)
}
pub async fn submit_circuit(
&self,
backend_name: &str,
config: IBMCircuitConfig,
) -> DeviceResult<String> {
#[cfg(feature = "ibm")]
{
use serde_json::json;
let token = self.get_valid_token().await?;
let payload = json!({
"backend": backend_name,
"name": config.name,
"qasm": config.qasm,
"shots": config.shots,
"optimization_level": config.optimization_level.unwrap_or(1),
"initial_layout": config.initial_layout.unwrap_or_default(),
});
let response = self
.client
.post(format!("{}/jobs", self.api_url))
.header("Authorization", format!("Bearer {token}"))
.json(&payload)
.send()
.await
.map_err(|e| DeviceError::Connection(e.to_string()))?;
if !response.status().is_success() {
let error_msg = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(DeviceError::JobSubmission(error_msg));
}
let job_response: IBMJobResponse = response
.json()
.await
.map_err(|e| DeviceError::Deserialization(e.to_string()))?;
Ok(job_response.id)
}
#[cfg(not(feature = "ibm"))]
Err(DeviceError::UnsupportedDevice(
"IBM Quantum support not enabled".to_string(),
))
}
pub async fn get_job_status(&self, job_id: &str) -> DeviceResult<IBMJobStatus> {
let token = self.get_valid_token().await?;
let response = self
.client
.get(format!("{}/jobs/{}", self.api_url, job_id))
.header("Authorization", format!("Bearer {token}"))
.send()
.await
.map_err(|e| DeviceError::Connection(e.to_string()))?;
if !response.status().is_success() {
let error_msg = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(DeviceError::APIError(error_msg));
}
let job: IBMJobResponse = response
.json()
.await
.map_err(|e| DeviceError::Deserialization(e.to_string()))?;
Ok(job.status)
}
pub async fn get_job_result(&self, job_id: &str) -> DeviceResult<IBMJobResult> {
let token = self.get_valid_token().await?;
let response = self
.client
.get(format!("{}/jobs/{}/result", self.api_url, job_id))
.header("Authorization", format!("Bearer {token}"))
.send()
.await
.map_err(|e| DeviceError::Connection(e.to_string()))?;
if !response.status().is_success() {
let error_msg = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(DeviceError::APIError(error_msg));
}
let result: IBMJobResult = response
.json()
.await
.map_err(|e| DeviceError::Deserialization(e.to_string()))?;
Ok(result)
}
pub async fn wait_for_job(
&self,
job_id: &str,
timeout_secs: Option<u64>,
) -> DeviceResult<IBMJobResult> {
let timeout = timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS);
let mut elapsed = 0;
let interval = 5;
while elapsed < timeout {
let status = self.get_job_status(job_id).await?;
match status {
IBMJobStatus::Completed => {
return self.get_job_result(job_id).await;
}
IBMJobStatus::Error => {
return Err(DeviceError::JobExecution(format!(
"Job {job_id} encountered an error"
)));
}
IBMJobStatus::Cancelled => {
return Err(DeviceError::JobExecution(format!(
"Job {job_id} was cancelled"
)));
}
_ => {
sleep(Duration::from_secs(interval));
elapsed += interval;
}
}
}
Err(DeviceError::Timeout(format!(
"Timed out waiting for job {job_id} to complete"
)))
}
pub async fn submit_circuits_parallel(
&self,
backend_name: &str,
configs: Vec<IBMCircuitConfig>,
) -> DeviceResult<Vec<String>> {
#[cfg(feature = "ibm")]
{
use tokio::task;
let client = Arc::new(self.clone());
let mut handles = vec![];
for config in configs {
let client_clone = client.clone();
let backend_name = backend_name.to_string();
let handle =
task::spawn(
async move { client_clone.submit_circuit(&backend_name, config).await },
);
handles.push(handle);
}
let mut job_ids = vec![];
for handle in handles {
match handle.await {
Ok(result) => match result {
Ok(job_id) => job_ids.push(job_id),
Err(e) => return Err(e),
},
Err(e) => {
return Err(DeviceError::JobSubmission(format!(
"Failed to join task: {e}"
)));
}
}
}
Ok(job_ids)
}
#[cfg(not(feature = "ibm"))]
Err(DeviceError::UnsupportedDevice(
"IBM Quantum support not enabled".to_string(),
))
}
pub fn circuit_to_qasm<const N: usize>(
_circuit: &Circuit<N>,
_initial_layout: Option<std::collections::HashMap<String, usize>>,
) -> DeviceResult<String> {
let mut qasm = String::from("OPENQASM 2.0;\ninclude \"qelib1.inc\";\n\n");
use std::fmt::Write;
writeln!(qasm, "qreg q[{N}];")
.map_err(|e| DeviceError::CircuitConversion(format!("Failed to write QASM: {e}")))?;
writeln!(qasm, "creg c[{N}];")
.map_err(|e| DeviceError::CircuitConversion(format!("Failed to write QASM: {e}")))?;
Ok(qasm)
}
}
#[cfg(not(feature = "ibm"))]
impl IBMQuantumClient {
pub fn new(_token: &str) -> DeviceResult<Self> {
Err(DeviceError::UnsupportedDevice(
"IBM Quantum support not enabled. Recompile with the 'ibm' feature.".to_string(),
))
}
pub async fn list_backends(&self) -> DeviceResult<Vec<IBMBackend>> {
Err(DeviceError::UnsupportedDevice(
"IBM Quantum support not enabled".to_string(),
))
}
pub async fn get_backend(&self, _backend_name: &str) -> DeviceResult<IBMBackend> {
Err(DeviceError::UnsupportedDevice(
"IBM Quantum support not enabled".to_string(),
))
}
pub async fn submit_circuit(
&self,
_backend_name: &str,
_config: IBMCircuitConfig,
) -> DeviceResult<String> {
Err(DeviceError::UnsupportedDevice(
"IBM Quantum support not enabled".to_string(),
))
}
pub async fn get_job_status(&self, _job_id: &str) -> DeviceResult<IBMJobStatus> {
Err(DeviceError::UnsupportedDevice(
"IBM Quantum support not enabled".to_string(),
))
}
pub async fn get_job_result(&self, _job_id: &str) -> DeviceResult<IBMJobResult> {
Err(DeviceError::UnsupportedDevice(
"IBM Quantum support not enabled".to_string(),
))
}
pub async fn wait_for_job(
&self,
_job_id: &str,
_timeout_secs: Option<u64>,
) -> DeviceResult<IBMJobResult> {
Err(DeviceError::UnsupportedDevice(
"IBM Quantum support not enabled".to_string(),
))
}
pub async fn submit_circuits_parallel(
&self,
_backend_name: &str,
_configs: Vec<IBMCircuitConfig>,
) -> DeviceResult<Vec<String>> {
Err(DeviceError::UnsupportedDevice(
"IBM Quantum support not enabled".to_string(),
))
}
pub fn circuit_to_qasm<const N: usize>(
_circuit: &Circuit<N>,
_initial_layout: Option<std::collections::HashMap<String, usize>>,
) -> DeviceResult<String> {
Err(DeviceError::UnsupportedDevice(
"IBM Quantum support not enabled".to_string(),
))
}
}