Skip to content

feat: Hamamatsu recording frame-rate cap (INTERNALFRAMERATE + fast readout)#583

Draft
hongquanli wants to merge 3 commits into
feat/record-zstack-acquisitionfrom
feat/hamamatsu-recording-fps
Draft

feat: Hamamatsu recording frame-rate cap (INTERNALFRAMERATE + fast readout)#583
hongquanli wants to merge 3 commits into
feat/record-zstack-acquisitionfrom
feat/hamamatsu-recording-fps

Conversation

@hongquanli

Copy link
Copy Markdown
Contributor

Summary

Adds HamamatsuCamera.set_frame_rate() so the Record + Z-Stack feature (#564) caps the ORCA's internal free-run rate on Hamamatsu instead of relying only on software downsampling.

Before this, HamamatsuCamera inherited the AbstractCamera.set_frame_rate no-op: recording ran the sensor in CONTINUOUS (internal-trigger) mode at its exposure/readout-limited maximum and the RecordingRouter downsampled to the target fps in software. That is correct (excess frames are cleanly rejected, not counted as drops), but at high fps / full resolution the read thread must process ~10.6 MB/frame it then discards — risking read-thread saturation and, in the worst case, under-delivery (blank trailing planes).

What it does

  • Overrides set_frame_rate(fps) to set DCAM INTERNALFRAMERATE, matching the ToupTek PRECISE_FRAMERATE contract: clamp the request to the property's valid range, set it, return the achieved rate; on any failure fall back to 1000/get_total_frame_time() (== prior behavior), so recording stays correct even where the properties are unsupported.
  • Forces fast readout first (DCAMPROP.READOUTSPEED.FASTEST) — high frame rates require it, and the sensor's default may be the low-noise (slow) mode whose INTERNALFRAMERATE ceiling is far below the target. Setting it first also raises the valuemax we then read, so the clamp isn't pinned to the slow-mode limit.

Stacking

Stacked on feat/record-zstack-acquisition (#564), not master, because the AbstractCamera.set_frame_rate base method this overrides only exists on #564's branch. Rebase onto master once #564 merges (same as #579).

Verification status

  • py_compile + black clean.
  • ⚠️ Not runnable on the dev machinedcamapi4.py dlopen's libdcamapi.so at import, absent on macOS, and simulation uses a different camera class. This code has never executed.
  • Hardware verification pending on ORCA-Fusion BT (C15440-20UP) at >30 fps. Draft until that passes. Smoke-test gate:
    • log line set_frame_rate(...) set INTERNALFRAMERATE -> X fps (X ≥ target = good)
    • output Zarr .zattrs: acquisition_complete: true, captured_frames == expected_frames, dropped_frames: 0
    • frames correctly exposed (fps cap must not perturb exposure/brightness)

Known follow-up (out of scope here)

Readout speed is left in fast mode after a recording (not restored). Acceptable for throughput-oriented recording; a follow-up should make readout speed configurable / restore it for low-noise low-fps recording.

🤖 Generated with Claude Code

hongquanli and others added 3 commits July 5, 2026 19:43
…RNALFRAMERATE

Add HamamatsuCamera.set_frame_rate() (was inheriting the AbstractCamera no-op)
so Record+Z-Stack recording caps the ORCA's internal free-run rate instead of
free-running at the exposure/readout-limited max and downsampling in software.

- Forces fast readout (DCAMPROP.READOUTSPEED.FASTEST) before reading the
  INTERNALFRAMERATE ceiling, so high (>30 fps) targets aren't pinned to the
  slow/low-noise mode's limit.
- Clamps the request to the property's valid range, sets it, and returns the
  achieved rate (base contract). Any failure falls back to the exposure-limited
  max (== prior behavior), so recording stays correct even if the props are
  unsupported.

Untested on hardware locally (no DCAM lib on the dev Mac); verified py_compile +
black only. Hardware verification pending on ORCA-Fusion BT (C15440-20UP).

Known follow-up: readout speed is left in fast mode after recording (not
restored); make it configurable / restore for low-noise low-fps recording.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ew nits

Address review of the Hamamatsu set_frame_rate override:
- Guard the exposure-limited-max fallback: get_strobe_time() can raise on a DCAM
  read error, which previously escaped the method despite the "returns a usable
  rate on any failure" docstring. Now falls back to the requested rate.
- Drop the unreachable `attr is None` branch (prop_getattr returns attr or False).
- Fix the misleading "camera default" log (readout speed is already set by then).
- Drop redundant float() on an already-float prop_getvalue result.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hongquanli

hongquanli commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Bench finding: set_frame_rate fails on ORCA-Fusion BT with DCAMERR.NOTWRITABLE

Observed (ORCA-Fusion BT, 10 fps recording):

squid.HamamatsuCamera - ERROR - Failed to set property 4208656=10.0: -2147481565 (camera_hamamatsu.py:113)
  • 4208656 = DCAM_IDPROP.INTERNALFRAMERATE, value 10.0 fps.
  • -2147481565 = 0x80000823 = DCAMERR.NOTWRITABLE — locked read-only in the current state (not a bad value; that would be OUTOFRANGE 0x80000822, and the prop_getattr range read succeeded).

Fail-safe worked: set_frame_rate returned its fallback and the recording proceeded via software downsampling. Data is correct; only the hardware fps cap was inactive.

Hypothesis 1 — ACCESSREADY / status-ordering — ❌ TESTED & REFUTED on hardware

Idea: INTERNALFRAMERATE is ACCESSREADY-gated (DCAM attr 0x2000 = "get/set only at READY status"), writable only after buf_alloc (READY), and we set it too early (STABLE, before start_streaming).

Result (2026-07-05, on the ORCA): prop_getattr(INTERNALFRAMERATE).is_writable() is False in BOTH STABLE and READY. So this is not a status/ordering gate — the property is inherently read-only in this configuration. Ordering fix is off the table.

Hypothesis 2 — the writable knob is the sibling INTERNAL_FRAMEINTERVAL — under test

On many Hamamatsu models the frame rate is set via INTERNAL_FRAMEINTERVAL (frame period, seconds), and INTERNALFRAMERATE (fps) is a derived/read-back value. Next probe (dumps the raw attr flags of both, and tries setting the interval to 0.1 s = 10 fps):

from control.dcam import Dcam, Dcamapi
from control.dcamapi4 import DCAM_IDPROP, DCAMPROP

Dcamapi.init(); cam = Dcam(0); cam.dev_open(0)
cam.prop_setvalue(DCAM_IDPROP.TRIGGERSOURCE, DCAMPROP.TRIGGERSOURCE.INTERNAL)
print("triggersource:", cam.prop_getvalue(DCAM_IDPROP.TRIGGERSOURCE),
      "(INTERNAL =", int(DCAMPROP.TRIGGERSOURCE.INTERNAL), ")")

for name, pid in [("INTERNALFRAMERATE", DCAM_IDPROP.INTERNALFRAMERATE),
                  ("INTERNAL_FRAMEINTERVAL", DCAM_IDPROP.INTERNAL_FRAMEINTERVAL),
                  ("EXPOSURETIME", DCAM_IDPROP.EXPOSURETIME)]:
    a = cam.prop_getattr(pid)
    print(f"{name}: attr={hex(a.attribute)} writable={a.is_writable()} "
          f"min={a.valuemin} max={a.valuemax} cur={cam.prop_getvalue(pid)}")

ok = cam.prop_setvalue(DCAM_IDPROP.INTERNAL_FRAMEINTERVAL, 0.1)   # 0.1 s = 10 fps
print("set INTERNAL_FRAMEINTERVAL=0.1:", ok, "lasterr:", cam.lasterr())
print("resulting INTERNALFRAMERATE:", cam.prop_getvalue(DCAM_IDPROP.INTERNALFRAMERATE))
cam.dev_close()

Fix decision tree

  • INTERNAL_FRAMEINTERVAL writable + set works → one-line swap: cap via INTERNAL_FRAMEINTERVAL = 1/fps instead of INTERNALFRAMERATE.
  • Both read-only → the free-run rate can't be lowered via these on this model/config; keep software downsampling and downgrade this handled-and-recovered failure from ERRORWARNING. The raw attr bitmask makes this unambiguous (absence of the WRITABLE 0x20000 bit = never settable).

Also tracked for this method: restore READOUTSPEED after recording (currently left at FASTEST).

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