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