Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
93099b4
refactor: Refactor AxisIterator and TimePlan classes
tlambert03 Sep 25, 2024
1847b56
add should_skip and create_event_kwargs patterns
tlambert03 Oct 27, 2024
602a05d
wip
tlambert03 Nov 1, 2024
a164b76
wip: decent, need to decide about should_skip and merging of event va…
tlambert03 Nov 1, 2024
a499420
small fixes
tlambert03 Feb 25, 2025
6836adc
wip
tlambert03 Feb 25, 2025
96fd032
better merge
tlambert03 Feb 25, 2025
4208b59
wip
tlambert03 Feb 25, 2025
e9ffd5e
stupid wip
tlambert03 Feb 26, 2025
1fd9267
pattern1
tlambert03 Feb 26, 2025
875713e
wip
tlambert03 Feb 26, 2025
af4040f
updates
tlambert03 Feb 28, 2025
7418037
updates
tlambert03 Feb 28, 2025
fbf5376
misc
tlambert03 Feb 28, 2025
d73e4b0
Merge branch 'main' into new-iterator-pattern
tlambert03 May 19, 2025
74fecb0
wip
tlambert03 May 20, 2025
02c1d42
update tests
tlambert03 May 22, 2025
b881a03
test infinite iter
tlambert03 May 23, 2025
b6cf368
wip
tlambert03 May 24, 2025
f8bea3f
wip
tlambert03 May 24, 2025
bc42f64
remove more
tlambert03 May 24, 2025
48955b9
MDASeq
tlambert03 May 24, 2025
fb1db42
break apart
tlambert03 May 24, 2025
d0782a0
rm x
tlambert03 May 24, 2025
4c130d0
rm axis iterable
tlambert03 May 24, 2025
3280c75
rename v2
tlambert03 May 24, 2025
c8320c7
split typing
tlambert03 May 24, 2025
0bc2ac4
is_finite
tlambert03 May 24, 2025
bebe163
rename
tlambert03 May 24, 2025
e4ce0f7
reorder
tlambert03 May 24, 2025
abf4010
add time plan with tests
tlambert03 May 24, 2025
a4c251f
time done
tlambert03 May 24, 2025
4b59bce
update time mda event
tlambert03 May 24, 2025
f1c8f63
do z
tlambert03 May 24, 2025
527dc7b
v1 and v2
tlambert03 May 25, 2025
5e6be93
more renaming
tlambert03 May 25, 2025
863d258
use __iter__
tlambert03 May 25, 2025
3f4b8f7
wip
tlambert03 May 25, 2025
41fc517
add grid
tlambert03 May 25, 2025
a9dab6e
update grid test
tlambert03 May 25, 2025
db37732
refactor: rename SimpleAxis to SimpleValueAxis and update related tests
tlambert03 May 25, 2025
6541adb
more tests
tlambert03 May 25, 2025
cfa6bb4
starting on old api parity
tlambert03 May 25, 2025
5f52b78
update importable
tlambert03 May 25, 2025
58fbba5
wip
tlambert03 May 25, 2025
3084ec4
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] May 25, 2025
5a044f4
Merge branch 'main' into refactor/multi-axis-sequence
tlambert03 May 25, 2025
f92764d
fixes
tlambert03 May 25, 2025
256bd63
revert changes to v1
tlambert03 May 25, 2025
b3d6721
Merge branch 'main' into refactor/multi-axis-sequence
tlambert03 May 25, 2025
42f358f
cleanup
tlambert03 May 25, 2025
0f918db
cleanup
tlambert03 May 25, 2025
57b9ec2
Refactor Position and StagePositions classes; remove sequence attribu…
tlambert03 May 26, 2025
8ceec45
Refactor Position class to remove sequence attribute and update relat…
tlambert03 May 26, 2025
6dc2675
lift up event_builder
tlambert03 May 26, 2025
b6d73a0
Add comprehensive tests for MDASequence and refactor multidimensional…
tlambert03 May 26, 2025
af2dea9
Rename MultiDimSequence to MultiAxisSequence in documentation and exa…
tlambert03 May 26, 2025
05c899c
starting on transformers
tlambert03 May 26, 2025
c3095a4
fixes
tlambert03 May 26, 2025
a678fb0
fixes
tlambert03 May 26, 2025
f85a770
remove mutable iterator
tlambert03 May 26, 2025
67e1634
model validator to compose transforms
tlambert03 May 26, 2025
fc10a44
small typing fix
tlambert03 May 26, 2025
8676e22
move shutter tests
tlambert03 May 26, 2025
be078d8
add keep shutter open
tlambert03 May 26, 2025
36a9b9c
Merge branch 'main' into refactor/multi-axis-sequence
tlambert03 May 26, 2025
0d2a845
merge in test format
tlambert03 May 26, 2025
c8927f6
wip
tlambert03 May 26, 2025
0317f0b
fix: set priority for AutoFocusTransform and update test for reset_ev…
tlambert03 May 26, 2025
956373e
minimize case duplication
tlambert03 May 26, 2025
edb79f0
dedupe
tlambert03 May 26, 2025
4eadcb8
work for py3.9
tlambert03 May 26, 2025
22ca36a
add broken test
tlambert03 May 26, 2025
77d8cda
wip
tlambert03 May 26, 2025
0715375
refactor
tlambert03 May 27, 2025
7c28258
fix: set event.sequence to None in MDASequence tests
tlambert03 May 27, 2025
2175138
fix test, make immuatable again
tlambert03 May 27, 2025
088c1af
fix older
tlambert03 May 27, 2025
943bbe2
bump min pydantic
tlambert03 May 27, 2025
c5f5cbc
more coverage
tlambert03 May 27, 2025
77fe0bd
update lock
tlambert03 May 27, 2025
7b88f2d
docs
tlambert03 May 27, 2025
cd6ed2f
move all names to v2
tlambert03 May 27, 2025
403b017
start reducing duplication
tlambert03 May 27, 2025
a97ad98
rename method
tlambert03 May 27, 2025
7e1874c
combine z
tlambert03 May 27, 2025
3107243
merge time implementations
tlambert03 May 27, 2025
8cc4e1a
fix: correct typo in usage notes for axes iterator documentation
tlambert03 May 27, 2025
78c029a
add MutableMDAEvent
tlambert03 May 27, 2025
71e298c
add mutable mda event
tlambert03 May 27, 2025
903b904
refactor: update MutableMDAEvent and MDAEvent classes for improved se…
tlambert03 May 27, 2025
56fd774
Merge branch 'main' into refactor/multi-axis-sequence
tlambert03 Jun 18, 2025
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
509 changes: 509 additions & 0 deletions docs/useq-schema-v2-migration-guide.md

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ classifiers = [
]
dynamic = ["version"]
dependencies = [
"pydantic >=2.6",
"pydantic >=2.10",
"numpy >=2.1.0; python_version >= '3.13'",
"numpy >=1.26.0; python_version >= '3.12'",
"numpy >=1.25.2",
Expand Down Expand Up @@ -123,15 +123,14 @@ keep-runtime-typing = true
[tool.ruff.lint.per-file-ignores]
"tests/*.py" = ["D", "S101", "E501", "SLF"]

[tool.ruff.lint.flake8-tidy-imports]
# Disallow all relative imports.
ban-relative-imports = "all"

# https://docs.pytest.org/en/6.2.x/customize.html
[tool.pytest.ini_options]
minversion = "6.0"
testpaths = ["tests"]
filterwarnings = ["error"]
filterwarnings = [
"error",
"ignore:.*Positions no longer have a sequence attribute",
]

# https://mypy.readthedocs.io/en/stable/config_file.html
[tool.mypy]
Expand All @@ -143,6 +142,10 @@ show_error_codes = true
pretty = true
plugins = ["pydantic.mypy"]

[tool.pyright]
include = ["src", "tests/v2"]
reportArgumentType = false

# https://coverage.readthedocs.io/en/6.4/config.html
[tool.coverage.run]
source = ["useq"]
Expand Down
7 changes: 4 additions & 3 deletions src/useq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@

from useq._actions import AcquireImage, Action, CustomAction, HardwareAutofocus
from useq._channel import Channel
from useq._enums import Axis, RelativeTo, Shape
from useq._grid import (
GridFromEdges,
GridRowsColumns,
GridWidthHeight,
MultiPointPlan,
RandomPoints,
RelativeMultiPointPlan,
Shape,
)
from useq._hardware_autofocus import AnyAutofocusPlan, AutoFocusPlan, AxesBasedAF
from useq._mda_event import Channel as EventChannel
from useq._mda_event import MDAEvent, PropertyTuple, SLMImage
from useq._mda_event import MDAEvent, MutableMDAEvent, PropertyTuple, SLMImage
from useq._mda_sequence import MDASequence
from useq._plate import WellPlate, WellPlatePlan
from useq._plate_registry import register_well_plates, registered_well_plate_keys
Expand All @@ -29,7 +29,6 @@
TIntervalDuration,
TIntervalLoops,
)
from useq._utils import Axis
from useq._z import (
AnyZPlan,
ZAboveBelow,
Expand Down Expand Up @@ -65,12 +64,14 @@
"MDASequence",
"MultiPhaseTimePlan",
"MultiPointPlan",
"MutableMDAEvent",
"OrderMode",
"Position", # alias for AbsolutePosition
"PropertyTuple",
"RandomPoints",
"RelativeMultiPointPlan",
"RelativePosition",
"RelativeTo",
"SLMImage",
"Shape",
"TDurationLoops",
Expand Down
18 changes: 12 additions & 6 deletions src/useq/_base_model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from pathlib import Path
from re import findall
from types import MappingProxyType
from typing import (
IO,
Expand Down Expand Up @@ -27,10 +26,11 @@
_T = TypeVar("_T", bound="FrozenModel")
_Y = TypeVar("_Y", bound="UseqModel")

PYDANTIC_VERSION = tuple(int(x) for x in findall(r"\d_", pydantic.__version__)[:3])
GET_DEFAULT_KWARGS: dict = {}
PYDANTIC_VERSION = tuple(int(x) for x in pydantic.__version__.split(".")[:2])
if PYDANTIC_VERSION >= (2, 10):
GET_DEFAULT_KWARGS = {"validated_data": {}}
GET_DEFAULT_KWARGS: dict = {"validated_data": {}}
else:
GET_DEFAULT_KWARGS = {}

Check warning on line 33 in src/useq/_base_model.py

View check run for this annotation

Codecov / codecov/patch

src/useq/_base_model.py#L33

Added line #L33 was not covered by tests


class _ReplaceableModel(BaseModel):
Expand Down Expand Up @@ -87,9 +87,9 @@
)


class UseqModel(FrozenModel):
class IOMixin(BaseModel):
@classmethod
def from_file(cls: type[_Y], path: Union[str, Path]) -> _Y:
def from_file(cls, path: Union[str, Path]) -> "Self":
"""Return an instance of this class from a file. Supports JSON and YAML."""
path = Path(path)
if path.suffix in {".yaml", ".yml"}:
Expand Down Expand Up @@ -148,3 +148,9 @@
exclude_none=exclude_none,
)
return yaml.safe_dump(data, stream=stream)


class MutableUseqModel(IOMixin, MutableModel): ...


class UseqModel(FrozenModel, IOMixin): ...
69 changes: 69 additions & 0 deletions src/useq/_enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from enum import Enum
from typing import Final, Literal


class Axis(str, Enum):
"""Recognized useq-schema axis keys.

Attributes
----------
TIME : Literal["t"]
Time axis.
POSITION : Literal["p"]
XY Stage Position axis.
GRID : Literal["g"]
Grid axis (usually an additional row/column iteration around a position).
CHANNEL : Literal["c"]
Channel axis.
Z : Literal["z"]
Z axis.
"""

TIME = "t"
POSITION = "p"
GRID = "g"
CHANNEL = "c"
Z = "z"

def __str__(self) -> Literal["t", "p", "g", "c", "z"]:
return self.value


# note: order affects the default axis_order in MDASequence
AXES: Final[tuple[Axis, ...]] = (
Axis.TIME,
Axis.POSITION,
Axis.GRID,
Axis.CHANNEL,
Axis.Z,
)


class RelativeTo(Enum):
"""Where the coordinates of the grid are relative to.

Attributes
----------
center : Literal['center']
Grid is centered around the origin.
top_left : Literal['top_left']
Grid is positioned such that the top left corner is at the origin.
"""

center = "center"
top_left = "top_left"


class Shape(Enum):
"""Shape of the bounding box for random points.

Attributes
----------
ELLIPSE : Literal['ellipse']
The bounding box is an ellipse.
RECTANGLE : Literal['rectangle']
The bounding box is a rectangle.
"""

ELLIPSE = "ellipse"
RECTANGLE = "rectangle"
124 changes: 41 additions & 83 deletions src/useq/_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@
import math
import warnings
from collections.abc import Iterable, Iterator, Sequence
from enum import Enum
from typing import (
TYPE_CHECKING,
Annotated,
Any,
Callable,
Generic,
Optional,
Union,
)

import numpy as np
from annotated_types import Ge, Gt
from pydantic import Field, field_validator, model_validator
from pydantic import BaseModel, Field, field_validator, model_validator
from typing_extensions import Self, TypeAlias

from useq._enums import RelativeTo, Shape
from useq._point_visiting import OrderMode, TraversalOrder
from useq._position import (
AbsolutePosition,
Expand All @@ -37,47 +38,15 @@
MIN_RANDOM_POINTS = 10000


class RelativeTo(Enum):
"""Where the coordinates of the grid are relative to.

Attributes
----------
center : Literal['center']
Grid is centered around the origin.
top_left : Literal['top_left']
Grid is positioned such that the top left corner is at the origin.
"""

center = "center"
top_left = "top_left"


# used in iter_indices below, to determine the order in which indices are yielded
class _GridPlan(_MultiPointPlan[PositionT]):
"""Base class for all grid plans.

Attributes
----------
overlap : float | Tuple[float, float]
Overlap between grid positions in percent. If a single value is provided, it is
used for both x and y. If a tuple is provided, the first value is used
for x and the second for y.
mode : OrderMode
Define the ways of ordering the grid positions. Options are
row_wise, column_wise, row_wise_snake, column_wise_snake and spiral.
By default, row_wise_snake.
fov_width : Optional[float]
Width of the field of view in microns. If not provided, acquisition engines
should use current width of the FOV based on the current objective and camera.
Engines MAY override this even if provided.
fov_height : Optional[float]
Height of the field of view in microns. If not provided, acquisition engines
should use current height of the FOV based on the current objective and camera.
Engines MAY override this even if provided.
"""

class _GridMixin(BaseModel, Generic[PositionT]):
overlap: tuple[float, float] = Field(default=(0.0, 0.0), frozen=True)
mode: OrderMode = Field(default=OrderMode.row_wise_snake, frozen=True)
fov_width: Optional[float] = None
fov_height: Optional[float] = None

@property
def is_relative(self) -> bool:
return True

@field_validator("overlap", mode="before")
def _validate_overlap(cls, v: Any) -> tuple[float, float]:
Expand Down Expand Up @@ -105,24 +74,18 @@ def _ncolumns(self, dx: float) -> int:
"""Return the number of columns, given a grid step size."""
raise NotImplementedError

def num_positions(self) -> int:
"""Return the number of individual positions in the grid.
def __iter__(self) -> Iterator[PositionT]: # type: ignore [override]
yield from self.iter_grid_positions()

Note: For GridFromEdges and GridWidthHeight, this will depend on field of view
size. If no field of view size is provided, the number of positions will be 1.
"""
if isinstance(self, (GridFromEdges, GridWidthHeight)) and (
self.fov_width is None or self.fov_height is None
):
raise ValueError(
"Retrieving the number of positions in a GridFromEdges or "
"GridWidthHeight plan requires the field of view size to be set."
)
def _step_size(self, fov_width: float, fov_height: float) -> tuple[float, float]:
dx = fov_width - (fov_width * self.overlap[0]) / 100
dy = fov_height - (fov_height * self.overlap[1]) / 100
return dx, dy

dx, dy = self._step_size(self.fov_width or 1, self.fov_height or 1)
rows = self._nrows(dy)
cols = self._ncolumns(dx)
return rows * cols
def _build_position(self, **kwargs: Any) -> PositionT:
"""Build a position object for this grid plan."""
pos_cls = RelativePosition if self.is_relative else AbsolutePosition
return pos_cls(**kwargs) # type: ignore

def iter_grid_positions(
self,
Expand All @@ -142,26 +105,36 @@ def iter_grid_positions(
x0 = self._offset_x(dx)
y0 = self._offset_y(dy)

pos_cls = RelativePosition if self.is_relative else AbsolutePosition
for idx, (r, c) in enumerate(order.generate_indices(rows, cols)):
yield pos_cls( # type: ignore [misc]
yield self._build_position(
x=x0 + c * dx,
y=y0 - r * dy,
row=r,
col=c,
name=f"{str(idx).zfill(4)}",
)

def __iter__(self) -> Iterator[PositionT]: # type: ignore [override]
yield from self.iter_grid_positions()
def num_positions(self) -> int:
"""Return the number of individual positions in the grid.

def _step_size(self, fov_width: float, fov_height: float) -> tuple[float, float]:
dx = fov_width - (fov_width * self.overlap[0]) / 100
dy = fov_height - (fov_height * self.overlap[1]) / 100
return dx, dy
Note: For GridFromEdges and GridWidthHeight, this will depend on field of view
size. If no field of view size is provided, the number of positions will be 1.
"""
if isinstance(self, (GridFromEdges, GridWidthHeight)) and (
self.fov_width is None or self.fov_height is None
):
raise ValueError(
"Retrieving the number of positions in a GridFromEdges or "
"GridWidthHeight plan requires the field of view size to be set."
)

dx, dy = self._step_size(self.fov_width or 1, self.fov_height or 1)
rows = self._nrows(dy)
cols = self._ncolumns(dx)
return rows * cols


class GridFromEdges(_GridPlan[AbsolutePosition]):
class GridFromEdges(_GridMixin, _MultiPointPlan[AbsolutePosition]):
"""Yield absolute stage positions to cover a bounded area.

The bounded area is defined by top, left, bottom and right edges in
Expand Down Expand Up @@ -253,7 +226,7 @@ def plot(self, *, show: bool = True) -> Axes:
)


class GridRowsColumns(_GridPlan[RelativePosition]):
class GridRowsColumns(_GridMixin, _MultiPointPlan[RelativePosition]):
"""Grid plan based on number of rows and columns.

Attributes
Expand Down Expand Up @@ -311,7 +284,7 @@ def _offset_y(self, dy: float) -> float:
GridRelative = GridRowsColumns


class GridWidthHeight(_GridPlan[RelativePosition]):
class GridWidthHeight(_GridMixin, _MultiPointPlan[RelativePosition]):
"""Grid plan based on total width and height.

Attributes
Expand Down Expand Up @@ -371,21 +344,6 @@ def _offset_y(self, dy: float) -> float:
# ------------------------ RANDOM ------------------------


class Shape(Enum):
"""Shape of the bounding box for random points.

Attributes
----------
ELLIPSE : Literal['ellipse']
The bounding box is an ellipse.
RECTANGLE : Literal['rectangle']
The bounding box is a rectangle.
"""

ELLIPSE = "ellipse"
RECTANGLE = "rectangle"


class RandomPoints(_MultiPointPlan[RelativePosition]):
"""Yield random points in a specified geometric shape.

Expand Down
Loading
Loading