Make observable metadata win over observable annotations (fixes #377) - #384
Make observable metadata win over observable annotations (fixes #377)#384ciaranra wants to merge 4 commits into
Conversation
|
Independent adversarial review returned REVISE. I verified the load-bearing findings against the code rather than taking them; all three confirmed. Converting this back to draft. Confirmed: the fix covers one of two entry points
let annotation_map = InfluenceBuilder::new(circuit)
.with_circuit_annotations(circuit)
.build();
influence_map.merge_dem_outputs_from(&annotation_map);
// ... metadata read afterwardsSo #377 still reproduces through Confirmed: this PR regresses the observable label on the real surface pathMy end-to-end verification compared error mechanisms only, so it could not see this. The surface generator writes observables JSON without a label ( observables = [{"id": 0, "records": logical_rec_offsets}]and puts the label solely on the annotation ( circuit.observable(obs_refs, label=f"logical_{basis.upper()}")Excluding observable annotations therefore removes Confirmed: the label conflict path is fail-silent in release
On failing fast and loudlyReviewing this against the project's fail-loud rule, the current answer is no, in three places:
Revised plan
The review confirmed the three tests are sound ( |
|
Parking this and splitting out the part that stands alone. Split out: #388 -- carrying observable labels from metadata JSON, plus rejecting a malformed Parked: the precedence change itself. Three reasons.
What should happen insteadAfter #387 fixes measurement-record resolution, a disagreement between metadata and annotations will mean the two sources genuinely differ. At that point the right response is probably to reject it loudly rather than silently pick a winner -- the conflict check prototyped on #377 already works and is what surfaced the Leaving this open as a placeholder for that decision rather than closing it, since the underlying question -- what happens when both sources describe the same output -- is still unanswered and worth answering deliberately. |
Fixes #377.
DetectorErrorModel::from_circuitreturned DEMs with spurious error mechanisms for any circuit carrying both circuit annotations and detector/observable metadata, which is what the surface-code generators produce. An independent Stim oracle agreed with the other DEM path and disagreed withfrom_circuit(86 mechanisms versus 76 at d=3).Root cause
An observable annotation silently overrode the observable declared in metadata JSON.
build_dem_from_circuitinstalled annotation-derived DEM outputs into the influence map (merge_dem_outputs_from) before it read the metadata attributes. The annotation's definition therefore won during mechanism generation even thoughwith_observables_jsonwas called with the caller's explicit declaration.Minimal reproduction -- metadata covers both measurements, the annotation covers only the first, so the probability identifies which definition was used:
The asymmetry is essential. With one measurement per qubit and a symmetric observable the two definitions produce identical DEMs, and an earlier version of this test proved nothing for exactly that reason.
The fix
Metadata is parsed before annotations are collected, and observable annotations are excluded when the circuit supplies observables JSON:
with_circuit_annotations_excluding_observablesis a newpub(crate)sibling sharing one inner implementation, so the ~25 existing callers of the public method are untouched. Tracked-Pauli annotations are still collected either way -- metadata has no equivalent for them.This matches the precedence the very next block already applied to observable records: metadata first, annotations only as fallback.
Coupled fix: observable labels from metadata
Excluding annotations initially broke
sampler_paths_preserve_output_split_for_noiseless_and_forced_faults, which expected a DEM-output label of"obs0"and gotNone.That was not the test encoding the bug.
parse_observables_jsondiscarded thelabelfield.ParsedObservablehad no label, and DEM-output labels were populated in exactly one place (influence_builder.rs:441) from annotation-derived outputs -- so annotations were the only possible source of an observable label, even though the metadata format already carries one and callers already write it.Editing that test to expect
Nonewould have been a real regression. Instead the metadata format is now self-sufficient:ParsedObservablecarrieslabel,parse_single_observablereads it, andtry_buildattaches it when emitting the observable. The previously-failing test now passes on its own merits, with the label coming from the metadata that always declared it.Testing
Four tests, each verified to FAIL when the corresponding change is reverted -- not merely to pass:
test_observable_metadata_wins_over_observable_annotationtest_observable_annotation_used_when_metadata_absenttest_observable_label_comes_from_metadata_without_annotationssampler_paths_preserve_output_split_for_noiseless_and_forced_faults(existing)cargo test -p pecos-qecgreen: 598 lib tests and 58 doctests, 0 failed.cargo check --workspace --all-targets,cargo clippy -p pecos-qec --all-targets,cargo fmt --all --checkall clean.End-to-end verification (draft criterion met)
Built the
pecos-rslibextension from this branch into an isolated venv and re-ran the original Stim comparison. Same script, same circuits, only the binary differs:from_circuitmechanismsdev(pre-fix)Covered:
interaction_basis="cx"andinteraction_basis="szz", szz_physical_prefixes=True, each in Z and X basis, d=3, 1 round, p2-only.influence == stimin both runs, confirming that path was unaffected and that nothing else shifted.The spurious mechanisms reported in #377 are gone.
One methodology note: a
pecos_rslib.pthinitially shadowed the built wheel with the repo source tree, which would have silently tested the pre-fix binary. The run above loads the wheel's ownpecos_rslib.abi3.sofrom the isolated venv.