Skip to main content

prism_q/sim/compiled/
mod.rs

1mod accumulator;
2mod bts;
3pub(crate) mod parity;
4mod propagation;
5pub(crate) mod rng;
6#[cfg(test)]
7mod sign_truth_table_tests;
8#[cfg(test)]
9mod tests;
10
11use std::hash::{Hash, Hasher};
12
13use crate::circuit::{Circuit, Instruction, SmallVec};
14use crate::error::{PrismError, Result};
15#[cfg(feature = "gpu")]
16use crate::gpu::kernels::bts::GpuBtsCache;
17use crate::sim::ShotsResult;
18use rand::{Rng, SeedableRng};
19use rand_chacha::ChaCha8Rng;
20
21use bts::{BTS_BATCH_SHOTS, bts_batched, bts_single_pass, sample_bts_meas_major};
22use rng::{Xoshiro256PlusPlus, binomial_sample};
23
24pub use accumulator::{
25    CorrelatorAccumulator, HistogramAccumulator, MarginalsAccumulator, NullAccumulator,
26    PauliExpectationAccumulator, ShotAccumulator, default_chunk_size, optimal_chunk_size,
27};
28pub use parity::ParityStats;
29pub(crate) use parity::{ParityBlock, ParityBlocks, SparseParity, XorDag};
30use parity::{build_parity_blocks_if_useful, build_xor_dag_if_useful, minimize_flip_row_weight};
31
32pub(crate) use crate::backend::word_ops::xor_words;
33pub(crate) use propagation::batch_propagate_backward;
34pub(crate) use propagation::propagate_backward;
35use propagation::{
36    build_measurement_rows, colmajor_forward_sim, compute_reference_bits, rowmul_phase,
37    rowmul_phase_into,
38};
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub(crate) struct PauliVec {
42    pub(crate) x: Vec<u64>,
43    pub(crate) z: Vec<u64>,
44}
45
46impl PauliVec {
47    pub fn new(num_words: usize) -> Self {
48        Self {
49            x: vec![0u64; num_words],
50            z: vec![0u64; num_words],
51        }
52    }
53
54    pub fn z_on_qubit(num_words: usize, qubit: usize) -> Self {
55        let mut pv = Self::new(num_words);
56        pv.z[qubit / 64] |= 1u64 << (qubit % 64);
57        pv
58    }
59
60    #[inline(always)]
61    pub fn is_diagonal(&self) -> bool {
62        self.x.iter().all(|&w| w == 0)
63    }
64
65    #[inline(always)]
66    pub fn has_x_or_y(&self, qubit: usize) -> bool {
67        get_bit(&self.x, qubit)
68    }
69}
70
71impl Hash for PauliVec {
72    fn hash<H: Hasher>(&self, state: &mut H) {
73        self.x.hash(state);
74        self.z.hash(state);
75    }
76}
77
78#[inline(always)]
79pub(crate) fn get_bit(words: &[u64], qubit: usize) -> bool {
80    (words[qubit / 64] >> (qubit % 64)) & 1 != 0
81}
82
83#[inline(always)]
84pub(crate) fn set_bit(words: &mut [u64], qubit: usize, val: bool) {
85    let word = qubit / 64;
86    let bit = qubit % 64;
87    if val {
88        words[word] |= 1u64 << bit;
89    } else {
90        words[word] &= !(1u64 << bit);
91    }
92}
93
94#[inline(always)]
95pub(crate) fn flip_bit(words: &mut [u64], qubit: usize) {
96    words[qubit / 64] ^= 1u64 << (qubit % 64);
97}
98
99fn gaussian_eliminate(rows: &mut [Vec<u64>], num_cols: usize) -> (usize, Vec<usize>) {
100    let num_rows = rows.len();
101    let mut pivot_cols: Vec<usize> = Vec::new();
102    let mut current_row = 0;
103
104    for col in 0..num_cols {
105        let word = col / 64;
106        let bit = col % 64;
107        let mask = 1u64 << bit;
108
109        let pivot = rows[current_row..num_rows]
110            .iter()
111            .position(|row| row[word] & mask != 0)
112            .map(|i| i + current_row);
113
114        let pivot_row = match pivot {
115            Some(r) => r,
116            None => continue,
117        };
118
119        if pivot_row != current_row {
120            rows.swap(pivot_row, current_row);
121        }
122
123        let (top, rest) = rows.split_at_mut(current_row + 1);
124        let pivot_data = &top[current_row];
125        for row in rest.iter_mut() {
126            if row[word] & mask != 0 {
127                for w in 0..row.len() {
128                    row[w] ^= pivot_data[w];
129                }
130            }
131        }
132
133        pivot_cols.push(col);
134        current_row += 1;
135    }
136
137    (current_row, pivot_cols)
138}
139
140const LUT_GROUP_SIZE: usize = 8;
141const LUT_MIN_RANK: usize = 8;
142
143struct FlipLut {
144    data: Vec<u64>,
145    m_words: usize,
146    num_full_groups: usize,
147    remainder_size: usize,
148}
149
150impl FlipLut {
151    fn build(flip_rows: &[Vec<u64>], m_words: usize) -> Self {
152        let rank = flip_rows.len();
153        let num_full_groups = rank / LUT_GROUP_SIZE;
154        let remainder_size = rank % LUT_GROUP_SIZE;
155        let total_groups = num_full_groups + usize::from(remainder_size > 0);
156        let entries_per_group = 1 << LUT_GROUP_SIZE;
157
158        let mut data = vec![0u64; total_groups * entries_per_group * m_words];
159
160        for g in 0..total_groups {
161            let group_start = g * LUT_GROUP_SIZE;
162            let k = if g < num_full_groups {
163                LUT_GROUP_SIZE
164            } else {
165                remainder_size
166            };
167            let lut_offset = g * entries_per_group * m_words;
168
169            for byte in 1..(1usize << k) {
170                let lowest = byte & byte.wrapping_neg();
171                let row_idx = group_start + lowest.trailing_zeros() as usize;
172                let prev = byte ^ lowest;
173
174                let dst_start = lut_offset + byte * m_words;
175                let src_start = lut_offset + prev * m_words;
176
177                for w in 0..m_words {
178                    data[dst_start + w] = data[src_start + w] ^ flip_rows[row_idx][w];
179                }
180            }
181        }
182
183        Self {
184            data,
185            m_words,
186            num_full_groups,
187            remainder_size,
188        }
189    }
190
191    #[inline(always)]
192    fn lookup(&self, group: usize, byte: usize) -> &[u64] {
193        let offset = (group * (1 << LUT_GROUP_SIZE) + byte) * self.m_words;
194        &self.data[offset..offset + self.m_words]
195    }
196}
197
198pub struct CompiledSampler {
199    flip_rows: Vec<Vec<u64>>,
200    ref_bits_packed: Vec<u64>,
201    rank: usize,
202    num_measurements: usize,
203    rng: ChaCha8Rng,
204    lut: Option<FlipLut>,
205    sparse: Option<SparseParity>,
206    xor_dag: Option<XorDag>,
207    parity_blocks: Option<ParityBlocks>,
208    #[cfg(feature = "gpu")]
209    gpu_context: Option<std::sync::Arc<crate::gpu::GpuContext>>,
210    #[cfg(feature = "gpu")]
211    gpu_bts_cache: Option<GpuBtsCache>,
212}
213
214/// Compiled sampler for measurement, detector, and observable records.
215pub struct CompiledDetectorSampler {
216    measurement_sampler: CompiledSampler,
217    detector_rows: Vec<Vec<usize>>,
218    observable_rows: Vec<Vec<usize>>,
219    num_measurements: usize,
220}
221
222/// Packed measurement, detector, and observable samples from one shot batch.
223#[derive(Debug, Clone)]
224pub struct DetectorSampleBatch {
225    pub measurements: PackedShots,
226    pub detectors: PackedShots,
227    pub observables: PackedShots,
228}
229
230fn pack_bools(bools: &[bool]) -> Vec<u64> {
231    let n_words = bools.len().div_ceil(64);
232    let mut packed = vec![0u64; n_words];
233    for (i, &b) in bools.iter().enumerate() {
234        if b {
235            packed[i / 64] |= 1u64 << (i % 64);
236        }
237    }
238    packed
239}
240
241#[cfg(feature = "parallel")]
242#[derive(Clone, Copy)]
243struct SendPtrU64(*mut u64);
244#[cfg(feature = "parallel")]
245// SAFETY: SendPtrU64 is used only for packed shot buffers partitioned by
246// disjoint word ranges before entering parallel workers.
247unsafe impl Send for SendPtrU64 {}
248#[cfg(feature = "parallel")]
249// SAFETY: The raw pointer wrapper is shared, but each worker writes only its
250// assigned non-overlapping range.
251unsafe impl Sync for SendPtrU64 {}
252#[cfg(feature = "parallel")]
253impl SendPtrU64 {
254    #[inline(always)]
255    unsafe fn copy_from_slice(self, dst_offset: usize, src: &[u64]) {
256        // SAFETY: same contract as the enclosing unsafe fn.
257        unsafe {
258            std::ptr::copy_nonoverlapping(src.as_ptr(), self.0.add(dst_offset), src.len());
259        }
260    }
261}
262
263#[inline(always)]
264fn scatter_meas_major_rows(
265    dst: &mut [u64],
266    dst_s_words: usize,
267    src: &[u64],
268    src_s_words: usize,
269    meas_indices: &[usize],
270) {
271    debug_assert_eq!(src.len(), meas_indices.len() * src_s_words);
272    for (local_m, &global_m) in meas_indices.iter().enumerate() {
273        let src_row = &src[local_m * src_s_words..(local_m + 1) * src_s_words];
274        let dst_offset = global_m * dst_s_words;
275        dst[dst_offset..dst_offset + src_s_words].copy_from_slice(src_row);
276    }
277}
278
279#[inline(always)]
280fn project_local_flip_row(local_row: &[u64], mapping: &[usize], m_words: usize) -> Vec<u64> {
281    let mut global_row = vec![0u64; m_words];
282    for (local_word, &word) in local_row.iter().enumerate() {
283        let mut bits = word;
284        while bits != 0 {
285            let bit = bits.trailing_zeros() as usize;
286            let local_m = local_word * 64 + bit;
287            debug_assert!(local_m < mapping.len());
288            let global_m = mapping[local_m];
289            global_row[global_m / 64] |= 1u64 << (global_m % 64);
290            bits &= bits - 1;
291        }
292    }
293    global_row
294}
295
296fn build_sparse_from_filtered_blocks(
297    block_samplers: &[CompiledSampler],
298    meas_map: &[(usize, usize)],
299    rank_offsets: &[usize],
300    num_global_measurements: usize,
301) -> SparseParity {
302    let mut row_offsets = Vec::with_capacity(num_global_measurements + 1);
303    let mut col_indices = Vec::new();
304
305    for &(block_idx, local_meas) in meas_map {
306        row_offsets.push(col_indices.len() as u32);
307        let sparse = block_samplers[block_idx]
308            .sparse
309            .as_ref()
310            .expect("filtered block samplers must retain sparse parity");
311        let rank_offset = rank_offsets[block_idx] as u32;
312        for &col in sparse.row_cols(local_meas) {
313            col_indices.push(rank_offset + col);
314        }
315    }
316    row_offsets.push(col_indices.len() as u32);
317
318    let non_det_rows: Vec<u32> = (0..num_global_measurements as u32)
319        .filter(|&m| row_offsets[m as usize + 1] != row_offsets[m as usize])
320        .collect();
321
322    SparseParity {
323        col_indices,
324        row_offsets,
325        num_rows: num_global_measurements,
326        non_det_rows,
327    }
328}
329
330fn build_filtered_parity_blocks(
331    block_samplers: &[CompiledSampler],
332    local_to_global: &[Vec<usize>],
333) -> Option<ParityBlocks> {
334    let mut blocks = Vec::new();
335
336    for (block_idx, sampler) in block_samplers.iter().enumerate() {
337        let mapping = &local_to_global[block_idx];
338        if mapping.is_empty() {
339            continue;
340        }
341
342        if let Some(parity_blocks) = &sampler.parity_blocks {
343            for block in &parity_blocks.blocks {
344                let meas_indices = block
345                    .meas_indices
346                    .iter()
347                    .map(|&local_m| mapping[local_m])
348                    .collect();
349                blocks.push(ParityBlock {
350                    meas_indices,
351                    sparse: block.sparse.clone(),
352                    block_rank: block.block_rank,
353                    ref_bits_packed: block.ref_bits_packed.clone(),
354                });
355            }
356            continue;
357        }
358
359        let sparse = sampler
360            .sparse
361            .as_ref()
362            .expect("filtered block samplers must retain sparse parity")
363            .clone();
364        blocks.push(ParityBlock {
365            meas_indices: mapping.clone(),
366            sparse,
367            block_rank: sampler.rank,
368            ref_bits_packed: sampler.ref_bits_packed.clone(),
369        });
370    }
371
372    ParityBlocks::from_blocks_if_useful(blocks)
373}
374
375impl CompiledSampler {
376    pub fn rank(&self) -> usize {
377        self.rank
378    }
379
380    /// Opt in to GPU-accelerated BTS sampling. The sampler routes
381    /// `sample_bulk_packed` through the GPU path when the circuit compiled
382    /// to a flat sparse parity (no parity blocks) and the shot count
383    /// crosses the GPU BTS threshold.
384    #[cfg(feature = "gpu")]
385    pub fn with_gpu(mut self, context: std::sync::Arc<crate::gpu::GpuContext>) -> Self {
386        self.gpu_context = Some(context);
387        self.gpu_bts_cache = None;
388        self
389    }
390
391    #[cfg(feature = "gpu")]
392    fn can_use_gpu_bts(&self) -> bool {
393        self.gpu_context.is_some()
394            && self.sparse.is_some()
395            && self.rank > 0
396            && self.parity_blocks.is_none()
397            && self.xor_dag.is_none()
398    }
399
400    #[cfg(feature = "gpu")]
401    pub(crate) fn should_use_gpu_bts(&self, num_shots: usize) -> bool {
402        let Some(sparse) = &self.sparse else {
403            return false;
404        };
405        if !self.can_use_gpu_bts()
406            || num_shots < crate::gpu::bts_min_shots()
407            || self.rank < crate::gpu::bts_min_rank()
408        {
409            return false;
410        }
411
412        let stats = sparse.stats();
413        let min_total_weight = sparse
414            .num_rows
415            .saturating_mul(crate::gpu::bts_min_weight_factor());
416        stats.total_weight >= min_total_weight
417    }
418
419    #[cfg(feature = "gpu")]
420    pub(crate) fn has_gpu_context(&self) -> bool {
421        self.gpu_context.is_some()
422    }
423
424    #[cfg(feature = "gpu")]
425    pub(crate) fn gpu_context(&self) -> Option<std::sync::Arc<crate::gpu::GpuContext>> {
426        self.gpu_context.clone()
427    }
428
429    #[cfg(feature = "gpu")]
430    fn should_try_gpu_counts(&self, total_shots: usize) -> bool {
431        if !self.should_use_gpu_bts(total_shots) || self.rank <= MAX_RANK_FOR_RANK_SPACE {
432            return false;
433        }
434
435        let m_words = self.num_measurements.div_ceil(64);
436        if m_words == 0 || m_words > crate::gpu::kernels::bts::GPU_COUNTS_MAX_WORDS {
437            return false;
438        }
439
440        let entropy_guard = total_shots.ilog2() as usize + 8;
441        self.rank <= entropy_guard
442    }
443
444    #[cfg(feature = "gpu")]
445    fn ensure_gpu_bts_cache(&mut self) -> Result<&mut GpuBtsCache> {
446        if self.gpu_bts_cache.is_none() {
447            let ctx = self
448                .gpu_context
449                .as_ref()
450                .expect("gpu BTS cache requested without gpu_context")
451                .clone();
452            let sparse = self
453                .sparse
454                .as_ref()
455                .expect("gpu BTS cache requested without sparse parity");
456            self.gpu_bts_cache = Some(GpuBtsCache::new(&ctx, sparse, &self.ref_bits_packed)?);
457        }
458        Ok(self
459            .gpu_bts_cache
460            .as_mut()
461            .expect("gpu BTS cache initialized above"))
462    }
463
464    pub fn num_measurements(&self) -> usize {
465        self.num_measurements
466    }
467
468    pub fn sparse(&self) -> Option<&SparseParity> {
469        self.sparse.as_ref()
470    }
471
472    pub fn parity_stats(&self) -> Option<ParityStats> {
473        self.sparse.as_ref().map(|s| s.stats())
474    }
475
476    pub fn sample(&mut self) -> Vec<bool> {
477        let num_meas_words = self.num_measurements.div_ceil(64);
478        let mut accum = vec![0u64; num_meas_words];
479        self.sample_into(&mut accum);
480        self.unpack_result(&accum)
481    }
482
483    pub(crate) fn sample_bulk_words_shot_major(&mut self, num_shots: usize) -> (Vec<u64>, usize) {
484        let m_words = self.num_measurements.div_ceil(64);
485        let mut accum = vec![0u64; num_shots * m_words];
486        let mut rand_buf = Vec::new();
487        self.sample_bulk_words_shot_major_reuse(&mut accum, &mut rand_buf, num_shots);
488        (accum, m_words)
489    }
490
491    pub(crate) fn sample_bulk_words_shot_major_reuse(
492        &mut self,
493        accum: &mut Vec<u64>,
494        rand_buf: &mut Vec<u8>,
495        num_shots: usize,
496    ) -> usize {
497        let m_words = self.num_measurements.div_ceil(64);
498        let needed = num_shots * m_words;
499        accum.resize(needed, 0);
500        accum[..needed].fill(0);
501        if num_shots == 0 || self.num_measurements == 0 || self.rank == 0 {
502            return m_words;
503        }
504
505        if let Some(lut) = &self.lut {
506            let total_groups = lut.num_full_groups + usize::from(lut.remainder_size > 0);
507            let bytes_per_shot = total_groups;
508            let total_bytes = num_shots * bytes_per_shot;
509            rand_buf.resize(total_bytes, 0);
510            {
511                let full_chunks = total_bytes / 8;
512                let tail = full_chunks * 8;
513                for i in 0..full_chunks {
514                    let r = self.rng.next_u64();
515                    rand_buf[i * 8..(i + 1) * 8].copy_from_slice(&r.to_le_bytes());
516                }
517                if tail < total_bytes {
518                    let r = self.rng.next_u64();
519                    let bytes = r.to_le_bytes();
520                    rand_buf[tail..total_bytes].copy_from_slice(&bytes[..total_bytes - tail]);
521                }
522                if lut.remainder_size > 0 {
523                    let remainder_mask = (1u8 << lut.remainder_size) - 1;
524                    let last_group = lut.num_full_groups;
525                    for s in 0..num_shots {
526                        rand_buf[s * bytes_per_shot + last_group] &= remainder_mask;
527                    }
528                }
529            }
530
531            let max_batch = if m_words > 0 {
532                (256 * 1024 / (m_words * 8)).max(64)
533            } else {
534                num_shots
535            };
536
537            let apply_seq = |dst: &mut [u64]| {
538                for tile_start in (0..num_shots).step_by(max_batch) {
539                    let tile_end = (tile_start + max_batch).min(num_shots);
540                    for g in 0..total_groups {
541                        for s in tile_start..tile_end {
542                            let byte = rand_buf[s * bytes_per_shot + g] as usize;
543                            let entry = lut.lookup(g, byte);
544                            let shot_base = s * m_words;
545                            xor_words(&mut dst[shot_base..shot_base + m_words], entry);
546                        }
547                    }
548                }
549            };
550
551            #[cfg(feature = "parallel")]
552            const PAR_SHOT_THRESHOLD: usize = 256;
553
554            #[cfg(feature = "parallel")]
555            if num_shots >= PAR_SHOT_THRESHOLD {
556                use rayon::prelude::*;
557                let shots_per_chunk =
558                    (num_shots.div_ceil(rayon::current_num_threads())).max(max_batch);
559                let chunk_m = shots_per_chunk * m_words;
560                accum
561                    .par_chunks_mut(chunk_m)
562                    .enumerate()
563                    .for_each(|(ci, chunk)| {
564                        let chunk_shots = chunk.len() / m_words;
565                        let chunk_start = ci * shots_per_chunk;
566                        for tile_start in (0..chunk_shots).step_by(max_batch) {
567                            let tile_end = (tile_start + max_batch).min(chunk_shots);
568                            for g in 0..total_groups {
569                                for s in tile_start..tile_end {
570                                    let gs = chunk_start + s;
571                                    let byte = rand_buf[gs * bytes_per_shot + g] as usize;
572                                    let entry = lut.lookup(g, byte);
573                                    let base = s * m_words;
574                                    xor_words(&mut chunk[base..base + m_words], entry);
575                                }
576                            }
577                        }
578                    });
579            } else {
580                apply_seq(accum);
581            }
582
583            #[cfg(not(feature = "parallel"))]
584            apply_seq(accum);
585
586            m_words
587        } else {
588            for s in 0..num_shots {
589                let shot_base = s * m_words;
590                let shot_accum = &mut accum[shot_base..shot_base + m_words];
591                for j in 0..self.rank {
592                    let bit = self.rng.next_u32() & 1;
593                    if bit != 0 {
594                        let row = &self.flip_rows[j];
595                        xor_words(shot_accum, row);
596                    }
597                }
598            }
599            m_words
600        }
601    }
602
603    fn should_use_bts(&self, num_shots: usize) -> bool {
604        if let Some(sparse) = &self.sparse {
605            if self.rank == 0 {
606                return false;
607            }
608            let m_words = self.num_measurements.div_ceil(64) as u64;
609            let lut_groups = (self.rank.div_ceil(LUT_GROUP_SIZE)) as u64;
610
611            let lut_alloc_bytes = num_shots as u64 * (lut_groups + m_words * 8);
612            if lut_alloc_bytes > MAX_LUT_ALLOC_BYTES {
613                return true;
614            }
615
616            let s_words = num_shots.div_ceil(64);
617            let stats = sparse.stats();
618            let bts_work = stats.total_weight as u64 * s_words as u64;
619            let lut_work = num_shots as u64 * lut_groups * m_words;
620            bts_work < lut_work
621        } else {
622            false
623        }
624    }
625
626    pub(crate) fn ref_bits_packed(&self) -> &[u64] {
627        &self.ref_bits_packed
628    }
629
630    pub fn sample_bulk(&mut self, num_shots: usize) -> Vec<Vec<bool>> {
631        self.sample_bulk_packed(num_shots).to_shots()
632    }
633
634    /// Materialise packed shots directly on the GPU.
635    ///
636    /// This is available only when the sampler has a GPU context and the
637    /// compiled circuit routes through the flat sparse BTS path. Use
638    /// [`DevicePackedShots::to_host`] to copy the full packed payload back, or
639    /// [`DevicePackedShots::marginals`] / [`DevicePackedShots::counts`] to
640    /// reduce on device first.
641    #[cfg(feature = "gpu")]
642    pub fn sample_bulk_packed_device(&mut self, num_shots: usize) -> Result<DevicePackedShots> {
643        if !self.can_use_gpu_bts() {
644            return Err(PrismError::BackendUnsupported {
645                backend: "CompiledSampler".to_string(),
646                operation: "sample_bulk_packed_device requires with_gpu() and flat sparse BTS"
647                    .to_string(),
648            });
649        }
650
651        let ctx = self
652            .gpu_context
653            .as_ref()
654            .expect("gpu BTS selected without gpu_context")
655            .clone();
656        let rank = self.rank;
657        let mut fast_rng = Xoshiro256PlusPlus::from_chacha(&mut self.rng);
658        let cache = self.ensure_gpu_bts_cache()?;
659        let data = crate::gpu::kernels::bts::launch_bts_sample_device(
660            &ctx,
661            &mut fast_rng,
662            rank,
663            num_shots,
664            cache,
665        )?;
666
667        Ok(DevicePackedShots {
668            context: ctx,
669            data,
670            num_shots,
671            num_measurements: self.num_measurements,
672            m_words: self.num_measurements.div_ceil(64),
673            s_words: num_shots.div_ceil(64),
674            layout: ShotLayout::MeasMajor,
675            rank,
676        })
677    }
678
679    /// GPU BTS sampling with on-device meas-major to shot-major bit-transpose.
680    ///
681    /// Returns `Some(Ok(data))` when the compiled circuit matches the GPU BTS
682    /// path (flat sparse, no xor_dag, shot threshold crossed). `data` is in
683    /// shot-major layout (`num_shots * m_words` u64s) so callers can skip the
684    /// host `into_shot_major_data()` transpose for downstream
685    /// shot-major consumers (noise apply, etc.). `None` signals "use the CPU
686    /// path for this sampler/shot-count combination".
687    #[cfg(feature = "gpu")]
688    pub(crate) fn try_sample_bulk_shot_major_gpu(
689        &mut self,
690        num_shots: usize,
691    ) -> Option<Result<Vec<u64>>> {
692        if !self.should_use_gpu_bts(num_shots) {
693            return None;
694        }
695        let ctx = self
696            .gpu_context
697            .as_ref()
698            .expect("gpu BTS selected without gpu_context")
699            .clone();
700        let rank = self.rank;
701        let mut fast_rng = Xoshiro256PlusPlus::from_chacha(&mut self.rng);
702        let cache = match self.ensure_gpu_bts_cache() {
703            Ok(c) => c,
704            Err(e) => return Some(Err(e)),
705        };
706        Some(crate::gpu::kernels::bts::launch_bts_sample_shot_major_host(
707            &ctx,
708            &mut fast_rng,
709            rank,
710            num_shots,
711            cache,
712        ))
713    }
714
715    #[inline(always)]
716    fn sample_into(&mut self, accum: &mut [u64]) {
717        if self.rank == 0 {
718            return;
719        }
720
721        if let Some(lut) = &self.lut {
722            let mut rand_buf = 0u64;
723            let mut rand_pos = 8usize;
724
725            for g in 0..lut.num_full_groups {
726                if rand_pos >= 8 {
727                    rand_buf = self.rng.next_u64();
728                    rand_pos = 0;
729                }
730                let byte = ((rand_buf >> (rand_pos * 8)) & 0xFF) as usize;
731                rand_pos += 1;
732                let entry = lut.lookup(g, byte);
733                xor_words(accum, entry);
734            }
735            if lut.remainder_size > 0 {
736                if rand_pos >= 8 {
737                    rand_buf = self.rng.next_u64();
738                }
739                let mask = (1u64 << lut.remainder_size) - 1;
740                let byte = (rand_buf & mask) as usize;
741                let entry = lut.lookup(lut.num_full_groups, byte);
742                xor_words(accum, entry);
743            }
744        } else {
745            for j in 0..self.rank {
746                let bit = self.rng.next_u32() & 1;
747                if bit != 0 {
748                    let row = &self.flip_rows[j];
749                    xor_words(accum, row);
750                }
751            }
752        }
753    }
754
755    #[inline(always)]
756    fn unpack_result(&self, accum: &[u64]) -> Vec<bool> {
757        let mut result = Vec::with_capacity(self.num_measurements);
758        for m in 0..self.num_measurements {
759            let w = m / 64;
760            let ref_word = if w < self.ref_bits_packed.len() {
761                self.ref_bits_packed[w]
762            } else {
763                0
764            };
765            let bit = ((accum[w] ^ ref_word) >> (m % 64)) & 1 != 0;
766            result.push(bit);
767        }
768        result
769    }
770
771    pub fn sample_bulk_packed(&mut self, num_shots: usize) -> PackedShots {
772        match self.sample_bulk_packed_inner(num_shots, false) {
773            Ok(packed) => packed,
774            Err(_) => unreachable!("compiled sampler CPU fallback should not fail"),
775        }
776    }
777
778    pub fn try_sample_bulk_packed(&mut self, num_shots: usize) -> Result<PackedShots> {
779        self.sample_bulk_packed_inner(num_shots, true)
780    }
781
782    fn sample_bulk_packed_inner(
783        &mut self,
784        num_shots: usize,
785        propagate_gpu_errors: bool,
786    ) -> Result<PackedShots> {
787        let m_words = self.num_measurements.div_ceil(64);
788        let s_words = num_shots.div_ceil(64);
789        if num_shots == 0 || self.num_measurements == 0 {
790            return Ok(PackedShots {
791                data: Vec::new(),
792                num_shots,
793                num_measurements: self.num_measurements,
794                m_words,
795                s_words,
796                layout: ShotLayout::ShotMajor,
797            });
798        }
799        if self.rank == 0 {
800            let mut data = vec![0u64; num_shots * m_words];
801            for s in 0..num_shots {
802                let base = s * m_words;
803                data[base..base + m_words].copy_from_slice(&self.ref_bits_packed);
804            }
805            return Ok(PackedShots {
806                data,
807                num_shots,
808                num_measurements: self.num_measurements,
809                m_words,
810                s_words,
811                layout: ShotLayout::ShotMajor,
812            });
813        }
814
815        if self.should_use_bts(num_shots) {
816            return self.sample_bulk_packed_bts(num_shots, m_words, s_words, propagate_gpu_errors);
817        }
818
819        let (mut data, _) = self.sample_bulk_words_shot_major(num_shots);
820        for s in 0..num_shots {
821            let base = s * m_words;
822            xor_words(&mut data[base..base + m_words], &self.ref_bits_packed);
823        }
824        Ok(PackedShots {
825            data,
826            num_shots,
827            num_measurements: self.num_measurements,
828            m_words,
829            s_words,
830            layout: ShotLayout::ShotMajor,
831        })
832    }
833
834    fn sample_bulk_packed_bts(
835        &mut self,
836        num_shots: usize,
837        m_words: usize,
838        s_words: usize,
839        propagate_gpu_errors: bool,
840    ) -> Result<PackedShots> {
841        #[cfg(not(feature = "gpu"))]
842        let _ = propagate_gpu_errors;
843
844        let num_meas = self.num_measurements;
845
846        if let Some(pb) = &self.parity_blocks {
847            let block_seeds: Vec<u64> = (0..pb.blocks.len()).map(|_| self.rng.next_u64()).collect();
848            let meas_major = if pb.direct_scatter {
849                let total_len = num_meas * s_words;
850                #[allow(clippy::uninit_vec)]
851                let mut meas_major = {
852                    let mut v = Vec::with_capacity(total_len);
853                    // SAFETY: Every measurement row is written exactly once by one parity
854                    // block before the buffer is read. The filtered parity blocks form a
855                    // partition of the global measurement rows.
856                    unsafe { v.set_len(total_len) };
857                    v
858                };
859
860                #[cfg(feature = "parallel")]
861                {
862                    use rayon::prelude::*;
863
864                    let ptr = SendPtrU64(meas_major.as_mut_ptr());
865                    pb.blocks
866                        .par_iter()
867                        .zip(block_seeds.par_iter())
868                        .for_each(|(block, &seed)| {
869                            let mut block_chacha = ChaCha8Rng::seed_from_u64(seed);
870                            let mut block_rng = Xoshiro256PlusPlus::from_chacha(&mut block_chacha);
871                            let block_data = sample_bts_meas_major(
872                                &block.sparse,
873                                num_shots,
874                                &block.ref_bits_packed,
875                                &mut block_rng,
876                                block.block_rank,
877                            );
878
879                            // SAFETY: Each parity block owns a disjoint set of global
880                            // measurement rows, so these row copies target non-overlapping
881                            // regions of the final meas-major output.
882                            unsafe {
883                                for (local_m, &global_m) in block.meas_indices.iter().enumerate() {
884                                    let row =
885                                        &block_data[local_m * s_words..(local_m + 1) * s_words];
886                                    ptr.copy_from_slice(global_m * s_words, row);
887                                }
888                            }
889                        });
890                }
891
892                #[cfg(not(feature = "parallel"))]
893                {
894                    for (block, &seed) in pb.blocks.iter().zip(block_seeds.iter()) {
895                        let mut block_chacha = ChaCha8Rng::seed_from_u64(seed);
896                        let mut block_rng = Xoshiro256PlusPlus::from_chacha(&mut block_chacha);
897                        let block_data = sample_bts_meas_major(
898                            &block.sparse,
899                            num_shots,
900                            &block.ref_bits_packed,
901                            &mut block_rng,
902                            block.block_rank,
903                        );
904                        scatter_meas_major_rows(
905                            &mut meas_major,
906                            s_words,
907                            &block_data,
908                            s_words,
909                            &block.meas_indices,
910                        );
911                    }
912                }
913
914                meas_major
915            } else {
916                #[cfg(feature = "parallel")]
917                let block_results: Vec<(Vec<u64>, &[usize])> = {
918                    use rayon::prelude::*;
919                    pb.blocks
920                        .par_iter()
921                        .zip(block_seeds.par_iter())
922                        .map(|(block, &seed)| {
923                            let mut block_chacha = ChaCha8Rng::seed_from_u64(seed);
924                            let mut block_rng = Xoshiro256PlusPlus::from_chacha(&mut block_chacha);
925                            let data = sample_bts_meas_major(
926                                &block.sparse,
927                                num_shots,
928                                &block.ref_bits_packed,
929                                &mut block_rng,
930                                block.block_rank,
931                            );
932                            (data, block.meas_indices.as_slice())
933                        })
934                        .collect()
935                };
936
937                #[cfg(not(feature = "parallel"))]
938                let block_results: Vec<(Vec<u64>, &[usize])> = pb
939                    .blocks
940                    .iter()
941                    .zip(block_seeds.iter())
942                    .map(|(block, &seed)| {
943                        let mut block_chacha = ChaCha8Rng::seed_from_u64(seed);
944                        let mut block_rng = Xoshiro256PlusPlus::from_chacha(&mut block_chacha);
945                        let data = sample_bts_meas_major(
946                            &block.sparse,
947                            num_shots,
948                            &block.ref_bits_packed,
949                            &mut block_rng,
950                            block.block_rank,
951                        );
952                        (data, block.meas_indices.as_slice())
953                    })
954                    .collect();
955
956                let mut meas_major = vec![0u64; num_meas * s_words];
957                for (block_data, meas_indices) in &block_results {
958                    scatter_meas_major_rows(
959                        &mut meas_major,
960                        s_words,
961                        block_data,
962                        s_words,
963                        meas_indices,
964                    );
965                }
966                meas_major
967            };
968
969            return Ok(PackedShots {
970                data: meas_major,
971                num_shots,
972                num_measurements: num_meas,
973                m_words,
974                s_words,
975                layout: ShotLayout::MeasMajor,
976            });
977        }
978
979        #[cfg(feature = "gpu")]
980        if self.should_use_gpu_bts(num_shots) {
981            match self.sample_bulk_packed_bts_gpu(num_shots, m_words, s_words) {
982                Ok(packed) => return Ok(packed),
983                Err(e) if propagate_gpu_errors => return Err(e),
984                Err(_) => {}
985            }
986        }
987
988        let sparse = self
989            .sparse
990            .as_ref()
991            .expect("sparse parity required for BTS (should_use_bts guards this)");
992
993        let mut fast_rng = Xoshiro256PlusPlus::from_chacha(&mut self.rng);
994
995        if num_shots <= BTS_BATCH_SHOTS {
996            let data = bts_single_pass(
997                sparse,
998                self.xor_dag.as_ref(),
999                num_shots,
1000                &self.ref_bits_packed,
1001                &mut fast_rng,
1002                self.rank,
1003            );
1004            return Ok(PackedShots {
1005                data,
1006                num_shots,
1007                num_measurements: num_meas,
1008                m_words,
1009                s_words,
1010                layout: ShotLayout::MeasMajor,
1011            });
1012        }
1013
1014        let data = bts_batched(
1015            sparse,
1016            self.xor_dag.as_ref(),
1017            num_shots,
1018            s_words,
1019            &self.ref_bits_packed,
1020            &mut fast_rng,
1021            self.rank,
1022        );
1023        Ok(PackedShots {
1024            data,
1025            num_shots,
1026            num_measurements: num_meas,
1027            m_words,
1028            s_words,
1029            layout: ShotLayout::MeasMajor,
1030        })
1031    }
1032
1033    #[cfg(feature = "gpu")]
1034    fn sample_bulk_packed_bts_gpu(
1035        &mut self,
1036        num_shots: usize,
1037        m_words: usize,
1038        s_words: usize,
1039    ) -> Result<PackedShots> {
1040        let ctx = self
1041            .gpu_context
1042            .as_ref()
1043            .expect("gpu BTS selected without gpu_context")
1044            .clone();
1045        let rank = self.rank;
1046        let mut fast_rng = Xoshiro256PlusPlus::from_chacha(&mut self.rng);
1047        let cache = self.ensure_gpu_bts_cache()?;
1048        let data = crate::gpu::kernels::bts::launch_bts_sample(
1049            &ctx,
1050            &mut fast_rng,
1051            rank,
1052            num_shots,
1053            cache,
1054        )?;
1055        Ok(PackedShots {
1056            data,
1057            num_shots,
1058            num_measurements: self.num_measurements,
1059            m_words,
1060            s_words,
1061            layout: ShotLayout::MeasMajor,
1062        })
1063    }
1064
1065    pub fn sample_chunked<A: ShotAccumulator>(&mut self, total_shots: usize, acc: &mut A) {
1066        let chunk_size = default_chunk_size(self.num_measurements);
1067        self.sample_chunked_with_size(total_shots, chunk_size, acc);
1068    }
1069
1070    pub fn sample_chunked_with_size<A: ShotAccumulator>(
1071        &mut self,
1072        total_shots: usize,
1073        chunk_size: usize,
1074        acc: &mut A,
1075    ) {
1076        let mut remaining = total_shots;
1077        while remaining > 0 {
1078            let this_batch = remaining.min(chunk_size);
1079            let packed = self.sample_bulk_packed(this_batch);
1080            acc.accumulate(&packed);
1081            remaining -= this_batch;
1082        }
1083    }
1084
1085    pub fn sample_counts(
1086        &mut self,
1087        total_shots: usize,
1088    ) -> std::collections::HashMap<Vec<u64>, u64> {
1089        match self.try_sample_counts(total_shots) {
1090            Ok(counts) => counts,
1091            Err(_) => self.sample_counts_cpu(total_shots),
1092        }
1093    }
1094
1095    pub fn try_sample_counts(
1096        &mut self,
1097        total_shots: usize,
1098    ) -> Result<std::collections::HashMap<Vec<u64>, u64>> {
1099        #[cfg(feature = "gpu")]
1100        if self.should_try_gpu_counts(total_shots) {
1101            return self.sample_bulk_packed_device(total_shots)?.counts();
1102        }
1103
1104        Ok(self.sample_counts_cpu(total_shots))
1105    }
1106
1107    fn sample_counts_cpu(
1108        &mut self,
1109        total_shots: usize,
1110    ) -> std::collections::HashMap<Vec<u64>, u64> {
1111        if self.rank > 0 && self.parity_blocks.is_none() {
1112            let num_outcomes = 1usize << self.rank;
1113
1114            if self.rank <= MAX_RANK_FOR_MULTINOMIAL
1115                && total_shots >= num_outcomes * MIN_SHOTS_PER_OUTCOME_MULTINOMIAL
1116            {
1117                return self.sample_counts_multinomial(total_shots);
1118            }
1119
1120            if self.rank <= MAX_RANK_FOR_RANK_SPACE
1121                && total_shots >= num_outcomes * MIN_SHOTS_PER_OUTCOME
1122            {
1123                return self.sample_counts_rank_space(total_shots);
1124            }
1125        }
1126        let mut acc = HistogramAccumulator::new();
1127        self.sample_chunked(total_shots, &mut acc);
1128        acc.into_counts()
1129    }
1130
1131    fn sample_counts_multinomial(
1132        &mut self,
1133        total_shots: usize,
1134    ) -> std::collections::HashMap<Vec<u64>, u64> {
1135        use std::collections::HashMap;
1136
1137        let m_words = self.num_measurements.div_ceil(64);
1138
1139        if total_shots == 0 || self.num_measurements == 0 {
1140            return HashMap::new();
1141        }
1142        if self.rank == 0 {
1143            let mut counts = HashMap::new();
1144            counts.insert(self.ref_bits_packed[..m_words].to_vec(), total_shots as u64);
1145            return counts;
1146        }
1147
1148        let rank = self.rank;
1149        let num_outcomes = 1usize << rank;
1150        let mut fast_rng = Xoshiro256PlusPlus::from_chacha(&mut self.rng);
1151        let mut counts = HashMap::new();
1152        let mut remaining = total_shots;
1153
1154        for key in 0..num_outcomes {
1155            if remaining == 0 {
1156                break;
1157            }
1158            let outcomes_left = num_outcomes - key;
1159            let count = if outcomes_left == 1 {
1160                remaining
1161            } else {
1162                binomial_sample(&mut fast_rng, remaining, 1.0 / outcomes_left as f64)
1163            };
1164
1165            if count > 0 {
1166                let mut outcome = self.ref_bits_packed[..m_words].to_vec();
1167                if let Some(lut) = &self.lut {
1168                    let total_groups = lut.num_full_groups + usize::from(lut.remainder_size > 0);
1169                    for g in 0..total_groups {
1170                        let byte = (key >> (g * 8)) & 0xFF;
1171                        let entry = lut.lookup(g, byte);
1172                        xor_words(&mut outcome, entry);
1173                    }
1174                } else {
1175                    for j in 0..rank {
1176                        if (key >> j) & 1 != 0 {
1177                            xor_words(&mut outcome, &self.flip_rows[j]);
1178                        }
1179                    }
1180                }
1181                counts.insert(outcome, count as u64);
1182            }
1183            remaining -= count;
1184        }
1185
1186        counts
1187    }
1188
1189    fn sample_counts_rank_space(
1190        &mut self,
1191        total_shots: usize,
1192    ) -> std::collections::HashMap<Vec<u64>, u64> {
1193        use std::collections::HashMap;
1194
1195        let m_words = self.num_measurements.div_ceil(64);
1196
1197        if total_shots == 0 || self.num_measurements == 0 {
1198            return HashMap::new();
1199        }
1200        if self.rank == 0 {
1201            let mut counts = HashMap::new();
1202            counts.insert(self.ref_bits_packed[..m_words].to_vec(), total_shots as u64);
1203            return counts;
1204        }
1205
1206        let rank = self.rank;
1207        let num_outcomes = 1usize << rank;
1208        let mut rank_counts = vec![0u64; num_outcomes];
1209        let mut fast_rng = Xoshiro256PlusPlus::from_chacha(&mut self.rng);
1210
1211        if let Some(lut) = &self.lut {
1212            let total_groups = lut.num_full_groups + usize::from(lut.remainder_size > 0);
1213            let bytes_per_shot = total_groups;
1214            let remainder_mask: u8 = if lut.remainder_size > 0 {
1215                (1u8 << lut.remainder_size) - 1
1216            } else {
1217                0xFF
1218            };
1219
1220            let chunk_size = (32 * 1024 * 1024 / bytes_per_shot).max(64);
1221            let mut rand_buf = vec![0u8; chunk_size * bytes_per_shot];
1222            let mut remaining = total_shots;
1223
1224            while remaining > 0 {
1225                let this_chunk = remaining.min(chunk_size);
1226                let total_bytes = this_chunk * bytes_per_shot;
1227
1228                let full_chunks = total_bytes / 8;
1229                let tail = full_chunks * 8;
1230                for i in 0..full_chunks {
1231                    let r = fast_rng.next_u64();
1232                    rand_buf[i * 8..(i + 1) * 8].copy_from_slice(&r.to_le_bytes());
1233                }
1234                if tail < total_bytes {
1235                    let r = fast_rng.next_u64();
1236                    let bytes = r.to_le_bytes();
1237                    rand_buf[tail..total_bytes].copy_from_slice(&bytes[..total_bytes - tail]);
1238                }
1239
1240                if lut.remainder_size > 0 {
1241                    let last_group = lut.num_full_groups;
1242                    for s in 0..this_chunk {
1243                        rand_buf[s * bytes_per_shot + last_group] &= remainder_mask;
1244                    }
1245                }
1246
1247                for s in 0..this_chunk {
1248                    let base = s * bytes_per_shot;
1249                    let mut key: usize = 0;
1250                    for g in 0..bytes_per_shot {
1251                        key |= (rand_buf[base + g] as usize) << (g * 8);
1252                    }
1253                    rank_counts[key] += 1;
1254                }
1255
1256                remaining -= this_chunk;
1257            }
1258        } else {
1259            let mut remaining = total_shots;
1260            while remaining > 0 {
1261                let this_chunk = remaining.min(4 * 1024 * 1024);
1262                for _ in 0..this_chunk {
1263                    let bits = fast_rng.next_u64();
1264                    let key = (bits as usize) & (num_outcomes - 1);
1265                    rank_counts[key] += 1;
1266                }
1267                remaining -= this_chunk;
1268            }
1269        }
1270
1271        let mut counts = HashMap::new();
1272        for (key, &count) in rank_counts.iter().enumerate() {
1273            if count == 0 {
1274                continue;
1275            }
1276            let mut outcome = self.ref_bits_packed[..m_words].to_vec();
1277            if let Some(lut) = &self.lut {
1278                let total_groups = lut.num_full_groups + usize::from(lut.remainder_size > 0);
1279                for g in 0..total_groups {
1280                    let byte = (key >> (g * 8)) & 0xFF;
1281                    let entry = lut.lookup(g, byte);
1282                    xor_words(&mut outcome, entry);
1283                }
1284            } else {
1285                for j in 0..rank {
1286                    if (key >> j) & 1 != 0 {
1287                        xor_words(&mut outcome, &self.flip_rows[j]);
1288                    }
1289                }
1290            }
1291            counts.insert(outcome, count);
1292        }
1293
1294        counts
1295    }
1296
1297    pub fn sample_marginals(&mut self, total_shots: usize) -> Vec<f64> {
1298        match self.try_sample_marginals(total_shots) {
1299            Ok(marginals) => marginals,
1300            Err(_) => self.sample_marginals_cpu(total_shots),
1301        }
1302    }
1303
1304    pub fn try_sample_marginals(&mut self, total_shots: usize) -> Result<Vec<f64>> {
1305        #[cfg(feature = "gpu")]
1306        if self.should_use_gpu_bts(total_shots) {
1307            return self.sample_bulk_packed_device(total_shots)?.marginals();
1308        }
1309
1310        Ok(self.sample_marginals_cpu(total_shots))
1311    }
1312
1313    fn sample_marginals_cpu(&mut self, total_shots: usize) -> Vec<f64> {
1314        let mut acc = MarginalsAccumulator::new(self.num_measurements);
1315        self.sample_chunked(total_shots, &mut acc);
1316        acc.marginals()
1317    }
1318
1319    pub fn sample_detection_events(
1320        &mut self,
1321        pairs: &[(usize, usize)],
1322        num_shots: usize,
1323    ) -> PackedShots {
1324        let sparse = self.sparse.as_ref().expect("sparse parity required");
1325        let det_sparse = sparse.compile_detection_events(pairs);
1326        let num_events = det_sparse.num_rows;
1327        let m_words = num_events.div_ceil(64);
1328        let s_words = num_shots.div_ceil(64);
1329
1330        if num_events == 0 || num_shots == 0 || self.rank == 0 {
1331            return PackedShots {
1332                data: vec![0u64; num_events * s_words],
1333                num_shots,
1334                num_measurements: num_events,
1335                m_words,
1336                s_words,
1337                layout: ShotLayout::MeasMajor,
1338            };
1339        }
1340
1341        let det_weight = det_sparse.stats().total_weight;
1342        let meas_weight = sparse.stats().total_weight;
1343
1344        if det_weight > meas_weight + num_events {
1345            let meas_packed = self.sample_bulk_packed(num_shots);
1346            let mut data = vec![0u64; num_events * s_words];
1347            for (e, &(m_a, m_b)) in pairs.iter().enumerate() {
1348                let src_a = &meas_packed.data[m_a * s_words..(m_a + 1) * s_words];
1349                let src_b = &meas_packed.data[m_b * s_words..(m_b + 1) * s_words];
1350                let dst = &mut data[e * s_words..(e + 1) * s_words];
1351                for (d, (&a, &b)) in dst.iter_mut().zip(src_a.iter().zip(src_b.iter())) {
1352                    *d = a ^ b;
1353                }
1354            }
1355            return PackedShots {
1356                data,
1357                num_shots,
1358                num_measurements: num_events,
1359                m_words,
1360                s_words,
1361                layout: ShotLayout::MeasMajor,
1362            };
1363        }
1364
1365        let det_ref = vec![0u64; m_words];
1366
1367        let mut fast_rng = Xoshiro256PlusPlus::from_chacha(&mut self.rng);
1368
1369        let data = if num_shots > BTS_BATCH_SHOTS {
1370            bts_batched(
1371                &det_sparse,
1372                None,
1373                num_shots,
1374                s_words,
1375                &det_ref,
1376                &mut fast_rng,
1377                self.rank,
1378            )
1379        } else {
1380            sample_bts_meas_major(&det_sparse, num_shots, &det_ref, &mut fast_rng, self.rank)
1381        };
1382
1383        PackedShots {
1384            data,
1385            num_shots,
1386            num_measurements: num_events,
1387            m_words,
1388            s_words,
1389            layout: ShotLayout::MeasMajor,
1390        }
1391    }
1392
1393    pub fn exact_counts(&self) -> Option<std::collections::HashMap<Vec<u64>, u64>> {
1394        if self.rank > MAX_RANK_FOR_GRAY_CODE {
1395            return None;
1396        }
1397        let sparse = self.sparse.as_ref()?;
1398        Some(gray_code_exact_counts(
1399            sparse,
1400            self.rank,
1401            &self.ref_bits_packed,
1402            self.num_measurements,
1403        ))
1404    }
1405
1406    pub fn marginal_probabilities(&self) -> Vec<f64> {
1407        let mut probs = vec![0.5f64; self.num_measurements];
1408        if let Some(sparse) = &self.sparse {
1409            for (m, p) in probs.iter_mut().enumerate() {
1410                if sparse.row_weight(m) == 0 {
1411                    let ref_bit = (self.ref_bits_packed[m / 64] >> (m % 64)) & 1;
1412                    *p = ref_bit as f64;
1413                }
1414            }
1415        } else {
1416            for (m, p) in probs.iter_mut().enumerate() {
1417                let mut depends_on_random = false;
1418                for row in &self.flip_rows {
1419                    let w = m / 64;
1420                    if w < row.len() && (row[w] >> (m % 64)) & 1 != 0 {
1421                        depends_on_random = true;
1422                        break;
1423                    }
1424                }
1425                if !depends_on_random {
1426                    let ref_bit = (self.ref_bits_packed[m / 64] >> (m % 64)) & 1;
1427                    *p = ref_bit as f64;
1428                }
1429            }
1430        }
1431        probs
1432    }
1433
1434    pub fn parity_report(&self) -> String {
1435        let mut report = format!(
1436            "CompiledSampler: {} measurements, rank {}, {} flip rows\n",
1437            self.num_measurements,
1438            self.rank,
1439            self.flip_rows.len()
1440        );
1441        if let Some(sparse) = &self.sparse {
1442            let stats = sparse.stats();
1443            report.push_str(&format!(
1444                "Parity matrix: {} rows, total weight {}\n\
1445                 Weight range: {} to {}, mean {:.1}\n\
1446                 Deterministic measurements: {}\n",
1447                sparse.num_rows,
1448                stats.total_weight,
1449                stats.min_weight,
1450                stats.max_weight,
1451                stats.mean_weight,
1452                stats.num_deterministic,
1453            ));
1454            let mut histogram = [0usize; 8];
1455            for m in 0..sparse.num_rows {
1456                let w = sparse.row_weight(m);
1457                let bucket = w.min(7);
1458                histogram[bucket] += 1;
1459            }
1460            report.push_str("Weight histogram: ");
1461            for (i, &count) in histogram.iter().enumerate() {
1462                if count > 0 {
1463                    if i < 7 {
1464                        report.push_str(&format!("w{}={} ", i, count));
1465                    } else {
1466                        report.push_str(&format!("w7+={} ", count));
1467                    }
1468                }
1469            }
1470            report.push('\n');
1471        } else {
1472            report.push_str("No sparse parity matrix available\n");
1473        }
1474        report
1475    }
1476
1477    pub fn detection_event_report(&self, pairs: &[(usize, usize)]) -> String {
1478        let sparse = match &self.sparse {
1479            Some(s) => s,
1480            None => return "No sparse parity matrix available\n".to_string(),
1481        };
1482        let det_sparse = sparse.compile_detection_events(pairs);
1483        let meas_stats = sparse.stats();
1484        let det_stats = det_sparse.stats();
1485
1486        let mut report = format!(
1487            "Detection events: {} pairs\n\
1488             Measurement parity: total_weight={}, mean={:.2}\n\
1489             Detection parity:   total_weight={}, mean={:.2}\n",
1490            pairs.len(),
1491            meas_stats.total_weight,
1492            meas_stats.mean_weight,
1493            det_stats.total_weight,
1494            det_stats.mean_weight,
1495        );
1496
1497        if meas_stats.total_weight > 0 {
1498            let reduction = 1.0 - det_stats.total_weight as f64 / meas_stats.total_weight as f64;
1499            report.push_str(&format!(
1500                "Weight reduction: {:.1}% ({:.1}x less work)\n",
1501                reduction * 100.0,
1502                if det_stats.total_weight > 0 {
1503                    meas_stats.total_weight as f64 / det_stats.total_weight as f64
1504                } else {
1505                    f64::INFINITY
1506                },
1507            ));
1508        }
1509
1510        let mut histogram = [0usize; 8];
1511        for m in 0..det_sparse.num_rows {
1512            let w = det_sparse.row_weight(m);
1513            histogram[w.min(7)] += 1;
1514        }
1515        report.push_str("Detection weight histogram: ");
1516        for (i, &count) in histogram.iter().enumerate() {
1517            if count > 0 {
1518                if i < 7 {
1519                    report.push_str(&format!("w{}={} ", i, count));
1520                } else {
1521                    report.push_str(&format!("w7+={} ", count));
1522                }
1523            }
1524        }
1525        report.push('\n');
1526        report
1527    }
1528}
1529
1530#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1531pub enum ShotLayout {
1532    ShotMajor,
1533    MeasMajor,
1534}
1535
1536#[inline]
1537pub(crate) fn measurement_tail_mask(num_measurements: usize) -> u64 {
1538    let tail = num_measurements % 64;
1539    if tail == 0 {
1540        u64::MAX
1541    } else {
1542        (1u64 << tail) - 1
1543    }
1544}
1545
1546#[inline]
1547pub(crate) fn shot_tail_mask(num_shots: usize) -> u64 {
1548    let tail = num_shots % 64;
1549    if tail == 0 {
1550        u64::MAX
1551    } else {
1552        (1u64 << tail) - 1
1553    }
1554}
1555
1556fn checked_packed_len(label: &str, rows: usize, words_per_row: usize) -> Result<usize> {
1557    rows.checked_mul(words_per_row)
1558        .ok_or_else(|| PrismError::InvalidParameter {
1559            message: format!("{label} packed raw shape overflows usize"),
1560        })
1561}
1562
1563fn validate_packed_len(
1564    label: &str,
1565    actual: usize,
1566    rows: usize,
1567    words_per_row: usize,
1568) -> Result<usize> {
1569    let expected = checked_packed_len(label, rows, words_per_row)?;
1570    if actual != expected {
1571        return Err(PrismError::InvalidParameter {
1572            message: format!(
1573                "{label} packed raw length {actual} does not match expected length {expected}"
1574            ),
1575        });
1576    }
1577    Ok(expected)
1578}
1579
1580fn validate_shot_major_padding(
1581    data: &[u64],
1582    num_shots: usize,
1583    num_measurements: usize,
1584    m_words: usize,
1585) -> Result<()> {
1586    let valid_mask = measurement_tail_mask(num_measurements);
1587    if let Some(shot) = first_shot_major_padding_violation(data, num_shots, m_words, valid_mask) {
1588        return Err(PrismError::InvalidParameter {
1589            message: format!("shot-major raw padding bits are set in shot {shot}"),
1590        });
1591    }
1592    Ok(())
1593}
1594
1595fn first_shot_major_padding_violation(
1596    data: &[u64],
1597    num_shots: usize,
1598    m_words: usize,
1599    tail_mask: u64,
1600) -> Option<usize> {
1601    if m_words == 0 || tail_mask == u64::MAX {
1602        return None;
1603    }
1604    let invalid_mask = !tail_mask;
1605    (0..num_shots).find(|&shot| data[shot * m_words + m_words - 1] & invalid_mask != 0)
1606}
1607
1608pub(crate) fn shot_major_padding_bits_set(
1609    data: &[u64],
1610    num_shots: usize,
1611    m_words: usize,
1612    tail_mask: u64,
1613) -> bool {
1614    first_shot_major_padding_violation(data, num_shots, m_words, tail_mask).is_some()
1615}
1616
1617fn validate_meas_major_padding(
1618    data: &[u64],
1619    num_shots: usize,
1620    num_measurements: usize,
1621    s_words: usize,
1622) -> Result<()> {
1623    if s_words == 0 {
1624        return Ok(());
1625    }
1626    let valid_mask = shot_tail_mask(num_shots);
1627    if valid_mask == u64::MAX {
1628        return Ok(());
1629    }
1630    let invalid_mask = !valid_mask;
1631    for measurement in 0..num_measurements {
1632        let idx = measurement * s_words + s_words - 1;
1633        if data[idx] & invalid_mask != 0 {
1634            return Err(PrismError::InvalidParameter {
1635                message: format!(
1636                    "measurement-major raw padding bits are set in measurement {measurement}"
1637                ),
1638            });
1639        }
1640    }
1641    Ok(())
1642}
1643
1644pub(crate) fn clear_meas_major_shot_padding(
1645    data: &mut [u64],
1646    num_shots: usize,
1647    num_measurements: usize,
1648    s_words: usize,
1649) {
1650    if s_words == 0 {
1651        return;
1652    }
1653    let valid_mask = shot_tail_mask(num_shots);
1654    if valid_mask == u64::MAX {
1655        return;
1656    }
1657    for measurement in 0..num_measurements {
1658        data[measurement * s_words + s_words - 1] &= valid_mask;
1659    }
1660}
1661
1662#[inline]
1663pub(crate) fn count_vec_key<S>(
1664    counts: &mut std::collections::HashMap<Vec<u64>, u64, S>,
1665    key: &[u64],
1666) where
1667    S: std::hash::BuildHasher,
1668{
1669    if let Some(count) = counts.get_mut(key) {
1670        *count += 1;
1671    } else {
1672        counts.insert(key.to_vec(), 1);
1673    }
1674}
1675
1676#[inline]
1677pub(crate) fn count_vec_key_masked<S>(
1678    counts: &mut std::collections::HashMap<Vec<u64>, u64, S>,
1679    key: &[u64],
1680    tail_mask: u64,
1681    scratch: &mut Vec<u64>,
1682) where
1683    S: std::hash::BuildHasher,
1684{
1685    if key.is_empty() || tail_mask == u64::MAX || (key[key.len() - 1] & !tail_mask) == 0 {
1686        count_vec_key(counts, key);
1687        return;
1688    }
1689
1690    scratch.clear();
1691    scratch.extend_from_slice(key);
1692    let last = scratch.len() - 1;
1693    scratch[last] &= tail_mask;
1694    if let Some(count) = counts.get_mut(scratch.as_slice()) {
1695        *count += 1;
1696    } else {
1697        counts.insert(scratch.clone(), 1);
1698    }
1699}
1700
1701#[derive(Debug, Clone)]
1702pub struct PackedShots {
1703    data: Vec<u64>,
1704    num_shots: usize,
1705    num_measurements: usize,
1706    m_words: usize,
1707    s_words: usize,
1708    layout: ShotLayout,
1709}
1710
1711impl PackedShots {
1712    pub const RAW_FORMAT_VERSION: u32 = 1;
1713
1714    pub fn raw_format_version(&self) -> u32 {
1715        Self::RAW_FORMAT_VERSION
1716    }
1717
1718    pub fn num_shots(&self) -> usize {
1719        self.num_shots
1720    }
1721
1722    pub fn num_measurements(&self) -> usize {
1723        self.num_measurements
1724    }
1725
1726    pub fn layout(&self) -> ShotLayout {
1727        self.layout
1728    }
1729
1730    #[inline(always)]
1731    pub fn get_bit(&self, shot: usize, measurement: usize) -> bool {
1732        match self.layout {
1733            ShotLayout::ShotMajor => {
1734                let base = shot * self.m_words;
1735                let w = measurement / 64;
1736                (self.data[base + w] >> (measurement % 64)) & 1 != 0
1737            }
1738            ShotLayout::MeasMajor => {
1739                let base = measurement * self.s_words;
1740                let w = shot / 64;
1741                (self.data[base + w] >> (shot % 64)) & 1 != 0
1742            }
1743        }
1744    }
1745
1746    pub fn s_words(&self) -> usize {
1747        self.s_words
1748    }
1749
1750    pub fn m_words(&self) -> usize {
1751        self.m_words
1752    }
1753
1754    pub(crate) fn measurement_tail_mask(&self) -> u64 {
1755        measurement_tail_mask(self.num_measurements)
1756    }
1757
1758    pub fn shot_words(&self, shot: usize) -> &[u64] {
1759        assert!(
1760            self.layout == ShotLayout::ShotMajor,
1761            "shot_words requires ShotMajor layout"
1762        );
1763        let base = shot * self.m_words;
1764        &self.data[base..base + self.m_words]
1765    }
1766
1767    pub fn meas_words(&self, m: usize) -> &[u64] {
1768        assert!(
1769            self.layout == ShotLayout::MeasMajor,
1770            "meas_words requires MeasMajor layout"
1771        );
1772        let base = m * self.s_words;
1773        &self.data[base..base + self.s_words]
1774    }
1775
1776    pub fn try_from_shot_major(
1777        data: Vec<u64>,
1778        num_shots: usize,
1779        num_measurements: usize,
1780    ) -> Result<Self> {
1781        let m_words = num_measurements.div_ceil(64);
1782        let s_words = num_shots.div_ceil(64);
1783        validate_packed_len("shot-major", data.len(), num_shots, m_words)?;
1784        validate_shot_major_padding(&data, num_shots, num_measurements, m_words)?;
1785        Ok(Self {
1786            data,
1787            num_shots,
1788            num_measurements,
1789            m_words,
1790            s_words,
1791            layout: ShotLayout::ShotMajor,
1792        })
1793    }
1794
1795    pub fn from_shot_major(data: Vec<u64>, num_shots: usize, num_measurements: usize) -> Self {
1796        Self::try_from_shot_major(data, num_shots, num_measurements)
1797            .unwrap_or_else(|e| panic!("invalid shot-major PackedShots raw data: {e}"))
1798    }
1799
1800    pub fn try_from_meas_major(
1801        data: Vec<u64>,
1802        num_shots: usize,
1803        num_measurements: usize,
1804    ) -> Result<Self> {
1805        let m_words = num_measurements.div_ceil(64);
1806        let s_words = num_shots.div_ceil(64);
1807        validate_packed_len("measurement-major", data.len(), num_measurements, s_words)?;
1808        validate_meas_major_padding(&data, num_shots, num_measurements, s_words)?;
1809        Ok(Self {
1810            data,
1811            num_shots,
1812            num_measurements,
1813            m_words,
1814            s_words,
1815            layout: ShotLayout::MeasMajor,
1816        })
1817    }
1818
1819    pub fn from_meas_major(data: Vec<u64>, num_shots: usize, num_measurements: usize) -> Self {
1820        Self::try_from_meas_major(data, num_shots, num_measurements)
1821            .unwrap_or_else(|e| panic!("invalid measurement-major PackedShots raw data: {e}"))
1822    }
1823
1824    /// Return the raw words for [`PackedShots::RAW_FORMAT_VERSION`].
1825    ///
1826    /// Version 1 stores little-endian bit order within each `u64`. Shot-major
1827    /// layout stores `num_shots` rows of `m_words()` words. Measurement-major
1828    /// layout stores `num_measurements` rows of `s_words()` words. Semantic
1829    /// padding bits are outside `num_measurements` for shot-major data and
1830    /// outside `num_shots` for measurement-major data.
1831    pub fn raw_data(&self) -> &[u64] {
1832        &self.data
1833    }
1834
1835    pub fn into_data(self) -> Vec<u64> {
1836        self.data
1837    }
1838
1839    pub(crate) fn into_shot_major_data(self) -> Vec<u64> {
1840        if self.layout == ShotLayout::ShotMajor {
1841            return self.data;
1842        }
1843
1844        let mut shot_major = vec![0u64; self.num_shots * self.m_words];
1845        for batch_start in (0..self.num_shots).step_by(64) {
1846            let batch_end = (batch_start + 64).min(self.num_shots);
1847            let batch_len = batch_end - batch_start;
1848            let sw_base = batch_start / 64;
1849            let bit_off = batch_start % 64;
1850            let batch_mask = if batch_len == 64 {
1851                u64::MAX
1852            } else {
1853                (1u64 << batch_len) - 1
1854            };
1855
1856            for m in 0..self.num_measurements {
1857                let mword = m / 64;
1858                let mbit = m % 64;
1859                let meas_row = &self.data[m * self.s_words..(m + 1) * self.s_words];
1860                let mut bits = meas_row[sw_base] >> bit_off;
1861                if bit_off != 0 && sw_base + 1 < self.s_words {
1862                    bits |= meas_row[sw_base + 1] << (64 - bit_off);
1863                }
1864                bits &= batch_mask;
1865
1866                while bits != 0 {
1867                    let shot_in_batch = bits.trailing_zeros() as usize;
1868                    let shot_base = (batch_start + shot_in_batch) * self.m_words;
1869                    shot_major[shot_base + mword] |= 1u64 << mbit;
1870                    bits &= bits - 1;
1871                }
1872            }
1873        }
1874
1875        shot_major
1876    }
1877
1878    pub fn to_shots(&self) -> Vec<Vec<bool>> {
1879        let mut shots = vec![vec![false; self.num_measurements]; self.num_shots];
1880        match self.layout {
1881            ShotLayout::ShotMajor => {
1882                let last_mask = self.measurement_tail_mask();
1883                for (s, shot) in shots.iter_mut().enumerate() {
1884                    let row = &self.data[s * self.m_words..(s + 1) * self.m_words];
1885                    for (mw, mut bits) in row.iter().copied().enumerate() {
1886                        let base = mw * 64;
1887                        if mw + 1 == self.m_words {
1888                            bits &= last_mask;
1889                        }
1890                        while bits != 0 {
1891                            let measurement = base + bits.trailing_zeros() as usize;
1892                            shot[measurement] = true;
1893                            bits &= bits - 1;
1894                        }
1895                    }
1896                }
1897            }
1898            ShotLayout::MeasMajor => {
1899                let last_mask = shot_tail_mask(self.num_shots);
1900                let mut measurement = 0;
1901                while measurement < self.num_measurements {
1902                    let row =
1903                        &self.data[measurement * self.s_words..(measurement + 1) * self.s_words];
1904                    for (sw, mut bits) in row.iter().copied().enumerate() {
1905                        if sw + 1 == self.s_words {
1906                            bits &= last_mask;
1907                        }
1908                        while bits != 0 {
1909                            let shot = sw * 64 + bits.trailing_zeros() as usize;
1910                            shots[shot][measurement] = true;
1911                            bits &= bits - 1;
1912                        }
1913                    }
1914                    measurement += 1;
1915                }
1916            }
1917        }
1918        shots
1919    }
1920
1921    /// Compute packed parity rows over this packed shot matrix.
1922    ///
1923    /// Each row lists measurement indices to XOR into one output bit.
1924    pub fn parity_rows(&self, rows: &[Vec<usize>]) -> Result<PackedShots> {
1925        validate_parity_rows(rows, self.num_measurements)?;
1926
1927        match self.layout {
1928            ShotLayout::MeasMajor => {
1929                let mut data = vec![0u64; rows.len() * self.s_words];
1930
1931                #[cfg(feature = "parallel")]
1932                if rows.len() * self.s_words >= 16_384 {
1933                    use rayon::prelude::*;
1934                    data.par_chunks_mut(self.s_words)
1935                        .zip(rows.par_iter())
1936                        .for_each(|(dst, row)| {
1937                            for &measurement in row {
1938                                let src_start = measurement * self.s_words;
1939                                xor_words(dst, &self.data[src_start..src_start + self.s_words]);
1940                            }
1941                        });
1942                    clear_meas_major_shot_padding(
1943                        &mut data,
1944                        self.num_shots,
1945                        rows.len(),
1946                        self.s_words,
1947                    );
1948                    return Ok(PackedShots::from_meas_major(
1949                        data,
1950                        self.num_shots,
1951                        rows.len(),
1952                    ));
1953                }
1954
1955                for (out_idx, row) in rows.iter().enumerate() {
1956                    let dst = &mut data[out_idx * self.s_words..(out_idx + 1) * self.s_words];
1957                    for &measurement in row {
1958                        let src_start = measurement * self.s_words;
1959                        xor_words(dst, &self.data[src_start..src_start + self.s_words]);
1960                    }
1961                }
1962                clear_meas_major_shot_padding(&mut data, self.num_shots, rows.len(), self.s_words);
1963                Ok(PackedShots::from_meas_major(
1964                    data,
1965                    self.num_shots,
1966                    rows.len(),
1967                ))
1968            }
1969            ShotLayout::ShotMajor => {
1970                let out_words = rows.len().div_ceil(64);
1971                let mut data = vec![0u64; self.num_shots * out_words];
1972                for shot in 0..self.num_shots {
1973                    let base = shot * out_words;
1974                    for (out_idx, row) in rows.iter().enumerate() {
1975                        let mut bit = false;
1976                        for &measurement in row {
1977                            bit ^= self.get_bit(shot, measurement);
1978                        }
1979                        if bit {
1980                            data[base + out_idx / 64] |= 1u64 << (out_idx % 64);
1981                        }
1982                    }
1983                }
1984                Ok(PackedShots::from_shot_major(
1985                    data,
1986                    self.num_shots,
1987                    rows.len(),
1988                ))
1989            }
1990        }
1991    }
1992
1993    pub fn counts(&self) -> std::collections::HashMap<Vec<u64>, u64> {
1994        if self.m_words > 8 {
1995            return self.counts_wide();
1996        }
1997        self.counts_packed()
1998            .into_iter()
1999            .map(|(k, v)| (k[..self.m_words].to_vec(), v))
2000            .collect()
2001    }
2002
2003    fn counts_wide(&self) -> std::collections::HashMap<Vec<u64>, u64> {
2004        use std::collections::HashMap;
2005
2006        let mut map = HashMap::new();
2007        let mw = self.m_words;
2008        let tail_mask = self.measurement_tail_mask();
2009        let mut scratch = Vec::new();
2010
2011        match self.layout {
2012            ShotLayout::ShotMajor => {
2013                if shot_major_padding_bits_set(&self.data, self.num_shots, mw, tail_mask) {
2014                    for s in 0..self.num_shots {
2015                        let base = s * mw;
2016                        count_vec_key_masked(
2017                            &mut map,
2018                            &self.data[base..base + mw],
2019                            tail_mask,
2020                            &mut scratch,
2021                        );
2022                    }
2023                } else {
2024                    for s in 0..self.num_shots {
2025                        let base = s * mw;
2026                        count_vec_key(&mut map, &self.data[base..base + mw]);
2027                    }
2028                }
2029            }
2030            ShotLayout::MeasMajor => {
2031                let batch_size = 64;
2032                let mut shot_buf = vec![0u64; batch_size * mw];
2033                for batch_start in (0..self.num_shots).step_by(batch_size) {
2034                    let batch_end = (batch_start + batch_size).min(self.num_shots);
2035                    let batch_len = batch_end - batch_start;
2036                    shot_buf[..batch_len * mw].fill(0);
2037
2038                    let sw_base = batch_start / 64;
2039                    let bit_off = batch_start % 64;
2040
2041                    for m in 0..self.num_measurements {
2042                        let mword = m / 64;
2043                        let mbit = m % 64;
2044                        let meas_row = &self.data[m * self.s_words..];
2045                        let word = meas_row[sw_base];
2046                        let shifted = word >> bit_off;
2047                        for s in 0..batch_len {
2048                            if (shifted >> s) & 1 != 0 {
2049                                shot_buf[s * mw + mword] |= 1u64 << mbit;
2050                            }
2051                        }
2052                    }
2053
2054                    for s in 0..batch_len {
2055                        let base = s * mw;
2056                        count_vec_key(&mut map, &shot_buf[base..base + mw]);
2057                    }
2058                }
2059            }
2060        }
2061
2062        map
2063    }
2064
2065    fn counts_packed(&self) -> std::collections::HashMap<[u64; 8], u64> {
2066        use std::collections::HashMap;
2067        let mut map = HashMap::new();
2068        let mw = self.m_words;
2069        debug_assert!(mw <= 8);
2070        let tail_mask = self.measurement_tail_mask();
2071
2072        match self.layout {
2073            ShotLayout::ShotMajor => {
2074                for s in 0..self.num_shots {
2075                    let base = s * mw;
2076                    let mut key = [0u64; 8];
2077                    key[..mw].copy_from_slice(&self.data[base..base + mw]);
2078                    if mw > 0 {
2079                        key[mw - 1] &= tail_mask;
2080                    }
2081                    *map.entry(key).or_insert(0) += 1;
2082                }
2083            }
2084            ShotLayout::MeasMajor => {
2085                let batch_size = 64;
2086                let mut shot_buf = vec![0u64; batch_size * mw];
2087                for batch_start in (0..self.num_shots).step_by(batch_size) {
2088                    let batch_end = (batch_start + batch_size).min(self.num_shots);
2089                    let batch_len = batch_end - batch_start;
2090                    shot_buf[..batch_len * mw].fill(0);
2091
2092                    let sw_base = batch_start / 64;
2093                    let bit_off = batch_start % 64;
2094
2095                    for m in 0..self.num_measurements {
2096                        let mword = m / 64;
2097                        let mbit = m % 64;
2098                        let meas_row = &self.data[m * self.s_words..];
2099                        let word = meas_row[sw_base];
2100                        let shifted = word >> bit_off;
2101                        for s in 0..batch_len {
2102                            if (shifted >> s) & 1 != 0 {
2103                                shot_buf[s * mw + mword] |= 1u64 << mbit;
2104                            }
2105                        }
2106                    }
2107
2108                    for s in 0..batch_len {
2109                        let base = s * mw;
2110                        let mut key = [0u64; 8];
2111                        key[..mw].copy_from_slice(&shot_buf[base..base + mw]);
2112                        *map.entry(key).or_insert(0) += 1;
2113                    }
2114                }
2115            }
2116        }
2117        map
2118    }
2119}
2120
2121fn validate_parity_rows(rows: &[Vec<usize>], num_measurements: usize) -> Result<()> {
2122    for row in rows {
2123        for &measurement in row {
2124            if measurement >= num_measurements {
2125                return Err(PrismError::InvalidParameter {
2126                    message: format!(
2127                        "measurement index {measurement} out of bounds for {num_measurements} measurements"
2128                    ),
2129                });
2130            }
2131        }
2132    }
2133    Ok(())
2134}
2135
2136impl CompiledDetectorSampler {
2137    pub fn num_measurements(&self) -> usize {
2138        self.num_measurements
2139    }
2140
2141    pub fn num_detectors(&self) -> usize {
2142        self.detector_rows.len()
2143    }
2144
2145    pub fn num_observables(&self) -> usize {
2146        self.observable_rows.len()
2147    }
2148
2149    pub fn rank(&self) -> usize {
2150        self.measurement_sampler.rank()
2151    }
2152
2153    pub fn detector_rows(&self) -> &[Vec<usize>] {
2154        &self.detector_rows
2155    }
2156
2157    pub fn observable_rows(&self) -> &[Vec<usize>] {
2158        &self.observable_rows
2159    }
2160
2161    #[cfg(feature = "gpu")]
2162    pub fn with_gpu(mut self, context: std::sync::Arc<crate::gpu::GpuContext>) -> Self {
2163        self.measurement_sampler = self.measurement_sampler.with_gpu(context);
2164        self
2165    }
2166
2167    pub fn sample_measurements_packed(&mut self, num_shots: usize) -> Result<PackedShots> {
2168        self.measurement_sampler.try_sample_bulk_packed(num_shots)
2169    }
2170
2171    pub fn sample_detectors_packed(&mut self, num_shots: usize) -> Result<PackedShots> {
2172        let measurements = self.sample_measurements_packed(num_shots)?;
2173        measurements.parity_rows(&self.detector_rows)
2174    }
2175
2176    pub fn sample_observables_packed(&mut self, num_shots: usize) -> Result<PackedShots> {
2177        let measurements = self.sample_measurements_packed(num_shots)?;
2178        measurements.parity_rows(&self.observable_rows)
2179    }
2180
2181    pub fn sample_packed(&mut self, num_shots: usize) -> Result<DetectorSampleBatch> {
2182        let measurements = self.sample_measurements_packed(num_shots)?;
2183        let detectors = measurements.parity_rows(&self.detector_rows)?;
2184        let observables = measurements.parity_rows(&self.observable_rows)?;
2185        Ok(DetectorSampleBatch {
2186            measurements,
2187            detectors,
2188            observables,
2189        })
2190    }
2191
2192    pub fn sample_detectors_chunked<A: ShotAccumulator>(
2193        &mut self,
2194        total_shots: usize,
2195        acc: &mut A,
2196    ) -> Result<()> {
2197        let chunk_size = default_chunk_size(self.detector_rows.len());
2198        let mut remaining = total_shots;
2199        while remaining > 0 {
2200            let batch = remaining.min(chunk_size);
2201            let packed = self.sample_detectors_packed(batch)?;
2202            acc.accumulate(&packed);
2203            remaining -= batch;
2204        }
2205        Ok(())
2206    }
2207
2208    pub fn sample_detector_counts(
2209        &mut self,
2210        total_shots: usize,
2211    ) -> Result<std::collections::HashMap<Vec<u64>, u64>> {
2212        let mut acc = HistogramAccumulator::new();
2213        self.sample_detectors_chunked(total_shots, &mut acc)?;
2214        Ok(acc.into_counts())
2215    }
2216}
2217
2218/// Device-resident packed shots emitted by the GPU BTS path.
2219///
2220/// The payload stays in measurement-major layout on the device until an
2221/// explicit host copy via [`Self::to_host`]. Marginals and exact counts can be
2222/// reduced first so higher-level workflows do not have to transfer the full
2223/// shot matrix.
2224#[cfg(feature = "gpu")]
2225#[derive(Debug)]
2226pub struct DevicePackedShots {
2227    context: std::sync::Arc<crate::gpu::GpuContext>,
2228    data: crate::gpu::GpuBuffer<u64>,
2229    num_shots: usize,
2230    num_measurements: usize,
2231    m_words: usize,
2232    s_words: usize,
2233    layout: ShotLayout,
2234    rank: usize,
2235}
2236
2237#[cfg(feature = "gpu")]
2238impl DevicePackedShots {
2239    pub fn num_shots(&self) -> usize {
2240        self.num_shots
2241    }
2242
2243    pub fn num_measurements(&self) -> usize {
2244        self.num_measurements
2245    }
2246
2247    pub fn layout(&self) -> ShotLayout {
2248        self.layout
2249    }
2250
2251    pub(crate) fn context(&self) -> &std::sync::Arc<crate::gpu::GpuContext> {
2252        &self.context
2253    }
2254
2255    pub fn m_words(&self) -> usize {
2256        self.m_words
2257    }
2258
2259    pub fn s_words(&self) -> usize {
2260        self.s_words
2261    }
2262
2263    pub(crate) fn data_mut(&mut self) -> &mut crate::gpu::GpuBuffer<u64> {
2264        &mut self.data
2265    }
2266
2267    /// Copy the full packed payload back to host memory.
2268    pub fn to_host(&self) -> Result<PackedShots> {
2269        let len = match self.layout {
2270            ShotLayout::ShotMajor => self.num_shots * self.m_words,
2271            ShotLayout::MeasMajor => self.num_measurements * self.s_words,
2272        };
2273        let mut host = vec![0u64; len];
2274        if len > 0 {
2275            self.data
2276                .copy_to_host(self.context.device(), &mut host)
2277                .map_err(|e| PrismError::BackendUnsupported {
2278                    backend: "gpu".to_string(),
2279                    operation: format!("copy device packed shots to host: {e}"),
2280                })?;
2281        }
2282
2283        Ok(match self.layout {
2284            ShotLayout::ShotMajor => {
2285                PackedShots::from_shot_major(host, self.num_shots, self.num_measurements)
2286            }
2287            ShotLayout::MeasMajor => {
2288                PackedShots::from_meas_major(host, self.num_shots, self.num_measurements)
2289            }
2290        })
2291    }
2292
2293    /// Return per-measurement one-counts without copying the full shot matrix.
2294    pub fn marginal_counts(&self) -> Result<Vec<u64>> {
2295        match self.layout {
2296            ShotLayout::MeasMajor => crate::gpu::kernels::bts::count_meas_major_marginals(
2297                &self.context,
2298                &self.data,
2299                self.num_measurements,
2300                self.num_shots,
2301                self.s_words,
2302            ),
2303            ShotLayout::ShotMajor => Ok(self.to_host()?.counts().into_iter().fold(
2304                vec![0u64; self.num_measurements],
2305                |mut acc, (key, count)| {
2306                    for m in 0..self.num_measurements {
2307                        if (key[m / 64] >> (m % 64)) & 1 != 0 {
2308                            acc[m] += count;
2309                        }
2310                    }
2311                    acc
2312                },
2313            )),
2314        }
2315    }
2316
2317    /// Return per-measurement marginal probabilities.
2318    pub fn marginals(&self) -> Result<Vec<f64>> {
2319        if self.num_shots == 0 {
2320            return Ok(vec![0.0; self.num_measurements]);
2321        }
2322        Ok(self
2323            .marginal_counts()?
2324            .into_iter()
2325            .map(|count| count as f64 / self.num_shots as f64)
2326            .collect())
2327    }
2328
2329    /// Return exact counts, using a GPU reduction when it reduces transfer.
2330    pub fn counts(&self) -> Result<std::collections::HashMap<Vec<u64>, u64>> {
2331        self.counts_with_rank_hint(self.rank)
2332    }
2333
2334    pub(crate) fn counts_with_rank_hint(
2335        &self,
2336        rank_hint: usize,
2337    ) -> Result<std::collections::HashMap<Vec<u64>, u64>> {
2338        match self.layout {
2339            ShotLayout::MeasMajor => {
2340                if let Some(counts) = crate::gpu::kernels::bts::try_count_meas_major(
2341                    &self.context,
2342                    &self.data,
2343                    self.num_measurements,
2344                    self.num_shots,
2345                    self.m_words,
2346                    self.s_words,
2347                    rank_hint,
2348                )? {
2349                    return Ok(counts);
2350                }
2351            }
2352            ShotLayout::ShotMajor => {
2353                let raw_transfer_bytes = self
2354                    .num_shots
2355                    .saturating_mul(self.m_words)
2356                    .saturating_mul(std::mem::size_of::<u64>());
2357                if let Some(counts) = crate::gpu::kernels::bts::try_count_shot_major(
2358                    &self.context,
2359                    &self.data,
2360                    self.num_shots,
2361                    self.m_words,
2362                    rank_hint,
2363                    raw_transfer_bytes,
2364                )? {
2365                    return Ok(counts);
2366                }
2367            }
2368        }
2369        Ok(self.to_host()?.counts())
2370    }
2371}
2372
2373fn gray_code_exact_counts(
2374    sparse: &SparseParity,
2375    rank: usize,
2376    ref_bits: &[u64],
2377    num_measurements: usize,
2378) -> std::collections::HashMap<Vec<u64>, u64> {
2379    use std::collections::HashMap;
2380
2381    let m_words = num_measurements.div_ceil(64);
2382    let mut meas_vec = ref_bits[..m_words].to_vec();
2383    let total: u64 = 1u64 << rank;
2384
2385    let mut col_words: Vec<Vec<u64>> = Vec::with_capacity(rank);
2386    for col in 0..rank {
2387        let mut cw = vec![0u64; m_words];
2388        for m in 0..num_measurements {
2389            let start = sparse.row_offsets[m] as usize;
2390            let end = sparse.row_offsets[m + 1] as usize;
2391            for &c in &sparse.col_indices[start..end] {
2392                if c as usize == col {
2393                    cw[m / 64] |= 1u64 << (m % 64);
2394                }
2395            }
2396        }
2397        col_words.push(cw);
2398    }
2399
2400    let mut counts: HashMap<Vec<u64>, u64> = HashMap::new();
2401    *counts.entry(meas_vec.clone()).or_insert(0) += 1;
2402
2403    for step in 1..total {
2404        let bit_to_flip = step.trailing_zeros() as usize;
2405        let col = &col_words[bit_to_flip];
2406        for (mw, cw) in meas_vec.iter_mut().zip(col.iter()) {
2407            *mw ^= cw;
2408        }
2409        *counts.entry(meas_vec.clone()).or_insert(0) += 1;
2410    }
2411
2412    counts
2413}
2414
2415const MAX_RANK_FOR_GRAY_CODE: usize = 25;
2416const MAX_RANK_FOR_MULTINOMIAL: usize = 22;
2417const MIN_SHOTS_PER_OUTCOME_MULTINOMIAL: usize = 8;
2418const MAX_RANK_FOR_RANK_SPACE: usize = 20;
2419const MIN_SHOTS_PER_OUTCOME: usize = 4;
2420const MAX_LUT_ALLOC_BYTES: u64 = 256 * 1024 * 1024;
2421
2422pub fn compile_forward(circuit: &Circuit, seed: u64) -> Result<CompiledSampler> {
2423    if !circuit.is_clifford_only() {
2424        return Err(PrismError::IncompatibleBackend {
2425            backend: "CompiledSampler".to_string(),
2426            reason: "circuit contains non-Clifford gates".to_string(),
2427        });
2428    }
2429
2430    let measurements: Vec<(usize, usize)> = circuit
2431        .instructions
2432        .iter()
2433        .filter_map(|inst| match inst {
2434            Instruction::Measure {
2435                qubit,
2436                classical_bit,
2437            } => Some((*qubit, *classical_bit)),
2438            _ => None,
2439        })
2440        .collect();
2441
2442    let num_measurements = measurements.len();
2443    if num_measurements == 0 {
2444        return Ok(CompiledSampler {
2445            flip_rows: Vec::new(),
2446            ref_bits_packed: Vec::new(),
2447            rank: 0,
2448            num_measurements: 0,
2449            rng: ChaCha8Rng::seed_from_u64(seed),
2450            lut: None,
2451            sparse: None,
2452            xor_dag: None,
2453            parity_blocks: None,
2454            #[cfg(feature = "gpu")]
2455            gpu_context: None,
2456            #[cfg(feature = "gpu")]
2457            gpu_bts_cache: None,
2458        });
2459    }
2460
2461    let n = circuit.num_qubits;
2462
2463    let (mut xz, mut phase, nw) = colmajor_forward_sim(n, &circuit.instructions)?;
2464    let stride = 2 * nw;
2465    let m = num_measurements;
2466    let m_words = m.div_ceil(64);
2467
2468    let rank_words = m_words;
2469    let total_rows = 2 * n;
2470    let mut gen_dep: Vec<Vec<u64>> = vec![vec![0u64; rank_words]; total_rows + 1];
2471    let mut ref_bits: Vec<bool> = vec![false; m];
2472    let mut rank = 0usize;
2473
2474    let mut flip_rows: Vec<Vec<u64>> = Vec::with_capacity(m);
2475    let mut p_data: Vec<u64> = vec![0u64; stride];
2476    let mut p_dep: Vec<u64> = vec![0u64; rank_words];
2477    let mut scratch: Vec<u64> = vec![0u64; stride];
2478    let scratch_idx = total_rows;
2479
2480    for (meas_idx, &(qubit, _)) in measurements.iter().enumerate() {
2481        let word = qubit / 64;
2482        let bit_mask = 1u64 << (qubit % 64);
2483
2484        let mut p: Option<usize> = None;
2485        for i in n..2 * n {
2486            if xz[i * stride + word] & bit_mask != 0 {
2487                p = Some(i);
2488                break;
2489            }
2490        }
2491
2492        if let Some(p_row) = p {
2493            // Random measurement, this is the k-th random degree of freedom
2494            let k = rank;
2495            rank += 1;
2496            flip_rows.push(vec![0u64; m_words]);
2497
2498            flip_rows[k][meas_idx / 64] |= 1u64 << (meas_idx % 64);
2499
2500            let p_base = p_row * stride;
2501            p_data.copy_from_slice(&xz[p_base..p_base + stride]);
2502            let p_phase = phase[p_row];
2503            p_dep.copy_from_slice(&gen_dep[p_row][..rank_words]);
2504
2505            for r in 0..total_rows {
2506                if r == p_row {
2507                    continue;
2508                }
2509                if xz[r * stride + word] & bit_mask == 0 {
2510                    continue;
2511                }
2512
2513                let r_base = r * stride;
2514                phase[r] = rowmul_phase(&p_data, &mut xz, r_base, nw, p_phase, phase[r]);
2515                xor_words(&mut gen_dep[r][..rank_words], &p_dep[..rank_words]);
2516            }
2517
2518            let dest_idx = p_row - n;
2519            let dest_base = dest_idx * stride;
2520            xz.copy_within(p_row * stride..p_row * stride + stride, dest_base);
2521            phase[dest_idx] = p_phase;
2522            gen_dep[dest_idx][..rank_words].copy_from_slice(&p_dep);
2523
2524            let p_base = p_row * stride;
2525            xz[p_base..p_base + stride].fill(0);
2526            xz[p_base + nw + word] |= bit_mask;
2527            phase[p_row] = false;
2528
2529            gen_dep[p_row][..rank_words].fill(0);
2530            gen_dep[p_row][k / 64] |= 1u64 << (k % 64);
2531
2532            ref_bits[meas_idx] = false;
2533        } else {
2534            scratch[..stride].fill(0);
2535            let mut scratch_phase = false;
2536            gen_dep[scratch_idx][..rank_words].fill(0);
2537
2538            for g in 0..n {
2539                let d_base = g * stride;
2540                if xz[d_base + word] & bit_mask == 0 {
2541                    continue;
2542                }
2543
2544                let s_base = (g + n) * stride;
2545                let s_phase = phase[g + n];
2546                scratch_phase =
2547                    rowmul_phase_into(&xz, s_base, &mut scratch, nw, s_phase, scratch_phase);
2548
2549                let (lo, hi) = gen_dep.split_at_mut(scratch_idx);
2550                for (dst, &src) in hi[0][..rank_words].iter_mut().zip(&lo[g + n][..rank_words]) {
2551                    *dst ^= src;
2552                }
2553            }
2554
2555            ref_bits[meas_idx] = scratch_phase;
2556
2557            for (w, &dep_word) in gen_dep[scratch_idx][..rank_words].iter().enumerate() {
2558                let mut bits = dep_word;
2559                while bits != 0 {
2560                    let bit_pos = bits.trailing_zeros() as usize;
2561                    let k = w * 64 + bit_pos;
2562                    if k < rank {
2563                        flip_rows[k][meas_idx / 64] |= 1u64 << (meas_idx % 64);
2564                    }
2565                    bits &= bits - 1;
2566                }
2567            }
2568        }
2569    }
2570
2571    let num_meas_words = m_words;
2572    minimize_flip_row_weight(&mut flip_rows);
2573
2574    let lut = if rank >= LUT_MIN_RANK {
2575        Some(FlipLut::build(&flip_rows, num_meas_words))
2576    } else {
2577        None
2578    };
2579
2580    let sparse = SparseParity::from_flip_rows(&flip_rows, num_measurements);
2581    let xor_dag = build_xor_dag_if_useful(&sparse);
2582    let ref_bits_packed = pack_bools(&ref_bits);
2583    let parity_blocks = build_parity_blocks_if_useful(&sparse, rank, &ref_bits_packed);
2584
2585    Ok(CompiledSampler {
2586        flip_rows,
2587        ref_bits_packed,
2588        rank,
2589        num_measurements,
2590        rng: ChaCha8Rng::seed_from_u64(seed),
2591        lut,
2592        sparse: Some(sparse),
2593        xor_dag,
2594        parity_blocks,
2595        #[cfg(feature = "gpu")]
2596        gpu_context: None,
2597        #[cfg(feature = "gpu")]
2598        gpu_bts_cache: None,
2599    })
2600}
2601
2602fn compile_measurements_filtered(
2603    circuit: &Circuit,
2604    blocks: &[Vec<usize>],
2605    seed: u64,
2606) -> Result<CompiledSampler> {
2607    let num_global_measurements: usize = circuit
2608        .instructions
2609        .iter()
2610        .filter(|i| matches!(i, Instruction::Measure { .. }))
2611        .count();
2612
2613    if num_global_measurements == 0 {
2614        return Ok(CompiledSampler {
2615            flip_rows: Vec::new(),
2616            ref_bits_packed: Vec::new(),
2617            rank: 0,
2618            num_measurements: 0,
2619            rng: ChaCha8Rng::seed_from_u64(seed),
2620            lut: None,
2621            sparse: None,
2622            xor_dag: None,
2623            parity_blocks: None,
2624            #[cfg(feature = "gpu")]
2625            gpu_context: None,
2626            #[cfg(feature = "gpu")]
2627            gpu_bts_cache: None,
2628        });
2629    }
2630
2631    let mut qubit_to_block: Vec<usize> = vec![0; circuit.num_qubits];
2632    for (bi, block) in blocks.iter().enumerate() {
2633        for &q in block {
2634            qubit_to_block[q] = bi;
2635        }
2636    }
2637
2638    let mut block_samplers: Vec<CompiledSampler> = Vec::with_capacity(blocks.len());
2639    for (bi, block) in blocks.iter().enumerate() {
2640        let (sub_circuit, _qubit_map, _classical_map) = circuit.extract_subcircuit(block);
2641        let block_seed = seed.wrapping_add(bi as u64 * 0x1234_5678);
2642        block_samplers.push(compile_measurements(&sub_circuit, block_seed)?);
2643    }
2644
2645    let mut meas_map: Vec<(usize, usize)> = Vec::with_capacity(num_global_measurements);
2646    let mut block_meas_count: Vec<usize> = vec![0; blocks.len()];
2647    for inst in &circuit.instructions {
2648        if let Instruction::Measure { qubit, .. } = inst {
2649            let bi = qubit_to_block[*qubit];
2650            let local_idx = block_meas_count[bi];
2651            block_meas_count[bi] += 1;
2652            meas_map.push((bi, local_idx));
2653        }
2654    }
2655
2656    let m_words = num_global_measurements.div_ceil(64);
2657    let total_rank: usize = block_samplers.iter().map(|s| s.rank).sum();
2658    let mut ref_bits_packed: Vec<u64> = vec![0u64; num_global_measurements.div_ceil(64)];
2659
2660    for (gi, &(bi, li)) in meas_map.iter().enumerate() {
2661        let src = &block_samplers[bi].ref_bits_packed;
2662        let bit = (src[li / 64] >> (li % 64)) & 1;
2663        if bit != 0 {
2664            ref_bits_packed[gi / 64] |= 1u64 << (gi % 64);
2665        }
2666    }
2667
2668    let mut local_to_global: Vec<Vec<usize>> = vec![Vec::new(); blocks.len()];
2669    for (gi, &(bi, _li)) in meas_map.iter().enumerate() {
2670        local_to_global[bi].push(gi);
2671    }
2672
2673    let mut rank_offsets = Vec::with_capacity(block_samplers.len());
2674    let mut rank_prefix = 0usize;
2675    for sampler in &block_samplers {
2676        rank_offsets.push(rank_prefix);
2677        rank_prefix += sampler.rank;
2678    }
2679    debug_assert_eq!(rank_prefix, total_rank);
2680
2681    let mut flip_rows: Vec<Vec<u64>> = Vec::with_capacity(total_rank);
2682    for (bi, sampler) in block_samplers.iter().enumerate() {
2683        let mapping = &local_to_global[bi];
2684        for local_row in &sampler.flip_rows {
2685            flip_rows.push(project_local_flip_row(local_row, mapping, m_words));
2686        }
2687    }
2688
2689    let lut = if total_rank >= LUT_MIN_RANK {
2690        Some(FlipLut::build(&flip_rows, m_words))
2691    } else {
2692        None
2693    };
2694
2695    let sparse = build_sparse_from_filtered_blocks(
2696        &block_samplers,
2697        &meas_map,
2698        &rank_offsets,
2699        num_global_measurements,
2700    );
2701    let xor_dag = build_xor_dag_if_useful(&sparse);
2702    let parity_blocks =
2703        build_filtered_parity_blocks(&block_samplers, &local_to_global).map(|mut blocks| {
2704            blocks.direct_scatter = true;
2705            blocks
2706        });
2707
2708    Ok(CompiledSampler {
2709        flip_rows,
2710        ref_bits_packed,
2711        rank: total_rank,
2712        num_measurements: num_global_measurements,
2713        rng: ChaCha8Rng::seed_from_u64(seed),
2714        lut,
2715        sparse: Some(sparse),
2716        xor_dag,
2717        parity_blocks,
2718        #[cfg(feature = "gpu")]
2719        gpu_context: None,
2720        #[cfg(feature = "gpu")]
2721        gpu_bts_cache: None,
2722    })
2723}
2724
2725/// Convert reset reuse into fresh qubit aliases and move measurements to the end.
2726pub(crate) fn defer_measure_reset_circuit(circuit: &Circuit) -> Result<Circuit> {
2727    if !circuit.is_clifford_only() {
2728        return Err(PrismError::IncompatibleBackend {
2729            backend: "CompiledSampler".to_string(),
2730            reason: "deferred measurement sampling requires Clifford gates".to_string(),
2731        });
2732    }
2733
2734    let has_measurements = circuit
2735        .instructions
2736        .iter()
2737        .any(|inst| matches!(inst, Instruction::Measure { .. }));
2738    if !has_measurements {
2739        return Err(PrismError::IncompatibleBackend {
2740            backend: "CompiledSampler".to_string(),
2741            reason: "deferred measurement sampling requires measurements".to_string(),
2742        });
2743    }
2744
2745    let mut aliases: Vec<usize> = (0..circuit.num_qubits).collect();
2746    let mut measured_aliases = vec![false; circuit.num_qubits];
2747    let mut next_qubit = circuit.num_qubits;
2748    let mut deferred_measurements: Vec<(usize, usize)> = Vec::new();
2749    let mut transformed = Circuit::new(circuit.num_qubits, circuit.num_classical_bits);
2750
2751    for inst in &circuit.instructions {
2752        match inst {
2753            Instruction::Gate { gate, targets } => {
2754                let mapped = map_deferred_targets(targets, &aliases, &measured_aliases)?;
2755                transformed.instructions.push(Instruction::Gate {
2756                    gate: gate.clone(),
2757                    targets: mapped,
2758                });
2759            }
2760            Instruction::Measure {
2761                qubit,
2762                classical_bit,
2763            } => {
2764                if *qubit >= aliases.len() {
2765                    return Err(PrismError::InvalidQubit {
2766                        index: *qubit,
2767                        register_size: aliases.len(),
2768                    });
2769                }
2770                if *classical_bit >= circuit.num_classical_bits {
2771                    return Err(PrismError::InvalidClassicalBit {
2772                        index: *classical_bit,
2773                        register_size: circuit.num_classical_bits,
2774                    });
2775                }
2776                let alias = aliases[*qubit];
2777                deferred_measurements.push((alias, *classical_bit));
2778                measured_aliases[alias] = true;
2779            }
2780            Instruction::Reset { qubit } => {
2781                if *qubit >= aliases.len() {
2782                    return Err(PrismError::InvalidQubit {
2783                        index: *qubit,
2784                        register_size: aliases.len(),
2785                    });
2786                }
2787                aliases[*qubit] = next_qubit;
2788                next_qubit += 1;
2789                measured_aliases.push(false);
2790                transformed.num_qubits = next_qubit;
2791            }
2792            Instruction::Barrier { qubits } => {
2793                let mut mapped = SmallVec::<[usize; 4]>::with_capacity(qubits.len());
2794                for &q in qubits {
2795                    if q >= aliases.len() {
2796                        return Err(PrismError::InvalidQubit {
2797                            index: q,
2798                            register_size: aliases.len(),
2799                        });
2800                    }
2801                    mapped.push(aliases[q]);
2802                }
2803                transformed
2804                    .instructions
2805                    .push(Instruction::Barrier { qubits: mapped });
2806            }
2807            Instruction::Conditional { .. } => {
2808                return Err(PrismError::IncompatibleBackend {
2809                    backend: "CompiledSampler".to_string(),
2810                    reason: "deferred measurement sampling does not support classical conditionals"
2811                        .to_string(),
2812                });
2813            }
2814        }
2815    }
2816
2817    transformed.num_qubits = next_qubit;
2818    transformed
2819        .instructions
2820        .reserve(deferred_measurements.len());
2821    for (qubit, classical_bit) in deferred_measurements {
2822        transformed.instructions.push(Instruction::Measure {
2823            qubit,
2824            classical_bit,
2825        });
2826    }
2827
2828    Ok(transformed)
2829}
2830
2831fn map_deferred_targets(
2832    targets: &SmallVec<[usize; 4]>,
2833    aliases: &[usize],
2834    measured_aliases: &[bool],
2835) -> Result<SmallVec<[usize; 4]>> {
2836    let mut mapped = SmallVec::<[usize; 4]>::with_capacity(targets.len());
2837    for &target in targets {
2838        if target >= aliases.len() {
2839            return Err(PrismError::InvalidQubit {
2840                index: target,
2841                register_size: aliases.len(),
2842            });
2843        }
2844        let alias = aliases[target];
2845        if measured_aliases[alias] {
2846            return Err(PrismError::IncompatibleBackend {
2847                backend: "CompiledSampler".to_string(),
2848                reason:
2849                    "deferred measurement sampling requires reset before reusing a measured qubit"
2850                        .to_string(),
2851            });
2852        }
2853        mapped.push(alias);
2854    }
2855    Ok(mapped)
2856}
2857
2858/// Compile a Clifford measurement circuit plus detector parity metadata.
2859///
2860/// `detector_rows` and `observable_rows` contain measurement record indices
2861/// in circuit measurement order. Circuits with reset reuse are rewritten to
2862/// terminal measurements before compiling.
2863pub fn compile_detector_sampler(
2864    circuit: &Circuit,
2865    detector_rows: Vec<Vec<usize>>,
2866    observable_rows: Vec<Vec<usize>>,
2867    seed: u64,
2868) -> Result<CompiledDetectorSampler> {
2869    let measurement_circuit = if circuit.has_resets() || !circuit.has_terminal_measurements_only() {
2870        defer_measure_reset_circuit(circuit)?
2871    } else {
2872        circuit.clone()
2873    };
2874
2875    let num_measurements = measurement_circuit
2876        .instructions
2877        .iter()
2878        .filter(|inst| matches!(inst, Instruction::Measure { .. }))
2879        .count();
2880    validate_parity_rows(&detector_rows, num_measurements)?;
2881    validate_parity_rows(&observable_rows, num_measurements)?;
2882
2883    let measurement_sampler = compile_measurements(&measurement_circuit, seed)?;
2884    Ok(CompiledDetectorSampler {
2885        measurement_sampler,
2886        detector_rows,
2887        observable_rows,
2888        num_measurements,
2889    })
2890}
2891
2892/// Compile a Clifford circuit's measurements into a fast sampler.
2893///
2894/// Selects forward (SGI stabilizer + dependency tracking) or backward (Pauli
2895/// propagation + Gaussian elimination) based on circuit depth. Forward wins
2896/// for deep circuits (gate_count >= 5×measurements).
2897/// Requires terminal measurements and does not support reset or conditional
2898/// operations on measured data.
2899pub fn compile_measurements(circuit: &Circuit, seed: u64) -> Result<CompiledSampler> {
2900    if !circuit.is_clifford_only() {
2901        return Err(PrismError::IncompatibleBackend {
2902            backend: "CompiledSampler".to_string(),
2903            reason: "circuit contains non-Clifford gates".to_string(),
2904        });
2905    }
2906
2907    let has_measurements = circuit
2908        .instructions
2909        .iter()
2910        .any(|inst| matches!(inst, Instruction::Measure { .. }));
2911    if has_measurements && circuit.has_resets() {
2912        return Err(PrismError::IncompatibleBackend {
2913            backend: "CompiledSampler".to_string(),
2914            reason: "compiled measurement sampling does not support reset instructions".to_string(),
2915        });
2916    }
2917    if has_measurements && !circuit.has_terminal_measurements_only() {
2918        return Err(PrismError::IncompatibleBackend {
2919            backend: "CompiledSampler".to_string(),
2920            reason: "compiled measurement sampling requires terminal measurements and does not support classical conditionals".to_string(),
2921        });
2922    }
2923
2924    if circuit.num_qubits >= 4 {
2925        let blocks = circuit.independent_subsystems();
2926        if blocks.len() > 1 {
2927            let max_block = blocks.iter().map(|b| b.len()).max().unwrap_or(0);
2928            if max_block < circuit.num_qubits {
2929                return compile_measurements_filtered(circuit, &blocks, seed);
2930            }
2931        }
2932    }
2933
2934    if circuit.num_qubits >= 64 {
2935        return compile_forward(circuit, seed);
2936    }
2937
2938    let measurement_rows = build_measurement_rows(circuit);
2939    let num_measurements = measurement_rows.len();
2940
2941    if num_measurements == 0 {
2942        return Ok(CompiledSampler {
2943            flip_rows: Vec::new(),
2944            ref_bits_packed: Vec::new(),
2945            rank: 0,
2946            num_measurements: 0,
2947            rng: ChaCha8Rng::seed_from_u64(seed),
2948            lut: None,
2949            sparse: None,
2950            xor_dag: None,
2951            parity_blocks: None,
2952            #[cfg(feature = "gpu")]
2953            gpu_context: None,
2954            #[cfg(feature = "gpu")]
2955            gpu_bts_cache: None,
2956        });
2957    }
2958
2959    let n = circuit.num_qubits;
2960
2961    let x_rows: Vec<Vec<u64>> = measurement_rows
2962        .iter()
2963        .map(|(p, _, _)| p.x.clone())
2964        .collect();
2965    let signs: Vec<bool> = measurement_rows.iter().map(|(_, _, s)| *s).collect();
2966
2967    let mut x_copy = x_rows.clone();
2968    let (rank, pivot_cols) = gaussian_eliminate(&mut x_copy, n);
2969
2970    let gate_count = circuit
2971        .instructions
2972        .iter()
2973        .filter(|i| {
2974            matches!(
2975                i,
2976                Instruction::Gate { .. } | Instruction::Conditional { .. }
2977            )
2978        })
2979        .count();
2980
2981    let ref_bits: Vec<bool> = if gate_count > 2 * num_measurements {
2982        let mini_outcomes = compute_reference_bits(&measurement_rows, n);
2983        mini_outcomes
2984            .iter()
2985            .zip(signs.iter())
2986            .map(|(&outcome, &sign)| outcome ^ sign)
2987            .collect()
2988    } else {
2989        use crate::backend::Backend;
2990        use crate::backend::stabilizer::StabilizerBackend;
2991        let mut stab = StabilizerBackend::new(seed);
2992        stab.init(circuit.num_qubits, circuit.num_classical_bits)?;
2993        stab.apply_instructions(&circuit.instructions)?;
2994        let ref_classical = stab.classical_results().to_vec();
2995        let classical_bit_order: Vec<usize> = measurement_rows.iter().map(|(_, c, _)| *c).collect();
2996        classical_bit_order
2997            .iter()
2998            .map(|&cbit| {
2999                if cbit < ref_classical.len() {
3000                    ref_classical[cbit]
3001                } else {
3002                    false
3003                }
3004            })
3005            .collect()
3006    };
3007
3008    let num_meas_words = num_measurements.div_ceil(64);
3009    let mut flip_rows: Vec<Vec<u64>> = vec![vec![0u64; num_meas_words]; rank];
3010
3011    for (j, &pcol) in pivot_cols.iter().enumerate() {
3012        for (i, x_row) in x_rows.iter().enumerate() {
3013            if get_bit(x_row, pcol) {
3014                flip_rows[j][i / 64] |= 1u64 << (i % 64);
3015            }
3016        }
3017    }
3018
3019    minimize_flip_row_weight(&mut flip_rows);
3020
3021    let lut = if rank >= LUT_MIN_RANK {
3022        Some(FlipLut::build(&flip_rows, num_meas_words))
3023    } else {
3024        None
3025    };
3026
3027    let sparse = SparseParity::from_flip_rows(&flip_rows, num_measurements);
3028    let xor_dag = build_xor_dag_if_useful(&sparse);
3029    let ref_bits_packed = pack_bools(&ref_bits);
3030    let parity_blocks = build_parity_blocks_if_useful(&sparse, rank, &ref_bits_packed);
3031
3032    Ok(CompiledSampler {
3033        flip_rows,
3034        ref_bits_packed,
3035        rank,
3036        num_measurements,
3037        rng: ChaCha8Rng::seed_from_u64(seed),
3038        lut,
3039        sparse: Some(sparse),
3040        xor_dag,
3041        parity_blocks,
3042        #[cfg(feature = "gpu")]
3043        gpu_context: None,
3044        #[cfg(feature = "gpu")]
3045        gpu_bts_cache: None,
3046    })
3047}
3048
3049/// Sample shots via the compiled (Heisenberg-picture) path.
3050///
3051/// Returns `Vec<Vec<bool>>`, inherently O(num_shots) memory.
3052/// For bounded-memory streaming at large shot counts, use
3053/// `compile_measurements` + `sample_chunked` / `sample_counts` directly.
3054///
3055/// Requires the same circuit subset as [`compile_measurements`], namely
3056/// terminal measurements with no resets or classical conditionals.
3057pub fn run_shots_compiled(circuit: &Circuit, num_shots: usize, seed: u64) -> Result<ShotsResult> {
3058    let mut sampler = compile_measurements(circuit, seed)?;
3059    let packed = sampler.sample_bulk_packed(num_shots);
3060    Ok(ShotsResult {
3061        shots: packed.to_shots(),
3062        num_classical_bits: circuit.num_classical_bits,
3063    })
3064}
3065
3066/// Like [`run_shots_compiled`] but routes BTS sampling through the GPU when the
3067/// circuit compiles to a flat sparse parity matrix and `num_shots` reaches the
3068/// GPU BTS threshold. The threshold defaults to
3069/// [`crate::gpu::BTS_MIN_SHOTS_DEFAULT`] and can be overridden with
3070/// `PRISM_GPU_BTS_MIN_SHOTS`. Requires the `gpu` feature and a working CUDA
3071/// context.
3072#[cfg(feature = "gpu")]
3073pub fn run_shots_compiled_with_gpu(
3074    circuit: &Circuit,
3075    num_shots: usize,
3076    seed: u64,
3077    context: std::sync::Arc<crate::gpu::GpuContext>,
3078) -> Result<ShotsResult> {
3079    let mut sampler = compile_measurements(circuit, seed)?.with_gpu(context);
3080    let packed = sampler.sample_bulk_packed(num_shots);
3081    Ok(ShotsResult {
3082        shots: packed.to_shots(),
3083        num_classical_bits: circuit.num_classical_bits,
3084    })
3085}