pymmcore-plus 0.9.3__py3-none-any.whl → 0.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pymmcore_plus/__init__.py +7 -4
- pymmcore_plus/_benchmark.py +203 -0
- pymmcore_plus/_build.py +6 -1
- pymmcore_plus/_cli.py +131 -31
- pymmcore_plus/_logger.py +19 -10
- pymmcore_plus/_pymmcore.py +12 -0
- pymmcore_plus/_util.py +139 -32
- pymmcore_plus/core/__init__.py +5 -0
- pymmcore_plus/core/_config.py +6 -4
- pymmcore_plus/core/_config_group.py +4 -3
- pymmcore_plus/core/_constants.py +135 -10
- pymmcore_plus/core/_device.py +4 -4
- pymmcore_plus/core/_metadata.py +3 -3
- pymmcore_plus/core/_mmcore_plus.py +254 -170
- pymmcore_plus/core/_property.py +6 -6
- pymmcore_plus/core/_sequencing.py +370 -233
- pymmcore_plus/core/events/__init__.py +6 -6
- pymmcore_plus/core/events/_device_signal_view.py +8 -6
- pymmcore_plus/core/events/_norm_slot.py +2 -4
- pymmcore_plus/core/events/_prop_event_mixin.py +7 -4
- pymmcore_plus/core/events/_protocol.py +5 -2
- pymmcore_plus/core/events/_psygnal.py +2 -2
- pymmcore_plus/experimental/__init__.py +0 -0
- pymmcore_plus/experimental/unicore/__init__.py +14 -0
- pymmcore_plus/experimental/unicore/_device_manager.py +173 -0
- pymmcore_plus/experimental/unicore/_proxy.py +127 -0
- pymmcore_plus/experimental/unicore/_unicore.py +703 -0
- pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
- pymmcore_plus/experimental/unicore/devices/_device.py +269 -0
- pymmcore_plus/experimental/unicore/devices/_properties.py +400 -0
- pymmcore_plus/experimental/unicore/devices/_stage.py +221 -0
- pymmcore_plus/install.py +16 -11
- pymmcore_plus/mda/__init__.py +1 -1
- pymmcore_plus/mda/_engine.py +320 -148
- pymmcore_plus/mda/_protocol.py +6 -4
- pymmcore_plus/mda/_runner.py +62 -51
- pymmcore_plus/mda/_thread_relay.py +5 -3
- pymmcore_plus/mda/events/__init__.py +2 -2
- pymmcore_plus/mda/events/_protocol.py +10 -2
- pymmcore_plus/mda/events/_psygnal.py +2 -2
- pymmcore_plus/mda/handlers/_5d_writer_base.py +106 -15
- pymmcore_plus/mda/handlers/__init__.py +7 -1
- pymmcore_plus/mda/handlers/_img_sequence_writer.py +11 -6
- pymmcore_plus/mda/handlers/_ome_tiff_writer.py +8 -4
- pymmcore_plus/mda/handlers/_ome_zarr_writer.py +82 -9
- pymmcore_plus/mda/handlers/_tensorstore_handler.py +374 -0
- pymmcore_plus/mda/handlers/_util.py +1 -1
- pymmcore_plus/metadata/__init__.py +36 -0
- pymmcore_plus/metadata/functions.py +353 -0
- pymmcore_plus/metadata/schema.py +472 -0
- pymmcore_plus/metadata/serialize.py +120 -0
- pymmcore_plus/mocks.py +51 -0
- pymmcore_plus/model/_config_file.py +5 -6
- pymmcore_plus/model/_config_group.py +29 -2
- pymmcore_plus/model/_core_device.py +12 -1
- pymmcore_plus/model/_core_link.py +2 -1
- pymmcore_plus/model/_device.py +39 -8
- pymmcore_plus/model/_microscope.py +39 -3
- pymmcore_plus/model/_pixel_size_config.py +27 -4
- pymmcore_plus/model/_property.py +13 -3
- pymmcore_plus/seq_tester.py +1 -1
- {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/METADATA +22 -12
- pymmcore_plus-0.13.0.dist-info/RECORD +71 -0
- {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/WHEEL +1 -1
- pymmcore_plus/core/_state.py +0 -244
- pymmcore_plus-0.9.3.dist-info/RECORD +0 -55
- {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -10,27 +10,18 @@ from collections import defaultdict
|
|
|
10
10
|
from contextlib import contextmanager, suppress
|
|
11
11
|
from datetime import datetime
|
|
12
12
|
from pathlib import Path
|
|
13
|
+
from re import Pattern
|
|
13
14
|
from textwrap import dedent
|
|
14
|
-
from threading import
|
|
15
|
-
from typing import
|
|
16
|
-
TYPE_CHECKING,
|
|
17
|
-
Any,
|
|
18
|
-
Callable,
|
|
19
|
-
Iterable,
|
|
20
|
-
Iterator,
|
|
21
|
-
NamedTuple,
|
|
22
|
-
Pattern,
|
|
23
|
-
Sequence,
|
|
24
|
-
TypeVar,
|
|
25
|
-
overload,
|
|
26
|
-
)
|
|
15
|
+
from threading import Thread
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Callable, NamedTuple, TypeVar, overload
|
|
27
17
|
|
|
28
|
-
import pymmcore
|
|
29
18
|
from psygnal import SignalInstance
|
|
30
19
|
|
|
20
|
+
import pymmcore_plus._pymmcore as pymmcore
|
|
31
21
|
from pymmcore_plus._logger import current_logfile, logger
|
|
32
22
|
from pymmcore_plus._util import find_micromanager, print_tabular_data
|
|
33
23
|
from pymmcore_plus.mda import MDAEngine, MDARunner, PMDAEngine
|
|
24
|
+
from pymmcore_plus.metadata.functions import summary_metadata
|
|
34
25
|
|
|
35
26
|
from ._adapter import DeviceAdapter
|
|
36
27
|
from ._config import Configuration
|
|
@@ -39,28 +30,26 @@ from ._constants import (
|
|
|
39
30
|
DeviceDetectionStatus,
|
|
40
31
|
DeviceInitializationState,
|
|
41
32
|
DeviceType,
|
|
33
|
+
FocusDirection,
|
|
42
34
|
PixelType,
|
|
43
35
|
PropertyType,
|
|
44
36
|
)
|
|
45
37
|
from ._device import Device
|
|
46
38
|
from ._metadata import Metadata
|
|
47
39
|
from ._property import DeviceProperty
|
|
48
|
-
from ._sequencing import can_sequence_events
|
|
49
|
-
from ._state import core_state
|
|
50
40
|
from .events import CMMCoreSignaler, PCoreSignaler, _get_auto_core_callback_class
|
|
51
41
|
|
|
52
42
|
if TYPE_CHECKING:
|
|
53
|
-
from
|
|
43
|
+
from collections.abc import Iterable, Iterator, Sequence
|
|
44
|
+
from typing import Literal, TypedDict, Unpack
|
|
54
45
|
|
|
55
46
|
import numpy as np
|
|
56
47
|
from useq import MDAEvent
|
|
57
48
|
|
|
58
49
|
from pymmcore_plus.mda._runner import SingleOutput
|
|
59
|
-
|
|
60
|
-
from ._state import StateDict
|
|
50
|
+
from pymmcore_plus.metadata.schema import SummaryMetaV1
|
|
61
51
|
|
|
62
52
|
_T = TypeVar("_T")
|
|
63
|
-
_F = TypeVar("_F", bound=Callable[..., Any])
|
|
64
53
|
ListOrTuple = list[_T] | tuple[_T, ...]
|
|
65
54
|
|
|
66
55
|
class PropertySchema(TypedDict, total=False):
|
|
@@ -84,10 +73,43 @@ if TYPE_CHECKING:
|
|
|
84
73
|
type: str
|
|
85
74
|
properties: dict[str, PropertySchema]
|
|
86
75
|
|
|
87
|
-
|
|
76
|
+
class SetContextKwargs(TypedDict, total=False):
|
|
77
|
+
"""All the valid keywords and their types for the `setContext` method."""
|
|
78
|
+
|
|
79
|
+
autoFocusDevice: str
|
|
80
|
+
autoFocusOffset: float
|
|
81
|
+
autoShutter: bool
|
|
82
|
+
cameraDevice: str
|
|
83
|
+
channelGroup: str
|
|
84
|
+
circularBufferMemoryFootprint: int
|
|
85
|
+
deviceAdapterSearchPaths: list[str]
|
|
86
|
+
deviceDelayMs: tuple[str, float]
|
|
87
|
+
exposure: float | tuple[str, float]
|
|
88
|
+
focusDevice: str
|
|
89
|
+
focusDirection: str
|
|
90
|
+
galvoDevice: str
|
|
91
|
+
galvoPosition: tuple[str, float, float]
|
|
92
|
+
imageProcessorDevice: str
|
|
93
|
+
multiROI: tuple[list[int], list[int], list[int], list[int]]
|
|
94
|
+
parentLabel: tuple[str, str]
|
|
95
|
+
pixelSizeAffine: tuple[str, list[float]]
|
|
96
|
+
pixelSizeUm: tuple[str, float]
|
|
97
|
+
position: float | tuple[str, float]
|
|
98
|
+
primaryLogFile: str | tuple[str, bool]
|
|
99
|
+
property: tuple[str, str, bool | float | int | str]
|
|
100
|
+
ROI: tuple[int, int, int, int] | tuple[str, int, int, int, int]
|
|
101
|
+
SLMDevice: str
|
|
102
|
+
SLMExposure: tuple[str, float]
|
|
103
|
+
shutterDevice: str
|
|
104
|
+
shutterOpen: bool | tuple[str, bool]
|
|
105
|
+
state: tuple[str, int]
|
|
106
|
+
stateLabel: tuple[str, str]
|
|
107
|
+
systemState: pymmcore.Configuration
|
|
108
|
+
timeoutMs: int
|
|
109
|
+
XYPosition: tuple[float, float] | tuple[str, float, float]
|
|
110
|
+
XYStageDevice: str
|
|
111
|
+
ZPosition: float | tuple[str, float]
|
|
88
112
|
|
|
89
|
-
else:
|
|
90
|
-
from wrapt import synchronized
|
|
91
113
|
|
|
92
114
|
_OBJDEV_REGEX = re.compile("(.+)?(nosepiece|obj(ective)?)(turret)?s?", re.IGNORECASE)
|
|
93
115
|
_CHANNEL_REGEX = re.compile("(chan{1,2}(el)?|filt(er)?)s?", re.IGNORECASE)
|
|
@@ -153,8 +175,6 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
153
175
|
Paths to search for device adapters, by default ()
|
|
154
176
|
"""
|
|
155
177
|
|
|
156
|
-
_lock = RLock()
|
|
157
|
-
|
|
158
178
|
@classmethod
|
|
159
179
|
def instance(cls) -> CMMCorePlus:
|
|
160
180
|
"""Return the global singleton instance of `CMMCorePlus`.
|
|
@@ -193,6 +213,9 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
193
213
|
if _instance is None:
|
|
194
214
|
_instance = self
|
|
195
215
|
|
|
216
|
+
if hasattr("self", "enableFeature"):
|
|
217
|
+
self.enableFeature("StrictInitializationChecks", True)
|
|
218
|
+
|
|
196
219
|
# TODO: test this on windows ... writing to the same file may be an issue there
|
|
197
220
|
if logfile := current_logfile(logger):
|
|
198
221
|
self.setPrimaryLogFile(str(logfile))
|
|
@@ -239,10 +262,11 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
239
262
|
if hasattr(self, "_weak_clean"):
|
|
240
263
|
atexit.unregister(self._weak_clean)
|
|
241
264
|
self.unloadAllDevices()
|
|
265
|
+
# clean up logging
|
|
266
|
+
self.setPrimaryLogFile("")
|
|
242
267
|
|
|
243
268
|
# Re-implemented methods from the CMMCore API
|
|
244
269
|
|
|
245
|
-
@synchronized(_lock)
|
|
246
270
|
def setProperty(
|
|
247
271
|
self, label: str, propName: str, propValue: bool | float | int | str
|
|
248
272
|
) -> None:
|
|
@@ -257,7 +281,6 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
257
281
|
with self._property_change_emission_ensured(label, (propName,)):
|
|
258
282
|
super().setProperty(label, propName, propValue)
|
|
259
283
|
|
|
260
|
-
@synchronized(_lock)
|
|
261
284
|
def setState(self, stateDeviceLabel: str, state: int) -> None:
|
|
262
285
|
"""Set state (by position) on `stateDeviceLabel`, with reliable event emission.
|
|
263
286
|
|
|
@@ -270,7 +293,6 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
270
293
|
with self._property_change_emission_ensured(stateDeviceLabel, STATE_PROPS):
|
|
271
294
|
super().setState(stateDeviceLabel, state)
|
|
272
295
|
|
|
273
|
-
@synchronized(_lock)
|
|
274
296
|
def setStateLabel(self, stateDeviceLabel: str, stateLabel: str) -> None:
|
|
275
297
|
"""Set state (by label) on `stateDeviceLabel`, with reliable event emission.
|
|
276
298
|
|
|
@@ -327,33 +349,37 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
327
349
|
try:
|
|
328
350
|
super().loadDevice(label, moduleName, deviceName)
|
|
329
351
|
except RuntimeError as e:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
352
|
+
if exc := self._load_error_with_info(label, moduleName, deviceName, str(e)):
|
|
353
|
+
raise exc from e
|
|
354
|
+
|
|
355
|
+
def _load_error_with_info(
|
|
356
|
+
self, label: str, moduleName: str, deviceName: str, msg: str = ""
|
|
357
|
+
) -> RuntimeError | None:
|
|
358
|
+
if label in self.getLoadedDevices():
|
|
359
|
+
lib = super().getDeviceLibrary(label)
|
|
360
|
+
name = super().getDeviceName(label)
|
|
361
|
+
if moduleName == lib and deviceName == name:
|
|
362
|
+
msg += f". Device {label!r} appears to be loaded already."
|
|
363
|
+
warnings.warn(msg, stacklevel=2)
|
|
364
|
+
return None
|
|
365
|
+
|
|
366
|
+
msg += f". Device {label!r} is already taken by {lib}::{name}"
|
|
367
|
+
else:
|
|
368
|
+
adapters = super().getDeviceAdapterNames()
|
|
369
|
+
if moduleName not in adapters:
|
|
370
|
+
msg += (
|
|
371
|
+
f". Adapter name {moduleName!r} not in list of known adapter "
|
|
372
|
+
f"names: {adapters}."
|
|
373
|
+
)
|
|
340
374
|
else:
|
|
341
|
-
|
|
342
|
-
if
|
|
375
|
+
devices = super().getAvailableDevices(moduleName)
|
|
376
|
+
if deviceName not in devices:
|
|
343
377
|
msg += (
|
|
344
|
-
f".
|
|
345
|
-
f"
|
|
378
|
+
f". Device name {deviceName!r} not in devices provided by "
|
|
379
|
+
f"adapter {moduleName!r}: {devices}"
|
|
346
380
|
)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if deviceName not in devices:
|
|
350
|
-
msg += (
|
|
351
|
-
f". Device name {deviceName!r} not in devices provided by "
|
|
352
|
-
f"adapter {moduleName!r}: {devices}"
|
|
353
|
-
)
|
|
354
|
-
raise RuntimeError(msg) from e
|
|
355
|
-
|
|
356
|
-
@synchronized(_lock)
|
|
381
|
+
return RuntimeError(msg)
|
|
382
|
+
|
|
357
383
|
def loadSystemConfiguration(
|
|
358
384
|
self, fileName: str | Path = "MMConfig_demo.cfg"
|
|
359
385
|
) -> None:
|
|
@@ -400,6 +426,14 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
400
426
|
"""
|
|
401
427
|
return DeviceType(super().getDeviceType(label))
|
|
402
428
|
|
|
429
|
+
def getFocusDirection(self, stageLabel: str) -> FocusDirection:
|
|
430
|
+
"""Return device type for a given device.
|
|
431
|
+
|
|
432
|
+
**Why Override?** The returned [`pymmcore_plus.FocusDirection`][] enum is more
|
|
433
|
+
interpretable than the raw `int` returned by `pymmcore`
|
|
434
|
+
"""
|
|
435
|
+
return FocusDirection(super().getFocusDirection(stageLabel))
|
|
436
|
+
|
|
403
437
|
def getPropertyType(self, label: str, propName: str) -> PropertyType:
|
|
404
438
|
"""Return the intrinsic property type for a given device and property.
|
|
405
439
|
|
|
@@ -573,7 +607,6 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
573
607
|
@overload
|
|
574
608
|
def getLastImageAndMD(self, *, fix: bool = True) -> tuple[np.ndarray, Metadata]: ...
|
|
575
609
|
|
|
576
|
-
@synchronized(_lock)
|
|
577
610
|
def getLastImageAndMD(
|
|
578
611
|
self, channel: int | None = None, slice: int | None = None, *, fix: bool = True
|
|
579
612
|
) -> tuple[np.ndarray, Metadata]:
|
|
@@ -609,7 +642,10 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
609
642
|
img = super().getLastImageMD(channel, slice, md)
|
|
610
643
|
else:
|
|
611
644
|
img = super().getLastImageMD(md)
|
|
612
|
-
return (
|
|
645
|
+
return (
|
|
646
|
+
self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img,
|
|
647
|
+
md,
|
|
648
|
+
)
|
|
613
649
|
|
|
614
650
|
@overload
|
|
615
651
|
def popNextImageAndMD(
|
|
@@ -619,9 +655,8 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
619
655
|
@overload
|
|
620
656
|
def popNextImageAndMD(self, *, fix: bool = True) -> tuple[np.ndarray, Metadata]: ...
|
|
621
657
|
|
|
622
|
-
@synchronized(_lock)
|
|
623
658
|
def popNextImageAndMD(
|
|
624
|
-
self, channel: int
|
|
659
|
+
self, channel: int = 0, slice: int = 0, *, fix: bool = True
|
|
625
660
|
) -> tuple[np.ndarray, Metadata]:
|
|
626
661
|
"""Gets and removes the next image (and metadata) from the circular buffer.
|
|
627
662
|
|
|
@@ -651,13 +686,13 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
651
686
|
Image and metadata
|
|
652
687
|
"""
|
|
653
688
|
md = Metadata()
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
img
|
|
658
|
-
|
|
689
|
+
img = super().popNextImageMD(channel, slice, md)
|
|
690
|
+
md = Metadata(md)
|
|
691
|
+
return (
|
|
692
|
+
self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img,
|
|
693
|
+
md,
|
|
694
|
+
)
|
|
659
695
|
|
|
660
|
-
@synchronized(_lock)
|
|
661
696
|
def popNextImage(self, *, fix: bool = True) -> np.ndarray:
|
|
662
697
|
"""Gets and removes the next image from the circular buffer.
|
|
663
698
|
|
|
@@ -672,9 +707,8 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
672
707
|
will be reshaped to (w, h, n_components) using `fixImage`.
|
|
673
708
|
"""
|
|
674
709
|
img: np.ndarray = super().popNextImage()
|
|
675
|
-
return self.fixImage(img) if fix else img
|
|
710
|
+
return self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img
|
|
676
711
|
|
|
677
|
-
@synchronized(_lock)
|
|
678
712
|
def getNBeforeLastImageAndMD(
|
|
679
713
|
self, n: int, *, fix: bool = True
|
|
680
714
|
) -> tuple[np.ndarray, Metadata]:
|
|
@@ -701,7 +735,7 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
701
735
|
"""
|
|
702
736
|
md = Metadata()
|
|
703
737
|
img = super().getNBeforeLastImageMD(n, md)
|
|
704
|
-
return self.fixImage(img) if fix else img, md
|
|
738
|
+
return self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img, md
|
|
705
739
|
|
|
706
740
|
def setConfig(self, groupName: str, configName: str) -> None:
|
|
707
741
|
"""Applies a configuration to a group.
|
|
@@ -1338,35 +1372,6 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1338
1372
|
"""
|
|
1339
1373
|
return self.setPosition(val)
|
|
1340
1374
|
|
|
1341
|
-
@overload
|
|
1342
|
-
def setPosition(self, position: float) -> None: ...
|
|
1343
|
-
|
|
1344
|
-
@overload
|
|
1345
|
-
def setPosition(self, stageLabel: str, position: float) -> None: ...
|
|
1346
|
-
|
|
1347
|
-
@synchronized(_lock)
|
|
1348
|
-
def setPosition(self, *args: Any, **kwargs: Any) -> None:
|
|
1349
|
-
"""Set position of the stage in microns.
|
|
1350
|
-
|
|
1351
|
-
**Why Override?** To add a lock to prevent concurrent calls across threads.
|
|
1352
|
-
"""
|
|
1353
|
-
return super().setPosition(*args, **kwargs)
|
|
1354
|
-
|
|
1355
|
-
@overload
|
|
1356
|
-
def setXYPosition(self, x: float, y: float) -> None: ...
|
|
1357
|
-
|
|
1358
|
-
@overload
|
|
1359
|
-
def setXYPosition(self, xyStageLabel: str, x: float, y: float) -> None: ...
|
|
1360
|
-
|
|
1361
|
-
@synchronized(_lock)
|
|
1362
|
-
def setXYPosition(self, *args: Any, **kwargs: Any) -> None:
|
|
1363
|
-
"""Sets the position of the XY stage in microns.
|
|
1364
|
-
|
|
1365
|
-
**Why Override?** To add a lock to prevent concurrent calls across threads.
|
|
1366
|
-
"""
|
|
1367
|
-
return super().setXYPosition(*args, **kwargs)
|
|
1368
|
-
|
|
1369
|
-
@synchronized(_lock)
|
|
1370
1375
|
def getCameraChannelNames(self) -> tuple[str, ...]:
|
|
1371
1376
|
"""Convenience method to call `getCameraChannelName` for all camera channels.
|
|
1372
1377
|
|
|
@@ -1377,11 +1382,11 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1377
1382
|
for i in range(self.getNumberOfCameraChannels())
|
|
1378
1383
|
)
|
|
1379
1384
|
|
|
1380
|
-
@synchronized(_lock)
|
|
1381
1385
|
def snapImage(self) -> None:
|
|
1382
1386
|
"""Acquires a single image with current settings.
|
|
1383
1387
|
|
|
1384
|
-
**Why Override?**
|
|
1388
|
+
**Why Override?** to emit the `imageSnapped` event after snapping an image.
|
|
1389
|
+
and to emit shutter property changes if `getAutoShutter` is `True`.
|
|
1385
1390
|
"""
|
|
1386
1391
|
if autoshutter := self.getAutoShutter():
|
|
1387
1392
|
self.events.propertyChanged.emit(self.getShutterDevice(), "State", True)
|
|
@@ -1470,7 +1475,11 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1470
1475
|
old_engine = self.mda.set_engine(engine)
|
|
1471
1476
|
self.events.mdaEngineRegistered.emit(engine, old_engine)
|
|
1472
1477
|
|
|
1473
|
-
def fixImage(
|
|
1478
|
+
def fixImage(
|
|
1479
|
+
self,
|
|
1480
|
+
img: np.ndarray,
|
|
1481
|
+
ncomponents: int | None = None,
|
|
1482
|
+
) -> np.ndarray:
|
|
1474
1483
|
"""Fix img shape/dtype based on `self.getNumberOfComponents()`.
|
|
1475
1484
|
|
|
1476
1485
|
:sparkles: *This method is new in `CMMCorePlus`.*
|
|
@@ -1492,12 +1501,34 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1492
1501
|
"""
|
|
1493
1502
|
if ncomponents is None:
|
|
1494
1503
|
ncomponents = self.getNumberOfComponents()
|
|
1495
|
-
if ncomponents == 4:
|
|
1504
|
+
if ncomponents == 4 and img.ndim != 3:
|
|
1496
1505
|
new_shape = (*img.shape, 4)
|
|
1497
|
-
img = img.view(dtype=f"u{img.dtype.itemsize//4}").reshape(new_shape)
|
|
1506
|
+
img = img.view(dtype=f"u{img.dtype.itemsize // 4}").reshape(new_shape)
|
|
1498
1507
|
img = img[..., [2, 1, 0]] # Convert from BGRA to RGB
|
|
1499
1508
|
return img
|
|
1500
1509
|
|
|
1510
|
+
def getPhysicalCameraDevice(self, channel_index: int = 0) -> str:
|
|
1511
|
+
"""Return the name of the actual camera device for a given channel index.
|
|
1512
|
+
|
|
1513
|
+
:sparkles: *This method is new in `CMMCorePlus`.* It provides a convenience
|
|
1514
|
+
for accessing the name of the actual camera device when using the multi-camera
|
|
1515
|
+
utility.
|
|
1516
|
+
"""
|
|
1517
|
+
cam_dev = self.getCameraDevice()
|
|
1518
|
+
# best as I can tell, this is a hard-coded string in Utilities/MultiCamera.cpp
|
|
1519
|
+
# (it also appears in ArduinoCounter.cpp). This appears to be "the way"
|
|
1520
|
+
# to get at the original camera when using the multi-camera utility.
|
|
1521
|
+
prop_name = f"Physical Camera {channel_index + 1}"
|
|
1522
|
+
if self.hasProperty(cam_dev, prop_name):
|
|
1523
|
+
return self.getProperty(cam_dev, prop_name)
|
|
1524
|
+
if channel_index > 0:
|
|
1525
|
+
warnings.warn(
|
|
1526
|
+
f"Camera {cam_dev} does not have a property {prop_name}. "
|
|
1527
|
+
f"Cannot get channel_index={channel_index}",
|
|
1528
|
+
stacklevel=2,
|
|
1529
|
+
)
|
|
1530
|
+
return cam_dev
|
|
1531
|
+
|
|
1501
1532
|
def getTaggedImage(self, channel_index: int = 0) -> TaggedImage:
|
|
1502
1533
|
"""Return getImage as named tuple with metadata.
|
|
1503
1534
|
|
|
@@ -1560,7 +1591,7 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1560
1591
|
|
|
1561
1592
|
try:
|
|
1562
1593
|
channel_group = self.getPropertyFromCache("Core", "ChannelGroup")
|
|
1563
|
-
channel = self.getCurrentConfigFromCache(channel_group)
|
|
1594
|
+
channel: str = self.getCurrentConfigFromCache(channel_group)
|
|
1564
1595
|
except Exception:
|
|
1565
1596
|
channel = "Default"
|
|
1566
1597
|
tags["Channel"] = channel
|
|
@@ -1570,15 +1601,9 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1570
1601
|
tags["Binning"] = self.getProperty(self.getCameraDevice(), "Binning")
|
|
1571
1602
|
|
|
1572
1603
|
if channel_index is not None:
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
if "Camera" not in tags:
|
|
1577
|
-
core_cam = tags.get("Core-Camera")
|
|
1578
|
-
phys_cam_key = f"{core_cam}-Physical Camera {channel_index+1}"
|
|
1579
|
-
if phys_cam_key in tags:
|
|
1580
|
-
tags["Camera"] = tags[phys_cam_key]
|
|
1581
|
-
# tags["Channel"] = tags[phys_cam_key] # ?? why did MMCoreJ do this?
|
|
1604
|
+
tags["CameraChannelIndex"] = channel_index
|
|
1605
|
+
tags["ChannelIndex"] = channel_index
|
|
1606
|
+
tags["Camera"] = self.getPhysicalCameraDevice(channel_index)
|
|
1582
1607
|
|
|
1583
1608
|
# these are added by AcqEngJ
|
|
1584
1609
|
# yyyy-MM-dd HH:mm:ss.mmmmmm # NOTE AcqEngJ omits microseconds
|
|
@@ -1643,7 +1668,7 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1643
1668
|
if numChannel is not None
|
|
1644
1669
|
else super().getImage()
|
|
1645
1670
|
)
|
|
1646
|
-
return self.fixImage(img) if fix else img
|
|
1671
|
+
return self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img
|
|
1647
1672
|
|
|
1648
1673
|
def startContinuousSequenceAcquisition(self, intervalMs: float = 0) -> None:
|
|
1649
1674
|
"""Start a ContinuousSequenceAcquisition.
|
|
@@ -1887,30 +1912,33 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1887
1912
|
super().definePixelSizeConfig(*args, **kwargs)
|
|
1888
1913
|
self.events.pixelSizeChanged.emit(0.0)
|
|
1889
1914
|
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
) -> tuple[list[int], list[int], list[int], list[int]]:
|
|
1893
|
-
"""Get multiple ROIs from the current camera device.
|
|
1915
|
+
# pymmcore-SWIG needs this, but pymmcore-nano doesn't
|
|
1916
|
+
if hasattr(pymmcore, "UnsignedVector"):
|
|
1894
1917
|
|
|
1895
|
-
|
|
1896
|
-
|
|
1918
|
+
def getMultiROI( # type: ignore [override]
|
|
1919
|
+
self, *_: Any
|
|
1920
|
+
) -> tuple[list[int], list[int], list[int], list[int]]:
|
|
1921
|
+
"""Get multiple ROIs from the current camera device.
|
|
1897
1922
|
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1923
|
+
Will fail if the camera does not support multiple ROIs. Will return empty
|
|
1924
|
+
vectors if multiple ROIs are not currently being used.
|
|
1925
|
+
|
|
1926
|
+
**Why Override?** So that the user doesn't need to pass in four empty
|
|
1927
|
+
pymmcore.UnsignedVector() objects.
|
|
1928
|
+
"""
|
|
1929
|
+
if _:
|
|
1930
|
+
warnings.warn( # pragma: no cover
|
|
1931
|
+
"Unlike pymmcore, CMMCorePlus.getMultiROI does not require "
|
|
1932
|
+
"arguments. Arguments are ignored.",
|
|
1933
|
+
stacklevel=2,
|
|
1934
|
+
)
|
|
1907
1935
|
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1936
|
+
xs = pymmcore.UnsignedVector() # type: ignore [attr-defined]
|
|
1937
|
+
ys = pymmcore.UnsignedVector() # type: ignore [attr-defined]
|
|
1938
|
+
ws = pymmcore.UnsignedVector() # type: ignore [attr-defined]
|
|
1939
|
+
hs = pymmcore.UnsignedVector() # type: ignore [attr-defined]
|
|
1940
|
+
super().getMultiROI(xs, ys, ws, hs)
|
|
1941
|
+
return list(xs), list(ys), list(ws), list(hs)
|
|
1914
1942
|
|
|
1915
1943
|
@overload
|
|
1916
1944
|
def setROI(self, x: int, y: int, width: int, height: int) -> None: ...
|
|
@@ -1974,7 +2002,12 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1974
2002
|
with open(filename, "a") as f:
|
|
1975
2003
|
f.write("\n".join(cfg))
|
|
1976
2004
|
|
|
1977
|
-
def describe(
|
|
2005
|
+
def describe(
|
|
2006
|
+
self,
|
|
2007
|
+
sort: str | None = None,
|
|
2008
|
+
show_config_groups: bool = False,
|
|
2009
|
+
show_available: bool = False,
|
|
2010
|
+
) -> None:
|
|
1978
2011
|
"""Print information table with the current configuration.
|
|
1979
2012
|
|
|
1980
2013
|
Intended to provide a quick overview of the microscope configuration during
|
|
@@ -1982,7 +2015,7 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1982
2015
|
|
|
1983
2016
|
:sparkles: *This method is new in `CMMCorePlus`.*
|
|
1984
2017
|
"""
|
|
1985
|
-
_current = {
|
|
2018
|
+
_current: dict[str, str] = {
|
|
1986
2019
|
self.getCameraDevice(): "Camera",
|
|
1987
2020
|
self.getXYStageDevice(): "XYStage",
|
|
1988
2021
|
self.getFocusDevice(): "Focus",
|
|
@@ -2006,38 +2039,62 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
2006
2039
|
|
|
2007
2040
|
print(f"{self.getVersionInfo()}, {self.getAPIVersionInfo()}")
|
|
2008
2041
|
print("Adapter path:", ",".join(self.getDeviceAdapterSearchPaths()))
|
|
2042
|
+
print("\nLoaded Devices:")
|
|
2009
2043
|
print_tabular_data(data, sort=sort)
|
|
2010
2044
|
|
|
2045
|
+
state = self.state(cached=False)
|
|
2046
|
+
if show_config_groups:
|
|
2047
|
+
group_data: defaultdict[str, list[str]] = defaultdict(list)
|
|
2048
|
+
groups = state["config_groups"]
|
|
2049
|
+
for group in groups:
|
|
2050
|
+
for pi, preset in enumerate(group["presets"]):
|
|
2051
|
+
for si, stng in enumerate(preset["settings"]):
|
|
2052
|
+
dev, prop, val = stng["dev"], stng["prop"], stng["val"]
|
|
2053
|
+
group_name = group["name"] if (pi == 0 and si == 0) else ""
|
|
2054
|
+
preset_name = preset["name"] if si == 0 else ""
|
|
2055
|
+
group_data["Group"].append(group_name)
|
|
2056
|
+
group_data["Preset"].append(preset_name)
|
|
2057
|
+
group_data["Device"].append(dev)
|
|
2058
|
+
group_data["Property"].append(prop)
|
|
2059
|
+
group_data["Value"].append(val)
|
|
2060
|
+
# add break between presets
|
|
2061
|
+
group_data["Group"].append("")
|
|
2062
|
+
group_data["Preset"].append("")
|
|
2063
|
+
group_data["Device"].append("")
|
|
2064
|
+
group_data["Property"].append("")
|
|
2065
|
+
group_data["Value"].append("")
|
|
2066
|
+
|
|
2067
|
+
print("\nConfig Groups:")
|
|
2068
|
+
print_tabular_data(group_data, sort=sort)
|
|
2069
|
+
|
|
2070
|
+
if show_available:
|
|
2071
|
+
avail_data: defaultdict[str, list[str]] = defaultdict(list)
|
|
2072
|
+
avail_adapters = self.getDeviceAdapterNames()
|
|
2073
|
+
for adapt in avail_adapters:
|
|
2074
|
+
with suppress(Exception):
|
|
2075
|
+
devices = self.getAvailableDevices(adapt)
|
|
2076
|
+
descriptions = self.getAvailableDeviceDescriptions(adapt)
|
|
2077
|
+
types = self.getAvailableDeviceTypes(adapt)
|
|
2078
|
+
for dev, desc, type_ in zip(devices, descriptions, types):
|
|
2079
|
+
avail_data["Library, DeviceName"].append(f"{adapt!r}, {dev!r}")
|
|
2080
|
+
avail_data["Type"].append(str(DeviceType(type_)))
|
|
2081
|
+
avail_data["Description"].append(desc)
|
|
2082
|
+
|
|
2083
|
+
print("\nAvailable Devices:")
|
|
2084
|
+
print_tabular_data(avail_data, sort=sort)
|
|
2085
|
+
|
|
2011
2086
|
def state(
|
|
2012
|
-
self,
|
|
2013
|
-
|
|
2014
|
-
devices: bool = True,
|
|
2015
|
-
image: bool = True,
|
|
2016
|
-
system_info: bool = False,
|
|
2017
|
-
system_status: bool = False,
|
|
2018
|
-
config_groups: bool | Sequence[str] = True,
|
|
2019
|
-
position: bool = False,
|
|
2020
|
-
autofocus: bool = False,
|
|
2021
|
-
pixel_size_configs: bool = False,
|
|
2022
|
-
device_types: bool = False,
|
|
2023
|
-
cached: bool = True,
|
|
2024
|
-
error_value: Any = None,
|
|
2025
|
-
) -> StateDict:
|
|
2087
|
+
self, *, cached: bool = True, include_time: bool = False, **_kwargs: Any
|
|
2088
|
+
) -> SummaryMetaV1:
|
|
2026
2089
|
"""Return info on the current state of the core."""
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
autofocus=autofocus,
|
|
2036
|
-
pixel_size_configs=pixel_size_configs,
|
|
2037
|
-
device_types=device_types,
|
|
2038
|
-
cached=cached,
|
|
2039
|
-
error_value=error_value,
|
|
2040
|
-
)
|
|
2090
|
+
if _kwargs:
|
|
2091
|
+
keys = ", ".join(_kwargs.keys())
|
|
2092
|
+
warnings.warn(
|
|
2093
|
+
f"CMMCorePlus.state no longer takes arguments: {keys}. Ignoring."
|
|
2094
|
+
"Please update your code as this may be an error in the future.",
|
|
2095
|
+
stacklevel=2,
|
|
2096
|
+
)
|
|
2097
|
+
return summary_metadata(self, include_time=include_time, cached=cached)
|
|
2041
2098
|
|
|
2042
2099
|
@contextmanager
|
|
2043
2100
|
def _property_change_emission_ensured(
|
|
@@ -2061,8 +2118,18 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
2061
2118
|
and self.getDeviceType(device) is DeviceType.StateDevice
|
|
2062
2119
|
):
|
|
2063
2120
|
properties = STATE_PROPS
|
|
2121
|
+
try:
|
|
2122
|
+
before = [self.getProperty(device, p) for p in properties]
|
|
2123
|
+
except Exception as e:
|
|
2124
|
+
logger.error(
|
|
2125
|
+
"Error getting properties %s on %s: %s. Cannot ensure signal emission",
|
|
2126
|
+
properties,
|
|
2127
|
+
device,
|
|
2128
|
+
e,
|
|
2129
|
+
)
|
|
2130
|
+
yield
|
|
2131
|
+
return
|
|
2064
2132
|
|
|
2065
|
-
before = [self.getProperty(device, p) for p in properties]
|
|
2066
2133
|
with _blockSignal(self.events, self.events.propertyChanged):
|
|
2067
2134
|
yield
|
|
2068
2135
|
after = [self.getProperty(device, p) for p in properties]
|
|
@@ -2071,7 +2138,7 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
2071
2138
|
self.events.propertyChanged.emit(device, properties[i], val)
|
|
2072
2139
|
|
|
2073
2140
|
@contextmanager
|
|
2074
|
-
def setContext(self, **kwargs:
|
|
2141
|
+
def setContext(self, **kwargs: Unpack[SetContextKwargs]) -> Iterator[None]:
|
|
2075
2142
|
"""Set core properties in a context restoring the initial values on exit.
|
|
2076
2143
|
|
|
2077
2144
|
:sparkles: *This method is new in `CMMCorePlus`.*
|
|
@@ -2080,9 +2147,12 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
2080
2147
|
----------
|
|
2081
2148
|
**kwargs : Any
|
|
2082
2149
|
Keyword arguments may be any `Name` for which `get<Name>` and `set<Name>`
|
|
2083
|
-
methods exist
|
|
2150
|
+
methods exist (where the first letter in `<Name>` may be either lower or
|
|
2151
|
+
upper case). For example, `setContext(exposure=10)` will call
|
|
2084
2152
|
`setExposure(10)` when entering the context and `setExposure(<initial>)`
|
|
2085
|
-
when exiting the context.
|
|
2153
|
+
when exiting the context. If the property is not found, a warning is logged
|
|
2154
|
+
and the property is skipped. If the value is a tuple, it is unpacked and
|
|
2155
|
+
passed to the `set<Name>` method (but lists are not unpacked).
|
|
2086
2156
|
|
|
2087
2157
|
Examples
|
|
2088
2158
|
--------
|
|
@@ -2102,11 +2172,16 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
2102
2172
|
try:
|
|
2103
2173
|
for name, v in kwargs.items():
|
|
2104
2174
|
name = name[0].upper() + name[1:]
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
getattr(self, f"set{name}")(v)
|
|
2108
|
-
except AttributeError:
|
|
2175
|
+
get_name, set_name = f"get{name}", f"set{name}"
|
|
2176
|
+
if not hasattr(self, get_name) or not hasattr(self, set_name):
|
|
2109
2177
|
logger.warning("%s is not a valid property, skipping.", name)
|
|
2178
|
+
continue
|
|
2179
|
+
|
|
2180
|
+
orig_values[name] = getattr(self, get_name)()
|
|
2181
|
+
if isinstance(v, tuple):
|
|
2182
|
+
getattr(self, set_name)(*v)
|
|
2183
|
+
else:
|
|
2184
|
+
getattr(self, set_name)(v)
|
|
2110
2185
|
yield
|
|
2111
2186
|
finally:
|
|
2112
2187
|
for k, v in orig_values.items():
|
|
@@ -2168,7 +2243,16 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
2168
2243
|
False
|
|
2169
2244
|
```
|
|
2170
2245
|
"""
|
|
2171
|
-
|
|
2246
|
+
warnings.warn(
|
|
2247
|
+
"canSequenceEvents is deprecated.\nPlease use "
|
|
2248
|
+
"`list(pymmcore_plus.core.iter_sequenced_events(core, [e1, e2]))` "
|
|
2249
|
+
"to see how this core will combine MDAEvents into SequencedEvents.",
|
|
2250
|
+
DeprecationWarning,
|
|
2251
|
+
stacklevel=2,
|
|
2252
|
+
)
|
|
2253
|
+
from ._sequencing import can_sequence_events
|
|
2254
|
+
|
|
2255
|
+
return can_sequence_events(self, e1, e2)
|
|
2172
2256
|
|
|
2173
2257
|
|
|
2174
2258
|
for name in (
|