pymmcore-plus 0.13.0__tar.gz → 0.13.2__tar.gz
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-0.13.0 → pymmcore_plus-0.13.2}/PKG-INFO +1 -1
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/_constants.py +8 -3
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/__init__.py +2 -1
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/_engine.py +6 -1
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/_runner.py +73 -33
- pymmcore_plus-0.13.2/src/pymmcore_plus/mda/handlers/__init__.py +40 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/handlers/_img_sequence_writer.py +6 -2
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/handlers/_tensorstore_handler.py +18 -4
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mocks.py +1 -3
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/model/_config_file.py +15 -2
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/test_mda.py +42 -2
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/test_model.py +9 -14
- pymmcore_plus-0.13.0/src/pymmcore_plus/mda/handlers/__init__.py +0 -11
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/.gitignore +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/LICENSE +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/README.md +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/pyproject.toml +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/__init__.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/_benchmark.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/_build.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/_cli.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/_logger.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/_pymmcore.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/_util.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/__init__.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/_adapter.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/_config.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/_config_group.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/_device.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/_metadata.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/_mmcore_plus.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/_property.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/_sequencing.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/events/__init__.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/events/_device_signal_view.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/events/_norm_slot.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/events/_prop_event_mixin.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/events/_protocol.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/events/_psygnal.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/events/_qsignals.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/experimental/__init__.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/experimental/unicore/__init__.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/experimental/unicore/_device_manager.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/experimental/unicore/_proxy.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/experimental/unicore/_unicore.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/experimental/unicore/devices/_device.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/experimental/unicore/devices/_properties.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/experimental/unicore/devices/_stage.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/install.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/_protocol.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/_thread_relay.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/events/__init__.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/events/_protocol.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/events/_psygnal.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/events/_qsignals.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/handlers/_5d_writer_base.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/handlers/_ome_tiff_writer.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/handlers/_ome_zarr_writer.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/handlers/_util.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/metadata/__init__.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/metadata/functions.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/metadata/schema.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/metadata/serialize.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/model/__init__.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/model/_config_group.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/model/_core_device.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/model/_core_link.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/model/_device.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/model/_microscope.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/model/_pixel_size_config.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/model/_property.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/py.typed +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/seq_tester.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/__init__.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/conftest.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/io/test_image_sequence_writer.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/io/test_ome_tiff.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/io/test_zarr_writers.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/local_config.cfg +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/test_adapter_class.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/test_bench.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/test_cli.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/test_config_group_class.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/test_core.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/test_device_class.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/test_events.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/test_metadata.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/test_misc.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/test_pixel_config_class.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/test_property_class.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/test_sequencing.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/test_slm_image.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/test_thread_relay.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/unicore/test_unicore.py +0 -0
- {pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/tests/unicore/test_xy_stage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pymmcore-plus
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.2
|
|
4
4
|
Summary: pymmcore superset providing improved APIs, event handling, and a pure python acquisition engine
|
|
5
5
|
Project-URL: Source, https://github.com/pymmcore-plus/pymmcore-plus
|
|
6
6
|
Project-URL: Tracker, https://github.com/pymmcore-plus/pymmcore-plus/issues
|
|
@@ -209,10 +209,15 @@ class PortType(IntEnum):
|
|
|
209
209
|
HIDPort = pymmcore.HIDPort
|
|
210
210
|
|
|
211
211
|
|
|
212
|
+
# NB:
|
|
213
|
+
# do *not* use `pymmcore.FocusDirection...` enums here.
|
|
214
|
+
# the MMCore API does not use the device enums (which is what pymmcore exposes)
|
|
215
|
+
# but instead translates MM::FocusDirectionTowardSample into a different number:
|
|
216
|
+
# https://github.com/micro-manager/mmCoreAndDevices/tree/MMCore/MMCore.cpp#L2063-L2074
|
|
212
217
|
class FocusDirection(IntEnum):
|
|
213
|
-
Unknown =
|
|
214
|
-
TowardSample =
|
|
215
|
-
AwayFromSample =
|
|
218
|
+
Unknown = 0
|
|
219
|
+
TowardSample = 1
|
|
220
|
+
AwayFromSample = -1
|
|
216
221
|
# aliases
|
|
217
222
|
FocusDirectionUnknown = Unknown
|
|
218
223
|
FocusDirectionTowardSample = TowardSample
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from ._engine import MDAEngine
|
|
2
2
|
from ._protocol import PMDAEngine
|
|
3
|
-
from ._runner import MDARunner
|
|
3
|
+
from ._runner import MDARunner, SupportsFrameReady
|
|
4
4
|
from ._thread_relay import mda_listeners_connected
|
|
5
5
|
from .events import PMDASignaler
|
|
6
6
|
|
|
@@ -9,5 +9,6 @@ __all__ = [
|
|
|
9
9
|
"MDARunner",
|
|
10
10
|
"PMDAEngine",
|
|
11
11
|
"PMDASignaler",
|
|
12
|
+
"SupportsFrameReady",
|
|
12
13
|
"mda_listeners_connected",
|
|
13
14
|
]
|
|
@@ -248,7 +248,12 @@ class MDAEngine(PMDAEngine):
|
|
|
248
248
|
self._mmc.setExposure(event.exposure)
|
|
249
249
|
except Exception as e:
|
|
250
250
|
logger.warning("Failed to set exposure. %s", e)
|
|
251
|
-
|
|
251
|
+
if event.properties is not None:
|
|
252
|
+
try:
|
|
253
|
+
for dev, prop, value in event.properties:
|
|
254
|
+
self._mmc.setProperty(dev, prop, value)
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.warning("Failed to set properties. %s", e)
|
|
252
257
|
if (
|
|
253
258
|
# (if autoshutter wasn't set at the beginning of the sequence
|
|
254
259
|
# then it never matters...)
|
|
@@ -5,8 +5,9 @@ import warnings
|
|
|
5
5
|
from collections.abc import Iterable, Iterator, Sequence
|
|
6
6
|
from contextlib import AbstractContextManager, nullcontext
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING, Any
|
|
8
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
9
9
|
from unittest.mock import MagicMock
|
|
10
|
+
from weakref import WeakSet
|
|
10
11
|
|
|
11
12
|
from useq import MDASequence
|
|
12
13
|
|
|
@@ -17,13 +18,40 @@ from ._thread_relay import mda_listeners_connected
|
|
|
17
18
|
from .events import PMDASignaler, _get_auto_MDA_callback_class
|
|
18
19
|
|
|
19
20
|
if TYPE_CHECKING:
|
|
20
|
-
from typing import TypeAlias
|
|
21
|
+
from typing import Protocol, TypeAlias
|
|
21
22
|
|
|
23
|
+
import numpy as np
|
|
22
24
|
from useq import MDAEvent
|
|
23
25
|
|
|
26
|
+
from pymmcore_plus.metadata.schema import FrameMetaV1
|
|
27
|
+
|
|
24
28
|
from ._engine import MDAEngine
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
class FrameReady0(Protocol):
|
|
31
|
+
"""Data handler with a no-argument `frameReady` method."""
|
|
32
|
+
|
|
33
|
+
def frameReady(self) -> Any: ...
|
|
34
|
+
|
|
35
|
+
class FrameReady1(Protocol):
|
|
36
|
+
"""Data handler with a `frameReady` method that takes `(image,)` ."""
|
|
37
|
+
|
|
38
|
+
def frameReady(self, img: np.ndarray, /) -> Any: ...
|
|
39
|
+
|
|
40
|
+
class FrameReady2(Protocol):
|
|
41
|
+
"""Data handler with a `frameReady` method that takes `(image, event)`."""
|
|
42
|
+
|
|
43
|
+
def frameReady(self, img: np.ndarray, event: MDAEvent, /) -> Any: ...
|
|
44
|
+
|
|
45
|
+
class FrameReady3(Protocol):
|
|
46
|
+
"""Data handler with a `frameReady` method that takes `(image, event, meta)`."""
|
|
47
|
+
|
|
48
|
+
def frameReady(
|
|
49
|
+
self, img: np.ndarray, event: MDAEvent, meta: FrameMetaV1, /
|
|
50
|
+
) -> Any: ...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
SupportsFrameReady: TypeAlias = "FrameReady0 | FrameReady1 | FrameReady2 | FrameReady3"
|
|
54
|
+
SingleOutput: TypeAlias = "Path | str | SupportsFrameReady"
|
|
27
55
|
|
|
28
56
|
MSG = (
|
|
29
57
|
"This sequence is a placeholder for a generator of events with unknown "
|
|
@@ -67,7 +95,7 @@ class MDARunner:
|
|
|
67
95
|
self._paused = False
|
|
68
96
|
self._paused_time: float = 0
|
|
69
97
|
self._pause_interval: float = 0.1 # sec to wait between checking pause state
|
|
70
|
-
|
|
98
|
+
self._handlers: WeakSet[SupportsFrameReady] = WeakSet()
|
|
71
99
|
self._canceled = False
|
|
72
100
|
self._sequence: MDASequence | None = None
|
|
73
101
|
# timer for the full sequence, reset only once at the beginning of the sequence
|
|
@@ -189,6 +217,10 @@ class MDARunner:
|
|
|
189
217
|
- A handler object that implements the `DataHandler` protocol, currently
|
|
190
218
|
meaning it has a `frameReady` method. See `mda_listeners_connected`
|
|
191
219
|
for more details.
|
|
220
|
+
|
|
221
|
+
During the course of the sequence, the `get_output_handlers` method can be
|
|
222
|
+
used to get the currently connected output handlers (including those that
|
|
223
|
+
were created automatically based on file paths).
|
|
192
224
|
"""
|
|
193
225
|
error = None
|
|
194
226
|
sequence = events if isinstance(events, MDASequence) else GeneratorMDASequence()
|
|
@@ -206,6 +238,31 @@ class MDARunner:
|
|
|
206
238
|
if error is not None:
|
|
207
239
|
raise error
|
|
208
240
|
|
|
241
|
+
def get_output_handlers(self) -> tuple[SupportsFrameReady, ...]:
|
|
242
|
+
"""Return the data handlers that are currently connected.
|
|
243
|
+
|
|
244
|
+
Output handlers are connected by passing them to the `output` parameter of the
|
|
245
|
+
`run` method; the run method accepts objects with a `frameReady` method *or*
|
|
246
|
+
strings representing paths. If a string is passed, a handler will be created
|
|
247
|
+
internally.
|
|
248
|
+
|
|
249
|
+
This method returns a tuple of currently connected handlers, including those
|
|
250
|
+
that were explicitly passed to `run()`, as well as those that were created based
|
|
251
|
+
on file paths. Internally, handlers are held by weak references, so if you want
|
|
252
|
+
the handler to persist, you must keep a reference to it. The only guaranteed
|
|
253
|
+
API that the handler will have is the `frameReady` method, but it could be any
|
|
254
|
+
user-defined object that implements that method.
|
|
255
|
+
|
|
256
|
+
Handlers are cleared each time `run()` is called, (but not at the end
|
|
257
|
+
of the sequence).
|
|
258
|
+
|
|
259
|
+
Returns
|
|
260
|
+
-------
|
|
261
|
+
tuple[SupportsFrameReady, ...]
|
|
262
|
+
Tuple of objects that (minimally) support the `frameReady` method.
|
|
263
|
+
"""
|
|
264
|
+
return tuple(self._handlers)
|
|
265
|
+
|
|
209
266
|
def seconds_elapsed(self) -> float:
|
|
210
267
|
"""Return the number of seconds since the start of the acquisition."""
|
|
211
268
|
return time.perf_counter() - self._sequence_t0
|
|
@@ -228,48 +285,31 @@ class MDARunner:
|
|
|
228
285
|
if isinstance(output, (str, Path)) or not isinstance(output, Sequence):
|
|
229
286
|
output = [output]
|
|
230
287
|
|
|
231
|
-
# convert all items to handler objects
|
|
232
|
-
|
|
288
|
+
# convert all items to handler objects, preserving order
|
|
289
|
+
_handlers: list[SupportsFrameReady] = []
|
|
233
290
|
for item in output:
|
|
234
291
|
if isinstance(item, (str, Path)):
|
|
235
|
-
|
|
292
|
+
_handlers.append(self._handler_for_path(item))
|
|
236
293
|
else:
|
|
237
|
-
|
|
238
|
-
# quick hack for now.
|
|
239
|
-
if not hasattr(item, "frameReady"):
|
|
294
|
+
if not callable(getattr(item, "frameReady", None)):
|
|
240
295
|
raise TypeError(
|
|
241
|
-
"Output handlers must have a frameReady method. "
|
|
296
|
+
"Output handlers must have a callable frameReady method. "
|
|
242
297
|
f"Got {item} with type {type(item)}."
|
|
243
298
|
)
|
|
244
|
-
|
|
299
|
+
_handlers.append(item)
|
|
245
300
|
|
|
246
|
-
|
|
301
|
+
self._handlers.clear()
|
|
302
|
+
self._handlers.update(_handlers)
|
|
303
|
+
return mda_listeners_connected(*_handlers, mda_events=self._signals)
|
|
247
304
|
|
|
248
|
-
def _handler_for_path(self, path: str | Path) ->
|
|
305
|
+
def _handler_for_path(self, path: str | Path) -> SupportsFrameReady:
|
|
249
306
|
"""Convert a string or Path into a handler object.
|
|
250
307
|
|
|
251
308
|
This method picks from the built-in handlers based on the extension of the path.
|
|
252
309
|
"""
|
|
253
|
-
|
|
254
|
-
if path.endswith(".zarr"):
|
|
255
|
-
from pymmcore_plus.mda.handlers import OMEZarrWriter
|
|
256
|
-
|
|
257
|
-
return OMEZarrWriter(path)
|
|
258
|
-
|
|
259
|
-
if path.endswith((".tiff", ".tif")):
|
|
260
|
-
from pymmcore_plus.mda.handlers import OMETiffWriter
|
|
261
|
-
|
|
262
|
-
return OMETiffWriter(path)
|
|
263
|
-
|
|
264
|
-
# FIXME: ugly hack for the moment to represent a non-existent directory
|
|
265
|
-
# there are many features that ImageSequenceWriter supports, and it's unclear
|
|
266
|
-
# how to infer them all from a single string.
|
|
267
|
-
if not (Path(path).suffix or Path(path).exists()):
|
|
268
|
-
from pymmcore_plus.mda.handlers import ImageSequenceWriter
|
|
269
|
-
|
|
270
|
-
return ImageSequenceWriter(path)
|
|
310
|
+
from pymmcore_plus.mda.handlers import handler_for_path
|
|
271
311
|
|
|
272
|
-
|
|
312
|
+
return cast("SupportsFrameReady", handler_for_path(path))
|
|
273
313
|
|
|
274
314
|
def _run(self, engine: PMDAEngine, events: Iterable[MDAEvent]) -> None:
|
|
275
315
|
"""Main execution of events, inside the try/except block of `run`."""
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ._img_sequence_writer import ImageSequenceWriter
|
|
6
|
+
from ._ome_tiff_writer import OMETiffWriter
|
|
7
|
+
from ._ome_zarr_writer import OMEZarrWriter
|
|
8
|
+
from ._tensorstore_handler import TensorStoreHandler
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ImageSequenceWriter",
|
|
12
|
+
"OMETiffWriter",
|
|
13
|
+
"OMEZarrWriter",
|
|
14
|
+
"TensorStoreHandler",
|
|
15
|
+
"handler_for_path",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def handler_for_path(path: str | Path) -> object:
|
|
20
|
+
"""Convert a string or Path into a handler object.
|
|
21
|
+
|
|
22
|
+
This method picks from the built-in handlers based on the extension of the path.
|
|
23
|
+
"""
|
|
24
|
+
if str(path).rstrip("/").rstrip(":").lower() == "memory":
|
|
25
|
+
return TensorStoreHandler(kvstore="memory://")
|
|
26
|
+
|
|
27
|
+
path = str(Path(path).expanduser().resolve())
|
|
28
|
+
if path.endswith(".zarr"):
|
|
29
|
+
return OMEZarrWriter(path)
|
|
30
|
+
|
|
31
|
+
if path.endswith((".tiff", ".tif")):
|
|
32
|
+
return OMETiffWriter(path)
|
|
33
|
+
|
|
34
|
+
# FIXME: ugly hack for the moment to represent a non-existent directory
|
|
35
|
+
# there are many features that ImageSequenceWriter supports, and it's unclear
|
|
36
|
+
# how to infer them all from a single string.
|
|
37
|
+
if not (Path(path).suffix or Path(path).exists()):
|
|
38
|
+
return ImageSequenceWriter(path)
|
|
39
|
+
|
|
40
|
+
raise ValueError(f"Could not infer a writer handler for path: '{path}'")
|
{pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/handlers/_img_sequence_writer.py
RENAMED
|
@@ -22,6 +22,8 @@ if TYPE_CHECKING:
|
|
|
22
22
|
import useq
|
|
23
23
|
from typing_extensions import TypeAlias # py310
|
|
24
24
|
|
|
25
|
+
from pymmcore_plus.metadata.schema import FrameMetaV1
|
|
26
|
+
|
|
25
27
|
ImgWriter: TypeAlias = Callable[[str, npt.NDArray], Any]
|
|
26
28
|
|
|
27
29
|
FRAME_KEY = "frame"
|
|
@@ -118,7 +120,7 @@ class ImageSequenceWriter:
|
|
|
118
120
|
shutil.rmtree(self._directory)
|
|
119
121
|
|
|
120
122
|
# ongoing dict of frame meta... stored for easy rewrite without reading
|
|
121
|
-
self._frame_metadata: dict[str,
|
|
123
|
+
self._frame_metadata: dict[str, FrameMetaV1] = {}
|
|
122
124
|
self._frame_meta_file = self._directory.joinpath(self.FRAME_META_PATH)
|
|
123
125
|
self._seq_meta_file = self._directory.joinpath(self.SEQ_META_PATH)
|
|
124
126
|
|
|
@@ -186,7 +188,9 @@ class ImageSequenceWriter:
|
|
|
186
188
|
# write final frame metadata to disk
|
|
187
189
|
self._frame_meta_file.write_bytes(json_dumps(self._frame_metadata, indent=2))
|
|
188
190
|
|
|
189
|
-
def frameReady(
|
|
191
|
+
def frameReady(
|
|
192
|
+
self, frame: np.ndarray, event: useq.MDAEvent, meta: FrameMetaV1, /
|
|
193
|
+
) -> None:
|
|
190
194
|
"""Write a frame to disk."""
|
|
191
195
|
frame_idx = next(self._counter)
|
|
192
196
|
if self._name_template:
|
{pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/handlers/_tensorstore_handler.py
RENAMED
|
@@ -111,6 +111,8 @@ class TensorStoreHandler:
|
|
|
111
111
|
self.delete_existing = delete_existing
|
|
112
112
|
self.spec = spec
|
|
113
113
|
|
|
114
|
+
self._current_sequence: useq.MDASequence | None = None
|
|
115
|
+
|
|
114
116
|
# storage of individual frame metadata
|
|
115
117
|
# maps position key to list of frame metadata
|
|
116
118
|
self.frame_metadatas: list[tuple[useq.MDAEvent, FrameMetaV1]] = []
|
|
@@ -176,13 +178,25 @@ class TensorStoreHandler:
|
|
|
176
178
|
|
|
177
179
|
return cls(path=path, **kwargs)
|
|
178
180
|
|
|
179
|
-
def
|
|
180
|
-
"""
|
|
181
|
+
def reset(self, sequence: useq.MDASequence) -> None:
|
|
182
|
+
"""Reset state to prepare for new `sequence`."""
|
|
181
183
|
self._frame_index = 0
|
|
182
184
|
self._store = None
|
|
183
185
|
self._futures.clear()
|
|
184
186
|
self.frame_metadatas.clear()
|
|
185
|
-
self.
|
|
187
|
+
self._current_sequence = sequence
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def current_sequence(self) -> useq.MDASequence | None:
|
|
191
|
+
"""Return current sequence, or none.
|
|
192
|
+
|
|
193
|
+
Use `.reset()` to initialize the handler for a new sequence.
|
|
194
|
+
"""
|
|
195
|
+
return self._current_sequence
|
|
196
|
+
|
|
197
|
+
def sequenceStarted(self, seq: useq.MDASequence, meta: SummaryMetaV1) -> None:
|
|
198
|
+
"""On sequence started, simply store the sequence."""
|
|
199
|
+
self.reset(seq)
|
|
186
200
|
|
|
187
201
|
def sequenceFinished(self, seq: useq.MDASequence) -> None:
|
|
188
202
|
"""On sequence finished, clear the current sequence."""
|
|
@@ -199,7 +213,7 @@ class TensorStoreHandler:
|
|
|
199
213
|
self.finalize_metadata()
|
|
200
214
|
|
|
201
215
|
def frameReady(
|
|
202
|
-
self, frame: np.ndarray, event: useq.MDAEvent, meta: FrameMetaV1
|
|
216
|
+
self, frame: np.ndarray, event: useq.MDAEvent, meta: FrameMetaV1, /
|
|
203
217
|
) -> None:
|
|
204
218
|
"""Write frame to the zarr array for the appropriate position."""
|
|
205
219
|
if self._store is None:
|
|
@@ -28,9 +28,7 @@ class MockSequenceableCore(MagicMock):
|
|
|
28
28
|
self.loadExposureSequence.return_value = None
|
|
29
29
|
self.loadStageSequence.return_value = None
|
|
30
30
|
self.loadXYStageSequence.return_value = None
|
|
31
|
-
|
|
32
|
-
if hasattr(CMMCorePlus, "loadSLMSequence"):
|
|
33
|
-
self.loadSLMSequence.return_value = None
|
|
31
|
+
self.loadSLMSequence.return_value = None
|
|
34
32
|
|
|
35
33
|
self.loadPropertySequence.return_value = None
|
|
36
34
|
|
|
@@ -209,18 +209,28 @@ def run_command(line: str, scope: Microscope) -> None:
|
|
|
209
209
|
return
|
|
210
210
|
|
|
211
211
|
exec_cmd, expected_n_args = COMMAND_EXECUTORS[command]
|
|
212
|
+
should_raise = command in SHOULD_RAISE
|
|
212
213
|
|
|
213
214
|
if (nargs := len(args) + 1) not in expected_n_args:
|
|
214
215
|
exp_str = " or ".join(map(str, expected_n_args))
|
|
215
|
-
|
|
216
|
+
msg = (
|
|
216
217
|
f"Invalid configuration line encountered for command {cmd_name}. "
|
|
217
218
|
f"Expected {exp_str} arguments, got {nargs}: {line!r}"
|
|
218
219
|
)
|
|
220
|
+
if should_raise:
|
|
221
|
+
raise ValueError(msg)
|
|
222
|
+
else:
|
|
223
|
+
warnings.warn(msg, RuntimeWarning, stacklevel=2)
|
|
224
|
+
return
|
|
219
225
|
|
|
220
226
|
try:
|
|
221
227
|
exec_cmd(scope, args)
|
|
222
228
|
except Exception as exc:
|
|
223
|
-
|
|
229
|
+
if should_raise:
|
|
230
|
+
raise ValueError(f"Error executing command {line!r}: {exc}") from exc
|
|
231
|
+
warnings.warn(
|
|
232
|
+
f"Failed to execute command {line!r}: {exc}", RuntimeWarning, stacklevel=2
|
|
233
|
+
)
|
|
224
234
|
|
|
225
235
|
|
|
226
236
|
def _exec_Device(scope: Microscope, args: Sequence[str]) -> None:
|
|
@@ -352,3 +362,6 @@ COMMAND_EXECUTORS: dict[CFGCommand, tuple[Executor, set[int]]] = {
|
|
|
352
362
|
CFGCommand.ParentID: (_exec_ParentID, {3}),
|
|
353
363
|
CFGCommand.FocusDirection: (_exec_FocusDirection, {3}),
|
|
354
364
|
}
|
|
365
|
+
|
|
366
|
+
# Commands that should raise when fail
|
|
367
|
+
SHOULD_RAISE = {CFGCommand.Device, CFGCommand.Property}
|
|
@@ -49,19 +49,33 @@ def test_mda_waiting(core: CMMCorePlus) -> None:
|
|
|
49
49
|
|
|
50
50
|
def test_setting_position(core: CMMCorePlus) -> None:
|
|
51
51
|
core.mda._running = True
|
|
52
|
-
event1 = MDAEvent(
|
|
52
|
+
event1 = MDAEvent(
|
|
53
|
+
exposure=123,
|
|
54
|
+
x_pos=123,
|
|
55
|
+
y_pos=456,
|
|
56
|
+
z_pos=1,
|
|
57
|
+
properties=[("Camera", "TestProperty1", 0.05)],
|
|
58
|
+
)
|
|
53
59
|
core.mda.engine.setup_event(event1)
|
|
54
60
|
assert tuple(core.getXYPosition()) == (123, 456)
|
|
55
61
|
assert core.getPosition() == 1
|
|
56
62
|
assert core.getExposure() == 123
|
|
63
|
+
assert core.getProperty("Camera", "TestProperty1") == "0.0500"
|
|
57
64
|
|
|
58
65
|
# check that we aren't check things like: if event.x_pos
|
|
59
66
|
# because then we will not set to zero
|
|
60
|
-
event2 = MDAEvent(
|
|
67
|
+
event2 = MDAEvent(
|
|
68
|
+
exposure=321,
|
|
69
|
+
x_pos=0,
|
|
70
|
+
y_pos=0,
|
|
71
|
+
z_pos=0,
|
|
72
|
+
properties=[("Camera", "TestProperty2", -0.07)],
|
|
73
|
+
)
|
|
61
74
|
core.mda.engine.setup_event(event2)
|
|
62
75
|
assert tuple(core.getXYPosition()) == (0, 0)
|
|
63
76
|
assert core.getPosition() == 0
|
|
64
77
|
assert core.getExposure() == 321
|
|
78
|
+
assert core.getProperty("Camera", "TestProperty2") == "-0.0700"
|
|
65
79
|
|
|
66
80
|
|
|
67
81
|
class BrokenEngine:
|
|
@@ -450,3 +464,29 @@ def test_queue_mda(core: CMMCorePlus) -> None:
|
|
|
450
464
|
# make sure that the engine's iterator was NOT used when running an iter(Queue)
|
|
451
465
|
mock_engine.event_iterator.assert_not_called()
|
|
452
466
|
assert mock_engine.setup_event.call_count == 2
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def test_get_handlers(core: CMMCorePlus) -> None:
|
|
470
|
+
"""Test that we can get the handlers"""
|
|
471
|
+
runner = core.mda
|
|
472
|
+
|
|
473
|
+
assert not runner.get_output_handlers()
|
|
474
|
+
on_start_names: list[str] = []
|
|
475
|
+
on_finish_names: list[str] = []
|
|
476
|
+
|
|
477
|
+
@runner.events.sequenceStarted.connect
|
|
478
|
+
def _on_start() -> None:
|
|
479
|
+
on_start_names.extend([type(h).__name__ for h in runner.get_output_handlers()])
|
|
480
|
+
|
|
481
|
+
@runner.events.sequenceFinished.connect
|
|
482
|
+
def _on_end() -> None:
|
|
483
|
+
on_finish_names.extend([type(h).__name__ for h in runner.get_output_handlers()])
|
|
484
|
+
|
|
485
|
+
runner.run([MDAEvent()], output="memory://")
|
|
486
|
+
|
|
487
|
+
# weakref is used to store the handlers,
|
|
488
|
+
# handlers should be cleared after the sequence is finished
|
|
489
|
+
assert not runner.get_output_handlers()
|
|
490
|
+
# but they should have been available during start and finish events
|
|
491
|
+
assert on_start_names == ["TensorStoreHandler"]
|
|
492
|
+
assert on_finish_names == ["TensorStoreHandler"]
|
|
@@ -4,8 +4,6 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
import pymmcore_plus
|
|
8
|
-
import pymmcore_plus._pymmcore
|
|
9
7
|
from pymmcore_plus import CMMCorePlus, DeviceType, find_micromanager
|
|
10
8
|
from pymmcore_plus.metadata import summary_metadata
|
|
11
9
|
from pymmcore_plus.model import CoreDevice, Device, Microscope
|
|
@@ -142,46 +140,47 @@ def test_load_errors() -> None:
|
|
|
142
140
|
model.load_config("Property,A,B,C,D,E")
|
|
143
141
|
with pytest.raises(ValueError, match="not an integer"):
|
|
144
142
|
model.load_config("Property,Core,Initialize,NotAnInt")
|
|
145
|
-
|
|
143
|
+
model.load_config("Device,Dichroic,DemoCamera,DWheel") # fine
|
|
144
|
+
with pytest.warns(RuntimeWarning, match="not an integer"):
|
|
146
145
|
model.load_config(
|
|
147
146
|
"""
|
|
148
147
|
Device,Dichroic,DemoCamera,DWheel
|
|
149
148
|
Label,Dichroic,NotAnInt,Q505LP
|
|
150
149
|
"""
|
|
151
150
|
)
|
|
152
|
-
with pytest.
|
|
151
|
+
with pytest.warns(RuntimeWarning, match="'NotAPreset' not found"):
|
|
153
152
|
model.load_config("PixelSize_um,NotAPreset,0.5")
|
|
154
|
-
with pytest.
|
|
153
|
+
with pytest.warns(RuntimeWarning, match="Expected a float"):
|
|
155
154
|
model.load_config(
|
|
156
155
|
"""
|
|
157
156
|
ConfigPixelSize,Res40x,Objective,Label,Nikon 40X Plan Flueor ELWD
|
|
158
157
|
PixelSize_um,Res40x,NotAFloat
|
|
159
158
|
"""
|
|
160
159
|
)
|
|
161
|
-
with pytest.
|
|
160
|
+
with pytest.warns(RuntimeWarning, match="'Res10x' not found"):
|
|
162
161
|
model.load_config("PixelSizeAffine,Res10x,1.0,0.0,0.0,0.0,1.1,0.0")
|
|
163
|
-
with pytest.
|
|
162
|
+
with pytest.warns(RuntimeWarning, match="Expected 8 arguments, got 5"):
|
|
164
163
|
model.load_config(
|
|
165
164
|
"""
|
|
166
165
|
ConfigPixelSize,Res40x,Objective,Label,Nikon 40X Plan Flueor ELWD
|
|
167
166
|
PixelSizeAffine,Res40x,1.0,0.0,0.0
|
|
168
167
|
"""
|
|
169
168
|
)
|
|
170
|
-
with pytest.
|
|
169
|
+
with pytest.warns(RuntimeWarning, match="Expected 6 floats"):
|
|
171
170
|
model.load_config(
|
|
172
171
|
"""
|
|
173
172
|
ConfigPixelSize,Res40x,Objective,Label,Nikon 40X Plan Flueor ELWD
|
|
174
173
|
PixelSizeAffine,Res40x,1.0,0.0,0.0,0.0,1.1,NoFloat
|
|
175
174
|
"""
|
|
176
175
|
)
|
|
177
|
-
with pytest.
|
|
176
|
+
with pytest.warns(RuntimeWarning, match="Expected a float"):
|
|
178
177
|
model.load_config(
|
|
179
178
|
"""
|
|
180
179
|
Device,Shutter,DemoCamera,DShutter
|
|
181
180
|
Delay,Shutter,NotAFloat
|
|
182
181
|
"""
|
|
183
182
|
)
|
|
184
|
-
with pytest.
|
|
183
|
+
with pytest.warns(RuntimeWarning, match="not a valid FocusDirection"):
|
|
185
184
|
model.load_config(
|
|
186
185
|
"""
|
|
187
186
|
Device,Z,DemoCamera,DStage
|
|
@@ -201,10 +200,6 @@ def test_scope_errs():
|
|
|
201
200
|
Microscope(devices=[CoreDevice()])
|
|
202
201
|
|
|
203
202
|
|
|
204
|
-
@pytest.mark.skipif(
|
|
205
|
-
pymmcore_plus._pymmcore.BACKEND == "pymmcore-nano",
|
|
206
|
-
reason="This is still hanging on pymmcore-nano",
|
|
207
|
-
)
|
|
208
203
|
def test_apply():
|
|
209
204
|
core1 = CMMCorePlus()
|
|
210
205
|
core1.loadSystemConfiguration()
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
from ._img_sequence_writer import ImageSequenceWriter
|
|
2
|
-
from ._ome_tiff_writer import OMETiffWriter
|
|
3
|
-
from ._ome_zarr_writer import OMEZarrWriter
|
|
4
|
-
from ._tensorstore_handler import TensorStoreHandler
|
|
5
|
-
|
|
6
|
-
__all__ = [
|
|
7
|
-
"ImageSequenceWriter",
|
|
8
|
-
"OMETiffWriter",
|
|
9
|
-
"OMEZarrWriter",
|
|
10
|
-
"TensorStoreHandler",
|
|
11
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/events/_device_signal_view.py
RENAMED
|
File without changes
|
|
File without changes
|
{pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/core/events/_prop_event_mixin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/experimental/unicore/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/experimental/unicore/_proxy.py
RENAMED
|
File without changes
|
{pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/experimental/unicore/_unicore.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/handlers/_5d_writer_base.py
RENAMED
|
File without changes
|
{pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/handlers/_ome_tiff_writer.py
RENAMED
|
File without changes
|
{pymmcore_plus-0.13.0 → pymmcore_plus-0.13.2}/src/pymmcore_plus/mda/handlers/_ome_zarr_writer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|