quantrs2_device/
ibm_runtime_v2.rs

1//! IBM Qiskit Runtime v2 Primitives
2//!
3//! This module provides the v2 API for IBM Runtime primitives, which includes:
4//! - `SamplerV2`: Non-session batch sampling with PUBs
5//! - `EstimatorV2`: Enhanced estimation with advanced error mitigation
6//! - PUBs (Primitive Unified Blocks): Bundled circuit/parameter/observable specifications
7//!
8//! ## Key Differences from v1
9//!
10//! - **No mandatory session**: Can run without a session context
11//! - **PUBs**: Structured input format for batching
12//! - **Enhanced error mitigation**: ZNE, PEC, twirling options
13//! - **Cost estimation**: Pre-execution cost estimation
14//!
15//! ## Example
16//!
17//! ```rust,ignore
18//! use quantrs2_device::ibm_runtime_v2::{SamplerV2, EstimatorV2, PUB, ResilienceOptions};
19//!
20//! // Create a PUB (Primitive Unified Block)
21//! let pub1 = PUB::new(circuit)
22//!     .with_parameter_values(vec![0.5, 1.0])
23//!     .with_shots(4096);
24//!
25//! // Use SamplerV2 without session
26//! let sampler = SamplerV2::new(client, "ibm_brisbane")?;
27//! let result = sampler.run(&[pub1]).await?;
28//!
29//! // Use EstimatorV2 with enhanced error mitigation
30//! let options = ResilienceOptions::default()
31//!     .with_zne(ZNEConfig::default())
32//!     .with_twirling(TwirlingConfig::default());
33//!
34//! let estimator = EstimatorV2::new(client, "ibm_brisbane")?
35//!     .with_resilience(options);
36//! let expectation = estimator.run(&pubs, &observables).await?;
37//! ```
38
39use std::collections::HashMap;
40use std::sync::Arc;
41
42use crate::ibm::IBMQuantumClient;
43use crate::{DeviceError, DeviceResult};
44
45/// Primitive Unified Block (PUB) for v2 primitives
46///
47/// A PUB bundles a circuit with its parameter values and execution options
48#[derive(Debug, Clone)]
49pub struct PUB {
50    /// Circuit in QASM 3.0 format
51    pub circuit_qasm: String,
52    /// Parameter values for the circuit (if parametrized)
53    pub parameter_values: Option<Vec<Vec<f64>>>,
54    /// Number of shots for this PUB
55    pub shots: Option<usize>,
56    /// Observable specification (for EstimatorV2)
57    pub observables: Option<Vec<ObservableV2>>,
58}
59
60impl PUB {
61    /// Create a new PUB from QASM 3.0 circuit string
62    pub fn new(circuit_qasm: impl Into<String>) -> Self {
63        Self {
64            circuit_qasm: circuit_qasm.into(),
65            parameter_values: None,
66            shots: None,
67            observables: None,
68        }
69    }
70
71    /// Create a PUB from a quantrs2 circuit
72    ///
73    /// Returns an error if circuit conversion fails
74    pub fn from_circuit<const N: usize>(
75        circuit: &quantrs2_circuit::prelude::Circuit<N>,
76    ) -> crate::DeviceResult<Self> {
77        // Convert circuit to QASM 3.0
78        let qasm_circuit = crate::qasm3::circuit_to_qasm3(circuit)?;
79        Ok(Self::new(qasm_circuit.to_string()))
80    }
81
82    /// Add parameter values
83    #[must_use]
84    pub fn with_parameter_values(mut self, values: Vec<Vec<f64>>) -> Self {
85        self.parameter_values = Some(values);
86        self
87    }
88
89    /// Set the number of shots
90    #[must_use]
91    pub fn with_shots(mut self, shots: usize) -> Self {
92        self.shots = Some(shots);
93        self
94    }
95
96    /// Add observables (for EstimatorV2)
97    #[must_use]
98    pub fn with_observables(mut self, observables: Vec<ObservableV2>) -> Self {
99        self.observables = Some(observables);
100        self
101    }
102}
103
104/// Observable specification for EstimatorV2
105#[derive(Debug, Clone)]
106pub struct ObservableV2 {
107    /// Pauli string representation
108    pub pauli_string: String,
109    /// Coefficient
110    pub coefficient: f64,
111    /// Target qubits
112    pub qubits: Vec<usize>,
113}
114
115impl ObservableV2 {
116    /// Create a Pauli Z observable
117    pub fn z(qubits: &[usize]) -> Self {
118        Self {
119            pauli_string: qubits.iter().map(|_| 'Z').collect(),
120            coefficient: 1.0,
121            qubits: qubits.to_vec(),
122        }
123    }
124
125    /// Create a Pauli X observable
126    pub fn x(qubits: &[usize]) -> Self {
127        Self {
128            pauli_string: qubits.iter().map(|_| 'X').collect(),
129            coefficient: 1.0,
130            qubits: qubits.to_vec(),
131        }
132    }
133
134    /// Create a Pauli Y observable
135    pub fn y(qubits: &[usize]) -> Self {
136        Self {
137            pauli_string: qubits.iter().map(|_| 'Y').collect(),
138            coefficient: 1.0,
139            qubits: qubits.to_vec(),
140        }
141    }
142
143    /// Create a custom Pauli observable
144    pub fn pauli(pauli_string: &str, qubits: &[usize], coefficient: f64) -> Self {
145        Self {
146            pauli_string: pauli_string.to_string(),
147            coefficient,
148            qubits: qubits.to_vec(),
149        }
150    }
151}
152
153/// Zero-noise extrapolation configuration
154#[derive(Debug, Clone)]
155pub struct ZNEConfig {
156    /// Noise scaling factors
157    pub noise_factors: Vec<f64>,
158    /// Extrapolation method
159    pub extrapolation: ExtrapolationMethod,
160    /// Number of samples per noise factor
161    pub samples_per_factor: usize,
162}
163
164impl Default for ZNEConfig {
165    fn default() -> Self {
166        Self {
167            noise_factors: vec![1.0, 2.0, 3.0],
168            extrapolation: ExtrapolationMethod::Linear,
169            samples_per_factor: 1,
170        }
171    }
172}
173
174/// Extrapolation method for ZNE
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum ExtrapolationMethod {
177    /// Linear extrapolation
178    Linear,
179    /// Polynomial extrapolation
180    Polynomial,
181    /// Exponential extrapolation
182    Exponential,
183    /// Richardson extrapolation
184    Richardson,
185}
186
187/// Probabilistic error cancellation configuration
188#[derive(Debug, Clone)]
189pub struct PECConfig {
190    /// Number of PEC samples
191    pub num_samples: usize,
192    /// Maximum noise strength
193    pub max_noise_strength: f64,
194}
195
196impl Default for PECConfig {
197    fn default() -> Self {
198        Self {
199            num_samples: 100,
200            max_noise_strength: 0.1,
201        }
202    }
203}
204
205/// Twirling configuration for error mitigation
206#[derive(Debug, Clone)]
207pub struct TwirlingConfig {
208    /// Enable Pauli twirling
209    pub enable_pauli_twirling: bool,
210    /// Number of twirling samples
211    pub num_randomizations: usize,
212    /// Gates to twirl
213    pub gates_to_twirl: Vec<String>,
214}
215
216impl Default for TwirlingConfig {
217    fn default() -> Self {
218        Self {
219            enable_pauli_twirling: true,
220            num_randomizations: 32,
221            gates_to_twirl: vec!["cx".to_string(), "cz".to_string()],
222        }
223    }
224}
225
226/// Measurement error mitigation configuration
227#[derive(Debug, Clone)]
228pub struct MeasurementMitigationConfig {
229    /// Enable matrix-free measurement mitigation (M3)
230    pub enable_m3: bool,
231    /// Number of calibration shots
232    pub calibration_shots: usize,
233    /// Maximum number of qubits for correlated mitigation
234    pub max_qubits_correlated: usize,
235}
236
237impl Default for MeasurementMitigationConfig {
238    fn default() -> Self {
239        Self {
240            enable_m3: true,
241            calibration_shots: 1024,
242            max_qubits_correlated: 3,
243        }
244    }
245}
246
247/// Resilience options for error mitigation
248#[derive(Debug, Clone, Default)]
249pub struct ResilienceOptions {
250    /// Zero-noise extrapolation configuration
251    pub zne: Option<ZNEConfig>,
252    /// Probabilistic error cancellation configuration
253    pub pec: Option<PECConfig>,
254    /// Twirling configuration
255    pub twirling: Option<TwirlingConfig>,
256    /// Measurement error mitigation configuration
257    pub measure: Option<MeasurementMitigationConfig>,
258    /// Resilience level (0-2)
259    pub level: usize,
260}
261
262impl ResilienceOptions {
263    /// Create options with ZNE enabled
264    #[must_use]
265    pub fn with_zne(mut self, config: ZNEConfig) -> Self {
266        self.zne = Some(config);
267        self
268    }
269
270    /// Create options with PEC enabled
271    #[must_use]
272    pub fn with_pec(mut self, config: PECConfig) -> Self {
273        self.pec = Some(config);
274        self
275    }
276
277    /// Create options with twirling enabled
278    #[must_use]
279    pub fn with_twirling(mut self, config: TwirlingConfig) -> Self {
280        self.twirling = Some(config);
281        self
282    }
283
284    /// Create options with measurement mitigation enabled
285    #[must_use]
286    pub fn with_measure(mut self, config: MeasurementMitigationConfig) -> Self {
287        self.measure = Some(config);
288        self
289    }
290
291    /// Set the resilience level
292    #[must_use]
293    pub fn with_level(mut self, level: usize) -> Self {
294        self.level = level.min(2);
295        self
296    }
297
298    /// Create level 0 options (no mitigation)
299    pub fn level0() -> Self {
300        Self {
301            level: 0,
302            ..Default::default()
303        }
304    }
305
306    /// Create level 1 options (basic mitigation)
307    pub fn level1() -> Self {
308        Self {
309            level: 1,
310            twirling: Some(TwirlingConfig::default()),
311            measure: Some(MeasurementMitigationConfig::default()),
312            ..Default::default()
313        }
314    }
315
316    /// Create level 2 options (full mitigation)
317    pub fn level2() -> Self {
318        Self {
319            level: 2,
320            zne: Some(ZNEConfig::default()),
321            twirling: Some(TwirlingConfig::default()),
322            measure: Some(MeasurementMitigationConfig::default()),
323            ..Default::default()
324        }
325    }
326}
327
328/// Options for SamplerV2
329#[derive(Debug, Clone)]
330pub struct SamplerV2Options {
331    /// Default number of shots
332    pub default_shots: usize,
333    /// Seed for reproducibility
334    pub seed: Option<u64>,
335    /// Dynamical decoupling sequence
336    pub dynamical_decoupling: Option<DynamicalDecouplingConfig>,
337    /// Skip transpilation
338    pub skip_transpilation: bool,
339    /// Optimization level (0-3)
340    pub optimization_level: usize,
341}
342
343impl Default for SamplerV2Options {
344    fn default() -> Self {
345        Self {
346            default_shots: 4096,
347            seed: None,
348            dynamical_decoupling: None,
349            skip_transpilation: false,
350            optimization_level: 1,
351        }
352    }
353}
354
355/// Dynamical decoupling configuration
356#[derive(Debug, Clone)]
357pub struct DynamicalDecouplingConfig {
358    /// DD sequence type
359    pub sequence: DDSequence,
360    /// Enable DD on all idle periods
361    pub enable_all_idles: bool,
362}
363
364/// Dynamical decoupling sequence types
365#[derive(Debug, Clone, Copy, PartialEq, Eq)]
366pub enum DDSequence {
367    /// XpXm sequence
368    XpXm,
369    /// XY4 sequence
370    XY4,
371    /// CPMG sequence
372    CPMG,
373}
374
375/// Options for EstimatorV2
376#[derive(Debug, Clone)]
377pub struct EstimatorV2Options {
378    /// Default number of shots
379    pub default_shots: usize,
380    /// Precision target (stopping criterion)
381    pub precision: Option<f64>,
382    /// Resilience options
383    pub resilience: ResilienceOptions,
384    /// Optimization level
385    pub optimization_level: usize,
386    /// Skip transpilation
387    pub skip_transpilation: bool,
388}
389
390impl Default for EstimatorV2Options {
391    fn default() -> Self {
392        Self {
393            default_shots: 4096,
394            precision: None,
395            resilience: ResilienceOptions::level1(),
396            optimization_level: 1,
397            skip_transpilation: false,
398        }
399    }
400}
401
402/// Result from SamplerV2 execution
403#[derive(Debug, Clone)]
404pub struct SamplerV2Result {
405    /// Per-PUB results
406    pub pub_results: Vec<SamplerPUBResult>,
407    /// Global metadata
408    pub metadata: SamplerV2Metadata,
409}
410
411/// Result for a single PUB
412#[derive(Debug, Clone)]
413pub struct SamplerPUBResult {
414    /// Quasi-probability distribution
415    pub data: HashMap<String, f64>,
416    /// Bit-packed samples (optional)
417    pub bitstrings: Option<Vec<String>>,
418    /// Number of shots
419    pub shots: usize,
420    /// Per-PUB metadata
421    pub metadata: HashMap<String, String>,
422}
423
424/// Metadata for SamplerV2 execution
425#[derive(Debug, Clone)]
426pub struct SamplerV2Metadata {
427    /// Job ID
428    pub job_id: String,
429    /// Backend name
430    pub backend: String,
431    /// Execution time (seconds)
432    pub execution_time: f64,
433    /// Total number of shots
434    pub total_shots: usize,
435}
436
437/// Result from EstimatorV2 execution
438#[derive(Debug, Clone)]
439pub struct EstimatorV2Result {
440    /// Per-PUB results
441    pub pub_results: Vec<EstimatorPUBResult>,
442    /// Global metadata
443    pub metadata: EstimatorV2Metadata,
444}
445
446/// Result for a single PUB (EstimatorV2)
447#[derive(Debug, Clone)]
448pub struct EstimatorPUBResult {
449    /// Expectation values for each observable
450    pub values: Vec<f64>,
451    /// Standard errors
452    pub std_errors: Vec<f64>,
453    /// Ensemble values (for ZNE/PEC)
454    pub ensemble_values: Option<Vec<Vec<f64>>>,
455    /// Per-PUB metadata
456    pub metadata: HashMap<String, String>,
457}
458
459/// Metadata for EstimatorV2 execution
460#[derive(Debug, Clone)]
461pub struct EstimatorV2Metadata {
462    /// Job ID
463    pub job_id: String,
464    /// Backend name
465    pub backend: String,
466    /// Execution time (seconds)
467    pub execution_time: f64,
468    /// Resilience data
469    pub resilience_data: Option<ResilienceData>,
470}
471
472/// Resilience processing data
473#[derive(Debug, Clone)]
474pub struct ResilienceData {
475    /// ZNE extrapolation data
476    pub zne_data: Option<ZNEData>,
477    /// PEC overhead
478    pub pec_overhead: Option<f64>,
479    /// Twirling samples used
480    pub twirling_samples: Option<usize>,
481}
482
483/// ZNE extrapolation data
484#[derive(Debug, Clone)]
485pub struct ZNEData {
486    /// Noise factors used
487    pub noise_factors: Vec<f64>,
488    /// Values at each noise factor
489    pub noisy_values: Vec<f64>,
490    /// Extrapolated value
491    pub extrapolated_value: f64,
492}
493
494/// Cost estimate for a job
495#[derive(Debug, Clone)]
496pub struct CostEstimate {
497    /// Estimated runtime in seconds
498    pub estimated_runtime_seconds: f64,
499    /// Estimated quantum seconds
500    pub estimated_quantum_seconds: f64,
501    /// Number of circuits
502    pub num_circuits: usize,
503    /// Total shots
504    pub total_shots: usize,
505}
506
507/// SamplerV2 primitive for v2 API
508#[cfg(feature = "ibm")]
509pub struct SamplerV2 {
510    /// IBM Quantum client
511    client: Arc<IBMQuantumClient>,
512    /// Backend name
513    backend: String,
514    /// Sampler options
515    options: SamplerV2Options,
516}
517
518#[cfg(not(feature = "ibm"))]
519pub struct SamplerV2 {
520    /// Backend name
521    backend: String,
522    /// Sampler options
523    options: SamplerV2Options,
524}
525
526#[cfg(feature = "ibm")]
527impl SamplerV2 {
528    /// Create a new SamplerV2
529    pub fn new(client: IBMQuantumClient, backend: &str) -> DeviceResult<Self> {
530        Ok(Self {
531            client: Arc::new(client),
532            backend: backend.to_string(),
533            options: SamplerV2Options::default(),
534        })
535    }
536
537    /// Create with custom options
538    pub fn with_options(
539        client: IBMQuantumClient,
540        backend: &str,
541        options: SamplerV2Options,
542    ) -> DeviceResult<Self> {
543        Ok(Self {
544            client: Arc::new(client),
545            backend: backend.to_string(),
546            options,
547        })
548    }
549
550    /// Get cost estimate for a job
551    pub async fn estimate_cost(&self, pubs: &[PUB]) -> DeviceResult<CostEstimate> {
552        let total_shots: usize = pubs
553            .iter()
554            .map(|p| p.shots.unwrap_or(self.options.default_shots))
555            .sum();
556
557        // Rough estimate based on circuit complexity and shots
558        let num_circuits = pubs.len();
559        let estimated_quantum_seconds = total_shots as f64 * 0.001; // ~1ms per shot
560        let estimated_runtime_seconds = estimated_quantum_seconds * 1.5; // 50% overhead
561
562        Ok(CostEstimate {
563            estimated_runtime_seconds,
564            estimated_quantum_seconds,
565            num_circuits,
566            total_shots,
567        })
568    }
569
570    /// Run the sampler on PUBs
571    pub async fn run(&self, pubs: &[PUB]) -> DeviceResult<SamplerV2Result> {
572        if pubs.is_empty() {
573            return Err(DeviceError::InvalidInput("No PUBs provided".to_string()));
574        }
575
576        let start_time = std::time::Instant::now();
577        let mut pub_results = Vec::new();
578        let mut total_shots = 0;
579
580        for (idx, pub_block) in pubs.iter().enumerate() {
581            let shots = pub_block.shots.unwrap_or(self.options.default_shots);
582            total_shots += shots;
583
584            // Submit circuit to IBM Runtime
585            let config = crate::ibm::IBMCircuitConfig {
586                name: format!("samplerv2_pub_{}", idx),
587                qasm: pub_block.circuit_qasm.clone(),
588                shots,
589                optimization_level: Some(self.options.optimization_level),
590                initial_layout: None,
591            };
592
593            let job_id = self.client.submit_circuit(&self.backend, config).await?;
594            let result = self.client.wait_for_job(&job_id, Some(600)).await?;
595
596            // Convert counts to quasi-probability distribution
597            let total_counts: usize = result.counts.values().sum();
598            let mut data = HashMap::new();
599            for (bitstring, count) in result.counts {
600                data.insert(bitstring, count as f64 / total_counts as f64);
601            }
602
603            let mut metadata = HashMap::new();
604            metadata.insert("job_id".to_string(), job_id);
605            metadata.insert("pub_index".to_string(), idx.to_string());
606
607            pub_results.push(SamplerPUBResult {
608                data,
609                bitstrings: None,
610                shots,
611                metadata,
612            });
613        }
614
615        let execution_time = start_time.elapsed().as_secs_f64();
616
617        Ok(SamplerV2Result {
618            pub_results,
619            metadata: SamplerV2Metadata {
620                job_id: format!("samplerv2_{}", uuid_simple()),
621                backend: self.backend.clone(),
622                execution_time,
623                total_shots,
624            },
625        })
626    }
627
628    /// Run with explicit parameter binding
629    pub async fn run_with_parameters(
630        &self,
631        pubs: &[PUB],
632        parameter_values: &[Vec<Vec<f64>>],
633    ) -> DeviceResult<SamplerV2Result> {
634        // Create new PUBs with bound parameters
635        let bound_pubs: Vec<PUB> = pubs
636            .iter()
637            .zip(parameter_values.iter())
638            .map(|(pub_block, params)| {
639                let mut new_pub = pub_block.clone();
640                new_pub.parameter_values = Some(params.clone());
641                new_pub
642            })
643            .collect();
644
645        self.run(&bound_pubs).await
646    }
647}
648
649#[cfg(not(feature = "ibm"))]
650impl SamplerV2 {
651    pub fn new(_client: IBMQuantumClient, backend: &str) -> DeviceResult<Self> {
652        Ok(Self {
653            backend: backend.to_string(),
654            options: SamplerV2Options::default(),
655        })
656    }
657
658    pub async fn run(&self, _pubs: &[PUB]) -> DeviceResult<SamplerV2Result> {
659        Err(DeviceError::UnsupportedDevice(
660            "IBM Runtime support not enabled".to_string(),
661        ))
662    }
663
664    pub async fn estimate_cost(&self, _pubs: &[PUB]) -> DeviceResult<CostEstimate> {
665        Err(DeviceError::UnsupportedDevice(
666            "IBM Runtime support not enabled".to_string(),
667        ))
668    }
669}
670
671/// EstimatorV2 primitive for v2 API
672#[cfg(feature = "ibm")]
673pub struct EstimatorV2 {
674    /// IBM Quantum client
675    client: Arc<IBMQuantumClient>,
676    /// Backend name
677    backend: String,
678    /// Estimator options
679    options: EstimatorV2Options,
680}
681
682#[cfg(not(feature = "ibm"))]
683pub struct EstimatorV2 {
684    /// Backend name
685    backend: String,
686    /// Estimator options
687    options: EstimatorV2Options,
688}
689
690#[cfg(feature = "ibm")]
691impl EstimatorV2 {
692    /// Create a new EstimatorV2
693    pub fn new(client: IBMQuantumClient, backend: &str) -> DeviceResult<Self> {
694        Ok(Self {
695            client: Arc::new(client),
696            backend: backend.to_string(),
697            options: EstimatorV2Options::default(),
698        })
699    }
700
701    /// Create with custom options
702    pub fn with_options(
703        client: IBMQuantumClient,
704        backend: &str,
705        options: EstimatorV2Options,
706    ) -> DeviceResult<Self> {
707        Ok(Self {
708            client: Arc::new(client),
709            backend: backend.to_string(),
710            options,
711        })
712    }
713
714    /// Set resilience options
715    #[must_use]
716    pub fn with_resilience(mut self, resilience: ResilienceOptions) -> Self {
717        self.options.resilience = resilience;
718        self
719    }
720
721    /// Get cost estimate for a job
722    pub async fn estimate_cost(&self, pubs: &[PUB]) -> DeviceResult<CostEstimate> {
723        let total_shots: usize = pubs
724            .iter()
725            .map(|p| p.shots.unwrap_or(self.options.default_shots))
726            .sum();
727
728        // Estimate includes resilience overhead
729        let resilience_factor = match self.options.resilience.level {
730            0 => 1.0,
731            1 => 1.5,
732            2 => 3.0,
733            _ => 1.0,
734        };
735
736        let num_circuits = pubs.len();
737        let estimated_quantum_seconds = total_shots as f64 * 0.001 * resilience_factor;
738        let estimated_runtime_seconds = estimated_quantum_seconds * 2.0;
739
740        Ok(CostEstimate {
741            estimated_runtime_seconds,
742            estimated_quantum_seconds,
743            num_circuits,
744            total_shots,
745        })
746    }
747
748    /// Run the estimator on PUBs
749    pub async fn run(&self, pubs: &[PUB]) -> DeviceResult<EstimatorV2Result> {
750        if pubs.is_empty() {
751            return Err(DeviceError::InvalidInput("No PUBs provided".to_string()));
752        }
753
754        let start_time = std::time::Instant::now();
755        let mut pub_results = Vec::new();
756
757        for (idx, pub_block) in pubs.iter().enumerate() {
758            let shots = pub_block.shots.unwrap_or(self.options.default_shots);
759            let observables = pub_block.observables.as_ref().ok_or_else(|| {
760                DeviceError::InvalidInput(format!("PUB {} missing observables", idx))
761            })?;
762
763            let mut values = Vec::new();
764            let mut std_errors = Vec::new();
765
766            for observable in observables {
767                // Build measurement circuit for this observable
768                let qasm = self.build_measurement_circuit(&pub_block.circuit_qasm, observable);
769
770                let config = crate::ibm::IBMCircuitConfig {
771                    name: format!("estimatorv2_pub_{}_obs_{}", idx, observable.pauli_string),
772                    qasm,
773                    shots,
774                    optimization_level: Some(self.options.optimization_level),
775                    initial_layout: None,
776                };
777
778                let job_id = self.client.submit_circuit(&self.backend, config).await?;
779                let result = self.client.wait_for_job(&job_id, Some(600)).await?;
780
781                // Compute expectation value
782                let (exp_val, std_err) = self.compute_expectation(&result, observable);
783
784                // Apply resilience (if enabled)
785                let (final_val, final_err) = if self.options.resilience.level > 0 {
786                    self.apply_resilience(exp_val, std_err, observable)?
787                } else {
788                    (exp_val, std_err)
789                };
790
791                values.push(final_val);
792                std_errors.push(final_err);
793            }
794
795            let mut metadata = HashMap::new();
796            metadata.insert("pub_index".to_string(), idx.to_string());
797            metadata.insert("num_observables".to_string(), observables.len().to_string());
798
799            pub_results.push(EstimatorPUBResult {
800                values,
801                std_errors,
802                ensemble_values: None,
803                metadata,
804            });
805        }
806
807        let execution_time = start_time.elapsed().as_secs_f64();
808
809        Ok(EstimatorV2Result {
810            pub_results,
811            metadata: EstimatorV2Metadata {
812                job_id: format!("estimatorv2_{}", uuid_simple()),
813                backend: self.backend.clone(),
814                execution_time,
815                resilience_data: None,
816            },
817        })
818    }
819
820    /// Build measurement circuit for observable
821    fn build_measurement_circuit(&self, base_qasm: &str, observable: &ObservableV2) -> String {
822        let mut qasm = base_qasm.to_string();
823
824        // Add basis rotation gates
825        for (i, pauli) in observable.pauli_string.chars().enumerate() {
826            if i < observable.qubits.len() {
827                let qubit = observable.qubits[i];
828                match pauli {
829                    'X' => qasm.push_str(&format!("h q[{}];\n", qubit)),
830                    'Y' => {
831                        qasm.push_str(&format!("sdg q[{}];\n", qubit));
832                        qasm.push_str(&format!("h q[{}];\n", qubit));
833                    }
834                    'Z' | 'I' => {}
835                    _ => {}
836                }
837            }
838        }
839
840        // Add measurements
841        for (i, qubit) in observable.qubits.iter().enumerate() {
842            qasm.push_str(&format!("measure q[{}] -> c[{}];\n", qubit, i));
843        }
844
845        qasm
846    }
847
848    /// Compute expectation value from counts
849    fn compute_expectation(
850        &self,
851        result: &crate::ibm::IBMJobResult,
852        observable: &ObservableV2,
853    ) -> (f64, f64) {
854        let total_shots: usize = result.counts.values().sum();
855        if total_shots == 0 {
856            return (0.0, 0.0);
857        }
858
859        let mut expectation = 0.0;
860        let mut squared_sum = 0.0;
861
862        for (bitstring, count) in &result.counts {
863            let eigenvalue = self.compute_eigenvalue(bitstring, observable);
864            let probability = *count as f64 / total_shots as f64;
865
866            expectation += eigenvalue * probability;
867            squared_sum += eigenvalue.powi(2) * probability;
868        }
869
870        expectation *= observable.coefficient;
871
872        let variance = squared_sum - expectation.powi(2);
873        let std_error = (variance / total_shots as f64).sqrt();
874
875        (expectation, std_error)
876    }
877
878    /// Compute eigenvalue for a bitstring
879    fn compute_eigenvalue(&self, bitstring: &str, observable: &ObservableV2) -> f64 {
880        let mut eigenvalue = 1.0;
881
882        for (i, pauli) in observable.pauli_string.chars().enumerate() {
883            if i < bitstring.len() && pauli != 'I' {
884                let bit = bitstring.chars().rev().nth(i).unwrap_or('0');
885                if bit == '1' {
886                    eigenvalue *= -1.0;
887                }
888            }
889        }
890
891        eigenvalue
892    }
893
894    /// Apply resilience techniques
895    fn apply_resilience(
896        &self,
897        value: f64,
898        std_err: f64,
899        _observable: &ObservableV2,
900    ) -> DeviceResult<(f64, f64)> {
901        // Placeholder for actual resilience implementation
902        // In production, this would apply ZNE, PEC, or twirling
903
904        if self.options.resilience.zne.is_some() {
905            // ZNE would extrapolate to zero noise
906            // For now, just return slightly adjusted values
907            Ok((value * 0.95, std_err * 1.1))
908        } else {
909            Ok((value, std_err))
910        }
911    }
912}
913
914#[cfg(not(feature = "ibm"))]
915impl EstimatorV2 {
916    pub fn new(_client: IBMQuantumClient, backend: &str) -> DeviceResult<Self> {
917        Ok(Self {
918            backend: backend.to_string(),
919            options: EstimatorV2Options::default(),
920        })
921    }
922
923    pub async fn run(&self, _pubs: &[PUB]) -> DeviceResult<EstimatorV2Result> {
924        Err(DeviceError::UnsupportedDevice(
925            "IBM Runtime support not enabled".to_string(),
926        ))
927    }
928
929    pub async fn estimate_cost(&self, _pubs: &[PUB]) -> DeviceResult<CostEstimate> {
930        Err(DeviceError::UnsupportedDevice(
931            "IBM Runtime support not enabled".to_string(),
932        ))
933    }
934
935    pub fn with_resilience(self, _resilience: ResilienceOptions) -> Self {
936        self
937    }
938}
939
940/// Generate a simple UUID-like string
941fn uuid_simple() -> String {
942    use std::time::{SystemTime, UNIX_EPOCH};
943    let timestamp = SystemTime::now()
944        .duration_since(UNIX_EPOCH)
945        .map(|d| d.as_nanos())
946        .unwrap_or(0);
947    format!("{:x}", timestamp)
948}
949
950#[cfg(test)]
951mod tests {
952    use super::*;
953
954    #[test]
955    fn test_pub_creation() {
956        let pub_block = PUB::new("OPENQASM 3.0;")
957            .with_shots(1000)
958            .with_parameter_values(vec![vec![0.5, 1.0]]);
959
960        assert_eq!(pub_block.shots, Some(1000));
961        assert!(pub_block.parameter_values.is_some());
962    }
963
964    #[test]
965    fn test_observable_v2_z() {
966        let obs = ObservableV2::z(&[0, 1]);
967        assert_eq!(obs.pauli_string, "ZZ");
968        assert_eq!(obs.qubits, vec![0, 1]);
969    }
970
971    #[test]
972    fn test_zne_config_default() {
973        let config = ZNEConfig::default();
974        assert_eq!(config.noise_factors, vec![1.0, 2.0, 3.0]);
975        assert_eq!(config.extrapolation, ExtrapolationMethod::Linear);
976    }
977
978    #[test]
979    fn test_resilience_options_levels() {
980        let level0 = ResilienceOptions::level0();
981        assert_eq!(level0.level, 0);
982        assert!(level0.zne.is_none());
983
984        let level1 = ResilienceOptions::level1();
985        assert_eq!(level1.level, 1);
986        assert!(level1.twirling.is_some());
987
988        let level2 = ResilienceOptions::level2();
989        assert_eq!(level2.level, 2);
990        assert!(level2.zne.is_some());
991    }
992
993    #[test]
994    fn test_sampler_v2_options_default() {
995        let options = SamplerV2Options::default();
996        assert_eq!(options.default_shots, 4096);
997        assert_eq!(options.optimization_level, 1);
998    }
999
1000    #[test]
1001    fn test_estimator_v2_options_default() {
1002        let options = EstimatorV2Options::default();
1003        assert_eq!(options.default_shots, 4096);
1004        assert_eq!(options.resilience.level, 1);
1005    }
1006
1007    #[test]
1008    fn test_cost_estimate() {
1009        let estimate = CostEstimate {
1010            estimated_runtime_seconds: 10.0,
1011            estimated_quantum_seconds: 5.0,
1012            num_circuits: 3,
1013            total_shots: 12000,
1014        };
1015
1016        assert_eq!(estimate.num_circuits, 3);
1017        assert_eq!(estimate.total_shots, 12000);
1018    }
1019
1020    #[test]
1021    fn test_dynamical_decoupling_config() {
1022        let config = DynamicalDecouplingConfig {
1023            sequence: DDSequence::XY4,
1024            enable_all_idles: true,
1025        };
1026
1027        assert_eq!(config.sequence, DDSequence::XY4);
1028    }
1029
1030    #[test]
1031    fn test_pec_config_default() {
1032        let config = PECConfig::default();
1033        assert_eq!(config.num_samples, 100);
1034    }
1035
1036    #[test]
1037    fn test_measurement_mitigation_config() {
1038        let config = MeasurementMitigationConfig::default();
1039        assert!(config.enable_m3);
1040        assert_eq!(config.calibration_shots, 1024);
1041    }
1042}