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
214pub 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#[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")]
245unsafe impl Send for SendPtrU64 {}
248#[cfg(feature = "parallel")]
249unsafe 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 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 #[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 #[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 #[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 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 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 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 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#[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 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 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 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 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 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
2725pub(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
2858pub 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
2892pub 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
3049pub 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#[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}