pymmcore-plus 0.15.0__py3-none-any.whl → 0.15.3__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/core/_constants.py +4 -0
- pymmcore_plus/core/_mmcore_plus.py +26 -16
- pymmcore_plus/core/_sequencing.py +1 -1
- pymmcore_plus/core/events/_deprecated.py +67 -0
- pymmcore_plus/core/events/_protocol.py +15 -5
- pymmcore_plus/core/events/_psygnal.py +33 -4
- pymmcore_plus/core/events/_qsignals.py +34 -6
- pymmcore_plus/experimental/unicore/__init__.py +7 -3
- pymmcore_plus/experimental/unicore/_device_manager.py +1 -1
- pymmcore_plus/experimental/unicore/core/_sequence_buffer.py +23 -27
- pymmcore_plus/experimental/unicore/core/_unicore.py +90 -23
- pymmcore_plus/experimental/unicore/devices/_camera.py +10 -5
- pymmcore_plus/experimental/unicore/devices/_generic_device.py +12 -0
- pymmcore_plus/experimental/unicore/devices/_properties.py +1 -1
- pymmcore_plus/experimental/unicore/devices/_shutter.py +30 -0
- pymmcore_plus/experimental/unicore/devices/_slm.py +1 -1
- pymmcore_plus/experimental/unicore/devices/_stage.py +1 -1
- pymmcore_plus/experimental/unicore/devices/_state.py +1 -1
- {pymmcore_plus-0.15.0.dist-info → pymmcore_plus-0.15.3.dist-info}/METADATA +2 -2
- {pymmcore_plus-0.15.0.dist-info → pymmcore_plus-0.15.3.dist-info}/RECORD +26 -22
- /pymmcore_plus/experimental/unicore/devices/{_device.py → _device_base.py} +0 -0
- {pymmcore_plus-0.15.0.dist-info → pymmcore_plus-0.15.3.dist-info}/WHEEL +0 -0
- {pymmcore_plus-0.15.0.dist-info → pymmcore_plus-0.15.3.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.15.0.dist-info → pymmcore_plus-0.15.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
1
3
|
from psygnal import Signal, SignalGroup, SignalInstance
|
|
2
4
|
|
|
3
5
|
from pymmcore_plus.mda import MDAEngine
|
|
4
6
|
|
|
7
|
+
from ._deprecated import DeprecatedSignalProxy
|
|
5
8
|
from ._prop_event_mixin import _DevicePropertyEventMixin
|
|
6
9
|
|
|
7
10
|
|
|
@@ -22,14 +25,19 @@ class CMMCoreSignaler(SignalGroup, _DevicePropertyEventMixin):
|
|
|
22
25
|
exposureChanged = Signal(str, float)
|
|
23
26
|
SLMExposureChanged = Signal(str, float)
|
|
24
27
|
|
|
28
|
+
# https://github.com/micro-manager/mmCoreAndDevices/pull/659
|
|
29
|
+
imageSnapped = Signal(str) # on snapImage()
|
|
30
|
+
# when (Continuous)SequenceAcquisition is stopped
|
|
31
|
+
sequenceAcquisitionStopped = Signal(str)
|
|
32
|
+
if TYPE_CHECKING: # see deprecated impl below
|
|
33
|
+
sequenceAcquisitionStarted = Signal(str)
|
|
34
|
+
|
|
25
35
|
# added for CMMCorePlus
|
|
26
|
-
imageSnapped = Signal() # whenever snapImage is called
|
|
27
36
|
mdaEngineRegistered = Signal(MDAEngine, MDAEngine)
|
|
28
37
|
continuousSequenceAcquisitionStarting = Signal()
|
|
29
38
|
continuousSequenceAcquisitionStarted = Signal()
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
sequenceAcquisitionStopped = Signal(str)
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
sequenceAcquisitionStarting = Signal(str)
|
|
33
41
|
autoShutterSet = Signal(bool)
|
|
34
42
|
configGroupDeleted = Signal(str)
|
|
35
43
|
configDeleted = Signal(str, str)
|
|
@@ -44,3 +52,24 @@ class CMMCoreSignaler(SignalGroup, _DevicePropertyEventMixin):
|
|
|
44
52
|
@property
|
|
45
53
|
def sLMExposureChanged(self) -> SignalInstance:
|
|
46
54
|
return self.SLMExposureChanged
|
|
55
|
+
|
|
56
|
+
if not TYPE_CHECKING:
|
|
57
|
+
_sequenceAcquisitionStarting = Signal(str)
|
|
58
|
+
_sequenceAcquisitionStarted = Signal(str)
|
|
59
|
+
|
|
60
|
+
# Deprecated signal wrappers for backwards compatibility
|
|
61
|
+
@property
|
|
62
|
+
def sequenceAcquisitionStarting(self) -> SignalInstance:
|
|
63
|
+
return DeprecatedSignalProxy(
|
|
64
|
+
self._sequenceAcquisitionStarting,
|
|
65
|
+
current_n_args=1,
|
|
66
|
+
deprecated_posargs=(-1, 0, False),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def sequenceAcquisitionStarted(self) -> SignalInstance:
|
|
71
|
+
return DeprecatedSignalProxy(
|
|
72
|
+
self._sequenceAcquisitionStarted,
|
|
73
|
+
current_n_args=1,
|
|
74
|
+
deprecated_posargs=(-1, 0, False),
|
|
75
|
+
)
|
|
@@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Optional
|
|
|
2
2
|
|
|
3
3
|
from qtpy.QtCore import QObject, Signal
|
|
4
4
|
|
|
5
|
+
from ._deprecated import DeprecatedSignalProxy
|
|
5
6
|
from ._prop_event_mixin import _PropertySignal
|
|
6
7
|
|
|
7
8
|
if TYPE_CHECKING:
|
|
@@ -25,17 +26,23 @@ class QCoreSignaler(QObject):
|
|
|
25
26
|
SLMExposureChanged = Signal(str, float)
|
|
26
27
|
sLMExposureChanged = SLMExposureChanged # alias
|
|
27
28
|
|
|
29
|
+
# https://github.com/micro-manager/mmCoreAndDevices/pull/659
|
|
30
|
+
imageSnapped = Signal(str) # on snapImage()
|
|
31
|
+
# when (Continuous)SequenceAcquisition is stopped
|
|
32
|
+
sequenceAcquisitionStopped = Signal(str)
|
|
33
|
+
if TYPE_CHECKING: # see deprecated impl below
|
|
34
|
+
sequenceAcquisitionStarted = Signal(str)
|
|
35
|
+
|
|
28
36
|
# added for CMMCorePlus
|
|
29
|
-
imageSnapped = Signal() # on snapImage()
|
|
30
37
|
mdaEngineRegistered = Signal(object, object) # new engine, old engine
|
|
31
38
|
# when continuousSequenceAcquisition is started
|
|
32
39
|
continuousSequenceAcquisitionStarting = Signal()
|
|
33
40
|
continuousSequenceAcquisitionStarted = Signal()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
# when SequenceAcquisition is started
|
|
44
|
+
sequenceAcquisitionStarting = Signal(str)
|
|
45
|
+
|
|
39
46
|
autoShutterSet = Signal(bool)
|
|
40
47
|
configGroupDeleted = Signal(str)
|
|
41
48
|
configDeleted = Signal(str, str)
|
|
@@ -77,3 +84,24 @@ class QCoreSignaler(QObject):
|
|
|
77
84
|
"""
|
|
78
85
|
# type ignored: can't use _DevicePropertyEventMixin due to metaclass conflict
|
|
79
86
|
return _PropertySignal(self, device, property)
|
|
87
|
+
|
|
88
|
+
if not TYPE_CHECKING:
|
|
89
|
+
_sequenceAcquisitionStarting = Signal(str)
|
|
90
|
+
_sequenceAcquisitionStarted = Signal(str)
|
|
91
|
+
|
|
92
|
+
# Deprecated signal wrappers for backwards compatibility
|
|
93
|
+
@property
|
|
94
|
+
def sequenceAcquisitionStarting(self):
|
|
95
|
+
return DeprecatedSignalProxy(
|
|
96
|
+
self._sequenceAcquisitionStarting,
|
|
97
|
+
current_n_args=1,
|
|
98
|
+
deprecated_posargs=(-1, 0, False),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def sequenceAcquisitionStarted(self):
|
|
103
|
+
return DeprecatedSignalProxy(
|
|
104
|
+
self._sequenceAcquisitionStarted,
|
|
105
|
+
current_n_args=1,
|
|
106
|
+
deprecated_posargs=(-1, 0, False),
|
|
107
|
+
)
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
from .core._unicore import UniMMCore
|
|
2
|
-
from .devices._camera import
|
|
3
|
-
from .devices.
|
|
2
|
+
from .devices._camera import CameraDevice
|
|
3
|
+
from .devices._device_base import Device
|
|
4
|
+
from .devices._generic_device import GenericDevice
|
|
4
5
|
from .devices._properties import PropertyInfo, pymm_property
|
|
6
|
+
from .devices._shutter import ShutterDevice
|
|
5
7
|
from .devices._slm import SLMDevice
|
|
6
8
|
from .devices._stage import StageDevice, XYStageDevice, XYStepperStageDevice
|
|
7
9
|
from .devices._state import StateDevice
|
|
8
10
|
|
|
9
11
|
__all__ = [
|
|
10
|
-
"
|
|
12
|
+
"CameraDevice",
|
|
11
13
|
"Device",
|
|
14
|
+
"GenericDevice",
|
|
12
15
|
"PropertyInfo",
|
|
13
16
|
"SLMDevice",
|
|
17
|
+
"ShutterDevice",
|
|
14
18
|
"StageDevice",
|
|
15
19
|
"StateDevice",
|
|
16
20
|
"UniMMCore",
|
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, TypeVar, cast
|
|
|
7
7
|
|
|
8
8
|
from pymmcore_plus.core._constants import DeviceInitializationState, DeviceType
|
|
9
9
|
|
|
10
|
-
from .devices.
|
|
10
|
+
from .devices._device_base import Device
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from collections.abc import Iterator
|
|
@@ -22,6 +22,11 @@ class BufferSlot(NamedTuple):
|
|
|
22
22
|
nbytes_total: int # full span in the pool (data + padding)
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
# TODO: version that doesn't use contiguous memory,
|
|
26
|
+
# but rather uses a deuqe of numpy arrays.
|
|
27
|
+
class SequenceStack: ...
|
|
28
|
+
|
|
29
|
+
|
|
25
30
|
class SequenceBuffer:
|
|
26
31
|
"""A lock-protected circular buffer backed by a single numpy byte array.
|
|
27
32
|
|
|
@@ -55,7 +60,7 @@ class SequenceBuffer:
|
|
|
55
60
|
self._overflow_occurred: bool = False
|
|
56
61
|
|
|
57
62
|
self._lock = threading.Lock() # not re-entrant, but slightly faster than RLock
|
|
58
|
-
self._pending_slot: tuple[NDArray, int]
|
|
63
|
+
self._pending_slot: deque[tuple[NDArray, int]] = deque()
|
|
59
64
|
|
|
60
65
|
# ---------------------------------------------------------------------
|
|
61
66
|
# Producer API - acquire a slot, fill it, finalize it
|
|
@@ -82,10 +87,6 @@ class SequenceBuffer:
|
|
|
82
87
|
# --- reserve space -------------------------------------------------
|
|
83
88
|
|
|
84
89
|
with self._lock:
|
|
85
|
-
if self._pending_slot is not None:
|
|
86
|
-
msg = "Cannot acquire a new slot before finalizing the pending one."
|
|
87
|
-
raise RuntimeError(msg)
|
|
88
|
-
|
|
89
90
|
# Calculate padding needed to align start address to dtype boundary
|
|
90
91
|
align_pad = (-self._head) % dtype_.itemsize
|
|
91
92
|
needed = nbytes_data + align_pad
|
|
@@ -112,7 +113,7 @@ class SequenceBuffer:
|
|
|
112
113
|
arr: NDArray[Any] = np.ndarray(
|
|
113
114
|
shape, dtype_, buffer=self._pool, offset=start
|
|
114
115
|
)
|
|
115
|
-
self._pending_slot
|
|
116
|
+
self._pending_slot.append((arr, needed))
|
|
116
117
|
return arr
|
|
117
118
|
|
|
118
119
|
def finalize_slot(self, metadata: Mapping[str, Any] | None = None) -> None:
|
|
@@ -123,12 +124,11 @@ class SequenceBuffer:
|
|
|
123
124
|
slot's metadata dictionary.
|
|
124
125
|
"""
|
|
125
126
|
with self._lock:
|
|
126
|
-
if
|
|
127
|
+
if not self._pending_slot:
|
|
127
128
|
msg = "No pending slot to finalize"
|
|
128
129
|
raise RuntimeError(msg)
|
|
129
130
|
|
|
130
|
-
self._pending_slot
|
|
131
|
-
arr, nbytes_total = slot
|
|
131
|
+
arr, nbytes_total = self._pending_slot.popleft()
|
|
132
132
|
self._slots.append(BufferSlot(arr, metadata, nbytes_total))
|
|
133
133
|
|
|
134
134
|
# Convenience: copy-in one-shot insert ------------------------------
|
|
@@ -153,7 +153,7 @@ class SequenceBuffer:
|
|
|
153
153
|
# ------------------------------------------------------------------
|
|
154
154
|
|
|
155
155
|
def pop_next(
|
|
156
|
-
self, *,
|
|
156
|
+
self, *, out: np.ndarray | None = None
|
|
157
157
|
) -> tuple[NDArray[Any], Mapping[str, Any]] | None:
|
|
158
158
|
"""Remove and return the oldest frame.
|
|
159
159
|
|
|
@@ -165,27 +165,25 @@ class SequenceBuffer:
|
|
|
165
165
|
if not self._slots:
|
|
166
166
|
return None
|
|
167
167
|
slot = self._slots.popleft()
|
|
168
|
-
self._evict_slot(slot)
|
|
169
168
|
|
|
170
|
-
if
|
|
171
|
-
|
|
169
|
+
if out is not None:
|
|
170
|
+
out[:] = slot.array
|
|
171
|
+
arr = out
|
|
172
172
|
else:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
arr = slot.array.view()
|
|
176
|
-
arr.flags.writeable = False
|
|
173
|
+
arr = slot.array.copy()
|
|
174
|
+
self._evict_slot(slot)
|
|
177
175
|
|
|
178
176
|
# return actual metadata, we're done with it.
|
|
179
177
|
return arr, (slot.metadata or {})
|
|
180
178
|
|
|
181
179
|
def peek_last(
|
|
182
|
-
self, *,
|
|
180
|
+
self, *, out: np.ndarray | None = None
|
|
183
181
|
) -> tuple[NDArray[Any], Mapping[str, Any]] | None:
|
|
184
182
|
"""Return the newest frame without removing it."""
|
|
185
|
-
return self.peek_nth_from_last(0,
|
|
183
|
+
return self.peek_nth_from_last(0, out=out)
|
|
186
184
|
|
|
187
185
|
def peek_nth_from_last(
|
|
188
|
-
self, n: int, *,
|
|
186
|
+
self, n: int, *, out: np.ndarray | None = None
|
|
189
187
|
) -> tuple[NDArray[Any], dict[str, Any]] | None:
|
|
190
188
|
"""Return the n-th most recent frame without removing it.
|
|
191
189
|
|
|
@@ -195,13 +193,11 @@ class SequenceBuffer:
|
|
|
195
193
|
if n < 0 or n >= len(self._slots):
|
|
196
194
|
return None
|
|
197
195
|
slot = self._slots[-(n + 1)]
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
arr =
|
|
196
|
+
if out is not None:
|
|
197
|
+
out[:] = slot.array
|
|
198
|
+
arr = out
|
|
201
199
|
else:
|
|
202
|
-
|
|
203
|
-
arr = slot.array.view()
|
|
204
|
-
arr.flags.writeable = False
|
|
200
|
+
arr = slot.array.copy()
|
|
205
201
|
|
|
206
202
|
# Return a copy of the metadata to avoid external modification
|
|
207
203
|
return arr, (dict(slot.metadata) if slot.metadata else {})
|
|
@@ -213,7 +209,7 @@ class SequenceBuffer:
|
|
|
213
209
|
def clear(self) -> None:
|
|
214
210
|
with self._lock:
|
|
215
211
|
self._slots.clear()
|
|
216
|
-
self._pending_slot
|
|
212
|
+
self._pending_slot.clear()
|
|
217
213
|
self._head = self._tail = self._bytes_in_use = 0
|
|
218
214
|
self._overflow_occurred = False
|
|
219
215
|
|
|
@@ -25,8 +25,9 @@ from pymmcore_plus.core import Keyword as KW
|
|
|
25
25
|
from pymmcore_plus.core._constants import PixelType
|
|
26
26
|
from pymmcore_plus.experimental.unicore._device_manager import PyDeviceManager
|
|
27
27
|
from pymmcore_plus.experimental.unicore._proxy import create_core_proxy
|
|
28
|
-
from pymmcore_plus.experimental.unicore.devices._camera import
|
|
29
|
-
from pymmcore_plus.experimental.unicore.devices.
|
|
28
|
+
from pymmcore_plus.experimental.unicore.devices._camera import CameraDevice
|
|
29
|
+
from pymmcore_plus.experimental.unicore.devices._device_base import Device
|
|
30
|
+
from pymmcore_plus.experimental.unicore.devices._shutter import ShutterDevice
|
|
30
31
|
from pymmcore_plus.experimental.unicore.devices._slm import SLMDevice
|
|
31
32
|
from pymmcore_plus.experimental.unicore.devices._stage import XYStageDevice, _BaseStage
|
|
32
33
|
from pymmcore_plus.experimental.unicore.devices._state import StateDevice
|
|
@@ -678,11 +679,11 @@ class UniMMCore(CMMCorePlus):
|
|
|
678
679
|
|
|
679
680
|
# --------------------------------------------------------------------- utils
|
|
680
681
|
|
|
681
|
-
def _py_camera(self, cameraLabel: str | None = None) ->
|
|
682
|
+
def _py_camera(self, cameraLabel: str | None = None) -> CameraDevice | None:
|
|
682
683
|
"""Return the *Python* Camera for ``label`` (or current), else ``None``."""
|
|
683
684
|
label = cameraLabel or self.getCameraDevice()
|
|
684
685
|
if label in self._pydevices:
|
|
685
|
-
return self._pydevices.get_device_of_type(label,
|
|
686
|
+
return self._pydevices.get_device_of_type(label, CameraDevice)
|
|
686
687
|
return None
|
|
687
688
|
|
|
688
689
|
def setCameraDevice(self, cameraLabel: DeviceLabel | str) -> None:
|
|
@@ -751,7 +752,7 @@ class UniMMCore(CMMCorePlus):
|
|
|
751
752
|
# ---------------------------------------------------------------- sequence common
|
|
752
753
|
|
|
753
754
|
def _start_sequence(
|
|
754
|
-
self, cam:
|
|
755
|
+
self, cam: CameraDevice, n_images: int | None, stop_on_overflow: bool
|
|
755
756
|
) -> None:
|
|
756
757
|
"""Initialise _seq state and call cam.start_sequence."""
|
|
757
758
|
shape, dtype = cam.shape(), np.dtype(cam.dtype())
|
|
@@ -786,13 +787,15 @@ class UniMMCore(CMMCorePlus):
|
|
|
786
787
|
img_number = next(counter)
|
|
787
788
|
elapsed_ms = (perf_counter_ns() - start_time) / 1e6
|
|
788
789
|
received = datetime.now().isoformat(sep=" ")
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
790
|
+
self._seq_buffer.finalize_slot(
|
|
791
|
+
{
|
|
792
|
+
**base_meta,
|
|
793
|
+
**cam_meta,
|
|
794
|
+
KW.Metadata_TimeInCore: received,
|
|
795
|
+
KW.Metadata_ImageNumber: str(img_number),
|
|
796
|
+
KW.Elapsed_Time_ms: f"{elapsed_ms:.2f}",
|
|
797
|
+
}
|
|
798
|
+
)
|
|
796
799
|
|
|
797
800
|
# Auto-stop when we've acquired the requested number of images
|
|
798
801
|
if n_images is not None and (img_number + 1) >= n_images:
|
|
@@ -808,8 +811,7 @@ class UniMMCore(CMMCorePlus):
|
|
|
808
811
|
|
|
809
812
|
self._acquisition_thread = AcquisitionThread(
|
|
810
813
|
image_generator=cam.start_sequence(
|
|
811
|
-
n_images
|
|
812
|
-
get_buffer_with_overflow_handling,
|
|
814
|
+
n_images, get_buffer_with_overflow_handling
|
|
813
815
|
),
|
|
814
816
|
finalize=finalize_with_metadata,
|
|
815
817
|
label=camera_label,
|
|
@@ -823,6 +825,7 @@ class UniMMCore(CMMCorePlus):
|
|
|
823
825
|
|
|
824
826
|
# ------------------------------------------------- startSequenceAcquisition
|
|
825
827
|
|
|
828
|
+
# startSequenceAcquisition
|
|
826
829
|
def _do_start_sequence_acquisition(
|
|
827
830
|
self, cameraLabel: str, numImages: int, intervalMs: float, stopOnOverflow: bool
|
|
828
831
|
) -> None:
|
|
@@ -835,6 +838,7 @@ class UniMMCore(CMMCorePlus):
|
|
|
835
838
|
|
|
836
839
|
# ------------------------------------------------- continuous acquisition
|
|
837
840
|
|
|
841
|
+
# startContinuousSequenceAcquisition
|
|
838
842
|
def _do_start_continuous_sequence_acquisition(self, intervalMs: float = 0) -> None:
|
|
839
843
|
if (cam := self._py_camera()) is None: # pragma: no cover
|
|
840
844
|
return pymmcore.CMMCore.startContinuousSequenceAcquisition(self, intervalMs)
|
|
@@ -879,27 +883,41 @@ class UniMMCore(CMMCorePlus):
|
|
|
879
883
|
|
|
880
884
|
# ---------------------------------------------------- getImages
|
|
881
885
|
|
|
882
|
-
def getLastImage(self) -> np.ndarray:
|
|
886
|
+
def getLastImage(self, *, out: np.ndarray | None = None) -> np.ndarray:
|
|
883
887
|
if self._py_camera() is None:
|
|
884
888
|
return super().getLastImage()
|
|
885
|
-
if
|
|
889
|
+
if (
|
|
890
|
+
not (self._seq_buffer)
|
|
891
|
+
or (result := self._seq_buffer.peek_last(out=out)) is None
|
|
892
|
+
):
|
|
886
893
|
raise IndexError("Circular buffer is empty.")
|
|
887
894
|
return result[0]
|
|
888
895
|
|
|
889
896
|
@overload
|
|
890
897
|
def getLastImageMD(
|
|
891
|
-
self,
|
|
898
|
+
self,
|
|
899
|
+
channel: int,
|
|
900
|
+
slice: int,
|
|
901
|
+
md: pymmcore.Metadata,
|
|
902
|
+
/,
|
|
903
|
+
*,
|
|
904
|
+
out: np.ndarray | None = None,
|
|
892
905
|
) -> np.ndarray: ...
|
|
893
906
|
@overload
|
|
894
|
-
def getLastImageMD(
|
|
895
|
-
|
|
907
|
+
def getLastImageMD(
|
|
908
|
+
self, md: pymmcore.Metadata, /, *, out: np.ndarray | None = None
|
|
909
|
+
) -> np.ndarray: ...
|
|
910
|
+
def getLastImageMD(self, *args: Any, out: np.ndarray | None = None) -> np.ndarray:
|
|
896
911
|
if self._py_camera() is None:
|
|
897
912
|
return super().getLastImageMD(*args)
|
|
898
913
|
md_object = args[0] if len(args) == 1 else args[-1]
|
|
899
914
|
if not isinstance(md_object, pymmcore.Metadata): # pragma: no cover
|
|
900
915
|
raise TypeError("Expected a Metadata object for the last argument.")
|
|
901
916
|
|
|
902
|
-
if
|
|
917
|
+
if (
|
|
918
|
+
not (self._seq_buffer)
|
|
919
|
+
or (result := self._seq_buffer.peek_last(out=out)) is None
|
|
920
|
+
):
|
|
903
921
|
raise IndexError("Circular buffer is empty.")
|
|
904
922
|
|
|
905
923
|
img, md = result
|
|
@@ -910,13 +928,20 @@ class UniMMCore(CMMCorePlus):
|
|
|
910
928
|
|
|
911
929
|
return img
|
|
912
930
|
|
|
913
|
-
def getNBeforeLastImageMD(
|
|
931
|
+
def getNBeforeLastImageMD(
|
|
932
|
+
self,
|
|
933
|
+
n: int,
|
|
934
|
+
md: pymmcore.Metadata,
|
|
935
|
+
/,
|
|
936
|
+
*,
|
|
937
|
+
out: np.ndarray | None = None,
|
|
938
|
+
) -> np.ndarray:
|
|
914
939
|
if self._py_camera() is None:
|
|
915
940
|
return super().getNBeforeLastImageMD(n, md)
|
|
916
941
|
|
|
917
942
|
if (
|
|
918
943
|
not (self._seq_buffer)
|
|
919
|
-
or (result := self._seq_buffer.peek_nth_from_last(n)) is None
|
|
944
|
+
or (result := self._seq_buffer.peek_nth_from_last(n, out=out)) is None
|
|
920
945
|
):
|
|
921
946
|
raise IndexError("Circular buffer is empty or n is out of range.")
|
|
922
947
|
|
|
@@ -1007,7 +1032,7 @@ class UniMMCore(CMMCorePlus):
|
|
|
1007
1032
|
|
|
1008
1033
|
return self._seq_buffer.size_bytes // bytes_per_frame
|
|
1009
1034
|
|
|
1010
|
-
def _predicted_bytes_per_frame(self, cam:
|
|
1035
|
+
def _predicted_bytes_per_frame(self, cam: CameraDevice) -> int:
|
|
1011
1036
|
# Estimate capacity based on camera settings and circular buffer size
|
|
1012
1037
|
shape, dtype = cam.shape(), np.dtype(cam.dtype())
|
|
1013
1038
|
return int(np.prod(shape) * dtype.itemsize)
|
|
@@ -1593,6 +1618,48 @@ class UniMMCore(CMMCorePlus):
|
|
|
1593
1618
|
except KeyError as e:
|
|
1594
1619
|
raise RuntimeError(str(e)) from e # convert to RuntimeError
|
|
1595
1620
|
|
|
1621
|
+
# ########################################################################
|
|
1622
|
+
# ------------------------ Shutter Device Methods ------------------------
|
|
1623
|
+
# ########################################################################
|
|
1624
|
+
|
|
1625
|
+
def _py_shutter(self, shutterLabel: str | None = None) -> ShutterDevice | None:
|
|
1626
|
+
"""Return the *Python* Shutter device for ``label``, else ``None``."""
|
|
1627
|
+
label = shutterLabel or self.getShutterDevice()
|
|
1628
|
+
if label in self._pydevices:
|
|
1629
|
+
return self._pydevices.get_device_of_type(label, ShutterDevice)
|
|
1630
|
+
return None
|
|
1631
|
+
|
|
1632
|
+
def setShutterDevice(self, shutterLabel: DeviceLabel | str) -> None:
|
|
1633
|
+
label = self._set_current_if_pydevice(KW.CoreShutter, shutterLabel)
|
|
1634
|
+
super().setShutterDevice(label)
|
|
1635
|
+
|
|
1636
|
+
def getShutterDevice(self) -> DeviceLabel | Literal[""]:
|
|
1637
|
+
"""Returns the label of the currently selected Shutter device.
|
|
1638
|
+
|
|
1639
|
+
Returns empty string if no Shutter device is selected.
|
|
1640
|
+
"""
|
|
1641
|
+
return self._pycore.current(KW.CoreShutter) or super().getShutterDevice()
|
|
1642
|
+
|
|
1643
|
+
@overload
|
|
1644
|
+
def getShutterOpen(self) -> bool: ...
|
|
1645
|
+
@overload
|
|
1646
|
+
def getShutterOpen(self, shutterLabel: DeviceLabel | str) -> bool: ...
|
|
1647
|
+
def getShutterOpen(self, shutterLabel: DeviceLabel | str | None = None) -> bool:
|
|
1648
|
+
shutterLabel = shutterLabel or self.getShutterDevice()
|
|
1649
|
+
if (shutter := self._py_shutter(shutterLabel)) is None:
|
|
1650
|
+
return super().getShutterOpen(shutterLabel)
|
|
1651
|
+
|
|
1652
|
+
with shutter:
|
|
1653
|
+
return shutter.get_open()
|
|
1654
|
+
|
|
1655
|
+
def _do_shutter_open(self, shutterLabel: str, state: bool, /) -> None:
|
|
1656
|
+
"""Open or close the shutter."""
|
|
1657
|
+
if (shutter := self._py_shutter(shutterLabel)) is None: # pragma: no cover
|
|
1658
|
+
return pymmcore.CMMCore.setShutterOpen(self, shutterLabel, state)
|
|
1659
|
+
|
|
1660
|
+
with shutter:
|
|
1661
|
+
shutter.set_open(state)
|
|
1662
|
+
|
|
1596
1663
|
|
|
1597
1664
|
# -------------------------------------------------------------------------------
|
|
1598
1665
|
|
|
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Callable, ClassVar, Literal
|
|
|
6
6
|
|
|
7
7
|
from pymmcore_plus.core._constants import DeviceType, Keyword, PixelFormat
|
|
8
8
|
|
|
9
|
-
from .
|
|
9
|
+
from ._device_base import Device
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
from collections.abc import Iterator, Mapping, Sequence
|
|
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
|
|
15
15
|
from numpy.typing import DTypeLike
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class
|
|
18
|
+
class CameraDevice(Device):
|
|
19
19
|
# mandatory methods for Camera device adapters
|
|
20
20
|
|
|
21
21
|
_TYPE: ClassVar[Literal[DeviceType.Camera]] = DeviceType.Camera
|
|
@@ -46,7 +46,7 @@ class Camera(Device):
|
|
|
46
46
|
@abstractmethod
|
|
47
47
|
def start_sequence(
|
|
48
48
|
self,
|
|
49
|
-
n: int,
|
|
49
|
+
n: int | None,
|
|
50
50
|
get_buffer: Callable[[Sequence[int], DTypeLike], np.ndarray],
|
|
51
51
|
) -> Iterator[Mapping]:
|
|
52
52
|
"""Start a sequence acquisition.
|
|
@@ -60,8 +60,9 @@ class Camera(Device):
|
|
|
60
60
|
|
|
61
61
|
Parameters
|
|
62
62
|
----------
|
|
63
|
-
n : int
|
|
64
|
-
|
|
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.
|
|
65
66
|
get_buffer : Callable[[Sequence[int], DTypeLike], np.ndarray]
|
|
66
67
|
A callable that returns a buffer for the camera to fill with image data.
|
|
67
68
|
You should call this with the shape of the image and the dtype
|
|
@@ -76,6 +77,10 @@ class Camera(Device):
|
|
|
76
77
|
"""
|
|
77
78
|
# EXAMPLE USAGE:
|
|
78
79
|
# shape, dtype = self.shape(), self.dtype()
|
|
80
|
+
# if n is None: # acquire indefinitely until stopped
|
|
81
|
+
# while True:
|
|
82
|
+
# yield ...
|
|
83
|
+
# return
|
|
79
84
|
# for _ in range(n):
|
|
80
85
|
# image = get_buffer(shape, dtype)
|
|
81
86
|
# get the image from the camera, and fill the buffer in place
|
|
@@ -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
|
|
@@ -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
|
+
"""
|
|
@@ -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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pymmcore-plus
|
|
3
|
-
Version: 0.15.
|
|
3
|
+
Version: 0.15.3
|
|
4
4
|
Summary: pymmcore superset providing improved APIs, event handling, and a pure python acquisition engine
|
|
5
5
|
Project-URL: Source, https://github.com/pymmcore-plus/pymmcore-plus
|
|
6
6
|
Project-URL: Tracker, https://github.com/pymmcore-plus/pymmcore-plus/issues
|
|
@@ -30,7 +30,7 @@ Requires-Dist: numpy>=1.26.0; python_version >= '3.12'
|
|
|
30
30
|
Requires-Dist: numpy>=2.1.0; python_version >= '3.13'
|
|
31
31
|
Requires-Dist: platformdirs>=3.0.0
|
|
32
32
|
Requires-Dist: psygnal>=0.10
|
|
33
|
-
Requires-Dist: pymmcore>=11.
|
|
33
|
+
Requires-Dist: pymmcore>=11.9.0.73.0
|
|
34
34
|
Requires-Dist: rich>=10.2.0
|
|
35
35
|
Requires-Dist: tensorstore!=0.1.72,>=0.1.67
|
|
36
36
|
Requires-Dist: tensorstore!=0.1.72,>=0.1.71; python_version >= '3.13'
|