1use crate::circuit::{Circuit, Instruction};
6use crate::error::Result;
7use crate::sim::ShotsResult;
8use crate::sim::compiled::batch_propagate_backward;
9use crate::sim::compiled::{PackedShots, ShotAccumulator, default_chunk_size, xor_words};
10use crate::sim::noise::NoiseModel;
11use rand::SeedableRng;
12use rand_chacha::ChaCha8Rng;
13
14struct F2DenseMatrix {
17 #[cfg(test)]
18 num_rows: usize,
19 #[cfg(test)]
20 num_cols: usize,
21 row_words: usize,
22 data: Vec<u64>,
23}
24
25impl F2DenseMatrix {
26 fn new(num_rows: usize, num_cols: usize) -> Self {
27 let row_words = num_cols.div_ceil(64);
28 Self {
29 #[cfg(test)]
30 num_rows,
31 #[cfg(test)]
32 num_cols,
33 row_words,
34 data: vec![0u64; num_rows * row_words],
35 }
36 }
37
38 #[inline(always)]
39 fn set(&mut self, row: usize, col: usize) {
40 self.data[row * self.row_words + col / 64] |= 1u64 << (col % 64);
41 }
42
43 #[inline(always)]
44 fn get(&self, row: usize, col: usize) -> bool {
45 (self.data[row * self.row_words + col / 64] >> (col % 64)) & 1 != 0
46 }
47
48 #[cfg(test)]
49 fn row(&self, row: usize) -> &[u64] {
50 let start = row * self.row_words;
51 &self.data[start..start + self.row_words]
52 }
53
54 #[cfg(test)]
55 fn xor_row(&mut self, dst_row: usize, src_row: usize) {
56 let rw = self.row_words;
57 let (dst_start, src_start) = (dst_row * rw, src_row * rw);
58 if dst_start < src_start {
59 let (left, right) = self.data.split_at_mut(src_start);
60 for w in 0..rw {
61 left[dst_start + w] ^= right[w];
62 }
63 } else {
64 let (left, right) = self.data.split_at_mut(dst_start);
65 for w in 0..rw {
66 right[w] ^= left[src_start + w];
67 }
68 }
69 }
70
71 #[cfg(test)]
72 fn swap_rows(&mut self, a: usize, b: usize) {
73 if a == b {
74 return;
75 }
76 let rw = self.row_words;
77 let (a_start, b_start) = (a * rw, b * rw);
78 for w in 0..rw {
79 self.data.swap(a_start + w, b_start + w);
80 }
81 }
82}
83
84#[cfg(test)]
92fn gf2_kernel(matrix: &F2DenseMatrix) -> Vec<Vec<u64>> {
93 let m = matrix.num_rows;
94 let n = matrix.num_cols;
95 let n_words = n.div_ceil(64);
96
97 let aug_cols = m + n;
98 let mut aug = F2DenseMatrix::new(n, aug_cols);
99
100 for r in 0..m {
101 for c in 0..n {
102 if matrix.get(r, c) {
103 aug.set(c, r);
104 }
105 }
106 }
107 for i in 0..n {
108 aug.set(i, m + i);
109 }
110
111 let mut pivot_row = 0;
112 for col in 0..m {
113 let mut found = None;
114 for r in pivot_row..n {
115 if aug.get(r, col) {
116 found = Some(r);
117 break;
118 }
119 }
120 let Some(pr) = found else { continue };
121
122 aug.swap_rows(pivot_row, pr);
123
124 for r in 0..n {
125 if r != pivot_row && aug.get(r, col) {
126 aug.xor_row(r, pivot_row);
127 }
128 }
129 pivot_row += 1;
130 }
131
132 let mut kernel = Vec::new();
133 let m_words = m.div_ceil(64);
134 for r in 0..n {
135 let row = aug.row(r);
136 let mt_zero = row[..m_words].iter().enumerate().all(|(w, &val)| {
137 if w == m_words - 1 && !m.is_multiple_of(64) {
138 val & ((1u64 << (m % 64)) - 1) == 0
139 } else {
140 val == 0
141 }
142 });
143 if mt_zero {
144 let mut kv = vec![0u64; n_words];
145 for c in 0..n {
146 if aug.get(r, m + c) {
147 kv[c / 64] |= 1u64 << (c % 64);
148 }
149 }
150 kernel.push(kv);
151 }
152 }
153
154 kernel
155}
156
157pub struct ErrorChainComplex {
165 e_matrix: F2DenseMatrix,
167 error_probs: Vec<f64>,
169 num_measurements: usize,
170 num_errors: usize,
171 boundary_dim: usize,
173 homology_dim: usize,
175}
176
177pub struct HomologicalSampler {
184 compiled: crate::sim::compiled::CompiledSampler,
185 syndrome_rank: usize,
187 #[cfg(test)]
189 class_probs: Vec<f64>,
190 class_cdf: Vec<f64>,
192 class_detections: Vec<Vec<u64>>,
195 boundary_dim: usize,
197 homology_dim: usize,
199 rng: ChaCha8Rng,
200}
201
202impl ErrorChainComplex {
203 pub fn build(circuit: &Circuit, noise: &NoiseModel, _seed: u64) -> Result<Self> {
208 let m = circuit
209 .instructions
210 .iter()
211 .filter(|i| matches!(i, Instruction::Measure { .. }))
212 .count();
213 if m == 0 {
214 return Ok(Self {
215 e_matrix: F2DenseMatrix::new(0, 0),
216 error_probs: Vec::new(),
217 num_measurements: 0,
218 num_errors: 0,
219 boundary_dim: circuit.num_qubits,
220 homology_dim: 0,
221 });
222 }
223
224 let m_words = m.div_ceil(64);
225 let n = circuit.num_qubits;
226
227 let mut x_packed: Vec<Vec<u64>> = vec![vec![0u64; m_words]; n];
228 let mut z_packed: Vec<Vec<u64>> = vec![vec![0u64; m_words]; n];
229 let mut sign_packed = vec![0u64; m_words];
230
231 let mut meas_idx = m;
232 for instr in circuit.instructions.iter().rev() {
233 if let Instruction::Measure { qubit, .. } = instr {
234 meas_idx -= 1;
235 let word = meas_idx / 64;
236 let bit = meas_idx % 64;
237 z_packed[*qubit][word] |= 1u64 << bit;
238 }
239 }
240
241 let mut error_probs = Vec::new();
242 let mut e_cols: Vec<Vec<u64>> = Vec::new();
243
244 for (instr_idx, instr) in circuit.instructions.iter().enumerate().rev() {
245 match instr {
246 Instruction::Gate { gate, targets } => {
247 let noise_events = &noise.after_gate[instr_idx];
248 for event in noise_events {
249 if event.channel.is_inert() {
252 continue;
253 }
254 let (px, py, pz) = event.pauli_probs();
255 let q = event.qubit();
256 let p_total = px + py + pz;
257 if p_total < 1e-15 {
258 continue;
259 }
260
261 let x_sens = &z_packed[q];
262 let z_sens = &x_packed[q];
263
264 if px > 1e-15 && x_sens.iter().any(|&w| w != 0) {
265 error_probs.push(px);
266 e_cols.push(x_sens.clone());
267 }
268
269 if pz > 1e-15 && z_sens.iter().any(|&w| w != 0) {
270 error_probs.push(pz);
271 e_cols.push(z_sens.clone());
272 }
273
274 if py > 1e-15 {
275 let mut y_sens = vec![0u64; m_words];
276 for w in 0..m_words {
277 y_sens[w] = x_sens[w] ^ z_sens[w];
278 }
279 if y_sens.iter().any(|&w| w != 0) {
280 error_probs.push(py);
281 e_cols.push(y_sens);
282 }
283 }
284 }
285
286 batch_propagate_backward(
287 &mut x_packed,
288 &mut z_packed,
289 &mut sign_packed,
290 gate,
291 targets.as_slice(),
292 m_words,
293 );
294 }
295 Instruction::Measure { .. }
296 | Instruction::Reset { .. }
297 | Instruction::Barrier { .. } => {}
298 Instruction::Conditional { gate, targets, .. } => {
299 batch_propagate_backward(
300 &mut x_packed,
301 &mut z_packed,
302 &mut sign_packed,
303 gate,
304 targets.as_slice(),
305 m_words,
306 );
307 }
308 Instruction::Region(_) => {
309 return Err(crate::error::PrismError::IncompatibleBackend {
310 backend: "HomologicalDetectorModel".to_string(),
311 reason: "error propagation does not support guarded regions".to_string(),
312 });
313 }
314 }
315 }
316
317 let p = error_probs.len();
318 let mut e_matrix = F2DenseMatrix::new(m, p);
319
320 for (col, col_data) in e_cols.iter().enumerate() {
321 for (w, &word) in col_data.iter().enumerate() {
322 if word == 0 {
323 continue;
324 }
325 let base = w * 64;
326 let mut bits = word;
327 while bits != 0 {
328 let bit = bits.trailing_zeros() as usize;
329 let row = base + bit;
330 if row < m {
331 e_matrix.set(row, col);
332 }
333 bits &= bits - 1;
334 }
335 }
336 }
337
338 let (boundary_dim, homology_dim) = Self::compute_boundary_space(circuit, n);
339
340 Ok(Self {
341 e_matrix,
342 error_probs,
343 num_measurements: m,
344 num_errors: p,
345 boundary_dim,
346 homology_dim,
347 })
348 }
349
350 fn compute_boundary_space(circuit: &Circuit, n: usize) -> (usize, usize) {
361 if n == 0 {
362 return (0, 0);
363 }
364
365 let n_words = n.div_ceil(64);
366 let mut stab_x: Vec<Vec<u64>> = vec![vec![0u64; n_words]; n];
367 let mut stab_z: Vec<Vec<u64>> = vec![vec![0u64; n_words]; n];
368 let mut stab_sign = vec![0u64; n_words];
369
370 for i in 0..n {
371 stab_z[i][i / 64] |= 1u64 << (i % 64);
372 }
373
374 for instr in circuit.instructions.iter() {
375 match instr {
376 Instruction::Gate { gate, targets } => {
377 batch_propagate_backward(
378 &mut stab_x,
379 &mut stab_z,
380 &mut stab_sign,
381 gate,
382 targets.as_slice(),
383 n_words,
384 );
385 }
386 Instruction::Conditional { gate, targets, .. } => {
387 batch_propagate_backward(
388 &mut stab_x,
389 &mut stab_z,
390 &mut stab_sign,
391 gate,
392 targets.as_slice(),
393 n_words,
394 );
395 }
396 _ => {}
397 }
398 }
399
400 let mut measured = vec![false; n];
401 for instr in &circuit.instructions {
402 if let Instruction::Measure { qubit, .. } = instr {
403 measured[*qubit] = true;
404 }
405 }
406 let num_measured = measured.iter().filter(|&&b| b).count();
407 let measured_indices: Vec<usize> = (0..n).filter(|&q| measured[q]).collect();
408
409 if num_measured == 0 {
410 return (n, 0);
411 }
412
413 let proj_words = num_measured.div_ceil(64);
414 let mut proj = vec![0u64; n * proj_words];
415
416 for stab_idx in 0..n {
417 for (proj_col, &q) in measured_indices.iter().enumerate() {
418 let x_bit = (stab_x[q][stab_idx / 64] >> (stab_idx % 64)) & 1;
419 if x_bit != 0 {
420 proj[stab_idx * proj_words + proj_col / 64] |= 1u64 << (proj_col % 64);
421 }
422 }
423 }
424
425 let mut rank = 0;
426 let mut pivot_row = 0;
427 for col in 0..num_measured {
428 let mut found = None;
429 for r in pivot_row..n {
430 if (proj[r * proj_words + col / 64] >> (col % 64)) & 1 != 0 {
431 found = Some(r);
432 break;
433 }
434 }
435 let Some(pr) = found else { continue };
436
437 if pr != pivot_row {
438 for w in 0..proj_words {
439 proj.swap(pivot_row * proj_words + w, pr * proj_words + w);
440 }
441 }
442
443 for r in 0..n {
444 if r != pivot_row && (proj[r * proj_words + col / 64] >> (col % 64)) & 1 != 0 {
445 for w in 0..proj_words {
446 proj[r * proj_words + w] ^= proj[pivot_row * proj_words + w];
447 }
448 }
449 }
450
451 pivot_row += 1;
452 rank += 1;
453 }
454
455 let boundary_dim = n - rank;
456 let homology_dim = n - num_measured + rank;
457 (boundary_dim, homology_dim)
458 }
459
460 pub fn boundary_dim(&self) -> usize {
463 self.boundary_dim
464 }
465
466 pub fn homology_dim(&self) -> usize {
468 self.homology_dim
469 }
470
471 pub fn noisy_marginals(&self, noiseless_marginals: &[f64]) -> Vec<f64> {
480 let m = self.num_measurements;
481 let p = self.num_errors;
482 if m == 0 || p == 0 {
483 return noiseless_marginals.to_vec();
484 }
485
486 let mut flip_factor = vec![1.0f64; m];
487 let rw = self.e_matrix.row_words;
488
489 for e in 0..p {
490 let factor = 1.0 - 2.0 * self.error_probs[e];
491 if (factor - 1.0).abs() < 1e-15 {
492 continue;
493 }
494
495 let col_word = e / 64;
496 let col_bit = 1u64 << (e % 64);
497
498 for (j, ff) in flip_factor.iter_mut().enumerate() {
499 if self.e_matrix.data[j * rw + col_word] & col_bit != 0 {
500 *ff *= factor;
501 }
502 }
503 }
504
505 let mut result = Vec::with_capacity(m);
506 for j in 0..m {
507 let p_j = noiseless_marginals[j];
508 let p_flip = (1.0 - flip_factor[j]) / 2.0;
509 result.push(p_j + (1.0 - 2.0 * p_j) * p_flip);
510 }
511 result
512 }
513}
514
515const MAX_SYNDROME_RANK: usize = 20;
516
517impl HomologicalSampler {
518 pub fn compile(circuit: &Circuit, noise: &NoiseModel, seed: u64) -> Result<Self> {
531 noise.ensure_pauli_only()?;
532 let ecc = ErrorChainComplex::build(circuit, noise, seed)?;
533 let m = ecc.num_measurements;
534 let p = ecc.num_errors;
535 let compiled = crate::sim::compiled::compile_measurements(circuit, seed)?;
536
537 if m == 0 || p == 0 {
538 return Ok(Self {
539 compiled,
540 syndrome_rank: 0,
541 #[cfg(test)]
542 class_probs: vec![1.0],
543 class_cdf: vec![1.0],
544 class_detections: vec![vec![0u64; m.div_ceil(64)]],
545 boundary_dim: ecc.boundary_dim,
546 homology_dim: ecc.homology_dim,
547 rng: ChaCha8Rng::seed_from_u64(seed),
548 });
549 }
550
551 let m_words = m.div_ceil(64);
552
553 let mut work = ecc.e_matrix.data.clone();
554 let rw = ecc.e_matrix.row_words;
555 let mut pivot_cols = Vec::new();
556 let mut pivot_row = 0;
557
558 for col in 0..p {
559 let mut found = None;
560 for r in pivot_row..m {
561 if (work[r * rw + col / 64] >> (col % 64)) & 1 != 0 {
562 found = Some(r);
563 break;
564 }
565 }
566 let Some(pr) = found else { continue };
567
568 if pr != pivot_row {
569 for w in 0..rw {
570 work.swap(pivot_row * rw + w, pr * rw + w);
571 }
572 }
573
574 for r in 0..m {
575 if r != pivot_row && (work[r * rw + col / 64] >> (col % 64)) & 1 != 0 {
576 for w in 0..rw {
577 work[r * rw + w] ^= work[pivot_row * rw + w];
578 }
579 }
580 }
581
582 pivot_cols.push(col);
583 pivot_row += 1;
584 }
585
586 let r = pivot_cols.len();
587 if r > MAX_SYNDROME_RANK {
588 return Err(crate::error::PrismError::IncompatibleBackend {
589 backend: "HomologicalSampler".to_string(),
590 reason: format!("syndrome rank {r} too large (max {MAX_SYNDROME_RANK})"),
591 });
592 }
593
594 let mut col_coords = vec![0usize; p];
597 for (basis_idx, &_pivot_col) in pivot_cols.iter().enumerate() {
598 for j in 0..p {
599 if (work[basis_idx * rw + j / 64] >> (j % 64)) & 1 != 0 {
600 col_coords[j] |= 1 << basis_idx;
601 }
602 }
603 }
604
605 let num_classes = 1usize << r;
606 let mut class_detections = Vec::with_capacity(num_classes);
607 for c in 0..num_classes {
608 let mut det = vec![0u64; m_words];
609 for (basis_idx, &pivot_col) in pivot_cols.iter().enumerate() {
610 if (c >> basis_idx) & 1 != 0 {
611 for row in 0..m {
612 if ecc.e_matrix.get(row, pivot_col) {
613 det[row / 64] ^= 1u64 << (row % 64);
614 }
615 }
616 }
617 }
618 class_detections.push(det);
619 }
620
621 let mut class_probs = vec![0.0_f64; num_classes];
623 class_probs[0] = 1.0;
624
625 for (j, &coord) in col_coords.iter().enumerate() {
626 let pj = ecc.error_probs[j];
627 if pj < 1e-15 {
628 continue;
629 }
630 if coord == 0 {
631 continue;
632 }
633 let mut new_probs = vec![0.0_f64; num_classes];
634 for c in 0..num_classes {
635 new_probs[c] = (1.0 - pj) * class_probs[c] + pj * class_probs[c ^ coord];
636 }
637 class_probs = new_probs;
638 }
639
640 let mut class_cdf = vec![0.0_f64; num_classes];
641 class_cdf[0] = class_probs[0];
642 for c in 1..num_classes {
643 class_cdf[c] = class_cdf[c - 1] + class_probs[c];
644 }
645 let total = class_cdf[num_classes - 1];
646 if total > 0.0 {
647 for v in &mut class_cdf {
648 *v /= total;
649 }
650 }
651
652 Ok(Self {
653 compiled,
654 syndrome_rank: r,
655 #[cfg(test)]
656 class_probs,
657 class_cdf,
658 class_detections,
659 boundary_dim: ecc.boundary_dim,
660 homology_dim: ecc.homology_dim,
661 rng: ChaCha8Rng::seed_from_u64(seed),
662 })
663 }
664
665 pub fn syndrome_rank(&self) -> usize {
667 self.syndrome_rank
668 }
669
670 pub fn boundary_dim(&self) -> usize {
673 self.boundary_dim
674 }
675
676 pub fn homology_dim(&self) -> usize {
678 self.homology_dim
679 }
680
681 pub fn sample(&mut self) -> Vec<bool> {
683 let mut outcome = self.compiled.sample();
684
685 let u: f64 = rand::RngExt::random(&mut self.rng);
686 let class = match self
687 .class_cdf
688 .binary_search_by(|p| p.partial_cmp(&u).unwrap_or(std::cmp::Ordering::Equal))
689 {
690 Ok(i) => i,
691 Err(i) => i.min(self.class_cdf.len() - 1),
692 };
693
694 let det = &self.class_detections[class];
695 for (mi, bit) in outcome.iter_mut().enumerate() {
696 let det_bit = (det[mi / 64] >> (mi % 64)) & 1 != 0;
697 *bit ^= det_bit;
698 }
699 outcome
700 }
701
702 pub fn sample_bulk(&mut self, num_shots: usize) -> Vec<Vec<bool>> {
703 (0..num_shots).map(|_| self.sample()).collect()
704 }
705
706 pub fn sample_packed(&mut self, num_shots: usize) -> PackedShots {
708 let m = self.compiled.num_measurements();
709 let m_words = m.div_ceil(64);
710 if num_shots == 0 || m == 0 {
711 return PackedShots::from_shot_major(Vec::new(), num_shots, m);
712 }
713
714 let mut accum = Vec::new();
715 let mut rand_buf = Vec::new();
716 self.compiled
717 .sample_bulk_words_shot_major_reuse(&mut accum, &mut rand_buf, num_shots);
718
719 let ref_bits = self.compiled.ref_bits_packed();
720 for s in 0..num_shots {
721 let base = s * m_words;
722 xor_words(&mut accum[base..base + m_words], ref_bits);
723 }
724
725 for s in 0..num_shots {
726 let u: f64 = rand::RngExt::random(&mut self.rng);
727 let class = match self
728 .class_cdf
729 .binary_search_by(|p| p.partial_cmp(&u).unwrap_or(std::cmp::Ordering::Equal))
730 {
731 Ok(i) => i,
732 Err(i) => i.min(self.class_cdf.len() - 1),
733 };
734
735 let det = &self.class_detections[class];
736 let base = s * m_words;
737 xor_words(&mut accum[base..base + m_words], det);
738 }
739
740 PackedShots::from_shot_major(accum, num_shots, m)
741 }
742
743 pub fn sample_chunked<A: ShotAccumulator>(&mut self, total_shots: usize, acc: &mut A) {
745 let chunk_size = default_chunk_size(self.compiled.num_measurements());
746 crate::sim::compiled::for_each_chunk(total_shots, chunk_size, |batch| {
747 let packed = self.sample_packed(batch);
748 acc.accumulate(&packed);
749 });
750 }
751
752 pub fn sample_marginals(&mut self, total_shots: usize) -> Vec<f64> {
754 crate::sim::compiled::marginals_from_chunks(self.compiled.num_measurements(), |acc| {
755 self.sample_chunked(total_shots, acc)
756 })
757 }
758}
759
760pub fn run_shots_homological(
765 circuit: &Circuit,
766 noise: &NoiseModel,
767 num_shots: usize,
768 seed: u64,
769) -> Result<ShotsResult> {
770 noise.validate_for(circuit)?;
771 let sampler = HomologicalSampler::compile(circuit, noise, seed)?;
772 run_shots_homological_inner(sampler, circuit, num_shots)
773}
774
775pub(crate) fn run_shots_homological_inner(
776 mut sampler: HomologicalSampler,
777 circuit: &Circuit,
778 num_shots: usize,
779) -> Result<ShotsResult> {
780 let classical_bit_order = circuit.classical_bit_order();
781 let num_classical = circuit.num_classical_bits;
782
783 let raw_shots = sampler.sample_bulk(num_shots);
784
785 let mut shots = Vec::with_capacity(num_shots);
786 for raw in &raw_shots {
787 let mut out = vec![false; num_classical];
788 for (mi, &cbit) in classical_bit_order.iter().enumerate() {
789 if cbit < num_classical {
790 out[cbit] = raw[mi];
791 }
792 }
793 shots.push(out);
794 }
795
796 Ok(
797 ShotsResult::from_shots(shots, circuit.num_classical_bits).with_metadata(
798 crate::sim::RunMetadata::exact(crate::sim::ResolvedBackend::CompiledStabilizer)
799 .with_engine(crate::sim::Engine::HomologicalSampler),
800 ),
801 )
802}
803
804pub fn noisy_marginals_analytical(
810 circuit: &Circuit,
811 noise: &NoiseModel,
812 seed: u64,
813) -> Result<Vec<f64>> {
814 noise.ensure_pauli_only()?;
815 let ecc = ErrorChainComplex::build(circuit, noise, seed)?;
816 let compiled = crate::sim::compiled::compile_measurements(circuit, seed)?;
817 let noiseless = compiled.marginal_probabilities();
818 let noisy = ecc.noisy_marginals(&noiseless);
819
820 let classical_bit_order: Vec<usize> = circuit
821 .instructions
822 .iter()
823 .filter_map(|inst| match inst {
824 Instruction::Measure { classical_bit, .. } => Some(*classical_bit),
825 _ => None,
826 })
827 .collect();
828 let num_classical = circuit.num_classical_bits;
829
830 let mut result = vec![0.5f64; num_classical];
831 for (mi, &cbit) in classical_bit_order.iter().enumerate() {
832 if cbit < num_classical && mi < noisy.len() {
833 result[cbit] = noisy[mi];
834 }
835 }
836 Ok(result)
837}
838
839#[cfg(test)]
840#[path = "homological_tests.rs"]
841mod tests;