Skip to main content

prism_q/sim/
mod.rs

1//! Simulation orchestration: routes a circuit to a backend and answers the
2//! terminal queries. Entry points: [`simulate`], [`run_qasm`], [`run_on`].
3
4pub mod braket;
5mod branching;
6pub mod calibration;
7mod clifford_register;
8pub mod compiled;
9mod decomposed;
10mod dispatch;
11mod dynamic;
12pub mod gradient;
13pub mod homological;
14mod metadata;
15pub mod noise;
16mod observable;
17mod probability;
18pub(crate) mod shots;
19pub mod stabilizer_rank;
20mod terminal_sampling;
21mod trajectory;
22pub mod unified_pauli;
23
24pub use braket::ResultValue;
25pub(crate) use decomposed::merge_probabilities;
26use decomposed::{
27    MIN_DECOMPOSITION_QUBITS, PreparedBlocks, run_decomposed, run_decomposed_prefused,
28    should_decompose,
29};
30pub use dispatch::BackendKind;
31use dispatch::{
32    AUTO_MPS_BOND_DIM, BackendPlan, ExecutionPlan, Family, MAX_AUTO_T_COUNT_SHOTS,
33    MAX_STABILIZER_RANK_QUBITS, MIN_BLOCK_FOR_FACTORED_STAB, MIN_FACTORED_STABILIZER_QUBITS,
34    MIN_QUBITS_FOR_SPD_AUTO, accel_for, approximate_route_name, auto_selects_cpu_statevector,
35    auto_spd_work_budget, build_statevector, has_temporal_clifford_opportunity, initial_state_plan,
36    mps_apply_cost, plan_for_family, plan_temporal_clifford, resolve, resolve_backend,
37    run_temporal_clifford, stabilizer_rank_budget, validate_explicit_backend,
38};
39pub use dynamic::{SimulateProgram, simulate_program};
40pub use metadata::{
41    BondReport, Engine, Exactness, ExpectationResult, Placement, ResolvedBackend, RunMetadata,
42};
43#[cfg(feature = "distributed")]
44pub(crate) use observable::pauli_sandwich;
45pub use observable::{ObservableExpectation, PauliObservable};
46pub(crate) use observable::{
47    finish_expectations, i_pow, pauli_expectation_from_masks, pauli_expectations_from_masks,
48    pauli_masks, pauli_sandwiches_from_masks, validate_observable,
49};
50pub use probability::{FactoredBlock, Probabilities, ProbabilitiesIter};
51pub use shots::{ShotsResult, bitstring};
52
53use std::collections::HashMap;
54
55use num_complex::Complex64;
56
57use crate::backend::sparse::MAX_SPARSE_INDEX_QUBITS;
58use crate::backend::statevector::StatevectorBackend;
59use crate::backend::{Backend, max_statevector_qubits};
60use crate::circuit::{Circuit, Instruction, SaveRecord, SaveSpec, SavedValue};
61use crate::error::{PrismError, Result};
62use crate::sim::noise::NoiseModel;
63use shots::{
64    counts_from_basis_samples, packed_shots_to_classical_bits, record_counts_to_classical_bits,
65    sample_shots, shots_from_basis_samples,
66};
67use terminal_sampling::{
68    sample_counts_from_probs, sample_counts_from_state, sample_shots_from_probs,
69    sample_shots_from_state,
70};
71use unified_pauli::{PauliTerm, SpdTruncation};
72
73type TerminalStatevector = (StatevectorBackend, Vec<(usize, usize)>);
74
75#[derive(Debug, Clone, Copy)]
76pub(crate) struct SimOptions {
77    pub(crate) probabilities: bool,
78}
79
80impl Default for SimOptions {
81    fn default() -> Self {
82        Self {
83            probabilities: true,
84        }
85    }
86}
87
88impl SimOptions {
89    pub(crate) fn classical_only() -> Self {
90        Self {
91            probabilities: false,
92        }
93    }
94}
95
96/// Result of a generic simulation run.
97#[derive(Debug, Clone)]
98pub struct RunOutcome {
99    /// Classical measurement outcomes, indexed by classical bit number.
100    /// `true` = measured |1⟩.
101    pub classical_bits: Vec<bool>,
102    /// Probability of each computational basis state (length 2^n).
103    ///
104    /// `None` means the selected backend cannot expose a dense probability
105    /// distribution for this circuit. Other probability extraction failures
106    /// are returned as errors by the query that produced this result.
107    pub probabilities: Option<Probabilities>,
108    /// Which engine ran, whether the answer is exact, and where the state lived.
109    pub metadata: RunMetadata,
110    /// What each [`Instruction::Save`] point recorded, in the order the points
111    /// were reached. Empty unless the circuit carries save points.
112    pub saves: Vec<SaveRecord>,
113}
114
115/// Frequency histogram returned by query-aware count sampling.
116#[derive(Debug, Clone)]
117pub struct CountsResult {
118    /// Histogram keyed by packed classical bits; same key layout as
119    /// [`ShotsResult::counts`], formattable with [`bitstring`].
120    pub counts: HashMap<Vec<u64>, u64>,
121    pub num_classical_bits: usize,
122    pub metadata: RunMetadata,
123}
124
125impl CountsResult {
126    pub fn into_counts(self) -> HashMap<Vec<u64>, u64> {
127        self.counts
128    }
129}
130
131/// Per-qubit marginal probabilities returned by query-aware marginal sampling.
132#[derive(Debug, Clone)]
133pub struct MarginalsResult {
134    /// `(P(0), P(1))` per qubit, indexed by qubit number.
135    pub marginals: Vec<(f64, f64)>,
136    pub metadata: RunMetadata,
137}
138
139impl MarginalsResult {
140    pub fn into_vec(self) -> Vec<(f64, f64)> {
141        self.marginals
142    }
143}
144
145/// Reduced density matrix of a qubit subset, returned by
146/// [`Simulate::reduced_density_matrix`].
147#[derive(Debug, Clone)]
148pub struct ReducedDensityMatrix {
149    /// The subsystem as it was requested, which fixes the index order below.
150    pub qubits: Vec<usize>,
151    /// Row major with side `2^k` over `k = qubits.len()`: `data[t * 2^k + t']`
152    /// is `<t|rho|t'>`, where bit `i` of `t` is the state of `qubits[i]`, so
153    /// `qubits[0]` is the lowest bit as `q[0]` is in a basis index. Trace one,
154    /// Hermitian to rounding.
155    pub data: Vec<Complex64>,
156    pub metadata: RunMetadata,
157}
158
159impl ReducedDensityMatrix {
160    /// `Tr(rho^2)`: 1 for a pure marginal, `2^-k` for the maximally mixed one.
161    pub fn purity(&self) -> f64 {
162        self.data.iter().map(|entry| entry.norm_sqr()).sum()
163    }
164}
165
166/// Operator variance of a weighted Pauli observable, returned by
167/// [`Simulate::observable_variance`].
168#[derive(Debug, Clone)]
169pub struct ObservableVariance {
170    /// `<H^2> - <H>^2` on the output state.
171    pub variance: f64,
172    /// `<H>` on the same state, evaluated on the way to the variance.
173    pub mean: f64,
174    pub metadata: RunMetadata,
175}
176
177/// Entanglement entropy of a subsystem, returned by
178/// [`Simulate::entanglement_entropy`].
179#[derive(Debug, Clone)]
180pub struct EntropyResult {
181    /// The subsystem as it was requested, the side of the cut the entropy is
182    /// read on.
183    pub subsystem: Vec<usize>,
184    /// Von Neumann entropy of the subsystem in nats: a Bell pair reads `ln 2`.
185    pub entropy: f64,
186    /// Schmidt values across the cut, descending, with squares summing to 1.
187    /// `None` where the backend holds the entropy without the spectrum that
188    /// stands behind it, as a stabilizer cut past the export cap does: its
189    /// `2^r` equal values do not fit.
190    pub schmidt_values: Option<Vec<f64>>,
191    pub metadata: RunMetadata,
192}
193
194/// Overlap between the output states of two runs, returned by
195/// [`Simulate::overlap`].
196#[derive(Debug, Clone)]
197pub struct OverlapResult {
198    /// `|<a|b>|^2` over the two normalized states: 1 for the same state up to
199    /// phase, 0 for orthogonal ones. The amplitude itself is not reported,
200    /// since a tableau keeps no global phase and every MPS truncation moves
201    /// one.
202    pub fidelity: f64,
203    /// Provenance of the run the terminal was called on, the left of the
204    /// inner product.
205    pub left: RunMetadata,
206    /// Provenance of the run passed as the argument.
207    pub right: RunMetadata,
208}
209
210/// Typestate marker: [`Simulate`] builder with no seed chosen yet.
211#[derive(Debug, Clone, Copy)]
212pub struct Unseeded;
213
214/// Typestate marker: [`Simulate`] builder with its RNG seed fixed.
215#[derive(Debug, Clone, Copy)]
216pub struct Seeded {
217    seed: u64,
218}
219
220/// A caller-supplied start: a `2^n` amplitude vector, or a `4^n` mixture in
221/// the density-matrix layout.
222#[derive(Debug, Clone, Copy)]
223pub(crate) enum StartState<'c> {
224    Amplitudes(&'c [Complex64]),
225    DensityMatrix(&'c [Complex64]),
226}
227
228impl StartState<'_> {
229    /// Load the start into `backend`, whose register width the buffer sets.
230    fn load(self, backend: &mut dyn Backend, num_classical_bits: usize) -> Result<()> {
231        match self {
232            Self::Amplitudes(amplitudes) => {
233                backend.init_from_amplitudes(amplitudes.to_vec(), num_classical_bits)
234            }
235            Self::DensityMatrix(rho) => {
236                backend.init_from_density_matrix(rho.to_vec(), num_classical_bits)
237            }
238        }
239    }
240}
241
242/// Builder for query-aware simulation requests.
243pub struct Simulate<'c, SeedState> {
244    circuit: &'c Circuit,
245    kind: BackendKind,
246    seed: SeedState,
247    noise_model: Option<&'c noise::NoiseModel>,
248    initial_state: Option<StartState<'c>>,
249    require_exact: bool,
250}
251
252impl<'c, SeedState> Simulate<'c, SeedState> {
253    /// Select an explicit backend kind instead of [`BackendKind::Auto`] routing.
254    #[inline]
255    pub fn backend(mut self, kind: BackendKind) -> Self {
256        self.kind = kind;
257        self
258    }
259
260    /// Reject a route that could return an approximate answer, rather than
261    /// taking it and saying so in the result.
262    ///
263    /// [`BackendKind::Auto`] sends a circuit past the statevector cap to an MPS
264    /// at a bounded bond dimension, which is the only route those circuits have;
265    /// the result reports [`Exactness::Approximate`] either way. Call this when
266    /// an approximate answer is worse than no answer, and the run returns
267    /// `IncompatibleBackend` naming the engine it would have used.
268    ///
269    /// A circuit whose gates all preserve computational basis states is the
270    /// exception: it stays on one basis state, so that route holds it at bond 1
271    /// and answers exactly.
272    ///
273    /// Routes that can be decided from the circuit are rejected before any state
274    /// is allocated; sparse Pauli dynamics only learns that it truncated while
275    /// propagating, so that one is caught on the finished result instead.
276    #[inline]
277    pub fn require_exact(mut self) -> Self {
278        self.require_exact = true;
279        self
280    }
281
282    /// Attach a noise model.
283    ///
284    /// [`Simulate::shots`] and [`Simulate::sample_counts`] accept one on any
285    /// backend with a per-shot pure state, averaging trajectories.
286    /// [`Simulate::run`], [`Simulate::marginals`],
287    /// [`Simulate::expectation_values`] and
288    /// [`Simulate::reduced_density_matrix`] answer from the exact mixture
289    /// instead, which only [`BackendKind::DensityMatrix`] and its device
290    /// sibling hold, so they require one of those, as does
291    /// [`Simulate::expectation_gradient_shift`] and, only to decline on it,
292    /// [`Simulate::entanglement_entropy`]: a mixture has no Schmidt
293    /// decomposition. [`Simulate::expectation_gradient`] rejects a noise model
294    /// on every backend.
295    #[inline]
296    pub fn noise(mut self, model: &'c noise::NoiseModel) -> Self {
297        self.noise_model = Some(model);
298        self
299    }
300
301    /// Start from `amplitudes` instead of |0...0⟩.
302    ///
303    /// Indexed with qubit 0 in the least significant bit, length `2^n` for the
304    /// circuit's `n` qubits, and normalized. A vector failing any of those is
305    /// rejected with `InvalidParameter` before the run.
306    ///
307    /// A start state also constrains the route, because shape-based dispatch
308    /// reads the circuit alone and its shortcuts hold only from |0...0⟩:
309    /// [`BackendKind::Auto`] resolves to the statevector, and every backend
310    /// other than the statevector (dense, device, or distributed) and
311    /// [`BackendKind::DensityMatrix`] reports `IncompatibleBackend`.
312    /// [`Simulate::expectation_gradient`] declines a start
313    /// state, as do [`Simulate::shots`] and [`Simulate::sample_counts`] with a
314    /// noise model attached, since trajectory replay has no start-state path.
315    #[inline]
316    pub fn initial_state(mut self, amplitudes: &'c [Complex64]) -> Self {
317        self.initial_state = Some(StartState::Amplitudes(amplitudes));
318        self
319    }
320
321    /// Start from the mixture `rho` instead of |0...0⟩.
322    ///
323    /// Row-major `2^n x 2^n` in the layout
324    /// [`DensityMatrixBackend::density_matrix`] exports, qubit 0 in the least
325    /// significant bit of both indices, so entry `(r << n) | c` is `<r|rho|c>`.
326    /// The buffer must be Hermitian, each entry pair agreeing to 1e-12, and
327    /// have unit trace to 1e-9; one failing either check, or of a length other
328    /// than `4^n`, is rejected with `InvalidParameter` naming the check.
329    /// Positive semidefiniteness is not checked, because the backend carries no
330    /// eigendecomposition to check it with.
331    ///
332    /// Only [`BackendKind::DensityMatrix`] and its device sibling hold a
333    /// mixture, so every other kind, [`BackendKind::Auto`] included, reports
334    /// `IncompatibleBackend`. The terminals that decline a start state decline
335    /// this one for the same reasons. Replaces an earlier
336    /// [`Simulate::initial_state`], as that call replaces this one.
337    ///
338    /// [`DensityMatrixBackend::density_matrix`]: crate::backend::density_matrix::DensityMatrixBackend::density_matrix
339    #[inline]
340    pub fn initial_density_matrix(mut self, rho: &'c [Complex64]) -> Self {
341        self.initial_state = Some(StartState::DensityMatrix(rho));
342        self
343    }
344
345    /// Shortcut for [`Simulate::backend`] with [`BackendKind::StatevectorGpu`].
346    #[cfg(feature = "gpu")]
347    #[inline]
348    pub fn gpu(self, context: std::sync::Arc<crate::gpu::GpuContext>) -> Self {
349        self.backend(BackendKind::StatevectorGpu { context })
350    }
351
352    /// Automatic backend selection with GPU acceleration opted in via `context`.
353    ///
354    /// Routes like [`BackendKind::Auto`], but a selected statevector or
355    /// stabilizer block that clears the qubit crossover with VRAM to spare runs
356    /// on the device. Unsupported cases fall back to the identical CPU path.
357    #[cfg(feature = "gpu")]
358    #[inline]
359    pub fn gpu_auto(self, context: std::sync::Arc<crate::gpu::GpuContext>) -> Self {
360        self.backend(BackendKind::AutoGpu { context })
361    }
362
363    /// Distribute the exact state vector across the ranks of `context`.
364    ///
365    /// With a single rank this behaves like [`Simulate::backend`] with
366    /// [`BackendKind::Statevector`].
367    #[cfg(feature = "distributed")]
368    pub fn distributed(
369        self,
370        context: std::sync::Arc<crate::distributed::DistributedContext>,
371    ) -> Self {
372        self.backend(BackendKind::StatevectorDistributed { context })
373    }
374}
375
376impl<'c> Simulate<'c, Unseeded> {
377    /// Query methods exist only on the seeded builder.
378    #[inline]
379    pub fn seed(self, seed: u64) -> Simulate<'c, Seeded> {
380        Simulate {
381            circuit: self.circuit,
382            kind: self.kind,
383            seed: Seeded { seed },
384            noise_model: self.noise_model,
385            initial_state: self.initial_state,
386            require_exact: self.require_exact,
387        }
388    }
389}
390
391impl<'c> Simulate<'c, Seeded> {
392    #[inline]
393    fn seed_value(&self) -> u64 {
394        self.seed.seed
395    }
396
397    /// Trajectory replay reinitializes a pure state per shot and the compiled
398    /// noisy samplers are tableau based, so neither carries a start state.
399    fn require_no_initial_state_under_noise(&self, terminal: &str) -> Result<()> {
400        if self.initial_state.is_some() {
401            return Err(reject_initial_state(
402                &self.kind,
403                terminal,
404                "noisy trajectory replay starts every shot from |0...0>; read the exact mixture \
405                 with `run`, `marginals`, or `expectation_values` on the density-matrix backend",
406            ));
407        }
408        Ok(())
409    }
410
411    /// Execute the circuit once.
412    ///
413    /// With a noise model attached the probabilities are the exact noisy
414    /// distribution rather than one trajectory, so the run needs the
415    /// density-matrix backend; the classical bits are one draw, matching
416    /// `shots(1)`. Readout error reaches the draw and not the state, so a
417    /// model carrying it is rejected rather than answered with two fields from
418    /// different distributions; `shots` and `sample_counts` apply it.
419    #[inline]
420    pub fn run(self) -> Result<RunOutcome> {
421        let seed = self.seed_value();
422        if self.require_exact {
423            reject_approximate_route(&self.kind, self.circuit)?;
424        }
425        if let Some(noise_model) = self.noise_model {
426            require_exact_mixture(&self.kind, "a single run")?;
427            reject_readout_at(self.circuit, noise_model, "a single run")?;
428            let probabilities = exact_noisy_probabilities(
429                &self.kind,
430                self.circuit,
431                noise_model,
432                self.initial_state,
433                seed,
434            )?;
435            let classical_bits =
436                sample_exact_noisy_shots(&probabilities, self.circuit, noise_model, 1, seed)
437                    .swap_remove(0);
438            let outcome = RunOutcome {
439                classical_bits,
440                probabilities: Some(probabilities),
441                metadata: exact_mixture_metadata(&self.kind),
442                saves: Vec::new(),
443            };
444            ensure_saves_recorded(self.circuit, &outcome)?;
445            return Ok(outcome);
446        }
447        if let Some(state) = self.initial_state {
448            return run_from_initial_state(
449                &self.kind,
450                self.circuit,
451                state,
452                seed,
453                &SimOptions::default(),
454            );
455        }
456        let outcome = run_with_internal(self.kind, self.circuit, seed, SimOptions::default())?;
457        ensure_exact_result(self.require_exact, &outcome.metadata)?;
458        Ok(outcome)
459    }
460
461    /// Refuse a circuit carrying save points on a terminal that cannot return
462    /// them. Only [`Simulate::run`] has a place to put the records.
463    fn reject_saves(&self, terminal: &str) -> Result<()> {
464        if self.circuit.save_count() == 0 {
465            return Ok(());
466        }
467        Err(PrismError::IncompatibleBackend {
468            backend: format!("{:?}", self.kind),
469            reason: format!(
470                "the circuit carries {} save point(s), which only `run` returns, so \
471                 `{terminal}` declines it",
472                self.circuit.save_count()
473            ),
474        })
475    }
476
477    /// [`reject_approximate_route`] for the shot terminals, which without a noise
478    /// model or a start state can sample through the stabilizer-rank expansion.
479    fn reject_approximate_shot_route(&self) -> Result<()> {
480        let sampling = self.noise_model.is_none() && self.initial_state.is_none();
481        reject_predicted_route(approximate_route_name(&self.kind, self.circuit, sampling))
482    }
483
484    /// Execute `num_shots` times, collecting per-shot classical bits. Accepts
485    /// an attached noise model.
486    #[inline]
487    pub fn shots(self, num_shots: usize) -> Result<ShotsResult> {
488        self.reject_saves("shots")?;
489        let seed = self.seed_value();
490        let require_exact = self.require_exact;
491        if require_exact {
492            self.reject_approximate_shot_route()?;
493        }
494        let result = if let Some(noise_model) = self.noise_model {
495            self.require_no_initial_state_under_noise("shot sampling")?;
496            run_shots_with_noise(self.kind, self.circuit, noise_model, num_shots, seed)?
497        } else if let Some(state) = self.initial_state {
498            shots_from_initial_state(&self.kind, self.circuit, state, num_shots, seed)?
499        } else {
500            run_shots_with(self.kind, self.circuit, num_shots, seed)?
501        };
502        ensure_exact_result(require_exact, &result.metadata)?;
503        Ok(result)
504    }
505
506    /// Sample a frequency histogram over `num_shots` executions. Accepts an
507    /// attached noise model.
508    ///
509    /// Counts may be sampled directly from the output distribution, so seeded
510    /// counts can differ from [`Simulate::shots`] plus [`ShotsResult::counts`]
511    /// while drawing from the identical distribution.
512    #[inline]
513    pub fn sample_counts(self, num_shots: usize) -> Result<CountsResult> {
514        self.reject_saves("sample_counts")?;
515        let seed = self.seed_value();
516        if self.require_exact {
517            self.reject_approximate_shot_route()?;
518        }
519        let (counts, metadata) = if let Some(noise_model) = self.noise_model {
520            self.require_no_initial_state_under_noise("count sampling")?;
521            let shots =
522                run_shots_with_noise(self.kind, self.circuit, noise_model, num_shots, seed)?;
523            (shots.counts(), shots.metadata)
524        } else if let Some(state) = self.initial_state {
525            let shots = shots_from_initial_state(&self.kind, self.circuit, state, num_shots, seed)?;
526            (shots.counts(), shots.metadata)
527        } else {
528            run_counts_with(self.kind, self.circuit, num_shots, seed)?
529        };
530        ensure_exact_result(self.require_exact, &metadata)?;
531        Ok(CountsResult {
532            counts,
533            num_classical_bits: self.circuit.num_classical_bits,
534            metadata,
535        })
536    }
537
538    /// Per-qubit marginal probabilities as `(P(0), P(1))` pairs. Rejects
539    /// backends without probability output, and with a noise model attached
540    /// answers exactly from the mixture, which needs the density-matrix
541    /// backend, and rejects a model carrying readout error, since
542    /// `sample_counts` is the terminal that applies it.
543    #[inline]
544    pub fn marginals(self) -> Result<MarginalsResult> {
545        self.reject_saves("marginals")?;
546        let seed = self.seed_value();
547        if self.require_exact {
548            reject_approximate_route(&self.kind, self.circuit)?;
549        }
550        if let Some(noise_model) = self.noise_model {
551            require_exact_mixture(&self.kind, "marginals")?;
552            reject_readout_at(self.circuit, noise_model, "marginals")?;
553            let probs = exact_noisy_probabilities(
554                &self.kind,
555                self.circuit,
556                noise_model,
557                self.initial_state,
558                seed,
559            )?;
560            return Ok(MarginalsResult {
561                marginals: probs.marginals(),
562                metadata: exact_mixture_metadata(&self.kind),
563            });
564        }
565        let result = if let Some(state) = self.initial_state {
566            marginals_from_initial_state(&self.kind, self.circuit, state, seed)?
567        } else {
568            run_marginals_result_with(self.kind, self.circuit, seed)?
569        };
570        ensure_exact_result(self.require_exact, &result.metadata)?;
571        Ok(result)
572    }
573
574    /// Compute `⟨ψ|P|ψ⟩` for each joint Pauli observable on the circuit's
575    /// output state, honoring the selected backend.
576    ///
577    /// Each observable is a product of single-qubit Paulis (identity factors
578    /// omitted). The circuit must be unitary. Clifford circuits propagate each
579    /// observable exactly. Non-Clifford circuits use the state vector while they
580    /// fit it; above that cap the selected backend evaluates the observable on
581    /// its own representation, and a backend without one reports
582    /// `BackendUnsupported` naming itself.
583    ///
584    /// With a noise model attached the value is the exact `Tr(rho P)` on the
585    /// evolved mixture, which needs the density-matrix backend. A model
586    /// carrying readout error is rejected: readout acts on the measurement
587    /// record, which no observable sees, and `shots` on the same model would
588    /// disagree by the readout rate.
589    #[inline]
590    pub fn expectation_values(self, observables: &[Vec<PauliTerm>]) -> Result<Vec<f64>> {
591        self.reject_saves("expectation_values")?;
592        self.expectation_values_reported(observables)
593            .map(ExpectationResult::into_values)
594    }
595
596    /// [`Simulate::expectation_values`] with the provenance of the run and, for
597    /// a route that estimates rather than evaluates, a standard error per value.
598    pub fn expectation_values_reported(
599        self,
600        observables: &[Vec<PauliTerm>],
601    ) -> Result<ExpectationResult> {
602        let seed = self.seed_value();
603        if self.require_exact {
604            reject_approximate_route(&self.kind, self.circuit)?;
605        }
606        if let Some(noise_model) = self.noise_model {
607            reject_readout_at(self.circuit, noise_model, "expectation values")?;
608        }
609        if let BackendKind::PauliPath { epsilon, max_terms } = self.kind {
610            reject_pauli_path_initial_state(self.initial_state)?;
611            return pauli_path_expectations(
612                self.circuit,
613                self.noise_model,
614                observables,
615                epsilon,
616                max_terms,
617            );
618        }
619        if let Some(noise_model) = self.noise_model {
620            require_exact_mixture(&self.kind, "expectation values")?;
621            require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
622            let values = noise::dm_expectation_values(
623                &self.kind,
624                self.circuit,
625                observables,
626                Some(noise_model),
627                self.initial_state,
628                seed,
629            )?;
630            return Ok(analytic_expectations(
631                values,
632                exact_mixture_metadata(&self.kind),
633            ));
634        }
635        if let Some(state) = self.initial_state {
636            require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
637            return expectation_values_from_initial_state(
638                &self.kind,
639                self.circuit,
640                state,
641                observables,
642                seed,
643            );
644        }
645        let result = run_expectation_values_reported(self.kind, self.circuit, observables, seed)?;
646        ensure_exact_result(self.require_exact, &result.metadata)?;
647        Ok(result)
648    }
649
650    /// Compute `⟨H⟩` and its grouped-measurement variance for a weighted
651    /// Pauli observable on the circuit's output state.
652    ///
653    /// The statevector family evaluates one traversal per qubit-wise-commuting
654    /// group and reports the variance; see [`ObservableExpectation::variance`]
655    /// for what the number means. Every other route, including runs with a
656    /// noise model or start state attached, evaluates term by term through
657    /// [`Simulate::expectation_values`] semantics and reports the weighted
658    /// mean with no variance. A noise model carrying readout error is rejected
659    /// for the same reason as there.
660    pub fn observable_expectation(
661        self,
662        observable: &PauliObservable,
663    ) -> Result<ObservableExpectation> {
664        self.reject_saves("observable_expectation")?;
665        self.observable_expectation_ref(observable)
666    }
667
668    /// [`Simulate::observable_expectation`] without consuming the builder, so
669    /// [`Simulate::observable_variance`] can evaluate `H` and `H^2` on one
670    /// request.
671    fn observable_expectation_ref(
672        &self,
673        observable: &PauliObservable,
674    ) -> Result<ObservableExpectation> {
675        let seed = self.seed_value();
676        if self.require_exact {
677            reject_approximate_route(&self.kind, self.circuit)?;
678        }
679        if let Some(noise_model) = self.noise_model {
680            reject_readout_at(self.circuit, noise_model, "observable expectation")?;
681        }
682        if let BackendKind::PauliPath { epsilon, max_terms } = self.kind {
683            reject_pauli_path_initial_state(self.initial_state)?;
684            let result = pauli_path_expectations(
685                self.circuit,
686                self.noise_model,
687                &observable_vecs(observable),
688                epsilon,
689                max_terms,
690            )?;
691            let metadata = result.metadata;
692            return Ok(weighted_observable_result(
693                observable,
694                &result.values,
695                None,
696                metadata,
697            ));
698        }
699        if let Some(noise_model) = self.noise_model {
700            require_exact_mixture(&self.kind, "expectation values")?;
701            require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
702            let values = noise::dm_expectation_values(
703                &self.kind,
704                self.circuit,
705                &observable_vecs(observable),
706                Some(noise_model),
707                self.initial_state,
708                seed,
709            )?;
710            return Ok(weighted_observable_result(
711                observable,
712                &values,
713                None,
714                exact_mixture_metadata(&self.kind),
715            ));
716        }
717        if let Some(state) = self.initial_state {
718            require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
719            let result = expectation_values_from_initial_state(
720                &self.kind,
721                self.circuit,
722                state,
723                &observable_vecs(observable),
724                seed,
725            )?;
726            return Ok(weighted_observable_result(
727                observable,
728                &result.values,
729                result.std_errors.as_deref(),
730                result.metadata,
731            ));
732        }
733        let result =
734            run_observable_expectation_reported(self.kind.clone(), self.circuit, observable, seed)?;
735        ensure_exact_result(self.require_exact, &result.metadata)?;
736        Ok(result)
737    }
738
739    /// `Var(H) = <H^2> - <H>^2` for a weighted Pauli observable on the
740    /// circuit's output state.
741    ///
742    /// This is the spread of the operator itself, the number a shot-based
743    /// estimate of `<H>` converges on dividing by the shot count. It is not
744    /// [`ObservableExpectation::variance`], which sums per-group variances and
745    /// so drops the covariance between measurement groups.
746    ///
747    /// Evaluates `H` and the square of its traceless part through
748    /// [`Simulate::observable_expectation`], so backend routing, noise, and
749    /// start states behave as they do there. The constant term is held out of
750    /// the square rather than cancelled inside it; see
751    /// [`PauliObservable::split_identity`]. The square carries up to `T^2`
752    /// terms over `H`'s `T`; see [`PauliObservable::square`].
753    pub fn observable_variance(self, observable: &PauliObservable) -> Result<ObservableVariance> {
754        self.reject_saves("observable_variance")?;
755        let (offset, traceless) = observable.split_identity();
756        let mean = self.observable_expectation_ref(observable)?;
757        let second = self.observable_expectation_ref(&traceless.square())?;
758        let centered = mean.mean - offset;
759        Ok(ObservableVariance {
760            variance: second.mean - centered * centered,
761            mean: mean.mean,
762            metadata: mean.metadata,
763        })
764    }
765
766    /// Joint probability distribution over `qubits`, `2^k` entries with
767    /// `qubits[0]` in the lowest bit.
768    ///
769    /// The subset generalizes [`Simulate::marginals`], which reports each
770    /// qubit on its own and so cannot show correlation: a Bell pair reads
771    /// `(0.5, 0.5)` twice there and `[0.5, 0, 0, 0.5]` here. Routing follows
772    /// [`Simulate::run`], including the exact mixture a noise model asks for
773    /// and its rejection of readout error, which acts on the measurement
774    /// record rather than on the state.
775    ///
776    /// A backend that exposes no distribution for the circuit reports
777    /// `BackendUnsupported` naming itself.
778    pub fn probabilities_of(self, qubits: &[usize]) -> Result<Vec<f64>> {
779        self.reject_saves("probabilities_of")?;
780        crate::backend::schmidt::validate_qubit_set(qubits, self.circuit.num_qubits)?;
781        let kind = format!("{:?}", self.kind);
782        let outcome = self.run()?;
783        let probabilities = outcome
784            .probabilities
785            .ok_or(PrismError::BackendUnsupported {
786                backend: kind,
787                operation: "a probability distribution to marginalize".into(),
788            })?;
789        Ok(probabilities.subset_marginal(qubits))
790    }
791
792    /// Full amplitude vector of the circuit's output state, honoring the
793    /// selected backend.
794    ///
795    /// Indexed with qubit 0 in the least significant bit, so `x q[0]` puts the
796    /// amplitude at index 1. The circuit must be unitary, for the reason
797    /// [`Simulate::reduced_density_matrix`] gives.
798    ///
799    /// A noise model declines: a mixture has no single amplitude vector, and
800    /// [`Simulate::reduced_density_matrix`] over the whole register is the
801    /// terminal that answers there. The density-matrix backend declines for
802    /// the same reason whether or not noise is attached.
803    ///
804    /// The vector holds `2^n` amplitudes, so a register past the dense export
805    /// cap reports `IncompatibleBackend` before allocating rather than after.
806    pub fn state_vector(self) -> Result<Vec<Complex64>> {
807        self.reject_saves("state_vector")?;
808        let seed = self.seed_value();
809        let diagnostic = Diagnostic::StateVector;
810        require_unitary_circuit(&self.kind, self.circuit, "a statevector requires")?;
811        if self.require_exact {
812            reject_approximate_route(&self.kind, self.circuit)?;
813        }
814        if self.noise_model.is_some() {
815            return Err(PrismError::IncompatibleBackend {
816                backend: format!("{:?}", self.kind),
817                reason: format!(
818                    "{} is a pure state; a noise model evolves a mixture, which \
819                     `reduced_density_matrix` over the whole register reports",
820                    diagnostic.terminal()
821                ),
822            });
823        }
824        let backend = diagnostic_backend(
825            &self.kind,
826            self.circuit,
827            self.initial_state,
828            seed,
829            diagnostic,
830            self.circuit.num_qubits,
831        )?;
832        ensure_exact_result(self.require_exact, &backend_metadata(&*backend))?;
833        backend.export_statevector()
834    }
835
836    /// Reduced density matrix of `qubits` on the circuit's output state,
837    /// honoring the selected backend.
838    ///
839    /// Row major with side `2^k`; [`ReducedDensityMatrix::data`] states the
840    /// index order. The subsystem is named once and may be the whole register.
841    /// The circuit must be unitary: the answer is read off one state, and a
842    /// measurement, reset or conditional leaves one seeded branch of several.
843    ///
844    /// An explicitly selected backend that holds no partial trace reports
845    /// `BackendUnsupported` naming itself. Under [`BackendKind::Auto`] a route
846    /// that cannot answer falls back to the statevector while the circuit fits
847    /// its cap, so the diagnostic is served rather than declined by a choice
848    /// the caller did not make. With a noise model attached the answer is the
849    /// marginal of the exact mixture, which needs the density-matrix backend.
850    pub fn reduced_density_matrix(self, qubits: &[usize]) -> Result<ReducedDensityMatrix> {
851        self.reject_saves("reduced_density_matrix")?;
852        let seed = self.seed_value();
853        let diagnostic = Diagnostic::ReducedDensityMatrix;
854        let terminal = diagnostic.terminal();
855        crate::backend::schmidt::validate_qubit_set(qubits, self.circuit.num_qubits)?;
856        require_unitary_circuit(
857            &self.kind,
858            self.circuit,
859            "a reduced density matrix requires",
860        )?;
861        if self.require_exact {
862            reject_approximate_route(&self.kind, self.circuit)?;
863        }
864        if let Some(noise_model) = self.noise_model {
865            reject_readout_at(self.circuit, noise_model, terminal)?;
866            require_exact_mixture(&self.kind, terminal)?;
867            let mut mixture = noise::evolve_density_matrix(
868                &self.kind,
869                self.circuit,
870                Some(noise_model),
871                self.initial_state,
872                seed,
873            )?;
874            return Ok(ReducedDensityMatrix {
875                qubits: qubits.to_vec(),
876                data: mixture.reduced_density_matrix(qubits)?,
877                metadata: exact_mixture_metadata(&self.kind),
878            });
879        }
880        let mut backend = diagnostic_backend(
881            &self.kind,
882            self.circuit,
883            self.initial_state,
884            seed,
885            diagnostic,
886            qubits.len(),
887        )?;
888        let metadata = backend_metadata(&*backend);
889        ensure_exact_result(self.require_exact, &metadata)?;
890        Ok(ReducedDensityMatrix {
891            qubits: qubits.to_vec(),
892            data: backend.reduced_density_matrix(qubits)?,
893            metadata,
894        })
895    }
896
897    /// Entanglement entropy of `subsystem` across its cut with the rest of the
898    /// register, in nats, honoring the selected backend.
899    ///
900    /// `subsystem` must leave both sides of the cut non-empty, and the circuit
901    /// must be unitary, for the reason [`Simulate::reduced_density_matrix`]
902    /// gives. The Schmidt values come back with the entropy, descending and
903    /// normalized.
904    ///
905    /// A backend that holds the entropy without the spectrum behind it, a
906    /// stabilizer cut past the export cap, answers with
907    /// [`EntropyResult::schmidt_values`] set to `None`.
908    /// An explicitly selected backend that holds neither reports
909    /// `BackendUnsupported` naming itself; under [`BackendKind::Auto`] such a
910    /// route falls back to the statevector while the circuit fits its cap. A
911    /// noise model declines outright: it sends the run to the density matrix,
912    /// whose mixed state has no Schmidt decomposition.
913    pub fn entanglement_entropy(self, subsystem: &[usize]) -> Result<EntropyResult> {
914        self.reject_saves("entanglement_entropy")?;
915        let seed = self.seed_value();
916        let diagnostic = Diagnostic::Entropy;
917        let terminal = diagnostic.terminal();
918        crate::backend::schmidt::validate_subsystem(subsystem, self.circuit.num_qubits)?;
919        require_unitary_circuit(&self.kind, self.circuit, "entanglement entropy requires")?;
920        if self.require_exact {
921            reject_approximate_route(&self.kind, self.circuit)?;
922        }
923        if let Some(noise_model) = self.noise_model {
924            reject_readout_at(self.circuit, noise_model, terminal)?;
925            require_exact_mixture(&self.kind, terminal)?;
926            let mut mixture = noise::evolve_density_matrix(
927                &self.kind,
928                self.circuit,
929                Some(noise_model),
930                self.initial_state,
931                seed,
932            )?;
933            return Ok(EntropyResult {
934                subsystem: subsystem.to_vec(),
935                entropy: mixture.entanglement_entropy(subsystem)?,
936                schmidt_values: None,
937                metadata: exact_mixture_metadata(&self.kind),
938            });
939        }
940        let mut backend = diagnostic_backend(
941            &self.kind,
942            self.circuit,
943            self.initial_state,
944            seed,
945            diagnostic,
946            subsystem.len(),
947        )?;
948        let metadata = backend_metadata(&*backend);
949        ensure_exact_result(self.require_exact, &metadata)?;
950        // A tableau past the export cap holds the entropy as a rank while its
951        // `2^r` equal values do not fit, so the spectrum declining is not the
952        // terminal declining. A backend holding neither reports the spectrum's
953        // own error, which is what its entropy raises too.
954        let (entropy, schmidt_values) = match backend.schmidt_values(subsystem) {
955            Ok(values) => (
956                crate::backend::schmidt::entropy_of_schmidt_values(&values),
957                Some(values),
958            ),
959            Err(declined) => match backend.entanglement_entropy(subsystem) {
960                Ok(entropy) => (entropy, None),
961                Err(_) => return Err(declined),
962            },
963        };
964        Ok(EntropyResult {
965            subsystem: subsystem.to_vec(),
966            entropy,
967            schmidt_values,
968            metadata,
969        })
970    }
971
972    /// `|<a|b>|^2` between this circuit's output state and `other`'s, honoring
973    /// the backend each side selected.
974    ///
975    /// The two circuits must declare the same width, and both must be unitary
976    /// for the reason [`Simulate::reduced_density_matrix`] gives. Each side
977    /// carries its own backend, seed and start state, and each resolves to a
978    /// single backend rather than the decomposed route, since two circuits
979    /// need not split into the same independent blocks.
980    ///
981    /// A pair of unlike representations is served by a dense export of both
982    /// states, so it reaches as far as the export cap does. A pair that shares
983    /// one answers natively at any width: two chains in the same site order,
984    /// two tableaux, two product states, or two sparse maps. A noise model on
985    /// either side is rejected, since the fidelity of two mixtures is not an
986    /// inner product.
987    pub fn overlap(self, other: Simulate<'_, Seeded>) -> Result<OverlapResult> {
988        self.reject_saves("overlap")?;
989        let diagnostic = Diagnostic::Overlap;
990        if self.circuit.num_qubits != other.circuit.num_qubits {
991            return Err(PrismError::InvalidParameter {
992                message: format!(
993                    "{} needs two circuits of the same width; got {} and {} qubits",
994                    diagnostic.terminal(),
995                    self.circuit.num_qubits,
996                    other.circuit.num_qubits
997                ),
998            });
999        }
1000        let (left_backend, left) = self.overlap_side(diagnostic)?;
1001        let (right_backend, right) = other.overlap_side(diagnostic)?;
1002        Ok(OverlapResult {
1003            fidelity: left_backend.overlap_sq(&*right_backend)?,
1004            left,
1005            right,
1006        })
1007    }
1008
1009    /// One side of [`Simulate::overlap`]: the guards both sides answer to,
1010    /// then the backend this side's kind resolves to, run and handed back with
1011    /// its provenance.
1012    fn overlap_side(self, diagnostic: Diagnostic) -> Result<(Box<dyn Backend>, RunMetadata)> {
1013        let seed = self.seed_value();
1014        let terminal = diagnostic.terminal();
1015        require_unitary_circuit(&self.kind, self.circuit, "a state overlap requires")?;
1016        if self.require_exact {
1017            reject_approximate_route(&self.kind, self.circuit)?;
1018        }
1019        if self.noise_model.is_some() {
1020            return Err(PrismError::IncompatibleBackend {
1021                backend: format!("{:?}", self.kind),
1022                reason: format!(
1023                    "{terminal} is an inner product of two pure states, and a noise model \
1024                     evolves a mixture, whose fidelity is a different computation; drop \
1025                     the model, or compare the mixtures through `reduced_density_matrix`"
1026                ),
1027            });
1028        }
1029        let backend = diagnostic_backend(
1030            &self.kind,
1031            self.circuit,
1032            self.initial_state,
1033            seed,
1034            diagnostic,
1035            0,
1036        )?;
1037        let metadata = backend_metadata(&*backend);
1038        ensure_exact_result(self.require_exact, &metadata)?;
1039        Ok((backend, metadata))
1040    }
1041
1042    /// Compute `⟨H⟩` and its exact gradient with respect to the bound
1043    /// parameters using the adjoint method.
1044    ///
1045    /// `hamiltonian` is a weighted Pauli sum `Σ c_k P_k` with real
1046    /// coefficients. `params` declares which gate instructions carry parameters.
1047    /// Runs on the statevector backend; the selected backend must be `Auto` or
1048    /// `Statevector`. The circuit must be unitary. See
1049    /// [`gradient::run_expectation_gradient`].
1050    #[inline]
1051    pub fn expectation_gradient(
1052        self,
1053        hamiltonian: &[(f64, Vec<PauliTerm>)],
1054        params: &crate::circuit::Parameters,
1055    ) -> Result<gradient::ExpectationGradient> {
1056        self.reject_saves("expectation_gradient")?;
1057        let seed = self.seed_value();
1058        if self.require_exact {
1059            reject_approximate_route(&self.kind, self.circuit)?;
1060        }
1061        if self.noise_model.is_some() {
1062            return Err(PrismError::IncompatibleBackend {
1063                backend: format!("{:?}", self.kind),
1064                reason: "the adjoint method backpropagates through a pure state, so no backend \
1065                         has a noisy adjoint path; drop the noise model, or take \
1066                         `expectation_gradient_shift` on the density-matrix backend"
1067                    .into(),
1068            });
1069        }
1070        if self.initial_state.is_some() {
1071            return Err(reject_initial_state(
1072                &self.kind,
1073                "the adjoint gradient",
1074                "the backward pass reconstructs the input register by inverting the circuit from \
1075                 |0...0>, so a start state would have to be inverted with it",
1076            ));
1077        }
1078        if !(self.kind.is_auto() || matches!(self.kind, BackendKind::Statevector)) {
1079            return Err(PrismError::IncompatibleBackend {
1080                backend: format!("{:?}", self.kind),
1081                reason:
1082                    "adjoint gradients run on the statevector backend; select Auto or Statevector"
1083                        .into(),
1084            });
1085        }
1086        gradient::run_expectation_gradient(self.circuit, hamiltonian, params, seed)
1087    }
1088
1089    /// Compute `⟨H⟩` and its gradient by the parameter-shift rule on the
1090    /// selected backend.
1091    ///
1092    /// Serves the cases [`Simulate::expectation_gradient`] declines: any
1093    /// backend with a native observable path, circuits containing `QftBlock`,
1094    /// widths past the statevector cap, and a noise model. It differentiates the
1095    /// same gate set (`Rx`, `Ry`, `Rz`, `Rzz`, `P`, `PauliRot`) at `1 + 2 * links`
1096    /// circuit evaluations against the adjoint's one, so the adjoint stays the
1097    /// better choice where it applies. A backend with no native observable path
1098    /// reports `BackendUnsupported` naming itself. Under a noise model every
1099    /// evaluation reads the exact mixture, so the backend must be
1100    /// [`BackendKind::DensityMatrix`] or its device sibling; the shift stays
1101    /// exact because the channels do not depend on the shifted angle. See
1102    /// [`gradient::run_expectation_gradient_shift`].
1103    #[inline]
1104    pub fn expectation_gradient_shift(
1105        self,
1106        hamiltonian: &[(f64, Vec<PauliTerm>)],
1107        params: &crate::circuit::Parameters,
1108    ) -> Result<gradient::ExpectationGradient> {
1109        self.reject_saves("expectation_gradient_shift")?;
1110        let seed = self.seed_value();
1111        if self.require_exact {
1112            reject_approximate_route(&self.kind, self.circuit)?;
1113        }
1114        if self.noise_model.is_some()
1115            && !self.kind.is_density_matrix()
1116            && !matches!(self.kind, BackendKind::PauliPath { .. })
1117        {
1118            return Err(PrismError::IncompatibleBackend {
1119                backend: format!("{:?}", self.kind),
1120                reason: "a parameter-shift gradient under a noise model evaluates the exact \
1121                         mixed state, which only the density-matrix backend holds; select \
1122                         `BackendKind::DensityMatrix` or its device sibling, or drop the noise \
1123                         model"
1124                    .into(),
1125            });
1126        }
1127        gradient::shift_gradient(
1128            &self.kind,
1129            self.circuit,
1130            hamiltonian,
1131            params,
1132            self.noise_model,
1133            self.initial_state,
1134            seed,
1135        )
1136    }
1137}
1138
1139/// Start a query-aware simulation request for `circuit`.
1140///
1141/// The returned builder defaults to automatic backend selection; chain
1142/// [`Simulate::seed`] to unlock the query methods.
1143///
1144/// # Examples
1145///
1146/// ```
1147/// use prism_q::{Circuit, Gate, simulate};
1148///
1149/// let mut circuit = Circuit::new(2, 0);
1150/// circuit.add_gate(Gate::H, &[0]);
1151/// circuit.add_gate(Gate::Cx, &[0, 1]);
1152///
1153/// let result = simulate(&circuit).seed(42).run()?;
1154/// let probs = result.probabilities.expect("no probabilities").to_vec();
1155/// // Bell state: ~50% |00>, ~50% |11>
1156/// assert!((probs[0] - 0.5).abs() < 1e-10);
1157/// assert!((probs[3] - 0.5).abs() < 1e-10);
1158/// # Ok::<(), prism_q::PrismError>(())
1159/// ```
1160#[inline]
1161pub fn simulate(circuit: &Circuit) -> Simulate<'_, Unseeded> {
1162    Simulate {
1163        circuit,
1164        kind: BackendKind::Auto,
1165        seed: Unseeded,
1166        noise_model: None,
1167        initial_state: None,
1168        require_exact: false,
1169    }
1170}
1171
1172/// Rejection naming the two terminals the Pauli path engine serves.
1173fn reject_pauli_path(terminal: &str) -> PrismError {
1174    PrismError::IncompatibleBackend {
1175        backend: "PauliPath".into(),
1176        reason: format!(
1177            "{terminal} is not served by Pauli path propagation, which answers \
1178             `expectation_values` and `observable_expectation` only"
1179        ),
1180    }
1181}
1182
1183fn reject_pauli_path_initial_state(state: Option<StartState<'_>>) -> Result<()> {
1184    match state {
1185        Some(_) => Err(reject_pauli_path("a start state")),
1186        None => Ok(()),
1187    }
1188}
1189
1190/// One Pauli path evaluation per observable, under `noise` when one is
1191/// attached and on the bare circuit otherwise.
1192///
1193/// The route is exact only when nothing was truncated, so exactness is decided
1194/// by the discarded mass the run reports rather than by the parameters it was
1195/// given: a budget that never binds still returns an exact value.
1196fn pauli_path_expectations(
1197    circuit: &Circuit,
1198    noise: Option<&NoiseModel>,
1199    observables: &[Vec<PauliTerm>],
1200    epsilon: f64,
1201    max_terms: usize,
1202) -> Result<ExpectationResult> {
1203    let empty;
1204    let noise = match noise {
1205        Some(model) => model,
1206        None => {
1207            empty = NoiseModel {
1208                after_gate: vec![Vec::new(); circuit.instructions.len()],
1209                readout: vec![None; circuit.num_classical_bits],
1210            };
1211            &empty
1212        }
1213    };
1214    let mut values = Vec::with_capacity(observables.len());
1215    let mut discarded = 0.0f64;
1216    for obs in observables {
1217        let result =
1218            unified_pauli::run_pauli_path_observable(circuit, noise, obs, epsilon, max_terms)?;
1219        discarded = discarded.max(result.total_discarded);
1220        values.push(result.mean);
1221    }
1222    let metadata = if discarded > 0.0 {
1223        RunMetadata::approximate(ResolvedBackend::PauliPath)
1224    } else {
1225        RunMetadata::exact(ResolvedBackend::PauliPath)
1226    };
1227    Ok(analytic_expectations(values, metadata))
1228}
1229
1230fn require_exact_mixture(kind: &BackendKind, terminal: &str) -> Result<()> {
1231    if kind.is_density_matrix() {
1232        return Ok(());
1233    }
1234    Err(PrismError::IncompatibleBackend {
1235        backend: format!("{kind:?}"),
1236        reason: format!(
1237            "{terminal} under a noise model reads the exact mixed state, which only the \
1238             density-matrix backend holds; select it, or average trajectories through `shots` \
1239             or `sample_counts`"
1240        ),
1241    })
1242}
1243
1244/// Gate for the terminals that answer from the state, mixed or pure, which
1245/// readout error is not part of.
1246///
1247/// The condition is the one under which a draw would actually differ. Entries
1248/// past the circuit's bit count never apply, matching what
1249/// `sample_exact_noisy_shots` consumes, so a model built for a wider circuit is
1250/// not rejected on bits this one does not have, and a zero-rate entry flips
1251/// nothing, so a sweep that starts at zero is not rejected at its first point.
1252fn reject_readout_at(
1253    circuit: &Circuit,
1254    noise_model: &noise::NoiseModel,
1255    terminal: &str,
1256) -> Result<()> {
1257    let inert = noise_model
1258        .readout
1259        .iter()
1260        .take(circuit.num_classical_bits)
1261        .all(|entry| entry.as_ref().is_none_or(|readout| readout.is_inert()));
1262    if inert {
1263        return Ok(());
1264    }
1265    Err(PrismError::InvalidParameter {
1266        message: format!(
1267            "{terminal} answers from the mixed state, which readout error is not part of: it \
1268             acts on the measurement record and is indexed by classical bit, not qubit. Drop \
1269             it from the model, or use `shots` or `sample_counts`, which apply it"
1270        ),
1271    })
1272}
1273
1274/// Gate for the terminals that cannot carry a start state through their own
1275/// machinery.
1276fn reject_initial_state(kind: &BackendKind, terminal: &str, instead: &str) -> PrismError {
1277    PrismError::IncompatibleBackend {
1278        backend: format!("{kind:?}"),
1279        reason: format!("{terminal} does not accept a start state; {instead}"),
1280    }
1281}
1282
1283/// Reject a start state whose width disagrees with the circuit's.
1284///
1285/// The circuit's declared register wins: the buffer sets the backend's width,
1286/// so a shorter or longer one would silently simulate a different register
1287/// than the one the instructions index. A register too wide to index with a
1288/// `usize` has no dense start state at all.
1289fn check_initial_state_len(state: StartState<'_>, num_qubits: usize) -> Result<()> {
1290    let (buffer, bits, entries) = match state {
1291        StartState::Amplitudes(amplitudes) => (amplitudes, num_qubits, "amplitudes"),
1292        StartState::DensityMatrix(rho) => (rho, 2 * num_qubits, "density matrix entries"),
1293    };
1294    let want = (bits < usize::BITS as usize).then(|| 1usize << bits);
1295    if want == Some(buffer.len()) {
1296        return Ok(());
1297    }
1298    let needs = match want {
1299        Some(count) => count.to_string(),
1300        None => format!("2^{bits}"),
1301    };
1302    Err(PrismError::InvalidParameter {
1303        message: format!(
1304            "start state has {} {entries}, but a {num_qubits}-qubit circuit needs {needs}",
1305            buffer.len()
1306        ),
1307    })
1308}
1309
1310/// Build the constrained backend for a start state and load it.
1311fn backend_from_initial_state(
1312    kind: &BackendKind,
1313    circuit: &Circuit,
1314    state: StartState<'_>,
1315    seed: u64,
1316) -> Result<Box<dyn Backend>> {
1317    if !kind.is_auto() {
1318        validate_explicit_backend(kind, circuit)?;
1319    }
1320    check_initial_state_len(state, circuit.num_qubits)?;
1321    let mut backend = initial_state_plan(kind, circuit.num_qubits, state)?.build(seed);
1322    state.load(&mut *backend, circuit.num_classical_bits)?;
1323    Ok(backend)
1324}
1325
1326/// Expand the gate forms `backend` has no native kernel for, `QftBlock` and
1327/// `PauliRot`, or on a backend with the `QftBlock` kernel fold expanded QFTs into
1328/// blocks. The stream stays borrowed when nothing changes.
1329fn expand_for_backend<'c>(
1330    backend: &dyn Backend,
1331    circuit: &'c Circuit,
1332) -> std::borrow::Cow<'c, Circuit> {
1333    use std::borrow::Cow;
1334    let expanded = if backend.supports_qft_block() {
1335        crate::circuit::recognize_qft_blocks(circuit)
1336    } else {
1337        crate::circuit::expand_qft_blocks(circuit)
1338    };
1339    if backend.supports_pauli_rotation() {
1340        return expanded;
1341    }
1342    match expanded {
1343        Cow::Borrowed(borrowed) => crate::circuit::expand_pauli_rotations(borrowed),
1344        Cow::Owned(owned) => {
1345            let rotations = crate::circuit::expand_pauli_rotations(&owned);
1346            if let Cow::Owned(expanded_rotations) = rotations {
1347                return Cow::Owned(expanded_rotations);
1348            }
1349            Cow::Owned(owned)
1350        }
1351    }
1352}
1353
1354/// Fuse `circuit` against what `backend` accepts and how wide a buffer it
1355/// sweeps, the two backend facts the pass pipeline is gated on.
1356fn fuse_for_backend<'a>(
1357    backend: &dyn Backend,
1358    circuit: &'a Circuit,
1359) -> std::borrow::Cow<'a, Circuit> {
1360    crate::circuit::fusion::fuse_circuit_for_width(
1361        circuit,
1362        backend.supports_fused_gates(),
1363        backend.fusion_state_qubits(circuit.num_qubits),
1364    )
1365}
1366
1367/// [`apply_fused_circuit`] for a terminal that has nowhere to put a save.
1368///
1369/// Only [`Simulate::run`] returns save records, so every other terminal
1370/// declines a circuit carrying a save point rather than running it and
1371/// discarding what the points recorded.
1372fn apply_fused_without_saves(
1373    backend: &mut dyn Backend,
1374    circuit: &Circuit,
1375    terminal: &str,
1376) -> Result<()> {
1377    if circuit.save_count() > 0 {
1378        return Err(PrismError::IncompatibleBackend {
1379            backend: backend.name().to_string(),
1380            reason: format!(
1381                "save points are returned by `run`, and `{terminal}` has nowhere to put them"
1382            ),
1383        });
1384    }
1385    apply_fused_circuit(backend, circuit).map(|_| ())
1386}
1387
1388/// Fuse `circuit` for `backend` and apply it, leaving initialization to the
1389/// caller. The start-state analogue of [`execute`], which owns the |0...0⟩ init.
1390fn apply_fused_circuit(backend: &mut dyn Backend, circuit: &Circuit) -> Result<Vec<SaveRecord>> {
1391    let expanded = expand_for_backend(&*backend, circuit);
1392    let fused = fuse_for_backend(&*backend, &expanded);
1393    apply_recording_saves(backend, &fused.instructions)
1394}
1395
1396fn run_from_initial_state(
1397    kind: &BackendKind,
1398    circuit: &Circuit,
1399    state: StartState<'_>,
1400    seed: u64,
1401    opts: &SimOptions,
1402) -> Result<RunOutcome> {
1403    let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
1404    let saves = apply_fused_circuit(&mut *backend, circuit)?;
1405
1406    let probabilities = if opts.probabilities {
1407        try_backend_probabilities(&*backend)?
1408    } else {
1409        None
1410    };
1411    Ok(RunOutcome {
1412        classical_bits: backend.classical_results().to_vec(),
1413        probabilities,
1414        metadata: backend_metadata(&*backend),
1415        saves,
1416    })
1417}
1418
1419/// Shots for a start state. Terminal measurements sample one evolved
1420/// distribution; anything else replays the start state per shot, which is what
1421/// mid-circuit collapse and classical feedback need.
1422fn shots_from_initial_state(
1423    kind: &BackendKind,
1424    circuit: &Circuit,
1425    state: StartState<'_>,
1426    num_shots: usize,
1427    seed: u64,
1428) -> Result<ShotsResult> {
1429    let bits = circuit.num_classical_bits;
1430    if circuit.has_terminal_measurements_only() && !circuit.has_resets() {
1431        let stripped = circuit.without_measurements();
1432        let mut backend = backend_from_initial_state(kind, &stripped, state, seed)?;
1433        apply_fused_circuit(&mut *backend, &stripped)?;
1434        let meas_map = circuit.measurement_map();
1435        let shots = if backend.supports_native_sampling() {
1436            let samples = backend.sample_basis_states(num_shots, seed)?;
1437            Some(shots_from_basis_samples(&samples, &meas_map, bits))
1438        } else {
1439            try_backend_probabilities(&*backend)?
1440                .map(|probs| sample_shots(&probs, &meas_map, bits, num_shots, seed))
1441        };
1442        if let Some(shots) = shots {
1443            return Ok(
1444                ShotsResult::from_shots(shots, bits).with_metadata(backend_metadata(&*backend))
1445            );
1446        }
1447    }
1448
1449    // Plan, expansion, and fusion are shot independent, so they are hoisted out
1450    // of the replay loop, matching `run_shots_per_shot`.
1451    if !kind.is_auto() {
1452        validate_explicit_backend(kind, circuit)?;
1453    }
1454    check_initial_state_len(state, circuit.num_qubits)?;
1455    let plan = initial_state_plan(kind, circuit.num_qubits, state)?;
1456    let probe = plan.build(seed);
1457    let expanded = expand_for_backend(&*probe, circuit);
1458    let fused = fuse_for_backend(&*probe, &expanded);
1459
1460    let route = plan.resolved();
1461    let states = [(route, circuit.num_qubits)];
1462    collect_shots(
1463        circuit,
1464        num_shots,
1465        seed,
1466        route,
1467        kind,
1468        &states,
1469        |shot_seed| {
1470            let mut backend = plan.build(shot_seed);
1471            state.load(&mut *backend, circuit.num_classical_bits)?;
1472            backend.apply_instructions(&fused.instructions)?;
1473            Ok((
1474                backend.classical_results().to_vec(),
1475                backend_metadata(&*backend),
1476            ))
1477        },
1478    )
1479}
1480
1481fn marginals_from_initial_state(
1482    kind: &BackendKind,
1483    circuit: &Circuit,
1484    state: StartState<'_>,
1485    seed: u64,
1486) -> Result<MarginalsResult> {
1487    let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
1488    apply_fused_without_saves(&mut *backend, circuit, "marginals")?;
1489    if backend.supports_pauli_expectation() {
1490        return marginals_from_pauli_expectations(&*backend, circuit.num_qubits);
1491    }
1492    Ok(MarginalsResult {
1493        marginals: Probabilities::Dense(backend.probabilities()?).marginals(),
1494        metadata: backend_metadata(&*backend),
1495    })
1496}
1497
1498fn expectation_values_from_initial_state(
1499    kind: &BackendKind,
1500    circuit: &Circuit,
1501    state: StartState<'_>,
1502    observables: &[Vec<PauliTerm>],
1503    seed: u64,
1504) -> Result<ExpectationResult> {
1505    // Before the run, matching the dense statevector path.
1506    let masks = observables
1507        .iter()
1508        .map(|obs| pauli_masks(obs, circuit.num_qubits))
1509        .collect::<Result<Vec<_>>>()?;
1510
1511    let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
1512    apply_fused_without_saves(&mut *backend, circuit, "expectation_values")?;
1513    let metadata = backend_metadata(&*backend);
1514    if backend.supports_pauli_expectation() {
1515        let values = backend.pauli_expectations(observables)?;
1516        return Ok(analytic_expectations(values, metadata));
1517    }
1518
1519    let evolved = backend.export_statevector()?;
1520    let (values, _) = pauli_expectations_from_masks(&evolved, &masks);
1521    Ok(analytic_expectations(values, metadata))
1522}
1523
1524/// Reject a result that turned out approximate, for a caller that opted out.
1525///
1526/// The backstop behind [`reject_approximate_route`], which decides from the
1527/// circuit alone. Sparse Pauli dynamics truncates on coefficient magnitudes it
1528/// only learns while propagating, so whether it stayed exact is not knowable
1529/// before the run.
1530fn ensure_exact_result(require_exact: bool, metadata: &RunMetadata) -> Result<()> {
1531    if require_exact && !metadata.is_exact() {
1532        return Err(PrismError::IncompatibleBackend {
1533            backend: format!("{:?}", metadata.backend),
1534            reason: "require_exact rejects an approximate result; this engine discarded state                      weight while running, which the route could not predict"
1535                .into(),
1536        });
1537    }
1538    Ok(())
1539}
1540
1541/// Reject an approximate route for a caller that opted out of one.
1542fn reject_approximate_route(kind: &BackendKind, circuit: &Circuit) -> Result<()> {
1543    reject_predicted_route(approximate_route_name(kind, circuit, false))
1544}
1545
1546/// The engine is named in the error, since a caller who asked for exactness
1547/// wants to know which one would have answered.
1548fn reject_predicted_route(engine: Option<&'static str>) -> Result<()> {
1549    match engine {
1550        Some(engine) => Err(PrismError::IncompatibleBackend {
1551            backend: engine.into(),
1552            reason: "require_exact rejects a route that can discard state weight; drop the \
1553                     requirement to accept the approximation, which the result reports, or \
1554                     select a backend that represents this circuit exactly"
1555                .into(),
1556        }),
1557        None => Ok(()),
1558    }
1559}
1560
1561/// `subject` names what needs the unitary circuit with its verb, so the
1562/// sentence reads for a plural terminal ("expectation values require") and for
1563/// a singular one ("a reduced density matrix requires").
1564fn require_unitary_circuit(kind: &BackendKind, circuit: &Circuit, subject: &str) -> Result<()> {
1565    if has_nonunitary_or_classical_ops(circuit) {
1566        return Err(PrismError::IncompatibleBackend {
1567            backend: format!("{kind:?}"),
1568            reason: format!(
1569                "{subject} a unitary circuit without measurements, resets, or conditionals"
1570            ),
1571        });
1572    }
1573    Ok(())
1574}
1575
1576/// Provenance of a result read off the exact mixture: the density matrix,
1577/// placed wherever `kind` holds it.
1578fn exact_mixture_metadata(kind: &BackendKind) -> RunMetadata {
1579    let metadata = RunMetadata::exact(ResolvedBackend::DensityMatrix);
1580    #[cfg(feature = "gpu")]
1581    if matches!(kind, BackendKind::DensityMatrixGpu { .. }) {
1582        let mut on_device = metadata;
1583        on_device.placement = Placement::Device;
1584        return on_device;
1585    }
1586    #[cfg(not(feature = "gpu"))]
1587    let _ = kind;
1588    metadata
1589}
1590
1591/// Exact output distribution of `circuit` under `noise_model`, evolved once on
1592/// the density matrix. The mixture carries every measurement branch at once,
1593/// so it answers for a whole shot only when the measurements are terminal.
1594fn exact_noisy_probabilities(
1595    kind: &BackendKind,
1596    circuit: &Circuit,
1597    noise_model: &noise::NoiseModel,
1598    initial_state: Option<StartState<'_>>,
1599    seed: u64,
1600) -> Result<Probabilities> {
1601    Ok(Probabilities::Dense(noise::density_matrix_probabilities(
1602        kind,
1603        circuit,
1604        noise_model,
1605        initial_state,
1606        seed,
1607    )?))
1608}
1609
1610/// Draw classical bits from the exact noisy distribution, then apply readout
1611/// error. Readout acts on the outcome rather than on the state, so it is not
1612/// carried by the distribution and has to be applied per draw, on a stream of
1613/// its own so it does not track the state draws.
1614fn sample_exact_noisy_shots(
1615    probs: &Probabilities,
1616    circuit: &Circuit,
1617    noise_model: &noise::NoiseModel,
1618    num_shots: usize,
1619    seed: u64,
1620) -> Vec<Vec<bool>> {
1621    let bits = circuit.num_classical_bits;
1622    let mut shots = sample_shots(probs, &circuit.measurement_map(), bits, num_shots, seed);
1623    if noise_model.readout.iter().any(Option::is_some) {
1624        let readout = trajectory::written_readout(circuit, &noise_model.readout);
1625        let mut rng = trajectory::noise_rng(seed);
1626        for shot in &mut shots {
1627            trajectory::apply_readout_errors(shot, &readout, &mut rng);
1628        }
1629    }
1630    shots
1631}
1632
1633#[inline]
1634fn probs_only_result(probs: Vec<f64>, metadata: RunMetadata) -> RunOutcome {
1635    RunOutcome {
1636        probabilities: Some(Probabilities::Dense(probs)),
1637        classical_bits: vec![],
1638        metadata,
1639        saves: Vec::new(),
1640    }
1641}
1642
1643/// Fail rather than return a run that quietly skipped a save point.
1644///
1645/// Routes that cannot read a state mid-circuit decline by name where they meet
1646/// the instruction. This is the backstop for a route that instead drops the
1647/// instruction while rewriting the circuit, which is what the decomposed and
1648/// exact-mixture paths do.
1649fn ensure_saves_recorded(circuit: &Circuit, outcome: &RunOutcome) -> Result<()> {
1650    let wanted = circuit.save_count();
1651    if wanted == outcome.saves.len() {
1652        return Ok(());
1653    }
1654    Err(PrismError::IncompatibleBackend {
1655        backend: format!("{:?}", outcome.metadata.backend),
1656        reason: format!(
1657            "circuit carries {wanted} save point(s) and the route that ran recorded {}",
1658            outcome.saves.len()
1659        ),
1660    })
1661}
1662
1663fn try_backend_probabilities(backend: &dyn Backend) -> Result<Option<Probabilities>> {
1664    if let Some(factored) = backend.block_probabilities() {
1665        return Ok(Some(factored));
1666    }
1667    match backend.probabilities() {
1668        Ok(probs) => Ok(Some(Probabilities::Dense(probs))),
1669        Err(PrismError::BackendUnsupported { .. }) => Ok(None),
1670        Err(err) => Err(err),
1671    }
1672}
1673
1674/// Core execution: fuse, init, apply, extract.
1675fn execute(backend: &mut dyn Backend, circuit: &Circuit, opts: &SimOptions) -> Result<RunOutcome> {
1676    let expanded = expand_for_backend(&*backend, circuit);
1677    let fused = fuse_for_backend(&*backend, &expanded);
1678    execute_circuit(backend, &fused, opts)
1679}
1680
1681/// The execution route a template settles on, for a caller that holds one
1682/// circuit across many bindings and fuses it itself.
1683///
1684/// Resolving the route from the template rather than from a bound circuit
1685/// matters twice over: it is the work being amortized, and a fused stream
1686/// misreports the circuit to dispatch (a fused Clifford circuit no longer
1687/// looks Clifford).
1688pub(crate) struct PreparedRoute {
1689    plan: BackendPlan,
1690    supports_fused: bool,
1691    fusion_state_qubits: usize,
1692    /// Backend held across points. `init` reuses its state buffer when the
1693    /// width matches, so a sweep pays one `2^n` allocation rather than one per
1694    /// point. Rebuilt when the seed changes, since the seed feeds its RNG.
1695    held: Option<(u64, Box<dyn Backend + Send>)>,
1696    /// Set when the template reads the RNG, so every point gets a fresh backend
1697    /// and measures what a solo run with its seed measures.
1698    draws_randomness: bool,
1699    values: PreparedValues,
1700    observable: PreparedObservable,
1701}
1702
1703/// Who answers [`Simulate::expectation_values`] for a prepared template.
1704#[derive(Clone, Copy)]
1705enum PreparedValues {
1706    Held,
1707    /// `Auto` from [`TENSOR_ROUTE_MIN_QUBITS`]: the bound circuit is offered to
1708    /// the scalar tensor route first, as `Simulate` offers it.
1709    TensorFirst,
1710    /// `Simulate` would take a route the held backend does not reproduce.
1711    Simulate,
1712}
1713
1714/// Who answers [`Simulate::observable_expectation`] for a prepared template.
1715#[derive(Clone, Copy)]
1716enum PreparedObservable {
1717    /// Term by term on the held backend, with no variance.
1718    Held,
1719    /// The grouped evaluator on the held statevector, with its variance.
1720    Grouped,
1721    /// `Simulate` would take a route the held backend does not reproduce.
1722    Simulate,
1723}
1724
1725impl PreparedRoute {
1726    /// True when the chosen backend accepts fused gates, so the caller should
1727    /// hand `run` a fused stream rather than the bound template.
1728    pub(crate) fn supports_fused(&self) -> bool {
1729        self.supports_fused
1730    }
1731
1732    /// The width `simulate` fuses at for the held backend, so a stream fused
1733    /// here matches the one a direct run applies.
1734    pub(crate) fn fusion_state_qubits(&self) -> usize {
1735        self.fusion_state_qubits
1736    }
1737
1738    /// Apply `circuit` verbatim, with no further fusion.
1739    pub(crate) fn run(&mut self, circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
1740        if self.draws_randomness {
1741            let mut backend = self.plan.build(seed);
1742            return execute_circuit(&mut *backend, circuit, &SimOptions::default());
1743        }
1744        execute_circuit(self.held_backend(seed), circuit, &SimOptions::default())
1745    }
1746
1747    /// [`Simulate::expectation_values`] on `bound`, evaluated by applying
1748    /// `applied` verbatim, or `None` when the caller should ask `Simulate`.
1749    pub(crate) fn expectation_values(
1750        &mut self,
1751        bound: &Circuit,
1752        applied: &Circuit,
1753        observables: &[Vec<PauliTerm>],
1754        seed: u64,
1755    ) -> Option<Result<Vec<f64>>> {
1756        match self.values {
1757            PreparedValues::Simulate => return None,
1758            PreparedValues::TensorFirst => {
1759                if let Some(result) = tensor_route_expectations(bound, observables) {
1760                    return Some(result.map(ExpectationResult::into_values));
1761                }
1762            }
1763            PreparedValues::Held => {}
1764        }
1765        Some(
1766            self.held_expectation_values(applied, observables, seed)
1767                .map(|(values, _)| values),
1768        )
1769    }
1770
1771    /// [`Simulate::observable_expectation`] on the circuit `applied` was bound
1772    /// from, or `None` when the caller should ask `Simulate`.
1773    pub(crate) fn observable_expectation(
1774        &mut self,
1775        applied: &Circuit,
1776        observable: &PauliObservable,
1777        seed: u64,
1778    ) -> Option<Result<ObservableExpectation>> {
1779        match self.observable {
1780            PreparedObservable::Simulate => None,
1781            PreparedObservable::Held => Some(
1782                self.held_expectation_values(applied, &observable_vecs(observable), seed)
1783                    .map(|(values, metadata)| {
1784                        weighted_observable_result(observable, &values, None, metadata)
1785                    }),
1786            ),
1787            PreparedObservable::Grouped => Some(self.held_grouped(applied, observable, seed)),
1788        }
1789    }
1790
1791    fn held_backend(&mut self, seed: u64) -> &mut dyn Backend {
1792        if !matches!(&self.held, Some((s, _)) if *s == seed) {
1793            self.held = Some((seed, self.plan.build(seed)));
1794        }
1795        let (_, backend) = self.held.as_mut().expect("just built");
1796        &mut **backend
1797    }
1798
1799    fn held_expectation_values(
1800        &mut self,
1801        circuit: &Circuit,
1802        observables: &[Vec<PauliTerm>],
1803        seed: u64,
1804    ) -> Result<(Vec<f64>, RunMetadata)> {
1805        for observable in observables {
1806            validate_observable(observable, circuit.num_qubits)?;
1807        }
1808        let backend = self.held_backend(seed);
1809        execute_circuit(&mut *backend, circuit, &SimOptions::classical_only())?;
1810        Ok((
1811            backend.pauli_expectations(observables)?,
1812            backend_metadata(backend),
1813        ))
1814    }
1815
1816    fn held_grouped(
1817        &mut self,
1818        circuit: &Circuit,
1819        observable: &PauliObservable,
1820        seed: u64,
1821    ) -> Result<ObservableExpectation> {
1822        for (_, factors) in observable.terms() {
1823            validate_observable(factors, circuit.num_qubits)?;
1824        }
1825        let backend = self.held_backend(seed);
1826        execute_circuit(&mut *backend, circuit, &SimOptions::classical_only())?;
1827        let statevector = backend
1828            .as_any()
1829            .and_then(|any| any.downcast_ref::<StatevectorBackend>())
1830            .expect("a grouped answer is settled only on a statevector plan");
1831        grouped_expectation_on_state(statevector, observable)
1832    }
1833}
1834
1835/// Settle the route for `template`, or `None` when it takes one that reshapes
1836/// execution (decomposition, stabilizer rank, temporal Clifford) or expands
1837/// `QftBlock`, where a caller-supplied stream has nowhere to go.
1838pub(crate) fn prepared_route(kind: &BackendKind, template: &Circuit) -> Option<PreparedRoute> {
1839    if !kind.is_auto() && validate_explicit_backend(kind, template).is_err() {
1840        return None;
1841    }
1842    let ProbabilityRoute::Direct {
1843        has_partial_independence,
1844    } = plan_probability_route(kind, template)
1845    else {
1846        return None;
1847    };
1848    let ExecutionPlan::Backend(plan) = resolve(kind, template, has_partial_independence) else {
1849        return None;
1850    };
1851    let probe = plan.build(0);
1852    let has_qft_block = crate::circuit::any_gate(&template.instructions, &mut |gate| {
1853        matches!(gate, crate::gates::Gate::QftBlock { .. })
1854    });
1855    if has_qft_block && !probe.supports_qft_block() {
1856        return None;
1857    }
1858    let has_pauli_rot = crate::circuit::any_gate(&template.instructions, &mut |gate| {
1859        matches!(gate, crate::gates::Gate::PauliRot(_))
1860    });
1861    if has_pauli_rot && !probe.supports_pauli_rotation() {
1862        return None;
1863    }
1864    let (values, observable) = prepared_expectations(kind, template, &*probe);
1865    Some(PreparedRoute {
1866        supports_fused: probe.supports_fused_gates(),
1867        fusion_state_qubits: probe.fusion_state_qubits(template.num_qubits),
1868        plan,
1869        held: None,
1870        draws_randomness: draws_randomness(template),
1871        values,
1872        observable,
1873    })
1874}
1875
1876/// Settle which expectation terminals the held backend `probe` answers the
1877/// way `Simulate` answers them.
1878///
1879/// Both decline a template the terminals reject, whose error `Simulate`
1880/// reports, and a Clifford template under `Auto` or a stabilizer kind, which
1881/// `Simulate` propagates through the Pauli route rather than a backend. The
1882/// weighted observable takes the grouped evaluator wherever `Simulate` would,
1883/// so its variance survives, and declines when the held backend is not the
1884/// statevector that evaluator reads.
1885fn prepared_expectations(
1886    kind: &BackendKind,
1887    template: &Circuit,
1888    probe: &dyn Backend,
1889) -> (PreparedValues, PreparedObservable) {
1890    let clifford_route =
1891        (kind.is_auto() || kind.is_stabilizer_family()) && template.is_clifford_only();
1892    if draws_randomness(template)
1893        || template.save_count() > 0
1894        || clifford_route
1895        || !probe.supports_pauli_expectation()
1896    {
1897        return (PreparedValues::Simulate, PreparedObservable::Simulate);
1898    }
1899    let fits_dense = template.num_qubits <= max_statevector_qubits();
1900    let values = if kind.is_auto() && fits_dense && template.num_qubits >= TENSOR_ROUTE_MIN_QUBITS {
1901        PreparedValues::TensorFirst
1902    } else {
1903        PreparedValues::Held
1904    };
1905    let grouped = match kind {
1906        BackendKind::Statevector => true,
1907        #[cfg(feature = "gpu")]
1908        BackendKind::StatevectorGpu { .. } => true,
1909        _ => kind.is_auto() && fits_dense,
1910    };
1911    let holds_statevector = probe
1912        .as_any()
1913        .is_some_and(|any| any.is::<StatevectorBackend>());
1914    let observable = match (grouped, holds_statevector) {
1915        (false, _) => PreparedObservable::Held,
1916        (true, true) => PreparedObservable::Grouped,
1917        (true, false) => PreparedObservable::Simulate,
1918    };
1919    (values, observable)
1920}
1921
1922/// Shared init → apply → extract logic.
1923fn execute_circuit(
1924    backend: &mut dyn Backend,
1925    circuit: &Circuit,
1926    opts: &SimOptions,
1927) -> Result<RunOutcome> {
1928    backend.init(circuit.num_qubits, circuit.num_classical_bits)?;
1929    let saves = apply_recording_saves(backend, &circuit.instructions)?;
1930
1931    let probabilities = if opts.probabilities {
1932        try_backend_probabilities(backend)?
1933    } else {
1934        None
1935    };
1936
1937    Ok(RunOutcome {
1938        classical_bits: backend.classical_results().to_vec(),
1939        probabilities,
1940        metadata: backend_metadata(backend),
1941        saves,
1942    })
1943}
1944
1945/// Apply `instructions`, reading the state at each save point.
1946///
1947/// The stream is handed to the backend in the segments between save points, so
1948/// no backend implements a save and none can batch across one. A circuit with
1949/// no save points takes one call.
1950fn apply_recording_saves(
1951    backend: &mut dyn Backend,
1952    instructions: &[Instruction],
1953) -> Result<Vec<SaveRecord>> {
1954    let first = instructions
1955        .iter()
1956        .position(|i| matches!(i, Instruction::Save { .. }));
1957    let Some(first) = first else {
1958        backend.apply_instructions(instructions)?;
1959        return Ok(Vec::new());
1960    };
1961
1962    let mut saves = Vec::new();
1963    let mut start = 0;
1964    for (i, inst) in instructions.iter().enumerate().skip(first) {
1965        let Instruction::Save { spec, label, .. } = inst else {
1966            continue;
1967        };
1968        backend.apply_instructions(&instructions[start..i])?;
1969        saves.push(SaveRecord {
1970            label: label.clone(),
1971            value: read_save(backend, *spec, label)?,
1972        });
1973        start = i + 1;
1974    }
1975    backend.apply_instructions(&instructions[start..])?;
1976    Ok(saves)
1977}
1978
1979/// Read one save off the backend that is mid-circuit.
1980///
1981/// Every spec maps to an export the backend already offers, so a backend that
1982/// cannot produce the requested form fails here with its own message rather
1983/// than needing a save implementation of its own.
1984fn read_save(backend: &mut dyn Backend, spec: SaveSpec, label: &str) -> Result<SavedValue> {
1985    let named = |err: crate::error::PrismError| match err {
1986        crate::error::PrismError::IncompatibleBackend { backend, reason } => {
1987            crate::error::PrismError::IncompatibleBackend {
1988                backend,
1989                reason: format!("save point `{label}`: {reason}"),
1990            }
1991        }
1992        other => other,
1993    };
1994    match spec {
1995        SaveSpec::StateVector => backend
1996            .export_statevector()
1997            .map(SavedValue::StateVector)
1998            .map_err(named),
1999        SaveSpec::Probabilities => backend
2000            .probabilities()
2001            .map(SavedValue::Probabilities)
2002            .map_err(named),
2003        SaveSpec::DensityMatrix => {
2004            let whole: Vec<usize> = (0..backend.num_qubits()).collect();
2005            backend
2006                .reduced_density_matrix(&whole)
2007                .map(SavedValue::DensityMatrix)
2008                .map_err(named)
2009        }
2010    }
2011}
2012
2013/// Provenance read off the engine that ran, after it ran. Exactness and
2014/// placement are reports rather than predictions: the MPS bound reflects what
2015/// this run discarded, and the placement reflects where the amplitudes ended up
2016/// after any device fallback.
2017pub(crate) fn backend_metadata(backend: &dyn Backend) -> RunMetadata {
2018    let mut metadata =
2019        RunMetadata::new(backend.resolved(), backend.exactness(), backend.placement());
2020    metadata.bond = backend.bond_report();
2021    metadata
2022}
2023
2024#[cfg(test)]
2025fn run(circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
2026    run_with(BackendKind::Auto, circuit, seed)
2027}
2028
2029/// Build the backend `kind` selects and run the circuit on it. For a
2030/// pre-constructed backend, use [`run_on`].
2031pub(crate) fn run_with(kind: BackendKind, circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
2032    run_with_internal(kind, circuit, seed, SimOptions::default())
2033}
2034
2035fn run_with_internal(
2036    kind: BackendKind,
2037    circuit: &Circuit,
2038    seed: u64,
2039    opts: SimOptions,
2040) -> Result<RunOutcome> {
2041    if !kind.is_auto() {
2042        validate_explicit_backend(&kind, circuit)?;
2043    }
2044    // The distributed backend runs the whole circuit across ranks in lockstep.
2045    // Subsystem decomposition, Clifford+T, and temporal-Clifford shortcuts all
2046    // reshape execution per sub-block, which would desynchronize the collective
2047    // calls every rank must issue in the same order. Dispatch directly.
2048    #[cfg(feature = "distributed")]
2049    if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
2050        let mut backend = resolve_backend(&kind, circuit, false).build(seed);
2051        if opts.probabilities {
2052            // The gather cap follows from the register width alone, so a
2053            // register past it is rejected here rather than after the run has
2054            // been paid for. Same check `probabilities()` applies, so the
2055            // message does not depend on where it surfaced.
2056            crate::backend::dense_probability_len(backend.name(), circuit.num_qubits)?;
2057        }
2058        return execute(&mut *backend, circuit, &opts);
2059    }
2060    let route = plan_probability_route(&kind, circuit);
2061    let outcome = run_route(&kind, circuit, seed, opts, &route)?;
2062    ensure_saves_recorded(circuit, &outcome)?;
2063    Ok(outcome)
2064}
2065
2066/// Execute one seed of a planned probability route. Shot loops plan once and
2067/// call this per seed.
2068fn run_route(
2069    kind: &BackendKind,
2070    circuit: &Circuit,
2071    seed: u64,
2072    opts: SimOptions,
2073    route: &ProbabilityRoute,
2074) -> Result<RunOutcome> {
2075    match route {
2076        ProbabilityRoute::FactoredStabilizer => {
2077            let mut backend =
2078                crate::backend::factored_stabilizer::FactoredStabilizerBackend::new(seed);
2079            let fs_opts = if circuit.num_qubits > 64 {
2080                SimOptions {
2081                    probabilities: false,
2082                }
2083            } else {
2084                opts
2085            };
2086            execute(&mut backend, circuit, &fs_opts)
2087        }
2088        ProbabilityRoute::Decomposed(components) => {
2089            run_decomposed(kind, components, circuit, seed, &opts)
2090        }
2091        ProbabilityRoute::TemporalClifford {
2092            has_partial_independence,
2093        } => match plan_temporal_clifford(kind, circuit) {
2094            Some(tc) => run_temporal_clifford(&tc, seed, opts.probabilities),
2095            None => run_direct(kind, circuit, seed, opts, *has_partial_independence),
2096        },
2097        ProbabilityRoute::Direct {
2098            has_partial_independence,
2099        } => run_direct(kind, circuit, seed, opts, *has_partial_independence),
2100    }
2101}
2102
2103fn run_direct(
2104    kind: &BackendKind,
2105    circuit: &Circuit,
2106    seed: u64,
2107    opts: SimOptions,
2108    has_partial_independence: bool,
2109) -> Result<RunOutcome> {
2110    match resolve(kind, circuit, has_partial_independence) {
2111            ExecutionPlan::Backend(plan) => {
2112                let mut backend = plan.build(seed);
2113                execute(&mut *backend, circuit, &opts)
2114            }
2115            ExecutionPlan::StabilizerRank => {
2116                let sr = stabilizer_rank::run_stabilizer_rank(circuit, seed)?;
2117                Ok(probs_only_result(
2118                    sr.probabilities,
2119                    RunMetadata::exact(ResolvedBackend::StabilizerRank),
2120                ))
2121            }
2122            ExecutionPlan::StochasticPauli { num_samples } => {
2123                Err(crate::error::PrismError::IncompatibleBackend {
2124                    backend: format!(
2125                        "{:?}",
2126                        BackendKind::StochasticPauli { num_samples }
2127                    ),
2128                    reason: "StochasticPauli produces marginal estimates only; use `simulate(...).marginals()`".into(),
2129                })
2130            }
2131            ExecutionPlan::DeterministicPauli { truncation } => {
2132                Err(crate::error::PrismError::IncompatibleBackend {
2133                    backend: format!(
2134                        "{:?}",
2135                        BackendKind::DeterministicPauli { truncation }
2136                    ),
2137                    reason: "DeterministicPauli produces marginals only; use `simulate(...).marginals()`".into(),
2138                })
2139            }
2140            ExecutionPlan::PauliPath => Err(reject_pauli_path("a single run")),
2141    }
2142}
2143
2144/// Execute a circuit on a pre-constructed backend. For automatic dispatch,
2145/// use [`simulate`].
2146pub fn run_on(backend: &mut dyn Backend, circuit: &Circuit) -> Result<RunOutcome> {
2147    execute(backend, circuit, &SimOptions::default())
2148}
2149
2150/// Execute a circuit on a pre-constructed backend from a start state other than
2151/// |0...0⟩, the [`run_on`] sibling for a caller holding its own amplitudes.
2152///
2153/// The backend must accept one; see [`Backend::init_from_amplitudes`] for the
2154/// validation applied to `initial_state` and for which backends decline it.
2155pub fn run_on_state(
2156    backend: &mut dyn Backend,
2157    circuit: &Circuit,
2158    initial_state: &[Complex64],
2159) -> Result<RunOutcome> {
2160    check_initial_state_len(StartState::Amplitudes(initial_state), circuit.num_qubits)?;
2161    backend.init_from_amplitudes(initial_state.to_vec(), circuit.num_classical_bits)?;
2162    let saves = apply_fused_circuit(backend, circuit)?;
2163    Ok(RunOutcome {
2164        classical_bits: backend.classical_results().to_vec(),
2165        probabilities: try_backend_probabilities(backend)?,
2166        metadata: backend_metadata(backend),
2167        saves,
2168    })
2169}
2170
2171/// Run several circuits, holding one backend across those that can share it.
2172///
2173/// Under the `parallel` feature, a batch whose circuits all sit at 16 qubits or
2174/// fewer splits across Rayon workers, each holding its own backend: a 200-point
2175/// sweep of a two-layer hardware-efficient ansatz ran 3.2x to 4.8x faster than a
2176/// loop over [`simulate`] at 4 to 12 qubits on a four-core i7-6700K, and one circuit
2177/// per core beat the kernels' own threads at 14 and 16. A wider circuit, a density
2178/// matrix from 9 qubits, or a GPU or distributed kind keeps the whole batch on one
2179/// thread.
2180///
2181/// On one thread the saving is the backend construction and its `2^n`
2182/// allocation, which `init` reuses when the next circuit has the same width;
2183/// route analysis is planned per circuit either way. On the same sweep that read
2184/// within about 10% of the loop, in both directions.
2185///
2186/// Results are identical to running each circuit on its own with the same seed.
2187/// A circuit that draws randomness (a measurement, a reset, or a classical
2188/// condition) gets a backend of its own, because a held backend would carry its
2189/// RNG forward into the next circuit and change what the next one measures.
2190///
2191/// # Errors
2192/// Returns the error of the first failing circuit in list order. A caller that
2193/// wants every circuit's result should call this per circuit.
2194pub fn run_batch(circuits: &[Circuit], kind: BackendKind, seed: u64) -> Result<Vec<RunOutcome>> {
2195    #[cfg(feature = "parallel")]
2196    if circuits.len() > 1
2197        && circuits
2198            .iter()
2199            .all(|c| runs_split_across_workers(&kind, c.num_qubits))
2200    {
2201        use rayon::prelude::*;
2202        let outcomes: Vec<Result<RunOutcome>> = circuits
2203            .par_iter()
2204            .map_init(
2205                || None,
2206                |held, circuit| run_batch_entry(&kind, circuit, seed, held),
2207            )
2208            .collect();
2209        return outcomes.into_iter().collect();
2210    }
2211
2212    let mut held = None;
2213    circuits
2214        .iter()
2215        .map(|circuit| run_batch_entry(&kind, circuit, seed, &mut held))
2216        .collect()
2217}
2218
2219type HeldBackend = Option<(dispatch::BackendPlan, usize, Box<dyn Backend + Send>)>;
2220
2221fn run_batch_entry(
2222    kind: &BackendKind,
2223    circuit: &Circuit,
2224    seed: u64,
2225    held: &mut HeldBackend,
2226) -> Result<RunOutcome> {
2227    let Some(plan) = batch_plan(kind, circuit) else {
2228        *held = None;
2229        return run_with_internal(kind.clone(), circuit, seed, SimOptions::default());
2230    };
2231    if !kind.is_auto() {
2232        validate_explicit_backend(kind, circuit)?;
2233    }
2234
2235    let reusable = matches!(
2236        &*held,
2237        Some((p, width, _)) if *width == circuit.num_qubits && dispatch::same_plan(p, &plan)
2238    );
2239    if !reusable {
2240        *held = Some((plan.clone(), circuit.num_qubits, plan.build(seed)));
2241    }
2242    let (_, _, backend) = held.as_mut().expect("just built");
2243    execute(&mut **backend, circuit, &SimOptions::default())
2244}
2245
2246/// Width below which separate runs split across Rayon workers, above the kernels'
2247/// own parallel floor: on a 4-core host a 200-circuit two-layer ansatz batch split
2248/// one circuit per core took 18% of the unsplit time at 14 qubits and 53% at 16, and
2249/// 10% more at 18. Under miri it follows the kernels' floor down.
2250#[cfg(all(feature = "parallel", not(miri)))]
2251const RUN_SPLIT_QUBITS: usize = 17;
2252#[cfg(all(feature = "parallel", miri))]
2253const RUN_SPLIT_QUBITS: usize = crate::backend::PARALLEL_THRESHOLD_QUBITS;
2254
2255/// Whether separate runs of `num_qubits`-wide circuits on `kind` should split across
2256/// Rayon workers: only below `RUN_SPLIT_QUBITS`, counting a density matrix at
2257/// twice its width since each worker holds a state, and never on a kind bound to one
2258/// device or rank context.
2259#[cfg(feature = "parallel")]
2260pub(crate) fn runs_split_across_workers(kind: &BackendKind, num_qubits: usize) -> bool {
2261    if bound_to_one_context(kind) {
2262        return false;
2263    }
2264    let width = if kind.is_density_matrix() {
2265        2 * num_qubits
2266    } else {
2267        num_qubits
2268    };
2269    width < RUN_SPLIT_QUBITS
2270}
2271
2272/// Whether per-shot runs on `kind` should split across Rayon workers, given the
2273/// resolved family and width of each state one shot holds. See
2274/// [`state_splits_across_workers`] for the per-state rule.
2275#[cfg(feature = "parallel")]
2276fn shots_split_across_workers(kind: &BackendKind, states: &[(ResolvedBackend, usize)]) -> bool {
2277    !bound_to_one_context(kind)
2278        && states
2279            .iter()
2280            .all(|&(family, width)| state_splits_across_workers(family, width))
2281}
2282
2283/// Whether separate runs each holding one `width`-qubit state of `family` should
2284/// split across Rayon workers: only below the width where that family's own
2285/// kernels go parallel, since each worker holds a state and a shot task nested
2286/// inside kernel joins can pile stolen frames onto one worker stack.
2287///
2288/// A tableau parallelizes its row loops only from `MIN_QUBITS_FOR_PAR_GATES`
2289/// qubits, and a product state parallelizes no gate, measurement or reset. A
2290/// density matrix counts at twice its width.
2291#[cfg(feature = "parallel")]
2292pub(crate) fn state_splits_across_workers(family: ResolvedBackend, width: usize) -> bool {
2293    use crate::backend::{MIN_QUBITS_FOR_PAR_GATES, PARALLEL_THRESHOLD_QUBITS};
2294    match family {
2295        ResolvedBackend::Stabilizer | ResolvedBackend::FactoredStabilizer => {
2296            width < MIN_QUBITS_FOR_PAR_GATES
2297        }
2298        ResolvedBackend::ProductState => true,
2299        ResolvedBackend::DensityMatrix => 2 * width < PARALLEL_THRESHOLD_QUBITS,
2300        _ => width < PARALLEL_THRESHOLD_QUBITS,
2301    }
2302}
2303
2304#[cfg(feature = "parallel")]
2305fn bound_to_one_context(kind: &BackendKind) -> bool {
2306    match kind {
2307        #[cfg(feature = "gpu")]
2308        BackendKind::AutoGpu { .. }
2309        | BackendKind::StatevectorGpu { .. }
2310        | BackendKind::DensityMatrixGpu { .. }
2311        | BackendKind::StabilizerGpu { .. } => true,
2312        #[cfg(feature = "distributed")]
2313        BackendKind::StatevectorDistributed { .. } => true,
2314        _ => false,
2315    }
2316}
2317
2318/// The plan a circuit can share, or `None` when it must run on its own.
2319///
2320/// A circuit that draws randomness needs a fresh RNG to match its solo run, and
2321/// anything that is not a direct backend route has no single plan to hold.
2322fn batch_plan(kind: &BackendKind, circuit: &Circuit) -> Option<dispatch::BackendPlan> {
2323    if draws_randomness(circuit) {
2324        return None;
2325    }
2326    let ProbabilityRoute::Direct {
2327        has_partial_independence,
2328    } = plan_probability_route(kind, circuit)
2329    else {
2330        return None;
2331    };
2332    match resolve(kind, circuit, has_partial_independence) {
2333        ExecutionPlan::Backend(plan) => Some(plan),
2334        _ => None,
2335    }
2336}
2337
2338/// True when a run of `circuit` reads the backend RNG, so a backend carried over
2339/// from an earlier run would change its outcome.
2340fn draws_randomness(circuit: &Circuit) -> bool {
2341    circuit.instructions.iter().any(|i| {
2342        matches!(
2343            i,
2344            Instruction::Measure { .. }
2345                | Instruction::Reset { .. }
2346                | Instruction::Conditional { .. }
2347                | Instruction::Region(_)
2348        )
2349    })
2350}
2351
2352/// Parse an OpenQASM string and execute with automatic backend selection.
2353pub fn run_qasm(qasm: &str, seed: u64) -> Result<RunOutcome> {
2354    let circuit = crate::circuit::openqasm::parse(qasm)?;
2355    simulate(&circuit).seed(seed).run()
2356}
2357
2358#[cfg(test)]
2359fn run_shots(circuit: &Circuit, num_shots: usize, seed: u64) -> Result<ShotsResult> {
2360    run_shots_with(BackendKind::Auto, circuit, num_shots, seed)
2361}
2362
2363pub(crate) fn supports_compiled_measurement_sampling(circuit: &Circuit) -> bool {
2364    circuit.is_clifford_only()
2365        && !circuit.has_resets()
2366        && circuit.has_terminal_measurements_only()
2367        && circuit
2368            .instructions
2369            .iter()
2370            .any(|inst| matches!(inst, Instruction::Measure { .. }))
2371}
2372
2373fn supports_deferred_measurement_sampling(circuit: &Circuit) -> bool {
2374    circuit.is_clifford_only()
2375        && (circuit.has_resets() || !circuit.has_terminal_measurements_only())
2376        && circuit
2377            .instructions
2378            .iter()
2379            .any(|inst| matches!(inst, Instruction::Measure { .. }))
2380        && !circuit.instructions.iter().any(|inst| {
2381            matches!(
2382                inst,
2383                Instruction::Conditional { .. } | Instruction::Region(_)
2384            )
2385        })
2386}
2387
2388fn is_clifford_sampler_kind(kind: &BackendKind) -> bool {
2389    if kind.is_auto() {
2390        return true;
2391    }
2392    match kind {
2393        BackendKind::Stabilizer | BackendKind::FactoredStabilizer => true,
2394        #[cfg(feature = "gpu")]
2395        BackendKind::StabilizerGpu { .. } => true,
2396        _ => false,
2397    }
2398}
2399
2400fn should_use_compiled_clifford_sampling(
2401    kind: &BackendKind,
2402    circuit: &Circuit,
2403    num_shots: usize,
2404) -> bool {
2405    num_shots >= 1
2406        && supports_compiled_measurement_sampling(circuit)
2407        && is_clifford_sampler_kind(kind)
2408}
2409
2410fn should_use_deferred_clifford_sampling(
2411    kind: &BackendKind,
2412    circuit: &Circuit,
2413    num_shots: usize,
2414) -> bool {
2415    num_shots >= 2
2416        && supports_deferred_measurement_sampling(circuit)
2417        && is_clifford_sampler_kind(kind)
2418}
2419
2420fn compile_measurements_for_kind(
2421    kind: &BackendKind,
2422    circuit: &Circuit,
2423    seed: u64,
2424) -> Result<compiled::CompiledSampler> {
2425    #[cfg(not(feature = "gpu"))]
2426    let _ = kind;
2427
2428    let sampler = compiled::compile_measurements(circuit, seed)?;
2429
2430    #[cfg(feature = "gpu")]
2431    if let BackendKind::StabilizerGpu { context } = kind {
2432        return Ok(sampler.with_gpu(context.clone()));
2433    }
2434
2435    Ok(sampler)
2436}
2437
2438/// Components to decompose with, and whether every gate is Clifford.
2439type Decomposition = (Vec<Vec<usize>>, bool);
2440
2441/// Independence analysis shared by the routing prelude in
2442/// `run_with_internal`, the shots slow path, and the terminal fast-path
2443/// candidacy. Returns the decomposition when full decomposition should fire,
2444/// plus the partial-independence flag otherwise.
2445fn analyze_independence(circuit: &Circuit) -> (Option<Decomposition>, bool) {
2446    if circuit.num_qubits >= MIN_DECOMPOSITION_QUBITS {
2447        let (components, clifford_only) = circuit.subsystems_and_clifford();
2448        if components.len() > 1 {
2449            if should_decompose(&components, circuit.num_qubits) {
2450                return (Some((components, clifford_only)), false);
2451            }
2452            return (None, true);
2453        }
2454    }
2455    (None, false)
2456}
2457
2458/// `(t_count, stabilizer_rank_budget)` when the auto Clifford+T family gate
2459/// passes. Callers apply their own per-entry-point T-count ceilings.
2460fn auto_clifford_t_budget(circuit: &Circuit) -> Option<(usize, usize)> {
2461    (circuit.is_clifford_plus_t() && circuit.has_t_gates()).then(|| {
2462        (
2463            circuit.t_count(),
2464            stabilizer_rank_budget(circuit.num_qubits),
2465        )
2466    })
2467}
2468
2469/// Auto-dispatch gate for the Clifford+T stabilizer-rank shortcut: the T
2470/// count must fit both the caller's ceiling and the size-derived
2471/// stabilizer-rank budget. Returns the T count when the shortcut applies.
2472pub(super) fn auto_stabilizer_rank_t_count(circuit: &Circuit, max_t: usize) -> Option<usize> {
2473    let (t, sr_budget) = auto_clifford_t_budget(circuit)?;
2474    (t <= max_t && t <= sr_budget).then_some(t)
2475}
2476
2477/// Routing precedence for the probability path: decomposition (with the
2478/// large sparse-Clifford factored-stabilizer override), then temporal Clifford,
2479/// then direct family resolution. `run_with_internal` executes this plan and
2480/// `auto_terminal_statevector_candidate` consults it, so the two cannot
2481/// drift apart.
2482enum ProbabilityRoute {
2483    FactoredStabilizer,
2484    Decomposed(Vec<Vec<usize>>),
2485    /// The temporal-Clifford predicate holds; `run_route` builds the plan and
2486    /// falls back to direct resolution should the split come back empty.
2487    TemporalClifford {
2488        has_partial_independence: bool,
2489    },
2490    Direct {
2491        has_partial_independence: bool,
2492    },
2493}
2494
2495fn plan_probability_route(kind: &BackendKind, circuit: &Circuit) -> ProbabilityRoute {
2496    let (decompose, has_partial_independence) = analyze_independence(circuit);
2497    if let Some((components, clifford_only)) = decompose {
2498        let max_block = components.iter().map(|c| c.len()).max().unwrap_or(0);
2499        if kind.is_auto()
2500            && clifford_only
2501            && circuit.num_qubits >= MIN_FACTORED_STABILIZER_QUBITS
2502            && max_block >= MIN_BLOCK_FOR_FACTORED_STAB
2503        {
2504            return ProbabilityRoute::FactoredStabilizer;
2505        }
2506        return ProbabilityRoute::Decomposed(components);
2507    }
2508    if has_temporal_clifford_opportunity(kind, circuit) {
2509        return ProbabilityRoute::TemporalClifford {
2510            has_partial_independence,
2511        };
2512    }
2513    ProbabilityRoute::Direct {
2514        has_partial_independence,
2515    }
2516}
2517
2518/// True when the auto probability route falls through to direct family
2519/// resolution and that resolver picks the CPU statevector.
2520fn auto_terminal_statevector_candidate(circuit: &Circuit) -> bool {
2521    match plan_probability_route(&BackendKind::Auto, circuit) {
2522        ProbabilityRoute::Direct {
2523            has_partial_independence,
2524        } => auto_selects_cpu_statevector(circuit, has_partial_independence),
2525        _ => false,
2526    }
2527}
2528
2529fn terminal_statevector_candidate(kind: &BackendKind, circuit: &Circuit) -> bool {
2530    if kind.is_auto() {
2531        return auto_terminal_statevector_candidate(circuit);
2532    }
2533    match kind {
2534        BackendKind::Statevector => true,
2535        #[cfg(feature = "gpu")]
2536        BackendKind::StatevectorGpu { .. } => true,
2537        _ => false,
2538    }
2539}
2540
2541fn try_terminal_statevector_backend(
2542    kind: &BackendKind,
2543    circuit: &Circuit,
2544    seed: u64,
2545) -> Result<Option<TerminalStatevector>> {
2546    if !circuit.has_terminal_measurements_only() {
2547        return Ok(None);
2548    }
2549
2550    let meas_map = circuit.measurement_map();
2551    if meas_map.is_empty() {
2552        return Ok(None);
2553    }
2554
2555    let stripped = circuit.without_measurements();
2556    if !terminal_statevector_candidate(kind, &stripped) {
2557        return Ok(None);
2558    }
2559
2560    let accel = accel_for(kind, Family::Statevector, stripped.num_qubits);
2561    let mut backend = build_statevector(&accel, seed);
2562    let expanded = expand_for_backend(&backend, &stripped);
2563    let fused = fuse_for_backend(&backend, &expanded);
2564    backend.init(fused.num_qubits, fused.num_classical_bits)?;
2565    backend.apply_instructions(&fused.instructions)?;
2566
2567    Ok(Some((backend, meas_map)))
2568}
2569
2570/// Build and run the backend for a terminal-measurement circuit when routing
2571/// lands on a single backend that samples from its own representation.
2572///
2573/// Returns `None` when the route is not a direct single backend, or when that
2574/// backend has no native sampler, leaving the dense probability path untouched.
2575/// The capability is probed before `init`, so a backend without one costs an
2576/// allocation and nothing else.
2577///
2578/// The product state is the one route taken past subsystem decomposition: it
2579/// already stores one factor per qubit, so splitting the circuit into
2580/// independent blocks pays a backend, a partition, and a merge per block to
2581/// rebuild what one native draw reads straight off the state, and above 64
2582/// qubits the merged block distribution does not exist at all. Every other
2583/// backend keeps the block split.
2584fn try_native_terminal_backend(
2585    kind: &BackendKind,
2586    stripped: &Circuit,
2587    seed: u64,
2588) -> Result<Option<Box<dyn Backend>>> {
2589    if !kind.is_auto() {
2590        validate_explicit_backend(kind, stripped)?;
2591    }
2592    let (decomposed, has_partial_independence) = match plan_probability_route(kind, stripped) {
2593        ProbabilityRoute::Direct {
2594            has_partial_independence,
2595        } => (false, has_partial_independence),
2596        ProbabilityRoute::Decomposed(_) => (true, false),
2597        _ => return Ok(None),
2598    };
2599    let ExecutionPlan::Backend(plan) = resolve(kind, stripped, has_partial_independence) else {
2600        return Ok(None);
2601    };
2602    if decomposed && !matches!(plan, BackendPlan::ProductState) {
2603        return Ok(None);
2604    }
2605    let mut backend = plan.build(seed);
2606    if !backend.supports_native_sampling() {
2607        return Ok(None);
2608    }
2609    execute(&mut *backend, stripped, &SimOptions::classical_only())?;
2610    Ok(Some(backend))
2611}
2612
2613/// Build and run the backend for a marginal query when routing lands on a
2614/// single backend that evaluates observables on its own representation.
2615///
2616/// Returns `None` when the route is not a direct single backend, or when that
2617/// backend has no native observable path, leaving the dense probability route
2618/// untouched. The capability is probed before `init`, so a backend without one
2619/// costs an allocation and nothing else.
2620///
2621/// The product state is carried past subsystem decomposition for the reason
2622/// [`try_native_terminal_backend`] carries it: it already holds one factor per
2623/// qubit, and the decomposed route would build the `2^n` merged distribution to
2624/// read marginals a per-qubit expectation answers directly. Every other backend
2625/// keeps the block split, which has no single backend holding the joint state.
2626fn try_native_marginal_backend(
2627    kind: &BackendKind,
2628    circuit: &Circuit,
2629    seed: u64,
2630) -> Result<Option<Box<dyn Backend>>> {
2631    if !kind.is_auto() {
2632        validate_explicit_backend(kind, circuit)?;
2633    }
2634    let (decomposed, has_partial_independence) = match plan_probability_route(kind, circuit) {
2635        ProbabilityRoute::Direct {
2636            has_partial_independence,
2637        } => (false, has_partial_independence),
2638        ProbabilityRoute::Decomposed(_) => (true, false),
2639        _ => return Ok(None),
2640    };
2641    let ExecutionPlan::Backend(plan) = resolve(kind, circuit, has_partial_independence) else {
2642        return Ok(None);
2643    };
2644    if decomposed && !matches!(plan, BackendPlan::ProductState) {
2645        return Ok(None);
2646    }
2647    let mut backend = plan.build(seed);
2648    if !backend.supports_pauli_expectation() {
2649        return Ok(None);
2650    }
2651    execute(&mut *backend, circuit, &SimOptions::classical_only())?;
2652    Ok(Some(backend))
2653}
2654
2655/// A source that answers every measurement in a circuit from a single state,
2656/// in the precedence [`prepare_shot_source`] applies. Shots and counts both
2657/// consume it, so the two cannot disagree about which shortcut applies.
2658enum ShotSource {
2659    /// `deferred` marks a sampler built from the measure/reset-deferred
2660    /// rewrite, whose renumbered measurements only read through `meas_map`.
2661    Compiled {
2662        sampler: Box<compiled::CompiledSampler>,
2663        meas_map: Vec<(usize, usize)>,
2664        deferred: bool,
2665    },
2666    TerminalStatevector {
2667        backend: Box<StatevectorBackend>,
2668        meas_map: Vec<(usize, usize)>,
2669    },
2670    /// A backend that draws basis states from its own representation.
2671    Native {
2672        backend: Box<dyn Backend>,
2673        meas_map: Vec<(usize, usize)>,
2674    },
2675    /// Dense output distribution of the measurement-stripped circuit, with the
2676    /// provenance of the run that produced it.
2677    TerminalProbabilities {
2678        probs: Probabilities,
2679        meas_map: Vec<(usize, usize)>,
2680        metadata: RunMetadata,
2681    },
2682    StabilizerRank,
2683    PerShot,
2684}
2685
2686impl ShotSource {
2687    /// Which engine will answer, decided by `prepare_shot_source` and read here
2688    /// so the shot and count entry points do not re-derive it. `None` for the
2689    /// per-shot route, which builds one backend per shot and stamps its own.
2690    fn metadata(&self) -> Option<RunMetadata> {
2691        match self {
2692            ShotSource::Compiled { .. } => Some(
2693                RunMetadata::exact(ResolvedBackend::CompiledStabilizer)
2694                    .with_engine(Engine::CompiledSampler),
2695            ),
2696            ShotSource::TerminalStatevector { backend, .. } => Some(backend_metadata(&**backend)),
2697            ShotSource::Native { backend, .. } => Some(backend_metadata(&**backend)),
2698            ShotSource::TerminalProbabilities { metadata, .. } => Some(metadata.clone()),
2699            ShotSource::StabilizerRank => Some(RunMetadata::exact(ResolvedBackend::StabilizerRank)),
2700            ShotSource::PerShot => None,
2701        }
2702    }
2703}
2704
2705/// Narrowest register `Auto` samples through the Clifford register rather than the
2706/// statevector. At 12 qubits the statevector still wins, 0.8 against 1.3 ms for 1024
2707/// shots; at 16 the register takes 1.5 ms against 3.3.
2708const MIN_AUTO_CLIFFORD_REGISTER_QUBITS: usize = 16;
2709
2710/// Whether terminal shots of `circuit` go to the Clifford register ahead of the
2711/// statevector: Clifford+T, at least [`MIN_AUTO_CLIFFORD_REGISTER_QUBITS`] wide, and
2712/// fewer T gates than qubits, within the register's width cap.
2713fn auto_prefers_clifford_register(circuit: &Circuit) -> bool {
2714    circuit.num_qubits >= MIN_AUTO_CLIFFORD_REGISTER_QUBITS
2715        && circuit.has_terminal_measurements_only()
2716        && !circuit.has_resets()
2717        && auto_clifford_t_budget(circuit).is_some_and(|(t, _)| {
2718            t < circuit.num_qubits && t <= clifford_register::MAX_REGISTER_QUBITS
2719        })
2720}
2721
2722/// Whether terminal shots of a circuit wider than [`MAX_STABILIZER_RANK_QUBITS`]
2723/// go to the stabilizer-rank expansion once the statevector has declined them.
2724fn auto_samples_wide_by_stabilizer_rank(circuit: &Circuit) -> bool {
2725    circuit.has_terminal_measurements_only()
2726        && circuit.num_qubits > MAX_STABILIZER_RANK_QUBITS
2727        && auto_stabilizer_rank_t_count(circuit, MAX_AUTO_T_COUNT_SHOTS).is_some()
2728}
2729
2730/// Whether `Auto` shots of `circuit`, without noise or a start state, run on the
2731/// exact stabilizer-rank expansion: the register and wide-circuit tests in
2732/// [`prepare_shot_source`], or the per-shot test in [`run_shots_per_shot`].
2733fn auto_samples_by_stabilizer_rank(circuit: &Circuit) -> bool {
2734    let folded = circuit.fold_static_guards();
2735    let circuit = folded.as_ref();
2736    if circuit.has_terminal_measurements_only() {
2737        auto_prefers_clifford_register(circuit) || auto_samples_wide_by_stabilizer_rank(circuit)
2738    } else {
2739        auto_stabilizer_rank_t_count(circuit, MAX_AUTO_T_COUNT_SHOTS).is_some()
2740    }
2741}
2742
2743/// Select and prepare the sampling source for `circuit`.
2744///
2745/// Preparation is real work: compiling a sampler, building and running a
2746/// backend, or executing the stripped circuit once. Call once per entry point
2747/// and match on the result rather than re-deriving the choice.
2748fn prepare_shot_source(
2749    kind: &BackendKind,
2750    circuit: &Circuit,
2751    num_shots: usize,
2752    seed: u64,
2753) -> Result<ShotSource> {
2754    if should_use_compiled_clifford_sampling(kind, circuit, num_shots) {
2755        return Ok(ShotSource::Compiled {
2756            sampler: Box::new(compile_measurements_for_kind(kind, circuit, seed)?),
2757            meas_map: circuit.measurement_map(),
2758            deferred: false,
2759        });
2760    }
2761
2762    if should_use_deferred_clifford_sampling(kind, circuit, num_shots)
2763        && let Ok(deferred) = compiled::defer_measure_reset_circuit(circuit)
2764    {
2765        return Ok(ShotSource::Compiled {
2766            sampler: Box::new(compile_measurements_for_kind(kind, &deferred, seed)?),
2767            meas_map: deferred.measurement_map(),
2768            deferred: true,
2769        });
2770    }
2771
2772    if kind.is_auto() && auto_prefers_clifford_register(circuit) {
2773        return Ok(ShotSource::StabilizerRank);
2774    }
2775
2776    if let Some((backend, meas_map)) = try_terminal_statevector_backend(kind, circuit, seed)? {
2777        return Ok(ShotSource::TerminalStatevector {
2778            backend: Box::new(backend),
2779            meas_map,
2780        });
2781    }
2782
2783    if matches!(kind, BackendKind::StabilizerRank) && circuit.has_t_gates() {
2784        return Ok(ShotSource::StabilizerRank);
2785    }
2786    if kind.is_auto() && auto_samples_wide_by_stabilizer_rank(circuit) {
2787        return Ok(ShotSource::StabilizerRank);
2788    }
2789
2790    if circuit.has_terminal_measurements_only()
2791        || (kind.is_density_matrix() && circuit.has_terminal_measurements_under_reset_channel())
2792    {
2793        let stripped = circuit.without_measurements();
2794        if let Some(backend) = try_native_terminal_backend(kind, &stripped, seed)? {
2795            return Ok(ShotSource::Native {
2796                backend,
2797                meas_map: circuit.measurement_map(),
2798            });
2799        }
2800        let result = run_with_internal(kind.clone(), &stripped, seed, SimOptions::default())?;
2801        if let Some(probs) = result.probabilities {
2802            return Ok(ShotSource::TerminalProbabilities {
2803                probs,
2804                meas_map: circuit.measurement_map(),
2805                metadata: result.metadata,
2806            });
2807        }
2808    }
2809
2810    Ok(ShotSource::PerShot)
2811}
2812
2813#[cfg(test)]
2814fn run_counts(circuit: &Circuit, num_shots: usize, seed: u64) -> Result<HashMap<Vec<u64>, u64>> {
2815    run_counts_with(BackendKind::Auto, circuit, num_shots, seed).map(|(counts, _)| counts)
2816}
2817
2818/// Execute a circuit multiple times with explicit backend selection and return counts.
2819///
2820/// For Clifford circuits with terminal measurements and no resets, Auto,
2821/// Stabilizer, FactoredStabilizer, and explicit `StabilizerGpu` route through
2822/// the compiled sampler's optimized counting path. Explicit `StabilizerGpu`
2823/// carries its GPU context into the compiled sampler so large shot runs avoid
2824/// the raw tableau measurement round-trips. Other circuits fall back to
2825/// per-shot simulation with counting.
2826///
2827/// Optimized terminal statevector paths sample counts directly from the output
2828/// distribution. The distribution is equivalent to materializing shots first,
2829/// but finite seeded counts may differ from `run_shots_with(...).counts()`.
2830pub(crate) fn run_counts_with(
2831    kind: BackendKind,
2832    circuit: &Circuit,
2833    num_shots: usize,
2834    seed: u64,
2835) -> Result<(HashMap<Vec<u64>, u64>, RunMetadata)> {
2836    #[cfg(feature = "distributed")]
2837    if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
2838        let shots = run_shots_with(kind, circuit, num_shots, seed)?;
2839        return Ok((shots.counts(), shots.metadata));
2840    }
2841
2842    let folded = circuit.fold_static_guards();
2843    let circuit = folded.as_ref();
2844
2845    let bits = circuit.num_classical_bits;
2846    let source = prepare_shot_source(&kind, circuit, num_shots, seed)?;
2847    let Some(metadata) = source.metadata() else {
2848        let shots = run_shots_per_shot(kind, circuit, num_shots, seed, ShotRows::CountedOnly)?;
2849        return Ok((shots.counts(), shots.metadata));
2850    };
2851    let counts = match source {
2852        ShotSource::Compiled {
2853            mut sampler,
2854            meas_map,
2855            deferred,
2856        } => {
2857            if deferred {
2858                let packed = sampler.try_sample_bulk_packed(num_shots)?;
2859                counts_of(
2860                    packed_shots_to_classical_bits(&packed, &meas_map, bits),
2861                    bits,
2862                )
2863            } else {
2864                record_counts_to_classical_bits(
2865                    sampler.try_sample_counts(num_shots)?,
2866                    &meas_map,
2867                    bits,
2868                )
2869            }
2870        }
2871        ShotSource::TerminalStatevector { backend, meas_map } => {
2872            if backend.is_gpu_resident() {
2873                let probs = backend.probabilities()?;
2874                sample_counts_from_probs(&probs, &meas_map, bits, num_shots, seed)
2875            } else {
2876                sample_counts_from_state(
2877                    backend.state_vector(),
2878                    backend.probability_scale(),
2879                    &meas_map,
2880                    bits,
2881                    num_shots,
2882                    seed,
2883                )
2884            }
2885        }
2886        ShotSource::Native {
2887            mut backend,
2888            meas_map,
2889        } => {
2890            let samples = backend.sample_basis_states(num_shots, seed)?;
2891            counts_from_basis_samples(&samples, &meas_map, bits)
2892        }
2893        ShotSource::TerminalProbabilities {
2894            probs, meas_map, ..
2895        } => counts_of(sample_shots(&probs, &meas_map, bits, num_shots, seed), bits),
2896        ShotSource::StabilizerRank => {
2897            stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed)?.counts()
2898        }
2899        ShotSource::PerShot => unreachable!("handled above"),
2900    };
2901    Ok((counts, metadata.with_shots(num_shots)))
2902}
2903
2904fn counts_of(shots: Vec<Vec<bool>>, num_classical_bits: usize) -> HashMap<Vec<u64>, u64> {
2905    ShotsResult::from_shots(shots, num_classical_bits).counts()
2906}
2907
2908#[cfg(test)]
2909fn run_marginals(circuit: &Circuit, seed: u64) -> Result<Vec<(f64, f64)>> {
2910    run_marginals_result_with(BackendKind::Auto, circuit, seed).map(MarginalsResult::into_vec)
2911}
2912
2913#[cfg(test)]
2914fn run_marginals_with(kind: BackendKind, circuit: &Circuit, seed: u64) -> Result<Vec<(f64, f64)>> {
2915    run_marginals_result_with(kind, circuit, seed).map(MarginalsResult::into_vec)
2916}
2917
2918/// Per-qubit marginals from native single-qubit Z expectations, for a backend
2919/// whose own representation answers them without a dense probability vector.
2920fn marginals_from_pauli_expectations(
2921    backend: &dyn Backend,
2922    num_qubits: usize,
2923) -> Result<MarginalsResult> {
2924    let observables: Vec<Vec<PauliTerm>> = (0..num_qubits).map(|q| vec![PauliTerm::z(q)]).collect();
2925    let expectations = backend.pauli_expectations(&observables)?;
2926    Ok(MarginalsResult {
2927        marginals: expectations_to_marginals(&expectations),
2928        metadata: backend_metadata(backend),
2929    })
2930}
2931
2932/// Route-level exactness per the [`Exactness`] convention: a policy that can
2933/// discard marks the route approximate even on a run that discarded nothing.
2934/// Only a zero-threshold policy stays exact, since it overflows into an error
2935/// instead of an approximation; a budget always drops once the sum passes it.
2936/// The realized bound is a coefficient magnitude rather than a state overlap,
2937/// so it stays `total_discarded` on the engine result and never a fidelity
2938/// bound.
2939fn spd_metadata(truncation: &SpdTruncation) -> RunMetadata {
2940    if truncation.can_discard() {
2941        RunMetadata::approximate(ResolvedBackend::DeterministicPauli)
2942    } else {
2943        RunMetadata::exact(ResolvedBackend::DeterministicPauli)
2944    }
2945}
2946
2947pub(crate) fn expectations_to_marginals(expectations: &[f64]) -> Vec<(f64, f64)> {
2948    expectations
2949        .iter()
2950        .map(|ez| {
2951            let p0 = ((1.0 + ez) / 2.0).clamp(0.0, 1.0);
2952            (p0, 1.0 - p0)
2953        })
2954        .collect()
2955}
2956
2957pub(super) fn has_nonunitary_or_classical_ops(circuit: &Circuit) -> bool {
2958    circuit.instructions.iter().any(|inst| {
2959        matches!(
2960            inst,
2961            Instruction::Measure { .. }
2962                | Instruction::Reset { .. }
2963                | Instruction::Conditional { .. }
2964                | Instruction::Region(_)
2965        )
2966    })
2967}
2968
2969fn supports_pauli_marginal_backend(circuit: &Circuit) -> bool {
2970    circuit.is_clifford_plus_t() && !has_nonunitary_or_classical_ops(circuit)
2971}
2972
2973/// Gate-set support is left to the engines, which accept Clifford gates and
2974/// Pauli rotations and report the offending gate by name.
2975fn validate_pauli_marginal_backend(kind: &BackendKind, circuit: &Circuit) -> Result<()> {
2976    if has_nonunitary_or_classical_ops(circuit) {
2977        return Err(PrismError::IncompatibleBackend {
2978            backend: format!("{kind:?}"),
2979            reason: "Pauli marginal backends require a unitary circuit without measurements, resets, or conditionals".into(),
2980        });
2981    }
2982    Ok(())
2983}
2984
2985fn run_marginals_result_with(
2986    kind: BackendKind,
2987    circuit: &Circuit,
2988    seed: u64,
2989) -> Result<MarginalsResult> {
2990    let n = circuit.num_qubits;
2991
2992    match &kind {
2993        BackendKind::StochasticPauli { num_samples } => {
2994            validate_pauli_marginal_backend(&kind, circuit)?;
2995            let spp = unified_pauli::run_spp(circuit, *num_samples, seed)?;
2996            return Ok(MarginalsResult {
2997                marginals: expectations_to_marginals(&spp.expectations),
2998                metadata: RunMetadata::approximate(ResolvedBackend::StochasticPauli)
2999                    .with_shots(*num_samples),
3000            });
3001        }
3002        BackendKind::DeterministicPauli { truncation } => {
3003            validate_pauli_marginal_backend(&kind, circuit)?;
3004            let spd = unified_pauli::run_spd_with(circuit, truncation)?;
3005            return Ok(MarginalsResult {
3006                marginals: expectations_to_marginals(&spd.expectations),
3007                metadata: spd_metadata(truncation),
3008            });
3009        }
3010        _ => {}
3011    }
3012
3013    if kind.is_auto()
3014        && supports_pauli_marginal_backend(circuit)
3015        && circuit.has_t_gates()
3016        && n >= MIN_QUBITS_FOR_SPD_AUTO
3017    {
3018        let exact = SpdTruncation::Threshold {
3019            epsilon: 0.0,
3020            max_terms: 0,
3021        };
3022        let spd = if n <= max_statevector_qubits() {
3023            unified_pauli::run_spd_exact_within(circuit, auto_spd_work_budget(circuit))?
3024        } else {
3025            Some(unified_pauli::run_spd_with(circuit, &exact)?)
3026        };
3027        if let Some(spd) = spd {
3028            return Ok(MarginalsResult {
3029                marginals: expectations_to_marginals(&spd.expectations),
3030                metadata: spd_metadata(&exact),
3031            });
3032        }
3033    }
3034
3035    // The distributed backend answers a marginal from rank-local sums plus one
3036    // `Allreduce`, so it never needs the 2^n gather the fallback below takes.
3037    #[cfg(feature = "distributed")]
3038    if let BackendKind::StatevectorDistributed { context } = &kind {
3039        let mut backend =
3040            crate::backend::distributed_statevector::DistributedStatevectorBackend::new(
3041                context.clone(),
3042                seed,
3043            );
3044        execute(&mut backend, circuit, &SimOptions::classical_only())?;
3045        return marginals_from_pauli_expectations(&backend, n);
3046    }
3047
3048    // A marginal is a `Z_q` expectation, so a unitary circuit gets the same
3049    // tensor routes the expectation terminal takes.
3050    if kind.is_auto() && !has_nonunitary_or_classical_ops(circuit) {
3051        let observables: Vec<Vec<PauliTerm>> = (0..n).map(|q| vec![PauliTerm::z(q)]).collect();
3052        let tensor_route = if n <= max_statevector_qubits() {
3053            tensor_route_expectations(circuit, &observables)
3054        } else {
3055            tensor_route_over_mps(circuit, &observables)
3056        };
3057        if let Some(result) = tensor_route {
3058            let result = result?;
3059            return Ok(MarginalsResult {
3060                marginals: expectations_to_marginals(&result.values),
3061                metadata: result.metadata,
3062            });
3063        }
3064    }
3065
3066    if let Some(backend) = try_native_marginal_backend(&kind, circuit, seed)? {
3067        return marginals_from_pauli_expectations(&*backend, n);
3068    }
3069
3070    let result = run_with(kind, circuit, seed)?;
3071    if let Some(probs) = &result.probabilities {
3072        Ok(MarginalsResult {
3073            marginals: probs.marginals(),
3074            metadata: result.metadata.clone(),
3075        })
3076    } else {
3077        Err(PrismError::BackendUnsupported {
3078            backend: "simulate".into(),
3079            operation: format!(
3080                "marginals for {} qubits without backend probability output",
3081                circuit.num_qubits
3082            ),
3083        })
3084    }
3085}
3086
3087/// Compute `⟨ψ|P|ψ⟩` for each joint Pauli observable on a unitary circuit's
3088/// output state, using automatic backend selection. See
3089/// [`Simulate::expectation_values`] for explicit backend control.
3090///
3091/// # Examples
3092///
3093/// ```
3094/// use prism_q::{Circuit, Gate, PauliTerm, run_expectation_values};
3095///
3096/// let mut bell = Circuit::new(2, 0);
3097/// bell.add_gate(Gate::H, &[0]);
3098/// bell.add_gate(Gate::Cx, &[0, 1]);
3099///
3100/// let observables = vec![
3101///     vec![PauliTerm::z(0)],
3102///     vec![PauliTerm::z(0), PauliTerm::z(1)],
3103/// ];
3104/// let values = run_expectation_values(&bell, &observables, 42)?;
3105/// assert!(values[0].abs() < 1e-10); // <Z0> = 0
3106/// assert!((values[1] - 1.0).abs() < 1e-10); // <Z0 Z1> = 1
3107/// # Ok::<(), prism_q::PrismError>(())
3108/// ```
3109pub fn run_expectation_values(
3110    circuit: &Circuit,
3111    observables: &[Vec<PauliTerm>],
3112    seed: u64,
3113) -> Result<Vec<f64>> {
3114    run_expectation_values_with(BackendKind::Auto, circuit, observables, seed)
3115}
3116
3117fn run_expectation_values_with(
3118    kind: BackendKind,
3119    circuit: &Circuit,
3120    observables: &[Vec<PauliTerm>],
3121    seed: u64,
3122) -> Result<Vec<f64>> {
3123    run_expectation_values_reported(kind, circuit, observables, seed)
3124        .map(ExpectationResult::into_values)
3125}
3126
3127fn run_expectation_values_reported(
3128    kind: BackendKind,
3129    circuit: &Circuit,
3130    observables: &[Vec<PauliTerm>],
3131    seed: u64,
3132) -> Result<ExpectationResult> {
3133    require_unitary_circuit(&kind, circuit, "expectation values require")?;
3134
3135    match &kind {
3136        BackendKind::StochasticPauli { num_samples } => {
3137            let mut values = Vec::with_capacity(observables.len());
3138            let mut std_errors = Vec::with_capacity(observables.len());
3139            for (i, obs) in observables.iter().enumerate() {
3140                let r = unified_pauli::run_spp_observable(
3141                    circuit,
3142                    obs,
3143                    *num_samples,
3144                    mix_seed(seed, i),
3145                )?;
3146                values.push(r.mean);
3147                std_errors.push(r.std_error);
3148            }
3149            Ok(ExpectationResult {
3150                values,
3151                std_errors: Some(std_errors),
3152                metadata: RunMetadata::approximate(ResolvedBackend::StochasticPauli)
3153                    .with_shots(*num_samples),
3154            })
3155        }
3156        BackendKind::DeterministicPauli { truncation } => {
3157            let mut values = Vec::with_capacity(observables.len());
3158            for obs in observables {
3159                let r = unified_pauli::run_spd_observable_with(circuit, obs, truncation)?;
3160                values.push(r.mean);
3161            }
3162            Ok(analytic_expectations(values, spd_metadata(truncation)))
3163        }
3164        _ if kind.is_auto() || kind.is_stabilizer_family() => {
3165            if circuit.is_clifford_only() {
3166                let mut values = Vec::with_capacity(observables.len());
3167                for obs in observables {
3168                    let r = unified_pauli::run_spd_observable(circuit, obs, 0.0, 0)?;
3169                    values.push(r.mean);
3170                }
3171                Ok(analytic_expectations(
3172                    values,
3173                    spd_metadata(&SpdTruncation::Threshold {
3174                        epsilon: 0.0,
3175                        max_terms: 0,
3176                    }),
3177                ))
3178            } else if kind.is_auto() {
3179                if circuit.num_qubits > max_statevector_qubits() {
3180                    if let Some(result) = tensor_route_over_mps(circuit, observables) {
3181                        return result;
3182                    }
3183                    return expectation_values_native(&kind, circuit, observables, seed);
3184                }
3185                if let Some(result) = tensor_route_expectations(circuit, observables) {
3186                    return result;
3187                }
3188                expectation_values_statevector(&kind, circuit, observables, seed)
3189            } else {
3190                Err(PrismError::IncompatibleBackend {
3191                    backend: format!("{kind:?}"),
3192                    reason: "stabilizer backends require a Clifford-only circuit".into(),
3193                })
3194            }
3195        }
3196        BackendKind::Statevector => {
3197            expectation_values_statevector(&kind, circuit, observables, seed)
3198        }
3199        #[cfg(feature = "gpu")]
3200        BackendKind::StatevectorGpu { .. } => {
3201            expectation_values_statevector(&kind, circuit, observables, seed)
3202        }
3203        // The scalar network prunes each observable to its light cone, which
3204        // the backend's stored network cannot: it no longer knows its gates.
3205        BackendKind::TensorNetwork => {
3206            validate_explicit_backend(&kind, circuit)?;
3207            for observable in observables {
3208                validate_observable(observable, circuit.num_qubits)?;
3209            }
3210            let values =
3211                crate::backend::tensornetwork::expectations_zero_state(circuit, observables)?;
3212            Ok(analytic_expectations(
3213                values,
3214                RunMetadata::exact(ResolvedBackend::TensorNetwork),
3215            ))
3216        }
3217        other => expectation_values_native(other, circuit, observables, seed),
3218    }
3219}
3220
3221/// Compute `⟨H⟩` and its grouped-measurement variance for a weighted Pauli
3222/// observable, using automatic backend selection. See
3223/// [`Simulate::observable_expectation`] for explicit backend control and
3224/// [`ObservableExpectation::variance`] for the variance contract.
3225///
3226/// # Examples
3227///
3228/// ```
3229/// use prism_q::{Circuit, Gate, PauliObservable, PauliTerm, run_observable_expectation};
3230///
3231/// let mut circuit = Circuit::new(2, 0);
3232/// circuit.add_gate(Gate::H, &[0]);
3233/// circuit.add_gate(Gate::Cx, &[0, 1]);
3234/// circuit.add_gate(Gate::T, &[0]);
3235///
3236/// let hamiltonian = PauliObservable::from_terms([
3237///     (1.0, vec![PauliTerm::z(0)]),
3238///     (1.0, vec![PauliTerm::z(0), PauliTerm::z(1)]),
3239/// ])?;
3240///
3241/// // The T phase moves nothing here: both terms are Z-only, one commuting
3242/// // group covers them, and the variance is exactly Var(H) with outcomes
3243/// // 2 and 0 at probability 1/2 each.
3244/// let result = run_observable_expectation(&circuit, &hamiltonian, 42)?;
3245/// assert!((result.mean - 1.0).abs() < 1e-10);
3246/// assert!((result.variance.unwrap() - 1.0).abs() < 1e-10);
3247/// # Ok::<(), prism_q::PrismError>(())
3248/// ```
3249pub fn run_observable_expectation(
3250    circuit: &Circuit,
3251    observable: &PauliObservable,
3252    seed: u64,
3253) -> Result<ObservableExpectation> {
3254    run_observable_expectation_reported(BackendKind::Auto, circuit, observable, seed)
3255}
3256
3257fn run_observable_expectation_reported(
3258    kind: BackendKind,
3259    circuit: &Circuit,
3260    observable: &PauliObservable,
3261    seed: u64,
3262) -> Result<ObservableExpectation> {
3263    require_unitary_circuit(&kind, circuit, "expectation values require")?;
3264
3265    let grouped_statevector = match &kind {
3266        BackendKind::Statevector => true,
3267        #[cfg(feature = "gpu")]
3268        BackendKind::StatevectorGpu { .. } => true,
3269        _ => {
3270            kind.is_auto()
3271                && !circuit.is_clifford_only()
3272                && circuit.num_qubits <= max_statevector_qubits()
3273        }
3274    };
3275    if grouped_statevector {
3276        return grouped_expectation_statevector(&kind, circuit, observable, seed);
3277    }
3278
3279    let result =
3280        run_expectation_values_reported(kind, circuit, &observable_vecs(observable), seed)?;
3281    Ok(weighted_observable_result(
3282        observable,
3283        &result.values,
3284        result.std_errors.as_deref(),
3285        result.metadata,
3286    ))
3287}
3288
3289fn observable_vecs(observable: &PauliObservable) -> Vec<Vec<PauliTerm>> {
3290    observable
3291        .terms()
3292        .iter()
3293        .map(|(_, factors)| factors.clone())
3294        .collect()
3295}
3296
3297/// Fold per-term values into the weighted mean; no grouped traversal ran, so
3298/// there is no variance to report. Independent per-term estimates combine
3299/// into the weighted-sum standard error.
3300fn weighted_observable_result(
3301    observable: &PauliObservable,
3302    values: &[f64],
3303    std_errors: Option<&[f64]>,
3304    metadata: RunMetadata,
3305) -> ObservableExpectation {
3306    let coefficients = observable.terms().iter().map(|(c, _)| *c);
3307    let mean = coefficients.clone().zip(values).map(|(c, v)| c * v).sum();
3308    let std_error = std_errors.map(|errors| {
3309        coefficients
3310            .zip(errors)
3311            .map(|(c, e)| (c * e).powi(2))
3312            .sum::<f64>()
3313            .sqrt()
3314    });
3315    ObservableExpectation {
3316        mean,
3317        variance: None,
3318        group_variances: None,
3319        std_error,
3320        metadata,
3321    }
3322}
3323
3324/// Evaluate a weighted observable on the statevector: small groups from one
3325/// shared batched traversal, each large group's mean and variance from a
3326/// dedicated moments pass.
3327///
3328/// `Var(H_g) = <H_g^2> - <H_g>^2` per commuting group. For a small group the
3329/// square expands into pairwise product strings appended to the same
3330/// traversal that serves its term means; a group past the pair budget takes a
3331/// single-pass moment accumulation instead, on the state as run when the
3332/// group is Z-only and on a basis-rotated copy otherwise.
3333fn grouped_expectation_statevector(
3334    kind: &BackendKind,
3335    circuit: &Circuit,
3336    observable: &PauliObservable,
3337    seed: u64,
3338) -> Result<ObservableExpectation> {
3339    let terms = observable.terms();
3340    // Validate before the 2^n simulation so bad observables fail cheaply. The
3341    // mask reduction shifts by qubit index, so it follows the `init` that
3342    // rejects a width no mask can address.
3343    for (_, factors) in terms {
3344        validate_observable(factors, circuit.num_qubits)?;
3345    }
3346
3347    let accel = accel_for(kind, Family::Statevector, circuit.num_qubits);
3348    let mut backend = build_statevector(&accel, seed);
3349    let expanded = expand_for_backend(&backend, circuit);
3350    let fused = fuse_for_backend(&backend, &expanded);
3351    backend.init(fused.num_qubits, fused.num_classical_bits)?;
3352    backend.apply_instructions(&fused.instructions)?;
3353    grouped_expectation_on_state(&backend, observable)
3354}
3355
3356/// The reduction half of [`grouped_expectation_statevector`], on a backend
3357/// that has already run the circuit.
3358fn grouped_expectation_on_state(
3359    backend: &StatevectorBackend,
3360    observable: &PauliObservable,
3361) -> Result<ObservableExpectation> {
3362    let num_qubits = backend.num_qubits();
3363    let terms = observable.terms();
3364    let masks = terms
3365        .iter()
3366        .map(|(_, factors)| pauli_masks(factors, num_qubits))
3367        .collect::<Result<Vec<_>>>()?;
3368    let metadata = backend_metadata(backend);
3369
3370    let grouping = observable.grouping();
3371
3372    // Small groups get `<H_g^2>` from pairwise product strings appended to the
3373    // shared traversal: qubit-wise-commuting strings multiply phase-free
3374    // (shared qubits carry equal axes and cancel to identity), so each pair is
3375    // one more mask. Large groups fall back to a dedicated moments pass, which
3376    // costs a fixed number of state sweeps where the pair expansion grows
3377    // quadratically. Each group's mean comes out of whichever pass serves its
3378    // variance, so a large group's terms stay out of the shared traversal.
3379    let mut combined = Vec::with_capacity(masks.len());
3380    let mut pair_blocks: Vec<(usize, usize, usize, Vec<f64>)> = Vec::new();
3381    let mut deferred: Vec<usize> = Vec::new();
3382    for (gi, group) in grouping.groups.iter().enumerate() {
3383        let members = &group.term_indices;
3384        if members.len() * (members.len() - 1) / 2 > MAX_PAIR_MASKS_PER_GROUP {
3385            deferred.push(gi);
3386            continue;
3387        }
3388        let first_term = combined.len();
3389        combined.extend(members.iter().map(|&i| masks[i]));
3390        let first_pair = combined.len();
3391        let mut pair_coefficients = Vec::with_capacity(members.len() * (members.len() - 1) / 2);
3392        for (pos, &i) in members.iter().enumerate() {
3393            for &j in &members[pos + 1..] {
3394                let product_x = masks[i].0 ^ masks[j].0;
3395                let product_z = masks[i].1 ^ masks[j].1;
3396                combined.push((product_x, product_z, (product_x & product_z).count_ones()));
3397                pair_coefficients.push(2.0 * terms[i].0 * terms[j].0);
3398            }
3399        }
3400        pair_blocks.push((gi, first_term, first_pair, pair_coefficients));
3401    }
3402
3403    // A device-resident state reduces every mask on the card; the host keeps
3404    // its state and norm for the moments pass below.
3405    let (values, host) = match backend.pauli_expectations_on_device(&combined) {
3406        Some(values) => (values?, None),
3407        None => {
3408            let state = backend.state_vector();
3409            let (values, norm) = pauli_expectations_from_masks(state, &combined);
3410            (values, Some((state, norm)))
3411        }
3412    };
3413
3414    let mut mean: f64 = terms
3415        .iter()
3416        .filter(|(_, factors)| factors.is_empty())
3417        .map(|(c, _)| c)
3418        .sum();
3419    let mut group_variances = vec![0.0; grouping.groups.len()];
3420
3421    for (gi, first_term, first_pair, pair_coefficients) in &pair_blocks {
3422        let group = &grouping.groups[*gi];
3423        let m1: f64 = group
3424            .term_indices
3425            .iter()
3426            .zip(&values[*first_term..])
3427            .map(|(&i, v)| terms[i].0 * v)
3428            .sum();
3429        let square_diag: f64 = group
3430            .term_indices
3431            .iter()
3432            .map(|&i| terms[i].0 * terms[i].0)
3433            .sum();
3434        let square_cross: f64 = pair_coefficients
3435            .iter()
3436            .zip(&values[*first_pair..])
3437            .map(|(c, v)| c * v)
3438            .sum();
3439        mean += m1;
3440        group_variances[*gi] = (square_diag + square_cross - m1 * m1).max(0.0);
3441    }
3442
3443    // A Z-only group reads the state as run, any other group a copy rotated so
3444    // its terms read as Z strings (`x | z` per term). A device-resident state
3445    // takes both on the card and is exported only when the rotated copy does
3446    // not fit there.
3447    if !deferred.is_empty() {
3448        let requests: Vec<(Vec<u64>, Vec<f64>, u64, u64)> = deferred
3449            .iter()
3450            .map(|&gi| {
3451                let group = &grouping.groups[gi];
3452                let (x_bits, y_bits) = if group.is_z_only() {
3453                    (0, 0)
3454                } else {
3455                    group.rotation_masks()
3456                };
3457                (
3458                    group
3459                        .term_indices
3460                        .iter()
3461                        .map(|&i| (masks[i].0 | masks[i].1) as u64)
3462                        .collect(),
3463                    group.term_indices.iter().map(|&i| terms[i].0).collect(),
3464                    x_bits as u64,
3465                    y_bits as u64,
3466                )
3467            })
3468            .collect();
3469        let on_device = match host {
3470            Some(_) => None,
3471            None => backend.group_moments_on_device(&requests).transpose()?,
3472        };
3473        let moments = match on_device {
3474            Some(moments) => moments,
3475            None => {
3476                let exported;
3477                let (state, norm): (&[Complex64], f64) = match host {
3478                    Some(host) => host,
3479                    None => {
3480                        exported = backend.export_statevector()?;
3481                        (&exported, crate::backend::state_norm_sqr(&exported))
3482                    }
3483                };
3484                host_group_moments(state, norm, &requests)
3485            }
3486        };
3487        for (&gi, (m1, m2)) in deferred.iter().zip(moments) {
3488            mean += m1;
3489            group_variances[gi] = (m2 - m1 * m1).max(0.0);
3490        }
3491    }
3492
3493    let variance = group_variances.iter().sum();
3494    Ok(ObservableExpectation {
3495        mean,
3496        variance: Some(variance),
3497        group_variances: Some(group_variances),
3498        std_error: None,
3499        metadata,
3500    })
3501}
3502
3503/// The host moments pass over each `(zmasks, coefficients, x_bits, y_bits)`
3504/// request of [`grouped_expectation_on_state`].
3505pub(crate) fn host_group_moments(
3506    state: &[Complex64],
3507    norm: f64,
3508    requests: &[(Vec<u64>, Vec<f64>, u64, u64)],
3509) -> Vec<(f64, f64)> {
3510    let mut rotated = Vec::new();
3511    requests
3512        .iter()
3513        .map(|(zmasks, coefficients, x_bits, y_bits)| {
3514            let zmasks: Vec<usize> = zmasks.iter().map(|&z| z as usize).collect();
3515            let rotation = x_bits | y_bits;
3516            if rotation == 0 {
3517                return observable::weighted_group_moments(state, &zmasks, coefficients, norm);
3518            }
3519            observable::rotate_to_z_basis(state, &mut rotated, *x_bits as usize, *y_bits as usize);
3520            let growth = 2f64.powi(rotation.count_ones() as i32);
3521            observable::weighted_group_moments(&rotated, &zmasks, coefficients, norm * growth)
3522        })
3523        .collect()
3524}
3525
3526/// Pair-expansion budget per commuting group. The dedicated pass pays a scratch
3527/// copy and a basis rotation per group that the pair expansion avoids. On the
3528/// 2000-string Jordan-Wigner fixture, sending every multi-term group through it
3529/// cost about 3x this budget's reduction at n=16 and n=20, and about 0.6x at
3530/// n=12.
3531const MAX_PAIR_MASKS_PER_GROUP: usize = 20;
3532
3533/// Values from a route that evaluates rather than samples, so there is no
3534/// interval to report.
3535fn analytic_expectations(values: Vec<f64>, metadata: RunMetadata) -> ExpectationResult {
3536    ExpectationResult {
3537        values,
3538        std_errors: None,
3539        metadata,
3540    }
3541}
3542
3543/// Which state diagnostic a terminal is asking for, carried into dispatch so
3544/// the route can be judged before the circuit runs.
3545#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3546enum Diagnostic {
3547    StateVector,
3548    ReducedDensityMatrix,
3549    Entropy,
3550    Overlap,
3551}
3552
3553impl Diagnostic {
3554    fn terminal(self) -> &'static str {
3555        match self {
3556            Diagnostic::StateVector => "a statevector",
3557            Diagnostic::ReducedDensityMatrix => "a reduced density matrix",
3558            Diagnostic::Entropy => "entanglement entropy",
3559            Diagnostic::Overlap => "a state overlap",
3560        }
3561    }
3562}
3563
3564/// Whether the backend `plan` builds implements `diagnostic` on its own
3565/// representation.
3566///
3567/// Read from the plan rather than from a built state so [`BackendKind::Auto`]
3568/// can re-route before paying for the run. A plan added without the kernel
3569/// reads as declining here, which is what its trait default does.
3570fn plan_answers(plan: &BackendPlan, diagnostic: Diagnostic) -> bool {
3571    match diagnostic {
3572        Diagnostic::ReducedDensityMatrix => matches!(
3573            plan,
3574            BackendPlan::Statevector { .. }
3575                | BackendPlan::Sparse
3576                | BackendPlan::Factored
3577                | BackendPlan::ProductState
3578                | BackendPlan::DensityMatrix { .. }
3579                | BackendPlan::Stabilizer { .. }
3580                | BackendPlan::FactoredStabilizer
3581        ),
3582        Diagnostic::Entropy => matches!(
3583            plan,
3584            BackendPlan::Statevector { .. }
3585                | BackendPlan::Mps { .. }
3586                | BackendPlan::ProductState
3587                | BackendPlan::Stabilizer { .. }
3588                | BackendPlan::FactoredStabilizer
3589        ),
3590        Diagnostic::StateVector | Diagnostic::Overlap => {
3591            !matches!(plan, BackendPlan::DensityMatrix { .. })
3592        }
3593    }
3594}
3595
3596/// The error for a route that propagates an observable and holds no state,
3597/// naming the route so the caller knows which engine `kind` reached.
3598fn stateless_route(kind: &BackendKind, diagnostic: Diagnostic, route: &str) -> PrismError {
3599    PrismError::IncompatibleBackend {
3600        backend: format!("{kind:?}"),
3601        reason: format!(
3602            "{} needs a backend that holds a state; the {route} route returns \
3603             probabilities only",
3604            diagnostic.terminal()
3605        ),
3606    }
3607}
3608
3609/// Price a diagnostic's output against the dense export cap before the circuit
3610/// runs: a reduced density matrix holds `4^k` entries, which depends on `k`
3611/// and the backend's name alone, both known once the plan is built.
3612fn check_diagnostic_width(backend: &dyn Backend, diagnostic: Diagnostic, k: usize) -> Result<()> {
3613    match diagnostic {
3614        Diagnostic::ReducedDensityMatrix => {
3615            crate::backend::reduced_density::reduced_density_side(backend.name(), k)?;
3616            Ok(())
3617        }
3618        Diagnostic::StateVector => {
3619            if k > crate::backend::schmidt::export_cap() {
3620                return Err(crate::backend::schmidt::export_cap_exceeded(
3621                    backend.name(),
3622                    "a dense statevector".to_string(),
3623                    k,
3624                ));
3625            }
3626            Ok(())
3627        }
3628        Diagnostic::Entropy | Diagnostic::Overlap => Ok(()),
3629    }
3630}
3631
3632/// Build the backend `kind` resolves to, run `circuit` on it, and hand it back
3633/// for one terminal read of its state.
3634///
3635/// Resolution goes straight to a single backend, as the native expectation
3636/// path does: a diagnostic is read off one state, and the decomposed route
3637/// holds one per independent block. Under [`BackendKind::Auto`] a resolved
3638/// plan that cannot answer `diagnostic` is replaced by the statevector while
3639/// the circuit fits its cap, since the route was the dispatcher's choice and
3640/// not the caller's; an explicit kind keeps its backend and declines.
3641fn diagnostic_backend(
3642    kind: &BackendKind,
3643    circuit: &Circuit,
3644    initial_state: Option<StartState<'_>>,
3645    seed: u64,
3646    diagnostic: Diagnostic,
3647    subsystem_len: usize,
3648) -> Result<Box<dyn Backend>> {
3649    if let Some(state) = initial_state {
3650        let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
3651        check_diagnostic_width(&*backend, diagnostic, subsystem_len)?;
3652        apply_fused_without_saves(&mut *backend, circuit, "state diagnostics")?;
3653        return Ok(backend);
3654    }
3655    if !kind.is_auto() {
3656        validate_explicit_backend(kind, circuit)?;
3657    }
3658    let (_, has_partial_independence) = analyze_independence(circuit);
3659    let mut plan = match resolve(kind, circuit, has_partial_independence) {
3660        ExecutionPlan::Backend(plan) => plan,
3661        ExecutionPlan::StabilizerRank => {
3662            return Err(stateless_route(kind, diagnostic, "stabilizer-rank"));
3663        }
3664        ExecutionPlan::StochasticPauli { .. } => {
3665            return Err(stateless_route(kind, diagnostic, "stochastic Pauli"));
3666        }
3667        ExecutionPlan::DeterministicPauli { .. } => {
3668            return Err(stateless_route(kind, diagnostic, "deterministic Pauli"));
3669        }
3670        ExecutionPlan::PauliPath => {
3671            return Err(stateless_route(kind, diagnostic, "Pauli path"));
3672        }
3673    };
3674    if kind.is_auto()
3675        && !plan_answers(&plan, diagnostic)
3676        && circuit.num_qubits <= max_statevector_qubits()
3677    {
3678        plan = plan_for_family(kind, Family::Statevector, circuit.num_qubits);
3679    }
3680    let mut backend: Box<dyn Backend> = plan.build(seed);
3681    check_diagnostic_width(&*backend, diagnostic, subsystem_len)?;
3682    execute(&mut *backend, circuit, &SimOptions::classical_only())?;
3683    Ok(backend)
3684}
3685
3686/// Width from which an `Auto` expectation terminal tries the scalar tensor
3687/// path first. Below it the statevector run takes a few milliseconds and the
3688/// bounded dry run that decides the route would cost as much as the run.
3689const TENSOR_ROUTE_MIN_QUBITS: usize = 18;
3690
3691/// The scalar tensor route for an `Auto` expectation terminal under the
3692/// statevector cap: `None` below [`TENSOR_ROUTE_MIN_QUBITS`], on a circuit the
3693/// scalar network cannot hold, or when any observable's bounded greedy plan
3694/// crosses the peak bound, so the caller takes its dense route. Observables
3695/// are validated first, so a bad one errors before any dry run.
3696fn tensor_route_expectations(
3697    circuit: &Circuit,
3698    observables: &[Vec<PauliTerm>],
3699) -> Option<Result<ExpectationResult>> {
3700    if circuit.num_qubits < TENSOR_ROUTE_MIN_QUBITS || observables.is_empty() {
3701        return None;
3702    }
3703    for observable in observables {
3704        if let Err(e) = validate_observable(observable, circuit.num_qubits) {
3705            return Some(Err(e));
3706        }
3707    }
3708    let planned = match crate::backend::tensornetwork::plan_bounded_expectations(
3709        circuit,
3710        observables,
3711        crate::backend::tensornetwork::AUTO_EXPECTATION_PEAK_BOUND,
3712    )? {
3713        Ok(planned) => planned,
3714        Err(e) => return Some(Err(e)),
3715    };
3716    Some(planned.contract().map(|values| {
3717        analytic_expectations(values, RunMetadata::exact(ResolvedBackend::TensorNetwork))
3718    }))
3719}
3720
3721/// MPS work per observable, in [`mps_apply_cost`] units, from which an `Auto`
3722/// expectation above the statevector cap plans the scalar tensor route before
3723/// running MPS. About 2 ms of MPS time, the most one observable's dry run took
3724/// across the calibration circuits (0.1 to 1.9 ms), so a declined or losing
3725/// plan costs at most about the MPS run it tried to replace.
3726const TENSOR_ROUTE_MPS_COST_PER_OBSERVABLE: f64 = 1.5e5;
3727
3728/// [`mps_apply_cost`] units one planned tensor-network element costs: about
3729/// 40 ns per element contracted against 15 ns per unit.
3730const TENSOR_ELEMENT_MPS_COST: f64 = 3.0;
3731
3732/// The scalar tensor route for an `Auto` expectation above the statevector cap,
3733/// where the alternative is the MPS `Auto` resolves to rather than a dense run.
3734/// `None` when `Auto` resolves to another backend, when the MPS estimate is
3735/// under [`TENSOR_ROUTE_MPS_COST_PER_OBSERVABLE`] per observable, when a plan
3736/// crosses the peak bound, or when the planned contraction costs more than the
3737/// MPS estimate.
3738fn tensor_route_over_mps(
3739    circuit: &Circuit,
3740    observables: &[Vec<PauliTerm>],
3741) -> Option<Result<ExpectationResult>> {
3742    if observables.is_empty() {
3743        return None;
3744    }
3745    let mps_cost = mps_apply_cost(circuit, AUTO_MPS_BOND_DIM);
3746    if mps_cost < TENSOR_ROUTE_MPS_COST_PER_OBSERVABLE * observables.len() as f64 {
3747        return None;
3748    }
3749    let (_, has_partial_independence) = analyze_independence(circuit);
3750    let ExecutionPlan::Backend(plan) =
3751        resolve(&BackendKind::Auto, circuit, has_partial_independence)
3752    else {
3753        return None;
3754    };
3755    if !matches!(plan, BackendPlan::Mps { .. }) {
3756        return None;
3757    }
3758    for observable in observables {
3759        if let Err(e) = validate_observable(observable, circuit.num_qubits) {
3760            return Some(Err(e));
3761        }
3762    }
3763    let planned = match crate::backend::tensornetwork::plan_bounded_expectations(
3764        circuit,
3765        observables,
3766        crate::backend::tensornetwork::AUTO_EXPECTATION_PEAK_BOUND,
3767    )? {
3768        Ok(planned) => planned,
3769        Err(e) => return Some(Err(e)),
3770    };
3771    if planned.total_elements() as f64 * TENSOR_ELEMENT_MPS_COST >= mps_cost {
3772        return None;
3773    }
3774    Some(planned.contract().map(|values| {
3775        analytic_expectations(values, RunMetadata::exact(ResolvedBackend::TensorNetwork))
3776    }))
3777}
3778
3779/// Evaluate `observables` on the backend `kind` resolves to, using that
3780/// backend's own representation.
3781///
3782/// Backends without a native Pauli path report `BackendUnsupported` naming
3783/// themselves, so a request that cannot be served says which engine could not
3784/// serve it rather than blaming the route that picked it.
3785fn expectation_values_native(
3786    kind: &BackendKind,
3787    circuit: &Circuit,
3788    observables: &[Vec<PauliTerm>],
3789    seed: u64,
3790) -> Result<ExpectationResult> {
3791    if !kind.is_auto() {
3792        validate_explicit_backend(kind, circuit)?;
3793    }
3794    // Before the run, matching the statevector path, so a typo in an observable
3795    // does not cost a 40-qubit simulation first.
3796    for observable in observables {
3797        validate_observable(observable, circuit.num_qubits)?;
3798    }
3799
3800    let (_, has_partial_independence) = analyze_independence(circuit);
3801    let ExecutionPlan::Backend(plan) = resolve(kind, circuit, has_partial_independence) else {
3802        return Err(PrismError::IncompatibleBackend {
3803            backend: format!("{kind:?}"),
3804            reason: "expectation values need a backend that holds a state; the stabilizer-rank \
3805                     route returns probabilities only"
3806                .into(),
3807        });
3808    };
3809
3810    let mut backend = plan.build(seed);
3811    if !backend.supports_pauli_expectation() {
3812        return Err(PrismError::BackendUnsupported {
3813            backend: backend.name().to_string(),
3814            operation: "Pauli expectation values".to_string(),
3815        });
3816    }
3817    execute(&mut *backend, circuit, &SimOptions::classical_only())?;
3818    let values = backend.pauli_expectations(observables)?;
3819    Ok(analytic_expectations(values, backend_metadata(&*backend)))
3820}
3821
3822fn expectation_values_statevector(
3823    kind: &BackendKind,
3824    circuit: &Circuit,
3825    observables: &[Vec<PauliTerm>],
3826    seed: u64,
3827) -> Result<ExpectationResult> {
3828    // Validate before the 2^n simulation so bad observables fail cheaply. The
3829    // mask reduction shifts by qubit index, so it follows the `init` that
3830    // rejects a width no mask can address.
3831    for obs in observables {
3832        validate_observable(obs, circuit.num_qubits)?;
3833    }
3834
3835    let accel = accel_for(kind, Family::Statevector, circuit.num_qubits);
3836    let mut backend = build_statevector(&accel, seed);
3837    let expanded = expand_for_backend(&backend, circuit);
3838    let fused = fuse_for_backend(&backend, &expanded);
3839    backend.init(fused.num_qubits, fused.num_classical_bits)?;
3840    let masks = observables
3841        .iter()
3842        .map(|obs| pauli_masks(obs, circuit.num_qubits))
3843        .collect::<Result<Vec<_>>>()?;
3844    backend.apply_instructions(&fused.instructions)?;
3845
3846    let values = match backend.pauli_expectations_on_device(&masks) {
3847        Some(values) => values?,
3848        None => pauli_expectations_from_masks(backend.state_vector(), &masks).0,
3849    };
3850    let metadata = backend_metadata(&backend);
3851    Ok(analytic_expectations(values, metadata))
3852}
3853
3854/// Multi-shot execution for the distributed statevector backend.
3855///
3856/// Every rank runs this function in lockstep. Circuits with only terminal
3857/// measurements run once and sample basis indices without gathering the dense
3858/// state on any rank. Circuits with mid-circuit measurements run once per shot,
3859/// prefused, with per-shot seeds matching the generic slow path.
3860#[cfg(feature = "distributed")]
3861fn run_shots_distributed(
3862    context: std::sync::Arc<crate::distributed::DistributedContext>,
3863    circuit: &Circuit,
3864    num_shots: usize,
3865    seed: u64,
3866) -> Result<ShotsResult> {
3867    use crate::backend::distributed_statevector::DistributedStatevectorBackend;
3868
3869    let meas_map = circuit.measurement_map();
3870    if meas_map.is_empty() {
3871        // No measurements means every shot is all false, but init must still
3872        // run so invalid rank counts and local qubit floor violations surface as
3873        // errors instead of fabricated output.
3874        let mut backend = DistributedStatevectorBackend::new(context, seed);
3875        backend.init(circuit.num_qubits, circuit.num_classical_bits)?;
3876        return Ok(ShotsResult::from_shots(
3877            vec![vec![false; circuit.num_classical_bits]; num_shots],
3878            circuit.num_classical_bits,
3879        )
3880        .with_metadata(backend_metadata(&backend)));
3881    }
3882
3883    if circuit.has_terminal_measurements_only() {
3884        let stripped = circuit.without_measurements();
3885        let mut backend = DistributedStatevectorBackend::new(context, seed);
3886        execute(&mut backend, &stripped, &SimOptions::classical_only())?;
3887        let samples = backend.sample_basis_states(num_shots, seed)?;
3888        return Ok(ShotsResult::from_shots(
3889            shots_from_basis_samples(&samples, &meas_map, circuit.num_classical_bits),
3890            circuit.num_classical_bits,
3891        )
3892        .with_metadata(backend_metadata(&backend)));
3893    }
3894
3895    let probe = DistributedStatevectorBackend::new(context.clone(), seed);
3896    let expanded = expand_for_backend(&probe, circuit);
3897    let fused = fuse_for_backend(&probe, &expanded);
3898    let opts = SimOptions::classical_only();
3899    let mut shots = Vec::with_capacity(num_shots);
3900    let mut metadata = RunMetadata::exact(ResolvedBackend::Distributed);
3901    for i in 0..num_shots {
3902        let shot_seed = mix_seed(seed, i);
3903        let mut backend = DistributedStatevectorBackend::new(context.clone(), shot_seed);
3904        let result = execute_circuit(&mut backend, &fused, &opts)?;
3905        metadata.weaken_with(&result.metadata);
3906        shots.push(result.classical_bits);
3907    }
3908    Ok(ShotsResult::from_shots(shots, circuit.num_classical_bits).with_metadata(metadata))
3909}
3910
3911/// Execute a circuit multiple times with explicit backend selection.
3912pub(crate) fn run_shots_with(
3913    kind: BackendKind,
3914    circuit: &Circuit,
3915    num_shots: usize,
3916    seed: u64,
3917) -> Result<ShotsResult> {
3918    // The distributed backend runs every rank in lockstep, so shot execution
3919    // must not route through shortcuts that reshape the collective call
3920    // sequence. Dispatch directly.
3921    #[cfg(feature = "distributed")]
3922    if let BackendKind::StatevectorDistributed { context } = &kind {
3923        return run_shots_distributed(context.clone(), circuit, num_shots, seed);
3924    }
3925
3926    // Once per shots call, not per shot: a guard that cannot depend on a
3927    // measurement is resolved here so the sampling predicates below see the
3928    // circuit that actually runs. A circuit with no guard borrows through.
3929    let folded = circuit.fold_static_guards();
3930    let circuit = folded.as_ref();
3931
3932    let bits = circuit.num_classical_bits;
3933    let source = prepare_shot_source(&kind, circuit, num_shots, seed)?;
3934    let Some(metadata) = source.metadata() else {
3935        return run_shots_per_shot(kind, circuit, num_shots, seed, ShotRows::Exchangeable);
3936    };
3937    let result = match source {
3938        ShotSource::Compiled {
3939            mut sampler,
3940            meas_map,
3941            ..
3942        } => {
3943            let packed = sampler.try_sample_bulk_packed(num_shots)?;
3944            ShotsResult::from_shots(
3945                packed_shots_to_classical_bits(&packed, &meas_map, bits),
3946                bits,
3947            )
3948        }
3949        ShotSource::TerminalStatevector { backend, meas_map } => {
3950            let shots = if backend.is_gpu_resident() {
3951                let probs = backend.probabilities()?;
3952                sample_shots_from_probs(&probs, &meas_map, bits, num_shots, seed)
3953            } else {
3954                sample_shots_from_state(
3955                    backend.state_vector(),
3956                    backend.probability_scale(),
3957                    &meas_map,
3958                    bits,
3959                    num_shots,
3960                    seed,
3961                )
3962            };
3963            ShotsResult::from_shots(shots, bits)
3964        }
3965        ShotSource::Native {
3966            mut backend,
3967            meas_map,
3968        } => {
3969            let samples = backend.sample_basis_states(num_shots, seed)?;
3970            ShotsResult::from_shots(shots_from_basis_samples(&samples, &meas_map, bits), bits)
3971        }
3972        ShotSource::TerminalProbabilities {
3973            probs, meas_map, ..
3974        } => ShotsResult::from_shots(sample_shots(&probs, &meas_map, bits, num_shots, seed), bits),
3975        ShotSource::StabilizerRank => {
3976            stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed)?
3977        }
3978        ShotSource::PerShot => unreachable!("handled above"),
3979    };
3980    Ok(result.with_metadata(metadata))
3981}
3982
3983/// Whether a caller of [`run_shots_per_shot`] reads the rows themselves or only
3984/// counts them, which spares the shuffle that keeps branched rows exchangeable.
3985#[derive(Clone, Copy, PartialEq, Eq)]
3986enum ShotRows {
3987    Exchangeable,
3988    CountedOnly,
3989}
3990
3991/// Run `circuit` once per shot, which mid-circuit measurements force, or on
3992/// the host statevector once per outcome history
3993/// ([`branching::branch_shots`]).
3994fn run_shots_per_shot(
3995    kind: BackendKind,
3996    circuit: &Circuit,
3997    num_shots: usize,
3998    seed: u64,
3999    rows: ShotRows,
4000) -> Result<ShotsResult> {
4001    // Pre-compute seed-independent analysis to avoid redundant work.
4002    if !kind.is_auto() {
4003        validate_explicit_backend(&kind, circuit)?;
4004    }
4005
4006    let (decompose, has_partial_independence) = analyze_independence(circuit);
4007
4008    if matches!(kind, BackendKind::StabilizerRank) {
4009        return stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed);
4010    }
4011    if matches!(
4012        kind,
4013        BackendKind::StochasticPauli { .. } | BackendKind::DeterministicPauli { .. }
4014    ) {
4015        return Err(crate::error::PrismError::IncompatibleBackend {
4016            backend: format!("{kind:?}"),
4017            reason: "Pauli propagation backends do not support mid-circuit measurements".into(),
4018        });
4019    }
4020    if kind.is_auto() && auto_stabilizer_rank_t_count(circuit, MAX_AUTO_T_COUNT_SHOTS).is_some() {
4021        return stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed);
4022    }
4023
4024    if has_temporal_clifford_opportunity(&kind, circuit) {
4025        if decompose.is_none()
4026            && let Some(tc) = plan_temporal_clifford(&kind, circuit)
4027        {
4028            let route = ResolvedBackend::Statevector;
4029            let states = [(route, circuit.num_qubits)];
4030            return collect_shots(
4031                circuit,
4032                num_shots,
4033                seed,
4034                route,
4035                &kind,
4036                &states,
4037                |shot_seed| {
4038                    let outcome = run_temporal_clifford(&tc, shot_seed, false)?;
4039                    Ok((outcome.classical_bits, outcome.metadata))
4040                },
4041            );
4042        }
4043        // Decomposable circuits with a temporal prefix keep the per-shot
4044        // full-pipeline route; the prefix spans blocks that decomposition
4045        // would otherwise split. Its blocks are partitioned and planned once.
4046        let opts = SimOptions::classical_only();
4047        let route = resolve_backend(&kind, circuit, has_partial_independence).resolved();
4048        let plan = plan_probability_route(&kind, circuit);
4049        let blocks = match &plan {
4050            ProbabilityRoute::Decomposed(components) => {
4051                Some(PreparedBlocks::new(&kind, components.clone(), circuit))
4052            }
4053            _ => None,
4054        };
4055        let states = [(route, circuit.num_qubits)];
4056        return collect_shots(
4057            circuit,
4058            num_shots,
4059            seed,
4060            route,
4061            &kind,
4062            &states,
4063            |shot_seed| {
4064                let outcome = match &blocks {
4065                    Some(blocks) => blocks.run(&kind, circuit, shot_seed, &opts)?,
4066                    None => run_route(&kind, circuit, shot_seed, opts, &plan)?,
4067                };
4068                Ok((outcome.classical_bits, outcome.metadata))
4069            },
4070        );
4071    }
4072
4073    let opts = SimOptions::classical_only();
4074
4075    if let Some((ref comps, _)) = decompose {
4076        let partitions = circuit.partition_subcircuits(comps);
4077        let block_plans: Vec<BackendPlan> = partitions
4078            .iter()
4079            .map(|(sub, _, _)| {
4080                if !kind.is_auto() {
4081                    validate_explicit_backend(&kind, sub)?;
4082                }
4083                Ok(resolve_backend(&kind, sub, false))
4084            })
4085            .collect::<Result<_>>()?;
4086        let fused_blocks: Vec<std::borrow::Cow<'_, Circuit>> = partitions
4087            .iter()
4088            .zip(&block_plans)
4089            .map(|((sub, _, _), plan)| {
4090                let probe = plan.build(seed);
4091                let expanded = expand_for_backend(&*probe, sub);
4092                std::borrow::Cow::Owned(fuse_for_backend(&*probe, &expanded).into_owned())
4093            })
4094            .collect();
4095
4096        let block_states: Vec<(ResolvedBackend, usize)> = block_plans
4097            .iter()
4098            .zip(comps)
4099            .map(|(plan, comp)| (plan.resolved(), comp.len()))
4100            .collect();
4101        collect_shots(
4102            circuit,
4103            num_shots,
4104            seed,
4105            ResolvedBackend::Decomposed,
4106            &kind,
4107            &block_states,
4108            |shot_seed| {
4109                let result = run_decomposed_prefused(
4110                    &block_plans,
4111                    comps,
4112                    &partitions,
4113                    &fused_blocks,
4114                    shot_seed,
4115                    &opts,
4116                    circuit,
4117                )?;
4118                Ok((result.classical_bits, result.metadata))
4119            },
4120        )
4121    } else {
4122        let plan = resolve_backend(&kind, circuit, has_partial_independence);
4123        let probe = plan.build(seed);
4124        let expanded = expand_for_backend(&*probe, circuit);
4125        let fused = fuse_for_backend(&*probe, &expanded);
4126
4127        let route = plan.resolved();
4128        let states = [(route, circuit.num_qubits)];
4129        if plan.is_host_statevector() && circuit.num_qubits < max_statevector_qubits() {
4130            #[cfg(feature = "parallel")]
4131            let parallel = shots_split_across_workers(&kind, &states)
4132                && circuit.num_qubits >= branching::MIN_QUBITS_FOR_PARALLEL_SPLITS;
4133            #[cfg(not(feature = "parallel"))]
4134            let parallel = false;
4135            let limits = branching::BranchLimits::for_width(circuit.num_qubits, parallel);
4136            return branching::branch_shots(
4137                &fused,
4138                num_shots,
4139                seed,
4140                &limits,
4141                rows == ShotRows::Exchangeable,
4142            );
4143        }
4144        collect_shots(
4145            circuit,
4146            num_shots,
4147            seed,
4148            route,
4149            &kind,
4150            &states,
4151            |shot_seed| {
4152                let mut backend = plan.build(shot_seed);
4153                let outcome = execute_circuit(&mut *backend, &fused, &opts)?;
4154                Ok((outcome.classical_bits, outcome.metadata))
4155            },
4156        )
4157    }
4158}
4159
4160pub(crate) const fn splitmix64(mut z: u64) -> u64 {
4161    z = z.wrapping_add(0x9E37_79B9_7F4A_7C15);
4162    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
4163    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
4164    z ^ (z >> 31)
4165}
4166
4167/// Seed for item `index` (a shot, trajectory, block, qubit or observable) of a run
4168/// seeded `seed`. Both inputs pass through SplitMix64, so runs on nearby seeds draw
4169/// unrelated streams rather than shifted copies of one another.
4170pub(crate) const fn mix_seed(seed: u64, index: usize) -> u64 {
4171    splitmix64(splitmix64(seed) ^ index as u64)
4172}
4173
4174/// Each shot evolves its own state, so `shot` returns the provenance of its own
4175/// run and the ensemble keeps the weakest claim across them. `route` names the
4176/// engine for a zero-shot request, which runs nothing to read provenance off.
4177///
4178/// Under the `parallel` feature the shots split across Rayon workers when
4179/// [`shots_split_across_workers`] accepts `kind` for `states`, the family and width
4180/// of each state one shot holds. Shot `i` always runs on `mix_seed(seed, i)` and
4181/// the fold runs in shot order, so the result does not depend on the thread
4182/// count.
4183fn collect_shots(
4184    circuit: &Circuit,
4185    num_shots: usize,
4186    seed: u64,
4187    route: ResolvedBackend,
4188    kind: &BackendKind,
4189    states: &[(ResolvedBackend, usize)],
4190    shot: impl Fn(u64) -> Result<(Vec<bool>, RunMetadata)> + Sync,
4191) -> Result<ShotsResult> {
4192    #[cfg(feature = "parallel")]
4193    if num_shots > 1 && shots_split_across_workers(kind, states) {
4194        use rayon::prelude::*;
4195        let runs: Vec<Result<(Vec<bool>, RunMetadata)>> = (0..num_shots)
4196            .into_par_iter()
4197            .map(|i| shot(mix_seed(seed, i)))
4198            .collect();
4199        return fold_shots(circuit, route, runs);
4200    }
4201    #[cfg(not(feature = "parallel"))]
4202    let _ = (kind, states);
4203    fold_shots(
4204        circuit,
4205        route,
4206        (0..num_shots).map(|i| shot(mix_seed(seed, i))),
4207    )
4208}
4209
4210fn fold_shots(
4211    circuit: &Circuit,
4212    route: ResolvedBackend,
4213    runs: impl IntoIterator<Item = Result<(Vec<bool>, RunMetadata)>>,
4214) -> Result<ShotsResult> {
4215    let runs = runs.into_iter();
4216    let mut shots = Vec::with_capacity(runs.size_hint().0);
4217    let mut metadata = RunMetadata::exact(route);
4218    for (i, run) in runs.enumerate() {
4219        let (bits, shot_metadata) = run?;
4220        if i == 0 {
4221            metadata = shot_metadata;
4222        } else {
4223            metadata.weaken_with(&shot_metadata);
4224        }
4225        shots.push(bits);
4226    }
4227    Ok(ShotsResult::from_shots(shots, circuit.num_classical_bits).with_metadata(metadata))
4228}
4229
4230/// Family choice for auto-routed non-Pauli noise trajectories. Restricted to
4231/// families whose trajectory operations (1q Kraus, qubit probability, reduced
4232/// density matrix, reset) are supported; the statevector leaf carries the
4233/// kind's acceleration. A two-qubit Kraus event entangles its pair, so it keeps
4234/// the circuit off the product state as an entangling gate would.
4235fn general_noise_plan(
4236    kind: &BackendKind,
4237    circuit: &Circuit,
4238    noise_model: &NoiseModel,
4239) -> BackendPlan {
4240    let family = if !circuit.has_entangling_gates() && !noise_model.has_two_qubit_kraus() {
4241        Family::ProductState
4242    } else if circuit.num_qubits > max_statevector_qubits() {
4243        if circuit.is_sparse_friendly() && circuit.num_qubits <= MAX_SPARSE_INDEX_QUBITS {
4244            Family::Sparse
4245        } else {
4246            Family::Mps
4247        }
4248    } else {
4249        Family::Statevector
4250    };
4251    plan_for_family(kind, family, circuit.num_qubits)
4252}
4253
4254/// Execute a noisy circuit for multiple shots with explicit backend selection.
4255///
4256/// For Clifford circuits with Auto/Stabilizer/FactoredStabilizer backends,
4257/// uses the compiled noisy sampler (fast O(n²·m) compile + O(events·m/64) per shot).
4258/// For all other cases, falls back to per-shot simulation with noise injection.
4259/// The compiled noisy path is limited to terminal measurements with no resets
4260/// or classical conditionals. Pauli noise on the host statevector, or on a
4261/// tensor network whose probabilities fit the dense cap, under the same limits
4262/// draws every shot's errors first and simulates each distinct pattern once;
4263/// see [`trajectory::PauliGroups`]. On the host statevector, a circuit with a
4264/// mid-circuit measurement, reset or condition shares the gates before the first
4265/// of them the same way, and each shot finishes its own trajectory from there.
4266pub(crate) fn run_shots_with_noise(
4267    kind: BackendKind,
4268    circuit: &Circuit,
4269    noise_model: &noise::NoiseModel,
4270    num_shots: usize,
4271    seed: u64,
4272) -> Result<ShotsResult> {
4273    noise_model.validate_for(circuit)?;
4274
4275    // Trajectory execution runs shots on Rayon worker threads, whose
4276    // scheduling order differs per rank. Per-shot distributed backends would
4277    // issue collectives out of lockstep and deadlock or corrupt exchanges.
4278    // Reject until a lockstep noisy path exists.
4279    #[cfg(feature = "distributed")]
4280    if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
4281        return Err(crate::error::PrismError::IncompatibleBackend {
4282            backend: format!("{kind:?}"),
4283            reason: "noisy shot sampling is not supported on the distributed backend; \
4284                     trajectory execution cannot keep rank collectives in lockstep"
4285                .into(),
4286        });
4287    }
4288
4289    if kind.is_density_matrix() {
4290        let probs = exact_noisy_probabilities(&kind, circuit, noise_model, None, seed)?;
4291        return Ok(ShotsResult::from_shots(
4292            sample_exact_noisy_shots(&probs, circuit, noise_model, num_shots, seed),
4293            circuit.num_classical_bits,
4294        )
4295        .with_metadata(exact_mixture_metadata(&kind)));
4296    }
4297
4298    if !kind.supports_noisy_per_shot() {
4299        return Err(crate::error::PrismError::IncompatibleBackend {
4300            backend: format!("{kind:?}"),
4301            reason: "this backend holds no per-shot pure state to inject noise into; select \
4302                     DensityMatrix for the exact mixed state, or a backend that evolves one \
4303                     state per trajectory"
4304                .into(),
4305        });
4306    }
4307
4308    let is_stabilizer_kind = kind.is_stabilizer_family();
4309
4310    if is_stabilizer_kind && !noise_model.has_only_pauli_channels() {
4311        return Err(crate::error::PrismError::IncompatibleBackend {
4312            backend: format!("{kind:?}"),
4313            reason: format!(
4314                "stabilizer backends only support Pauli/depolarizing noise; use {} for amplitude damping, phase damping, thermal relaxation, or custom Kraus",
4315                BackendKind::general_noise_backend_names()
4316            ),
4317        });
4318    }
4319
4320    if !noise_model.has_only_pauli_channels() && !kind.supports_general_noise() {
4321        return Err(crate::error::PrismError::IncompatibleBackend {
4322            backend: format!("{kind:?}"),
4323            reason: format!(
4324                "non-Pauli noise requires {}",
4325                BackendKind::general_noise_backend_names()
4326            ),
4327        });
4328    }
4329
4330    if is_stabilizer_kind && !circuit.is_clifford_only() {
4331        return Err(crate::error::PrismError::IncompatibleBackend {
4332            backend: format!("{kind:?}"),
4333            reason: "circuit contains non-Clifford gates".into(),
4334        });
4335    }
4336
4337    if !kind.is_auto() {
4338        validate_explicit_backend(&kind, circuit)?;
4339    }
4340
4341    if noise_model.has_only_pauli_channels() {
4342        let use_compiled = (kind.is_auto()
4343            || matches!(
4344                kind,
4345                BackendKind::Stabilizer | BackendKind::FactoredStabilizer
4346            ))
4347            && supports_compiled_measurement_sampling(circuit)
4348            || {
4349                #[cfg(feature = "gpu")]
4350                {
4351                    matches!(kind, BackendKind::StabilizerGpu { .. })
4352                        && supports_compiled_measurement_sampling(circuit)
4353                }
4354                #[cfg(not(feature = "gpu"))]
4355                {
4356                    false
4357                }
4358            };
4359
4360        if use_compiled {
4361            #[cfg(feature = "gpu")]
4362            if let BackendKind::StabilizerGpu { context } = &kind {
4363                return noise::run_shots_noisy_with_gpu(
4364                    circuit,
4365                    noise_model,
4366                    num_shots,
4367                    seed,
4368                    context.clone(),
4369                );
4370            }
4371            return noise::run_shots_noisy(circuit, noise_model, num_shots, seed);
4372        }
4373    }
4374
4375    let plan = if kind.is_auto() && !noise_model.has_only_pauli_channels() {
4376        general_noise_plan(&kind, circuit, noise_model)
4377    } else {
4378        resolve_backend(&kind, circuit, false)
4379    };
4380    // Noise events are indexed per instruction, so trajectories apply the
4381    // stream raw and the Pauli-rotation lowering pass cannot run. The
4382    // statevector applies the gate natively (its device path lowers inline);
4383    // any other backend without the kernel is rejected before a shot starts.
4384    let has_pauli_rot = crate::circuit::any_gate(&circuit.instructions, &mut |gate| {
4385        matches!(gate, crate::gates::Gate::PauliRot(_))
4386    });
4387    if has_pauli_rot
4388        && !matches!(plan, BackendPlan::Statevector { .. })
4389        && !plan.build(seed).supports_pauli_rotation()
4390    {
4391        return Err(crate::error::PrismError::IncompatibleBackend {
4392            backend: format!("{:?}", plan.resolved()),
4393            reason: "noisy trajectories apply the instruction stream raw so noise events \
4394                     stay aligned to it, which leaves no room for the Pauli-rotation \
4395                     lowering this backend needs; run on the statevector, or expand the \
4396                     rotations with circuit::expand_pauli_rotations and attach the noise \
4397                     model to the expanded circuit"
4398                .into(),
4399        });
4400    }
4401    // A correlated two-qubit Kraus branch reads a two-qubit reduced density
4402    // matrix, which only the host statevector answers. Without this the model
4403    // clears every routing gate and fails part way through the first shot, on a
4404    // backend an Auto route may have picked for the caller.
4405    if noise_model.has_two_qubit_kraus() && !plan.build(seed).supports_two_qubit_kraus() {
4406        return Err(crate::error::PrismError::IncompatibleBackend {
4407            backend: format!("{:?}", plan.resolved()),
4408            reason: "a two-qubit Kraus channel needs the pair's reduced density matrix \
4409                     its branch probabilities are drawn from, and a kernel for the branch \
4410                     operator, which is not unitary. A tableau and a product state have no \
4411                     such kernel, and a tensor network traces out no arbitrary pair; run on \
4412                     a statevector, sparse, factored or MPS route, or evaluate the channel \
4413                     exactly on BackendKind::DensityMatrix"
4414                .into(),
4415        });
4416    }
4417    let host_statevector = matches!(plan, BackendPlan::Statevector { .. }) && !plan.is_gpu();
4418    let dense_tensor_network = matches!(plan, BackendPlan::TensorNetwork { .. })
4419        && crate::backend::tensor_probability_len("tensor network", circuit.num_qubits).is_ok();
4420    if (host_statevector || dense_tensor_network)
4421        && let Some(groups) = trajectory::PauliGroups::sample(circuit, noise_model, num_shots, seed)
4422    {
4423        let build = |s| plan.build(s);
4424        return trajectory::run_pauli_groups(
4425            &groups,
4426            circuit,
4427            noise_model,
4428            seed,
4429            dense_tensor_network.then_some(&build as trajectory::GroupBackendFactory<'_>),
4430            plan.resolved(),
4431        );
4432    }
4433    // A checkpoint, a group state and a shot state at once.
4434    if host_statevector
4435        && circuit.num_qubits + 1 < max_statevector_qubits()
4436        && let Some(groups) =
4437            trajectory::PauliGroups::sample_replayed(circuit, noise_model, num_shots, seed)
4438    {
4439        return trajectory::run_replayed_groups(
4440            &groups,
4441            circuit,
4442            noise_model,
4443            seed,
4444            plan.resolved(),
4445        );
4446    }
4447    let route = plan.resolved();
4448    trajectory::run_trajectories(
4449        |s| plan.build(s),
4450        circuit,
4451        noise_model,
4452        num_shots,
4453        seed,
4454        plan.is_gpu(),
4455        route,
4456    )
4457}
4458
4459#[cfg(test)]
4460mod tests;
4461
4462#[cfg(all(test, feature = "gpu"))]
4463mod gpu_stub_tests;
4464
4465#[cfg(test)]
4466mod terminal_candidate_matrix_tests;
4467
4468#[cfg(test)]
4469mod diagnostic_terminal_tests;