pymmcore-plus 0.9.4__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 +133 -30
  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.4.dist-info → pymmcore_plus-0.13.0.dist-info}/METADATA +22 -11
  63. pymmcore_plus-0.13.0.dist-info/RECORD +71 -0
  64. {pymmcore_plus-0.9.4.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.4.dist-info/RECORD +0 -55
  67. {pymmcore_plus-0.9.4.dist-info → pymmcore_plus-0.13.0.dist-info}/entry_points.txt +0 -0
  68. {pymmcore_plus-0.9.4.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, ContextManager, Iterator, Protocol
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 pbar
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 = "", color: str = "") -> Iterator[None]:
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], ContextManager[None]]:
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(msg: str) -> Iterator[None]:
80
- log_msg(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":
@@ -6,8 +6,8 @@ from .events import PMDASignaler
6
6
 
7
7
  __all__ = [
8
8
  "MDAEngine",
9
- "PMDAEngine",
10
9
  "MDARunner",
10
+ "PMDAEngine",
11
11
  "PMDASignaler",
12
12
  "mda_listeners_connected",
13
13
  ]