pymmcore-plus 0.13.7__py3-none-any.whl → 0.15.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/_pymmcore.py +4 -2
- pymmcore_plus/core/__init__.py +34 -1
- pymmcore_plus/core/_constants.py +21 -3
- pymmcore_plus/core/_device.py +739 -19
- pymmcore_plus/core/_mmcore_plus.py +260 -47
- pymmcore_plus/core/events/_protocol.py +49 -34
- pymmcore_plus/core/events/_psygnal.py +2 -2
- pymmcore_plus/experimental/unicore/__init__.py +7 -1
- pymmcore_plus/experimental/unicore/_proxy.py +20 -3
- pymmcore_plus/experimental/unicore/core/__init__.py +0 -0
- pymmcore_plus/experimental/unicore/core/_sequence_buffer.py +318 -0
- pymmcore_plus/experimental/unicore/core/_unicore.py +1702 -0
- pymmcore_plus/experimental/unicore/devices/_camera.py +196 -0
- pymmcore_plus/experimental/unicore/devices/_device.py +54 -28
- pymmcore_plus/experimental/unicore/devices/_properties.py +8 -1
- pymmcore_plus/experimental/unicore/devices/_slm.py +82 -0
- pymmcore_plus/experimental/unicore/devices/_state.py +152 -0
- pymmcore_plus/mda/events/_protocol.py +8 -8
- pymmcore_plus/mda/handlers/_tensorstore_handler.py +3 -1
- {pymmcore_plus-0.13.7.dist-info → pymmcore_plus-0.15.0.dist-info}/METADATA +14 -37
- {pymmcore_plus-0.13.7.dist-info → pymmcore_plus-0.15.0.dist-info}/RECORD +26 -20
- pymmcore_plus/experimental/unicore/_unicore.py +0 -703
- {pymmcore_plus-0.13.7.dist-info → pymmcore_plus-0.15.0.dist-info}/WHEEL +0 -0
- {pymmcore_plus-0.13.7.dist-info → pymmcore_plus-0.15.0.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.13.7.dist-info → pymmcore_plus-0.15.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1702 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import warnings
|
|
5
|
+
from collections.abc import Iterator, MutableMapping, Sequence
|
|
6
|
+
from contextlib import suppress
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from itertools import count
|
|
9
|
+
from time import perf_counter_ns
|
|
10
|
+
from typing import (
|
|
11
|
+
TYPE_CHECKING,
|
|
12
|
+
Any,
|
|
13
|
+
Callable,
|
|
14
|
+
Literal,
|
|
15
|
+
TypeVar,
|
|
16
|
+
cast,
|
|
17
|
+
overload,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
import pymmcore_plus._pymmcore as pymmcore
|
|
23
|
+
from pymmcore_plus.core import CMMCorePlus, DeviceType, Keyword
|
|
24
|
+
from pymmcore_plus.core import Keyword as KW
|
|
25
|
+
from pymmcore_plus.core._constants import PixelType
|
|
26
|
+
from pymmcore_plus.experimental.unicore._device_manager import PyDeviceManager
|
|
27
|
+
from pymmcore_plus.experimental.unicore._proxy import create_core_proxy
|
|
28
|
+
from pymmcore_plus.experimental.unicore.devices._camera import Camera
|
|
29
|
+
from pymmcore_plus.experimental.unicore.devices._device import Device
|
|
30
|
+
from pymmcore_plus.experimental.unicore.devices._slm import SLMDevice
|
|
31
|
+
from pymmcore_plus.experimental.unicore.devices._stage import XYStageDevice, _BaseStage
|
|
32
|
+
from pymmcore_plus.experimental.unicore.devices._state import StateDevice
|
|
33
|
+
|
|
34
|
+
from ._sequence_buffer import SequenceBuffer
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from collections.abc import Mapping, Sequence
|
|
38
|
+
from typing import Literal, NewType
|
|
39
|
+
|
|
40
|
+
from numpy.typing import DTypeLike
|
|
41
|
+
from pymmcore import (
|
|
42
|
+
AdapterName,
|
|
43
|
+
AffineTuple,
|
|
44
|
+
DeviceLabel,
|
|
45
|
+
DeviceName,
|
|
46
|
+
PropertyName,
|
|
47
|
+
StateLabel,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
from pymmcore_plus.core._constants import DeviceInitializationState, PropertyType
|
|
51
|
+
|
|
52
|
+
PyDeviceLabel = NewType("PyDeviceLabel", DeviceLabel)
|
|
53
|
+
_T = TypeVar("_T")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BufferOverflowStop(Exception):
|
|
57
|
+
"""Exception raised to signal graceful stop on buffer overflow."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
CURRENT = {
|
|
61
|
+
KW.CoreCamera: None,
|
|
62
|
+
KW.CoreShutter: None,
|
|
63
|
+
KW.CoreFocus: None,
|
|
64
|
+
KW.CoreXYStage: None,
|
|
65
|
+
KW.CoreAutoFocus: None,
|
|
66
|
+
KW.CoreSLM: None,
|
|
67
|
+
KW.CoreGalvo: None,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class _CoreDevice:
|
|
72
|
+
"""A virtual core device.
|
|
73
|
+
|
|
74
|
+
This mirrors the pattern used in CMMCore, where there is a virtual "core" device
|
|
75
|
+
that maintains state about various "current" (real) devices. When a call is made to
|
|
76
|
+
`setSomeThing()` without specifying a device label, the CoreDevice is used to
|
|
77
|
+
determine which real device to use.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, state_cache: PropertyStateCache) -> None:
|
|
81
|
+
self._state_cache = state_cache
|
|
82
|
+
self._pycurrent: dict[Keyword, PyDeviceLabel | None] = {}
|
|
83
|
+
self.reset_current()
|
|
84
|
+
|
|
85
|
+
def reset_current(self) -> None:
|
|
86
|
+
self._pycurrent.update(CURRENT)
|
|
87
|
+
|
|
88
|
+
def current(self, keyword: Keyword) -> PyDeviceLabel | None:
|
|
89
|
+
return self._pycurrent[keyword]
|
|
90
|
+
|
|
91
|
+
def set_current(self, keyword: Keyword, label: str | None) -> None:
|
|
92
|
+
self._pycurrent[keyword] = cast("PyDeviceLabel", label)
|
|
93
|
+
self._state_cache[(KW.CoreDevice, keyword)] = label
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
_DEFAULT_BUFFER_SIZE_MB: int = 1000
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class UniMMCore(CMMCorePlus):
|
|
100
|
+
"""Unified Core object that first checks for python, then C++ devices."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
103
|
+
self._pydevices = PyDeviceManager() # manager for python devices
|
|
104
|
+
self._state_cache = PropertyStateCache() # threadsafe cache for property states
|
|
105
|
+
self._pycore = _CoreDevice(self._state_cache) # virtual core for python
|
|
106
|
+
self._stop_event: threading.Event = threading.Event()
|
|
107
|
+
self._acquisition_thread: AcquisitionThread | None = None # TODO: implement
|
|
108
|
+
self._seq_buffer = SequenceBuffer(size_mb=_DEFAULT_BUFFER_SIZE_MB)
|
|
109
|
+
|
|
110
|
+
super().__init__(*args, **kwargs)
|
|
111
|
+
|
|
112
|
+
def _set_current_if_pydevice(self, keyword: Keyword, label: str) -> str:
|
|
113
|
+
"""Helper function to set the current core device if it is a python device.
|
|
114
|
+
|
|
115
|
+
If the label is a python device, the current device is set and the label is
|
|
116
|
+
cleared (in preparation for calling `super().setDevice()`), otherwise the
|
|
117
|
+
label is returned unchanged.
|
|
118
|
+
"""
|
|
119
|
+
if label in self._pydevices:
|
|
120
|
+
self._pycore.set_current(keyword, label)
|
|
121
|
+
label = ""
|
|
122
|
+
elif not label:
|
|
123
|
+
self._pycore.set_current(keyword, None)
|
|
124
|
+
return label
|
|
125
|
+
|
|
126
|
+
# -----------------------------------------------------------------------
|
|
127
|
+
# ------------------------ General Core methods ------------------------
|
|
128
|
+
# -----------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
def reset(self) -> None:
|
|
131
|
+
with suppress(TimeoutError):
|
|
132
|
+
self.waitForSystem()
|
|
133
|
+
self.unloadAllDevices()
|
|
134
|
+
self._pycore.reset_current()
|
|
135
|
+
super().reset()
|
|
136
|
+
|
|
137
|
+
# ------------------------------------------------------------------------
|
|
138
|
+
# ----------------- Functionality for All Devices ------------------------
|
|
139
|
+
# ------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
def loadDevice(
|
|
142
|
+
self, label: str, moduleName: AdapterName | str, deviceName: DeviceName | str
|
|
143
|
+
) -> None:
|
|
144
|
+
"""Loads a device from the plugin library, or python module.
|
|
145
|
+
|
|
146
|
+
In the standard MM case, this will load a device from the plugin library:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
core.loadDevice("cam", "DemoCamera", "DCam")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
For python devices, this will load a device from a python module:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
core.loadDevice("pydev", "package.module", "DeviceClass")
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
CMMCorePlus.loadDevice(self, label, moduleName, deviceName)
|
|
161
|
+
except RuntimeError as e:
|
|
162
|
+
# it was a C++ device, should have worked ... raise the error
|
|
163
|
+
if moduleName not in super().getDeviceAdapterNames():
|
|
164
|
+
pydev = self._get_py_device_instance(moduleName, deviceName)
|
|
165
|
+
self.loadPyDevice(label, pydev)
|
|
166
|
+
return
|
|
167
|
+
if exc := self._load_error_with_info(label, moduleName, deviceName, str(e)):
|
|
168
|
+
raise exc from e
|
|
169
|
+
|
|
170
|
+
def _get_py_device_instance(self, module_name: str, cls_name: str) -> Device:
|
|
171
|
+
"""Import and instantiate a python device from `module_name.cls_name`."""
|
|
172
|
+
try:
|
|
173
|
+
module = __import__(module_name, fromlist=[cls_name])
|
|
174
|
+
except ImportError as e:
|
|
175
|
+
raise type(e)(
|
|
176
|
+
f"{module_name!r} is not a known Micro-manager DeviceAdapter, or "
|
|
177
|
+
"an importable python module "
|
|
178
|
+
) from e
|
|
179
|
+
try:
|
|
180
|
+
cls = getattr(module, cls_name)
|
|
181
|
+
except AttributeError as e:
|
|
182
|
+
raise AttributeError(
|
|
183
|
+
f"Could not find class {cls_name!r} in python module {module_name!r}"
|
|
184
|
+
) from e
|
|
185
|
+
if isinstance(cls, type) and issubclass(cls, Device):
|
|
186
|
+
return cls()
|
|
187
|
+
raise TypeError(f"{cls_name} is not a subclass of Device")
|
|
188
|
+
|
|
189
|
+
def loadPyDevice(self, label: str, device: Device) -> None:
|
|
190
|
+
"""Load a `unicore.Device` as a python device.
|
|
191
|
+
|
|
192
|
+
This API allows you to create python-side Device objects that can be used in
|
|
193
|
+
tandem with the C++ devices. Whenever a method is called that would normally
|
|
194
|
+
interact with a C++ device, this class will first check if a python device with
|
|
195
|
+
the same label exists, and if so, use that instead.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
label : str
|
|
200
|
+
The label to assign to the device.
|
|
201
|
+
device : unicore.Device
|
|
202
|
+
The device object to load. Use the appropriate subclass of `Device` for the
|
|
203
|
+
type of device you are creating.
|
|
204
|
+
"""
|
|
205
|
+
if label in self.getLoadedDevices():
|
|
206
|
+
raise ValueError(f"The specified device label {label!r} is already in use")
|
|
207
|
+
self._pydevices.load(label, device, create_core_proxy(self))
|
|
208
|
+
|
|
209
|
+
load_py_device = loadPyDevice
|
|
210
|
+
|
|
211
|
+
def unloadDevice(self, label: DeviceLabel | str) -> None:
|
|
212
|
+
if label not in self._pydevices: # pragma: no cover
|
|
213
|
+
return super().unloadDevice(label)
|
|
214
|
+
self._pydevices.unload(label)
|
|
215
|
+
|
|
216
|
+
def unloadAllDevices(self) -> None:
|
|
217
|
+
self._pydevices.unload_all()
|
|
218
|
+
super().unloadAllDevices()
|
|
219
|
+
|
|
220
|
+
def initializeDevice(self, label: DeviceLabel | str) -> None:
|
|
221
|
+
if label not in self._pydevices: # pragma: no cover
|
|
222
|
+
return super().initializeDevice(label)
|
|
223
|
+
return self._pydevices.initialize(label)
|
|
224
|
+
|
|
225
|
+
def initializeAllDevices(self) -> None:
|
|
226
|
+
super().initializeAllDevices()
|
|
227
|
+
return self._pydevices.initialize_all()
|
|
228
|
+
|
|
229
|
+
def getDeviceInitializationState(self, label: str) -> DeviceInitializationState:
|
|
230
|
+
if label not in self._pydevices: # pragma: no cover
|
|
231
|
+
return super().getDeviceInitializationState(label)
|
|
232
|
+
return self._pydevices.get_initialization_state(label)
|
|
233
|
+
|
|
234
|
+
def getLoadedDevices(self) -> tuple[DeviceLabel, ...]:
|
|
235
|
+
return tuple(self._pydevices) + tuple(super().getLoadedDevices())
|
|
236
|
+
|
|
237
|
+
def getLoadedDevicesOfType(self, devType: int) -> tuple[DeviceLabel, ...]:
|
|
238
|
+
pydevs = self._pydevices.get_labels_of_type(devType)
|
|
239
|
+
return pydevs + super().getLoadedDevicesOfType(devType)
|
|
240
|
+
|
|
241
|
+
def getDeviceType(self, label: str) -> DeviceType:
|
|
242
|
+
if label not in self._pydevices: # pragma: no cover
|
|
243
|
+
return super().getDeviceType(label)
|
|
244
|
+
return self._pydevices[label].type()
|
|
245
|
+
|
|
246
|
+
def getDeviceLibrary(self, label: DeviceLabel | str) -> AdapterName:
|
|
247
|
+
if label not in self._pydevices: # pragma: no cover
|
|
248
|
+
return super().getDeviceLibrary(label)
|
|
249
|
+
return cast("AdapterName", self._pydevices[label].__module__)
|
|
250
|
+
|
|
251
|
+
def getDeviceName(self, label: DeviceLabel | str) -> DeviceName:
|
|
252
|
+
if label not in self._pydevices: # pragma: no cover
|
|
253
|
+
return super().getDeviceName(label)
|
|
254
|
+
return cast("DeviceName", self._pydevices[label].name())
|
|
255
|
+
|
|
256
|
+
def getDeviceDescription(self, label: DeviceLabel | str) -> str:
|
|
257
|
+
if label not in self._pydevices: # pragma: no cover
|
|
258
|
+
return super().getDeviceDescription(label)
|
|
259
|
+
return self._pydevices[label].description()
|
|
260
|
+
|
|
261
|
+
# ---------------------------- Properties ---------------------------
|
|
262
|
+
|
|
263
|
+
def getDevicePropertyNames(
|
|
264
|
+
self, label: DeviceLabel | str
|
|
265
|
+
) -> tuple[PropertyName, ...]:
|
|
266
|
+
if label not in self._pydevices: # pragma: no cover
|
|
267
|
+
return super().getDevicePropertyNames(label)
|
|
268
|
+
names = tuple(self._pydevices[label].get_property_names())
|
|
269
|
+
return cast("tuple[PropertyName, ...]", names)
|
|
270
|
+
|
|
271
|
+
def hasProperty(
|
|
272
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
273
|
+
) -> bool:
|
|
274
|
+
if label not in self._pydevices: # pragma: no cover
|
|
275
|
+
return super().hasProperty(label, propName)
|
|
276
|
+
return self._pydevices[label].has_property(propName)
|
|
277
|
+
|
|
278
|
+
def getProperty(
|
|
279
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
280
|
+
) -> Any: # broadening to Any, because pydevices can return non-string values?
|
|
281
|
+
if label not in self._pydevices: # pragma: no cover
|
|
282
|
+
return super().getProperty(label, propName)
|
|
283
|
+
with self._pydevices[label] as dev:
|
|
284
|
+
value = dev.get_property_value(propName)
|
|
285
|
+
self._state_cache[(label, propName)] = value
|
|
286
|
+
return value
|
|
287
|
+
|
|
288
|
+
def getPropertyFromCache(
|
|
289
|
+
self, deviceLabel: DeviceLabel | str, propName: PropertyName | str
|
|
290
|
+
) -> Any:
|
|
291
|
+
if deviceLabel not in self._pydevices: # pragma: no cover
|
|
292
|
+
return super().getPropertyFromCache(deviceLabel, propName)
|
|
293
|
+
return self._state_cache[(deviceLabel, propName)]
|
|
294
|
+
|
|
295
|
+
def setProperty(
|
|
296
|
+
self, label: str, propName: str, propValue: bool | float | int | str
|
|
297
|
+
) -> None:
|
|
298
|
+
if label not in self._pydevices: # pragma: no cover
|
|
299
|
+
return super().setProperty(label, propName, propValue)
|
|
300
|
+
with self._pydevices[label] as dev:
|
|
301
|
+
dev.set_property_value(propName, propValue)
|
|
302
|
+
self._state_cache[(label, propName)] = propValue
|
|
303
|
+
|
|
304
|
+
def getPropertyType(self, label: str, propName: str) -> PropertyType:
|
|
305
|
+
if label not in self._pydevices: # pragma: no cover
|
|
306
|
+
return super().getPropertyType(label, propName)
|
|
307
|
+
return self._pydevices[label].get_property_info(propName).type
|
|
308
|
+
|
|
309
|
+
def hasPropertyLimits(
|
|
310
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
311
|
+
) -> bool:
|
|
312
|
+
if label not in self._pydevices: # pragma: no cover
|
|
313
|
+
return super().hasPropertyLimits(label, propName)
|
|
314
|
+
with self._pydevices[label] as dev:
|
|
315
|
+
return dev.get_property_info(propName).limits is not None
|
|
316
|
+
|
|
317
|
+
def getPropertyLowerLimit(
|
|
318
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
319
|
+
) -> float:
|
|
320
|
+
if label not in self._pydevices: # pragma: no cover
|
|
321
|
+
return super().getPropertyLowerLimit(label, propName)
|
|
322
|
+
with self._pydevices[label] as dev:
|
|
323
|
+
if lims := dev.get_property_info(propName).limits:
|
|
324
|
+
return lims[0]
|
|
325
|
+
return 0
|
|
326
|
+
|
|
327
|
+
def getPropertyUpperLimit(
|
|
328
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
329
|
+
) -> float:
|
|
330
|
+
if label not in self._pydevices: # pragma: no cover
|
|
331
|
+
return super().getPropertyUpperLimit(label, propName)
|
|
332
|
+
with self._pydevices[label] as dev:
|
|
333
|
+
if lims := dev.get_property_info(propName).limits:
|
|
334
|
+
return lims[1]
|
|
335
|
+
return 0
|
|
336
|
+
|
|
337
|
+
def getAllowedPropertyValues(
|
|
338
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
339
|
+
) -> tuple[str, ...]:
|
|
340
|
+
if label not in self._pydevices: # pragma: no cover
|
|
341
|
+
return super().getAllowedPropertyValues(label, propName)
|
|
342
|
+
with self._pydevices[label] as dev:
|
|
343
|
+
return tuple(dev.get_property_info(propName).allowed_values or ())
|
|
344
|
+
|
|
345
|
+
def isPropertyPreInit(
|
|
346
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
347
|
+
) -> bool:
|
|
348
|
+
if label not in self._pydevices: # pragma: no cover
|
|
349
|
+
return super().isPropertyPreInit(label, propName)
|
|
350
|
+
with self._pydevices[label] as dev:
|
|
351
|
+
return dev.get_property_info(propName).is_pre_init
|
|
352
|
+
|
|
353
|
+
def isPropertyReadOnly(
|
|
354
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
355
|
+
) -> bool:
|
|
356
|
+
if label not in self._pydevices: # pragma: no cover
|
|
357
|
+
return super().isPropertyReadOnly(label, propName)
|
|
358
|
+
with self._pydevices[label] as dev:
|
|
359
|
+
return dev.is_property_read_only(propName)
|
|
360
|
+
|
|
361
|
+
def isPropertySequenceable(
|
|
362
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
363
|
+
) -> bool:
|
|
364
|
+
if label not in self._pydevices: # pragma: no cover
|
|
365
|
+
return super().isPropertySequenceable(label, propName)
|
|
366
|
+
with self._pydevices[label] as dev:
|
|
367
|
+
return dev.is_property_sequenceable(propName)
|
|
368
|
+
|
|
369
|
+
def getPropertySequenceMaxLength(
|
|
370
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
371
|
+
) -> int:
|
|
372
|
+
if label not in self._pydevices: # pragma: no cover
|
|
373
|
+
return super().getPropertySequenceMaxLength(label, propName)
|
|
374
|
+
with self._pydevices[label] as dev:
|
|
375
|
+
return dev.get_property_info(propName).sequence_max_length
|
|
376
|
+
|
|
377
|
+
def loadPropertySequence(
|
|
378
|
+
self,
|
|
379
|
+
label: DeviceLabel | str,
|
|
380
|
+
propName: PropertyName | str,
|
|
381
|
+
eventSequence: Sequence[Any],
|
|
382
|
+
) -> None:
|
|
383
|
+
if label not in self._pydevices: # pragma: no cover
|
|
384
|
+
return super().loadPropertySequence(label, propName, eventSequence)
|
|
385
|
+
with self._pydevices[label] as dev:
|
|
386
|
+
dev.load_property_sequence(propName, eventSequence)
|
|
387
|
+
|
|
388
|
+
def startPropertySequence(
|
|
389
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
390
|
+
) -> None:
|
|
391
|
+
if label not in self._pydevices: # pragma: no cover
|
|
392
|
+
return super().startPropertySequence(label, propName)
|
|
393
|
+
with self._pydevices[label] as dev:
|
|
394
|
+
dev.start_property_sequence(propName)
|
|
395
|
+
|
|
396
|
+
def stopPropertySequence(
|
|
397
|
+
self, label: DeviceLabel | str, propName: PropertyName | str
|
|
398
|
+
) -> None:
|
|
399
|
+
if label not in self._pydevices: # pragma: no cover
|
|
400
|
+
return super().stopPropertySequence(label, propName)
|
|
401
|
+
with self._pydevices[label] as dev:
|
|
402
|
+
dev.stop_property_sequence(propName)
|
|
403
|
+
|
|
404
|
+
# ------------------------------ Ready State ----------------------------
|
|
405
|
+
|
|
406
|
+
def deviceBusy(self, label: DeviceLabel | str) -> bool:
|
|
407
|
+
if label not in self._pydevices: # pragma: no cover
|
|
408
|
+
return super().deviceBusy(label)
|
|
409
|
+
with self._pydevices[label] as dev:
|
|
410
|
+
return dev.busy()
|
|
411
|
+
|
|
412
|
+
def waitForDevice(self, label: DeviceLabel | str) -> None:
|
|
413
|
+
if label not in self._pydevices: # pragma: no cover
|
|
414
|
+
return super().waitForDevice(label)
|
|
415
|
+
self._pydevices.wait_for(label, self.getTimeoutMs())
|
|
416
|
+
|
|
417
|
+
# def waitForConfig
|
|
418
|
+
|
|
419
|
+
# probably only needed because C++ method is not virtual
|
|
420
|
+
def systemBusy(self) -> bool:
|
|
421
|
+
return self.deviceTypeBusy(DeviceType.AnyType)
|
|
422
|
+
|
|
423
|
+
# probably only needed because C++ method is not virtual
|
|
424
|
+
def waitForSystem(self) -> None:
|
|
425
|
+
self.waitForDeviceType(DeviceType.AnyType)
|
|
426
|
+
|
|
427
|
+
def waitForDeviceType(self, devType: int) -> None:
|
|
428
|
+
super().waitForDeviceType(devType)
|
|
429
|
+
self._pydevices.wait_for_device_type(devType, self.getTimeoutMs())
|
|
430
|
+
|
|
431
|
+
def deviceTypeBusy(self, devType: int) -> bool:
|
|
432
|
+
if super().deviceTypeBusy(devType):
|
|
433
|
+
return True # pragma: no cover
|
|
434
|
+
|
|
435
|
+
for label in self._pydevices.get_labels_of_type(devType):
|
|
436
|
+
with self._pydevices[label] as dev:
|
|
437
|
+
if dev.busy():
|
|
438
|
+
return True
|
|
439
|
+
return False
|
|
440
|
+
|
|
441
|
+
def getDeviceDelayMs(self, label: DeviceLabel | str) -> float:
|
|
442
|
+
if label not in self._pydevices: # pragma: no cover
|
|
443
|
+
return super().getDeviceDelayMs(label)
|
|
444
|
+
return 0 # pydevices don't yet support delays
|
|
445
|
+
|
|
446
|
+
def setDeviceDelayMs(self, label: DeviceLabel | str, delayMs: float) -> None:
|
|
447
|
+
if label not in self._pydevices: # pragma: no cover
|
|
448
|
+
return super().setDeviceDelayMs(label, delayMs)
|
|
449
|
+
if delayMs != 0: # pragma: no cover
|
|
450
|
+
raise NotImplementedError("Python devices do not support delays")
|
|
451
|
+
|
|
452
|
+
def usesDeviceDelay(self, label: DeviceLabel | str) -> bool:
|
|
453
|
+
if label not in self._pydevices: # pragma: no cover
|
|
454
|
+
return super().usesDeviceDelay(label)
|
|
455
|
+
return False
|
|
456
|
+
|
|
457
|
+
# ########################################################################
|
|
458
|
+
# ---------------------------- XYStageDevice -----------------------------
|
|
459
|
+
# ########################################################################
|
|
460
|
+
|
|
461
|
+
def setXYStageDevice(self, xyStageLabel: DeviceLabel | str) -> None:
|
|
462
|
+
label = self._set_current_if_pydevice(KW.CoreXYStage, xyStageLabel)
|
|
463
|
+
super().setXYStageDevice(label)
|
|
464
|
+
|
|
465
|
+
def getXYStageDevice(self) -> DeviceLabel | Literal[""]:
|
|
466
|
+
"""Returns the label of the currently selected XYStage device.
|
|
467
|
+
|
|
468
|
+
Returns empty string if no XYStage device is selected.
|
|
469
|
+
"""
|
|
470
|
+
return self._pycore.current(KW.CoreXYStage) or super().getXYStageDevice()
|
|
471
|
+
|
|
472
|
+
@overload
|
|
473
|
+
def setXYPosition(self, x: float, y: float, /) -> None: ...
|
|
474
|
+
@overload
|
|
475
|
+
def setXYPosition(
|
|
476
|
+
self, xyStageLabel: DeviceLabel | str, x: float, y: float, /
|
|
477
|
+
) -> None: ...
|
|
478
|
+
def setXYPosition(self, *args: Any) -> None:
|
|
479
|
+
"""Sets the position of the XY stage in microns."""
|
|
480
|
+
label, args = _ensure_label(args, min_args=3, getter=self.getXYStageDevice)
|
|
481
|
+
if label not in self._pydevices: # pragma: no cover
|
|
482
|
+
return super().setXYPosition(label, *args)
|
|
483
|
+
|
|
484
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
485
|
+
dev.set_position_um(*args)
|
|
486
|
+
|
|
487
|
+
@overload
|
|
488
|
+
def getXYPosition(self) -> tuple[float, float]: ...
|
|
489
|
+
@overload
|
|
490
|
+
def getXYPosition(self, xyStageLabel: DeviceLabel | str) -> tuple[float, float]: ...
|
|
491
|
+
def getXYPosition(
|
|
492
|
+
self, xyStageLabel: DeviceLabel | str = ""
|
|
493
|
+
) -> tuple[float, float]:
|
|
494
|
+
"""Obtains the current position of the XY stage in microns."""
|
|
495
|
+
label = xyStageLabel or self.getXYStageDevice()
|
|
496
|
+
if label not in self._pydevices: # pragma: no cover
|
|
497
|
+
return tuple(super().getXYPosition(label)) # type: ignore
|
|
498
|
+
|
|
499
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
500
|
+
return dev.get_position_um()
|
|
501
|
+
|
|
502
|
+
# reimplementation needed because the C++ method are not virtual
|
|
503
|
+
@overload
|
|
504
|
+
def getXPosition(self) -> float: ...
|
|
505
|
+
@overload
|
|
506
|
+
def getXPosition(self, xyStageLabel: DeviceLabel | str) -> float: ...
|
|
507
|
+
def getXPosition(self, xyStageLabel: DeviceLabel | str = "") -> float:
|
|
508
|
+
"""Obtains the current position of the X axis of the XY stage in microns."""
|
|
509
|
+
return self.getXYPosition(xyStageLabel)[0]
|
|
510
|
+
|
|
511
|
+
# reimplementation needed because the C++ method are not virtual
|
|
512
|
+
@overload
|
|
513
|
+
def getYPosition(self) -> float: ...
|
|
514
|
+
@overload
|
|
515
|
+
def getYPosition(self, xyStageLabel: DeviceLabel | str) -> float: ...
|
|
516
|
+
def getYPosition(self, xyStageLabel: DeviceLabel | str = "") -> float:
|
|
517
|
+
"""Obtains the current position of the Y axis of the XY stage in microns."""
|
|
518
|
+
return self.getXYPosition(xyStageLabel)[1]
|
|
519
|
+
|
|
520
|
+
def getXYStageSequenceMaxLength(self, xyStageLabel: DeviceLabel | str) -> int:
|
|
521
|
+
"""Gets the maximum length of an XY stage's position sequence."""
|
|
522
|
+
if xyStageLabel not in self._pydevices: # pragma: no cover
|
|
523
|
+
return super().getXYStageSequenceMaxLength(xyStageLabel)
|
|
524
|
+
dev = self._pydevices.get_device_of_type(xyStageLabel, XYStageDevice)
|
|
525
|
+
return dev.get_sequence_max_length()
|
|
526
|
+
|
|
527
|
+
def isXYStageSequenceable(self, xyStageLabel: DeviceLabel | str) -> bool:
|
|
528
|
+
"""Queries XY stage if it can be used in a sequence."""
|
|
529
|
+
if xyStageLabel not in self._pydevices: # pragma: no cover
|
|
530
|
+
return super().isXYStageSequenceable(xyStageLabel)
|
|
531
|
+
dev = self._pydevices.get_device_of_type(xyStageLabel, XYStageDevice)
|
|
532
|
+
return dev.is_sequenceable()
|
|
533
|
+
|
|
534
|
+
def loadXYStageSequence(
|
|
535
|
+
self,
|
|
536
|
+
xyStageLabel: DeviceLabel | str,
|
|
537
|
+
xSequence: Sequence[float],
|
|
538
|
+
ySequence: Sequence[float],
|
|
539
|
+
/,
|
|
540
|
+
) -> None:
|
|
541
|
+
"""Transfer a sequence of stage positions to the xy stage.
|
|
542
|
+
|
|
543
|
+
xSequence and ySequence must have the same length. This should only be called
|
|
544
|
+
for XY stages that are sequenceable
|
|
545
|
+
"""
|
|
546
|
+
if xyStageLabel not in self._pydevices: # pragma: no cover
|
|
547
|
+
return super().loadXYStageSequence(xyStageLabel, xSequence, ySequence)
|
|
548
|
+
if len(xSequence) != len(ySequence):
|
|
549
|
+
raise ValueError("xSequence and ySequence must have the same length")
|
|
550
|
+
dev = self._pydevices.get_device_of_type(xyStageLabel, XYStageDevice)
|
|
551
|
+
seq = tuple(zip(xSequence, ySequence))
|
|
552
|
+
if len(seq) > dev.get_sequence_max_length():
|
|
553
|
+
raise ValueError(
|
|
554
|
+
f"Sequence is too long. Max length is {dev.get_sequence_max_length()}"
|
|
555
|
+
)
|
|
556
|
+
dev.send_sequence(seq)
|
|
557
|
+
|
|
558
|
+
@overload
|
|
559
|
+
def setOriginX(self) -> None: ...
|
|
560
|
+
@overload
|
|
561
|
+
def setOriginX(self, xyStageLabel: DeviceLabel | str) -> None: ...
|
|
562
|
+
def setOriginX(self, xyStageLabel: DeviceLabel | str = "") -> None:
|
|
563
|
+
"""Zero the given XY stage's X coordinate at the current position."""
|
|
564
|
+
label = xyStageLabel or self.getXYStageDevice()
|
|
565
|
+
if label not in self._pydevices: # pragma: no cover
|
|
566
|
+
return super().setOriginX(label)
|
|
567
|
+
|
|
568
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
569
|
+
dev.set_origin_x()
|
|
570
|
+
|
|
571
|
+
@overload
|
|
572
|
+
def setOriginY(self) -> None: ...
|
|
573
|
+
@overload
|
|
574
|
+
def setOriginY(self, xyStageLabel: DeviceLabel | str) -> None: ...
|
|
575
|
+
def setOriginY(self, xyStageLabel: DeviceLabel | str = "") -> None:
|
|
576
|
+
"""Zero the given XY stage's Y coordinate at the current position."""
|
|
577
|
+
label = xyStageLabel or self.getXYStageDevice()
|
|
578
|
+
if label not in self._pydevices: # pragma: no cover
|
|
579
|
+
return super().setOriginY(label)
|
|
580
|
+
|
|
581
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
582
|
+
dev.set_origin_y()
|
|
583
|
+
|
|
584
|
+
@overload
|
|
585
|
+
def setOriginXY(self) -> None: ...
|
|
586
|
+
@overload
|
|
587
|
+
def setOriginXY(self, xyStageLabel: DeviceLabel | str) -> None: ...
|
|
588
|
+
def setOriginXY(self, xyStageLabel: DeviceLabel | str = "") -> None:
|
|
589
|
+
"""Zero the given XY stage's coordinates at the current position."""
|
|
590
|
+
label = xyStageLabel or self.getXYStageDevice()
|
|
591
|
+
if label not in self._pydevices: # pragma: no cover
|
|
592
|
+
return super().setOriginXY(label)
|
|
593
|
+
|
|
594
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
595
|
+
dev.set_origin()
|
|
596
|
+
|
|
597
|
+
@overload
|
|
598
|
+
def setAdapterOriginXY(self, newXUm: float, newYUm: float, /) -> None: ...
|
|
599
|
+
@overload
|
|
600
|
+
def setAdapterOriginXY(
|
|
601
|
+
self, xyStageLabel: DeviceLabel | str, newXUm: float, newYUm: float, /
|
|
602
|
+
) -> None: ...
|
|
603
|
+
def setAdapterOriginXY(self, *args: Any) -> None:
|
|
604
|
+
"""Enable software translation of coordinates for the current XY stage.
|
|
605
|
+
|
|
606
|
+
The current position of the stage becomes (newXUm, newYUm). It is recommended
|
|
607
|
+
that setOriginXY() be used instead where available.
|
|
608
|
+
"""
|
|
609
|
+
label, args = _ensure_label(args, min_args=3, getter=self.getXYStageDevice)
|
|
610
|
+
if label not in self._pydevices: # pragma: no cover
|
|
611
|
+
return super().setAdapterOriginXY(label, *args)
|
|
612
|
+
|
|
613
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
614
|
+
dev.set_adapter_origin_um(*args)
|
|
615
|
+
|
|
616
|
+
@overload
|
|
617
|
+
def setRelativeXYPosition(self, dx: float, dy: float, /) -> None: ...
|
|
618
|
+
@overload
|
|
619
|
+
def setRelativeXYPosition(
|
|
620
|
+
self, xyStageLabel: DeviceLabel | str, dx: float, dy: float, /
|
|
621
|
+
) -> None: ...
|
|
622
|
+
def setRelativeXYPosition(self, *args: Any) -> None:
|
|
623
|
+
"""Sets the relative position of the XY stage in microns."""
|
|
624
|
+
label, args = _ensure_label(args, min_args=3, getter=self.getXYStageDevice)
|
|
625
|
+
if label not in self._pydevices: # pragma: no cover
|
|
626
|
+
return super().setRelativeXYPosition(label, *args)
|
|
627
|
+
|
|
628
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
629
|
+
dev.set_relative_position_um(*args)
|
|
630
|
+
|
|
631
|
+
def startXYStageSequence(self, xyStageLabel: DeviceLabel | str) -> None:
|
|
632
|
+
"""Starts an ongoing sequence of triggered events in an XY stage.
|
|
633
|
+
|
|
634
|
+
This should only be called for stages that are sequenceable
|
|
635
|
+
"""
|
|
636
|
+
label = xyStageLabel or self.getXYStageDevice()
|
|
637
|
+
if label not in self._pydevices: # pragma: no cover
|
|
638
|
+
return super().startXYStageSequence(label)
|
|
639
|
+
|
|
640
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
641
|
+
dev.start_sequence()
|
|
642
|
+
|
|
643
|
+
def stopXYStageSequence(self, xyStageLabel: DeviceLabel | str) -> None:
|
|
644
|
+
"""Stops an ongoing sequence of triggered events in an XY stage.
|
|
645
|
+
|
|
646
|
+
This should only be called for stages that are sequenceable
|
|
647
|
+
"""
|
|
648
|
+
label = xyStageLabel or self.getXYStageDevice()
|
|
649
|
+
if label not in self._pydevices: # pragma: no cover
|
|
650
|
+
return super().stopXYStageSequence(label)
|
|
651
|
+
|
|
652
|
+
with self._pydevices.get_device_of_type(label, XYStageDevice) as dev:
|
|
653
|
+
dev.stop_sequence()
|
|
654
|
+
|
|
655
|
+
# -----------------------------------------------------------------------
|
|
656
|
+
# ---------------------------- Any Stage --------------------------------
|
|
657
|
+
# -----------------------------------------------------------------------
|
|
658
|
+
|
|
659
|
+
def home(self, xyOrZStageLabel: DeviceLabel | str) -> None:
|
|
660
|
+
"""Perform a hardware homing operation for an XY or focus/Z stage."""
|
|
661
|
+
if xyOrZStageLabel not in self._pydevices:
|
|
662
|
+
return super().home(xyOrZStageLabel)
|
|
663
|
+
|
|
664
|
+
dev = self._pydevices.get_device_of_type(xyOrZStageLabel, _BaseStage)
|
|
665
|
+
dev.home()
|
|
666
|
+
|
|
667
|
+
def stop(self, xyOrZStageLabel: DeviceLabel | str) -> None:
|
|
668
|
+
"""Stop the XY or focus/Z stage."""
|
|
669
|
+
if xyOrZStageLabel not in self._pydevices:
|
|
670
|
+
return super().stop(xyOrZStageLabel)
|
|
671
|
+
|
|
672
|
+
dev = self._pydevices.get_device_of_type(xyOrZStageLabel, _BaseStage)
|
|
673
|
+
dev.stop()
|
|
674
|
+
|
|
675
|
+
# ########################################################################
|
|
676
|
+
# ------------------------ Camera Device Methods -------------------------
|
|
677
|
+
# ########################################################################
|
|
678
|
+
|
|
679
|
+
# --------------------------------------------------------------------- utils
|
|
680
|
+
|
|
681
|
+
def _py_camera(self, cameraLabel: str | None = None) -> Camera | None:
|
|
682
|
+
"""Return the *Python* Camera for ``label`` (or current), else ``None``."""
|
|
683
|
+
label = cameraLabel or self.getCameraDevice()
|
|
684
|
+
if label in self._pydevices:
|
|
685
|
+
return self._pydevices.get_device_of_type(label, Camera)
|
|
686
|
+
return None
|
|
687
|
+
|
|
688
|
+
def setCameraDevice(self, cameraLabel: DeviceLabel | str) -> None:
|
|
689
|
+
"""Set the camera device."""
|
|
690
|
+
label = self._set_current_if_pydevice(KW.CoreCamera, cameraLabel)
|
|
691
|
+
super().setCameraDevice(label)
|
|
692
|
+
|
|
693
|
+
def getCameraDevice(self) -> DeviceLabel | Literal[""]:
|
|
694
|
+
"""Returns the label of the currently selected camera device.
|
|
695
|
+
|
|
696
|
+
Returns empty string if no camera device is selected.
|
|
697
|
+
"""
|
|
698
|
+
return self._pycore.current(KW.CoreCamera) or super().getCameraDevice()
|
|
699
|
+
|
|
700
|
+
# --------------------------------------------------------------------- snap
|
|
701
|
+
|
|
702
|
+
_current_image_buffer: np.ndarray | None = None
|
|
703
|
+
|
|
704
|
+
def _do_snap_image(self) -> None:
|
|
705
|
+
if (cam := self._py_camera()) is None:
|
|
706
|
+
return pymmcore.CMMCore.snapImage(self)
|
|
707
|
+
|
|
708
|
+
buf = None
|
|
709
|
+
|
|
710
|
+
def _get_buffer(shape: Sequence[int], dtype: DTypeLike) -> np.ndarray:
|
|
711
|
+
"""Get a buffer for the camera image."""
|
|
712
|
+
nonlocal buf
|
|
713
|
+
buf = np.empty(shape, dtype=dtype)
|
|
714
|
+
return buf
|
|
715
|
+
|
|
716
|
+
# synchronous call - consume one item from the generator
|
|
717
|
+
with cam:
|
|
718
|
+
for _ in cam.start_sequence(1, get_buffer=_get_buffer):
|
|
719
|
+
if buf is not None:
|
|
720
|
+
self._current_image_buffer = buf
|
|
721
|
+
else: # pragma: no cover # bad camera implementation
|
|
722
|
+
warnings.warn(
|
|
723
|
+
"Camera device did not provide an image buffer.",
|
|
724
|
+
RuntimeWarning,
|
|
725
|
+
stacklevel=2,
|
|
726
|
+
)
|
|
727
|
+
return
|
|
728
|
+
|
|
729
|
+
# --------------------------------------------------------------------- getImage
|
|
730
|
+
|
|
731
|
+
@overload
|
|
732
|
+
def getImage(self, *, fix: bool = True) -> np.ndarray: ...
|
|
733
|
+
@overload
|
|
734
|
+
def getImage(self, numChannel: int, *, fix: bool = True) -> np.ndarray: ...
|
|
735
|
+
|
|
736
|
+
def getImage(
|
|
737
|
+
self, numChannel: int | None = None, *, fix: bool = True
|
|
738
|
+
) -> np.ndarray:
|
|
739
|
+
if self._py_camera() is None: # pragma: no cover
|
|
740
|
+
if numChannel is not None:
|
|
741
|
+
return super().getImage(numChannel, fix=fix)
|
|
742
|
+
return super().getImage(fix=fix)
|
|
743
|
+
|
|
744
|
+
if self._current_image_buffer is None:
|
|
745
|
+
raise RuntimeError(
|
|
746
|
+
"No image buffer available. Call snapImage() before calling getImage()."
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
return self._current_image_buffer
|
|
750
|
+
|
|
751
|
+
# ---------------------------------------------------------------- sequence common
|
|
752
|
+
|
|
753
|
+
def _start_sequence(
|
|
754
|
+
self, cam: Camera, n_images: int | None, stop_on_overflow: bool
|
|
755
|
+
) -> None:
|
|
756
|
+
"""Initialise _seq state and call cam.start_sequence."""
|
|
757
|
+
shape, dtype = cam.shape(), np.dtype(cam.dtype())
|
|
758
|
+
camera_label = cam.get_label()
|
|
759
|
+
|
|
760
|
+
n_components = shape[2] if len(shape) > 2 else 1
|
|
761
|
+
base_meta: dict[str, Any] = {
|
|
762
|
+
KW.Binning: cam.get_property_value(KW.Binning),
|
|
763
|
+
KW.Metadata_CameraLabel: camera_label,
|
|
764
|
+
KW.Metadata_Height: str(shape[0]),
|
|
765
|
+
KW.Metadata_Width: str(shape[1]),
|
|
766
|
+
KW.Metadata_ROI_X: "0",
|
|
767
|
+
KW.Metadata_ROI_Y: "0",
|
|
768
|
+
KW.PixelType: PixelType.for_bytes(dtype.itemsize, n_components),
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
def get_buffer_with_overflow_handling(
|
|
772
|
+
shape: Sequence[int], dtype: DTypeLike
|
|
773
|
+
) -> np.ndarray:
|
|
774
|
+
try:
|
|
775
|
+
return self._seq_buffer.acquire_slot(shape, dtype)
|
|
776
|
+
except BufferError:
|
|
777
|
+
if not stop_on_overflow: # we shouldn't get here...
|
|
778
|
+
raise # pragma: no cover
|
|
779
|
+
raise BufferOverflowStop() from None
|
|
780
|
+
|
|
781
|
+
# Keep track of images acquired for metadata and auto-stop
|
|
782
|
+
counter = count()
|
|
783
|
+
|
|
784
|
+
# Create metadata-injecting wrapper for finalize callback
|
|
785
|
+
def finalize_with_metadata(cam_meta: Mapping) -> None:
|
|
786
|
+
img_number = next(counter)
|
|
787
|
+
elapsed_ms = (perf_counter_ns() - start_time) / 1e6
|
|
788
|
+
received = datetime.now().isoformat(sep=" ")
|
|
789
|
+
meta_update = {
|
|
790
|
+
**cam_meta,
|
|
791
|
+
KW.Metadata_TimeInCore: received,
|
|
792
|
+
KW.Metadata_ImageNumber: str(img_number),
|
|
793
|
+
KW.Elapsed_Time_ms: f"{elapsed_ms:.2f}",
|
|
794
|
+
}
|
|
795
|
+
self._seq_buffer.finalize_slot({**base_meta, **meta_update})
|
|
796
|
+
|
|
797
|
+
# Auto-stop when we've acquired the requested number of images
|
|
798
|
+
if n_images is not None and (img_number + 1) >= n_images:
|
|
799
|
+
self._stop_event.set()
|
|
800
|
+
|
|
801
|
+
# Reset the circular buffer and stop event -------------
|
|
802
|
+
|
|
803
|
+
self._stop_event.clear()
|
|
804
|
+
self._seq_buffer.clear()
|
|
805
|
+
self._seq_buffer.overwrite_on_overflow = not stop_on_overflow
|
|
806
|
+
|
|
807
|
+
# Create the Acquisition Thread ---------
|
|
808
|
+
|
|
809
|
+
self._acquisition_thread = AcquisitionThread(
|
|
810
|
+
image_generator=cam.start_sequence(
|
|
811
|
+
n_images or 2**63 - 1,
|
|
812
|
+
get_buffer_with_overflow_handling,
|
|
813
|
+
),
|
|
814
|
+
finalize=finalize_with_metadata,
|
|
815
|
+
label=camera_label,
|
|
816
|
+
stop_event=self._stop_event,
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
# Zoom zoom ---------
|
|
820
|
+
|
|
821
|
+
start_time = perf_counter_ns()
|
|
822
|
+
self._acquisition_thread.start()
|
|
823
|
+
|
|
824
|
+
# ------------------------------------------------- startSequenceAcquisition
|
|
825
|
+
|
|
826
|
+
def _do_start_sequence_acquisition(
|
|
827
|
+
self, cameraLabel: str, numImages: int, intervalMs: float, stopOnOverflow: bool
|
|
828
|
+
) -> None:
|
|
829
|
+
if (cam := self._py_camera(cameraLabel)) is None: # pragma: no cover
|
|
830
|
+
return pymmcore.CMMCore.startSequenceAcquisition(
|
|
831
|
+
self, cameraLabel, numImages, intervalMs, stopOnOverflow
|
|
832
|
+
)
|
|
833
|
+
with cam:
|
|
834
|
+
self._start_sequence(cam, numImages, stopOnOverflow)
|
|
835
|
+
|
|
836
|
+
# ------------------------------------------------- continuous acquisition
|
|
837
|
+
|
|
838
|
+
def _do_start_continuous_sequence_acquisition(self, intervalMs: float = 0) -> None:
|
|
839
|
+
if (cam := self._py_camera()) is None: # pragma: no cover
|
|
840
|
+
return pymmcore.CMMCore.startContinuousSequenceAcquisition(self, intervalMs)
|
|
841
|
+
with cam:
|
|
842
|
+
self._start_sequence(cam, None, False)
|
|
843
|
+
|
|
844
|
+
# ---------------------------------------------------------------- stopSequence
|
|
845
|
+
|
|
846
|
+
def _do_stop_sequence_acquisition(self, cameraLabel: str) -> None:
|
|
847
|
+
if self._py_camera(cameraLabel) is None: # pragma: no cover
|
|
848
|
+
pymmcore.CMMCore.stopSequenceAcquisition(self, cameraLabel)
|
|
849
|
+
|
|
850
|
+
if self._acquisition_thread is not None:
|
|
851
|
+
self._stop_event.set()
|
|
852
|
+
self._acquisition_thread.join()
|
|
853
|
+
self._acquisition_thread = None
|
|
854
|
+
|
|
855
|
+
# ------------------------------------------------------------------ queries
|
|
856
|
+
@overload
|
|
857
|
+
def isSequenceRunning(self) -> bool: ...
|
|
858
|
+
@overload
|
|
859
|
+
def isSequenceRunning(self, cameraLabel: DeviceLabel | str) -> bool: ...
|
|
860
|
+
def isSequenceRunning(self, cameraLabel: DeviceLabel | str | None = None) -> bool:
|
|
861
|
+
if self._py_camera(cameraLabel) is None:
|
|
862
|
+
return super().isSequenceRunning()
|
|
863
|
+
|
|
864
|
+
if self._acquisition_thread is None:
|
|
865
|
+
return False
|
|
866
|
+
|
|
867
|
+
# Check if the thread is actually still alive
|
|
868
|
+
if not self._acquisition_thread.is_alive():
|
|
869
|
+
# Thread has finished, clean it up
|
|
870
|
+
self._acquisition_thread = None
|
|
871
|
+
return False
|
|
872
|
+
|
|
873
|
+
return True
|
|
874
|
+
|
|
875
|
+
def getRemainingImageCount(self) -> int:
|
|
876
|
+
if self._py_camera() is None:
|
|
877
|
+
return super().getRemainingImageCount()
|
|
878
|
+
return len(self._seq_buffer) if self._seq_buffer is not None else 0
|
|
879
|
+
|
|
880
|
+
# ---------------------------------------------------- getImages
|
|
881
|
+
|
|
882
|
+
def getLastImage(self) -> np.ndarray:
|
|
883
|
+
if self._py_camera() is None:
|
|
884
|
+
return super().getLastImage()
|
|
885
|
+
if not (self._seq_buffer) or (result := self._seq_buffer.peek_last()) is None:
|
|
886
|
+
raise IndexError("Circular buffer is empty.")
|
|
887
|
+
return result[0]
|
|
888
|
+
|
|
889
|
+
@overload
|
|
890
|
+
def getLastImageMD(
|
|
891
|
+
self, channel: int, slice: int, md: pymmcore.Metadata, /
|
|
892
|
+
) -> np.ndarray: ...
|
|
893
|
+
@overload
|
|
894
|
+
def getLastImageMD(self, md: pymmcore.Metadata, /) -> np.ndarray: ...
|
|
895
|
+
def getLastImageMD(self, *args: Any) -> np.ndarray:
|
|
896
|
+
if self._py_camera() is None:
|
|
897
|
+
return super().getLastImageMD(*args)
|
|
898
|
+
md_object = args[0] if len(args) == 1 else args[-1]
|
|
899
|
+
if not isinstance(md_object, pymmcore.Metadata): # pragma: no cover
|
|
900
|
+
raise TypeError("Expected a Metadata object for the last argument.")
|
|
901
|
+
|
|
902
|
+
if not (self._seq_buffer) or (result := self._seq_buffer.peek_last()) is None:
|
|
903
|
+
raise IndexError("Circular buffer is empty.")
|
|
904
|
+
|
|
905
|
+
img, md = result
|
|
906
|
+
for k, v in md.items():
|
|
907
|
+
tag = pymmcore.MetadataSingleTag(k, "_", False)
|
|
908
|
+
tag.SetValue(str(v))
|
|
909
|
+
md_object.SetTag(tag)
|
|
910
|
+
|
|
911
|
+
return img
|
|
912
|
+
|
|
913
|
+
def getNBeforeLastImageMD(self, n: int, md: pymmcore.Metadata, /) -> np.ndarray:
|
|
914
|
+
if self._py_camera() is None:
|
|
915
|
+
return super().getNBeforeLastImageMD(n, md)
|
|
916
|
+
|
|
917
|
+
if (
|
|
918
|
+
not (self._seq_buffer)
|
|
919
|
+
or (result := self._seq_buffer.peek_nth_from_last(n)) is None
|
|
920
|
+
):
|
|
921
|
+
raise IndexError("Circular buffer is empty or n is out of range.")
|
|
922
|
+
|
|
923
|
+
img, md_data = result
|
|
924
|
+
for k, v in md_data.items():
|
|
925
|
+
tag = pymmcore.MetadataSingleTag(k, "_", False)
|
|
926
|
+
tag.SetValue(str(v))
|
|
927
|
+
md.SetTag(tag)
|
|
928
|
+
|
|
929
|
+
return img
|
|
930
|
+
|
|
931
|
+
# ---------------------------------------------------- popNext
|
|
932
|
+
|
|
933
|
+
def _pop_or_raise(self) -> tuple[np.ndarray, Mapping]:
|
|
934
|
+
if not self._seq_buffer or (data := self._seq_buffer.pop_next()) is None:
|
|
935
|
+
raise IndexError("Circular buffer is empty.")
|
|
936
|
+
return data
|
|
937
|
+
|
|
938
|
+
def popNextImage(self, *, fix: bool = True) -> np.ndarray:
|
|
939
|
+
if self._py_camera() is None:
|
|
940
|
+
return super().popNextImage(fix=fix)
|
|
941
|
+
return self._pop_or_raise()[0]
|
|
942
|
+
|
|
943
|
+
@overload
|
|
944
|
+
def popNextImageMD(
|
|
945
|
+
self, channel: int, slice: int, md: pymmcore.Metadata, /
|
|
946
|
+
) -> np.ndarray: ...
|
|
947
|
+
@overload
|
|
948
|
+
def popNextImageMD(self, md: pymmcore.Metadata, /) -> np.ndarray: ...
|
|
949
|
+
def popNextImageMD(self, *args: Any) -> np.ndarray:
|
|
950
|
+
if self._py_camera() is None:
|
|
951
|
+
return super().popNextImageMD(*args)
|
|
952
|
+
|
|
953
|
+
md_object = args[0] if len(args) == 1 else args[-1]
|
|
954
|
+
if not isinstance(md_object, pymmcore.Metadata): # pragma: no cover
|
|
955
|
+
raise TypeError("Expected a Metadata object for the last argument.")
|
|
956
|
+
|
|
957
|
+
img, md = self._pop_or_raise()
|
|
958
|
+
for k, v in md.items():
|
|
959
|
+
tag = pymmcore.MetadataSingleTag(k, "_", False)
|
|
960
|
+
tag.SetValue(str(v))
|
|
961
|
+
md_object.SetTag(tag)
|
|
962
|
+
return img
|
|
963
|
+
|
|
964
|
+
# ---------------------------------------------------------------- circular buffer
|
|
965
|
+
|
|
966
|
+
def setCircularBufferMemoryFootprint(self, sizeMB: int) -> None:
|
|
967
|
+
"""Set the circular buffer memory footprint in MB."""
|
|
968
|
+
if self._py_camera() is None:
|
|
969
|
+
return super().setCircularBufferMemoryFootprint(sizeMB)
|
|
970
|
+
|
|
971
|
+
if sizeMB <= 0: # pragma: no cover
|
|
972
|
+
raise ValueError("Buffer size must be greater than 0 MB")
|
|
973
|
+
|
|
974
|
+
# TODO: what if sequence is running?
|
|
975
|
+
if self.isSequenceRunning():
|
|
976
|
+
self.stopSequenceAcquisition()
|
|
977
|
+
|
|
978
|
+
self._seq_buffer = SequenceBuffer(size_mb=sizeMB)
|
|
979
|
+
|
|
980
|
+
def initializeCircularBuffer(self) -> None:
|
|
981
|
+
"""Initialize the circular buffer."""
|
|
982
|
+
if self._py_camera() is None:
|
|
983
|
+
return super().initializeCircularBuffer()
|
|
984
|
+
|
|
985
|
+
self._seq_buffer.clear()
|
|
986
|
+
|
|
987
|
+
def getBufferFreeCapacity(self) -> int:
|
|
988
|
+
"""Get the number of free slots in the circular buffer."""
|
|
989
|
+
if (cam := self._py_camera()) is None:
|
|
990
|
+
return super().getBufferFreeCapacity()
|
|
991
|
+
|
|
992
|
+
if (bytes_per_frame := self._predicted_bytes_per_frame(cam)) <= 0:
|
|
993
|
+
return 0 # pragma: no cover # Invalid frame size
|
|
994
|
+
|
|
995
|
+
if (free_bytes := self._seq_buffer.free_bytes) <= 0:
|
|
996
|
+
return 0
|
|
997
|
+
|
|
998
|
+
return free_bytes // bytes_per_frame
|
|
999
|
+
|
|
1000
|
+
def getBufferTotalCapacity(self) -> int:
|
|
1001
|
+
"""Get the total capacity of the circular buffer."""
|
|
1002
|
+
if (cam := self._py_camera()) is None:
|
|
1003
|
+
return super().getBufferTotalCapacity()
|
|
1004
|
+
|
|
1005
|
+
if (bytes_per_frame := self._predicted_bytes_per_frame(cam)) <= 0:
|
|
1006
|
+
return 0 # pragma: no cover # Invalid frame size
|
|
1007
|
+
|
|
1008
|
+
return self._seq_buffer.size_bytes // bytes_per_frame
|
|
1009
|
+
|
|
1010
|
+
def _predicted_bytes_per_frame(self, cam: Camera) -> int:
|
|
1011
|
+
# Estimate capacity based on camera settings and circular buffer size
|
|
1012
|
+
shape, dtype = cam.shape(), np.dtype(cam.dtype())
|
|
1013
|
+
return int(np.prod(shape) * dtype.itemsize)
|
|
1014
|
+
|
|
1015
|
+
def getCircularBufferMemoryFootprint(self) -> int:
|
|
1016
|
+
"""Get the circular buffer memory footprint in MB."""
|
|
1017
|
+
if self._py_camera() is None:
|
|
1018
|
+
return super().getCircularBufferMemoryFootprint()
|
|
1019
|
+
|
|
1020
|
+
return int(self._seq_buffer.size_mb)
|
|
1021
|
+
|
|
1022
|
+
def clearCircularBuffer(self) -> None:
|
|
1023
|
+
"""Clear all images from the circular buffer."""
|
|
1024
|
+
if self._py_camera() is None:
|
|
1025
|
+
return super().clearCircularBuffer()
|
|
1026
|
+
|
|
1027
|
+
self._seq_buffer.clear()
|
|
1028
|
+
|
|
1029
|
+
def isBufferOverflowed(self) -> bool:
|
|
1030
|
+
"""Check if the circular buffer has overflowed."""
|
|
1031
|
+
if self._py_camera() is None:
|
|
1032
|
+
return super().isBufferOverflowed()
|
|
1033
|
+
|
|
1034
|
+
return self._seq_buffer.overflow_occurred
|
|
1035
|
+
|
|
1036
|
+
# ----------------------------------------------------------------- image info
|
|
1037
|
+
|
|
1038
|
+
def getImageBitDepth(self) -> int:
|
|
1039
|
+
if (cam := self._py_camera()) is None: # pragma: no cover
|
|
1040
|
+
return super().getImageBitDepth()
|
|
1041
|
+
dtype = np.dtype(cam.dtype())
|
|
1042
|
+
return dtype.itemsize * 8
|
|
1043
|
+
|
|
1044
|
+
def getBytesPerPixel(self) -> int:
|
|
1045
|
+
if (cam := self._py_camera()) is None: # pragma: no cover
|
|
1046
|
+
return super().getBytesPerPixel()
|
|
1047
|
+
dtype = np.dtype(cam.dtype())
|
|
1048
|
+
return dtype.itemsize
|
|
1049
|
+
|
|
1050
|
+
def getImageBufferSize(self) -> int:
|
|
1051
|
+
if (cam := self._py_camera()) is None: # pragma: no cover
|
|
1052
|
+
return super().getImageBufferSize()
|
|
1053
|
+
shape, dtype = cam.shape(), np.dtype(cam.dtype())
|
|
1054
|
+
return int(np.prod(shape) * dtype.itemsize)
|
|
1055
|
+
|
|
1056
|
+
def getImageHeight(self) -> int:
|
|
1057
|
+
if (cam := self._py_camera()) is None: # pragma: no cover
|
|
1058
|
+
return super().getImageHeight()
|
|
1059
|
+
return cam.shape()[0]
|
|
1060
|
+
|
|
1061
|
+
def getImageWidth(self) -> int:
|
|
1062
|
+
if (cam := self._py_camera()) is None: # pragma: no cover
|
|
1063
|
+
return super().getImageWidth()
|
|
1064
|
+
return cam.shape()[1]
|
|
1065
|
+
|
|
1066
|
+
def getNumberOfComponents(self) -> int:
|
|
1067
|
+
if (cam := self._py_camera()) is None: # pragma: no cover
|
|
1068
|
+
return super().getNumberOfComponents()
|
|
1069
|
+
shape = cam.shape()
|
|
1070
|
+
return 1 if len(shape) == 2 else shape[2]
|
|
1071
|
+
|
|
1072
|
+
def getNumberOfCameraChannels(self) -> int:
|
|
1073
|
+
if self._py_camera() is None: # pragma: no cover
|
|
1074
|
+
return super().getNumberOfCameraChannels()
|
|
1075
|
+
raise NotImplementedError(
|
|
1076
|
+
"getNumberOfCameraChannels is not implemented for Python cameras."
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
def getCameraChannelName(self, channelNr: int) -> str:
|
|
1080
|
+
"""Get the name of the camera channel."""
|
|
1081
|
+
if self._py_camera() is None: # pragma: no cover
|
|
1082
|
+
return super().getCameraChannelName(channelNr)
|
|
1083
|
+
raise NotImplementedError(
|
|
1084
|
+
"getCameraChannelName is not implemented for Python cameras."
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
@overload
|
|
1088
|
+
def getExposure(self) -> float: ...
|
|
1089
|
+
@overload
|
|
1090
|
+
def getExposure(self, cameraLabel: DeviceLabel | str, /) -> float: ...
|
|
1091
|
+
def getExposure(self, cameraLabel: DeviceLabel | str | None = None) -> float:
|
|
1092
|
+
"""Get the exposure time in milliseconds."""
|
|
1093
|
+
if (cam := self._py_camera(cameraLabel)) is None: # pragma: no cover
|
|
1094
|
+
if cameraLabel is None:
|
|
1095
|
+
return super().getExposure()
|
|
1096
|
+
return super().getExposure(cameraLabel)
|
|
1097
|
+
|
|
1098
|
+
with cam:
|
|
1099
|
+
return cam.get_exposure()
|
|
1100
|
+
|
|
1101
|
+
@overload
|
|
1102
|
+
def setExposure(self, exp: float, /) -> None: ...
|
|
1103
|
+
@overload
|
|
1104
|
+
def setExposure(self, cameraLabel: DeviceLabel | str, dExp: float, /) -> None: ...
|
|
1105
|
+
def setExposure(self, *args: Any) -> None:
|
|
1106
|
+
"""Set the exposure time in milliseconds."""
|
|
1107
|
+
label, args = _ensure_label(args, min_args=2, getter=self.getCameraDevice)
|
|
1108
|
+
if (cam := self._py_camera(label)) is None: # pragma: no cover
|
|
1109
|
+
return super().setExposure(label, *args)
|
|
1110
|
+
with cam:
|
|
1111
|
+
cam.set_exposure(*args)
|
|
1112
|
+
|
|
1113
|
+
def _do_set_roi(self, label: str, x: int, y: int, width: int, height: int) -> None:
|
|
1114
|
+
if self._py_camera(label) is not None:
|
|
1115
|
+
raise NotImplementedError(
|
|
1116
|
+
"setROI is not yet implemented for Python cameras."
|
|
1117
|
+
)
|
|
1118
|
+
return pymmcore.CMMCore.setROI(self, label, x, y, width, height)
|
|
1119
|
+
|
|
1120
|
+
@overload
|
|
1121
|
+
def getROI(self) -> list[int]: ...
|
|
1122
|
+
@overload
|
|
1123
|
+
def getROI(self, label: DeviceLabel | str) -> list[int]: ...
|
|
1124
|
+
def getROI(self, label: DeviceLabel | str = "") -> list[int]:
|
|
1125
|
+
"""Get the current region of interest (ROI) for the camera."""
|
|
1126
|
+
if self._py_camera(label) is None: # pragma: no cover
|
|
1127
|
+
raise NotImplementedError(
|
|
1128
|
+
"getROI is not yet implemented for Python cameras."
|
|
1129
|
+
)
|
|
1130
|
+
return super().getROI(label)
|
|
1131
|
+
|
|
1132
|
+
def clearROI(self) -> None:
|
|
1133
|
+
"""Clear the current region of interest (ROI) for the camera."""
|
|
1134
|
+
if self._py_camera() is not None: # pragma: no cover
|
|
1135
|
+
raise NotImplementedError(
|
|
1136
|
+
"clearROI is not yet implemented for Python cameras."
|
|
1137
|
+
)
|
|
1138
|
+
return super().clearROI()
|
|
1139
|
+
|
|
1140
|
+
def isExposureSequenceable(self, cameraLabel: DeviceLabel | str) -> bool:
|
|
1141
|
+
"""Check if the camera supports exposure sequences."""
|
|
1142
|
+
if (cam := self._py_camera(cameraLabel)) is None: # pragma: no cover
|
|
1143
|
+
return super().isExposureSequenceable(cameraLabel)
|
|
1144
|
+
with cam:
|
|
1145
|
+
return cam.is_property_sequenceable(KW.Exposure)
|
|
1146
|
+
|
|
1147
|
+
def loadExposureSequence(
|
|
1148
|
+
self, cameraLabel: DeviceLabel | str, exposureSequence_ms: Sequence[float]
|
|
1149
|
+
) -> None:
|
|
1150
|
+
"""Transfer a sequence of exposure times to the camera."""
|
|
1151
|
+
if (cam := self._py_camera(cameraLabel)) is None: # pragma: no cover
|
|
1152
|
+
return super().loadExposureSequence(cameraLabel, exposureSequence_ms)
|
|
1153
|
+
with cam:
|
|
1154
|
+
cam.load_property_sequence(KW.Exposure, exposureSequence_ms)
|
|
1155
|
+
|
|
1156
|
+
def getExposureSequenceMaxLength(self, cameraLabel: DeviceLabel | str) -> int:
|
|
1157
|
+
"""Get the maximum length of the exposure sequence."""
|
|
1158
|
+
if (cam := self._py_camera(cameraLabel)) is None: # pragma: no cover
|
|
1159
|
+
return super().getExposureSequenceMaxLength(cameraLabel)
|
|
1160
|
+
with cam:
|
|
1161
|
+
return cam.get_property_info(KW.Exposure).sequence_max_length
|
|
1162
|
+
|
|
1163
|
+
def startExposureSequence(self, cameraLabel: DeviceLabel | str) -> None:
|
|
1164
|
+
"""Start a sequence of exposures."""
|
|
1165
|
+
if (cam := self._py_camera(cameraLabel)) is None: # pragma: no cover
|
|
1166
|
+
return super().startExposureSequence(cameraLabel)
|
|
1167
|
+
with cam:
|
|
1168
|
+
cam.start_property_sequence(KW.Exposure)
|
|
1169
|
+
|
|
1170
|
+
def stopExposureSequence(self, cameraLabel: DeviceLabel | str) -> None:
|
|
1171
|
+
"""Stop a sequence of exposures."""
|
|
1172
|
+
if (cam := self._py_camera(cameraLabel)) is None: # pragma: no cover
|
|
1173
|
+
return super().stopExposureSequence(cameraLabel)
|
|
1174
|
+
with cam:
|
|
1175
|
+
cam.stop_property_sequence(KW.Exposure)
|
|
1176
|
+
|
|
1177
|
+
def prepareSequenceAcquisition(self, cameraLabel: DeviceLabel | str) -> None:
|
|
1178
|
+
"""Prepare the camera for sequence acquisition."""
|
|
1179
|
+
if self._py_camera(cameraLabel) is None: # pragma: no cover
|
|
1180
|
+
return super().prepareSequenceAcquisition(cameraLabel)
|
|
1181
|
+
# TODO: Implement prepareSequenceAcquisition for Python cameras?
|
|
1182
|
+
|
|
1183
|
+
@overload
|
|
1184
|
+
def getPixelSizeAffine(self) -> AffineTuple: ...
|
|
1185
|
+
@overload
|
|
1186
|
+
def getPixelSizeAffine(self, cached: bool, /) -> AffineTuple: ...
|
|
1187
|
+
def getPixelSizeAffine(self, cached: bool = False) -> AffineTuple:
|
|
1188
|
+
"""Get the pixel size affine transformation matrix."""
|
|
1189
|
+
if not (res_id := self.getCurrentPixelSizeConfig(cached)): # pragma: no cover
|
|
1190
|
+
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0) # null affine
|
|
1191
|
+
|
|
1192
|
+
cam = self._py_camera()
|
|
1193
|
+
if cam is not None:
|
|
1194
|
+
with cam:
|
|
1195
|
+
binning = float(cam.get_property_value(KW.Binning))
|
|
1196
|
+
else:
|
|
1197
|
+
binning = 1.0
|
|
1198
|
+
if cam is None or binning == 1:
|
|
1199
|
+
return tuple(super().getPixelSizeAffine(cached)) # type: ignore
|
|
1200
|
+
|
|
1201
|
+
# in CMMCore, they scale the pixel size affine by the binning factor and mag
|
|
1202
|
+
# but they won't pay attention to our camera so we have to reimplement it here
|
|
1203
|
+
af = self.getPixelSizeAffineByID(res_id)
|
|
1204
|
+
if (factor := binning / self.getMagnificationFactor()) != 1.0:
|
|
1205
|
+
af = cast("AffineTuple", tuple(v * factor for v in af))
|
|
1206
|
+
return af
|
|
1207
|
+
|
|
1208
|
+
@overload
|
|
1209
|
+
def getPixelSizeUm(self) -> float: ...
|
|
1210
|
+
@overload
|
|
1211
|
+
def getPixelSizeUm(self, cached: bool) -> float: ...
|
|
1212
|
+
def getPixelSizeUm(self, cached: bool = False) -> float:
|
|
1213
|
+
"""Get the pixel size in micrometers."""
|
|
1214
|
+
if not (res_id := self.getCurrentPixelSizeConfig(cached)): # pragma: no cover
|
|
1215
|
+
return 0.0
|
|
1216
|
+
|
|
1217
|
+
# in CMMCore, they scale the pixel size by the binning factor and mag
|
|
1218
|
+
# but they won't pay attention to our camera so we have to reimplement it here
|
|
1219
|
+
cam = self._py_camera()
|
|
1220
|
+
if cam is None or (binning := float(cam.get_property_value(KW.Binning))) == 1:
|
|
1221
|
+
return super().getPixelSizeUm(cached)
|
|
1222
|
+
|
|
1223
|
+
return self.getPixelSizeUmByID(res_id) * binning / self.getMagnificationFactor()
|
|
1224
|
+
|
|
1225
|
+
# ########################################################################
|
|
1226
|
+
# ------------------------- SLM Device Methods -------------------------
|
|
1227
|
+
# ########################################################################
|
|
1228
|
+
|
|
1229
|
+
# --------------------------------------------------------------------- utils
|
|
1230
|
+
|
|
1231
|
+
def _py_slm(self, slmLabel: str | None = None) -> SLMDevice | None:
|
|
1232
|
+
"""Return the *Python* SLM for ``label`` (or current), else ``None``."""
|
|
1233
|
+
label = slmLabel or self.getSLMDevice()
|
|
1234
|
+
if label in self._pydevices:
|
|
1235
|
+
return self._pydevices.get_device_of_type(label, SLMDevice)
|
|
1236
|
+
return None # pragma: no cover
|
|
1237
|
+
|
|
1238
|
+
def setSLMDevice(self, slmLabel: DeviceLabel | str) -> None:
|
|
1239
|
+
"""Set the SLM device."""
|
|
1240
|
+
label = self._set_current_if_pydevice(KW.CoreSLM, slmLabel)
|
|
1241
|
+
super().setSLMDevice(label)
|
|
1242
|
+
|
|
1243
|
+
def getSLMDevice(self) -> DeviceLabel | Literal[""]:
|
|
1244
|
+
"""Returns the label of the currently selected SLM device.
|
|
1245
|
+
|
|
1246
|
+
Returns empty string if no SLM device is selected.
|
|
1247
|
+
"""
|
|
1248
|
+
return self._pycore.current(KW.CoreSLM) or super().getSLMDevice()
|
|
1249
|
+
|
|
1250
|
+
# ------------------------------------------------------------------- set image
|
|
1251
|
+
|
|
1252
|
+
@overload
|
|
1253
|
+
def setSLMImage(self, pixels: np.ndarray, /) -> None: ...
|
|
1254
|
+
@overload
|
|
1255
|
+
def setSLMImage(
|
|
1256
|
+
self, slmLabel: DeviceLabel | str, pixels: np.ndarray, /
|
|
1257
|
+
) -> None: ...
|
|
1258
|
+
def setSLMImage(self, *args: Any) -> None:
|
|
1259
|
+
"""Load the image into the SLM device adapter."""
|
|
1260
|
+
label, args = _ensure_label(args, min_args=2, getter=self.getSLMDevice)
|
|
1261
|
+
if (slm := self._py_slm(label)) is None: # pragma: no cover
|
|
1262
|
+
return super().setSLMImage(label, *args)
|
|
1263
|
+
|
|
1264
|
+
with slm:
|
|
1265
|
+
shape, dtype = slm.shape(), np.dtype(slm.dtype())
|
|
1266
|
+
arr = np.asarray(args[0], dtype=dtype)
|
|
1267
|
+
if not arr.shape == shape: # pragma: no cover
|
|
1268
|
+
raise ValueError(
|
|
1269
|
+
f"Image shape {arr.shape} doesn't match SLM shape {shape}."
|
|
1270
|
+
)
|
|
1271
|
+
slm.set_image(arr)
|
|
1272
|
+
|
|
1273
|
+
def getSLMImage(self, slmLabel: DeviceLabel | str | None = None) -> np.ndarray:
|
|
1274
|
+
"""Get the current image from the SLM device."""
|
|
1275
|
+
if (slm := self._py_slm(slmLabel)) is None:
|
|
1276
|
+
raise NotImplementedError(
|
|
1277
|
+
"getSLMImage is not implemented for C++ SLM devices. "
|
|
1278
|
+
"(This method is unique to Python SLM devices.)"
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1281
|
+
with slm:
|
|
1282
|
+
return slm.get_image()
|
|
1283
|
+
|
|
1284
|
+
@overload
|
|
1285
|
+
def setSLMPixelsTo(self, intensity: int, /) -> None: ...
|
|
1286
|
+
@overload
|
|
1287
|
+
def setSLMPixelsTo(self, red: int, green: int, blue: int, /) -> None: ...
|
|
1288
|
+
@overload
|
|
1289
|
+
def setSLMPixelsTo(
|
|
1290
|
+
self, slmLabel: DeviceLabel | str, intensity: int, /
|
|
1291
|
+
) -> None: ...
|
|
1292
|
+
@overload
|
|
1293
|
+
def setSLMPixelsTo(
|
|
1294
|
+
self, slmLabel: DeviceLabel | str, red: int, green: int, blue: int, /
|
|
1295
|
+
) -> None: ...
|
|
1296
|
+
def setSLMPixelsTo(self, *args: Any) -> None:
|
|
1297
|
+
"""Set all pixels of the SLM to a uniform intensity or RGB values."""
|
|
1298
|
+
if len(args) < 1 or len(args) > 4: # pragma: no cover
|
|
1299
|
+
raise ValueError("setSLMPixelsTo requires 1 to 4 arguments.")
|
|
1300
|
+
|
|
1301
|
+
label = args[0] if len(args) in (2, 4) else self.getSLMDevice()
|
|
1302
|
+
if (slm := self._py_slm(label)) is None: # pragma: no cover
|
|
1303
|
+
return super().setSLMPixelsTo(*args)
|
|
1304
|
+
|
|
1305
|
+
with slm:
|
|
1306
|
+
shape = slm.shape()
|
|
1307
|
+
dtype = slm.dtype()
|
|
1308
|
+
|
|
1309
|
+
# Determine if we have RGB (3 or 4 args) or single intensity (1 or 2 args)
|
|
1310
|
+
if len(args) == 1: # setSLMPixelsTo(intensity)
|
|
1311
|
+
pixels = np.full(shape, args[0], dtype=dtype)
|
|
1312
|
+
elif len(args) == 2: # setSLMPixelsTo(slmLabel, intensity)
|
|
1313
|
+
pixels = np.full(shape, args[1], dtype=dtype)
|
|
1314
|
+
elif len(args) == 3: # setSLMPixelsTo(red, green, blue)
|
|
1315
|
+
rgb_values = args
|
|
1316
|
+
pixels = np.broadcast_to(rgb_values, (*shape[:2], 3))
|
|
1317
|
+
elif len(args) == 4: # setSLMPixelsTo(slmLabel, red, green, blue)
|
|
1318
|
+
rgb_values = args[1:4]
|
|
1319
|
+
pixels = np.broadcast_to(rgb_values, (*shape[:2], 3))
|
|
1320
|
+
if len(shape) == 2 and pixels.ndim == 3:
|
|
1321
|
+
# Grayscale SLM - convert RGB to grayscale (simple average)
|
|
1322
|
+
pixels = np.mean(pixels, axis=2, dtype=dtype).astype(dtype)
|
|
1323
|
+
|
|
1324
|
+
slm.set_image(pixels)
|
|
1325
|
+
|
|
1326
|
+
@overload
|
|
1327
|
+
def displaySLMImage(self) -> None: ...
|
|
1328
|
+
@overload
|
|
1329
|
+
def displaySLMImage(self, slmLabel: DeviceLabel | str, /) -> None: ...
|
|
1330
|
+
def displaySLMImage(self, slmLabel: DeviceLabel | str | None = None) -> None:
|
|
1331
|
+
"""Command the SLM to display the loaded image."""
|
|
1332
|
+
label = slmLabel or self.getSLMDevice()
|
|
1333
|
+
if (slm := self._py_slm(label)) is None: # pragma: no cover
|
|
1334
|
+
if slmLabel is None:
|
|
1335
|
+
return super().displaySLMImage(label)
|
|
1336
|
+
return super().displaySLMImage(slmLabel)
|
|
1337
|
+
|
|
1338
|
+
with slm:
|
|
1339
|
+
slm.display_image()
|
|
1340
|
+
|
|
1341
|
+
# ------------------------------------------------------------------ exposure
|
|
1342
|
+
|
|
1343
|
+
@overload
|
|
1344
|
+
def setSLMExposure(self, interval_ms: float, /) -> None: ...
|
|
1345
|
+
@overload
|
|
1346
|
+
def setSLMExposure(
|
|
1347
|
+
self, slmLabel: DeviceLabel | str, interval_ms: float, /
|
|
1348
|
+
) -> None: ...
|
|
1349
|
+
def setSLMExposure(self, *args: Any) -> None:
|
|
1350
|
+
"""Command the SLM to turn off after a specified interval."""
|
|
1351
|
+
label, args = _ensure_label(args, min_args=2, getter=self.getSLMDevice)
|
|
1352
|
+
if (slm := self._py_slm(label)) is None: # pragma: no cover
|
|
1353
|
+
return super().setSLMExposure(label, *args)
|
|
1354
|
+
|
|
1355
|
+
with slm:
|
|
1356
|
+
slm.set_exposure(args[0])
|
|
1357
|
+
|
|
1358
|
+
@overload
|
|
1359
|
+
def getSLMExposure(self) -> float: ...
|
|
1360
|
+
@overload
|
|
1361
|
+
def getSLMExposure(self, slmLabel: DeviceLabel | str, /) -> float: ...
|
|
1362
|
+
def getSLMExposure(self, slmLabel: DeviceLabel | str | None = None) -> float:
|
|
1363
|
+
"""Find out the exposure interval of an SLM."""
|
|
1364
|
+
if (slm := self._py_slm(slmLabel)) is None: # pragma: no cover
|
|
1365
|
+
label = slmLabel or self.getSLMDevice()
|
|
1366
|
+
return super().getSLMExposure(label)
|
|
1367
|
+
|
|
1368
|
+
with slm:
|
|
1369
|
+
return slm.get_exposure()
|
|
1370
|
+
|
|
1371
|
+
# ----------------------------------------------------------------- dimensions
|
|
1372
|
+
|
|
1373
|
+
@overload
|
|
1374
|
+
def getSLMWidth(self) -> int: ...
|
|
1375
|
+
@overload
|
|
1376
|
+
def getSLMWidth(self, slmLabel: DeviceLabel | str, /) -> int: ...
|
|
1377
|
+
def getSLMWidth(self, slmLabel: DeviceLabel | str | None = None) -> int:
|
|
1378
|
+
"""Returns the width of the SLM in pixels."""
|
|
1379
|
+
if (slm := self._py_slm(slmLabel)) is None: # pragma: no cover
|
|
1380
|
+
label = slmLabel or self.getSLMDevice()
|
|
1381
|
+
return super().getSLMWidth(label)
|
|
1382
|
+
|
|
1383
|
+
with slm:
|
|
1384
|
+
return slm.shape()[1] # width is second dimension
|
|
1385
|
+
|
|
1386
|
+
@overload
|
|
1387
|
+
def getSLMHeight(self) -> int: ...
|
|
1388
|
+
@overload
|
|
1389
|
+
def getSLMHeight(self, slmLabel: DeviceLabel | str, /) -> int: ...
|
|
1390
|
+
def getSLMHeight(self, slmLabel: DeviceLabel | str | None = None) -> int:
|
|
1391
|
+
"""Returns the height of the SLM in pixels."""
|
|
1392
|
+
if (slm := self._py_slm(slmLabel)) is None: # pragma: no cover
|
|
1393
|
+
label = slmLabel or self.getSLMDevice()
|
|
1394
|
+
return super().getSLMHeight(label)
|
|
1395
|
+
|
|
1396
|
+
with slm:
|
|
1397
|
+
return slm.shape()[0] # height is first dimension
|
|
1398
|
+
|
|
1399
|
+
@overload
|
|
1400
|
+
def getSLMNumberOfComponents(self) -> int: ...
|
|
1401
|
+
@overload
|
|
1402
|
+
def getSLMNumberOfComponents(self, slmLabel: DeviceLabel | str, /) -> int: ...
|
|
1403
|
+
def getSLMNumberOfComponents(
|
|
1404
|
+
self, slmLabel: DeviceLabel | str | None = None
|
|
1405
|
+
) -> int:
|
|
1406
|
+
"""Returns the number of color components (channels) in the SLM."""
|
|
1407
|
+
if (slm := self._py_slm(slmLabel)) is None: # pragma: no cover
|
|
1408
|
+
label = slmLabel or self.getSLMDevice()
|
|
1409
|
+
return super().getSLMNumberOfComponents(label)
|
|
1410
|
+
|
|
1411
|
+
with slm:
|
|
1412
|
+
shape = slm.shape()
|
|
1413
|
+
return 1 if len(shape) == 2 else shape[2]
|
|
1414
|
+
|
|
1415
|
+
@overload
|
|
1416
|
+
def getSLMBytesPerPixel(self) -> int: ...
|
|
1417
|
+
@overload
|
|
1418
|
+
def getSLMBytesPerPixel(self, slmLabel: DeviceLabel | str, /) -> int: ...
|
|
1419
|
+
def getSLMBytesPerPixel(self, slmLabel: DeviceLabel | str | None = None) -> int:
|
|
1420
|
+
"""Returns the number of bytes per pixel for the SLM."""
|
|
1421
|
+
if (slm := self._py_slm(slmLabel)) is None: # pragma: no cover
|
|
1422
|
+
label = slmLabel or self.getSLMDevice()
|
|
1423
|
+
return super().getSLMBytesPerPixel(label)
|
|
1424
|
+
|
|
1425
|
+
with slm:
|
|
1426
|
+
dtype = np.dtype(slm.dtype())
|
|
1427
|
+
return dtype.itemsize
|
|
1428
|
+
|
|
1429
|
+
# ------------------------------------------------------------------ sequences
|
|
1430
|
+
|
|
1431
|
+
def getSLMSequenceMaxLength(self, slmLabel: DeviceLabel | str) -> int:
|
|
1432
|
+
"""Get the maximum length of an image sequence that can be uploaded."""
|
|
1433
|
+
if (slm := self._py_slm(slmLabel)) is None: # pragma: no cover
|
|
1434
|
+
return super().getSLMSequenceMaxLength(slmLabel)
|
|
1435
|
+
|
|
1436
|
+
with slm:
|
|
1437
|
+
return slm.get_sequence_max_length()
|
|
1438
|
+
|
|
1439
|
+
def loadSLMSequence(
|
|
1440
|
+
self,
|
|
1441
|
+
slmLabel: DeviceLabel | str,
|
|
1442
|
+
imageSequence: Sequence[bytes | np.ndarray],
|
|
1443
|
+
) -> None:
|
|
1444
|
+
"""Load a sequence of images to the SLM."""
|
|
1445
|
+
if (slm := self._py_slm(slmLabel)) is None: # pragma: no cover
|
|
1446
|
+
return super().loadSLMSequence(slmLabel, imageSequence) # type: ignore[arg-type]
|
|
1447
|
+
|
|
1448
|
+
with slm:
|
|
1449
|
+
if (m := slm.get_sequence_max_length()) == 0:
|
|
1450
|
+
raise RuntimeError(f"SLM {slmLabel!r} does not support sequences.")
|
|
1451
|
+
|
|
1452
|
+
shape = slm.shape()
|
|
1453
|
+
dtype = np.dtype(slm.dtype())
|
|
1454
|
+
|
|
1455
|
+
np_arrays: list[np.ndarray] = []
|
|
1456
|
+
for i, img_bytes in enumerate(imageSequence):
|
|
1457
|
+
if isinstance(img_bytes, bytes):
|
|
1458
|
+
arr = np.frombuffer(img_bytes, dtype=dtype).reshape(shape)
|
|
1459
|
+
else:
|
|
1460
|
+
arr = np.asarray(img_bytes, dtype=dtype)
|
|
1461
|
+
if arr.shape != shape:
|
|
1462
|
+
raise ValueError(
|
|
1463
|
+
f"Image {i} shape {arr.shape} does not "
|
|
1464
|
+
f"match SLM shape {shape}"
|
|
1465
|
+
)
|
|
1466
|
+
np_arrays.append(arr)
|
|
1467
|
+
if len(np_arrays) > (m := slm.get_sequence_max_length()):
|
|
1468
|
+
raise ValueError(
|
|
1469
|
+
f"Sequence length {len(np_arrays)} exceeds maximum {m}."
|
|
1470
|
+
)
|
|
1471
|
+
slm.send_sequence(np_arrays)
|
|
1472
|
+
|
|
1473
|
+
def startSLMSequence(self, slmLabel: DeviceLabel | str) -> None:
|
|
1474
|
+
"""Start a sequence of images on the SLM."""
|
|
1475
|
+
if (slm := self._py_slm(slmLabel)) is None: # pragma: no cover
|
|
1476
|
+
return super().startSLMSequence(slmLabel)
|
|
1477
|
+
|
|
1478
|
+
with slm:
|
|
1479
|
+
slm.start_sequence()
|
|
1480
|
+
|
|
1481
|
+
def stopSLMSequence(self, slmLabel: DeviceLabel | str) -> None:
|
|
1482
|
+
"""Stop a sequence of images on the SLM."""
|
|
1483
|
+
if (slm := self._py_slm(slmLabel)) is None: # pragma: no cover
|
|
1484
|
+
return super().stopSLMSequence(slmLabel)
|
|
1485
|
+
|
|
1486
|
+
with slm:
|
|
1487
|
+
slm.stop_sequence()
|
|
1488
|
+
|
|
1489
|
+
# ########################################################################
|
|
1490
|
+
# ------------------------ State Device Methods -------------------------
|
|
1491
|
+
# ########################################################################
|
|
1492
|
+
|
|
1493
|
+
# --------------------------------------------------------------------- utils
|
|
1494
|
+
|
|
1495
|
+
def _py_state(self, stateLabel: str | None = None) -> StateDevice | None:
|
|
1496
|
+
"""Return the *Python* State device for ``label``, else ``None``."""
|
|
1497
|
+
label = stateLabel or ""
|
|
1498
|
+
if label in self._pydevices:
|
|
1499
|
+
return self._pydevices.get_device_of_type(label, StateDevice)
|
|
1500
|
+
return None # pragma: no cover
|
|
1501
|
+
|
|
1502
|
+
# ------------------------------------------------------------------- setState
|
|
1503
|
+
|
|
1504
|
+
def setState(self, stateDeviceLabel: DeviceLabel | str, state: int) -> None:
|
|
1505
|
+
"""Set state (position) on the specific device."""
|
|
1506
|
+
if (state_dev := self._py_state(stateDeviceLabel)) is None: # pragma: no cover
|
|
1507
|
+
return super().setState(stateDeviceLabel, state)
|
|
1508
|
+
|
|
1509
|
+
with state_dev:
|
|
1510
|
+
state_dev.set_position_or_label(state)
|
|
1511
|
+
|
|
1512
|
+
# ------------------------------------------------------------------- getState
|
|
1513
|
+
|
|
1514
|
+
def getState(self, stateDeviceLabel: DeviceLabel | str) -> int:
|
|
1515
|
+
"""Return the current state (position) on the specific device."""
|
|
1516
|
+
if (state_dev := self._py_state(stateDeviceLabel)) is None: # pragma: no cover
|
|
1517
|
+
return super().getState(stateDeviceLabel)
|
|
1518
|
+
|
|
1519
|
+
with state_dev:
|
|
1520
|
+
return int(state_dev.get_property_value(KW.State))
|
|
1521
|
+
|
|
1522
|
+
# ---------------------------------------------------------------- getNumberOfStates
|
|
1523
|
+
|
|
1524
|
+
def getNumberOfStates(self, stateDeviceLabel: DeviceLabel | str) -> int:
|
|
1525
|
+
"""Return the total number of available positions (states)."""
|
|
1526
|
+
if (state_dev := self._py_state(stateDeviceLabel)) is None: # pragma: no cover
|
|
1527
|
+
return super().getNumberOfStates(stateDeviceLabel)
|
|
1528
|
+
|
|
1529
|
+
with state_dev:
|
|
1530
|
+
return state_dev.get_property_info(KW.State).number_of_allowed_values
|
|
1531
|
+
|
|
1532
|
+
# ----------------------------------------------------------------- setStateLabel
|
|
1533
|
+
|
|
1534
|
+
def setStateLabel(
|
|
1535
|
+
self, stateDeviceLabel: DeviceLabel | str, stateLabel: str
|
|
1536
|
+
) -> None:
|
|
1537
|
+
"""Set device state using the previously assigned label (string)."""
|
|
1538
|
+
if (state_dev := self._py_state(stateDeviceLabel)) is None: # pragma: no cover
|
|
1539
|
+
return super().setStateLabel(stateDeviceLabel, stateLabel)
|
|
1540
|
+
|
|
1541
|
+
with state_dev:
|
|
1542
|
+
try:
|
|
1543
|
+
state_dev.set_position_or_label(stateLabel)
|
|
1544
|
+
except KeyError as e:
|
|
1545
|
+
raise RuntimeError(str(e)) from e # convert to RuntimeError
|
|
1546
|
+
|
|
1547
|
+
# ----------------------------------------------------------------- getStateLabel
|
|
1548
|
+
|
|
1549
|
+
def getStateLabel(self, stateDeviceLabel: DeviceLabel | str) -> StateLabel:
|
|
1550
|
+
"""Return the current state as the label (string)."""
|
|
1551
|
+
if (state_dev := self._py_state(stateDeviceLabel)) is None: # pragma: no cover
|
|
1552
|
+
return super().getStateLabel(stateDeviceLabel)
|
|
1553
|
+
|
|
1554
|
+
with state_dev:
|
|
1555
|
+
return cast("StateLabel", state_dev.get_property_value(KW.Label))
|
|
1556
|
+
|
|
1557
|
+
# --------------------------------------------------------------- defineStateLabel
|
|
1558
|
+
|
|
1559
|
+
def defineStateLabel(
|
|
1560
|
+
self, stateDeviceLabel: DeviceLabel | str, state: int, label: str
|
|
1561
|
+
) -> None:
|
|
1562
|
+
"""Define a label for the specific state."""
|
|
1563
|
+
if (state_dev := self._py_state(stateDeviceLabel)) is None: # pragma: no cover
|
|
1564
|
+
return super().defineStateLabel(stateDeviceLabel, state, label)
|
|
1565
|
+
|
|
1566
|
+
with state_dev:
|
|
1567
|
+
state_dev.assign_label_to_position(state, label)
|
|
1568
|
+
|
|
1569
|
+
# ----------------------------------------------------------------- getStateLabels
|
|
1570
|
+
|
|
1571
|
+
def getStateLabels(
|
|
1572
|
+
self, stateDeviceLabel: DeviceLabel | str
|
|
1573
|
+
) -> tuple[StateLabel, ...]:
|
|
1574
|
+
"""Return labels for all states."""
|
|
1575
|
+
if (state_dev := self._py_state(stateDeviceLabel)) is None: # pragma: no cover
|
|
1576
|
+
return super().getStateLabels(stateDeviceLabel)
|
|
1577
|
+
|
|
1578
|
+
with state_dev:
|
|
1579
|
+
return tuple(state_dev.get_property_info(KW.Label).allowed_values or [])
|
|
1580
|
+
|
|
1581
|
+
# ------------------------------------------------------------- getStateFromLabel
|
|
1582
|
+
|
|
1583
|
+
def getStateFromLabel(
|
|
1584
|
+
self, stateDeviceLabel: DeviceLabel | str, stateLabel: str
|
|
1585
|
+
) -> int:
|
|
1586
|
+
"""Obtain the state for a given label."""
|
|
1587
|
+
if (state_dev := self._py_state(stateDeviceLabel)) is None: # pragma: no cover
|
|
1588
|
+
return super().getStateFromLabel(stateDeviceLabel, stateLabel)
|
|
1589
|
+
|
|
1590
|
+
with state_dev:
|
|
1591
|
+
try:
|
|
1592
|
+
return state_dev.get_position_for_label(stateLabel)
|
|
1593
|
+
except KeyError as e:
|
|
1594
|
+
raise RuntimeError(str(e)) from e # convert to RuntimeError
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
# -------------------------------------------------------------------------------
|
|
1598
|
+
|
|
1599
|
+
|
|
1600
|
+
def _ensure_label(
|
|
1601
|
+
args: tuple[_T, ...], min_args: int, getter: Callable[[], str]
|
|
1602
|
+
) -> tuple[str, tuple[_T, ...]]:
|
|
1603
|
+
"""Ensure we have a device label.
|
|
1604
|
+
|
|
1605
|
+
Designed to be used with overloaded methods that MAY take a device label as the
|
|
1606
|
+
first argument.
|
|
1607
|
+
|
|
1608
|
+
If the number of arguments is less than `min_args`, the label is obtained from the
|
|
1609
|
+
getter function. If the number of arguments is greater than or equal to `min_args`,
|
|
1610
|
+
the label is the first argument and the remaining arguments are returned as a tuple
|
|
1611
|
+
"""
|
|
1612
|
+
if len(args) < min_args:
|
|
1613
|
+
# we didn't get the label
|
|
1614
|
+
return getter(), args
|
|
1615
|
+
return cast("str", args[0]), args[1:]
|
|
1616
|
+
|
|
1617
|
+
|
|
1618
|
+
class PropertyStateCache(MutableMapping[tuple[str, str], Any]):
|
|
1619
|
+
"""A thread-safe cache for property states.
|
|
1620
|
+
|
|
1621
|
+
Keys are tuples of (device_label, property_name), and values are the last known
|
|
1622
|
+
value of that property.
|
|
1623
|
+
"""
|
|
1624
|
+
|
|
1625
|
+
def __init__(self) -> None:
|
|
1626
|
+
self._store: dict[tuple[str, str], Any] = {}
|
|
1627
|
+
self._lock = threading.Lock()
|
|
1628
|
+
|
|
1629
|
+
def __getitem__(self, key: tuple[str, str]) -> Any:
|
|
1630
|
+
with self._lock:
|
|
1631
|
+
try:
|
|
1632
|
+
return self._store[key]
|
|
1633
|
+
except KeyError: # pragma: no cover
|
|
1634
|
+
prop, dev = key
|
|
1635
|
+
raise KeyError(
|
|
1636
|
+
f"Property {prop!r} of device {dev!r} not found in cache"
|
|
1637
|
+
) from None
|
|
1638
|
+
|
|
1639
|
+
def __setitem__(self, key: tuple[str, str], value: Any) -> None:
|
|
1640
|
+
with self._lock:
|
|
1641
|
+
self._store[key] = value
|
|
1642
|
+
|
|
1643
|
+
def __delitem__(self, key: tuple[str, str]) -> None:
|
|
1644
|
+
with self._lock:
|
|
1645
|
+
del self._store[key]
|
|
1646
|
+
|
|
1647
|
+
def __contains__(self, key: object) -> bool:
|
|
1648
|
+
with self._lock:
|
|
1649
|
+
return key in self._store
|
|
1650
|
+
|
|
1651
|
+
def __iter__(self) -> Iterator[tuple[str, str]]:
|
|
1652
|
+
with self._lock:
|
|
1653
|
+
return iter(self._store.copy()) # Prevent modifications during iteration
|
|
1654
|
+
|
|
1655
|
+
def __len__(self) -> int:
|
|
1656
|
+
with self._lock:
|
|
1657
|
+
return len(self._store)
|
|
1658
|
+
|
|
1659
|
+
def __repr__(self) -> str:
|
|
1660
|
+
with self._lock:
|
|
1661
|
+
return f"{self.__class__.__name__}({self._store!r})"
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
# Threading ------------------------------------------------------
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
class AcquisitionThread(threading.Thread):
|
|
1668
|
+
"""A thread for running sequence acquisition in the background."""
|
|
1669
|
+
|
|
1670
|
+
def __init__(
|
|
1671
|
+
self,
|
|
1672
|
+
image_generator: Iterator[Mapping],
|
|
1673
|
+
finalize: Callable[[Mapping], None],
|
|
1674
|
+
label: str,
|
|
1675
|
+
stop_event: threading.Event,
|
|
1676
|
+
) -> None:
|
|
1677
|
+
super().__init__(daemon=True)
|
|
1678
|
+
self.image_iterator = image_generator
|
|
1679
|
+
self.finalize = finalize
|
|
1680
|
+
self.label = label
|
|
1681
|
+
self.stop_event = stop_event
|
|
1682
|
+
|
|
1683
|
+
def run(self) -> None:
|
|
1684
|
+
"""Run the sequence and handle the generator pattern."""
|
|
1685
|
+
try:
|
|
1686
|
+
for metadata in self.image_iterator:
|
|
1687
|
+
self.finalize(metadata)
|
|
1688
|
+
if self.stop_event.is_set():
|
|
1689
|
+
break
|
|
1690
|
+
except BufferOverflowStop:
|
|
1691
|
+
# Buffer overflow is a graceful stop condition, not an error
|
|
1692
|
+
# this was likely raised by the Unicore above in _start_sequence
|
|
1693
|
+
pass
|
|
1694
|
+
except BufferError:
|
|
1695
|
+
raise # pragma: no cover
|
|
1696
|
+
except Exception as e: # pragma: no cover
|
|
1697
|
+
raise RuntimeError(
|
|
1698
|
+
f"Error in device {self.label!r} during sequence acquisition: {e}"
|
|
1699
|
+
) from e
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
# -------------------------------------------------------------------------------
|