Skip to main content

prism_q/sim/
shots.rs

1//! Shot collections and probability-weighted shot sampling.
2
3use std::collections::HashMap;
4
5use rand::RngExt;
6use rand::SeedableRng;
7use rand_chacha::ChaCha8Rng;
8
9use super::Probabilities;
10use super::compiled;
11use super::metadata::{ResolvedBackend, RunMetadata};
12
13/// Marker on a result no route has stamped yet; `every_terminal_stamps_metadata`
14/// asserts none reaches a caller.
15pub(crate) const UNSTAMPED: &str = "unstamped";
16
17/// Result of a multi-shot simulation run.
18#[derive(Debug, Clone)]
19pub struct ShotsResult {
20    /// `shots[i][j]` is the j-th classical bit from the i-th shot.
21    pub shots: Vec<Vec<bool>>,
22    pub(crate) num_classical_bits: usize,
23    /// Which engine ran, whether the answer is exact, and where the state lived.
24    pub metadata: RunMetadata,
25}
26
27impl ShotsResult {
28    /// Build with placeholder provenance; the entry point that picked the
29    /// engine overwrites it through [`ShotsResult::with_metadata`], which
30    /// `every_terminal_stamps_metadata` pins.
31    pub(crate) fn from_shots(shots: Vec<Vec<bool>>, num_classical_bits: usize) -> Self {
32        Self {
33            shots,
34            num_classical_bits,
35            metadata: RunMetadata::exact(ResolvedBackend::Other(UNSTAMPED)),
36        }
37    }
38
39    pub(crate) fn with_metadata(mut self, metadata: RunMetadata) -> Self {
40        let shots = self.shots.len();
41        self.metadata = metadata.with_shots(shots);
42        self
43    }
44
45    /// Keys are packed `Vec<u64>` where bit `i` of word `i/64` corresponds
46    /// to classical bit `i`. Use [`bitstring`] to format keys for display.
47    pub fn counts(&self) -> HashMap<Vec<u64>, u64> {
48        let m_words = self.num_classical_bits.div_ceil(64).max(1);
49        let mut counts: HashMap<Vec<u64>, u64> = HashMap::new();
50        for shot in &self.shots {
51            let mut key = vec![0u64; m_words];
52            for (i, &b) in shot.iter().enumerate() {
53                if b {
54                    key[i / 64] |= 1u64 << (i % 64);
55                }
56            }
57            *counts.entry(key).or_insert(0) += 1;
58        }
59        counts
60    }
61
62    pub fn num_shots(&self) -> usize {
63        self.shots.len()
64    }
65
66    pub fn num_classical_bits(&self) -> usize {
67        self.num_classical_bits
68    }
69}
70
71#[cfg(test)]
72impl ShotsResult {
73    pub(crate) fn marginal(&self, bit: usize) -> f64 {
74        self.shots.iter().filter(|s| s[bit]).count() as f64 / self.shots.len() as f64
75    }
76
77    pub(crate) fn coherent_fraction(&self) -> f64 {
78        self.shots
79            .iter()
80            .filter(|s| s.iter().all(|&b| b == s[0]))
81            .count() as f64
82            / self.shots.len() as f64
83    }
84}
85
86impl std::fmt::Display for ShotsResult {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        let counts = self.counts();
89        let mut entries: Vec<_> = counts.into_iter().collect();
90        entries.sort_by_key(|e| std::cmp::Reverse(e.1));
91        for (bits, count) in &entries {
92            let bs = bitstring(bits, self.num_classical_bits);
93            writeln!(f, "{bs}: {count}")?;
94        }
95        Ok(())
96    }
97}
98
99/// Format a packed `Vec<u64>` key (from [`ShotsResult::counts`]) as a binary string.
100///
101/// Bit 0 of the first word corresponds to classical bit 0 (leftmost character).
102pub fn bitstring(key: &[u64], num_bits: usize) -> String {
103    let mut s = String::with_capacity(num_bits);
104    for i in 0..num_bits {
105        let word = i / 64;
106        let bit = i % 64;
107        if word < key.len() && (key[word] >> bit) & 1 == 1 {
108            s.push('1');
109        } else {
110            s.push('0');
111        }
112    }
113    s
114}
115
116pub(crate) fn build_cdf(probs: &[f64]) -> Vec<f64> {
117    let mut cdf = Vec::with_capacity(probs.len());
118    let mut acc = 0.0;
119    for &p in probs {
120        acc += p;
121        cdf.push(acc);
122    }
123    if let Some(last) = cdf.last_mut() {
124        *last = 1.0;
125    }
126    cdf
127}
128
129pub(crate) fn sample_from_cdf(cdf: &[f64], r: f64) -> usize {
130    match cdf.binary_search_by(|p| p.partial_cmp(&r).unwrap_or(std::cmp::Ordering::Equal)) {
131        Ok(i) => i,
132        Err(i) => i.min(cdf.len() - 1),
133    }
134}
135
136pub(crate) fn sample_shots(
137    probs: &Probabilities,
138    meas_map: &[(usize, usize)],
139    num_classical_bits: usize,
140    num_shots: usize,
141    seed: u64,
142) -> Vec<Vec<bool>> {
143    let mut rng = ChaCha8Rng::seed_from_u64(seed);
144
145    if meas_map.is_empty() {
146        return vec![vec![false; num_classical_bits]; num_shots];
147    }
148
149    let mut shots = vec![vec![false; num_classical_bits]; num_shots];
150
151    match probs {
152        Probabilities::Dense(v) => {
153            let cdf = build_cdf(v);
154            for shot in &mut shots {
155                let r: f64 = rng.random();
156                let state_idx = sample_from_cdf(&cdf, r);
157                for &(qubit, cbit) in meas_map {
158                    shot[cbit] = (state_idx >> qubit) & 1 == 1;
159                }
160            }
161        }
162        Probabilities::Factored { blocks, .. } => {
163            let block_cdfs: Vec<Vec<f64>> = blocks.iter().map(|b| build_cdf(&b.probs)).collect();
164            for shot in &mut shots {
165                let mut global_idx = 0usize;
166                for (block, cdf) in blocks.iter().zip(block_cdfs.iter()) {
167                    let r: f64 = rng.random();
168                    let local_idx = sample_from_cdf(cdf, r);
169                    let mut m = block.mask;
170                    let mut bit = 0;
171                    while m != 0 {
172                        let pos = m.trailing_zeros() as usize;
173                        if local_idx & (1 << bit) != 0 {
174                            global_idx |= 1 << pos;
175                        }
176                        bit += 1;
177                        m &= m.wrapping_sub(1);
178                    }
179                }
180                for &(qubit, cbit) in meas_map {
181                    shot[cbit] = (global_idx >> qubit) & 1 == 1;
182                }
183            }
184        }
185    }
186
187    shots
188}
189
190/// Project packed per-qubit outcomes from a native backend sampler onto
191/// classical bits. Later measurements of the same classical bit win, matching
192/// [`sample_shots`].
193pub(crate) fn shots_from_basis_samples(
194    samples: &crate::backend::BasisSamples,
195    meas_map: &[(usize, usize)],
196    num_classical_bits: usize,
197) -> Vec<Vec<bool>> {
198    let dense_identity_map = meas_map.len() == num_classical_bits
199        && meas_map
200            .iter()
201            .enumerate()
202            .all(|(idx, &(qubit, classical_bit))| idx == qubit && idx == classical_bit);
203    if dense_identity_map {
204        return samples.to_shots(num_classical_bits);
205    }
206
207    let mut shots = vec![vec![false; num_classical_bits]; samples.num_shots()];
208    for (index, shot) in shots.iter_mut().enumerate() {
209        for &(qubit, cbit) in meas_map {
210            shot[cbit] = samples.bit(index, qubit);
211        }
212    }
213    shots
214}
215
216pub(super) fn packed_shots_to_classical_bits(
217    packed: &compiled::PackedShots,
218    meas_map: &[(usize, usize)],
219    num_classical_bits: usize,
220) -> Vec<Vec<bool>> {
221    let dense_identity_map = meas_map.len() == num_classical_bits
222        && meas_map
223            .iter()
224            .enumerate()
225            .all(|(idx, &(_, classical_bit))| idx == classical_bit);
226    if dense_identity_map {
227        return packed.to_shots();
228    }
229
230    let mut shots = vec![vec![false; num_classical_bits]; packed.num_shots()];
231    let mut seen = vec![false; num_classical_bits];
232    let unique_classical_bits = meas_map.iter().all(|&(_, classical_bit)| {
233        if classical_bit >= num_classical_bits {
234            return true;
235        }
236        if seen[classical_bit] {
237            false
238        } else {
239            seen[classical_bit] = true;
240            true
241        }
242    });
243
244    if unique_classical_bits {
245        match packed.layout() {
246            compiled::ShotLayout::ShotMajor => {
247                let m_words = packed.m_words();
248                let data = packed.raw_data();
249                for (shot_idx, shot) in shots.iter_mut().enumerate() {
250                    let row = &data[shot_idx * m_words..(shot_idx + 1) * m_words];
251                    for (measurement, &(_, classical_bit)) in meas_map.iter().enumerate() {
252                        if classical_bit >= num_classical_bits {
253                            continue;
254                        }
255                        let word = row[measurement / 64];
256                        shot[classical_bit] = (word >> (measurement % 64)) & 1 != 0;
257                    }
258                }
259            }
260            compiled::ShotLayout::MeasMajor => {
261                let s_words = packed.s_words();
262                let data = packed.raw_data();
263                let tail = packed.num_shots() % 64;
264                let last_mask = if tail == 0 {
265                    u64::MAX
266                } else {
267                    (1u64 << tail) - 1
268                };
269                for (measurement, &(_, classical_bit)) in meas_map.iter().enumerate() {
270                    if classical_bit >= num_classical_bits {
271                        continue;
272                    }
273                    let row = &data[measurement * s_words..(measurement + 1) * s_words];
274                    for (sw, mut bits) in row.iter().copied().enumerate() {
275                        if sw + 1 == s_words {
276                            bits &= last_mask;
277                        }
278                        while bits != 0 {
279                            let shot = sw * 64 + bits.trailing_zeros() as usize;
280                            shots[shot][classical_bit] = true;
281                            bits &= bits - 1;
282                        }
283                    }
284                }
285            }
286        }
287        return shots;
288    }
289
290    for (measurement, &(_, classical_bit)) in meas_map.iter().enumerate() {
291        if classical_bit >= num_classical_bits {
292            continue;
293        }
294        for (shot_idx, shot) in shots.iter_mut().enumerate() {
295            shot[classical_bit] = packed.get_bit(shot_idx, measurement);
296        }
297    }
298    shots
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use crate::sim::compiled::PackedShots;
305    use crate::sim::probability::{FactoredBlock, Probabilities};
306
307    fn basis_samples_fixture(num_shots: usize, num_qubits: usize) -> crate::backend::BasisSamples {
308        let mut samples = crate::backend::BasisSamples::new(num_shots, num_qubits);
309        for shot in 0..num_shots {
310            for qubit in 0..num_qubits {
311                if (shot * 31 + qubit * 7) % 3 == 0 {
312                    samples.set(shot, qubit);
313                }
314            }
315        }
316        samples
317    }
318
319    fn shots_per_bit(
320        samples: &crate::backend::BasisSamples,
321        meas_map: &[(usize, usize)],
322        num_classical_bits: usize,
323    ) -> Vec<Vec<bool>> {
324        let mut shots = vec![vec![false; num_classical_bits]; samples.num_shots()];
325        for (index, shot) in shots.iter_mut().enumerate() {
326            for &(qubit, cbit) in meas_map {
327                shot[cbit] = samples.bit(index, qubit);
328            }
329        }
330        shots
331    }
332
333    #[test]
334    fn basis_sample_fast_path_matches_per_bit_expansion() {
335        for num_qubits in [4usize, 64, 70, 129] {
336            let samples = basis_samples_fixture(17, num_qubits);
337            let meas_map: Vec<(usize, usize)> = (0..num_qubits).map(|q| (q, q)).collect();
338            assert_eq!(
339                shots_from_basis_samples(&samples, &meas_map, num_qubits),
340                shots_per_bit(&samples, &meas_map, num_qubits)
341            );
342        }
343    }
344
345    #[test]
346    fn basis_sample_fast_path_ignores_qubits_above_the_classical_register() {
347        let samples = basis_samples_fixture(9, 70);
348        let meas_map: Vec<(usize, usize)> = (0..5).map(|q| (q, q)).collect();
349        assert_eq!(
350            shots_from_basis_samples(&samples, &meas_map, 5),
351            shots_per_bit(&samples, &meas_map, 5)
352        );
353    }
354
355    // A classical register wider than the qubit register: the straddling word
356    // lies past the last word the samples hold, so nothing may be masked off.
357    #[test]
358    fn basis_sample_unpack_keeps_every_bit_of_a_narrow_register() {
359        let samples = basis_samples_fixture(5, 70);
360        let unpacked = samples.to_shots(130);
361        for (shot, row) in unpacked.iter().enumerate() {
362            for (qubit, &got) in row[..70].iter().enumerate() {
363                assert_eq!(got, samples.bit(shot, qubit), "shot {shot} qubit {qubit}");
364            }
365            assert!(row[70..].iter().all(|b| !b));
366        }
367    }
368
369    #[test]
370    fn basis_sample_permuted_map_takes_the_general_path() {
371        let samples = basis_samples_fixture(6, 8);
372        let meas_map = [(3, 0), (0, 1), (7, 2)];
373        assert_eq!(
374            shots_from_basis_samples(&samples, &meas_map, 3),
375            shots_per_bit(&samples, &meas_map, 3)
376        );
377    }
378
379    #[test]
380    fn build_cdf_normalizes_last_to_one() {
381        let cdf = build_cdf(&[0.2, 0.3, 0.4999]);
382        assert_eq!(cdf.len(), 3);
383        assert!((cdf[0] - 0.2).abs() < 1e-12);
384        assert!((cdf[1] - 0.5).abs() < 1e-12);
385        assert!((cdf[2] - 1.0).abs() < 1e-12);
386    }
387
388    #[test]
389    fn build_cdf_empty_and_single() {
390        let empty = build_cdf(&[]);
391        assert!(empty.is_empty());
392        let single = build_cdf(&[0.42]);
393        assert_eq!(single, vec![1.0]);
394    }
395
396    #[test]
397    fn sample_from_cdf_bounds() {
398        let cdf = [0.25, 0.5, 0.75, 1.0];
399        assert_eq!(sample_from_cdf(&cdf, 0.0), 0);
400        assert_eq!(sample_from_cdf(&cdf, 0.3), 1);
401        assert_eq!(sample_from_cdf(&cdf, 0.99), 3);
402        assert_eq!(sample_from_cdf(&cdf, 1.0), 3);
403    }
404
405    #[test]
406    fn bitstring_packs_bits_lsb_first() {
407        let bits = vec![0b1011u64];
408        let s = bitstring(&bits, 4);
409        assert_eq!(s, "1101");
410    }
411
412    #[test]
413    fn bitstring_short_key_pads_zero() {
414        let s = bitstring(&[], 3);
415        assert_eq!(s, "000");
416    }
417
418    #[test]
419    fn shots_result_counts_and_display() {
420        let result = ShotsResult::from_shots(
421            vec![vec![true, false], vec![true, false], vec![false, true]],
422            2,
423        );
424        assert_eq!(result.num_shots(), 3);
425        assert_eq!(result.num_classical_bits(), 2);
426        let counts = result.counts();
427        assert_eq!(counts.len(), 2);
428        let s = format!("{}", result);
429        assert!(s.contains("10: 2"));
430        assert!(s.contains("01: 1"));
431    }
432
433    #[test]
434    fn sample_shots_empty_meas_map_returns_all_false() {
435        let probs = Probabilities::Dense(vec![1.0]);
436        let shots = sample_shots(&probs, &[], 3, 4, 42);
437        assert_eq!(shots.len(), 4);
438        for shot in shots {
439            assert_eq!(shot, vec![false, false, false]);
440        }
441    }
442
443    #[test]
444    fn sample_shots_dense_deterministic() {
445        let probs = Probabilities::Dense(vec![0.0, 1.0]);
446        let shots = sample_shots(&probs, &[(0, 0)], 1, 5, 42);
447        for shot in shots {
448            assert_eq!(shot, vec![true]);
449        }
450    }
451
452    #[test]
453    fn sample_shots_factored_reconstructs_global_index() {
454        let probs = Probabilities::Factored {
455            blocks: vec![
456                FactoredBlock {
457                    probs: vec![0.0, 1.0],
458                    mask: 0b001,
459                },
460                FactoredBlock {
461                    probs: vec![0.0, 0.0, 0.0, 1.0],
462                    mask: 0b110,
463                },
464            ],
465            total_qubits: 3,
466        };
467        let shots = sample_shots(&probs, &[(0, 0), (1, 1), (2, 2)], 3, 10, 42);
468        for shot in shots {
469            assert_eq!(shot, vec![true, true, true]);
470        }
471    }
472
473    #[test]
474    fn packed_shots_identity_fast_path() {
475        let mut data = vec![0u64; 4];
476        data[0] = 0b101;
477        data[1] = 0b010;
478        data[2] = 0b111;
479        data[3] = 0b000;
480        let packed = PackedShots::from_shot_major(data, 4, 3);
481        let meas_map = [(0, 0), (0, 1), (0, 2)];
482        let shots = packed_shots_to_classical_bits(&packed, &meas_map, 3);
483        assert_eq!(shots.len(), 4);
484        assert_eq!(shots[0], vec![true, false, true]);
485        assert_eq!(shots[1], vec![false, true, false]);
486        assert_eq!(shots[2], vec![true, true, true]);
487        assert_eq!(shots[3], vec![false, false, false]);
488    }
489
490    #[test]
491    fn packed_shots_non_identity_mapping() {
492        let mut data = vec![0u64; 2];
493        data[0] = 0b011;
494        data[1] = 0b100;
495        let packed = PackedShots::from_shot_major(data, 2, 3);
496        let meas_map = [(0, 2), (0, 1), (0, 0)];
497        let shots = packed_shots_to_classical_bits(&packed, &meas_map, 3);
498        assert_eq!(shots[0], vec![false, true, true]);
499        assert_eq!(shots[1], vec![true, false, false]);
500    }
501
502    #[test]
503    fn packed_shots_out_of_range_classical_bit_skipped() {
504        let data = vec![0b111u64];
505        let packed = PackedShots::from_shot_major(data, 1, 3);
506        let meas_map = [(0, 0), (0, 5), (0, 1)];
507        let shots = packed_shots_to_classical_bits(&packed, &meas_map, 2);
508        assert_eq!(shots[0], vec![true, true]);
509    }
510}