pymmcore-plus 0.14.0__py3-none-any.whl → 0.15.2__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 +25 -0
- pymmcore_plus/_ipy_completion.py +363 -0
- pymmcore_plus/_pymmcore.py +4 -2
- pymmcore_plus/core/_constants.py +25 -3
- pymmcore_plus/core/_mmcore_plus.py +110 -55
- pymmcore_plus/core/_sequencing.py +1 -1
- pymmcore_plus/core/events/_deprecated.py +67 -0
- pymmcore_plus/core/events/_protocol.py +64 -39
- pymmcore_plus/core/events/_psygnal.py +35 -6
- pymmcore_plus/core/events/_qsignals.py +34 -6
- pymmcore_plus/experimental/unicore/__init__.py +12 -2
- pymmcore_plus/experimental/unicore/_device_manager.py +1 -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 +314 -0
- pymmcore_plus/experimental/unicore/core/_unicore.py +1769 -0
- pymmcore_plus/experimental/unicore/devices/_camera.py +201 -0
- pymmcore_plus/experimental/unicore/devices/{_device.py → _device_base.py} +54 -28
- pymmcore_plus/experimental/unicore/devices/_generic_device.py +12 -0
- pymmcore_plus/experimental/unicore/devices/_properties.py +9 -2
- pymmcore_plus/experimental/unicore/devices/_shutter.py +30 -0
- pymmcore_plus/experimental/unicore/devices/_slm.py +82 -0
- pymmcore_plus/experimental/unicore/devices/_stage.py +1 -1
- pymmcore_plus/experimental/unicore/devices/_state.py +152 -0
- pymmcore_plus/mda/events/_protocol.py +8 -8
- {pymmcore_plus-0.14.0.dist-info → pymmcore_plus-0.15.2.dist-info}/METADATA +2 -2
- {pymmcore_plus-0.14.0.dist-info → pymmcore_plus-0.15.2.dist-info}/RECORD +30 -21
- pymmcore_plus/experimental/unicore/_unicore.py +0 -703
- {pymmcore_plus-0.14.0.dist-info → pymmcore_plus-0.15.2.dist-info}/WHEEL +0 -0
- {pymmcore_plus-0.14.0.dist-info → pymmcore_plus-0.15.2.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.14.0.dist-info → pymmcore_plus-0.15.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from types import MappingProxyType
|
|
5
|
+
from typing import TYPE_CHECKING, Callable, ClassVar, Literal
|
|
6
|
+
|
|
7
|
+
from pymmcore_plus.core._constants import DeviceType, Keyword, PixelFormat
|
|
8
|
+
|
|
9
|
+
from ._device_base import Device
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from numpy.typing import DTypeLike
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CameraDevice(Device):
|
|
19
|
+
# mandatory methods for Camera device adapters
|
|
20
|
+
|
|
21
|
+
_TYPE: ClassVar[Literal[DeviceType.Camera]] = DeviceType.Camera
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def get_exposure(self) -> float:
|
|
25
|
+
"""Get the current exposure time in milliseconds."""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def set_exposure(self, exposure: float) -> None:
|
|
30
|
+
"""Set the exposure time in milliseconds."""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def shape(self) -> tuple[int, ...]:
|
|
35
|
+
"""Return the shape of the image buffer.
|
|
36
|
+
|
|
37
|
+
This is used when querying Width, Height, *and* number of components.
|
|
38
|
+
If the camera is grayscale, it should return (width, height).
|
|
39
|
+
If the camera is color, it should return (width, height, n_channels).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def dtype(self) -> DTypeLike:
|
|
44
|
+
"""Return the data type of the image buffer."""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def start_sequence(
|
|
48
|
+
self,
|
|
49
|
+
n: int | None,
|
|
50
|
+
get_buffer: Callable[[Sequence[int], DTypeLike], np.ndarray],
|
|
51
|
+
) -> Iterator[Mapping]:
|
|
52
|
+
"""Start a sequence acquisition.
|
|
53
|
+
|
|
54
|
+
This method should be implemented by the camera device adapter and should
|
|
55
|
+
yield metadata for each acquired image. The implementation should call
|
|
56
|
+
get_buffer() to get a buffer, fill it with image data, then yield the
|
|
57
|
+
metadata for that image.
|
|
58
|
+
|
|
59
|
+
The core will handle threading and synchronization. This function may block.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
n : int | None
|
|
64
|
+
If an integer, this is the number of images to acquire.
|
|
65
|
+
If None, the camera should acquire images indefinitely until stopped.
|
|
66
|
+
get_buffer : Callable[[Sequence[int], DTypeLike], np.ndarray]
|
|
67
|
+
A callable that returns a buffer for the camera to fill with image data.
|
|
68
|
+
You should call this with the shape of the image and the dtype
|
|
69
|
+
of the image data. The core will produce a buffer of the requested shape
|
|
70
|
+
and dtype, and you should fill it (in place) with the image data.
|
|
71
|
+
|
|
72
|
+
Yields
|
|
73
|
+
------
|
|
74
|
+
Mapping
|
|
75
|
+
Metadata for each acquired image. This should be yielded after the
|
|
76
|
+
corresponding buffer has been filled with image data.
|
|
77
|
+
"""
|
|
78
|
+
# EXAMPLE USAGE:
|
|
79
|
+
# shape, dtype = self.shape(), self.dtype()
|
|
80
|
+
# if n is None: # acquire indefinitely until stopped
|
|
81
|
+
# while True:
|
|
82
|
+
# yield ...
|
|
83
|
+
# return
|
|
84
|
+
# for _ in range(n):
|
|
85
|
+
# image = get_buffer(shape, dtype)
|
|
86
|
+
# get the image from the camera, and fill the buffer in place
|
|
87
|
+
# image[:] = <your_camera_data>
|
|
88
|
+
# notify the core that the buffer is ready, and provide any metadata
|
|
89
|
+
# yield {"key": "value", ...} # metadata for the image
|
|
90
|
+
|
|
91
|
+
# TODO:
|
|
92
|
+
# Open question: who is responsible for key pieces of metadata?
|
|
93
|
+
# in CMMCore, each of the camera device adapters is responsible for
|
|
94
|
+
# injecting to following bits of metadata:
|
|
95
|
+
# - MM::g_Keyword_Metadata_CameraLabel
|
|
96
|
+
# - MM::g_Keyword_Elapsed_Time_ms (GetCurrentMMTime - start_time)
|
|
97
|
+
# - MM::g_Keyword_Metadata_ROI_X
|
|
98
|
+
# - MM::g_Keyword_Metadata_ROI_Y
|
|
99
|
+
# - MM::g_Keyword_Binning
|
|
100
|
+
# --- while the CircularBuffer InsertMultiChannel is responsible for adding:
|
|
101
|
+
# - MM::g_Keyword_Metadata_ImageNumber
|
|
102
|
+
# - MM::g_Keyword_Elapsed_Time_ms
|
|
103
|
+
# - MM::g_Keyword_Metadata_TimeInCore
|
|
104
|
+
# - MM::g_Keyword_Metadata_Width
|
|
105
|
+
# - MM::g_Keyword_Metadata_Height
|
|
106
|
+
# - MM::g_Keyword_PixelType
|
|
107
|
+
|
|
108
|
+
# Standard Properties --------------------------------------------
|
|
109
|
+
|
|
110
|
+
# these are the standard properties that cameras may implement.
|
|
111
|
+
# Cameras are not required to implement all of these properties, and they may
|
|
112
|
+
# implement additional properties as well.
|
|
113
|
+
# To implement a property, you MUST define a `get_<snake_name>` method and
|
|
114
|
+
# MAY define a `set_<snake_name>` method.
|
|
115
|
+
# To modify the standard properties, you can use the following methods in your
|
|
116
|
+
# __init__, (after calling super().__init__()):
|
|
117
|
+
# self.set_property_value(name, ...)
|
|
118
|
+
# self.set_property_allowed_values(name, ...)
|
|
119
|
+
# self.set_property_limits(name, ...)
|
|
120
|
+
# self.set_property_sequence_max_length(name, ...)
|
|
121
|
+
|
|
122
|
+
STANDARD_PROPERTIES: ClassVar = MappingProxyType(
|
|
123
|
+
{
|
|
124
|
+
Keyword.ActualInterval_ms: ("actual_interval_ms", float),
|
|
125
|
+
Keyword.Binning: ("binning", int),
|
|
126
|
+
Keyword.CameraID: ("camera_id", str),
|
|
127
|
+
Keyword.CameraName: ("camera_name", str),
|
|
128
|
+
Keyword.CCDTemperature: ("ccd_temperature", float),
|
|
129
|
+
Keyword.CCDTemperatureSetPoint: ("ccd_temperature_set_point", float),
|
|
130
|
+
Keyword.EMGain: ("em_gain", float),
|
|
131
|
+
Keyword.Exposure: ("exposure", float),
|
|
132
|
+
Keyword.Gain: ("gain", float),
|
|
133
|
+
Keyword.Interval_ms: ("interval_ms", float),
|
|
134
|
+
Keyword.Offset: ("offset", float),
|
|
135
|
+
# Keyword.PixelType: ("pixel_type", str), # don't use. use PixelFormat
|
|
136
|
+
"PixelFormat": ("pixel_format", PixelFormat),
|
|
137
|
+
Keyword.ReadoutMode: ("readout_mode", str),
|
|
138
|
+
Keyword.ReadoutTime: ("readout_time", float),
|
|
139
|
+
Keyword.Metadata_ROI_X: ("roi_x", int),
|
|
140
|
+
Keyword.Metadata_ROI_Y: ("roi_y", int),
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# optional methods
|
|
145
|
+
# def get_camera_name(self) -> str:
|
|
146
|
+
# def set_camera_name(self, value: str) -> None:
|
|
147
|
+
# def get_camera_id(self) -> str:
|
|
148
|
+
# def set_camera_id(self, value: str) -> None:
|
|
149
|
+
# def get_binning(self) -> str:
|
|
150
|
+
# def set_binning(self, value: str) -> None:
|
|
151
|
+
# def get_pixel_format(self) -> PixelFormat: ...
|
|
152
|
+
# def set_pixel_format(self, value: PixelFormat) -> None: ...
|
|
153
|
+
# def get_gain(self) -> str:
|
|
154
|
+
# def set_gain(self, value: str) -> None:
|
|
155
|
+
# def get_offset(self) -> str:
|
|
156
|
+
# def set_offset(self, value: str) -> None:
|
|
157
|
+
# def get_readout_mode(self) -> str:
|
|
158
|
+
# def set_readout_mode(self, value: str) -> None:
|
|
159
|
+
# def get_readout_time(self) -> str:
|
|
160
|
+
# def set_readout_time(self, value: str) -> None:
|
|
161
|
+
# def get_actual_interval_ms(self) -> str:
|
|
162
|
+
# def set_actual_interval_ms(self, value: str) -> None:
|
|
163
|
+
# def get_interval_ms(self) -> str:
|
|
164
|
+
# def set_interval_ms(self, value: str) -> None:
|
|
165
|
+
# def get_em_gain(self) -> str:
|
|
166
|
+
# def set_em_gain(self, value: str) -> None:
|
|
167
|
+
# def get_ccd_temperature(self) -> str:
|
|
168
|
+
# def set_ccd_temperature(self, value: str) -> None:
|
|
169
|
+
# def get_ccd_temperature_set_point(self) -> str:
|
|
170
|
+
# def set_ccd_temperature_set_point(self, value: str) -> None:
|
|
171
|
+
|
|
172
|
+
def __init__(self) -> None:
|
|
173
|
+
super().__init__()
|
|
174
|
+
self.register_standard_properties()
|
|
175
|
+
|
|
176
|
+
def register_standard_properties(self) -> None:
|
|
177
|
+
"""Inspect the class for standard properties and register them."""
|
|
178
|
+
cls = type(self)
|
|
179
|
+
for name, (snake_name, prop_type) in self.STANDARD_PROPERTIES.items():
|
|
180
|
+
if getter := getattr(cls, f"get_{snake_name}", None):
|
|
181
|
+
setter = getattr(cls, f"set_{snake_name}", None)
|
|
182
|
+
seq_loader = getattr(cls, f"load_{snake_name}_sequence", None)
|
|
183
|
+
seq_starter = getattr(cls, f"start_{snake_name}_sequence", None)
|
|
184
|
+
seq_stopper = getattr(cls, f"stop_{snake_name}_sequence", None)
|
|
185
|
+
self.register_property(
|
|
186
|
+
name=name,
|
|
187
|
+
property_type=prop_type,
|
|
188
|
+
getter=getter,
|
|
189
|
+
setter=setter,
|
|
190
|
+
sequence_loader=seq_loader,
|
|
191
|
+
sequence_starter=seq_starter,
|
|
192
|
+
sequence_stopper=seq_stopper,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Standard Properties, default implementations -------------------
|
|
196
|
+
|
|
197
|
+
# We always implement a standard binning getter. It does not
|
|
198
|
+
# mean that the camera supports binning, unless they implement a setter.
|
|
199
|
+
def get_binning(self) -> int:
|
|
200
|
+
"""Get the binning factor for the camera."""
|
|
201
|
+
return 1 # pragma: no cover
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import threading
|
|
4
4
|
from abc import ABC
|
|
5
5
|
from collections import ChainMap
|
|
6
|
+
from enum import EnumMeta
|
|
6
7
|
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, TypeVar, final
|
|
7
8
|
|
|
8
9
|
from pymmcore_plus.core import DeviceType
|
|
@@ -73,11 +74,11 @@ class Device(_Lockable, ABC):
|
|
|
73
74
|
|
|
74
75
|
def __init_subclass__(cls) -> None:
|
|
75
76
|
"""Initialize the property controllers."""
|
|
76
|
-
cls._cls_prop_controllers = {
|
|
77
|
-
|
|
78
|
-
for p in
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
cls._cls_prop_controllers = {}
|
|
78
|
+
for base in cls.__mro__:
|
|
79
|
+
for p in base.__dict__.values():
|
|
80
|
+
if isinstance(p, PropertyController):
|
|
81
|
+
cls._cls_prop_controllers[p.property.name] = p
|
|
81
82
|
return super().__init_subclass__()
|
|
82
83
|
|
|
83
84
|
def register_property(
|
|
@@ -93,6 +94,9 @@ class Device(_Lockable, ABC):
|
|
|
93
94
|
is_read_only: bool = False,
|
|
94
95
|
is_pre_init: bool = False,
|
|
95
96
|
property_type: PropArg = None,
|
|
97
|
+
sequence_loader: Callable[[TDev, Sequence[TProp]], None] | None = None,
|
|
98
|
+
sequence_starter: Callable[[TDev], None] | None = None,
|
|
99
|
+
sequence_stopper: Callable[[TDev], None] | None = None,
|
|
96
100
|
) -> None:
|
|
97
101
|
"""Manually register a property.
|
|
98
102
|
|
|
@@ -108,6 +112,9 @@ class Device(_Lockable, ABC):
|
|
|
108
112
|
if property_type is None and default_value is not None:
|
|
109
113
|
property_type = type(default_value)
|
|
110
114
|
|
|
115
|
+
if isinstance(property_type, EnumMeta) and allowed_values is None:
|
|
116
|
+
allowed_values = tuple(property_type)
|
|
117
|
+
|
|
111
118
|
prop_info = PropertyInfo(
|
|
112
119
|
name=name,
|
|
113
120
|
default_value=default_value,
|
|
@@ -120,7 +127,14 @@ class Device(_Lockable, ABC):
|
|
|
120
127
|
is_pre_init=is_pre_init,
|
|
121
128
|
type=PropertyType.create(property_type),
|
|
122
129
|
)
|
|
123
|
-
controller = PropertyController(
|
|
130
|
+
controller = PropertyController(
|
|
131
|
+
property=prop_info,
|
|
132
|
+
fget=getter,
|
|
133
|
+
fset=setter,
|
|
134
|
+
fseq_load=sequence_loader,
|
|
135
|
+
fseq_start=sequence_starter,
|
|
136
|
+
fseq_stop=sequence_stopper,
|
|
137
|
+
)
|
|
124
138
|
self._prop_controllers_[name] = controller
|
|
125
139
|
|
|
126
140
|
def initialize(self) -> None:
|
|
@@ -154,26 +168,38 @@ class Device(_Lockable, ABC):
|
|
|
154
168
|
|
|
155
169
|
# PROPERTIES
|
|
156
170
|
|
|
171
|
+
def _get_prop_or_raise(self, prop_name: str) -> PropertyController:
|
|
172
|
+
"""Get a property controller by name or raise an error."""
|
|
173
|
+
if prop_name not in self._prop_controllers_:
|
|
174
|
+
raise KeyError(
|
|
175
|
+
f"Device {self.get_label()!r} has no property {prop_name!r}."
|
|
176
|
+
)
|
|
177
|
+
return self._prop_controllers_[prop_name]
|
|
178
|
+
|
|
179
|
+
def has_property(self, prop_name: str) -> bool:
|
|
180
|
+
"""Return `True` if the device has a property with the given name."""
|
|
181
|
+
return prop_name in self._prop_controllers_
|
|
182
|
+
|
|
157
183
|
def get_property_names(self) -> KeysView[str]:
|
|
158
184
|
"""Return the names of the properties."""
|
|
159
185
|
return self._prop_controllers_.keys()
|
|
160
186
|
|
|
161
|
-
def
|
|
187
|
+
def get_property_info(self, prop_name: str) -> PropertyInfo:
|
|
162
188
|
"""Return the property controller for a property."""
|
|
163
|
-
return self.
|
|
189
|
+
return self._get_prop_or_raise(prop_name).property
|
|
164
190
|
|
|
165
191
|
def get_property_value(self, prop_name: str) -> Any:
|
|
166
192
|
"""Return the value of a property."""
|
|
167
193
|
# TODO: catch errors
|
|
168
|
-
ctrl = self.
|
|
194
|
+
ctrl = self._get_prop_or_raise(prop_name)
|
|
169
195
|
if ctrl.fget is None:
|
|
170
196
|
return ctrl.property.last_value
|
|
171
|
-
return
|
|
197
|
+
return ctrl.__get__(self, self.__class__)
|
|
172
198
|
|
|
173
199
|
def set_property_value(self, prop_name: str, value: Any) -> None:
|
|
174
200
|
"""Set the value of a property."""
|
|
175
201
|
# TODO: catch errors
|
|
176
|
-
ctrl = self.
|
|
202
|
+
ctrl = self._get_prop_or_raise(prop_name)
|
|
177
203
|
if ctrl.is_read_only:
|
|
178
204
|
raise ValueError(f"Property {prop_name!r} is read-only.")
|
|
179
205
|
if ctrl.fset is not None:
|
|
@@ -181,41 +207,41 @@ class Device(_Lockable, ABC):
|
|
|
181
207
|
else:
|
|
182
208
|
ctrl.property.last_value = ctrl.validate(value)
|
|
183
209
|
|
|
184
|
-
def load_property_sequence(self, prop_name: str, sequence: Sequence[Any]) -> None:
|
|
185
|
-
"""Load a sequence into a property."""
|
|
186
|
-
self._prop_controllers_[prop_name].load_sequence(self, sequence)
|
|
187
|
-
|
|
188
|
-
def start_property_sequence(self, prop_name: str) -> None:
|
|
189
|
-
"""Start a sequence of a property."""
|
|
190
|
-
self._prop_controllers_[prop_name].start_sequence(self)
|
|
191
|
-
|
|
192
|
-
def stop_property_sequence(self, prop_name: str) -> None:
|
|
193
|
-
"""Stop a sequence of a property."""
|
|
194
|
-
self._prop_controllers_[prop_name].stop_sequence(self)
|
|
195
|
-
|
|
196
210
|
def set_property_allowed_values(
|
|
197
211
|
self, prop_name: str, allowed_values: Sequence[Any]
|
|
198
212
|
) -> None:
|
|
199
213
|
"""Set the allowed values of a property."""
|
|
200
|
-
self.
|
|
214
|
+
self._get_prop_or_raise(prop_name).property.allowed_values = allowed_values
|
|
201
215
|
|
|
202
216
|
def set_property_limits(
|
|
203
217
|
self, prop_name: str, limits: tuple[float, float] | None
|
|
204
218
|
) -> None:
|
|
205
219
|
"""Set the limits of a property."""
|
|
206
|
-
self.
|
|
220
|
+
self._get_prop_or_raise(prop_name).property.limits = limits
|
|
207
221
|
|
|
208
222
|
def set_property_sequence_max_length(self, prop_name: str, max_length: int) -> None:
|
|
209
223
|
"""Set the sequence max length of a property."""
|
|
210
|
-
self.
|
|
224
|
+
self._get_prop_or_raise(prop_name).property.sequence_max_length = max_length
|
|
225
|
+
|
|
226
|
+
def load_property_sequence(self, prop_name: str, sequence: Sequence[Any]) -> None:
|
|
227
|
+
"""Load a sequence into a property."""
|
|
228
|
+
self._get_prop_or_raise(prop_name).load_sequence(self, sequence)
|
|
229
|
+
|
|
230
|
+
def start_property_sequence(self, prop_name: str) -> None:
|
|
231
|
+
"""Start a sequence of a property."""
|
|
232
|
+
self._get_prop_or_raise(prop_name).start_sequence(self)
|
|
233
|
+
|
|
234
|
+
def stop_property_sequence(self, prop_name: str) -> None:
|
|
235
|
+
"""Stop a sequence of a property."""
|
|
236
|
+
self._get_prop_or_raise(prop_name).stop_sequence(self)
|
|
211
237
|
|
|
212
238
|
def is_property_sequenceable(self, prop_name: str) -> bool:
|
|
213
239
|
"""Return `True` if the property is sequenceable."""
|
|
214
|
-
return self.
|
|
240
|
+
return self._get_prop_or_raise(prop_name).is_sequenceable
|
|
215
241
|
|
|
216
242
|
def is_property_read_only(self, prop_name: str) -> bool:
|
|
217
243
|
"""Return `True` if the property is read-only."""
|
|
218
|
-
return self.
|
|
244
|
+
return self._get_prop_or_raise(prop_name).is_read_only
|
|
219
245
|
|
|
220
246
|
|
|
221
247
|
SeqT = TypeVar("SeqT")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from pymmcore_plus.core._constants import DeviceType
|
|
2
|
+
|
|
3
|
+
from ._device_base import Device
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GenericDevice(Device):
|
|
7
|
+
"""Generic device API, e.g. for devices that don't fit into other categories.
|
|
8
|
+
|
|
9
|
+
Generic Devices generally only use the device property interface.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
_TYPE = DeviceType.GenericDevice
|
|
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
|
|
|
21
21
|
|
|
22
22
|
from typing_extensions import Self, TypeAlias
|
|
23
23
|
|
|
24
|
-
from .
|
|
24
|
+
from ._device_base import Device
|
|
25
25
|
|
|
26
26
|
PropArg: TypeAlias = (
|
|
27
27
|
PropertyType | type | Literal["float", "integer", "string", "boolean"] | None
|
|
@@ -76,6 +76,13 @@ class PropertyInfo(Generic[TProp]):
|
|
|
76
76
|
is_read_only: bool | None = None
|
|
77
77
|
is_pre_init: bool = False
|
|
78
78
|
|
|
79
|
+
@property
|
|
80
|
+
def number_of_allowed_values(self) -> int:
|
|
81
|
+
"""Return the number of allowed values."""
|
|
82
|
+
if self.allowed_values is None:
|
|
83
|
+
return 0
|
|
84
|
+
return len(self.allowed_values)
|
|
85
|
+
|
|
79
86
|
@property
|
|
80
87
|
def is_sequenceable(self) -> bool:
|
|
81
88
|
"""Return True if the property is sequenceable."""
|
|
@@ -291,7 +298,7 @@ def pymm_property(
|
|
|
291
298
|
is_pre_init: bool = ...,
|
|
292
299
|
name: str | None = ...,
|
|
293
300
|
property_type: PropArg = ...,
|
|
294
|
-
) -> Callable[[Callable[[TDev],
|
|
301
|
+
) -> Callable[[Callable[[TDev], TProp]], PropertyController[TDev, TProp]]: ...
|
|
295
302
|
def pymm_property(
|
|
296
303
|
fget: Callable[[TDev], TProp] | None = None,
|
|
297
304
|
*,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
|
|
5
|
+
from pymmcore_plus.core._constants import DeviceType
|
|
6
|
+
|
|
7
|
+
from ._device_base import Device
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ShutterDevice(Device):
|
|
11
|
+
"""Shutter device API, e.g. for physical shutters or electronic shutter control.
|
|
12
|
+
|
|
13
|
+
Or any 2-state device that can be either open or closed.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
_TYPE = DeviceType.ShutterDevice
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def get_open(self) -> bool:
|
|
20
|
+
"""Return True if the shutter is open, False if it is closed."""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def set_open(self, open: bool) -> None:
|
|
24
|
+
"""Set the shutter to open or closed.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
open : bool
|
|
29
|
+
True to open the shutter, False to close it.
|
|
30
|
+
"""
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import ClassVar, Literal
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from numpy.typing import DTypeLike
|
|
7
|
+
|
|
8
|
+
from pymmcore_plus.core._constants import DeviceType
|
|
9
|
+
|
|
10
|
+
from ._device_base import SequenceableDevice
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SLMDevice(SequenceableDevice[np.ndarray]):
|
|
14
|
+
"""ABC for Spatial Light Modulator (SLM) devices.
|
|
15
|
+
|
|
16
|
+
SLM devices are capable of displaying images. They are expected to represent a
|
|
17
|
+
rectangular grid of pixels that can be either 8-bit or 32-bit. Illumination (light
|
|
18
|
+
source on or off) is logically independent of displaying the image.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
_TYPE: ClassVar[Literal[DeviceType.SLM]] = DeviceType.SLM
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def shape(self) -> tuple[int, ...]:
|
|
25
|
+
"""Return the shape of the SLM image buffer.
|
|
26
|
+
|
|
27
|
+
This is used when querying Width, Height, *and* number of components.
|
|
28
|
+
If the SLM is grayscale, it should return (width, height).
|
|
29
|
+
If the SLM is color, it should return (width, height, n_channels).
|
|
30
|
+
"""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def dtype(self) -> DTypeLike:
|
|
35
|
+
"""Return the data type of the image buffer."""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def set_image(self, pixels: np.ndarray) -> None:
|
|
40
|
+
"""Load the image into the SLM device adapter."""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
def get_image(self) -> np.ndarray:
|
|
44
|
+
"""Get the current image from the SLM device adapter.
|
|
45
|
+
|
|
46
|
+
This is useful for verifying that the image was set correctly.
|
|
47
|
+
"""
|
|
48
|
+
raise NotImplementedError("This SLM device does not support getting images.")
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def display_image(self) -> None:
|
|
52
|
+
"""Command the SLM to display the loaded image."""
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def set_exposure(self, interval_ms: float) -> None:
|
|
56
|
+
"""Command the SLM to turn off after a specified interval."""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def get_exposure(self) -> float:
|
|
61
|
+
"""Find out the exposure interval of an SLM."""
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
# Sequence methods from SequenceableDevice
|
|
65
|
+
def get_sequence_max_length(self) -> int:
|
|
66
|
+
"""Return the maximum length of an image sequence that can be uploaded."""
|
|
67
|
+
return 0 # Override in subclasses that support sequencing
|
|
68
|
+
|
|
69
|
+
def send_sequence(self, sequence: Sequence[np.ndarray]) -> None:
|
|
70
|
+
"""Load a sequence of images to the SLM."""
|
|
71
|
+
# Default implementation - override in subclasses that support sequencing
|
|
72
|
+
raise NotImplementedError("This SLM device does not support sequences.")
|
|
73
|
+
|
|
74
|
+
def start_sequence(self) -> None:
|
|
75
|
+
"""Start a sequence of images on the SLM."""
|
|
76
|
+
# Default implementation - override in subclasses that support sequencing
|
|
77
|
+
raise NotImplementedError("This SLM device does not support sequences.")
|
|
78
|
+
|
|
79
|
+
def stop_sequence(self) -> None:
|
|
80
|
+
"""Stop a sequence of images on the SLM."""
|
|
81
|
+
# Default implementation - override in subclasses that support sequencing
|
|
82
|
+
raise NotImplementedError("This SLM device does not support sequences.")
|
|
@@ -4,7 +4,7 @@ from typing import ClassVar, Literal
|
|
|
4
4
|
from pymmcore_plus.core import DeviceType
|
|
5
5
|
from pymmcore_plus.core._constants import Keyword
|
|
6
6
|
|
|
7
|
-
from .
|
|
7
|
+
from ._device_base import SeqT, SequenceableDevice
|
|
8
8
|
|
|
9
9
|
__all__ = ["_BaseStage"]
|
|
10
10
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from typing import TYPE_CHECKING, cast
|
|
5
|
+
|
|
6
|
+
from pymmcore_plus.core._constants import DeviceType, Keyword
|
|
7
|
+
|
|
8
|
+
from ._device_base import Device
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Iterable, Mapping
|
|
12
|
+
from typing import ClassVar, Literal
|
|
13
|
+
|
|
14
|
+
from pymmcore import StateLabel
|
|
15
|
+
from typing_extensions import Self
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StateDevice(Device):
|
|
19
|
+
"""State device API, e.g. filter wheel, objective turret, etc.
|
|
20
|
+
|
|
21
|
+
A state device is a device that at any point in time is in a single state out of a
|
|
22
|
+
list of possible states, like a filter wheel, an objective turret, etc. The
|
|
23
|
+
interface contains functions to get and set the state, to give states human readable
|
|
24
|
+
labels, and functions to make it possible to treat the state device as a shutter.
|
|
25
|
+
|
|
26
|
+
In terms of implementation, this base class provides the basic functionality by
|
|
27
|
+
presenting state and label as properties, which it keeps in sync with the
|
|
28
|
+
underlying device.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
state_labels: Mapping[int, str] | Iterable[tuple[int, str]]
|
|
33
|
+
A mapping (or iterable of 2-tuples) of integer state indices to string labels.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# Mandatory methods for state devices
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def get_state(self) -> int:
|
|
40
|
+
"""Get the current state of the device (integer index)."""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def set_state(self, position: int) -> None:
|
|
45
|
+
"""Set the state of the device (integer index)."""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
# ------------------ The rest is base class implementation ------------------
|
|
49
|
+
# (adaptors may override these methods if desired)
|
|
50
|
+
|
|
51
|
+
_TYPE: ClassVar[Literal[DeviceType.State]] = DeviceType.State
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_count(cls, count: int) -> Self:
|
|
55
|
+
"""Simplified constructor with just a number of states."""
|
|
56
|
+
if count < 1:
|
|
57
|
+
raise ValueError("State device must have at least one state.")
|
|
58
|
+
return cls({i: f"State-{i}" for i in range(count)})
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self, state_labels: Mapping[int, str] | Iterable[tuple[int, str]], /
|
|
62
|
+
) -> None:
|
|
63
|
+
super().__init__()
|
|
64
|
+
if not (states := dict(state_labels)): # pragma: no cover
|
|
65
|
+
raise ValueError("State device must have at least one state.")
|
|
66
|
+
|
|
67
|
+
self._state_to_label: dict[int, StateLabel] = states # type: ignore[assignment]
|
|
68
|
+
# reverse mapping for O(1) lookup
|
|
69
|
+
self._label_to_state: dict[str, int] = {lbl: p for p, lbl in states.items()}
|
|
70
|
+
|
|
71
|
+
self.register_standard_properties()
|
|
72
|
+
|
|
73
|
+
def register_standard_properties(self) -> None:
|
|
74
|
+
"""Inspect the class for standard properties and register them."""
|
|
75
|
+
states, labels = zip(*self._state_to_label.items())
|
|
76
|
+
cls = type(self)
|
|
77
|
+
self.register_property(
|
|
78
|
+
name=Keyword.State,
|
|
79
|
+
default_value=states[0],
|
|
80
|
+
allowed_values=states,
|
|
81
|
+
getter=cls.get_state,
|
|
82
|
+
setter=cls._set_state,
|
|
83
|
+
)
|
|
84
|
+
self.register_property(
|
|
85
|
+
name=Keyword.Label.value,
|
|
86
|
+
default_value=labels[0],
|
|
87
|
+
allowed_values=labels,
|
|
88
|
+
getter=cls._get_current_label,
|
|
89
|
+
setter=cls._set_current_label,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def set_position_or_label(self, pos_or_label: int | str) -> None:
|
|
93
|
+
"""Set the position of the device by index or label."""
|
|
94
|
+
if isinstance(pos_or_label, str):
|
|
95
|
+
label = pos_or_label
|
|
96
|
+
pos = self.get_position_for_label(pos_or_label)
|
|
97
|
+
else:
|
|
98
|
+
pos = int(pos_or_label)
|
|
99
|
+
label = self._state_to_label.get(pos, "")
|
|
100
|
+
if pos not in self._state_to_label:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"Position {pos} is not a valid state. "
|
|
103
|
+
f"Available states: {self._state_to_label.keys()}"
|
|
104
|
+
)
|
|
105
|
+
self.set_property_value(Keyword.State, pos) # will trigger set_state
|
|
106
|
+
self.set_property_value(Keyword.Label.value, label)
|
|
107
|
+
|
|
108
|
+
def assign_label_to_position(self, pos: int, label: str) -> None:
|
|
109
|
+
"""Assign a User-defined label to a position."""
|
|
110
|
+
if not isinstance(pos, int):
|
|
111
|
+
raise TypeError(f"Position must be an integer, got {type(pos).__name__}.")
|
|
112
|
+
|
|
113
|
+
# update internal state
|
|
114
|
+
self._state_to_label[pos] = label = cast("StateLabel", str(label))
|
|
115
|
+
self._label_to_state[label] = pos
|
|
116
|
+
self._update_allowed_labels()
|
|
117
|
+
|
|
118
|
+
def get_position_for_label(self, label: str) -> int:
|
|
119
|
+
"""Return the position corresponding to the provided label."""
|
|
120
|
+
if label not in self._label_to_state:
|
|
121
|
+
raise KeyError(
|
|
122
|
+
f"Label not defined: {label!r}. "
|
|
123
|
+
f"Available labels: {self._state_to_label.values()}"
|
|
124
|
+
)
|
|
125
|
+
return self._label_to_state[label]
|
|
126
|
+
|
|
127
|
+
# ------------------ private methods for internal use ------------------
|
|
128
|
+
|
|
129
|
+
def _update_allowed_labels(self) -> None:
|
|
130
|
+
"""Update the allowed values for the label property."""
|
|
131
|
+
label_prop_info = self.get_property_info(Keyword.Label)
|
|
132
|
+
label_prop_info.allowed_values = list(self._state_to_label.values())
|
|
133
|
+
|
|
134
|
+
def _set_state(self, state: int) -> None:
|
|
135
|
+
# internal method to set the state, called by the property setter
|
|
136
|
+
# to keep the label and state property in sync
|
|
137
|
+
self.set_state(state) # call the device-specific method
|
|
138
|
+
label = self._state_to_label.get(state, "")
|
|
139
|
+
self.set_property_value(Keyword.Label, label)
|
|
140
|
+
|
|
141
|
+
def _get_current_label(self) -> str:
|
|
142
|
+
# internal method to get the current label, called by the property getter
|
|
143
|
+
# to keep the label and state property in sync
|
|
144
|
+
pos = self.get_property_value(Keyword.State)
|
|
145
|
+
return self._state_to_label.get(pos, "")
|
|
146
|
+
|
|
147
|
+
def _set_current_label(self, label: str) -> None:
|
|
148
|
+
# internal method to set the label, called by the property setter
|
|
149
|
+
# to keep the label and state property in sync
|
|
150
|
+
pos = self._label_to_state.get(label)
|
|
151
|
+
if pos != self.get_property_value(Keyword.State):
|
|
152
|
+
self.set_property_value(Keyword.State, pos) # will trigger set_state
|