pymmcore-plus 0.15.4__py3-none-any.whl → 0.16.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 +20 -1
- pymmcore_plus/_accumulator.py +23 -5
- pymmcore_plus/_cli.py +44 -26
- pymmcore_plus/_discovery.py +344 -0
- pymmcore_plus/_logger.py +1 -1
- pymmcore_plus/_util.py +9 -245
- pymmcore_plus/core/_device.py +20 -7
- pymmcore_plus/core/_mmcore_plus.py +16 -8
- pymmcore_plus/core/_property.py +34 -28
- pymmcore_plus/core/events/_device_signal_view.py +8 -1
- pymmcore_plus/experimental/unicore/_device_manager.py +20 -13
- pymmcore_plus/experimental/unicore/core/_unicore.py +4 -1
- pymmcore_plus/install.py +149 -18
- pymmcore_plus/mda/_engine.py +268 -73
- pymmcore_plus/metadata/_ome.py +499 -0
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.16.0.dist-info}/METADATA +3 -2
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.16.0.dist-info}/RECORD +20 -18
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.16.0.dist-info}/WHEEL +0 -0
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.16.0.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.16.0.dist-info}/licenses/LICENSE +0 -0
pymmcore_plus/mda/_engine.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
+
import warnings
|
|
5
|
+
import weakref
|
|
4
6
|
from contextlib import suppress
|
|
7
|
+
from functools import cache
|
|
5
8
|
from itertools import product
|
|
6
9
|
from typing import TYPE_CHECKING, Literal, NamedTuple, cast
|
|
7
10
|
|
|
@@ -11,7 +14,7 @@ from useq import AcquireImage, HardwareAutofocus, MDAEvent, MDASequence
|
|
|
11
14
|
|
|
12
15
|
from pymmcore_plus._logger import logger
|
|
13
16
|
from pymmcore_plus._util import retry
|
|
14
|
-
from pymmcore_plus.core._constants import Keyword
|
|
17
|
+
from pymmcore_plus.core._constants import FocusDirection, Keyword
|
|
15
18
|
from pymmcore_plus.core._sequencing import SequencedEvent, iter_sequenced_events
|
|
16
19
|
from pymmcore_plus.metadata import (
|
|
17
20
|
FrameMetaV1,
|
|
@@ -28,6 +31,7 @@ if TYPE_CHECKING:
|
|
|
28
31
|
from typing import TypeAlias
|
|
29
32
|
|
|
30
33
|
from numpy.typing import NDArray
|
|
34
|
+
from typing_extensions import TypedDict
|
|
31
35
|
|
|
32
36
|
from pymmcore_plus.core import CMMCorePlus
|
|
33
37
|
|
|
@@ -35,6 +39,13 @@ if TYPE_CHECKING:
|
|
|
35
39
|
|
|
36
40
|
IncludePositionArg: TypeAlias = Literal[True, False, "unsequenced-only"]
|
|
37
41
|
|
|
42
|
+
class StateDict(TypedDict, total=False):
|
|
43
|
+
xy_position: Sequence[float]
|
|
44
|
+
z_position: float
|
|
45
|
+
exposure: float
|
|
46
|
+
autoshutter: bool
|
|
47
|
+
config_groups: dict[str, str]
|
|
48
|
+
|
|
38
49
|
|
|
39
50
|
# these are SLM devices that have a known pixel_on_value.
|
|
40
51
|
# there is currently no way to extract this information from the core,
|
|
@@ -67,10 +78,22 @@ class MDAEngine(PMDAEngine):
|
|
|
67
78
|
reports that the events can be sequenced. This can be set after instantiation.
|
|
68
79
|
By default, this is `True`, however in various testing and demo scenarios, you
|
|
69
80
|
may wish to set it to `False` in order to avoid unexpected behavior.
|
|
81
|
+
restore_initial_state : bool | None
|
|
82
|
+
Whether to restore the initial hardware state after the MDA sequence completes.
|
|
83
|
+
If `True`, the engine will capture the initial state (positions,
|
|
84
|
+
config groups, exposure settings) before the sequence starts and restore it
|
|
85
|
+
after completion. If `None` (the default), `restore_initial_state` will
|
|
86
|
+
be set to `True` if FocusDirection is known (i.e. not Unknown).
|
|
70
87
|
"""
|
|
71
88
|
|
|
72
|
-
def __init__(
|
|
73
|
-
self
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
mmc: CMMCorePlus,
|
|
92
|
+
*,
|
|
93
|
+
use_hardware_sequencing: bool = True,
|
|
94
|
+
restore_initial_state: bool | None = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
self._mmcore_ref = weakref.ref(mmc)
|
|
74
97
|
self.use_hardware_sequencing: bool = use_hardware_sequencing
|
|
75
98
|
# if True, always set XY position, even if the commanded position is the same
|
|
76
99
|
# as the last commanded position (this does *not* query the stage for the
|
|
@@ -81,6 +104,12 @@ class MDAEngine(PMDAEngine):
|
|
|
81
104
|
# omitted by default when performing triggered acquisition because it's slow.
|
|
82
105
|
self._include_frame_position_metadata: IncludePositionArg = "unsequenced-only"
|
|
83
106
|
|
|
107
|
+
# whether to restore the initial hardware state after sequence completion
|
|
108
|
+
self.restore_initial_state: bool | None = restore_initial_state
|
|
109
|
+
|
|
110
|
+
# stored initial state for restoration (if restore_initial_state is True)
|
|
111
|
+
self._initial_state: StateDict = {}
|
|
112
|
+
|
|
84
113
|
# used to check if the hardware autofocus is engaged when the sequence begins.
|
|
85
114
|
# if it is, we will re-engage it after the autofocus action (if successful).
|
|
86
115
|
self._af_was_engaged: bool = False
|
|
@@ -94,7 +123,7 @@ class MDAEngine(PMDAEngine):
|
|
|
94
123
|
# This is used to determine whether we need to re-enable autoshutter after
|
|
95
124
|
# the sequence is done (assuming a event.keep_shutter_open was requested)
|
|
96
125
|
# Note: getAutoShutter() is True when no config is loaded at all
|
|
97
|
-
self._autoshutter_was_set: bool =
|
|
126
|
+
self._autoshutter_was_set: bool = mmc.getAutoShutter()
|
|
98
127
|
|
|
99
128
|
self._last_config: tuple[str, str] = ("", "")
|
|
100
129
|
self._last_xy_pos: tuple[float | None, float | None] = (None, None)
|
|
@@ -122,7 +151,9 @@ class MDAEngine(PMDAEngine):
|
|
|
122
151
|
@property
|
|
123
152
|
def mmcore(self) -> CMMCorePlus:
|
|
124
153
|
"""The `CMMCorePlus` instance to use for hardware control."""
|
|
125
|
-
|
|
154
|
+
if (mmc := self._mmcore_ref()) is None: # pragma: no cover
|
|
155
|
+
raise RuntimeError("The CMMCorePlus instance has been garbage collected.")
|
|
156
|
+
return mmc
|
|
126
157
|
|
|
127
158
|
# ===================== Protocol Implementation =====================
|
|
128
159
|
|
|
@@ -131,26 +162,42 @@ class MDAEngine(PMDAEngine):
|
|
|
131
162
|
# clear z_correction for new sequence
|
|
132
163
|
self._z_correction.clear()
|
|
133
164
|
|
|
134
|
-
if not self.
|
|
165
|
+
if not (core := self._mmcore_ref()): # pragma: no cover
|
|
135
166
|
from pymmcore_plus.core import CMMCorePlus
|
|
136
167
|
|
|
137
|
-
|
|
168
|
+
core = CMMCorePlus.instance()
|
|
169
|
+
self._mmcore_ref = weakref.ref(core)
|
|
170
|
+
|
|
171
|
+
# just in case a non-programmatic changes have been made in the meantime
|
|
172
|
+
# https://github.com/pymmcore-plus/pymmcore-plus/issues/503
|
|
173
|
+
core._last_config = ("", "") # noqa: SLF001
|
|
174
|
+
core._last_xy_position.clear() # noqa: SLF001
|
|
138
175
|
|
|
139
176
|
self._update_config_device_props()
|
|
140
177
|
# get if the autofocus is engaged at the start of the sequence
|
|
141
|
-
self._af_was_engaged =
|
|
178
|
+
self._af_was_engaged = core.isContinuousFocusLocked()
|
|
179
|
+
|
|
180
|
+
# capture initial state if restoration is enabled
|
|
181
|
+
if self.restore_initial_state is None:
|
|
182
|
+
fd = core.getFocusDevice()
|
|
183
|
+
self.restore_initial_state = (
|
|
184
|
+
fd is not None and core.getFocusDirection(fd) != FocusDirection.Unknown
|
|
185
|
+
)
|
|
142
186
|
|
|
143
|
-
if
|
|
187
|
+
if self.restore_initial_state:
|
|
188
|
+
self._initial_state = self._capture_state()
|
|
189
|
+
|
|
190
|
+
if px_size := core.getPixelSizeUm():
|
|
144
191
|
self._update_grid_fov_sizes(px_size, sequence)
|
|
145
192
|
|
|
146
|
-
self._autoshutter_was_set =
|
|
193
|
+
self._autoshutter_was_set = core.getAutoShutter()
|
|
147
194
|
return self.get_summary_metadata(mda_sequence=sequence)
|
|
148
195
|
|
|
149
196
|
def get_summary_metadata(self, mda_sequence: MDASequence | None) -> SummaryMetaV1:
|
|
150
|
-
return summary_metadata(self.
|
|
197
|
+
return summary_metadata(self.mmcore, mda_sequence=mda_sequence)
|
|
151
198
|
|
|
152
199
|
def _update_grid_fov_sizes(self, px_size: float, sequence: MDASequence) -> None:
|
|
153
|
-
*_, x_size, y_size = self.
|
|
200
|
+
*_, x_size, y_size = self.mmcore.getROI()
|
|
154
201
|
fov_width = x_size * px_size
|
|
155
202
|
fov_height = y_size * px_size
|
|
156
203
|
|
|
@@ -176,14 +223,15 @@ class MDAEngine(PMDAEngine):
|
|
|
176
223
|
self.setup_sequenced_event(event)
|
|
177
224
|
else:
|
|
178
225
|
self.setup_single_event(event)
|
|
179
|
-
self.
|
|
226
|
+
self.mmcore.waitForSystem()
|
|
180
227
|
|
|
181
228
|
def exec_event(self, event: MDAEvent) -> Iterable[PImagePayload]:
|
|
182
229
|
"""Execute an individual event and return the image data."""
|
|
183
230
|
action = getattr(event, "action", None)
|
|
231
|
+
core = self.mmcore
|
|
184
232
|
if isinstance(action, HardwareAutofocus):
|
|
185
233
|
# skip if no autofocus device is found
|
|
186
|
-
if not
|
|
234
|
+
if not core.getAutoFocusDevice():
|
|
187
235
|
logger.warning("No autofocus device found. Cannot execute autofocus.")
|
|
188
236
|
return
|
|
189
237
|
|
|
@@ -213,7 +261,7 @@ class MDAEngine(PMDAEngine):
|
|
|
213
261
|
# did not fail, re-engage it. NOTE: we need to do that AFTER the runner calls
|
|
214
262
|
# `setup_event`, so we can't do it inside the exec_event autofocus action above.
|
|
215
263
|
if self._af_was_engaged and self._af_succeeded:
|
|
216
|
-
|
|
264
|
+
core.enableContinuousFocus(True)
|
|
217
265
|
|
|
218
266
|
if isinstance(event, SequencedEvent):
|
|
219
267
|
yield from self.exec_sequenced_event(event)
|
|
@@ -231,7 +279,7 @@ class MDAEngine(PMDAEngine):
|
|
|
231
279
|
yield from events
|
|
232
280
|
return
|
|
233
281
|
|
|
234
|
-
yield from iter_sequenced_events(self.
|
|
282
|
+
yield from iter_sequenced_events(self.mmcore, events)
|
|
235
283
|
|
|
236
284
|
# ===================== Regular Events =====================
|
|
237
285
|
|
|
@@ -254,15 +302,16 @@ class MDAEngine(PMDAEngine):
|
|
|
254
302
|
|
|
255
303
|
self._set_event_channel(event)
|
|
256
304
|
|
|
305
|
+
mmcore = self.mmcore
|
|
257
306
|
if event.exposure is not None:
|
|
258
307
|
try:
|
|
259
|
-
|
|
308
|
+
mmcore.setExposure(event.exposure)
|
|
260
309
|
except Exception as e:
|
|
261
310
|
logger.warning("Failed to set exposure. %s", e)
|
|
262
311
|
if event.properties is not None:
|
|
263
312
|
try:
|
|
264
313
|
for dev, prop, value in event.properties:
|
|
265
|
-
|
|
314
|
+
mmcore.setProperty(dev, prop, value)
|
|
266
315
|
except Exception as e:
|
|
267
316
|
logger.warning("Failed to set properties. %s", e)
|
|
268
317
|
if (
|
|
@@ -272,11 +321,11 @@ class MDAEngine(PMDAEngine):
|
|
|
272
321
|
# if we want to leave the shutter open after this event, and autoshutter
|
|
273
322
|
# is currently enabled...
|
|
274
323
|
and event.keep_shutter_open
|
|
275
|
-
and
|
|
324
|
+
and mmcore.getAutoShutter()
|
|
276
325
|
):
|
|
277
326
|
# we have to disable autoshutter and open the shutter
|
|
278
|
-
|
|
279
|
-
|
|
327
|
+
mmcore.setAutoShutter(False)
|
|
328
|
+
mmcore.setShutterOpen(True)
|
|
280
329
|
|
|
281
330
|
def exec_single_event(self, event: MDAEvent) -> Iterator[PImagePayload]:
|
|
282
331
|
"""Execute a single (non-triggered) event and return the image data.
|
|
@@ -288,8 +337,9 @@ class MDAEngine(PMDAEngine):
|
|
|
288
337
|
if event.slm_image is not None:
|
|
289
338
|
self._exec_event_slm_image(event.slm_image)
|
|
290
339
|
|
|
340
|
+
mmcore = self.mmcore
|
|
291
341
|
try:
|
|
292
|
-
|
|
342
|
+
mmcore.snapImage()
|
|
293
343
|
# taking event time after snapImage includes exposure time
|
|
294
344
|
# not sure that's what we want, but it's currently consistent with the
|
|
295
345
|
# timing of the sequenced event runner (where Elapsed_Time_ms is taken after
|
|
@@ -300,21 +350,21 @@ class MDAEngine(PMDAEngine):
|
|
|
300
350
|
logger.warning("Failed to snap image. %s", e)
|
|
301
351
|
return
|
|
302
352
|
if not event.keep_shutter_open:
|
|
303
|
-
|
|
353
|
+
mmcore.setShutterOpen(False)
|
|
304
354
|
|
|
305
355
|
# most cameras will only have a single channel
|
|
306
356
|
# but Multi-camera may have multiple, and we need to retrieve a buffer for each
|
|
307
|
-
for cam in range(
|
|
357
|
+
for cam in range(mmcore.getNumberOfCameraChannels()):
|
|
308
358
|
meta = self.get_frame_metadata(
|
|
309
359
|
event,
|
|
310
360
|
runner_time_ms=event_time_ms,
|
|
311
|
-
camera_device=
|
|
361
|
+
camera_device=mmcore.getPhysicalCameraDevice(cam),
|
|
312
362
|
include_position=self._include_frame_position_metadata is not False,
|
|
313
363
|
)
|
|
314
364
|
# Note, the third element is actually a MutableMapping, but mypy doesn't
|
|
315
365
|
# see TypedDict as a subclass of MutableMapping yet.
|
|
316
366
|
# https://github.com/python/mypy/issues/4976
|
|
317
|
-
yield ImagePayload(
|
|
367
|
+
yield ImagePayload(mmcore.getImage(cam), event, meta) # type: ignore[misc]
|
|
318
368
|
|
|
319
369
|
def get_frame_metadata(
|
|
320
370
|
self,
|
|
@@ -329,7 +379,7 @@ class MDAEngine(PMDAEngine):
|
|
|
329
379
|
else:
|
|
330
380
|
prop_values = ()
|
|
331
381
|
return frame_metadata(
|
|
332
|
-
self.
|
|
382
|
+
self.mmcore,
|
|
333
383
|
cached=True,
|
|
334
384
|
runner_time_ms=runner_time_ms,
|
|
335
385
|
camera_device=camera_device,
|
|
@@ -342,14 +392,14 @@ class MDAEngine(PMDAEngine):
|
|
|
342
392
|
"""Teardown state of system (hardware, etc.) after `event`."""
|
|
343
393
|
# autoshutter was set at the beginning of the sequence, and this event
|
|
344
394
|
# doesn't want to leave the shutter open. Re-enable autoshutter.
|
|
345
|
-
core = self.
|
|
395
|
+
core = self.mmcore
|
|
346
396
|
if not event.keep_shutter_open and self._autoshutter_was_set:
|
|
347
397
|
core.setAutoShutter(True)
|
|
348
398
|
# FIXME: this may not be hitting as intended...
|
|
349
399
|
# https://github.com/pymmcore-plus/pymmcore-plus/pull/353#issuecomment-2159176491
|
|
350
400
|
if isinstance(event, SequencedEvent):
|
|
351
401
|
if event.exposure_sequence:
|
|
352
|
-
core.stopExposureSequence(
|
|
402
|
+
core.stopExposureSequence(core.getCameraDevice())
|
|
353
403
|
if event.x_sequence:
|
|
354
404
|
core.stopXYStageSequence(core.getXYStageDevice())
|
|
355
405
|
if event.z_sequence:
|
|
@@ -359,7 +409,133 @@ class MDAEngine(PMDAEngine):
|
|
|
359
409
|
|
|
360
410
|
def teardown_sequence(self, sequence: MDASequence) -> None:
|
|
361
411
|
"""Perform any teardown required after the sequence has been executed."""
|
|
362
|
-
|
|
412
|
+
# restore initial state if enabled and state was captured
|
|
413
|
+
if self.restore_initial_state and self._initial_state:
|
|
414
|
+
self._restore_initial_state()
|
|
415
|
+
|
|
416
|
+
def _capture_state(self) -> StateDict:
|
|
417
|
+
"""Capture the current hardware state for later restoration."""
|
|
418
|
+
state: StateDict = {}
|
|
419
|
+
if (core := self._mmcore_ref()) is None:
|
|
420
|
+
return state
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
# capture XY position
|
|
424
|
+
if core.getXYStageDevice():
|
|
425
|
+
state["xy_position"] = core.getXYPosition()
|
|
426
|
+
except Exception as e:
|
|
427
|
+
logger.warning("Failed to capture XY position: %s", e)
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
# capture Z position
|
|
431
|
+
if core.getFocusDevice():
|
|
432
|
+
state["z_position"] = core.getZPosition()
|
|
433
|
+
except Exception as e:
|
|
434
|
+
logger.warning("Failed to capture Z position: %s", e)
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
state["exposure"] = core.getExposure()
|
|
438
|
+
except Exception as e:
|
|
439
|
+
logger.warning("Failed to capture exposure setting: %s", e)
|
|
440
|
+
|
|
441
|
+
# capture config group states
|
|
442
|
+
try:
|
|
443
|
+
state_groups = state.setdefault("config_groups", {})
|
|
444
|
+
for group in core.getAvailableConfigGroups():
|
|
445
|
+
if current_config := core.getCurrentConfig(group):
|
|
446
|
+
state_groups[group] = current_config
|
|
447
|
+
except Exception as e:
|
|
448
|
+
logger.warning("Failed to get available config groups: %s", e)
|
|
449
|
+
|
|
450
|
+
# capture autoshutter state
|
|
451
|
+
try:
|
|
452
|
+
state["autoshutter"] = core.getAutoShutter()
|
|
453
|
+
except Exception as e:
|
|
454
|
+
logger.warning("Failed to capture autoshutter state: %s", e)
|
|
455
|
+
|
|
456
|
+
return state
|
|
457
|
+
|
|
458
|
+
def _restore_initial_state(self) -> None:
|
|
459
|
+
"""Restore the hardware state that was captured before the sequence."""
|
|
460
|
+
if not self._initial_state or (core := self._mmcore_ref()) is None:
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
# !!! We need to be careful about the order of Z and XY restoration:
|
|
464
|
+
#
|
|
465
|
+
# If FocusDirection is Unknown, we cannot safely restore Z *or* XY stage
|
|
466
|
+
# positions: we simply refuse and warn.
|
|
467
|
+
#
|
|
468
|
+
# If focus_dir is TowardSample, and we are restoring a Z-position that is
|
|
469
|
+
# *lower* than the current position or
|
|
470
|
+
# if focus_dir is AwayFromSample, and we are restoring a Z-position that is
|
|
471
|
+
# *higher* than the current position, then we need to move Z *before* moving XY,
|
|
472
|
+
# otherwise we may crash the objective into the sample.
|
|
473
|
+
# Otherwise, we should move XY first, then Z.
|
|
474
|
+
target_z = self._initial_state.get("z_position")
|
|
475
|
+
move_z_first = False
|
|
476
|
+
focus_dir = FocusDirection.Unknown
|
|
477
|
+
if target_z is not None and (focus_device := core.getFocusDevice()):
|
|
478
|
+
focus_dir = core.getFocusDirection(focus_device)
|
|
479
|
+
cur_z = core.getZPosition()
|
|
480
|
+
# focus_dir TowardSample => increasing position brings obj. closer to sample
|
|
481
|
+
if cur_z > target_z:
|
|
482
|
+
if focus_dir == FocusDirection.TowardSample:
|
|
483
|
+
move_z_first = True
|
|
484
|
+
elif focus_dir == FocusDirection.AwayFromSample:
|
|
485
|
+
move_z_first = True
|
|
486
|
+
|
|
487
|
+
if focus_dir == FocusDirection.Unknown:
|
|
488
|
+
_warn_focus_dir(focus_device)
|
|
489
|
+
else:
|
|
490
|
+
|
|
491
|
+
def _move_z() -> None:
|
|
492
|
+
if target_z is not None:
|
|
493
|
+
try:
|
|
494
|
+
if core.getFocusDevice():
|
|
495
|
+
core.setZPosition(target_z)
|
|
496
|
+
except Exception as e:
|
|
497
|
+
logger.warning("Failed to restore Z position: %s", e)
|
|
498
|
+
|
|
499
|
+
if move_z_first:
|
|
500
|
+
_move_z()
|
|
501
|
+
|
|
502
|
+
# restore XY position
|
|
503
|
+
if "xy_position" in self._initial_state:
|
|
504
|
+
try:
|
|
505
|
+
if core.getXYStageDevice():
|
|
506
|
+
core.setXYPosition(*self._initial_state["xy_position"])
|
|
507
|
+
except Exception as e:
|
|
508
|
+
logger.warning("Failed to restore XY position: %s", e)
|
|
509
|
+
|
|
510
|
+
if not move_z_first:
|
|
511
|
+
_move_z()
|
|
512
|
+
|
|
513
|
+
# restore exposure
|
|
514
|
+
if "exposure" in self._initial_state:
|
|
515
|
+
try:
|
|
516
|
+
core.setExposure(self._initial_state["exposure"])
|
|
517
|
+
except Exception as e:
|
|
518
|
+
logger.warning("Failed to restore exposure setting: %s", e)
|
|
519
|
+
|
|
520
|
+
# restore config group states
|
|
521
|
+
for key, value in self._initial_state.get("config_groups", {}).items():
|
|
522
|
+
try:
|
|
523
|
+
core.setConfig(key, value)
|
|
524
|
+
except Exception as e:
|
|
525
|
+
logger.warning(
|
|
526
|
+
"Failed to restore config group %s to %s: %s", key, value, e
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# restore autoshutter state
|
|
530
|
+
if "autoshutter" in self._initial_state:
|
|
531
|
+
try:
|
|
532
|
+
core.setAutoShutter(self._initial_state["autoshutter"])
|
|
533
|
+
except Exception as e:
|
|
534
|
+
logger.warning("Failed to restore autoshutter state: %s", e)
|
|
535
|
+
|
|
536
|
+
core.waitForSystem()
|
|
537
|
+
# clear the state after restoration
|
|
538
|
+
self._initial_state = {}
|
|
363
539
|
|
|
364
540
|
# ===================== Sequenced Events =====================
|
|
365
541
|
|
|
@@ -369,7 +545,7 @@ class MDAEngine(PMDAEngine):
|
|
|
369
545
|
`SequencedEvent` is a special pymmcore-plus specific subclass of
|
|
370
546
|
`useq.MDAEvent`.
|
|
371
547
|
"""
|
|
372
|
-
core = self.
|
|
548
|
+
core = self.mmcore
|
|
373
549
|
if event.exposure_sequence:
|
|
374
550
|
cam_device = core.getCameraDevice()
|
|
375
551
|
with suppress(RuntimeError):
|
|
@@ -408,7 +584,7 @@ class MDAEngine(PMDAEngine):
|
|
|
408
584
|
`setup_event`, which *is* part of the protocol), but it is made public
|
|
409
585
|
in case a user wants to subclass this engine and override this method.
|
|
410
586
|
"""
|
|
411
|
-
core = self.
|
|
587
|
+
core = self.mmcore
|
|
412
588
|
|
|
413
589
|
self._load_sequenced_event(event)
|
|
414
590
|
|
|
@@ -451,8 +627,9 @@ class MDAEngine(PMDAEngine):
|
|
|
451
627
|
self, timeout: float = 5.0, poll_interval: float = 0.2
|
|
452
628
|
) -> None:
|
|
453
629
|
tot = 0.0
|
|
454
|
-
self.
|
|
455
|
-
|
|
630
|
+
core = self.mmcore
|
|
631
|
+
core.stopSequenceAcquisition()
|
|
632
|
+
while core.isSequenceRunning():
|
|
456
633
|
time.sleep(poll_interval)
|
|
457
634
|
tot += poll_interval
|
|
458
635
|
if tot >= timeout:
|
|
@@ -483,23 +660,24 @@ class MDAEngine(PMDAEngine):
|
|
|
483
660
|
if event.slm_image is not None:
|
|
484
661
|
self._exec_event_slm_image(event.slm_image)
|
|
485
662
|
|
|
663
|
+
core = self.mmcore
|
|
486
664
|
# Start sequence
|
|
487
665
|
# Note that the overload of startSequenceAcquisition that takes a camera
|
|
488
666
|
# label does NOT automatically initialize a circular buffer. So if this call
|
|
489
667
|
# is changed to accept the camera in the future, that should be kept in mind.
|
|
490
|
-
|
|
668
|
+
core.startSequenceAcquisition(
|
|
491
669
|
n_events,
|
|
492
670
|
0, # intervalMS # TODO: add support for this
|
|
493
671
|
True, # stopOnOverflow
|
|
494
672
|
)
|
|
495
673
|
self.post_sequence_started(event)
|
|
496
674
|
|
|
497
|
-
n_channels =
|
|
675
|
+
n_channels = core.getNumberOfCameraChannels()
|
|
498
676
|
count = 0
|
|
499
677
|
iter_events = product(event.events, range(n_channels))
|
|
500
678
|
# block until the sequence is done, popping images in the meantime
|
|
501
|
-
while
|
|
502
|
-
if remaining :=
|
|
679
|
+
while core.isSequenceRunning():
|
|
680
|
+
if remaining := core.getRemainingImageCount():
|
|
503
681
|
yield self._next_seqimg_payload(
|
|
504
682
|
*next(iter_events), remaining=remaining - 1, event_t0=event_t0_ms
|
|
505
683
|
)
|
|
@@ -507,10 +685,10 @@ class MDAEngine(PMDAEngine):
|
|
|
507
685
|
else:
|
|
508
686
|
time.sleep(0.001)
|
|
509
687
|
|
|
510
|
-
if
|
|
688
|
+
if core.isBufferOverflowed(): # pragma: no cover
|
|
511
689
|
raise MemoryError("Buffer overflowed")
|
|
512
690
|
|
|
513
|
-
while remaining :=
|
|
691
|
+
while remaining := core.getRemainingImageCount():
|
|
514
692
|
yield self._next_seqimg_payload(
|
|
515
693
|
*next(iter_events), remaining=remaining - 1, event_t0=event_t0_ms
|
|
516
694
|
)
|
|
@@ -536,7 +714,8 @@ class MDAEngine(PMDAEngine):
|
|
|
536
714
|
) -> PImagePayload:
|
|
537
715
|
"""Grab next image from the circular buffer and return it as an ImagePayload."""
|
|
538
716
|
_slice = 0 # ?
|
|
539
|
-
|
|
717
|
+
core = self.mmcore
|
|
718
|
+
img, mm_meta = core.popNextImageAndMD(channel, _slice)
|
|
540
719
|
try:
|
|
541
720
|
seq_time = float(mm_meta.get(Keyword.Elapsed_Time_ms))
|
|
542
721
|
except Exception:
|
|
@@ -548,7 +727,7 @@ class MDAEngine(PMDAEngine):
|
|
|
548
727
|
# see: https://github.com/micro-manager/mmCoreAndDevices/pull/468
|
|
549
728
|
camera_device = mm_meta.GetSingleTag("Camera").GetValue()
|
|
550
729
|
except Exception:
|
|
551
|
-
camera_device =
|
|
730
|
+
camera_device = core.getPhysicalCameraDevice(channel)
|
|
552
731
|
|
|
553
732
|
# TODO: determine whether we want to try to populate changing property values
|
|
554
733
|
# during the course of a triggered sequence
|
|
@@ -573,26 +752,27 @@ class MDAEngine(PMDAEngine):
|
|
|
573
752
|
|
|
574
753
|
Returns the change in ZPosition that occurred during the autofocus event.
|
|
575
754
|
"""
|
|
755
|
+
core = self.mmcore
|
|
576
756
|
# switch off autofocus device if it is on
|
|
577
|
-
|
|
757
|
+
core.enableContinuousFocus(False)
|
|
578
758
|
|
|
579
759
|
if action.autofocus_motor_offset is not None:
|
|
580
760
|
# set the autofocus device offset
|
|
581
761
|
# if name is given explicitly, use it, otherwise use setAutoFocusOffset
|
|
582
762
|
# (see docs for setAutoFocusOffset for additional details)
|
|
583
763
|
if name := getattr(action, "autofocus_device_name", None):
|
|
584
|
-
|
|
764
|
+
core.setPosition(name, action.autofocus_motor_offset)
|
|
585
765
|
else:
|
|
586
|
-
|
|
587
|
-
|
|
766
|
+
core.setAutoFocusOffset(action.autofocus_motor_offset)
|
|
767
|
+
core.waitForSystem()
|
|
588
768
|
|
|
589
769
|
@retry(exceptions=RuntimeError, tries=action.max_retries, logger=logger.warning)
|
|
590
770
|
def _perform_full_focus(previous_z: float) -> float:
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
return
|
|
771
|
+
core.fullFocus()
|
|
772
|
+
core.waitForSystem()
|
|
773
|
+
return core.getZPosition() - previous_z
|
|
594
774
|
|
|
595
|
-
return _perform_full_focus(
|
|
775
|
+
return _perform_full_focus(core.getZPosition())
|
|
596
776
|
|
|
597
777
|
def _set_event_xy_position(self, event: MDAEvent) -> None:
|
|
598
778
|
event_x, event_y = event.x_pos, event.y_pos
|
|
@@ -600,13 +780,14 @@ class MDAEngine(PMDAEngine):
|
|
|
600
780
|
if event_x is None and event_y is None:
|
|
601
781
|
return
|
|
602
782
|
|
|
783
|
+
core = self.mmcore
|
|
603
784
|
# skip if no XY stage device is found
|
|
604
|
-
if not
|
|
785
|
+
if not core.getXYStageDevice():
|
|
605
786
|
logger.warning("No XY stage device found. Cannot set XY position.")
|
|
606
787
|
return
|
|
607
788
|
|
|
608
789
|
# Retrieve the last commanded XY position.
|
|
609
|
-
last_x, last_y =
|
|
790
|
+
last_x, last_y = core._last_xy_position.get(None) or (None, None) # noqa: SLF001
|
|
610
791
|
if (
|
|
611
792
|
not self.force_set_xy_position
|
|
612
793
|
and (event_x is None or event_x == last_x)
|
|
@@ -615,12 +796,12 @@ class MDAEngine(PMDAEngine):
|
|
|
615
796
|
return
|
|
616
797
|
|
|
617
798
|
if event_x is None or event_y is None:
|
|
618
|
-
cur_x, cur_y =
|
|
799
|
+
cur_x, cur_y = core.getXYPosition()
|
|
619
800
|
event_x = cur_x if event_x is None else event_x
|
|
620
801
|
event_y = cur_y if event_y is None else event_y
|
|
621
802
|
|
|
622
803
|
try:
|
|
623
|
-
|
|
804
|
+
core.setXYPosition(event_x, event_y)
|
|
624
805
|
except Exception as e:
|
|
625
806
|
logger.warning("Failed to set XY position. %s", e)
|
|
626
807
|
|
|
@@ -631,32 +812,33 @@ class MDAEngine(PMDAEngine):
|
|
|
631
812
|
# comparison with _last_config is a fast/rough check ... which may miss subtle
|
|
632
813
|
# differences if device properties have been individually set in the meantime.
|
|
633
814
|
# could also compare to the system state, with:
|
|
634
|
-
# data = self.
|
|
635
|
-
# if self.
|
|
815
|
+
# data = self.mmcore.getConfigData(ch.group, ch.config)
|
|
816
|
+
# if self.mmcore.getSystemStateCache().isConfigurationIncluded(data):
|
|
636
817
|
# ...
|
|
637
|
-
if (ch.group, ch.config) != self.
|
|
818
|
+
if (ch.group, ch.config) != self.mmcore._last_config: # noqa: SLF001
|
|
638
819
|
try:
|
|
639
|
-
self.
|
|
820
|
+
self.mmcore.setConfig(ch.group, ch.config)
|
|
640
821
|
except Exception as e:
|
|
641
822
|
logger.warning("Failed to set channel. %s", e)
|
|
642
823
|
|
|
643
824
|
def _set_event_z(self, event: MDAEvent) -> None:
|
|
644
825
|
# skip if no Z stage device is found
|
|
645
|
-
if not self.
|
|
826
|
+
if not self.mmcore.getFocusDevice():
|
|
646
827
|
logger.warning("No Z stage device found. Cannot set Z position.")
|
|
647
828
|
return
|
|
648
829
|
|
|
649
830
|
p_idx = event.index.get("p", None)
|
|
650
831
|
correction = self._z_correction.setdefault(p_idx, 0.0)
|
|
651
|
-
self.
|
|
832
|
+
self.mmcore.setZPosition(cast("float", event.z_pos) + correction)
|
|
652
833
|
|
|
653
834
|
def _set_event_slm_image(self, event: MDAEvent) -> None:
|
|
654
835
|
if not event.slm_image:
|
|
655
836
|
return
|
|
837
|
+
core = self.mmcore
|
|
656
838
|
try:
|
|
657
839
|
# Get the SLM device
|
|
658
840
|
if not (
|
|
659
|
-
slm_device := event.slm_image.device or
|
|
841
|
+
slm_device := event.slm_image.device or core.getSLMDevice()
|
|
660
842
|
): # pragma: no cover
|
|
661
843
|
raise ValueError("No SLM device found or specified.")
|
|
662
844
|
|
|
@@ -666,14 +848,14 @@ class MDAEngine(PMDAEngine):
|
|
|
666
848
|
if slm_array.ndim == 0:
|
|
667
849
|
value = slm_array.item()
|
|
668
850
|
if isinstance(value, bool):
|
|
669
|
-
dev_name =
|
|
851
|
+
dev_name = core.getDeviceName(slm_device)
|
|
670
852
|
on_value = _SLM_DEVICES_PIXEL_ON_VALUES.get(dev_name, 1)
|
|
671
853
|
value = on_value if value else 0
|
|
672
|
-
|
|
854
|
+
core.setSLMPixelsTo(slm_device, int(value))
|
|
673
855
|
elif slm_array.size == 3:
|
|
674
856
|
# if it's a 3-valued array, we assume it's RGB
|
|
675
857
|
r, g, b = slm_array.astype(int)
|
|
676
|
-
|
|
858
|
+
core.setSLMPixelsTo(slm_device, r, g, b)
|
|
677
859
|
elif slm_array.ndim in (2, 3):
|
|
678
860
|
# if it's a 2D/3D array, we assume it's an image
|
|
679
861
|
# where 3D is RGB with shape (h, w, 3)
|
|
@@ -683,30 +865,31 @@ class MDAEngine(PMDAEngine):
|
|
|
683
865
|
)
|
|
684
866
|
# convert boolean on/off values to pixel values
|
|
685
867
|
if slm_array.dtype == bool:
|
|
686
|
-
dev_name =
|
|
868
|
+
dev_name = core.getDeviceName(slm_device)
|
|
687
869
|
on_value = _SLM_DEVICES_PIXEL_ON_VALUES.get(dev_name, 1)
|
|
688
870
|
slm_array = np.where(slm_array, on_value, 0).astype(np.uint8)
|
|
689
|
-
|
|
871
|
+
core.setSLMImage(slm_device, slm_array)
|
|
690
872
|
if event.slm_image.exposure:
|
|
691
|
-
|
|
873
|
+
core.setSLMExposure(slm_device, event.slm_image.exposure)
|
|
692
874
|
except Exception as e:
|
|
693
875
|
logger.warning("Failed to set SLM Image: %s", e)
|
|
694
876
|
|
|
695
877
|
def _exec_event_slm_image(self, img: useq.SLMImage) -> None:
|
|
696
|
-
if slm_device := (img.device or self.
|
|
878
|
+
if slm_device := (img.device or self.mmcore.getSLMDevice()):
|
|
697
879
|
try:
|
|
698
|
-
self.
|
|
880
|
+
self.mmcore.displaySLMImage(slm_device)
|
|
699
881
|
except Exception as e:
|
|
700
882
|
logger.warning("Failed to set SLM Image: %s", e)
|
|
701
883
|
|
|
702
884
|
def _update_config_device_props(self) -> None:
|
|
703
885
|
# store devices/props that make up each config group for faster lookup
|
|
704
886
|
self._config_device_props.clear()
|
|
705
|
-
|
|
706
|
-
|
|
887
|
+
core = self.mmcore
|
|
888
|
+
for grp in core.getAvailableConfigGroups():
|
|
889
|
+
for preset in core.getAvailableConfigs(grp):
|
|
707
890
|
# ordered/unique list of (device, property) tuples for each group
|
|
708
891
|
self._config_device_props[grp] = tuple(
|
|
709
|
-
{(i[0], i[1]): None for i in
|
|
892
|
+
{(i[0], i[1]): None for i in core.getConfigData(grp, preset)}
|
|
710
893
|
)
|
|
711
894
|
|
|
712
895
|
def _get_current_props(self, *groups: str) -> tuple[PropertyValue, ...]:
|
|
@@ -721,7 +904,7 @@ class MDAEngine(PMDAEngine):
|
|
|
721
904
|
{
|
|
722
905
|
"dev": dev,
|
|
723
906
|
"prop": prop,
|
|
724
|
-
"val": self.
|
|
907
|
+
"val": self.mmcore.getPropertyFromCache(dev, prop),
|
|
725
908
|
}
|
|
726
909
|
for group in groups
|
|
727
910
|
if (dev_props := self._config_device_props.get(group))
|
|
@@ -733,3 +916,15 @@ class ImagePayload(NamedTuple):
|
|
|
733
916
|
image: NDArray
|
|
734
917
|
event: MDAEvent
|
|
735
918
|
metadata: FrameMetaV1 | SummaryMetaV1
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
@cache
|
|
922
|
+
def _warn_focus_dir(focus_device: str) -> None:
|
|
923
|
+
warnings.warn(
|
|
924
|
+
"Focus direction is unknown: refusing to restore initial XYZ position "
|
|
925
|
+
"for safety reasons. Please set FocusDirection in your config file:\n\n"
|
|
926
|
+
f" FocusDirection,{focus_device},<1 or -1>\n\n"
|
|
927
|
+
"Or use the `Hardware Configuration Wizard > Stage Focus Direction`",
|
|
928
|
+
stacklevel=3,
|
|
929
|
+
category=RuntimeWarning,
|
|
930
|
+
)
|