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
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from collections.abc import Iterator, MutableMapping, Sequence
|
|
5
|
+
from contextlib import suppress
|
|
6
|
+
from typing import TYPE_CHECKING, Any, cast, overload
|
|
7
|
+
|
|
8
|
+
from pymmcore_plus.core import (
|
|
9
|
+
CMMCorePlus,
|
|
10
|
+
DeviceType,
|
|
11
|
+
Keyword,
|
|
12
|
+
)
|
|
13
|
+
from pymmcore_plus.core import Keyword as KW
|
|
14
|
+
|
|
15
|
+
from ._device_manager import PyDeviceManager
|
|
16
|
+
from ._proxy import create_core_proxy
|
|
17
|
+
from .devices._device import Device
|
|
18
|
+
from .devices._stage import XYStageDevice, _BaseStage
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
from typing import Callable, Literal, NewType, TypeVar
|
|
23
|
+
|
|
24
|
+
from pymmcore import AdapterName, DeviceLabel, DeviceName, PropertyName
|
|
25
|
+
|
|
26
|
+
from pymmcore_plus.core._constants import DeviceInitializationState, PropertyType
|
|
27
|
+
|
|
28
|
+
PyDeviceLabel = NewType("PyDeviceLabel", DeviceLabel)
|
|
29
|
+
|
|
30
|
+
_T = TypeVar("_T")
|
|
31
|
+
|
|
32
|
+
CURRENT = {
|
|
33
|
+
KW.CoreCamera: None,
|
|
34
|
+
KW.CoreShutter: None,
|
|
35
|
+
KW.CoreFocus: None,
|
|
36
|
+
KW.CoreXYStage: None,
|
|
37
|
+
KW.CoreAutoFocus: None,
|
|
38
|
+
KW.CoreSLM: None,
|
|
39
|
+
KW.CoreGalvo: None,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _CoreDevice:
|
|
44
|
+
"""A virtual core device.
|
|
45
|
+
|
|
46
|
+
This mirrors the pattern used in CMMCore, where there is a virtual "core" device
|
|
47
|
+
that maintains state about various "current" (real) devices. When a call is made to
|
|
48
|
+
`setSomeThing()` without specifying a device label, the CoreDevice is used to
|
|
49
|
+
determine which real device to use.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, state_cache: PropertyStateCache) -> None:
|
|
53
|
+
self._state_cache = state_cache
|
|
54
|
+
self._pycurrent: dict[Keyword, PyDeviceLabel | None] = {}
|
|
55
|
+
self.reset_current()
|
|
56
|
+
|
|
57
|
+
def reset_current(self) -> None:
|
|
58
|
+
self._pycurrent.update(CURRENT)
|
|
59
|
+
|
|
60
|
+
def current(self, keyword: Keyword) -> PyDeviceLabel | None:
|
|
61
|
+
return self._pycurrent[keyword]
|
|
62
|
+
|
|
63
|
+
def set_current(self, keyword: Keyword, label: str | None) -> None:
|
|
64
|
+
self._pycurrent[keyword] = cast("PyDeviceLabel", label)
|
|
65
|
+
self._state_cache[(KW.CoreDevice, keyword)] = label
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class UniMMCore(CMMCorePlus):
|
|
69
|
+
"""Unified Core object that first checks for python, then C++ devices."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, mm_path: str | None = None, adapter_paths: Sequence[str] = ()):
|
|
72
|
+
super().__init__(mm_path, adapter_paths)
|
|
73
|
+
self._pydevices = PyDeviceManager() # manager for python devices
|
|
74
|
+
self._state_cache = PropertyStateCache() # threadsafe cache for property states
|
|
75
|
+
self._pycore = _CoreDevice(self._state_cache) # virtual core for python
|
|
76
|
+
|
|
77
|
+
def _set_current_if_pydevice(self, keyword: Keyword, label: str) -> str:
|
|
78
|
+
"""Helper function to set the current core device if it is a python device.
|
|
79
|
+
|
|
80
|
+
If the label is a python device, the current device is set and the label is
|
|
81
|
+
cleared (in preparation for calling `super().setDevice()`), otherwise the
|
|
82
|
+
label is returned unchanged.
|
|
83
|
+
"""
|
|
84
|
+
if label in self._pydevices:
|
|
85
|
+
self._pycore.set_current(keyword, label)
|
|
86
|
+
label = ""
|
|
87
|
+
elif not label:
|
|
88
|
+
self._pycore.set_current(keyword, None)
|
|
89
|
+
return label
|
|
90
|
+
|
|
91
|
+
# -----------------------------------------------------------------------
|
|
92
|
+
# ------------------------ General Core methods ------------------------
|
|
93
|
+
# -----------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
def reset(self) -> None:
|
|
96
|
+
with suppress(TimeoutError):
|
|
97
|
+
self.waitForSystem()
|
|
98
|
+
self.unloadAllDevices()
|
|
99
|
+
self._pycore.reset_current()
|
|
100
|
+
super().reset()
|
|
101
|
+
|
|
102
|
+
# -----------------------------------------------------------------------
|
|
103
|
+
# ----------------- Functionality for All Devices ------------------------
|
|
104
|
+
# -----------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
def loadDevice(
|
|
107
|
+
self, label: str, moduleName: AdapterName | str, deviceName: DeviceName | str
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Loads a device from the plugin library, or python module.
|
|
110
|
+
|
|
111
|
+
In the standard MM case, this will load a device from the plugin library:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
core.loadDevice("cam", "DemoCamera", "DCam")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
For python devices, this will load a device from a python module:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
core.loadDevice("pydev", "package.module", "DeviceClass")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
CMMCorePlus.loadDevice(self, label, moduleName, deviceName)
|
|
126
|
+
except RuntimeError as e:
|
|
127
|
+
# it was a C++ device, should have worked ... raise the error
|
|
128
|
+
if moduleName not in super().getDeviceAdapterNames():
|
|
129
|
+
pydev = self._get_py_device_instance(moduleName, deviceName)
|
|
130
|
+
self.loadPyDevice(label, pydev)
|
|
131
|
+
return
|
|
132
|
+
if exc := self._load_error_with_info(label, moduleName, deviceName, str(e)):
|
|
133
|
+
raise exc from e
|
|
134
|
+
|
|
135
|
+
def _get_py_device_instance(self, module_name: str, cls_name: str) -> Device:
|
|
136
|
+
"""Import and instantiate a python device from `module_name.cls_name`."""
|
|
137
|
+
try:
|
|
138
|
+
module = __import__(module_name, fromlist=[cls_name])
|
|
139
|
+
except ImportError as e:
|
|
140
|
+
raise type(e)(
|
|
141
|
+
f"{module_name!r} is not a known Micro-manager DeviceAdapter, or "
|
|
142
|
+
"an importable python module "
|
|
143
|
+
) from e
|
|
144
|
+
try:
|
|
145
|
+
cls = getattr(module, cls_name)
|
|
146
|
+
except AttributeError as e:
|
|
147
|
+
raise AttributeError(
|
|
148
|
+
f"Could not find class {cls_name!r} in python module {module_name!r}"
|
|
149
|
+
) from e
|
|
150
|
+
if isinstance(cls, type) and issubclass(cls, Device):
|
|
151
|
+
return cls()
|
|
152
|
+
raise TypeError(f"{cls_name} is not a subclass of Device")
|
|
153
|
+
|
|
154
|
+
def loadPyDevice(self, label: str, device: Device) -> None:
|
|
155
|
+
"""Load a `unicore.Device` as a python device.
|
|
156
|
+
|
|
157
|
+
This API allows you to create python-side Device objects that can be used in
|
|
158
|
+
tandem with the C++ devices. Whenever a method is called that would normally
|
|
159
|
+
interact with a C++ device, this class will first check if a python device with
|
|
160
|
+
the same label exists, and if so, use that instead.
|
|
161
|
+
|
|
162
|
+
Parameters
|
|
163
|
+
----------
|
|
164
|
+
label : str
|
|
165
|
+
The label to assign to the device.
|
|
166
|
+
device : unicore.Device
|
|
167
|
+
The device object to load. Use the appropriate subclass of `Device` for the
|
|
168
|
+
type of device you are creating.
|
|
169
|
+
"""
|
|
170
|
+
if label in self.getLoadedDevices():
|
|
171
|
+
raise ValueError(f"The specified device label {label!r} is already in use")
|
|
172
|
+
self._pydevices.load(label, device, create_core_proxy(self))
|
|
173
|
+
|
|
174
|
+
load_py_device = loadPyDevice
|
|
175
|
+
|
|
176
|
+
def unloadDevice(self, label: DeviceLabel | str) -> None:
|
|
177
|
+
if label not in self._pydevices: # pragma: no cover
|
|
178
|
+
return super().unloadDevice(label)
|
|
179
|
+
self._pydevices.unload(label)
|
|
180
|
+
|
|
181
|
+
def unloadAllDevices(self) -> None:
|
|
182
|
+
self._pydevices.unload_all()
|
|
183
|
+
super().unloadAllDevices()
|
|
184
|
+
|
|
185
|
+
def initializeDevice(self, label: DeviceLabel | str) -> None:
|
|
186
|
+
if label not in self._pydevices: # pragma: no cover
|
|
187
|
+
return super().initializeDevice(label)
|
|
188
|
+
return self._pydevices.initialize(label)
|
|
189
|
+
|
|
190
|
+
def initializeAllDevices(self) -> None:
|
|
191
|
+
super().initializeAllDevices()
|
|
192
|
+
return self._pydevices.initialize_all()
|
|
193
|
+
|
|
194
|
+
def getDeviceInitializationState(self, label: str) -> DeviceInitializationState:
|
|
195
|
+
if label not in self._pydevices: # pragma: no cover
|
|
196
|
+
return super().getDeviceInitializationState(label)
|
|
197
|
+
return self._pydevices.get_initialization_state(label)
|
|
198
|
+
|
|
199
|
+
def getLoadedDevices(self) -> tuple[DeviceLabel, ...]:
|
|
200
|
+
return tuple(self._pydevices) + tuple(super().getLoadedDevices())
|
|
201
|
+
|
|
202
|
+
def getLoadedDevicesOfType(self, devType: int) -> tuple[DeviceLabel, ...]:
|
|
203
|
+
pydevs = self._pydevices.get_labels_of_type(devType)
|
|
204
|
+
return pydevs + super().getLoadedDevicesOfType(devType)
|
|
205
|
+
|
|
206
|
+
def getDeviceType(self, label: str) -> DeviceType:
|
|
207
|
+
if label not in self._pydevices: # pragma: no cover
|
|
208
|
+
return super().getDeviceType(label)
|
|
209
|
+
return self._pydevices[label].type()
|
|
210
|
+
|
|
211
|
+
def getDeviceLibrary(self, label: DeviceLabel | str) -> AdapterName:
|
|
212
|
+
if label not in self._pydevices: # pragma: no cover
|
|
213
|
+
return super().getDeviceLibrary(label)
|
|
214
|
+
return cast("AdapterName", self._pydevices[label].__module__)
|
|
215
|
+
|
|
216
|
+
def getDeviceName(self, label: DeviceLabel | str) -> DeviceName:
|
|
217
|
+
if label not in self._pydevices: # pragma: no cover
|
|
218
|
+
return super().getDeviceName(label)
|
|
219
|
+
return cast("DeviceName", self._pydevices[label].name())
|
|
220
|
+
|
|
221
|
+
def getDeviceDescription(self, label: DeviceLabel | str) -> str:
|
|
222
|
+
if label not in self._pydevices: # pragma: no cover
|
|
223
|
+
return super().getDeviceDescription(label)
|
|
224
|
+
return self._pydevices[label].description()
|
|
225
|
+
|
|
226
|
+
# ---------------------------- Properties ---------------------------
|
|
227
|
+
|
|
228
|
+
def getDevicePropertyNames(
|
|
229
|
+
self, label: DeviceLabel | str
|
|
230
|
+
) -> tuple[PropertyName, ...]:
|
|
231
|
+
if label not in self._pydevices: # pragma: no cover
|
|
232
|
+
return super().getDevicePropertyNames(label)
|
|
233
|
+
names = tuple(self._pydevices[label].get_property_names())
|
|
234
|
+
return cast("tuple[PropertyName, ...]", names)
|
|
235
|
+
|
|
236
|
+
def hasProperty(
|
|
237
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
238
|
+
) -> bool:
|
|
239
|
+
if label not in self._pydevices: # pragma: no cover
|
|
240
|
+
return super().hasProperty(label, propName)
|
|
241
|
+
return propName in self._pydevices[label].get_property_names()
|
|
242
|
+
|
|
243
|
+
def getProperty(
|
|
244
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
245
|
+
) -> Any: # broadening to Any, because pydevices can return non-string values?
|
|
246
|
+
if label not in self._pydevices: # pragma: no cover
|
|
247
|
+
return super().getProperty(label, propName)
|
|
248
|
+
with self._pydevices[label] as dev:
|
|
249
|
+
value = dev.get_property_value(propName)
|
|
250
|
+
self._state_cache[(label, propName)] = value
|
|
251
|
+
return value
|
|
252
|
+
|
|
253
|
+
def getPropertyFromCache(
|
|
254
|
+
self, deviceLabel: DeviceLabel | str, propName: PropertyName | str
|
|
255
|
+
) -> Any:
|
|
256
|
+
if deviceLabel not in self._pydevices:
|
|
257
|
+
return super().getPropertyFromCache(deviceLabel, propName)
|
|
258
|
+
return self._state_cache[(deviceLabel, propName)]
|
|
259
|
+
|
|
260
|
+
def setProperty(
|
|
261
|
+
self, label: str, propName: str, propValue: bool | float | int | str
|
|
262
|
+
) -> None:
|
|
263
|
+
if label not in self._pydevices: # pragma: no cover
|
|
264
|
+
return super().setProperty(label, propName, propValue)
|
|
265
|
+
with self._pydevices[label] as dev:
|
|
266
|
+
dev.set_property_value(propName, propValue)
|
|
267
|
+
self._state_cache[(label, propName)] = propValue
|
|
268
|
+
|
|
269
|
+
def getPropertyType(self, label: str, propName: str) -> PropertyType:
|
|
270
|
+
if label not in self._pydevices: # pragma: no cover
|
|
271
|
+
return super().getPropertyType(label, propName)
|
|
272
|
+
return self._pydevices[label].property(propName).type
|
|
273
|
+
|
|
274
|
+
def hasPropertyLimits(
|
|
275
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
276
|
+
) -> bool:
|
|
277
|
+
if label not in self._pydevices: # pragma: no cover
|
|
278
|
+
return super().hasPropertyLimits(label, propName)
|
|
279
|
+
with self._pydevices[label] as dev:
|
|
280
|
+
return dev.property(propName).limits is not None
|
|
281
|
+
|
|
282
|
+
def getPropertyLowerLimit(
|
|
283
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
284
|
+
) -> float:
|
|
285
|
+
if label not in self._pydevices: # pragma: no cover
|
|
286
|
+
return super().getPropertyLowerLimit(label, propName)
|
|
287
|
+
with self._pydevices[label] as dev:
|
|
288
|
+
if lims := dev.property(propName).limits:
|
|
289
|
+
return lims[0]
|
|
290
|
+
return 0
|
|
291
|
+
|
|
292
|
+
def getPropertyUpperLimit(
|
|
293
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
294
|
+
) -> float:
|
|
295
|
+
if label not in self._pydevices: # pragma: no cover
|
|
296
|
+
return super().getPropertyUpperLimit(label, propName)
|
|
297
|
+
with self._pydevices[label] as dev:
|
|
298
|
+
if lims := dev.property(propName).limits:
|
|
299
|
+
return lims[1]
|
|
300
|
+
return 0
|
|
301
|
+
|
|
302
|
+
def getAllowedPropertyValues(
|
|
303
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
304
|
+
) -> tuple[str, ...]:
|
|
305
|
+
if label not in self._pydevices: # pragma: no cover
|
|
306
|
+
return super().getAllowedPropertyValues(label, propName)
|
|
307
|
+
with self._pydevices[label] as dev:
|
|
308
|
+
return tuple(dev.property(propName).allowed_values or ())
|
|
309
|
+
|
|
310
|
+
def isPropertyPreInit(
|
|
311
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
312
|
+
) -> bool:
|
|
313
|
+
if label not in self._pydevices: # pragma: no cover
|
|
314
|
+
return super().isPropertyPreInit(label, propName)
|
|
315
|
+
with self._pydevices[label] as dev:
|
|
316
|
+
return dev.property(propName).is_pre_init
|
|
317
|
+
|
|
318
|
+
def isPropertyReadOnly(
|
|
319
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
320
|
+
) -> bool:
|
|
321
|
+
if label not in self._pydevices: # pragma: no cover
|
|
322
|
+
return super().isPropertyReadOnly(label, propName)
|
|
323
|
+
with self._pydevices[label] as dev:
|
|
324
|
+
return dev.is_property_read_only(propName)
|
|
325
|
+
|
|
326
|
+
def isPropertySequenceable(
|
|
327
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
328
|
+
) -> bool:
|
|
329
|
+
if label not in self._pydevices: # pragma: no cover
|
|
330
|
+
return super().isPropertySequenceable(label, propName)
|
|
331
|
+
with self._pydevices[label] as dev:
|
|
332
|
+
return dev.is_property_sequenceable(propName)
|
|
333
|
+
|
|
334
|
+
def getPropertySequenceMaxLength(
|
|
335
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
336
|
+
) -> int:
|
|
337
|
+
if label not in self._pydevices: # pragma: no cover
|
|
338
|
+
return super().getPropertySequenceMaxLength(label, propName)
|
|
339
|
+
with self._pydevices[label] as dev:
|
|
340
|
+
return dev.property(propName).sequence_max_length
|
|
341
|
+
|
|
342
|
+
def loadPropertySequence(
|
|
343
|
+
self,
|
|
344
|
+
label: DeviceLabel | str,
|
|
345
|
+
propName: PropertyName | str,
|
|
346
|
+
eventSequence: Sequence[Any],
|
|
347
|
+
) -> None:
|
|
348
|
+
if label not in self._pydevices: # pragma: no cover
|
|
349
|
+
return super().loadPropertySequence(label, propName, eventSequence)
|
|
350
|
+
with self._pydevices[label] as dev:
|
|
351
|
+
dev.load_property_sequence(propName, eventSequence)
|
|
352
|
+
|
|
353
|
+
def startPropertySequence(
|
|
354
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
355
|
+
) -> None:
|
|
356
|
+
if label not in self._pydevices: # pragma: no cover
|
|
357
|
+
return super().startPropertySequence(label, propName)
|
|
358
|
+
with self._pydevices[label] as dev:
|
|
359
|
+
dev.start_property_sequence(propName)
|
|
360
|
+
|
|
361
|
+
def stopPropertySequence(
|
|
362
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
363
|
+
) -> None:
|
|
364
|
+
if label not in self._pydevices: # pragma: no cover
|
|
365
|
+
return super().stopPropertySequence(label, propName)
|
|
366
|
+
with self._pydevices[label] as dev:
|
|
367
|
+
dev.stop_property_sequence(propName)
|
|
368
|
+
|
|
369
|
+
# ------------------------------ Ready State ----------------------------
|
|
370
|
+
|
|
371
|
+
def deviceBusy(self, label: DeviceLabel | str) -> bool:
|
|
372
|
+
if label not in self._pydevices: # pragma: no cover
|
|
373
|
+
return super().deviceBusy(label)
|
|
374
|
+
with self._pydevices[label] as dev:
|
|
375
|
+
return dev.busy()
|
|
376
|
+
|
|
377
|
+
def waitForDevice(self, label: DeviceLabel | str) -> None:
|
|
378
|
+
if label not in self._pydevices: # pragma: no cover
|
|
379
|
+
return super().waitForDevice(label)
|
|
380
|
+
self._pydevices.wait_for(label, self.getTimeoutMs())
|
|
381
|
+
|
|
382
|
+
# def waitForConfig
|
|
383
|
+
|
|
384
|
+
# probably only needed because C++ method is not virtual
|
|
385
|
+
def systemBusy(self) -> bool:
|
|
386
|
+
return self.deviceTypeBusy(DeviceType.AnyType)
|
|
387
|
+
|
|
388
|
+
# probably only needed because C++ method is not virtual
|
|
389
|
+
def waitForSystem(self) -> None:
|
|
390
|
+
self.waitForDeviceType(DeviceType.AnyType)
|
|
391
|
+
|
|
392
|
+
def waitForDeviceType(self, devType: int) -> None:
|
|
393
|
+
super().waitForDeviceType(devType)
|
|
394
|
+
self._pydevices.wait_for_device_type(devType, self.getTimeoutMs())
|
|
395
|
+
|
|
396
|
+
def deviceTypeBusy(self, devType: int) -> bool:
|
|
397
|
+
if super().deviceTypeBusy(devType):
|
|
398
|
+
return True
|
|
399
|
+
|
|
400
|
+
for label in self._pydevices.get_labels_of_type(devType):
|
|
401
|
+
with self._pydevices[label] as dev:
|
|
402
|
+
if dev.busy():
|
|
403
|
+
return True
|
|
404
|
+
return False
|
|
405
|
+
|
|
406
|
+
def getDeviceDelayMs(self, label: DeviceLabel | str) -> float:
|
|
407
|
+
if label not in self._pydevices: # pragma: no cover
|
|
408
|
+
return super().getDeviceDelayMs(label)
|
|
409
|
+
return 0 # pydevices don't yet support delays
|
|
410
|
+
|
|
411
|
+
def setDeviceDelayMs(self, label: DeviceLabel | str, delayMs: float) -> None:
|
|
412
|
+
if label not in self._pydevices: # pragma: no cover
|
|
413
|
+
return super().setDeviceDelayMs(label, delayMs)
|
|
414
|
+
if delayMs != 0:
|
|
415
|
+
raise NotImplementedError("Python devices do not support delays")
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
def usesDeviceDelay(self, label: DeviceLabel | str) -> bool:
|
|
419
|
+
if label not in self._pydevices: # pragma: no cover
|
|
420
|
+
return super().usesDeviceDelay(label)
|
|
421
|
+
return False
|
|
422
|
+
|
|
423
|
+
# -----------------------------------------------------------------------
|
|
424
|
+
# ---------------------------- XYStageDevice ----------------------------
|
|
425
|
+
# -----------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
def setXYStageDevice(self, xyStageLabel: DeviceLabel | str) -> None:
|
|
428
|
+
label = self._set_current_if_pydevice(KW.CoreXYStage, xyStageLabel)
|
|
429
|
+
super().setXYStageDevice(label)
|
|
430
|
+
|
|
431
|
+
def getXYStageDevice(self) -> DeviceLabel | Literal[""]:
|
|
432
|
+
"""Returns the label of the currently selected XYStage device.
|
|
433
|
+
|
|
434
|
+
Returns empty string if no XYStage device is selected.
|
|
435
|
+
"""
|
|
436
|
+
return self._pycore.current(KW.CoreXYStage) or super().getXYStageDevice()
|
|
437
|
+
|
|
438
|
+
@overload
|
|
439
|
+
def setXYPosition(self, x: float, y: float, /) -> None: ...
|
|
440
|
+
@overload
|
|
441
|
+
def setXYPosition(
|
|
442
|
+
self, xyStageLabel: DeviceLabel | str, x: float, y: float, /
|
|
443
|
+
) -> None: ...
|
|
444
|
+
def setXYPosition(self, *args: Any) -> None:
|
|
445
|
+
"""Sets the position of the XY stage in microns."""
|
|
446
|
+
label, args = _ensure_label(args, min_args=3, getter=self.getXYStageDevice)
|
|
447
|
+
if label not in self._pydevices: # pragma: no cover
|
|
448
|
+
return super().setXYPosition(label, *args)
|
|
449
|
+
|
|
450
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
451
|
+
dev.set_position_um(*args)
|
|
452
|
+
|
|
453
|
+
@overload
|
|
454
|
+
def getXYPosition(self) -> tuple[float, float]: ...
|
|
455
|
+
@overload
|
|
456
|
+
def getXYPosition(self, xyStageLabel: DeviceLabel | str) -> tuple[float, float]: ...
|
|
457
|
+
def getXYPosition(
|
|
458
|
+
self, xyStageLabel: DeviceLabel | str = ""
|
|
459
|
+
) -> tuple[float, float]:
|
|
460
|
+
"""Obtains the current position of the XY stage in microns."""
|
|
461
|
+
label = xyStageLabel or self.getXYStageDevice()
|
|
462
|
+
if label not in self._pydevices: # pragma: no cover
|
|
463
|
+
return tuple(super().getXYPosition(label)) # type: ignore
|
|
464
|
+
|
|
465
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
466
|
+
return dev.get_position_um()
|
|
467
|
+
|
|
468
|
+
# reimplementation needed because the C++ method are not virtual
|
|
469
|
+
@overload
|
|
470
|
+
def getXPosition(self) -> float: ...
|
|
471
|
+
@overload
|
|
472
|
+
def getXPosition(self, xyStageLabel: DeviceLabel | str) -> float: ...
|
|
473
|
+
def getXPosition(self, xyStageLabel: DeviceLabel | str = "") -> float:
|
|
474
|
+
"""Obtains the current position of the X axis of the XY stage in microns."""
|
|
475
|
+
return self.getXYPosition(xyStageLabel)[0]
|
|
476
|
+
|
|
477
|
+
# reimplementation needed because the C++ method are not virtual
|
|
478
|
+
@overload
|
|
479
|
+
def getYPosition(self) -> float: ...
|
|
480
|
+
@overload
|
|
481
|
+
def getYPosition(self, xyStageLabel: DeviceLabel | str) -> float: ...
|
|
482
|
+
def getYPosition(self, xyStageLabel: DeviceLabel | str = "") -> float:
|
|
483
|
+
"""Obtains the current position of the Y axis of the XY stage in microns."""
|
|
484
|
+
return self.getXYPosition(xyStageLabel)[1]
|
|
485
|
+
|
|
486
|
+
def getXYStageSequenceMaxLength(self, xyStageLabel: DeviceLabel | str) -> int:
|
|
487
|
+
"""Gets the maximum length of an XY stage's position sequence."""
|
|
488
|
+
if xyStageLabel not in self._pydevices: # pragma: no cover
|
|
489
|
+
return super().getXYStageSequenceMaxLength(xyStageLabel)
|
|
490
|
+
dev = self._pydevices.get_device_of_type(xyStageLabel, XYStageDevice)
|
|
491
|
+
return dev.get_sequence_max_length()
|
|
492
|
+
|
|
493
|
+
def isXYStageSequenceable(self, xyStageLabel: DeviceLabel | str) -> bool:
|
|
494
|
+
"""Queries XY stage if it can be used in a sequence."""
|
|
495
|
+
if xyStageLabel not in self._pydevices: # pragma: no cover
|
|
496
|
+
return super().isXYStageSequenceable(xyStageLabel)
|
|
497
|
+
dev = self._pydevices.get_device_of_type(xyStageLabel, XYStageDevice)
|
|
498
|
+
return dev.is_sequenceable()
|
|
499
|
+
|
|
500
|
+
def loadXYStageSequence(
|
|
501
|
+
self,
|
|
502
|
+
xyStageLabel: DeviceLabel | str,
|
|
503
|
+
xSequence: Sequence[float],
|
|
504
|
+
ySequence: Sequence[float],
|
|
505
|
+
/,
|
|
506
|
+
) -> None:
|
|
507
|
+
"""Transfer a sequence of stage positions to the xy stage.
|
|
508
|
+
|
|
509
|
+
xSequence and ySequence must have the same length. This should only be called
|
|
510
|
+
for XY stages that are sequenceable
|
|
511
|
+
"""
|
|
512
|
+
if xyStageLabel not in self._pydevices: # pragma: no cover
|
|
513
|
+
return super().loadXYStageSequence(xyStageLabel, xSequence, ySequence)
|
|
514
|
+
if len(xSequence) != len(ySequence):
|
|
515
|
+
raise ValueError("xSequence and ySequence must have the same length")
|
|
516
|
+
dev = self._pydevices.get_device_of_type(xyStageLabel, XYStageDevice)
|
|
517
|
+
seq = tuple(zip(xSequence, ySequence))
|
|
518
|
+
if len(seq) > dev.get_sequence_max_length():
|
|
519
|
+
raise ValueError(
|
|
520
|
+
f"Sequence is too long. Max length is {dev.get_sequence_max_length()}"
|
|
521
|
+
)
|
|
522
|
+
dev.send_sequence(seq)
|
|
523
|
+
|
|
524
|
+
@overload
|
|
525
|
+
def setOriginX(self) -> None: ...
|
|
526
|
+
@overload
|
|
527
|
+
def setOriginX(self, xyStageLabel: DeviceLabel | str) -> None: ...
|
|
528
|
+
def setOriginX(self, xyStageLabel: DeviceLabel | str = "") -> None:
|
|
529
|
+
"""Zero the given XY stage's X coordinate at the current position."""
|
|
530
|
+
label = xyStageLabel or self.getXYStageDevice()
|
|
531
|
+
if label not in self._pydevices: # pragma: no cover
|
|
532
|
+
return super().setOriginX(label)
|
|
533
|
+
|
|
534
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
535
|
+
dev.set_origin_x()
|
|
536
|
+
|
|
537
|
+
@overload
|
|
538
|
+
def setOriginY(self) -> None: ...
|
|
539
|
+
@overload
|
|
540
|
+
def setOriginY(self, xyStageLabel: DeviceLabel | str) -> None: ...
|
|
541
|
+
def setOriginY(self, xyStageLabel: DeviceLabel | str = "") -> None:
|
|
542
|
+
"""Zero the given XY stage's Y coordinate at the current position."""
|
|
543
|
+
label = xyStageLabel or self.getXYStageDevice()
|
|
544
|
+
if label not in self._pydevices: # pragma: no cover
|
|
545
|
+
return super().setOriginY(label)
|
|
546
|
+
|
|
547
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
548
|
+
dev.set_origin_y()
|
|
549
|
+
|
|
550
|
+
@overload
|
|
551
|
+
def setOriginXY(self) -> None: ...
|
|
552
|
+
@overload
|
|
553
|
+
def setOriginXY(self, xyStageLabel: DeviceLabel | str) -> None: ...
|
|
554
|
+
def setOriginXY(self, xyStageLabel: DeviceLabel | str = "") -> None:
|
|
555
|
+
"""Zero the given XY stage's coordinates at the current position."""
|
|
556
|
+
label = xyStageLabel or self.getXYStageDevice()
|
|
557
|
+
if label not in self._pydevices: # pragma: no cover
|
|
558
|
+
return super().setOriginXY(label)
|
|
559
|
+
|
|
560
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
561
|
+
dev.set_origin()
|
|
562
|
+
|
|
563
|
+
@overload
|
|
564
|
+
def setAdapterOriginXY(self, newXUm: float, newYUm: float, /) -> None: ...
|
|
565
|
+
@overload
|
|
566
|
+
def setAdapterOriginXY(
|
|
567
|
+
self, xyStageLabel: DeviceLabel | str, newXUm: float, newYUm: float, /
|
|
568
|
+
) -> None: ...
|
|
569
|
+
def setAdapterOriginXY(self, *args: Any) -> None:
|
|
570
|
+
"""Enable software translation of coordinates for the current XY stage.
|
|
571
|
+
|
|
572
|
+
The current position of the stage becomes (newXUm, newYUm). It is recommended
|
|
573
|
+
that setOriginXY() be used instead where available.
|
|
574
|
+
"""
|
|
575
|
+
label, args = _ensure_label(args, min_args=3, getter=self.getXYStageDevice)
|
|
576
|
+
if label not in self._pydevices: # pragma: no cover
|
|
577
|
+
return super().setAdapterOriginXY(label, *args)
|
|
578
|
+
|
|
579
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
580
|
+
dev.set_adapter_origin_um(*args)
|
|
581
|
+
|
|
582
|
+
@overload
|
|
583
|
+
def setRelativeXYPosition(self, dx: float, dy: float, /) -> None: ...
|
|
584
|
+
@overload
|
|
585
|
+
def setRelativeXYPosition(
|
|
586
|
+
self, xyStageLabel: DeviceLabel | str, dx: float, dy: float, /
|
|
587
|
+
) -> None: ...
|
|
588
|
+
def setRelativeXYPosition(self, *args: Any) -> None:
|
|
589
|
+
"""Sets the relative position of the XY stage in microns."""
|
|
590
|
+
label, args = _ensure_label(args, min_args=3, getter=self.getXYStageDevice)
|
|
591
|
+
if label not in self._pydevices: # pragma: no cover
|
|
592
|
+
return super().setRelativeXYPosition(label, *args)
|
|
593
|
+
|
|
594
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
595
|
+
dev.set_relative_position_um(*args)
|
|
596
|
+
|
|
597
|
+
def startXYStageSequence(self, xyStageLabel: DeviceLabel | str) -> None:
|
|
598
|
+
"""Starts an ongoing sequence of triggered events in an XY stage.
|
|
599
|
+
|
|
600
|
+
This should only be called for stages that are sequenceable
|
|
601
|
+
"""
|
|
602
|
+
label = xyStageLabel or self.getXYStageDevice()
|
|
603
|
+
if label not in self._pydevices: # pragma: no cover
|
|
604
|
+
return super().startXYStageSequence(label)
|
|
605
|
+
|
|
606
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
607
|
+
dev.start_sequence()
|
|
608
|
+
|
|
609
|
+
def stopXYStageSequence(self, xyStageLabel: DeviceLabel | str) -> None:
|
|
610
|
+
"""Stops an ongoing sequence of triggered events in an XY stage.
|
|
611
|
+
|
|
612
|
+
This should only be called for stages that are sequenceable
|
|
613
|
+
"""
|
|
614
|
+
label = xyStageLabel or self.getXYStageDevice()
|
|
615
|
+
if label not in self._pydevices: # pragma: no cover
|
|
616
|
+
return super().stopXYStageSequence(label)
|
|
617
|
+
|
|
618
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
619
|
+
dev.stop_sequence()
|
|
620
|
+
|
|
621
|
+
# -----------------------------------------------------------------------
|
|
622
|
+
# ---------------------------- Any Stage --------------------------------
|
|
623
|
+
# -----------------------------------------------------------------------
|
|
624
|
+
|
|
625
|
+
def home(self, xyOrZStageLabel: DeviceLabel | str) -> None:
|
|
626
|
+
"""Perform a hardware homing operation for an XY or focus/Z stage."""
|
|
627
|
+
if xyOrZStageLabel not in self._pydevices:
|
|
628
|
+
return super().home(xyOrZStageLabel)
|
|
629
|
+
|
|
630
|
+
dev = self._pydevices.get_device_of_type(xyOrZStageLabel, _BaseStage)
|
|
631
|
+
dev.home()
|
|
632
|
+
|
|
633
|
+
def stop(self, xyOrZStageLabel: DeviceLabel | str) -> None:
|
|
634
|
+
"""Stop the XY or focus/Z stage."""
|
|
635
|
+
if xyOrZStageLabel not in self._pydevices:
|
|
636
|
+
return super().stop(xyOrZStageLabel)
|
|
637
|
+
|
|
638
|
+
dev = self._pydevices.get_device_of_type(xyOrZStageLabel, _BaseStage)
|
|
639
|
+
dev.stop()
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _ensure_label(
|
|
643
|
+
args: tuple[_T, ...], min_args: int, getter: Callable[[], str]
|
|
644
|
+
) -> tuple[str, tuple[_T, ...]]:
|
|
645
|
+
"""Ensure we have a device label.
|
|
646
|
+
|
|
647
|
+
Designed to be used with overloaded methods that MAY take a device label as the
|
|
648
|
+
first argument.
|
|
649
|
+
|
|
650
|
+
If the number of arguments is less than `min_args`, the label is obtained from the
|
|
651
|
+
getter function. If the number of arguments is greater than or equal to `min_args`,
|
|
652
|
+
the label is the first argument and the remaining arguments are returned as a tuple
|
|
653
|
+
"""
|
|
654
|
+
if len(args) < min_args:
|
|
655
|
+
# we didn't get the label
|
|
656
|
+
return getter(), args
|
|
657
|
+
return cast(str, args[0]), args[1:]
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
class PropertyStateCache(MutableMapping[tuple[str, str], Any]):
|
|
661
|
+
"""A thread-safe cache for property states.
|
|
662
|
+
|
|
663
|
+
Keys are tuples of (device_label, property_name), and values are the last known
|
|
664
|
+
value of that property.
|
|
665
|
+
"""
|
|
666
|
+
|
|
667
|
+
def __init__(self) -> None:
|
|
668
|
+
self._store: dict[tuple[str, str], Any] = {}
|
|
669
|
+
self._lock = threading.Lock()
|
|
670
|
+
|
|
671
|
+
def __getitem__(self, key: tuple[str, str]) -> Any:
|
|
672
|
+
with self._lock:
|
|
673
|
+
try:
|
|
674
|
+
return self._store[key]
|
|
675
|
+
except KeyError: # pragma: no cover
|
|
676
|
+
prop, dev = key
|
|
677
|
+
raise KeyError(
|
|
678
|
+
f"Property {prop!r} of device {dev!r} not found in cache"
|
|
679
|
+
) from None
|
|
680
|
+
|
|
681
|
+
def __setitem__(self, key: tuple[str, str], value: Any) -> None:
|
|
682
|
+
with self._lock:
|
|
683
|
+
self._store[key] = value
|
|
684
|
+
|
|
685
|
+
def __delitem__(self, key: tuple[str, str]) -> None:
|
|
686
|
+
with self._lock:
|
|
687
|
+
del self._store[key]
|
|
688
|
+
|
|
689
|
+
def __contains__(self, key: object) -> bool:
|
|
690
|
+
with self._lock:
|
|
691
|
+
return key in self._store
|
|
692
|
+
|
|
693
|
+
def __iter__(self) -> Iterator[tuple[str, str]]:
|
|
694
|
+
with self._lock:
|
|
695
|
+
return iter(self._store.copy()) # Prevent modifications during iteration
|
|
696
|
+
|
|
697
|
+
def __len__(self) -> int:
|
|
698
|
+
with self._lock:
|
|
699
|
+
return len(self._store)
|
|
700
|
+
|
|
701
|
+
def __repr__(self) -> str:
|
|
702
|
+
with self._lock:
|
|
703
|
+
return f"{self.__class__.__name__}({self._store!r})"
|