pymmcore-plus 0.9.4__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 +133 -30
- 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.4.dist-info → pymmcore_plus-0.13.0.dist-info}/METADATA +22 -11
- pymmcore_plus-0.13.0.dist-info/RECORD +71 -0
- {pymmcore_plus-0.9.4.dist-info → pymmcore_plus-0.13.0.dist-info}/WHEEL +1 -1
- pymmcore_plus/core/_state.py +0 -244
- pymmcore_plus-0.9.4.dist-info/RECORD +0 -55
- {pymmcore_plus-0.9.4.dist-info → pymmcore_plus-0.13.0.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.9.4.dist-info → pymmcore_plus-0.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,104 +1,104 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from contextlib import suppress
|
|
6
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
5
7
|
|
|
8
|
+
from pydantic import Field, model_validator
|
|
6
9
|
from useq import AcquireImage, MDAEvent
|
|
7
10
|
|
|
8
|
-
from pymmcore_plus.core._constants import DeviceType
|
|
11
|
+
from pymmcore_plus.core._constants import DeviceType, Keyword
|
|
9
12
|
|
|
10
13
|
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Iterable, Iterator
|
|
15
|
+
from typing import Self
|
|
16
|
+
|
|
17
|
+
from useq._mda_event import Channel as EventChannel
|
|
18
|
+
|
|
11
19
|
from pymmcore_plus import CMMCorePlus
|
|
12
20
|
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
"""Subclass of MDAEvent that represents a sequence of triggered events.
|
|
22
|
+
T = TypeVar("T")
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
calculate sequences for x, y, z, and exposure based on an a sequence of events.
|
|
19
|
-
"""
|
|
24
|
+
__all__ = ["SequencedEvent", "get_all_sequenceable", "iter_sequenced_events"]
|
|
20
25
|
|
|
21
|
-
events: Tuple[MDAEvent, ...] # noqa: UP006
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
def iter_sequenced_events(
|
|
28
|
+
core: CMMCorePlus, events: Iterable[MDAEvent]
|
|
29
|
+
) -> Iterator[MDAEvent | SequencedEvent]:
|
|
30
|
+
"""Iterate over a sequence of MDAEvents, yielding SequencedEvents when possible.
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
core : CMMCorePlus
|
|
35
|
+
The core object to use for determining sequenceable properties.
|
|
36
|
+
events : Iterable[MDAEvent]
|
|
37
|
+
The events to iterate over.
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if e.properties:
|
|
50
|
-
for dev, prop, val in e.properties:
|
|
51
|
-
prop_seqs.setdefault((dev, prop), []).append(val)
|
|
52
|
-
|
|
53
|
-
# filter out any sequences that are all the same value
|
|
54
|
-
return {k: v for k, v in prop_seqs.items() if len(set(v)) > 1}
|
|
55
|
-
|
|
56
|
-
@classmethod
|
|
57
|
-
def create(cls, events: Sequence[MDAEvent]) -> SequencedEvent:
|
|
58
|
-
"""Create a SequencedEvent from a sequence of events.
|
|
59
|
-
|
|
60
|
-
This pre-calculates sequences of length > 1 for x, y, z positions, and exposure.
|
|
61
|
-
Channel configs and other sequenceable properties are determined by the
|
|
62
|
-
`property_sequences` method, which requires access to a core instance.
|
|
63
|
-
"""
|
|
64
|
-
_events = tuple(events)
|
|
65
|
-
if len(_events) <= 1:
|
|
66
|
-
raise ValueError("Sequences must have at least two events.")
|
|
67
|
-
|
|
68
|
-
data: dict[str, list] = {a: [] for a in ("z_pos", "x_pos", "y_pos", "exposure")}
|
|
69
|
-
for event, attr in product(_events, list(data)):
|
|
70
|
-
# do we need to check if not None?
|
|
71
|
-
# the only problem might occur if some are None and some are not
|
|
72
|
-
data[attr].append(getattr(event, attr))
|
|
73
|
-
|
|
74
|
-
x_seq = data["x_pos"] if len(set(data["x_pos"])) > 1 else ()
|
|
75
|
-
y_seq = data["y_pos"] if len(set(data["y_pos"])) > 1 else ()
|
|
76
|
-
if len(x_seq) != len(y_seq): # pragma: no cover
|
|
77
|
-
raise ValueError(
|
|
78
|
-
"X and Y sequences must be the same length: "
|
|
79
|
-
f"{len(x_seq)=}, {len(y_seq)=}"
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
e0 = _events[0]
|
|
83
|
-
return cls(
|
|
84
|
-
events=_events,
|
|
85
|
-
exposure_sequence=(
|
|
86
|
-
data["exposure"] if len(set(data["exposure"])) > 1 else ()
|
|
87
|
-
),
|
|
88
|
-
x_sequence=x_seq,
|
|
89
|
-
y_sequence=y_seq,
|
|
90
|
-
z_sequence=data["z_pos"] if len(set(data["z_pos"])) > 1 else (),
|
|
91
|
-
# use the first event to provide all other values like min_start_time, etc.
|
|
92
|
-
**(e0.model_dump() if hasattr(e0, "model_dump") else e0.dict()),
|
|
93
|
-
)
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
Iterator[MDAEvent | SequencedEvent]
|
|
42
|
+
A new iterator that will combine multiple MDAEvents into a single SequencedEvent
|
|
43
|
+
when possible, based on the sequenceable properties of the core object.
|
|
44
|
+
Note that `SequencedEvent` itself is a subclass of `MDAEvent`, but it's up to
|
|
45
|
+
the engine to check `isinstance(event, SequencedEvent)` in order to handle
|
|
46
|
+
SequencedEvents differently.
|
|
47
|
+
"""
|
|
48
|
+
combiner = EventCombiner(core)
|
|
49
|
+
for e in events:
|
|
50
|
+
if (flushed := combiner.feed_event(e)) is not None:
|
|
51
|
+
yield flushed
|
|
52
|
+
|
|
53
|
+
if (leftover := combiner.flush()) is not None:
|
|
54
|
+
yield leftover
|
|
94
55
|
|
|
95
56
|
|
|
96
|
-
|
|
57
|
+
class SequencedEvent(MDAEvent):
|
|
58
|
+
"""Subclass of MDAEvent that represents a sequence of triggered events."""
|
|
59
|
+
|
|
60
|
+
events: tuple[MDAEvent, ...] = Field(repr=False)
|
|
61
|
+
|
|
62
|
+
exposure_sequence: tuple[float, ...] = Field(default_factory=tuple)
|
|
63
|
+
x_sequence: tuple[float, ...] = Field(default_factory=tuple)
|
|
64
|
+
y_sequence: tuple[float, ...] = Field(default_factory=tuple)
|
|
65
|
+
z_sequence: tuple[float, ...] = Field(default_factory=tuple)
|
|
66
|
+
slm_sequence: tuple[bytes, ...] = Field(default_factory=tuple)
|
|
67
|
+
|
|
68
|
+
# all other property sequences
|
|
69
|
+
property_sequences: dict[tuple[str, str], list[str]] = Field(default_factory=dict)
|
|
70
|
+
# static properties should be added to MDAEvent.properties as usual
|
|
71
|
+
|
|
72
|
+
@model_validator(mode="after")
|
|
73
|
+
def _check_lengths(self) -> Self:
|
|
74
|
+
if len(self.x_sequence) != len(self.y_sequence):
|
|
75
|
+
raise ValueError("XY sequence lengths must match")
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def __repr_args__(self) -> Iterable[tuple[str | None, Any]]:
|
|
79
|
+
for k, v in super().__repr_args__():
|
|
80
|
+
if isinstance(v, tuple):
|
|
81
|
+
v = f"({len(v)} items)"
|
|
82
|
+
if isinstance(v, dict):
|
|
83
|
+
v = f"({len(v)} items)"
|
|
84
|
+
yield k, v
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_all_sequenceable(
|
|
88
|
+
core: CMMCorePlus, include_properties: bool = True
|
|
89
|
+
) -> dict[tuple[str | DeviceType, str], int]:
|
|
97
90
|
"""Return all sequenceable devices in `core`.
|
|
98
91
|
|
|
99
92
|
This is just a convenience function to help determine which devices can be
|
|
100
93
|
sequenced on a given configuration.
|
|
101
94
|
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
core : CMMCorePlus
|
|
98
|
+
The core object to use for determining sequenceable properties.
|
|
99
|
+
include_properties : bool
|
|
100
|
+
Whether to check/include all device properties in the result.
|
|
101
|
+
|
|
102
102
|
Returns
|
|
103
103
|
-------
|
|
104
104
|
dict[tuple[str | DeviceType, str], int]
|
|
@@ -114,9 +114,10 @@ def get_all_sequenceable(core: CMMCorePlus) -> dict[tuple[str | DeviceType, str]
|
|
|
114
114
|
"""
|
|
115
115
|
d: dict[tuple[str | DeviceType, str], int] = {}
|
|
116
116
|
for device in core.iterDevices():
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
if include_properties:
|
|
118
|
+
for prop in device.properties:
|
|
119
|
+
if prop.isSequenceable():
|
|
120
|
+
d[(prop.device, prop.name)] = prop.sequenceMaxLength()
|
|
120
121
|
if device.type() == DeviceType.Stage:
|
|
121
122
|
# isStageLinearSequenceable?
|
|
122
123
|
if core.isStageSequenceable(device.label):
|
|
@@ -133,165 +134,301 @@ def get_all_sequenceable(core: CMMCorePlus) -> dict[tuple[str | DeviceType, str]
|
|
|
133
134
|
return d
|
|
134
135
|
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
def can_sequence_events(
|
|
138
|
-
core: CMMCorePlus,
|
|
139
|
-
e1: MDAEvent,
|
|
140
|
-
e2: MDAEvent,
|
|
141
|
-
cur_length: int = ...,
|
|
142
|
-
*,
|
|
143
|
-
return_reason: Literal[False] = ...,
|
|
144
|
-
) -> bool: ...
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
@overload
|
|
148
|
-
def can_sequence_events(
|
|
149
|
-
core: CMMCorePlus,
|
|
150
|
-
e1: MDAEvent,
|
|
151
|
-
e2: MDAEvent,
|
|
152
|
-
cur_length: int = ...,
|
|
153
|
-
*,
|
|
154
|
-
return_reason: Literal[True],
|
|
155
|
-
) -> tuple[bool, str]: ...
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def can_sequence_events(
|
|
159
|
-
core: CMMCorePlus,
|
|
160
|
-
e1: MDAEvent,
|
|
161
|
-
e2: MDAEvent,
|
|
162
|
-
cur_length: int = -1,
|
|
163
|
-
*,
|
|
164
|
-
return_reason: bool = False,
|
|
165
|
-
) -> bool | tuple[bool, str]:
|
|
166
|
-
"""Check whether two [`useq.MDAEvent`][] are sequenceable.
|
|
137
|
+
# ==============================================
|
|
167
138
|
|
|
168
|
-
Micro-manager calls hardware triggering "sequencing". Two events can be
|
|
169
|
-
sequenced if *all* device properties that are changing between the first and
|
|
170
|
-
second event support sequencing.
|
|
171
139
|
|
|
172
|
-
|
|
173
|
-
"
|
|
174
|
-
the `...SequenceMaxLength()` method corresponding to the device property.
|
|
140
|
+
class EventCombiner:
|
|
141
|
+
"""Helper class to combine multiple MDAEvents into a single SequencedEvent.
|
|
175
142
|
|
|
176
|
-
See:
|
|
143
|
+
See also: `iter_sequenced_events`, which is the primary way that this class is used.
|
|
177
144
|
|
|
178
145
|
Parameters
|
|
179
146
|
----------
|
|
180
147
|
core : CMMCorePlus
|
|
181
|
-
The core
|
|
182
|
-
e1 : MDAEvent
|
|
183
|
-
The first event.
|
|
184
|
-
e2 : MDAEvent
|
|
185
|
-
The second event.
|
|
186
|
-
cur_length : int
|
|
187
|
-
The current length of the sequence. Used when checking
|
|
188
|
-
`.get<...>SequenceMaxLength` for a given property. If the current length
|
|
189
|
-
is greater than the max length, the events cannot be sequenced. By default
|
|
190
|
-
-1, which means the current length is not checked.
|
|
191
|
-
return_reason : bool
|
|
192
|
-
If True, return a tuple of (bool, str) where the str is a reason for failure.
|
|
193
|
-
Otherwise just return a bool.
|
|
194
|
-
|
|
195
|
-
Returns
|
|
196
|
-
-------
|
|
197
|
-
bool | tuple[bool, str]
|
|
198
|
-
If return_reason is True, return a tuple of a boolean indicating whether the
|
|
199
|
-
events can be sequenced and a string describing the reason for failure if the
|
|
200
|
-
events cannot be sequenced. Otherwise just return a boolean indicating
|
|
201
|
-
whether the events can be sequenced.
|
|
202
|
-
|
|
203
|
-
Examples
|
|
204
|
-
--------
|
|
205
|
-
!!! note
|
|
206
|
-
|
|
207
|
-
The results here will depend on the current state of the core and devices.
|
|
208
|
-
|
|
209
|
-
```python
|
|
210
|
-
>>> from useq import MDAEvent
|
|
211
|
-
>>> core = CMMCorePlus.instance()
|
|
212
|
-
>>> core.loadSystemConfiguration()
|
|
213
|
-
>>> can_sequence_events(core, MDAEvent(), MDAEvent())
|
|
214
|
-
(True, "")
|
|
215
|
-
>>> can_sequence_events(core, MDAEvent(x_pos=1), MDAEvent(x_pos=2))
|
|
216
|
-
(False, "Stage 'XY' is not sequenceable")
|
|
217
|
-
>>> can_sequence_events(
|
|
218
|
-
... core,
|
|
219
|
-
... MDAEvent(channel={'config': 'DAPI'}),
|
|
220
|
-
... MDAEvent(channel={'config': 'FITC'})
|
|
221
|
-
... )
|
|
222
|
-
(False, "'Dichroic-Label' is not sequenceable")
|
|
223
|
-
```
|
|
148
|
+
The core object to use for determining sequenceable properties
|
|
224
149
|
"""
|
|
225
150
|
|
|
226
|
-
def
|
|
227
|
-
|
|
151
|
+
def __init__(self, core: CMMCorePlus) -> None:
|
|
152
|
+
self.core = core
|
|
153
|
+
self.max_lengths: dict[Keyword | tuple[str, str], int] = (
|
|
154
|
+
_get_max_sequence_lengths(core) # type: ignore [assignment]
|
|
155
|
+
)
|
|
228
156
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
157
|
+
# cached property values for each channel
|
|
158
|
+
self._channel_props: dict[EventChannel, dict[tuple[str, str], Any]] = {}
|
|
159
|
+
# cached max sequence lengths for each property
|
|
160
|
+
self._prop_lengths: dict[tuple[str, str], int] = {}
|
|
161
|
+
|
|
162
|
+
# growing list of MDAEvents to be combined into a single SequencedEvent
|
|
163
|
+
self.event_batch: list[MDAEvent] = []
|
|
164
|
+
|
|
165
|
+
# whether a given attribute has changed in the current batch
|
|
166
|
+
self.attribute_changes: dict[Keyword | tuple[str, str], bool] = {}
|
|
167
|
+
self.first_event_props: dict[tuple[str, str], Any] = {}
|
|
168
|
+
self._reset_tracking()
|
|
169
|
+
|
|
170
|
+
def _reset_tracking(self) -> None:
|
|
171
|
+
self.event_batch.clear()
|
|
172
|
+
self.attribute_changes.clear()
|
|
173
|
+
self.first_event_props.clear()
|
|
174
|
+
|
|
175
|
+
def feed_event(self, event: MDAEvent) -> MDAEvent | SequencedEvent | None:
|
|
176
|
+
"""Feed one new event into the combiner.
|
|
177
|
+
|
|
178
|
+
Returns a flushed MDAEvent/SequencedEvent if the new event *cannot* extend the
|
|
179
|
+
current batch, or `None` otherwise.
|
|
180
|
+
"""
|
|
181
|
+
if not self.event_batch:
|
|
182
|
+
# Starting a new batch
|
|
183
|
+
self.event_batch.append(event)
|
|
184
|
+
self.first_event_props = self._event_properties(event)
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
if self.can_extend(event):
|
|
188
|
+
# Extend the current batch
|
|
189
|
+
self.event_batch.append(event)
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
# we've hit the end of the sequence
|
|
193
|
+
# first, flus the existing batch...
|
|
194
|
+
flushed = self._create_sequenced_event()
|
|
195
|
+
|
|
196
|
+
# Then start a new batch with this new event...
|
|
197
|
+
self._reset_tracking()
|
|
198
|
+
self.event_batch.append(event)
|
|
199
|
+
self.first_event_props = self._event_properties(event)
|
|
200
|
+
|
|
201
|
+
# then return the flushed event
|
|
202
|
+
return flushed
|
|
203
|
+
|
|
204
|
+
def can_extend(self, event: MDAEvent) -> bool:
|
|
205
|
+
"""Return True if the new event can be added to the current batch."""
|
|
206
|
+
# cannot add pre-existing SequencedEvents to the sequence
|
|
207
|
+
if not self.event_batch:
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
e0 = self.event_batch[0]
|
|
211
|
+
|
|
212
|
+
# cannot sequence on top of SequencedEvents
|
|
213
|
+
if isinstance(e0, SequencedEvent) or isinstance(event, SequencedEvent):
|
|
214
|
+
return False
|
|
215
|
+
# cannot sequence on top of non-'AcquireImage' events
|
|
216
|
+
acq = (AcquireImage, type(None))
|
|
217
|
+
if not isinstance(e0.action, acq) or not isinstance(event.action, acq):
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
new_chunk_len = len(self.event_batch) + 1
|
|
221
|
+
|
|
222
|
+
# NOTE: these should be ordered from "fastest to check / most likely to fail",
|
|
223
|
+
# to "slowest to check / most likely to pass"
|
|
224
|
+
|
|
225
|
+
# If it's a new timepoint, and they have a different start time
|
|
226
|
+
# we don't (yet) support sequencing.
|
|
227
|
+
if (
|
|
228
|
+
event.index.get("t") != e0.index.get("t")
|
|
229
|
+
and event.min_start_time != e0.min_start_time
|
|
230
|
+
):
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
# Exposure
|
|
234
|
+
if event.exposure != e0.exposure:
|
|
235
|
+
if new_chunk_len > self.max_lengths[Keyword.CoreCamera]:
|
|
236
|
+
return False
|
|
237
|
+
self.attribute_changes[Keyword.CoreCamera] = True
|
|
238
|
+
|
|
239
|
+
# XY
|
|
240
|
+
if event.x_pos != e0.x_pos or event.y_pos != e0.y_pos:
|
|
241
|
+
if new_chunk_len > self.max_lengths[Keyword.CoreXYStage]:
|
|
242
|
+
return False
|
|
243
|
+
self.attribute_changes[Keyword.CoreXYStage] = True
|
|
244
|
+
|
|
245
|
+
# Z
|
|
246
|
+
if event.z_pos != e0.z_pos:
|
|
247
|
+
if new_chunk_len > self.max_lengths[Keyword.CoreFocus]:
|
|
248
|
+
return False
|
|
249
|
+
self.attribute_changes[Keyword.CoreFocus] = True
|
|
250
|
+
|
|
251
|
+
# SLM
|
|
252
|
+
if event.slm_image != e0.slm_image:
|
|
253
|
+
if new_chunk_len > self.max_lengths[Keyword.CoreSLM]:
|
|
254
|
+
return False
|
|
255
|
+
self.attribute_changes[Keyword.CoreSLM] = True
|
|
256
|
+
|
|
257
|
+
# properties
|
|
258
|
+
event_props = self._event_properties(event)
|
|
259
|
+
all_props = event_props.keys() | self.first_event_props.keys()
|
|
260
|
+
for dev_prop in all_props:
|
|
261
|
+
new_val = event_props.get(dev_prop)
|
|
262
|
+
old_val = self.first_event_props.get(dev_prop)
|
|
263
|
+
if new_val != old_val:
|
|
264
|
+
# if the property has changed, (or is missing in one dict)
|
|
265
|
+
if new_chunk_len > self._get_property_max_length(dev_prop):
|
|
266
|
+
return False
|
|
267
|
+
self.attribute_changes[dev_prop] = True
|
|
268
|
+
|
|
269
|
+
return True
|
|
270
|
+
|
|
271
|
+
def flush(self) -> MDAEvent | SequencedEvent | None:
|
|
272
|
+
"""Flush any remaining events in the buffer."""
|
|
273
|
+
if not self.event_batch:
|
|
274
|
+
return None
|
|
275
|
+
result = self._create_sequenced_event()
|
|
276
|
+
self._reset_tracking()
|
|
277
|
+
return result
|
|
278
|
+
|
|
279
|
+
def _create_sequenced_event(self) -> MDAEvent | SequencedEvent:
|
|
280
|
+
"""Convert self.event_batch into a SequencedEvent.
|
|
281
|
+
|
|
282
|
+
If the batch contains only a single event, that event is returned directly.
|
|
283
|
+
"""
|
|
284
|
+
if not self.event_batch:
|
|
285
|
+
raise RuntimeError("Cannot flush an empty chunk")
|
|
286
|
+
|
|
287
|
+
first_event = self.event_batch[0]
|
|
288
|
+
|
|
289
|
+
if (num_events := len(self.event_batch)) == 1:
|
|
290
|
+
return first_event
|
|
291
|
+
|
|
292
|
+
exposures: list[float | None] = []
|
|
293
|
+
x_positions: list[float | None] = []
|
|
294
|
+
y_positions: list[float | None] = []
|
|
295
|
+
z_positions: list[float | None] = []
|
|
296
|
+
slm_images: list[Any] = []
|
|
297
|
+
property_sequences: defaultdict[tuple[str, str], list[Any]] = defaultdict(list)
|
|
298
|
+
static_props: list[tuple[str, str, Any]] = []
|
|
299
|
+
|
|
300
|
+
# Single pass
|
|
301
|
+
for e in self.event_batch:
|
|
302
|
+
exposures.append(e.exposure)
|
|
303
|
+
x_positions.append(e.x_pos)
|
|
304
|
+
y_positions.append(e.y_pos)
|
|
305
|
+
z_positions.append(e.z_pos)
|
|
306
|
+
slm_images.append(e.slm_image)
|
|
307
|
+
for dev_prop, val in self._event_properties(e).items():
|
|
308
|
+
property_sequences[dev_prop].append(val)
|
|
309
|
+
|
|
310
|
+
# remove any property sequences that are static
|
|
311
|
+
for key, prop_seq in list(property_sequences.items()):
|
|
312
|
+
if not self.attribute_changes.get(key):
|
|
313
|
+
static_props.append((*key, prop_seq[0]))
|
|
314
|
+
property_sequences.pop(key)
|
|
315
|
+
elif len(prop_seq) != num_events:
|
|
316
|
+
raise RuntimeError(
|
|
317
|
+
"Property sequence length mismatch. "
|
|
318
|
+
"Please report this with an example."
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
exp_changed = self.attribute_changes.get(Keyword.CoreCamera)
|
|
322
|
+
xy_changed = self.attribute_changes.get(Keyword.CoreXYStage)
|
|
323
|
+
z_changed = self.attribute_changes.get(Keyword.CoreFocus)
|
|
324
|
+
slm_changed = self.attribute_changes.get(Keyword.CoreSLM)
|
|
325
|
+
|
|
326
|
+
exp_seq = tuple(exposures) if exp_changed else ()
|
|
327
|
+
x_seq = tuple(x_positions) if xy_changed else ()
|
|
328
|
+
y_seq = tuple(y_positions) if xy_changed else ()
|
|
329
|
+
z_seq = tuple(z_positions) if z_changed else ()
|
|
330
|
+
slm_seq = tuple(slm_images) if slm_changed else ()
|
|
331
|
+
|
|
332
|
+
return SequencedEvent(
|
|
333
|
+
events=tuple(self.event_batch),
|
|
334
|
+
exposure_sequence=exp_seq,
|
|
335
|
+
x_sequence=x_seq,
|
|
336
|
+
y_sequence=y_seq,
|
|
337
|
+
z_sequence=z_seq,
|
|
338
|
+
slm_sequence=slm_seq,
|
|
339
|
+
property_sequences=property_sequences,
|
|
340
|
+
properties=static_props,
|
|
341
|
+
# all other "standard" MDAEvent fields are derived from the first event
|
|
342
|
+
# the engine will use these values if the corresponding sequence is empty
|
|
343
|
+
x_pos=first_event.x_pos,
|
|
344
|
+
y_pos=first_event.y_pos,
|
|
345
|
+
z_pos=first_event.z_pos,
|
|
346
|
+
exposure=first_event.exposure,
|
|
347
|
+
channel=first_event.channel,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# -------------- helper methods to query props & max lengths ----------------
|
|
351
|
+
|
|
352
|
+
def _event_properties(self, event: MDAEvent) -> dict[tuple[str, str], Any]:
|
|
353
|
+
"""Return a dict of all property values for a given event."""
|
|
354
|
+
props: dict[tuple[str, str], Any] = {}
|
|
355
|
+
|
|
356
|
+
if (ch := event.channel) is not None:
|
|
357
|
+
props.update(self._get_channel_properties(ch))
|
|
358
|
+
if event.properties:
|
|
359
|
+
for dev, prop, val in event.properties:
|
|
360
|
+
props[(dev, prop)] = val
|
|
361
|
+
return props
|
|
362
|
+
|
|
363
|
+
def _get_channel_properties(self, ch: EventChannel) -> dict[tuple[str, str], Any]:
|
|
364
|
+
"""Get (and cache) property values for a given channel."""
|
|
365
|
+
if ch not in self._channel_props:
|
|
366
|
+
cfg = self.core.getConfigData(ch.group, ch.config, native=True)
|
|
367
|
+
data: dict[tuple[str, str], Any] = {}
|
|
368
|
+
for n in range(cfg.size()):
|
|
369
|
+
s = cfg.getSetting(n)
|
|
370
|
+
data[(s.getDeviceLabel(), s.getPropertyName())] = s.getPropertyValue()
|
|
371
|
+
self._channel_props[ch] = data
|
|
372
|
+
|
|
373
|
+
return self._channel_props[ch]
|
|
374
|
+
|
|
375
|
+
def _get_property_max_length(self, dev_prop: tuple[str, str]) -> int:
|
|
376
|
+
"""Get (and cache) the max sequence length for a given property."""
|
|
377
|
+
if dev_prop not in self._prop_lengths:
|
|
378
|
+
max_length = 0
|
|
379
|
+
with suppress(RuntimeError):
|
|
380
|
+
dev, prop = dev_prop
|
|
381
|
+
if self.core.isPropertySequenceable(dev, prop):
|
|
382
|
+
max_length = self.core.getPropertySequenceMaxLength(dev, prop)
|
|
383
|
+
self._prop_lengths[dev_prop] = max_length
|
|
384
|
+
return self._prop_lengths[dev_prop]
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _get_max_sequence_lengths(core: CMMCorePlus) -> dict[Keyword, int]:
|
|
388
|
+
max_lengths: dict[Keyword, int] = {}
|
|
389
|
+
for keyword, get_device, is_sequenceable, get_max_length in (
|
|
390
|
+
(
|
|
391
|
+
Keyword.CoreCamera,
|
|
392
|
+
core.getCameraDevice,
|
|
393
|
+
core.isExposureSequenceable,
|
|
394
|
+
core.getExposureSequenceMaxLength,
|
|
395
|
+
),
|
|
396
|
+
(
|
|
397
|
+
Keyword.CoreFocus,
|
|
398
|
+
core.getFocusDevice,
|
|
399
|
+
core.isStageSequenceable,
|
|
400
|
+
core.getStageSequenceMaxLength,
|
|
401
|
+
),
|
|
402
|
+
(
|
|
403
|
+
Keyword.CoreXYStage,
|
|
404
|
+
core.getXYStageDevice,
|
|
405
|
+
core.isXYStageSequenceable,
|
|
406
|
+
core.getXYStageSequenceMaxLength,
|
|
407
|
+
),
|
|
408
|
+
(
|
|
409
|
+
Keyword.CoreSLM,
|
|
410
|
+
core.getSLMDevice,
|
|
411
|
+
lambda _device: True, # there is no isSLMSequenceable method
|
|
412
|
+
core.getSLMSequenceMaxLength,
|
|
413
|
+
),
|
|
283
414
|
):
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
415
|
+
max_lengths[keyword] = 0
|
|
416
|
+
with suppress(RuntimeError):
|
|
417
|
+
if (device := get_device()) and is_sequenceable(device):
|
|
418
|
+
max_lengths[keyword] = get_max_length(device)
|
|
419
|
+
return max_lengths
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def can_sequence_events(core: CMMCorePlus, e1: MDAEvent, e2: MDAEvent) -> bool:
|
|
423
|
+
"""Check whether two [`useq.MDAEvent`][] are sequenceable.
|
|
424
|
+
|
|
425
|
+
!!! warning
|
|
426
|
+
This function is deprecated and should not be used in new code. It is only
|
|
427
|
+
retained for backwards compatibility.
|
|
428
|
+
"""
|
|
429
|
+
# this is an old function that simply exists to return a value in the deprecated
|
|
430
|
+
# core.canSequenceEvents method. It is not used in the current implementation
|
|
431
|
+
# and should not be used in new code.
|
|
432
|
+
combiner = EventCombiner(core)
|
|
433
|
+
combiner.feed_event(e1)
|
|
434
|
+
return combiner.can_extend(e2)
|