In-situ electrochemistry alignment + movie burn-in/export overhaul - #121
Open
CSSFrancis wants to merge 3 commits into
Open
In-situ electrochemistry alignment + movie burn-in/export overhaul#121CSSFrancis wants to merge 3 commits into
CSSFrancis wants to merge 3 commits into
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on
feat/seg-fast-engine— it touches 7 files that branch alsochanges, so the diff here is just this work (~1,100 lines, 3 commits).
Retarget to
mainonce 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
.mrcauto-discovers a matching record in the same folder andregisters E and I as navigator chips;
File ▸ Load In-Situ Data…loads onefrom 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/(fps*sum_count)where a summedmovie's saved period is
sum_count/fps— wrong bysum_count**2. Here122.1 → 30.525 fps. The timestamps are ground truth.
camera_length != -1, but animaging exposure records 0 — so a real-space image got
nm^-1units,the unset
-1pixel size andkx/kyaxes, whileSpecimen Pixel Size X = 1.14786 nmsat in the same file.frames_per_secondis corrected to match, rather thanflipping
metadata_extract's deliberate key-beats-axis precedence.Own
.mprparser rather thangalvani(GPLv3+, so licence-compatible): itmis-reads extended column IDs — its v0 path takes every other byte, so ID 438
step time/sreads as 182 — and failed on 1 of the 6 real files. Validatedagainst 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 onetext_overlaysentrydiffering only in where its string comes from:
builtin:"label",builtin:"time", orinsitu_channel. One action, one source list, onetimeline lane, one inspector. Legacy specs migrate in place once.
Resampling now differs by control, on purpose: spatial
downsamplebox-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
_busyclearssynchronously but the index hook's write-back is marshalled from the
_NavDispatcherthread and lands after it. One master, rest follow.sync_overlay_widgetscleared the overlay dict but never removed the labelwidgets — every resync stacked another copy, so one add read as two.
reached the spec and the export drew it at the original position.
_bake_posterimported_overlay_value_atfrompipeline, where it hasnever existed — latent until the timestamp became an overlay, then every
poster silently failed to bake.
works in seconds, so the cursor only reached the first ~3%.
inter-frame deltas starved high-frequency detail, so the previous frame's
timestamp ghosted through as text tearing.
Verification
(
electron/tests/insitu_echem.spec.ts; skips when the dataset is absent).Includes a real cursor drag — samples monotonic at exactly the mouse step.
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.