Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
017a64a
Add DetectorErrorModel.from_guppy and rework lowered QIS replay to st…
ciaranra May 18, 2026
4003dc7
Trace Guppy result() tags to MeasIds for reorder-robust tag-reference…
ciaranra May 18, 2026
4d7299c
Accept D0/L0 id form in from_guppy detectors/observables (normalized …
ciaranra May 18, 2026
46243b0
Document tracked-Pauli qubit-numbering limitation in from_guppy
ciaranra May 18, 2026
53301ed
Add sound HUGR-based result()-tag to measurement extraction in pecos-…
ciaranra May 19, 2026
85acfd9
Excise proven-unsound runtime result()-tag linkage; keep sound HUGR e…
ciaranra May 19, 2026
3802864
Remove diagnostic dump scaffolding from result_tags tests
ciaranra May 19, 2026
29be0f1
Make result_tags clippy-clean (doc backticks, elide lifetime)
ciaranra May 19, 2026
d139cf3
Wire sound HUGR-backed result_tags into from_guppy (Rust-centric, loo…
ciaranra May 19, 2026
b49bac5
Merge remote-tracking branch 'origin/dev' into dem-polish
ciaranra May 19, 2026
f1fa2d6
Address external review: harden HUGR extractor, fix replay (#5/#6), r…
ciaranra May 19, 2026
aa4fb56
Revert broken dynamic-control guard, fail-loud on out-of-range metada…
ciaranra May 19, 2026
a545393
Route all circuit-ingest and public DEM paths through fallible try_bu…
ciaranra May 19, 2026
8971de1
Enforce num_measurements/influence-map consistency in try_build, clos…
ciaranra May 19, 2026
7b1a04c
Replace TRY004 noqas with a private _MetadataError type; drop stray c…
ciaranra May 19, 2026
5225593
Consolidate detector/observable schema validation into the Rust DEM b…
ciaranra May 19, 2026
1fd2937
Tolerate redundant records+meas_ids co-presence, fail loud on non-red…
ciaranra May 19, 2026
b47bbce
Correct stale proposal 001 and try_build rustdoc to match redundancy-…
ciaranra May 19, 2026
b8e73b1
Wire sound result_tags into from_guppy for the straight-line case; co…
ciaranra May 20, 2026
e072aac
Fix result_tags rewriter: strict-parse + redundancy-check instead of …
ciaranra May 20, 2026
e313899
Round-10 doc nits: clarify result_tags is from_guppy-only and alterna…
ciaranra May 20, 2026
653b04c
Add proposal 002: runtime-loop result_tags via dataflow-bound measure…
ciaranra May 20, 2026
a620255
Add proposals 003 (hand-authored tracked Paulis) and 004 (sound DEM f…
ciaranra May 20, 2026
e6338f6
Add proposals 005 (array-valued result) and 006 (linear-combination r…
ciaranra May 20, 2026
64d44e4
Move docs/proposals/ to pecos-docs; update code refs to by-name-only;…
ciaranra May 20, 2026
6ba4adc
Merge origin/dev into dem-polish: pymdown-extensions + idna dependenc…
ciaranra May 20, 2026
d178482
Fix typos pre-commit hook flag in reject_tracked_pauli rustdoc (mis-i…
ciaranra May 20, 2026
f9b2ed0
Drop "in pecos-docs" pointer from public code refs; refresh uv.lock f…
ciaranra May 20, 2026
2f2c283
Remove all 'see proposal' / pecos-docs references from public source;…
ciaranra May 20, 2026
02ca5a4
Remove stale doc stubs and the in-source pointer to a moved future-wo…
ciaranra May 20, 2026
9815c00
Extract _batched_stabilizers and _normalize_ancilla_budget into pecos…
ciaranra May 20, 2026
77d725b
Add ancilla_budget kwarg to get_num_qubits with same clamping as norm…
ciaranra May 20, 2026
e342ef1
Support ancilla-budgeted surface-code memory in from_guppy via batche…
ciaranra May 20, 2026
9b50098
Address round-4 review: fix ruff/black format conflict, add concrete …
ciaranra May 20, 2026
9abf4a5
Add constrained-surface mismatched-num_measurements fail-loud test th…
ciaranra May 21, 2026
3206712
Harden constrained-ancilla from_guppy path: fail-loud trace contract,…
ciaranra May 21, 2026
2c53db3
Generalize surface/sampler DEM paths: patch-faithful traced_qis + cac…
ciaranra May 21, 2026
791b4be
Apply just lint: rustfmt, black, doc-markdown backticks, and typo fix
ciaranra May 21, 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
3 changes: 3 additions & 0 deletions crates/pecos-hugr-qis/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ The compiler supports standard LLVM optimization levels:
pub mod array;
pub mod compiler;
pub mod prelude;
pub mod result_tags;
mod utils;

// Re-export main types and functions
Expand All @@ -74,6 +75,8 @@ pub use compiler::{
// Re-export read_hugr_envelope from utils
pub use utils::read_hugr_envelope;

pub use result_tags::{extract_result_tag_measurements, measurement_op_count};

// Re-export inkwell's OptimizationLevel for convenience
pub use tket::hugr::llvm::inkwell::OptimizationLevel;

Expand Down
220 changes: 220 additions & 0 deletions crates/pecos-hugr-qis/src/result_tags.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
//! Extract the Guppy `result(tag, ...)` -> measurement binding from a HUGR.
//!
//! This is the *sound* source of the tag<->measurement association: in the
//! compiled HUGR, a `tket.result` op's dataflow input is wired (transitively)
//! from the measurement op(s) that produced its value. That wiring is fixed at
//! compile time and is immune to any later QIS/Selene measurement reordering,
//! unlike a runtime op-stream heuristic.
//!
//! Measurement identity here is the *ordinal* of the measurement op in HUGR
//! traversal order. This module only recovers the structural binding; whether
//! that HUGR ordinal coincides with the QIS-trace `result_id`/`MeasId` order
//! is a separate property of the Guppy -> HUGR / Guppy -> trace pipelines
//! agreeing on measurement ordering. Within the narrow scope this module
//! supports (straight-line `result_bool <- tket.bool:read <-
//! Measure/MeasureFree`), that correspondence is **committed-test verified**
//! end-to-end by
//! `tests/qec/test_from_guppy_result_tags.py::test_result_tags_match_positional_records`
//! (a scrambled-`result()`-order Guppy program: `result_tags` DEM
//! byte-identical to the positional-records DEM). Outside that scope
//! (computed / constant / array-valued `result()`, runtime loops) the
//! correspondence is undefined and the extractor / runtime-loop guard reject
//! the case rather than relying on it.
//!
//! Note: a *runtime* loop (e.g. `for _ in range(comptime(n))`, as the surface
//! code uses for rounds) is NOT unrolled in the HUGR -- it has one static
//! measure/result op executed n times. Static extraction therefore yields
//! `tag -> static-measure-op`; expanding that to per-iteration runtime `MeasIds`
//! requires a separate static-op -> runtime-measurement correspondence.

use std::collections::{BTreeMap, HashMap};

use tket::hugr::ops::OpType;
use tket::hugr::types::Term;
use tket::hugr::{HugrView, IncomingPort, Node};

fn extension_ids(op: &OpType) -> Option<(&str, String)> {
let ext = op.as_extension_op()?;
Some((
ext.extension_id().as_ref(),
ext.unqualified_id().to_string(),
))
}

fn is_measurement(op: &OpType) -> bool {
matches!(
extension_ids(op),
Some((ext, ref name))
if ext == "tket.quantum" && (name == "Measure" || name == "MeasureFree")
)
}

/// Number of *static* measurement ops in the HUGR.
///
/// For a straight-line program this equals the runtime measurement count; for
/// a program with a runtime loop it is strictly smaller (the loop body's
/// measure op is counted once). Callers use the mismatch to detect that
/// per-occurrence tag binding is not statically available.
#[must_use]
pub fn measurement_op_count<H: HugrView<Node = Node>>(hugr: &H) -> usize {
hugr.nodes()
.filter(|&n| is_measurement(hugr.get_optype(n)))
.count()
}

/// Map each `result(tag, <measurement>)` to the measurement ordinal it records.
///
/// **Sound by construction, narrow by design.** Only the canonical pattern
/// `result(tag, <a single raw measurement bit>)` is recognized: a
/// `tket.result:result_bool` op whose value input is *exactly*
/// `tket.bool:read` of a measurement op. The compiled chain is verified to be
/// precisely `result_bool <- tket.bool:read <- Measure/MeasureFree`.
///
/// Any other shape is **deliberately excluded** (the tag is omitted from the
/// returned map) rather than guessed at -- e.g. computed values
/// (`result("x", m0 == m1)` lowers through `tket.bool:eq`), constants
/// (`result("x", True)` lowers through a `Const`), and array-valued
/// `result(...)` (`result_array_bool` lowers through `collections.borrow_arr`
/// machinery that does not cleanly expose per-element measurement provenance).
/// Resolving those structurally would silently misbind (equality is not
/// parity; an empty record set is not a detector), so they are not returned.
///
/// A tag repeated across the program accumulates its ordinals in traversal
/// order; callers handle occurrence disambiguation / loop guarding.
#[must_use]
pub fn extract_result_tag_measurements<H: HugrView<Node = Node>>(
hugr: &H,
) -> BTreeMap<String, Vec<usize>> {
// Pass 1: ordinal for every measurement op, in traversal order.
let mut meas_ordinal: HashMap<Node, usize> = HashMap::new();
for node in hugr.nodes() {
if is_measurement(hugr.get_optype(node)) {
let next = meas_ordinal.len();
meas_ordinal.insert(node, next);
}
}

// single_linked_output source op, if any.
let src_op = |node: Node, port: usize| -> Option<Node> {
hugr.single_linked_output(node, IncomingPort::from(port))
.map(|(s, _)| s)
};

// Pass 2: accept only result_bool <- tket.bool:read <- measurement.
let mut out: BTreeMap<String, Vec<usize>> = BTreeMap::new();
for node in hugr.nodes() {
let op = hugr.get_optype(node);
let Some((ext, name)) = extension_ids(op) else {
continue;
};
if ext != "tket.result" || name != "result_bool" {
continue; // arrays / non-bool result ops: not soundly resolvable
}
let Some(ext_op) = op.as_extension_op() else {
continue;
};
let Some(tag) = ext_op.args().iter().find_map(|a| match a {
Term::String(s) => Some(s.clone()),
_ => None,
}) else {
continue;
};

// result_bool value input (port 0) must be exactly `tket.bool:read`.
let Some(read) = src_op(node, 0) else {
continue;
};
match extension_ids(hugr.get_optype(read)) {
Some((e, ref n)) if e == "tket.bool" && n == "read" => {}
_ => continue, // e.g. tket.bool:eq (computed) -> exclude
}
// ... whose input (port 0) must be a measurement op.
let Some(meas) = src_op(read, 0) else {
continue;
};
let Some(&ord) = meas_ordinal.get(&meas) else {
continue; // e.g. a Const -> exclude
};
out.entry(tag).or_default().push(ord);
}
out
}

#[cfg(test)]
mod tests {
use super::*;
use crate::read_hugr_envelope;

// Fixtures compiled from Guppy (committed so the regression does not
// depend on a Python toolchain at test time):
// scrambled: result() declared c,a,b over measures a,b,c (raw scalars)
// looped: for _ in range(comptime(3)): result("synx", measure(q))
// computed: result("eq", m0==m1) ; result("const", True)
// arr: result("pair", measure_array(qs)) (array-valued)
const SCRAMBLED: &[u8] = include_bytes!("../tests/fixtures/scrambled.hugr");
const LOOPED: &[u8] = include_bytes!("../tests/fixtures/looped.hugr");
const COMPUTED: &[u8] = include_bytes!("../tests/fixtures/computed.hugr");
const ARR: &[u8] = include_bytes!("../tests/fixtures/arr.hugr");

/// Foundation: `result()` declared in scrambled order (c, a, b) over
/// measurements made in order (a, b, c) must still bind each tag to ITS
/// OWN measurement. This is the exact case the prior runtime read/store
/// heuristic got wrong (it produced `{tag_c: [0,1,2]}`); the HUGR
/// structural binding is immune to declaration/measurement-order skew.
#[test]
fn scrambled_binds_each_tag_to_its_measurement() {
let hugr = read_hugr_envelope(SCRAMBLED).unwrap();
let map = extract_result_tag_measurements(&hugr);
assert_eq!(
map,
BTreeMap::from([
("tag_a".to_string(), vec![0]),
("tag_b".to_string(), vec![1]),
("tag_c".to_string(), vec![2]),
]),
"tag must bind to its own measurement regardless of result() order",
);
}

/// Documents the known limitation: a runtime `for _ in range(comptime(n))`
/// loop is NOT unrolled in the HUGR, so a tag emitted once per iteration
/// has a single static measure op. Per-iteration expansion needs a
/// separate static-op -> runtime-measurement correspondence.
#[test]
fn looped_tag_is_single_static_measure_op() {
let hugr = read_hugr_envelope(LOOPED).unwrap();
let map = extract_result_tag_measurements(&hugr);
assert_eq!(
map.get("synx").map(Vec::as_slice),
Some([0].as_slice()),
"runtime loop is not unrolled in HUGR: one static measure op",
);
}

/// Soundness: a computed `result("eq", m0 == m1)` (lowers through
/// `tket.bool:eq`) and a constant `result("const", True)` (lowers through
/// a `Const`) must NOT be returned -- resolving them would silently
/// misbind (equality is not parity; no measurement at all).
#[test]
fn computed_and_constant_tags_are_excluded() {
let hugr = read_hugr_envelope(COMPUTED).unwrap();
let map = extract_result_tag_measurements(&hugr);
assert!(
!map.contains_key("eq") && !map.contains_key("const"),
"computed/constant tags must be excluded, got {map:?}",
);
}

/// Soundness: an array-valued `result("pair", measure_array(qs))` lowers
/// through `collections.borrow_arr` machinery with no clean per-element
/// measurement provenance, so it must NOT be returned.
#[test]
fn array_valued_tag_is_excluded() {
let hugr = read_hugr_envelope(ARR).unwrap();
let map = extract_result_tag_measurements(&hugr);
assert!(
!map.contains_key("pair"),
"array-valued result tag must be excluded, got {map:?}",
);
}
}
Binary file added crates/pecos-hugr-qis/tests/fixtures/arr.hugr
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion crates/pecos-qec/src/fault_tolerance/dem_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ mod mem_builder;
pub(crate) mod sampler;
mod types;

pub use builder::{DemBuilder, DemBuilderError};
pub use builder::{DemBuilder, DemBuilderError, resolve_result_tags};
pub use dem_sampler::{SamplingEngine, SamplingStatistics};
pub use equivalence::{
ComparisonDetails, ComparisonMethod, DemParseError, EffectKey, EquivalenceResult,
Expand Down
Loading
Loading