You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
SDK side of OpenwaterHealth/openmotion-bloodflow-app#361 (SEEDLESS Frames, formerly NO_TA). Experimental engineering-test feature requested by @bahartl: run the first N frames of a scan with the seed off, the TA pulsing 2 ms (deliberately inducing TA self-lasing), and camera exposure at 2295 µs, then revert to normal scan parameters mid-scan.
Everything is host-side register writes — no firmware or FPGA changes (the TA FPGA is an edge-triggered one-shot, so TA_PULSE_WIDTH is writable mid-scan without a trigger restart).
Scope:
ScanRequest.seedless_frames: int = 0 (0 = off)
omotion/seedless.py — SeedlessController: register snapshot via I2C read-back, pre-trigger apply (seed gains → 0, TA → raw 6250, EE/OPT_PULSE_WIDTH_UL widened to raw 7813, exposure 0xFF), frame-ID-driven restore at frame N with strict ordering (TA width → 50 ms wait → safety limits → seed gains → exposure; reverse order latches TA_shutdown), idempotent force-restore on all teardown/cancel paths
Trigger config: LaserPulseSkipDelayUsec → 2500 when seedless, so scheduled dark frames stay outside the 2295 µs exposure window
FrameClassificationStage: tag frames 1..N seedless, N+1..N+80 seedless_tx (guard band for exposure smear); dtype <U8 → <U12; excluded from BFI/BVI + dark correction like warmup; N recorded in scan metadata
Unit tests (no hardware): restore ordering, latch idempotency, snapshot-fallback, classification tags
Bench verification per spec §Testing
Deliberate safety note: the 2 ms TA pulse exceeds the 1.000 ms safety-FPGA pulse-width upper limit; widening the UL for the seedless window was explicitly authorized by Ethan for this engineering test. The controller restores baseline limits on every exit path, and a console power-cycle also clears them (volatile registers).
Branch rebased onto latest next (clean; suite grew to 573 passing) and the seedless experiment re-run with the camera crop (1720×1280, right 200 columns dropped) per Ethan's request. All four regime × crop combinations captured on sensor-fw 1.8.2-dev.1, N=25, mask 0x66.
Crop effect on the contrast numbers (frames 11..25 seedless vs light, pedestal-subtracted)
scan (left / right)
seedless K
light K
separation (light K / seedless K)
boosted, full frame
0.095 / 0.089
0.399 / 0.406
4.19 / 4.57
boosted, cropped
0.086 / 0.081
0.382 / 0.389
4.44 / 4.82
stock, full frame
0.120 / 0.128
0.393 / 0.400
3.27 / 3.13
stock, cropped
0.111 / 0.121
0.389 / 0.396
3.49 / 3.28
Cropping lowers measured K in every regime — seeded frames by ~4%, unseeded by ~10% — while intensity rises slightly (e.g. light 243→248): the dropped right-edge columns are dimmer (vignetted), and that intensity nonuniformity was inflating σ/µ beyond true speckle contrast. Unseeded frames benefit more because their K is small enough for the nonuniformity term to matter proportionally more.
Intensity congruence is unaffected (boosted/light = 1.037 L / 1.029 R).
Crop verified real: every frame's histogram totals 2,201,606 (1720×1280) vs 2,457,606 full-frame.
Two structural defects this session exposed and fixed (both bench-only discoveries)
Mid-scan rate-LL re-arm was deterministically latching the safety FPGA. The seedless trigger override (LaserPulseSkipDelayUsec=2500) makes each dark frame's return interval 22.5 ms — below the 23.125 ms baseline RATE_LL — so the frame-N restore's re-arm guaranteed a rate_lower_limit_fail latch at the next dark frame of EVERY seedless scan (visible as the trailing safety_se=4 in all previous telemetry; normal scans clear this limit by only ~75 µs). The mid-scan restore now defers the rate-LL re-arm to the teardown restore, which runs after stop_trigger.
The 50 ms fault-clear pulse was marginal on the EE FPGA — it failed twice in a row, and the stale latch starved two scans to 0 frames (TA_shutdown held; fsync also stops ≈2 counts in, mechanism console-side, worth a firmware look). The clear now asserts 300 ms, verifies via STATUS (0x24) read-back, retries 3×, and apply() aborts pre-trigger if the fault won't clear.
Also found (SDK-side, pre-existing): firmware configure_camera_sensor short-circuits when isConfigured && isPowered, so debug-flag changes (crop/raw) do NOT take effect on already-up cameras — a camera power-cycle before configure is required. Worth documenting on the sensor-fw side.
Commits: 5e10167 (apply-lock, rebased), 8e0…/latestfix: verify+retry safety-fault clear; defer rate-LL re-arm to teardown, spec updates. Suite: 573 passed, 0 failed. All laser/safety registers verified restored after every run.
Per-camera, pipeline-corrected contrast (refines the pooled quick-look numbers above)
Cameras = mask 0x66 → positions 1, 2, 5, 6 per side (pipeline gains g = 4, 2, 2, 4). Corrections now mirror the real pipeline: pedestal 128 (pedestal_for_fw for FW > 1.5.2), dark correction with variance decomposition (dark.py), and shot-noise subtractionvar −= ADC_GAIN·mean·g_cam (shot_noise.py, ADC_GAIN = 0.0815 DN/e⁻). Dark baseline = each scan's 10 seed-off preamble frames — justified because the NRM scan's true terminal dark reads u1 = 128.0 (= pedestal exactly), i.e. seeded CW background ≈ 0 on this bench; 15 s scans contain no usable interior scheduled dark (interval 600), and the BST terminal stop frame caught light (stop-timing artifact — do not use as a dark).
Crop scans (1720×1280), frames 11..25 seedless vs light, K = σ_corrected/µ_dc:
scan / side / cam (g)
seedless µ_dc / K
light µ_dc / K
separation
BST L1(4)
256.6 / 0.0690
246.7 / 0.3261
4.7
BST L2(2)
239.6 / 0.0895
232.2 / 0.4327
4.8
BST L5(2)
278.3 / 0.0893
266.0 / 0.4343
4.9
BST L6(4)
251.2 / 0.0669
244.1 / 0.3280
4.9
BST R1(4)
174.1 / 0.0645
169.0 / 0.3300
5.1
BST R2(2)
186.9 / 0.0689
182.9 / 0.4439
6.4
BST R5(2)
189.4 / 0.0771
183.8 / 0.4420
5.7
BST R6(4)
175.5 / 0.0662
169.9 / 0.3313
5.0
(NRM regime table in the analysis script; seedless K per camera matches BST within ~0.01.)
Observations for the scientists:
Seeded light K splits cleanly by gain family: g=4 cams (1,6) ≈ 0.33; g=2 cams (2,5) ≈ 0.44 — a real optical/speckle-sampling difference the pooled 0.39 was averaging over.
Unseeded K is nearly camera-independent after shot subtraction (0.065–0.090 across all 8 cam-sides) — consistent with a genuine low-coherence contrast floor rather than optics-dependent speckle.
Shot-noise subtraction matters most exactly where it should: seedless frames on g=4 cams drop ~12% (0.079→0.069) vs <1% on bright seeded frames — K²_shot/K² is largest when K is small.
Corrected seedless K is the same in stock and boosted regimes (e.g. L1: 0.0707 vs 0.0690) — K is intensity-independent once shot noise is removed, which is the internal-consistency check on the transfer-function experiment: the boost buys SNR and mean-congruence, not contrast.
Fully-corrected separation: 4.7–6.8× per camera.
Not applied vs. the full pipeline: dark-interval interpolation (n/a in 15 s scans), calibration C_min/C_max and BFI/BVI conversion, timestamp repair (irrelevant to K).
Per Ethan: the purpose of the seedless frames is to determine C_min, and the light frames — on a static phantom — measure C_max. Full 8-camera coverage per module, boosted seedless regime, crop on, sensor-fw 1.8.2-dev.1, pipeline-corrected (pedestal 128, dark correction from seed-off preamble, shot-noise subtraction with the per-camera gain map). Per-frame K averaged as average-of-ratios (consistent with the C_max convention fix in 8532853).
Method note — 16-camera scans stall: full 0xFF/0xFF scans stalled the sensor histogram streams (left died at frame 51, right at 89; a rerun at frame 0; uniform across cameras = firmware TX stall, not a camera fault). This is the known parked backpressure limit (ticket #116 / PR #113). All-8 coverage was captured instead as two half-mask scans — 0x66 (cams 1,2,5,6) + 0x99 (cams 0,3,4,7) — same phantom, same session; 4 cams/side streams flawlessly (~3020 chunks, 604/604 frames, RESTORE ALL OK on every run).
side
cam
gain
C_min (seedless K)
C_max (light K)
C_max/C_min
left
0
16
0.0745
0.3271
4.4
left
1
4
0.0690
0.3261
4.7
left
2
2
0.0895
0.4327
4.8
left
3
1
0.0967
0.5189
5.4
left
4
1
0.1049
0.5233
5.0
left
5
2
0.0893
0.4343
4.9
left
6
4
0.0669
0.3280
4.9
left
7
16
0.0750
0.3325
4.4
right
0
16
0.0780
0.3362
4.3
right
1
4
0.0645
0.3300
5.1
right
2
2
0.0689
0.4439
6.4
right
3
1
0.1087
0.5171
4.8
right
4
1
0.0986
0.5240
5.3
right
5
2
0.0771
0.4420
5.7
right
6
4
0.0662
0.3313
5.0
right
7
16
0.0856
0.3452
4.0
Structure worth the scientists' attention:
C_max orders cleanly by gain family: g=1 (cams 3,4) ≈ 0.52 · g=2 (2,5) ≈ 0.43–0.44 · g=4 (1,6) ≈ 0.33 · g=16 (0,7) ≈ 0.33–0.35. The center (low-gain) cameras see the highest speckle contrast; the g=16 edge cams land at the g=4 level, not lower.
C_min is much flatter (0.065–0.109) but not featureless: the g=1 cams show the highest C_min (0.097–0.109) — their shot-noise term is smallest, so more residual variance survives the correction; whether the remainder is instrumental or a real coherence floor difference is a question for the team.
Separation is 4.0–6.4× on every camera — a usable C_min/C_max span across the whole array.
Seedless (C_min) intensity on the g=16 edge cams runs ~97–154 DN — dimmer but still well-resolved above the preamble dark baseline.
Data: science_out/ scans SEEDLESS_{NRM,BST}_CROP (mask 0x66) and SEEDLESS_{NRM,BST}_CROP_M99 (mask 0x99), with matching telemetry CSVs. Self-lase ≤ ~2 s per scan throughout (watchdog armed, never needed on the half-mask runs).
Activity
docs: SEEDLESS frames implementation plan (refs #146)
docs: SEEDLESS frames implementation plan (refs #146)
feat: tag seedless/seedless_tx frames in classification (refs #146)
docs+test: clarify seedless tags in classify docstring + stale-preced…
feat: SeedlessWatchStage fires restore callback at frame N (refs #146)
fix: add reset() to SeedlessWatchStage + align logger name (refs #146)
feat: plumb seedless_frames through pipeline factory (refs #146)
feat: SeedlessController -- apply/restore seedless register config (r…
docs+test: clarify seedless restore failsafe ordering + add safety-wi…
feat: record seedless_frames in scan metadata + DB sdk_flags (refs #146)
test: add seedless_frames to scan_db_sink_empty _meta mock (refs #146)
feat: seedless_frames ScanRequest field + scan workflow wiring (refs #…
fix: restore seedless window in outer finally (console-failure exit p…
docs: seedless_frames API entry + spec as-built notes (refs #146)
19 remaining items
feat: seedless_frames ScanRequest field + scan workflow wiring (refs #…
fix: restore seedless window in outer finally (console-failure exit p…
docs: seedless_frames API entry + spec as-built notes (refs #146)
fix: widen seedless_tx guard band 80->120 for 16-camera restore laten…
fix: relax EE/OPT rate lower limit for seedless window (bench finding)
fix: clear latched safety fault at seedless apply start (bench finding)
docs: record bench validation + rate-limit/fault-clear additions (refs …
fix: serialize apply() under the controller lock (bench race finding)
docs: bench science results + dark-preamble correction in spec (refs #…
fix: verify+retry safety-fault clear; defer rate-LL re-arm to teardow…
docs: spec bench notes -- verified fault clear + teardown-only rate r…
boringethan commentedon Jul 18, 2026
Bench session 2026-07-17 (evening): DEBUG_FLAG_CAMERA_CROP × seedless, rebase, two structural fixes
Branch rebased onto latest
next(clean; suite grew to 573 passing) and the seedless experiment re-run with the camera crop (1720×1280, right 200 columns dropped) per Ethan's request. All four regime × crop combinations captured on sensor-fw 1.8.2-dev.1, N=25, mask 0x66.Crop effect on the contrast numbers (frames 11..25 seedless vs
light, pedestal-subtracted)Two structural defects this session exposed and fixed (both bench-only discoveries)
LaserPulseSkipDelayUsec=2500) makes each dark frame's return interval 22.5 ms — below the 23.125 ms baselineRATE_LL— so the frame-N restore's re-arm guaranteed arate_lower_limit_faillatch at the next dark frame of EVERY seedless scan (visible as the trailingsafety_se=4in all previous telemetry; normal scans clear this limit by only ~75 µs). The mid-scan restore now defers the rate-LL re-arm to the teardown restore, which runs afterstop_trigger.apply()aborts pre-trigger if the fault won't clear.Also found (SDK-side, pre-existing): firmware
configure_camera_sensorshort-circuits whenisConfigured && isPowered, so debug-flag changes (crop/raw) do NOT take effect on already-up cameras — a camera power-cycle before configure is required. Worth documenting on the sensor-fw side.Commits:
5e10167(apply-lock, rebased),8e0…/latestfix: verify+retry safety-fault clear; defer rate-LL re-arm to teardown, spec updates. Suite: 573 passed, 0 failed. All laser/safety registers verified restored after every run.boringethan commentedon Jul 18, 2026
Per-camera, pipeline-corrected contrast (refines the pooled quick-look numbers above)
Cameras = mask 0x66 → positions 1, 2, 5, 6 per side (pipeline gains g = 4, 2, 2, 4). Corrections now mirror the real pipeline: pedestal 128 (
pedestal_for_fwfor FW > 1.5.2), dark correction with variance decomposition (dark.py), and shot-noise subtractionvar −= ADC_GAIN·mean·g_cam(shot_noise.py, ADC_GAIN = 0.0815 DN/e⁻). Dark baseline = each scan's 10 seed-off preamble frames — justified because the NRM scan's true terminal dark reads u1 = 128.0 (= pedestal exactly), i.e. seeded CW background ≈ 0 on this bench; 15 s scans contain no usable interior scheduled dark (interval 600), and the BST terminal stop frame caught light (stop-timing artifact — do not use as a dark).Crop scans (1720×1280), frames 11..25 seedless vs light, K = σ_corrected/µ_dc:
(NRM regime table in the analysis script; seedless K per camera matches BST within ~0.01.)
Observations for the scientists:
Not applied vs. the full pipeline: dark-interval interpolation (n/a in 15 s scans), calibration C_min/C_max and BFI/BVI conversion, timestamp repair (irrelevant to K).
boringethan commentedon Jul 18, 2026
All-8-camera C_min / C_max measurement (Ethan's framing: seedless → C_min, static-phantom light → C_max)
Per Ethan: the purpose of the seedless frames is to determine C_min, and the light frames — on a static phantom — measure C_max. Full 8-camera coverage per module, boosted seedless regime, crop on, sensor-fw 1.8.2-dev.1, pipeline-corrected (pedestal 128, dark correction from seed-off preamble, shot-noise subtraction with the per-camera gain map). Per-frame K averaged as average-of-ratios (consistent with the C_max convention fix in
8532853).Method note — 16-camera scans stall: full
0xFF/0xFFscans stalled the sensor histogram streams (left died at frame 51, right at 89; a rerun at frame 0; uniform across cameras = firmware TX stall, not a camera fault). This is the known parked backpressure limit (ticket #116 / PR #113). All-8 coverage was captured instead as two half-mask scans —0x66(cams 1,2,5,6) +0x99(cams 0,3,4,7) — same phantom, same session; 4 cams/side streams flawlessly (~3020 chunks, 604/604 frames, RESTORE ALL OK on every run).Structure worth the scientists' attention:
Data:
science_out/scansSEEDLESS_{NRM,BST}_CROP(mask 0x66) andSEEDLESS_{NRM,BST}_CROP_M99(mask 0x99), with matching telemetry CSVs. Self-lase ≤ ~2 s per scan throughout (watchdog armed, never needed on the half-mask runs).