1use std::collections::HashMap;
18use std::sync::{Arc, Mutex};
19use std::time::Duration;
20
21#[derive(Debug, Clone)]
25pub struct MockBackendConfig {
26 pub name: String,
28 pub max_qubits: usize,
30 pub max_shots: usize,
32 pub latency_ms: u64,
34 pub error_rate: f64,
36 pub fail_rate: f64,
38 pub gate_set: Vec<String>,
40 pub connectivity: Vec<(usize, usize)>,
42 pub rng_seed: u64,
44}
45
46impl Default for MockBackendConfig {
47 fn default() -> Self {
48 Self {
49 name: "mock_backend".to_string(),
50 max_qubits: 32,
51 max_shots: 8192,
52 latency_ms: 0,
53 error_rate: 0.0,
54 fail_rate: 0.0,
55 gate_set: vec![],
56 connectivity: vec![],
57 rng_seed: 42,
58 }
59 }
60}
61
62impl MockBackendConfig {
63 pub fn perfect(n_qubits: usize) -> Self {
65 Self {
66 max_qubits: n_qubits,
67 ..Default::default()
68 }
69 }
70
71 pub fn ibm_nairobi_like() -> Self {
73 Self {
74 name: "mock_ibm_nairobi".to_string(),
75 max_qubits: 7,
76 max_shots: 4096,
77 latency_ms: 100,
78 error_rate: 0.001,
79 fail_rate: 0.0,
80 gate_set: vec![
81 "cx".to_string(),
82 "rz".to_string(),
83 "sx".to_string(),
84 "x".to_string(),
85 ],
86 connectivity: vec![(0, 1), (1, 2), (1, 3), (3, 5), (4, 5), (5, 6)],
87 rng_seed: 0,
88 }
89 }
90
91 pub fn always_fails() -> Self {
93 Self {
94 name: "failing_backend".to_string(),
95 fail_rate: 1.0,
96 ..Default::default()
97 }
98 }
99
100 pub fn with_noise(n_qubits: usize, error_rate: f64) -> Self {
102 Self {
103 name: "noisy_mock_backend".to_string(),
104 max_qubits: n_qubits,
105 error_rate,
106 ..Default::default()
107 }
108 }
109}
110
111#[derive(Debug, Clone)]
115pub struct MockJobRecord {
116 pub job_id: String,
118 pub circuit_qasm: String,
120 pub shots: usize,
122 pub submitted_at: std::time::SystemTime,
124 pub result: Option<HashMap<String, usize>>,
126 pub failed: bool,
128 pub error_message: Option<String>,
130}
131
132#[derive(Debug)]
136pub enum MockBackendError {
137 JobFailed(String),
139 TooManyQubits {
141 requested: usize,
143 max: usize,
145 },
146 TooManyShots {
148 requested: usize,
150 max: usize,
152 },
153 InvalidCircuit(String),
155}
156
157impl std::fmt::Display for MockBackendError {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 match self {
160 MockBackendError::JobFailed(msg) => write!(f, "mock job failed: {msg}"),
161 MockBackendError::TooManyQubits { requested, max } => {
162 write!(f, "requested {requested} qubits, backend max is {max}")
163 }
164 MockBackendError::TooManyShots { requested, max } => {
165 write!(f, "requested {requested} shots, backend max is {max}")
166 }
167 MockBackendError::InvalidCircuit(msg) => write!(f, "invalid circuit: {msg}"),
168 }
169 }
170}
171
172impl std::error::Error for MockBackendError {}
173
174pub struct MockQuantumBackend {
182 pub config: MockBackendConfig,
184 job_records: Arc<Mutex<Vec<MockJobRecord>>>,
185}
186
187impl std::fmt::Debug for MockQuantumBackend {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 f.debug_struct("MockQuantumBackend")
190 .field("config", &self.config)
191 .finish()
192 }
193}
194
195impl MockQuantumBackend {
196 pub fn new(config: MockBackendConfig) -> Self {
198 Self {
199 config,
200 job_records: Arc::new(Mutex::new(vec![])),
201 }
202 }
203
204 pub fn job_count(&self) -> usize {
206 self.job_records
207 .lock()
208 .unwrap_or_else(|e| e.into_inner())
209 .len()
210 }
211
212 pub fn all_jobs(&self) -> Vec<MockJobRecord> {
214 self.job_records
215 .lock()
216 .unwrap_or_else(|e| e.into_inner())
217 .clone()
218 }
219
220 pub fn last_job(&self) -> Option<MockJobRecord> {
222 self.job_records
223 .lock()
224 .unwrap_or_else(|e| e.into_inner())
225 .last()
226 .cloned()
227 }
228
229 pub fn run(
235 &self,
236 circuit_qasm: &str,
237 shots: usize,
238 ) -> Result<HashMap<String, usize>, MockBackendError> {
239 if shots > self.config.max_shots {
241 return Err(MockBackendError::TooManyShots {
242 requested: shots,
243 max: self.config.max_shots,
244 });
245 }
246
247 let n_qubits = self.parse_qubit_count(circuit_qasm).unwrap_or(1);
251 if n_qubits > self.config.max_qubits {
252 return Err(MockBackendError::TooManyQubits {
253 requested: n_qubits,
254 max: self.config.max_qubits,
255 });
256 }
257
258 if self.config.latency_ms > 0 {
260 std::thread::sleep(Duration::from_millis(self.config.latency_ms));
261 }
262
263 let raw_id = fastrand::u64(..);
265 let job_id = format!("mock-job-{raw_id}");
266
267 let failed = self.config.fail_rate > 0.0 && fastrand::f64() < self.config.fail_rate;
269
270 if failed {
271 let record = MockJobRecord {
272 job_id,
273 circuit_qasm: circuit_qasm.to_string(),
274 shots,
275 submitted_at: std::time::SystemTime::now(),
276 result: None,
277 failed: true,
278 error_message: Some("simulated backend failure".to_string()),
279 };
280 self.job_records
281 .lock()
282 .unwrap_or_else(|e| e.into_inner())
283 .push(record);
284 return Err(MockBackendError::JobFailed(
285 "simulated backend failure".to_string(),
286 ));
287 }
288
289 let counts = self.generate_counts(n_qubits, shots);
291
292 let record = MockJobRecord {
293 job_id,
294 circuit_qasm: circuit_qasm.to_string(),
295 shots,
296 submitted_at: std::time::SystemTime::now(),
297 result: Some(counts.clone()),
298 failed: false,
299 error_message: None,
300 };
301 self.job_records
302 .lock()
303 .unwrap_or_else(|e| e.into_inner())
304 .push(record);
305
306 Ok(counts)
307 }
308
309 fn parse_qubit_count(&self, qasm: &str) -> Option<usize> {
313 for line in qasm.lines() {
314 let trimmed = line.trim();
315 if trimmed.starts_with("qreg") {
316 if let Some(start) = trimmed.find('[') {
317 if let Some(end) = trimmed.find(']') {
318 if start < end {
319 return trimmed[start + 1..end].parse().ok();
320 }
321 }
322 }
323 }
324 }
325 None
326 }
327
328 fn generate_counts(&self, n_qubits: usize, shots: usize) -> HashMap<String, usize> {
333 if n_qubits == 0 || shots == 0 {
335 return HashMap::new();
336 }
337
338 let mut rng = fastrand::Rng::with_seed(
339 self.config
340 .rng_seed
341 .wrapping_add(n_qubits as u64)
342 .wrapping_add(shots as u64),
343 );
344
345 let n_states = 1usize << n_qubits.min(63); let mut counts: HashMap<String, usize> = HashMap::new();
347
348 for _ in 0..shots {
349 let state = rng.usize(..n_states);
350
351 let noisy_state = if self.config.error_rate > 0.0 {
353 let mut s = state;
354 for bit in 0..n_qubits {
355 if rng.f64() < self.config.error_rate {
356 s ^= 1 << bit;
357 }
358 }
359 s
360 } else {
361 state
362 };
363
364 let bitstring = format!("{noisy_state:0>n_qubits$b}");
365 *counts.entry(bitstring).or_insert(0) += 1;
366 }
367
368 counts
369 }
370
371 pub fn capabilities(&self) -> HashMap<String, String> {
373 let mut caps = HashMap::new();
374 caps.insert("name".to_string(), self.config.name.clone());
375 caps.insert("n_qubits".to_string(), self.config.max_qubits.to_string());
376 caps.insert("max_shots".to_string(), self.config.max_shots.to_string());
377 caps.insert("simulator".to_string(), "true".to_string());
378 caps.insert(
379 "error_rate".to_string(),
380 format!("{:.6}", self.config.error_rate),
381 );
382 caps.insert(
383 "supported_gates".to_string(),
384 if self.config.gate_set.is_empty() {
385 "all".to_string()
386 } else {
387 self.config.gate_set.join(",")
388 },
389 );
390 caps.insert(
391 "connectivity".to_string(),
392 if self.config.connectivity.is_empty() {
393 "all-to-all".to_string()
394 } else {
395 self.config
396 .connectivity
397 .iter()
398 .map(|(a, b)| format!("{a}-{b}"))
399 .collect::<Vec<_>>()
400 .join(",")
401 },
402 );
403 caps
404 }
405
406 pub fn supports_gate(&self, gate_name: &str) -> bool {
410 self.config.gate_set.is_empty() || self.config.gate_set.iter().any(|g| g == gate_name)
411 }
412
413 pub fn is_connected(&self, q0: usize, q1: usize) -> bool {
417 self.config.connectivity.is_empty()
418 || self
419 .config
420 .connectivity
421 .iter()
422 .any(|&(a, b)| (a == q0 && b == q1) || (a == q1 && b == q0))
423 }
424}
425
426#[cfg(test)]
429mod tests {
430 use super::*;
431
432 const BELL_QASM: &str = "OPENQASM 2.0;\n\
433 include \"qelib1.inc\";\n\
434 qreg q[2];\n\
435 creg c[2];\n\
436 h q[0];\n\
437 cx q[0],q[1];\n\
438 measure q[0] -> c[0];\n\
439 measure q[1] -> c[1];\n";
440
441 #[test]
442 fn test_default_config() {
443 let cfg = MockBackendConfig::default();
444 assert_eq!(cfg.name, "mock_backend");
445 assert_eq!(cfg.max_qubits, 32);
446 assert_eq!(cfg.max_shots, 8192);
447 assert_eq!(cfg.error_rate, 0.0);
448 assert_eq!(cfg.fail_rate, 0.0);
449 }
450
451 #[test]
452 fn test_parse_qubit_count() {
453 let backend = MockQuantumBackend::new(MockBackendConfig::default());
454 assert_eq!(backend.parse_qubit_count(BELL_QASM), Some(2));
455 assert_eq!(backend.parse_qubit_count("no qreg here"), None);
456 assert_eq!(backend.parse_qubit_count("qreg q[5];"), Some(5));
457 }
458
459 #[test]
460 fn test_counts_sum_to_shots() {
461 let backend = MockQuantumBackend::new(MockBackendConfig::perfect(3));
462 let shots = 1024;
463 let counts = backend.run(BELL_QASM, shots).expect("run succeeded");
464 let total: usize = counts.values().sum();
465 assert_eq!(total, shots);
466 }
467
468 #[test]
469 fn test_capabilities_contains_name() {
470 let backend = MockQuantumBackend::new(MockBackendConfig::ibm_nairobi_like());
471 let caps = backend.capabilities();
472 assert_eq!(
473 caps.get("name").map(String::as_str),
474 Some("mock_ibm_nairobi")
475 );
476 assert_eq!(caps.get("n_qubits").map(String::as_str), Some("7"));
477 }
478
479 #[test]
480 fn test_supports_gate_empty_means_all() {
481 let backend = MockQuantumBackend::new(MockBackendConfig::default());
482 assert!(backend.supports_gate("any_gate"));
483 assert!(backend.supports_gate("cx"));
484 }
485
486 #[test]
487 fn test_supports_gate_restricted() {
488 let backend = MockQuantumBackend::new(MockBackendConfig::ibm_nairobi_like());
489 assert!(backend.supports_gate("cx"));
490 assert!(!backend.supports_gate("ccx"));
491 }
492}