pub mod braket;
pub mod compiled;
mod decomposed;
mod dispatch;
pub mod gradient;
pub mod homological;
mod metadata;
pub mod noise;
mod observable;
mod probability;
pub(crate) mod shots;
pub mod stabilizer_rank;
mod terminal_sampling;
mod trajectory;
pub mod unified_pauli;
pub use braket::ResultValue;
pub(crate) use decomposed::merge_probabilities;
use decomposed::{
MIN_DECOMPOSITION_QUBITS, run_decomposed, run_decomposed_prefused, should_decompose,
};
pub use dispatch::BackendKind;
use dispatch::{
AUTO_SPD_MAX_TERMS, BackendPlan, ExecutionPlan, Family, MAX_AUTO_T_COUNT_EXACT,
MAX_AUTO_T_COUNT_SHOTS, MAX_STABILIZER_RANK_QUBITS, MIN_BLOCK_FOR_FACTORED_STAB,
MIN_FACTORED_STABILIZER_QUBITS, MIN_QUBITS_FOR_SPD_AUTO, accel_for, approximate_route_name,
auto_selects_cpu_statevector, build_statevector, has_temporal_clifford_opportunity,
initial_state_plan, plan_for_family, plan_temporal_clifford, resolve, resolve_backend,
run_temporal_clifford, stabilizer_rank_budget, validate_explicit_backend,
};
pub use metadata::{Engine, Exactness, ExpectationResult, Placement, ResolvedBackend, RunMetadata};
#[cfg(feature = "distributed")]
pub(crate) use observable::pauli_sandwich;
pub use observable::{ObservableExpectation, PauliObservable};
pub(crate) use observable::{
finish_expectations, i_pow, pauli_expectation_from_masks, pauli_expectations_from_masks,
pauli_masks, pauli_sandwiches_from_masks, validate_observable,
};
pub use probability::{FactoredBlock, Probabilities, ProbabilitiesIter};
pub use shots::{ShotsResult, bitstring};
use std::collections::HashMap;
use num_complex::Complex64;
use crate::backend::sparse::MAX_SPARSE_INDEX_QUBITS;
use crate::backend::statevector::StatevectorBackend;
use crate::backend::{Backend, max_statevector_qubits};
use crate::circuit::{Circuit, Instruction};
use crate::error::{PrismError, Result};
use crate::sim::noise::NoiseModel;
use shots::{packed_shots_to_classical_bits, sample_shots, shots_from_basis_samples};
use terminal_sampling::{
sample_counts_from_probs, sample_counts_from_state, sample_shots_from_probs,
sample_shots_from_state,
};
use unified_pauli::PauliTerm;
type TerminalStatevector = (StatevectorBackend, Vec<(usize, usize)>);
#[derive(Debug, Clone, Copy)]
pub(crate) struct SimOptions {
pub(crate) probabilities: bool,
}
impl Default for SimOptions {
fn default() -> Self {
Self {
probabilities: true,
}
}
}
impl SimOptions {
pub(crate) fn classical_only() -> Self {
Self {
probabilities: false,
}
}
}
#[derive(Debug, Clone)]
pub struct RunOutcome {
pub classical_bits: Vec<bool>,
pub probabilities: Option<Probabilities>,
pub metadata: RunMetadata,
}
#[derive(Debug, Clone)]
pub struct CountsResult {
pub counts: HashMap<Vec<u64>, u64>,
pub num_classical_bits: usize,
pub metadata: RunMetadata,
}
impl CountsResult {
pub fn into_counts(self) -> HashMap<Vec<u64>, u64> {
self.counts
}
}
#[derive(Debug, Clone)]
pub struct MarginalsResult {
pub marginals: Vec<(f64, f64)>,
pub metadata: RunMetadata,
}
impl MarginalsResult {
pub fn into_vec(self) -> Vec<(f64, f64)> {
self.marginals
}
}
#[derive(Debug, Clone)]
pub struct ReducedDensityMatrix {
pub qubits: Vec<usize>,
pub data: Vec<Complex64>,
pub metadata: RunMetadata,
}
impl ReducedDensityMatrix {
pub fn purity(&self) -> f64 {
self.data.iter().map(|entry| entry.norm_sqr()).sum()
}
}
#[derive(Debug, Clone)]
pub struct ObservableVariance {
pub variance: f64,
pub mean: f64,
pub metadata: RunMetadata,
}
#[derive(Debug, Clone)]
pub struct EntropyResult {
pub subsystem: Vec<usize>,
pub entropy: f64,
pub schmidt_values: Option<Vec<f64>>,
pub metadata: RunMetadata,
}
#[derive(Debug, Clone)]
pub struct OverlapResult {
pub fidelity: f64,
pub left: RunMetadata,
pub right: RunMetadata,
}
#[derive(Debug, Clone, Copy)]
pub struct Unseeded;
#[derive(Debug, Clone, Copy)]
pub struct Seeded {
seed: u64,
}
pub struct Simulate<'c, SeedState> {
circuit: &'c Circuit,
kind: BackendKind,
seed: SeedState,
noise_model: Option<&'c noise::NoiseModel>,
initial_state: Option<&'c [Complex64]>,
require_exact: bool,
}
impl<'c, SeedState> Simulate<'c, SeedState> {
#[inline]
pub fn backend(mut self, kind: BackendKind) -> Self {
self.kind = kind;
self
}
#[inline]
pub fn require_exact(mut self) -> Self {
self.require_exact = true;
self
}
#[inline]
pub fn noise(mut self, model: &'c noise::NoiseModel) -> Self {
self.noise_model = Some(model);
self
}
#[inline]
pub fn initial_state(mut self, amplitudes: &'c [Complex64]) -> Self {
self.initial_state = Some(amplitudes);
self
}
#[cfg(feature = "gpu")]
#[inline]
pub fn gpu(self, context: std::sync::Arc<crate::gpu::GpuContext>) -> Self {
self.backend(BackendKind::StatevectorGpu { context })
}
#[cfg(feature = "gpu")]
#[inline]
pub fn gpu_auto(self, context: std::sync::Arc<crate::gpu::GpuContext>) -> Self {
self.backend(BackendKind::AutoGpu { context })
}
#[cfg(feature = "distributed")]
pub fn distributed(
self,
context: std::sync::Arc<crate::distributed::DistributedContext>,
) -> Self {
self.backend(BackendKind::StatevectorDistributed { context })
}
}
impl<'c> Simulate<'c, Unseeded> {
#[inline]
pub fn seed(self, seed: u64) -> Simulate<'c, Seeded> {
Simulate {
circuit: self.circuit,
kind: self.kind,
seed: Seeded { seed },
noise_model: self.noise_model,
initial_state: self.initial_state,
require_exact: self.require_exact,
}
}
}
impl<'c> Simulate<'c, Seeded> {
#[inline]
fn seed_value(&self) -> u64 {
self.seed.seed
}
fn require_no_initial_state_under_noise(&self, terminal: &str) -> Result<()> {
if self.initial_state.is_some() {
return Err(reject_initial_state(
&self.kind,
terminal,
"noisy trajectory replay starts every shot from |0...0>; read the exact mixture \
with `run`, `marginals`, or `expectation_values` on the density-matrix backend",
));
}
Ok(())
}
#[inline]
pub fn run(self) -> Result<RunOutcome> {
let seed = self.seed_value();
if self.require_exact {
reject_approximate_route(&self.kind, self.circuit)?;
}
if let Some(noise_model) = self.noise_model {
require_exact_mixture(&self.kind, "a single run")?;
reject_readout_at(self.circuit, noise_model, "a single run")?;
let probabilities = exact_noisy_probabilities(
&self.kind,
self.circuit,
noise_model,
self.initial_state,
seed,
)?;
let classical_bits =
sample_exact_noisy_shots(&probabilities, self.circuit, noise_model, 1, seed)
.swap_remove(0);
return Ok(RunOutcome {
classical_bits,
probabilities: Some(probabilities),
metadata: exact_mixture_metadata(&self.kind),
});
}
if let Some(state) = self.initial_state {
return run_from_initial_state(
&self.kind,
self.circuit,
state,
seed,
&SimOptions::default(),
);
}
let outcome = run_with_internal(self.kind, self.circuit, seed, SimOptions::default())?;
ensure_exact_result(self.require_exact, &outcome.metadata)?;
Ok(outcome)
}
#[inline]
pub fn shots(self, num_shots: usize) -> Result<ShotsResult> {
let seed = self.seed_value();
let require_exact = self.require_exact;
if require_exact {
reject_approximate_route(&self.kind, self.circuit)?;
}
let result = if let Some(noise_model) = self.noise_model {
self.require_no_initial_state_under_noise("shot sampling")?;
run_shots_with_noise(self.kind, self.circuit, noise_model, num_shots, seed)?
} else if let Some(state) = self.initial_state {
shots_from_initial_state(&self.kind, self.circuit, state, num_shots, seed)?
} else {
run_shots_with(self.kind, self.circuit, num_shots, seed)?
};
ensure_exact_result(require_exact, &result.metadata)?;
Ok(result)
}
#[inline]
pub fn sample_counts(self, num_shots: usize) -> Result<CountsResult> {
let seed = self.seed_value();
if self.require_exact {
reject_approximate_route(&self.kind, self.circuit)?;
}
let (counts, metadata) = if let Some(noise_model) = self.noise_model {
self.require_no_initial_state_under_noise("count sampling")?;
let shots =
run_shots_with_noise(self.kind, self.circuit, noise_model, num_shots, seed)?;
(shots.counts(), shots.metadata)
} else if let Some(state) = self.initial_state {
let shots = shots_from_initial_state(&self.kind, self.circuit, state, num_shots, seed)?;
(shots.counts(), shots.metadata)
} else {
run_counts_with(self.kind, self.circuit, num_shots, seed)?
};
ensure_exact_result(self.require_exact, &metadata)?;
Ok(CountsResult {
counts,
num_classical_bits: self.circuit.num_classical_bits,
metadata,
})
}
#[inline]
pub fn marginals(self) -> Result<MarginalsResult> {
let seed = self.seed_value();
if self.require_exact {
reject_approximate_route(&self.kind, self.circuit)?;
}
if let Some(noise_model) = self.noise_model {
require_exact_mixture(&self.kind, "marginals")?;
reject_readout_at(self.circuit, noise_model, "marginals")?;
let probs = exact_noisy_probabilities(
&self.kind,
self.circuit,
noise_model,
self.initial_state,
seed,
)?;
return Ok(MarginalsResult {
marginals: probs.marginals(),
metadata: exact_mixture_metadata(&self.kind),
});
}
let result = if let Some(state) = self.initial_state {
marginals_from_initial_state(&self.kind, self.circuit, state, seed)?
} else {
run_marginals_result_with(self.kind, self.circuit, seed)?
};
ensure_exact_result(self.require_exact, &result.metadata)?;
Ok(result)
}
#[inline]
pub fn expectation_values(self, observables: &[Vec<PauliTerm>]) -> Result<Vec<f64>> {
self.expectation_values_reported(observables)
.map(ExpectationResult::into_values)
}
pub fn expectation_values_reported(
self,
observables: &[Vec<PauliTerm>],
) -> Result<ExpectationResult> {
let seed = self.seed_value();
if self.require_exact {
reject_approximate_route(&self.kind, self.circuit)?;
}
if let Some(noise_model) = self.noise_model {
reject_readout_at(self.circuit, noise_model, "expectation values")?;
}
if let BackendKind::PauliPath { epsilon, max_terms } = self.kind {
reject_pauli_path_initial_state(self.initial_state)?;
return pauli_path_expectations(
self.circuit,
self.noise_model,
observables,
epsilon,
max_terms,
);
}
if let Some(noise_model) = self.noise_model {
require_exact_mixture(&self.kind, "expectation values")?;
require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
let values = noise::dm_expectation_values(
&self.kind,
self.circuit,
observables,
Some(noise_model),
self.initial_state,
seed,
)?;
return Ok(analytic_expectations(
values,
exact_mixture_metadata(&self.kind),
));
}
if let Some(state) = self.initial_state {
require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
return expectation_values_from_initial_state(
&self.kind,
self.circuit,
state,
observables,
seed,
);
}
let result = run_expectation_values_reported(self.kind, self.circuit, observables, seed)?;
ensure_exact_result(self.require_exact, &result.metadata)?;
Ok(result)
}
pub fn observable_expectation(
self,
observable: &PauliObservable,
) -> Result<ObservableExpectation> {
self.observable_expectation_ref(observable)
}
fn observable_expectation_ref(
&self,
observable: &PauliObservable,
) -> Result<ObservableExpectation> {
let seed = self.seed_value();
if self.require_exact {
reject_approximate_route(&self.kind, self.circuit)?;
}
if let Some(noise_model) = self.noise_model {
reject_readout_at(self.circuit, noise_model, "observable expectation")?;
}
if let BackendKind::PauliPath { epsilon, max_terms } = self.kind {
reject_pauli_path_initial_state(self.initial_state)?;
let result = pauli_path_expectations(
self.circuit,
self.noise_model,
&observable_vecs(observable),
epsilon,
max_terms,
)?;
let metadata = result.metadata;
return Ok(weighted_observable_result(
observable,
&result.values,
None,
metadata,
));
}
if let Some(noise_model) = self.noise_model {
require_exact_mixture(&self.kind, "expectation values")?;
require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
let values = noise::dm_expectation_values(
&self.kind,
self.circuit,
&observable_vecs(observable),
Some(noise_model),
self.initial_state,
seed,
)?;
return Ok(weighted_observable_result(
observable,
&values,
None,
exact_mixture_metadata(&self.kind),
));
}
if let Some(state) = self.initial_state {
require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
let result = expectation_values_from_initial_state(
&self.kind,
self.circuit,
state,
&observable_vecs(observable),
seed,
)?;
return Ok(weighted_observable_result(
observable,
&result.values,
result.std_errors.as_deref(),
result.metadata,
));
}
let result =
run_observable_expectation_reported(self.kind.clone(), self.circuit, observable, seed)?;
ensure_exact_result(self.require_exact, &result.metadata)?;
Ok(result)
}
pub fn observable_variance(self, observable: &PauliObservable) -> Result<ObservableVariance> {
let (offset, traceless) = observable.split_identity();
let mean = self.observable_expectation_ref(observable)?;
let second = self.observable_expectation_ref(&traceless.square())?;
let centered = mean.mean - offset;
Ok(ObservableVariance {
variance: second.mean - centered * centered,
mean: mean.mean,
metadata: mean.metadata,
})
}
pub fn probabilities_of(self, qubits: &[usize]) -> Result<Vec<f64>> {
crate::backend::schmidt::validate_qubit_set(qubits, self.circuit.num_qubits)?;
let kind = format!("{:?}", self.kind);
let outcome = self.run()?;
let probabilities = outcome
.probabilities
.ok_or(PrismError::BackendUnsupported {
backend: kind,
operation: "a probability distribution to marginalize".into(),
})?;
Ok(probabilities.subset_marginal(qubits))
}
pub fn state_vector(self) -> Result<Vec<Complex64>> {
let seed = self.seed_value();
let diagnostic = Diagnostic::StateVector;
require_unitary_circuit(&self.kind, self.circuit, "a statevector requires")?;
if self.require_exact {
reject_approximate_route(&self.kind, self.circuit)?;
}
if self.noise_model.is_some() {
return Err(PrismError::IncompatibleBackend {
backend: format!("{:?}", self.kind),
reason: format!(
"{} is a pure state; a noise model evolves a mixture, which \
`reduced_density_matrix` over the whole register reports",
diagnostic.terminal()
),
});
}
let backend = diagnostic_backend(
&self.kind,
self.circuit,
self.initial_state,
seed,
diagnostic,
self.circuit.num_qubits,
)?;
ensure_exact_result(self.require_exact, &backend_metadata(&*backend))?;
backend.export_statevector()
}
pub fn reduced_density_matrix(self, qubits: &[usize]) -> Result<ReducedDensityMatrix> {
let seed = self.seed_value();
let diagnostic = Diagnostic::ReducedDensityMatrix;
let terminal = diagnostic.terminal();
crate::backend::schmidt::validate_qubit_set(qubits, self.circuit.num_qubits)?;
require_unitary_circuit(
&self.kind,
self.circuit,
"a reduced density matrix requires",
)?;
if self.require_exact {
reject_approximate_route(&self.kind, self.circuit)?;
}
if let Some(noise_model) = self.noise_model {
reject_readout_at(self.circuit, noise_model, terminal)?;
require_exact_mixture(&self.kind, terminal)?;
let mut mixture = noise::evolve_density_matrix(
&self.kind,
self.circuit,
Some(noise_model),
self.initial_state,
seed,
)?;
return Ok(ReducedDensityMatrix {
qubits: qubits.to_vec(),
data: mixture.reduced_density_matrix(qubits)?,
metadata: exact_mixture_metadata(&self.kind),
});
}
let mut backend = diagnostic_backend(
&self.kind,
self.circuit,
self.initial_state,
seed,
diagnostic,
qubits.len(),
)?;
let metadata = backend_metadata(&*backend);
ensure_exact_result(self.require_exact, &metadata)?;
Ok(ReducedDensityMatrix {
qubits: qubits.to_vec(),
data: backend.reduced_density_matrix(qubits)?,
metadata,
})
}
pub fn entanglement_entropy(self, subsystem: &[usize]) -> Result<EntropyResult> {
let seed = self.seed_value();
let diagnostic = Diagnostic::Entropy;
let terminal = diagnostic.terminal();
crate::backend::schmidt::validate_subsystem(subsystem, self.circuit.num_qubits)?;
require_unitary_circuit(&self.kind, self.circuit, "entanglement entropy requires")?;
if self.require_exact {
reject_approximate_route(&self.kind, self.circuit)?;
}
if let Some(noise_model) = self.noise_model {
reject_readout_at(self.circuit, noise_model, terminal)?;
require_exact_mixture(&self.kind, terminal)?;
let mut mixture = noise::evolve_density_matrix(
&self.kind,
self.circuit,
Some(noise_model),
self.initial_state,
seed,
)?;
return Ok(EntropyResult {
subsystem: subsystem.to_vec(),
entropy: mixture.entanglement_entropy(subsystem)?,
schmidt_values: None,
metadata: exact_mixture_metadata(&self.kind),
});
}
let mut backend = diagnostic_backend(
&self.kind,
self.circuit,
self.initial_state,
seed,
diagnostic,
subsystem.len(),
)?;
let metadata = backend_metadata(&*backend);
ensure_exact_result(self.require_exact, &metadata)?;
let (entropy, schmidt_values) = match backend.schmidt_values(subsystem) {
Ok(values) => (
crate::backend::schmidt::entropy_of_schmidt_values(&values),
Some(values),
),
Err(declined) => match backend.entanglement_entropy(subsystem) {
Ok(entropy) => (entropy, None),
Err(_) => return Err(declined),
},
};
Ok(EntropyResult {
subsystem: subsystem.to_vec(),
entropy,
schmidt_values,
metadata,
})
}
pub fn overlap(self, other: Simulate<'_, Seeded>) -> Result<OverlapResult> {
let diagnostic = Diagnostic::Overlap;
if self.circuit.num_qubits != other.circuit.num_qubits {
return Err(PrismError::InvalidParameter {
message: format!(
"{} needs two circuits of the same width; got {} and {} qubits",
diagnostic.terminal(),
self.circuit.num_qubits,
other.circuit.num_qubits
),
});
}
let (left_backend, left) = self.overlap_side(diagnostic)?;
let (right_backend, right) = other.overlap_side(diagnostic)?;
Ok(OverlapResult {
fidelity: left_backend.overlap_sq(&*right_backend)?,
left,
right,
})
}
fn overlap_side(self, diagnostic: Diagnostic) -> Result<(Box<dyn Backend>, RunMetadata)> {
let seed = self.seed_value();
let terminal = diagnostic.terminal();
require_unitary_circuit(&self.kind, self.circuit, "a state overlap requires")?;
if self.require_exact {
reject_approximate_route(&self.kind, self.circuit)?;
}
if self.noise_model.is_some() {
return Err(PrismError::IncompatibleBackend {
backend: format!("{:?}", self.kind),
reason: format!(
"{terminal} is an inner product of two pure states, and a noise model \
evolves a mixture, whose fidelity is a different computation; drop \
the model, or compare the mixtures through `reduced_density_matrix`"
),
});
}
let backend = diagnostic_backend(
&self.kind,
self.circuit,
self.initial_state,
seed,
diagnostic,
0,
)?;
let metadata = backend_metadata(&*backend);
ensure_exact_result(self.require_exact, &metadata)?;
Ok((backend, metadata))
}
#[inline]
pub fn expectation_gradient(
self,
hamiltonian: &[(f64, Vec<PauliTerm>)],
params: &crate::circuit::Parameters,
) -> Result<gradient::ExpectationGradient> {
let seed = self.seed_value();
if self.require_exact {
reject_approximate_route(&self.kind, self.circuit)?;
}
if self.noise_model.is_some() {
return Err(PrismError::IncompatibleBackend {
backend: format!("{:?}", self.kind),
reason: "the adjoint method backpropagates through a pure state, so no backend \
has a noisy adjoint path; drop the noise model, or take \
`expectation_gradient_shift` on the density-matrix backend"
.into(),
});
}
if self.initial_state.is_some() {
return Err(reject_initial_state(
&self.kind,
"the adjoint gradient",
"the backward pass reconstructs the input register by inverting the circuit from \
|0...0>, so a start state would have to be inverted with it",
));
}
if !(self.kind.is_auto() || matches!(self.kind, BackendKind::Statevector)) {
return Err(PrismError::IncompatibleBackend {
backend: format!("{:?}", self.kind),
reason:
"adjoint gradients run on the statevector backend; select Auto or Statevector"
.into(),
});
}
gradient::run_expectation_gradient(self.circuit, hamiltonian, params, seed)
}
#[inline]
pub fn expectation_gradient_shift(
self,
hamiltonian: &[(f64, Vec<PauliTerm>)],
params: &crate::circuit::Parameters,
) -> Result<gradient::ExpectationGradient> {
let seed = self.seed_value();
if self.require_exact {
reject_approximate_route(&self.kind, self.circuit)?;
}
if self.noise_model.is_some()
&& !self.kind.is_density_matrix()
&& !matches!(self.kind, BackendKind::PauliPath { .. })
{
return Err(PrismError::IncompatibleBackend {
backend: format!("{:?}", self.kind),
reason: "a parameter-shift gradient under a noise model evaluates the exact \
mixed state, which only the density-matrix backend holds; select \
`BackendKind::DensityMatrix` or its device sibling, or drop the noise \
model"
.into(),
});
}
gradient::shift_gradient(
&self.kind,
self.circuit,
hamiltonian,
params,
self.noise_model,
self.initial_state,
seed,
)
}
}
#[inline]
pub fn simulate(circuit: &Circuit) -> Simulate<'_, Unseeded> {
Simulate {
circuit,
kind: BackendKind::Auto,
seed: Unseeded,
noise_model: None,
initial_state: None,
require_exact: false,
}
}
fn reject_pauli_path(terminal: &str) -> PrismError {
PrismError::IncompatibleBackend {
backend: "PauliPath".into(),
reason: format!(
"{terminal} is not served by Pauli path propagation, which answers \
`expectation_values` and `observable_expectation` only"
),
}
}
fn reject_pauli_path_initial_state(state: Option<&[Complex64]>) -> Result<()> {
match state {
Some(_) => Err(reject_pauli_path("a start state")),
None => Ok(()),
}
}
fn pauli_path_expectations(
circuit: &Circuit,
noise: Option<&NoiseModel>,
observables: &[Vec<PauliTerm>],
epsilon: f64,
max_terms: usize,
) -> Result<ExpectationResult> {
let empty;
let noise = match noise {
Some(model) => model,
None => {
empty = NoiseModel {
after_gate: vec![Vec::new(); circuit.instructions.len()],
readout: vec![None; circuit.num_classical_bits],
};
&empty
}
};
let mut values = Vec::with_capacity(observables.len());
let mut discarded = 0.0f64;
for obs in observables {
let result =
unified_pauli::run_pauli_path_observable(circuit, noise, obs, epsilon, max_terms)?;
discarded = discarded.max(result.total_discarded);
values.push(result.mean);
}
let metadata = if discarded > 0.0 {
RunMetadata::approximate(ResolvedBackend::PauliPath)
} else {
RunMetadata::exact(ResolvedBackend::PauliPath)
};
Ok(analytic_expectations(values, metadata))
}
fn require_exact_mixture(kind: &BackendKind, terminal: &str) -> Result<()> {
if kind.is_density_matrix() {
return Ok(());
}
Err(PrismError::IncompatibleBackend {
backend: format!("{kind:?}"),
reason: format!(
"{terminal} under a noise model reads the exact mixed state, which only the \
density-matrix backend holds; select it, or average trajectories through `shots` \
or `sample_counts`"
),
})
}
fn reject_readout_at(
circuit: &Circuit,
noise_model: &noise::NoiseModel,
terminal: &str,
) -> Result<()> {
let inert = noise_model
.readout
.iter()
.take(circuit.num_classical_bits)
.all(|entry| entry.as_ref().is_none_or(|readout| readout.is_inert()));
if inert {
return Ok(());
}
Err(PrismError::InvalidParameter {
message: format!(
"{terminal} answers from the mixed state, which readout error is not part of: it \
acts on the measurement record and is indexed by classical bit, not qubit. Drop \
it from the model, or use `shots` or `sample_counts`, which apply it"
),
})
}
fn reject_initial_state(kind: &BackendKind, terminal: &str, instead: &str) -> PrismError {
PrismError::IncompatibleBackend {
backend: format!("{kind:?}"),
reason: format!("{terminal} does not accept a start state; {instead}"),
}
}
fn check_initial_state_len(state: &[Complex64], num_qubits: usize) -> Result<()> {
let want = (num_qubits < usize::BITS as usize).then(|| 1usize << num_qubits);
if want == Some(state.len()) {
return Ok(());
}
let needs = match want {
Some(count) => count.to_string(),
None => format!("2^{num_qubits}"),
};
Err(PrismError::InvalidParameter {
message: format!(
"start state has {} amplitudes, but a {num_qubits}-qubit circuit needs {needs}",
state.len()
),
})
}
fn backend_from_initial_state(
kind: &BackendKind,
circuit: &Circuit,
state: &[Complex64],
seed: u64,
) -> Result<Box<dyn Backend>> {
if !kind.is_auto() {
validate_explicit_backend(kind, circuit)?;
}
check_initial_state_len(state, circuit.num_qubits)?;
let mut backend = initial_state_plan(kind, circuit.num_qubits)?.build(seed);
backend.init_from_amplitudes(state.to_vec(), circuit.num_classical_bits)?;
Ok(backend)
}
fn expand_for_backend<'c>(
backend: &dyn Backend,
circuit: &'c Circuit,
) -> std::borrow::Cow<'c, Circuit> {
use std::borrow::Cow;
let expanded = if backend.supports_qft_block() {
Cow::Borrowed(circuit)
} else {
crate::circuit::expand_qft_blocks(circuit)
};
if backend.supports_pauli_rotation() {
return expanded;
}
match expanded {
Cow::Borrowed(borrowed) => crate::circuit::expand_pauli_rotations(borrowed),
Cow::Owned(owned) => {
let rotations = crate::circuit::expand_pauli_rotations(&owned);
if let Cow::Owned(expanded_rotations) = rotations {
return Cow::Owned(expanded_rotations);
}
Cow::Owned(owned)
}
}
}
fn fuse_for_backend<'a>(
backend: &dyn Backend,
circuit: &'a Circuit,
) -> std::borrow::Cow<'a, Circuit> {
crate::circuit::fusion::fuse_circuit_for_width(
circuit,
backend.supports_fused_gates(),
backend.fusion_state_qubits(circuit.num_qubits),
)
}
fn apply_fused_circuit(backend: &mut dyn Backend, circuit: &Circuit) -> Result<()> {
let expanded = expand_for_backend(&*backend, circuit);
let fused = fuse_for_backend(&*backend, &expanded);
backend.apply_instructions(&fused.instructions)
}
fn run_from_initial_state(
kind: &BackendKind,
circuit: &Circuit,
state: &[Complex64],
seed: u64,
opts: &SimOptions,
) -> Result<RunOutcome> {
let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
apply_fused_circuit(&mut *backend, circuit)?;
let probabilities = if opts.probabilities {
try_backend_probabilities(&*backend)?
} else {
None
};
Ok(RunOutcome {
classical_bits: backend.classical_results().to_vec(),
probabilities,
metadata: backend_metadata(&*backend),
})
}
fn shots_from_initial_state(
kind: &BackendKind,
circuit: &Circuit,
state: &[Complex64],
num_shots: usize,
seed: u64,
) -> Result<ShotsResult> {
let bits = circuit.num_classical_bits;
if circuit.has_terminal_measurements_only() {
let stripped = circuit.without_measurements();
let outcome = run_from_initial_state(kind, &stripped, state, seed, &SimOptions::default())?;
if let Some(probs) = outcome.probabilities {
let meas_map = circuit.measurement_map();
return Ok(ShotsResult::from_shots(
sample_shots(&probs, &meas_map, bits, num_shots, seed),
bits,
)
.with_metadata(outcome.metadata));
}
}
if !kind.is_auto() {
validate_explicit_backend(kind, circuit)?;
}
check_initial_state_len(state, circuit.num_qubits)?;
let plan = initial_state_plan(kind, circuit.num_qubits)?;
let probe = plan.build(seed);
let expanded = expand_for_backend(&*probe, circuit);
let fused = fuse_for_backend(&*probe, &expanded);
collect_shots(circuit, num_shots, seed, plan.resolved(), |shot_seed| {
let mut backend = plan.build(shot_seed);
backend.init_from_amplitudes(state.to_vec(), circuit.num_classical_bits)?;
backend.apply_instructions(&fused.instructions)?;
Ok((
backend.classical_results().to_vec(),
backend_metadata(&*backend),
))
})
}
fn marginals_from_initial_state(
kind: &BackendKind,
circuit: &Circuit,
state: &[Complex64],
seed: u64,
) -> Result<MarginalsResult> {
let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
apply_fused_circuit(&mut *backend, circuit)?;
if backend.supports_pauli_expectation() {
return marginals_from_pauli_expectations(&*backend, circuit.num_qubits);
}
Ok(MarginalsResult {
marginals: Probabilities::Dense(backend.probabilities()?).marginals(),
metadata: backend_metadata(&*backend),
})
}
fn expectation_values_from_initial_state(
kind: &BackendKind,
circuit: &Circuit,
state: &[Complex64],
observables: &[Vec<PauliTerm>],
seed: u64,
) -> Result<ExpectationResult> {
let masks = observables
.iter()
.map(|obs| pauli_masks(obs, circuit.num_qubits))
.collect::<Result<Vec<_>>>()?;
let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
apply_fused_circuit(&mut *backend, circuit)?;
let metadata = backend_metadata(&*backend);
if backend.supports_pauli_expectation() {
let values = backend.pauli_expectations(observables)?;
return Ok(analytic_expectations(values, metadata));
}
let evolved = backend.export_statevector()?;
let norm = crate::backend::state_norm_sqr(&evolved);
let values = pauli_expectations_from_masks(&evolved, &masks, norm);
Ok(analytic_expectations(values, metadata))
}
fn ensure_exact_result(require_exact: bool, metadata: &RunMetadata) -> Result<()> {
if require_exact && !metadata.is_exact() {
return Err(PrismError::IncompatibleBackend {
backend: format!("{:?}", metadata.backend),
reason: "require_exact rejects an approximate result; this engine discarded state weight while running, which the route could not predict"
.into(),
});
}
Ok(())
}
fn reject_approximate_route(kind: &BackendKind, circuit: &Circuit) -> Result<()> {
match approximate_route_name(kind, circuit) {
Some(engine) => Err(PrismError::IncompatibleBackend {
backend: engine.into(),
reason: "require_exact rejects a route that can discard state weight; drop the \
requirement to accept the approximation, which the result reports, or \
select a backend that represents this circuit exactly"
.into(),
}),
None => Ok(()),
}
}
fn require_unitary_circuit(kind: &BackendKind, circuit: &Circuit, subject: &str) -> Result<()> {
if has_nonunitary_or_classical_ops(circuit) {
return Err(PrismError::IncompatibleBackend {
backend: format!("{kind:?}"),
reason: format!(
"{subject} a unitary circuit without measurements, resets, or conditionals"
),
});
}
Ok(())
}
fn exact_mixture_metadata(kind: &BackendKind) -> RunMetadata {
let metadata = RunMetadata::exact(ResolvedBackend::DensityMatrix);
#[cfg(feature = "gpu")]
if matches!(kind, BackendKind::DensityMatrixGpu { .. }) {
let mut on_device = metadata;
on_device.placement = Placement::Device;
return on_device;
}
#[cfg(not(feature = "gpu"))]
let _ = kind;
metadata
}
fn exact_noisy_probabilities(
kind: &BackendKind,
circuit: &Circuit,
noise_model: &noise::NoiseModel,
initial_state: Option<&[Complex64]>,
seed: u64,
) -> Result<Probabilities> {
Ok(Probabilities::Dense(noise::density_matrix_probabilities(
kind,
circuit,
noise_model,
initial_state,
seed,
)?))
}
fn sample_exact_noisy_shots(
probs: &Probabilities,
circuit: &Circuit,
noise_model: &noise::NoiseModel,
num_shots: usize,
seed: u64,
) -> Vec<Vec<bool>> {
let bits = circuit.num_classical_bits;
let mut shots = sample_shots(probs, &circuit.measurement_map(), bits, num_shots, seed);
if noise_model.readout.iter().any(Option::is_some) {
let readout = trajectory::written_readout(circuit, &noise_model.readout);
let mut rng = trajectory::noise_rng(seed);
for shot in &mut shots {
trajectory::apply_readout_errors(shot, &readout, &mut rng);
}
}
shots
}
#[inline]
fn probs_only_result(probs: Vec<f64>, metadata: RunMetadata) -> RunOutcome {
RunOutcome {
probabilities: Some(Probabilities::Dense(probs)),
classical_bits: vec![],
metadata,
}
}
fn try_backend_probabilities(backend: &dyn Backend) -> Result<Option<Probabilities>> {
if let Some(factored) = backend.block_probabilities() {
return Ok(Some(factored));
}
match backend.probabilities() {
Ok(probs) => Ok(Some(Probabilities::Dense(probs))),
Err(PrismError::BackendUnsupported { .. }) => Ok(None),
Err(err) => Err(err),
}
}
fn execute(backend: &mut dyn Backend, circuit: &Circuit, opts: &SimOptions) -> Result<RunOutcome> {
let expanded = expand_for_backend(&*backend, circuit);
let fused = fuse_for_backend(&*backend, &expanded);
execute_circuit(backend, &fused, opts)
}
pub(crate) struct PreparedRoute {
plan: BackendPlan,
supports_fused: bool,
held: Option<(u64, Box<dyn Backend + Send>)>,
}
impl PreparedRoute {
pub(crate) fn supports_fused(&self) -> bool {
self.supports_fused
}
pub(crate) fn run(&mut self, circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
if !matches!(&self.held, Some((s, _)) if *s == seed) {
self.held = Some((seed, self.plan.build(seed)));
}
let (_, backend) = self.held.as_mut().expect("just built");
execute_circuit(&mut **backend, circuit, &SimOptions::default())
}
}
pub(crate) fn prepared_route(kind: &BackendKind, template: &Circuit) -> Option<PreparedRoute> {
if !kind.is_auto() && validate_explicit_backend(kind, template).is_err() {
return None;
}
let ProbabilityRoute::Direct {
has_partial_independence,
} = plan_probability_route(kind, template)
else {
return None;
};
let ExecutionPlan::Backend(plan) = resolve(kind, template, has_partial_independence) else {
return None;
};
let probe = plan.build(0);
let has_qft_block = crate::circuit::any_gate(&template.instructions, &mut |gate| {
matches!(gate, crate::gates::Gate::QftBlock { .. })
});
if has_qft_block && !probe.supports_qft_block() {
return None;
}
let has_pauli_rot = crate::circuit::any_gate(&template.instructions, &mut |gate| {
matches!(gate, crate::gates::Gate::PauliRot(_))
});
if has_pauli_rot && !probe.supports_pauli_rotation() {
return None;
}
Some(PreparedRoute {
supports_fused: probe.supports_fused_gates(),
plan,
held: None,
})
}
fn execute_circuit(
backend: &mut dyn Backend,
circuit: &Circuit,
opts: &SimOptions,
) -> Result<RunOutcome> {
backend.init(circuit.num_qubits, circuit.num_classical_bits)?;
backend.apply_instructions(&circuit.instructions)?;
let probabilities = if opts.probabilities {
try_backend_probabilities(backend)?
} else {
None
};
Ok(RunOutcome {
classical_bits: backend.classical_results().to_vec(),
probabilities,
metadata: backend_metadata(backend),
})
}
pub(crate) fn backend_metadata(backend: &dyn Backend) -> RunMetadata {
RunMetadata::new(backend.resolved(), backend.exactness(), backend.placement())
}
#[cfg(test)]
fn run(circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
run_with(BackendKind::Auto, circuit, seed)
}
pub(crate) fn run_with(kind: BackendKind, circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
run_with_internal(kind, circuit, seed, SimOptions::default())
}
fn run_with_internal(
kind: BackendKind,
circuit: &Circuit,
seed: u64,
opts: SimOptions,
) -> Result<RunOutcome> {
if !kind.is_auto() {
validate_explicit_backend(&kind, circuit)?;
}
#[cfg(feature = "distributed")]
if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
let mut backend = resolve_backend(&kind, circuit, false).build(seed);
if opts.probabilities {
crate::backend::dense_probability_len(backend.name(), circuit.num_qubits)?;
}
return execute(&mut *backend, circuit, &opts);
}
let route = plan_probability_route(&kind, circuit);
run_route(&kind, circuit, seed, opts, &route)
}
fn run_route(
kind: &BackendKind,
circuit: &Circuit,
seed: u64,
opts: SimOptions,
route: &ProbabilityRoute,
) -> Result<RunOutcome> {
match route {
ProbabilityRoute::FactoredStabilizer => {
let mut backend =
crate::backend::factored_stabilizer::FactoredStabilizerBackend::new(seed);
let fs_opts = if circuit.num_qubits > 64 {
SimOptions {
probabilities: false,
}
} else {
opts
};
execute(&mut backend, circuit, &fs_opts)
}
ProbabilityRoute::Decomposed(components) => {
run_decomposed(kind, components, circuit, seed, &opts)
}
ProbabilityRoute::StabilizerRank => {
let sr = stabilizer_rank::run_stabilizer_rank(circuit, seed)?;
let metadata = RunMetadata::exact(ResolvedBackend::StabilizerRank);
Ok(probs_only_result(sr.probabilities, metadata))
}
ProbabilityRoute::TemporalClifford {
has_partial_independence,
} => match plan_temporal_clifford(kind, circuit) {
Some(tc) => run_temporal_clifford(&tc, seed, opts.probabilities),
None => run_direct(kind, circuit, seed, opts, *has_partial_independence),
},
ProbabilityRoute::Direct {
has_partial_independence,
} => run_direct(kind, circuit, seed, opts, *has_partial_independence),
}
}
fn run_direct(
kind: &BackendKind,
circuit: &Circuit,
seed: u64,
opts: SimOptions,
has_partial_independence: bool,
) -> Result<RunOutcome> {
match resolve(kind, circuit, has_partial_independence) {
ExecutionPlan::Backend(plan) => {
let mut backend = plan.build(seed);
execute(&mut *backend, circuit, &opts)
}
ExecutionPlan::StabilizerRank => {
let sr = stabilizer_rank::run_stabilizer_rank(circuit, seed)?;
Ok(probs_only_result(
sr.probabilities,
RunMetadata::exact(ResolvedBackend::StabilizerRank),
))
}
ExecutionPlan::StochasticPauli { num_samples } => {
Err(crate::error::PrismError::IncompatibleBackend {
backend: format!(
"{:?}",
BackendKind::StochasticPauli { num_samples }
),
reason: "StochasticPauli produces marginal estimates only; use `simulate(...).marginals()`".into(),
})
}
ExecutionPlan::DeterministicPauli { epsilon, max_terms } => {
Err(crate::error::PrismError::IncompatibleBackend {
backend: format!(
"{:?}",
BackendKind::DeterministicPauli { epsilon, max_terms }
),
reason: "DeterministicPauli produces marginals only; use `simulate(...).marginals()`".into(),
})
}
ExecutionPlan::PauliPath => Err(reject_pauli_path("a single run")),
}
}
pub fn run_on(backend: &mut dyn Backend, circuit: &Circuit) -> Result<RunOutcome> {
execute(backend, circuit, &SimOptions::default())
}
pub fn run_on_state(
backend: &mut dyn Backend,
circuit: &Circuit,
initial_state: &[Complex64],
) -> Result<RunOutcome> {
check_initial_state_len(initial_state, circuit.num_qubits)?;
backend.init_from_amplitudes(initial_state.to_vec(), circuit.num_classical_bits)?;
apply_fused_circuit(backend, circuit)?;
Ok(RunOutcome {
classical_bits: backend.classical_results().to_vec(),
probabilities: try_backend_probabilities(backend)?,
metadata: backend_metadata(backend),
})
}
pub fn run_qasm(qasm: &str, seed: u64) -> Result<RunOutcome> {
let circuit = crate::circuit::openqasm::parse(qasm)?;
simulate(&circuit).seed(seed).run()
}
#[cfg(test)]
fn run_shots(circuit: &Circuit, num_shots: usize, seed: u64) -> Result<ShotsResult> {
run_shots_with(BackendKind::Auto, circuit, num_shots, seed)
}
pub(crate) fn supports_compiled_measurement_sampling(circuit: &Circuit) -> bool {
circuit.is_clifford_only()
&& !circuit.has_resets()
&& circuit.has_terminal_measurements_only()
&& circuit
.instructions
.iter()
.any(|inst| matches!(inst, Instruction::Measure { .. }))
}
fn supports_deferred_measurement_sampling(circuit: &Circuit) -> bool {
circuit.is_clifford_only()
&& (circuit.has_resets() || !circuit.has_terminal_measurements_only())
&& circuit
.instructions
.iter()
.any(|inst| matches!(inst, Instruction::Measure { .. }))
&& !circuit.instructions.iter().any(|inst| {
matches!(
inst,
Instruction::Conditional { .. } | Instruction::Region(_)
)
})
}
fn is_clifford_sampler_kind(kind: &BackendKind) -> bool {
if kind.is_auto() {
return true;
}
match kind {
BackendKind::Stabilizer | BackendKind::FactoredStabilizer => true,
#[cfg(feature = "gpu")]
BackendKind::StabilizerGpu { .. } => true,
_ => false,
}
}
fn should_use_compiled_clifford_sampling(
kind: &BackendKind,
circuit: &Circuit,
num_shots: usize,
) -> bool {
num_shots >= 2
&& supports_compiled_measurement_sampling(circuit)
&& is_clifford_sampler_kind(kind)
}
fn should_use_deferred_clifford_sampling(
kind: &BackendKind,
circuit: &Circuit,
num_shots: usize,
) -> bool {
num_shots >= 2
&& supports_deferred_measurement_sampling(circuit)
&& is_clifford_sampler_kind(kind)
}
fn compile_measurements_for_kind(
kind: &BackendKind,
circuit: &Circuit,
seed: u64,
) -> Result<compiled::CompiledSampler> {
#[cfg(not(feature = "gpu"))]
let _ = kind;
let sampler = compiled::compile_measurements(circuit, seed)?;
#[cfg(feature = "gpu")]
if let BackendKind::StabilizerGpu { context } = kind {
return Ok(sampler.with_gpu(context.clone()));
}
Ok(sampler)
}
fn analyze_independence(circuit: &Circuit) -> (Option<Vec<Vec<usize>>>, bool) {
if circuit.num_qubits >= MIN_DECOMPOSITION_QUBITS {
let components = circuit.independent_subsystems();
if components.len() > 1 {
if should_decompose(&components, circuit.num_qubits) {
return (Some(components), false);
}
return (None, true);
}
}
(None, false)
}
fn auto_clifford_t_budget(circuit: &Circuit) -> Option<(usize, usize)> {
(circuit.is_clifford_plus_t() && circuit.has_t_gates()).then(|| {
(
circuit.t_count(),
stabilizer_rank_budget(circuit.num_qubits),
)
})
}
pub(super) fn auto_stabilizer_rank_t_count(circuit: &Circuit, max_t: usize) -> Option<usize> {
let (t, sr_budget) = auto_clifford_t_budget(circuit)?;
(t <= max_t && t <= sr_budget).then_some(t)
}
enum ProbabilityRoute {
FactoredStabilizer,
Decomposed(Vec<Vec<usize>>),
StabilizerRank,
TemporalClifford {
has_partial_independence: bool,
},
Direct {
has_partial_independence: bool,
},
}
fn plan_probability_route(kind: &BackendKind, circuit: &Circuit) -> ProbabilityRoute {
let (decompose, has_partial_independence) = analyze_independence(circuit);
if let Some(components) = decompose {
let max_block = components.iter().map(|c| c.len()).max().unwrap_or(0);
if kind.is_auto()
&& circuit.is_clifford_only()
&& circuit.num_qubits >= MIN_FACTORED_STABILIZER_QUBITS
&& max_block >= MIN_BLOCK_FOR_FACTORED_STAB
{
return ProbabilityRoute::FactoredStabilizer;
}
return ProbabilityRoute::Decomposed(components);
}
if kind.is_auto()
&& circuit.num_qubits <= MAX_STABILIZER_RANK_QUBITS
&& !has_nonunitary_or_classical_ops(circuit)
&& auto_stabilizer_rank_t_count(circuit, MAX_AUTO_T_COUNT_EXACT).is_some()
{
return ProbabilityRoute::StabilizerRank;
}
if has_temporal_clifford_opportunity(kind, circuit) {
return ProbabilityRoute::TemporalClifford {
has_partial_independence,
};
}
ProbabilityRoute::Direct {
has_partial_independence,
}
}
fn auto_terminal_statevector_candidate(circuit: &Circuit) -> bool {
match plan_probability_route(&BackendKind::Auto, circuit) {
ProbabilityRoute::Direct {
has_partial_independence,
} => auto_selects_cpu_statevector(circuit, has_partial_independence),
_ => false,
}
}
fn terminal_statevector_candidate(kind: &BackendKind, circuit: &Circuit) -> bool {
if kind.is_auto() {
return auto_terminal_statevector_candidate(circuit);
}
match kind {
BackendKind::Statevector => true,
#[cfg(feature = "gpu")]
BackendKind::StatevectorGpu { .. } => true,
_ => false,
}
}
fn try_terminal_statevector_backend(
kind: &BackendKind,
circuit: &Circuit,
seed: u64,
) -> Result<Option<TerminalStatevector>> {
if !circuit.has_terminal_measurements_only() {
return Ok(None);
}
let meas_map = circuit.measurement_map();
if meas_map.is_empty() {
return Ok(None);
}
let stripped = circuit.without_measurements();
if !terminal_statevector_candidate(kind, &stripped) {
return Ok(None);
}
let accel = accel_for(kind, Family::Statevector, stripped.num_qubits);
let mut backend = build_statevector(&accel, seed);
let expanded = expand_for_backend(&backend, &stripped);
let fused = fuse_for_backend(&backend, &expanded);
backend.init(fused.num_qubits, fused.num_classical_bits)?;
backend.apply_instructions(&fused.instructions)?;
Ok(Some((backend, meas_map)))
}
fn try_native_terminal_backend(
kind: &BackendKind,
stripped: &Circuit,
seed: u64,
) -> Result<Option<Box<dyn Backend>>> {
if !kind.is_auto() {
validate_explicit_backend(kind, stripped)?;
}
let (decomposed, has_partial_independence) = match plan_probability_route(kind, stripped) {
ProbabilityRoute::Direct {
has_partial_independence,
} => (false, has_partial_independence),
ProbabilityRoute::Decomposed(_) => (true, false),
_ => return Ok(None),
};
let ExecutionPlan::Backend(plan) = resolve(kind, stripped, has_partial_independence) else {
return Ok(None);
};
if decomposed && !matches!(plan, BackendPlan::ProductState) {
return Ok(None);
}
let mut backend = plan.build(seed);
if !backend.supports_native_sampling() {
return Ok(None);
}
execute(&mut *backend, stripped, &SimOptions::classical_only())?;
Ok(Some(backend))
}
fn try_native_marginal_backend(
kind: &BackendKind,
circuit: &Circuit,
seed: u64,
) -> Result<Option<Box<dyn Backend>>> {
if !kind.is_auto() {
validate_explicit_backend(kind, circuit)?;
}
let (decomposed, has_partial_independence) = match plan_probability_route(kind, circuit) {
ProbabilityRoute::Direct {
has_partial_independence,
} => (false, has_partial_independence),
ProbabilityRoute::Decomposed(_) => (true, false),
_ => return Ok(None),
};
let ExecutionPlan::Backend(plan) = resolve(kind, circuit, has_partial_independence) else {
return Ok(None);
};
if decomposed && !matches!(plan, BackendPlan::ProductState) {
return Ok(None);
}
let mut backend = plan.build(seed);
if !backend.supports_pauli_expectation() {
return Ok(None);
}
execute(&mut *backend, circuit, &SimOptions::classical_only())?;
Ok(Some(backend))
}
enum ShotSource {
Compiled {
sampler: Box<compiled::CompiledSampler>,
meas_map: Vec<(usize, usize)>,
deferred: bool,
},
TerminalStatevector {
backend: Box<StatevectorBackend>,
meas_map: Vec<(usize, usize)>,
},
Native {
backend: Box<dyn Backend>,
meas_map: Vec<(usize, usize)>,
},
TerminalProbabilities {
probs: Probabilities,
meas_map: Vec<(usize, usize)>,
metadata: RunMetadata,
},
StabilizerRank,
PerShot,
}
impl ShotSource {
fn metadata(&self) -> Option<RunMetadata> {
match self {
ShotSource::Compiled { .. } => Some(
RunMetadata::exact(ResolvedBackend::CompiledStabilizer)
.with_engine(Engine::CompiledSampler),
),
ShotSource::TerminalStatevector { backend, .. } => Some(backend_metadata(&**backend)),
ShotSource::Native { backend, .. } => Some(backend_metadata(&**backend)),
ShotSource::TerminalProbabilities { metadata, .. } => Some(metadata.clone()),
ShotSource::StabilizerRank => Some(RunMetadata::exact(ResolvedBackend::StabilizerRank)),
ShotSource::PerShot => None,
}
}
}
fn prepare_shot_source(
kind: &BackendKind,
circuit: &Circuit,
num_shots: usize,
seed: u64,
) -> Result<ShotSource> {
if should_use_compiled_clifford_sampling(kind, circuit, num_shots) {
return Ok(ShotSource::Compiled {
sampler: Box::new(compile_measurements_for_kind(kind, circuit, seed)?),
meas_map: circuit.measurement_map(),
deferred: false,
});
}
if should_use_deferred_clifford_sampling(kind, circuit, num_shots) {
if let Ok(deferred) = compiled::defer_measure_reset_circuit(circuit) {
return Ok(ShotSource::Compiled {
sampler: Box::new(compile_measurements_for_kind(kind, &deferred, seed)?),
meas_map: deferred.measurement_map(),
deferred: true,
});
}
}
if let Some((backend, meas_map)) = try_terminal_statevector_backend(kind, circuit, seed)? {
return Ok(ShotSource::TerminalStatevector {
backend: Box::new(backend),
meas_map,
});
}
if matches!(kind, BackendKind::StabilizerRank) && circuit.has_t_gates() {
return Ok(ShotSource::StabilizerRank);
}
if kind.is_auto()
&& circuit.has_terminal_measurements_only()
&& circuit.num_qubits > MAX_STABILIZER_RANK_QUBITS
&& auto_stabilizer_rank_t_count(circuit, MAX_AUTO_T_COUNT_SHOTS).is_some()
{
return Ok(ShotSource::StabilizerRank);
}
if circuit.has_terminal_measurements_only() {
let stripped = circuit.without_measurements();
if let Some(backend) = try_native_terminal_backend(kind, &stripped, seed)? {
return Ok(ShotSource::Native {
backend,
meas_map: circuit.measurement_map(),
});
}
let result = run_with_internal(kind.clone(), &stripped, seed, SimOptions::default())?;
if let Some(probs) = result.probabilities {
return Ok(ShotSource::TerminalProbabilities {
probs,
meas_map: circuit.measurement_map(),
metadata: result.metadata,
});
}
}
Ok(ShotSource::PerShot)
}
#[cfg(test)]
fn run_counts(circuit: &Circuit, num_shots: usize, seed: u64) -> Result<HashMap<Vec<u64>, u64>> {
run_counts_with(BackendKind::Auto, circuit, num_shots, seed).map(|(counts, _)| counts)
}
pub(crate) fn run_counts_with(
kind: BackendKind,
circuit: &Circuit,
num_shots: usize,
seed: u64,
) -> Result<(HashMap<Vec<u64>, u64>, RunMetadata)> {
#[cfg(feature = "distributed")]
if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
let shots = run_shots_with(kind, circuit, num_shots, seed)?;
return Ok((shots.counts(), shots.metadata));
}
let folded = circuit.fold_static_guards();
let circuit = folded.as_ref();
let bits = circuit.num_classical_bits;
let source = prepare_shot_source(&kind, circuit, num_shots, seed)?;
let Some(metadata) = source.metadata() else {
let shots = run_shots_per_shot(kind, circuit, num_shots, seed)?;
return Ok((shots.counts(), shots.metadata));
};
let counts = match source {
ShotSource::Compiled {
mut sampler,
meas_map,
deferred,
} => {
if deferred {
let packed = sampler.try_sample_bulk_packed(num_shots)?;
counts_of(
packed_shots_to_classical_bits(&packed, &meas_map, bits),
bits,
)
} else {
sampler.try_sample_counts(num_shots)?
}
}
ShotSource::TerminalStatevector { backend, meas_map } => {
if backend.is_gpu_resident() {
let probs = backend.probabilities()?;
sample_counts_from_probs(&probs, &meas_map, bits, num_shots, seed)
} else {
sample_counts_from_state(
backend.state_vector(),
backend.probability_scale(),
&meas_map,
bits,
num_shots,
seed,
)
}
}
ShotSource::Native {
mut backend,
meas_map,
} => {
let samples = backend.sample_basis_states(num_shots, seed)?;
counts_of(shots_from_basis_samples(&samples, &meas_map, bits), bits)
}
ShotSource::TerminalProbabilities {
probs, meas_map, ..
} => counts_of(sample_shots(&probs, &meas_map, bits, num_shots, seed), bits),
ShotSource::StabilizerRank => {
stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed)?.counts()
}
ShotSource::PerShot => unreachable!("handled above"),
};
Ok((counts, metadata.with_shots(num_shots)))
}
fn counts_of(shots: Vec<Vec<bool>>, num_classical_bits: usize) -> HashMap<Vec<u64>, u64> {
ShotsResult::from_shots(shots, num_classical_bits).counts()
}
#[cfg(test)]
fn run_marginals(circuit: &Circuit, seed: u64) -> Result<Vec<(f64, f64)>> {
run_marginals_result_with(BackendKind::Auto, circuit, seed).map(MarginalsResult::into_vec)
}
#[cfg(test)]
fn run_marginals_with(kind: BackendKind, circuit: &Circuit, seed: u64) -> Result<Vec<(f64, f64)>> {
run_marginals_result_with(kind, circuit, seed).map(MarginalsResult::into_vec)
}
fn marginals_from_pauli_expectations(
backend: &dyn Backend,
num_qubits: usize,
) -> Result<MarginalsResult> {
let observables: Vec<Vec<PauliTerm>> = (0..num_qubits).map(|q| vec![PauliTerm::z(q)]).collect();
let expectations = backend.pauli_expectations(&observables)?;
Ok(MarginalsResult {
marginals: expectations_to_marginals(&expectations),
metadata: backend_metadata(backend),
})
}
fn spd_metadata(epsilon: f64) -> RunMetadata {
if epsilon > 0.0 {
RunMetadata::approximate(ResolvedBackend::DeterministicPauli)
} else {
RunMetadata::exact(ResolvedBackend::DeterministicPauli)
}
}
pub(crate) fn expectations_to_marginals(expectations: &[f64]) -> Vec<(f64, f64)> {
expectations
.iter()
.map(|ez| {
let p0 = ((1.0 + ez) / 2.0).clamp(0.0, 1.0);
(p0, 1.0 - p0)
})
.collect()
}
pub(super) fn has_nonunitary_or_classical_ops(circuit: &Circuit) -> bool {
circuit.instructions.iter().any(|inst| {
matches!(
inst,
Instruction::Measure { .. }
| Instruction::Reset { .. }
| Instruction::Conditional { .. }
| Instruction::Region(_)
)
})
}
fn supports_pauli_marginal_backend(circuit: &Circuit) -> bool {
circuit.is_clifford_plus_t() && !has_nonunitary_or_classical_ops(circuit)
}
fn validate_pauli_marginal_backend(kind: &BackendKind, circuit: &Circuit) -> Result<()> {
if has_nonunitary_or_classical_ops(circuit) {
return Err(PrismError::IncompatibleBackend {
backend: format!("{kind:?}"),
reason: "Pauli marginal backends require a unitary circuit without measurements, resets, or conditionals".into(),
});
}
Ok(())
}
fn run_marginals_result_with(
kind: BackendKind,
circuit: &Circuit,
seed: u64,
) -> Result<MarginalsResult> {
let n = circuit.num_qubits;
match &kind {
BackendKind::StochasticPauli { num_samples } => {
validate_pauli_marginal_backend(&kind, circuit)?;
let spp = unified_pauli::run_spp(circuit, *num_samples, seed)?;
return Ok(MarginalsResult {
marginals: expectations_to_marginals(&spp.expectations),
metadata: RunMetadata::approximate(ResolvedBackend::StochasticPauli)
.with_shots(*num_samples),
});
}
BackendKind::DeterministicPauli { epsilon, max_terms } => {
validate_pauli_marginal_backend(&kind, circuit)?;
let spd = unified_pauli::run_spd(circuit, *epsilon, *max_terms)?;
return Ok(MarginalsResult {
marginals: expectations_to_marginals(&spd.expectations),
metadata: spd_metadata(*epsilon),
});
}
_ => {}
}
if kind.is_auto()
&& supports_pauli_marginal_backend(circuit)
&& circuit.has_t_gates()
&& n >= MIN_QUBITS_FOR_SPD_AUTO
{
let spd = unified_pauli::run_spd(circuit, 0.0, AUTO_SPD_MAX_TERMS)?;
return Ok(MarginalsResult {
marginals: expectations_to_marginals(&spd.expectations),
metadata: spd_metadata(0.0),
});
}
#[cfg(feature = "distributed")]
if let BackendKind::StatevectorDistributed { context } = &kind {
let mut backend =
crate::backend::distributed_statevector::DistributedStatevectorBackend::new(
context.clone(),
seed,
);
execute(&mut backend, circuit, &SimOptions::classical_only())?;
return marginals_from_pauli_expectations(&backend, n);
}
if let Some(backend) = try_native_marginal_backend(&kind, circuit, seed)? {
return marginals_from_pauli_expectations(&*backend, n);
}
let result = run_with(kind, circuit, seed)?;
if let Some(probs) = &result.probabilities {
Ok(MarginalsResult {
marginals: probs.marginals(),
metadata: result.metadata.clone(),
})
} else {
Err(PrismError::BackendUnsupported {
backend: "simulate".into(),
operation: format!(
"marginals for {} qubits without backend probability output",
circuit.num_qubits
),
})
}
}
pub fn run_expectation_values(
circuit: &Circuit,
observables: &[Vec<PauliTerm>],
seed: u64,
) -> Result<Vec<f64>> {
run_expectation_values_with(BackendKind::Auto, circuit, observables, seed)
}
fn run_expectation_values_with(
kind: BackendKind,
circuit: &Circuit,
observables: &[Vec<PauliTerm>],
seed: u64,
) -> Result<Vec<f64>> {
run_expectation_values_reported(kind, circuit, observables, seed)
.map(ExpectationResult::into_values)
}
fn run_expectation_values_reported(
kind: BackendKind,
circuit: &Circuit,
observables: &[Vec<PauliTerm>],
seed: u64,
) -> Result<ExpectationResult> {
require_unitary_circuit(&kind, circuit, "expectation values require")?;
match &kind {
BackendKind::StochasticPauli { num_samples } => {
let mut values = Vec::with_capacity(observables.len());
let mut std_errors = Vec::with_capacity(observables.len());
for (i, obs) in observables.iter().enumerate() {
let r = unified_pauli::run_spp_observable(
circuit,
obs,
*num_samples,
seed.wrapping_add(i as u64),
)?;
values.push(r.mean);
std_errors.push(r.std_error);
}
Ok(ExpectationResult {
values,
std_errors: Some(std_errors),
metadata: RunMetadata::approximate(ResolvedBackend::StochasticPauli)
.with_shots(*num_samples),
})
}
BackendKind::DeterministicPauli { epsilon, max_terms } => {
let mut values = Vec::with_capacity(observables.len());
for obs in observables {
let r = unified_pauli::run_spd_observable(circuit, obs, *epsilon, *max_terms)?;
values.push(r.mean);
}
Ok(analytic_expectations(values, spd_metadata(*epsilon)))
}
_ if kind.is_auto() || kind.is_stabilizer_family() => {
if circuit.is_clifford_only() {
let mut values = Vec::with_capacity(observables.len());
for obs in observables {
let r = unified_pauli::run_spd_observable(circuit, obs, 0.0, 0)?;
values.push(r.mean);
}
Ok(analytic_expectations(values, spd_metadata(0.0)))
} else if kind.is_auto() {
if circuit.num_qubits > max_statevector_qubits() {
return expectation_values_native(&kind, circuit, observables, seed);
}
expectation_values_statevector(&kind, circuit, observables, seed)
} else {
Err(PrismError::IncompatibleBackend {
backend: format!("{kind:?}"),
reason: "stabilizer backends require a Clifford-only circuit".into(),
})
}
}
BackendKind::Statevector => {
expectation_values_statevector(&kind, circuit, observables, seed)
}
#[cfg(feature = "gpu")]
BackendKind::StatevectorGpu { .. } => {
expectation_values_statevector(&kind, circuit, observables, seed)
}
other => expectation_values_native(other, circuit, observables, seed),
}
}
pub fn run_observable_expectation(
circuit: &Circuit,
observable: &PauliObservable,
seed: u64,
) -> Result<ObservableExpectation> {
run_observable_expectation_reported(BackendKind::Auto, circuit, observable, seed)
}
fn run_observable_expectation_reported(
kind: BackendKind,
circuit: &Circuit,
observable: &PauliObservable,
seed: u64,
) -> Result<ObservableExpectation> {
require_unitary_circuit(&kind, circuit, "expectation values require")?;
let grouped_statevector = match &kind {
BackendKind::Statevector => true,
#[cfg(feature = "gpu")]
BackendKind::StatevectorGpu { .. } => true,
_ => {
kind.is_auto()
&& !circuit.is_clifford_only()
&& circuit.num_qubits <= max_statevector_qubits()
}
};
if grouped_statevector {
return grouped_expectation_statevector(&kind, circuit, observable, seed);
}
let result =
run_expectation_values_reported(kind, circuit, &observable_vecs(observable), seed)?;
Ok(weighted_observable_result(
observable,
&result.values,
result.std_errors.as_deref(),
result.metadata,
))
}
fn observable_vecs(observable: &PauliObservable) -> Vec<Vec<PauliTerm>> {
observable
.terms()
.iter()
.map(|(_, factors)| factors.clone())
.collect()
}
fn weighted_observable_result(
observable: &PauliObservable,
values: &[f64],
std_errors: Option<&[f64]>,
metadata: RunMetadata,
) -> ObservableExpectation {
let coefficients = observable.terms().iter().map(|(c, _)| *c);
let mean = coefficients.clone().zip(values).map(|(c, v)| c * v).sum();
let std_error = std_errors.map(|errors| {
coefficients
.zip(errors)
.map(|(c, e)| (c * e).powi(2))
.sum::<f64>()
.sqrt()
});
ObservableExpectation {
mean,
variance: None,
group_variances: None,
std_error,
metadata,
}
}
fn grouped_expectation_statevector(
kind: &BackendKind,
circuit: &Circuit,
observable: &PauliObservable,
seed: u64,
) -> Result<ObservableExpectation> {
let terms = observable.terms();
for (_, factors) in terms {
validate_observable(factors, circuit.num_qubits)?;
}
let accel = accel_for(kind, Family::Statevector, circuit.num_qubits);
let mut backend = build_statevector(&accel, seed);
let expanded = expand_for_backend(&backend, circuit);
let fused = fuse_for_backend(&backend, &expanded);
backend.init(fused.num_qubits, fused.num_classical_bits)?;
let masks = terms
.iter()
.map(|(_, factors)| pauli_masks(factors, circuit.num_qubits))
.collect::<Result<Vec<_>>>()?;
backend.apply_instructions(&fused.instructions)?;
let metadata = backend_metadata(&backend);
let grouping = observable.grouping();
let mut combined = masks.clone();
let mut pair_blocks: Vec<(usize, usize, Vec<f64>)> = Vec::new();
let mut deferred: Vec<usize> = Vec::new();
for (gi, group) in grouping.groups.iter().enumerate() {
let members = &group.term_indices;
if members.len() * (members.len() - 1) / 2 > MAX_PAIR_MASKS_PER_GROUP {
deferred.push(gi);
continue;
}
let first_mask = combined.len();
let mut pair_coefficients = Vec::with_capacity(members.len() * (members.len() - 1) / 2);
for (pos, &i) in members.iter().enumerate() {
for &j in &members[pos + 1..] {
let product_x = masks[i].0 ^ masks[j].0;
let product_z = masks[i].1 ^ masks[j].1;
combined.push((product_x, product_z, (product_x & product_z).count_ones()));
pair_coefficients.push(2.0 * terms[i].0 * terms[j].0);
}
}
pair_blocks.push((gi, first_mask, pair_coefficients));
}
let (values, host) = match pauli_expectations_on_device(&backend, &combined) {
Some(values) => (values?, None),
None => {
let state = backend.state_vector();
let norm = crate::backend::state_norm_sqr(state);
let values = pauli_expectations_from_masks(state, &combined, norm);
(values, Some((state, norm)))
}
};
let mean: f64 = terms.iter().zip(&values).map(|((c, _), v)| c * v).sum();
let mut group_variances = vec![0.0; grouping.groups.len()];
for (gi, first_mask, pair_coefficients) in &pair_blocks {
let group = &grouping.groups[*gi];
let m1: f64 = group
.term_indices
.iter()
.map(|&i| terms[i].0 * values[i])
.sum();
let square_diag: f64 = group
.term_indices
.iter()
.map(|&i| terms[i].0 * terms[i].0)
.sum();
let square_cross: f64 = pair_coefficients
.iter()
.zip(&values[*first_mask..])
.map(|(c, v)| c * v)
.sum();
group_variances[*gi] = (square_diag + square_cross - m1 * m1).max(0.0);
}
if !deferred.is_empty() {
let exported;
let (state, norm): (&[Complex64], f64) = match host {
Some(host) => host,
None => {
exported = backend.export_statevector()?;
(&exported, crate::backend::state_norm_sqr(&exported))
}
};
let mut scratch: Option<StatevectorBackend> = None;
for &gi in &deferred {
let group = &grouping.groups[gi];
let coefficients: Vec<f64> = group.term_indices.iter().map(|&i| terms[i].0).collect();
let (m1, m2) = if group.is_z_only() {
let zmasks: Vec<usize> = group.term_indices.iter().map(|&i| masks[i].1).collect();
observable::weighted_group_moments(state, &zmasks, &coefficients, norm)
} else {
let zmasks: Vec<usize> = group
.term_indices
.iter()
.map(|&i| masks[i].0 | masks[i].1)
.collect();
let rotation_circuit = group.basis_rotation_circuit(circuit.num_qubits);
let rotation = crate::circuit::fusion::fuse_circuit(&rotation_circuit, true);
let rotated = scratch.get_or_insert_with(|| StatevectorBackend::new(seed));
rotated.init_from_amplitudes(state.to_vec(), 0)?;
rotated.apply_instructions(&rotation.instructions)?;
observable::weighted_group_moments(
rotated.state_vector(),
&zmasks,
&coefficients,
norm,
)
};
group_variances[gi] = (m2 - m1 * m1).max(0.0);
}
}
let variance = group_variances.iter().sum();
Ok(ObservableExpectation {
mean,
variance: Some(variance),
group_variances: Some(group_variances),
std_error: None,
metadata,
})
}
const MAX_PAIR_MASKS_PER_GROUP: usize = 20;
fn analytic_expectations(values: Vec<f64>, metadata: RunMetadata) -> ExpectationResult {
ExpectationResult {
values,
std_errors: None,
metadata,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Diagnostic {
StateVector,
ReducedDensityMatrix,
Entropy,
Overlap,
}
impl Diagnostic {
fn terminal(self) -> &'static str {
match self {
Diagnostic::StateVector => "a statevector",
Diagnostic::ReducedDensityMatrix => "a reduced density matrix",
Diagnostic::Entropy => "entanglement entropy",
Diagnostic::Overlap => "a state overlap",
}
}
}
fn plan_answers(plan: &BackendPlan, diagnostic: Diagnostic) -> bool {
match diagnostic {
Diagnostic::ReducedDensityMatrix => matches!(
plan,
BackendPlan::Statevector { .. }
| BackendPlan::Sparse
| BackendPlan::Factored
| BackendPlan::ProductState
| BackendPlan::DensityMatrix { .. }
| BackendPlan::Stabilizer { .. }
| BackendPlan::FactoredStabilizer
),
Diagnostic::Entropy => matches!(
plan,
BackendPlan::Statevector { .. }
| BackendPlan::Mps { .. }
| BackendPlan::ProductState
| BackendPlan::Stabilizer { .. }
| BackendPlan::FactoredStabilizer
),
Diagnostic::StateVector | Diagnostic::Overlap => {
!matches!(plan, BackendPlan::DensityMatrix { .. })
}
}
}
fn stateless_route(kind: &BackendKind, diagnostic: Diagnostic, route: &str) -> PrismError {
PrismError::IncompatibleBackend {
backend: format!("{kind:?}"),
reason: format!(
"{} needs a backend that holds a state; the {route} route returns \
probabilities only",
diagnostic.terminal()
),
}
}
fn check_diagnostic_width(backend: &dyn Backend, diagnostic: Diagnostic, k: usize) -> Result<()> {
match diagnostic {
Diagnostic::ReducedDensityMatrix => {
crate::backend::reduced_density::reduced_density_side(backend.name(), k)?;
Ok(())
}
Diagnostic::StateVector => {
if k > crate::backend::schmidt::export_cap() {
return Err(crate::backend::schmidt::export_cap_exceeded(
backend.name(),
format!("dense statevector of {k} qubits"),
));
}
Ok(())
}
Diagnostic::Entropy | Diagnostic::Overlap => Ok(()),
}
}
fn diagnostic_backend(
kind: &BackendKind,
circuit: &Circuit,
initial_state: Option<&[Complex64]>,
seed: u64,
diagnostic: Diagnostic,
subsystem_len: usize,
) -> Result<Box<dyn Backend>> {
if let Some(state) = initial_state {
let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
check_diagnostic_width(&*backend, diagnostic, subsystem_len)?;
apply_fused_circuit(&mut *backend, circuit)?;
return Ok(backend);
}
if !kind.is_auto() {
validate_explicit_backend(kind, circuit)?;
}
let (_, has_partial_independence) = analyze_independence(circuit);
let mut plan = match resolve(kind, circuit, has_partial_independence) {
ExecutionPlan::Backend(plan) => plan,
ExecutionPlan::StabilizerRank => {
return Err(stateless_route(kind, diagnostic, "stabilizer-rank"));
}
ExecutionPlan::StochasticPauli { .. } => {
return Err(stateless_route(kind, diagnostic, "stochastic Pauli"));
}
ExecutionPlan::DeterministicPauli { .. } => {
return Err(stateless_route(kind, diagnostic, "deterministic Pauli"));
}
ExecutionPlan::PauliPath => {
return Err(stateless_route(kind, diagnostic, "Pauli path"));
}
};
if kind.is_auto()
&& !plan_answers(&plan, diagnostic)
&& circuit.num_qubits <= max_statevector_qubits()
{
plan = plan_for_family(kind, Family::Statevector, circuit.num_qubits);
}
let mut backend: Box<dyn Backend> = plan.build(seed);
check_diagnostic_width(&*backend, diagnostic, subsystem_len)?;
execute(&mut *backend, circuit, &SimOptions::classical_only())?;
Ok(backend)
}
fn expectation_values_native(
kind: &BackendKind,
circuit: &Circuit,
observables: &[Vec<PauliTerm>],
seed: u64,
) -> Result<ExpectationResult> {
if !kind.is_auto() {
validate_explicit_backend(kind, circuit)?;
}
for observable in observables {
validate_observable(observable, circuit.num_qubits)?;
}
let (_, has_partial_independence) = analyze_independence(circuit);
let ExecutionPlan::Backend(plan) = resolve(kind, circuit, has_partial_independence) else {
return Err(PrismError::IncompatibleBackend {
backend: format!("{kind:?}"),
reason: "expectation values need a backend that holds a state; the stabilizer-rank \
route returns probabilities only"
.into(),
});
};
let mut backend = plan.build(seed);
if !backend.supports_pauli_expectation() {
return Err(PrismError::BackendUnsupported {
backend: backend.name().to_string(),
operation: "Pauli expectation values".to_string(),
});
}
execute(&mut *backend, circuit, &SimOptions::classical_only())?;
let values = backend.pauli_expectations(observables)?;
Ok(analytic_expectations(values, backend_metadata(&*backend)))
}
fn expectation_values_statevector(
kind: &BackendKind,
circuit: &Circuit,
observables: &[Vec<PauliTerm>],
seed: u64,
) -> Result<ExpectationResult> {
for obs in observables {
validate_observable(obs, circuit.num_qubits)?;
}
let accel = accel_for(kind, Family::Statevector, circuit.num_qubits);
let mut backend = build_statevector(&accel, seed);
let expanded = expand_for_backend(&backend, circuit);
let fused = fuse_for_backend(&backend, &expanded);
backend.init(fused.num_qubits, fused.num_classical_bits)?;
let masks = observables
.iter()
.map(|obs| pauli_masks(obs, circuit.num_qubits))
.collect::<Result<Vec<_>>>()?;
backend.apply_instructions(&fused.instructions)?;
let values = match pauli_expectations_on_device(&backend, &masks) {
Some(values) => values?,
None => {
let state = backend.state_vector();
let norm = crate::backend::state_norm_sqr(state);
pauli_expectations_from_masks(state, &masks, norm)
}
};
let metadata = backend_metadata(&backend);
Ok(analytic_expectations(values, metadata))
}
fn pauli_expectations_on_device(
backend: &StatevectorBackend,
masks: &[(usize, usize, u32)],
) -> Option<Result<Vec<f64>>> {
if !backend.is_gpu_resident() {
return None;
}
let request: Vec<(u64, u64)> = masks
.iter()
.map(|&(xmask, zmask, _)| (xmask as u64, zmask as u64))
.chain(std::iter::once((0, 0)))
.collect();
let sums = match backend.gpu_pauli_sums(&request)? {
Ok(sums) => sums,
Err(e) => return Some(Err(e)),
};
let norm = sums[masks.len()].re;
if norm == 0.0 {
return Some(Ok(vec![0.0; masks.len()]));
}
Some(Ok(masks
.iter()
.zip(&sums)
.map(|(&(_, _, num_y), sum)| (sum * i_pow(num_y)).re / norm)
.collect()))
}
#[cfg(feature = "distributed")]
fn run_shots_distributed(
context: std::sync::Arc<crate::distributed::DistributedContext>,
circuit: &Circuit,
num_shots: usize,
seed: u64,
) -> Result<ShotsResult> {
use crate::backend::distributed_statevector::DistributedStatevectorBackend;
let meas_map = circuit.measurement_map();
if meas_map.is_empty() {
let mut backend = DistributedStatevectorBackend::new(context, seed);
backend.init(circuit.num_qubits, circuit.num_classical_bits)?;
return Ok(ShotsResult::from_shots(
vec![vec![false; circuit.num_classical_bits]; num_shots],
circuit.num_classical_bits,
)
.with_metadata(backend_metadata(&backend)));
}
if circuit.has_terminal_measurements_only() {
let stripped = circuit.without_measurements();
let mut backend = DistributedStatevectorBackend::new(context, seed);
execute(&mut backend, &stripped, &SimOptions::classical_only())?;
let samples = backend.sample_basis_states(num_shots, seed)?;
return Ok(ShotsResult::from_shots(
shots_from_basis_samples(&samples, &meas_map, circuit.num_classical_bits),
circuit.num_classical_bits,
)
.with_metadata(backend_metadata(&backend)));
}
let probe = DistributedStatevectorBackend::new(context.clone(), seed);
let expanded = expand_for_backend(&probe, circuit);
let fused = fuse_for_backend(&probe, &expanded);
let opts = SimOptions::classical_only();
let mut shots = Vec::with_capacity(num_shots);
let mut metadata = RunMetadata::exact(ResolvedBackend::Distributed);
for i in 0..num_shots {
let shot_seed = seed.wrapping_add(i as u64);
let mut backend = DistributedStatevectorBackend::new(context.clone(), shot_seed);
let result = execute_circuit(&mut backend, &fused, &opts)?;
metadata.weaken_with(&result.metadata);
shots.push(result.classical_bits);
}
Ok(ShotsResult::from_shots(shots, circuit.num_classical_bits).with_metadata(metadata))
}
pub(crate) fn run_shots_with(
kind: BackendKind,
circuit: &Circuit,
num_shots: usize,
seed: u64,
) -> Result<ShotsResult> {
#[cfg(feature = "distributed")]
if let BackendKind::StatevectorDistributed { context } = &kind {
return run_shots_distributed(context.clone(), circuit, num_shots, seed);
}
let folded = circuit.fold_static_guards();
let circuit = folded.as_ref();
let bits = circuit.num_classical_bits;
let source = prepare_shot_source(&kind, circuit, num_shots, seed)?;
let Some(metadata) = source.metadata() else {
return run_shots_per_shot(kind, circuit, num_shots, seed);
};
let result = match source {
ShotSource::Compiled {
mut sampler,
meas_map,
..
} => {
let packed = sampler.try_sample_bulk_packed(num_shots)?;
ShotsResult::from_shots(
packed_shots_to_classical_bits(&packed, &meas_map, bits),
bits,
)
}
ShotSource::TerminalStatevector { backend, meas_map } => {
let shots = if backend.is_gpu_resident() {
let probs = backend.probabilities()?;
sample_shots_from_probs(&probs, &meas_map, bits, num_shots, seed)
} else {
sample_shots_from_state(
backend.state_vector(),
backend.probability_scale(),
&meas_map,
bits,
num_shots,
seed,
)
};
ShotsResult::from_shots(shots, bits)
}
ShotSource::Native {
mut backend,
meas_map,
} => {
let samples = backend.sample_basis_states(num_shots, seed)?;
ShotsResult::from_shots(shots_from_basis_samples(&samples, &meas_map, bits), bits)
}
ShotSource::TerminalProbabilities {
probs, meas_map, ..
} => ShotsResult::from_shots(sample_shots(&probs, &meas_map, bits, num_shots, seed), bits),
ShotSource::StabilizerRank => {
stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed)?
}
ShotSource::PerShot => unreachable!("handled above"),
};
Ok(result.with_metadata(metadata))
}
fn run_shots_per_shot(
kind: BackendKind,
circuit: &Circuit,
num_shots: usize,
seed: u64,
) -> Result<ShotsResult> {
if !kind.is_auto() {
validate_explicit_backend(&kind, circuit)?;
}
let (decompose, has_partial_independence) = analyze_independence(circuit);
if matches!(kind, BackendKind::StabilizerRank) {
return stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed);
}
if matches!(
kind,
BackendKind::StochasticPauli { .. } | BackendKind::DeterministicPauli { .. }
) {
return Err(crate::error::PrismError::IncompatibleBackend {
backend: format!("{kind:?}"),
reason: "Pauli propagation backends do not support mid-circuit measurements".into(),
});
}
if kind.is_auto() && auto_stabilizer_rank_t_count(circuit, MAX_AUTO_T_COUNT_SHOTS).is_some() {
return stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed);
}
if has_temporal_clifford_opportunity(&kind, circuit) {
if decompose.is_none() {
if let Some(tc) = plan_temporal_clifford(&kind, circuit) {
let route = ResolvedBackend::Statevector;
return collect_shots(circuit, num_shots, seed, route, |shot_seed| {
let outcome = run_temporal_clifford(&tc, shot_seed, false)?;
Ok((outcome.classical_bits, outcome.metadata))
});
}
}
let opts = SimOptions::classical_only();
let route = resolve_backend(&kind, circuit, has_partial_independence).resolved();
let plan = plan_probability_route(&kind, circuit);
return collect_shots(circuit, num_shots, seed, route, |shot_seed| {
let outcome = run_route(&kind, circuit, shot_seed, opts, &plan)?;
Ok((outcome.classical_bits, outcome.metadata))
});
}
let opts = SimOptions::classical_only();
if let Some(ref comps) = decompose {
let partitions = circuit.partition_subcircuits(comps);
let block_plans: Vec<BackendPlan> = partitions
.iter()
.map(|(sub, _, _)| {
if !kind.is_auto() {
validate_explicit_backend(&kind, sub)?;
}
Ok(resolve_backend(&kind, sub, false))
})
.collect::<Result<_>>()?;
let fused_blocks: Vec<std::borrow::Cow<'_, Circuit>> = partitions
.iter()
.zip(&block_plans)
.map(|((sub, _, _), plan)| {
let probe = plan.build(seed);
let expanded = expand_for_backend(&*probe, sub);
std::borrow::Cow::Owned(fuse_for_backend(&*probe, &expanded).into_owned())
})
.collect();
collect_shots(
circuit,
num_shots,
seed,
ResolvedBackend::Decomposed,
|shot_seed| {
let result = run_decomposed_prefused(
&block_plans,
comps,
&partitions,
&fused_blocks,
shot_seed,
&opts,
circuit,
)?;
Ok((result.classical_bits, result.metadata))
},
)
} else {
let plan = resolve_backend(&kind, circuit, has_partial_independence);
let probe = plan.build(seed);
let expanded = expand_for_backend(&*probe, circuit);
let fused = fuse_for_backend(&*probe, &expanded);
collect_shots(circuit, num_shots, seed, plan.resolved(), |shot_seed| {
let mut backend = plan.build(shot_seed);
let outcome = execute_circuit(&mut *backend, &fused, &opts)?;
Ok((outcome.classical_bits, outcome.metadata))
})
}
}
fn collect_shots(
circuit: &Circuit,
num_shots: usize,
seed: u64,
route: ResolvedBackend,
mut shot: impl FnMut(u64) -> Result<(Vec<bool>, RunMetadata)>,
) -> Result<ShotsResult> {
let mut shots = Vec::with_capacity(num_shots);
let mut metadata = RunMetadata::exact(route);
for i in 0..num_shots {
let (bits, shot_metadata) = shot(seed.wrapping_add(i as u64))?;
if i == 0 {
metadata = shot_metadata;
} else {
metadata.weaken_with(&shot_metadata);
}
shots.push(bits);
}
Ok(ShotsResult::from_shots(shots, circuit.num_classical_bits).with_metadata(metadata))
}
fn general_noise_plan(kind: &BackendKind, circuit: &Circuit) -> BackendPlan {
let family = if !circuit.has_entangling_gates() {
Family::ProductState
} else if circuit.num_qubits > max_statevector_qubits() {
if circuit.is_sparse_friendly() && circuit.num_qubits <= MAX_SPARSE_INDEX_QUBITS {
Family::Sparse
} else {
Family::Mps
}
} else {
Family::Statevector
};
plan_for_family(kind, family, circuit.num_qubits)
}
pub(crate) fn run_shots_with_noise(
kind: BackendKind,
circuit: &Circuit,
noise_model: &noise::NoiseModel,
num_shots: usize,
seed: u64,
) -> Result<ShotsResult> {
noise_model.validate_for(circuit)?;
#[cfg(feature = "distributed")]
if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
return Err(crate::error::PrismError::IncompatibleBackend {
backend: format!("{kind:?}"),
reason: "noisy shot sampling is not supported on the distributed backend; \
trajectory execution cannot keep rank collectives in lockstep"
.into(),
});
}
if kind.is_density_matrix() {
let probs = exact_noisy_probabilities(&kind, circuit, noise_model, None, seed)?;
return Ok(ShotsResult::from_shots(
sample_exact_noisy_shots(&probs, circuit, noise_model, num_shots, seed),
circuit.num_classical_bits,
)
.with_metadata(exact_mixture_metadata(&kind)));
}
if !kind.supports_noisy_per_shot() {
return Err(crate::error::PrismError::IncompatibleBackend {
backend: format!("{kind:?}"),
reason: "this backend holds no per-shot pure state to inject noise into; select \
DensityMatrix for the exact mixed state, or a backend that evolves one \
state per trajectory"
.into(),
});
}
let is_stabilizer_kind = kind.is_stabilizer_family();
if is_stabilizer_kind && !noise_model.has_only_pauli_channels() {
return Err(crate::error::PrismError::IncompatibleBackend {
backend: format!("{kind:?}"),
reason: format!(
"stabilizer backends only support Pauli/depolarizing noise; use {} for amplitude damping, phase damping, thermal relaxation, or custom Kraus",
BackendKind::general_noise_backend_names()
),
});
}
if !noise_model.has_only_pauli_channels() && !kind.supports_general_noise() {
return Err(crate::error::PrismError::IncompatibleBackend {
backend: format!("{kind:?}"),
reason: format!(
"non-Pauli noise requires {}",
BackendKind::general_noise_backend_names()
),
});
}
if is_stabilizer_kind && !circuit.is_clifford_only() {
return Err(crate::error::PrismError::IncompatibleBackend {
backend: format!("{kind:?}"),
reason: "circuit contains non-Clifford gates".into(),
});
}
if !kind.is_auto() {
validate_explicit_backend(&kind, circuit)?;
}
if noise_model.has_only_pauli_channels() {
let use_compiled = (kind.is_auto()
|| matches!(
kind,
BackendKind::Stabilizer | BackendKind::FactoredStabilizer
))
&& supports_compiled_measurement_sampling(circuit)
|| {
#[cfg(feature = "gpu")]
{
matches!(kind, BackendKind::StabilizerGpu { .. })
&& supports_compiled_measurement_sampling(circuit)
}
#[cfg(not(feature = "gpu"))]
{
false
}
};
if use_compiled {
#[cfg(feature = "gpu")]
if let BackendKind::StabilizerGpu { context } = &kind {
return noise::run_shots_noisy_with_gpu(
circuit,
noise_model,
num_shots,
seed,
context.clone(),
);
}
return noise::run_shots_noisy(circuit, noise_model, num_shots, seed);
}
}
let plan = if kind.is_auto() && !noise_model.has_only_pauli_channels() {
general_noise_plan(&kind, circuit)
} else {
resolve_backend(&kind, circuit, false)
};
let has_pauli_rot = crate::circuit::any_gate(&circuit.instructions, &mut |gate| {
matches!(gate, crate::gates::Gate::PauliRot(_))
});
if has_pauli_rot
&& !matches!(plan, BackendPlan::Statevector { .. })
&& !plan.build(seed).supports_pauli_rotation()
{
return Err(crate::error::PrismError::IncompatibleBackend {
backend: format!("{:?}", plan.resolved()),
reason: "noisy trajectories apply the instruction stream raw so noise events \
stay aligned to it, which leaves no room for the Pauli-rotation \
lowering this backend needs; run on the statevector, or expand the \
rotations with circuit::expand_pauli_rotations and attach the noise \
model to the expanded circuit"
.into(),
});
}
if noise_model.has_two_qubit_kraus() && !plan.build(seed).supports_two_qubit_kraus() {
return Err(crate::error::PrismError::IncompatibleBackend {
backend: format!("{:?}", plan.resolved()),
reason: "a two-qubit Kraus channel needs the two-qubit reduced density matrix \
its branch probabilities are drawn from, which only the host \
statevector provides; run on BackendKind::Statevector, or evaluate \
the channel exactly on BackendKind::DensityMatrix"
.into(),
});
}
let route = plan.resolved();
trajectory::run_trajectories(
|s| plan.build(s),
circuit,
noise_model,
num_shots,
seed,
plan.is_gpu(),
route,
)
}
#[cfg(test)]
mod tests;
#[cfg(all(test, feature = "gpu"))]
mod gpu_stub_tests;
#[cfg(test)]
mod terminal_candidate_matrix_tests;
#[cfg(test)]
mod diagnostic_terminal_tests;