Skip to content

In-situ electrochemistry alignment + movie burn-in/export overhaul - #121

Open
CSSFrancis wants to merge 3 commits into
feat/seg-fast-enginefrom
feat/insitu-echem-export
Open

In-situ electrochemistry alignment + movie burn-in/export overhaul#121
CSSFrancis wants to merge 3 commits into
feat/seg-fast-enginefrom
feat/insitu-echem-export

Conversation

@CSSFrancis

Copy link
Copy Markdown
Owner

Stacked on feat/seg-fast-engine — it touches 7 files that branch also
changes, so the diff here is just this work (~1,100 lines, 3 commits).
Retarget to main once the seg PR merges.

What this does

Aligns a potentiostat record with an in-situ movie and lets you burn the
result into an exported video.

An in-situ experiment records the same event twice — the camera's frames and
the instrument's E/I — on two machines with two unsynchronised clocks and two
sampling rates. spyde/insitu/ reads both (DE timestamp sidecars; BioLogic
.mpr/.mpt/.mps), solves the offset, and resamples onto the frame times.
Opening a .mrc auto-discovers a matching record in the same folder and
registers E and I as navigator chips; File ▸ Load In-Situ Data… loads one
from elsewhere.

The load-bearing decision

Alignment matches on SPAN, not on the wall clocks. On the reference
dataset the two PCs disagreed by 59 min 41 s (the EC box was on UTC+1,
the camera stamps UTC). Duration agreement got 0.104 s over 259.3 s and
needs no clock agreement at all — immune to unsynced PCs, timezones and DST.
The absolute route stays as a fallback, and the implied UTC offset is
reported rather than hidden.

Three upstream RosettaSciIO bugs, fixed at load

  1. Time axis 4× too fast. Derived 1/(fps*sum_count) where a summed
    movie's saved period is sum_count/fps — wrong by sum_count**2. Here
    122.1 → 30.525 fps. The timestamps are ground truth.
  2. TEM images calibrated as diffraction. camera_length != -1, but an
    imaging exposure records 0 — so a real-space image got nm^-1 units,
    the unset -1 pixel size and kx/ky axes, while
    Specimen Pixel Size X = 1.14786 nm sat in the same file.
  3. The recorded frames_per_second is corrected to match, rather than
    flipping metadata_extract's deliberate key-beats-axis precedence.

Own .mpr parser rather than galvani (GPLv3+, so licence-compatible): it
mis-reads extended column IDs — its v0 path takes every other byte, so ID 438
step time/s reads as 182 — and failed on 1 of the 6 real files. Validated
against an ASCII export of the same run to 1e-8.

Movie export

One burn-in object. There were three ways to put text on a frame (free
text in annotations, the timestamp as a bool param at a hardcoded (6,4),
instrument channels in text_overlays). Now one text_overlays entry
differing only in where its string comes from: builtin:"label",
builtin:"time", or insitu_channel. One action, one source list, one
timeline lane, one inspector. Legacy specs migrate in place once.

Resampling now differs by control, on purpose: spatial downsample
box-means (already did); fps INTEGRATES the frames each output frame
stands for (30.5 → 12 fps averages ~3 — real SNR on noisy data); speed
segments SUB-SELECT
— a 32× jump advances ~81 frames but keeps the
fps-sized window, since integrating 81 would smear a second of change into
one picture.

Bugs found and fixed along the way

  • Stacked lane cursors jumped to and fro mid-drag: _busy clears
    synchronously but the index hook's write-back is marshalled from the
    _NavDispatcher thread and lands after it. One master, rest follow.
  • sync_overlay_widgets cleared the overlay dict but never removed the label
    widgets — every resync stacked another copy, so one add read as two.
  • Burn-in labels had no drag handler, so moving one in the editor never
    reached the spec and the export drew it at the original position.
  • _bake_poster imported _overlay_value_at from pipeline, where it has
    never existed — latent until the timestamp became an overlay, then every
    poster silently failed to bake.
  • Uncalibrated navigator lanes plotted over frame index while the selector
    works in seconds, so the cursor only reached the first ~3%.
  • Encoder quality 7 → 9 + a 24-frame GOP cap: a fast-forward's large
    inter-frame deltas starved high-frequency detail, so the previous frame's
    timestamp ghosted through as text tearing.

Verification

  • 976 passed, 1 xfailed across all 54 suites touching these changes.
  • 8/8 e2e against a real 132 GB DE acquisition + its BioLogic records
    (electron/tests/insitu_echem.spec.ts; skips when the dataset is absent).
    Includes a real cursor drag — samples monotonic at exactly the mouse step.
  • Typecheck clean, frontend builds.

Not verified

The movie editor UI is headless-only: the rail grouping, the burn-in
inspector, dragging a label and re-exporting, and whether the fast-forward
tearing is actually gone. Those need a real session.

…me times

An in-situ experiment records the same event twice -- the camera's frames and
the potentiostat's E/I -- on two machines with two unsynchronised clocks and
two sampling rates. `spyde/insitu/` reads both and puts them on one axis:

* `de_movie.py` -- the DE `*_movie_timestamps.csv` / `*_info.txt` sidecars.
* `eclab.py` -- BioLogic `.mpr` (binary), `.mpt`/`.txt` (ASCII), `.mps`.
* `align.py` -- the clock solve + resample onto the frame times.
* `attach.py` -- registers the channels as navigator chips on the tree.

Alignment matches on SPAN, not on the wall clocks. On the reference dataset
the two PCs disagreed by 59 min 41 s (the EC box was on UTC+1, the camera
stamps UTC); duration agreement got 0.104 s over 259.3 s and needs no clock
agreement at all. The absolute route stays as a fallback and the implied UTC
offset is reported rather than hidden.

Opening a `.mrc` now also fixes two RosettaSciIO calibration bugs from the
info file:

* the time axis is derived `1/(fps*sum_count)` where a summed movie's saved
  period is `sum_count/fps` -- wrong by `sum_count**2`, so a routine 2-frame
  sum lands 4x too fast (122.1 -> 30.525 fps here). The timestamps are ground
  truth and the recorded fps is corrected to match, rather than flipping
  metadata_extract's deliberate key-beats-axis precedence.
* imaging-vs-diffraction is decided with `camera_length != -1`, but an
  imaging exposure records 0 -- so a TEM image was calibrated as diffraction
  at the unset -1 nm^-1 with kx/ky axes, while `Specimen Pixel Size X =
  1.14786 nm` sat in the same file.

Auto-discovery stays silent unless a record's duration actually matches the
movie; File > Load In-Situ Data... loads one from elsewhere.

Our own .mpr parser rather than galvani (GPLv3+, so licence-compatible): it
mis-reads extended column IDs -- its v0 path takes every other byte, so ID
438 `step time/s` reads as 182 -- and failed on 1 of the 6 real files.
Validated against an ASCII export of the same run to 1e-8.
Dragging a lane's time cursor made every lane jump to and fro for the whole
drag. `_busy` is cleared synchronously when the drag handler returns, but the
selector's index hook runs on the `_NavDispatcher` thread and its widget
writes are marshalled onto the main thread -- so they land AFTER the guard is
false and write the last committed (index-quantised) position onto the line
still under the pointer. The pointer has moved on, the next `pointer_move`
drags it forward again, and it oscillates.

Whichever line is being dragged is now the MASTER for the length of the drag
and `_set_all_lines` skips it; `pointer_up` hands the role back so the
committed position can then settle every lane. Two guards because there are
two races: `_busy` for the synchronous `set -> pointer_move` echo, `_master`
for the asynchronous write-back.

Verified with a real drag in the app: samples 86.5, 108.5, 130.5, 152.5,
174.5, 196.5 -- monotonic at exactly the 22 px/step the mouse moved.
BURN-IN. There were three ways to put text on a frame: free text in
`annotations` (own button, lane, inspector), the timestamp as a bool param
drawn at a hardcoded (6,4), and instrument channels in `text_overlays`. They
are one object now -- a `text_overlays` entry differing only in where its
string comes from (`builtin:"label"`, `builtin:"time"`, `insitu_channel`).
One action `movie_add_burnin(source)`, one backend-owned source list, one
Burn-in timeline lane, one inspector, one rail group. Legacy specs migrate in
place ONCE (`burnin_migrated`), after which the overlay list is the only
truth -- re-deriving it on every read left `params["timestamp"]`
authoritative, so deleting the timestamp clip was undone by the next read.

Four bugs this exposed or fixed:
* `sync_overlay_widgets` cleared the text-overlay DICT but never removed the
  label widgets from the plot, so every resync stacked another copy -- one
  add read as two.
* the labels had NO drag handler (only annotations did), so moving one in the
  editor never reached the spec and the export drew it at the original xy.
  This was the editor and the movie disagreeing about where things sit.
* `_bake_poster` imported `_overlay_value_at` from `pipeline`, where it has
  never existed. Latent: it only ran when a movie had text overlays, which
  was uncommon until the timestamp became one -- then every poster silently
  failed to bake.
* burnt-in text used an absolute px size while the timestamp scaled with the
  frame, so an 18 pt overlay was a speck on a 4k movie; and `.2f` rendered a
  uA current as "0.00".

RESAMPLING. The three reducers now differ on purpose: spatial `downsample`
box-means (already did); fps INTEGRATES the source frames each output frame
stands for (30.5 -> 12 fps averages ~3, real SNR on noisy data); speed
segments SUB-SELECT -- a 32x jump advances ~81 frames but keeps the fps-sized
window, since integrating 81 would smear a second of change into one picture.
Slow motion narrows it, or consecutive output frames double-count.
`read_frame_integrated` accumulates one frame at a time, so the memory-safety
contract holds for any window.

ENCODER. Quality 7 -> 9 and a 24-frame GOP cap. A fast-forward's large
inter-frame deltas starved the high-frequency detail, so the previous frame's
timestamp ghosted through -- text tearing while the image looked fine.

PLAYBACK. The speed cycle reaches x32 (a 7914-frame acquisition is still 32 s
at x8) and the badge shows whenever a speed above 1x is selected, not only
while the clock runs.
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