use std::collections::HashMap;
use std::sync::Arc;
#[cfg(feature = "ibm")]
use std::time::{Duration, Instant};
#[cfg(feature = "ibm")]
use tokio::sync::RwLock;
use crate::ibm::{IBMJobResult, IBMJobStatus, IBMQuantumClient};
use crate::{DeviceError, DeviceResult};
#[derive(Debug, Clone)]
pub struct SessionConfig {
pub max_time: u64,
pub close_on_complete: bool,
pub max_circuits_per_job: usize,
pub optimization_level: usize,
pub resilience_level: usize,
pub dynamic_circuits: bool,
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
max_time: 7200, close_on_complete: true,
max_circuits_per_job: 100,
optimization_level: 1,
resilience_level: 1,
dynamic_circuits: false,
}
}
}
impl SessionConfig {
pub fn interactive() -> Self {
Self {
max_time: 900, close_on_complete: false,
max_circuits_per_job: 10,
optimization_level: 1,
resilience_level: 1,
dynamic_circuits: false,
}
}
pub fn batch() -> Self {
Self {
max_time: 28800, close_on_complete: true,
max_circuits_per_job: 300,
optimization_level: 3,
resilience_level: 2,
dynamic_circuits: false,
}
}
pub fn dynamic() -> Self {
Self {
max_time: 3600,
close_on_complete: true,
max_circuits_per_job: 50,
optimization_level: 1,
resilience_level: 1,
dynamic_circuits: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionState {
Creating,
Active,
Closing,
Closed,
Error,
}
#[cfg(feature = "ibm")]
pub struct Session {
id: String,
client: Arc<IBMQuantumClient>,
backend: String,
config: SessionConfig,
state: Arc<RwLock<SessionState>>,
created_at: Instant,
job_count: Arc<RwLock<usize>>,
}
#[cfg(not(feature = "ibm"))]
pub struct Session {
id: String,
backend: String,
config: SessionConfig,
}
#[cfg(feature = "ibm")]
impl Session {
pub async fn new(
client: IBMQuantumClient,
backend: &str,
config: SessionConfig,
) -> DeviceResult<Self> {
let session_id = format!(
"session_{}_{}",
backend,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0)
);
Ok(Self {
id: session_id,
client: Arc::new(client),
backend: backend.to_string(),
config,
state: Arc::new(RwLock::new(SessionState::Active)),
created_at: Instant::now(),
job_count: Arc::new(RwLock::new(0)),
})
}
pub fn id(&self) -> &str {
&self.id
}
pub fn backend(&self) -> &str {
&self.backend
}
pub fn config(&self) -> &SessionConfig {
&self.config
}
pub async fn state(&self) -> SessionState {
self.state.read().await.clone()
}
pub async fn is_active(&self) -> bool {
let state = self.state.read().await;
*state == SessionState::Active
}
pub fn duration(&self) -> Duration {
self.created_at.elapsed()
}
pub fn remaining_time(&self) -> Option<Duration> {
let elapsed = self.created_at.elapsed().as_secs();
if elapsed >= self.config.max_time {
None
} else {
Some(Duration::from_secs(self.config.max_time - elapsed))
}
}
pub async fn job_count(&self) -> usize {
*self.job_count.read().await
}
async fn increment_job_count(&self) {
let mut count = self.job_count.write().await;
*count += 1;
}
pub fn client(&self) -> &IBMQuantumClient {
&self.client
}
pub async fn close(&self) -> DeviceResult<()> {
let mut state = self.state.write().await;
if *state == SessionState::Closed {
return Ok(());
}
*state = SessionState::Closing;
*state = SessionState::Closed;
Ok(())
}
}
#[cfg(not(feature = "ibm"))]
impl Session {
pub async fn new(
_client: IBMQuantumClient,
backend: &str,
config: SessionConfig,
) -> DeviceResult<Self> {
Ok(Self {
id: "stub_session".to_string(),
backend: backend.to_string(),
config,
})
}
pub fn id(&self) -> &str {
&self.id
}
pub fn backend(&self) -> &str {
&self.backend
}
pub fn config(&self) -> &SessionConfig {
&self.config
}
pub async fn is_active(&self) -> bool {
false
}
pub async fn close(&self) -> DeviceResult<()> {
Err(DeviceError::UnsupportedDevice(
"IBM Runtime support not enabled".to_string(),
))
}
}
#[derive(Debug, Clone)]
pub struct SamplerResult {
pub quasi_dists: Vec<HashMap<String, f64>>,
pub metadata: Vec<HashMap<String, String>>,
pub shots: usize,
}
impl SamplerResult {
pub fn most_probable(&self, circuit_idx: usize) -> Option<(&str, f64)> {
self.quasi_dists.get(circuit_idx).and_then(|dist| {
dist.iter()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(k, v)| (k.as_str(), *v))
})
}
pub fn probability_of(&self, bitstring: &str) -> Vec<f64> {
self.quasi_dists
.iter()
.map(|dist| *dist.get(bitstring).unwrap_or(&0.0))
.collect()
}
}
#[cfg(feature = "ibm")]
pub struct Sampler<'a> {
session: &'a Session,
options: SamplerOptions,
}
#[cfg(not(feature = "ibm"))]
pub struct Sampler<'a> {
_phantom: std::marker::PhantomData<&'a ()>,
options: SamplerOptions,
}
#[derive(Debug, Clone)]
pub struct SamplerOptions {
pub shots: usize,
pub seed: Option<u64>,
pub skip_transpilation: bool,
pub dynamical_decoupling: Option<String>,
}
impl Default for SamplerOptions {
fn default() -> Self {
Self {
shots: 4096,
seed: None,
skip_transpilation: false,
dynamical_decoupling: None,
}
}
}
#[cfg(feature = "ibm")]
impl<'a> Sampler<'a> {
pub fn new(session: &'a Session) -> Self {
Self {
session,
options: SamplerOptions::default(),
}
}
pub fn with_options(session: &'a Session, options: SamplerOptions) -> Self {
Self { session, options }
}
pub async fn run<const N: usize>(
&self,
circuit: &quantrs2_circuit::prelude::Circuit<N>,
parameter_values: Option<&[f64]>,
) -> DeviceResult<SamplerResult> {
self.run_batch(&[circuit], parameter_values.map(|p| vec![p.to_vec()]))
.await
}
pub async fn run_batch<const N: usize>(
&self,
circuits: &[&quantrs2_circuit::prelude::Circuit<N>],
_parameter_values: Option<Vec<Vec<f64>>>,
) -> DeviceResult<SamplerResult> {
if !self.session.is_active().await {
return Err(DeviceError::SessionError(
"Session is not active".to_string(),
));
}
if self.session.remaining_time().is_none() {
return Err(DeviceError::SessionError("Session has expired".to_string()));
}
let mut quasi_dists = Vec::new();
let mut metadata = Vec::new();
for (idx, _circuit) in circuits.iter().enumerate() {
let qasm = format!(
"OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[{}];\ncreg c[{}];\n",
N, N
);
let config = crate::ibm::IBMCircuitConfig {
name: format!("sampler_circuit_{}", idx),
qasm,
shots: self.options.shots,
optimization_level: Some(self.session.config.optimization_level),
initial_layout: None,
};
let job_id = self
.session
.client
.submit_circuit(self.session.backend(), config)
.await?;
let result = self.session.client.wait_for_job(&job_id, Some(300)).await?;
let total: usize = result.counts.values().sum();
let mut dist = HashMap::new();
for (bitstring, count) in result.counts {
dist.insert(bitstring, count as f64 / total as f64);
}
quasi_dists.push(dist);
let mut meta = HashMap::new();
meta.insert("job_id".to_string(), job_id);
meta.insert("backend".to_string(), self.session.backend().to_string());
metadata.push(meta);
}
self.session.increment_job_count().await;
Ok(SamplerResult {
quasi_dists,
metadata,
shots: self.options.shots,
})
}
}
#[cfg(not(feature = "ibm"))]
impl<'a> Sampler<'a> {
pub fn new(_session: &'a Session) -> Self {
Self {
_phantom: std::marker::PhantomData,
options: SamplerOptions::default(),
}
}
pub async fn run<const N: usize>(
&self,
_circuit: &quantrs2_circuit::prelude::Circuit<N>,
_parameter_values: Option<&[f64]>,
) -> DeviceResult<SamplerResult> {
Err(DeviceError::UnsupportedDevice(
"IBM Runtime support not enabled".to_string(),
))
}
}
#[derive(Debug, Clone)]
pub struct EstimatorResult {
pub values: Vec<f64>,
pub std_errors: Vec<f64>,
pub metadata: Vec<HashMap<String, String>>,
}
impl EstimatorResult {
pub fn value(&self, idx: usize) -> Option<f64> {
self.values.get(idx).copied()
}
pub fn std_error(&self, idx: usize) -> Option<f64> {
self.std_errors.get(idx).copied()
}
pub fn mean(&self) -> f64 {
if self.values.is_empty() {
0.0
} else {
self.values.iter().sum::<f64>() / self.values.len() as f64
}
}
pub fn variance(&self) -> f64 {
if self.values.len() < 2 {
return 0.0;
}
let mean = self.mean();
self.values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (self.values.len() - 1) as f64
}
}
#[derive(Debug, Clone)]
pub struct Observable {
pub pauli_string: String,
pub coefficient: f64,
pub qubits: Vec<usize>,
}
impl Observable {
pub fn z(qubits: &[usize]) -> Self {
let pauli_string = qubits.iter().map(|_| 'Z').collect();
Self {
pauli_string,
coefficient: 1.0,
qubits: qubits.to_vec(),
}
}
pub fn x(qubits: &[usize]) -> Self {
let pauli_string = qubits.iter().map(|_| 'X').collect();
Self {
pauli_string,
coefficient: 1.0,
qubits: qubits.to_vec(),
}
}
pub fn y(qubits: &[usize]) -> Self {
let pauli_string = qubits.iter().map(|_| 'Y').collect();
Self {
pauli_string,
coefficient: 1.0,
qubits: qubits.to_vec(),
}
}
pub fn identity(n_qubits: usize) -> Self {
Self {
pauli_string: "I".repeat(n_qubits),
coefficient: 1.0,
qubits: (0..n_qubits).collect(),
}
}
pub fn pauli(pauli_string: &str, qubits: &[usize], coefficient: f64) -> Self {
Self {
pauli_string: pauli_string.to_string(),
coefficient,
qubits: qubits.to_vec(),
}
}
}
#[derive(Debug, Clone)]
pub struct EstimatorOptions {
pub shots: usize,
pub precision: Option<f64>,
pub resilience_level: usize,
pub skip_transpilation: bool,
}
impl Default for EstimatorOptions {
fn default() -> Self {
Self {
shots: 4096,
precision: None,
resilience_level: 1,
skip_transpilation: false,
}
}
}
#[cfg(feature = "ibm")]
pub struct Estimator<'a> {
session: &'a Session,
options: EstimatorOptions,
}
#[cfg(not(feature = "ibm"))]
pub struct Estimator<'a> {
_phantom: std::marker::PhantomData<&'a ()>,
options: EstimatorOptions,
}
#[cfg(feature = "ibm")]
impl<'a> Estimator<'a> {
pub fn new(session: &'a Session) -> Self {
Self {
session,
options: EstimatorOptions::default(),
}
}
pub fn with_options(session: &'a Session, options: EstimatorOptions) -> Self {
Self { session, options }
}
pub async fn run<const N: usize>(
&self,
circuit: &quantrs2_circuit::prelude::Circuit<N>,
observable: &Observable,
parameter_values: Option<&[f64]>,
) -> DeviceResult<EstimatorResult> {
self.run_batch(
&[circuit],
&[observable],
parameter_values.map(|p| vec![p.to_vec()]),
)
.await
}
pub async fn run_batch<const N: usize>(
&self,
circuits: &[&quantrs2_circuit::prelude::Circuit<N>],
observables: &[&Observable],
_parameter_values: Option<Vec<Vec<f64>>>,
) -> DeviceResult<EstimatorResult> {
if !self.session.is_active().await {
return Err(DeviceError::SessionError(
"Session is not active".to_string(),
));
}
if self.session.remaining_time().is_none() {
return Err(DeviceError::SessionError("Session has expired".to_string()));
}
let mut values = Vec::new();
let mut std_errors = Vec::new();
let mut metadata = Vec::new();
for (idx, (_circuit, observable)) in circuits.iter().zip(observables.iter()).enumerate() {
let qasm = self.build_measurement_circuit::<N>(observable);
let config = crate::ibm::IBMCircuitConfig {
name: format!("estimator_circuit_{}", idx),
qasm,
shots: self.options.shots,
optimization_level: Some(self.session.config.optimization_level),
initial_layout: None,
};
let job_id = self
.session
.client
.submit_circuit(self.session.backend(), config)
.await?;
let result = self.session.client.wait_for_job(&job_id, Some(300)).await?;
let (exp_value, std_err) = self.compute_expectation(&result, observable);
values.push(exp_value);
std_errors.push(std_err);
let mut meta = HashMap::new();
meta.insert("job_id".to_string(), job_id);
meta.insert("observable".to_string(), observable.pauli_string.clone());
metadata.push(meta);
}
self.session.increment_job_count().await;
Ok(EstimatorResult {
values,
std_errors,
metadata,
})
}
fn build_measurement_circuit<const N: usize>(&self, observable: &Observable) -> String {
let mut qasm = format!(
"OPENQASM 2.0;\ninclude \"qelib1.inc\";\nqreg q[{}];\ncreg c[{}];\n",
N, N
);
for (i, pauli) in observable.pauli_string.chars().enumerate() {
if i < observable.qubits.len() {
let qubit = observable.qubits[i];
match pauli {
'X' => {
qasm.push_str(&format!("h q[{}];\n", qubit));
}
'Y' => {
qasm.push_str(&format!("sdg q[{}];\n", qubit));
qasm.push_str(&format!("h q[{}];\n", qubit));
}
'Z' | 'I' => {
}
_ => {}
}
}
}
for (i, qubit) in observable.qubits.iter().enumerate() {
qasm.push_str(&format!("measure q[{}] -> c[{}];\n", qubit, i));
}
qasm
}
fn compute_expectation(&self, result: &IBMJobResult, observable: &Observable) -> (f64, f64) {
let total_shots: usize = result.counts.values().sum();
if total_shots == 0 {
return (0.0, 0.0);
}
let mut expectation = 0.0;
let mut squared_sum = 0.0;
for (bitstring, count) in &result.counts {
let eigenvalue = self.compute_eigenvalue(bitstring, observable);
let probability = *count as f64 / total_shots as f64;
expectation += eigenvalue * probability;
squared_sum += eigenvalue.powi(2) * probability;
}
expectation *= observable.coefficient;
let variance = squared_sum - expectation.powi(2);
let std_error = (variance / total_shots as f64).sqrt();
(expectation, std_error)
}
fn compute_eigenvalue(&self, bitstring: &str, observable: &Observable) -> f64 {
let mut eigenvalue = 1.0;
for (i, pauli) in observable.pauli_string.chars().enumerate() {
if i < bitstring.len() && pauli != 'I' {
let bit = bitstring.chars().rev().nth(i).unwrap_or('0');
if bit == '1' {
eigenvalue *= -1.0;
}
}
}
eigenvalue
}
}
#[cfg(not(feature = "ibm"))]
impl<'a> Estimator<'a> {
pub fn new(_session: &'a Session) -> Self {
Self {
_phantom: std::marker::PhantomData,
options: EstimatorOptions::default(),
}
}
pub async fn run<const N: usize>(
&self,
_circuit: &quantrs2_circuit::prelude::Circuit<N>,
_observable: &Observable,
_parameter_values: Option<&[f64]>,
) -> DeviceResult<EstimatorResult> {
Err(DeviceError::UnsupportedDevice(
"IBM Runtime support not enabled".to_string(),
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecutionMode {
Interactive,
Batch,
Dedicated,
}
#[derive(Debug, Clone)]
pub struct RuntimeJob {
pub id: String,
pub session_id: Option<String>,
pub status: IBMJobStatus,
pub primitive: String,
pub created_at: String,
pub backend: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_session_config_default() {
let config = SessionConfig::default();
assert_eq!(config.max_time, 7200);
assert!(config.close_on_complete);
assert_eq!(config.optimization_level, 1);
}
#[test]
fn test_session_config_interactive() {
let config = SessionConfig::interactive();
assert_eq!(config.max_time, 900);
assert!(!config.close_on_complete);
}
#[test]
fn test_session_config_batch() {
let config = SessionConfig::batch();
assert_eq!(config.max_time, 28800);
assert_eq!(config.optimization_level, 3);
}
#[test]
fn test_observable_z() {
let obs = Observable::z(&[0, 1]);
assert_eq!(obs.pauli_string, "ZZ");
assert_eq!(obs.coefficient, 1.0);
assert_eq!(obs.qubits, vec![0, 1]);
}
#[test]
fn test_observable_x() {
let obs = Observable::x(&[0]);
assert_eq!(obs.pauli_string, "X");
}
#[test]
fn test_observable_y() {
let obs = Observable::y(&[0, 1, 2]);
assert_eq!(obs.pauli_string, "YYY");
}
#[test]
fn test_observable_identity() {
let obs = Observable::identity(4);
assert_eq!(obs.pauli_string, "IIII");
}
#[test]
fn test_sampler_options_default() {
let options = SamplerOptions::default();
assert_eq!(options.shots, 4096);
assert!(options.seed.is_none());
}
#[test]
fn test_estimator_options_default() {
let options = EstimatorOptions::default();
assert_eq!(options.shots, 4096);
assert_eq!(options.resilience_level, 1);
}
#[test]
fn test_sampler_result_most_probable() {
let mut dist = HashMap::new();
dist.insert("00".to_string(), 0.7);
dist.insert("11".to_string(), 0.3);
let result = SamplerResult {
quasi_dists: vec![dist],
metadata: vec![HashMap::new()],
shots: 1000,
};
let (bitstring, prob) = result.most_probable(0).unwrap();
assert_eq!(bitstring, "00");
assert!((prob - 0.7).abs() < 1e-10);
}
#[test]
fn test_estimator_result_mean() {
let result = EstimatorResult {
values: vec![0.5, 0.3, 0.2],
std_errors: vec![0.01, 0.01, 0.01],
metadata: vec![HashMap::new(); 3],
};
let mean = result.mean();
assert!((mean - (0.5 + 0.3 + 0.2) / 3.0).abs() < 1e-10);
}
#[test]
fn test_estimator_result_variance() {
let result = EstimatorResult {
values: vec![1.0, 2.0, 3.0],
std_errors: vec![0.1, 0.1, 0.1],
metadata: vec![HashMap::new(); 3],
};
let variance = result.variance();
assert!(variance > 0.0);
}
}