quantrs2_device/
ibm_runtime.rs

1//! IBM Qiskit Runtime primitives and session management.
2//!
3//! This module provides Qiskit Runtime-compatible primitives:
4//! - `Sampler`: For sampling quasi-probability distributions
5//! - `Estimator`: For computing expectation values
6//! - `Session`: For managing persistent runtime sessions
7//!
8//! ## Example
9//!
10//! ```rust,ignore
11//! use quantrs2_device::ibm_runtime::{Sampler, Estimator, Session, SessionConfig};
12//!
13//! // Create a session
14//! let session = Session::new(client, "ibm_brisbane", SessionConfig::default()).await?;
15//!
16//! // Use Sampler primitive
17//! let sampler = Sampler::new(&session);
18//! let result = sampler.run(&circuit, None).await?;
19//!
20//! // Use Estimator primitive
21//! let estimator = Estimator::new(&session);
22//! let expectation = estimator.run(&circuit, &observable).await?;
23//!
24//! // Session auto-closes on drop
25//! ```
26
27use 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/// Configuration for a Qiskit Runtime session
39#[derive(Debug, Clone)]
40pub struct SessionConfig {
41    /// Maximum session duration in seconds
42    pub max_time: u64,
43    /// Whether to close session on completion
44    pub close_on_complete: bool,
45    /// Maximum number of circuits per job
46    pub max_circuits_per_job: usize,
47    /// Optimization level (0-3)
48    pub optimization_level: usize,
49    /// Resilience level (0-2) for error mitigation
50    pub resilience_level: usize,
51    /// Enable dynamic circuits
52    pub dynamic_circuits: bool,
53}
54
55impl Default for SessionConfig {
56    fn default() -> Self {
57        Self {
58            max_time: 7200, // 2 hours
59            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    /// Create a configuration for short interactive sessions
70    pub fn interactive() -> Self {
71        Self {
72            max_time: 900, // 15 minutes
73            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    /// Create a configuration for long batch jobs
82    pub fn batch() -> Self {
83        Self {
84            max_time: 28800, // 8 hours
85            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    /// Create a configuration for dynamic circuit execution
94    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/// Session state
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub enum SessionState {
109    /// Session is being created
110    Creating,
111    /// Session is active and accepting jobs
112    Active,
113    /// Session is closing
114    Closing,
115    /// Session is closed
116    Closed,
117    /// Session encountered an error
118    Error,
119}
120
121/// A Qiskit Runtime session for persistent execution context
122#[cfg(feature = "ibm")]
123pub struct Session {
124    /// Session ID
125    id: String,
126    /// IBM Quantum client
127    client: Arc<IBMQuantumClient>,
128    /// Backend name
129    backend: String,
130    /// Session configuration
131    config: SessionConfig,
132    /// Session state
133    state: Arc<RwLock<SessionState>>,
134    /// Session creation time
135    created_at: Instant,
136    /// Number of jobs executed in this session
137    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    /// Create a new runtime session
150    pub async fn new(
151        client: IBMQuantumClient,
152        backend: &str,
153        config: SessionConfig,
154    ) -> DeviceResult<Self> {
155        // In a real implementation, this would call the IBM Runtime API
156        // to create a session. For now, we simulate session creation.
157        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    /// Get the session ID
178    pub fn id(&self) -> &str {
179        &self.id
180    }
181
182    /// Get the backend name
183    pub fn backend(&self) -> &str {
184        &self.backend
185    }
186
187    /// Get the session configuration
188    pub fn config(&self) -> &SessionConfig {
189        &self.config
190    }
191
192    /// Get the current session state
193    pub async fn state(&self) -> SessionState {
194        self.state.read().await.clone()
195    }
196
197    /// Check if the session is active
198    pub async fn is_active(&self) -> bool {
199        let state = self.state.read().await;
200        *state == SessionState::Active
201    }
202
203    /// Get the session duration
204    pub fn duration(&self) -> Duration {
205        self.created_at.elapsed()
206    }
207
208    /// Get the remaining session time
209    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    /// Get the number of jobs executed in this session
219    pub async fn job_count(&self) -> usize {
220        *self.job_count.read().await
221    }
222
223    /// Increment job count
224    async fn increment_job_count(&self) {
225        let mut count = self.job_count.write().await;
226        *count += 1;
227    }
228
229    /// Get the IBM Quantum client
230    pub fn client(&self) -> &IBMQuantumClient {
231        &self.client
232    }
233
234    /// Close the session
235    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        // In a real implementation, this would call the IBM Runtime API
243        // to close the session
244        *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/// Result from a Sampler primitive execution
287#[derive(Debug, Clone)]
288pub struct SamplerResult {
289    /// Quasi-probability distribution for each circuit
290    pub quasi_dists: Vec<HashMap<String, f64>>,
291    /// Metadata for the execution
292    pub metadata: Vec<HashMap<String, String>>,
293    /// Number of shots used
294    pub shots: usize,
295}
296
297impl SamplerResult {
298    /// Get the most probable bitstring for a circuit
299    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    /// Get the probabilities for a specific bitstring across all circuits
308    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/// Sampler primitive for sampling quasi-probability distributions
317///
318/// Compatible with Qiskit Runtime's Sampler primitive
319#[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/// Options for the Sampler primitive
332#[derive(Debug, Clone)]
333pub struct SamplerOptions {
334    /// Number of shots
335    pub shots: usize,
336    /// Seed for random number generation (for reproducibility)
337    pub seed: Option<u64>,
338    /// Skip transpilation
339    pub skip_transpilation: bool,
340    /// Dynamical decoupling sequence
341    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    /// Create a new Sampler primitive
358    pub fn new(session: &'a Session) -> Self {
359        Self {
360            session,
361            options: SamplerOptions::default(),
362        }
363    }
364
365    /// Create a Sampler with custom options
366    pub fn with_options(session: &'a Session, options: SamplerOptions) -> Self {
367        Self { session, options }
368    }
369
370    /// Run the sampler on a single circuit
371    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    /// Run the sampler on multiple circuits
381    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        // Check remaining time
393        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        // Convert circuits to QASM and submit
401        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            // Convert counts to quasi-probability distribution
424            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/// Result from an Estimator primitive execution
468#[derive(Debug, Clone)]
469pub struct EstimatorResult {
470    /// Expectation values for each circuit-observable pair
471    pub values: Vec<f64>,
472    /// Standard errors for each expectation value
473    pub std_errors: Vec<f64>,
474    /// Metadata for the execution
475    pub metadata: Vec<HashMap<String, String>>,
476}
477
478impl EstimatorResult {
479    /// Get the expectation value for a specific index
480    pub fn value(&self, idx: usize) -> Option<f64> {
481        self.values.get(idx).copied()
482    }
483
484    /// Get the standard error for a specific index
485    pub fn std_error(&self, idx: usize) -> Option<f64> {
486        self.std_errors.get(idx).copied()
487    }
488
489    /// Get the mean expectation value across all circuits
490    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    /// Get the variance of expectation values
499    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/// Observable specification for the Estimator
509#[derive(Debug, Clone)]
510pub struct Observable {
511    /// Pauli string representation (e.g., "ZZII", "XXXX")
512    pub pauli_string: String,
513    /// Coefficient for this observable
514    pub coefficient: f64,
515    /// Qubits this observable acts on
516    pub qubits: Vec<usize>,
517}
518
519impl Observable {
520    /// Create a Z observable on specific qubits
521    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    /// Create an X observable on specific qubits
531    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    /// Create a Y observable on specific qubits
541    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    /// Create an identity observable
551    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    /// Create a custom Pauli observable
560    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/// Options for the Estimator primitive
570#[derive(Debug, Clone)]
571pub struct EstimatorOptions {
572    /// Number of shots per circuit
573    pub shots: usize,
574    /// Precision target (stopping criterion)
575    pub precision: Option<f64>,
576    /// Resilience level (0-2)
577    pub resilience_level: usize,
578    /// Skip transpilation
579    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/// Estimator primitive for computing expectation values
594///
595/// Compatible with Qiskit Runtime's Estimator primitive
596#[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    /// Create a new Estimator primitive
611    pub fn new(session: &'a Session) -> Self {
612        Self {
613            session,
614            options: EstimatorOptions::default(),
615        }
616    }
617
618    /// Create an Estimator with custom options
619    pub fn with_options(session: &'a Session, options: EstimatorOptions) -> Self {
620        Self { session, options }
621    }
622
623    /// Run the estimator on a single circuit with a single observable
624    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    /// Run the estimator on multiple circuit-observable pairs
639    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 each circuit-observable pair
660        for (idx, (_circuit, observable)) in circuits.iter().zip(observables.iter()).enumerate() {
661            // Build measurement circuit based on observable
662            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            // Calculate expectation value from measurement results
681            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    /// Build a measurement circuit for the given observable
701    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        // Add basis rotation gates based on Pauli string
708        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                        // Rotate to X basis: H gate
714                        qasm.push_str(&format!("h q[{}];\n", qubit));
715                    }
716                    'Y' => {
717                        // Rotate to Y basis: S†H gates
718                        qasm.push_str(&format!("sdg q[{}];\n", qubit));
719                        qasm.push_str(&format!("h q[{}];\n", qubit));
720                    }
721                    'Z' | 'I' => {
722                        // Z basis is computational basis, no rotation needed
723                    }
724                    _ => {}
725                }
726            }
727        }
728
729        // Add measurements
730        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    /// Compute expectation value from measurement results
738    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            // Calculate eigenvalue for this bitstring
749            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        // Standard error: sqrt(Var / n)
759        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    /// Compute the eigenvalue for a measurement outcome
766    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                // Get the bit value (assuming little-endian)
772                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/// Batch execution mode for runtime primitives
805#[derive(Debug, Clone, Copy, PartialEq, Eq)]
806pub enum ExecutionMode {
807    /// Interactive mode with immediate feedback
808    Interactive,
809    /// Batch mode for large workloads
810    Batch,
811    /// Dedicated mode with reserved resources
812    Dedicated,
813}
814
815/// Runtime job information
816#[derive(Debug, Clone)]
817pub struct RuntimeJob {
818    /// Job ID
819    pub id: String,
820    /// Session ID (if part of a session)
821    pub session_id: Option<String>,
822    /// Job status
823    pub status: IBMJobStatus,
824    /// Primitive type (sampler or estimator)
825    pub primitive: String,
826    /// Creation timestamp
827    pub created_at: String,
828    /// Backend name
829    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}