Skip to content

Commit 3c97fff

Browse files
committed
QEC annotations, fault introspection, ML decoder, multi-decoder sweep, noise model unification
Unified PauliAnnotation system, fault introspection API, lookup table decoder, per-gate noise model, DEM-level decoder comparison infrastructure, and p_init->p_prep rename. Key additions: - PauliAnnotation (Detector/Observable/Operator) replacing three separate types - Fault introspection: gate_fault_locations(), events(), for_each_fault_combo(w) - LookupDecoder: ML lookup table from weighted fault enumeration - Consistent per-gate noise model (p/3 for 1q, p/15 for 2q) across all DEM paths - DemCheckMatrix in pecos-decoder-core for DEM-to-check-matrix conversion - DemAwareDecoder: wraps BP+OSD/LSD/UnionFind/RelayBP for DEM-level decoding - Multi-decoder sweep: --decoder pymatching tesseract bp_osd etc. with side-by-side reports - Parallel Tesseract decode_batch via rayon thread-local instances (~5x speedup) - Full (non-decomposed) DEMs for Tesseract/check-matrix decoders - Surface code circuit builder migrated to typed PauliAnnotation - TickCircuit idle gate insertion, annotation transfer to/from DagCircuit - T1/T2 idle noise, PauliWeights with pattern-based matching - Rename p_init to p_prep throughout (preserve external selene_sim API) - Fix Y anticommutation bug, prep-gate propagation stop, probability sum validation
1 parent 0ab008f commit 3c97fff

File tree

114 files changed

+10868
-4296
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

114 files changed

+10868
-4296
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/benchmarks/benches/modules/gpu_influence_sampler.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
2121
use criterion::{BenchmarkId, Criterion, Throughput, measurement::Measurement};
2222
use pecos_gpu_sims::{GpuInfluenceMapData, GpuInfluenceSampler};
23-
use pecos_qec::fault_tolerance::noisy_sampler::{NoisySampler, UniformNoiseModel};
23+
use pecos_qec::fault_tolerance::dem_builder::DemSampler;
2424
use pecos_qec::fault_tolerance::{DagFaultInfluenceMap, InfluenceBuilder};
2525
use pecos_quantum::DagCircuit;
2626
use std::hint::black_box;
@@ -106,7 +106,7 @@ fn build_influence_maps(
106106
num_data: usize,
107107
) -> (DagFaultInfluenceMap, GpuInfluenceMapData) {
108108
let logical_qubits: Vec<usize> = (0..num_data).collect();
109-
let builder = InfluenceBuilder::new(circuit).with_logical_z(logical_qubits);
109+
let builder = InfluenceBuilder::new(circuit).with_z(logical_qubits);
110110
let influence_map = builder.build();
111111

112112
let (
@@ -155,11 +155,11 @@ fn bench_cpu_vs_gpu_surface_codes<M: Measurement>(c: &mut Criterion<M>) {
155155
group.throughput(Throughput::Elements(u64::from(num_shots)));
156156

157157
// CPU benchmark
158-
let noise = UniformNoiseModel::depolarizing(p_error);
159-
let mut cpu_sampler = NoisySampler::new(&cpu_map, noise, seed);
158+
let probs = vec![p_error; cpu_map.locations.len()];
159+
let cpu_sampler = DemSampler::from_influence_map(&cpu_map, &probs);
160160

161161
group.bench_with_input(BenchmarkId::new("CPU", &label), &(), |b, ()| {
162-
b.iter(|| black_box(cpu_sampler.sample(num_shots as usize)));
162+
b.iter(|| black_box(cpu_sampler.sample_statistics(num_shots as usize, seed)));
163163
});
164164

165165
// GPU benchmark
@@ -196,11 +196,11 @@ fn bench_gpu_sampler_shot_scaling<M: Measurement>(c: &mut Criterion<M>) {
196196
group.throughput(Throughput::Elements(u64::from(num_shots)));
197197

198198
// CPU benchmark
199-
let noise = UniformNoiseModel::depolarizing(p_error);
200-
let mut cpu_sampler = NoisySampler::new(&cpu_map, noise, seed);
199+
let probs = vec![p_error; cpu_map.locations.len()];
200+
let cpu_sampler = DemSampler::from_influence_map(&cpu_map, &probs);
201201

202202
group.bench_with_input(BenchmarkId::new("CPU", &label), &num_shots, |b, &shots| {
203-
b.iter(|| black_box(cpu_sampler.sample(shots as usize)));
203+
b.iter(|| black_box(cpu_sampler.sample_statistics(shots as usize, seed)));
204204
});
205205

206206
// GPU benchmark

crates/pecos-core/src/gate_type.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,16 @@ pub enum GateType {
108108
/// Free/deallocate a qubit
109109
QFree = 136,
110110
Idle = 200,
111+
/// Meta-gate: Pauli operator annotation for fault tracking.
112+
///
113+
/// This gate carries a Pauli string but has no effect on quantum state.
114+
/// Its position in the circuit determines which faults can flip the operator
115+
/// (only faults before this node are relevant). The propagator uses it as a
116+
/// backward propagation start point.
117+
///
118+
/// The Pauli string is encoded in `params`: each param encodes
119+
/// `qubit * 4 + pauli_type` where pauli_type is 1=X, 2=Y, 3=Z.
120+
PauliOperatorMeta = 210,
111121
MeasCrosstalkGlobalPayload = 218,
112122
MeasCrosstalkLocalPayload = 219,
113123
/// Custom/unrecognized gate type, with actual name stored in metadata
@@ -164,13 +174,23 @@ impl From<u8> for GateType {
164174
200 => GateType::Idle,
165175
218 => GateType::MeasCrosstalkGlobalPayload,
166176
219 => GateType::MeasCrosstalkLocalPayload,
177+
210 => GateType::PauliOperatorMeta,
167178
255 => GateType::Custom,
168179
_ => panic!("Invalid gate type ID: {value}"),
169180
}
170181
}
171182
}
172183

173184
impl GateType {
185+
/// Returns true if this gate type is a meta-gate (annotation, not physical).
186+
///
187+
/// Meta-gates have a position in the DAG but do not affect quantum state
188+
/// and should not create fault locations or receive noise.
189+
#[must_use]
190+
pub const fn is_meta(self) -> bool {
191+
matches!(self, GateType::PauliOperatorMeta)
192+
}
193+
174194
/// Returns the number of angle parameters this gate type requires
175195
///
176196
/// # Returns
@@ -215,7 +235,8 @@ impl GateType {
215235
| GateType::PZ
216236
| GateType::QAlloc
217237
| GateType::QFree
218-
| GateType::Custom => 0,
238+
| GateType::Custom
239+
| GateType::PauliOperatorMeta => 0,
219240

220241
// Gates with one parameter
221242
GateType::RX
@@ -277,7 +298,11 @@ impl GateType {
277298
| GateType::Idle
278299
| GateType::MeasCrosstalkGlobalPayload
279300
| GateType::MeasCrosstalkLocalPayload
280-
| GateType::Custom => 1,
301+
| GateType::Custom
302+
// PauliOperatorMeta is variable-arity but returns 1 here because
303+
// gate validation checks `is_multiple_of(quantum_arity())` and any
304+
// count is a multiple of 1. The actual qubit count is in the gate.
305+
| GateType::PauliOperatorMeta => 1,
281306

282307
// Two-qubit gates
283308
GateType::CX
@@ -405,6 +430,7 @@ impl fmt::Display for GateType {
405430
GateType::MeasCrosstalkGlobalPayload => write!(f, "MeasCrosstalkGlobalPayload"),
406431
GateType::MeasCrosstalkLocalPayload => write!(f, "MeasCrosstalkLocalPayload"),
407432
GateType::Custom => write!(f, "Custom"),
433+
GateType::PauliOperatorMeta => write!(f, "PauliOperator"),
408434
}
409435
}
410436
}

crates/pecos-cuquantum-sys/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ env_logger.workspace = true
2626

2727
[features]
2828
default = []
29-
# Generate bindings at build time (requires cuQuantum headers)
30-
runtime-bindgen = []
3129

3230
[package.metadata.docs.rs]
3331
# Don't try to build docs on docs.rs (no CUDA/cuQuantum available)

crates/pecos-decoder-core/src/dem.rs

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,178 @@ pub mod utils {
200200
}
201201
}
202202

203+
/// Check matrix representation extracted from a Detector Error Model.
204+
///
205+
/// Converts a DEM string into the matrices needed by check-matrix-based
206+
/// decoders (BP+OSD, UnionFind, RelayBP, etc.):
207+
///
208+
/// - **check_matrix** `H[d][m]`: 1 if error mechanism `m` flips detector `d`
209+
/// - **observable_matrix** `L[o][m]`: 1 if mechanism `m` flips observable `o`
210+
/// - **error_priors** `p[m]`: probability of mechanism `m`
211+
///
212+
/// # Example
213+
///
214+
/// ```
215+
/// use pecos_decoder_core::dem::DemCheckMatrix;
216+
///
217+
/// let dem = "error(0.01) D0 D1 L0\nerror(0.02) D1 D2";
218+
/// let dcm = DemCheckMatrix::from_dem_str(dem).unwrap();
219+
/// assert_eq!(dcm.num_detectors, 3);
220+
/// assert_eq!(dcm.num_observables, 1);
221+
/// assert_eq!(dcm.num_mechanisms, 2);
222+
/// assert_eq!(dcm.error_priors, vec![0.01, 0.02]);
223+
/// ```
224+
#[derive(Debug, Clone)]
225+
pub struct DemCheckMatrix {
226+
/// Check matrix: rows = detectors, columns = error mechanisms.
227+
pub check_matrix: ndarray::Array2<u8>,
228+
/// Observable matrix: rows = observables, columns = error mechanisms.
229+
pub observable_matrix: ndarray::Array2<u8>,
230+
/// Error probability per mechanism.
231+
pub error_priors: Vec<f64>,
232+
/// Number of detectors (rows of check_matrix).
233+
pub num_detectors: usize,
234+
/// Number of observables (rows of observable_matrix).
235+
pub num_observables: usize,
236+
/// Number of error mechanisms (columns of both matrices).
237+
pub num_mechanisms: usize,
238+
}
239+
240+
impl DemCheckMatrix {
241+
/// Parse a DEM string into check matrix form.
242+
///
243+
/// Each `error(p) D_i D_j ... L_k ...` line becomes one column in the
244+
/// check matrix (for the D entries) and one column in the observable
245+
/// matrix (for the L entries). Decomposed mechanisms (`D0 ^ D1`) are
246+
/// combined by XOR.
247+
///
248+
/// # Errors
249+
///
250+
/// Returns [`DecoderError`] if the DEM string is malformed.
251+
pub fn from_dem_str(dem: &str) -> Result<Self, DecoderError> {
252+
// First pass: collect mechanisms and find dimensions.
253+
let mut mechanisms: Vec<(f64, Vec<u32>, Vec<u32>)> = Vec::new();
254+
let mut max_detector: Option<u32> = None;
255+
let mut max_observable: Option<u32> = None;
256+
257+
for line in dem.lines() {
258+
let line = line.trim();
259+
if line.is_empty() || line.starts_with('#') {
260+
continue;
261+
}
262+
if !line.starts_with("error(") {
263+
// Skip non-error lines (detector, logical_observable, etc.)
264+
continue;
265+
}
266+
267+
// Parse "error(p) D0 D1 ... L0 ..." or "error(p) D0 ^ D1 ..."
268+
let close_paren = line.find(')').ok_or_else(|| {
269+
DecoderError::InvalidConfiguration("Missing closing parenthesis in error line".into())
270+
})?;
271+
let prob_str = &line[6..close_paren];
272+
let probability: f64 = prob_str.parse().map_err(|_| {
273+
DecoderError::InvalidConfiguration(format!("Invalid probability: {prob_str}"))
274+
})?;
275+
276+
let tokens_str = &line[close_paren + 1..];
277+
278+
// Handle decomposed mechanisms (with ^) by XOR-ing components.
279+
let mut det_set = std::collections::BTreeSet::new();
280+
let mut obs_set = std::collections::BTreeSet::new();
281+
282+
for component in tokens_str.split('^') {
283+
for token in component.split_whitespace() {
284+
if let Some(d_str) = token.strip_prefix('D') {
285+
let d: u32 = d_str.parse().map_err(|_| {
286+
DecoderError::InvalidConfiguration(format!("Invalid detector: {token}"))
287+
})?;
288+
// XOR: toggle membership
289+
if !det_set.remove(&d) {
290+
det_set.insert(d);
291+
}
292+
max_detector = Some(max_detector.map_or(d, |m| m.max(d)));
293+
} else if let Some(l_str) = token.strip_prefix('L') {
294+
let l: u32 = l_str.parse().map_err(|_| {
295+
DecoderError::InvalidConfiguration(format!("Invalid observable: {token}"))
296+
})?;
297+
if !obs_set.remove(&l) {
298+
obs_set.insert(l);
299+
}
300+
max_observable = Some(max_observable.map_or(l, |m| m.max(l)));
301+
}
302+
}
303+
}
304+
305+
let detectors: Vec<u32> = det_set.into_iter().collect();
306+
let observables: Vec<u32> = obs_set.into_iter().collect();
307+
mechanisms.push((probability, detectors, observables));
308+
}
309+
310+
let num_detectors = max_detector.map_or(0, |m| m as usize + 1);
311+
let num_observables = max_observable.map_or(0, |m| m as usize + 1);
312+
let num_mechanisms = mechanisms.len();
313+
314+
// Build matrices.
315+
let mut check_matrix = ndarray::Array2::<u8>::zeros((num_detectors, num_mechanisms));
316+
let mut observable_matrix = ndarray::Array2::<u8>::zeros((num_observables, num_mechanisms));
317+
let mut error_priors = Vec::with_capacity(num_mechanisms);
318+
319+
for (col, (prob, detectors, observables)) in mechanisms.iter().enumerate() {
320+
error_priors.push(*prob);
321+
for &d in detectors {
322+
check_matrix[[d as usize, col]] = 1;
323+
}
324+
for &o in observables {
325+
observable_matrix[[o as usize, col]] = 1;
326+
}
327+
}
328+
329+
Ok(Self {
330+
check_matrix,
331+
observable_matrix,
332+
error_priors,
333+
num_detectors,
334+
num_observables,
335+
num_mechanisms,
336+
})
337+
}
338+
339+
/// Compute the observable prediction from a correction vector.
340+
///
341+
/// Given a binary correction vector (one entry per mechanism, from a
342+
/// check-matrix decoder), returns the observable mask as
343+
/// `observable_matrix @ correction (mod 2)`.
344+
#[must_use]
345+
pub fn observables_from_correction(&self, correction: &[u8]) -> Vec<u8> {
346+
let mut obs = vec![0u8; self.num_observables];
347+
for (o, row) in self.observable_matrix.rows().into_iter().enumerate() {
348+
let mut sum = 0u8;
349+
for (m, &val) in row.iter().enumerate() {
350+
if val != 0 && m < correction.len() && correction[m] != 0 {
351+
sum ^= 1;
352+
}
353+
}
354+
obs[o] = sum;
355+
}
356+
obs
357+
}
358+
359+
/// Pack observable predictions into a bitmask (u64).
360+
///
361+
/// Bit `i` is set if observable `i` is predicted to flip.
362+
#[must_use]
363+
pub fn observables_mask_from_correction(&self, correction: &[u8]) -> u64 {
364+
let obs = self.observables_from_correction(correction);
365+
let mut mask = 0u64;
366+
for (i, &v) in obs.iter().enumerate() {
367+
if v != 0 {
368+
mask |= 1 << i;
369+
}
370+
}
371+
mask
372+
}
373+
}
374+
203375
/// Information about a detector error model
204376
#[derive(Debug, Clone, PartialEq)]
205377
pub struct DemInfo {
@@ -301,6 +473,63 @@ mod tests {
301473
assert_eq!(observables, 2); // L0 and L1
302474
}
303475

476+
#[test]
477+
fn test_dem_check_matrix_basic() {
478+
let dem = "error(0.01) D0 D1 L0\nerror(0.02) D1 D2\nerror(0.03) D0 D2 L0";
479+
let dcm = DemCheckMatrix::from_dem_str(dem).unwrap();
480+
481+
assert_eq!(dcm.num_detectors, 3);
482+
assert_eq!(dcm.num_observables, 1);
483+
assert_eq!(dcm.num_mechanisms, 3);
484+
assert_eq!(dcm.error_priors, vec![0.01, 0.02, 0.03]);
485+
486+
// Check matrix: mechanism 0 -> D0,D1; mechanism 1 -> D1,D2; mechanism 2 -> D0,D2
487+
assert_eq!(dcm.check_matrix[[0, 0]], 1); // D0, mech 0
488+
assert_eq!(dcm.check_matrix[[1, 0]], 1); // D1, mech 0
489+
assert_eq!(dcm.check_matrix[[2, 0]], 0); // D2, mech 0
490+
assert_eq!(dcm.check_matrix[[0, 1]], 0); // D0, mech 1
491+
assert_eq!(dcm.check_matrix[[1, 1]], 1); // D1, mech 1
492+
assert_eq!(dcm.check_matrix[[2, 1]], 1); // D2, mech 1
493+
494+
// Observable matrix: mechanism 0 -> L0; mechanism 1 -> none; mechanism 2 -> L0
495+
assert_eq!(dcm.observable_matrix[[0, 0]], 1);
496+
assert_eq!(dcm.observable_matrix[[0, 1]], 0);
497+
assert_eq!(dcm.observable_matrix[[0, 2]], 1);
498+
}
499+
500+
#[test]
501+
fn test_dem_check_matrix_observables_from_correction() {
502+
let dem = "error(0.01) D0 L0\nerror(0.01) D1 L1\nerror(0.01) D0 D1 L0 L1";
503+
let dcm = DemCheckMatrix::from_dem_str(dem).unwrap();
504+
505+
// Correction activates mechanism 0 -> L0 flips
506+
assert_eq!(dcm.observables_mask_from_correction(&[1, 0, 0]), 0b01);
507+
// Correction activates mechanism 2 -> L0 and L1 flip
508+
assert_eq!(dcm.observables_mask_from_correction(&[0, 0, 1]), 0b11);
509+
// Correction activates mechanisms 0 and 2 -> L0 xor L0 = 0, L1 flips
510+
assert_eq!(dcm.observables_mask_from_correction(&[1, 0, 1]), 0b10);
511+
}
512+
513+
#[test]
514+
fn test_dem_check_matrix_decomposed() {
515+
// Decomposed mechanism: D0 ^ D1 means XOR
516+
let dem = "error(0.01) D0 D1 ^ D1 D2";
517+
let dcm = DemCheckMatrix::from_dem_str(dem).unwrap();
518+
519+
// D1 appears in both components -> XOR cancels it
520+
assert_eq!(dcm.check_matrix[[0, 0]], 1); // D0
521+
assert_eq!(dcm.check_matrix[[1, 0]], 0); // D1 cancels
522+
assert_eq!(dcm.check_matrix[[2, 0]], 1); // D2
523+
}
524+
525+
#[test]
526+
fn test_dem_check_matrix_empty() {
527+
let dem = "";
528+
let dcm = DemCheckMatrix::from_dem_str(dem).unwrap();
529+
assert_eq!(dcm.num_mechanisms, 0);
530+
assert_eq!(dcm.num_detectors, 0);
531+
}
532+
304533
#[test]
305534
fn test_dem_config_builder() {
306535
let config = DemConfigBuilder::new()

crates/pecos-decoder-core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub use advanced::{
2828
pub use config::{
2929
BatchConfig, ConfigBuilder, DecoderConfig, DecodingMethod, PerformanceConfig, SolverType,
3030
};
31-
pub use dem::{DemConfig, DemConfigBuilder, DemDecoder, DemInfo};
31+
pub use dem::{DemCheckMatrix, DemConfig, DemConfigBuilder, DemDecoder, DemInfo};
3232
pub use errors::{ConfigError, DecoderError, ErrorConvert, GraphError, MatrixError};
3333
pub use matrix::{CheckMatrixConfig, CheckMatrixDecoder, SparseCheckMatrix};
3434
pub use results::{

0 commit comments

Comments
 (0)