Declared-metadata annotation evidence¶
caption.declared.v1 is the one shipped annotation template. Its
evidence comes from the metadata the generator declared when it composed each
scene, never from measuring record.iq: the generator already holds every
signal’s class, bandwidth, carrier, timing, and signal-to-noise ratio (SNR)
exactly, so the annotation path reads that truth instead of re-estimating it
from the waveform. Component count equals declared emitter count by
construction: a scene with N declared emitters is annotated as exactly N
components, up to the configured component_cap (default 32), beyond which
the run either errors or truncates with a citable overflow marker. Nothing in
the pipeline can promote noise into a component.
Scene-report annotations are training labels for models that ingest raw I/Q (the complex in-phase and quadrature samples), so the evidence and prose contracts below are label-quality contracts. Evidence stays exact and structured; prose stays inferential and hedged, because that is the epistemic voice a deployed waveform-only model must learn.
Evidence sources: sanitized evidence in, never a record out¶
build_evidence(record, *, config)
is the single
StoredRecord-to-evidence
conversion, and every route reaches it. The record-addressed routes call it
while assembling the prompt: signal_dataset, locally or on Dataproc, and
the Signal Dataset snapshot route
reaches it through an EvidenceSource, under a local Spark driver and on
Dataproc alike. Every route therefore shares one builder, one system prompt,
and one provider response contract; routes differ only in configuration.
EvidenceSource (see the annotation API
reference)
is the shard-streamed route’s
trust boundary, and it has exactly one abstract method: __iter__.
Implementations read record metadata however their storage backend requires and
convert inside the iterator; only (sample_id, evidence) pairs cross the
boundary. No LabeledScene, StoredRecord, or
raw archive bytes appear in the surface or in any yielded value.
WebDatasetSourceis the shipped implementation. It wraps the verified shard metadata iterator, which decodes each sample’s.jsonmember and never decodes.iq.npy, and it runs identically under a local Spark driver and inside Dataproc Serverless executors.LocalHdf5Sourceships beside it as a metadata-only reference implementation, wrappinghandle.read_metadata(sample_id). It is on no run path; the snapshot route reads metadata through the record-addressed loop instead.
The claim shape: payload and bookkeeping are structurally separable¶
Every claim in declared-evidence.v1 carries exactly one payload branch
(quantity with a value and unit, or label with a canonical string) and one
bookkeeping sub-object holding provenance, source field, derivation method,
and any availability limitation. A training pipeline that wants payloads only
drops the single bookkeeping sibling per claim; it never string-matches
bookkeeping field names. Provenance is one of declared by generator
(copied verbatim), derived from declared metadata (arithmetic on declared
fields), or capture configuration (receiver settings). A derived claim always
names its method; a declared one names it only when the field it copies is
ambiguous, as bandwidth_hz is: one emitter declares a frequency-hopping
span there and another a measured 99%-energy figure, so the claim says which.
A value that cannot be written as a number becomes a visible limitation
(quantity: null plus a bookkeeping.limitation), never a filled-in number.
Which limitation says why:
Limitation |
Meaning |
|---|---|
|
the generator declined to say, or said something unusable |
|
it said infinity: a component with no noise, or no power |
|
the receiver frame the claim needs was never declared |
|
the record declares that frame two ways, and they disagree |
|
a frequency relation over a hop span, which is not an occupancy |
|
a time relation over a presence envelope, not on-air time |
This is why a stock narrowband-baseline annotation states no SNR: its shipped
configuration leaves declared SNR infinite, so the claim reads
unbounded_declared_value. Declare a finite per-emitter SNR in the generation
config, as multi-emitter-baseline does at 20 dB, and the quantity appears.
Pairwise interactions are interval arithmetic over declared extents, never measurements, and they are withheld rather than approximated when the extents do not mean what the relation’s name asks. A frequency-hopping link declares the span it sweeps rather than what it occupies at any instant, and a beacon declares an extent covering the capture while transmitting for a thousandth of it; intersecting either would report emitters as permanently co-channel or permanently coincident. Where the generator declares a duty cycle, it is surfaced as its own claim so the envelope can be read correctly.
Field selection is configuration; internals are unreachable¶
Which fields are surfaced is user configuration; the builder’s extractor registry defines what can be read at all.
Field group |
Extractor |
Why |
|---|---|---|
Modulation class, family, taxonomy |
Yes |
Inferable from the waveform |
Bandwidth, carrier offset, timing |
Yes |
Inferable from the waveform |
|
Yes |
Inferable from the waveform |
Declared device or platform identity |
Yes, where a dataset provides it |
A dataset-level fact, marked declared |
|
None |
Unknowable from a waveform; training on them teaches a model to fabricate |
No configuration can surface a field with no extractor. A canonical-label sanitizer additionally rejects identifier-shaped strings beneath every configured label.
declared_evidence:
component_quantity_fields: [occupied_bandwidth_hz, frequency_offset_hz, snr_db, sinr_db]
component_label_fields: [class_name, family]
Configure sinr_db alongside snr_db, not instead of it. Declared snr_db is
per-emitter signal-to-noise and ignores co-channel emitters entirely.
multi-emitter-baseline packs three to eight emitters into ±700 kHz and labels
every one of them 20 dB, so once several share a channel the recoverable SINR
falls well below the declared SNR and can go negative.
The 2026-08-08 declared-evidence
run shows the same
gap on a denser hand-assembled dataset, though only through the model’s own
prose: on one scene it reports per-component SINRs from about -3 dB to +0.1 dB,
and elsewhere on the page down to about -5 dB, against 20 dB declared SNR. That
page records no SINR column of its own. Surfacing
snr_db alone teaches a model to read 20 dB off a waveform whose recoverable
SINR is near zero.
The component cap is configuration; truncation is visible¶
declared_evidence.component_cap bounds the component list (default 32).
Under the default overflow: "error", a scene declaring more emitters than
the cap fails the run loudly. Under overflow: "annotate_truncated", the
first cap components in declaration order are included and the evidence
carries a citable /overflow marker stating the declared total and included
count; the scene-level component_count claim always states the true
declared total, and the system prompt instructs the model to narrate the
truncation. There is no silent truncation path.
Naming signals: observables only¶
The prompt requires an inferential voice with values rounded to a few
significant figures, and it requires every signal to be named by what a
receiver can observe: frequency position first, adding bandwidth or
modulation class only to separate co-located signals. Internal identifiers
like component-0 are JSON keys in the evidence and must never appear in
prose, for the same reason device_id and generator_name are excluded from
evidence entirely: a model reading only a waveform cannot recover an index
that encodes generator emitter order, so training on one teaches fabrication.
What can reject a generated label¶
Two things, and nothing else: the reply does not parse into the narrative model, or the resulting row exceeds its serialized byte budget. There are no paragraph-count, character, or citation limits, because a cap on prose discards correct training labels without protecting anything.
This makes no assertion about whether the prose reflects the evidence; no automated check can. Hosted-model output is non-deterministic, so judge narrative quality by comparing the embedded evidence against the text, with sampled human review or a larger model.
The measured-evidence template is gone¶
scene_report.evidence.v2 measured its evidence out of the waveform with a
local STFT. It has been removed, and caption.declared.v1 is the only
annotation template.
A thresholded spectrogram merges co-channel emitters into one component, fragments a hopping emitter into several, and under-reports SNR for anything wider than one FFT bin. Its detector could also report components that correspond to no declared emitter. Every one of those is a wrong training label, and the generator already knows what it placed, so measuring it back out could only lose information and invent errors.