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