Skip to content

Commit

Permalink
feat: add MDAEvent.slm_image (#204)
Browse files Browse the repository at this point in the history
* feat: add MDAEvent.slm_image

* new assertion

* fix 3.8

* add exposure
  • Loading branch information
tlambert03 authored Nov 21, 2024
1 parent d5f0525 commit ede3511
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 1 deletion.
48 changes: 47 additions & 1 deletion src/useq/_mda_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
Tuple,
)

from pydantic import Field, field_validator
import numpy as np
import numpy.typing as npt
from pydantic import Field, field_validator, model_validator

from useq._actions import AcquireImage, AnyAction
from useq._base_model import UseqModel
Expand Down Expand Up @@ -52,6 +54,43 @@ def __eq__(self, _value: object) -> bool:
return super().__eq__(_value)


class SLMImage(UseqModel):
"""SLM Image in a MDA event.
This object can be cast to a numpy.array using `np.asarray` or `np.array`.
Attributes
----------
data: npt.ArrayLike
Image data. Anything that can be cast to a numpy array. For pydantic simplicity,
we mark this as Any, but in practice it should be numpy.typing.ArrayLike (which
is anything that can be cast to a numpy array using `np.asarray`).
device: Optional[str]
Optional name of the SLM device to use. If not provided, the "default" SLM
device should be used. (It is left to the backend to determine what device that
is). By default, `None`.
exposure: Optional[float]
Exposure time for the SLM specifically (if different from the detector), in
milliseconds. If not provided, the exposure on the owning MDAEvent should be
used. By default, `None`.
"""

data: Any
device: Optional[str] = None
exposure: Optional[float] = None

@model_validator(mode="before")
def _cast_data(cls, v: Any) -> Any:
"""Can single, non-dict values to be the data."""
if not isinstance(v, dict):
v = {"data": v}
return v

def __array__(self, *args: Any, **kwargs: Any) -> npt.NDArray:
"""Cast the image data to a numpy array."""
return np.asarray(self.data, *args, **kwargs)


class PropertyTuple(NamedTuple):
"""Three-tuple capturing a device, property, and value.
Expand Down Expand Up @@ -110,6 +149,12 @@ class MDAEvent(UseqModel):
z_pos : float | None
Z position in microns. If not provided, implies use current position. By
default, `None`.
slm_image : SLMImage | None
Image data to display on an SLM device. `SLMImage` is a simple pydantic object
with two attributes: `data` and `device`. `data` is the image data (anything
that can be cast to a numpy array), `device` is the name of the SLM device to
use. If not provided, the "default" SLM device should be used. By default,
`None`.
sequence : MDASequence | None
A reference to the [`useq.MDASequence`][] this event belongs to. This is a
read-only attribute. By default, `None`.
Expand Down Expand Up @@ -146,6 +191,7 @@ class MDAEvent(UseqModel):
x_pos: Optional[float] = None
y_pos: Optional[float] = None
z_pos: Optional[float] = None
slm_image: Optional[SLMImage] = None
sequence: Optional["MDASequence"] = Field(default=None, repr=False)
properties: Optional[List[PropertyTuple]] = None
metadata: Dict[str, Any] = Field(default_factory=dict)
Expand Down
19 changes: 19 additions & 0 deletions tests/test_sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
ZRangeAround,
ZRelativePositions,
)
from useq._mda_event import SLMImage
from useq._position import RelativePosition

_T = List[Tuple[Any, Sequence[float]]]
Expand Down Expand Up @@ -462,3 +463,21 @@ def test_reset_event_timer() -> None:
assert not events[1].reset_event_timer
assert events[2].reset_event_timer
assert not events[3].reset_event_timer


def test_slm_image() -> None:
data = [[0, 0], [1, 1]]

# directly passing data
event = MDAEvent(slm_image=data)
assert isinstance(event.slm_image, SLMImage)

# we can cast SLMIamge to a numpy array
assert isinstance(np.asarray(event.slm_image), np.ndarray)
np.testing.assert_array_equal(event.slm_image, np.array(data))

# variant that also specifies device label
event2 = MDAEvent(slm_image={"data": data, "device": "SLM"})
assert event2.slm_image is not None
np.testing.assert_array_equal(event2.slm_image, np.array(data))
assert event2.slm_image.device == "SLM"

0 comments on commit ede3511

Please sign in to comment.