0016 — Extraction trace-store layout: zero-pad-to-max-T traces, cached patches, and the molecule_key content hash
- Status: accepted
- Date: 2026-06-29
- Deciders: bioedca
- PRD anchor: §5.1 (
/traces,/patches,/molecules,/settings,molecule_key), §7.10 (cross-file join key), Appendix E Stages 14–15, §9 M1, §11.2 - Milestone: M1 (S8 — per-frame background + Sum integration → coordinate-tagged traces)
Context and problem statement
M1 S7 (ADR-0015) produced the donor-anchored molecule list; the integration
primitive integrate_traces (M0.5 S5) already computes the 10-frame temporal-MA
ring background and the top-hat I = TOT − bg·N_psf Sum integration, returning
both the corrected intensity and the uncorrected disk sum. S8 must persist
that result: it is the first writer of extraction data into a .tether.
The open questions the schema skeleton (frozen at M0) deliberately left for the
first writer to settle — /traces, /patches, /settings are forward-declared as
empty container groups, so their per-record payload layout is additive data
this PR defines:
- How are traces laid out when one experiment spans many movies of differing
frame count (
/tracesis a single rectangular array, §5.1)? - What is cached in
/patches? - How is
molecule_key— the cross-file content identity (§7.10) — computed, and how does it differ frommolecule_id? - What goes in the not-yet-computed
/moleculesfields (corrections, bleach, ML class) at extraction, before M3 corrections / M5 ML exist?
Decision drivers
- Additive-only over the M0 freeze — write data into the pre-declared frozen
container groups; never alter structure (
schema-guardstays green, ADR-0005). - Faithful to §5.1 — rectangular zero-padded
/traces, raw and corrected intensities, per-frame background, a cached patch per molecule. - The apparent-E substrate (ADR-0003) — extraction precedes corrections; the store must represent "no factor yet" unambiguously, never a fabricated value.
- A stable cross-file join key (§7.10) —
molecule_keymust survive re-location of the same molecule into a split/subset file and absorb float-repr jitter. - Provenance travels with the datum (NFR-REPRO) — the movie row, the provisional condition, the effective parameters, and the registration confidence are written alongside the traces.
Considered options
Trace layout (/traces).
- A. Six rectangular (n_molecules, max_n_frames) arrays —
{donor,acceptor}_{raw,corrected,background} — chunked + gzip, zero-padded to the
experiment-max frame count as movies are appended; each molecule's frame_range
delimits its valid native extent inside the pad. Chosen — §5.1's "single
rectangular array zero-padded to the experiment-max n_frames" (mirroring
tMAVEN's concatenate_smds pad-to-maxt); storing raw (uncorrected) and
corrected satisfies both §5.1 and the S8 mandate (the uncorrected trace feeds
M3 bleach detection); the per-frame background is kept for QA + reconstruction.
- B. One per-molecule variable-length dataset. Rejected — fragments the store,
defeats vectorized cohort reads, and diverges from the §5.1 rectangular contract.
- C. Store only corrected, reconstruct raw on demand. Rejected — S8 explicitly
requires the uncorrected trace persisted (bleach detection input), and §5.1 says
raw and corrected.
Float precision. float32 for traces + patches — the raw/corrected/background
redundancy is deliberate (§5.1), and float32 halves the (gzip-compressed) store at
ample precision for disk-sum intensities; the FRET histogram / S9 oracle are
scale-robust. Storage, not a scientific factor, so no Consensus gate.
Patch cache (/patches).
- D. One window×window temporal-mean crop per molecule per channel. Chosen —
a denoised representative thumbnail for movie-less curation + the static overlap
view (§5.1), cheap and sufficient.
- E. Full per-frame patch stack. Rejected — redundant with /traces + the
memmap round-trip (§5.2); large for no curation benefit.
molecule_key vs molecule_id.
- F. molecule_id = fresh UUID (stable once assigned); molecule_key =
sha256(movie_sha256 | qx | qy) with donor_xy quantized to 0.1 px. Chosen —
§5.1 distinguishes the two: molecule_id is the globally stable UUID, inherited
unchanged by a split/subset; molecule_key is the content identity that joins
the same molecule across files (§7.10). Quantizing to 0.1 px (below the 8 px
detection min-separation, so no collision between distinct molecules) absorbs
float-repr jitter so a re-located molecule hashes identically. Deterministic, no
salt.
Un-computed /molecules fields at extraction.
- G. alpha/gamma/correction_confidence/quality_class = NaN; delta = 0
(inert, ADR-0008); bleach_frames = (−1, −1); curation_label = 0;
condition_id = the provisional filename parse (= condition_id_provisional,
validated at M4). Chosen — NaN/−1/0 are unambiguous "not computed yet"
sentinels (the apparent-E substrate, ADR-0003); they are never read as a factor (a
finite-factor gate precedes any M3 median, so no NaN reaches E). The registration
molecule_tags (a low-confidence-registration tag for an over-gate fit, §7.1) is
imprinted onto every molecule of the movie.
Decision outcome
Chosen: A + D + F + G, in a new tether.imaging.extract module that follows the
additive-HDF5-write discipline of tether.imaging.calibrate.write_calibration
(ADR-0014): open an existing compatible project r+
(assert_is_compatible_project — refuse a foreign/future/partial file), write only
data into the frozen container groups, movie write-once. Public API:
molecule_key(movie_sha256, donor_xy)andMOLECULE_KEY_QUANTUM_PX = 0.1.extract_molecules(donor_channel, acceptor_channel, molecules, …)→MoleculeTraces(donor + acceptorIntegratedTraces+ temporal-mean patches + the effective parameters).write_extraction(project, *, movie, molecules, traces, parsed, registration_map, settings)→ the freshmolecule_ids; appends/movies+/molecules+ the six/tracesarrays (zero-pad-to-max-T) +/patches+ write-once/settings/extraction.MovieMetadata(the/moviesrow provenance) and minimal readersread_molecules/read_traces/read_patches(the write↔read round-trip; the M2 browser builds the rich view).
Row i of /molecules, every /traces array, and every /patches array is the
same molecule (positional trace↔molecule join).
Consequences
- Good: §5.1 extraction persistence is homed faithfully and additively — proved
by a test asserting
diff_manifest(build_manifest(), introspect(written_file)) == [](the written file differs from a fresh skeleton only by added groups/datasets — the same additive-only invariantschema-guardenforces, applied here to the writer's output rather than to the committed golden the CI gate diffs). The apparent-E substrate (ADR-0003) extends to the data model:NaN/−1sentinels, never a fabricated factor. - New tunable
molecule_key_quantum_px(0.1 px) registered in §11.2. - Trade-off: the raw/corrected/background redundancy triples the trace byte count
before compression — accepted per §5.1 (gzip +
float32keep it modest; raw is the bleach-detection input, not reconstructable-for-free given the temporal-MA background). - Deferred to S9: the real-data extraction-vs-Deep-LASI recall/Pearson/RMS oracle +
the
tether extractCLI (these tests are synthetic top-hats with exact expected intensities); to M2: the curationanalysis_windownarrowing (defaults to the full native window here) and the rich/patchesoverlap UI.
More information
PRD §5.1 (/traces zero-pad-to-maxt, /patches, /molecules, /settings,
molecule_key), §7.10 (cross-file join key), Appendix E Stages 14–15
(deeplasi/functions/traces/extractTracesC.m:13-33, classes/TRACERdata.m:38), §9 M1,
§11.2. ADR-0015 (the donor-anchored molecule list this consumes), ADR-0014 (the
RegistrationMap/tags and the additive-/calibration write precedent), ADR-0008
(δ inert), ADR-0003 (apparent-E / never-fabricate), ADR-0005 (M0 schema freeze).
src/tether/imaging/extract.py, tests/test_extract.py.