pymmcore-plus 0.13.4__py3-none-any.whl → 0.13.6__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/_cli.py CHANGED
@@ -8,6 +8,7 @@ from contextlib import suppress
8
8
  from pathlib import Path
9
9
  from typing import Optional, Union, cast
10
10
 
11
+ from pymmcore_plus._util import get_device_interface_version
11
12
  from pymmcore_plus.core._device import Device
12
13
  from pymmcore_plus.core._mmcore_plus import CMMCorePlus
13
14
 
@@ -114,9 +115,15 @@ def _list() -> None:
114
115
  for parent, items in found.items():
115
116
  print(f":file_folder:[bold green] {parent}")
116
117
  for item in items:
118
+ version = ""
119
+ for _lib in (parent / item).glob("*_dal_*"):
120
+ with suppress(Exception):
121
+ div = get_device_interface_version(_lib)
122
+ version = f" (Dev. Interface {div})"
123
+ break
117
124
  bullet = " [bold yellow]*" if first else " •"
118
125
  using = " [bold blue](active)" if first else ""
119
- print(f"{bullet} [cyan]{item}{using}")
126
+ print(f"{bullet} [cyan]{item}{version}{using}")
120
127
  first = False
121
128
  else:
122
129
  print(":x: [bold red]There are no pymmcore-plus Micro-Manager files.")
@@ -157,7 +164,7 @@ def install(
157
164
  help="Installation directory.",
158
165
  ),
159
166
  release: str = typer.Option(
160
- "latest", "-r", "--release", help="Release date. e.g. 20210201"
167
+ "latest-compatible", "-r", "--release", help="Release date. e.g. 20210201"
161
168
  ),
162
169
  plain_output: bool = typer.Option(
163
170
  False,
@@ -289,7 +296,7 @@ def run(
289
296
  mda.setdefault("channels", []).append(_c)
290
297
  if channel_group is not None:
291
298
  for c in mda.get("channels", []):
292
- cast(dict, c)["group"] = channel_group
299
+ cast("dict", c)["group"] = channel_group
293
300
 
294
301
  # this will raise if anything has gone wrong.
295
302
  _mda = MDASequence(**mda)
@@ -1,12 +1,30 @@
1
1
  """Internal module to choose between pymmcore and pymmcore-nano."""
2
2
 
3
+ import re
4
+ from typing import NamedTuple
5
+
3
6
  try:
4
7
  from pymmcore_nano import * # noqa F403
5
8
  from pymmcore_nano import __version__
6
9
 
7
10
  BACKEND = "pymmcore-nano"
11
+ NANO = True
8
12
  except ImportError:
9
13
  from pymmcore import * # noqa F403
10
- from pymmcore import __version__ # noqa F401
14
+ from pymmcore import __version__
11
15
 
12
16
  BACKEND = "pymmcore"
17
+ NANO = False
18
+
19
+
20
+ class VersionInfo(NamedTuple):
21
+ """Version info for the backend."""
22
+
23
+ major: int
24
+ minor: int
25
+ micro: int
26
+ device_interface: int
27
+ build: int
28
+
29
+
30
+ version_info = VersionInfo(*(int(x) for x in re.findall(r"\d+", __version__)))
pymmcore_plus/_util.py CHANGED
@@ -94,7 +94,7 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
94
94
  """
95
95
  from ._logger import logger
96
96
 
97
- # we use a dict here to avoid duplicates
97
+ # we use a dict here to avoid duplicates, while retaining order
98
98
  full_list: dict[str, None] = {}
99
99
 
100
100
  # environment variable takes precedence
@@ -114,13 +114,42 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
114
114
  return path
115
115
  full_list[path] = None
116
116
 
117
+ # then look for mm-device-adapters
118
+ with suppress(ImportError):
119
+ import mm_device_adapters
120
+
121
+ from . import _pymmcore
122
+
123
+ mm_dev_div = mm_device_adapters.__version__.split(".")[0]
124
+ pymm_div = str(_pymmcore.version_info.device_interface)
125
+
126
+ if pymm_div != mm_dev_div: # pragma: no cover
127
+ warnings.warn(
128
+ "mm-device-adapters installed, but its device interface "
129
+ f"version ({mm_dev_div}) "
130
+ f"does not match the device interface version of {_pymmcore.BACKEND}"
131
+ f"({pymm_div}). You may wish to run"
132
+ f" `pip install --force-reinstall mm-device-adapters=={pymm_div}`. "
133
+ "mm-device-adapters will be ignored.",
134
+ stacklevel=2,
135
+ )
136
+ else:
137
+ path = mm_device_adapters.device_adapter_path()
138
+ if return_first:
139
+ logger.debug("using MM path from mm-device-adapters: %s", path)
140
+ return str(path)
141
+ full_list[path] = None
142
+
117
143
  # then look in user_data_dir
118
144
  _folders = (p for p in USER_DATA_MM_PATH.glob("Micro-Manager*") if p.is_dir())
119
- user_install = sorted(_folders, reverse=True)
120
- if user_install:
121
- if return_first:
122
- logger.debug("using MM path from user install: %s", user_install[0])
123
- return str(user_install[0])
145
+ if user_install := sorted(_folders, reverse=True):
146
+ if return_first and (
147
+ first := next(
148
+ (x for x in user_install if _mm_path_has_compatible_div(x)), None
149
+ )
150
+ ):
151
+ logger.debug("using MM path from user install: %s", first)
152
+ return str(first)
124
153
  for x in user_install:
125
154
  full_list[str(x)] = None
126
155
 
@@ -130,9 +159,13 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
130
159
  p for p in PYMMCORE_PLUS_PATH.glob(f"**/Micro-Manager*{sfx}") if p.is_dir()
131
160
  ]
132
161
  if local_install:
133
- if return_first:
134
- logger.debug("using MM path from local install: %s", local_install[0])
135
- return str(local_install[0])
162
+ if return_first and (
163
+ first := next(
164
+ (x for x in local_install if _mm_path_has_compatible_div(x)), None
165
+ )
166
+ ): # pragma: no cover
167
+ logger.debug("using MM path from local install: %s", first)
168
+ return str(first)
136
169
  for x in local_install:
137
170
  full_list[str(x)] = None
138
171
 
@@ -153,8 +186,9 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
153
186
  "could not find micromanager directory. Please run 'mmcore install'"
154
187
  )
155
188
  return None
156
- logger.debug("using MM path found in applications: %s", pth)
157
- return str(pth)
189
+ if _mm_path_has_compatible_div(pth): # pragma: no cover
190
+ logger.debug("using MM path found in applications: %s", pth)
191
+ return str(pth)
158
192
  if pth is not None:
159
193
  full_list[str(pth)] = None
160
194
  return list(full_list)
@@ -229,15 +263,15 @@ def _qt_app_is_running() -> bool:
229
263
  return False # pragma: no cover
230
264
 
231
265
 
232
- MMCORE_PLUS_SIGNALS_BACKEND = "MMCORE_PLUS_SIGNALS_BACKEND"
266
+ PYMM_SIGNALS_BACKEND = "PYMM_SIGNALS_BACKEND"
233
267
 
234
268
 
235
269
  def signals_backend() -> Literal["qt", "psygnal"]:
236
270
  """Return the name of the event backend to use."""
237
- env_var = os.environ.get(MMCORE_PLUS_SIGNALS_BACKEND, "auto").lower()
271
+ env_var = os.environ.get(PYMM_SIGNALS_BACKEND, "auto").lower()
238
272
  if env_var not in {"qt", "psygnal", "auto"}:
239
273
  warnings.warn(
240
- f"{MMCORE_PLUS_SIGNALS_BACKEND} must be one of ['qt', 'psygnal', 'auto']. "
274
+ f"{PYMM_SIGNALS_BACKEND} must be one of ['qt', 'psygnal', 'auto']. "
241
275
  f"not: {env_var!r}. Using 'auto'.",
242
276
  stacklevel=1,
243
277
  )
@@ -250,7 +284,7 @@ def signals_backend() -> Literal["qt", "psygnal"]:
250
284
  if qt_app_running or list(_imported_qt_modules()):
251
285
  return "qt"
252
286
  warnings.warn(
253
- f"{MMCORE_PLUS_SIGNALS_BACKEND} set to 'qt', but no Qt app is running. "
287
+ f"{PYMM_SIGNALS_BACKEND} set to 'qt', but no Qt app is running. "
254
288
  "Falling back to 'psygnal'.",
255
289
  stacklevel=1,
256
290
  )
@@ -618,3 +652,34 @@ def timestamp() -> str:
618
652
  with suppress(Exception):
619
653
  now = now.astimezone()
620
654
  return now.isoformat()
655
+
656
+
657
+ def get_device_interface_version(lib_path: str | Path) -> int:
658
+ """Return the device interface version from the given library path."""
659
+ import ctypes
660
+
661
+ if sys.platform.startswith("win"):
662
+ lib = ctypes.WinDLL(lib_path)
663
+ else:
664
+ lib = ctypes.CDLL(lib_path)
665
+
666
+ try:
667
+ func = lib.GetDeviceInterfaceVersion
668
+ except AttributeError:
669
+ raise RuntimeError(
670
+ f"Function 'GetDeviceInterfaceVersion' not found in {lib_path}"
671
+ ) from None
672
+
673
+ func.restype = ctypes.c_long
674
+ func.argtypes = []
675
+ return func() # type: ignore[no-any-return]
676
+
677
+
678
+ def _mm_path_has_compatible_div(folder: Path | str) -> bool:
679
+ from . import _pymmcore
680
+
681
+ div = _pymmcore.version_info.device_interface
682
+ for lib_path in Path(folder).glob("*mmgr_dal*"):
683
+ with suppress(Exception):
684
+ return get_device_interface_version(lib_path) == div
685
+ return False # pragma: no cover
@@ -94,6 +94,14 @@ class CFGCommand(str, Enum):
94
94
  PixelSizeAffine = pymmcore.g_CFGCommand_PixelSizeAffine
95
95
  ParentID = pymmcore.g_CFGCommand_ParentID
96
96
  FocusDirection = pymmcore.g_CFGCommand_FocusDirection
97
+
98
+ if hasattr(pymmcore, "g_CFGCommand_PixelSizedxdz"):
99
+ PixelSize_dxdz = pymmcore.g_CFGCommand_PixelSizedxdz
100
+ if hasattr(pymmcore, "g_CFGCommand_PixelSizedydz"):
101
+ PixelSize_dydz = pymmcore.g_CFGCommand_PixelSizedydz
102
+ if hasattr(pymmcore, "g_CFGCommand_PixelSizeOptimalZUm"):
103
+ PixelSize_OptimalZUm = pymmcore.g_CFGCommand_PixelSizeOptimalZUm
104
+
97
105
  #
98
106
  FieldDelimiters = pymmcore.g_FieldDelimiters
99
107
 
@@ -20,8 +20,7 @@ class Metadata(pymmcore.Metadata):
20
20
  def __init__(self, *args: Any, **kwargs: Any) -> None:
21
21
  super().__init__()
22
22
  if args and isinstance(args[0], Mapping):
23
- for k, v in args[0].items():
24
- self[k] = v
23
+ kwargs = {**args[0], **kwargs}
25
24
  for k, v in kwargs.items():
26
25
  self[k] = v
27
26
 
@@ -76,15 +75,19 @@ class Metadata(pymmcore.Metadata):
76
75
  return json.dumps(dict(self))
77
76
 
78
77
  def keys(self) -> KeysView[str]:
79
- return cast(KeysView, metadata_keys(self))
78
+ return cast("KeysView", metadata_keys(self))
80
79
 
81
80
  def items(self) -> ItemsView[str, str]:
82
- return cast(ItemsView, metadata_items(self))
81
+ return cast("ItemsView", metadata_items(self))
83
82
 
84
83
  def values(self) -> ValuesView[str]:
85
- return cast(ValuesView, metadata_values(self))
84
+ return cast("ValuesView", metadata_values(self))
86
85
 
87
86
 
88
87
  metadata_keys = new_class("metadata_keys", (KeysView,), {})
89
88
  metadata_items = new_class("metadata_items", (ItemsView,), {})
90
89
  metadata_values = new_class("metadata_values", (ValuesView,), {})
90
+
91
+ # Register the new classes with the `collections.abc` module
92
+ # so that isistance() works as expected.
93
+ Mapping.register(Metadata)
@@ -13,7 +13,7 @@ from pathlib import Path
13
13
  from re import Pattern
14
14
  from textwrap import dedent
15
15
  from threading import Thread
16
- from typing import TYPE_CHECKING, Any, Callable, NamedTuple, TypeVar, overload
16
+ from typing import TYPE_CHECKING, Any, Callable, NamedTuple, TypeVar, cast, overload
17
17
 
18
18
  from psygnal import SignalInstance
19
19
 
@@ -207,21 +207,52 @@ class CMMCorePlus(pymmcore.CMMCore):
207
207
 
208
208
  def __init__(self, mm_path: str | None = None, adapter_paths: Sequence[str] = ()):
209
209
  super().__init__()
210
+ if os.getenv("PYMM_DEBUG_LOG", "0").lower() in ("1", "true"):
211
+ self.enableDebugLog(True)
212
+ if os.getenv("PYMM_STDERR_LOG", "0").lower() in ("1", "true"):
213
+ self.enableStderrLog(True)
214
+ if buf_size := os.getenv("PYMM_BUFFER_SIZE_MB", ""):
215
+ try:
216
+ buf_size_int = int(buf_size)
217
+ if buf_size_int:
218
+ self.setCircularBufferMemoryFootprint(buf_size_int)
219
+ except (ValueError, TypeError):
220
+ warnings.warn("PYMM_BUFFER_SIZE_MB must be an integer", stacklevel=2)
210
221
 
211
222
  # Set the first instance of this class as the global singleton
212
223
  global _instance
213
224
  if _instance is None:
214
225
  _instance = self
215
226
 
216
- if hasattr("self", "enableFeature"):
217
- self.enableFeature("StrictInitializationChecks", True)
227
+ if hasattr(self, "enableFeature"):
228
+ strict = True
229
+ if env_strict := os.getenv("PYMM_STRICT_INIT_CHECKS", "").lower():
230
+ if env_strict in ("1", "true"):
231
+ strict = True
232
+ elif env_strict in ("0", "false"):
233
+ strict = False
234
+ self.enableFeature("StrictInitializationChecks", strict)
235
+
236
+ parallel = True
237
+ if env_parallel := os.getenv("PYMM_PARALLEL_INIT", "").lower():
238
+ if env_parallel in ("1", "true"):
239
+ parallel = True
240
+ elif env_parallel in ("0", "false"):
241
+ parallel = False
242
+ self.enableFeature("ParallelDeviceInitialization", parallel)
218
243
 
219
244
  # TODO: test this on windows ... writing to the same file may be an issue there
220
245
  if logfile := current_logfile(logger):
221
246
  self.setPrimaryLogFile(str(logfile))
222
247
  logger.debug("Initialized core %s", self)
223
248
 
224
- self._last_config: str | None = None # last loaded config file
249
+ # some internal state, remembering the last arguments passed to various
250
+ # functions. These are subject to change: do not depend on externally
251
+ self._last_sys_config: str | None = None # last loaded config file
252
+ self._last_config: tuple[str, str] = ("", "")
253
+ # last position set by setXYPosition, None means currentXYStageDevice
254
+ self._last_xy_position: dict[str | None, tuple[float, float]] = {}
255
+
225
256
  self._mm_path = mm_path or find_micromanager()
226
257
  if not adapter_paths and self._mm_path:
227
258
  adapter_paths = [self._mm_path]
@@ -348,7 +379,7 @@ class CMMCorePlus(pymmcore.CMMCore):
348
379
  """
349
380
  try:
350
381
  super().loadDevice(label, moduleName, deviceName)
351
- except RuntimeError as e:
382
+ except (RuntimeError, ValueError) as e:
352
383
  if exc := self._load_error_with_info(label, moduleName, deviceName, str(e)):
353
384
  raise exc from e
354
385
 
@@ -399,15 +430,15 @@ class CMMCorePlus(pymmcore.CMMCore):
399
430
  fpath = Path(self._mm_path) / fileName
400
431
  if not fpath.exists():
401
432
  raise FileNotFoundError(f"Path does not exist: {fpath}")
402
- self._last_config = str(fpath.resolve())
403
- super().loadSystemConfiguration(self._last_config)
433
+ self._last_sys_config = str(fpath.resolve())
434
+ super().loadSystemConfiguration(self._last_sys_config)
404
435
 
405
436
  def systemConfigurationFile(self) -> str | None:
406
437
  """Return the path to the last loaded system configuration file, or `None`.
407
438
 
408
439
  :sparkles: *This method is new in `CMMCorePlus`.*
409
440
  """
410
- return self._last_config
441
+ return self._last_sys_config
411
442
 
412
443
  def unloadAllDevices(self) -> None:
413
444
  """Unload all devices from the core and reset all configuration data.
@@ -642,10 +673,7 @@ class CMMCorePlus(pymmcore.CMMCore):
642
673
  img = super().getLastImageMD(channel, slice, md)
643
674
  else:
644
675
  img = super().getLastImageMD(md)
645
- return (
646
- self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img,
647
- md,
648
- )
676
+ return (self.fixImage(img) if fix and not pymmcore.NANO else img, md)
649
677
 
650
678
  @overload
651
679
  def popNextImageAndMD(
@@ -687,11 +715,7 @@ class CMMCorePlus(pymmcore.CMMCore):
687
715
  """
688
716
  md = Metadata()
689
717
  img = super().popNextImageMD(channel, slice, md)
690
- md = Metadata(md)
691
- return (
692
- self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img,
693
- md,
694
- )
718
+ return (self.fixImage(img) if fix and not pymmcore.NANO else img, md)
695
719
 
696
720
  def popNextImage(self, *, fix: bool = True) -> np.ndarray:
697
721
  """Gets and removes the next image from the circular buffer.
@@ -707,7 +731,7 @@ class CMMCorePlus(pymmcore.CMMCore):
707
731
  will be reshaped to (w, h, n_components) using `fixImage`.
708
732
  """
709
733
  img: np.ndarray = super().popNextImage()
710
- return self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img
734
+ return self.fixImage(img) if fix and not pymmcore.NANO else img
711
735
 
712
736
  def getNBeforeLastImageAndMD(
713
737
  self, n: int, *, fix: bool = True
@@ -735,7 +759,7 @@ class CMMCorePlus(pymmcore.CMMCore):
735
759
  """
736
760
  md = Metadata()
737
761
  img = super().getNBeforeLastImageMD(n, md)
738
- return self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img, md
762
+ return self.fixImage(img) if fix and not pymmcore.NANO else img, md
739
763
 
740
764
  def setConfig(self, groupName: str, configName: str) -> None:
741
765
  """Applies a configuration to a group.
@@ -747,6 +771,7 @@ class CMMCorePlus(pymmcore.CMMCore):
747
771
  """
748
772
  super().setConfig(groupName, configName)
749
773
  self.events.configSet.emit(groupName, configName)
774
+ self._last_config = (groupName, configName)
750
775
 
751
776
  # NEW methods
752
777
 
@@ -1348,6 +1373,25 @@ class CMMCorePlus(pymmcore.CMMCore):
1348
1373
  self.waitForDevice(self.getXYStageDevice())
1349
1374
  self.waitForDevice(self.getFocusDevice())
1350
1375
 
1376
+ @overload
1377
+ def setXYPosition(self, x: float, y: float, /) -> None: ...
1378
+ @overload
1379
+ def setXYPosition(self, xyStageLabel: str, x: float, y: float, /) -> None: ...
1380
+ def setXYPosition(self, *args: str | float) -> None:
1381
+ """Sets the position of the XY stage in microns.
1382
+
1383
+ **Why Override?** to store the last commanded stage position internally.
1384
+ """
1385
+ if len(args) == 2:
1386
+ label: str | None = None
1387
+ x, y = cast("tuple[float, float]", args)
1388
+ elif len(args) == 3:
1389
+ label, x, y = args # type: ignore
1390
+ else:
1391
+ raise ValueError("Invalid number of arguments. Expected 2 or 3.")
1392
+ super().setXYPosition(*args) # type: ignore
1393
+ self._last_xy_position[label] = (x, y)
1394
+
1351
1395
  def getZPosition(self) -> float:
1352
1396
  """Obtains the current position of the Z axis of the Z stage in microns.
1353
1397
 
@@ -1668,7 +1712,7 @@ class CMMCorePlus(pymmcore.CMMCore):
1668
1712
  if numChannel is not None
1669
1713
  else super().getImage()
1670
1714
  )
1671
- return self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img
1715
+ return self.fixImage(img) if fix and not pymmcore.NANO else img
1672
1716
 
1673
1717
  def startContinuousSequenceAcquisition(self, intervalMs: float = 0) -> None:
1674
1718
  """Start a ContinuousSequenceAcquisition.
@@ -1982,9 +2026,10 @@ class CMMCorePlus(pymmcore.CMMCore):
1982
2026
  **Why Override?** To also save pixel size configurations.
1983
2027
  """
1984
2028
  super().saveSystemConfiguration(filename)
1985
- # saveSystemConfiguration does not save the pixel size config so here
1986
- # we add to the saved file also any pixel size config.
1987
- self._save_pixel_configurations(filename)
2029
+ if pymmcore.version_info < (11, 5):
2030
+ # saveSystemConfiguration does not save the pixel size config so hereq
2031
+ # we add to the saved file also any pixel size config.
2032
+ self._save_pixel_configurations(filename)
1988
2033
 
1989
2034
  def _save_pixel_configurations(self, filename: str) -> None:
1990
2035
  px_configs = self.getAvailablePixelSizeConfigs()
@@ -3,10 +3,10 @@ from __future__ import annotations
3
3
  from collections import defaultdict
4
4
  from collections.abc import Iterable
5
5
  from contextlib import suppress
6
- from typing import TYPE_CHECKING, Any, TypeVar
6
+ from typing import TYPE_CHECKING, Any, Optional, TypeVar
7
7
 
8
8
  from pydantic import Field, model_validator
9
- from useq import AcquireImage, MDAEvent
9
+ from useq import AcquireImage, MDAEvent, MDASequence
10
10
 
11
11
  from pymmcore_plus.core._constants import DeviceType, Keyword
12
12
 
@@ -65,6 +65,9 @@ class SequencedEvent(MDAEvent):
65
65
  z_sequence: tuple[float, ...] = Field(default_factory=tuple)
66
66
  slm_sequence: tuple[bytes, ...] = Field(default_factory=tuple)
67
67
 
68
+ # re-defining this from MDAEvent to circumvent a strange issue with pydantic 2.11
69
+ sequence: Optional[MDASequence] = Field(default=None, repr=False) # noqa: UP007
70
+
68
71
  # all other property sequences
69
72
  property_sequences: dict[tuple[str, str], list[str]] = Field(default_factory=dict)
70
73
  # static properties should be added to MDAEvent.properties as usual
@@ -6,7 +6,7 @@ from ._protocol import PCoreSignaler
6
6
  from ._psygnal import CMMCoreSignaler
7
7
 
8
8
  if TYPE_CHECKING:
9
- from ._qsignals import QCoreSignaler # noqa: TC004
9
+ from ._qsignals import QCoreSignaler
10
10
 
11
11
 
12
12
  __all__ = [
@@ -654,7 +654,7 @@ def _ensure_label(
654
654
  if len(args) < min_args:
655
655
  # we didn't get the label
656
656
  return getter(), args
657
- return cast(str, args[0]), args[1:]
657
+ return cast("str", args[0]), args[1:]
658
658
 
659
659
 
660
660
  class PropertyStateCache(MutableMapping[tuple[str, str], Any]):
@@ -173,7 +173,7 @@ class PropertyController(Generic[TDev, TProp]):
173
173
  f"of property {self.property.name!r}: {self.property.limits}."
174
174
  ) from e
175
175
  min_, max_ = self.property.limits
176
- if not min_ <= cast(float, value) <= max_:
176
+ if not min_ <= cast("float", value) <= max_:
177
177
  raise ValueError(
178
178
  f"Value {value!r} is not within the allowed range of property "
179
179
  f"{self.property.name!r}: {self.property.limits}."
pymmcore_plus/install.py CHANGED
@@ -8,8 +8,9 @@ import subprocess
8
8
  import sys
9
9
  import tempfile
10
10
  from contextlib import contextmanager, nullcontext
11
+ from functools import cache
11
12
  from pathlib import Path
12
- from platform import system
13
+ from platform import machine, system
13
14
  from typing import TYPE_CHECKING, Callable, Protocol
14
15
  from urllib.request import urlopen, urlretrieve
15
16
 
@@ -57,7 +58,35 @@ except ImportError: # pragma: no cover
57
58
 
58
59
 
59
60
  PLATFORM = system()
61
+ MACH = machine()
60
62
  BASE_URL = "https://download.micro-manager.org"
63
+ plat = {"Darwin": "Mac", "Windows": "Windows", "Linux": "Linux"}.get(PLATFORM)
64
+ DOWNLOADS_URL = f"{BASE_URL}/nightly/2.0/{plat}/"
65
+
66
+
67
+ # Dates of release for each interface version.
68
+ # generally running `mmcore install -r <some_date>` will bring in devices with
69
+ # the NEW interface.
70
+ INTERFACES: dict[int, str] = {
71
+ 73: "20250318",
72
+ 72: "20250318",
73
+ 71: "20221031",
74
+ 70: "20210219",
75
+ 69: "20180712",
76
+ 68: "20171107",
77
+ 67: "20160609",
78
+ 66: "20160608",
79
+ 65: "20150528",
80
+ 64: "20150515",
81
+ 63: "20150505",
82
+ 62: "20150501",
83
+ 61: "20140801",
84
+ 60: "20140618",
85
+ 59: "20140515",
86
+ 58: "20140514",
87
+ 57: "20140125",
88
+ 56: "20140120",
89
+ }
61
90
 
62
91
 
63
92
  def _get_download_name(url: str) -> str:
@@ -141,10 +170,10 @@ def _mac_install(dmg: Path, dest: Path, log_msg: _MsgLogger) -> None:
141
170
  os.rename(_tmp / "ImageJ.app", install_path / "ImageJ.app")
142
171
 
143
172
 
173
+ @cache
144
174
  def available_versions() -> dict[str, str]:
145
175
  """Return a map of version -> url available for download."""
146
- plat = {"Darwin": "Mac", "Windows": "Windows"}[PLATFORM]
147
- with urlopen(f"{BASE_URL}/nightly/2.0/{plat}/") as resp:
176
+ with urlopen(DOWNLOADS_URL) as resp:
148
177
  html = resp.read().decode("utf-8")
149
178
 
150
179
  all_links = re.findall(r"href=\"([^\"]+)\"", html)
@@ -187,7 +216,7 @@ def _download_url(url: str, output_path: Path, show_progress: bool = True) -> No
187
216
 
188
217
  def install(
189
218
  dest: Path | str = USER_DATA_MM_PATH,
190
- release: str = "latest",
219
+ release: str = "latest-compatible",
191
220
  log_msg: _MsgLogger = _pretty_print,
192
221
  ) -> None:
193
222
  """Install Micro-Manager to `dest`.
@@ -199,14 +228,20 @@ def install(
199
228
  folder in the user's data directory: `appdirs.user_data_dir()`.
200
229
  release : str, optional
201
230
  Which release to install, by default "latest". Should be a date in the form
202
- YYYYMMDD, or "latest" to install the latest nightly release.
231
+ YYYYMMDD, "latest" to install the latest nightly release, or "latest-compatible"
232
+ to install the latest nightly release that is compatible with the
233
+ device interface version of the current pymmcore version.
203
234
  log_msg : _MsgLogger, optional
204
235
  Callback to log messages, must have signature:
205
236
  `def logger(text: str, color: str = "", emoji: str = ""): ...`
206
237
  May ignore color and emoji.
207
238
  """
208
- if PLATFORM not in ("Darwin", "Windows"): # pragma: no cover
209
- log_msg(f"Unsupported platform: {PLATFORM!r}", "bold red", ":x:")
239
+ if PLATFORM not in ("Darwin", "Windows") or (
240
+ PLATFORM == "Darwin" and MACH == "arm64"
241
+ ): # pragma: no cover
242
+ log_msg(
243
+ f"Unsupported platform/architecture: {PLATFORM}/{MACH}", "bold red", ":x:"
244
+ )
210
245
  log_msg(
211
246
  "Consider building from source (mmcore build-dev).",
212
247
  "bold yellow",
@@ -214,6 +249,32 @@ def install(
214
249
  )
215
250
  raise sys.exit(1)
216
251
 
252
+ if release == "latest-compatible":
253
+ from pymmcore_plus import _pymmcore
254
+
255
+ div = _pymmcore.version_info.device_interface
256
+ # date when the device interface version FOLLOWING the version that this
257
+ # pymmcore supports was released.
258
+ next_div_date = INTERFACES.get(div + 1, None)
259
+
260
+ # if div is equal to the greatest known interface version, use latest
261
+ if div == max(INTERFACES.keys()) or next_div_date is None:
262
+ release = "latest"
263
+ else: # pragma: no cover
264
+ # otherwise, find the date of the release in available_versions() that
265
+ # is less than the next_div date.
266
+ available = available_versions()
267
+ release = max(
268
+ (date for date in available if date < next_div_date),
269
+ default="unavailable",
270
+ )
271
+ if release == "unavailable":
272
+ # fallback to latest if no compatible versions found
273
+ raise ValueError(
274
+ "Unable to find a compatible release for device interface"
275
+ f"{div} at {DOWNLOADS_URL} "
276
+ )
277
+
217
278
  if release == "latest":
218
279
  plat = {
219
280
  "Darwin": "macos/Micro-Manager-x86_64-latest.dmg",
@@ -72,6 +72,10 @@ class MDAEngine(PMDAEngine):
72
72
  def __init__(self, mmc: CMMCorePlus, use_hardware_sequencing: bool = True) -> None:
73
73
  self._mmc = mmc
74
74
  self.use_hardware_sequencing: bool = use_hardware_sequencing
75
+ # if True, always set XY position, even if the commanded position is the same
76
+ # as the last commanded position (this does *not* query the stage for the
77
+ # current position).
78
+ self.force_set_xy_position: bool = True
75
79
 
76
80
  # whether to include position metadata when fetching on-frame metadata
77
81
  # omitted by default when performing triggered acquisition because it's slow.
@@ -92,6 +96,9 @@ class MDAEngine(PMDAEngine):
92
96
  # Note: getAutoShutter() is True when no config is loaded at all
93
97
  self._autoshutter_was_set: bool = self._mmc.getAutoShutter()
94
98
 
99
+ self._last_config: tuple[str, str] = ("", "")
100
+ self._last_xy_pos: tuple[float | None, float | None] = (None, None)
101
+
95
102
  # -----
96
103
  # The following values are stored during setup_sequence simply to speed up
97
104
  # retrieval of metadata during each frame.
@@ -238,18 +245,15 @@ class MDAEngine(PMDAEngine):
238
245
  if event.keep_shutter_open:
239
246
  ...
240
247
 
241
- if event.x_pos is not None or event.y_pos is not None:
242
- self._set_event_position(event)
248
+ self._set_event_xy_position(event)
249
+
243
250
  if event.z_pos is not None:
244
251
  self._set_event_z(event)
245
252
  if event.slm_image is not None:
246
253
  self._set_event_slm_image(event)
247
- if event.channel is not None:
248
- try:
249
- # possible speedup by setting manually.
250
- self._mmc.setConfig(event.channel.group, event.channel.config)
251
- except Exception as e:
252
- logger.warning("Failed to set channel. %s", e)
254
+
255
+ self._set_event_channel(event)
256
+
253
257
  if event.exposure is not None:
254
258
  try:
255
259
  self._mmc.setExposure(event.exposure)
@@ -413,11 +417,7 @@ class MDAEngine(PMDAEngine):
413
417
  # the call below, we won't be able to query `core.getCurrentConfig()`
414
418
  # not sure that's necessary; and this is here for tests to pass for now,
415
419
  # but this could be removed.
416
- if event.channel is not None:
417
- try:
418
- core.setConfig(event.channel.group, event.channel.config)
419
- except Exception as e:
420
- logger.warning("Failed to set channel. %s", e)
420
+ self._set_event_channel(event)
421
421
 
422
422
  if event.slm_image:
423
423
  self._set_event_slm_image(event)
@@ -430,8 +430,8 @@ class MDAEngine(PMDAEngine):
430
430
  # start sequences or set non-sequenced values
431
431
  if event.x_sequence:
432
432
  core.startXYStageSequence(core.getXYStageDevice())
433
- elif event.x_pos is not None or event.y_pos is not None:
434
- self._set_event_position(event)
433
+ else:
434
+ self._set_event_xy_position(event)
435
435
 
436
436
  if event.z_sequence:
437
437
  core.startStageSequence(core.getFocusDevice())
@@ -594,15 +594,51 @@ class MDAEngine(PMDAEngine):
594
594
 
595
595
  return _perform_full_focus(self._mmc.getZPosition())
596
596
 
597
- def _set_event_position(self, event: MDAEvent) -> None:
597
+ def _set_event_xy_position(self, event: MDAEvent) -> None:
598
+ event_x, event_y = event.x_pos, event.y_pos
599
+ # If neither coordinate is provided, do nothing.
600
+ if event_x is None and event_y is None:
601
+ return
602
+
598
603
  # skip if no XY stage device is found
599
604
  if not self._mmc.getXYStageDevice():
600
605
  logger.warning("No XY stage device found. Cannot set XY position.")
601
606
  return
602
607
 
603
- x = event.x_pos if event.x_pos is not None else self._mmc.getXPosition()
604
- y = event.y_pos if event.y_pos is not None else self._mmc.getYPosition()
605
- self._mmc.setXYPosition(x, y)
608
+ # Retrieve the last commanded XY position.
609
+ last_x, last_y = self._mmc._last_xy_position.get(None) or (None, None) # noqa: SLF001
610
+ if (
611
+ not self.force_set_xy_position
612
+ and (event_x is None or event_x == last_x)
613
+ and (event_y is None or event_y == last_y)
614
+ ):
615
+ return
616
+
617
+ if event_x is None or event_y is None:
618
+ cur_x, cur_y = self._mmc.getXYPosition()
619
+ event_x = cur_x if event_x is None else event_x
620
+ event_y = cur_y if event_y is None else event_y
621
+
622
+ try:
623
+ self._mmc.setXYPosition(event_x, event_y)
624
+ except Exception as e:
625
+ logger.warning("Failed to set XY position. %s", e)
626
+
627
+ def _set_event_channel(self, event: MDAEvent) -> None:
628
+ if (ch := event.channel) is None:
629
+ return
630
+
631
+ # comparison with _last_config is a fast/rough check ... which may miss subtle
632
+ # differences if device properties have been individually set in the meantime.
633
+ # could also compare to the system state, with:
634
+ # data = self._mmc.getConfigData(ch.group, ch.config)
635
+ # if self._mmc.getSystemStateCache().isConfigurationIncluded(data):
636
+ # ...
637
+ if (ch.group, ch.config) != self._mmc._last_config: # noqa: SLF001
638
+ try:
639
+ self._mmc.setConfig(ch.group, ch.config)
640
+ except Exception as e:
641
+ logger.warning("Failed to set channel. %s", e)
606
642
 
607
643
  def _set_event_z(self, event: MDAEvent) -> None:
608
644
  # skip if no Z stage device is found
@@ -8,7 +8,7 @@ from ._protocol import PMDASignaler
8
8
  from ._psygnal import MDASignaler
9
9
 
10
10
  if TYPE_CHECKING:
11
- from ._qsignals import QMDASignaler # noqa: TC004
11
+ from ._qsignals import QMDASignaler
12
12
 
13
13
 
14
14
  __all__ = [
@@ -302,7 +302,7 @@ class TensorStoreHandler:
302
302
 
303
303
  # HACK
304
304
  if self.ts_driver == "zarr":
305
- meta = cast(dict, spec.setdefault("metadata", {}))
305
+ meta = cast("dict", spec.setdefault("metadata", {}))
306
306
  if "dimension_separator" not in meta:
307
307
  meta["dimension_separator"] = "/"
308
308
  return spec
@@ -289,6 +289,15 @@ def pixel_size_config(core: CMMCorePlus, *, config_name: str) -> PixelSizeConfig
289
289
  affine = core.getPixelSizeAffineByID(config_name)
290
290
  if affine != (1.0, 0.0, 0.0, 0.0, 1.0, 0.0):
291
291
  info["pixel_size_affine"] = affine
292
+ # added in v11.5
293
+ if hasattr(core, "getPixelSizedxdz") and (px := core.getPixelSizedxdz(config_name)):
294
+ info["pixel_size_dxdz"] = px
295
+ if hasattr(core, "getPixelSizedydz") and (px := core.getPixelSizedydz(config_name)):
296
+ info["pixel_size_dydz"] = px
297
+ if hasattr(core, "getPixelSizeOptimalZUm") and (
298
+ z := core.getPixelSizeOptimalZUm(config_name)
299
+ ):
300
+ info["pixel_size_optimal_z_um"] = z
292
301
  return info
293
302
 
294
303
 
@@ -331,11 +331,30 @@ class PixelSizeConfigPreset(ConfigPreset):
331
331
  corrected for binning and known magnification devices. The affine transform
332
332
  consists of the first two rows of a 3x3 matrix, the third row is always assumed
333
333
  to be 0.0 0.0 1.0.
334
+
335
+ pixel_size_dxdz : float
336
+ *Not Required*. The angle between the camera's x axis and the axis (direction)
337
+ of the z drive for the given pixel size configuration. This angle is
338
+ dimensionless (i.e. the ratio of the translation in x caused by a translation
339
+ in z, i.e. dx / dz). If missing, assume 0.0.
340
+ pixel_size_dydz : float
341
+ *Not Required*. The angle between the camera's y axis and the axis (direction)
342
+ of the z drive for the given pixel size configuration. This angle is
343
+ dimensionless (i.e. the ratio of the translation in y caused by a translation
344
+ in z, i.e. dy / dz). If missing, assume 0.0.
345
+ pixel_size_optimal_z_um : float
346
+ *Not Required*. User-defined optimal Z step size is for this pixel size config.
347
+ If missing, assume 0.0.
334
348
  """
335
349
 
336
350
  pixel_size_um: float
337
351
  pixel_size_affine: NotRequired[AffineTuple]
338
352
 
353
+ # added in MMCore v 11.5
354
+ pixel_size_dxdz: NotRequired[float] # default 0.0
355
+ pixel_size_dydz: NotRequired[float] # default 0.0
356
+ pixel_size_optimal_z_um: NotRequired[float] # default 0.0
357
+
339
358
 
340
359
  class ConfigGroup(TypedDict):
341
360
  """A group of configuration presets.
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import warnings
6
6
  from typing import TYPE_CHECKING, Any, Callable
7
7
 
8
- from pymmcore_plus import CFGCommand, DeviceType, FocusDirection, Keyword
8
+ from pymmcore_plus import CFGCommand, DeviceType, FocusDirection, Keyword, _pymmcore
9
9
  from pymmcore_plus._util import timestamp
10
10
 
11
11
  from ._config_group import ConfigGroup, ConfigPreset, Setting
@@ -162,6 +162,12 @@ def iter_pixel_size_presets(scope: Microscope) -> Iterable[str]:
162
162
  yield _serialize(CFGCommand.PixelSize_um, p.name, p.pixel_size_um)
163
163
  if p.affine != DEFAULT_AFFINE:
164
164
  yield _serialize(CFGCommand.PixelSizeAffine, p.name, *p.affine)
165
+ if p.angle_dxdz and (cmd := getattr(CFGCommand, "PixelSizeAngleDxdz", None)):
166
+ yield _serialize(cmd, p.name, p.angle_dxdz)
167
+ if p.angle_dydz and (cmd := getattr(CFGCommand, "PixelSizeAngleDydz", None)):
168
+ yield _serialize(cmd, p.name, p.angle_dydz)
169
+ if p.optimalz_um and (cmd := getattr(CFGCommand, "PixelSize_OptimalZUm", None)):
170
+ yield _serialize(cmd, p.name, p.optimalz_um)
165
171
 
166
172
 
167
173
  # Order will determine the order of the sections in the file
@@ -180,10 +186,17 @@ CONFIG_SECTIONS: dict[str, Callable[[Microscope], Iterable[str]]] = {
180
186
  "Camera-synchronized devices": lambda _: [],
181
187
  "Labels": iter_labels,
182
188
  "Configuration presets": iter_config_presets,
183
- "Roles": iter_roles, # MMStudio puts this above Cam-Synched devices, MMCore here.
184
- "PixelSize settings": iter_pixel_size_presets,
185
189
  }
186
190
 
191
+
192
+ if _pymmcore.version_info >= (11, 5):
193
+ CONFIG_SECTIONS["PixelSize settings"] = iter_pixel_size_presets
194
+ CONFIG_SECTIONS["Roles"] = iter_roles
195
+ else:
196
+ CONFIG_SECTIONS["Roles"] = iter_roles
197
+ CONFIG_SECTIONS["PixelSize settings"] = iter_pixel_size_presets
198
+
199
+
187
200
  # ------------------ Deserialization ------------------
188
201
 
189
202
  # TODO: ... I think the command subclasses are probably overkill here.
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
7
7
 
8
8
  from pymmcore_plus import CMMCorePlus, DeviceType, FocusDirection, Keyword
9
9
  from pymmcore_plus._util import no_stdout
10
+ from pymmcore_plus.core._constants import DeviceInitializationState
10
11
 
11
12
  from ._core_link import CoreObject
12
13
  from ._property import Property
@@ -410,6 +411,11 @@ def get_available_devices(core: CMMCorePlus) -> list[AvailableDevice]:
410
411
  for hub in core.getLoadedDevicesOfType(DeviceType.Hub):
411
412
  lib_name = core.getDeviceLibrary(hub)
412
413
  hub_dev = library_to_hub.get((lib_name, hub))
414
+ if (
415
+ core.getDeviceInitializationState(hub)
416
+ != DeviceInitializationState.InitializedSuccessfully
417
+ ):
418
+ continue
413
419
  for child in core.getInstalledDevices(hub):
414
420
  dev = AvailableDevice(
415
421
  library=lib_name, adapter_name=child, library_hub=hub_dev
@@ -31,6 +31,9 @@ class PixelSizePreset(ConfigPreset):
31
31
 
32
32
  pixel_size_um: float = 0.0
33
33
  affine: AffineTuple = DEFAULT_AFFINE
34
+ angle_dxdz: float = 0.0
35
+ angle_dydz: float = 0.0
36
+ optimalz_um: float = 0.0
34
37
 
35
38
  @classmethod
36
39
  def from_metadata(cls, meta: PixelSizeConfigPreset) -> Self: # type: ignore [override]
@@ -38,6 +41,12 @@ class PixelSizePreset(ConfigPreset):
38
41
  obj.pixel_size_um = meta["pixel_size_um"]
39
42
  if "pixel_size_affine" in meta:
40
43
  obj.affine = meta["pixel_size_affine"]
44
+ if "pixel_size_dxdz" in meta:
45
+ obj.angle_dxdz = meta["pixel_size_dxdz"]
46
+ if "pixel_size_dydz" in meta:
47
+ obj.angle_dydz = meta["pixel_size_dydz"]
48
+ if "pixel_size_optimal_z_um" in meta:
49
+ obj.optimalz_um = meta["pixel_size_optimal_z_um"]
41
50
  return obj
42
51
 
43
52
  def __rich_repr__(self, *, defaults: bool = False) -> Iterable[tuple[str, Any]]:
@@ -88,8 +88,8 @@ class Setting:
88
88
 
89
89
  @classmethod
90
90
  def _from_list(cls, val: list) -> Self:
91
- (dev, prop), (typ, val) = val
92
- return cls(dev, prop, typ, val)
91
+ (dev, prop), (type_, val) = val
92
+ return cls(dev, prop, type_, val)
93
93
 
94
94
 
95
95
  @dataclass
@@ -100,8 +100,8 @@ class SettingEvent(Setting):
100
100
 
101
101
  @classmethod
102
102
  def _from_list(cls, val: list) -> Self:
103
- (dev, prop), (typ, val), count = val
104
- return cls(dev, prop, typ, val, count)
103
+ (dev, prop), (type_, val), count = val
104
+ return cls(dev, prop, type_, val, count)
105
105
 
106
106
 
107
107
  @dataclass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymmcore-plus
3
- Version: 0.13.4
3
+ Version: 0.13.6
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: platformdirs>=3.0.0
30
30
  Requires-Dist: psygnal>=0.7
31
31
  Requires-Dist: pymmcore>=10.7.0.71.0
32
32
  Requires-Dist: rich>=10.2.0
33
- Requires-Dist: tensorstore; python_version < '3.13'
33
+ Requires-Dist: tensorstore<=0.1.71
34
34
  Requires-Dist: typer>=0.4.2
35
35
  Requires-Dist: typing-extensions
36
36
  Requires-Dist: useq-schema>=0.7.0
@@ -40,11 +40,12 @@ Requires-Dist: typer>=0.4.2; extra == 'cli'
40
40
  Provides-Extra: dev
41
41
  Requires-Dist: ipython; extra == 'dev'
42
42
  Requires-Dist: mypy; extra == 'dev'
43
- Requires-Dist: pdbpp; extra == 'dev'
43
+ Requires-Dist: pdbpp; (sys_platform != 'win32') and extra == 'dev'
44
44
  Requires-Dist: pre-commit; extra == 'dev'
45
45
  Requires-Dist: ruff; extra == 'dev'
46
46
  Requires-Dist: tensorstore-stubs; extra == 'dev'
47
47
  Provides-Extra: docs
48
+ Requires-Dist: mkdocs-autorefs==1.3.1; extra == 'docs'
48
49
  Requires-Dist: mkdocs-material; extra == 'docs'
49
50
  Requires-Dist: mkdocs-typer==0.0.3; extra == 'docs'
50
51
  Requires-Dist: mkdocs>=1.4; extra == 'docs'
@@ -62,8 +63,10 @@ Requires-Dist: pyside2>=5.15; extra == 'pyside2'
62
63
  Provides-Extra: pyside6
63
64
  Requires-Dist: pyside6<6.8,>=6.4.0; extra == 'pyside6'
64
65
  Provides-Extra: test
66
+ Requires-Dist: mm-device-adapters; (sys_platform == 'darwin' and platform_machine == 'x86_64') and extra == 'test'
67
+ Requires-Dist: mm-device-adapters; (sys_platform == 'win32') and extra == 'test'
65
68
  Requires-Dist: msgpack; extra == 'test'
66
- Requires-Dist: msgspec; (python_version < '3.13') and extra == 'test'
69
+ Requires-Dist: msgspec; extra == 'test'
67
70
  Requires-Dist: pytest-cov>=4; extra == 'test'
68
71
  Requires-Dist: pytest-qt>=4; extra == 'test'
69
72
  Requires-Dist: pytest>=7.3.2; extra == 'test'
@@ -99,7 +102,7 @@ environments**.
99
102
  [CMMCorePlus
100
103
  documentation](https://pymmcore-plus.github.io/pymmcore-plus/api/cmmcoreplus/)
101
104
  for details.
102
- - `pymmcore-plus` includes an [acquisition engine](https://pymmcore-plus.github.io/pymmcore-plus/guides/mda_engine/)
105
+ - `pymmcore-plus` includes an [acquisition engine](https://pymmcore-plus.github.io/pymmcore-plus/guides/mda_engine/)
103
106
  that drives micro-manager for conventional multi-dimensional experiments. It accepts an
104
107
  [MDASequence](https://pymmcore-plus.github.io/useq-schema/schema/sequence/)
105
108
  from [useq-schema](https://pymmcore-plus.github.io/useq-schema/) for
@@ -112,7 +115,7 @@ environments**.
112
115
 
113
116
  ## Documentation
114
117
 
115
- https://pymmcore-plus.github.io/pymmcore-plus/
118
+ <https://pymmcore-plus.github.io/pymmcore-plus/>
116
119
 
117
120
  ## Why not just use `pymmcore` directly?
118
121
 
@@ -139,20 +142,22 @@ python users are accustomed to. This library:
139
142
 
140
143
  ## How does this relate to `Pycro-Manager`?
141
144
 
142
- [Pycro-Manager](https://github.com/micro-manager/pycro-manager) is an impressive
143
- library written by Henry Pinkard designed to make it easier to work with and
144
- control the Java Micro-manager application using python. As such, it requires
145
- Java to be installed and running in the background (either via the micro-manager
146
- GUI application directly, or via a headless process). The python half
147
- communicates with the Java half using ZeroMQ messaging.
145
+ [Pycro-Manager](https://github.com/micro-manager/pycro-manager) is designed to
146
+ make it easier to work with and control the Java Micro-manager application
147
+ (MMStudio) using python. As such, it requires Java to be installed and for
148
+ MMStudio to be running a server in another process. The python half communicates
149
+ with the Java half using ZeroMQ messaging.
148
150
 
149
151
  **In brief**: while `Pycro-Manager` provides a python API to control the Java
150
152
  Micro-manager application (which in turn controls the C++ core), `pymmcore-plus`
151
153
  provides a python API to control the C++ core directly, without the need for
152
- Java in the loop. Each has its own advantages and disadvantages! With
153
- pycro-manager you immediately get the entire existing micro-manager ecosystem
154
- and GUI application. With pymmcore-plus you don't need to install Java, and you
155
- have direct access to the memory buffers used by the C++ core.
154
+ Java in the loop. Each has its own advantages and disadvantages! With
155
+ pycro-manager you retain the entire existing micro-manager ecosystem
156
+ and GUI application. With pymmcore-plus, the entire thing is python: you
157
+ don't need to install Java, and you have direct access to the memory buffers
158
+ used by the C++ core. Work on recreating the gui application in python
159
+ being done in [`pymmcore-widgets`](https://github.com/pymmcore-plus/pymmcore-widgets)
160
+ and [`pymmcore-gui`](https://github.com/pymmcore-plus/pymmcore-gui).
156
161
 
157
162
  ## Quickstart
158
163
 
@@ -191,7 +196,7 @@ mmcore install
191
196
 
192
197
  (you can also download these manually from [micro-manager.org](https://micro-manager.org/Micro-Manager_Nightly_Builds))
193
198
 
194
- _See [installation documentation ](https://pymmcore-plus.github.io/pymmcore-plus/install/) for more details._
199
+ _See [installation documentation](https://pymmcore-plus.github.io/pymmcore-plus/install/) for more details._
195
200
 
196
201
  ### Usage
197
202
 
@@ -1,25 +1,25 @@
1
1
  pymmcore_plus/__init__.py,sha256=9-vK2P2jkJJ2REhCjFDBbJu0wrZM0jvDcf-d2GsjTk0,1415
2
2
  pymmcore_plus/_benchmark.py,sha256=YJICxXleFQVbOluJdq4OujnIcTkkuMVzeB8GJ8nUv5I,6011
3
3
  pymmcore_plus/_build.py,sha256=RPTAuwCZWGL5IDJj4JZo1DIIouUsIqS3vnbPbG2_bRE,10993
4
- pymmcore_plus/_cli.py,sha256=FWdIvr6Rh9DVAItFaz9fWx7CbbF8ikOHWICp5h0NHTw,16393
4
+ pymmcore_plus/_cli.py,sha256=ofh7Yac4VqVuWrEYZ9ZpY4jR8FYzzxfjihvs1_gOqpM,16768
5
5
  pymmcore_plus/_logger.py,sha256=d7ldqxY0rGWORKdIzNUiFc9BW6cFBx57kHWtXyY1HE0,5416
6
- pymmcore_plus/_pymmcore.py,sha256=QGlCEEx2pz5JsRLy3nQX3afAvV-_rm6ptWsv05U5hxI,328
7
- pymmcore_plus/_util.py,sha256=mz5fuyzOhoMARyKYeri8FnR6eHwXsOh45WNZblewS1E,20435
8
- pymmcore_plus/install.py,sha256=OLKkssJbQ9VSU0Rclkke0fb4Ng1YKb3Ij9rYYbQuusM,8705
6
+ pymmcore_plus/_pymmcore.py,sha256=tcWtTRte9AFQznLGn6CmwLW0W3Rsse8N8NQ5L7JwKCc,630
7
+ pymmcore_plus/_util.py,sha256=oeJiDgt5_EoC8srmGz-QH_yNCgsxJ4k0C43qrAspL2A,22706
8
+ pymmcore_plus/install.py,sha256=U4TbQXbUc12aMtGRF_SkinNOTDCuuzIhME5Oup_4ds0,10768
9
9
  pymmcore_plus/mocks.py,sha256=jNUfmffD1OArtIvEmqWsy7GCrtTpssVF03flH8cEYx8,1867
10
10
  pymmcore_plus/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- pymmcore_plus/seq_tester.py,sha256=PJFQmb-JwGhliwIgje9BOUNcaayDsA-2x278xoumfig,3768
11
+ pymmcore_plus/seq_tester.py,sha256=ielLx2ZUJrOXVCojk64UXTeKDoARxt8QkQjt5AE5Gng,3776
12
12
  pymmcore_plus/core/__init__.py,sha256=rYHv5JQVMVDlwYD1wodCc5L9ZbpVld1C_swGx4CRogA,1011
13
13
  pymmcore_plus/core/_adapter.py,sha256=eu2BhGe_dnoQrIsh-u3poxWXsiF2Y8pfbKIGWbUgOk8,2857
14
14
  pymmcore_plus/core/_config.py,sha256=yWwOnW6f37lLt83MnodNce04az-g8YDjyo7BvMiTc8s,10672
15
15
  pymmcore_plus/core/_config_group.py,sha256=R-o4xuPDBPQAC3s-mFsiKwHVKWR38L9qq_aoWdPrAq8,8542
16
- pymmcore_plus/core/_constants.py,sha256=6foxGbek3tgnUHYUtQ7NCqwIIqqGYW1W56HjrhZqsA0,12829
16
+ pymmcore_plus/core/_constants.py,sha256=yS_YVRZQkpvTuQdOPFJdetaFpvXh4CTvd7i0qDIiAuA,13200
17
17
  pymmcore_plus/core/_device.py,sha256=Pz9Ekhss2c9IBA3B7WyMU2cCwc19Dp_dGVhMkzqUaIE,7762
18
- pymmcore_plus/core/_metadata.py,sha256=3vb3d36XgNnUY44dpZ4Ccw0tvn4KCinZ8zrnDllmABI,2645
19
- pymmcore_plus/core/_mmcore_plus.py,sha256=I5IqUmR-IgdftHO5dkvzA99kAxRZoj-8_tkTaJcfLNQ,91819
18
+ pymmcore_plus/core/_metadata.py,sha256=L8x1gX_zXPz02BUqc7eqJM_Bey2G0RyX30SOBs2aBNc,2755
19
+ pymmcore_plus/core/_mmcore_plus.py,sha256=15_QvUuURNP23rtN_nkWcqVOx6ohwizZyYPEta6O7ww,94187
20
20
  pymmcore_plus/core/_property.py,sha256=QsQEzqOAedR24zEJ1Ge4kwScfT_7NOApVcgz6QxBJrI,8265
21
- pymmcore_plus/core/_sequencing.py,sha256=Vb6hbRsb8RxSPUAlNSVWTM4Yvg7YYf9ZbewK7u_b-QM,16692
22
- pymmcore_plus/core/events/__init__.py,sha256=Bb1Ga97GzY2z3fAeJkPs1wxbTXa1o_p6nIKof_UCvZs,1093
21
+ pymmcore_plus/core/_sequencing.py,sha256=QmaCoyWzR9lX-3ldZxGYqAiEqOn8gts3X0qmskZXzQo,16887
22
+ pymmcore_plus/core/events/__init__.py,sha256=F8r10LEBLrAV8qfkXScSkpqfExdT2XoOx92OqSturpc,1078
23
23
  pymmcore_plus/core/events/_device_signal_view.py,sha256=t-NfBdg3E4rms4vDFxkkR5XtrpLxaBT7mfPwkpIsbVk,1079
24
24
  pymmcore_plus/core/events/_norm_slot.py,sha256=8DCBoLHGh7cbB1OB19IJYwL6sFBFmkD8IakfBOvFbw8,2907
25
25
  pymmcore_plus/core/events/_prop_event_mixin.py,sha256=FvJJnpEKrOR-_Sp3-NNCwFoUUHwmNKiHruo0Y1vybsY,4042
@@ -30,17 +30,17 @@ pymmcore_plus/experimental/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
30
30
  pymmcore_plus/experimental/unicore/__init__.py,sha256=OcjUZ4tq-NtWDR5R3JFivsRePliQSIQ7Z92k_8Gfz2Q,361
31
31
  pymmcore_plus/experimental/unicore/_device_manager.py,sha256=c5DAMsnK06xOy6G7YjHdUughc7xdFtzeo10woO5G_KE,6418
32
32
  pymmcore_plus/experimental/unicore/_proxy.py,sha256=Sl_Jiwd4RlcKgmsrEUNZT38YPFGlQonELAg_n3sfbdo,4020
33
- pymmcore_plus/experimental/unicore/_unicore.py,sha256=HM1rTpFFAtn5nuO9vJGsYGVkyTzeV-EY2KYdAY7EbWM,29027
33
+ pymmcore_plus/experimental/unicore/_unicore.py,sha256=3-HkZbbrQxrUgvEx3A6XEStDH9C3c6NdG_Kb8y1KubY,29029
34
34
  pymmcore_plus/experimental/unicore/devices/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
35
  pymmcore_plus/experimental/unicore/devices/_device.py,sha256=PfX4BSpVMPXyNCNWkZ0Xy1-72ZZdME5zm7NgtCRu8ts,9751
36
- pymmcore_plus/experimental/unicore/devices/_properties.py,sha256=yqVyoXb3VSbHahN2mXOIgeKOS7pUQeiJIZ_Y2d55dTI,15387
36
+ pymmcore_plus/experimental/unicore/devices/_properties.py,sha256=KuHlSNNpmfj_Q1m1uyfycpE5C63jLg5Mm_D57In2oPo,15389
37
37
  pymmcore_plus/experimental/unicore/devices/_stage.py,sha256=Ab4uibYq1cjIBtwcthCxH2FudGq9UMjub-qVeRpApQY,7892
38
38
  pymmcore_plus/mda/__init__.py,sha256=7VH-MqOcuK1JNSOG9HhES6Ac-Z-LuT8a0f2xPbGEt7w,344
39
- pymmcore_plus/mda/_engine.py,sha256=QxUXlDNITCVILfZ2GGoIDe8iSOaMXn6mswhvnsmUzm8,29914
39
+ pymmcore_plus/mda/_engine.py,sha256=U83PnmbaWChxUCEskKqHVy-Xa8KAZAMuvwPSVTAwsKg,31205
40
40
  pymmcore_plus/mda/_protocol.py,sha256=10CDJ9H57oX1z0oqK3eShXyQhufHvvu3_8wdaCYpPIg,3254
41
41
  pymmcore_plus/mda/_runner.py,sha256=NSOhpll6_WxDLO19FTs19dASJcHcOoVOCy7q_QzX_Ms,18523
42
42
  pymmcore_plus/mda/_thread_relay.py,sha256=Ww-9gyvLEzwRhnpL1dpze71wL7IRlhH8K3Q1dmJIxgs,6193
43
- pymmcore_plus/mda/events/__init__.py,sha256=rHTyhQZJ54dz-KtetvN22GvAY2ilR03x8v4H0qUR070,1191
43
+ pymmcore_plus/mda/events/__init__.py,sha256=v7YsVXzd3cTavFs2v3_PEhjqP4CtuRE0nWUoJA3CvYU,1176
44
44
  pymmcore_plus/mda/events/_protocol.py,sha256=9Q7LjYOgEWQGS8gHMV97UXM9bhoVW2OeyoPyNsQbwzw,1659
45
45
  pymmcore_plus/mda/events/_psygnal.py,sha256=TdN1mFGpTPXmEs9iwFKSC1svv87PDZkT2JZvl0tEGrQ,640
46
46
  pymmcore_plus/mda/events/_qsignals.py,sha256=tULQg-e_NX197DxJXaWHn1zLJ-4tzc9QyOAnsobEDtA,554
@@ -49,23 +49,23 @@ pymmcore_plus/mda/handlers/__init__.py,sha256=TbgpRdcs3BRdCf6uXJlwo_IIbxM6xXaLoc
49
49
  pymmcore_plus/mda/handlers/_img_sequence_writer.py,sha256=XUJovvdWViTkn2VZr4XcovNIuBNZF4J4cCHIdwAs1WE,11639
50
50
  pymmcore_plus/mda/handlers/_ome_tiff_writer.py,sha256=pqqdl3KQd0tH5Gp4rHVgYqqh2Y8iwoKRXTjwq1JLy1E,6239
51
51
  pymmcore_plus/mda/handlers/_ome_zarr_writer.py,sha256=cKg3kJR7TId6M2qC1nJMLlxkv5vlfA5XEAlTIr9kt_E,12275
52
- pymmcore_plus/mda/handlers/_tensorstore_handler.py,sha256=_Hqfgc2I8n97KPT7quU0p4tqSlomtLbTO-e78bIB6hA,15280
52
+ pymmcore_plus/mda/handlers/_tensorstore_handler.py,sha256=rgLyuTJjV1m5j-O5tLm-BggIblJpdrHa_FB17_S7rug,15282
53
53
  pymmcore_plus/mda/handlers/_util.py,sha256=pZydpKAXtQ_gjq5x1yNK1D0hfS7NUL2nH9ivOBg4abc,1600
54
54
  pymmcore_plus/metadata/__init__.py,sha256=0o_v53kwR4U_RLlCnr7GD1G6OdFlVuUByIqXiaaM5uk,699
55
- pymmcore_plus/metadata/functions.py,sha256=EjwB-6UO8c8AUriawhbE7x6ZAR1vJAxc72iYqyes5PQ,12506
56
- pymmcore_plus/metadata/schema.py,sha256=j7nMwjCBXaAC0zKA2OsF201dsOB_3b2ggjqIa7EiVPQ,17368
55
+ pymmcore_plus/metadata/functions.py,sha256=Nw2zMbJx0c6aJs6I_uaLGz6cop0IIPfRZOR-qx-SQbc,12937
56
+ pymmcore_plus/metadata/schema.py,sha256=NxKujQChIXFT48OirNebankGaHNAD0GcA77tjkG4uGs,18390
57
57
  pymmcore_plus/metadata/serialize.py,sha256=hpXJm0tzILELf6OYECMg0sQhuf-h25ob6_DDl-TUUME,3805
58
58
  pymmcore_plus/model/__init__.py,sha256=zKZkkSpNK4ERu-VMdi9gvRrj1aXAjNaYxlYB5PdYSg0,479
59
- pymmcore_plus/model/_config_file.py,sha256=nCAFh5dA7kYpoWTIwzoG4CHbdLwCYBBDGSOvZosFCFw,13711
59
+ pymmcore_plus/model/_config_file.py,sha256=ks9cR9q7G2a8xx4A8frtJbIL__KVS7j0WPyp0DPtn_g,14281
60
60
  pymmcore_plus/model/_config_group.py,sha256=vL_-EWH-Nsb8xTgFqpYIFaJzBk_RDBFchBnQ61DMSvI,3407
61
61
  pymmcore_plus/model/_core_device.py,sha256=viwMgrCTZn1XYIyjC8w4xj1XAmoowZmCb93isGbG8BE,2722
62
62
  pymmcore_plus/model/_core_link.py,sha256=dsbT0gncfa3TAORSaWUrZR9rcI_nOLX9e5BTmyo-UYo,2737
63
- pymmcore_plus/model/_device.py,sha256=-0s3NkonDoaMrNy_hn5EDz-c4o33ZiJSQkV_kdBteoo,16115
63
+ pymmcore_plus/model/_device.py,sha256=AX3rO2gbY7AXJyMN3FfI_n2jl2V0IAPuBh7MiDA5SqY,16344
64
64
  pymmcore_plus/model/_microscope.py,sha256=69VV6cuevinOK_LhYEkQygHGesvCZefdn9YNt3mV618,11353
65
- pymmcore_plus/model/_pixel_size_config.py,sha256=smoOmT54nSkg52RaSQzTFG0YwyMR_SEq_lkS-JyJW9U,3514
65
+ pymmcore_plus/model/_pixel_size_config.py,sha256=RXk8AAwARe8clsXue0GZfOTb1bxyXIsO0ibcDLHM4_s,3889
66
66
  pymmcore_plus/model/_property.py,sha256=NQzNtnEzSCR9ogwx1cfi8X-qbJ_cBSJKdSBAaoKoPQ0,3720
67
- pymmcore_plus-0.13.4.dist-info/METADATA,sha256=tmhgzAJdqyf1tO_aUNpfIslpMGlqcp49oZ2-kSX7qNs,9594
68
- pymmcore_plus-0.13.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
69
- pymmcore_plus-0.13.4.dist-info/entry_points.txt,sha256=NtFyndrQzBpUNJyil-8e5hMGke2utAf7mkGavTLcLOY,51
70
- pymmcore_plus-0.13.4.dist-info/licenses/LICENSE,sha256=OHJjRpOPKKRc7FEnpehNWdR5LRBdBhUtIFG-ZI0dCEA,1522
71
- pymmcore_plus-0.13.4.dist-info/RECORD,,
67
+ pymmcore_plus-0.13.6.dist-info/METADATA,sha256=LPZrzi2nVttLeRhGn-UlURw4NUoOVOG34fUn59_S3Yc,9961
68
+ pymmcore_plus-0.13.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
69
+ pymmcore_plus-0.13.6.dist-info/entry_points.txt,sha256=NtFyndrQzBpUNJyil-8e5hMGke2utAf7mkGavTLcLOY,51
70
+ pymmcore_plus-0.13.6.dist-info/licenses/LICENSE,sha256=OHJjRpOPKKRc7FEnpehNWdR5LRBdBhUtIFG-ZI0dCEA,1522
71
+ pymmcore_plus-0.13.6.dist-info/RECORD,,