quantrs2_tytan/sampler/hardware/
ibm_quantum.rs1use scirs2_core::ndarray::{Array, Ix2};
7use scirs2_core::random::{thread_rng, Rng, RngExt};
8use std::collections::HashMap;
9
10use quantrs2_anneal::QuboModel;
11
12use super::super::{SampleResult, Sampler, SamplerError, SamplerResult};
13
14#[derive(Debug, Clone)]
16pub enum IBMBackend {
17 Simulator,
19 Hardware(String),
21 AnyHardware,
23}
24
25#[derive(Debug, Clone)]
27pub struct IBMQuantumConfig {
28 pub api_token: String,
30 pub backend: IBMBackend,
32 pub max_circuit_depth: usize,
34 pub optimization_level: u8,
36 pub shots: usize,
38 pub error_mitigation: bool,
40}
41
42impl Default for IBMQuantumConfig {
43 fn default() -> Self {
44 Self {
45 api_token: String::new(),
46 backend: IBMBackend::Simulator,
47 max_circuit_depth: 100,
48 optimization_level: 1,
49 shots: 1024,
50 error_mitigation: true,
51 }
52 }
53}
54
55pub struct IBMQuantumSampler {
60 config: IBMQuantumConfig,
61}
62
63impl IBMQuantumSampler {
64 #[must_use]
70 pub const fn new(config: IBMQuantumConfig) -> Self {
71 Self { config }
72 }
73
74 #[must_use]
80 pub fn with_token(api_token: &str) -> Self {
81 Self {
82 config: IBMQuantumConfig {
83 api_token: api_token.to_string(),
84 ..Default::default()
85 },
86 }
87 }
88
89 #[must_use]
91 pub fn with_backend(mut self, backend: IBMBackend) -> Self {
92 self.config.backend = backend;
93 self
94 }
95
96 #[must_use]
98 pub const fn with_error_mitigation(mut self, enabled: bool) -> Self {
99 self.config.error_mitigation = enabled;
100 self
101 }
102
103 #[must_use]
105 pub fn with_optimization_level(mut self, level: u8) -> Self {
106 self.config.optimization_level = level.min(3);
107 self
108 }
109}
110
111impl Sampler for IBMQuantumSampler {
112 fn run_qubo(
113 &self,
114 qubo: &(Array<f64, Ix2>, HashMap<String, usize>),
115 shots: usize,
116 ) -> SamplerResult<Vec<SampleResult>> {
117 let (matrix, var_map) = qubo;
119
120 let n_vars = var_map.len();
122
123 if n_vars > 127 {
125 return Err(SamplerError::InvalidParameter(
126 "IBM Quantum currently supports up to 127 qubits".to_string(),
127 ));
128 }
129
130 let idx_to_var: HashMap<usize, String> = var_map
132 .iter()
133 .map(|(var, &idx)| (idx, var.clone()))
134 .collect();
135
136 let mut qubo_model = QuboModel::new(n_vars);
138
139 for i in 0..n_vars {
141 if matrix[[i, i]] != 0.0 {
142 qubo_model.set_linear(i, matrix[[i, i]])?;
143 }
144
145 for j in (i + 1)..n_vars {
146 if matrix[[i, j]] != 0.0 {
147 qubo_model.set_quadratic(i, j, matrix[[i, j]])?;
148 }
149 }
150 }
151
152 #[cfg(feature = "ibm_quantum")]
154 {
155 if self.config.api_token.is_empty() {
157 return Err(SamplerError::ApiError(
158 "IBM Quantum API token not configured. Use with_token() to provide credentials.".to_string(),
159 ));
160 }
161
162 let mut operator_terms: Vec<serde_json::Value> = Vec::new();
164 for i in 0..n_vars {
165 if matrix[[i, i]] != 0.0 {
166 operator_terms.push(serde_json::json!({
167 "coeff": matrix[[i, i]],
168 "pauli": format!("{}Z{}", "I".repeat(i), "I".repeat(n_vars - i - 1))
169 }));
170 }
171 for j in (i + 1)..n_vars {
172 if matrix[[i, j]] != 0.0 {
173 let mut pauli = "I".repeat(n_vars);
175 let mut pauli_chars: Vec<char> = pauli.chars().collect();
176 pauli_chars[i] = 'Z';
177 pauli_chars[j] = 'Z';
178 pauli = pauli_chars.iter().collect();
179 operator_terms.push(serde_json::json!({
180 "coeff": matrix[[i, j]],
181 "pauli": pauli
182 }));
183 }
184 }
185 }
186
187 let backend_name = match &self.config.backend {
188 IBMBackend::Simulator => "ibmq_qasm_simulator",
189 IBMBackend::Hardware(name) => name.as_str(),
190 IBMBackend::AnyHardware => "ibmq_manila",
191 };
192
193 let payload = serde_json::json!({
194 "backend": {"name": backend_name},
195 "header": {"backend_name": backend_name},
196 "config": {
197 "shots": shots,
198 "optimization_level": self.config.optimization_level,
199 "error_mitigation": self.config.error_mitigation,
200 "max_credits": 10
201 },
202 "experiments": [{
203 "header": {
204 "n_qubits": n_vars,
205 "name": "qubo_qaoa"
206 },
207 "qubo_operator": operator_terms
208 }]
209 });
210
211 let client = reqwest::blocking::Client::builder()
213 .timeout(std::time::Duration::from_secs(30))
214 .build()
215 .map_err(|e| SamplerError::ApiError(format!("Failed to build HTTP client: {e}")))?;
216
217 let jobs_endpoint = "https://api.quantum-computing.ibm.com/runtime/jobs";
218
219 let response = client
220 .post(jobs_endpoint)
221 .header("Authorization", format!("Bearer {}", self.config.api_token))
222 .header("Content-Type", "application/json")
223 .json(&payload)
224 .send()
225 .map_err(|e| {
226 SamplerError::ApiError(format!(
227 "Failed to submit IBM Quantum job: {e}. \
228 Ensure API token is valid and network is accessible."
229 ))
230 })?;
231
232 if !response.status().is_success() {
233 let status = response.status();
234 let body = response
235 .text()
236 .unwrap_or_else(|_| "<unreadable>".to_string());
237 return Err(SamplerError::ApiError(format!(
238 "IBM Quantum job submission failed (HTTP {status}): {body}"
239 )));
240 }
241
242 let job_response: serde_json::Value = response.json().map_err(|e| {
243 SamplerError::ApiError(format!("Failed to parse IBM Quantum response: {e}"))
244 })?;
245
246 let job_id = job_response["id"]
247 .as_str()
248 .ok_or_else(|| {
249 SamplerError::ApiError("Missing job ID in IBM Quantum response".to_string())
250 })?
251 .to_string();
252
253 let max_polls = 720u64; let mut poll_count = 0u64;
256 loop {
257 if poll_count >= max_polls {
258 return Err(SamplerError::ApiError(format!(
259 "IBM Quantum job {job_id} timed out after {max_polls} polls"
260 )));
261 }
262 poll_count += 1;
263 std::thread::sleep(std::time::Duration::from_secs(5));
264
265 let status_url = format!("{jobs_endpoint}/{job_id}");
266 let status_resp = client
267 .get(&status_url)
268 .header("Authorization", format!("Bearer {}", self.config.api_token))
269 .send()
270 .map_err(|e| {
271 SamplerError::ApiError(format!("Failed to poll job status: {e}"))
272 })?;
273
274 let status_json: serde_json::Value = status_resp.json().map_err(|e| {
275 SamplerError::ApiError(format!("Failed to parse status response: {e}"))
276 })?;
277
278 match status_json["status"].as_str() {
279 Some("Completed") | Some("DONE") => break,
280 Some("Failed") | Some("ERROR") => {
281 let reason = status_json["error_message"]
282 .as_str()
283 .unwrap_or("unknown reason");
284 return Err(SamplerError::ApiError(format!(
285 "IBM Quantum job failed: {reason}"
286 )));
287 }
288 Some("Cancelled") | Some("CANCELLED") => {
289 return Err(SamplerError::ApiError(
290 "IBM Quantum job was cancelled".to_string(),
291 ));
292 }
293 _ => continue,
294 }
295 }
296
297 let result_url = format!("{jobs_endpoint}/{job_id}/results");
299 let result_resp = client
300 .get(&result_url)
301 .header("Authorization", format!("Bearer {}", self.config.api_token))
302 .send()
303 .map_err(|e| SamplerError::ApiError(format!("Failed to retrieve results: {e}")))?;
304
305 let result_json: serde_json::Value = result_resp.json().map_err(|e| {
306 SamplerError::ApiError(format!("Failed to parse result response: {e}"))
307 })?;
308
309 if let Some(counts_map) = result_json["results"][0]["data"]["counts"].as_object() {
311 let mut parsed_results: Vec<SampleResult> = Vec::with_capacity(counts_map.len());
312 for (bitstring, count_val) in counts_map {
313 let occurrences = count_val.as_u64().unwrap_or(1) as usize;
314 let assignments: HashMap<String, bool> = bitstring
315 .chars()
316 .rev()
317 .enumerate()
318 .filter_map(|(bit_idx, ch)| {
319 idx_to_var
320 .get(&bit_idx)
321 .map(|name| (name.clone(), ch == '1'))
322 })
323 .collect();
324
325 let mut energy = 0.0f64;
326 for (var_name, &val) in &assignments {
327 if val {
328 let i = var_map[var_name];
329 energy += matrix[[i, i]];
330 for (other_var, &other_val) in &assignments {
331 let j = var_map[other_var];
332 if i < j && other_val {
333 energy += matrix[[i, j]];
334 }
335 }
336 }
337 }
338
339 parsed_results.push(SampleResult {
340 assignments,
341 energy,
342 occurrences,
343 });
344 }
345
346 parsed_results.sort_by(|a, b| {
347 a.energy
348 .partial_cmp(&b.energy)
349 .unwrap_or(std::cmp::Ordering::Equal)
350 });
351
352 return Ok(parsed_results);
353 }
354 }
356
357 let mut results = Vec::new();
359 let mut rng = thread_rng();
360
361 let effective_shots = if self.config.error_mitigation {
363 shots * 2 } else {
365 shots
366 };
367
368 let unique_solutions = (effective_shots / 10).max(1).min(100);
370
371 for _ in 0..unique_solutions {
372 let assignments: HashMap<String, bool> = idx_to_var
373 .values()
374 .map(|name| (name.clone(), rng.random::<bool>()))
375 .collect();
376
377 let mut energy = 0.0;
379 for (var_name, &val) in &assignments {
380 let i = var_map[var_name];
381 if val {
382 energy += matrix[[i, i]];
383 for (other_var, &other_val) in &assignments {
384 let j = var_map[other_var];
385 if i < j && other_val {
386 energy += matrix[[i, j]];
387 }
388 }
389 }
390 }
391
392 let occurrences = rng.random_range(1..=(effective_shots / unique_solutions + 10));
394
395 results.push(SampleResult {
396 assignments,
397 energy,
398 occurrences,
399 });
400 }
401
402 results.sort_by(|a, b| {
404 a.energy
405 .partial_cmp(&b.energy)
406 .unwrap_or(std::cmp::Ordering::Equal)
407 });
408
409 Ok(results)
410 }
411
412 fn run_hobo(
413 &self,
414 hobo: &(
415 Array<f64, scirs2_core::ndarray::IxDyn>,
416 HashMap<String, usize>,
417 ),
418 shots: usize,
419 ) -> SamplerResult<Vec<SampleResult>> {
420 use scirs2_core::ndarray::Ix2;
421
422 if hobo.0.ndim() <= 2 {
424 let qubo_matrix = hobo.0.clone().into_dimensionality::<Ix2>().map_err(|e| {
426 SamplerError::InvalidParameter(format!(
427 "Failed to convert HOBO to QUBO dimensionality: {e}"
428 ))
429 })?;
430 let qubo = (qubo_matrix, hobo.1.clone());
431 self.run_qubo(&qubo, shots)
432 } else {
433 Err(SamplerError::InvalidParameter(
435 "IBM Quantum doesn't support HOBO problems directly. Use a quadratization technique first.".to_string()
436 ))
437 }
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn test_ibm_quantum_config() {
447 let config = IBMQuantumConfig::default();
448 assert_eq!(config.optimization_level, 1);
449 assert_eq!(config.shots, 1024);
450 assert!(config.error_mitigation);
451 }
452
453 #[test]
454 fn test_ibm_quantum_sampler_creation() {
455 let sampler = IBMQuantumSampler::with_token("test_token")
456 .with_backend(IBMBackend::Simulator)
457 .with_error_mitigation(true)
458 .with_optimization_level(2);
459
460 assert_eq!(sampler.config.api_token, "test_token");
461 assert_eq!(sampler.config.optimization_level, 2);
462 assert!(sampler.config.error_mitigation);
463 }
464
465 #[test]
466 fn test_ibm_quantum_backend_types() {
467 let simulator = IBMBackend::Simulator;
468 let hardware = IBMBackend::Hardware("ibmq_lima".to_string());
469 let any = IBMBackend::AnyHardware;
470
471 let _sim_clone = simulator;
473 let _hw_clone = hardware;
474 let _any_clone = any;
475 }
476}