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}