pymmcore-plus 0.9.3__py3-none-any.whl → 0.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. pymmcore_plus/__init__.py +7 -4
  2. pymmcore_plus/_benchmark.py +203 -0
  3. pymmcore_plus/_build.py +6 -1
  4. pymmcore_plus/_cli.py +131 -31
  5. pymmcore_plus/_logger.py +19 -10
  6. pymmcore_plus/_pymmcore.py +12 -0
  7. pymmcore_plus/_util.py +139 -32
  8. pymmcore_plus/core/__init__.py +5 -0
  9. pymmcore_plus/core/_config.py +6 -4
  10. pymmcore_plus/core/_config_group.py +4 -3
  11. pymmcore_plus/core/_constants.py +135 -10
  12. pymmcore_plus/core/_device.py +4 -4
  13. pymmcore_plus/core/_metadata.py +3 -3
  14. pymmcore_plus/core/_mmcore_plus.py +254 -170
  15. pymmcore_plus/core/_property.py +6 -6
  16. pymmcore_plus/core/_sequencing.py +370 -233
  17. pymmcore_plus/core/events/__init__.py +6 -6
  18. pymmcore_plus/core/events/_device_signal_view.py +8 -6
  19. pymmcore_plus/core/events/_norm_slot.py +2 -4
  20. pymmcore_plus/core/events/_prop_event_mixin.py +7 -4
  21. pymmcore_plus/core/events/_protocol.py +5 -2
  22. pymmcore_plus/core/events/_psygnal.py +2 -2
  23. pymmcore_plus/experimental/__init__.py +0 -0
  24. pymmcore_plus/experimental/unicore/__init__.py +14 -0
  25. pymmcore_plus/experimental/unicore/_device_manager.py +173 -0
  26. pymmcore_plus/experimental/unicore/_proxy.py +127 -0
  27. pymmcore_plus/experimental/unicore/_unicore.py +703 -0
  28. pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
  29. pymmcore_plus/experimental/unicore/devices/_device.py +269 -0
  30. pymmcore_plus/experimental/unicore/devices/_properties.py +400 -0
  31. pymmcore_plus/experimental/unicore/devices/_stage.py +221 -0
  32. pymmcore_plus/install.py +16 -11
  33. pymmcore_plus/mda/__init__.py +1 -1
  34. pymmcore_plus/mda/_engine.py +320 -148
  35. pymmcore_plus/mda/_protocol.py +6 -4
  36. pymmcore_plus/mda/_runner.py +62 -51
  37. pymmcore_plus/mda/_thread_relay.py +5 -3
  38. pymmcore_plus/mda/events/__init__.py +2 -2
  39. pymmcore_plus/mda/events/_protocol.py +10 -2
  40. pymmcore_plus/mda/events/_psygnal.py +2 -2
  41. pymmcore_plus/mda/handlers/_5d_writer_base.py +106 -15
  42. pymmcore_plus/mda/handlers/__init__.py +7 -1
  43. pymmcore_plus/mda/handlers/_img_sequence_writer.py +11 -6
  44. pymmcore_plus/mda/handlers/_ome_tiff_writer.py +8 -4
  45. pymmcore_plus/mda/handlers/_ome_zarr_writer.py +82 -9
  46. pymmcore_plus/mda/handlers/_tensorstore_handler.py +374 -0
  47. pymmcore_plus/mda/handlers/_util.py +1 -1
  48. pymmcore_plus/metadata/__init__.py +36 -0
  49. pymmcore_plus/metadata/functions.py +353 -0
  50. pymmcore_plus/metadata/schema.py +472 -0
  51. pymmcore_plus/metadata/serialize.py +120 -0
  52. pymmcore_plus/mocks.py +51 -0
  53. pymmcore_plus/model/_config_file.py +5 -6
  54. pymmcore_plus/model/_config_group.py +29 -2
  55. pymmcore_plus/model/_core_device.py +12 -1
  56. pymmcore_plus/model/_core_link.py +2 -1
  57. pymmcore_plus/model/_device.py +39 -8
  58. pymmcore_plus/model/_microscope.py +39 -3
  59. pymmcore_plus/model/_pixel_size_config.py +27 -4
  60. pymmcore_plus/model/_property.py +13 -3
  61. pymmcore_plus/seq_tester.py +1 -1
  62. {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/METADATA +22 -12
  63. pymmcore_plus-0.13.0.dist-info/RECORD +71 -0
  64. {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/WHEEL +1 -1
  65. pymmcore_plus/core/_state.py +0 -244
  66. pymmcore_plus-0.9.3.dist-info/RECORD +0 -55
  67. {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/entry_points.txt +0 -0
  68. {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING, Any, List, Type
1
+ from typing import TYPE_CHECKING, Any
2
2
 
3
3
  from pymmcore_plus._util import signals_backend
4
4
 
@@ -6,19 +6,19 @@ from ._protocol import PCoreSignaler
6
6
  from ._psygnal import CMMCoreSignaler
7
7
 
8
8
  if TYPE_CHECKING:
9
- from ._qsignals import QCoreSignaler # noqa: TCH004
9
+ from ._qsignals import QCoreSignaler # noqa: TC004
10
10
 
11
11
 
12
12
  __all__ = [
13
13
  "CMMCoreSignaler",
14
- "QCoreSignaler",
15
14
  "PCoreSignaler",
16
- "_get_auto_core_callback_class",
15
+ "QCoreSignaler",
17
16
  "_denormalize_slot",
17
+ "_get_auto_core_callback_class",
18
18
  ]
19
19
 
20
20
 
21
- def _get_auto_core_callback_class() -> Type[PCoreSignaler]:
21
+ def _get_auto_core_callback_class() -> type[PCoreSignaler]:
22
22
  if signals_backend() == "qt":
23
23
  from ._qsignals import QCoreSignaler
24
24
 
@@ -26,7 +26,7 @@ def _get_auto_core_callback_class() -> Type[PCoreSignaler]:
26
26
  return CMMCoreSignaler
27
27
 
28
28
 
29
- def __dir__() -> List[str]: # pragma: no cover
29
+ def __dir__() -> list[str]: # pragma: no cover
30
30
  return [*list(globals()), "QCoreSignaler"]
31
31
 
32
32
 
@@ -1,14 +1,16 @@
1
- from typing import TYPE_CHECKING, Any, Optional
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
2
4
 
3
5
  if TYPE_CHECKING:
4
6
  from pymmcore_plus.core import CMMCorePlus
5
7
 
6
- from ._prop_event_mixin import _C
8
+ from ._prop_event_mixin import _C
7
9
 
8
10
 
9
11
  class _DevicePropValueSignal:
10
12
  def __init__(
11
- self, device_label: str, property_name: Optional[str], mmcore: "CMMCorePlus"
13
+ self, device_label: str, property_name: str | None, mmcore: CMMCorePlus
12
14
  ) -> None:
13
15
  self._dev = device_label
14
16
  self._prop = property_name
@@ -18,13 +20,13 @@ class _DevicePropValueSignal:
18
20
  sig = self._mmc.events.devicePropertyChanged(self._dev, self._prop)
19
21
  return sig.connect(callback) # type: ignore
20
22
 
21
- def disconnect(self, callback: _C) -> None:
23
+ def disconnect(self, callback: _C | None = None) -> None:
22
24
  sig = self._mmc.events.devicePropertyChanged(self._dev, self._prop)
23
- return sig.disconnect(callback) # type: ignore
25
+ sig.disconnect(callback)
24
26
 
25
27
  def emit(self, *args: Any) -> Any:
26
28
  """Emits the signal with the given arguments."""
27
29
  self._mmc.events.devicePropertyChanged(self._dev, self._prop).emit(*args)
28
30
 
29
- def __call__(self, property: str) -> "_DevicePropValueSignal":
31
+ def __call__(self, property: str) -> _DevicePropValueSignal:
30
32
  return _DevicePropValueSignal(self._dev, property, self._mmc)
@@ -8,13 +8,11 @@ from types import MethodType
8
8
  from typing import TYPE_CHECKING, Any, Callable, Union
9
9
 
10
10
  if TYPE_CHECKING:
11
- from typing import Tuple
12
-
13
11
  from typing_extensions import TypeGuard # py310
14
12
 
15
- MethodRef = Tuple[weakref.ReferenceType[object], str, Callable | None]
13
+ MethodRef = tuple[weakref.ReferenceType[object], str, Callable | None]
16
14
  NormedCallback = Union[MethodRef, Callable]
17
- StoredSlot = Tuple[NormedCallback, int | None]
15
+ StoredSlot = tuple[NormedCallback, int | None]
18
16
  ReducerFunc = Callable[[tuple, tuple], tuple]
19
17
 
20
18
 
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, Tuple, TypeVar
3
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar, TypeVar
4
4
 
5
5
  from ._norm_slot import denormalize_slot, normalize_slot
6
6
  from ._protocol import PCoreSignaler
@@ -8,8 +8,8 @@ from ._protocol import PCoreSignaler
8
8
  if TYPE_CHECKING:
9
9
  from ._norm_slot import NormedCallback
10
10
 
11
- PropKey = Tuple[str, str | None, NormedCallback]
12
- PropKeyDict = Dict[PropKey, Callable]
11
+ PropKey = tuple[str, str | None, NormedCallback]
12
+ PropKeyDict = dict[PropKey, Callable]
13
13
 
14
14
 
15
15
  _C = TypeVar("_C", bound=Callable[..., Any])
@@ -64,8 +64,11 @@ class _PropertySignal:
64
64
  self._events.propertyChanged.connect(_wrapper)
65
65
  return callback
66
66
 
67
- def disconnect(self, callback: Callable) -> None:
67
+ def disconnect(self, callback: Callable | None = None) -> None:
68
68
  """Disconnect `callback` from this device and/or property."""
69
+ if callback is None:
70
+ self._events.propertyChanged.disconnect()
71
+ return
69
72
  key = (self._device, self._property, normalize_slot(callback))
70
73
  cb = self._events.property_callbacks.pop(key, None)
71
74
  if cb is None:
@@ -12,8 +12,11 @@ class PSignalInstance(Protocol):
12
12
  def connect(self, slot: Callable) -> Any:
13
13
  """Connect slot to this signal."""
14
14
 
15
- def disconnect(self, slot: Callable) -> Any:
16
- """Disconnect slot from this signal."""
15
+ def disconnect(self, slot: Optional[Callable] = None) -> Any:
16
+ """Disconnect slot from this signal.
17
+
18
+ If `None`, all slots should be disconnected.
19
+ """
17
20
 
18
21
  def emit(self, *args: Any) -> Any:
19
22
  """Emits the signal with the given arguments."""
@@ -1,11 +1,11 @@
1
- from psygnal import Signal, SignalInstance
1
+ from psygnal import Signal, SignalGroup, SignalInstance
2
2
 
3
3
  from pymmcore_plus.mda import MDAEngine
4
4
 
5
5
  from ._prop_event_mixin import _DevicePropertyEventMixin
6
6
 
7
7
 
8
- class CMMCoreSignaler(_DevicePropertyEventMixin):
8
+ class CMMCoreSignaler(SignalGroup, _DevicePropertyEventMixin):
9
9
  """Signals that will be emitted from CMMCorePlus objects."""
10
10
 
11
11
  # native MMCore callback events
File without changes
@@ -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)