pymmcore-plus 0.12.0__py3-none-any.whl → 0.13.1__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.
Files changed (48) hide show
  1. pymmcore_plus/__init__.py +3 -3
  2. pymmcore_plus/_benchmark.py +203 -0
  3. pymmcore_plus/_cli.py +78 -13
  4. pymmcore_plus/_logger.py +10 -2
  5. pymmcore_plus/_pymmcore.py +12 -0
  6. pymmcore_plus/_util.py +16 -10
  7. pymmcore_plus/core/__init__.py +3 -0
  8. pymmcore_plus/core/_config.py +1 -1
  9. pymmcore_plus/core/_config_group.py +2 -2
  10. pymmcore_plus/core/_constants.py +27 -3
  11. pymmcore_plus/core/_device.py +4 -4
  12. pymmcore_plus/core/_metadata.py +1 -1
  13. pymmcore_plus/core/_mmcore_plus.py +184 -118
  14. pymmcore_plus/core/_property.py +3 -5
  15. pymmcore_plus/core/_sequencing.py +369 -234
  16. pymmcore_plus/core/events/__init__.py +3 -3
  17. pymmcore_plus/experimental/__init__.py +0 -0
  18. pymmcore_plus/experimental/unicore/__init__.py +14 -0
  19. pymmcore_plus/experimental/unicore/_device_manager.py +173 -0
  20. pymmcore_plus/experimental/unicore/_proxy.py +127 -0
  21. pymmcore_plus/experimental/unicore/_unicore.py +703 -0
  22. pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
  23. pymmcore_plus/experimental/unicore/devices/_device.py +269 -0
  24. pymmcore_plus/experimental/unicore/devices/_properties.py +400 -0
  25. pymmcore_plus/experimental/unicore/devices/_stage.py +221 -0
  26. pymmcore_plus/install.py +10 -7
  27. pymmcore_plus/mda/__init__.py +1 -1
  28. pymmcore_plus/mda/_engine.py +152 -43
  29. pymmcore_plus/mda/_runner.py +8 -1
  30. pymmcore_plus/mda/events/__init__.py +2 -2
  31. pymmcore_plus/mda/handlers/__init__.py +1 -1
  32. pymmcore_plus/mda/handlers/_ome_zarr_writer.py +2 -2
  33. pymmcore_plus/mda/handlers/_tensorstore_handler.py +6 -2
  34. pymmcore_plus/metadata/__init__.py +3 -3
  35. pymmcore_plus/metadata/functions.py +18 -8
  36. pymmcore_plus/metadata/schema.py +6 -5
  37. pymmcore_plus/mocks.py +49 -0
  38. pymmcore_plus/model/_config_file.py +1 -1
  39. pymmcore_plus/model/_core_device.py +10 -1
  40. pymmcore_plus/model/_device.py +17 -6
  41. pymmcore_plus/model/_property.py +11 -2
  42. pymmcore_plus/seq_tester.py +1 -1
  43. {pymmcore_plus-0.12.0.dist-info → pymmcore_plus-0.13.1.dist-info}/METADATA +14 -6
  44. pymmcore_plus-0.13.1.dist-info/RECORD +71 -0
  45. {pymmcore_plus-0.12.0.dist-info → pymmcore_plus-0.13.1.dist-info}/WHEEL +1 -1
  46. pymmcore_plus-0.12.0.dist-info/RECORD +0 -59
  47. {pymmcore_plus-0.12.0.dist-info → pymmcore_plus-0.13.1.dist-info}/entry_points.txt +0 -0
  48. {pymmcore_plus-0.12.0.dist-info → pymmcore_plus-0.13.1.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})"