pymmcore-plus 0.13.5__py3-none-any.whl → 0.13.7__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
@@ -1,4 +1,5 @@
1
1
  # do NOT use __future__.annotations here. It breaks typer.
2
+ import contextlib
2
3
  import os
3
4
  import shutil
4
5
  import subprocess
@@ -8,6 +9,7 @@ from contextlib import suppress
8
9
  from pathlib import Path
9
10
  from typing import Optional, Union, cast
10
11
 
12
+ from pymmcore_plus._util import get_device_interface_version
11
13
  from pymmcore_plus.core._device import Device
12
14
  from pymmcore_plus.core._mmcore_plus import CMMCorePlus
13
15
 
@@ -114,9 +116,15 @@ def _list() -> None:
114
116
  for parent, items in found.items():
115
117
  print(f":file_folder:[bold green] {parent}")
116
118
  for item in items:
119
+ version = ""
120
+ for _lib in (parent / item).glob("*_dal_*"):
121
+ with suppress(Exception):
122
+ div = get_device_interface_version(_lib)
123
+ version = f" (Dev. Interface {div})"
124
+ break
117
125
  bullet = " [bold yellow]*" if first else " •"
118
126
  using = " [bold blue](active)" if first else ""
119
- print(f"{bullet} [cyan]{item}{using}")
127
+ print(f"{bullet} [cyan]{item}{version}{using}")
120
128
  first = False
121
129
  else:
122
130
  print(":x: [bold red]There are no pymmcore-plus Micro-Manager files.")
@@ -136,12 +144,13 @@ def mmstudio() -> None: # pragma: no cover
136
144
  if mm
137
145
  else None
138
146
  )
139
- if not app: # pragma: no cover
147
+ if not mm or not app: # pragma: no cover
140
148
  print(f":x: [bold red]No MMStudio application found in {mm!r}")
141
149
  print("[magenta]run `mmcore install` to install a version of Micro-Manager")
142
150
  raise typer.Exit(1)
143
151
  cmd = ["open", "-a", str(app)] if PLATFORM == "Darwin" else [str(app)]
144
- raise typer.Exit(subprocess.run(cmd).returncode)
152
+ with contextlib.chdir(mm):
153
+ raise typer.Exit(subprocess.run(cmd).returncode)
145
154
 
146
155
 
147
156
  @app.command()
@@ -157,7 +166,7 @@ def install(
157
166
  help="Installation directory.",
158
167
  ),
159
168
  release: str = typer.Option(
160
- "latest", "-r", "--release", help="Release date. e.g. 20210201"
169
+ "latest-compatible", "-r", "--release", help="Release date. e.g. 20210201"
161
170
  ),
162
171
  plain_output: bool = typer.Option(
163
172
  False,
@@ -1,5 +1,8 @@
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__
@@ -8,7 +11,20 @@ try:
8
11
  NANO = True
9
12
  except ImportError:
10
13
  from pymmcore import * # noqa F403
11
- from pymmcore import __version__ # noqa F401
14
+ from pymmcore import __version__
12
15
 
13
16
  BACKEND = "pymmcore"
14
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
 
@@ -148,13 +181,17 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
148
181
  app_path = applications[sys.platform]
149
182
  pth = next(app_path.glob("[m,M]icro-[m,M]anager*"), None)
150
183
  if return_first:
151
- if pth is None:
152
- logger.error(
153
- "could not find micromanager directory. Please run 'mmcore install'"
154
- )
155
- return None
156
- logger.debug("using MM path found in applications: %s", pth)
157
- return str(pth)
184
+ if pth and _mm_path_has_compatible_div(pth): # pragma: no cover
185
+ logger.debug("using MM path found in applications: %s", pth)
186
+ return str(pth)
187
+ from . import _pymmcore
188
+
189
+ div = _pymmcore.version_info.device_interface
190
+ logger.error(
191
+ f"could not find micromanager directory for device interface {div}. "
192
+ "Please run 'mmcore install'"
193
+ )
194
+ return None
158
195
  if pth is not None:
159
196
  full_list[str(pth)] = None
160
197
  return list(full_list)
@@ -229,15 +266,15 @@ def _qt_app_is_running() -> bool:
229
266
  return False # pragma: no cover
230
267
 
231
268
 
232
- MMCORE_PLUS_SIGNALS_BACKEND = "MMCORE_PLUS_SIGNALS_BACKEND"
269
+ PYMM_SIGNALS_BACKEND = "PYMM_SIGNALS_BACKEND"
233
270
 
234
271
 
235
272
  def signals_backend() -> Literal["qt", "psygnal"]:
236
273
  """Return the name of the event backend to use."""
237
- env_var = os.environ.get(MMCORE_PLUS_SIGNALS_BACKEND, "auto").lower()
274
+ env_var = os.environ.get(PYMM_SIGNALS_BACKEND, "auto").lower()
238
275
  if env_var not in {"qt", "psygnal", "auto"}:
239
276
  warnings.warn(
240
- f"{MMCORE_PLUS_SIGNALS_BACKEND} must be one of ['qt', 'psygnal', 'auto']. "
277
+ f"{PYMM_SIGNALS_BACKEND} must be one of ['qt', 'psygnal', 'auto']. "
241
278
  f"not: {env_var!r}. Using 'auto'.",
242
279
  stacklevel=1,
243
280
  )
@@ -250,7 +287,7 @@ def signals_backend() -> Literal["qt", "psygnal"]:
250
287
  if qt_app_running or list(_imported_qt_modules()):
251
288
  return "qt"
252
289
  warnings.warn(
253
- f"{MMCORE_PLUS_SIGNALS_BACKEND} set to 'qt', but no Qt app is running. "
290
+ f"{PYMM_SIGNALS_BACKEND} set to 'qt', but no Qt app is running. "
254
291
  "Falling back to 'psygnal'.",
255
292
  stacklevel=1,
256
293
  )
@@ -618,3 +655,34 @@ def timestamp() -> str:
618
655
  with suppress(Exception):
619
656
  now = now.astimezone()
620
657
  return now.isoformat()
658
+
659
+
660
+ def get_device_interface_version(lib_path: str | Path) -> int:
661
+ """Return the device interface version from the given library path."""
662
+ import ctypes
663
+
664
+ if sys.platform.startswith("win"):
665
+ lib = ctypes.WinDLL(str(lib_path))
666
+ else:
667
+ lib = ctypes.CDLL(str(lib_path))
668
+
669
+ try:
670
+ func = lib.GetDeviceInterfaceVersion
671
+ except AttributeError:
672
+ raise RuntimeError(
673
+ f"Function 'GetDeviceInterfaceVersion' not found in {lib_path}"
674
+ ) from None
675
+
676
+ func.restype = ctypes.c_long
677
+ func.argtypes = []
678
+ return func() # type: ignore[no-any-return]
679
+
680
+
681
+ def _mm_path_has_compatible_div(folder: Path | str) -> bool:
682
+ from . import _pymmcore
683
+
684
+ div = _pymmcore.version_info.device_interface
685
+ for lib_path in Path(folder).glob("*mmgr_dal*"):
686
+ with suppress(Exception):
687
+ return get_device_interface_version(lib_path) == div
688
+ 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
 
@@ -207,14 +207,39 @@ 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):
@@ -354,7 +379,7 @@ class CMMCorePlus(pymmcore.CMMCore):
354
379
  """
355
380
  try:
356
381
  super().loadDevice(label, moduleName, deviceName)
357
- except RuntimeError as e:
382
+ except (RuntimeError, ValueError) as e:
358
383
  if exc := self._load_error_with_info(label, moduleName, deviceName, str(e)):
359
384
  raise exc from e
360
385
 
@@ -2001,9 +2026,10 @@ class CMMCorePlus(pymmcore.CMMCore):
2001
2026
  **Why Override?** To also save pixel size configurations.
2002
2027
  """
2003
2028
  super().saveSystemConfiguration(filename)
2004
- # saveSystemConfiguration does not save the pixel size config so here
2005
- # we add to the saved file also any pixel size config.
2006
- 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)
2007
2033
 
2008
2034
  def _save_pixel_configurations(self, filename: str) -> None:
2009
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
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",
@@ -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]]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymmcore-plus
3
- Version: 0.13.5
3
+ Version: 0.13.7
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
@@ -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'
@@ -63,7 +64,7 @@ Provides-Extra: pyside6
63
64
  Requires-Dist: pyside6<6.8,>=6.4.0; extra == 'pyside6'
64
65
  Provides-Extra: test
65
66
  Requires-Dist: msgpack; extra == 'test'
66
- Requires-Dist: msgspec; (python_version < '3.13') and extra == 'test'
67
+ Requires-Dist: msgspec; extra == 'test'
67
68
  Requires-Dist: pytest-cov>=4; extra == 'test'
68
69
  Requires-Dist: pytest-qt>=4; extra == 'test'
69
70
  Requires-Dist: pytest>=7.3.2; extra == 'test'
@@ -1,11 +1,11 @@
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=vysJhdBua23NFSILU0CSMs84hjKVbQl4ttUnk5ouMsk,16395
4
+ pymmcore_plus/_cli.py,sha256=wmqlG8UGBNiTefRppSnRHwnD3w-5DAhN168lIBujbRA,16831
5
5
  pymmcore_plus/_logger.py,sha256=d7ldqxY0rGWORKdIzNUiFc9BW6cFBx57kHWtXyY1HE0,5416
6
- pymmcore_plus/_pymmcore.py,sha256=HzV-vW7QT0BhkIh7aWwQKwAtmt27iNnHGzXXzg3_6n0,361
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=VGcb_nQu6BBWZsQihed0TrCHdTRADKlBo_qSnIbeF0Q,22814
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
11
  pymmcore_plus/seq_tester.py,sha256=ielLx2ZUJrOXVCojk64UXTeKDoARxt8QkQjt5AE5Gng,3776
@@ -13,12 +13,12 @@ pymmcore_plus/core/__init__.py,sha256=rYHv5JQVMVDlwYD1wodCc5L9ZbpVld1C_swGx4CRog
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
18
  pymmcore_plus/core/_metadata.py,sha256=L8x1gX_zXPz02BUqc7eqJM_Bey2G0RyX30SOBs2aBNc,2755
19
- pymmcore_plus/core/_mmcore_plus.py,sha256=XEdoYyMPfk4zJxXqSzVaq7J5_qtngNYP7ebf_blaOQY,92887
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
21
+ pymmcore_plus/core/_sequencing.py,sha256=QmaCoyWzR9lX-3ldZxGYqAiEqOn8gts3X0qmskZXzQo,16887
22
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
@@ -52,20 +52,20 @@ pymmcore_plus/mda/handlers/_ome_zarr_writer.py,sha256=cKg3kJR7TId6M2qC1nJMLlxkv5
52
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.5.dist-info/METADATA,sha256=HKYfO-wVXV-thoxMJNDaIejT08X5m81K915GzyCEH4c,9710
68
- pymmcore_plus-0.13.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
69
- pymmcore_plus-0.13.5.dist-info/entry_points.txt,sha256=NtFyndrQzBpUNJyil-8e5hMGke2utAf7mkGavTLcLOY,51
70
- pymmcore_plus-0.13.5.dist-info/licenses/LICENSE,sha256=OHJjRpOPKKRc7FEnpehNWdR5LRBdBhUtIFG-ZI0dCEA,1522
71
- pymmcore_plus-0.13.5.dist-info/RECORD,,
67
+ pymmcore_plus-0.13.7.dist-info/METADATA,sha256=Xf0FN1PVDxE4QgDL3JaKGuGutrKAnTOm1nfb0OUdFDU,9765
68
+ pymmcore_plus-0.13.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
69
+ pymmcore_plus-0.13.7.dist-info/entry_points.txt,sha256=NtFyndrQzBpUNJyil-8e5hMGke2utAf7mkGavTLcLOY,51
70
+ pymmcore_plus-0.13.7.dist-info/licenses/LICENSE,sha256=OHJjRpOPKKRc7FEnpehNWdR5LRBdBhUtIFG-ZI0dCEA,1522
71
+ pymmcore_plus-0.13.7.dist-info/RECORD,,