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)]
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}