pymmcore-plus 0.9.3__py3-none-any.whl → 0.13.0__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 +7 -4
- pymmcore_plus/_benchmark.py +203 -0
- pymmcore_plus/_build.py +6 -1
- pymmcore_plus/_cli.py +131 -31
- pymmcore_plus/_logger.py +19 -10
- pymmcore_plus/_pymmcore.py +12 -0
- pymmcore_plus/_util.py +139 -32
- pymmcore_plus/core/__init__.py +5 -0
- pymmcore_plus/core/_config.py +6 -4
- pymmcore_plus/core/_config_group.py +4 -3
- pymmcore_plus/core/_constants.py +135 -10
- pymmcore_plus/core/_device.py +4 -4
- pymmcore_plus/core/_metadata.py +3 -3
- pymmcore_plus/core/_mmcore_plus.py +254 -170
- pymmcore_plus/core/_property.py +6 -6
- pymmcore_plus/core/_sequencing.py +370 -233
- pymmcore_plus/core/events/__init__.py +6 -6
- pymmcore_plus/core/events/_device_signal_view.py +8 -6
- pymmcore_plus/core/events/_norm_slot.py +2 -4
- pymmcore_plus/core/events/_prop_event_mixin.py +7 -4
- pymmcore_plus/core/events/_protocol.py +5 -2
- pymmcore_plus/core/events/_psygnal.py +2 -2
- 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 +16 -11
- pymmcore_plus/mda/__init__.py +1 -1
- pymmcore_plus/mda/_engine.py +320 -148
- pymmcore_plus/mda/_protocol.py +6 -4
- pymmcore_plus/mda/_runner.py +62 -51
- pymmcore_plus/mda/_thread_relay.py +5 -3
- pymmcore_plus/mda/events/__init__.py +2 -2
- pymmcore_plus/mda/events/_protocol.py +10 -2
- pymmcore_plus/mda/events/_psygnal.py +2 -2
- pymmcore_plus/mda/handlers/_5d_writer_base.py +106 -15
- pymmcore_plus/mda/handlers/__init__.py +7 -1
- pymmcore_plus/mda/handlers/_img_sequence_writer.py +11 -6
- pymmcore_plus/mda/handlers/_ome_tiff_writer.py +8 -4
- pymmcore_plus/mda/handlers/_ome_zarr_writer.py +82 -9
- pymmcore_plus/mda/handlers/_tensorstore_handler.py +374 -0
- pymmcore_plus/mda/handlers/_util.py +1 -1
- pymmcore_plus/metadata/__init__.py +36 -0
- pymmcore_plus/metadata/functions.py +353 -0
- pymmcore_plus/metadata/schema.py +472 -0
- pymmcore_plus/metadata/serialize.py +120 -0
- pymmcore_plus/mocks.py +51 -0
- pymmcore_plus/model/_config_file.py +5 -6
- pymmcore_plus/model/_config_group.py +29 -2
- pymmcore_plus/model/_core_device.py +12 -1
- pymmcore_plus/model/_core_link.py +2 -1
- pymmcore_plus/model/_device.py +39 -8
- pymmcore_plus/model/_microscope.py +39 -3
- pymmcore_plus/model/_pixel_size_config.py +27 -4
- pymmcore_plus/model/_property.py +13 -3
- pymmcore_plus/seq_tester.py +1 -1
- {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/METADATA +22 -12
- pymmcore_plus-0.13.0.dist-info/RECORD +71 -0
- {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/WHEEL +1 -1
- pymmcore_plus/core/_state.py +0 -244
- pymmcore_plus-0.9.3.dist-info/RECORD +0 -55
- {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/licenses/LICENSE +0 -0
pymmcore_plus/mda/_engine.py
CHANGED
|
@@ -1,54 +1,50 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
-
from
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
Any,
|
|
8
|
-
Iterable,
|
|
9
|
-
Iterator,
|
|
10
|
-
Mapping,
|
|
11
|
-
NamedTuple,
|
|
12
|
-
cast,
|
|
13
|
-
)
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from itertools import product
|
|
6
|
+
from typing import TYPE_CHECKING, Literal, NamedTuple, cast
|
|
14
7
|
|
|
8
|
+
import numpy as np
|
|
9
|
+
import useq
|
|
15
10
|
from useq import HardwareAutofocus, MDAEvent, MDASequence
|
|
16
11
|
|
|
17
12
|
from pymmcore_plus._logger import logger
|
|
18
13
|
from pymmcore_plus._util import retry
|
|
19
|
-
from pymmcore_plus.core._constants import
|
|
20
|
-
from pymmcore_plus.core._sequencing import SequencedEvent
|
|
14
|
+
from pymmcore_plus.core._constants import Keyword
|
|
15
|
+
from pymmcore_plus.core._sequencing import SequencedEvent, iter_sequenced_events
|
|
16
|
+
from pymmcore_plus.metadata import (
|
|
17
|
+
FrameMetaV1,
|
|
18
|
+
PropertyValue,
|
|
19
|
+
SummaryMetaV1,
|
|
20
|
+
frame_metadata,
|
|
21
|
+
summary_metadata,
|
|
22
|
+
)
|
|
21
23
|
|
|
22
24
|
from ._protocol import PMDAEngine
|
|
23
25
|
|
|
24
26
|
if TYPE_CHECKING:
|
|
25
|
-
from
|
|
27
|
+
from collections.abc import Iterable, Iterator, Sequence
|
|
28
|
+
from typing import TypeAlias
|
|
26
29
|
|
|
27
30
|
from numpy.typing import NDArray
|
|
28
31
|
|
|
29
|
-
from pymmcore_plus.core import CMMCorePlus
|
|
32
|
+
from pymmcore_plus.core import CMMCorePlus
|
|
30
33
|
|
|
31
34
|
from ._protocol import PImagePayload
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"Core-Galvo": str,
|
|
46
|
-
"Core-ImageProcessor": str,
|
|
47
|
-
"Core-SLM": str,
|
|
48
|
-
"Core-Shutter": str,
|
|
49
|
-
"AffineTransform": str,
|
|
50
|
-
},
|
|
51
|
-
)
|
|
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
|
+
}
|
|
52
48
|
|
|
53
49
|
|
|
54
50
|
class MDAEngine(PMDAEngine):
|
|
@@ -57,6 +53,9 @@ class MDAEngine(PMDAEngine):
|
|
|
57
53
|
This implements the [`PMDAEngine`][pymmcore_plus.mda.PMDAEngine] protocol, and
|
|
58
54
|
uses a [`CMMCorePlus`][pymmcore_plus.CMMCorePlus] instance to control the hardware.
|
|
59
55
|
|
|
56
|
+
It may be subclassed to provide custom behavior, or to override specific methods.
|
|
57
|
+
<https://pymmcore-plus.github.io/pymmcore-plus/guides/custom_engine/>
|
|
58
|
+
|
|
60
59
|
Attributes
|
|
61
60
|
----------
|
|
62
61
|
mmcore: CMMCorePlus
|
|
@@ -66,14 +65,17 @@ class MDAEngine(PMDAEngine):
|
|
|
66
65
|
attempt to combine MDAEvents into a single `SequencedEvent` if
|
|
67
66
|
[`core.canSequenceEvents()`][pymmcore_plus.CMMCorePlus.canSequenceEvents]
|
|
68
67
|
reports that the events can be sequenced. This can be set after instantiation.
|
|
69
|
-
By default, this is `
|
|
70
|
-
|
|
71
|
-
set to `True` to improve performance.
|
|
68
|
+
By default, this is `True`, however in various testing and demo scenarios, you
|
|
69
|
+
may wish to set it to `False` in order to avoid unexpected behavior.
|
|
72
70
|
"""
|
|
73
71
|
|
|
74
|
-
def __init__(self, mmc: CMMCorePlus, use_hardware_sequencing: bool =
|
|
72
|
+
def __init__(self, mmc: CMMCorePlus, use_hardware_sequencing: bool = True) -> None:
|
|
75
73
|
self._mmc = mmc
|
|
76
|
-
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"
|
|
77
79
|
|
|
78
80
|
# used to check if the hardware autofocus is engaged when the sequence begins.
|
|
79
81
|
# if it is, we will re-engage it after the autofocus action (if successful).
|
|
@@ -90,6 +92,26 @@ class MDAEngine(PMDAEngine):
|
|
|
90
92
|
# Note: getAutoShutter() is True when no config is loaded at all
|
|
91
93
|
self._autoshutter_was_set: bool = self._mmc.getAutoShutter()
|
|
92
94
|
|
|
95
|
+
# -----
|
|
96
|
+
# The following values are stored during setup_sequence simply to speed up
|
|
97
|
+
# retrieval of metadata during each frame.
|
|
98
|
+
# sequence of (device, property) of all properties used in any of the presets
|
|
99
|
+
# in the channel group.
|
|
100
|
+
self._config_device_props: dict[str, Sequence[tuple[str, str]]] = {}
|
|
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
|
+
|
|
93
115
|
@property
|
|
94
116
|
def mmcore(self) -> CMMCorePlus:
|
|
95
117
|
"""The `CMMCorePlus` instance to use for hardware control."""
|
|
@@ -97,7 +119,7 @@ class MDAEngine(PMDAEngine):
|
|
|
97
119
|
|
|
98
120
|
# ===================== Protocol Implementation =====================
|
|
99
121
|
|
|
100
|
-
def setup_sequence(self, sequence: MDASequence) ->
|
|
122
|
+
def setup_sequence(self, sequence: MDASequence) -> SummaryMetaV1 | None:
|
|
101
123
|
"""Setup the hardware for the entire sequence."""
|
|
102
124
|
# clear z_correction for new sequence
|
|
103
125
|
self._z_correction.clear()
|
|
@@ -107,6 +129,7 @@ class MDAEngine(PMDAEngine):
|
|
|
107
129
|
|
|
108
130
|
self._mmc = CMMCorePlus.instance()
|
|
109
131
|
|
|
132
|
+
self._update_config_device_props()
|
|
110
133
|
# get if the autofocus is engaged at the start of the sequence
|
|
111
134
|
self._af_was_engaged = self._mmc.isContinuousFocusLocked()
|
|
112
135
|
|
|
@@ -114,30 +137,10 @@ class MDAEngine(PMDAEngine):
|
|
|
114
137
|
self._update_grid_fov_sizes(px_size, sequence)
|
|
115
138
|
|
|
116
139
|
self._autoshutter_was_set = self._mmc.getAutoShutter()
|
|
117
|
-
return self.get_summary_metadata()
|
|
140
|
+
return self.get_summary_metadata(mda_sequence=sequence)
|
|
118
141
|
|
|
119
|
-
def get_summary_metadata(self) ->
|
|
120
|
-
|
|
121
|
-
pt = PixelType.for_bytes(
|
|
122
|
-
self._mmc.getBytesPerPixel(), self._mmc.getNumberOfComponents()
|
|
123
|
-
)
|
|
124
|
-
affine = self._mmc.getPixelSizeAffine(True) # true == cached
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
"DateAndTime": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"),
|
|
128
|
-
"PixelType": str(pt),
|
|
129
|
-
"PixelSize_um": self._mmc.getPixelSizeUm(),
|
|
130
|
-
"PixelSizeAffine": ";".join(str(x) for x in affine),
|
|
131
|
-
"Core-XYStage": self._mmc.getXYStageDevice(),
|
|
132
|
-
"Core-Focus": self._mmc.getFocusDevice(),
|
|
133
|
-
"Core-Autofocus": self._mmc.getAutoFocusDevice(),
|
|
134
|
-
"Core-Camera": self._mmc.getCameraDevice(),
|
|
135
|
-
"Core-Galvo": self._mmc.getGalvoDevice(),
|
|
136
|
-
"Core-ImageProcessor": self._mmc.getImageProcessorDevice(),
|
|
137
|
-
"Core-SLM": self._mmc.getSLMDevice(),
|
|
138
|
-
"Core-Shutter": self._mmc.getShutterDevice(),
|
|
139
|
-
"AffineTransform": "Undefined",
|
|
140
|
-
}
|
|
142
|
+
def get_summary_metadata(self, mda_sequence: MDASequence | None) -> SummaryMetaV1:
|
|
143
|
+
return summary_metadata(self._mmc, mda_sequence=mda_sequence)
|
|
141
144
|
|
|
142
145
|
def _update_grid_fov_sizes(self, px_size: float, sequence: MDASequence) -> None:
|
|
143
146
|
*_, x_size, y_size = self._mmc.getROI()
|
|
@@ -175,7 +178,7 @@ class MDAEngine(PMDAEngine):
|
|
|
175
178
|
# skip if no autofocus device is found
|
|
176
179
|
if not self._mmc.getAutoFocusDevice():
|
|
177
180
|
logger.warning("No autofocus device found. Cannot execute autofocus.")
|
|
178
|
-
return
|
|
181
|
+
return
|
|
179
182
|
|
|
180
183
|
try:
|
|
181
184
|
# execute hardware autofocus
|
|
@@ -190,7 +193,7 @@ class MDAEngine(PMDAEngine):
|
|
|
190
193
|
self._z_correction[p_idx] = new_correction + self._z_correction.get(
|
|
191
194
|
p_idx, 0.0
|
|
192
195
|
)
|
|
193
|
-
return
|
|
196
|
+
return
|
|
194
197
|
|
|
195
198
|
# if the autofocus was engaged at the start of the sequence AND autofocus action
|
|
196
199
|
# did not fail, re-engage it. NOTE: we need to do that AFTER the runner calls
|
|
@@ -214,21 +217,7 @@ class MDAEngine(PMDAEngine):
|
|
|
214
217
|
yield from events
|
|
215
218
|
return
|
|
216
219
|
|
|
217
|
-
|
|
218
|
-
for event in events:
|
|
219
|
-
# if the sequence is empty or the current event can be sequenced with the
|
|
220
|
-
# previous event, add it to the sequence
|
|
221
|
-
if not seq or self._mmc.canSequenceEvents(seq[-1], event, len(seq)):
|
|
222
|
-
seq.append(event)
|
|
223
|
-
else:
|
|
224
|
-
# otherwise, yield a SequencedEvent if the sequence has accumulated
|
|
225
|
-
# more than one event, otherwise yield the single event
|
|
226
|
-
yield seq[0] if len(seq) == 1 else SequencedEvent.create(seq)
|
|
227
|
-
# add this current event and start a new sequence
|
|
228
|
-
seq = [event]
|
|
229
|
-
# yield any remaining events
|
|
230
|
-
if seq:
|
|
231
|
-
yield seq[0] if len(seq) == 1 else SequencedEvent.create(seq)
|
|
220
|
+
yield from iter_sequenced_events(self._mmc, events)
|
|
232
221
|
|
|
233
222
|
# ===================== Regular Events =====================
|
|
234
223
|
|
|
@@ -246,9 +235,11 @@ class MDAEngine(PMDAEngine):
|
|
|
246
235
|
self._set_event_position(event)
|
|
247
236
|
if event.z_pos is not None:
|
|
248
237
|
self._set_event_z(event)
|
|
249
|
-
|
|
238
|
+
if event.slm_image is not None:
|
|
239
|
+
self._set_event_slm_image(event)
|
|
250
240
|
if event.channel is not None:
|
|
251
241
|
try:
|
|
242
|
+
# possible speedup by setting manually.
|
|
252
243
|
self._mmc.setConfig(event.channel.group, event.channel.config)
|
|
253
244
|
except Exception as e:
|
|
254
245
|
logger.warning("Failed to set channel. %s", e)
|
|
@@ -278,49 +269,77 @@ class MDAEngine(PMDAEngine):
|
|
|
278
269
|
`exec_event`, which *is* part of the protocol), but it is made public
|
|
279
270
|
in case a user wants to subclass this engine and override this method.
|
|
280
271
|
"""
|
|
272
|
+
if event.slm_image is not None:
|
|
273
|
+
self._exec_event_slm_image(event.slm_image)
|
|
274
|
+
|
|
281
275
|
try:
|
|
282
276
|
self._mmc.snapImage()
|
|
277
|
+
# taking event time after snapImage includes exposure time
|
|
278
|
+
# not sure that's what we want, but it's currently consistent with the
|
|
279
|
+
# timing of the sequenced event runner (where Elapsed_Time_ms is taken after
|
|
280
|
+
# the image is acquired, not before the exposure starts)
|
|
281
|
+
t0 = event.metadata.get("runner_t0") or time.perf_counter()
|
|
282
|
+
event_time_ms = (time.perf_counter() - t0) * 1000
|
|
283
283
|
except Exception as e:
|
|
284
284
|
logger.warning("Failed to snap image. %s", e)
|
|
285
|
-
return
|
|
285
|
+
return
|
|
286
286
|
if not event.keep_shutter_open:
|
|
287
287
|
self._mmc.setShutterOpen(False)
|
|
288
|
-
yield ImagePayload(self._mmc.getImage(), event, self.get_frame_metadata())
|
|
289
|
-
|
|
290
|
-
def get_frame_metadata(
|
|
291
|
-
self, meta: Metadata | None = None, channel_index: int | None = None
|
|
292
|
-
) -> dict[str, Any]:
|
|
293
|
-
# TODO:
|
|
294
288
|
|
|
295
|
-
#
|
|
296
|
-
#
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
tags[f"{dev}-{label}"] = val
|
|
309
|
-
|
|
310
|
-
# these are added by AcqEngJ
|
|
311
|
-
# yyyy-MM-dd HH:mm:ss.mmmmmm # NOTE AcqEngJ omits microseconds
|
|
312
|
-
tags["Time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
289
|
+
# most cameras will only have a single channel
|
|
290
|
+
# but Multi-camera may have multiple, and we need to retrieve a buffer for each
|
|
291
|
+
for cam in range(self._mmc.getNumberOfCameraChannels()):
|
|
292
|
+
meta = self.get_frame_metadata(
|
|
293
|
+
event,
|
|
294
|
+
runner_time_ms=event_time_ms,
|
|
295
|
+
camera_device=self._mmc.getPhysicalCameraDevice(cam),
|
|
296
|
+
include_position=self._include_frame_position_metadata is not False,
|
|
297
|
+
)
|
|
298
|
+
# Note, the third element is actually a MutableMapping, but mypy doesn't
|
|
299
|
+
# see TypedDict as a subclass of MutableMapping yet.
|
|
300
|
+
# https://github.com/python/mypy/issues/4976
|
|
301
|
+
yield ImagePayload(self._mmc.getImage(cam), event, meta) # type: ignore[misc]
|
|
313
302
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
303
|
+
def get_frame_metadata(
|
|
304
|
+
self,
|
|
305
|
+
event: MDAEvent,
|
|
306
|
+
prop_values: tuple[PropertyValue, ...] | None = None,
|
|
307
|
+
runner_time_ms: float = 0.0,
|
|
308
|
+
include_position: bool = True,
|
|
309
|
+
camera_device: str | None = None,
|
|
310
|
+
) -> FrameMetaV1:
|
|
311
|
+
if prop_values is None and (ch := event.channel):
|
|
312
|
+
prop_values = self._get_current_props(ch.group)
|
|
313
|
+
else:
|
|
314
|
+
prop_values = ()
|
|
315
|
+
return frame_metadata(
|
|
316
|
+
self._mmc,
|
|
317
|
+
cached=True,
|
|
318
|
+
runner_time_ms=runner_time_ms,
|
|
319
|
+
camera_device=camera_device,
|
|
320
|
+
property_values=prop_values,
|
|
321
|
+
mda_event=event,
|
|
322
|
+
include_position=include_position,
|
|
323
|
+
)
|
|
317
324
|
|
|
318
325
|
def teardown_event(self, event: MDAEvent) -> None:
|
|
319
326
|
"""Teardown state of system (hardware, etc.) after `event`."""
|
|
320
327
|
# autoshutter was set at the beginning of the sequence, and this event
|
|
321
328
|
# doesn't want to leave the shutter open. Re-enable autoshutter.
|
|
329
|
+
core = self._mmc
|
|
322
330
|
if not event.keep_shutter_open and self._autoshutter_was_set:
|
|
323
|
-
|
|
331
|
+
core.setAutoShutter(True)
|
|
332
|
+
# FIXME: this may not be hitting as intended...
|
|
333
|
+
# https://github.com/pymmcore-plus/pymmcore-plus/pull/353#issuecomment-2159176491
|
|
334
|
+
if isinstance(event, SequencedEvent):
|
|
335
|
+
if event.exposure_sequence:
|
|
336
|
+
core.stopExposureSequence(self._mmc.getCameraDevice())
|
|
337
|
+
if event.x_sequence:
|
|
338
|
+
core.stopXYStageSequence(core.getXYStageDevice())
|
|
339
|
+
if event.z_sequence:
|
|
340
|
+
core.stopStageSequence(core.getFocusDevice())
|
|
341
|
+
for dev, prop in event.property_sequences:
|
|
342
|
+
core.stopPropertySequence(dev, prop)
|
|
324
343
|
|
|
325
344
|
def teardown_sequence(self, sequence: MDASequence) -> None:
|
|
326
345
|
"""Perform any teardown required after the sequence has been executed."""
|
|
@@ -328,63 +347,93 @@ class MDAEngine(PMDAEngine):
|
|
|
328
347
|
|
|
329
348
|
# ===================== Sequenced Events =====================
|
|
330
349
|
|
|
331
|
-
def
|
|
332
|
-
"""
|
|
350
|
+
def _load_sequenced_event(self, event: SequencedEvent) -> None:
|
|
351
|
+
"""Load a `SequencedEvent` into the core.
|
|
333
352
|
|
|
334
|
-
|
|
335
|
-
`
|
|
336
|
-
in case a user wants to subclass this engine and override this method.
|
|
353
|
+
`SequencedEvent` is a special pymmcore-plus specific subclass of
|
|
354
|
+
`useq.MDAEvent`.
|
|
337
355
|
"""
|
|
338
356
|
core = self._mmc
|
|
339
|
-
cam_device = self._mmc.getCameraDevice()
|
|
340
|
-
|
|
341
357
|
if event.exposure_sequence:
|
|
358
|
+
cam_device = core.getCameraDevice()
|
|
359
|
+
with suppress(RuntimeError):
|
|
360
|
+
core.stopExposureSequence(cam_device)
|
|
342
361
|
core.loadExposureSequence(cam_device, event.exposure_sequence)
|
|
343
362
|
if event.x_sequence: # y_sequence is implied and will be the same length
|
|
344
363
|
stage = core.getXYStageDevice()
|
|
364
|
+
with suppress(RuntimeError):
|
|
365
|
+
core.stopXYStageSequence(stage)
|
|
345
366
|
core.loadXYStageSequence(stage, event.x_sequence, event.y_sequence)
|
|
346
367
|
if event.z_sequence:
|
|
347
|
-
# these notes are from Nico Stuurman in AcqEngJ
|
|
348
|
-
# https://github.com/micro-manager/AcqEngJ/pull/108
|
|
349
|
-
# at least some zStages freak out (in this case, NIDAQ board) when you
|
|
350
|
-
# try to load a sequence while the sequence is still running. Nothing in
|
|
351
|
-
# the engine stops a stage sequence if all goes well.
|
|
352
|
-
# Stopping a sequence if it is not running hopefully will not harm anyone.
|
|
353
368
|
zstage = core.getFocusDevice()
|
|
354
|
-
|
|
369
|
+
with suppress(RuntimeError):
|
|
370
|
+
core.stopStageSequence(zstage)
|
|
355
371
|
core.loadStageSequence(zstage, event.z_sequence)
|
|
356
|
-
if
|
|
357
|
-
|
|
372
|
+
if event.slm_sequence:
|
|
373
|
+
slm = core.getSLMDevice()
|
|
374
|
+
with suppress(RuntimeError):
|
|
375
|
+
core.stopSLMSequence(slm)
|
|
376
|
+
core.loadSLMSequence(slm, event.slm_sequence) # type: ignore[arg-type]
|
|
377
|
+
if event.property_sequences:
|
|
378
|
+
for (dev, prop), value_sequence in event.property_sequences.items():
|
|
379
|
+
with suppress(RuntimeError):
|
|
380
|
+
core.stopPropertySequence(dev, prop)
|
|
358
381
|
core.loadPropertySequence(dev, prop, value_sequence)
|
|
359
382
|
|
|
360
|
-
#
|
|
383
|
+
# set all static properties, these won't change over the course of the sequence.
|
|
384
|
+
if event.properties:
|
|
385
|
+
for dev, prop, value in event.properties:
|
|
386
|
+
core.setProperty(dev, prop, value)
|
|
387
|
+
|
|
388
|
+
def setup_sequenced_event(self, event: SequencedEvent) -> None:
|
|
389
|
+
"""Setup hardware for a sequenced (triggered) event.
|
|
390
|
+
|
|
391
|
+
This method is not part of the PMDAEngine protocol (it is called by
|
|
392
|
+
`setup_event`, which *is* part of the protocol), but it is made public
|
|
393
|
+
in case a user wants to subclass this engine and override this method.
|
|
394
|
+
"""
|
|
395
|
+
core = self._mmc
|
|
396
|
+
|
|
397
|
+
self._load_sequenced_event(event)
|
|
398
|
+
|
|
399
|
+
# this is probably not necessary. loadSequenceEvent will have already
|
|
400
|
+
# set all the config properties individually/manually. However, without
|
|
401
|
+
# the call below, we won't be able to query `core.getCurrentConfig()`
|
|
402
|
+
# not sure that's necessary; and this is here for tests to pass for now,
|
|
403
|
+
# but this could be removed.
|
|
404
|
+
if event.channel is not None:
|
|
405
|
+
try:
|
|
406
|
+
core.setConfig(event.channel.group, event.channel.config)
|
|
407
|
+
except Exception as e:
|
|
408
|
+
logger.warning("Failed to set channel. %s", e)
|
|
409
|
+
|
|
410
|
+
if event.slm_image:
|
|
411
|
+
self._set_event_slm_image(event)
|
|
361
412
|
|
|
362
413
|
# preparing a Sequence while another is running is dangerous.
|
|
363
414
|
if core.isSequenceRunning():
|
|
364
415
|
self._await_sequence_acquisition()
|
|
365
|
-
core.prepareSequenceAcquisition(
|
|
416
|
+
core.prepareSequenceAcquisition(core.getCameraDevice())
|
|
366
417
|
|
|
367
418
|
# start sequences or set non-sequenced values
|
|
368
419
|
if event.x_sequence:
|
|
369
|
-
core.startXYStageSequence(
|
|
420
|
+
core.startXYStageSequence(core.getXYStageDevice())
|
|
370
421
|
elif event.x_pos is not None or event.y_pos is not None:
|
|
371
422
|
self._set_event_position(event)
|
|
372
423
|
|
|
373
424
|
if event.z_sequence:
|
|
374
|
-
core.startStageSequence(
|
|
425
|
+
core.startStageSequence(core.getFocusDevice())
|
|
375
426
|
elif event.z_pos is not None:
|
|
376
427
|
self._set_event_z(event)
|
|
377
428
|
|
|
378
429
|
if event.exposure_sequence:
|
|
379
|
-
core.startExposureSequence(
|
|
430
|
+
core.startExposureSequence(core.getCameraDevice())
|
|
380
431
|
elif event.exposure is not None:
|
|
381
432
|
core.setExposure(event.exposure)
|
|
382
433
|
|
|
383
|
-
if
|
|
384
|
-
for dev, prop in
|
|
434
|
+
if event.property_sequences:
|
|
435
|
+
for dev, prop in event.property_sequences:
|
|
385
436
|
core.startPropertySequence(dev, prop)
|
|
386
|
-
elif event.channel is not None:
|
|
387
|
-
core.setConfig(event.channel.group, event.channel.config)
|
|
388
437
|
|
|
389
438
|
def _await_sequence_acquisition(
|
|
390
439
|
self, timeout: float = 5.0, poll_interval: float = 0.2
|
|
@@ -414,9 +463,14 @@ class MDAEngine(PMDAEngine):
|
|
|
414
463
|
`exec_event`, which *is* part of the protocol), but it is made public
|
|
415
464
|
in case a user wants to subclass this engine and override this method.
|
|
416
465
|
"""
|
|
417
|
-
# TODO: add support for multiple camera devices
|
|
418
466
|
n_events = len(event.events)
|
|
419
467
|
|
|
468
|
+
t0 = event.metadata.get("runner_t0") or time.perf_counter()
|
|
469
|
+
event_t0_ms = (time.perf_counter() - t0) * 1000
|
|
470
|
+
|
|
471
|
+
if event.slm_image is not None:
|
|
472
|
+
self._exec_event_slm_image(event.slm_image)
|
|
473
|
+
|
|
420
474
|
# Start sequence
|
|
421
475
|
# Note that the overload of startSequenceAcquisition that takes a camera
|
|
422
476
|
# label does NOT automatically initialize a circular buffer. So if this call
|
|
@@ -426,15 +480,17 @@ class MDAEngine(PMDAEngine):
|
|
|
426
480
|
0, # intervalMS # TODO: add support for this
|
|
427
481
|
True, # stopOnOverflow
|
|
428
482
|
)
|
|
429
|
-
|
|
430
483
|
self.post_sequence_started(event)
|
|
431
484
|
|
|
485
|
+
n_channels = self._mmc.getNumberOfCameraChannels()
|
|
432
486
|
count = 0
|
|
433
|
-
iter_events =
|
|
487
|
+
iter_events = product(event.events, range(n_channels))
|
|
434
488
|
# block until the sequence is done, popping images in the meantime
|
|
435
489
|
while self._mmc.isSequenceRunning():
|
|
436
|
-
if self._mmc.getRemainingImageCount():
|
|
437
|
-
yield self.
|
|
490
|
+
if remaining := self._mmc.getRemainingImageCount():
|
|
491
|
+
yield self._next_seqimg_payload(
|
|
492
|
+
*next(iter_events), remaining=remaining - 1, event_t0=event_t0_ms
|
|
493
|
+
)
|
|
438
494
|
count += 1
|
|
439
495
|
else:
|
|
440
496
|
time.sleep(0.001)
|
|
@@ -442,23 +498,61 @@ class MDAEngine(PMDAEngine):
|
|
|
442
498
|
if self._mmc.isBufferOverflowed(): # pragma: no cover
|
|
443
499
|
raise MemoryError("Buffer overflowed")
|
|
444
500
|
|
|
445
|
-
while self._mmc.getRemainingImageCount():
|
|
446
|
-
yield self.
|
|
501
|
+
while remaining := self._mmc.getRemainingImageCount():
|
|
502
|
+
yield self._next_seqimg_payload(
|
|
503
|
+
*next(iter_events), remaining=remaining - 1, event_t0=event_t0_ms
|
|
504
|
+
)
|
|
447
505
|
count += 1
|
|
448
506
|
|
|
449
|
-
|
|
507
|
+
# necessary?
|
|
508
|
+
expected_images = n_events * n_channels
|
|
509
|
+
if count != expected_images:
|
|
450
510
|
logger.warning(
|
|
451
511
|
"Unexpected number of images returned from sequence. "
|
|
452
512
|
"Expected %s, got %s",
|
|
453
|
-
|
|
513
|
+
expected_images,
|
|
454
514
|
count,
|
|
455
515
|
)
|
|
456
516
|
|
|
457
|
-
def
|
|
517
|
+
def _next_seqimg_payload(
|
|
518
|
+
self,
|
|
519
|
+
event: MDAEvent,
|
|
520
|
+
channel: int = 0,
|
|
521
|
+
*,
|
|
522
|
+
event_t0: float = 0.0,
|
|
523
|
+
remaining: int = 0,
|
|
524
|
+
) -> PImagePayload:
|
|
458
525
|
"""Grab next image from the circular buffer and return it as an ImagePayload."""
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
526
|
+
_slice = 0 # ?
|
|
527
|
+
img, mm_meta = self._mmc.popNextImageAndMD(channel, _slice)
|
|
528
|
+
try:
|
|
529
|
+
seq_time = float(mm_meta.get(Keyword.Elapsed_Time_ms))
|
|
530
|
+
except Exception:
|
|
531
|
+
seq_time = 0.0
|
|
532
|
+
try:
|
|
533
|
+
# note, when present in circular buffer meta, this key is called "Camera".
|
|
534
|
+
# It's NOT actually Keyword.CoreCamera (but it's the same value)
|
|
535
|
+
# it is hardcoded in various places in mmCoreAndDevices, see:
|
|
536
|
+
# see: https://github.com/micro-manager/mmCoreAndDevices/pull/468
|
|
537
|
+
camera_device = mm_meta.GetSingleTag("Camera").GetValue()
|
|
538
|
+
except Exception:
|
|
539
|
+
camera_device = self._mmc.getPhysicalCameraDevice(channel)
|
|
540
|
+
|
|
541
|
+
# TODO: determine whether we want to try to populate changing property values
|
|
542
|
+
# during the course of a triggered sequence
|
|
543
|
+
meta = self.get_frame_metadata(
|
|
544
|
+
event,
|
|
545
|
+
prop_values=(),
|
|
546
|
+
runner_time_ms=event_t0 + seq_time,
|
|
547
|
+
camera_device=camera_device,
|
|
548
|
+
include_position=self._include_frame_position_metadata is True,
|
|
549
|
+
)
|
|
550
|
+
meta["hardware_triggered"] = True
|
|
551
|
+
meta["images_remaining_in_buffer"] = remaining
|
|
552
|
+
meta["camera_metadata"] = dict(mm_meta)
|
|
553
|
+
|
|
554
|
+
# https://github.com/python/mypy/issues/4976
|
|
555
|
+
return ImagePayload(img, event, meta) # type: ignore[return-value]
|
|
462
556
|
|
|
463
557
|
# ===================== EXTRA =====================
|
|
464
558
|
|
|
@@ -508,8 +602,86 @@ class MDAEngine(PMDAEngine):
|
|
|
508
602
|
correction = self._z_correction.setdefault(p_idx, 0.0)
|
|
509
603
|
self._mmc.setZPosition(cast("float", event.z_pos) + correction)
|
|
510
604
|
|
|
605
|
+
def _set_event_slm_image(self, event: MDAEvent) -> None:
|
|
606
|
+
if not event.slm_image:
|
|
607
|
+
return
|
|
608
|
+
try:
|
|
609
|
+
# Get the SLM device
|
|
610
|
+
if not (
|
|
611
|
+
slm_device := event.slm_image.device or self._mmc.getSLMDevice()
|
|
612
|
+
): # pragma: no cover
|
|
613
|
+
raise ValueError("No SLM device found or specified.")
|
|
614
|
+
|
|
615
|
+
# cast to numpy array
|
|
616
|
+
slm_array = np.asarray(event.slm_image)
|
|
617
|
+
# if it's a single value, we can just set all pixels to that value
|
|
618
|
+
if slm_array.ndim == 0:
|
|
619
|
+
value = slm_array.item()
|
|
620
|
+
if isinstance(value, bool):
|
|
621
|
+
dev_name = self._mmc.getDeviceName(slm_device)
|
|
622
|
+
on_value = _SLM_DEVICES_PIXEL_ON_VALUES.get(dev_name, 1)
|
|
623
|
+
value = on_value if value else 0
|
|
624
|
+
self._mmc.setSLMPixelsTo(slm_device, int(value))
|
|
625
|
+
elif slm_array.size == 3:
|
|
626
|
+
# if it's a 3-valued array, we assume it's RGB
|
|
627
|
+
r, g, b = slm_array.astype(int)
|
|
628
|
+
self._mmc.setSLMPixelsTo(slm_device, r, g, b)
|
|
629
|
+
elif slm_array.ndim in (2, 3):
|
|
630
|
+
# if it's a 2D/3D array, we assume it's an image
|
|
631
|
+
# where 3D is RGB with shape (h, w, 3)
|
|
632
|
+
if slm_array.ndim == 3 and slm_array.shape[2] != 3:
|
|
633
|
+
raise ValueError( # pragma: no cover
|
|
634
|
+
"SLM image must be 2D or 3D with 3 channels (RGB)."
|
|
635
|
+
)
|
|
636
|
+
# convert boolean on/off values to pixel values
|
|
637
|
+
if slm_array.dtype == bool:
|
|
638
|
+
dev_name = self._mmc.getDeviceName(slm_device)
|
|
639
|
+
on_value = _SLM_DEVICES_PIXEL_ON_VALUES.get(dev_name, 1)
|
|
640
|
+
slm_array = np.where(slm_array, on_value, 0).astype(np.uint8)
|
|
641
|
+
self._mmc.setSLMImage(slm_device, slm_array)
|
|
642
|
+
if event.slm_image.exposure:
|
|
643
|
+
self._mmc.setSLMExposure(slm_device, event.slm_image.exposure)
|
|
644
|
+
except Exception as e:
|
|
645
|
+
logger.warning("Failed to set SLM Image: %s", e)
|
|
646
|
+
|
|
647
|
+
def _exec_event_slm_image(self, img: useq.SLMImage) -> None:
|
|
648
|
+
if slm_device := (img.device or self._mmc.getSLMDevice()):
|
|
649
|
+
try:
|
|
650
|
+
self._mmc.displaySLMImage(slm_device)
|
|
651
|
+
except Exception as e:
|
|
652
|
+
logger.warning("Failed to set SLM Image: %s", e)
|
|
653
|
+
|
|
654
|
+
def _update_config_device_props(self) -> None:
|
|
655
|
+
# store devices/props that make up each config group for faster lookup
|
|
656
|
+
self._config_device_props.clear()
|
|
657
|
+
for grp in self._mmc.getAvailableConfigGroups():
|
|
658
|
+
for preset in self._mmc.getAvailableConfigs(grp):
|
|
659
|
+
# ordered/unique list of (device, property) tuples for each group
|
|
660
|
+
self._config_device_props[grp] = tuple(
|
|
661
|
+
{(i[0], i[1]): None for i in self._mmc.getConfigData(grp, preset)}
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
def _get_current_props(self, *groups: str) -> tuple[PropertyValue, ...]:
|
|
665
|
+
"""Faster version of core.getConfigGroupState(group).
|
|
666
|
+
|
|
667
|
+
MMCore does some excess iteration that we want to avoid here. It calls
|
|
668
|
+
GetAvailableConfigs and then calls getConfigData for *every* preset in the
|
|
669
|
+
group, (not only the one being requested). We go straight to cached data
|
|
670
|
+
for the group we want.
|
|
671
|
+
"""
|
|
672
|
+
return tuple(
|
|
673
|
+
{
|
|
674
|
+
"dev": dev,
|
|
675
|
+
"prop": prop,
|
|
676
|
+
"val": self._mmc.getPropertyFromCache(dev, prop),
|
|
677
|
+
}
|
|
678
|
+
for group in groups
|
|
679
|
+
if (dev_props := self._config_device_props.get(group))
|
|
680
|
+
for dev, prop in dev_props
|
|
681
|
+
)
|
|
682
|
+
|
|
511
683
|
|
|
512
684
|
class ImagePayload(NamedTuple):
|
|
513
685
|
image: NDArray
|
|
514
686
|
event: MDAEvent
|
|
515
|
-
metadata:
|
|
687
|
+
metadata: FrameMetaV1 | SummaryMetaV1
|