use scirs2_core::ndarray::{Array, Ix2};
use scirs2_core::random::{thread_rng, Rng, RngExt};
use std::collections::HashMap;
use quantrs2_anneal::QuboModel;
use super::super::{SampleResult, Sampler, SamplerError, SamplerResult};
#[derive(Debug, Clone)]
pub enum IBMBackend {
Simulator,
Hardware(String),
AnyHardware,
}
#[derive(Debug, Clone)]
pub struct IBMQuantumConfig {
pub api_token: String,
pub backend: IBMBackend,
pub max_circuit_depth: usize,
pub optimization_level: u8,
pub shots: usize,
pub error_mitigation: bool,
}
impl Default for IBMQuantumConfig {
fn default() -> Self {
Self {
api_token: String::new(),
backend: IBMBackend::Simulator,
max_circuit_depth: 100,
optimization_level: 1,
shots: 1024,
error_mitigation: true,
}
}
}
pub struct IBMQuantumSampler {
config: IBMQuantumConfig,
}
impl IBMQuantumSampler {
#[must_use]
pub const fn new(config: IBMQuantumConfig) -> Self {
Self { config }
}
#[must_use]
pub fn with_token(api_token: &str) -> Self {
Self {
config: IBMQuantumConfig {
api_token: api_token.to_string(),
..Default::default()
},
}
}
#[must_use]
pub fn with_backend(mut self, backend: IBMBackend) -> Self {
self.config.backend = backend;
self
}
#[must_use]
pub const fn with_error_mitigation(mut self, enabled: bool) -> Self {
self.config.error_mitigation = enabled;
self
}
#[must_use]
pub fn with_optimization_level(mut self, level: u8) -> Self {
self.config.optimization_level = level.min(3);
self
}
}
impl Sampler for IBMQuantumSampler {
fn run_qubo(
&self,
qubo: &(Array<f64, Ix2>, HashMap<String, usize>),
shots: usize,
) -> SamplerResult<Vec<SampleResult>> {
let (matrix, var_map) = qubo;
let n_vars = var_map.len();
if n_vars > 127 {
return Err(SamplerError::InvalidParameter(
"IBM Quantum currently supports up to 127 qubits".to_string(),
));
}
let idx_to_var: HashMap<usize, String> = var_map
.iter()
.map(|(var, &idx)| (idx, var.clone()))
.collect();
let mut qubo_model = QuboModel::new(n_vars);
for i in 0..n_vars {
if matrix[[i, i]] != 0.0 {
qubo_model.set_linear(i, matrix[[i, i]])?;
}
for j in (i + 1)..n_vars {
if matrix[[i, j]] != 0.0 {
qubo_model.set_quadratic(i, j, matrix[[i, j]])?;
}
}
}
#[cfg(feature = "ibm_quantum")]
{
if self.config.api_token.is_empty() {
return Err(SamplerError::ApiError(
"IBM Quantum API token not configured. Use with_token() to provide credentials.".to_string(),
));
}
let mut operator_terms: Vec<serde_json::Value> = Vec::new();
for i in 0..n_vars {
if matrix[[i, i]] != 0.0 {
operator_terms.push(serde_json::json!({
"coeff": matrix[[i, i]],
"pauli": format!("{}Z{}", "I".repeat(i), "I".repeat(n_vars - i - 1))
}));
}
for j in (i + 1)..n_vars {
if matrix[[i, j]] != 0.0 {
let mut pauli = "I".repeat(n_vars);
let mut pauli_chars: Vec<char> = pauli.chars().collect();
pauli_chars[i] = 'Z';
pauli_chars[j] = 'Z';
pauli = pauli_chars.iter().collect();
operator_terms.push(serde_json::json!({
"coeff": matrix[[i, j]],
"pauli": pauli
}));
}
}
}
let backend_name = match &self.config.backend {
IBMBackend::Simulator => "ibmq_qasm_simulator",
IBMBackend::Hardware(name) => name.as_str(),
IBMBackend::AnyHardware => "ibmq_manila",
};
let payload = serde_json::json!({
"backend": {"name": backend_name},
"header": {"backend_name": backend_name},
"config": {
"shots": shots,
"optimization_level": self.config.optimization_level,
"error_mitigation": self.config.error_mitigation,
"max_credits": 10
},
"experiments": [{
"header": {
"n_qubits": n_vars,
"name": "qubo_qaoa"
},
"qubo_operator": operator_terms
}]
});
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| SamplerError::ApiError(format!("Failed to build HTTP client: {e}")))?;
let jobs_endpoint = "https://api.quantum-computing.ibm.com/runtime/jobs";
let response = client
.post(jobs_endpoint)
.header("Authorization", format!("Bearer {}", self.config.api_token))
.header("Content-Type", "application/json")
.json(&payload)
.send()
.map_err(|e| {
SamplerError::ApiError(format!(
"Failed to submit IBM Quantum job: {e}. \
Ensure API token is valid and network is accessible."
))
})?;
if !response.status().is_success() {
let status = response.status();
let body = response
.text()
.unwrap_or_else(|_| "<unreadable>".to_string());
return Err(SamplerError::ApiError(format!(
"IBM Quantum job submission failed (HTTP {status}): {body}"
)));
}
let job_response: serde_json::Value = response.json().map_err(|e| {
SamplerError::ApiError(format!("Failed to parse IBM Quantum response: {e}"))
})?;
let job_id = job_response["id"]
.as_str()
.ok_or_else(|| {
SamplerError::ApiError("Missing job ID in IBM Quantum response".to_string())
})?
.to_string();
let max_polls = 720u64; let mut poll_count = 0u64;
loop {
if poll_count >= max_polls {
return Err(SamplerError::ApiError(format!(
"IBM Quantum job {job_id} timed out after {max_polls} polls"
)));
}
poll_count += 1;
std::thread::sleep(std::time::Duration::from_secs(5));
let status_url = format!("{jobs_endpoint}/{job_id}");
let status_resp = client
.get(&status_url)
.header("Authorization", format!("Bearer {}", self.config.api_token))
.send()
.map_err(|e| {
SamplerError::ApiError(format!("Failed to poll job status: {e}"))
})?;
let status_json: serde_json::Value = status_resp.json().map_err(|e| {
SamplerError::ApiError(format!("Failed to parse status response: {e}"))
})?;
match status_json["status"].as_str() {
Some("Completed") | Some("DONE") => break,
Some("Failed") | Some("ERROR") => {
let reason = status_json["error_message"]
.as_str()
.unwrap_or("unknown reason");
return Err(SamplerError::ApiError(format!(
"IBM Quantum job failed: {reason}"
)));
}
Some("Cancelled") | Some("CANCELLED") => {
return Err(SamplerError::ApiError(
"IBM Quantum job was cancelled".to_string(),
));
}
_ => continue,
}
}
let result_url = format!("{jobs_endpoint}/{job_id}/results");
let result_resp = client
.get(&result_url)
.header("Authorization", format!("Bearer {}", self.config.api_token))
.send()
.map_err(|e| SamplerError::ApiError(format!("Failed to retrieve results: {e}")))?;
let result_json: serde_json::Value = result_resp.json().map_err(|e| {
SamplerError::ApiError(format!("Failed to parse result response: {e}"))
})?;
if let Some(counts_map) = result_json["results"][0]["data"]["counts"].as_object() {
let mut parsed_results: Vec<SampleResult> = Vec::with_capacity(counts_map.len());
for (bitstring, count_val) in counts_map {
let occurrences = count_val.as_u64().unwrap_or(1) as usize;
let assignments: HashMap<String, bool> = bitstring
.chars()
.rev()
.enumerate()
.filter_map(|(bit_idx, ch)| {
idx_to_var
.get(&bit_idx)
.map(|name| (name.clone(), ch == '1'))
})
.collect();
let mut energy = 0.0f64;
for (var_name, &val) in &assignments {
if val {
let i = var_map[var_name];
energy += matrix[[i, i]];
for (other_var, &other_val) in &assignments {
let j = var_map[other_var];
if i < j && other_val {
energy += matrix[[i, j]];
}
}
}
}
parsed_results.push(SampleResult {
assignments,
energy,
occurrences,
});
}
parsed_results.sort_by(|a, b| {
a.energy
.partial_cmp(&b.energy)
.unwrap_or(std::cmp::Ordering::Equal)
});
return Ok(parsed_results);
}
}
let mut results = Vec::new();
let mut rng = thread_rng();
let effective_shots = if self.config.error_mitigation {
shots * 2 } else {
shots
};
let unique_solutions = (effective_shots / 10).max(1).min(100);
for _ in 0..unique_solutions {
let assignments: HashMap<String, bool> = idx_to_var
.values()
.map(|name| (name.clone(), rng.random::<bool>()))
.collect();
let mut energy = 0.0;
for (var_name, &val) in &assignments {
let i = var_map[var_name];
if val {
energy += matrix[[i, i]];
for (other_var, &other_val) in &assignments {
let j = var_map[other_var];
if i < j && other_val {
energy += matrix[[i, j]];
}
}
}
}
let occurrences = rng.random_range(1..=(effective_shots / unique_solutions + 10));
results.push(SampleResult {
assignments,
energy,
occurrences,
});
}
results.sort_by(|a, b| {
a.energy
.partial_cmp(&b.energy)
.unwrap_or(std::cmp::Ordering::Equal)
});
Ok(results)
}
fn run_hobo(
&self,
hobo: &(
Array<f64, scirs2_core::ndarray::IxDyn>,
HashMap<String, usize>,
),
shots: usize,
) -> SamplerResult<Vec<SampleResult>> {
use scirs2_core::ndarray::Ix2;
if hobo.0.ndim() <= 2 {
let qubo_matrix = hobo.0.clone().into_dimensionality::<Ix2>().map_err(|e| {
SamplerError::InvalidParameter(format!(
"Failed to convert HOBO to QUBO dimensionality: {e}"
))
})?;
let qubo = (qubo_matrix, hobo.1.clone());
self.run_qubo(&qubo, shots)
} else {
Err(SamplerError::InvalidParameter(
"IBM Quantum doesn't support HOBO problems directly. Use a quadratization technique first.".to_string()
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ibm_quantum_config() {
let config = IBMQuantumConfig::default();
assert_eq!(config.optimization_level, 1);
assert_eq!(config.shots, 1024);
assert!(config.error_mitigation);
}
#[test]
fn test_ibm_quantum_sampler_creation() {
let sampler = IBMQuantumSampler::with_token("test_token")
.with_backend(IBMBackend::Simulator)
.with_error_mitigation(true)
.with_optimization_level(2);
assert_eq!(sampler.config.api_token, "test_token");
assert_eq!(sampler.config.optimization_level, 2);
assert!(sampler.config.error_mitigation);
}
#[test]
fn test_ibm_quantum_backend_types() {
let simulator = IBMBackend::Simulator;
let hardware = IBMBackend::Hardware("ibmq_lima".to_string());
let any = IBMBackend::AnyHardware;
let _sim_clone = simulator;
let _hw_clone = hardware;
let _any_clone = any;
}
}