pymmcore-plus 0.13.6__py3-none-any.whl → 0.14.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 +2 -0
- pymmcore_plus/_accumulator.py +258 -0
- pymmcore_plus/_cli.py +4 -2
- pymmcore_plus/_util.py +11 -8
- pymmcore_plus/core/__init__.py +34 -1
- pymmcore_plus/core/_device.py +739 -19
- pymmcore_plus/core/_mmcore_plus.py +165 -8
- pymmcore_plus/mda/handlers/_tensorstore_handler.py +3 -1
- {pymmcore_plus-0.13.6.dist-info → pymmcore_plus-0.14.0.dist-info}/METADATA +14 -39
- {pymmcore_plus-0.13.6.dist-info → pymmcore_plus-0.14.0.dist-info}/RECORD +13 -12
- {pymmcore_plus-0.13.6.dist-info → pymmcore_plus-0.14.0.dist-info}/WHEEL +0 -0
- {pymmcore_plus-0.13.6.dist-info → pymmcore_plus-0.14.0.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.13.6.dist-info → pymmcore_plus-0.14.0.dist-info}/licenses/LICENSE +0 -0
pymmcore_plus/__init__.py
CHANGED
|
@@ -8,6 +8,7 @@ except PackageNotFoundError: # pragma: no cover
|
|
|
8
8
|
__version__ = "unknown"
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
from ._accumulator import AbstractChangeAccumulator
|
|
11
12
|
from ._logger import configure_logging
|
|
12
13
|
from ._util import find_micromanager, use_micromanager
|
|
13
14
|
from .core import (
|
|
@@ -34,6 +35,7 @@ from .core.events import CMMCoreSignaler, PCoreSignaler
|
|
|
34
35
|
from .mda._runner import GeneratorMDASequence
|
|
35
36
|
|
|
36
37
|
__all__ = [
|
|
38
|
+
"AbstractChangeAccumulator",
|
|
37
39
|
"ActionType",
|
|
38
40
|
"CFGCommand",
|
|
39
41
|
"CFGGroup",
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Accumulate `setX` calls to a device value or property."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import abc
|
|
6
|
+
import sys
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, TypeVar
|
|
10
|
+
|
|
11
|
+
import psygnal
|
|
12
|
+
|
|
13
|
+
from pymmcore_plus.core._constants import DeviceType
|
|
14
|
+
from pymmcore_plus.core._mmcore_plus import CMMCorePlus
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from typing_extensions import Self
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
DT = TypeVar("DT", bound=DeviceType)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AbstractChangeAccumulator(ABC, Generic[T]):
|
|
23
|
+
"""Abstract base class for accumulating a series of `setX` calls to a device.
|
|
24
|
+
|
|
25
|
+
A `ChangeAccumulator`` is a class that accumulates a series of `setX` calls to a
|
|
26
|
+
device, retaining an internal target value, and emitting a signal when the device
|
|
27
|
+
has reached its target and is idle. It can be shared by multiple players (e.g.
|
|
28
|
+
widgets, or other classes) that want to control the same device, and allows them all
|
|
29
|
+
to issue relative/absolute moves, and be notified when the device is idle.
|
|
30
|
+
|
|
31
|
+
A common use case is to accumulate setPosition calls made to a stage device, where
|
|
32
|
+
you might want to accumulate a series of relative moves, and snap an image only when
|
|
33
|
+
the stage is idle after reaching its target position.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
finished = psygnal.Signal()
|
|
37
|
+
"""Signal emitted when the device has reached its target and is idle."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, zero: T) -> None:
|
|
40
|
+
self._zero = zero
|
|
41
|
+
self._reset()
|
|
42
|
+
|
|
43
|
+
def _reset(self) -> None:
|
|
44
|
+
self._base: T | None = None
|
|
45
|
+
self._delta: T | None = None
|
|
46
|
+
|
|
47
|
+
# ------------------------ Public API ------------------------
|
|
48
|
+
|
|
49
|
+
def add_relative(self, delta: T) -> None:
|
|
50
|
+
"""Add a relative value to the target."""
|
|
51
|
+
if self._delta is None:
|
|
52
|
+
self._base = self._get_value()
|
|
53
|
+
self._delta = delta
|
|
54
|
+
else:
|
|
55
|
+
self._delta = self._add(self._delta, delta)
|
|
56
|
+
self._issue_move()
|
|
57
|
+
|
|
58
|
+
def set_absolute(self, target: T) -> None:
|
|
59
|
+
"""Assign an absolute value to the target.
|
|
60
|
+
|
|
61
|
+
This will reset the accumulated state and issue a move to the target position.
|
|
62
|
+
After the move finishes, new `move_relative()` calls are interpreted
|
|
63
|
+
relative to *target*.
|
|
64
|
+
"""
|
|
65
|
+
self._base = target # anchor for later relatives
|
|
66
|
+
self._delta = self._zero # target == base + delta
|
|
67
|
+
self._issue_move()
|
|
68
|
+
|
|
69
|
+
def poll_done(self) -> bool:
|
|
70
|
+
"""Check if the device is done moving.
|
|
71
|
+
|
|
72
|
+
This should be called repeatedly by some event loop driver.
|
|
73
|
+
|
|
74
|
+
Returns True exactly once when:
|
|
75
|
+
1. The device is idle (not busy) AND
|
|
76
|
+
2. The last issued move command has been completed
|
|
77
|
+
|
|
78
|
+
After returning True it resets its state and will return False until the next
|
|
79
|
+
move_relative() call.
|
|
80
|
+
"""
|
|
81
|
+
# if we have no base or delta, we're not moving
|
|
82
|
+
if self._delta is None:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
# if the device is busy, we're not done
|
|
86
|
+
if self._is_busy():
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
# no new work, we're done
|
|
90
|
+
self._reset()
|
|
91
|
+
self.finished.emit()
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def is_moving(self) -> bool:
|
|
96
|
+
"""Returns True if the device is moving."""
|
|
97
|
+
return self._delta is not None
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def target(self) -> T | None:
|
|
101
|
+
"""The target position of the stage. Or None if not moving."""
|
|
102
|
+
if self._base is None or self._delta is None:
|
|
103
|
+
return None
|
|
104
|
+
return self._add(self._base, self._delta)
|
|
105
|
+
|
|
106
|
+
# ------------------------ Public API ------------------------
|
|
107
|
+
|
|
108
|
+
def _issue_move(self) -> None:
|
|
109
|
+
# self._base and self._delta are guaranteed to be not None here
|
|
110
|
+
target = self._add(self._base, self._delta) # type: ignore[arg-type]
|
|
111
|
+
# issue the move command
|
|
112
|
+
try:
|
|
113
|
+
self._set_value(target)
|
|
114
|
+
except Exception: # pragma: no cover
|
|
115
|
+
from pymmcore_plus._logger import logger
|
|
116
|
+
|
|
117
|
+
logger.exception(f"Error setting {type(self)} to {target}")
|
|
118
|
+
|
|
119
|
+
# ------------------------ Abstract methods ------------------------
|
|
120
|
+
|
|
121
|
+
@abstractmethod
|
|
122
|
+
def _get_value(self) -> T:
|
|
123
|
+
"""Get the current position of the device."""
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def _set_value(self, value: T) -> None:
|
|
127
|
+
"""Set the position of the device."""
|
|
128
|
+
|
|
129
|
+
@abstractmethod
|
|
130
|
+
def _add(self, a: T, b: T) -> T:
|
|
131
|
+
"""Add two values together.
|
|
132
|
+
|
|
133
|
+
Provided for more complex types like sequences.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def _is_busy(self) -> bool:
|
|
138
|
+
"""Return True if the device is busy."""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class FloatChangeAccumulator(AbstractChangeAccumulator[float]):
|
|
142
|
+
def __init__(self) -> None:
|
|
143
|
+
super().__init__(zero=0.0)
|
|
144
|
+
|
|
145
|
+
def _add(self, a: float, b: float) -> float:
|
|
146
|
+
return a + b
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
ZIP_STRICT = {"strict": True} if sys.version_info >= (3, 10) else {}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class SequenceChangeAccumulator(AbstractChangeAccumulator[Sequence[float]]):
|
|
153
|
+
def __init__(self, sequence_length: int) -> None:
|
|
154
|
+
self.sequence_length = sequence_length
|
|
155
|
+
super().__init__(zero=[0.0] * sequence_length)
|
|
156
|
+
|
|
157
|
+
def _add(self, a: Sequence[float], b: Sequence[float]) -> Sequence[float]:
|
|
158
|
+
return [x + y for x, y in zip(a, b, **ZIP_STRICT)]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class DeviceAccumulator(abc.ABC, Generic[DT]):
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
*,
|
|
165
|
+
device_label: str,
|
|
166
|
+
mmcore: CMMCorePlus | None = None,
|
|
167
|
+
**kwargs: Any,
|
|
168
|
+
) -> None:
|
|
169
|
+
self._mmcore = mmcore or CMMCorePlus.instance()
|
|
170
|
+
dev_type = self._device_type()
|
|
171
|
+
if not self._mmcore.getDeviceType(device_label) == dev_type: # pragma: no cover
|
|
172
|
+
raise ValueError(
|
|
173
|
+
f"Cannot create {self.__class__.__name__}. "
|
|
174
|
+
f"Device {device_label!r} is not a {dev_type.name}. "
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
self._device_label = device_label
|
|
178
|
+
super().__init__(**kwargs)
|
|
179
|
+
|
|
180
|
+
def _is_busy(self) -> bool:
|
|
181
|
+
return self._mmcore.deviceBusy(self._device_label)
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
@abstractmethod
|
|
185
|
+
def _device_type(cls) -> DT:
|
|
186
|
+
"""Return the device type for this class."""
|
|
187
|
+
|
|
188
|
+
_CACHE: ClassVar[dict[tuple[int, str], DeviceAccumulator]] = {}
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def get_cached(cls, device: str, mmcore: CMMCorePlus | None = None) -> Self:
|
|
192
|
+
"""Get a cached instance of the class for the given (device, core) pair.
|
|
193
|
+
|
|
194
|
+
This is intended to be called on the subclass for the device type you want to
|
|
195
|
+
create. For example, if you want to create a `PositionChangeAccumulator` for a
|
|
196
|
+
`StageDevice`, you would call: `PositionChangeAccumulator.get_cached(device)`.
|
|
197
|
+
|
|
198
|
+
But it may also be called on the base class, in which case it will still return
|
|
199
|
+
the correct subclass instance, but you will not have type safety on the return
|
|
200
|
+
type.
|
|
201
|
+
"""
|
|
202
|
+
mmcore = mmcore or CMMCorePlus.instance()
|
|
203
|
+
cache_key = (id(mmcore), device)
|
|
204
|
+
device_type = mmcore.getDeviceType(device)
|
|
205
|
+
if cache_key not in DeviceAccumulator._CACHE:
|
|
206
|
+
if device_type == cls._device_type():
|
|
207
|
+
cls._CACHE[cache_key] = cls(device_label=device, mmcore=mmcore)
|
|
208
|
+
else:
|
|
209
|
+
for sub in cls.__subclasses__():
|
|
210
|
+
if sub._device_type() == device_type: # noqa: SLF001
|
|
211
|
+
cls._CACHE[cache_key] = sub(device_label=device, mmcore=mmcore)
|
|
212
|
+
break
|
|
213
|
+
else:
|
|
214
|
+
raise ValueError(
|
|
215
|
+
"No matching DeviceTypeMixin subclass found for device type "
|
|
216
|
+
f"{device_type.name} (for device {device!r})."
|
|
217
|
+
)
|
|
218
|
+
obj = cls._CACHE[cache_key]
|
|
219
|
+
if not isinstance(obj, cls):
|
|
220
|
+
raise TypeError(
|
|
221
|
+
f"Cannot create {cls.__name__} for {device!r}. "
|
|
222
|
+
f"Device is a {device_type!r}, not a {cls._device_type().name}. "
|
|
223
|
+
)
|
|
224
|
+
return obj
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class PositionChangeAccumulator(DeviceAccumulator, FloatChangeAccumulator):
|
|
228
|
+
"""Accumulator for single axis stage devices."""
|
|
229
|
+
|
|
230
|
+
def __init__(self, device_label: str, mmcore: CMMCorePlus | None = None) -> None:
|
|
231
|
+
super().__init__(device_label=device_label, mmcore=mmcore)
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def _device_type(cls) -> Literal[DeviceType.StageDevice]:
|
|
235
|
+
return DeviceType.StageDevice
|
|
236
|
+
|
|
237
|
+
def _get_value(self) -> float:
|
|
238
|
+
return self._mmcore.getPosition(self._device_label)
|
|
239
|
+
|
|
240
|
+
def _set_value(self, value: float) -> None:
|
|
241
|
+
self._mmcore.setPosition(self._device_label, value)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class XYPositionChangeAccumulator(DeviceAccumulator, SequenceChangeAccumulator):
|
|
245
|
+
"""Accumulator for XY stage devices."""
|
|
246
|
+
|
|
247
|
+
def __init__(self, device_label: str, mmcore: CMMCorePlus | None = None) -> None:
|
|
248
|
+
super().__init__(device_label=device_label, mmcore=mmcore, sequence_length=2)
|
|
249
|
+
|
|
250
|
+
@classmethod
|
|
251
|
+
def _device_type(cls) -> Literal[DeviceType.XYStageDevice]:
|
|
252
|
+
return DeviceType.XYStageDevice
|
|
253
|
+
|
|
254
|
+
def _get_value(self) -> Sequence[float]:
|
|
255
|
+
return self._mmcore.getXYPosition(self._device_label)
|
|
256
|
+
|
|
257
|
+
def _set_value(self, value: Sequence[float]) -> None:
|
|
258
|
+
self._mmcore.setXYPosition(self._device_label, *value)
|
pymmcore_plus/_cli.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# do NOT use __future__.annotations here. It breaks typer.
|
|
2
|
+
import contextlib
|
|
2
3
|
import os
|
|
3
4
|
import shutil
|
|
4
5
|
import subprocess
|
|
@@ -143,12 +144,13 @@ def mmstudio() -> None: # pragma: no cover
|
|
|
143
144
|
if mm
|
|
144
145
|
else None
|
|
145
146
|
)
|
|
146
|
-
if not app: # pragma: no cover
|
|
147
|
+
if not mm or not app: # pragma: no cover
|
|
147
148
|
print(f":x: [bold red]No MMStudio application found in {mm!r}")
|
|
148
149
|
print("[magenta]run `mmcore install` to install a version of Micro-Manager")
|
|
149
150
|
raise typer.Exit(1)
|
|
150
151
|
cmd = ["open", "-a", str(app)] if PLATFORM == "Darwin" else [str(app)]
|
|
151
|
-
|
|
152
|
+
with contextlib.chdir(mm):
|
|
153
|
+
raise typer.Exit(subprocess.run(cmd).returncode)
|
|
152
154
|
|
|
153
155
|
|
|
154
156
|
@app.command()
|
pymmcore_plus/_util.py
CHANGED
|
@@ -181,14 +181,17 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
|
181
181
|
app_path = applications[sys.platform]
|
|
182
182
|
pth = next(app_path.glob("[m,M]icro-[m,M]anager*"), None)
|
|
183
183
|
if return_first:
|
|
184
|
-
if pth
|
|
185
|
-
logger.error(
|
|
186
|
-
"could not find micromanager directory. Please run 'mmcore install'"
|
|
187
|
-
)
|
|
188
|
-
return None
|
|
189
|
-
if _mm_path_has_compatible_div(pth): # pragma: no cover
|
|
184
|
+
if pth and _mm_path_has_compatible_div(pth): # pragma: no cover
|
|
190
185
|
logger.debug("using MM path found in applications: %s", pth)
|
|
191
186
|
return str(pth)
|
|
187
|
+
from . import _pymmcore
|
|
188
|
+
|
|
189
|
+
div = _pymmcore.version_info.device_interface
|
|
190
|
+
logger.error(
|
|
191
|
+
f"could not find micromanager directory for device interface {div}. "
|
|
192
|
+
"Please run 'mmcore install'"
|
|
193
|
+
)
|
|
194
|
+
return None
|
|
192
195
|
if pth is not None:
|
|
193
196
|
full_list[str(pth)] = None
|
|
194
197
|
return list(full_list)
|
|
@@ -659,9 +662,9 @@ def get_device_interface_version(lib_path: str | Path) -> int:
|
|
|
659
662
|
import ctypes
|
|
660
663
|
|
|
661
664
|
if sys.platform.startswith("win"):
|
|
662
|
-
lib = ctypes.WinDLL(lib_path)
|
|
665
|
+
lib = ctypes.WinDLL(str(lib_path))
|
|
663
666
|
else:
|
|
664
|
-
lib = ctypes.CDLL(lib_path)
|
|
667
|
+
lib = ctypes.CDLL(str(lib_path))
|
|
665
668
|
|
|
666
669
|
try:
|
|
667
670
|
func = lib.GetDeviceInterfaceVersion
|
pymmcore_plus/core/__init__.py
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
__all__ = [
|
|
2
2
|
"ActionType",
|
|
3
|
+
"AutoFocusDevice",
|
|
3
4
|
"CFGCommand",
|
|
4
5
|
"CFGGroup",
|
|
5
6
|
"CMMCorePlus",
|
|
7
|
+
"CameraDevice",
|
|
6
8
|
"ConfigGroup",
|
|
7
9
|
"Configuration",
|
|
10
|
+
"CoreDevice",
|
|
11
|
+
"Device",
|
|
8
12
|
"Device",
|
|
9
13
|
"DeviceAdapter",
|
|
10
14
|
"DeviceDetectionStatus",
|
|
@@ -13,12 +17,24 @@ __all__ = [
|
|
|
13
17
|
"DeviceProperty",
|
|
14
18
|
"DeviceType",
|
|
15
19
|
"FocusDirection",
|
|
20
|
+
"GalvoDevice",
|
|
21
|
+
"GenericDevice",
|
|
22
|
+
"HubDevice",
|
|
23
|
+
"ImageProcessorDevice",
|
|
16
24
|
"Keyword",
|
|
25
|
+
"MagnifierDevice",
|
|
17
26
|
"Metadata",
|
|
18
27
|
"PixelFormat",
|
|
19
28
|
"PortType",
|
|
20
29
|
"PropertyType",
|
|
30
|
+
"SLMDevice",
|
|
21
31
|
"SequencedEvent",
|
|
32
|
+
"SerialDevice",
|
|
33
|
+
"ShutterDevice",
|
|
34
|
+
"SignalIODevice",
|
|
35
|
+
"StageDevice",
|
|
36
|
+
"StateDevice",
|
|
37
|
+
"XYStageDevice",
|
|
22
38
|
"iter_sequenced_events",
|
|
23
39
|
]
|
|
24
40
|
|
|
@@ -39,7 +55,24 @@ from ._constants import (
|
|
|
39
55
|
PortType,
|
|
40
56
|
PropertyType,
|
|
41
57
|
)
|
|
42
|
-
from ._device import
|
|
58
|
+
from ._device import (
|
|
59
|
+
AutoFocusDevice,
|
|
60
|
+
CameraDevice,
|
|
61
|
+
CoreDevice,
|
|
62
|
+
Device,
|
|
63
|
+
GalvoDevice,
|
|
64
|
+
GenericDevice,
|
|
65
|
+
HubDevice,
|
|
66
|
+
ImageProcessorDevice,
|
|
67
|
+
MagnifierDevice,
|
|
68
|
+
SerialDevice,
|
|
69
|
+
ShutterDevice,
|
|
70
|
+
SignalIODevice,
|
|
71
|
+
SLMDevice,
|
|
72
|
+
StageDevice,
|
|
73
|
+
StateDevice,
|
|
74
|
+
XYStageDevice,
|
|
75
|
+
)
|
|
43
76
|
from ._metadata import Metadata
|
|
44
77
|
from ._mmcore_plus import CMMCorePlus
|
|
45
78
|
from ._property import DeviceProperty
|