Skip to main content

prism_q/sim/
mod.rs

1//! Simulation orchestration.
2//!
3//! Connects the circuit IR to a backend. This module is deliberately thin,
4//! the complexity lives in the backends and the parser. Entry points:
5//! [`simulate`], [`run_qasm`], [`run_on`].
6
7pub mod braket;
8pub mod compiled;
9mod decomposed;
10mod dispatch;
11pub mod gradient;
12pub mod homological;
13mod metadata;
14pub mod noise;
15mod observable;
16mod probability;
17pub(crate) mod shots;
18pub mod stabilizer_rank;
19mod terminal_sampling;
20mod trajectory;
21pub mod unified_pauli;
22
23pub use braket::ResultValue;
24pub(crate) use decomposed::merge_probabilities;
25use decomposed::{
26    MIN_DECOMPOSITION_QUBITS, run_decomposed, run_decomposed_prefused, should_decompose,
27};
28pub use dispatch::BackendKind;
29use dispatch::{
30    AUTO_SPD_MAX_TERMS, BackendPlan, ExecutionPlan, Family, MAX_AUTO_T_COUNT_EXACT,
31    MAX_AUTO_T_COUNT_SHOTS, MAX_STABILIZER_RANK_QUBITS, MIN_BLOCK_FOR_FACTORED_STAB,
32    MIN_FACTORED_STABILIZER_QUBITS, MIN_QUBITS_FOR_SPD_AUTO, accel_for, approximate_route_name,
33    auto_selects_cpu_statevector, build_statevector, has_temporal_clifford_opportunity,
34    initial_state_plan, plan_for_family, plan_temporal_clifford, resolve, resolve_backend,
35    run_temporal_clifford, stabilizer_rank_budget, validate_explicit_backend,
36};
37pub use metadata::{Engine, Exactness, ExpectationResult, Placement, ResolvedBackend, RunMetadata};
38#[cfg(feature = "distributed")]
39pub(crate) use observable::pauli_sandwich;
40pub use observable::{ObservableExpectation, PauliObservable};
41pub(crate) use observable::{
42    finish_expectations, i_pow, pauli_expectation_from_masks, pauli_expectations_from_masks,
43    pauli_masks, pauli_sandwiches_from_masks, validate_observable,
44};
45pub use probability::{FactoredBlock, Probabilities, ProbabilitiesIter};
46pub use shots::{ShotsResult, bitstring};
47
48use std::collections::HashMap;
49
50use num_complex::Complex64;
51
52use crate::backend::sparse::MAX_SPARSE_INDEX_QUBITS;
53use crate::backend::statevector::StatevectorBackend;
54use crate::backend::{Backend, max_statevector_qubits};
55use crate::circuit::{Circuit, Instruction};
56use crate::error::{PrismError, Result};
57use crate::sim::noise::NoiseModel;
58use shots::{packed_shots_to_classical_bits, sample_shots, shots_from_basis_samples};
59use terminal_sampling::{
60    sample_counts_from_probs, sample_counts_from_state, sample_shots_from_probs,
61    sample_shots_from_state,
62};
63use unified_pauli::PauliTerm;
64
65type TerminalStatevector = (StatevectorBackend, Vec<(usize, usize)>);
66
67#[derive(Debug, Clone, Copy)]
68pub(crate) struct SimOptions {
69    pub(crate) probabilities: bool,
70}
71
72impl Default for SimOptions {
73    fn default() -> Self {
74        Self {
75            probabilities: true,
76        }
77    }
78}
79
80impl SimOptions {
81    pub(crate) fn classical_only() -> Self {
82        Self {
83            probabilities: false,
84        }
85    }
86}
87
88/// Result of a generic simulation run.
89#[derive(Debug, Clone)]
90pub struct RunOutcome {
91    /// Classical measurement outcomes, indexed by classical bit number.
92    /// `true` = measured |1⟩.
93    pub classical_bits: Vec<bool>,
94    /// Probability of each computational basis state (length 2^n).
95    ///
96    /// `None` means the selected backend cannot expose a dense probability
97    /// distribution for this circuit. Other probability extraction failures
98    /// are returned as errors by the query that produced this result.
99    pub probabilities: Option<Probabilities>,
100    /// Which engine ran, whether the answer is exact, and where the state lived.
101    pub metadata: RunMetadata,
102}
103
104/// Frequency histogram returned by query-aware count sampling.
105#[derive(Debug, Clone)]
106pub struct CountsResult {
107    /// Histogram keyed by packed classical bits; same key layout as
108    /// [`ShotsResult::counts`], formattable with [`bitstring`].
109    pub counts: HashMap<Vec<u64>, u64>,
110    pub num_classical_bits: usize,
111    pub metadata: RunMetadata,
112}
113
114impl CountsResult {
115    pub fn into_counts(self) -> HashMap<Vec<u64>, u64> {
116        self.counts
117    }
118}
119
120/// Per-qubit marginal probabilities returned by query-aware marginal sampling.
121#[derive(Debug, Clone)]
122pub struct MarginalsResult {
123    /// `(P(0), P(1))` per qubit, indexed by qubit number.
124    pub marginals: Vec<(f64, f64)>,
125    pub metadata: RunMetadata,
126}
127
128impl MarginalsResult {
129    pub fn into_vec(self) -> Vec<(f64, f64)> {
130        self.marginals
131    }
132}
133
134/// Reduced density matrix of a qubit subset, returned by
135/// [`Simulate::reduced_density_matrix`].
136#[derive(Debug, Clone)]
137pub struct ReducedDensityMatrix {
138    /// The subsystem as it was requested, which fixes the index order below.
139    pub qubits: Vec<usize>,
140    /// Row major with side `2^k` over `k = qubits.len()`: `data[t * 2^k + t']`
141    /// is `<t|rho|t'>`, where bit `i` of `t` is the state of `qubits[i]`, so
142    /// `qubits[0]` is the lowest bit as `q[0]` is in a basis index. Trace one,
143    /// Hermitian to rounding.
144    pub data: Vec<Complex64>,
145    pub metadata: RunMetadata,
146}
147
148impl ReducedDensityMatrix {
149    /// `Tr(rho^2)`: 1 for a pure marginal, `2^-k` for the maximally mixed one.
150    pub fn purity(&self) -> f64 {
151        self.data.iter().map(|entry| entry.norm_sqr()).sum()
152    }
153}
154
155/// Operator variance of a weighted Pauli observable, returned by
156/// [`Simulate::observable_variance`].
157#[derive(Debug, Clone)]
158pub struct ObservableVariance {
159    /// `<H^2> - <H>^2` on the output state.
160    pub variance: f64,
161    /// `<H>` on the same state, evaluated on the way to the variance.
162    pub mean: f64,
163    pub metadata: RunMetadata,
164}
165
166/// Entanglement entropy of a subsystem, returned by
167/// [`Simulate::entanglement_entropy`].
168#[derive(Debug, Clone)]
169pub struct EntropyResult {
170    /// The subsystem as it was requested, the side of the cut the entropy is
171    /// read on.
172    pub subsystem: Vec<usize>,
173    /// Von Neumann entropy of the subsystem in nats: a Bell pair reads `ln 2`.
174    pub entropy: f64,
175    /// Schmidt values across the cut, descending, with squares summing to 1.
176    /// `None` where the backend holds the entropy without the spectrum that
177    /// stands behind it, as a stabilizer cut past the export cap does: its
178    /// `2^r` equal values do not fit.
179    pub schmidt_values: Option<Vec<f64>>,
180    pub metadata: RunMetadata,
181}
182
183/// Overlap between the output states of two runs, returned by
184/// [`Simulate::overlap`].
185#[derive(Debug, Clone)]
186pub struct OverlapResult {
187    /// `|<a|b>|^2` over the two normalized states: 1 for the same state up to
188    /// phase, 0 for orthogonal ones. The amplitude itself is not reported,
189    /// since a tableau keeps no global phase and every MPS truncation moves
190    /// one.
191    pub fidelity: f64,
192    /// Provenance of the run the terminal was called on, the left of the
193    /// inner product.
194    pub left: RunMetadata,
195    /// Provenance of the run passed as the argument.
196    pub right: RunMetadata,
197}
198
199/// Typestate marker: [`Simulate`] builder with no seed chosen yet.
200#[derive(Debug, Clone, Copy)]
201pub struct Unseeded;
202
203/// Typestate marker: [`Simulate`] builder with its RNG seed fixed.
204#[derive(Debug, Clone, Copy)]
205pub struct Seeded {
206    seed: u64,
207}
208
209/// Builder for query-aware simulation requests.
210pub struct Simulate<'c, SeedState> {
211    circuit: &'c Circuit,
212    kind: BackendKind,
213    seed: SeedState,
214    noise_model: Option<&'c noise::NoiseModel>,
215    initial_state: Option<&'c [Complex64]>,
216    require_exact: bool,
217}
218
219impl<'c, SeedState> Simulate<'c, SeedState> {
220    /// Select an explicit backend kind instead of [`BackendKind::Auto`] routing.
221    #[inline]
222    pub fn backend(mut self, kind: BackendKind) -> Self {
223        self.kind = kind;
224        self
225    }
226
227    /// Reject a route that could return an approximate answer, rather than
228    /// taking it and saying so in the result.
229    ///
230    /// [`BackendKind::Auto`] sends a circuit past the statevector cap to an MPS
231    /// at a bounded bond dimension, which is the only route those circuits have;
232    /// the result reports [`Exactness::Approximate`] either way. Call this when
233    /// an approximate answer is worse than no answer, and the run returns
234    /// `IncompatibleBackend` naming the engine it would have used.
235    ///
236    /// Routes that can be decided from the circuit are rejected before any state
237    /// is allocated; sparse Pauli dynamics only learns that it truncated while
238    /// propagating, so that one is caught on the finished result instead.
239    #[inline]
240    pub fn require_exact(mut self) -> Self {
241        self.require_exact = true;
242        self
243    }
244
245    /// Attach a noise model.
246    ///
247    /// [`Simulate::shots`] and [`Simulate::sample_counts`] accept one on any
248    /// backend with a per-shot pure state, averaging trajectories.
249    /// [`Simulate::run`], [`Simulate::marginals`],
250    /// [`Simulate::expectation_values`] and
251    /// [`Simulate::reduced_density_matrix`] answer from the exact mixture
252    /// instead, which only [`BackendKind::DensityMatrix`] and its device
253    /// sibling hold, so they require one of those, as does
254    /// [`Simulate::expectation_gradient_shift`] and, only to decline on it,
255    /// [`Simulate::entanglement_entropy`]: a mixture has no Schmidt
256    /// decomposition. [`Simulate::expectation_gradient`] rejects a noise model
257    /// on every backend.
258    #[inline]
259    pub fn noise(mut self, model: &'c noise::NoiseModel) -> Self {
260        self.noise_model = Some(model);
261        self
262    }
263
264    /// Start from `amplitudes` instead of |0...0⟩.
265    ///
266    /// Indexed with qubit 0 in the least significant bit, length `2^n` for the
267    /// circuit's `n` qubits, and normalized. A vector failing any of those is
268    /// rejected with `InvalidParameter` before the run.
269    ///
270    /// A start state also constrains the route, because shape-based dispatch
271    /// reads the circuit alone and its shortcuts hold only from |0...0⟩:
272    /// [`BackendKind::Auto`] resolves to the statevector, and every backend
273    /// other than the statevector (dense, device, or distributed) and
274    /// [`BackendKind::DensityMatrix`] reports `IncompatibleBackend`.
275    /// [`Simulate::expectation_gradient`] declines a start
276    /// state, as do [`Simulate::shots`] and [`Simulate::sample_counts`] with a
277    /// noise model attached, since trajectory replay has no start-state path.
278    #[inline]
279    pub fn initial_state(mut self, amplitudes: &'c [Complex64]) -> Self {
280        self.initial_state = Some(amplitudes);
281        self
282    }
283
284    /// Shortcut for [`Simulate::backend`] with [`BackendKind::StatevectorGpu`].
285    #[cfg(feature = "gpu")]
286    #[inline]
287    pub fn gpu(self, context: std::sync::Arc<crate::gpu::GpuContext>) -> Self {
288        self.backend(BackendKind::StatevectorGpu { context })
289    }
290
291    /// Automatic backend selection with GPU acceleration opted in via `context`.
292    ///
293    /// Routes like [`BackendKind::Auto`], but a selected statevector or
294    /// stabilizer block that clears the qubit crossover with VRAM to spare runs
295    /// on the device. Unsupported cases fall back to the identical CPU path.
296    #[cfg(feature = "gpu")]
297    #[inline]
298    pub fn gpu_auto(self, context: std::sync::Arc<crate::gpu::GpuContext>) -> Self {
299        self.backend(BackendKind::AutoGpu { context })
300    }
301
302    /// Distribute the exact state vector across the ranks of `context`.
303    ///
304    /// With a single rank this behaves like [`Simulate::backend`] with
305    /// [`BackendKind::Statevector`].
306    #[cfg(feature = "distributed")]
307    pub fn distributed(
308        self,
309        context: std::sync::Arc<crate::distributed::DistributedContext>,
310    ) -> Self {
311        self.backend(BackendKind::StatevectorDistributed { context })
312    }
313}
314
315impl<'c> Simulate<'c, Unseeded> {
316    /// Query methods exist only on the seeded builder.
317    #[inline]
318    pub fn seed(self, seed: u64) -> Simulate<'c, Seeded> {
319        Simulate {
320            circuit: self.circuit,
321            kind: self.kind,
322            seed: Seeded { seed },
323            noise_model: self.noise_model,
324            initial_state: self.initial_state,
325            require_exact: self.require_exact,
326        }
327    }
328}
329
330impl<'c> Simulate<'c, Seeded> {
331    #[inline]
332    fn seed_value(&self) -> u64 {
333        self.seed.seed
334    }
335
336    /// Trajectory replay reinitializes a pure state per shot and the compiled
337    /// noisy samplers are tableau based, so neither carries a start state.
338    fn require_no_initial_state_under_noise(&self, terminal: &str) -> Result<()> {
339        if self.initial_state.is_some() {
340            return Err(reject_initial_state(
341                &self.kind,
342                terminal,
343                "noisy trajectory replay starts every shot from |0...0>; read the exact mixture \
344                 with `run`, `marginals`, or `expectation_values` on the density-matrix backend",
345            ));
346        }
347        Ok(())
348    }
349
350    /// Execute the circuit once.
351    ///
352    /// With a noise model attached the probabilities are the exact noisy
353    /// distribution rather than one trajectory, so the run needs the
354    /// density-matrix backend; the classical bits are one draw, matching
355    /// `shots(1)`. Readout error reaches the draw and not the state, so a
356    /// model carrying it is rejected rather than answered with two fields from
357    /// different distributions; `shots` and `sample_counts` apply it.
358    #[inline]
359    pub fn run(self) -> Result<RunOutcome> {
360        let seed = self.seed_value();
361        if self.require_exact {
362            reject_approximate_route(&self.kind, self.circuit)?;
363        }
364        if let Some(noise_model) = self.noise_model {
365            require_exact_mixture(&self.kind, "a single run")?;
366            reject_readout_at(self.circuit, noise_model, "a single run")?;
367            let probabilities = exact_noisy_probabilities(
368                &self.kind,
369                self.circuit,
370                noise_model,
371                self.initial_state,
372                seed,
373            )?;
374            let classical_bits =
375                sample_exact_noisy_shots(&probabilities, self.circuit, noise_model, 1, seed)
376                    .swap_remove(0);
377            return Ok(RunOutcome {
378                classical_bits,
379                probabilities: Some(probabilities),
380                metadata: exact_mixture_metadata(&self.kind),
381            });
382        }
383        if let Some(state) = self.initial_state {
384            return run_from_initial_state(
385                &self.kind,
386                self.circuit,
387                state,
388                seed,
389                &SimOptions::default(),
390            );
391        }
392        let outcome = run_with_internal(self.kind, self.circuit, seed, SimOptions::default())?;
393        ensure_exact_result(self.require_exact, &outcome.metadata)?;
394        Ok(outcome)
395    }
396
397    /// Execute `num_shots` times, collecting per-shot classical bits. Accepts
398    /// an attached noise model.
399    #[inline]
400    pub fn shots(self, num_shots: usize) -> Result<ShotsResult> {
401        let seed = self.seed_value();
402        let require_exact = self.require_exact;
403        if require_exact {
404            reject_approximate_route(&self.kind, self.circuit)?;
405        }
406        let result = if let Some(noise_model) = self.noise_model {
407            self.require_no_initial_state_under_noise("shot sampling")?;
408            run_shots_with_noise(self.kind, self.circuit, noise_model, num_shots, seed)?
409        } else if let Some(state) = self.initial_state {
410            shots_from_initial_state(&self.kind, self.circuit, state, num_shots, seed)?
411        } else {
412            run_shots_with(self.kind, self.circuit, num_shots, seed)?
413        };
414        ensure_exact_result(require_exact, &result.metadata)?;
415        Ok(result)
416    }
417
418    /// Sample a frequency histogram over `num_shots` executions. Accepts an
419    /// attached noise model.
420    ///
421    /// Counts may be sampled directly from the output distribution, so seeded
422    /// counts can differ from [`Simulate::shots`] plus [`ShotsResult::counts`]
423    /// while drawing from the identical distribution.
424    #[inline]
425    pub fn sample_counts(self, num_shots: usize) -> Result<CountsResult> {
426        let seed = self.seed_value();
427        if self.require_exact {
428            reject_approximate_route(&self.kind, self.circuit)?;
429        }
430        let (counts, metadata) = if let Some(noise_model) = self.noise_model {
431            self.require_no_initial_state_under_noise("count sampling")?;
432            let shots =
433                run_shots_with_noise(self.kind, self.circuit, noise_model, num_shots, seed)?;
434            (shots.counts(), shots.metadata)
435        } else if let Some(state) = self.initial_state {
436            let shots = shots_from_initial_state(&self.kind, self.circuit, state, num_shots, seed)?;
437            (shots.counts(), shots.metadata)
438        } else {
439            run_counts_with(self.kind, self.circuit, num_shots, seed)?
440        };
441        ensure_exact_result(self.require_exact, &metadata)?;
442        Ok(CountsResult {
443            counts,
444            num_classical_bits: self.circuit.num_classical_bits,
445            metadata,
446        })
447    }
448
449    /// Per-qubit marginal probabilities as `(P(0), P(1))` pairs. Rejects
450    /// backends without probability output, and with a noise model attached
451    /// answers exactly from the mixture, which needs the density-matrix
452    /// backend, and rejects a model carrying readout error, since
453    /// `sample_counts` is the terminal that applies it.
454    #[inline]
455    pub fn marginals(self) -> Result<MarginalsResult> {
456        let seed = self.seed_value();
457        if self.require_exact {
458            reject_approximate_route(&self.kind, self.circuit)?;
459        }
460        if let Some(noise_model) = self.noise_model {
461            require_exact_mixture(&self.kind, "marginals")?;
462            reject_readout_at(self.circuit, noise_model, "marginals")?;
463            let probs = exact_noisy_probabilities(
464                &self.kind,
465                self.circuit,
466                noise_model,
467                self.initial_state,
468                seed,
469            )?;
470            return Ok(MarginalsResult {
471                marginals: probs.marginals(),
472                metadata: exact_mixture_metadata(&self.kind),
473            });
474        }
475        let result = if let Some(state) = self.initial_state {
476            marginals_from_initial_state(&self.kind, self.circuit, state, seed)?
477        } else {
478            run_marginals_result_with(self.kind, self.circuit, seed)?
479        };
480        ensure_exact_result(self.require_exact, &result.metadata)?;
481        Ok(result)
482    }
483
484    /// Compute `⟨ψ|P|ψ⟩` for each joint Pauli observable on the circuit's
485    /// output state, honoring the selected backend.
486    ///
487    /// Each observable is a product of single-qubit Paulis (identity factors
488    /// omitted). The circuit must be unitary. Clifford circuits propagate each
489    /// observable exactly. Non-Clifford circuits use the state vector while they
490    /// fit it; above that cap the selected backend evaluates the observable on
491    /// its own representation, and a backend without one reports
492    /// `BackendUnsupported` naming itself.
493    ///
494    /// With a noise model attached the value is the exact `Tr(rho P)` on the
495    /// evolved mixture, which needs the density-matrix backend. A model
496    /// carrying readout error is rejected: readout acts on the measurement
497    /// record, which no observable sees, and `shots` on the same model would
498    /// disagree by the readout rate.
499    #[inline]
500    pub fn expectation_values(self, observables: &[Vec<PauliTerm>]) -> Result<Vec<f64>> {
501        self.expectation_values_reported(observables)
502            .map(ExpectationResult::into_values)
503    }
504
505    /// [`Simulate::expectation_values`] with the provenance of the run and, for
506    /// a route that estimates rather than evaluates, a standard error per value.
507    pub fn expectation_values_reported(
508        self,
509        observables: &[Vec<PauliTerm>],
510    ) -> Result<ExpectationResult> {
511        let seed = self.seed_value();
512        if self.require_exact {
513            reject_approximate_route(&self.kind, self.circuit)?;
514        }
515        if let Some(noise_model) = self.noise_model {
516            reject_readout_at(self.circuit, noise_model, "expectation values")?;
517        }
518        if let BackendKind::PauliPath { epsilon, max_terms } = self.kind {
519            reject_pauli_path_initial_state(self.initial_state)?;
520            return pauli_path_expectations(
521                self.circuit,
522                self.noise_model,
523                observables,
524                epsilon,
525                max_terms,
526            );
527        }
528        if let Some(noise_model) = self.noise_model {
529            require_exact_mixture(&self.kind, "expectation values")?;
530            require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
531            let values = noise::dm_expectation_values(
532                &self.kind,
533                self.circuit,
534                observables,
535                Some(noise_model),
536                self.initial_state,
537                seed,
538            )?;
539            return Ok(analytic_expectations(
540                values,
541                exact_mixture_metadata(&self.kind),
542            ));
543        }
544        if let Some(state) = self.initial_state {
545            require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
546            return expectation_values_from_initial_state(
547                &self.kind,
548                self.circuit,
549                state,
550                observables,
551                seed,
552            );
553        }
554        let result = run_expectation_values_reported(self.kind, self.circuit, observables, seed)?;
555        ensure_exact_result(self.require_exact, &result.metadata)?;
556        Ok(result)
557    }
558
559    /// Compute `⟨H⟩` and its grouped-measurement variance for a weighted
560    /// Pauli observable on the circuit's output state.
561    ///
562    /// The statevector family evaluates one traversal per qubit-wise-commuting
563    /// group and reports the variance; see [`ObservableExpectation::variance`]
564    /// for what the number means. Every other route, including runs with a
565    /// noise model or start state attached, evaluates term by term through
566    /// [`Simulate::expectation_values`] semantics and reports the weighted
567    /// mean with no variance. A noise model carrying readout error is rejected
568    /// for the same reason as there.
569    pub fn observable_expectation(
570        self,
571        observable: &PauliObservable,
572    ) -> Result<ObservableExpectation> {
573        self.observable_expectation_ref(observable)
574    }
575
576    /// [`Simulate::observable_expectation`] without consuming the builder, so
577    /// [`Simulate::observable_variance`] can evaluate `H` and `H^2` on one
578    /// request.
579    fn observable_expectation_ref(
580        &self,
581        observable: &PauliObservable,
582    ) -> Result<ObservableExpectation> {
583        let seed = self.seed_value();
584        if self.require_exact {
585            reject_approximate_route(&self.kind, self.circuit)?;
586        }
587        if let Some(noise_model) = self.noise_model {
588            reject_readout_at(self.circuit, noise_model, "observable expectation")?;
589        }
590        if let BackendKind::PauliPath { epsilon, max_terms } = self.kind {
591            reject_pauli_path_initial_state(self.initial_state)?;
592            let result = pauli_path_expectations(
593                self.circuit,
594                self.noise_model,
595                &observable_vecs(observable),
596                epsilon,
597                max_terms,
598            )?;
599            let metadata = result.metadata;
600            return Ok(weighted_observable_result(
601                observable,
602                &result.values,
603                None,
604                metadata,
605            ));
606        }
607        if let Some(noise_model) = self.noise_model {
608            require_exact_mixture(&self.kind, "expectation values")?;
609            require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
610            let values = noise::dm_expectation_values(
611                &self.kind,
612                self.circuit,
613                &observable_vecs(observable),
614                Some(noise_model),
615                self.initial_state,
616                seed,
617            )?;
618            return Ok(weighted_observable_result(
619                observable,
620                &values,
621                None,
622                exact_mixture_metadata(&self.kind),
623            ));
624        }
625        if let Some(state) = self.initial_state {
626            require_unitary_circuit(&self.kind, self.circuit, "expectation values require")?;
627            let result = expectation_values_from_initial_state(
628                &self.kind,
629                self.circuit,
630                state,
631                &observable_vecs(observable),
632                seed,
633            )?;
634            return Ok(weighted_observable_result(
635                observable,
636                &result.values,
637                result.std_errors.as_deref(),
638                result.metadata,
639            ));
640        }
641        let result =
642            run_observable_expectation_reported(self.kind.clone(), self.circuit, observable, seed)?;
643        ensure_exact_result(self.require_exact, &result.metadata)?;
644        Ok(result)
645    }
646
647    /// `Var(H) = <H^2> - <H>^2` for a weighted Pauli observable on the
648    /// circuit's output state.
649    ///
650    /// This is the spread of the operator itself, the number a shot-based
651    /// estimate of `<H>` converges on dividing by the shot count. It is not
652    /// [`ObservableExpectation::variance`], which sums per-group variances and
653    /// so drops the covariance between measurement groups.
654    ///
655    /// Evaluates `H` and the square of its traceless part through
656    /// [`Simulate::observable_expectation`], so backend routing, noise, and
657    /// start states behave as they do there. The constant term is held out of
658    /// the square rather than cancelled inside it; see
659    /// [`PauliObservable::split_identity`]. The square carries up to `T^2`
660    /// terms over `H`'s `T`; see [`PauliObservable::square`].
661    pub fn observable_variance(self, observable: &PauliObservable) -> Result<ObservableVariance> {
662        let (offset, traceless) = observable.split_identity();
663        let mean = self.observable_expectation_ref(observable)?;
664        let second = self.observable_expectation_ref(&traceless.square())?;
665        let centered = mean.mean - offset;
666        Ok(ObservableVariance {
667            variance: second.mean - centered * centered,
668            mean: mean.mean,
669            metadata: mean.metadata,
670        })
671    }
672
673    /// Joint probability distribution over `qubits`, `2^k` entries with
674    /// `qubits[0]` in the lowest bit.
675    ///
676    /// The subset generalizes [`Simulate::marginals`], which reports each
677    /// qubit on its own and so cannot show correlation: a Bell pair reads
678    /// `(0.5, 0.5)` twice there and `[0.5, 0, 0, 0.5]` here. Routing follows
679    /// [`Simulate::run`], including the exact mixture a noise model asks for
680    /// and its rejection of readout error, which acts on the measurement
681    /// record rather than on the state.
682    ///
683    /// A backend that exposes no distribution for the circuit reports
684    /// `BackendUnsupported` naming itself.
685    pub fn probabilities_of(self, qubits: &[usize]) -> Result<Vec<f64>> {
686        crate::backend::schmidt::validate_qubit_set(qubits, self.circuit.num_qubits)?;
687        let kind = format!("{:?}", self.kind);
688        let outcome = self.run()?;
689        let probabilities = outcome
690            .probabilities
691            .ok_or(PrismError::BackendUnsupported {
692                backend: kind,
693                operation: "a probability distribution to marginalize".into(),
694            })?;
695        Ok(probabilities.subset_marginal(qubits))
696    }
697
698    /// Full amplitude vector of the circuit's output state, honoring the
699    /// selected backend.
700    ///
701    /// Indexed with qubit 0 in the least significant bit, so `x q[0]` puts the
702    /// amplitude at index 1. The circuit must be unitary, for the reason
703    /// [`Simulate::reduced_density_matrix`] gives.
704    ///
705    /// A noise model declines: a mixture has no single amplitude vector, and
706    /// [`Simulate::reduced_density_matrix`] over the whole register is the
707    /// terminal that answers there. The density-matrix backend declines for
708    /// the same reason whether or not noise is attached.
709    ///
710    /// The vector holds `2^n` amplitudes, so a register past the dense export
711    /// cap reports `IncompatibleBackend` before allocating rather than after.
712    pub fn state_vector(self) -> Result<Vec<Complex64>> {
713        let seed = self.seed_value();
714        let diagnostic = Diagnostic::StateVector;
715        require_unitary_circuit(&self.kind, self.circuit, "a statevector requires")?;
716        if self.require_exact {
717            reject_approximate_route(&self.kind, self.circuit)?;
718        }
719        if self.noise_model.is_some() {
720            return Err(PrismError::IncompatibleBackend {
721                backend: format!("{:?}", self.kind),
722                reason: format!(
723                    "{} is a pure state; a noise model evolves a mixture, which \
724                     `reduced_density_matrix` over the whole register reports",
725                    diagnostic.terminal()
726                ),
727            });
728        }
729        let backend = diagnostic_backend(
730            &self.kind,
731            self.circuit,
732            self.initial_state,
733            seed,
734            diagnostic,
735            self.circuit.num_qubits,
736        )?;
737        ensure_exact_result(self.require_exact, &backend_metadata(&*backend))?;
738        backend.export_statevector()
739    }
740
741    /// Reduced density matrix of `qubits` on the circuit's output state,
742    /// honoring the selected backend.
743    ///
744    /// Row major with side `2^k`; [`ReducedDensityMatrix::data`] states the
745    /// index order. The subsystem is named once and may be the whole register.
746    /// The circuit must be unitary: the answer is read off one state, and a
747    /// measurement, reset or conditional leaves one seeded branch of several.
748    ///
749    /// An explicitly selected backend that holds no partial trace reports
750    /// `BackendUnsupported` naming itself. Under [`BackendKind::Auto`] a route
751    /// that cannot answer falls back to the statevector while the circuit fits
752    /// its cap, so the diagnostic is served rather than declined by a choice
753    /// the caller did not make. With a noise model attached the answer is the
754    /// marginal of the exact mixture, which needs the density-matrix backend.
755    pub fn reduced_density_matrix(self, qubits: &[usize]) -> Result<ReducedDensityMatrix> {
756        let seed = self.seed_value();
757        let diagnostic = Diagnostic::ReducedDensityMatrix;
758        let terminal = diagnostic.terminal();
759        crate::backend::schmidt::validate_qubit_set(qubits, self.circuit.num_qubits)?;
760        require_unitary_circuit(
761            &self.kind,
762            self.circuit,
763            "a reduced density matrix requires",
764        )?;
765        if self.require_exact {
766            reject_approximate_route(&self.kind, self.circuit)?;
767        }
768        if let Some(noise_model) = self.noise_model {
769            reject_readout_at(self.circuit, noise_model, terminal)?;
770            require_exact_mixture(&self.kind, terminal)?;
771            let mut mixture = noise::evolve_density_matrix(
772                &self.kind,
773                self.circuit,
774                Some(noise_model),
775                self.initial_state,
776                seed,
777            )?;
778            return Ok(ReducedDensityMatrix {
779                qubits: qubits.to_vec(),
780                data: mixture.reduced_density_matrix(qubits)?,
781                metadata: exact_mixture_metadata(&self.kind),
782            });
783        }
784        let mut backend = diagnostic_backend(
785            &self.kind,
786            self.circuit,
787            self.initial_state,
788            seed,
789            diagnostic,
790            qubits.len(),
791        )?;
792        let metadata = backend_metadata(&*backend);
793        ensure_exact_result(self.require_exact, &metadata)?;
794        Ok(ReducedDensityMatrix {
795            qubits: qubits.to_vec(),
796            data: backend.reduced_density_matrix(qubits)?,
797            metadata,
798        })
799    }
800
801    /// Entanglement entropy of `subsystem` across its cut with the rest of the
802    /// register, in nats, honoring the selected backend.
803    ///
804    /// `subsystem` must leave both sides of the cut non-empty, and the circuit
805    /// must be unitary, for the reason [`Simulate::reduced_density_matrix`]
806    /// gives. The Schmidt values come back with the entropy, descending and
807    /// normalized.
808    ///
809    /// A backend that holds the entropy without the spectrum behind it, a
810    /// stabilizer cut past the export cap, answers with
811    /// [`EntropyResult::schmidt_values`] set to `None`.
812    /// An explicitly selected backend that holds neither reports
813    /// `BackendUnsupported` naming itself; under [`BackendKind::Auto`] such a
814    /// route falls back to the statevector while the circuit fits its cap. A
815    /// noise model declines outright: it sends the run to the density matrix,
816    /// whose mixed state has no Schmidt decomposition.
817    pub fn entanglement_entropy(self, subsystem: &[usize]) -> Result<EntropyResult> {
818        let seed = self.seed_value();
819        let diagnostic = Diagnostic::Entropy;
820        let terminal = diagnostic.terminal();
821        crate::backend::schmidt::validate_subsystem(subsystem, self.circuit.num_qubits)?;
822        require_unitary_circuit(&self.kind, self.circuit, "entanglement entropy requires")?;
823        if self.require_exact {
824            reject_approximate_route(&self.kind, self.circuit)?;
825        }
826        if let Some(noise_model) = self.noise_model {
827            reject_readout_at(self.circuit, noise_model, terminal)?;
828            require_exact_mixture(&self.kind, terminal)?;
829            let mut mixture = noise::evolve_density_matrix(
830                &self.kind,
831                self.circuit,
832                Some(noise_model),
833                self.initial_state,
834                seed,
835            )?;
836            return Ok(EntropyResult {
837                subsystem: subsystem.to_vec(),
838                entropy: mixture.entanglement_entropy(subsystem)?,
839                schmidt_values: None,
840                metadata: exact_mixture_metadata(&self.kind),
841            });
842        }
843        let mut backend = diagnostic_backend(
844            &self.kind,
845            self.circuit,
846            self.initial_state,
847            seed,
848            diagnostic,
849            subsystem.len(),
850        )?;
851        let metadata = backend_metadata(&*backend);
852        ensure_exact_result(self.require_exact, &metadata)?;
853        // A tableau past the export cap holds the entropy as a rank while its
854        // `2^r` equal values do not fit, so the spectrum declining is not the
855        // terminal declining. A backend holding neither reports the spectrum's
856        // own error, which is what its entropy raises too.
857        let (entropy, schmidt_values) = match backend.schmidt_values(subsystem) {
858            Ok(values) => (
859                crate::backend::schmidt::entropy_of_schmidt_values(&values),
860                Some(values),
861            ),
862            Err(declined) => match backend.entanglement_entropy(subsystem) {
863                Ok(entropy) => (entropy, None),
864                Err(_) => return Err(declined),
865            },
866        };
867        Ok(EntropyResult {
868            subsystem: subsystem.to_vec(),
869            entropy,
870            schmidt_values,
871            metadata,
872        })
873    }
874
875    /// `|<a|b>|^2` between this circuit's output state and `other`'s, honoring
876    /// the backend each side selected.
877    ///
878    /// The two circuits must declare the same width, and both must be unitary
879    /// for the reason [`Simulate::reduced_density_matrix`] gives. Each side
880    /// carries its own backend, seed and start state, and each resolves to a
881    /// single backend rather than the decomposed route, since two circuits
882    /// need not split into the same independent blocks.
883    ///
884    /// A pair of unlike representations is served by a dense export of both
885    /// states, so it reaches as far as the export cap does. A pair that shares
886    /// one answers natively at any width: two chains in the same site order,
887    /// two tableaux, two product states, or two sparse maps. A noise model on
888    /// either side is rejected, since the fidelity of two mixtures is not an
889    /// inner product.
890    pub fn overlap(self, other: Simulate<'_, Seeded>) -> Result<OverlapResult> {
891        let diagnostic = Diagnostic::Overlap;
892        if self.circuit.num_qubits != other.circuit.num_qubits {
893            return Err(PrismError::InvalidParameter {
894                message: format!(
895                    "{} needs two circuits of the same width; got {} and {} qubits",
896                    diagnostic.terminal(),
897                    self.circuit.num_qubits,
898                    other.circuit.num_qubits
899                ),
900            });
901        }
902        let (left_backend, left) = self.overlap_side(diagnostic)?;
903        let (right_backend, right) = other.overlap_side(diagnostic)?;
904        Ok(OverlapResult {
905            fidelity: left_backend.overlap_sq(&*right_backend)?,
906            left,
907            right,
908        })
909    }
910
911    /// One side of [`Simulate::overlap`]: the guards both sides answer to,
912    /// then the backend this side's kind resolves to, run and handed back with
913    /// its provenance.
914    fn overlap_side(self, diagnostic: Diagnostic) -> Result<(Box<dyn Backend>, RunMetadata)> {
915        let seed = self.seed_value();
916        let terminal = diagnostic.terminal();
917        require_unitary_circuit(&self.kind, self.circuit, "a state overlap requires")?;
918        if self.require_exact {
919            reject_approximate_route(&self.kind, self.circuit)?;
920        }
921        if self.noise_model.is_some() {
922            return Err(PrismError::IncompatibleBackend {
923                backend: format!("{:?}", self.kind),
924                reason: format!(
925                    "{terminal} is an inner product of two pure states, and a noise model \
926                     evolves a mixture, whose fidelity is a different computation; drop \
927                     the model, or compare the mixtures through `reduced_density_matrix`"
928                ),
929            });
930        }
931        let backend = diagnostic_backend(
932            &self.kind,
933            self.circuit,
934            self.initial_state,
935            seed,
936            diagnostic,
937            0,
938        )?;
939        let metadata = backend_metadata(&*backend);
940        ensure_exact_result(self.require_exact, &metadata)?;
941        Ok((backend, metadata))
942    }
943
944    /// Compute `⟨H⟩` and its exact gradient with respect to the bound
945    /// parameters using the adjoint method.
946    ///
947    /// `hamiltonian` is a weighted Pauli sum `Σ c_k P_k` with real
948    /// coefficients. `params` declares which gate instructions carry parameters.
949    /// Runs on the statevector backend; the selected backend must be `Auto` or
950    /// `Statevector`. The circuit must be unitary. See
951    /// [`gradient::run_expectation_gradient`].
952    #[inline]
953    pub fn expectation_gradient(
954        self,
955        hamiltonian: &[(f64, Vec<PauliTerm>)],
956        params: &crate::circuit::Parameters,
957    ) -> Result<gradient::ExpectationGradient> {
958        let seed = self.seed_value();
959        if self.require_exact {
960            reject_approximate_route(&self.kind, self.circuit)?;
961        }
962        if self.noise_model.is_some() {
963            return Err(PrismError::IncompatibleBackend {
964                backend: format!("{:?}", self.kind),
965                reason: "the adjoint method backpropagates through a pure state, so no backend \
966                         has a noisy adjoint path; drop the noise model, or take \
967                         `expectation_gradient_shift` on the density-matrix backend"
968                    .into(),
969            });
970        }
971        if self.initial_state.is_some() {
972            return Err(reject_initial_state(
973                &self.kind,
974                "the adjoint gradient",
975                "the backward pass reconstructs the input register by inverting the circuit from \
976                 |0...0>, so a start state would have to be inverted with it",
977            ));
978        }
979        if !(self.kind.is_auto() || matches!(self.kind, BackendKind::Statevector)) {
980            return Err(PrismError::IncompatibleBackend {
981                backend: format!("{:?}", self.kind),
982                reason:
983                    "adjoint gradients run on the statevector backend; select Auto or Statevector"
984                        .into(),
985            });
986        }
987        gradient::run_expectation_gradient(self.circuit, hamiltonian, params, seed)
988    }
989
990    /// Compute `⟨H⟩` and its gradient by the parameter-shift rule on the
991    /// selected backend.
992    ///
993    /// Serves the cases [`Simulate::expectation_gradient`] declines: any
994    /// backend with a native observable path, circuits containing `QftBlock`,
995    /// widths past the statevector cap, and a noise model. It differentiates the
996    /// same gate set (`Rx`, `Ry`, `Rz`, `Rzz`, `P`, `PauliRot`) at `1 + 2 * links`
997    /// circuit evaluations against the adjoint's one, so the adjoint stays the
998    /// better choice where it applies. A backend with no native observable path
999    /// reports `BackendUnsupported` naming itself. Under a noise model every
1000    /// evaluation reads the exact mixture, so the backend must be
1001    /// [`BackendKind::DensityMatrix`] or its device sibling; the shift stays
1002    /// exact because the channels do not depend on the shifted angle. See
1003    /// [`gradient::run_expectation_gradient_shift`].
1004    #[inline]
1005    pub fn expectation_gradient_shift(
1006        self,
1007        hamiltonian: &[(f64, Vec<PauliTerm>)],
1008        params: &crate::circuit::Parameters,
1009    ) -> Result<gradient::ExpectationGradient> {
1010        let seed = self.seed_value();
1011        if self.require_exact {
1012            reject_approximate_route(&self.kind, self.circuit)?;
1013        }
1014        if self.noise_model.is_some()
1015            && !self.kind.is_density_matrix()
1016            && !matches!(self.kind, BackendKind::PauliPath { .. })
1017        {
1018            return Err(PrismError::IncompatibleBackend {
1019                backend: format!("{:?}", self.kind),
1020                reason: "a parameter-shift gradient under a noise model evaluates the exact \
1021                         mixed state, which only the density-matrix backend holds; select \
1022                         `BackendKind::DensityMatrix` or its device sibling, or drop the noise \
1023                         model"
1024                    .into(),
1025            });
1026        }
1027        gradient::shift_gradient(
1028            &self.kind,
1029            self.circuit,
1030            hamiltonian,
1031            params,
1032            self.noise_model,
1033            self.initial_state,
1034            seed,
1035        )
1036    }
1037}
1038
1039/// Start a query-aware simulation request for `circuit`.
1040///
1041/// The returned builder defaults to automatic backend selection; chain
1042/// [`Simulate::seed`] to unlock the query methods.
1043///
1044/// # Examples
1045///
1046/// ```
1047/// use prism_q::{Circuit, Gate, simulate};
1048///
1049/// let mut circuit = Circuit::new(2, 0);
1050/// circuit.add_gate(Gate::H, &[0]);
1051/// circuit.add_gate(Gate::Cx, &[0, 1]);
1052///
1053/// let result = simulate(&circuit).seed(42).run()?;
1054/// let probs = result.probabilities.expect("no probabilities").to_vec();
1055/// // Bell state: ~50% |00>, ~50% |11>
1056/// assert!((probs[0] - 0.5).abs() < 1e-10);
1057/// assert!((probs[3] - 0.5).abs() < 1e-10);
1058/// # Ok::<(), prism_q::PrismError>(())
1059/// ```
1060#[inline]
1061pub fn simulate(circuit: &Circuit) -> Simulate<'_, Unseeded> {
1062    Simulate {
1063        circuit,
1064        kind: BackendKind::Auto,
1065        seed: Unseeded,
1066        noise_model: None,
1067        initial_state: None,
1068        require_exact: false,
1069    }
1070}
1071
1072/// Gate for the terminals that answer a noise model from the exact mixture,
1073/// which only the density matrix holds.
1074/// Rejection naming the two terminals the Pauli path engine serves.
1075fn reject_pauli_path(terminal: &str) -> PrismError {
1076    PrismError::IncompatibleBackend {
1077        backend: "PauliPath".into(),
1078        reason: format!(
1079            "{terminal} is not served by Pauli path propagation, which answers \
1080             `expectation_values` and `observable_expectation` only"
1081        ),
1082    }
1083}
1084
1085fn reject_pauli_path_initial_state(state: Option<&[Complex64]>) -> Result<()> {
1086    match state {
1087        Some(_) => Err(reject_pauli_path("a start state")),
1088        None => Ok(()),
1089    }
1090}
1091
1092/// One Pauli path evaluation per observable, under `noise` when one is
1093/// attached and on the bare circuit otherwise.
1094///
1095/// The route is exact only when nothing was truncated, so exactness is decided
1096/// by the discarded mass the run reports rather than by the parameters it was
1097/// given: a budget that never binds still returns an exact value.
1098fn pauli_path_expectations(
1099    circuit: &Circuit,
1100    noise: Option<&NoiseModel>,
1101    observables: &[Vec<PauliTerm>],
1102    epsilon: f64,
1103    max_terms: usize,
1104) -> Result<ExpectationResult> {
1105    let empty;
1106    let noise = match noise {
1107        Some(model) => model,
1108        None => {
1109            empty = NoiseModel {
1110                after_gate: vec![Vec::new(); circuit.instructions.len()],
1111                readout: vec![None; circuit.num_classical_bits],
1112            };
1113            &empty
1114        }
1115    };
1116    let mut values = Vec::with_capacity(observables.len());
1117    let mut discarded = 0.0f64;
1118    for obs in observables {
1119        let result =
1120            unified_pauli::run_pauli_path_observable(circuit, noise, obs, epsilon, max_terms)?;
1121        discarded = discarded.max(result.total_discarded);
1122        values.push(result.mean);
1123    }
1124    let metadata = if discarded > 0.0 {
1125        RunMetadata::approximate(ResolvedBackend::PauliPath)
1126    } else {
1127        RunMetadata::exact(ResolvedBackend::PauliPath)
1128    };
1129    Ok(analytic_expectations(values, metadata))
1130}
1131
1132fn require_exact_mixture(kind: &BackendKind, terminal: &str) -> Result<()> {
1133    if kind.is_density_matrix() {
1134        return Ok(());
1135    }
1136    Err(PrismError::IncompatibleBackend {
1137        backend: format!("{kind:?}"),
1138        reason: format!(
1139            "{terminal} under a noise model reads the exact mixed state, which only the \
1140             density-matrix backend holds; select it, or average trajectories through `shots` \
1141             or `sample_counts`"
1142        ),
1143    })
1144}
1145
1146/// Gate for the terminals that answer from the state, mixed or pure, which
1147/// readout error is not part of.
1148///
1149/// The condition is the one under which a draw would actually differ. Entries
1150/// past the circuit's bit count never apply, matching what
1151/// `sample_exact_noisy_shots` consumes, so a model built for a wider circuit is
1152/// not rejected on bits this one does not have, and a zero-rate entry flips
1153/// nothing, so a sweep that starts at zero is not rejected at its first point.
1154fn reject_readout_at(
1155    circuit: &Circuit,
1156    noise_model: &noise::NoiseModel,
1157    terminal: &str,
1158) -> Result<()> {
1159    let inert = noise_model
1160        .readout
1161        .iter()
1162        .take(circuit.num_classical_bits)
1163        .all(|entry| entry.as_ref().is_none_or(|readout| readout.is_inert()));
1164    if inert {
1165        return Ok(());
1166    }
1167    Err(PrismError::InvalidParameter {
1168        message: format!(
1169            "{terminal} answers from the mixed state, which readout error is not part of: it \
1170             acts on the measurement record and is indexed by classical bit, not qubit. Drop \
1171             it from the model, or use `shots` or `sample_counts`, which apply it"
1172        ),
1173    })
1174}
1175
1176/// Gate for the terminals that cannot carry a start state through their own
1177/// machinery.
1178fn reject_initial_state(kind: &BackendKind, terminal: &str, instead: &str) -> PrismError {
1179    PrismError::IncompatibleBackend {
1180        backend: format!("{kind:?}"),
1181        reason: format!("{terminal} does not accept a start state; {instead}"),
1182    }
1183}
1184
1185/// Reject a start state whose width disagrees with the circuit's.
1186///
1187/// The circuit's declared register wins: the amplitude vector sets the backend's
1188/// width, so a shorter or longer one would silently simulate a different
1189/// register than the one the instructions index. A register too wide to index
1190/// with a `usize` has no dense start state at all.
1191fn check_initial_state_len(state: &[Complex64], num_qubits: usize) -> Result<()> {
1192    let want = (num_qubits < usize::BITS as usize).then(|| 1usize << num_qubits);
1193    if want == Some(state.len()) {
1194        return Ok(());
1195    }
1196    let needs = match want {
1197        Some(count) => count.to_string(),
1198        None => format!("2^{num_qubits}"),
1199    };
1200    Err(PrismError::InvalidParameter {
1201        message: format!(
1202            "start state has {} amplitudes, but a {num_qubits}-qubit circuit needs {needs}",
1203            state.len()
1204        ),
1205    })
1206}
1207
1208/// Build the constrained backend for a start state and load it.
1209fn backend_from_initial_state(
1210    kind: &BackendKind,
1211    circuit: &Circuit,
1212    state: &[Complex64],
1213    seed: u64,
1214) -> Result<Box<dyn Backend>> {
1215    if !kind.is_auto() {
1216        validate_explicit_backend(kind, circuit)?;
1217    }
1218    check_initial_state_len(state, circuit.num_qubits)?;
1219    let mut backend = initial_state_plan(kind, circuit.num_qubits)?.build(seed);
1220    backend.init_from_amplitudes(state.to_vec(), circuit.num_classical_bits)?;
1221    Ok(backend)
1222}
1223
1224/// Expand the gate forms `backend` has no native kernel for, `QftBlock` and
1225/// `PauliRot`, leaving the stream borrowed when both probes accept it.
1226fn expand_for_backend<'c>(
1227    backend: &dyn Backend,
1228    circuit: &'c Circuit,
1229) -> std::borrow::Cow<'c, Circuit> {
1230    use std::borrow::Cow;
1231    let expanded = if backend.supports_qft_block() {
1232        Cow::Borrowed(circuit)
1233    } else {
1234        crate::circuit::expand_qft_blocks(circuit)
1235    };
1236    if backend.supports_pauli_rotation() {
1237        return expanded;
1238    }
1239    match expanded {
1240        Cow::Borrowed(borrowed) => crate::circuit::expand_pauli_rotations(borrowed),
1241        Cow::Owned(owned) => {
1242            let rotations = crate::circuit::expand_pauli_rotations(&owned);
1243            if let Cow::Owned(expanded_rotations) = rotations {
1244                return Cow::Owned(expanded_rotations);
1245            }
1246            Cow::Owned(owned)
1247        }
1248    }
1249}
1250
1251/// Fuse `circuit` against what `backend` accepts and how wide a buffer it
1252/// sweeps, the two backend facts the pass pipeline is gated on.
1253fn fuse_for_backend<'a>(
1254    backend: &dyn Backend,
1255    circuit: &'a Circuit,
1256) -> std::borrow::Cow<'a, Circuit> {
1257    crate::circuit::fusion::fuse_circuit_for_width(
1258        circuit,
1259        backend.supports_fused_gates(),
1260        backend.fusion_state_qubits(circuit.num_qubits),
1261    )
1262}
1263
1264/// Fuse `circuit` for `backend` and apply it, leaving initialization to the
1265/// caller. The start-state analogue of [`execute`], which owns the |0...0⟩ init.
1266fn apply_fused_circuit(backend: &mut dyn Backend, circuit: &Circuit) -> Result<()> {
1267    let expanded = expand_for_backend(&*backend, circuit);
1268    let fused = fuse_for_backend(&*backend, &expanded);
1269    backend.apply_instructions(&fused.instructions)
1270}
1271
1272fn run_from_initial_state(
1273    kind: &BackendKind,
1274    circuit: &Circuit,
1275    state: &[Complex64],
1276    seed: u64,
1277    opts: &SimOptions,
1278) -> Result<RunOutcome> {
1279    let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
1280    apply_fused_circuit(&mut *backend, circuit)?;
1281
1282    let probabilities = if opts.probabilities {
1283        try_backend_probabilities(&*backend)?
1284    } else {
1285        None
1286    };
1287    Ok(RunOutcome {
1288        classical_bits: backend.classical_results().to_vec(),
1289        probabilities,
1290        metadata: backend_metadata(&*backend),
1291    })
1292}
1293
1294/// Shots for a start state. Terminal measurements sample one evolved
1295/// distribution; anything else replays the start state per shot, which is what
1296/// mid-circuit collapse and classical feedback need.
1297fn shots_from_initial_state(
1298    kind: &BackendKind,
1299    circuit: &Circuit,
1300    state: &[Complex64],
1301    num_shots: usize,
1302    seed: u64,
1303) -> Result<ShotsResult> {
1304    let bits = circuit.num_classical_bits;
1305    if circuit.has_terminal_measurements_only() {
1306        let stripped = circuit.without_measurements();
1307        let outcome = run_from_initial_state(kind, &stripped, state, seed, &SimOptions::default())?;
1308        if let Some(probs) = outcome.probabilities {
1309            let meas_map = circuit.measurement_map();
1310            return Ok(ShotsResult::from_shots(
1311                sample_shots(&probs, &meas_map, bits, num_shots, seed),
1312                bits,
1313            )
1314            .with_metadata(outcome.metadata));
1315        }
1316    }
1317
1318    // Plan, expansion, and fusion are shot independent, so they are hoisted out
1319    // of the replay loop, matching `run_shots_per_shot`.
1320    if !kind.is_auto() {
1321        validate_explicit_backend(kind, circuit)?;
1322    }
1323    check_initial_state_len(state, circuit.num_qubits)?;
1324    let plan = initial_state_plan(kind, circuit.num_qubits)?;
1325    let probe = plan.build(seed);
1326    let expanded = expand_for_backend(&*probe, circuit);
1327    let fused = fuse_for_backend(&*probe, &expanded);
1328
1329    collect_shots(circuit, num_shots, seed, plan.resolved(), |shot_seed| {
1330        let mut backend = plan.build(shot_seed);
1331        backend.init_from_amplitudes(state.to_vec(), circuit.num_classical_bits)?;
1332        backend.apply_instructions(&fused.instructions)?;
1333        Ok((
1334            backend.classical_results().to_vec(),
1335            backend_metadata(&*backend),
1336        ))
1337    })
1338}
1339
1340fn marginals_from_initial_state(
1341    kind: &BackendKind,
1342    circuit: &Circuit,
1343    state: &[Complex64],
1344    seed: u64,
1345) -> Result<MarginalsResult> {
1346    let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
1347    apply_fused_circuit(&mut *backend, circuit)?;
1348    if backend.supports_pauli_expectation() {
1349        return marginals_from_pauli_expectations(&*backend, circuit.num_qubits);
1350    }
1351    Ok(MarginalsResult {
1352        marginals: Probabilities::Dense(backend.probabilities()?).marginals(),
1353        metadata: backend_metadata(&*backend),
1354    })
1355}
1356
1357fn expectation_values_from_initial_state(
1358    kind: &BackendKind,
1359    circuit: &Circuit,
1360    state: &[Complex64],
1361    observables: &[Vec<PauliTerm>],
1362    seed: u64,
1363) -> Result<ExpectationResult> {
1364    // Before the run, matching the dense statevector path.
1365    let masks = observables
1366        .iter()
1367        .map(|obs| pauli_masks(obs, circuit.num_qubits))
1368        .collect::<Result<Vec<_>>>()?;
1369
1370    let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
1371    apply_fused_circuit(&mut *backend, circuit)?;
1372    let metadata = backend_metadata(&*backend);
1373    if backend.supports_pauli_expectation() {
1374        let values = backend.pauli_expectations(observables)?;
1375        return Ok(analytic_expectations(values, metadata));
1376    }
1377
1378    let evolved = backend.export_statevector()?;
1379    let norm = crate::backend::state_norm_sqr(&evolved);
1380    let values = pauli_expectations_from_masks(&evolved, &masks, norm);
1381    Ok(analytic_expectations(values, metadata))
1382}
1383
1384/// Reject a result that turned out approximate, for a caller that opted out.
1385///
1386/// The backstop behind [`reject_approximate_route`], which decides from the
1387/// circuit alone. Sparse Pauli dynamics truncates on coefficient magnitudes it
1388/// only learns while propagating, so whether it stayed exact is not knowable
1389/// before the run.
1390fn ensure_exact_result(require_exact: bool, metadata: &RunMetadata) -> Result<()> {
1391    if require_exact && !metadata.is_exact() {
1392        return Err(PrismError::IncompatibleBackend {
1393            backend: format!("{:?}", metadata.backend),
1394            reason: "require_exact rejects an approximate result; this engine discarded state                      weight while running, which the route could not predict"
1395                .into(),
1396        });
1397    }
1398    Ok(())
1399}
1400
1401/// Reject an approximate route for a caller that opted out of one. The engine
1402/// is named in the error, since a caller who asked for exactness wants to know
1403/// which one would have answered.
1404fn reject_approximate_route(kind: &BackendKind, circuit: &Circuit) -> Result<()> {
1405    match approximate_route_name(kind, circuit) {
1406        Some(engine) => Err(PrismError::IncompatibleBackend {
1407            backend: engine.into(),
1408            reason: "require_exact rejects a route that can discard state weight; drop the \
1409                     requirement to accept the approximation, which the result reports, or \
1410                     select a backend that represents this circuit exactly"
1411                .into(),
1412        }),
1413        None => Ok(()),
1414    }
1415}
1416
1417/// `subject` names what needs the unitary circuit with its verb, so the
1418/// sentence reads for a plural terminal ("expectation values require") and for
1419/// a singular one ("a reduced density matrix requires").
1420fn require_unitary_circuit(kind: &BackendKind, circuit: &Circuit, subject: &str) -> Result<()> {
1421    if has_nonunitary_or_classical_ops(circuit) {
1422        return Err(PrismError::IncompatibleBackend {
1423            backend: format!("{kind:?}"),
1424            reason: format!(
1425                "{subject} a unitary circuit without measurements, resets, or conditionals"
1426            ),
1427        });
1428    }
1429    Ok(())
1430}
1431
1432/// Provenance of a result read off the exact mixture: the density matrix,
1433/// placed wherever `kind` holds it.
1434fn exact_mixture_metadata(kind: &BackendKind) -> RunMetadata {
1435    let metadata = RunMetadata::exact(ResolvedBackend::DensityMatrix);
1436    #[cfg(feature = "gpu")]
1437    if matches!(kind, BackendKind::DensityMatrixGpu { .. }) {
1438        let mut on_device = metadata;
1439        on_device.placement = Placement::Device;
1440        return on_device;
1441    }
1442    #[cfg(not(feature = "gpu"))]
1443    let _ = kind;
1444    metadata
1445}
1446
1447/// Exact output distribution of `circuit` under `noise_model`, evolved once on
1448/// the density matrix. The mixture carries every measurement branch at once,
1449/// so it answers for a whole shot only when the measurements are terminal.
1450fn exact_noisy_probabilities(
1451    kind: &BackendKind,
1452    circuit: &Circuit,
1453    noise_model: &noise::NoiseModel,
1454    initial_state: Option<&[Complex64]>,
1455    seed: u64,
1456) -> Result<Probabilities> {
1457    Ok(Probabilities::Dense(noise::density_matrix_probabilities(
1458        kind,
1459        circuit,
1460        noise_model,
1461        initial_state,
1462        seed,
1463    )?))
1464}
1465
1466/// Draw classical bits from the exact noisy distribution, then apply readout
1467/// error. Readout acts on the outcome rather than on the state, so it is not
1468/// carried by the distribution and has to be applied per draw, on a stream of
1469/// its own so it does not track the state draws.
1470fn sample_exact_noisy_shots(
1471    probs: &Probabilities,
1472    circuit: &Circuit,
1473    noise_model: &noise::NoiseModel,
1474    num_shots: usize,
1475    seed: u64,
1476) -> Vec<Vec<bool>> {
1477    let bits = circuit.num_classical_bits;
1478    let mut shots = sample_shots(probs, &circuit.measurement_map(), bits, num_shots, seed);
1479    if noise_model.readout.iter().any(Option::is_some) {
1480        let readout = trajectory::written_readout(circuit, &noise_model.readout);
1481        let mut rng = trajectory::noise_rng(seed);
1482        for shot in &mut shots {
1483            trajectory::apply_readout_errors(shot, &readout, &mut rng);
1484        }
1485    }
1486    shots
1487}
1488
1489#[inline]
1490fn probs_only_result(probs: Vec<f64>, metadata: RunMetadata) -> RunOutcome {
1491    RunOutcome {
1492        probabilities: Some(Probabilities::Dense(probs)),
1493        classical_bits: vec![],
1494        metadata,
1495    }
1496}
1497
1498fn try_backend_probabilities(backend: &dyn Backend) -> Result<Option<Probabilities>> {
1499    if let Some(factored) = backend.block_probabilities() {
1500        return Ok(Some(factored));
1501    }
1502    match backend.probabilities() {
1503        Ok(probs) => Ok(Some(Probabilities::Dense(probs))),
1504        Err(PrismError::BackendUnsupported { .. }) => Ok(None),
1505        Err(err) => Err(err),
1506    }
1507}
1508
1509/// Core execution: fuse, init, apply, extract.
1510fn execute(backend: &mut dyn Backend, circuit: &Circuit, opts: &SimOptions) -> Result<RunOutcome> {
1511    let expanded = expand_for_backend(&*backend, circuit);
1512    let fused = fuse_for_backend(&*backend, &expanded);
1513    execute_circuit(backend, &fused, opts)
1514}
1515
1516/// The execution route a template settles on, for a caller that holds one
1517/// circuit across many bindings and fuses it itself.
1518///
1519/// Resolving the route from the template rather than from a bound circuit
1520/// matters twice over: it is the work being amortized, and a fused stream
1521/// misreports the circuit to dispatch (a fused Clifford circuit no longer
1522/// looks Clifford).
1523pub(crate) struct PreparedRoute {
1524    plan: BackendPlan,
1525    supports_fused: bool,
1526    /// Backend held across points. `init` reuses its state buffer when the
1527    /// width matches, so a sweep pays one `2^n` allocation rather than one per
1528    /// point. Rebuilt when the seed changes, since the seed feeds its RNG.
1529    held: Option<(u64, Box<dyn Backend + Send>)>,
1530}
1531
1532impl PreparedRoute {
1533    /// True when the chosen backend accepts fused gates, so the caller should
1534    /// hand `run` a fused stream rather than the bound template.
1535    pub(crate) fn supports_fused(&self) -> bool {
1536        self.supports_fused
1537    }
1538
1539    /// Apply `circuit` verbatim, with no further fusion.
1540    pub(crate) fn run(&mut self, circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
1541        if !matches!(&self.held, Some((s, _)) if *s == seed) {
1542            self.held = Some((seed, self.plan.build(seed)));
1543        }
1544        let (_, backend) = self.held.as_mut().expect("just built");
1545        execute_circuit(&mut **backend, circuit, &SimOptions::default())
1546    }
1547}
1548
1549/// Settle the route for `template`, or `None` when it takes one that reshapes
1550/// execution (decomposition, stabilizer rank, temporal Clifford) or expands
1551/// `QftBlock`, where a caller-supplied stream has nowhere to go.
1552pub(crate) fn prepared_route(kind: &BackendKind, template: &Circuit) -> Option<PreparedRoute> {
1553    if !kind.is_auto() && validate_explicit_backend(kind, template).is_err() {
1554        return None;
1555    }
1556    let ProbabilityRoute::Direct {
1557        has_partial_independence,
1558    } = plan_probability_route(kind, template)
1559    else {
1560        return None;
1561    };
1562    let ExecutionPlan::Backend(plan) = resolve(kind, template, has_partial_independence) else {
1563        return None;
1564    };
1565    let probe = plan.build(0);
1566    let has_qft_block = crate::circuit::any_gate(&template.instructions, &mut |gate| {
1567        matches!(gate, crate::gates::Gate::QftBlock { .. })
1568    });
1569    if has_qft_block && !probe.supports_qft_block() {
1570        return None;
1571    }
1572    let has_pauli_rot = crate::circuit::any_gate(&template.instructions, &mut |gate| {
1573        matches!(gate, crate::gates::Gate::PauliRot(_))
1574    });
1575    if has_pauli_rot && !probe.supports_pauli_rotation() {
1576        return None;
1577    }
1578    Some(PreparedRoute {
1579        supports_fused: probe.supports_fused_gates(),
1580        plan,
1581        held: None,
1582    })
1583}
1584
1585/// Shared init → apply → extract logic.
1586fn execute_circuit(
1587    backend: &mut dyn Backend,
1588    circuit: &Circuit,
1589    opts: &SimOptions,
1590) -> Result<RunOutcome> {
1591    backend.init(circuit.num_qubits, circuit.num_classical_bits)?;
1592    backend.apply_instructions(&circuit.instructions)?;
1593
1594    let probabilities = if opts.probabilities {
1595        try_backend_probabilities(backend)?
1596    } else {
1597        None
1598    };
1599
1600    Ok(RunOutcome {
1601        classical_bits: backend.classical_results().to_vec(),
1602        probabilities,
1603        metadata: backend_metadata(backend),
1604    })
1605}
1606
1607/// Provenance read off the engine that ran, after it ran. Exactness and
1608/// placement are reports rather than predictions: the MPS bound reflects what
1609/// this run discarded, and the placement reflects where the amplitudes ended up
1610/// after any device fallback.
1611pub(crate) fn backend_metadata(backend: &dyn Backend) -> RunMetadata {
1612    RunMetadata::new(backend.resolved(), backend.exactness(), backend.placement())
1613}
1614
1615#[cfg(test)]
1616fn run(circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
1617    run_with(BackendKind::Auto, circuit, seed)
1618}
1619
1620/// Constructs the backend internally based on [`BackendKind`], then runs
1621/// the circuit. For a pre-constructed backend instance, use [`run_on`].
1622pub(crate) fn run_with(kind: BackendKind, circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
1623    run_with_internal(kind, circuit, seed, SimOptions::default())
1624}
1625
1626fn run_with_internal(
1627    kind: BackendKind,
1628    circuit: &Circuit,
1629    seed: u64,
1630    opts: SimOptions,
1631) -> Result<RunOutcome> {
1632    if !kind.is_auto() {
1633        validate_explicit_backend(&kind, circuit)?;
1634    }
1635    // The distributed backend runs the whole circuit across ranks in lockstep.
1636    // Subsystem decomposition, Clifford+T, and temporal-Clifford shortcuts all
1637    // reshape execution per sub-block, which would desynchronize the collective
1638    // calls every rank must issue in the same order. Dispatch directly.
1639    #[cfg(feature = "distributed")]
1640    if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
1641        let mut backend = resolve_backend(&kind, circuit, false).build(seed);
1642        if opts.probabilities {
1643            // The gather cap follows from the register width alone, so a
1644            // register past it is rejected here rather than after the run has
1645            // been paid for. Same check `probabilities()` applies, so the
1646            // message does not depend on where it surfaced.
1647            crate::backend::dense_probability_len(backend.name(), circuit.num_qubits)?;
1648        }
1649        return execute(&mut *backend, circuit, &opts);
1650    }
1651    let route = plan_probability_route(&kind, circuit);
1652    run_route(&kind, circuit, seed, opts, &route)
1653}
1654
1655/// Execute one seed of a planned probability route. Shot loops plan once and
1656/// call this per seed.
1657fn run_route(
1658    kind: &BackendKind,
1659    circuit: &Circuit,
1660    seed: u64,
1661    opts: SimOptions,
1662    route: &ProbabilityRoute,
1663) -> Result<RunOutcome> {
1664    match route {
1665        ProbabilityRoute::FactoredStabilizer => {
1666            let mut backend =
1667                crate::backend::factored_stabilizer::FactoredStabilizerBackend::new(seed);
1668            let fs_opts = if circuit.num_qubits > 64 {
1669                SimOptions {
1670                    probabilities: false,
1671                }
1672            } else {
1673                opts
1674            };
1675            execute(&mut backend, circuit, &fs_opts)
1676        }
1677        ProbabilityRoute::Decomposed(components) => {
1678            run_decomposed(kind, components, circuit, seed, &opts)
1679        }
1680        ProbabilityRoute::StabilizerRank => {
1681            let sr = stabilizer_rank::run_stabilizer_rank(circuit, seed)?;
1682            let metadata = RunMetadata::exact(ResolvedBackend::StabilizerRank);
1683            Ok(probs_only_result(sr.probabilities, metadata))
1684        }
1685        ProbabilityRoute::TemporalClifford {
1686            has_partial_independence,
1687        } => match plan_temporal_clifford(kind, circuit) {
1688            Some(tc) => run_temporal_clifford(&tc, seed, opts.probabilities),
1689            None => run_direct(kind, circuit, seed, opts, *has_partial_independence),
1690        },
1691        ProbabilityRoute::Direct {
1692            has_partial_independence,
1693        } => run_direct(kind, circuit, seed, opts, *has_partial_independence),
1694    }
1695}
1696
1697fn run_direct(
1698    kind: &BackendKind,
1699    circuit: &Circuit,
1700    seed: u64,
1701    opts: SimOptions,
1702    has_partial_independence: bool,
1703) -> Result<RunOutcome> {
1704    match resolve(kind, circuit, has_partial_independence) {
1705            ExecutionPlan::Backend(plan) => {
1706                let mut backend = plan.build(seed);
1707                execute(&mut *backend, circuit, &opts)
1708            }
1709            ExecutionPlan::StabilizerRank => {
1710                let sr = stabilizer_rank::run_stabilizer_rank(circuit, seed)?;
1711                Ok(probs_only_result(
1712                    sr.probabilities,
1713                    RunMetadata::exact(ResolvedBackend::StabilizerRank),
1714                ))
1715            }
1716            ExecutionPlan::StochasticPauli { num_samples } => {
1717                Err(crate::error::PrismError::IncompatibleBackend {
1718                    backend: format!(
1719                        "{:?}",
1720                        BackendKind::StochasticPauli { num_samples }
1721                    ),
1722                    reason: "StochasticPauli produces marginal estimates only; use `simulate(...).marginals()`".into(),
1723                })
1724            }
1725            ExecutionPlan::DeterministicPauli { epsilon, max_terms } => {
1726                Err(crate::error::PrismError::IncompatibleBackend {
1727                    backend: format!(
1728                        "{:?}",
1729                        BackendKind::DeterministicPauli { epsilon, max_terms }
1730                    ),
1731                    reason: "DeterministicPauli produces marginals only; use `simulate(...).marginals()`".into(),
1732                })
1733            }
1734            ExecutionPlan::PauliPath => Err(reject_pauli_path("a single run")),
1735    }
1736}
1737
1738/// Execute a circuit on a pre-constructed backend. For automatic dispatch,
1739/// use [`simulate`].
1740pub fn run_on(backend: &mut dyn Backend, circuit: &Circuit) -> Result<RunOutcome> {
1741    execute(backend, circuit, &SimOptions::default())
1742}
1743
1744/// Execute a circuit on a pre-constructed backend from a start state other than
1745/// |0...0⟩, the [`run_on`] sibling for a caller holding its own amplitudes.
1746///
1747/// The backend must accept one; see [`Backend::init_from_amplitudes`] for the
1748/// validation applied to `initial_state` and for which backends decline it.
1749pub fn run_on_state(
1750    backend: &mut dyn Backend,
1751    circuit: &Circuit,
1752    initial_state: &[Complex64],
1753) -> Result<RunOutcome> {
1754    check_initial_state_len(initial_state, circuit.num_qubits)?;
1755    backend.init_from_amplitudes(initial_state.to_vec(), circuit.num_classical_bits)?;
1756    apply_fused_circuit(backend, circuit)?;
1757    Ok(RunOutcome {
1758        classical_bits: backend.classical_results().to_vec(),
1759        probabilities: try_backend_probabilities(backend)?,
1760        metadata: backend_metadata(backend),
1761    })
1762}
1763
1764/// Parse an OpenQASM string and execute with automatic backend selection.
1765pub fn run_qasm(qasm: &str, seed: u64) -> Result<RunOutcome> {
1766    let circuit = crate::circuit::openqasm::parse(qasm)?;
1767    simulate(&circuit).seed(seed).run()
1768}
1769
1770#[cfg(test)]
1771fn run_shots(circuit: &Circuit, num_shots: usize, seed: u64) -> Result<ShotsResult> {
1772    run_shots_with(BackendKind::Auto, circuit, num_shots, seed)
1773}
1774
1775pub(crate) fn supports_compiled_measurement_sampling(circuit: &Circuit) -> bool {
1776    circuit.is_clifford_only()
1777        && !circuit.has_resets()
1778        && circuit.has_terminal_measurements_only()
1779        && circuit
1780            .instructions
1781            .iter()
1782            .any(|inst| matches!(inst, Instruction::Measure { .. }))
1783}
1784
1785fn supports_deferred_measurement_sampling(circuit: &Circuit) -> bool {
1786    circuit.is_clifford_only()
1787        && (circuit.has_resets() || !circuit.has_terminal_measurements_only())
1788        && circuit
1789            .instructions
1790            .iter()
1791            .any(|inst| matches!(inst, Instruction::Measure { .. }))
1792        && !circuit.instructions.iter().any(|inst| {
1793            matches!(
1794                inst,
1795                Instruction::Conditional { .. } | Instruction::Region(_)
1796            )
1797        })
1798}
1799
1800fn is_clifford_sampler_kind(kind: &BackendKind) -> bool {
1801    if kind.is_auto() {
1802        return true;
1803    }
1804    match kind {
1805        BackendKind::Stabilizer | BackendKind::FactoredStabilizer => true,
1806        #[cfg(feature = "gpu")]
1807        BackendKind::StabilizerGpu { .. } => true,
1808        _ => false,
1809    }
1810}
1811
1812fn should_use_compiled_clifford_sampling(
1813    kind: &BackendKind,
1814    circuit: &Circuit,
1815    num_shots: usize,
1816) -> bool {
1817    num_shots >= 2
1818        && supports_compiled_measurement_sampling(circuit)
1819        && is_clifford_sampler_kind(kind)
1820}
1821
1822fn should_use_deferred_clifford_sampling(
1823    kind: &BackendKind,
1824    circuit: &Circuit,
1825    num_shots: usize,
1826) -> bool {
1827    num_shots >= 2
1828        && supports_deferred_measurement_sampling(circuit)
1829        && is_clifford_sampler_kind(kind)
1830}
1831
1832fn compile_measurements_for_kind(
1833    kind: &BackendKind,
1834    circuit: &Circuit,
1835    seed: u64,
1836) -> Result<compiled::CompiledSampler> {
1837    #[cfg(not(feature = "gpu"))]
1838    let _ = kind;
1839
1840    let sampler = compiled::compile_measurements(circuit, seed)?;
1841
1842    #[cfg(feature = "gpu")]
1843    if let BackendKind::StabilizerGpu { context } = kind {
1844        return Ok(sampler.with_gpu(context.clone()));
1845    }
1846
1847    Ok(sampler)
1848}
1849
1850/// Independence analysis shared by the routing prelude in
1851/// `run_with_internal`, the shots slow path, and the terminal fast-path
1852/// candidacy. Returns the components to decompose with when full
1853/// decomposition should fire, plus the partial-independence flag otherwise.
1854fn analyze_independence(circuit: &Circuit) -> (Option<Vec<Vec<usize>>>, bool) {
1855    if circuit.num_qubits >= MIN_DECOMPOSITION_QUBITS {
1856        let components = circuit.independent_subsystems();
1857        if components.len() > 1 {
1858            if should_decompose(&components, circuit.num_qubits) {
1859                return (Some(components), false);
1860            }
1861            return (None, true);
1862        }
1863    }
1864    (None, false)
1865}
1866
1867/// `(t_count, stabilizer_rank_budget)` when the auto Clifford+T family gate
1868/// passes. Callers apply their own per-entry-point T-count ceilings.
1869fn auto_clifford_t_budget(circuit: &Circuit) -> Option<(usize, usize)> {
1870    (circuit.is_clifford_plus_t() && circuit.has_t_gates()).then(|| {
1871        (
1872            circuit.t_count(),
1873            stabilizer_rank_budget(circuit.num_qubits),
1874        )
1875    })
1876}
1877
1878/// Auto-dispatch gate for the Clifford+T stabilizer-rank shortcut: the T
1879/// count must fit both the caller's ceiling and the size-derived
1880/// stabilizer-rank budget. Returns the T count when the shortcut applies.
1881pub(super) fn auto_stabilizer_rank_t_count(circuit: &Circuit, max_t: usize) -> Option<usize> {
1882    let (t, sr_budget) = auto_clifford_t_budget(circuit)?;
1883    (t <= max_t && t <= sr_budget).then_some(t)
1884}
1885
1886/// Routing precedence for the probability path: decomposition (with the
1887/// large sparse-Clifford factored-stabilizer override), then the Clifford+T
1888/// stabilizer-rank shortcut, then temporal Clifford, then direct family
1889/// resolution. `run_with_internal` executes this plan and
1890/// `auto_terminal_statevector_candidate` consults it, so the two cannot
1891/// drift apart.
1892enum ProbabilityRoute {
1893    FactoredStabilizer,
1894    Decomposed(Vec<Vec<usize>>),
1895    StabilizerRank,
1896    /// The temporal-Clifford predicate holds; `run_route` builds the plan and
1897    /// falls back to direct resolution should the split come back empty.
1898    TemporalClifford {
1899        has_partial_independence: bool,
1900    },
1901    Direct {
1902        has_partial_independence: bool,
1903    },
1904}
1905
1906fn plan_probability_route(kind: &BackendKind, circuit: &Circuit) -> ProbabilityRoute {
1907    let (decompose, has_partial_independence) = analyze_independence(circuit);
1908    if let Some(components) = decompose {
1909        let max_block = components.iter().map(|c| c.len()).max().unwrap_or(0);
1910        if kind.is_auto()
1911            && circuit.is_clifford_only()
1912            && circuit.num_qubits >= MIN_FACTORED_STABILIZER_QUBITS
1913            && max_block >= MIN_BLOCK_FOR_FACTORED_STAB
1914        {
1915            return ProbabilityRoute::FactoredStabilizer;
1916        }
1917        return ProbabilityRoute::Decomposed(components);
1918    }
1919    if kind.is_auto()
1920        && circuit.num_qubits <= MAX_STABILIZER_RANK_QUBITS
1921        && !has_nonunitary_or_classical_ops(circuit)
1922        && auto_stabilizer_rank_t_count(circuit, MAX_AUTO_T_COUNT_EXACT).is_some()
1923    {
1924        return ProbabilityRoute::StabilizerRank;
1925    }
1926    if has_temporal_clifford_opportunity(kind, circuit) {
1927        return ProbabilityRoute::TemporalClifford {
1928            has_partial_independence,
1929        };
1930    }
1931    ProbabilityRoute::Direct {
1932        has_partial_independence,
1933    }
1934}
1935
1936/// True when the auto probability route falls through to direct family
1937/// resolution and that resolver picks the CPU statevector.
1938fn auto_terminal_statevector_candidate(circuit: &Circuit) -> bool {
1939    match plan_probability_route(&BackendKind::Auto, circuit) {
1940        ProbabilityRoute::Direct {
1941            has_partial_independence,
1942        } => auto_selects_cpu_statevector(circuit, has_partial_independence),
1943        _ => false,
1944    }
1945}
1946
1947fn terminal_statevector_candidate(kind: &BackendKind, circuit: &Circuit) -> bool {
1948    if kind.is_auto() {
1949        return auto_terminal_statevector_candidate(circuit);
1950    }
1951    match kind {
1952        BackendKind::Statevector => true,
1953        #[cfg(feature = "gpu")]
1954        BackendKind::StatevectorGpu { .. } => true,
1955        _ => false,
1956    }
1957}
1958
1959fn try_terminal_statevector_backend(
1960    kind: &BackendKind,
1961    circuit: &Circuit,
1962    seed: u64,
1963) -> Result<Option<TerminalStatevector>> {
1964    if !circuit.has_terminal_measurements_only() {
1965        return Ok(None);
1966    }
1967
1968    let meas_map = circuit.measurement_map();
1969    if meas_map.is_empty() {
1970        return Ok(None);
1971    }
1972
1973    let stripped = circuit.without_measurements();
1974    if !terminal_statevector_candidate(kind, &stripped) {
1975        return Ok(None);
1976    }
1977
1978    let accel = accel_for(kind, Family::Statevector, stripped.num_qubits);
1979    let mut backend = build_statevector(&accel, seed);
1980    let expanded = expand_for_backend(&backend, &stripped);
1981    let fused = fuse_for_backend(&backend, &expanded);
1982    backend.init(fused.num_qubits, fused.num_classical_bits)?;
1983    backend.apply_instructions(&fused.instructions)?;
1984
1985    Ok(Some((backend, meas_map)))
1986}
1987
1988/// Build and run the backend for a terminal-measurement circuit when routing
1989/// lands on a single backend that samples from its own representation.
1990///
1991/// Returns `None` when the route is not a direct single backend, or when that
1992/// backend has no native sampler, leaving the dense probability path untouched.
1993/// The capability is probed before `init`, so a backend without one costs an
1994/// allocation and nothing else.
1995///
1996/// The product state is the one route taken past subsystem decomposition: it
1997/// already stores one factor per qubit, so splitting the circuit into
1998/// independent blocks pays a backend, a partition, and a merge per block to
1999/// rebuild what one native draw reads straight off the state, and above 64
2000/// qubits the merged block distribution does not exist at all. Every other
2001/// backend keeps the block split.
2002fn try_native_terminal_backend(
2003    kind: &BackendKind,
2004    stripped: &Circuit,
2005    seed: u64,
2006) -> Result<Option<Box<dyn Backend>>> {
2007    if !kind.is_auto() {
2008        validate_explicit_backend(kind, stripped)?;
2009    }
2010    let (decomposed, has_partial_independence) = match plan_probability_route(kind, stripped) {
2011        ProbabilityRoute::Direct {
2012            has_partial_independence,
2013        } => (false, has_partial_independence),
2014        ProbabilityRoute::Decomposed(_) => (true, false),
2015        _ => return Ok(None),
2016    };
2017    let ExecutionPlan::Backend(plan) = resolve(kind, stripped, has_partial_independence) else {
2018        return Ok(None);
2019    };
2020    if decomposed && !matches!(plan, BackendPlan::ProductState) {
2021        return Ok(None);
2022    }
2023    let mut backend = plan.build(seed);
2024    if !backend.supports_native_sampling() {
2025        return Ok(None);
2026    }
2027    execute(&mut *backend, stripped, &SimOptions::classical_only())?;
2028    Ok(Some(backend))
2029}
2030
2031/// Build and run the backend for a marginal query when routing lands on a
2032/// single backend that evaluates observables on its own representation.
2033///
2034/// Returns `None` when the route is not a direct single backend, or when that
2035/// backend has no native observable path, leaving the dense probability route
2036/// untouched. The capability is probed before `init`, so a backend without one
2037/// costs an allocation and nothing else.
2038///
2039/// The product state is carried past subsystem decomposition for the reason
2040/// [`try_native_terminal_backend`] carries it: it already holds one factor per
2041/// qubit, and the decomposed route would build the `2^n` merged distribution to
2042/// read marginals a per-qubit expectation answers directly. Every other backend
2043/// keeps the block split, which has no single backend holding the joint state.
2044fn try_native_marginal_backend(
2045    kind: &BackendKind,
2046    circuit: &Circuit,
2047    seed: u64,
2048) -> Result<Option<Box<dyn Backend>>> {
2049    if !kind.is_auto() {
2050        validate_explicit_backend(kind, circuit)?;
2051    }
2052    let (decomposed, has_partial_independence) = match plan_probability_route(kind, circuit) {
2053        ProbabilityRoute::Direct {
2054            has_partial_independence,
2055        } => (false, has_partial_independence),
2056        ProbabilityRoute::Decomposed(_) => (true, false),
2057        _ => return Ok(None),
2058    };
2059    let ExecutionPlan::Backend(plan) = resolve(kind, circuit, has_partial_independence) else {
2060        return Ok(None);
2061    };
2062    if decomposed && !matches!(plan, BackendPlan::ProductState) {
2063        return Ok(None);
2064    }
2065    let mut backend = plan.build(seed);
2066    if !backend.supports_pauli_expectation() {
2067        return Ok(None);
2068    }
2069    execute(&mut *backend, circuit, &SimOptions::classical_only())?;
2070    Ok(Some(backend))
2071}
2072
2073/// A source that answers every measurement in a circuit from a single state,
2074/// in the precedence [`prepare_shot_source`] applies. Shots and counts both
2075/// consume it, so the two cannot disagree about which shortcut applies.
2076enum ShotSource {
2077    /// `deferred` marks a sampler built from the measure/reset-deferred
2078    /// rewrite, whose renumbered measurements only read through `meas_map`.
2079    Compiled {
2080        sampler: Box<compiled::CompiledSampler>,
2081        meas_map: Vec<(usize, usize)>,
2082        deferred: bool,
2083    },
2084    TerminalStatevector {
2085        backend: Box<StatevectorBackend>,
2086        meas_map: Vec<(usize, usize)>,
2087    },
2088    /// A backend that draws basis states from its own representation.
2089    Native {
2090        backend: Box<dyn Backend>,
2091        meas_map: Vec<(usize, usize)>,
2092    },
2093    /// Dense output distribution of the measurement-stripped circuit, with the
2094    /// provenance of the run that produced it.
2095    TerminalProbabilities {
2096        probs: Probabilities,
2097        meas_map: Vec<(usize, usize)>,
2098        metadata: RunMetadata,
2099    },
2100    StabilizerRank,
2101    PerShot,
2102}
2103
2104impl ShotSource {
2105    /// Which engine will answer, decided by `prepare_shot_source` and read here
2106    /// so the shot and count entry points do not re-derive it. `None` for the
2107    /// per-shot route, which builds one backend per shot and stamps its own.
2108    fn metadata(&self) -> Option<RunMetadata> {
2109        match self {
2110            ShotSource::Compiled { .. } => Some(
2111                RunMetadata::exact(ResolvedBackend::CompiledStabilizer)
2112                    .with_engine(Engine::CompiledSampler),
2113            ),
2114            ShotSource::TerminalStatevector { backend, .. } => Some(backend_metadata(&**backend)),
2115            ShotSource::Native { backend, .. } => Some(backend_metadata(&**backend)),
2116            ShotSource::TerminalProbabilities { metadata, .. } => Some(metadata.clone()),
2117            ShotSource::StabilizerRank => Some(RunMetadata::exact(ResolvedBackend::StabilizerRank)),
2118            ShotSource::PerShot => None,
2119        }
2120    }
2121}
2122
2123/// Select and prepare the sampling source for `circuit`.
2124///
2125/// Preparation is real work: compiling a sampler, building and running a
2126/// backend, or executing the stripped circuit once. Call once per entry point
2127/// and match on the result rather than re-deriving the choice.
2128fn prepare_shot_source(
2129    kind: &BackendKind,
2130    circuit: &Circuit,
2131    num_shots: usize,
2132    seed: u64,
2133) -> Result<ShotSource> {
2134    if should_use_compiled_clifford_sampling(kind, circuit, num_shots) {
2135        return Ok(ShotSource::Compiled {
2136            sampler: Box::new(compile_measurements_for_kind(kind, circuit, seed)?),
2137            meas_map: circuit.measurement_map(),
2138            deferred: false,
2139        });
2140    }
2141
2142    if should_use_deferred_clifford_sampling(kind, circuit, num_shots) {
2143        if let Ok(deferred) = compiled::defer_measure_reset_circuit(circuit) {
2144            return Ok(ShotSource::Compiled {
2145                sampler: Box::new(compile_measurements_for_kind(kind, &deferred, seed)?),
2146                meas_map: deferred.measurement_map(),
2147                deferred: true,
2148            });
2149        }
2150    }
2151
2152    if let Some((backend, meas_map)) = try_terminal_statevector_backend(kind, circuit, seed)? {
2153        return Ok(ShotSource::TerminalStatevector {
2154            backend: Box::new(backend),
2155            meas_map,
2156        });
2157    }
2158
2159    if matches!(kind, BackendKind::StabilizerRank) && circuit.has_t_gates() {
2160        return Ok(ShotSource::StabilizerRank);
2161    }
2162    if kind.is_auto()
2163        && circuit.has_terminal_measurements_only()
2164        && circuit.num_qubits > MAX_STABILIZER_RANK_QUBITS
2165        && auto_stabilizer_rank_t_count(circuit, MAX_AUTO_T_COUNT_SHOTS).is_some()
2166    {
2167        return Ok(ShotSource::StabilizerRank);
2168    }
2169
2170    if circuit.has_terminal_measurements_only() {
2171        let stripped = circuit.without_measurements();
2172        if let Some(backend) = try_native_terminal_backend(kind, &stripped, seed)? {
2173            return Ok(ShotSource::Native {
2174                backend,
2175                meas_map: circuit.measurement_map(),
2176            });
2177        }
2178        let result = run_with_internal(kind.clone(), &stripped, seed, SimOptions::default())?;
2179        if let Some(probs) = result.probabilities {
2180            return Ok(ShotSource::TerminalProbabilities {
2181                probs,
2182                meas_map: circuit.measurement_map(),
2183                metadata: result.metadata,
2184            });
2185        }
2186    }
2187
2188    Ok(ShotSource::PerShot)
2189}
2190
2191#[cfg(test)]
2192fn run_counts(circuit: &Circuit, num_shots: usize, seed: u64) -> Result<HashMap<Vec<u64>, u64>> {
2193    run_counts_with(BackendKind::Auto, circuit, num_shots, seed).map(|(counts, _)| counts)
2194}
2195
2196/// Execute a circuit multiple times with explicit backend selection and return counts.
2197///
2198/// For Clifford circuits with terminal measurements and no resets, Auto,
2199/// Stabilizer, FactoredStabilizer, and explicit `StabilizerGpu` route through
2200/// the compiled sampler's optimized counting path. Explicit `StabilizerGpu`
2201/// carries its GPU context into the compiled sampler so large shot runs avoid
2202/// the raw tableau measurement round-trips. Other circuits fall back to
2203/// per-shot simulation with counting.
2204///
2205/// Optimized terminal statevector paths sample counts directly from the output
2206/// distribution. The distribution is equivalent to materializing shots first,
2207/// but finite seeded counts may differ from `run_shots_with(...).counts()`.
2208pub(crate) fn run_counts_with(
2209    kind: BackendKind,
2210    circuit: &Circuit,
2211    num_shots: usize,
2212    seed: u64,
2213) -> Result<(HashMap<Vec<u64>, u64>, RunMetadata)> {
2214    #[cfg(feature = "distributed")]
2215    if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
2216        let shots = run_shots_with(kind, circuit, num_shots, seed)?;
2217        return Ok((shots.counts(), shots.metadata));
2218    }
2219
2220    let folded = circuit.fold_static_guards();
2221    let circuit = folded.as_ref();
2222
2223    let bits = circuit.num_classical_bits;
2224    let source = prepare_shot_source(&kind, circuit, num_shots, seed)?;
2225    let Some(metadata) = source.metadata() else {
2226        let shots = run_shots_per_shot(kind, circuit, num_shots, seed)?;
2227        return Ok((shots.counts(), shots.metadata));
2228    };
2229    let counts = match source {
2230        ShotSource::Compiled {
2231            mut sampler,
2232            meas_map,
2233            deferred,
2234        } => {
2235            if deferred {
2236                let packed = sampler.try_sample_bulk_packed(num_shots)?;
2237                counts_of(
2238                    packed_shots_to_classical_bits(&packed, &meas_map, bits),
2239                    bits,
2240                )
2241            } else {
2242                sampler.try_sample_counts(num_shots)?
2243            }
2244        }
2245        ShotSource::TerminalStatevector { backend, meas_map } => {
2246            if backend.is_gpu_resident() {
2247                let probs = backend.probabilities()?;
2248                sample_counts_from_probs(&probs, &meas_map, bits, num_shots, seed)
2249            } else {
2250                sample_counts_from_state(
2251                    backend.state_vector(),
2252                    backend.probability_scale(),
2253                    &meas_map,
2254                    bits,
2255                    num_shots,
2256                    seed,
2257                )
2258            }
2259        }
2260        ShotSource::Native {
2261            mut backend,
2262            meas_map,
2263        } => {
2264            let samples = backend.sample_basis_states(num_shots, seed)?;
2265            counts_of(shots_from_basis_samples(&samples, &meas_map, bits), bits)
2266        }
2267        ShotSource::TerminalProbabilities {
2268            probs, meas_map, ..
2269        } => counts_of(sample_shots(&probs, &meas_map, bits, num_shots, seed), bits),
2270        ShotSource::StabilizerRank => {
2271            stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed)?.counts()
2272        }
2273        ShotSource::PerShot => unreachable!("handled above"),
2274    };
2275    Ok((counts, metadata.with_shots(num_shots)))
2276}
2277
2278fn counts_of(shots: Vec<Vec<bool>>, num_classical_bits: usize) -> HashMap<Vec<u64>, u64> {
2279    ShotsResult::from_shots(shots, num_classical_bits).counts()
2280}
2281
2282#[cfg(test)]
2283fn run_marginals(circuit: &Circuit, seed: u64) -> Result<Vec<(f64, f64)>> {
2284    run_marginals_result_with(BackendKind::Auto, circuit, seed).map(MarginalsResult::into_vec)
2285}
2286
2287#[cfg(test)]
2288fn run_marginals_with(kind: BackendKind, circuit: &Circuit, seed: u64) -> Result<Vec<(f64, f64)>> {
2289    run_marginals_result_with(kind, circuit, seed).map(MarginalsResult::into_vec)
2290}
2291
2292/// Per-qubit marginals from native single-qubit Z expectations, for a backend
2293/// whose own representation answers them without a dense probability vector.
2294fn marginals_from_pauli_expectations(
2295    backend: &dyn Backend,
2296    num_qubits: usize,
2297) -> Result<MarginalsResult> {
2298    let observables: Vec<Vec<PauliTerm>> = (0..num_qubits).map(|q| vec![PauliTerm::z(q)]).collect();
2299    let expectations = backend.pauli_expectations(&observables)?;
2300    Ok(MarginalsResult {
2301        marginals: expectations_to_marginals(&expectations),
2302        metadata: backend_metadata(backend),
2303    })
2304}
2305
2306/// Route-level exactness per the [`Exactness`] convention: only `epsilon > 0`
2307/// can discard terms, so it marks the route approximate even on a run that
2308/// discarded nothing; `epsilon == 0` overflows into an error instead of an
2309/// approximation and stays exact. The realized bound is a coefficient
2310/// magnitude rather than a state overlap, so it stays `total_discarded` on
2311/// the engine result and never a fidelity bound.
2312fn spd_metadata(epsilon: f64) -> RunMetadata {
2313    if epsilon > 0.0 {
2314        RunMetadata::approximate(ResolvedBackend::DeterministicPauli)
2315    } else {
2316        RunMetadata::exact(ResolvedBackend::DeterministicPauli)
2317    }
2318}
2319
2320pub(crate) fn expectations_to_marginals(expectations: &[f64]) -> Vec<(f64, f64)> {
2321    expectations
2322        .iter()
2323        .map(|ez| {
2324            let p0 = ((1.0 + ez) / 2.0).clamp(0.0, 1.0);
2325            (p0, 1.0 - p0)
2326        })
2327        .collect()
2328}
2329
2330pub(super) fn has_nonunitary_or_classical_ops(circuit: &Circuit) -> bool {
2331    circuit.instructions.iter().any(|inst| {
2332        matches!(
2333            inst,
2334            Instruction::Measure { .. }
2335                | Instruction::Reset { .. }
2336                | Instruction::Conditional { .. }
2337                | Instruction::Region(_)
2338        )
2339    })
2340}
2341
2342fn supports_pauli_marginal_backend(circuit: &Circuit) -> bool {
2343    circuit.is_clifford_plus_t() && !has_nonunitary_or_classical_ops(circuit)
2344}
2345
2346/// Gate-set support is left to the engines, which accept Clifford gates and
2347/// Pauli rotations and report the offending gate by name.
2348fn validate_pauli_marginal_backend(kind: &BackendKind, circuit: &Circuit) -> Result<()> {
2349    if has_nonunitary_or_classical_ops(circuit) {
2350        return Err(PrismError::IncompatibleBackend {
2351            backend: format!("{kind:?}"),
2352            reason: "Pauli marginal backends require a unitary circuit without measurements, resets, or conditionals".into(),
2353        });
2354    }
2355    Ok(())
2356}
2357
2358fn run_marginals_result_with(
2359    kind: BackendKind,
2360    circuit: &Circuit,
2361    seed: u64,
2362) -> Result<MarginalsResult> {
2363    let n = circuit.num_qubits;
2364
2365    match &kind {
2366        BackendKind::StochasticPauli { num_samples } => {
2367            validate_pauli_marginal_backend(&kind, circuit)?;
2368            let spp = unified_pauli::run_spp(circuit, *num_samples, seed)?;
2369            return Ok(MarginalsResult {
2370                marginals: expectations_to_marginals(&spp.expectations),
2371                metadata: RunMetadata::approximate(ResolvedBackend::StochasticPauli)
2372                    .with_shots(*num_samples),
2373            });
2374        }
2375        BackendKind::DeterministicPauli { epsilon, max_terms } => {
2376            validate_pauli_marginal_backend(&kind, circuit)?;
2377            let spd = unified_pauli::run_spd(circuit, *epsilon, *max_terms)?;
2378            return Ok(MarginalsResult {
2379                marginals: expectations_to_marginals(&spd.expectations),
2380                metadata: spd_metadata(*epsilon),
2381            });
2382        }
2383        _ => {}
2384    }
2385
2386    if kind.is_auto()
2387        && supports_pauli_marginal_backend(circuit)
2388        && circuit.has_t_gates()
2389        && n >= MIN_QUBITS_FOR_SPD_AUTO
2390    {
2391        let spd = unified_pauli::run_spd(circuit, 0.0, AUTO_SPD_MAX_TERMS)?;
2392        return Ok(MarginalsResult {
2393            marginals: expectations_to_marginals(&spd.expectations),
2394            metadata: spd_metadata(0.0),
2395        });
2396    }
2397
2398    // The distributed backend answers a marginal from rank-local sums plus one
2399    // `Allreduce`, so it never needs the 2^n gather the fallback below takes.
2400    #[cfg(feature = "distributed")]
2401    if let BackendKind::StatevectorDistributed { context } = &kind {
2402        let mut backend =
2403            crate::backend::distributed_statevector::DistributedStatevectorBackend::new(
2404                context.clone(),
2405                seed,
2406            );
2407        execute(&mut backend, circuit, &SimOptions::classical_only())?;
2408        return marginals_from_pauli_expectations(&backend, n);
2409    }
2410
2411    if let Some(backend) = try_native_marginal_backend(&kind, circuit, seed)? {
2412        return marginals_from_pauli_expectations(&*backend, n);
2413    }
2414
2415    let result = run_with(kind, circuit, seed)?;
2416    if let Some(probs) = &result.probabilities {
2417        Ok(MarginalsResult {
2418            marginals: probs.marginals(),
2419            metadata: result.metadata.clone(),
2420        })
2421    } else {
2422        Err(PrismError::BackendUnsupported {
2423            backend: "simulate".into(),
2424            operation: format!(
2425                "marginals for {} qubits without backend probability output",
2426                circuit.num_qubits
2427            ),
2428        })
2429    }
2430}
2431
2432/// Compute `⟨ψ|P|ψ⟩` for each joint Pauli observable on a unitary circuit's
2433/// output state, using automatic backend selection. See
2434/// [`Simulate::expectation_values`] for explicit backend control.
2435///
2436/// # Examples
2437///
2438/// ```
2439/// use prism_q::{Circuit, Gate, PauliTerm, run_expectation_values};
2440///
2441/// let mut bell = Circuit::new(2, 0);
2442/// bell.add_gate(Gate::H, &[0]);
2443/// bell.add_gate(Gate::Cx, &[0, 1]);
2444///
2445/// let observables = vec![
2446///     vec![PauliTerm::z(0)],
2447///     vec![PauliTerm::z(0), PauliTerm::z(1)],
2448/// ];
2449/// let values = run_expectation_values(&bell, &observables, 42)?;
2450/// assert!(values[0].abs() < 1e-10); // <Z0> = 0
2451/// assert!((values[1] - 1.0).abs() < 1e-10); // <Z0 Z1> = 1
2452/// # Ok::<(), prism_q::PrismError>(())
2453/// ```
2454pub fn run_expectation_values(
2455    circuit: &Circuit,
2456    observables: &[Vec<PauliTerm>],
2457    seed: u64,
2458) -> Result<Vec<f64>> {
2459    run_expectation_values_with(BackendKind::Auto, circuit, observables, seed)
2460}
2461
2462fn run_expectation_values_with(
2463    kind: BackendKind,
2464    circuit: &Circuit,
2465    observables: &[Vec<PauliTerm>],
2466    seed: u64,
2467) -> Result<Vec<f64>> {
2468    run_expectation_values_reported(kind, circuit, observables, seed)
2469        .map(ExpectationResult::into_values)
2470}
2471
2472fn run_expectation_values_reported(
2473    kind: BackendKind,
2474    circuit: &Circuit,
2475    observables: &[Vec<PauliTerm>],
2476    seed: u64,
2477) -> Result<ExpectationResult> {
2478    require_unitary_circuit(&kind, circuit, "expectation values require")?;
2479
2480    match &kind {
2481        BackendKind::StochasticPauli { num_samples } => {
2482            let mut values = Vec::with_capacity(observables.len());
2483            let mut std_errors = Vec::with_capacity(observables.len());
2484            for (i, obs) in observables.iter().enumerate() {
2485                let r = unified_pauli::run_spp_observable(
2486                    circuit,
2487                    obs,
2488                    *num_samples,
2489                    seed.wrapping_add(i as u64),
2490                )?;
2491                values.push(r.mean);
2492                std_errors.push(r.std_error);
2493            }
2494            Ok(ExpectationResult {
2495                values,
2496                std_errors: Some(std_errors),
2497                metadata: RunMetadata::approximate(ResolvedBackend::StochasticPauli)
2498                    .with_shots(*num_samples),
2499            })
2500        }
2501        BackendKind::DeterministicPauli { epsilon, max_terms } => {
2502            let mut values = Vec::with_capacity(observables.len());
2503            for obs in observables {
2504                let r = unified_pauli::run_spd_observable(circuit, obs, *epsilon, *max_terms)?;
2505                values.push(r.mean);
2506            }
2507            Ok(analytic_expectations(values, spd_metadata(*epsilon)))
2508        }
2509        _ if kind.is_auto() || kind.is_stabilizer_family() => {
2510            if circuit.is_clifford_only() {
2511                let mut values = Vec::with_capacity(observables.len());
2512                for obs in observables {
2513                    let r = unified_pauli::run_spd_observable(circuit, obs, 0.0, 0)?;
2514                    values.push(r.mean);
2515                }
2516                Ok(analytic_expectations(values, spd_metadata(0.0)))
2517            } else if kind.is_auto() {
2518                if circuit.num_qubits > max_statevector_qubits() {
2519                    return expectation_values_native(&kind, circuit, observables, seed);
2520                }
2521                expectation_values_statevector(&kind, circuit, observables, seed)
2522            } else {
2523                Err(PrismError::IncompatibleBackend {
2524                    backend: format!("{kind:?}"),
2525                    reason: "stabilizer backends require a Clifford-only circuit".into(),
2526                })
2527            }
2528        }
2529        BackendKind::Statevector => {
2530            expectation_values_statevector(&kind, circuit, observables, seed)
2531        }
2532        #[cfg(feature = "gpu")]
2533        BackendKind::StatevectorGpu { .. } => {
2534            expectation_values_statevector(&kind, circuit, observables, seed)
2535        }
2536        other => expectation_values_native(other, circuit, observables, seed),
2537    }
2538}
2539
2540/// Compute `⟨H⟩` and its grouped-measurement variance for a weighted Pauli
2541/// observable, using automatic backend selection. See
2542/// [`Simulate::observable_expectation`] for explicit backend control and
2543/// [`ObservableExpectation::variance`] for the variance contract.
2544///
2545/// # Examples
2546///
2547/// ```
2548/// use prism_q::{Circuit, Gate, PauliObservable, PauliTerm, run_observable_expectation};
2549///
2550/// let mut circuit = Circuit::new(2, 0);
2551/// circuit.add_gate(Gate::H, &[0]);
2552/// circuit.add_gate(Gate::Cx, &[0, 1]);
2553/// circuit.add_gate(Gate::T, &[0]);
2554///
2555/// let hamiltonian = PauliObservable::from_terms([
2556///     (1.0, vec![PauliTerm::z(0)]),
2557///     (1.0, vec![PauliTerm::z(0), PauliTerm::z(1)]),
2558/// ])?;
2559///
2560/// // The T phase moves nothing here: both terms are Z-only, one commuting
2561/// // group covers them, and the variance is exactly Var(H) with outcomes
2562/// // 2 and 0 at probability 1/2 each.
2563/// let result = run_observable_expectation(&circuit, &hamiltonian, 42)?;
2564/// assert!((result.mean - 1.0).abs() < 1e-10);
2565/// assert!((result.variance.unwrap() - 1.0).abs() < 1e-10);
2566/// # Ok::<(), prism_q::PrismError>(())
2567/// ```
2568pub fn run_observable_expectation(
2569    circuit: &Circuit,
2570    observable: &PauliObservable,
2571    seed: u64,
2572) -> Result<ObservableExpectation> {
2573    run_observable_expectation_reported(BackendKind::Auto, circuit, observable, seed)
2574}
2575
2576fn run_observable_expectation_reported(
2577    kind: BackendKind,
2578    circuit: &Circuit,
2579    observable: &PauliObservable,
2580    seed: u64,
2581) -> Result<ObservableExpectation> {
2582    require_unitary_circuit(&kind, circuit, "expectation values require")?;
2583
2584    let grouped_statevector = match &kind {
2585        BackendKind::Statevector => true,
2586        #[cfg(feature = "gpu")]
2587        BackendKind::StatevectorGpu { .. } => true,
2588        _ => {
2589            kind.is_auto()
2590                && !circuit.is_clifford_only()
2591                && circuit.num_qubits <= max_statevector_qubits()
2592        }
2593    };
2594    if grouped_statevector {
2595        return grouped_expectation_statevector(&kind, circuit, observable, seed);
2596    }
2597
2598    let result =
2599        run_expectation_values_reported(kind, circuit, &observable_vecs(observable), seed)?;
2600    Ok(weighted_observable_result(
2601        observable,
2602        &result.values,
2603        result.std_errors.as_deref(),
2604        result.metadata,
2605    ))
2606}
2607
2608fn observable_vecs(observable: &PauliObservable) -> Vec<Vec<PauliTerm>> {
2609    observable
2610        .terms()
2611        .iter()
2612        .map(|(_, factors)| factors.clone())
2613        .collect()
2614}
2615
2616/// Fold per-term values into the weighted mean; no grouped traversal ran, so
2617/// there is no variance to report. Independent per-term estimates combine
2618/// into the weighted-sum standard error.
2619fn weighted_observable_result(
2620    observable: &PauliObservable,
2621    values: &[f64],
2622    std_errors: Option<&[f64]>,
2623    metadata: RunMetadata,
2624) -> ObservableExpectation {
2625    let coefficients = observable.terms().iter().map(|(c, _)| *c);
2626    let mean = coefficients.clone().zip(values).map(|(c, v)| c * v).sum();
2627    let std_error = std_errors.map(|errors| {
2628        coefficients
2629            .zip(errors)
2630            .map(|(c, e)| (c * e).powi(2))
2631            .sum::<f64>()
2632            .sqrt()
2633    });
2634    ObservableExpectation {
2635        mean,
2636        variance: None,
2637        group_variances: None,
2638        std_error,
2639        metadata,
2640    }
2641}
2642
2643/// Evaluate a weighted observable on the statevector: the mean and most group
2644/// variances from one shared batched traversal, large groups from a dedicated
2645/// moments pass.
2646///
2647/// `Var(H_g) = <H_g^2> - <H_g>^2` per commuting group. For a small group the
2648/// square expands into pairwise product strings appended to the same
2649/// traversal that serves the term means; a group past the pair budget takes a
2650/// single-pass moment accumulation instead, on the state as run when the
2651/// group is Z-only and on a basis-rotated copy otherwise.
2652fn grouped_expectation_statevector(
2653    kind: &BackendKind,
2654    circuit: &Circuit,
2655    observable: &PauliObservable,
2656    seed: u64,
2657) -> Result<ObservableExpectation> {
2658    let terms = observable.terms();
2659    // Validate before the 2^n simulation so bad observables fail cheaply. The
2660    // mask reduction shifts by qubit index, so it follows the `init` that
2661    // rejects a width no mask can address.
2662    for (_, factors) in terms {
2663        validate_observable(factors, circuit.num_qubits)?;
2664    }
2665
2666    let accel = accel_for(kind, Family::Statevector, circuit.num_qubits);
2667    let mut backend = build_statevector(&accel, seed);
2668    let expanded = expand_for_backend(&backend, circuit);
2669    let fused = fuse_for_backend(&backend, &expanded);
2670    backend.init(fused.num_qubits, fused.num_classical_bits)?;
2671    let masks = terms
2672        .iter()
2673        .map(|(_, factors)| pauli_masks(factors, circuit.num_qubits))
2674        .collect::<Result<Vec<_>>>()?;
2675    backend.apply_instructions(&fused.instructions)?;
2676    let metadata = backend_metadata(&backend);
2677
2678    let grouping = observable.grouping();
2679
2680    // Small groups get `<H_g^2>` from pairwise product strings appended to the
2681    // shared traversal: qubit-wise-commuting strings multiply phase-free
2682    // (shared qubits carry equal axes and cancel to identity), so each pair is
2683    // one more mask. Large groups fall back to a dedicated moments pass, which
2684    // costs a fixed number of state sweeps where the pair expansion grows
2685    // quadratically.
2686    let mut combined = masks.clone();
2687    let mut pair_blocks: Vec<(usize, usize, Vec<f64>)> = Vec::new();
2688    let mut deferred: Vec<usize> = Vec::new();
2689    for (gi, group) in grouping.groups.iter().enumerate() {
2690        let members = &group.term_indices;
2691        if members.len() * (members.len() - 1) / 2 > MAX_PAIR_MASKS_PER_GROUP {
2692            deferred.push(gi);
2693            continue;
2694        }
2695        let first_mask = combined.len();
2696        let mut pair_coefficients = Vec::with_capacity(members.len() * (members.len() - 1) / 2);
2697        for (pos, &i) in members.iter().enumerate() {
2698            for &j in &members[pos + 1..] {
2699                let product_x = masks[i].0 ^ masks[j].0;
2700                let product_z = masks[i].1 ^ masks[j].1;
2701                combined.push((product_x, product_z, (product_x & product_z).count_ones()));
2702                pair_coefficients.push(2.0 * terms[i].0 * terms[j].0);
2703            }
2704        }
2705        pair_blocks.push((gi, first_mask, pair_coefficients));
2706    }
2707
2708    // A device-resident state reduces every mask on the card; the host keeps
2709    // its state and norm for the moments pass below.
2710    let (values, host) = match pauli_expectations_on_device(&backend, &combined) {
2711        Some(values) => (values?, None),
2712        None => {
2713            let state = backend.state_vector();
2714            let norm = crate::backend::state_norm_sqr(state);
2715            let values = pauli_expectations_from_masks(state, &combined, norm);
2716            (values, Some((state, norm)))
2717        }
2718    };
2719
2720    let mean: f64 = terms.iter().zip(&values).map(|((c, _), v)| c * v).sum();
2721    let mut group_variances = vec![0.0; grouping.groups.len()];
2722
2723    for (gi, first_mask, pair_coefficients) in &pair_blocks {
2724        let group = &grouping.groups[*gi];
2725        let m1: f64 = group
2726            .term_indices
2727            .iter()
2728            .map(|&i| terms[i].0 * values[i])
2729            .sum();
2730        let square_diag: f64 = group
2731            .term_indices
2732            .iter()
2733            .map(|&i| terms[i].0 * terms[i].0)
2734            .sum();
2735        let square_cross: f64 = pair_coefficients
2736            .iter()
2737            .zip(&values[*first_mask..])
2738            .map(|(c, v)| c * v)
2739            .sum();
2740        group_variances[*gi] = (square_diag + square_cross - m1 * m1).max(0.0);
2741    }
2742
2743    // The moments pass runs on the host, so a device-resident state is
2744    // exported here and only when a group needs it.
2745    if !deferred.is_empty() {
2746        let exported;
2747        let (state, norm): (&[Complex64], f64) = match host {
2748            Some(host) => host,
2749            None => {
2750                exported = backend.export_statevector()?;
2751                (&exported, crate::backend::state_norm_sqr(&exported))
2752            }
2753        };
2754        let mut scratch: Option<StatevectorBackend> = None;
2755        for &gi in &deferred {
2756            let group = &grouping.groups[gi];
2757            let coefficients: Vec<f64> = group.term_indices.iter().map(|&i| terms[i].0).collect();
2758            let (m1, m2) = if group.is_z_only() {
2759                let zmasks: Vec<usize> = group.term_indices.iter().map(|&i| masks[i].1).collect();
2760                observable::weighted_group_moments(state, &zmasks, &coefficients, norm)
2761            } else {
2762                let zmasks: Vec<usize> = group
2763                    .term_indices
2764                    .iter()
2765                    .map(|&i| masks[i].0 | masks[i].1)
2766                    .collect();
2767                let rotation_circuit = group.basis_rotation_circuit(circuit.num_qubits);
2768                let rotation = crate::circuit::fusion::fuse_circuit(&rotation_circuit, true);
2769                let rotated = scratch.get_or_insert_with(|| StatevectorBackend::new(seed));
2770                rotated.init_from_amplitudes(state.to_vec(), 0)?;
2771                rotated.apply_instructions(&rotation.instructions)?;
2772                observable::weighted_group_moments(
2773                    rotated.state_vector(),
2774                    &zmasks,
2775                    &coefficients,
2776                    norm,
2777                )
2778            };
2779            group_variances[gi] = (m2 - m1 * m1).max(0.0);
2780        }
2781    }
2782
2783    let variance = group_variances.iter().sum();
2784    Ok(ObservableExpectation {
2785        mean,
2786        variance: Some(variance),
2787        group_variances: Some(group_variances),
2788        std_error: None,
2789        metadata,
2790    })
2791}
2792
2793/// Pair-expansion budget per commuting group. Measured on the 2000-string
2794/// Jordan-Wigner fixture at n=20: one extra general mask in the shared
2795/// traversal costs about 0.6 ms while a scratch-rotation moments pass costs
2796/// about 11 ms, so groups whose pair count stays under that ratio expand
2797/// inline and larger groups take the dedicated pass.
2798const MAX_PAIR_MASKS_PER_GROUP: usize = 20;
2799
2800/// Values from a route that evaluates rather than samples, so there is no
2801/// interval to report.
2802fn analytic_expectations(values: Vec<f64>, metadata: RunMetadata) -> ExpectationResult {
2803    ExpectationResult {
2804        values,
2805        std_errors: None,
2806        metadata,
2807    }
2808}
2809
2810/// Which state diagnostic a terminal is asking for, carried into dispatch so
2811/// the route can be judged before the circuit runs.
2812#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2813enum Diagnostic {
2814    StateVector,
2815    ReducedDensityMatrix,
2816    Entropy,
2817    Overlap,
2818}
2819
2820impl Diagnostic {
2821    fn terminal(self) -> &'static str {
2822        match self {
2823            Diagnostic::StateVector => "a statevector",
2824            Diagnostic::ReducedDensityMatrix => "a reduced density matrix",
2825            Diagnostic::Entropy => "entanglement entropy",
2826            Diagnostic::Overlap => "a state overlap",
2827        }
2828    }
2829}
2830
2831/// Whether the backend `plan` builds implements `diagnostic` on its own
2832/// representation.
2833///
2834/// Read from the plan rather than from a built state so [`BackendKind::Auto`]
2835/// can re-route before paying for the run. A plan added without the kernel
2836/// reads as declining here, which is what its trait default does.
2837fn plan_answers(plan: &BackendPlan, diagnostic: Diagnostic) -> bool {
2838    match diagnostic {
2839        Diagnostic::ReducedDensityMatrix => matches!(
2840            plan,
2841            BackendPlan::Statevector { .. }
2842                | BackendPlan::Sparse
2843                | BackendPlan::Factored
2844                | BackendPlan::ProductState
2845                | BackendPlan::DensityMatrix { .. }
2846                | BackendPlan::Stabilizer { .. }
2847                | BackendPlan::FactoredStabilizer
2848        ),
2849        Diagnostic::Entropy => matches!(
2850            plan,
2851            BackendPlan::Statevector { .. }
2852                | BackendPlan::Mps { .. }
2853                | BackendPlan::ProductState
2854                | BackendPlan::Stabilizer { .. }
2855                | BackendPlan::FactoredStabilizer
2856        ),
2857        Diagnostic::StateVector | Diagnostic::Overlap => {
2858            !matches!(plan, BackendPlan::DensityMatrix { .. })
2859        }
2860    }
2861}
2862
2863/// The error for a route that propagates an observable and holds no state,
2864/// naming the route so the caller knows which engine `kind` reached.
2865fn stateless_route(kind: &BackendKind, diagnostic: Diagnostic, route: &str) -> PrismError {
2866    PrismError::IncompatibleBackend {
2867        backend: format!("{kind:?}"),
2868        reason: format!(
2869            "{} needs a backend that holds a state; the {route} route returns \
2870             probabilities only",
2871            diagnostic.terminal()
2872        ),
2873    }
2874}
2875
2876/// Price a diagnostic's output against the dense export cap before the circuit
2877/// runs: a reduced density matrix holds `4^k` entries, which depends on `k`
2878/// and the backend's name alone, both known once the plan is built.
2879fn check_diagnostic_width(backend: &dyn Backend, diagnostic: Diagnostic, k: usize) -> Result<()> {
2880    match diagnostic {
2881        Diagnostic::ReducedDensityMatrix => {
2882            crate::backend::reduced_density::reduced_density_side(backend.name(), k)?;
2883            Ok(())
2884        }
2885        Diagnostic::StateVector => {
2886            if k > crate::backend::schmidt::export_cap() {
2887                return Err(crate::backend::schmidt::export_cap_exceeded(
2888                    backend.name(),
2889                    format!("dense statevector of {k} qubits"),
2890                ));
2891            }
2892            Ok(())
2893        }
2894        Diagnostic::Entropy | Diagnostic::Overlap => Ok(()),
2895    }
2896}
2897
2898/// Build the backend `kind` resolves to, run `circuit` on it, and hand it back
2899/// for one terminal read of its state.
2900///
2901/// Resolution goes straight to a single backend, as the native expectation
2902/// path does: a diagnostic is read off one state, and the decomposed route
2903/// holds one per independent block. Under [`BackendKind::Auto`] a resolved
2904/// plan that cannot answer `diagnostic` is replaced by the statevector while
2905/// the circuit fits its cap, since the route was the dispatcher's choice and
2906/// not the caller's; an explicit kind keeps its backend and declines.
2907fn diagnostic_backend(
2908    kind: &BackendKind,
2909    circuit: &Circuit,
2910    initial_state: Option<&[Complex64]>,
2911    seed: u64,
2912    diagnostic: Diagnostic,
2913    subsystem_len: usize,
2914) -> Result<Box<dyn Backend>> {
2915    if let Some(state) = initial_state {
2916        let mut backend = backend_from_initial_state(kind, circuit, state, seed)?;
2917        check_diagnostic_width(&*backend, diagnostic, subsystem_len)?;
2918        apply_fused_circuit(&mut *backend, circuit)?;
2919        return Ok(backend);
2920    }
2921    if !kind.is_auto() {
2922        validate_explicit_backend(kind, circuit)?;
2923    }
2924    let (_, has_partial_independence) = analyze_independence(circuit);
2925    let mut plan = match resolve(kind, circuit, has_partial_independence) {
2926        ExecutionPlan::Backend(plan) => plan,
2927        ExecutionPlan::StabilizerRank => {
2928            return Err(stateless_route(kind, diagnostic, "stabilizer-rank"));
2929        }
2930        ExecutionPlan::StochasticPauli { .. } => {
2931            return Err(stateless_route(kind, diagnostic, "stochastic Pauli"));
2932        }
2933        ExecutionPlan::DeterministicPauli { .. } => {
2934            return Err(stateless_route(kind, diagnostic, "deterministic Pauli"));
2935        }
2936        ExecutionPlan::PauliPath => {
2937            return Err(stateless_route(kind, diagnostic, "Pauli path"));
2938        }
2939    };
2940    if kind.is_auto()
2941        && !plan_answers(&plan, diagnostic)
2942        && circuit.num_qubits <= max_statevector_qubits()
2943    {
2944        plan = plan_for_family(kind, Family::Statevector, circuit.num_qubits);
2945    }
2946    let mut backend: Box<dyn Backend> = plan.build(seed);
2947    check_diagnostic_width(&*backend, diagnostic, subsystem_len)?;
2948    execute(&mut *backend, circuit, &SimOptions::classical_only())?;
2949    Ok(backend)
2950}
2951
2952/// Evaluate `observables` on the backend `kind` resolves to, using that
2953/// backend's own representation.
2954///
2955/// Backends without a native Pauli path report `BackendUnsupported` naming
2956/// themselves, so a request that cannot be served says which engine could not
2957/// serve it rather than blaming the route that picked it.
2958fn expectation_values_native(
2959    kind: &BackendKind,
2960    circuit: &Circuit,
2961    observables: &[Vec<PauliTerm>],
2962    seed: u64,
2963) -> Result<ExpectationResult> {
2964    if !kind.is_auto() {
2965        validate_explicit_backend(kind, circuit)?;
2966    }
2967    // Before the run, matching the statevector path, so a typo in an observable
2968    // does not cost a 40-qubit simulation first.
2969    for observable in observables {
2970        validate_observable(observable, circuit.num_qubits)?;
2971    }
2972
2973    let (_, has_partial_independence) = analyze_independence(circuit);
2974    let ExecutionPlan::Backend(plan) = resolve(kind, circuit, has_partial_independence) else {
2975        return Err(PrismError::IncompatibleBackend {
2976            backend: format!("{kind:?}"),
2977            reason: "expectation values need a backend that holds a state; the stabilizer-rank \
2978                     route returns probabilities only"
2979                .into(),
2980        });
2981    };
2982
2983    let mut backend = plan.build(seed);
2984    if !backend.supports_pauli_expectation() {
2985        return Err(PrismError::BackendUnsupported {
2986            backend: backend.name().to_string(),
2987            operation: "Pauli expectation values".to_string(),
2988        });
2989    }
2990    execute(&mut *backend, circuit, &SimOptions::classical_only())?;
2991    let values = backend.pauli_expectations(observables)?;
2992    Ok(analytic_expectations(values, backend_metadata(&*backend)))
2993}
2994
2995fn expectation_values_statevector(
2996    kind: &BackendKind,
2997    circuit: &Circuit,
2998    observables: &[Vec<PauliTerm>],
2999    seed: u64,
3000) -> Result<ExpectationResult> {
3001    // Validate before the 2^n simulation so bad observables fail cheaply. The
3002    // mask reduction shifts by qubit index, so it follows the `init` that
3003    // rejects a width no mask can address.
3004    for obs in observables {
3005        validate_observable(obs, circuit.num_qubits)?;
3006    }
3007
3008    let accel = accel_for(kind, Family::Statevector, circuit.num_qubits);
3009    let mut backend = build_statevector(&accel, seed);
3010    let expanded = expand_for_backend(&backend, circuit);
3011    let fused = fuse_for_backend(&backend, &expanded);
3012    backend.init(fused.num_qubits, fused.num_classical_bits)?;
3013    let masks = observables
3014        .iter()
3015        .map(|obs| pauli_masks(obs, circuit.num_qubits))
3016        .collect::<Result<Vec<_>>>()?;
3017    backend.apply_instructions(&fused.instructions)?;
3018
3019    let values = match pauli_expectations_on_device(&backend, &masks) {
3020        Some(values) => values?,
3021        None => {
3022            let state = backend.state_vector();
3023            let norm = crate::backend::state_norm_sqr(state);
3024            pauli_expectations_from_masks(state, &masks, norm)
3025        }
3026    };
3027    let metadata = backend_metadata(&backend);
3028    Ok(analytic_expectations(values, metadata))
3029}
3030
3031/// [`pauli_expectations_from_masks`] evaluated on a device-resident state:
3032/// one reduction launch over every mask plus an appended identity mask that
3033/// supplies the norm, so nothing but `16 * (masks.len() + 1)` bytes leaves the
3034/// card. `None` when the state lives on the host.
3035fn pauli_expectations_on_device(
3036    backend: &StatevectorBackend,
3037    masks: &[(usize, usize, u32)],
3038) -> Option<Result<Vec<f64>>> {
3039    if !backend.is_gpu_resident() {
3040        return None;
3041    }
3042    let request: Vec<(u64, u64)> = masks
3043        .iter()
3044        .map(|&(xmask, zmask, _)| (xmask as u64, zmask as u64))
3045        .chain(std::iter::once((0, 0)))
3046        .collect();
3047    let sums = match backend.gpu_pauli_sums(&request)? {
3048        Ok(sums) => sums,
3049        Err(e) => return Some(Err(e)),
3050    };
3051    let norm = sums[masks.len()].re;
3052    if norm == 0.0 {
3053        return Some(Ok(vec![0.0; masks.len()]));
3054    }
3055    Some(Ok(masks
3056        .iter()
3057        .zip(&sums)
3058        .map(|(&(_, _, num_y), sum)| (sum * i_pow(num_y)).re / norm)
3059        .collect()))
3060}
3061
3062/// Multi-shot execution for the distributed statevector backend.
3063///
3064/// Every rank runs this function in lockstep. Circuits with only terminal
3065/// measurements run once and sample basis indices without gathering the dense
3066/// state on any rank. Circuits with mid-circuit measurements run once per shot,
3067/// prefused, with per-shot seeds matching the generic slow path.
3068#[cfg(feature = "distributed")]
3069fn run_shots_distributed(
3070    context: std::sync::Arc<crate::distributed::DistributedContext>,
3071    circuit: &Circuit,
3072    num_shots: usize,
3073    seed: u64,
3074) -> Result<ShotsResult> {
3075    use crate::backend::distributed_statevector::DistributedStatevectorBackend;
3076
3077    let meas_map = circuit.measurement_map();
3078    if meas_map.is_empty() {
3079        // No measurements means every shot is all false, but init must still
3080        // run so invalid rank counts and local qubit floor violations surface as
3081        // errors instead of fabricated output.
3082        let mut backend = DistributedStatevectorBackend::new(context, seed);
3083        backend.init(circuit.num_qubits, circuit.num_classical_bits)?;
3084        return Ok(ShotsResult::from_shots(
3085            vec![vec![false; circuit.num_classical_bits]; num_shots],
3086            circuit.num_classical_bits,
3087        )
3088        .with_metadata(backend_metadata(&backend)));
3089    }
3090
3091    if circuit.has_terminal_measurements_only() {
3092        let stripped = circuit.without_measurements();
3093        let mut backend = DistributedStatevectorBackend::new(context, seed);
3094        execute(&mut backend, &stripped, &SimOptions::classical_only())?;
3095        let samples = backend.sample_basis_states(num_shots, seed)?;
3096        return Ok(ShotsResult::from_shots(
3097            shots_from_basis_samples(&samples, &meas_map, circuit.num_classical_bits),
3098            circuit.num_classical_bits,
3099        )
3100        .with_metadata(backend_metadata(&backend)));
3101    }
3102
3103    let probe = DistributedStatevectorBackend::new(context.clone(), seed);
3104    let expanded = expand_for_backend(&probe, circuit);
3105    let fused = fuse_for_backend(&probe, &expanded);
3106    let opts = SimOptions::classical_only();
3107    let mut shots = Vec::with_capacity(num_shots);
3108    let mut metadata = RunMetadata::exact(ResolvedBackend::Distributed);
3109    for i in 0..num_shots {
3110        let shot_seed = seed.wrapping_add(i as u64);
3111        let mut backend = DistributedStatevectorBackend::new(context.clone(), shot_seed);
3112        let result = execute_circuit(&mut backend, &fused, &opts)?;
3113        metadata.weaken_with(&result.metadata);
3114        shots.push(result.classical_bits);
3115    }
3116    Ok(ShotsResult::from_shots(shots, circuit.num_classical_bits).with_metadata(metadata))
3117}
3118
3119/// Execute a circuit multiple times with explicit backend selection.
3120pub(crate) fn run_shots_with(
3121    kind: BackendKind,
3122    circuit: &Circuit,
3123    num_shots: usize,
3124    seed: u64,
3125) -> Result<ShotsResult> {
3126    // The distributed backend runs every rank in lockstep, so shot execution
3127    // must not route through shortcuts that reshape the collective call
3128    // sequence. Dispatch directly.
3129    #[cfg(feature = "distributed")]
3130    if let BackendKind::StatevectorDistributed { context } = &kind {
3131        return run_shots_distributed(context.clone(), circuit, num_shots, seed);
3132    }
3133
3134    // Once per shots call, not per shot: a guard that cannot depend on a
3135    // measurement is resolved here so the sampling predicates below see the
3136    // circuit that actually runs. A circuit with no guard borrows through.
3137    let folded = circuit.fold_static_guards();
3138    let circuit = folded.as_ref();
3139
3140    let bits = circuit.num_classical_bits;
3141    let source = prepare_shot_source(&kind, circuit, num_shots, seed)?;
3142    let Some(metadata) = source.metadata() else {
3143        return run_shots_per_shot(kind, circuit, num_shots, seed);
3144    };
3145    let result = match source {
3146        ShotSource::Compiled {
3147            mut sampler,
3148            meas_map,
3149            ..
3150        } => {
3151            let packed = sampler.try_sample_bulk_packed(num_shots)?;
3152            ShotsResult::from_shots(
3153                packed_shots_to_classical_bits(&packed, &meas_map, bits),
3154                bits,
3155            )
3156        }
3157        ShotSource::TerminalStatevector { backend, meas_map } => {
3158            let shots = if backend.is_gpu_resident() {
3159                let probs = backend.probabilities()?;
3160                sample_shots_from_probs(&probs, &meas_map, bits, num_shots, seed)
3161            } else {
3162                sample_shots_from_state(
3163                    backend.state_vector(),
3164                    backend.probability_scale(),
3165                    &meas_map,
3166                    bits,
3167                    num_shots,
3168                    seed,
3169                )
3170            };
3171            ShotsResult::from_shots(shots, bits)
3172        }
3173        ShotSource::Native {
3174            mut backend,
3175            meas_map,
3176        } => {
3177            let samples = backend.sample_basis_states(num_shots, seed)?;
3178            ShotsResult::from_shots(shots_from_basis_samples(&samples, &meas_map, bits), bits)
3179        }
3180        ShotSource::TerminalProbabilities {
3181            probs, meas_map, ..
3182        } => ShotsResult::from_shots(sample_shots(&probs, &meas_map, bits, num_shots, seed), bits),
3183        ShotSource::StabilizerRank => {
3184            stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed)?
3185        }
3186        ShotSource::PerShot => unreachable!("handled above"),
3187    };
3188    Ok(result.with_metadata(metadata))
3189}
3190
3191/// Run `circuit` once per shot, which mid-circuit measurements force.
3192fn run_shots_per_shot(
3193    kind: BackendKind,
3194    circuit: &Circuit,
3195    num_shots: usize,
3196    seed: u64,
3197) -> Result<ShotsResult> {
3198    // Pre-compute seed-independent analysis to avoid redundant work.
3199    if !kind.is_auto() {
3200        validate_explicit_backend(&kind, circuit)?;
3201    }
3202
3203    let (decompose, has_partial_independence) = analyze_independence(circuit);
3204
3205    if matches!(kind, BackendKind::StabilizerRank) {
3206        return stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed);
3207    }
3208    if matches!(
3209        kind,
3210        BackendKind::StochasticPauli { .. } | BackendKind::DeterministicPauli { .. }
3211    ) {
3212        return Err(crate::error::PrismError::IncompatibleBackend {
3213            backend: format!("{kind:?}"),
3214            reason: "Pauli propagation backends do not support mid-circuit measurements".into(),
3215        });
3216    }
3217    if kind.is_auto() && auto_stabilizer_rank_t_count(circuit, MAX_AUTO_T_COUNT_SHOTS).is_some() {
3218        return stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed);
3219    }
3220
3221    if has_temporal_clifford_opportunity(&kind, circuit) {
3222        if decompose.is_none() {
3223            if let Some(tc) = plan_temporal_clifford(&kind, circuit) {
3224                let route = ResolvedBackend::Statevector;
3225                return collect_shots(circuit, num_shots, seed, route, |shot_seed| {
3226                    let outcome = run_temporal_clifford(&tc, shot_seed, false)?;
3227                    Ok((outcome.classical_bits, outcome.metadata))
3228                });
3229            }
3230        }
3231        // Decomposable circuits with a temporal prefix keep the per-shot
3232        // full-pipeline route; the prefix spans blocks that decomposition
3233        // would otherwise split.
3234        let opts = SimOptions::classical_only();
3235        let route = resolve_backend(&kind, circuit, has_partial_independence).resolved();
3236        let plan = plan_probability_route(&kind, circuit);
3237        return collect_shots(circuit, num_shots, seed, route, |shot_seed| {
3238            let outcome = run_route(&kind, circuit, shot_seed, opts, &plan)?;
3239            Ok((outcome.classical_bits, outcome.metadata))
3240        });
3241    }
3242
3243    let opts = SimOptions::classical_only();
3244
3245    if let Some(ref comps) = decompose {
3246        let partitions = circuit.partition_subcircuits(comps);
3247        let block_plans: Vec<BackendPlan> = partitions
3248            .iter()
3249            .map(|(sub, _, _)| {
3250                if !kind.is_auto() {
3251                    validate_explicit_backend(&kind, sub)?;
3252                }
3253                Ok(resolve_backend(&kind, sub, false))
3254            })
3255            .collect::<Result<_>>()?;
3256        let fused_blocks: Vec<std::borrow::Cow<'_, Circuit>> = partitions
3257            .iter()
3258            .zip(&block_plans)
3259            .map(|((sub, _, _), plan)| {
3260                let probe = plan.build(seed);
3261                let expanded = expand_for_backend(&*probe, sub);
3262                std::borrow::Cow::Owned(fuse_for_backend(&*probe, &expanded).into_owned())
3263            })
3264            .collect();
3265
3266        collect_shots(
3267            circuit,
3268            num_shots,
3269            seed,
3270            ResolvedBackend::Decomposed,
3271            |shot_seed| {
3272                let result = run_decomposed_prefused(
3273                    &block_plans,
3274                    comps,
3275                    &partitions,
3276                    &fused_blocks,
3277                    shot_seed,
3278                    &opts,
3279                    circuit,
3280                )?;
3281                Ok((result.classical_bits, result.metadata))
3282            },
3283        )
3284    } else {
3285        let plan = resolve_backend(&kind, circuit, has_partial_independence);
3286        let probe = plan.build(seed);
3287        let expanded = expand_for_backend(&*probe, circuit);
3288        let fused = fuse_for_backend(&*probe, &expanded);
3289
3290        collect_shots(circuit, num_shots, seed, plan.resolved(), |shot_seed| {
3291            let mut backend = plan.build(shot_seed);
3292            let outcome = execute_circuit(&mut *backend, &fused, &opts)?;
3293            Ok((outcome.classical_bits, outcome.metadata))
3294        })
3295    }
3296}
3297
3298/// Each shot evolves its own state, so `shot` returns the provenance of its own
3299/// run and the ensemble keeps the weakest claim across them. `route` names the
3300/// engine for a zero-shot request, which runs nothing to read provenance off.
3301fn collect_shots(
3302    circuit: &Circuit,
3303    num_shots: usize,
3304    seed: u64,
3305    route: ResolvedBackend,
3306    mut shot: impl FnMut(u64) -> Result<(Vec<bool>, RunMetadata)>,
3307) -> Result<ShotsResult> {
3308    let mut shots = Vec::with_capacity(num_shots);
3309    let mut metadata = RunMetadata::exact(route);
3310    for i in 0..num_shots {
3311        let (bits, shot_metadata) = shot(seed.wrapping_add(i as u64))?;
3312        if i == 0 {
3313            metadata = shot_metadata;
3314        } else {
3315            metadata.weaken_with(&shot_metadata);
3316        }
3317        shots.push(bits);
3318    }
3319    Ok(ShotsResult::from_shots(shots, circuit.num_classical_bits).with_metadata(metadata))
3320}
3321
3322/// Family choice for auto-routed non-Pauli noise trajectories. Restricted to
3323/// families whose trajectory operations (1q Kraus, qubit probability, reduced
3324/// density matrix, reset) are supported; the statevector leaf carries the
3325/// kind's acceleration.
3326fn general_noise_plan(kind: &BackendKind, circuit: &Circuit) -> BackendPlan {
3327    let family = if !circuit.has_entangling_gates() {
3328        Family::ProductState
3329    } else if circuit.num_qubits > max_statevector_qubits() {
3330        if circuit.is_sparse_friendly() && circuit.num_qubits <= MAX_SPARSE_INDEX_QUBITS {
3331            Family::Sparse
3332        } else {
3333            Family::Mps
3334        }
3335    } else {
3336        Family::Statevector
3337    };
3338    plan_for_family(kind, family, circuit.num_qubits)
3339}
3340
3341/// Execute a noisy circuit for multiple shots with explicit backend selection.
3342///
3343/// For Clifford circuits with Auto/Stabilizer/FactoredStabilizer backends,
3344/// uses the compiled noisy sampler (fast O(n²·m) compile + O(events·m/64) per shot).
3345/// For all other cases, falls back to per-shot simulation with noise injection.
3346/// The compiled noisy path is limited to terminal measurements with no resets
3347/// or classical conditionals.
3348pub(crate) fn run_shots_with_noise(
3349    kind: BackendKind,
3350    circuit: &Circuit,
3351    noise_model: &noise::NoiseModel,
3352    num_shots: usize,
3353    seed: u64,
3354) -> Result<ShotsResult> {
3355    noise_model.validate_for(circuit)?;
3356
3357    // Trajectory execution runs shots on Rayon worker threads, whose
3358    // scheduling order differs per rank. Per-shot distributed backends would
3359    // issue collectives out of lockstep and deadlock or corrupt exchanges.
3360    // Reject until a lockstep noisy path exists.
3361    #[cfg(feature = "distributed")]
3362    if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
3363        return Err(crate::error::PrismError::IncompatibleBackend {
3364            backend: format!("{kind:?}"),
3365            reason: "noisy shot sampling is not supported on the distributed backend; \
3366                     trajectory execution cannot keep rank collectives in lockstep"
3367                .into(),
3368        });
3369    }
3370
3371    if kind.is_density_matrix() {
3372        let probs = exact_noisy_probabilities(&kind, circuit, noise_model, None, seed)?;
3373        return Ok(ShotsResult::from_shots(
3374            sample_exact_noisy_shots(&probs, circuit, noise_model, num_shots, seed),
3375            circuit.num_classical_bits,
3376        )
3377        .with_metadata(exact_mixture_metadata(&kind)));
3378    }
3379
3380    if !kind.supports_noisy_per_shot() {
3381        return Err(crate::error::PrismError::IncompatibleBackend {
3382            backend: format!("{kind:?}"),
3383            reason: "this backend holds no per-shot pure state to inject noise into; select \
3384                     DensityMatrix for the exact mixed state, or a backend that evolves one \
3385                     state per trajectory"
3386                .into(),
3387        });
3388    }
3389
3390    let is_stabilizer_kind = kind.is_stabilizer_family();
3391
3392    if is_stabilizer_kind && !noise_model.has_only_pauli_channels() {
3393        return Err(crate::error::PrismError::IncompatibleBackend {
3394            backend: format!("{kind:?}"),
3395            reason: format!(
3396                "stabilizer backends only support Pauli/depolarizing noise; use {} for amplitude damping, phase damping, thermal relaxation, or custom Kraus",
3397                BackendKind::general_noise_backend_names()
3398            ),
3399        });
3400    }
3401
3402    if !noise_model.has_only_pauli_channels() && !kind.supports_general_noise() {
3403        return Err(crate::error::PrismError::IncompatibleBackend {
3404            backend: format!("{kind:?}"),
3405            reason: format!(
3406                "non-Pauli noise requires {}",
3407                BackendKind::general_noise_backend_names()
3408            ),
3409        });
3410    }
3411
3412    if is_stabilizer_kind && !circuit.is_clifford_only() {
3413        return Err(crate::error::PrismError::IncompatibleBackend {
3414            backend: format!("{kind:?}"),
3415            reason: "circuit contains non-Clifford gates".into(),
3416        });
3417    }
3418
3419    if !kind.is_auto() {
3420        validate_explicit_backend(&kind, circuit)?;
3421    }
3422
3423    if noise_model.has_only_pauli_channels() {
3424        let use_compiled = (kind.is_auto()
3425            || matches!(
3426                kind,
3427                BackendKind::Stabilizer | BackendKind::FactoredStabilizer
3428            ))
3429            && supports_compiled_measurement_sampling(circuit)
3430            || {
3431                #[cfg(feature = "gpu")]
3432                {
3433                    matches!(kind, BackendKind::StabilizerGpu { .. })
3434                        && supports_compiled_measurement_sampling(circuit)
3435                }
3436                #[cfg(not(feature = "gpu"))]
3437                {
3438                    false
3439                }
3440            };
3441
3442        if use_compiled {
3443            #[cfg(feature = "gpu")]
3444            if let BackendKind::StabilizerGpu { context } = &kind {
3445                return noise::run_shots_noisy_with_gpu(
3446                    circuit,
3447                    noise_model,
3448                    num_shots,
3449                    seed,
3450                    context.clone(),
3451                );
3452            }
3453            return noise::run_shots_noisy(circuit, noise_model, num_shots, seed);
3454        }
3455    }
3456
3457    let plan = if kind.is_auto() && !noise_model.has_only_pauli_channels() {
3458        general_noise_plan(&kind, circuit)
3459    } else {
3460        resolve_backend(&kind, circuit, false)
3461    };
3462    // Noise events are indexed per instruction, so trajectories apply the
3463    // stream raw and the Pauli-rotation lowering pass cannot run. The
3464    // statevector applies the gate natively (its device path lowers inline);
3465    // any other backend without the kernel is rejected before a shot starts.
3466    let has_pauli_rot = crate::circuit::any_gate(&circuit.instructions, &mut |gate| {
3467        matches!(gate, crate::gates::Gate::PauliRot(_))
3468    });
3469    if has_pauli_rot
3470        && !matches!(plan, BackendPlan::Statevector { .. })
3471        && !plan.build(seed).supports_pauli_rotation()
3472    {
3473        return Err(crate::error::PrismError::IncompatibleBackend {
3474            backend: format!("{:?}", plan.resolved()),
3475            reason: "noisy trajectories apply the instruction stream raw so noise events \
3476                     stay aligned to it, which leaves no room for the Pauli-rotation \
3477                     lowering this backend needs; run on the statevector, or expand the \
3478                     rotations with circuit::expand_pauli_rotations and attach the noise \
3479                     model to the expanded circuit"
3480                .into(),
3481        });
3482    }
3483    // A correlated two-qubit Kraus branch reads a two-qubit reduced density
3484    // matrix, which only the host statevector answers. Without this the model
3485    // clears every routing gate and fails part way through the first shot, on a
3486    // backend an Auto route may have picked for the caller.
3487    if noise_model.has_two_qubit_kraus() && !plan.build(seed).supports_two_qubit_kraus() {
3488        return Err(crate::error::PrismError::IncompatibleBackend {
3489            backend: format!("{:?}", plan.resolved()),
3490            reason: "a two-qubit Kraus channel needs the two-qubit reduced density matrix \
3491                     its branch probabilities are drawn from, which only the host \
3492                     statevector provides; run on BackendKind::Statevector, or evaluate \
3493                     the channel exactly on BackendKind::DensityMatrix"
3494                .into(),
3495        });
3496    }
3497    let route = plan.resolved();
3498    trajectory::run_trajectories(
3499        |s| plan.build(s),
3500        circuit,
3501        noise_model,
3502        num_shots,
3503        seed,
3504        plan.is_gpu(),
3505        route,
3506    )
3507}
3508
3509#[cfg(test)]
3510mod tests;
3511
3512#[cfg(all(test, feature = "gpu"))]
3513mod gpu_stub_tests;
3514
3515#[cfg(test)]
3516mod terminal_candidate_matrix_tests;
3517
3518#[cfg(test)]
3519mod diagnostic_terminal_tests;