1use std::collections::HashMap;
28use std::sync::Arc;
29#[cfg(feature = "ibm")]
30use std::time::{Duration, Instant};
31
32#[cfg(feature = "ibm")]
33use tokio::sync::RwLock;
34
35use crate::ibm::{IBMJobResult, IBMJobStatus, IBMQuantumClient};
36use crate::{DeviceError, DeviceResult};
37
38#[derive(Debug, Clone)]
40pub struct SessionConfig {
41 pub max_time: u64,
43 pub close_on_complete: bool,
45 pub max_circuits_per_job: usize,
47 pub optimization_level: usize,
49 pub resilience_level: usize,
51 pub dynamic_circuits: bool,
53}
54
55impl Default for SessionConfig {
56 fn default() -> Self {
57 Self {
58 max_time: 7200, close_on_complete: true,
60 max_circuits_per_job: 100,
61 optimization_level: 1,
62 resilience_level: 1,
63 dynamic_circuits: false,
64 }
65 }
66}
67
68impl SessionConfig {
69 pub fn interactive() -> Self {
71 Self {
72 max_time: 900, close_on_complete: false,
74 max_circuits_per_job: 10,
75 optimization_level: 1,
76 resilience_level: 1,
77 dynamic_circuits: false,
78 }
79 }
80
81 pub fn batch() -> Self {
83 Self {
84 max_time: 28800, close_on_complete: true,
86 max_circuits_per_job: 300,
87 optimization_level: 3,
88 resilience_level: 2,
89 dynamic_circuits: false,
90 }
91 }
92
93 pub fn dynamic() -> Self {
95 Self {
96 max_time: 3600,
97 close_on_complete: true,
98 max_circuits_per_job: 50,
99 optimization_level: 1,
100 resilience_level: 1,
101 dynamic_circuits: true,
102 }
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108pub enum SessionState {
109 Creating,
111 Active,
113 Closing,
115 Closed,
117 Error,
119}
120
121#[cfg(feature = "ibm")]
123pub struct Session {
124 id: String,
126 client: Arc<IBMQuantumClient>,
128 backend: String,
130 config: SessionConfig,
132 state: Arc<RwLock<SessionState>>,
134 created_at: Instant,
136 job_count: Arc<RwLock<usize>>,
138}
139
140#[cfg(not(feature = "ibm"))]
141pub struct Session {
142 id: String,
143 backend: String,
144 config: SessionConfig,
145}
146
147#[cfg(feature = "ibm")]
148impl Session {
149 pub async fn new(
151 client: IBMQuantumClient,
152 backend: &str,
153 config: SessionConfig,
154 ) -> DeviceResult<Self> {
155 let session_id = format!(
158 "session_{}_{}",
159 backend,
160 std::time::SystemTime::now()
161 .duration_since(std::time::UNIX_EPOCH)
162 .map(|d| d.as_millis())
163 .unwrap_or(0)
164 );
165
166 Ok(Self {
167 id: session_id,
168 client: Arc::new(client),
169 backend: backend.to_string(),
170 config,
171 state: Arc::new(RwLock::new(SessionState::Active)),
172 created_at: Instant::now(),
173 job_count: Arc::new(RwLock::new(0)),
174 })
175 }
176
177 pub fn id(&self) -> &str {
179 &self.id
180 }
181
182 pub fn backend(&self) -> &str {
184 &self.backend
185 }
186
187 pub fn config(&self) -> &SessionConfig {
189 &self.config
190 }
191
192 pub async fn state(&self) -> SessionState {
194 self.state.read().await.clone()
195 }
196
197 pub async fn is_active(&self) -> bool {
199 let state = self.state.read().await;
200 *state == SessionState::Active
201 }
202
203 pub fn duration(&self) -> Duration {
205 self.created_at.elapsed()
206 }
207
208 pub fn remaining_time(&self) -> Option<Duration> {
210 let elapsed = self.created_at.elapsed().as_secs();
211 if elapsed >= self.config.max_time {
212 None
213 } else {
214 Some(Duration::from_secs(self.config.max_time - elapsed))
215 }
216 }
217
218 pub async fn job_count(&self) -> usize {
220 *self.job_count.read().await
221 }
222
223 async fn increment_job_count(&self) {
225 let mut count = self.job_count.write().await;
226 *count += 1;
227 }
228
229 pub fn client(&self) -> &IBMQuantumClient {
231 &self.client
232 }
233
234 pub async fn close(&self) -> DeviceResult<()> {
236 let mut state = self.state.write().await;
237 if *state == SessionState::Closed {
238 return Ok(());
239 }
240
241 *state = SessionState::Closing;
242 *state = SessionState::Closed;
245 Ok(())
246 }
247}
248
249#[cfg(not(feature = "ibm"))]
250impl Session {
251 pub async fn new(
252 _client: IBMQuantumClient,
253 backend: &str,
254 config: SessionConfig,
255 ) -> DeviceResult<Self> {
256 Ok(Self {
257 id: "stub_session".to_string(),
258 backend: backend.to_string(),
259 config,
260 })
261 }
262
263 pub fn id(&self) -> &str {
264 &self.id
265 }
266
267 pub fn backend(&self) -> &str {
268 &self.backend
269 }
270
271 pub fn config(&self) -> &SessionConfig {
272 &self.config
273 }
274
275 pub async fn is_active(&self) -> bool {
276 false
277 }
278
279 pub async fn close(&self) -> DeviceResult<()> {
280 Err(DeviceError::UnsupportedDevice(
281 "IBM Runtime support not enabled".to_string(),
282 ))
283 }
284}
285
286#[derive(Debug, Clone)]
288pub struct SamplerResult {
289 pub quasi_dists: Vec<HashMap<String, f64>>,
291 pub metadata: Vec<HashMap<String, String>>,
293 pub shots: usize,
295}
296
297impl SamplerResult {
298 pub fn most_probable(&self, circuit_idx: usize) -> Option<(&str, f64)> {
300 self.quasi_dists.get(circuit_idx).and_then(|dist| {
301 dist.iter()
302 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
303 .map(|(k, v)| (k.as_str(), *v))
304 })
305 }
306
307 pub fn probability_of(&self, bitstring: &str) -> Vec<f64> {
309 self.quasi_dists
310 .iter()
311 .map(|dist| *dist.get(bitstring).unwrap_or(&0.0))
312 .collect()
313 }
314}
315
316#[cfg(feature = "ibm")]
320pub struct Sampler<'a> {
321 session: &'a Session,
322 options: SamplerOptions,
323}
324
325#[cfg(not(feature = "ibm"))]
326pub struct Sampler<'a> {
327 _phantom: std::marker::PhantomData<&'a ()>,
328 options: SamplerOptions,
329}
330
331#[derive(Debug, Clone)]
333pub struct SamplerOptions {
334 pub shots: usize,
336 pub seed: Option<u64>,
338 pub skip_transpilation: bool,
340 pub dynamical_decoupling: Option<String>,
342}
343
344impl Default for SamplerOptions {
345 fn default() -> Self {
346 Self {
347 shots: 4096,
348 seed: None,
349 skip_transpilation: false,
350 dynamical_decoupling: None,
351 }
352 }
353}
354
355#[cfg(feature = "ibm")]
356impl<'a> Sampler<'a> {
357 pub fn new(session: &'a Session) -> Self {
359 Self {
360 session,
361 options: SamplerOptions::default(),
362 }
363 }
364
365 pub fn with_options(session: &'a Session, options: SamplerOptions) -> Self {
367 Self { session, options }
368 }
369
370 pub async fn run<const N: usize>(
372 &self,
373 circuit: &quantrs2_circuit::prelude::Circuit<N>,
374 parameter_values: Option<&[f64]>,
375 ) -> DeviceResult<SamplerResult> {
376 self.run_batch(&[circuit], parameter_values.map(|p| vec![p.to_vec()]))
377 .await
378 }
379
380 pub async fn run_batch<const N: usize>(
382 &self,
383 circuits: &[&quantrs2_circuit::prelude::Circuit<N>],
384 _parameter_values: Option<Vec<Vec<f64>>>,
385 ) -> DeviceResult<SamplerResult> {
386 if !self.session.is_active().await {
387 return Err(DeviceError::SessionError(
388 "Session is not active".to_string(),
389 ));
390 }
391
392 if self.session.remaining_time().is_none() {
394 return Err(DeviceError::SessionError("Session has expired".to_string()));
395 }
396
397 let mut quasi_dists = Vec::new();
398 let mut metadata = Vec::new();
399
400 for (idx, _circuit) in circuits.iter().enumerate() {
402 let qasm = format!(
403 "OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[{}];\ncreg c[{}];\n",
404 N, N
405 );
406
407 let config = crate::ibm::IBMCircuitConfig {
408 name: format!("sampler_circuit_{}", idx),
409 qasm,
410 shots: self.options.shots,
411 optimization_level: Some(self.session.config.optimization_level),
412 initial_layout: None,
413 };
414
415 let job_id = self
416 .session
417 .client
418 .submit_circuit(self.session.backend(), config)
419 .await?;
420
421 let result = self.session.client.wait_for_job(&job_id, Some(300)).await?;
422
423 let total: usize = result.counts.values().sum();
425 let mut dist = HashMap::new();
426 for (bitstring, count) in result.counts {
427 dist.insert(bitstring, count as f64 / total as f64);
428 }
429 quasi_dists.push(dist);
430
431 let mut meta = HashMap::new();
432 meta.insert("job_id".to_string(), job_id);
433 meta.insert("backend".to_string(), self.session.backend().to_string());
434 metadata.push(meta);
435 }
436
437 self.session.increment_job_count().await;
438
439 Ok(SamplerResult {
440 quasi_dists,
441 metadata,
442 shots: self.options.shots,
443 })
444 }
445}
446
447#[cfg(not(feature = "ibm"))]
448impl<'a> Sampler<'a> {
449 pub fn new(_session: &'a Session) -> Self {
450 Self {
451 _phantom: std::marker::PhantomData,
452 options: SamplerOptions::default(),
453 }
454 }
455
456 pub async fn run<const N: usize>(
457 &self,
458 _circuit: &quantrs2_circuit::prelude::Circuit<N>,
459 _parameter_values: Option<&[f64]>,
460 ) -> DeviceResult<SamplerResult> {
461 Err(DeviceError::UnsupportedDevice(
462 "IBM Runtime support not enabled".to_string(),
463 ))
464 }
465}
466
467#[derive(Debug, Clone)]
469pub struct EstimatorResult {
470 pub values: Vec<f64>,
472 pub std_errors: Vec<f64>,
474 pub metadata: Vec<HashMap<String, String>>,
476}
477
478impl EstimatorResult {
479 pub fn value(&self, idx: usize) -> Option<f64> {
481 self.values.get(idx).copied()
482 }
483
484 pub fn std_error(&self, idx: usize) -> Option<f64> {
486 self.std_errors.get(idx).copied()
487 }
488
489 pub fn mean(&self) -> f64 {
491 if self.values.is_empty() {
492 0.0
493 } else {
494 self.values.iter().sum::<f64>() / self.values.len() as f64
495 }
496 }
497
498 pub fn variance(&self) -> f64 {
500 if self.values.len() < 2 {
501 return 0.0;
502 }
503 let mean = self.mean();
504 self.values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (self.values.len() - 1) as f64
505 }
506}
507
508#[derive(Debug, Clone)]
510pub struct Observable {
511 pub pauli_string: String,
513 pub coefficient: f64,
515 pub qubits: Vec<usize>,
517}
518
519impl Observable {
520 pub fn z(qubits: &[usize]) -> Self {
522 let pauli_string = qubits.iter().map(|_| 'Z').collect();
523 Self {
524 pauli_string,
525 coefficient: 1.0,
526 qubits: qubits.to_vec(),
527 }
528 }
529
530 pub fn x(qubits: &[usize]) -> Self {
532 let pauli_string = qubits.iter().map(|_| 'X').collect();
533 Self {
534 pauli_string,
535 coefficient: 1.0,
536 qubits: qubits.to_vec(),
537 }
538 }
539
540 pub fn y(qubits: &[usize]) -> Self {
542 let pauli_string = qubits.iter().map(|_| 'Y').collect();
543 Self {
544 pauli_string,
545 coefficient: 1.0,
546 qubits: qubits.to_vec(),
547 }
548 }
549
550 pub fn identity(n_qubits: usize) -> Self {
552 Self {
553 pauli_string: "I".repeat(n_qubits),
554 coefficient: 1.0,
555 qubits: (0..n_qubits).collect(),
556 }
557 }
558
559 pub fn pauli(pauli_string: &str, qubits: &[usize], coefficient: f64) -> Self {
561 Self {
562 pauli_string: pauli_string.to_string(),
563 coefficient,
564 qubits: qubits.to_vec(),
565 }
566 }
567}
568
569#[derive(Debug, Clone)]
571pub struct EstimatorOptions {
572 pub shots: usize,
574 pub precision: Option<f64>,
576 pub resilience_level: usize,
578 pub skip_transpilation: bool,
580}
581
582impl Default for EstimatorOptions {
583 fn default() -> Self {
584 Self {
585 shots: 4096,
586 precision: None,
587 resilience_level: 1,
588 skip_transpilation: false,
589 }
590 }
591}
592
593#[cfg(feature = "ibm")]
597pub struct Estimator<'a> {
598 session: &'a Session,
599 options: EstimatorOptions,
600}
601
602#[cfg(not(feature = "ibm"))]
603pub struct Estimator<'a> {
604 _phantom: std::marker::PhantomData<&'a ()>,
605 options: EstimatorOptions,
606}
607
608#[cfg(feature = "ibm")]
609impl<'a> Estimator<'a> {
610 pub fn new(session: &'a Session) -> Self {
612 Self {
613 session,
614 options: EstimatorOptions::default(),
615 }
616 }
617
618 pub fn with_options(session: &'a Session, options: EstimatorOptions) -> Self {
620 Self { session, options }
621 }
622
623 pub async fn run<const N: usize>(
625 &self,
626 circuit: &quantrs2_circuit::prelude::Circuit<N>,
627 observable: &Observable,
628 parameter_values: Option<&[f64]>,
629 ) -> DeviceResult<EstimatorResult> {
630 self.run_batch(
631 &[circuit],
632 &[observable],
633 parameter_values.map(|p| vec![p.to_vec()]),
634 )
635 .await
636 }
637
638 pub async fn run_batch<const N: usize>(
640 &self,
641 circuits: &[&quantrs2_circuit::prelude::Circuit<N>],
642 observables: &[&Observable],
643 _parameter_values: Option<Vec<Vec<f64>>>,
644 ) -> DeviceResult<EstimatorResult> {
645 if !self.session.is_active().await {
646 return Err(DeviceError::SessionError(
647 "Session is not active".to_string(),
648 ));
649 }
650
651 if self.session.remaining_time().is_none() {
652 return Err(DeviceError::SessionError("Session has expired".to_string()));
653 }
654
655 let mut values = Vec::new();
656 let mut std_errors = Vec::new();
657 let mut metadata = Vec::new();
658
659 for (idx, (_circuit, observable)) in circuits.iter().zip(observables.iter()).enumerate() {
661 let qasm = self.build_measurement_circuit::<N>(observable);
663
664 let config = crate::ibm::IBMCircuitConfig {
665 name: format!("estimator_circuit_{}", idx),
666 qasm,
667 shots: self.options.shots,
668 optimization_level: Some(self.session.config.optimization_level),
669 initial_layout: None,
670 };
671
672 let job_id = self
673 .session
674 .client
675 .submit_circuit(self.session.backend(), config)
676 .await?;
677
678 let result = self.session.client.wait_for_job(&job_id, Some(300)).await?;
679
680 let (exp_value, std_err) = self.compute_expectation(&result, observable);
682 values.push(exp_value);
683 std_errors.push(std_err);
684
685 let mut meta = HashMap::new();
686 meta.insert("job_id".to_string(), job_id);
687 meta.insert("observable".to_string(), observable.pauli_string.clone());
688 metadata.push(meta);
689 }
690
691 self.session.increment_job_count().await;
692
693 Ok(EstimatorResult {
694 values,
695 std_errors,
696 metadata,
697 })
698 }
699
700 fn build_measurement_circuit<const N: usize>(&self, observable: &Observable) -> String {
702 let mut qasm = format!(
703 "OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[{}];\ncreg c[{}];\n",
704 N, N
705 );
706
707 for (i, pauli) in observable.pauli_string.chars().enumerate() {
709 if i < observable.qubits.len() {
710 let qubit = observable.qubits[i];
711 match pauli {
712 'X' => {
713 qasm.push_str(&format!("h q[{}];\n", qubit));
715 }
716 'Y' => {
717 qasm.push_str(&format!("sdg q[{}];\n", qubit));
719 qasm.push_str(&format!("h q[{}];\n", qubit));
720 }
721 'Z' | 'I' => {
722 }
724 _ => {}
725 }
726 }
727 }
728
729 for (i, qubit) in observable.qubits.iter().enumerate() {
731 qasm.push_str(&format!("measure q[{}] -> c[{}];\n", qubit, i));
732 }
733
734 qasm
735 }
736
737 fn compute_expectation(&self, result: &IBMJobResult, observable: &Observable) -> (f64, f64) {
739 let total_shots: usize = result.counts.values().sum();
740 if total_shots == 0 {
741 return (0.0, 0.0);
742 }
743
744 let mut expectation = 0.0;
745 let mut squared_sum = 0.0;
746
747 for (bitstring, count) in &result.counts {
748 let eigenvalue = self.compute_eigenvalue(bitstring, observable);
750 let probability = *count as f64 / total_shots as f64;
751
752 expectation += eigenvalue * probability;
753 squared_sum += eigenvalue.powi(2) * probability;
754 }
755
756 expectation *= observable.coefficient;
757
758 let variance = squared_sum - expectation.powi(2);
760 let std_error = (variance / total_shots as f64).sqrt();
761
762 (expectation, std_error)
763 }
764
765 fn compute_eigenvalue(&self, bitstring: &str, observable: &Observable) -> f64 {
767 let mut eigenvalue = 1.0;
768
769 for (i, pauli) in observable.pauli_string.chars().enumerate() {
770 if i < bitstring.len() && pauli != 'I' {
771 let bit = bitstring.chars().rev().nth(i).unwrap_or('0');
773 if bit == '1' {
774 eigenvalue *= -1.0;
775 }
776 }
777 }
778
779 eigenvalue
780 }
781}
782
783#[cfg(not(feature = "ibm"))]
784impl<'a> Estimator<'a> {
785 pub fn new(_session: &'a Session) -> Self {
786 Self {
787 _phantom: std::marker::PhantomData,
788 options: EstimatorOptions::default(),
789 }
790 }
791
792 pub async fn run<const N: usize>(
793 &self,
794 _circuit: &quantrs2_circuit::prelude::Circuit<N>,
795 _observable: &Observable,
796 _parameter_values: Option<&[f64]>,
797 ) -> DeviceResult<EstimatorResult> {
798 Err(DeviceError::UnsupportedDevice(
799 "IBM Runtime support not enabled".to_string(),
800 ))
801 }
802}
803
804#[derive(Debug, Clone, Copy, PartialEq, Eq)]
806pub enum ExecutionMode {
807 Interactive,
809 Batch,
811 Dedicated,
813}
814
815#[derive(Debug, Clone)]
817pub struct RuntimeJob {
818 pub id: String,
820 pub session_id: Option<String>,
822 pub status: IBMJobStatus,
824 pub primitive: String,
826 pub created_at: String,
828 pub backend: String,
830}
831
832#[cfg(test)]
833mod tests {
834 use super::*;
835
836 #[test]
837 fn test_session_config_default() {
838 let config = SessionConfig::default();
839 assert_eq!(config.max_time, 7200);
840 assert!(config.close_on_complete);
841 assert_eq!(config.optimization_level, 1);
842 }
843
844 #[test]
845 fn test_session_config_interactive() {
846 let config = SessionConfig::interactive();
847 assert_eq!(config.max_time, 900);
848 assert!(!config.close_on_complete);
849 }
850
851 #[test]
852 fn test_session_config_batch() {
853 let config = SessionConfig::batch();
854 assert_eq!(config.max_time, 28800);
855 assert_eq!(config.optimization_level, 3);
856 }
857
858 #[test]
859 fn test_observable_z() {
860 let obs = Observable::z(&[0, 1]);
861 assert_eq!(obs.pauli_string, "ZZ");
862 assert_eq!(obs.coefficient, 1.0);
863 assert_eq!(obs.qubits, vec![0, 1]);
864 }
865
866 #[test]
867 fn test_observable_x() {
868 let obs = Observable::x(&[0]);
869 assert_eq!(obs.pauli_string, "X");
870 }
871
872 #[test]
873 fn test_observable_y() {
874 let obs = Observable::y(&[0, 1, 2]);
875 assert_eq!(obs.pauli_string, "YYY");
876 }
877
878 #[test]
879 fn test_observable_identity() {
880 let obs = Observable::identity(4);
881 assert_eq!(obs.pauli_string, "IIII");
882 }
883
884 #[test]
885 fn test_sampler_options_default() {
886 let options = SamplerOptions::default();
887 assert_eq!(options.shots, 4096);
888 assert!(options.seed.is_none());
889 }
890
891 #[test]
892 fn test_estimator_options_default() {
893 let options = EstimatorOptions::default();
894 assert_eq!(options.shots, 4096);
895 assert_eq!(options.resilience_level, 1);
896 }
897
898 #[test]
899 fn test_sampler_result_most_probable() {
900 let mut dist = HashMap::new();
901 dist.insert("00".to_string(), 0.7);
902 dist.insert("11".to_string(), 0.3);
903
904 let result = SamplerResult {
905 quasi_dists: vec![dist],
906 metadata: vec![HashMap::new()],
907 shots: 1000,
908 };
909
910 let (bitstring, prob) = result.most_probable(0).unwrap();
911 assert_eq!(bitstring, "00");
912 assert!((prob - 0.7).abs() < 1e-10);
913 }
914
915 #[test]
916 fn test_estimator_result_mean() {
917 let result = EstimatorResult {
918 values: vec![0.5, 0.3, 0.2],
919 std_errors: vec![0.01, 0.01, 0.01],
920 metadata: vec![HashMap::new(); 3],
921 };
922
923 let mean = result.mean();
924 assert!((mean - (0.5 + 0.3 + 0.2) / 3.0).abs() < 1e-10);
925 }
926
927 #[test]
928 fn test_estimator_result_variance() {
929 let result = EstimatorResult {
930 values: vec![1.0, 2.0, 3.0],
931 std_errors: vec![0.1, 0.1, 0.1],
932 metadata: vec![HashMap::new(); 3],
933 };
934
935 let variance = result.variance();
936 assert!(variance > 0.0);
937 }
938}