Skip to main content

prism_q/sim/
metadata.rs

1//! Provenance carried by every simulation result: which engine ran, whether the
2//! answer is exact, and where the state lived.
3
4/// Expectation values with the uncertainty of the estimate that produced them.
5///
6/// Returned by [`Simulate::expectation_values_reported`]; the plain
7/// [`Simulate::expectation_values`] returns the values alone.
8///
9/// [`Simulate::expectation_values`]: crate::sim::Simulate::expectation_values
10/// [`Simulate::expectation_values_reported`]: crate::sim::Simulate::expectation_values_reported
11#[derive(Debug, Clone)]
12pub struct ExpectationResult {
13    /// One value per observable, in the order they were supplied.
14    pub values: Vec<f64>,
15    /// One standard error per value, `None` when the route evaluates rather
16    /// than samples. Such a route reports exactness through
17    /// [`RunMetadata::exactness`] instead of an interval of width zero.
18    pub std_errors: Option<Vec<f64>>,
19    pub metadata: RunMetadata,
20}
21
22impl ExpectationResult {
23    pub fn into_values(self) -> Vec<f64> {
24        self.values
25    }
26}
27
28/// Engine a run resolved to, after [`BackendKind::Auto`] dispatch.
29///
30/// [`BackendKind::Auto`]: crate::sim::BackendKind::Auto
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum ResolvedBackend {
33    ProductState,
34    Stabilizer,
35    FactoredStabilizer,
36    Sparse,
37    Mps,
38    Factored,
39    TensorNetwork,
40    Statevector,
41    DensityMatrix,
42    Distributed,
43    StabilizerRank,
44    StochasticPauli,
45    DeterministicPauli,
46    /// Heisenberg Pauli propagation through a noise model.
47    PauliPath,
48    /// The compiled Clifford sampler, which answers from a propagated parity
49    /// map rather than from a `Backend`.
50    CompiledStabilizer,
51    /// One engine per independent block, merged. The decomposed route holds no
52    /// joint state, so no single engine names the result.
53    Decomposed,
54    /// A backend outside the built-in set, named by [`Backend::name`].
55    ///
56    /// [`Backend::name`]: crate::backend::Backend::name
57    Other(&'static str),
58}
59
60/// Sampler behind a result whose [`ResolvedBackend`] covers more than one.
61///
62/// [`ResolvedBackend::CompiledStabilizer`] is stamped by four samplers: the
63/// noiseless compiled sampler, and three that the noisy shot entry point picks
64/// between at run time on the shot count and the depth ratio. The backend alone
65/// does not say which ran; each variant names the sampler that did.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum Engine {
68    /// `CompiledSampler`: the noiseless parity map.
69    CompiledSampler,
70    /// `NoisyCompiledSampler`: the parity map with the flip tables folded in.
71    NoisyCompiledSampler,
72    /// The Pauli frame sampler, which replays a reference record per batch.
73    FrameSampler,
74    /// `HomologicalSampler`: syndrome classes precomputed, O(1) per shot.
75    HomologicalSampler,
76}
77
78/// Whether a result is exact for the circuit as given.
79#[derive(Debug, Clone, Copy, PartialEq)]
80pub enum Exactness {
81    Exact,
82    /// The engine that ran can discard state weight or estimate by sampling.
83    ///
84    /// This marks the route, not the run: an MPS at bond 256 on a circuit that
85    /// never fills a bond truncates nothing and still reports `Approximate`,
86    /// with `fidelity_lower_bound` of 1.0. The variant answers whether the
87    /// answer could have been approximated, the bound answers whether it was.
88    /// `None` means the engine reports no bound.
89    Approximate {
90        fidelity_lower_bound: Option<f64>,
91    },
92}
93
94/// Where the state lived during the run.
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum Placement {
97    Host,
98    Device,
99}
100
101/// How a result was produced, attached to every [`Simulate`] result type.
102///
103/// A GPU-attached run below the device crossover reports [`Placement::Host`],
104/// and automatic dispatch onto an approximate backend reports
105/// [`Exactness::Approximate`].
106///
107/// [`Simulate`]: crate::sim::Simulate
108#[derive(Debug, Clone)]
109pub struct RunMetadata {
110    pub backend: ResolvedBackend,
111    /// Which sampler ran, when `backend` is a label several share. `None`
112    /// when the backend is the whole answer, which is every backend other than
113    /// [`ResolvedBackend::CompiledStabilizer`].
114    pub engine: Option<Engine>,
115    pub exactness: Exactness,
116    pub placement: Placement,
117    /// Shots drawn, for a result estimated by sampling. `None` for an analytic
118    /// result.
119    pub shots: Option<usize>,
120}
121
122impl RunMetadata {
123    pub(crate) fn new(
124        backend: ResolvedBackend,
125        exactness: Exactness,
126        placement: Placement,
127    ) -> Self {
128        Self {
129            backend,
130            engine: None,
131            exactness,
132            placement,
133            shots: None,
134        }
135    }
136
137    pub(crate) fn exact(backend: ResolvedBackend) -> Self {
138        Self::new(backend, Exactness::Exact, Placement::Host)
139    }
140
141    pub(crate) fn approximate(backend: ResolvedBackend) -> Self {
142        Self::new(
143            backend,
144            Exactness::Approximate {
145                fidelity_lower_bound: None,
146            },
147            Placement::Host,
148        )
149    }
150
151    pub(crate) fn with_shots(mut self, shots: usize) -> Self {
152        self.shots = Some(shots);
153        self
154    }
155
156    pub(crate) fn with_engine(mut self, engine: Engine) -> Self {
157        self.engine = Some(engine);
158        self
159    }
160
161    pub fn is_exact(&self) -> bool {
162        matches!(self.exactness, Exactness::Exact)
163    }
164
165    /// Reported fidelity floor for the produced state, `None` when the result
166    /// is exact or the engine reports none.
167    ///
168    /// For the MPS backend this is 1 minus the summed per-SVD relative
169    /// discarded weights: a first-order truncation estimate, not a
170    /// certificate. Errors compound across SVDs, and summing the weights
171    /// understates the compounded error, since the strict bound on the
172    /// infidelity is the square of the summed square roots. The two agree
173    /// only when a single SVD truncates. The sparse backend at a raised
174    /// pruning threshold reports 1 minus the absolute squared weight it
175    /// dropped, the same first-order estimate; it renormalizes what it keeps,
176    /// so the dropped weight is the whole of the error it reports. The
177    /// budgeted Pauli engines leave this `None` and report their additive
178    /// observable bound on the result types instead.
179    pub fn fidelity_lower_bound(&self) -> Option<f64> {
180        match self.exactness {
181            Exactness::Exact => None,
182            Exactness::Approximate {
183                fidelity_lower_bound,
184            } => fidelity_lower_bound,
185        }
186    }
187
188    /// Fold in one more run of the same route, keeping the weaker claim. Shots
189    /// on a per-shot route each evolve their own state, so the ensemble is only
190    /// as exact as its worst member and the bound is a minimum, not a product.
191    pub(crate) fn weaken_with(&mut self, other: &RunMetadata) {
192        if other.placement != self.placement {
193            self.placement = Placement::Host;
194        }
195        match (self.exactness, other.exactness) {
196            (_, Exactness::Exact) => {}
197            (Exactness::Exact, approx) => self.exactness = approx,
198            (
199                Exactness::Approximate {
200                    fidelity_lower_bound: a,
201                },
202                Exactness::Approximate {
203                    fidelity_lower_bound: b,
204                },
205            ) => {
206                self.exactness = Exactness::Approximate {
207                    fidelity_lower_bound: match (a, b) {
208                        (Some(a), Some(b)) => Some(a.min(b)),
209                        _ => None,
210                    },
211                };
212            }
213        }
214    }
215
216    /// Metadata for a run whose blocks were evolved separately. Each block holds
217    /// part of one state, so the bound is the product; exactness is the weakest
218    /// of the parts and placement is `Device` only when every part was, so the
219    /// merged result never claims more than the block that claimed least. A part
220    /// that reports no bound poisons the product, since an unquantified
221    /// truncation bounds nothing.
222    pub(crate) fn decomposed(parts: impl IntoIterator<Item = RunMetadata>) -> Self {
223        let mut exact = true;
224        let mut bound = Some(1.0f64);
225        let mut all_device = true;
226        for part in parts {
227            all_device &= part.placement == Placement::Device;
228            if let Exactness::Approximate {
229                fidelity_lower_bound,
230            } = part.exactness
231            {
232                exact = false;
233                bound = match (bound, fidelity_lower_bound) {
234                    (Some(acc), Some(b)) => Some(acc * b),
235                    _ => None,
236                };
237            }
238        }
239        let exactness = if exact {
240            Exactness::Exact
241        } else {
242            Exactness::Approximate {
243                fidelity_lower_bound: bound,
244            }
245        };
246        let placement = if all_device {
247            Placement::Device
248        } else {
249            Placement::Host
250        };
251        Self::new(ResolvedBackend::Decomposed, exactness, placement)
252    }
253}