Skip to content

Report BpTrellis no-path outcomes with a cause and run the escalation ladder on one model - #839

Merged
ciaranra merged 8 commits into
devfrom
bp-trellis-outcomes
Sep 28, 2026
Merged

ciaranra merged 8 commits into
devfrom
bp-trellis-outcomes

Conversation

@ciaranra

@ciaranra ciaranra commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Summary

Makes a BpTrellis no-path a per-shot outcome with a cause instead of only an error, and runs the escalation ladder as (k, delta) rungs on one engine model with one BP refresh per shot.

Before this change a no-path aborted a whole batch with DecodingFailed, and escalation_ks built a complete engine model per rung and re-ran the untouched-detector precheck and BP scoring for every rung.

Engine (pecos-trellis)

  • TrellisDecoder::prepare(syndrome): dimension check, residual precheck (reports the lowest detector whose residual no probabilistic mechanism can change), one BP refresh into scratch. Readiness is cleared at entry and published only on completion, so a failed prepare can never decode the previous shot.
  • TrellisDecoder::attempt(PruneParams { k, delta }): runs the DP on the prepared shot with the given pruning parameters. Parameters are validated against the decoder's metric mode first; a model configured with BP scoring but holding no BP graph (built exact, or a genuine N-ary kernel) rejects a pruned override rather than silently scoring prior-only; calling attempt without a ready prepare is a panic.
  • decode_attempt is now prepare followed by attempt at the configured parameters. The byte-frozen parity snapshots from Add byte-frozen bitwise parity snapshots for the trellis engine and BpTrellis facade #832 pass unchanged in both crates (regeneration was forbidden during this work), so the engine's numbers are bit-identical, including the streaming decoder, which passes its configured values through the same range function.
  • TrellisDecodeAttempt::NoPath gains dropped_states; TrellisResult gains bp_runs; new bp_refreshes(), forced_observables(), prune_params().

Facade (pecos-bp-trellis)

  • BpTrellisConfig::escalation: Vec<EscalationRung { k, delta }> replaces escalation_ks. Rungs are validated individually; there is no ordering or dominance rule between them (a narrower rung can succeed where a wider one fails, and a test pins that case). A non-empty ladder on an exact base is rejected, since an exact base never drops a state.
  • BpTrellisDecoder holds one TrellisDecoder. decode_outcome returns Decoded(TrellisResult) or NoPath(NoPathReport) with a cause: Residual { detector } (no attempt is run), Infeasible (an attempt found an empty column with nothing pruned, which proves the syndrome is unexplainable under the DEM; the ladder stops there, whether at the base or at a rung), or Exhausted (every attempt ran out of states after pruning). The report carries the placeholder mask (the initial forced contribution of probability-one mechanisms, not a correction), rungs_tried, transitions summed over attempts, and bp_runs/bp_seconds counted once per shot.
  • decode keeps its error contract; the message now names the cause. decode_batch is unchanged; decode_batch_outcomes is its per-shot-outcome twin. The ObservableDecoder implementation stays strict, so the DecoderSpec and batch.decode() routes are unchanged.

Python (pecos_rslib_exp)

  • BpTrellisDecoder.from_dem(...) and bp_trellis(...) accept escalation=[(k, delta), ...] alongside the kept escalation_ks=[...] (rungs at the base delta). Supplying both raises ValueError. Spec equality is on the resolved rungs; repr round-trips, printing whichever form reproduces the ladder.
  • decode_syndrome, decode_from_defects and decode_batch(shots, workers=1) take on_no_path="raise" (default, unchanged behaviour) or "report", which returns a BpTrellisNoPath in place for a no-path shot (cause, detector, placeholder_flips, rungs_tried, transitions, bp_runs, bp_seconds, no_path=True). The mask getter is deliberately not named observable_flips, so code written for a decoded correction raises AttributeError on a report instead of silently consuming a placeholder. Other errors still raise with their shot index. BpTrellisResult gains no_path=False and bp_runs.
  • The spec factory accepts no on_no_path; the spec route stays strict.

Docs

The user guide carries the full description; the workflow page, the experimental guide and the crate README point to it.

Verification

  • cargo test --locked -p pecos-trellis -p pecos-bp-trellis -p pecos-frontier --no-fail-fast, both snapshot tests with the committed JSON untouched, cold cargo clippy --locked --workspace --all-targets -- -D warnings, cargo fmt --all -- --check, the pecos-rslib-exp extension rebuild, the BpTrellis and Frontier Python suites (199 tests), and pre-commit run --all-files twice: all clean.
  • Every new guard has a row in the crate's MUTANTS.md with its exact edit and killer; 71 mutants were applied in a workspace copy and all fail their named test. Two independent review arms (engine parity; API, Python surface and docs) found no correctness defect; the surviving mutants they found (a half-exact capability probe, a residual shot with BP enabled, and Python no-path getters asserted only against zeros) now have killers.
  • The frontier crate changed only in two unit-test struct literals that name the new zero-valued fields; its snapshot and upstream fixtures pass unchanged.

Review follow-ups

Applied after review, each verified by execution rather than asserted:

  • The Exhausted message is pluralized, so a one-rung ladder reads after 1 escalation rung. Its three pins and the two mutation rows whose recorded output embedded the old wording were re-run, not hand-edited.
  • exhausted_ladder_propagates_the_final_rung_error is renamed to exhausted_ladder_reports_the_attempted_rung_count: it pins the synthesized Exhausted message and no longer propagates a rung's own error.
  • The Python no-path mask getter is placeholder_flips, matching the Rust field name. The suite now pins the separation in both directions with assert not hasattr(...).
  • Two rows in exp/pecos-trellis/tests/MUTANTS.md were missing the First failing line verbatim column the file's own contract requires; both mutants were applied and re-run to fill them. exhausted_message_wrong's edit anchor was refreshed for the new format string, and two new guards (exhausted_message_always_plural, python_no_path_aliases_observable_flips) were added, applied and killed.

Re-verification after these changes: cargo fmt --all -- --check, cold cargo clippy --locked -p pecos-rslib-exp -p pecos-trellis -p pecos-bp-trellis --all-targets -- -D warnings, cargo test --locked -p pecos-trellis -p pecos-bp-trellis -p pecos-frontier --no-fail-fast (21 test binaries), the BpTrellis Python suites (48 tests) against a rebuilt extension, and pre-commit run --all-files: all clean, with the frozen snapshot JSON untouched.

Independent cross-review of the follow-ups

The review follow-ups above were themselves cross-reviewed by a separate arm, scoped to those commits only. It returned REVISE with three guard defects, all confirmed by execution here and all fixed:

  • The Python pin match="after 1 escalation rung" is a regex search, so it also accepted rungs — a live always-plural mutant passed that test. It is now r"after 1 escalation rung\\Z", and the same mutant fails it: Regex pattern did not match. Expected regex: 'after 1 escalation rung\\Z'.
  • The new exhausted_message_always_plural row named an integration killer, but the file's documented command is --lib NAME, which runs 0 tests and exits 0 for that name (verified: 0 passed; 0 failed; 13 filtered out). The killer is now listed among the --test bp_trellis exceptions, and the row records both its Rust and Python killers.
  • Verbatim diagnostic cells used &#96; inside single-backtick code spans, which Markdown renders as the literal entity rather than a backtick, so the recorded output did not match the real compiler diagnostic. All 30 such cells across both files now use double-backtick delimiters; only delimiters changed, no quoted content.

The reviewing arm found no defect in the runtime rename or the pluralization themselves, and confirmed rename completeness across bindings, the spec route, docs, tests, stubs and examples.

Not in this PR

  • A default ladder stays empty. The evaluation campaign's best measured ladder is (64, 100.0); turning it on is a separate decision.
  • Non-aborting batch.decode() through the spec route needs a per-shot channel in the provider protocol (v2) and is not attempted here.

@ciaranra
ciaranra merged commit 7926fe4 into dev Sep 28, 2026
45 checks passed
@ciaranra
ciaranra deleted the bp-trellis-outcomes branch September 28, 2026 17:44
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