Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 16 additions & 3 deletions src/useq/_iter_sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

class MDAEventDict(TypedDict, total=False):
index: ReadOnlyDict
non_dimension_coords: ReadOnlyDict
channel: EventChannel | None
exposure: float | None
min_start_time: float | None
Expand Down Expand Up @@ -152,7 +153,6 @@ def _iter_sequence(
continue # pragma: no cover
# get axes objects for this event
index, time, position, grid, channel, z_pos = _parse_axes(zip(order, item))

# skip if necessary
if _should_skip(position, channel, index, sequence.z_plan):
continue
Expand All @@ -166,8 +166,21 @@ def _iter_sequence(
)
# determine x, y, z positions
event_kwargs.update(_xyzpos(position, channel, sequence.z_plan, grid, z_pos))
if position and position.name:
event_kwargs["pos_name"] = position.name
if position:
if position.name:
event_kwargs["pos_name"] = position.name

pr = position.row
pc = position.col
if pr is not None or pc is not None:
nd_coords = event_kwargs.setdefault(
"non_dimension_coords", ReadOnlyDict()
)
if pr is not None:
nd_coords.data["r"] = pr
if pc is not None:
nd_coords.data["c"] = pc

if channel:
event_kwargs["channel"] = EventChannel.model_construct(
config=channel.config, group=channel.group
Expand Down
12 changes: 10 additions & 2 deletions src/useq/_mda_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ def __get_pydantic_core_schema__(
cls, source: type[Any], handler: GetCoreSchemaHandler
) -> core_schema.CoreSchema:
return core_schema.dict_schema(
keys_schema=core_schema.str_schema(), values_schema=core_schema.int_schema()
keys_schema=core_schema.str_schema(),
values_schema=core_schema.int_schema(),
serialization=core_schema.plain_serializer_function_ser_schema(dict),
)


Expand All @@ -158,6 +160,12 @@ class MDAEvent(UseqModel):
index : dict[str, int]
Index of this event in the sequence. This is a mapping of axis name
to index. For example: `{'t': 4, 'c': 0, 'z': 5},`
non_dimension_coords : dict[str, int]
Additional coordinates to associate with this event. This is a mapping of
coordinate name to index. For example: `{'r': 2, 'c': 3}` for a row/column
declaration. In that case, there will likely be a 1-dimensional axis for
position (e.g. `{'p': 0}`, `{'p': 1}`) in `index`, while the row/column
provides additional context (but is not a true axis of the sequence).
channel : Channel | None
Channel to use for this event. If `None`, implies use current channel.
By default, `None`. `Channel` is a simple pydantic object with two attributes:
Expand Down Expand Up @@ -220,6 +228,7 @@ class MDAEvent(UseqModel):
"""

index: ReadOnlyDict = Field(default_factory=ReadOnlyDict)
non_dimension_coords: ReadOnlyDict = Field(default_factory=ReadOnlyDict)
channel: Optional[Channel] = None
exposure: Optional[float] = Field(default=None, gt=0.0)
min_start_time: Optional[float] = None # time in sec
Expand All @@ -240,7 +249,6 @@ def _validate_channel(cls, val: Any) -> Any:
return Channel(config=val) if isinstance(val, str) else val

if field_serializer is not None:
_si = field_serializer("index", mode="plain")(lambda v: dict(v))
_sx = field_serializer("x_pos", mode="plain")(_float_or_none)
_sy = field_serializer("y_pos", mode="plain")(_float_or_none)
_sz = field_serializer("z_pos", mode="plain")(_float_or_none)
8 changes: 5 additions & 3 deletions src/useq/_plate.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,11 @@ def all_well_positions(self) -> Sequence[Position]:
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
Position(x=x * 1000, y=y * 1000, name=name, row=r, col=c) # convert to µm
for (y, x), name, (r, c) in zip(
self.selected_well_coordinates,
self.selected_well_names,
zip(*self.selected_wells or ()),
)
]

Expand Down
15 changes: 9 additions & 6 deletions src/useq/_position.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from collections.abc import Iterator
from typing import TYPE_CHECKING, Generic, Optional, SupportsIndex, TypeVar

from pydantic import Field

from useq._base_model import FrozenModel, MutableModel

if TYPE_CHECKING:
Expand Down Expand Up @@ -42,9 +40,8 @@ class PositionBase(MutableModel):
name: Optional[str] = None
sequence: Optional["MDASequence"] = None

# excluded from serialization
row: Optional[int] = Field(default=None, exclude=True)
col: Optional[int] = Field(default=None, exclude=True)
row: Optional[int] = None
col: Optional[int] = None

def __add__(self, other: "RelativePosition") -> "Self":
"""Add two positions together to create a new position."""
Expand All @@ -58,7 +55,13 @@ def __add__(self, other: "RelativePosition") -> "Self":
z += other.z
if (name := self.name) and other.name:
name = f"{name}_{other.name}"
kwargs = {**self.model_dump(), "x": x, "y": y, "z": z, "name": name}
kwargs = {
**self.model_dump(),
"x": x,
"y": y,
"z": z,
"name": name,
}
return type(self).model_construct(**kwargs) # type: ignore [return-value]

def __round__(self, ndigits: "SupportsIndex | None" = None) -> "Self":
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
def mda1() -> MDASequence:
return MDASequence(
axis_order="tpgcz",
metadata={"some info": "something"},
metadata={"some_info": "something"},
stage_positions=[
(10, 20),
{
Expand Down
132 changes: 37 additions & 95 deletions tests/fixtures/mda.json
Original file line number Diff line number Diff line change
@@ -1,129 +1,71 @@
{
"autofocus_plan": {
"autofocus_device_name": "Z",
"autofocus_motor_offset": 50.0,
"axes": [
"c"
]
},
"axis_order": [
"t",
"p",
"g",
"c",
"z"
],
"channels": [
{
"acquire_every": 1,
"camera": null,
"config": "Cy5",
"do_stack": true,
"exposure": 50.0,
"group": "Channel",
"z_offset": 0.0
},
{
"acquire_every": 1,
"camera": null,
"config": "FITC",
"do_stack": true,
"exposure": 100.0,
"group": "Channel",
"z_offset": 0.0
},
{
"acquire_every": 3,
"camera": null,
"config": "DAPI",
"do_stack": false,
"exposure": null,
"group": "Channel",
"z_offset": 0.0
}
],
"grid_plan": {
"columns": 1,
"fov_height": null,
"fov_width": null,
"mode": "row_wise_snake",
"overlap": [
0.0,
0.0
],
"relative_to": "center",
"rows": 2
},
"keep_shutter_open_across": [],
"metadata": {
"some info": "something"
"some_info": "something"
},
"axis_order": ["t", "p", "g", "c", "z"],
"stage_positions": [
{
"name": null,
"sequence": null,
"x": 10.0,
"y": 20.0,
"z": null
"y": 20.0
},
{
"x": 10.0,
"y": 20.0,
"z": 50.0,
"name": "test_name",
"sequence": {
"autofocus_plan": null,
"axis_order": [
"t",
"p",
"g",
"c",
"z"
],
"channels": [],
"axis_order": ["t", "p", "g", "c", "z"],
"grid_plan": {
"columns": 3,
"fov_height": null,
"fov_width": null,
"mode": "row_wise_snake",
"overlap": [
0.0,
0.0
],
"relative_to": "center",
"rows": 2
"rows": 2,
"columns": 3
},
"keep_shutter_open_across": [],
"metadata": {},
"stage_positions": [],
"time_plan": null,
"z_plan": {
"above": 10.0,
"below": 0.0,
"go_up": true,
"step": 1.0
}
},
"x": 10.0,
"y": 20.0,
"z": 50.0
}
}
],
"grid_plan": {
"rows": 2,
"columns": 1
},
"channels": [
{
"config": "Cy5",
"exposure": 50.0
},
{
"config": "FITC",
"exposure": 100.0
},
{
"config": "DAPI",
"do_stack": false,
"acquire_every": 3
}
],
"time_plan": {
"phases": [
{
"interval": 3.0,
"loops": 3,
"prioritize_duration": false
"loops": 3
},
{
"duration": 2400.0,
"interval": 10.0,
"prioritize_duration": true
"duration": 2400.0
}
],
"prioritize_duration": false
]
},
"z_plan": {
"go_up": true,
"range": 1.0,
"step": 0.5
},
"autofocus_plan": {
"autofocus_device_name": "Z",
"autofocus_motor_offset": 50.0,
"axes": ["c"]
}
}
2 changes: 1 addition & 1 deletion tests/fixtures/mda.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ grid_plan:
columns: 1
rows: 2
metadata:
some info: something
some_info: something
stage_positions:
- x: 10.0
y: 20.0
Expand Down
7 changes: 5 additions & 2 deletions tests/test_serialization.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
from pathlib import Path

import pytest
Expand All @@ -13,7 +12,11 @@ def test_serialization(mda1: MDASequence, ext: str) -> None:
mda = MDASequence.from_file(str(FILE))
assert mda == mda1
if ext == "json":
assert json.loads(mda.model_dump_json(exclude={"uid"})) == json.loads(text)
# NOTE: this is extremely sensitive to the format of the JSON fixture file
# things MUST be in the same order there... so if you get errors here, double
# check the order of the fields in the fixture file.
dump = mda.model_dump_json(exclude_unset=True)
assert dump == text.replace("\n", "").replace(" ", "")
else:
assert mda.yaml() == text

Expand Down