Skip to content

Carry observable labels from metadata JSON and reject malformed ones - #388

Merged
ciaranra merged 3 commits into
devfrom
observable-labels-from-metadata
Jul 29, 2026
Merged

Carry observable labels from metadata JSON and reject malformed ones#388
ciaranra merged 3 commits into
devfrom
observable-labels-from-metadata

Conversation

@ciaranra

@ciaranra ciaranra commented Jul 28, 2026

Copy link
Copy Markdown
Member

The observables metadata format carries a label, and callers already write one, but parse_observables_json discarded it -- so a label could only ever reach a DEM through a circuit annotation.

Split out of #384, which is parked. This part stands on its own: it makes the metadata format self-sufficient, independently of any decision about annotation-versus-metadata precedence.

The gap

ParsedObservable was { id, records, meas_ids } -- no label. parse_single_observable read the id and measurement refs and ignored "label" entirely. DEM-output labels are populated in exactly one place, influence_builder.rs:441, from annotation-derived outputs.

So a caller writing

[{"id": 0, "records": [-1], "label": "logical_Z"}]

got an unlabelled observable, with no indication the field had been dropped.

The change

ParsedObservable carries label, parse_single_observable reads it, and try_build attaches it when emitting the observable.

A malformed label is now rejected rather than silently treated as absent:

observable label must be a string or null, got 42

"label": 42 previously parsed as "no label", which is the same silent-discard pattern the richer DEM metadata parser already rejects. An explicit null remains equivalent to omitting the field.

Testing

Both behaviours mutation-verified -- each is killed by reverting the corresponding change:

reverted result
label attached to the DemOutput killed
malformed-label rejection (back to silent as_str drop) killed
  • test_observable_label_comes_from_metadata_without_annotations -- a metadata label reaches the DEM with no annotation present.

  • test_observable_metadata_rejects_non_string_label -- "label": 42 is a ParseError naming the problem.

  • test_observable_metadata_accepts_null_label -- explicit null parses as no label.

  • cargo test -p pecos-qec green: 598 lib tests and 58 doctests, 0 failed.

  • cargo clippy -p pecos-qec --all-targets clean, forced cold; cargo fmt --all --check clean.

Relationship to #384

#384 proposed making metadata authoritative over annotations. That is parked: investigation in #386 established that the annotation-derived observable on the surface path was corrupted rather than legitimately disagreeing, so the precedence question should be decided after #387 fixes measurement-record resolution -- at which point a disagreement means something real and can be rejected loudly.

This label work was originally required by #384 (excluding annotations otherwise lost labels) but is not contingent on it.

Interaction with #385, and a doc correction

#385 (now on dev) made add_observable panic in every build when two definitions of the same observable disagree about the label or the Pauli. Before this PR, metadata labels were discarded, so metadata could never contribute a label and never conflict. After it, it can.

Independent review confirmed by running: a circuit with annotation label logical_Z and metadata label from_metadata on one observable now panics, where on dev it silently keeps one and discards the other. That is #385's stated target case -- its doc names "metadata JSON and a circuit annotation contradicting each other" explicitly -- so the two changes are complementary rather than conflicting. This PR is what makes that case reachable.

Real generators are unaffected. The surface builders write observables JSON with id/records and no label (circuit_builder.py:2681, logical_circuit.py:1008), so the annotation label wins uncontested. Verified by reading both writers and by running a surface-shaped mimic.

Review also found the try_* constructors' docs stale as a result: they promise "returns parser errors instead of dropping malformed metadata", but a metadata-versus-annotation conflict -- newly reachable because of this PR -- panics rather than returning DemBuilderError. All four (try_from_circuit, try_from_circuit_with_noise_config, try_from_tick_circuit, try_from_tick_circuit_with_noise_config) now carry a # Panics section saying so. Malformed metadata is still an error; contradictory sources are not a parse failure and are not reported as one.

@ciaranra
ciaranra merged commit 3ac12a6 into dev Jul 29, 2026
61 checks passed
@ciaranra
ciaranra deleted the observable-labels-from-metadata branch July 29, 2026 04:20
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