-
Notifications
You must be signed in to change notification settings - Fork 2
fix: Zarr v3 push API improvements and 6D mode fixes #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
883ff23
2996192
d7a9934
9a346b0
efbc0a9
4312ba6
0bc265e
342f902
510d651
fad892a
61f0feb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -927,6 +927,8 @@ def open_zarr_tensorstore(path: Path, array_path: str = "0") -> Optional[Any]: | |||||||||||||||||||||||||||||||
| spec = { | ||||||||||||||||||||||||||||||||
| "driver": driver, | ||||||||||||||||||||||||||||||||
| "kvstore": {"driver": "file", "path": str(full_path)}, | ||||||||||||||||||||||||||||||||
| # Revalidate metadata on each read (default "open" only checks at open time) | ||||||||||||||||||||||||||||||||
| "recheck_cached_metadata": True, | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||
|
|
@@ -1415,8 +1417,6 @@ class LightweightViewer(QWidget): | |||||||||||||||||||||||||||||||
| _refresh_timer: Optional[QTimer] | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Zarr push-based API state | ||||||||||||||||||||||||||||||||
| _zarr_acquisition_path: Optional[Path] | ||||||||||||||||||||||||||||||||
| _zarr_acquisition_store: Optional[Any] # zarr.Array | ||||||||||||||||||||||||||||||||
| _zarr_acquisition_active: bool | ||||||||||||||||||||||||||||||||
| _zarr_channel_map: Dict[str, int] | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -1458,8 +1458,6 @@ def __init__(self, dataset_path: str = ""): | |||||||||||||||||||||||||||||||
| self._load_pending: bool = False # True if load is scheduled | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Zarr push-based API state | ||||||||||||||||||||||||||||||||
| self._zarr_acquisition_path: Optional[Path] = None | ||||||||||||||||||||||||||||||||
| self._zarr_acquisition_store = None # zarr.Array handle | ||||||||||||||||||||||||||||||||
| self._zarr_acquisition_active: bool = False | ||||||||||||||||||||||||||||||||
| self._zarr_channel_map: Dict[str, int] = {} | ||||||||||||||||||||||||||||||||
| self._zarr_debounce_timer: Optional[QTimer] = None # Debounce for zarr loads | ||||||||||||||||||||||||||||||||
|
|
@@ -2122,29 +2120,33 @@ def end_acquisition(self): | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def start_zarr_acquisition( | ||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||
| zarr_path: str, | ||||||||||||||||||||||||||||||||
| fov_paths: List[str], | ||||||||||||||||||||||||||||||||
| channels: List[str], | ||||||||||||||||||||||||||||||||
| num_z: int, | ||||||||||||||||||||||||||||||||
| fov_labels: List[str], | ||||||||||||||||||||||||||||||||
| height: int, | ||||||||||||||||||||||||||||||||
| width: int, | ||||||||||||||||||||||||||||||||
| fov_paths: Optional[List[str]] = None, | ||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||
| """Configure viewer for zarr-based live acquisition. | ||||||||||||||||||||||||||||||||
| """Configure viewer for per-FOV zarr-based live acquisition. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Call this at acquisition start before any notify_zarr_frame() calls. | ||||||||||||||||||||||||||||||||
| Sets up the viewer with channel configuration and opens the zarr store. | ||||||||||||||||||||||||||||||||
| Sets up the viewer with channel configuration. Each FOV has its own | ||||||||||||||||||||||||||||||||
| 5D zarr store with shape (T, C, Z, Y, X). | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| For 6D zarr structures (FOV, T, C, Z, Y, X), use start_zarr_acquisition_6d() instead. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||
| zarr_path: Path to the zarr store being written to (for single/6d structures) | ||||||||||||||||||||||||||||||||
| fov_paths: List of zarr paths, one per FOV (e.g., ["fov_0.zarr", "fov_1.zarr"]) | ||||||||||||||||||||||||||||||||
| channels: Channel names, e.g. ["DAPI", "GFP", "RFP"] | ||||||||||||||||||||||||||||||||
| num_z: Number of z-levels | ||||||||||||||||||||||||||||||||
| fov_labels: FOV labels, e.g. ["A1:0", "A1:1", "A2:0"] | ||||||||||||||||||||||||||||||||
| height: Image height in pixels | ||||||||||||||||||||||||||||||||
| width: Image width in pixels | ||||||||||||||||||||||||||||||||
| fov_paths: Optional list of zarr paths, one per FOV (for per_fov/hcs structures). | ||||||||||||||||||||||||||||||||
| If provided, zarr_path is ignored and each FOV loads from its own store. | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| # Validate inputs | ||||||||||||||||||||||||||||||||
| if not fov_paths: | ||||||||||||||||||||||||||||||||
| raise ValueError("fov_paths must not be empty") | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Stop any running animations and pending loads | ||||||||||||||||||||||||||||||||
| self._stop_play_animation(self._time_play_timer, self._time_play_btn) | ||||||||||||||||||||||||||||||||
| self._stop_play_animation(self._fov_play_timer, self._fov_play_btn) | ||||||||||||||||||||||||||||||||
|
|
@@ -2154,7 +2156,6 @@ def start_zarr_acquisition( | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Close any existing zarr stores | ||||||||||||||||||||||||||||||||
| with self._zarr_stores_lock: | ||||||||||||||||||||||||||||||||
| self._zarr_acquisition_store = None | ||||||||||||||||||||||||||||||||
| self._zarr_fov_stores.clear() | ||||||||||||||||||||||||||||||||
| # Also clear 6D regions state (in case switching modes) | ||||||||||||||||||||||||||||||||
| self._zarr_region_stores.clear() | ||||||||||||||||||||||||||||||||
|
|
@@ -2176,22 +2177,17 @@ def start_zarr_acquisition( | |||||||||||||||||||||||||||||||
| self._image_width = width | ||||||||||||||||||||||||||||||||
| self._fov_labels = list(fov_labels) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Handle per-FOV paths (for per_fov and hcs structures) | ||||||||||||||||||||||||||||||||
| if fov_paths: | ||||||||||||||||||||||||||||||||
| if len(fov_paths) != len(fov_labels): | ||||||||||||||||||||||||||||||||
| logger.warning( | ||||||||||||||||||||||||||||||||
| f"fov_paths length ({len(fov_paths)}) does not match " | ||||||||||||||||||||||||||||||||
| f"fov_labels length ({len(fov_labels)}), truncating to shorter" | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| min_len = min(len(fov_paths), len(fov_labels)) | ||||||||||||||||||||||||||||||||
| fov_paths = fov_paths[:min_len] | ||||||||||||||||||||||||||||||||
| fov_labels = list(fov_labels)[:min_len] | ||||||||||||||||||||||||||||||||
| self._fov_labels = fov_labels | ||||||||||||||||||||||||||||||||
| self._zarr_fov_paths = [Path(p) for p in fov_paths] | ||||||||||||||||||||||||||||||||
| self._zarr_acquisition_path = None # Not used in per-FOV mode | ||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||
| self._zarr_fov_paths = [] | ||||||||||||||||||||||||||||||||
| self._zarr_acquisition_path = Path(zarr_path) | ||||||||||||||||||||||||||||||||
| # Validate and set per-FOV paths | ||||||||||||||||||||||||||||||||
| if len(fov_paths) != len(fov_labels): | ||||||||||||||||||||||||||||||||
| logger.warning( | ||||||||||||||||||||||||||||||||
| f"fov_paths length ({len(fov_paths)}) does not match " | ||||||||||||||||||||||||||||||||
| f"fov_labels length ({len(fov_labels)}), truncating to shorter" | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| min_len = min(len(fov_paths), len(fov_labels)) | ||||||||||||||||||||||||||||||||
| fov_paths = list(fov_paths)[:min_len] | ||||||||||||||||||||||||||||||||
| fov_labels = list(fov_labels)[:min_len] | ||||||||||||||||||||||||||||||||
| self._fov_labels = fov_labels | ||||||||||||||||||||||||||||||||
| self._zarr_fov_paths = [Path(p) for p in fov_paths] | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Build channel name to index map | ||||||||||||||||||||||||||||||||
| self._zarr_channel_map = {name: i for i, name in enumerate(self._channel_names)} | ||||||||||||||||||||||||||||||||
|
|
@@ -2235,8 +2231,8 @@ def start_zarr_acquisition( | |||||||||||||||||||||||||||||||
| self._update_fov_slider_visibility() | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| logger.info( | ||||||||||||||||||||||||||||||||
| f"NDViewer: Started zarr acquisition with {len(channels)} channels, " | ||||||||||||||||||||||||||||||||
| f"{num_z} z-levels, {len(fov_labels)} FOVs at {zarr_path}" | ||||||||||||||||||||||||||||||||
| f"NDViewer: Started per-FOV zarr acquisition with {len(channels)} channels, " | ||||||||||||||||||||||||||||||||
| f"{num_z} z-levels, {len(self._zarr_fov_paths)} FOVs" | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def start_zarr_acquisition_6d( | ||||||||||||||||||||||||||||||||
|
|
@@ -2272,7 +2268,6 @@ def start_zarr_acquisition_6d( | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Close any existing zarr stores | ||||||||||||||||||||||||||||||||
| with self._zarr_stores_lock: | ||||||||||||||||||||||||||||||||
| self._zarr_acquisition_store = None | ||||||||||||||||||||||||||||||||
| self._zarr_fov_stores.clear() | ||||||||||||||||||||||||||||||||
| self._zarr_region_stores.clear() | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -2335,8 +2330,7 @@ def start_zarr_acquisition_6d( | |||||||||||||||||||||||||||||||
| for i, c in enumerate(self._channel_names) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Clear single-store state (not used in 6d_regions mode) | ||||||||||||||||||||||||||||||||
| self._zarr_acquisition_path = None | ||||||||||||||||||||||||||||||||
| # Clear per-FOV state (not used in 6d_regions mode) | ||||||||||||||||||||||||||||||||
| self._zarr_fov_paths = [] | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Enable 6d_regions mode | ||||||||||||||||||||||||||||||||
|
|
@@ -2512,15 +2506,19 @@ def _load_zarr_plane( | |||||||||||||||||||||||||||||||
| """Load a single plane from the zarr store using tensorstore. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Uses tensorstore to support both zarr v2 and v3 formats. | ||||||||||||||||||||||||||||||||
| Supports two modes: | ||||||||||||||||||||||||||||||||
| - 6D regions mode: each region has a 6D array (FOV, T, C, Z, Y, X) | ||||||||||||||||||||||||||||||||
| - Per-FOV mode: each FOV has a 5D array (T, C, Z, Y, X) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||
| t: Timepoint index | ||||||||||||||||||||||||||||||||
| fov_idx: FOV index | ||||||||||||||||||||||||||||||||
| fov_idx: FOV index (global index, converted to region/local for 6D mode) | ||||||||||||||||||||||||||||||||
| z: Z-level index | ||||||||||||||||||||||||||||||||
| channel_idx: Channel index | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||
| Image plane as numpy array, or zeros if not available. | ||||||||||||||||||||||||||||||||
| Image plane as numpy array, or zeros if data not available or | ||||||||||||||||||||||||||||||||
| no zarr mode is configured. | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| cache_key = ("zarr", t, fov_idx, z, channel_idx) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -2558,7 +2556,8 @@ def _load_zarr_plane( | |||||||||||||||||||||||||||||||
| logger.debug( | ||||||||||||||||||||||||||||||||
| f"Opening zarr store for region {region_idx}: {region_path}" | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| ts_arr = open_zarr_tensorstore(region_path, array_path="0") | ||||||||||||||||||||||||||||||||
| # 6D mode: array is directly at acquisition.zarr, not at /0 | ||||||||||||||||||||||||||||||||
| ts_arr = open_zarr_tensorstore(region_path, array_path="") | ||||||||||||||||||||||||||||||||
| if ts_arr is None: | ||||||||||||||||||||||||||||||||
| logger.debug( | ||||||||||||||||||||||||||||||||
|
Comment on lines
+2559
to
2562
|
||||||||||||||||||||||||||||||||
| # 6D mode: array is directly at acquisition.zarr, not at /0 | |
| ts_arr = open_zarr_tensorstore(region_path, array_path="") | |
| if ts_arr is None: | |
| logger.debug( | |
| # 6D mode: prefer array at acquisition.zarr root, but fall back to /0 for | |
| # older/simulated layouts that store the array under a "0" subdirectory. | |
| ts_arr = open_zarr_tensorstore(region_path, array_path="") | |
| if ts_arr is None: | |
| logger.debug( | |
| f"Root-level zarr array not found for region {region_idx}, " | |
| f"trying '0' subdirectory layout" | |
| ) | |
| ts_arr = open_zarr_tensorstore(region_path, array_path="0") | |
| if ts_arr is None: | |
| logger.debug( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code] Fixed in commit 9a346b0 - updated simulator to create 6D array at zarr root instead of /0 subdirectory.
Copilot
AI
Jan 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR description’s test plan suggests running simulate_zarr_acquisition.py --structure 6d_regions, but the simulator’s CLI only accepts --structure values "single", "6d", "per_fov", and "hcs" (simulate_zarr_acquisition.py:591-595). This discrepancy will make it hard to follow the documented test plan; either the CLI needs a 6d_regions option or the PR description/test instructions should be updated to refer to --structure 6d with the appropriate multi-region flags.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code] Fixed - updated PR description to use --structure 6d instead of --structure 6d_regions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switching 6D regions mode to open the array at the store root (array_path="") fixes the mismatch with Squid-written datasets, but the simulator currently creates 6D zarrs with the array under a "0" subdirectory, and the static 6D loader _load_zarr_v3_6d() still opens array_path="0". This inconsistency means simulated 6D acquisitions are unlikely to display correctly in live mode, and real 6D datasets with root-level arrays will still fail when opened via the file loader. To avoid hard-to-debug behavior, align all 6D code paths on a single layout (either move simulator writes and _load_zarr_v3_6d to use root-level arrays, or keep using the "0" subpath everywhere and revert this call).