Skip to content

Make conflicting observable label and Pauli definitions fail in every build - #385

Merged
ciaranra merged 2 commits into
devfrom
loud-metadata-failures
Jul 29, 2026
Merged

Make conflicting observable label and Pauli definitions fail in every build#385
ciaranra merged 2 commits into
devfrom
loud-metadata-failures

Conversation

@ciaranra

@ciaranra ciaranra commented Jul 28, 2026

Copy link
Copy Markdown
Member

Two definitions of the same observable that disagree about the label or the Pauli were resolved silently in release builds. Now they fail in every build, with a message naming both values.

Found during the review of #384.

The defect

DetectorErrorModel::add_observable merges a second definition of an already-declared observable. Records combine by parity, which is correct -- they accumulate. But the label and the Pauli identify the observable rather than accumulate, so two definitions disagreeing about either describe different observables sharing an id.

That was handled with debug_assert_eq!. Debug builds panicked; release builds silently kept whichever definition arrived first and discarded the other, producing a quietly mislabelled DEM with no diagnostic. Reachable from user input -- metadata JSON and a circuit annotation contradicting each other is exactly the situation #377 was about.

The change

Both checks are now assert_eq!/assert!, so they fire in every build:

conflicting labels for observable L0: already labelled "first", relabelled "second".
Two definitions sharing an observable id must agree; supply the label from one
source only, or give them distinct ids

Two refinements from review:

  • Validate before mutating. The first revision merged kind and records before checking the label and Pauli, so a caught panic left a half-merged output. Validation now happens first. (Notably the borrow checker enforces this shape: moving the mutation earlier fails to compile, because validation borrows fields the mutation moves.)
  • Compare Paulis ignoring global phase. DemOutput::with_pauli strips phase because it carries no DEM meaning, but pauli is a public field that can be set directly. Comparing raw would have rejected -X against +X -- a spurious panic on input the library explicitly treats as equivalent. A new pauli_eq_ignoring_phase compares phase-free.

Deliberately not converted

The remaining debug_assert!s in this module are not all internal invariants -- an earlier version of this description claimed they were, and that was wrong. FaultMechanism::from_sorted, from_sorted_with_tracked_paulis and MeasurementMechanism::from_sorted are pub and validate their sortedness precondition debug-only, while xor depends on it.

They are left as-is deliberately: the precondition is stated in the function name, these are per-mechanism constructors on the DEM construction path, and always-validating would add an O(n) scan to each call. That is the "trust the caller, state the contract" case rather than boundary validation. Converting them, or adding fallible variants, is a reasonable separate change if the API is revisited.

add_observable and its ~21 callers are infallible, so returning Result would be a public-API redesign rather than a fix to the silent path. Panicking loudly is the smaller change that removes the actual defect.

Also out of scope

Review flagged two wider instances of the same disease, filed for follow-up rather than bundled: apply_pecos_metadata_json and ParsedDem still do last-writer-wins via *existing = target on paths that already return Result and could report properly; and duplicate tracked-Pauli / detector ids remain silently merged.

Testing

The control is what makes this worth reading. On dev, in release:

test result: ok. 1 passed; ... 592 filtered out

Only one of the three tests exists -- both conflict tests were gated #[cfg(debug_assertions)], so the release-mode silent path was untested by construction. That gate is removed.

Each production change was verified to be killed by reverting it:

reverted result
phase-free Pauli comparison killed
assert_eq! back to debug_assert_eq! (release) killed
validate-before-mutate killed

The should_panic strings now match the added detail (already labelled "first", relabelled "second"), not just the unchanged prefix, so they would fail if the improved messages regressed.

New tests: duplicate_observable_definitions_ignore_global_phase, conflicting_definitions_do_not_mutate_before_panicking.

  • cargo test -p pecos-qec green in debug and release: 597 lib tests, 0 failed.
  • cargo clippy -p pecos-qec --all-targets clean, forced cold; cargo fmt --all --check clean.

@ciaranra ciaranra self-assigned this Jul 28, 2026
@ciaranra
ciaranra merged commit 4253900 into dev Jul 29, 2026
61 checks passed
@ciaranra
ciaranra deleted the loud-metadata-failures branch July 29, 2026 02:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant