Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2a92639
feat: add plate_row and plate_col fields to PositionBase
ieivanov Mar 24, 2026
fa4f009
feat: add plate_row and plate_col fields to PositionBase
ieivanov Mar 24, 2026
e1fdddc
feat: add configurable name_pattern to grid plans
ieivanov Mar 25, 2026
436d880
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Mar 25, 2026
3ad1742
feat: auto-generate position name from plate_row/plate_col
ieivanov Mar 25, 2026
e29b780
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Mar 25, 2026
c44ebc9
docs: update docstrings for plate_row, plate_col, and name_pattern
ieivanov Mar 25, 2026
4161d7d
feat: enforce name matches plate_row/plate_col
ieivanov Mar 25, 2026
b8dde70
feat: allow plate_row and plate_col to be int or str
ieivanov Mar 25, 2026
3e2de02
test: add tests for plate_row/plate_col, name_pattern, and composite …
ieivanov Mar 25, 2026
0d8a19a
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Mar 25, 2026
439849c
feat: relax name validation for string plate_row/plate_col
ieivanov Mar 25, 2026
013dfda
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Mar 25, 2026
5cbb6f1
fix: resolve mypy type narrowing for plate_row/plate_col
ieivanov Mar 25, 2026
710ee3c
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Mar 25, 2026
4b15a8a
Merge branch 'add-plate-row-col' of https://github.com/ieivanov/useq-…
ieivanov Mar 26, 2026
3fe9125
Apply suggestion from @tlambert03
tlambert03 Mar 30, 2026
e94b678
minimal diff
tlambert03 Mar 30, 2026
4390d1c
don't exclude fields from ser
tlambert03 Mar 30, 2026
ead12dc
Rename row and col attributes to grid_row and grid_col in PositionBas…
tlambert03 Mar 30, 2026
1f30ea4
Add Pyright configuration and refactor plate position tests for clarity
tlambert03 Mar 30, 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
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,6 @@ ignore = [
"tox.ini",
"setup.py",
]

[tool.pyright]
reportArgumentType = false
8 changes: 5 additions & 3 deletions src/useq/_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ def iter_grid_positions(
yield pos_cls( # type: ignore [misc]
x=x0 + c * dx,
y=y0 - r * dy,
row=r,
col=c,
grid_row=r,
grid_col=c,
name=f"{str(idx).zfill(4)}",
)

Expand Down Expand Up @@ -517,7 +517,9 @@ def iter_grid_positions(
except ValueError:
pos = []
for idx, (x, y, r, c) in enumerate(pos):
yield AbsolutePosition(x=x, y=y, row=r, col=c, name=f"{str(idx).zfill(4)}")
yield AbsolutePosition(
x=x, y=y, grid_row=r, grid_col=c, name=f"{str(idx).zfill(4)}"
)

def _cached_tiles(
self,
Expand Down
30 changes: 24 additions & 6 deletions src/useq/_plate.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,19 +335,37 @@ def _transorm_coords(self, coords: np.ndarray) -> np.ndarray:
def all_well_positions(self) -> Sequence[Position]:
"""Return all wells (centers) as Position objects."""
return [
Position(x=x * 1000, y=y * 1000, name=name) # convert to µm
for (y, x), name in zip(
self.all_well_coordinates, self.all_well_names.reshape(-1), strict=False
Position(
x=x * 1000,
y=y * 1000,
name=name,
plate_row=int(row),
plate_col=int(col),
)
for (y, x), name, (row, col) in zip(
self.all_well_coordinates,
self.all_well_names.reshape(-1),
self.all_well_indices.reshape(-1, 2),
strict=False,
)
]

@cached_property
def selected_well_positions(self) -> Sequence[Position]:
"""Return selected wells (centers) as Position objects."""
return [
Position(x=x * 1000, y=y * 1000, name=name) # convert to µm
for (y, x), name in zip(
self.selected_well_coordinates, self.selected_well_names, strict=False
Position(
x=x * 1000,
y=y * 1000,
name=name,
plate_row=int(row),
plate_col=int(col),
)
for (y, x), name, (row, col) in zip(
self.selected_well_coordinates,
self.selected_well_names,
self.selected_well_indices,
strict=False,
)
]

Expand Down
40 changes: 34 additions & 6 deletions src/useq/_position.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from typing import TYPE_CHECKING, Any, Generic, Optional, SupportsIndex, TypeVar

import numpy as np
from pydantic import Field, model_validator
from pydantic import model_validator
from typing_extensions import deprecated

from useq._base_model import FrozenModel, MutableModel
from useq._mda_event import PropertyTuple
Expand Down Expand Up @@ -34,9 +35,13 @@ class PositionBase(MutableModel):
Optional name for the position.
sequence : MDASequence | None
Optional MDASequence relative this position.
row : int | None
plate_row : int | None
Optional 0-based row index for well plate positions.
plate_col : int | None
Optional 0-based column index for well plate positions.
grid_row : int | None
Optional row index, when used in a grid.
col : int | None
grid_col : int | None
Optional column index, when used in a grid.
"""

Expand All @@ -46,10 +51,28 @@ class PositionBase(MutableModel):
name: str | None = None
sequence: Optional["MDASequence"] = None
properties: list[PropertyTuple] | None = None
plate_row: int | None = None
plate_col: int | None = None
grid_row: int | None = None
grid_col: int | None = None

# excluded from serialization
row: int | None = Field(default=None, exclude=True)
col: int | None = Field(default=None, exclude=True)
@property
@deprecated("Use 'grid_row' instead.")
def row(self) -> int | None:
return self.grid_row

@row.setter
def row(self, value: int | None) -> None:
self.grid_row = value

@property
@deprecated("Use 'grid_col' instead.")
def col(self) -> int | None:
return self.grid_col

@col.setter
def col(self, value: int | None) -> None:
self.grid_col = value

def __add__(self, other: "RelativePosition") -> "Self":
"""Add two positions together to create a new position."""
Expand Down Expand Up @@ -88,6 +111,11 @@ def __round__(self, ndigits: "SupportsIndex | None" = None) -> "Self":
@model_validator(mode="before")
@classmethod
def _cast(cls, value: Any) -> Any:
if isinstance(value, dict):
if "row" in value and "grid_row" not in value:
value["grid_row"] = value.pop("row")
if "col" in value and "grid_col" not in value:
value["grid_col"] = value.pop("col")
if isinstance(value, (np.ndarray, tuple)):
x = y = z = None
if len(value) > 0:
Expand Down
9 changes: 8 additions & 1 deletion tests/fixtures/mda.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,24 @@
"setup": null,
"stage_positions": [
{
"grid_col": null,
"name": null,
"plate_col": null,
"plate_row": null,
"properties": null,
"grid_row": null,
"sequence": null,
"x": 10.0,
"y": 20.0,
"z": null
},
{
"grid_col": null,
"name": "test_name",
"plate_col": null,
"plate_row": null,
"properties": null,
"grid_row": null,
"sequence": {
"autofocus_plan": null,
"axis_order": [
Expand Down Expand Up @@ -105,7 +113,6 @@
"step": 1.0
}
},
"properties": null,
"x": 10.0,
"y": 20.0,
"z": 50.0
Expand Down
52 changes: 52 additions & 0 deletions tests/test_plate_positions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Tests for plate_row/plate_col on Position."""

from __future__ import annotations

import json

import pytest
import yaml

import useq


def test_plate_row_col() -> None:
pos = useq.Position(x=1000, y=2000, plate_row=0, plate_col=1)
assert (pos.plate_row, pos.plate_col) == (0, 1)
assert useq.Position(x=100).plate_row is None


@pytest.mark.parametrize("fmt", ["json", "yaml"])
def test_plate_round_trip(fmt: str) -> None:
seq = useq.MDASequence(
stage_positions=[useq.Position(x=1000, y=1000, plate_row=0, plate_col=1)]
)
if fmt == "json":
data = json.loads(seq.model_dump_json())
else:
data = yaml.safe_load(seq.yaml())
seq2 = useq.MDASequence.model_validate(data)
assert seq2.stage_positions[0].plate_row == 0
assert seq2.stage_positions[0].plate_col == 1


def test_plate_coords_propagate_through_add() -> None:
p1 = useq.Position(x=1000, plate_row=0, plate_col=0, name="A1")
p2 = useq.RelativePosition(x=50, name="0000")
result = p1 + p2
assert (result.plate_row, result.plate_col) == (0, 0)


@pytest.mark.parametrize("attr", ["selected_well_positions", "image_positions"])
def test_well_plate_plan_sets_plate_coords(attr: str) -> None:
pp = useq.WellPlatePlan(
plate="24-well",
a1_center_xy=(0, 0),
selected_wells=([0], [0]),
well_points_plan=useq.GridRowsColumns(
rows=1, columns=2, fov_width=1, fov_height=1
),
)
for pos in getattr(pp, attr):
assert isinstance(pos.plate_row, int)
assert isinstance(pos.plate_col, int)
Loading