Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add MDAEvent.slm_image #204

Merged
merged 4 commits into from
Nov 21, 2024
Merged
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
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"