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.
- pymmcore_plus/__init__.py +3 -3
- pymmcore_plus/_benchmark.py +203 -0
- pymmcore_plus/_cli.py +78 -13
- pymmcore_plus/_logger.py +10 -2
- pymmcore_plus/_pymmcore.py +12 -0
- pymmcore_plus/_util.py +16 -10
- pymmcore_plus/core/__init__.py +3 -0
- pymmcore_plus/core/_config.py +1 -1
- pymmcore_plus/core/_config_group.py +2 -2
- pymmcore_plus/core/_constants.py +27 -3
- pymmcore_plus/core/_device.py +4 -4
- pymmcore_plus/core/_metadata.py +1 -1
- pymmcore_plus/core/_mmcore_plus.py +184 -118
- pymmcore_plus/core/_property.py +3 -5
- pymmcore_plus/core/_sequencing.py +369 -234
- pymmcore_plus/core/events/__init__.py +3 -3
- pymmcore_plus/experimental/__init__.py +0 -0
- pymmcore_plus/experimental/unicore/__init__.py +14 -0
- pymmcore_plus/experimental/unicore/_device_manager.py +173 -0
- pymmcore_plus/experimental/unicore/_proxy.py +127 -0
- pymmcore_plus/experimental/unicore/_unicore.py +703 -0
- pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
- pymmcore_plus/experimental/unicore/devices/_device.py +269 -0
- pymmcore_plus/experimental/unicore/devices/_properties.py +400 -0
- pymmcore_plus/experimental/unicore/devices/_stage.py +221 -0
- pymmcore_plus/install.py +10 -7
- pymmcore_plus/mda/__init__.py +1 -1
- pymmcore_plus/mda/_engine.py +152 -43
- pymmcore_plus/mda/_runner.py +8 -1
- pymmcore_plus/mda/events/__init__.py +2 -2
- pymmcore_plus/mda/handlers/__init__.py +1 -1
- pymmcore_plus/mda/handlers/_ome_zarr_writer.py +2 -2
- pymmcore_plus/mda/handlers/_tensorstore_handler.py +6 -2
- pymmcore_plus/metadata/__init__.py +3 -3
- pymmcore_plus/metadata/functions.py +18 -8
- pymmcore_plus/metadata/schema.py +6 -5
- pymmcore_plus/mocks.py +49 -0
- pymmcore_plus/model/_config_file.py +1 -1
- pymmcore_plus/model/_core_device.py +10 -1
- pymmcore_plus/model/_device.py +17 -6
- pymmcore_plus/model/_property.py +11 -2
- pymmcore_plus/seq_tester.py +1 -1
- {pymmcore_plus-0.12.0.dist-info → pymmcore_plus-0.13.1.dist-info}/METADATA +14 -6
- pymmcore_plus-0.13.1.dist-info/RECORD +71 -0
- {pymmcore_plus-0.12.0.dist-info → pymmcore_plus-0.13.1.dist-info}/WHEEL +1 -1
- pymmcore_plus-0.12.0.dist-info/RECORD +0 -59
- {pymmcore_plus-0.12.0.dist-info → pymmcore_plus-0.13.1.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.12.0.dist-info → pymmcore_plus-0.13.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from ._unicore import UniMMCore
|
|
2
|
+
from .devices._device import Device
|
|
3
|
+
from .devices._properties import PropertyInfo, pymm_property
|
|
4
|
+
from .devices._stage import StageDevice, XYStageDevice, XYStepperStageDevice
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Device",
|
|
8
|
+
"PropertyInfo",
|
|
9
|
+
"StageDevice",
|
|
10
|
+
"UniMMCore",
|
|
11
|
+
"XYStageDevice",
|
|
12
|
+
"XYStepperStageDevice",
|
|
13
|
+
"pymm_property",
|
|
14
|
+
]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
6
|
+
from typing import TYPE_CHECKING, TypeVar, cast
|
|
7
|
+
|
|
8
|
+
from pymmcore_plus.core._constants import DeviceInitializationState, DeviceType
|
|
9
|
+
|
|
10
|
+
from .devices._device import Device
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
|
|
15
|
+
from pymmcore import DeviceLabel
|
|
16
|
+
|
|
17
|
+
from ._proxy import CMMCoreProxy
|
|
18
|
+
|
|
19
|
+
DevT = TypeVar("DevT", bound=Device)
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PyDeviceManager:
|
|
24
|
+
"""Manages loaded Python devices."""
|
|
25
|
+
|
|
26
|
+
__slots__ = ("_devices",)
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._devices: dict[str, Device] = {}
|
|
30
|
+
|
|
31
|
+
def load(self, label: str, device: Device, proxy: CMMCoreProxy) -> None:
|
|
32
|
+
"""Load a device and assign it a label."""
|
|
33
|
+
if label in self._devices: # pragma: no cover
|
|
34
|
+
# we probably won't ever get here because the core checks this too
|
|
35
|
+
raise ValueError(f"The specified device label {label!r} is already in use")
|
|
36
|
+
self._devices[label] = device
|
|
37
|
+
device._label_ = label
|
|
38
|
+
device._core_proxy_ = proxy
|
|
39
|
+
|
|
40
|
+
def initialize(self, label: str) -> None:
|
|
41
|
+
"""Initialize the device with the given label."""
|
|
42
|
+
device = self[label]
|
|
43
|
+
try:
|
|
44
|
+
device.initialize()
|
|
45
|
+
device._initialized_ = True
|
|
46
|
+
except Exception as e:
|
|
47
|
+
device._initialized_ = e
|
|
48
|
+
logger.exception(f"Failed to initialize device {label!r}")
|
|
49
|
+
|
|
50
|
+
def initialize_all(self) -> None:
|
|
51
|
+
if not (labels := self.get_labels_of_type(DeviceType.Any)):
|
|
52
|
+
return # pragma: no cover
|
|
53
|
+
|
|
54
|
+
# Initialize all devices in parallel
|
|
55
|
+
with ThreadPoolExecutor() as executor:
|
|
56
|
+
for future in as_completed(
|
|
57
|
+
executor.submit(self.initialize, label) for label in labels
|
|
58
|
+
):
|
|
59
|
+
future.result()
|
|
60
|
+
|
|
61
|
+
def wait_for(
|
|
62
|
+
self, label: str, timeout_ms: float = 5000, polling_interval: float = 0.01
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Wait for the device to not be busy."""
|
|
65
|
+
device = self[label]
|
|
66
|
+
deadline = time.perf_counter() + timeout_ms / 1000
|
|
67
|
+
|
|
68
|
+
while True:
|
|
69
|
+
with device:
|
|
70
|
+
if not device.busy():
|
|
71
|
+
return
|
|
72
|
+
if time.perf_counter() > deadline:
|
|
73
|
+
raise TimeoutError(
|
|
74
|
+
f"Wait for device {label!r} timed out after {timeout_ms} ms"
|
|
75
|
+
)
|
|
76
|
+
time.sleep(polling_interval)
|
|
77
|
+
|
|
78
|
+
def wait_for_device_type(self, dev_type: int, timeout_ms: float = 5000) -> None:
|
|
79
|
+
if not (labels := self.get_labels_of_type(dev_type)):
|
|
80
|
+
return # pragma: no cover
|
|
81
|
+
|
|
82
|
+
# Wait for all python devices of the given type in parallel
|
|
83
|
+
with ThreadPoolExecutor() as executor:
|
|
84
|
+
futures = (
|
|
85
|
+
executor.submit(self.wait_for, lbl, timeout_ms) for lbl in labels
|
|
86
|
+
)
|
|
87
|
+
for future in as_completed(futures):
|
|
88
|
+
future.result() # Raises any exceptions from wait_for_device
|
|
89
|
+
|
|
90
|
+
def get_initialization_state(self, label: str) -> DeviceInitializationState:
|
|
91
|
+
"""Return the initialization state of the device with the given label."""
|
|
92
|
+
state = self[label]._initialized_
|
|
93
|
+
if state is True:
|
|
94
|
+
return DeviceInitializationState.InitializedSuccessfully
|
|
95
|
+
if state is False:
|
|
96
|
+
return DeviceInitializationState.Uninitialized
|
|
97
|
+
return DeviceInitializationState.InitializationFailed
|
|
98
|
+
|
|
99
|
+
def unload(self, label_or_device: str | Device) -> None:
|
|
100
|
+
"""Unload a loaded device by label or instance."""
|
|
101
|
+
if isinstance(label_or_device, Device):
|
|
102
|
+
if label_or_device not in self._devices.values():
|
|
103
|
+
raise ValueError("Device instance is not loaded") # pragma: no cover
|
|
104
|
+
_device, label = label_or_device, label_or_device._label_
|
|
105
|
+
else:
|
|
106
|
+
_device, label = self[label_or_device], label_or_device
|
|
107
|
+
|
|
108
|
+
with _device as dev:
|
|
109
|
+
dev.shutdown()
|
|
110
|
+
dev._initialized_ = False
|
|
111
|
+
dev._label_ = ""
|
|
112
|
+
dev._core_proxy_ = None
|
|
113
|
+
self._devices.pop(label)
|
|
114
|
+
|
|
115
|
+
def unload_all(self) -> None:
|
|
116
|
+
"""Unload all loaded devices."""
|
|
117
|
+
for label in list(self._devices):
|
|
118
|
+
self.unload(label)
|
|
119
|
+
|
|
120
|
+
def __len__(self) -> int:
|
|
121
|
+
"""Return the number of loaded device labels."""
|
|
122
|
+
return len(self._devices)
|
|
123
|
+
|
|
124
|
+
def __iter__(self) -> Iterator[DeviceLabel]:
|
|
125
|
+
"""Return an iterator over the loaded device labels."""
|
|
126
|
+
return iter(self._devices) # type: ignore
|
|
127
|
+
|
|
128
|
+
def __contains__(self, label: str) -> bool:
|
|
129
|
+
"""Return True if the device with the given label is loaded."""
|
|
130
|
+
return label in self._devices
|
|
131
|
+
|
|
132
|
+
def __getitem__(self, label: str) -> Device:
|
|
133
|
+
"""Get device by label, raising KeyError if it does not exist."""
|
|
134
|
+
if label not in self._devices:
|
|
135
|
+
raise KeyError(f"No device with label '{label!r}'")
|
|
136
|
+
return self._devices[label]
|
|
137
|
+
|
|
138
|
+
def get_initialized(
|
|
139
|
+
self, label: str, *, require_initialized: bool = True
|
|
140
|
+
) -> Device:
|
|
141
|
+
"""Get device by label, returning None if it does not exist.
|
|
142
|
+
|
|
143
|
+
This method is a convenience wrapper around __getitem__ that ensures the device
|
|
144
|
+
is both loaded and initialized.
|
|
145
|
+
"""
|
|
146
|
+
if (device := self[label])._initialized_ is not True and require_initialized:
|
|
147
|
+
raise ValueError(f"Device {label!r} is not initialized")
|
|
148
|
+
return device
|
|
149
|
+
|
|
150
|
+
def get_device_of_type(self, label: str, *types: type[DevT]) -> DevT:
|
|
151
|
+
"""Get device by label, ensuring it is of the correct type.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
label : str
|
|
156
|
+
The label of the device to retrieve.
|
|
157
|
+
types : type[DevT]
|
|
158
|
+
The type(s) the device must be an instance of.
|
|
159
|
+
"""
|
|
160
|
+
device = self[label]
|
|
161
|
+
if isinstance(device, types):
|
|
162
|
+
return device
|
|
163
|
+
raise ValueError(
|
|
164
|
+
f"Device {label!r} is the wrong device type for the requested operation"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def get_labels_of_type(self, dev_type: int) -> tuple[DeviceLabel, ...]:
|
|
168
|
+
"""Get the labels of all devices that are instances of the given type(s)."""
|
|
169
|
+
return tuple(
|
|
170
|
+
cast("DeviceLabel", label)
|
|
171
|
+
for label, device in self._devices.items()
|
|
172
|
+
if dev_type == DeviceType.Any or device.type() == dev_type
|
|
173
|
+
)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Proxy objects expose a subset of an object's API.
|
|
2
|
+
|
|
3
|
+
Useful, e.g., for passing a core-like object to python-side device adapters without
|
|
4
|
+
exposing the entirety of the core.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import types
|
|
10
|
+
from itertools import chain
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Protocol, TypeVar, cast, get_type_hints
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
|
|
16
|
+
from pymmcore_plus.core._mmcore_plus import CMMCorePlus
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PSignalInstance(Protocol):
|
|
22
|
+
"""A signal instance that can only emit."""
|
|
23
|
+
|
|
24
|
+
def emit(self, *args: Any) -> Any:
|
|
25
|
+
"""Emits the signal with the given arguments."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CoreEventsProxy(Protocol):
|
|
29
|
+
"""Signals that Device Adapters can emit directly."""
|
|
30
|
+
|
|
31
|
+
propertyChanged: PSignalInstance # (str, str, str)
|
|
32
|
+
stagePositionChanged: PSignalInstance # (str, float)
|
|
33
|
+
XYStagePositionChanged: PSignalInstance # (str, float, float)
|
|
34
|
+
exposureChanged: PSignalInstance # (str, float)
|
|
35
|
+
SLMExposureChanged: PSignalInstance # (str, float)
|
|
36
|
+
# channelGroupChanged: PSignalInstance # (str)
|
|
37
|
+
# configGroupChanged: PSignalInstance # (str, str)
|
|
38
|
+
# configSet: PSignalInstance # (str, str)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CMMCoreProxy(Protocol):
|
|
42
|
+
"""Exposed CMMCcorePlus attributes that devices may access."""
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def events(self) -> CoreEventsProxy:
|
|
46
|
+
"""Events that devices may emit."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def create_core_proxy(core: CMMCorePlus) -> CMMCoreProxy:
|
|
50
|
+
"""Create a proxy object for CMMCorePlus that only exposes CMMCoreProxy."""
|
|
51
|
+
return create_proxy(core, CMMCoreProxy, {"events": CoreEventsProxy})
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class _ImmutableModule(types.ModuleType):
|
|
58
|
+
__frozen__ = False
|
|
59
|
+
|
|
60
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
61
|
+
if self.__frozen__:
|
|
62
|
+
raise AttributeError( # pragma: no cover
|
|
63
|
+
f"Attributes on proxy {self.__name__!r} cannot be modified."
|
|
64
|
+
)
|
|
65
|
+
super().__setattr__(name, value)
|
|
66
|
+
|
|
67
|
+
def __delattr__(self, name: str) -> None:
|
|
68
|
+
raise AttributeError( # pragma: no cover
|
|
69
|
+
f"Attributes on proxy {self.__name__!r} cannot be modified."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def create_proxy(
|
|
74
|
+
obj: Any, protocol: type[T], sub_proxies: Mapping[str, type] | None = None
|
|
75
|
+
) -> T:
|
|
76
|
+
"""Create a proxy object that implements the given protocol.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
obj : Any
|
|
81
|
+
The object to proxy.
|
|
82
|
+
protocol : type[T]
|
|
83
|
+
The protocol template to implement. The names and annotations of the protocol
|
|
84
|
+
define the attributes that will be exposed on the proxy.
|
|
85
|
+
sub_proxies : Mapping[str, type], optional
|
|
86
|
+
A mapping of attribute names to sub-protocols. If an attribute is in this
|
|
87
|
+
mapping, it will be proxied with the corresponding sub-protocol. For example,
|
|
88
|
+
if `sub_proxies={"foo": FooProtocol}`, then `proxy.foo` will be a proxy object
|
|
89
|
+
that implements `FooProtocol`.
|
|
90
|
+
|
|
91
|
+
Examples
|
|
92
|
+
--------
|
|
93
|
+
```python
|
|
94
|
+
class MyProtocol(Protocol):
|
|
95
|
+
def foo(self) -> None: ...
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class MyClass:
|
|
99
|
+
def foo(self) -> None: ...
|
|
100
|
+
def bar(self) -> None: ...
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
proxy = create_proxy(MyClass(), MyProtocol)
|
|
104
|
+
proxy.foo() # OK
|
|
105
|
+
proxy.bar() # AttributeError
|
|
106
|
+
```
|
|
107
|
+
"""
|
|
108
|
+
sub_proxies = sub_proxies or {}
|
|
109
|
+
allowed_names = {
|
|
110
|
+
x
|
|
111
|
+
for x in chain(dir(protocol), get_type_hints(protocol))
|
|
112
|
+
if not x.startswith("_")
|
|
113
|
+
}
|
|
114
|
+
proxy = _ImmutableModule(protocol.__name__)
|
|
115
|
+
for attr_name in allowed_names:
|
|
116
|
+
attr = getattr(obj, attr_name)
|
|
117
|
+
if subprotocol := sub_proxies.get(attr_name):
|
|
118
|
+
# look for nested sub-proxies on attr_name, e.g. `attr_name.sub_attr`
|
|
119
|
+
sub = {
|
|
120
|
+
k.split(".", 1)[1]: v
|
|
121
|
+
for k, v in sub_proxies.items()
|
|
122
|
+
if k.startswith(f"{attr_name}.")
|
|
123
|
+
}
|
|
124
|
+
attr = create_proxy(attr, subprotocol, sub)
|
|
125
|
+
setattr(proxy, attr_name, attr)
|
|
126
|
+
proxy.__frozen__ = True
|
|
127
|
+
return cast("T", proxy)
|