use std::collections::HashMap;
use std::sync::Arc;
use crate::ibm::IBMQuantumClient;
use crate::{DeviceError, DeviceResult};
#[derive(Debug, Clone)]
pub struct PUB {
pub circuit_qasm: String,
pub parameter_values: Option<Vec<Vec<f64>>>,
pub shots: Option<usize>,
pub observables: Option<Vec<ObservableV2>>,
}
impl PUB {
pub fn new(circuit_qasm: impl Into<String>) -> Self {
Self {
circuit_qasm: circuit_qasm.into(),
parameter_values: None,
shots: None,
observables: None,
}
}
pub fn from_circuit<const N: usize>(
circuit: &quantrs2_circuit::prelude::Circuit<N>,
) -> crate::DeviceResult<Self> {
let qasm_circuit = crate::qasm3::circuit_to_qasm3(circuit)?;
Ok(Self::new(qasm_circuit.to_string()))
}
#[must_use]
pub fn with_parameter_values(mut self, values: Vec<Vec<f64>>) -> Self {
self.parameter_values = Some(values);
self
}
#[must_use]
pub fn with_shots(mut self, shots: usize) -> Self {
self.shots = Some(shots);
self
}
#[must_use]
pub fn with_observables(mut self, observables: Vec<ObservableV2>) -> Self {
self.observables = Some(observables);
self
}
}
#[derive(Debug, Clone)]
pub struct ObservableV2 {
pub pauli_string: String,
pub coefficient: f64,
pub qubits: Vec<usize>,
}
impl ObservableV2 {
pub fn z(qubits: &[usize]) -> Self {
Self {
pauli_string: qubits.iter().map(|_| 'Z').collect(),
coefficient: 1.0,
qubits: qubits.to_vec(),
}
}
pub fn x(qubits: &[usize]) -> Self {
Self {
pauli_string: qubits.iter().map(|_| 'X').collect(),
coefficient: 1.0,
qubits: qubits.to_vec(),
}
}
pub fn y(qubits: &[usize]) -> Self {
Self {
pauli_string: qubits.iter().map(|_| 'Y').collect(),
coefficient: 1.0,
qubits: qubits.to_vec(),
}
}
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 ZNEConfig {
pub noise_factors: Vec<f64>,
pub extrapolation: ExtrapolationMethod,
pub samples_per_factor: usize,
}
impl Default for ZNEConfig {
fn default() -> Self {
Self {
noise_factors: vec![1.0, 2.0, 3.0],
extrapolation: ExtrapolationMethod::Linear,
samples_per_factor: 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExtrapolationMethod {
Linear,
Polynomial,
Exponential,
Richardson,
}
#[derive(Debug, Clone)]
pub struct PECConfig {
pub num_samples: usize,
pub max_noise_strength: f64,
}
impl Default for PECConfig {
fn default() -> Self {
Self {
num_samples: 100,
max_noise_strength: 0.1,
}
}
}
#[derive(Debug, Clone)]
pub struct TwirlingConfig {
pub enable_pauli_twirling: bool,
pub num_randomizations: usize,
pub gates_to_twirl: Vec<String>,
}
impl Default for TwirlingConfig {
fn default() -> Self {
Self {
enable_pauli_twirling: true,
num_randomizations: 32,
gates_to_twirl: vec!["cx".to_string(), "cz".to_string()],
}
}
}
#[derive(Debug, Clone)]
pub struct MeasurementMitigationConfig {
pub enable_m3: bool,
pub calibration_shots: usize,
pub max_qubits_correlated: usize,
}
impl Default for MeasurementMitigationConfig {
fn default() -> Self {
Self {
enable_m3: true,
calibration_shots: 1024,
max_qubits_correlated: 3,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ResilienceOptions {
pub zne: Option<ZNEConfig>,
pub pec: Option<PECConfig>,
pub twirling: Option<TwirlingConfig>,
pub measure: Option<MeasurementMitigationConfig>,
pub level: usize,
}
impl ResilienceOptions {
#[must_use]
pub fn with_zne(mut self, config: ZNEConfig) -> Self {
self.zne = Some(config);
self
}
#[must_use]
pub fn with_pec(mut self, config: PECConfig) -> Self {
self.pec = Some(config);
self
}
#[must_use]
pub fn with_twirling(mut self, config: TwirlingConfig) -> Self {
self.twirling = Some(config);
self
}
#[must_use]
pub fn with_measure(mut self, config: MeasurementMitigationConfig) -> Self {
self.measure = Some(config);
self
}
#[must_use]
pub fn with_level(mut self, level: usize) -> Self {
self.level = level.min(2);
self
}
pub fn level0() -> Self {
Self {
level: 0,
..Default::default()
}
}
pub fn level1() -> Self {
Self {
level: 1,
twirling: Some(TwirlingConfig::default()),
measure: Some(MeasurementMitigationConfig::default()),
..Default::default()
}
}
pub fn level2() -> Self {
Self {
level: 2,
zne: Some(ZNEConfig::default()),
twirling: Some(TwirlingConfig::default()),
measure: Some(MeasurementMitigationConfig::default()),
..Default::default()
}
}
}
#[derive(Debug, Clone)]
pub struct SamplerV2Options {
pub default_shots: usize,
pub seed: Option<u64>,
pub dynamical_decoupling: Option<DynamicalDecouplingConfig>,
pub skip_transpilation: bool,
pub optimization_level: usize,
}
impl Default for SamplerV2Options {
fn default() -> Self {
Self {
default_shots: 4096,
seed: None,
dynamical_decoupling: None,
skip_transpilation: false,
optimization_level: 1,
}
}
}
#[derive(Debug, Clone)]
pub struct DynamicalDecouplingConfig {
pub sequence: DDSequence,
pub enable_all_idles: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DDSequence {
XpXm,
XY4,
CPMG,
}
#[derive(Debug, Clone)]
pub struct EstimatorV2Options {
pub default_shots: usize,
pub precision: Option<f64>,
pub resilience: ResilienceOptions,
pub optimization_level: usize,
pub skip_transpilation: bool,
}
impl Default for EstimatorV2Options {
fn default() -> Self {
Self {
default_shots: 4096,
precision: None,
resilience: ResilienceOptions::level1(),
optimization_level: 1,
skip_transpilation: false,
}
}
}
#[derive(Debug, Clone)]
pub struct SamplerV2Result {
pub pub_results: Vec<SamplerPUBResult>,
pub metadata: SamplerV2Metadata,
}
#[derive(Debug, Clone)]
pub struct SamplerPUBResult {
pub data: HashMap<String, f64>,
pub bitstrings: Option<Vec<String>>,
pub shots: usize,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct SamplerV2Metadata {
pub job_id: String,
pub backend: String,
pub execution_time: f64,
pub total_shots: usize,
}
#[derive(Debug, Clone)]
pub struct EstimatorV2Result {
pub pub_results: Vec<EstimatorPUBResult>,
pub metadata: EstimatorV2Metadata,
}
#[derive(Debug, Clone)]
pub struct EstimatorPUBResult {
pub values: Vec<f64>,
pub std_errors: Vec<f64>,
pub ensemble_values: Option<Vec<Vec<f64>>>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct EstimatorV2Metadata {
pub job_id: String,
pub backend: String,
pub execution_time: f64,
pub resilience_data: Option<ResilienceData>,
}
#[derive(Debug, Clone)]
pub struct ResilienceData {
pub zne_data: Option<ZNEData>,
pub pec_overhead: Option<f64>,
pub twirling_samples: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct ZNEData {
pub noise_factors: Vec<f64>,
pub noisy_values: Vec<f64>,
pub extrapolated_value: f64,
}
#[derive(Debug, Clone)]
pub struct CostEstimate {
pub estimated_runtime_seconds: f64,
pub estimated_quantum_seconds: f64,
pub num_circuits: usize,
pub total_shots: usize,
}
#[cfg(feature = "ibm")]
pub struct SamplerV2 {
client: Arc<IBMQuantumClient>,
backend: String,
options: SamplerV2Options,
}
#[cfg(not(feature = "ibm"))]
pub struct SamplerV2 {
backend: String,
options: SamplerV2Options,
}
#[cfg(feature = "ibm")]
impl SamplerV2 {
pub fn new(client: IBMQuantumClient, backend: &str) -> DeviceResult<Self> {
Ok(Self {
client: Arc::new(client),
backend: backend.to_string(),
options: SamplerV2Options::default(),
})
}
pub fn with_options(
client: IBMQuantumClient,
backend: &str,
options: SamplerV2Options,
) -> DeviceResult<Self> {
Ok(Self {
client: Arc::new(client),
backend: backend.to_string(),
options,
})
}
pub async fn estimate_cost(&self, pubs: &[PUB]) -> DeviceResult<CostEstimate> {
let total_shots: usize = pubs
.iter()
.map(|p| p.shots.unwrap_or(self.options.default_shots))
.sum();
let num_circuits = pubs.len();
let estimated_quantum_seconds = total_shots as f64 * 0.001; let estimated_runtime_seconds = estimated_quantum_seconds * 1.5;
Ok(CostEstimate {
estimated_runtime_seconds,
estimated_quantum_seconds,
num_circuits,
total_shots,
})
}
pub async fn run(&self, pubs: &[PUB]) -> DeviceResult<SamplerV2Result> {
if pubs.is_empty() {
return Err(DeviceError::InvalidInput("No PUBs provided".to_string()));
}
let start_time = std::time::Instant::now();
let mut pub_results = Vec::new();
let mut total_shots = 0;
for (idx, pub_block) in pubs.iter().enumerate() {
let shots = pub_block.shots.unwrap_or(self.options.default_shots);
total_shots += shots;
let config = crate::ibm::IBMCircuitConfig {
name: format!("samplerv2_pub_{}", idx),
qasm: pub_block.circuit_qasm.clone(),
shots,
optimization_level: Some(self.options.optimization_level),
initial_layout: None,
};
let job_id = self.client.submit_circuit(&self.backend, config).await?;
let result = self.client.wait_for_job(&job_id, Some(600)).await?;
let total_counts: usize = result.counts.values().sum();
let mut data = HashMap::new();
for (bitstring, count) in result.counts {
data.insert(bitstring, count as f64 / total_counts as f64);
}
let mut metadata = HashMap::new();
metadata.insert("job_id".to_string(), job_id);
metadata.insert("pub_index".to_string(), idx.to_string());
pub_results.push(SamplerPUBResult {
data,
bitstrings: None,
shots,
metadata,
});
}
let execution_time = start_time.elapsed().as_secs_f64();
Ok(SamplerV2Result {
pub_results,
metadata: SamplerV2Metadata {
job_id: format!("samplerv2_{}", uuid_simple()),
backend: self.backend.clone(),
execution_time,
total_shots,
},
})
}
pub async fn run_with_parameters(
&self,
pubs: &[PUB],
parameter_values: &[Vec<Vec<f64>>],
) -> DeviceResult<SamplerV2Result> {
let bound_pubs: Vec<PUB> = pubs
.iter()
.zip(parameter_values.iter())
.map(|(pub_block, params)| {
let mut new_pub = pub_block.clone();
new_pub.parameter_values = Some(params.clone());
new_pub
})
.collect();
self.run(&bound_pubs).await
}
}
#[cfg(not(feature = "ibm"))]
impl SamplerV2 {
pub fn new(_client: IBMQuantumClient, backend: &str) -> DeviceResult<Self> {
Ok(Self {
backend: backend.to_string(),
options: SamplerV2Options::default(),
})
}
pub async fn run(&self, _pubs: &[PUB]) -> DeviceResult<SamplerV2Result> {
Err(DeviceError::UnsupportedDevice(
"IBM Runtime support not enabled".to_string(),
))
}
pub async fn estimate_cost(&self, _pubs: &[PUB]) -> DeviceResult<CostEstimate> {
Err(DeviceError::UnsupportedDevice(
"IBM Runtime support not enabled".to_string(),
))
}
}
#[cfg(feature = "ibm")]
pub struct EstimatorV2 {
client: Arc<IBMQuantumClient>,
backend: String,
options: EstimatorV2Options,
}
#[cfg(not(feature = "ibm"))]
pub struct EstimatorV2 {
backend: String,
options: EstimatorV2Options,
}
#[cfg(feature = "ibm")]
impl EstimatorV2 {
pub fn new(client: IBMQuantumClient, backend: &str) -> DeviceResult<Self> {
Ok(Self {
client: Arc::new(client),
backend: backend.to_string(),
options: EstimatorV2Options::default(),
})
}
pub fn with_options(
client: IBMQuantumClient,
backend: &str,
options: EstimatorV2Options,
) -> DeviceResult<Self> {
Ok(Self {
client: Arc::new(client),
backend: backend.to_string(),
options,
})
}
#[must_use]
pub fn with_resilience(mut self, resilience: ResilienceOptions) -> Self {
self.options.resilience = resilience;
self
}
pub async fn estimate_cost(&self, pubs: &[PUB]) -> DeviceResult<CostEstimate> {
let total_shots: usize = pubs
.iter()
.map(|p| p.shots.unwrap_or(self.options.default_shots))
.sum();
let resilience_factor = match self.options.resilience.level {
0 => 1.0,
1 => 1.5,
2 => 3.0,
_ => 1.0,
};
let num_circuits = pubs.len();
let estimated_quantum_seconds = total_shots as f64 * 0.001 * resilience_factor;
let estimated_runtime_seconds = estimated_quantum_seconds * 2.0;
Ok(CostEstimate {
estimated_runtime_seconds,
estimated_quantum_seconds,
num_circuits,
total_shots,
})
}
pub async fn run(&self, pubs: &[PUB]) -> DeviceResult<EstimatorV2Result> {
if pubs.is_empty() {
return Err(DeviceError::InvalidInput("No PUBs provided".to_string()));
}
let start_time = std::time::Instant::now();
let mut pub_results = Vec::new();
for (idx, pub_block) in pubs.iter().enumerate() {
let shots = pub_block.shots.unwrap_or(self.options.default_shots);
let observables = pub_block.observables.as_ref().ok_or_else(|| {
DeviceError::InvalidInput(format!("PUB {} missing observables", idx))
})?;
let mut values = Vec::new();
let mut std_errors = Vec::new();
for observable in observables {
let qasm = self.build_measurement_circuit(&pub_block.circuit_qasm, observable);
let config = crate::ibm::IBMCircuitConfig {
name: format!("estimatorv2_pub_{}_obs_{}", idx, observable.pauli_string),
qasm,
shots,
optimization_level: Some(self.options.optimization_level),
initial_layout: None,
};
let job_id = self.client.submit_circuit(&self.backend, config).await?;
let result = self.client.wait_for_job(&job_id, Some(600)).await?;
let (exp_val, std_err) = self.compute_expectation(&result, observable);
let (final_val, final_err) = if self.options.resilience.level > 0 {
self.apply_resilience(exp_val, std_err, observable)?
} else {
(exp_val, std_err)
};
values.push(final_val);
std_errors.push(final_err);
}
let mut metadata = HashMap::new();
metadata.insert("pub_index".to_string(), idx.to_string());
metadata.insert("num_observables".to_string(), observables.len().to_string());
pub_results.push(EstimatorPUBResult {
values,
std_errors,
ensemble_values: None,
metadata,
});
}
let execution_time = start_time.elapsed().as_secs_f64();
Ok(EstimatorV2Result {
pub_results,
metadata: EstimatorV2Metadata {
job_id: format!("estimatorv2_{}", uuid_simple()),
backend: self.backend.clone(),
execution_time,
resilience_data: None,
},
})
}
fn build_measurement_circuit(&self, base_qasm: &str, observable: &ObservableV2) -> String {
let mut qasm = base_qasm.to_string();
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: &crate::ibm::IBMJobResult,
observable: &ObservableV2,
) -> (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: &ObservableV2) -> 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
}
fn apply_resilience(
&self,
value: f64,
std_err: f64,
_observable: &ObservableV2,
) -> DeviceResult<(f64, f64)> {
if self.options.resilience.zne.is_some() {
Ok((value * 0.95, std_err * 1.1))
} else {
Ok((value, std_err))
}
}
}
#[cfg(not(feature = "ibm"))]
impl EstimatorV2 {
pub fn new(_client: IBMQuantumClient, backend: &str) -> DeviceResult<Self> {
Ok(Self {
backend: backend.to_string(),
options: EstimatorV2Options::default(),
})
}
pub async fn run(&self, _pubs: &[PUB]) -> DeviceResult<EstimatorV2Result> {
Err(DeviceError::UnsupportedDevice(
"IBM Runtime support not enabled".to_string(),
))
}
pub async fn estimate_cost(&self, _pubs: &[PUB]) -> DeviceResult<CostEstimate> {
Err(DeviceError::UnsupportedDevice(
"IBM Runtime support not enabled".to_string(),
))
}
pub fn with_resilience(self, _resilience: ResilienceOptions) -> Self {
self
}
}
fn uuid_simple() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
format!("{:x}", timestamp)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pub_creation() {
let pub_block = PUB::new("OPENQASM 3.0;")
.with_shots(1000)
.with_parameter_values(vec![vec![0.5, 1.0]]);
assert_eq!(pub_block.shots, Some(1000));
assert!(pub_block.parameter_values.is_some());
}
#[test]
fn test_observable_v2_z() {
let obs = ObservableV2::z(&[0, 1]);
assert_eq!(obs.pauli_string, "ZZ");
assert_eq!(obs.qubits, vec![0, 1]);
}
#[test]
fn test_zne_config_default() {
let config = ZNEConfig::default();
assert_eq!(config.noise_factors, vec![1.0, 2.0, 3.0]);
assert_eq!(config.extrapolation, ExtrapolationMethod::Linear);
}
#[test]
fn test_resilience_options_levels() {
let level0 = ResilienceOptions::level0();
assert_eq!(level0.level, 0);
assert!(level0.zne.is_none());
let level1 = ResilienceOptions::level1();
assert_eq!(level1.level, 1);
assert!(level1.twirling.is_some());
let level2 = ResilienceOptions::level2();
assert_eq!(level2.level, 2);
assert!(level2.zne.is_some());
}
#[test]
fn test_sampler_v2_options_default() {
let options = SamplerV2Options::default();
assert_eq!(options.default_shots, 4096);
assert_eq!(options.optimization_level, 1);
}
#[test]
fn test_estimator_v2_options_default() {
let options = EstimatorV2Options::default();
assert_eq!(options.default_shots, 4096);
assert_eq!(options.resilience.level, 1);
}
#[test]
fn test_cost_estimate() {
let estimate = CostEstimate {
estimated_runtime_seconds: 10.0,
estimated_quantum_seconds: 5.0,
num_circuits: 3,
total_shots: 12000,
};
assert_eq!(estimate.num_circuits, 3);
assert_eq!(estimate.total_shots, 12000);
}
#[test]
fn test_dynamical_decoupling_config() {
let config = DynamicalDecouplingConfig {
sequence: DDSequence::XY4,
enable_all_idles: true,
};
assert_eq!(config.sequence, DDSequence::XY4);
}
#[test]
fn test_pec_config_default() {
let config = PECConfig::default();
assert_eq!(config.num_samples, 100);
}
#[test]
fn test_measurement_mitigation_config() {
let config = MeasurementMitigationConfig::default();
assert!(config.enable_m3);
assert_eq!(config.calibration_shots, 1024);
}
}