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 +10 -3
- pymmcore_plus/_pymmcore.py +19 -1
- pymmcore_plus/_util.py +80 -15
- pymmcore_plus/core/_constants.py +8 -0
- pymmcore_plus/core/_metadata.py +8 -5
- pymmcore_plus/core/_mmcore_plus.py +68 -23
- pymmcore_plus/core/_sequencing.py +5 -2
- pymmcore_plus/core/events/__init__.py +1 -1
- pymmcore_plus/experimental/unicore/_unicore.py +1 -1
- pymmcore_plus/experimental/unicore/devices/_properties.py +1 -1
- pymmcore_plus/install.py +68 -7
- pymmcore_plus/mda/_engine.py +55 -19
- pymmcore_plus/mda/events/__init__.py +1 -1
- pymmcore_plus/mda/handlers/_tensorstore_handler.py +1 -1
- pymmcore_plus/metadata/functions.py +9 -0
- pymmcore_plus/metadata/schema.py +19 -0
- pymmcore_plus/model/_config_file.py +16 -3
- pymmcore_plus/model/_device.py +6 -0
- pymmcore_plus/model/_pixel_size_config.py +9 -0
- pymmcore_plus/seq_tester.py +4 -4
- {pymmcore_plus-0.13.4.dist-info → pymmcore_plus-0.13.6.dist-info}/METADATA +22 -17
- {pymmcore_plus-0.13.4.dist-info → pymmcore_plus-0.13.6.dist-info}/RECORD +25 -25
- {pymmcore_plus-0.13.4.dist-info → pymmcore_plus-0.13.6.dist-info}/WHEEL +0 -0
- {pymmcore_plus-0.13.4.dist-info → pymmcore_plus-0.13.6.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.13.4.dist-info → pymmcore_plus-0.13.6.dist-info}/licenses/LICENSE +0 -0
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)
|
pymmcore_plus/_pymmcore.py
CHANGED
|
@@ -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__
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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(
|
|
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"{
|
|
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"{
|
|
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
|
pymmcore_plus/core/_constants.py
CHANGED
|
@@ -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
|
|
pymmcore_plus/core/_metadata.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
217
|
-
|
|
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
|
-
|
|
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.
|
|
403
|
-
super().loadSystemConfiguration(self.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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,
|
|
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")
|
|
209
|
-
|
|
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",
|
pymmcore_plus/mda/_engine.py
CHANGED
|
@@ -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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
434
|
-
self.
|
|
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
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
|
@@ -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
|
|
pymmcore_plus/metadata/schema.py
CHANGED
|
@@ -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.
|
pymmcore_plus/model/_device.py
CHANGED
|
@@ -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]]:
|
pymmcore_plus/seq_tester.py
CHANGED
|
@@ -88,8 +88,8 @@ class Setting:
|
|
|
88
88
|
|
|
89
89
|
@classmethod
|
|
90
90
|
def _from_list(cls, val: list) -> Self:
|
|
91
|
-
(dev, prop), (
|
|
92
|
-
return cls(dev, prop,
|
|
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), (
|
|
104
|
-
return cls(dev, prop,
|
|
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.
|
|
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
|
|
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;
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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.
|
|
153
|
-
pycro-manager you
|
|
154
|
-
and GUI application. With pymmcore-plus
|
|
155
|
-
have direct access to the memory buffers
|
|
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
|
|
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=
|
|
4
|
+
pymmcore_plus/_cli.py,sha256=ofh7Yac4VqVuWrEYZ9ZpY4jR8FYzzxfjihvs1_gOqpM,16768
|
|
5
5
|
pymmcore_plus/_logger.py,sha256=d7ldqxY0rGWORKdIzNUiFc9BW6cFBx57kHWtXyY1HE0,5416
|
|
6
|
-
pymmcore_plus/_pymmcore.py,sha256=
|
|
7
|
-
pymmcore_plus/_util.py,sha256=
|
|
8
|
-
pymmcore_plus/install.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
19
|
-
pymmcore_plus/core/_mmcore_plus.py,sha256=
|
|
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=
|
|
22
|
-
pymmcore_plus/core/events/__init__.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
56
|
-
pymmcore_plus/metadata/schema.py,sha256=
|
|
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=
|
|
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
|
|
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=
|
|
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.
|
|
68
|
-
pymmcore_plus-0.13.
|
|
69
|
-
pymmcore_plus-0.13.
|
|
70
|
-
pymmcore_plus-0.13.
|
|
71
|
-
pymmcore_plus-0.13.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|