Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f478d7a
feat(multi-plane): params + pure plane-offset/validation helpers
hongquanli Jul 4, 2026
efe75a1
feat(multi-plane): extra _squid attrs passthrough on ZarrAcquisitionC…
hongquanli Jul 4, 2026
18a75fa
feat(multi-plane): per-plane recording loop, paths, and metadata in R…
hongquanli Jul 4, 2026
ebd4880
feat(multi-plane): Bottom Z / Nz / dz recording fields with dz hidden…
hongquanli Jul 4, 2026
bec545d
fix(multi-plane): move plane-0 Z move before illumination; restore ca…
hongquanli Jul 4, 2026
a2d37fa
refactor(multi-plane): compact recording UI — Nz leads FPS/Dur row, d…
hongquanli Jul 4, 2026
d29cb3e
refactor(multi-plane): single-row recording controls — Nz, Z offset, …
hongquanli Jul 4, 2026
7762b1b
refactor(multi-plane): integer FPS/Dur spinboxes, drop fps suffix
hongquanli Jul 4, 2026
26925c1
feat(multi-plane): show recording Z offset only when Laser AF is enab…
hongquanli Jul 4, 2026
28c9c1d
merge: bring in feat/record-zstack-acquisition (#564) settings-reuse …
hongquanli Jul 5, 2026
0fea746
test(record-zstack): cover Nz/dz/bottom-Z fields in the settings roun…
hongquanli Jul 5, 2026
c31e223
feat(multi-plane): confirm recording plane summary before starting ac…
hongquanli Jul 5, 2026
2da4a8a
refactor(multi-plane): FPS/Duration lead the recording row; Bottom-Z …
hongquanli Jul 5, 2026
177f71a
refactor(multi-plane): align Bottom-Z offset row under Nz via QGridLa…
hongquanli Jul 5, 2026
2a70e89
Merge remote-tracking branch 'origin/feat/record-zstack-acquisition' …
hongquanli Jul 6, 2026
621aae7
refactor(multi-plane): dynamic recording-row reflow with aligned columns
hongquanli Jul 6, 2026
a24e519
Merge remote-tracking branch 'origin/feat/record-zstack-acquisition' …
hongquanli Jul 6, 2026
0becc4e
fix(record-zstack): probe frame shape in CONTINUOUS mode so software-…
Alpaca233 Jul 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions software/control/acquisition_yaml_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ class RecordZStackYAMLData:
recording_channel: Optional[Dict] = None
fps: float = 10.0
duration_s: float = 1.0
recording_z_offset_um: float = 0.0
recording_bottom_z_offset_um: float = 0.0
recording_nz: int = 1
recording_dz_um: float = 1.0

zstack_enabled: bool = False
zstack_channels: List[Dict] = field(default_factory=list)
Expand Down Expand Up @@ -110,7 +112,9 @@ def _parse_record_zstack_yaml_data(data: dict, acq: dict) -> RecordZStackYAMLDat
recording_channel=recording.get("channel"),
fps=recording.get("fps", 10.0),
duration_s=recording.get("duration_s", 1.0),
recording_z_offset_um=recording.get("z_offset_um", 0.0),
recording_bottom_z_offset_um=recording.get("bottom_z_offset_um", 0.0),
recording_nz=recording.get("nz", 1),
recording_dz_um=recording.get("dz_um", 1.0),
zstack_enabled=z_stack.get("enabled", False),
zstack_channels=z_stack.get("channels", []),
z_min_um=z_stack.get("z_min_um", -3.0),
Expand Down
22 changes: 20 additions & 2 deletions software/control/core/record_zstack_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ def zstack_offsets_um(z_min_um: float, z_max_um: float, step_um: float) -> List[
return [round(z_min_um + i * step_um, 6) for i in range(zstack_plane_count(z_min_um, z_max_um, step_um))]


def recording_plane_offsets_um(bottom_um: float, nz: int, dz_um: float) -> List[float]:
"""Offsets (µm, relative to the z reference) of the Nz recording planes.

Plane j sits at bottom_um + j*dz_um. Nz=1 is the single-plane case and
ignores dz_um (the widget hides dz entirely for Nz=1).
"""
if nz < 1:
raise ValueError("require Nz >= 1")
if nz > 1 and dz_um <= 0:
raise ValueError("require dz > 0 when Nz > 1")
return [round(bottom_um + j * dz_um, 6) for j in range(nz)]


def _build_objective_info(objective_store, camera) -> dict:
"""Build the informational `objective:` YAML section.

Expand Down Expand Up @@ -92,7 +105,9 @@ def _save_record_zstack_yaml(
"channel": _serialize_for_yaml(params.recording_channel) if params.recording_channel else None,
"fps": params.fps,
"duration_s": params.duration_s,
"z_offset_um": params.recording_z_offset_um,
"bottom_z_offset_um": params.recording_bottom_z_offset_um,
"nz": params.recording_Nz,
"dz_um": params.recording_dz_um,
},
"z_stack": {
"enabled": params.zstack_enabled,
Expand Down Expand Up @@ -139,7 +154,10 @@ class RecordZStackAcquisitionParameters:
recording_channel: Optional[AcquisitionChannel] = None
fps: float = 10.0
duration_s: float = 1.0
recording_z_offset_um: float = 0.0
# Recording planes: plane j at z_ref + recording_bottom_z_offset_um + j*recording_dz_um.
recording_bottom_z_offset_um: float = 0.0
recording_Nz: int = 1
recording_dz_um: float = 1.0
# z-stack phase
zstack_enabled: bool = False
zstack_channels: List[AcquisitionChannel] = field(default_factory=list)
Expand Down
235 changes: 152 additions & 83 deletions software/control/core/record_zstack_worker.py

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions software/control/core/zarr_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ class ZarrAcquisitionConfig:
compression: ZarrCompression = ZarrCompression.FAST
is_hcs: bool = True # Default to HCS (5D); non-HCS uses 6D with FOV dimension
plate_name: str = "plate"
# Optional extra keys merged into the "_squid" attributes at metadata-write
# time (e.g. per-plane recording metadata: plane_index, plane_z_offset_um).
extra_squid_attrs: Optional[Dict[str, object]] = None

@property
def ndim(self) -> int:
Expand Down Expand Up @@ -621,6 +624,9 @@ def _write_zarr_metadata(self) -> None:
},
}

if config.extra_squid_attrs:
zattrs["_squid"].update(config.extra_squid_attrs)

# Write metadata to zarr.json attributes (strict Zarr v3 compliance)
# For HCS, output_path is the array path ({fov}/0), but OME-NGFF metadata
# should be at the parent group level ({fov}/zarr.json), not the array level.
Expand Down
251 changes: 221 additions & 30 deletions software/control/widgets.py

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion software/squid/camera/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,20 @@ def get_is_streaming(self):
return self._streaming_thread and self._streaming_thread.is_alive()

@debug_log
def read_camera_frame(self) -> CameraFrame:
def read_camera_frame(self) -> Optional[CameraFrame]:
# Match the real camera / AbstractCamera contract: wait for a frame rather
# than returning whatever _current_frame happens to be right now. In
# CONTINUOUS mode _current_frame is None until the streaming thread produces
# the first frame, so an immediate return would hand callers None (e.g. the
# recording frame-shape probe, which then can't size its dataset). The
# software-trigger path is unaffected: send_trigger() sets _current_frame
# synchronously before the read, so this returns immediately. Bounded by a
# timeout so a stopped/idle stream returns (the last frame or None) instead
# of blocking forever.
timeout_s = (self._exposure_time_ms / 1000.0) * 1.02 + 4
deadline = time.time() + timeout_s
while self._current_frame is None and time.time() < deadline:
time.sleep(0.001)
return self._current_frame

@debug_log
Expand Down
8 changes: 6 additions & 2 deletions software/tests/control/test_acquisition_yaml_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,9 @@ def test_parse_record_zstack_yaml_select_wells(self, tmp_path):
name: BF LED matrix full
fps: 15.0
duration_s: 2.0
z_offset_um: 1.5
bottom_z_offset_um: 1.5
nz: 3
dz_um: 0.8
z_stack:
enabled: true
channels:
Expand Down Expand Up @@ -326,7 +328,9 @@ def test_parse_record_zstack_yaml_select_wells(self, tmp_path):
assert result.recording_channel == {"name": "BF LED matrix full"}
assert result.fps == 15.0
assert result.duration_s == 2.0
assert result.recording_z_offset_um == 1.5
assert result.recording_bottom_z_offset_um == 1.5
assert result.recording_nz == 3
assert result.recording_dz_um == 0.8
assert result.zstack_enabled is True
assert result.zstack_channels == [{"name": "Fluorescence 488 nm Ex"}]
assert result.z_min_um == -2.0
Expand Down
Loading
Loading