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.
- pymmcore_plus/__init__.py +7 -4
- pymmcore_plus/_benchmark.py +203 -0
- pymmcore_plus/_build.py +6 -1
- pymmcore_plus/_cli.py +131 -31
- pymmcore_plus/_logger.py +19 -10
- pymmcore_plus/_pymmcore.py +12 -0
- pymmcore_plus/_util.py +139 -32
- pymmcore_plus/core/__init__.py +5 -0
- pymmcore_plus/core/_config.py +6 -4
- pymmcore_plus/core/_config_group.py +4 -3
- pymmcore_plus/core/_constants.py +135 -10
- pymmcore_plus/core/_device.py +4 -4
- pymmcore_plus/core/_metadata.py +3 -3
- pymmcore_plus/core/_mmcore_plus.py +254 -170
- pymmcore_plus/core/_property.py +6 -6
- pymmcore_plus/core/_sequencing.py +370 -233
- pymmcore_plus/core/events/__init__.py +6 -6
- pymmcore_plus/core/events/_device_signal_view.py +8 -6
- pymmcore_plus/core/events/_norm_slot.py +2 -4
- pymmcore_plus/core/events/_prop_event_mixin.py +7 -4
- pymmcore_plus/core/events/_protocol.py +5 -2
- pymmcore_plus/core/events/_psygnal.py +2 -2
- 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 +16 -11
- pymmcore_plus/mda/__init__.py +1 -1
- pymmcore_plus/mda/_engine.py +320 -148
- pymmcore_plus/mda/_protocol.py +6 -4
- pymmcore_plus/mda/_runner.py +62 -51
- pymmcore_plus/mda/_thread_relay.py +5 -3
- pymmcore_plus/mda/events/__init__.py +2 -2
- pymmcore_plus/mda/events/_protocol.py +10 -2
- pymmcore_plus/mda/events/_psygnal.py +2 -2
- pymmcore_plus/mda/handlers/_5d_writer_base.py +106 -15
- pymmcore_plus/mda/handlers/__init__.py +7 -1
- pymmcore_plus/mda/handlers/_img_sequence_writer.py +11 -6
- pymmcore_plus/mda/handlers/_ome_tiff_writer.py +8 -4
- pymmcore_plus/mda/handlers/_ome_zarr_writer.py +82 -9
- pymmcore_plus/mda/handlers/_tensorstore_handler.py +374 -0
- pymmcore_plus/mda/handlers/_util.py +1 -1
- pymmcore_plus/metadata/__init__.py +36 -0
- pymmcore_plus/metadata/functions.py +353 -0
- pymmcore_plus/metadata/schema.py +472 -0
- pymmcore_plus/metadata/serialize.py +120 -0
- pymmcore_plus/mocks.py +51 -0
- pymmcore_plus/model/_config_file.py +5 -6
- pymmcore_plus/model/_config_group.py +29 -2
- pymmcore_plus/model/_core_device.py +12 -1
- pymmcore_plus/model/_core_link.py +2 -1
- pymmcore_plus/model/_device.py +39 -8
- pymmcore_plus/model/_microscope.py +39 -3
- pymmcore_plus/model/_pixel_size_config.py +27 -4
- pymmcore_plus/model/_property.py +13 -3
- pymmcore_plus/seq_tester.py +1 -1
- {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/METADATA +22 -12
- pymmcore_plus-0.13.0.dist-info/RECORD +71 -0
- {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/WHEEL +1 -1
- pymmcore_plus/core/_state.py +0 -244
- pymmcore_plus-0.9.3.dist-info/RECORD +0 -55
- {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from typing import ClassVar, Literal
|
|
3
|
+
|
|
4
|
+
from pymmcore_plus.core import DeviceType
|
|
5
|
+
from pymmcore_plus.core._constants import Keyword
|
|
6
|
+
|
|
7
|
+
from ._device import SeqT, SequenceableDevice
|
|
8
|
+
|
|
9
|
+
__all__ = ["_BaseStage"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _BaseStage(SequenceableDevice[SeqT]):
|
|
13
|
+
"""Shared logic for Stage and XYStage devices."""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def home(self) -> None:
|
|
17
|
+
"""Move the stage to its home position."""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def stop(self) -> None:
|
|
21
|
+
"""Stop the stage."""
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def set_origin(self) -> None:
|
|
25
|
+
"""Zero the stage's coordinates at the current position."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StageDevice(_BaseStage[float]):
|
|
29
|
+
"""ABC for Stage devices."""
|
|
30
|
+
|
|
31
|
+
_TYPE: ClassVar[Literal[DeviceType.Stage]] = DeviceType.Stage
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def set_position_um(self, val: float) -> None:
|
|
35
|
+
"""Set the position of the stage in microns."""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def get_position_um(self) -> float:
|
|
39
|
+
"""Returns the current position of the stage in microns."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# TODO: consider if we can just subclass StageDevice instead of _BaseStage
|
|
43
|
+
class XYStageDevice(_BaseStage[tuple[float, float]]):
|
|
44
|
+
"""ABC for XYStage devices."""
|
|
45
|
+
|
|
46
|
+
_TYPE: ClassVar[Literal[DeviceType.XYStage]] = DeviceType.XYStage
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def set_position_um(self, x: float, y: float) -> None:
|
|
50
|
+
"""Set the position of the XY stage in microns."""
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def get_position_um(self) -> tuple[float, float]:
|
|
54
|
+
"""Returns the current position of the XY stage in microns."""
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def set_origin_x(self) -> None:
|
|
58
|
+
"""Zero the stage's X coordinates at the current position."""
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def set_origin_y(self) -> None:
|
|
62
|
+
"""Zero the stage's Y coordinates at the current position."""
|
|
63
|
+
|
|
64
|
+
# ----------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def set_relative_position_um(self, dx: float, dy: float) -> None:
|
|
67
|
+
"""Move the stage by a relative amount.
|
|
68
|
+
|
|
69
|
+
Can be overridden for more efficient implementations.
|
|
70
|
+
"""
|
|
71
|
+
x, y = self.get_position_um()
|
|
72
|
+
self.set_position_um(x + dx, y + dy)
|
|
73
|
+
|
|
74
|
+
def set_adapter_origin_um(self, x: float, y: float) -> None:
|
|
75
|
+
"""Alter the software coordinate translation between micrometers and steps.
|
|
76
|
+
|
|
77
|
+
... such that the current position becomes the given coordinates.
|
|
78
|
+
"""
|
|
79
|
+
# I don't quite understand what this method is supposed to do yet.
|
|
80
|
+
# I believe it's here to give device adapter implementations a way to to set
|
|
81
|
+
# the origin of some translation between micrometers and steps, rather than to
|
|
82
|
+
# directly update the origin on the device itself.
|
|
83
|
+
|
|
84
|
+
def set_origin(self) -> None:
|
|
85
|
+
"""Zero the stage's coordinates at the current position.
|
|
86
|
+
|
|
87
|
+
This is a convenience method that calls `set_origin_x` and `set_origin_y`.
|
|
88
|
+
Can be overridden for more efficient implementations.
|
|
89
|
+
"""
|
|
90
|
+
self.set_origin_x()
|
|
91
|
+
self.set_origin_y()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class XYStepperStageDevice(XYStageDevice):
|
|
95
|
+
"""ABC for XYStage devices that support stepper motors.
|
|
96
|
+
|
|
97
|
+
In this variant, rather than providing `set_position_um` and `get_position_um`,
|
|
98
|
+
you provide `set_position_steps`, `get_position_steps`, `get_step_size_x_um`,
|
|
99
|
+
and `get_step_size_y_um`. A default implementation of `set_position_um` and
|
|
100
|
+
`get_position_um` is then provided that uses these methods, taking into account
|
|
101
|
+
the XY-mirroring properties of the device.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def set_position_steps(self, x: int, y: int) -> None:
|
|
106
|
+
"""Set the position of the XY stage in steps."""
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def get_position_steps(self) -> tuple[int, int]:
|
|
110
|
+
"""Returns the current position of the XY stage in steps."""
|
|
111
|
+
|
|
112
|
+
@abstractmethod
|
|
113
|
+
def get_step_size_x_um(self) -> float:
|
|
114
|
+
"""Returns the step size of the X axis in microns."""
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def get_step_size_y_um(self) -> float:
|
|
118
|
+
"""Returns the step size of the Y axis in microns."""
|
|
119
|
+
|
|
120
|
+
# ----------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def __init__(self) -> None:
|
|
123
|
+
super().__init__()
|
|
124
|
+
self.register_property(name=Keyword.Transpose_MirrorX, default_value=False)
|
|
125
|
+
self.register_property(name=Keyword.Transpose_MirrorY, default_value=False)
|
|
126
|
+
self._origin_x_steps: int = 0
|
|
127
|
+
self._origin_y_steps: int = 0
|
|
128
|
+
|
|
129
|
+
def set_position_um(self, x: float, y: float) -> None:
|
|
130
|
+
"""Set the position of the XY stage in microns."""
|
|
131
|
+
# Converts the given micrometer coordinates to steps and sets the position.
|
|
132
|
+
mirror_x, mirror_y = self._get_orientation()
|
|
133
|
+
|
|
134
|
+
steps_x = int(x / self.get_step_size_x_um())
|
|
135
|
+
steps_y = int(y / self.get_step_size_y_um())
|
|
136
|
+
|
|
137
|
+
if mirror_x:
|
|
138
|
+
steps_x = -steps_x
|
|
139
|
+
if mirror_y:
|
|
140
|
+
steps_y = -steps_y
|
|
141
|
+
|
|
142
|
+
x_steps = self._origin_x_steps + steps_x
|
|
143
|
+
y_steps = self._origin_y_steps + steps_y
|
|
144
|
+
self.set_position_steps(x_steps, y_steps)
|
|
145
|
+
|
|
146
|
+
self.core.events.XYStagePositionChanged.emit(self.get_label(), x, y)
|
|
147
|
+
|
|
148
|
+
def get_position_um(self) -> tuple[float, float]:
|
|
149
|
+
"""Get the position of the XY stage in microns."""
|
|
150
|
+
# Converts the current steps to micrometer coordinates and returns the position.
|
|
151
|
+
mirror_x, mirror_y = self._get_orientation()
|
|
152
|
+
x_steps, y_steps = self.get_position_steps()
|
|
153
|
+
|
|
154
|
+
x = (self._origin_x_steps - x_steps) * self.get_step_size_x_um()
|
|
155
|
+
y = (self._origin_y_steps - y_steps) * self.get_step_size_y_um()
|
|
156
|
+
if not mirror_x:
|
|
157
|
+
x = -x
|
|
158
|
+
if not mirror_y:
|
|
159
|
+
y = -y
|
|
160
|
+
|
|
161
|
+
return x, y
|
|
162
|
+
|
|
163
|
+
def set_relative_position_steps(self, dx: int, dy: int) -> None:
|
|
164
|
+
"""Move the stage by a relative amount.
|
|
165
|
+
|
|
166
|
+
Can be overridden for more efficient implementations.
|
|
167
|
+
"""
|
|
168
|
+
x_steps, y_steps = self.get_position_steps()
|
|
169
|
+
self.set_position_steps(x_steps + dx, y_steps + dy)
|
|
170
|
+
|
|
171
|
+
def set_relative_position_um(self, dx: float, dy: float) -> None:
|
|
172
|
+
"""Default implementation for relative motion.
|
|
173
|
+
|
|
174
|
+
Can be overridden for more efficient implementations.
|
|
175
|
+
"""
|
|
176
|
+
mirror_x, mirror_y = self._get_orientation()
|
|
177
|
+
|
|
178
|
+
if mirror_x:
|
|
179
|
+
dx = -dx
|
|
180
|
+
if mirror_y:
|
|
181
|
+
dy = -dy
|
|
182
|
+
|
|
183
|
+
steps_x = int(dx / self.get_step_size_x_um())
|
|
184
|
+
steps_y = int(dy / self.get_step_size_y_um())
|
|
185
|
+
|
|
186
|
+
self.set_relative_position_steps(steps_x, steps_y)
|
|
187
|
+
|
|
188
|
+
x, y = self.get_position_um()
|
|
189
|
+
self.core.events.XYStagePositionChanged.emit(self.get_label(), x, y)
|
|
190
|
+
|
|
191
|
+
def set_adapter_origin_um(self, x: float = 0.0, y: float = 0.0) -> None:
|
|
192
|
+
"""Alter the software coordinate translation between micrometers and steps.
|
|
193
|
+
|
|
194
|
+
... such that the current position becomes the given coordinates.
|
|
195
|
+
"""
|
|
196
|
+
mirror_x, mirror_y = self._get_orientation()
|
|
197
|
+
x_steps, y_steps = self.get_position_steps()
|
|
198
|
+
|
|
199
|
+
steps_x = int(x / self.get_step_size_x_um())
|
|
200
|
+
steps_y = int(y / self.get_step_size_y_um())
|
|
201
|
+
|
|
202
|
+
self._origin_x_steps = x_steps + (steps_x if mirror_x else -steps_x)
|
|
203
|
+
self._origin_y_steps = y_steps + (steps_y if mirror_y else -steps_y)
|
|
204
|
+
|
|
205
|
+
def set_origin(self) -> None:
|
|
206
|
+
"""Zero the stage's coordinates at the current position."""
|
|
207
|
+
self.set_adapter_origin_um()
|
|
208
|
+
|
|
209
|
+
def set_origin_x(self) -> None:
|
|
210
|
+
"""Zero the stage's X coordinates at the current position."""
|
|
211
|
+
raise NotImplementedError # pragma: no cover
|
|
212
|
+
|
|
213
|
+
def set_origin_y(self) -> None:
|
|
214
|
+
"""Zero the stage's Y coordinates at the current position."""
|
|
215
|
+
raise NotImplementedError # pragma: no cover
|
|
216
|
+
|
|
217
|
+
def _get_orientation(self) -> tuple[bool, bool]:
|
|
218
|
+
return (
|
|
219
|
+
self.get_property_value(Keyword.Transpose_MirrorX),
|
|
220
|
+
self.get_property_value(Keyword.Transpose_MirrorY),
|
|
221
|
+
)
|
pymmcore_plus/install.py
CHANGED
|
@@ -10,7 +10,7 @@ import tempfile
|
|
|
10
10
|
from contextlib import contextmanager, nullcontext
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from platform import system
|
|
13
|
-
from typing import TYPE_CHECKING, Callable,
|
|
13
|
+
from typing import TYPE_CHECKING, Callable, Protocol
|
|
14
14
|
from urllib.request import urlopen, urlretrieve
|
|
15
15
|
|
|
16
16
|
import typer
|
|
@@ -18,6 +18,8 @@ import typer
|
|
|
18
18
|
from pymmcore_plus._util import USER_DATA_MM_PATH
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Iterator
|
|
22
|
+
from contextlib import AbstractContextManager
|
|
21
23
|
|
|
22
24
|
class _MsgLogger(Protocol):
|
|
23
25
|
def __call__(self, text: str, color: str = "", emoji: str = "") -> None: ...
|
|
@@ -35,23 +37,21 @@ try:
|
|
|
35
37
|
rich_print(f"{emoji}{color}{text}")
|
|
36
38
|
|
|
37
39
|
@contextmanager
|
|
38
|
-
def _spinner(
|
|
39
|
-
text: str = "", color: str = "bold blue"
|
|
40
|
-
) -> Iterator[progress.Progress]:
|
|
40
|
+
def _spinner(text: str, color: str = "bold blue") -> Iterator[None]:
|
|
41
41
|
with progress.Progress(
|
|
42
42
|
progress.SpinnerColumn(), progress.TextColumn(f"[{color}]{text}")
|
|
43
43
|
) as pbar:
|
|
44
44
|
pbar.add_task(description=text, total=None)
|
|
45
|
-
yield
|
|
45
|
+
yield None
|
|
46
46
|
|
|
47
47
|
except ImportError: # pragma: no cover
|
|
48
|
-
progress = None
|
|
48
|
+
progress = None # type: ignore
|
|
49
49
|
|
|
50
50
|
def _pretty_print(text: str, color: str = "", emoji: str = "") -> None:
|
|
51
51
|
print(text)
|
|
52
52
|
|
|
53
53
|
@contextmanager
|
|
54
|
-
def _spinner(text: str
|
|
54
|
+
def _spinner(text: str, color: str = "") -> Iterator[None]:
|
|
55
55
|
print(text)
|
|
56
56
|
yield
|
|
57
57
|
|
|
@@ -70,14 +70,14 @@ def _get_download_name(url: str) -> str:
|
|
|
70
70
|
return ""
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
def _get_spinner(log_msg: _MsgLogger) -> Callable[[str],
|
|
73
|
+
def _get_spinner(log_msg: _MsgLogger) -> Callable[[str], AbstractContextManager]:
|
|
74
74
|
if log_msg is _pretty_print:
|
|
75
75
|
spinner = _spinner
|
|
76
76
|
else:
|
|
77
77
|
|
|
78
78
|
@contextmanager
|
|
79
|
-
def spinner(
|
|
80
|
-
log_msg(
|
|
79
|
+
def spinner(text: str, color: str = "") -> Iterator[None]:
|
|
80
|
+
log_msg(text)
|
|
81
81
|
yield
|
|
82
82
|
|
|
83
83
|
return spinner
|
|
@@ -177,7 +177,7 @@ def _download_url(url: str, output_path: Path, show_progress: bool = True) -> No
|
|
|
177
177
|
pbar.update(task_id, advance=block_size)
|
|
178
178
|
|
|
179
179
|
else:
|
|
180
|
-
pbar = nullcontext()
|
|
180
|
+
pbar = nullcontext() # type: ignore
|
|
181
181
|
|
|
182
182
|
def hook(count: float, block_size: float, total_size: float) -> None: ...
|
|
183
183
|
|
|
@@ -207,6 +207,11 @@ def install(
|
|
|
207
207
|
"""
|
|
208
208
|
if PLATFORM not in ("Darwin", "Windows"): # pragma: no cover
|
|
209
209
|
log_msg(f"Unsupported platform: {PLATFORM!r}", "bold red", ":x:")
|
|
210
|
+
log_msg(
|
|
211
|
+
"Consider building from source (mmcore build-dev).",
|
|
212
|
+
"bold yellow",
|
|
213
|
+
":light_bulb:",
|
|
214
|
+
)
|
|
210
215
|
raise sys.exit(1)
|
|
211
216
|
|
|
212
217
|
if release == "latest":
|