diff --git a/src/ome_writers/_useq.py b/src/ome_writers/_useq.py index 2c01dbe..b33cd01 100644 --- a/src/ome_writers/_useq.py +++ b/src/ome_writers/_useq.py @@ -394,12 +394,11 @@ def _build_well_plate_positions(plate_plan: useq.WellPlatePlan) -> list[Position plate_column = str(col_idx + 1) for fov_idx, well_pos in enumerate(well_positions): pos = next(plate_iter) # grab the next AbsolutePosition in the outer loop - # NOTE: the pos.name is a useq's auto-generated name, either WellName_fovN - # for multi-fovs (e.g. A1_0000, etc) or just WellName for single fov - # (e.g. A1, B3, etc). We replace it with `fov{fov_idx}. + # Use the grid point's name from useq (set by name_pattern) + fov_name = getattr(well_pos, "name", None) or f"{fov_idx:04d}" positions.append( Position( - name=f"fov{fov_idx}", + name=fov_name, plate_row=plate_row, plate_column=plate_column, grid_row=getattr(well_pos, "row", None), @@ -421,6 +420,20 @@ def _row_idx_to_letter(index: int) -> str: return name +def _plate_row_to_str(value: int | str | None) -> str | None: + """Convert plate_row to string: int -> letter, str -> as-is, None -> None.""" + if value is None: + return None + return _row_idx_to_letter(value) if isinstance(value, int) else str(value) + + +def _plate_col_to_str(value: int | str | None) -> str | None: + """Convert plate_col to string: int -> 1-based str, str -> as-is, None -> None.""" + if value is None: + return None + return str(value + 1) if isinstance(value, int) else str(value) + + def _pos_with_grid_point( name: str, pos: useq.Position, @@ -453,10 +466,18 @@ def _pos_with_grid_point( name = f"{name}_{suffix}" if name else suffix else: name = f"{name}_g{gp_idx:04d}" if name else f"{gp_idx:04d}" + plate_row = getattr(pos, "plate_row", None) + plate_col = getattr(pos, "plate_col", None) + # When positions have plate info, use the grid point's name from useq + # (set by grid plan's name_pattern, e.g., "0000000") + if plate_row is not None and plate_col is not None: + name = getattr(gp, "name", None) or f"{gp_idx:04d}" return Position( name=name, grid_row=grid_row, grid_column=grid_col, + plate_row=_plate_row_to_str(plate_row), + plate_column=_plate_col_to_str(plate_col), x_coord=x_coord, y_coord=y_coord, z_coord=pos.z, @@ -498,6 +519,8 @@ def _build_stage_positions_plan(seq: useq.MDASequence) -> list[Position]: else: grid = global_grid or None + plate_row = getattr(pos, "plate_row", None) + plate_col = getattr(pos, "plate_col", None) if grid: for gp_idx, gp in enumerate(grid): positions.append( @@ -514,6 +537,8 @@ def _build_stage_positions_plan(seq: useq.MDASequence) -> list[Position]: z_coord=pos.z, grid_column=pos.col, grid_row=pos.row, + plate_row=_plate_row_to_str(plate_row), + plate_column=_plate_col_to_str(plate_col), ) ) @@ -521,16 +546,33 @@ def _build_stage_positions_plan(seq: useq.MDASequence) -> list[Position]: def _plate_from_useq(seq: useq.MDASequence) -> Plate | None: - """Convert a useq WellPlatePlan to an ome-writers Plate.""" + """Convert a useq WellPlatePlan or plate-annotated positions to a Plate.""" import useq useq_plate = seq.stage_positions - if not isinstance(useq_plate, useq.WellPlatePlan): - return None + if isinstance(useq_plate, useq.WellPlatePlan): + plate = useq_plate.plate + return Plate( + row_names=[_row_idx_to_letter(i) for i in range(plate.rows)], + column_names=[str(i + 1) for i in range(plate.columns)], + name=plate.name or None, + ) - plate = useq_plate.plate - return Plate( - row_names=[_row_idx_to_letter(i) for i in range(plate.rows)], - column_names=[str(i + 1) for i in range(plate.columns)], - name=plate.name or None, - ) + # Check if positions have plate_row/plate_col annotations + if useq_plate: + row_names: set[str] = set() + col_names: set[str] = set() + for p in useq_plate: + pr = getattr(p, "plate_row", None) + pc = getattr(p, "plate_col", None) + if pr is not None: + row_names.add(_plate_row_to_str(pr)) # type: ignore[arg-type] + if pc is not None: + col_names.add(_plate_col_to_str(pc)) # type: ignore[arg-type] + if row_names and col_names: + return Plate( + row_names=sorted(row_names), + column_names=sorted(col_names), + ) + + return None diff --git a/tests/test_useq.py b/tests/test_useq.py index 76effb0..2f1a45c 100644 --- a/tests/test_useq.py +++ b/tests/test_useq.py @@ -153,10 +153,10 @@ class Case: ), expected_dim_names=["p", "t", "c", "y", "x"], expected_positions=[ - ExpectedPosition("fov0", "A", "1", grid_row=0, grid_col=0), - ExpectedPosition("fov1", "A", "1", grid_row=0, grid_col=1), - ExpectedPosition("fov0", "B", "2", grid_row=0, grid_col=0), - ExpectedPosition("fov1", "B", "2", grid_row=0, grid_col=1), + ExpectedPosition("0000", "A", "1", grid_row=0, grid_col=0), + ExpectedPosition("0001", "A", "1", grid_row=0, grid_col=1), + ExpectedPosition("0000", "B", "2", grid_row=0, grid_col=0), + ExpectedPosition("0001", "B", "2", grid_row=0, grid_col=1), ], id="well_plate_with_points", ), @@ -716,11 +716,11 @@ def test_well_plate_fov_folder_names(tmp_path: Path, zarr_backend: str) -> None: for _ in seq: stream.append(dummy_frame) - # Check that zarr folders have the expected "fov0", "fov1" names + # Check that zarr folders have the expected names from useq's name_pattern # For WellPlatePlan with plate layout, the structure should be: - # test_fov_names.ome.zarr/A/1/fov0/, A/1/fov1/, B/2/fov0/, B/2/fov1/ + # test_fov_names.ome.zarr/A/1/0000/, A/1/0001/, B/2/0000/, B/2/0001/ zarr_root = tmp_path / "test_fov_names.ome.zarr" - assert (zarr_root / "A" / "1" / "fov0").exists(), "Expected fov0 in well A1" - assert (zarr_root / "A" / "1" / "fov1").exists(), "Expected fov1 in well A1" - assert (zarr_root / "B" / "2" / "fov0").exists(), "Expected fov0 in well B2" - assert (zarr_root / "B" / "2" / "fov1").exists(), "Expected fov1 in well B2" + assert (zarr_root / "A" / "1" / "0000").exists(), "Expected 0000 in well A1" + assert (zarr_root / "A" / "1" / "0001").exists(), "Expected 0001 in well A1" + assert (zarr_root / "B" / "2" / "0000").exists(), "Expected 0000 in well B2" + assert (zarr_root / "B" / "2" / "0001").exists(), "Expected 0001 in well B2"