pymmcore-plus 0.12.0__py3-none-any.whl → 0.13.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pymmcore_plus/__init__.py +3 -3
- pymmcore_plus/_benchmark.py +203 -0
- pymmcore_plus/_cli.py +78 -13
- pymmcore_plus/_logger.py +10 -2
- pymmcore_plus/_pymmcore.py +12 -0
- pymmcore_plus/_util.py +16 -10
- pymmcore_plus/core/__init__.py +3 -0
- pymmcore_plus/core/_config.py +1 -1
- pymmcore_plus/core/_config_group.py +2 -2
- pymmcore_plus/core/_constants.py +27 -3
- pymmcore_plus/core/_device.py +4 -4
- pymmcore_plus/core/_metadata.py +1 -1
- pymmcore_plus/core/_mmcore_plus.py +184 -118
- pymmcore_plus/core/_property.py +3 -5
- pymmcore_plus/core/_sequencing.py +369 -234
- pymmcore_plus/core/events/__init__.py +3 -3
- pymmcore_plus/experimental/__init__.py +0 -0
- pymmcore_plus/experimental/unicore/__init__.py +14 -0
- pymmcore_plus/experimental/unicore/_device_manager.py +173 -0
- pymmcore_plus/experimental/unicore/_proxy.py +127 -0
- pymmcore_plus/experimental/unicore/_unicore.py +703 -0
- pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
- pymmcore_plus/experimental/unicore/devices/_device.py +269 -0
- pymmcore_plus/experimental/unicore/devices/_properties.py +400 -0
- pymmcore_plus/experimental/unicore/devices/_stage.py +221 -0
- pymmcore_plus/install.py +10 -7
- pymmcore_plus/mda/__init__.py +1 -1
- pymmcore_plus/mda/_engine.py +152 -43
- pymmcore_plus/mda/_runner.py +8 -1
- pymmcore_plus/mda/events/__init__.py +2 -2
- pymmcore_plus/mda/handlers/__init__.py +1 -1
- pymmcore_plus/mda/handlers/_ome_zarr_writer.py +2 -2
- pymmcore_plus/mda/handlers/_tensorstore_handler.py +6 -2
- pymmcore_plus/metadata/__init__.py +3 -3
- pymmcore_plus/metadata/functions.py +18 -8
- pymmcore_plus/metadata/schema.py +6 -5
- pymmcore_plus/mocks.py +49 -0
- pymmcore_plus/model/_config_file.py +1 -1
- pymmcore_plus/model/_core_device.py +10 -1
- pymmcore_plus/model/_device.py +17 -6
- pymmcore_plus/model/_property.py +11 -2
- pymmcore_plus/seq_tester.py +1 -1
- {pymmcore_plus-0.12.0.dist-info → pymmcore_plus-0.13.1.dist-info}/METADATA +14 -6
- pymmcore_plus-0.13.1.dist-info/RECORD +71 -0
- {pymmcore_plus-0.12.0.dist-info → pymmcore_plus-0.13.1.dist-info}/WHEEL +1 -1
- pymmcore_plus-0.12.0.dist-info/RECORD +0 -59
- {pymmcore_plus-0.12.0.dist-info → pymmcore_plus-0.13.1.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.12.0.dist-info → pymmcore_plus-0.13.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from typing import ClassVar, Literal
|
|
3
|
+
|
|
4
|
+
from pymmcore_plus.core import DeviceType
|
|
5
|
+
from pymmcore_plus.core._constants import Keyword
|
|
6
|
+
|
|
7
|
+
from ._device import SeqT, SequenceableDevice
|
|
8
|
+
|
|
9
|
+
__all__ = ["_BaseStage"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _BaseStage(SequenceableDevice[SeqT]):
|
|
13
|
+
"""Shared logic for Stage and XYStage devices."""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def home(self) -> None:
|
|
17
|
+
"""Move the stage to its home position."""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def stop(self) -> None:
|
|
21
|
+
"""Stop the stage."""
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def set_origin(self) -> None:
|
|
25
|
+
"""Zero the stage's coordinates at the current position."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StageDevice(_BaseStage[float]):
|
|
29
|
+
"""ABC for Stage devices."""
|
|
30
|
+
|
|
31
|
+
_TYPE: ClassVar[Literal[DeviceType.Stage]] = DeviceType.Stage
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def set_position_um(self, val: float) -> None:
|
|
35
|
+
"""Set the position of the stage in microns."""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def get_position_um(self) -> float:
|
|
39
|
+
"""Returns the current position of the stage in microns."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# TODO: consider if we can just subclass StageDevice instead of _BaseStage
|
|
43
|
+
class XYStageDevice(_BaseStage[tuple[float, float]]):
|
|
44
|
+
"""ABC for XYStage devices."""
|
|
45
|
+
|
|
46
|
+
_TYPE: ClassVar[Literal[DeviceType.XYStage]] = DeviceType.XYStage
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def set_position_um(self, x: float, y: float) -> None:
|
|
50
|
+
"""Set the position of the XY stage in microns."""
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def get_position_um(self) -> tuple[float, float]:
|
|
54
|
+
"""Returns the current position of the XY stage in microns."""
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def set_origin_x(self) -> None:
|
|
58
|
+
"""Zero the stage's X coordinates at the current position."""
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def set_origin_y(self) -> None:
|
|
62
|
+
"""Zero the stage's Y coordinates at the current position."""
|
|
63
|
+
|
|
64
|
+
# ----------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def set_relative_position_um(self, dx: float, dy: float) -> None:
|
|
67
|
+
"""Move the stage by a relative amount.
|
|
68
|
+
|
|
69
|
+
Can be overridden for more efficient implementations.
|
|
70
|
+
"""
|
|
71
|
+
x, y = self.get_position_um()
|
|
72
|
+
self.set_position_um(x + dx, y + dy)
|
|
73
|
+
|
|
74
|
+
def set_adapter_origin_um(self, x: float, y: float) -> None:
|
|
75
|
+
"""Alter the software coordinate translation between micrometers and steps.
|
|
76
|
+
|
|
77
|
+
... such that the current position becomes the given coordinates.
|
|
78
|
+
"""
|
|
79
|
+
# I don't quite understand what this method is supposed to do yet.
|
|
80
|
+
# I believe it's here to give device adapter implementations a way to to set
|
|
81
|
+
# the origin of some translation between micrometers and steps, rather than to
|
|
82
|
+
# directly update the origin on the device itself.
|
|
83
|
+
|
|
84
|
+
def set_origin(self) -> None:
|
|
85
|
+
"""Zero the stage's coordinates at the current position.
|
|
86
|
+
|
|
87
|
+
This is a convenience method that calls `set_origin_x` and `set_origin_y`.
|
|
88
|
+
Can be overridden for more efficient implementations.
|
|
89
|
+
"""
|
|
90
|
+
self.set_origin_x()
|
|
91
|
+
self.set_origin_y()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class XYStepperStageDevice(XYStageDevice):
|
|
95
|
+
"""ABC for XYStage devices that support stepper motors.
|
|
96
|
+
|
|
97
|
+
In this variant, rather than providing `set_position_um` and `get_position_um`,
|
|
98
|
+
you provide `set_position_steps`, `get_position_steps`, `get_step_size_x_um`,
|
|
99
|
+
and `get_step_size_y_um`. A default implementation of `set_position_um` and
|
|
100
|
+
`get_position_um` is then provided that uses these methods, taking into account
|
|
101
|
+
the XY-mirroring properties of the device.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def set_position_steps(self, x: int, y: int) -> None:
|
|
106
|
+
"""Set the position of the XY stage in steps."""
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def get_position_steps(self) -> tuple[int, int]:
|
|
110
|
+
"""Returns the current position of the XY stage in steps."""
|
|
111
|
+
|
|
112
|
+
@abstractmethod
|
|
113
|
+
def get_step_size_x_um(self) -> float:
|
|
114
|
+
"""Returns the step size of the X axis in microns."""
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def get_step_size_y_um(self) -> float:
|
|
118
|
+
"""Returns the step size of the Y axis in microns."""
|
|
119
|
+
|
|
120
|
+
# ----------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def __init__(self) -> None:
|
|
123
|
+
super().__init__()
|
|
124
|
+
self.register_property(name=Keyword.Transpose_MirrorX, default_value=False)
|
|
125
|
+
self.register_property(name=Keyword.Transpose_MirrorY, default_value=False)
|
|
126
|
+
self._origin_x_steps: int = 0
|
|
127
|
+
self._origin_y_steps: int = 0
|
|
128
|
+
|
|
129
|
+
def set_position_um(self, x: float, y: float) -> None:
|
|
130
|
+
"""Set the position of the XY stage in microns."""
|
|
131
|
+
# Converts the given micrometer coordinates to steps and sets the position.
|
|
132
|
+
mirror_x, mirror_y = self._get_orientation()
|
|
133
|
+
|
|
134
|
+
steps_x = int(x / self.get_step_size_x_um())
|
|
135
|
+
steps_y = int(y / self.get_step_size_y_um())
|
|
136
|
+
|
|
137
|
+
if mirror_x:
|
|
138
|
+
steps_x = -steps_x
|
|
139
|
+
if mirror_y:
|
|
140
|
+
steps_y = -steps_y
|
|
141
|
+
|
|
142
|
+
x_steps = self._origin_x_steps + steps_x
|
|
143
|
+
y_steps = self._origin_y_steps + steps_y
|
|
144
|
+
self.set_position_steps(x_steps, y_steps)
|
|
145
|
+
|
|
146
|
+
self.core.events.XYStagePositionChanged.emit(self.get_label(), x, y)
|
|
147
|
+
|
|
148
|
+
def get_position_um(self) -> tuple[float, float]:
|
|
149
|
+
"""Get the position of the XY stage in microns."""
|
|
150
|
+
# Converts the current steps to micrometer coordinates and returns the position.
|
|
151
|
+
mirror_x, mirror_y = self._get_orientation()
|
|
152
|
+
x_steps, y_steps = self.get_position_steps()
|
|
153
|
+
|
|
154
|
+
x = (self._origin_x_steps - x_steps) * self.get_step_size_x_um()
|
|
155
|
+
y = (self._origin_y_steps - y_steps) * self.get_step_size_y_um()
|
|
156
|
+
if not mirror_x:
|
|
157
|
+
x = -x
|
|
158
|
+
if not mirror_y:
|
|
159
|
+
y = -y
|
|
160
|
+
|
|
161
|
+
return x, y
|
|
162
|
+
|
|
163
|
+
def set_relative_position_steps(self, dx: int, dy: int) -> None:
|
|
164
|
+
"""Move the stage by a relative amount.
|
|
165
|
+
|
|
166
|
+
Can be overridden for more efficient implementations.
|
|
167
|
+
"""
|
|
168
|
+
x_steps, y_steps = self.get_position_steps()
|
|
169
|
+
self.set_position_steps(x_steps + dx, y_steps + dy)
|
|
170
|
+
|
|
171
|
+
def set_relative_position_um(self, dx: float, dy: float) -> None:
|
|
172
|
+
"""Default implementation for relative motion.
|
|
173
|
+
|
|
174
|
+
Can be overridden for more efficient implementations.
|
|
175
|
+
"""
|
|
176
|
+
mirror_x, mirror_y = self._get_orientation()
|
|
177
|
+
|
|
178
|
+
if mirror_x:
|
|
179
|
+
dx = -dx
|
|
180
|
+
if mirror_y:
|
|
181
|
+
dy = -dy
|
|
182
|
+
|
|
183
|
+
steps_x = int(dx / self.get_step_size_x_um())
|
|
184
|
+
steps_y = int(dy / self.get_step_size_y_um())
|
|
185
|
+
|
|
186
|
+
self.set_relative_position_steps(steps_x, steps_y)
|
|
187
|
+
|
|
188
|
+
x, y = self.get_position_um()
|
|
189
|
+
self.core.events.XYStagePositionChanged.emit(self.get_label(), x, y)
|
|
190
|
+
|
|
191
|
+
def set_adapter_origin_um(self, x: float = 0.0, y: float = 0.0) -> None:
|
|
192
|
+
"""Alter the software coordinate translation between micrometers and steps.
|
|
193
|
+
|
|
194
|
+
... such that the current position becomes the given coordinates.
|
|
195
|
+
"""
|
|
196
|
+
mirror_x, mirror_y = self._get_orientation()
|
|
197
|
+
x_steps, y_steps = self.get_position_steps()
|
|
198
|
+
|
|
199
|
+
steps_x = int(x / self.get_step_size_x_um())
|
|
200
|
+
steps_y = int(y / self.get_step_size_y_um())
|
|
201
|
+
|
|
202
|
+
self._origin_x_steps = x_steps + (steps_x if mirror_x else -steps_x)
|
|
203
|
+
self._origin_y_steps = y_steps + (steps_y if mirror_y else -steps_y)
|
|
204
|
+
|
|
205
|
+
def set_origin(self) -> None:
|
|
206
|
+
"""Zero the stage's coordinates at the current position."""
|
|
207
|
+
self.set_adapter_origin_um()
|
|
208
|
+
|
|
209
|
+
def set_origin_x(self) -> None:
|
|
210
|
+
"""Zero the stage's X coordinates at the current position."""
|
|
211
|
+
raise NotImplementedError # pragma: no cover
|
|
212
|
+
|
|
213
|
+
def set_origin_y(self) -> None:
|
|
214
|
+
"""Zero the stage's Y coordinates at the current position."""
|
|
215
|
+
raise NotImplementedError # pragma: no cover
|
|
216
|
+
|
|
217
|
+
def _get_orientation(self) -> tuple[bool, bool]:
|
|
218
|
+
return (
|
|
219
|
+
self.get_property_value(Keyword.Transpose_MirrorX),
|
|
220
|
+
self.get_property_value(Keyword.Transpose_MirrorY),
|
|
221
|
+
)
|
pymmcore_plus/install.py
CHANGED
|
@@ -37,14 +37,12 @@ try:
|
|
|
37
37
|
rich_print(f"{emoji}{color}{text}")
|
|
38
38
|
|
|
39
39
|
@contextmanager
|
|
40
|
-
def _spinner(
|
|
41
|
-
text: str = "", color: str = "bold blue"
|
|
42
|
-
) -> Iterator[progress.Progress]:
|
|
40
|
+
def _spinner(text: str, color: str = "bold blue") -> Iterator[None]:
|
|
43
41
|
with progress.Progress(
|
|
44
42
|
progress.SpinnerColumn(), progress.TextColumn(f"[{color}]{text}")
|
|
45
43
|
) as pbar:
|
|
46
44
|
pbar.add_task(description=text, total=None)
|
|
47
|
-
yield
|
|
45
|
+
yield None
|
|
48
46
|
|
|
49
47
|
except ImportError: # pragma: no cover
|
|
50
48
|
progress = None # type: ignore
|
|
@@ -53,7 +51,7 @@ except ImportError: # pragma: no cover
|
|
|
53
51
|
print(text)
|
|
54
52
|
|
|
55
53
|
@contextmanager
|
|
56
|
-
def _spinner(text: str
|
|
54
|
+
def _spinner(text: str, color: str = "") -> Iterator[None]:
|
|
57
55
|
print(text)
|
|
58
56
|
yield
|
|
59
57
|
|
|
@@ -78,8 +76,8 @@ def _get_spinner(log_msg: _MsgLogger) -> Callable[[str], AbstractContextManager]
|
|
|
78
76
|
else:
|
|
79
77
|
|
|
80
78
|
@contextmanager
|
|
81
|
-
def spinner(
|
|
82
|
-
log_msg(
|
|
79
|
+
def spinner(text: str, color: str = "") -> Iterator[None]:
|
|
80
|
+
log_msg(text)
|
|
83
81
|
yield
|
|
84
82
|
|
|
85
83
|
return spinner
|
|
@@ -209,6 +207,11 @@ def install(
|
|
|
209
207
|
"""
|
|
210
208
|
if PLATFORM not in ("Darwin", "Windows"): # pragma: no cover
|
|
211
209
|
log_msg(f"Unsupported platform: {PLATFORM!r}", "bold red", ":x:")
|
|
210
|
+
log_msg(
|
|
211
|
+
"Consider building from source (mmcore build-dev).",
|
|
212
|
+
"bold yellow",
|
|
213
|
+
":light_bulb:",
|
|
214
|
+
)
|
|
212
215
|
raise sys.exit(1)
|
|
213
216
|
|
|
214
217
|
if release == "latest":
|
pymmcore_plus/mda/__init__.py
CHANGED
pymmcore_plus/mda/_engine.py
CHANGED
|
@@ -3,18 +3,16 @@ from __future__ import annotations
|
|
|
3
3
|
import time
|
|
4
4
|
from contextlib import suppress
|
|
5
5
|
from itertools import product
|
|
6
|
-
from typing import
|
|
7
|
-
TYPE_CHECKING,
|
|
8
|
-
NamedTuple,
|
|
9
|
-
cast,
|
|
10
|
-
)
|
|
6
|
+
from typing import TYPE_CHECKING, Literal, NamedTuple, cast
|
|
11
7
|
|
|
8
|
+
import numpy as np
|
|
9
|
+
import useq
|
|
12
10
|
from useq import HardwareAutofocus, MDAEvent, MDASequence
|
|
13
11
|
|
|
14
12
|
from pymmcore_plus._logger import logger
|
|
15
13
|
from pymmcore_plus._util import retry
|
|
16
14
|
from pymmcore_plus.core._constants import Keyword
|
|
17
|
-
from pymmcore_plus.core._sequencing import SequencedEvent
|
|
15
|
+
from pymmcore_plus.core._sequencing import SequencedEvent, iter_sequenced_events
|
|
18
16
|
from pymmcore_plus.metadata import (
|
|
19
17
|
FrameMetaV1,
|
|
20
18
|
PropertyValue,
|
|
@@ -27,6 +25,7 @@ from ._protocol import PMDAEngine
|
|
|
27
25
|
|
|
28
26
|
if TYPE_CHECKING:
|
|
29
27
|
from collections.abc import Iterable, Iterator, Sequence
|
|
28
|
+
from typing import TypeAlias
|
|
30
29
|
|
|
31
30
|
from numpy.typing import NDArray
|
|
32
31
|
|
|
@@ -34,6 +33,19 @@ if TYPE_CHECKING:
|
|
|
34
33
|
|
|
35
34
|
from ._protocol import PImagePayload
|
|
36
35
|
|
|
36
|
+
IncludePositionArg: TypeAlias = Literal[True, False, "unsequenced-only"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# these are SLM devices that have a known pixel_on_value.
|
|
40
|
+
# there is currently no way to extract this information from the core,
|
|
41
|
+
# so it is hard-coded here.
|
|
42
|
+
# maps device_name -> pixel_on_value
|
|
43
|
+
_SLM_DEVICES_PIXEL_ON_VALUES: dict[str, int] = {
|
|
44
|
+
"MightexPolygon1000": 255,
|
|
45
|
+
"Mosaic3": 1,
|
|
46
|
+
"GenericSLM": 255,
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
|
|
38
50
|
class MDAEngine(PMDAEngine):
|
|
39
51
|
"""The default MDAengine that ships with pymmcore-plus.
|
|
@@ -59,7 +71,11 @@ class MDAEngine(PMDAEngine):
|
|
|
59
71
|
|
|
60
72
|
def __init__(self, mmc: CMMCorePlus, use_hardware_sequencing: bool = True) -> None:
|
|
61
73
|
self._mmc = mmc
|
|
62
|
-
self.use_hardware_sequencing = use_hardware_sequencing
|
|
74
|
+
self.use_hardware_sequencing: bool = use_hardware_sequencing
|
|
75
|
+
|
|
76
|
+
# whether to include position metadata when fetching on-frame metadata
|
|
77
|
+
# omitted by default when performing triggered acquisition because it's slow.
|
|
78
|
+
self._include_frame_position_metadata: IncludePositionArg = "unsequenced-only"
|
|
63
79
|
|
|
64
80
|
# used to check if the hardware autofocus is engaged when the sequence begins.
|
|
65
81
|
# if it is, we will re-engage it after the autofocus action (if successful).
|
|
@@ -83,6 +99,19 @@ class MDAEngine(PMDAEngine):
|
|
|
83
99
|
# in the channel group.
|
|
84
100
|
self._config_device_props: dict[str, Sequence[tuple[str, str]]] = {}
|
|
85
101
|
|
|
102
|
+
@property
|
|
103
|
+
def include_frame_position_metadata(self) -> IncludePositionArg:
|
|
104
|
+
return self._include_frame_position_metadata
|
|
105
|
+
|
|
106
|
+
@include_frame_position_metadata.setter
|
|
107
|
+
def include_frame_position_metadata(self, value: IncludePositionArg) -> None:
|
|
108
|
+
if value not in (True, False, "unsequenced-only"): # pragma: no cover
|
|
109
|
+
raise ValueError(
|
|
110
|
+
"include_frame_position_metadata must be True, False, or "
|
|
111
|
+
"'unsequenced-only'"
|
|
112
|
+
)
|
|
113
|
+
self._include_frame_position_metadata = value
|
|
114
|
+
|
|
86
115
|
@property
|
|
87
116
|
def mmcore(self) -> CMMCorePlus:
|
|
88
117
|
"""The `CMMCorePlus` instance to use for hardware control."""
|
|
@@ -188,21 +217,7 @@ class MDAEngine(PMDAEngine):
|
|
|
188
217
|
yield from events
|
|
189
218
|
return
|
|
190
219
|
|
|
191
|
-
|
|
192
|
-
for event in events:
|
|
193
|
-
# if the sequence is empty or the current event can be sequenced with the
|
|
194
|
-
# previous event, add it to the sequence
|
|
195
|
-
if not seq or self._mmc.canSequenceEvents(seq[-1], event, len(seq)):
|
|
196
|
-
seq.append(event)
|
|
197
|
-
else:
|
|
198
|
-
# otherwise, yield a SequencedEvent if the sequence has accumulated
|
|
199
|
-
# more than one event, otherwise yield the single event
|
|
200
|
-
yield seq[0] if len(seq) == 1 else SequencedEvent.create(seq)
|
|
201
|
-
# add this current event and start a new sequence
|
|
202
|
-
seq = [event]
|
|
203
|
-
# yield any remaining events
|
|
204
|
-
if seq:
|
|
205
|
-
yield seq[0] if len(seq) == 1 else SequencedEvent.create(seq)
|
|
220
|
+
yield from iter_sequenced_events(self._mmc, events)
|
|
206
221
|
|
|
207
222
|
# ===================== Regular Events =====================
|
|
208
223
|
|
|
@@ -220,7 +235,8 @@ class MDAEngine(PMDAEngine):
|
|
|
220
235
|
self._set_event_position(event)
|
|
221
236
|
if event.z_pos is not None:
|
|
222
237
|
self._set_event_z(event)
|
|
223
|
-
|
|
238
|
+
if event.slm_image is not None:
|
|
239
|
+
self._set_event_slm_image(event)
|
|
224
240
|
if event.channel is not None:
|
|
225
241
|
try:
|
|
226
242
|
# possible speedup by setting manually.
|
|
@@ -232,7 +248,12 @@ class MDAEngine(PMDAEngine):
|
|
|
232
248
|
self._mmc.setExposure(event.exposure)
|
|
233
249
|
except Exception as e:
|
|
234
250
|
logger.warning("Failed to set exposure. %s", e)
|
|
235
|
-
|
|
251
|
+
if event.properties is not None:
|
|
252
|
+
try:
|
|
253
|
+
for dev, prop, value in event.properties:
|
|
254
|
+
self._mmc.setProperty(dev, prop, value)
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.warning("Failed to set properties. %s", e)
|
|
236
257
|
if (
|
|
237
258
|
# (if autoshutter wasn't set at the beginning of the sequence
|
|
238
259
|
# then it never matters...)
|
|
@@ -253,6 +274,9 @@ class MDAEngine(PMDAEngine):
|
|
|
253
274
|
`exec_event`, which *is* part of the protocol), but it is made public
|
|
254
275
|
in case a user wants to subclass this engine and override this method.
|
|
255
276
|
"""
|
|
277
|
+
if event.slm_image is not None:
|
|
278
|
+
self._exec_event_slm_image(event.slm_image)
|
|
279
|
+
|
|
256
280
|
try:
|
|
257
281
|
self._mmc.snapImage()
|
|
258
282
|
# taking event time after snapImage includes exposure time
|
|
@@ -274,6 +298,7 @@ class MDAEngine(PMDAEngine):
|
|
|
274
298
|
event,
|
|
275
299
|
runner_time_ms=event_time_ms,
|
|
276
300
|
camera_device=self._mmc.getPhysicalCameraDevice(cam),
|
|
301
|
+
include_position=self._include_frame_position_metadata is not False,
|
|
277
302
|
)
|
|
278
303
|
# Note, the third element is actually a MutableMapping, but mypy doesn't
|
|
279
304
|
# see TypedDict as a subclass of MutableMapping yet.
|
|
@@ -285,6 +310,7 @@ class MDAEngine(PMDAEngine):
|
|
|
285
310
|
event: MDAEvent,
|
|
286
311
|
prop_values: tuple[PropertyValue, ...] | None = None,
|
|
287
312
|
runner_time_ms: float = 0.0,
|
|
313
|
+
include_position: bool = True,
|
|
288
314
|
camera_device: str | None = None,
|
|
289
315
|
) -> FrameMetaV1:
|
|
290
316
|
if prop_values is None and (ch := event.channel):
|
|
@@ -298,6 +324,7 @@ class MDAEngine(PMDAEngine):
|
|
|
298
324
|
camera_device=camera_device,
|
|
299
325
|
property_values=prop_values,
|
|
300
326
|
mda_event=event,
|
|
327
|
+
include_position=include_position,
|
|
301
328
|
)
|
|
302
329
|
|
|
303
330
|
def teardown_event(self, event: MDAEvent) -> None:
|
|
@@ -316,7 +343,7 @@ class MDAEngine(PMDAEngine):
|
|
|
316
343
|
core.stopXYStageSequence(core.getXYStageDevice())
|
|
317
344
|
if event.z_sequence:
|
|
318
345
|
core.stopStageSequence(core.getFocusDevice())
|
|
319
|
-
for dev, prop in event.property_sequences
|
|
346
|
+
for dev, prop in event.property_sequences:
|
|
320
347
|
core.stopPropertySequence(dev, prop)
|
|
321
348
|
|
|
322
349
|
def teardown_sequence(self, sequence: MDASequence) -> None:
|
|
@@ -325,17 +352,15 @@ class MDAEngine(PMDAEngine):
|
|
|
325
352
|
|
|
326
353
|
# ===================== Sequenced Events =====================
|
|
327
354
|
|
|
328
|
-
def
|
|
329
|
-
"""
|
|
355
|
+
def _load_sequenced_event(self, event: SequencedEvent) -> None:
|
|
356
|
+
"""Load a `SequencedEvent` into the core.
|
|
330
357
|
|
|
331
|
-
|
|
332
|
-
`
|
|
333
|
-
in case a user wants to subclass this engine and override this method.
|
|
358
|
+
`SequencedEvent` is a special pymmcore-plus specific subclass of
|
|
359
|
+
`useq.MDAEvent`.
|
|
334
360
|
"""
|
|
335
361
|
core = self._mmc
|
|
336
|
-
cam_device = self._mmc.getCameraDevice()
|
|
337
|
-
|
|
338
362
|
if event.exposure_sequence:
|
|
363
|
+
cam_device = core.getCameraDevice()
|
|
339
364
|
with suppress(RuntimeError):
|
|
340
365
|
core.stopExposureSequence(cam_device)
|
|
341
366
|
core.loadExposureSequence(cam_device, event.exposure_sequence)
|
|
@@ -349,40 +374,71 @@ class MDAEngine(PMDAEngine):
|
|
|
349
374
|
with suppress(RuntimeError):
|
|
350
375
|
core.stopStageSequence(zstage)
|
|
351
376
|
core.loadStageSequence(zstage, event.z_sequence)
|
|
352
|
-
if
|
|
353
|
-
|
|
377
|
+
if event.slm_sequence:
|
|
378
|
+
slm = core.getSLMDevice()
|
|
379
|
+
with suppress(RuntimeError):
|
|
380
|
+
core.stopSLMSequence(slm)
|
|
381
|
+
core.loadSLMSequence(slm, event.slm_sequence) # type: ignore[arg-type]
|
|
382
|
+
if event.property_sequences:
|
|
383
|
+
for (dev, prop), value_sequence in event.property_sequences.items():
|
|
354
384
|
with suppress(RuntimeError):
|
|
355
385
|
core.stopPropertySequence(dev, prop)
|
|
356
386
|
core.loadPropertySequence(dev, prop, value_sequence)
|
|
357
387
|
|
|
358
|
-
#
|
|
388
|
+
# set all static properties, these won't change over the course of the sequence.
|
|
389
|
+
if event.properties:
|
|
390
|
+
for dev, prop, value in event.properties:
|
|
391
|
+
core.setProperty(dev, prop, value)
|
|
392
|
+
|
|
393
|
+
def setup_sequenced_event(self, event: SequencedEvent) -> None:
|
|
394
|
+
"""Setup hardware for a sequenced (triggered) event.
|
|
395
|
+
|
|
396
|
+
This method is not part of the PMDAEngine protocol (it is called by
|
|
397
|
+
`setup_event`, which *is* part of the protocol), but it is made public
|
|
398
|
+
in case a user wants to subclass this engine and override this method.
|
|
399
|
+
"""
|
|
400
|
+
core = self._mmc
|
|
401
|
+
|
|
402
|
+
self._load_sequenced_event(event)
|
|
403
|
+
|
|
404
|
+
# this is probably not necessary. loadSequenceEvent will have already
|
|
405
|
+
# set all the config properties individually/manually. However, without
|
|
406
|
+
# the call below, we won't be able to query `core.getCurrentConfig()`
|
|
407
|
+
# not sure that's necessary; and this is here for tests to pass for now,
|
|
408
|
+
# but this could be removed.
|
|
409
|
+
if event.channel is not None:
|
|
410
|
+
try:
|
|
411
|
+
core.setConfig(event.channel.group, event.channel.config)
|
|
412
|
+
except Exception as e:
|
|
413
|
+
logger.warning("Failed to set channel. %s", e)
|
|
414
|
+
|
|
415
|
+
if event.slm_image:
|
|
416
|
+
self._set_event_slm_image(event)
|
|
359
417
|
|
|
360
418
|
# preparing a Sequence while another is running is dangerous.
|
|
361
419
|
if core.isSequenceRunning():
|
|
362
420
|
self._await_sequence_acquisition()
|
|
363
|
-
core.prepareSequenceAcquisition(
|
|
421
|
+
core.prepareSequenceAcquisition(core.getCameraDevice())
|
|
364
422
|
|
|
365
423
|
# start sequences or set non-sequenced values
|
|
366
424
|
if event.x_sequence:
|
|
367
|
-
core.startXYStageSequence(
|
|
425
|
+
core.startXYStageSequence(core.getXYStageDevice())
|
|
368
426
|
elif event.x_pos is not None or event.y_pos is not None:
|
|
369
427
|
self._set_event_position(event)
|
|
370
428
|
|
|
371
429
|
if event.z_sequence:
|
|
372
|
-
core.startStageSequence(
|
|
430
|
+
core.startStageSequence(core.getFocusDevice())
|
|
373
431
|
elif event.z_pos is not None:
|
|
374
432
|
self._set_event_z(event)
|
|
375
433
|
|
|
376
434
|
if event.exposure_sequence:
|
|
377
|
-
core.startExposureSequence(
|
|
435
|
+
core.startExposureSequence(core.getCameraDevice())
|
|
378
436
|
elif event.exposure is not None:
|
|
379
437
|
core.setExposure(event.exposure)
|
|
380
438
|
|
|
381
|
-
if
|
|
382
|
-
for dev, prop in
|
|
439
|
+
if event.property_sequences:
|
|
440
|
+
for dev, prop in event.property_sequences:
|
|
383
441
|
core.startPropertySequence(dev, prop)
|
|
384
|
-
elif event.channel is not None:
|
|
385
|
-
core.setConfig(event.channel.group, event.channel.config)
|
|
386
442
|
|
|
387
443
|
def _await_sequence_acquisition(
|
|
388
444
|
self, timeout: float = 5.0, poll_interval: float = 0.2
|
|
@@ -417,6 +473,9 @@ class MDAEngine(PMDAEngine):
|
|
|
417
473
|
t0 = event.metadata.get("runner_t0") or time.perf_counter()
|
|
418
474
|
event_t0_ms = (time.perf_counter() - t0) * 1000
|
|
419
475
|
|
|
476
|
+
if event.slm_image is not None:
|
|
477
|
+
self._exec_event_slm_image(event.slm_image)
|
|
478
|
+
|
|
420
479
|
# Start sequence
|
|
421
480
|
# Note that the overload of startSequenceAcquisition that takes a camera
|
|
422
481
|
# label does NOT automatically initialize a circular buffer. So if this call
|
|
@@ -491,6 +550,7 @@ class MDAEngine(PMDAEngine):
|
|
|
491
550
|
prop_values=(),
|
|
492
551
|
runner_time_ms=event_t0 + seq_time,
|
|
493
552
|
camera_device=camera_device,
|
|
553
|
+
include_position=self._include_frame_position_metadata is True,
|
|
494
554
|
)
|
|
495
555
|
meta["hardware_triggered"] = True
|
|
496
556
|
meta["images_remaining_in_buffer"] = remaining
|
|
@@ -547,6 +607,55 @@ class MDAEngine(PMDAEngine):
|
|
|
547
607
|
correction = self._z_correction.setdefault(p_idx, 0.0)
|
|
548
608
|
self._mmc.setZPosition(cast("float", event.z_pos) + correction)
|
|
549
609
|
|
|
610
|
+
def _set_event_slm_image(self, event: MDAEvent) -> None:
|
|
611
|
+
if not event.slm_image:
|
|
612
|
+
return
|
|
613
|
+
try:
|
|
614
|
+
# Get the SLM device
|
|
615
|
+
if not (
|
|
616
|
+
slm_device := event.slm_image.device or self._mmc.getSLMDevice()
|
|
617
|
+
): # pragma: no cover
|
|
618
|
+
raise ValueError("No SLM device found or specified.")
|
|
619
|
+
|
|
620
|
+
# cast to numpy array
|
|
621
|
+
slm_array = np.asarray(event.slm_image)
|
|
622
|
+
# if it's a single value, we can just set all pixels to that value
|
|
623
|
+
if slm_array.ndim == 0:
|
|
624
|
+
value = slm_array.item()
|
|
625
|
+
if isinstance(value, bool):
|
|
626
|
+
dev_name = self._mmc.getDeviceName(slm_device)
|
|
627
|
+
on_value = _SLM_DEVICES_PIXEL_ON_VALUES.get(dev_name, 1)
|
|
628
|
+
value = on_value if value else 0
|
|
629
|
+
self._mmc.setSLMPixelsTo(slm_device, int(value))
|
|
630
|
+
elif slm_array.size == 3:
|
|
631
|
+
# if it's a 3-valued array, we assume it's RGB
|
|
632
|
+
r, g, b = slm_array.astype(int)
|
|
633
|
+
self._mmc.setSLMPixelsTo(slm_device, r, g, b)
|
|
634
|
+
elif slm_array.ndim in (2, 3):
|
|
635
|
+
# if it's a 2D/3D array, we assume it's an image
|
|
636
|
+
# where 3D is RGB with shape (h, w, 3)
|
|
637
|
+
if slm_array.ndim == 3 and slm_array.shape[2] != 3:
|
|
638
|
+
raise ValueError( # pragma: no cover
|
|
639
|
+
"SLM image must be 2D or 3D with 3 channels (RGB)."
|
|
640
|
+
)
|
|
641
|
+
# convert boolean on/off values to pixel values
|
|
642
|
+
if slm_array.dtype == bool:
|
|
643
|
+
dev_name = self._mmc.getDeviceName(slm_device)
|
|
644
|
+
on_value = _SLM_DEVICES_PIXEL_ON_VALUES.get(dev_name, 1)
|
|
645
|
+
slm_array = np.where(slm_array, on_value, 0).astype(np.uint8)
|
|
646
|
+
self._mmc.setSLMImage(slm_device, slm_array)
|
|
647
|
+
if event.slm_image.exposure:
|
|
648
|
+
self._mmc.setSLMExposure(slm_device, event.slm_image.exposure)
|
|
649
|
+
except Exception as e:
|
|
650
|
+
logger.warning("Failed to set SLM Image: %s", e)
|
|
651
|
+
|
|
652
|
+
def _exec_event_slm_image(self, img: useq.SLMImage) -> None:
|
|
653
|
+
if slm_device := (img.device or self._mmc.getSLMDevice()):
|
|
654
|
+
try:
|
|
655
|
+
self._mmc.displaySLMImage(slm_device)
|
|
656
|
+
except Exception as e:
|
|
657
|
+
logger.warning("Failed to set SLM Image: %s", e)
|
|
658
|
+
|
|
550
659
|
def _update_config_device_props(self) -> None:
|
|
551
660
|
# store devices/props that make up each config group for faster lookup
|
|
552
661
|
self._config_device_props.clear()
|
pymmcore_plus/mda/_runner.py
CHANGED
|
@@ -274,7 +274,14 @@ class MDARunner:
|
|
|
274
274
|
def _run(self, engine: PMDAEngine, events: Iterable[MDAEvent]) -> None:
|
|
275
275
|
"""Main execution of events, inside the try/except block of `run`."""
|
|
276
276
|
teardown_event = getattr(engine, "teardown_event", lambda e: None)
|
|
277
|
-
|
|
277
|
+
if isinstance(events, Iterator):
|
|
278
|
+
# if an iterator is passed directly, then we use that iterator
|
|
279
|
+
# instead of the engine's event_iterator. Directly passing an iterator
|
|
280
|
+
# is an advanced use case, (for example, `iter(Queue(), None)` for event-
|
|
281
|
+
# driven acquisition) and we don't want the engine to interfere with it.
|
|
282
|
+
event_iterator = iter
|
|
283
|
+
else:
|
|
284
|
+
event_iterator = getattr(engine, "event_iterator", iter)
|
|
278
285
|
_events: Iterator[MDAEvent] = event_iterator(events)
|
|
279
286
|
self._reset_event_timer()
|
|
280
287
|
self._sequence_t0 = self._t0
|
|
@@ -8,12 +8,12 @@ from ._protocol import PMDASignaler
|
|
|
8
8
|
from ._psygnal import MDASignaler
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
|
-
from ._qsignals import QMDASignaler # noqa:
|
|
11
|
+
from ._qsignals import QMDASignaler # noqa: TC004
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
__all__ = [
|
|
15
|
-
"PMDASignaler",
|
|
16
15
|
"MDASignaler",
|
|
16
|
+
"PMDASignaler",
|
|
17
17
|
"QMDASignaler",
|
|
18
18
|
"_get_auto_MDA_callback_class",
|
|
19
19
|
]
|
|
@@ -134,8 +134,8 @@ class OMEZarrWriter(_5DWriterBase["zarr.Array"]):
|
|
|
134
134
|
|
|
135
135
|
# if we don't check this here, we'll get an error when creating the first array
|
|
136
136
|
if (
|
|
137
|
-
not overwrite and any(self._group.arrays())
|
|
138
|
-
): # pragma: no cover
|
|
137
|
+
not overwrite and any(self._group.arrays())
|
|
138
|
+
) or self._group.attrs: # pragma: no cover
|
|
139
139
|
path = self._group.store.path if hasattr(self._group.store, "path") else ""
|
|
140
140
|
raise ValueError(
|
|
141
141
|
f"There is already data in {path!r}. Use 'overwrite=True' to overwrite."
|