Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
80f63c7
Add pecos-frontier: native Frontier approximate logical-ML decoder
ciaranra Aug 3, 2026
fc6d9e5
Align log_evidence with upstream total-mass semantics, fail-fast conf…
ciaranra Aug 3, 2026
9fc312a
Fix wide-label tie-break word order, reject duplicate mechanism indic…
ciaranra Aug 3, 2026
160a6c4
Port upstream score_alpha suffix-compatibility pruning with golden-fi…
ciaranra Aug 3, 2026
d0c2a8c
Add bridge_ab example for cross-implementation A/B against upstream f…
ciaranra Aug 3, 2026
9655dff
Clarify frontier feature is a native implementation, not an upstream …
ciaranra Aug 3, 2026
7b6cc7d
Move pecos-frontier to exp/ pending broader validation; drop meta-cra…
ciaranra Aug 3, 2026
919530a
Add orchestrator-owned ordering and committee golden fixtures from up…
ciaranra Aug 3, 2026
78958a9
Add deadline ordering generation and forward/backward committee decod…
ciaranra Aug 3, 2026
dcff371
Expose FrontierDecoder and FrontierCommitteeDecoder in pecos-rslib-ex…
ciaranra Aug 3, 2026
05f09f9
Add paired DUT/reference decoder comparison with joint outcome counts
ciaranra Aug 4, 2026
1dd92a7
Apply lint autofixes to decoder comparison test
ciaranra Aug 4, 2026
cd98f46
Add versioned shot-corpus save/load with resolved-seed provenance
ciaranra Aug 6, 2026
2d42473
Fix ruff import organization in frontier decoder test
ciaranra Aug 6, 2026
c0aba75
Authenticate the whole corpus file, bound degenerate dimensions, and …
ciaranra Aug 6, 2026
9d87ba0
Fix 32-bit dimension-count overflow in DEM parsers and make decode-co…
ciaranra Aug 8, 2026
71545e7
Merge remote-tracking branch 'origin/decoder-eval-harness' into front…
ciaranra Aug 8, 2026
26dd112
Extend bridge_ab to fired-index syndromes for models beyond 128 detec…
ciaranra Aug 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ pecos-eeg = { version = "0.2.0-dev.0", path = "exp/pecos-eeg" }
pecos-stab-tn = { version = "0.2.0-dev.0", path = "exp/pecos-stab-tn" }
pecos-experimental = { version = "0.2.0-dev.0", path = "exp/pecos-experimental" }
pecos-foreign = { version = "0.2.0-dev.0", path = "crates/pecos-foreign" }
pecos-frontier = { version = "0.2.0-dev.0", path = "exp/pecos-frontier" }
pecos-fusion-blossom = { version = "0.2.0-dev.0", path = "crates/pecos-fusion-blossom" }
pecos-gpu-sims = { version = "0.2.0-dev.0", path = "crates/pecos-gpu-sims" }
pecos-hugr = { version = "0.2.0-dev.0", path = "crates/pecos-hugr" }
Expand Down
63 changes: 55 additions & 8 deletions crates/pecos-decoder-core/src/dem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@

use crate::errors::DecoderError;

fn dimension_count(max_index: Option<u32>, kind: &str) -> Result<usize, DecoderError> {
max_index.map_or(Ok(0), |index| {
let count = u64::from(index) + 1;
usize::try_from(count).map_err(|_| {
DecoderError::InvalidConfiguration(format!(
"{kind} count for index {index} does not fit usize on this platform"
))
})
})
}

/// Trait for decoders that can be constructed from detector error models
pub trait DemDecoder: super::Decoder {
/// Configuration type for DEM construction
Expand Down Expand Up @@ -173,8 +184,20 @@ pub mod utils {
}
}

let detector_count = max_detector.map_or(0, |m| m + 1);
let observable_count = max_observable.map_or(0, |m| m + 1);
let detector_count = max_detector.map_or(Ok(0), |index| {
index.checked_add(1).ok_or_else(|| {
DecoderError::InvalidConfiguration(format!(
"detector count for index {index} does not fit usize on this platform"
))
})
})?;
let observable_count = max_observable.map_or(Ok(0), |index| {
index.checked_add(1).ok_or_else(|| {
DecoderError::InvalidConfiguration(format!(
"observable count for index {index} does not fit usize on this platform"
))
})
})?;

Ok((detector_count, observable_count))
}
Expand Down Expand Up @@ -398,8 +421,8 @@ impl SparseDem {
Ok(Self {
mechanisms,
detector_coords,
num_detectors: max_detector.map_or(0, |m| m as usize + 1),
num_observables: max_observable.map_or(0, |m| m as usize + 1),
num_detectors: dimension_count(max_detector, "detector")?,
num_observables: dimension_count(max_observable, "observable")?,
})
}
}
Expand Down Expand Up @@ -552,8 +575,8 @@ impl DemCheckMatrix {
mechanisms.push((probability, detectors, observables));
}

let num_detectors = max_detector.map_or(0, |m| m as usize + 1);
let num_observables = max_observable.map_or(0, |m| m as usize + 1);
let num_detectors = dimension_count(max_detector, "detector")?;
let num_observables = dimension_count(max_observable, "observable")?;
let num_mechanisms = mechanisms.len();

// Build matrices.
Expand Down Expand Up @@ -839,8 +862,8 @@ impl DemMatchingGraph {
fault_id += 1;
}

let num_detectors = max_detector.map_or(0, |m| m as usize + 1);
let num_observables = max_observable.map_or(0, |m| m as usize + 1);
let num_detectors = dimension_count(max_detector, "detector")?;
let num_observables = dimension_count(max_observable, "observable")?;

let edges = Self::merge_parallel_edges(edges);

Expand Down Expand Up @@ -1262,6 +1285,30 @@ mod tests {
assert_eq!(dets, 8, "parse_dem_metadata must count bare detector D7");
}

#[test]
fn sparse_dem_max_u32_detector_index_is_platform_checked() {
let dem = "error(0.01) D4294967295\n";
let parsed = SparseDem::from_dem_str(dem);

#[cfg(target_pointer_width = "64")]
{
assert_eq!(parsed.unwrap().num_detectors, 4_294_967_296);
assert_eq!(utils::parse_dem_metadata(dem).unwrap().0, 4_294_967_296);
}

#[cfg(target_pointer_width = "32")]
{
// On 32-bit targets, the promoted u64 count reaches the fallible
// usize::try_from branch instead of wrapping or panicking.
let error = parsed.unwrap_err();
assert!(matches!(error, DecoderError::InvalidConfiguration(_)));
assert!(error.to_string().contains("4294967295"));

let metadata_error = utils::parse_dem_metadata(dem).unwrap_err();
assert!(metadata_error.to_string().contains("4294967295"));
}
}

#[test]
fn test_parsers_reject_malformed_detector_token() {
// A `D<bad>` / `L<bad>` token in an error line is malformed. All three
Expand Down
28 changes: 28 additions & 0 deletions exp/pecos-frontier/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "pecos-frontier"
version.workspace = true
edition.workspace = true
readme = "README.md"
authors.workspace = true
homepage.workspace = true
repository.workspace = true
license.workspace = true
keywords.workspace = true
categories.workspace = true
description = "Frontier approximate logical maximum-likelihood decoder for PECOS"
publish = false

[dependencies]
pecos-decoder-core.workspace = true

[lib]
name = "pecos_frontier"

[dev-dependencies]
rand.workspace = true
rand_xoshiro.workspace = true
serde.workspace = true
serde_json.workspace = true

[lints]
workspace = true
18 changes: 18 additions & 0 deletions exp/pecos-frontier/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# PECOS Frontier Decoder

Native Rust implementation of the Frontier approximate logical maximum-likelihood
decoder (Leverrier & Urbanke, arXiv:2606.20513). Not a wrap of the upstream
`frontier` package; the upstream implementation is used as a verification oracle.

**Experimental** (`exp/`): the algorithm core is enumeration- and upstream-verified
(per-shot parity on matched models), but the crate has not yet accumulated real-user
mileage. Graduation to `crates/` and registration in the `pecos-decoders` meta-crate
are planned once it has been exercised more broadly (larger code families, Python
bindings, human users).

Pruning ranks accumulated prefix log mass plus a `score_alpha`-weighted
suffix-compatibility estimate. Unpruned results are exact and upstream-verified.

Deterministic ordering and tie-breaking are bitwise reproducible for a fixed
build and platform. The platform's `ln` and `exp` implementations may differ
across platforms.
113 changes: 113 additions & 0 deletions exp/pecos-frontier/examples/bridge_ab.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2026 The PECOS Developers
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
// in compliance with the License. You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions and limitations under
// the License.

//! Cross-implementation A/B harness: decode upstream-frontier sample shots
//! with `FrontierDecoder` on the identical model and column order.
//!
//! Input JSON (produced by an external extraction script from the upstream
//! `frontier` package): `{num_detectors, num_observables, mechanisms:
//! [[p, [detectors], [observables]], ...], shots: [{syndrome, truth_logical}]}`
//! where mechanism order IS the processing order and `syndrome` packs detector
//! `i` into bit `i`.
//!
//! Usage: `bridge_ab <model.json> <k> <delta> <score_alpha>`
//! Prints one `shot,predicted,truth,status` line per shot plus a summary line.

use pecos_decoder_core::dem::SparseDem;
use pecos_frontier::{FrontierConfig, FrontierDecoder};
use serde::Deserialize;
use std::collections::BTreeMap;

#[derive(Deserialize)]
struct BridgeModel {
num_detectors: usize,
num_observables: usize,
mechanisms: Vec<(f64, Vec<u32>, Vec<u32>)>,
shots: Vec<Shot>,
}

#[derive(Deserialize)]
struct Shot {
/// Fired detector indices (supports arbitrary detector counts).
fired: Vec<u32>,
truth_logical: u128,
}

fn main() {
let mut args = std::env::args().skip(1);
let path = args
.next()
.expect("usage: bridge_ab <model.json> <k> <delta> <score_alpha>");
let k: usize = args.next().expect("missing k").parse().expect("k");
let delta: f64 = args.next().expect("missing delta").parse().expect("delta");
let score_alpha: f64 = args
.next()
.expect("missing score_alpha")
.parse()
.expect("score_alpha");

let model: BridgeModel =
serde_json::from_str(&std::fs::read_to_string(&path).expect("read model json"))
.expect("parse model json");
let dem = SparseDem {
mechanisms: model.mechanisms,
detector_coords: BTreeMap::new(),
num_detectors: model.num_detectors,
num_observables: model.num_observables,
};
let config = FrontierConfig {
k,
delta,
score_alpha,
column_order: None,
};
let mut decoder = FrontierDecoder::from_sparse_dem(&dem, config).expect("build decoder");

let mut failures = 0_u32;
let mut no_path = 0_u32;
let started = std::time::Instant::now();
assert!(
model.num_observables <= 128,
"bridge truth_logical is u128; wider observables need a format change"
);
let mut syndrome = vec![0_u8; model.num_detectors];
for (shot, entry) in model.shots.iter().enumerate() {
syndrome.fill(0);
for &fired in &entry.fired {
syndrome[fired as usize] = 1;
}
if let Ok(result) = decoder.decode(&syndrome) {
let words = result.predicted.words();
assert!(words.iter().skip(2).all(|&w| w == 0), "label fits u128");
let predicted = u128::from(words.first().copied().unwrap_or(0))
| (u128::from(words.get(1).copied().unwrap_or(0)) << 64);
let status = if predicted == entry.truth_logical {
"ok"
} else {
failures += 1;
"logical_fail"
};
println!("{shot},{predicted},{},{status}", entry.truth_logical);
} else {
failures += 1;
no_path += 1;
println!("{shot},,{},no_path", entry.truth_logical);
}
}
let elapsed = started.elapsed().as_secs_f64();
let trials = u32::try_from(model.shots.len()).expect("shot count fits u32");
println!(
"SUMMARY trials={trials} fail={failures} no_path={no_path} fer={} k={k} delta={delta} alpha={score_alpha} decode_s_mean={}",
f64::from(failures) / f64::from(trials),
elapsed / f64::from(trials),
);
}
Loading