pymmcore-plus 0.9.3__py3-none-any.whl → 0.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pymmcore_plus/__init__.py +7 -4
- pymmcore_plus/_benchmark.py +203 -0
- pymmcore_plus/_build.py +6 -1
- pymmcore_plus/_cli.py +131 -31
- pymmcore_plus/_logger.py +19 -10
- pymmcore_plus/_pymmcore.py +12 -0
- pymmcore_plus/_util.py +139 -32
- pymmcore_plus/core/__init__.py +5 -0
- pymmcore_plus/core/_config.py +6 -4
- pymmcore_plus/core/_config_group.py +4 -3
- pymmcore_plus/core/_constants.py +135 -10
- pymmcore_plus/core/_device.py +4 -4
- pymmcore_plus/core/_metadata.py +3 -3
- pymmcore_plus/core/_mmcore_plus.py +254 -170
- pymmcore_plus/core/_property.py +6 -6
- pymmcore_plus/core/_sequencing.py +370 -233
- pymmcore_plus/core/events/__init__.py +6 -6
- pymmcore_plus/core/events/_device_signal_view.py +8 -6
- pymmcore_plus/core/events/_norm_slot.py +2 -4
- pymmcore_plus/core/events/_prop_event_mixin.py +7 -4
- pymmcore_plus/core/events/_protocol.py +5 -2
- pymmcore_plus/core/events/_psygnal.py +2 -2
- pymmcore_plus/experimental/__init__.py +0 -0
- pymmcore_plus/experimental/unicore/__init__.py +14 -0
- pymmcore_plus/experimental/unicore/_device_manager.py +173 -0
- pymmcore_plus/experimental/unicore/_proxy.py +127 -0
- pymmcore_plus/experimental/unicore/_unicore.py +703 -0
- pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
- pymmcore_plus/experimental/unicore/devices/_device.py +269 -0
- pymmcore_plus/experimental/unicore/devices/_properties.py +400 -0
- pymmcore_plus/experimental/unicore/devices/_stage.py +221 -0
- pymmcore_plus/install.py +16 -11
- pymmcore_plus/mda/__init__.py +1 -1
- pymmcore_plus/mda/_engine.py +320 -148
- pymmcore_plus/mda/_protocol.py +6 -4
- pymmcore_plus/mda/_runner.py +62 -51
- pymmcore_plus/mda/_thread_relay.py +5 -3
- pymmcore_plus/mda/events/__init__.py +2 -2
- pymmcore_plus/mda/events/_protocol.py +10 -2
- pymmcore_plus/mda/events/_psygnal.py +2 -2
- pymmcore_plus/mda/handlers/_5d_writer_base.py +106 -15
- pymmcore_plus/mda/handlers/__init__.py +7 -1
- pymmcore_plus/mda/handlers/_img_sequence_writer.py +11 -6
- pymmcore_plus/mda/handlers/_ome_tiff_writer.py +8 -4
- pymmcore_plus/mda/handlers/_ome_zarr_writer.py +82 -9
- pymmcore_plus/mda/handlers/_tensorstore_handler.py +374 -0
- pymmcore_plus/mda/handlers/_util.py +1 -1
- pymmcore_plus/metadata/__init__.py +36 -0
- pymmcore_plus/metadata/functions.py +353 -0
- pymmcore_plus/metadata/schema.py +472 -0
- pymmcore_plus/metadata/serialize.py +120 -0
- pymmcore_plus/mocks.py +51 -0
- pymmcore_plus/model/_config_file.py +5 -6
- pymmcore_plus/model/_config_group.py +29 -2
- pymmcore_plus/model/_core_device.py +12 -1
- pymmcore_plus/model/_core_link.py +2 -1
- pymmcore_plus/model/_device.py +39 -8
- pymmcore_plus/model/_microscope.py +39 -3
- pymmcore_plus/model/_pixel_size_config.py +27 -4
- pymmcore_plus/model/_property.py +13 -3
- pymmcore_plus/seq_tester.py +1 -1
- {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/METADATA +22 -12
- pymmcore_plus-0.13.0.dist-info/RECORD +71 -0
- {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/WHEEL +1 -1
- pymmcore_plus/core/_state.py +0 -244
- pymmcore_plus-0.9.3.dist-info/RECORD +0 -55
- {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/licenses/LICENSE +0 -0
pymmcore_plus/_util.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import datetime
|
|
3
4
|
import importlib
|
|
4
5
|
import os
|
|
5
6
|
import platform
|
|
7
|
+
import re
|
|
6
8
|
import sys
|
|
7
9
|
import warnings
|
|
8
10
|
from collections import defaultdict
|
|
@@ -16,7 +18,9 @@ from typing import TYPE_CHECKING, cast, overload
|
|
|
16
18
|
from platformdirs import user_data_dir
|
|
17
19
|
|
|
18
20
|
if TYPE_CHECKING:
|
|
19
|
-
from
|
|
21
|
+
from collections.abc import Iterator
|
|
22
|
+
from re import Pattern
|
|
23
|
+
from typing import Any, Callable, Literal, TypeVar
|
|
20
24
|
|
|
21
25
|
QtConnectionType = Literal["AutoConnection", "DirectConnection", "QueuedConnection"]
|
|
22
26
|
|
|
@@ -35,11 +39,12 @@ except ImportError:
|
|
|
35
39
|
from contextlib import nullcontext as no_stdout
|
|
36
40
|
|
|
37
41
|
|
|
38
|
-
__all__ = ["find_micromanager", "
|
|
42
|
+
__all__ = ["find_micromanager", "no_stdout", "retry", "signals_backend"]
|
|
39
43
|
|
|
40
44
|
APP_NAME = "pymmcore-plus"
|
|
41
45
|
USER_DATA_DIR = Path(user_data_dir(appname=APP_NAME))
|
|
42
46
|
USER_DATA_MM_PATH = USER_DATA_DIR / "mm"
|
|
47
|
+
CURRENT_MM_PATH = USER_DATA_MM_PATH / ".current_mm"
|
|
43
48
|
PYMMCORE_PLUS_PATH = Path(__file__).parent.parent
|
|
44
49
|
|
|
45
50
|
|
|
@@ -57,16 +62,17 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
|
57
62
|
In order, this will look for:
|
|
58
63
|
|
|
59
64
|
1. An environment variable named `MICROMANAGER_PATH`
|
|
60
|
-
2. A
|
|
65
|
+
2. A path stored in the `CURRENT_MM_PATH` file (set by `use_micromanager`).
|
|
66
|
+
3. A `Micro-Manager*` folder in the `pymmcore-plus` user data directory
|
|
61
67
|
(this is the default install location when running `mmcore install`)
|
|
62
68
|
|
|
63
69
|
- **Windows**: C:\Users\\[user]\AppData\Local\pymmcore-plus\pymmcore-plus
|
|
64
70
|
- **macOS**: ~/Library/Application Support/pymmcore-plus
|
|
65
71
|
- **Linux**: ~/.local/share/pymmcore-plus
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
4. A `Micro-Manager*` folder in the `pymmcore_plus` package directory (this is the
|
|
68
74
|
default install location when running `python -m pymmcore_plus.install`)
|
|
69
|
-
|
|
75
|
+
5. The default micro-manager install location:
|
|
70
76
|
|
|
71
77
|
- **Windows**: `C:/Program Files/`
|
|
72
78
|
- **macOS**: `/Applications`
|
|
@@ -88,14 +94,25 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
|
88
94
|
"""
|
|
89
95
|
from ._logger import logger
|
|
90
96
|
|
|
97
|
+
# we use a dict here to avoid duplicates
|
|
98
|
+
full_list: dict[str, None] = {}
|
|
99
|
+
|
|
91
100
|
# environment variable takes precedence
|
|
92
|
-
full_list: list[str] = []
|
|
93
101
|
env_path = os.getenv("MICROMANAGER_PATH")
|
|
94
102
|
if env_path and os.path.isdir(env_path):
|
|
95
103
|
if return_first:
|
|
96
104
|
logger.debug("using MM path from env var: %s", env_path)
|
|
97
105
|
return env_path
|
|
98
|
-
full_list
|
|
106
|
+
full_list[env_path] = None
|
|
107
|
+
|
|
108
|
+
# then check for a path in CURRENT_MM_PATH
|
|
109
|
+
if CURRENT_MM_PATH.exists():
|
|
110
|
+
path = CURRENT_MM_PATH.read_text().strip()
|
|
111
|
+
if os.path.isdir(path):
|
|
112
|
+
if return_first:
|
|
113
|
+
logger.debug("using MM path from current_mm: %s", path)
|
|
114
|
+
return path
|
|
115
|
+
full_list[path] = None
|
|
99
116
|
|
|
100
117
|
# then look in user_data_dir
|
|
101
118
|
_folders = (p for p in USER_DATA_MM_PATH.glob("Micro-Manager*") if p.is_dir())
|
|
@@ -104,7 +121,8 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
|
104
121
|
if return_first:
|
|
105
122
|
logger.debug("using MM path from user install: %s", user_install[0])
|
|
106
123
|
return str(user_install[0])
|
|
107
|
-
|
|
124
|
+
for x in user_install:
|
|
125
|
+
full_list[str(x)] = None
|
|
108
126
|
|
|
109
127
|
# then look for an installation in this folder (from `pymmcore_plus.install`)
|
|
110
128
|
sfx = "_win" if os.name == "nt" else "_mac"
|
|
@@ -115,7 +133,8 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
|
115
133
|
if return_first:
|
|
116
134
|
logger.debug("using MM path from local install: %s", local_install[0])
|
|
117
135
|
return str(local_install[0])
|
|
118
|
-
|
|
136
|
+
for x in local_install:
|
|
137
|
+
full_list[str(x)] = None
|
|
119
138
|
|
|
120
139
|
applications = {
|
|
121
140
|
"darwin": Path("/Applications/"),
|
|
@@ -137,20 +156,76 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
|
137
156
|
logger.debug("using MM path found in applications: %s", pth)
|
|
138
157
|
return str(pth)
|
|
139
158
|
if pth is not None:
|
|
140
|
-
full_list
|
|
141
|
-
return full_list
|
|
159
|
+
full_list[str(pth)] = None
|
|
160
|
+
return list(full_list)
|
|
142
161
|
|
|
143
162
|
|
|
144
|
-
def
|
|
163
|
+
def _match_mm_pattern(pattern: str | Pattern[str]) -> Path | None:
|
|
164
|
+
"""Locate an existing Micro-Manager folder using a regex pattern."""
|
|
165
|
+
for _path in find_micromanager(return_first=False):
|
|
166
|
+
if not isinstance(pattern, re.Pattern):
|
|
167
|
+
pattern = str(pattern)
|
|
168
|
+
if re.search(pattern, _path) is not None:
|
|
169
|
+
return Path(_path)
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def use_micromanager(
|
|
174
|
+
path: str | Path | None = None, pattern: str | Pattern[str] | None = None
|
|
175
|
+
) -> Path | None:
|
|
176
|
+
"""Set the preferred Micro-Manager path.
|
|
177
|
+
|
|
178
|
+
This sets the preferred micromanager path, and persists across sessions.
|
|
179
|
+
This path takes precedence over everything *except* the `MICROMANAGER_PATH`
|
|
180
|
+
environment variable.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
path : str | Path | None
|
|
185
|
+
Path to an existing directory. This directory should contain micro-manager
|
|
186
|
+
device adapters. If `None`, the path will be determined using `pattern`.
|
|
187
|
+
pattern : str Pattern | | None
|
|
188
|
+
A regex pattern to match against the micromanager paths found by
|
|
189
|
+
`find_micromanager`. If no match is found, a `FileNotFoundError` will be raised.
|
|
190
|
+
"""
|
|
191
|
+
if path is None:
|
|
192
|
+
if pattern is None: # pragma: no cover
|
|
193
|
+
raise ValueError("One of 'path' or 'pattern' must be provided")
|
|
194
|
+
if (path := _match_mm_pattern(pattern)) is None:
|
|
195
|
+
options = "\n".join(find_micromanager(return_first=False))
|
|
196
|
+
raise FileNotFoundError(
|
|
197
|
+
f"No micromanager path found matching: {pattern!r}. Options:\n{options}"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if not isinstance(path, Path): # pragma: no cover
|
|
201
|
+
path = Path(path)
|
|
202
|
+
|
|
203
|
+
path = path.expanduser().resolve()
|
|
204
|
+
if not path.is_dir(): # pragma: no cover
|
|
205
|
+
if not path.exists():
|
|
206
|
+
raise FileNotFoundError(f"Path not found: {path!r}")
|
|
207
|
+
raise NotADirectoryError(f"Not a directory: {path!r}")
|
|
208
|
+
|
|
209
|
+
USER_DATA_MM_PATH.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
CURRENT_MM_PATH.write_text(str(path))
|
|
211
|
+
return path
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _imported_qt_modules() -> Iterator[str]:
|
|
145
215
|
for modname in {"PyQt5", "PySide2", "PyQt6", "PySide6"}:
|
|
146
216
|
if modname in sys.modules:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
217
|
+
yield modname
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _qt_app_is_running() -> bool:
|
|
221
|
+
for modname in _imported_qt_modules():
|
|
222
|
+
try:
|
|
223
|
+
# in broken environments modname can be a namespace package...
|
|
224
|
+
# and QtWidgets will still be unavailable
|
|
225
|
+
QtWidgets = importlib.import_module(".QtWidgets", modname)
|
|
226
|
+
except ImportError: # pragma: no cover
|
|
227
|
+
continue
|
|
228
|
+
return QtWidgets.QApplication.instance() is not None
|
|
154
229
|
return False # pragma: no cover
|
|
155
230
|
|
|
156
231
|
|
|
@@ -168,10 +243,11 @@ def signals_backend() -> Literal["qt", "psygnal"]:
|
|
|
168
243
|
)
|
|
169
244
|
env_var = "auto"
|
|
170
245
|
|
|
246
|
+
qt_app_running = _qt_app_is_running()
|
|
171
247
|
if env_var == "auto":
|
|
172
|
-
return "qt" if
|
|
248
|
+
return "qt" if qt_app_running else "psygnal"
|
|
173
249
|
if env_var == "qt":
|
|
174
|
-
if
|
|
250
|
+
if qt_app_running or list(_imported_qt_modules()):
|
|
175
251
|
return "qt"
|
|
176
252
|
warnings.warn(
|
|
177
253
|
f"{MMCORE_PLUS_SIGNALS_BACKEND} set to 'qt', but no Qt app is running. "
|
|
@@ -342,13 +418,10 @@ def _sorted_rows(data: dict, sort: str | None) -> list[tuple]:
|
|
|
342
418
|
"""Return a list of rows, sorted by the given column name."""
|
|
343
419
|
rows = list(zip(*data.values()))
|
|
344
420
|
if sort is not None:
|
|
345
|
-
|
|
421
|
+
with suppress(ValueError):
|
|
422
|
+
# silently ignore if the sort column is not found
|
|
346
423
|
sort_idx = [x.lower() for x in data].index(sort.lower())
|
|
347
|
-
|
|
348
|
-
raise ValueError(
|
|
349
|
-
f"invalid sort column: {sort!r}. Must be one of {list(data)}"
|
|
350
|
-
) from None
|
|
351
|
-
rows.sort(key=lambda x: x[sort_idx])
|
|
424
|
+
rows.sort(key=lambda x: x[sort_idx])
|
|
352
425
|
return rows
|
|
353
426
|
|
|
354
427
|
|
|
@@ -440,9 +513,13 @@ def listeners_connected(
|
|
|
440
513
|
|
|
441
514
|
ctype = getattr(Qt.ConnectionType, qt_connection_type)
|
|
442
515
|
token = signal.connect(slot, ctype) # type: ignore
|
|
443
|
-
tokens[attr_name].add(token)
|
|
444
516
|
else:
|
|
445
|
-
|
|
517
|
+
token = signal.connect(slot)
|
|
518
|
+
|
|
519
|
+
# This only seems to happen on PySide2
|
|
520
|
+
if token is None or isinstance(token, bool):
|
|
521
|
+
token = slot
|
|
522
|
+
tokens[attr_name].add(token)
|
|
446
523
|
|
|
447
524
|
try:
|
|
448
525
|
yield
|
|
@@ -470,16 +547,25 @@ def system_info() -> dict[str, str]:
|
|
|
470
547
|
|
|
471
548
|
This backs the `mmcore info` command in the CLI.
|
|
472
549
|
"""
|
|
473
|
-
import pymmcore
|
|
474
|
-
|
|
475
550
|
import pymmcore_plus
|
|
476
551
|
|
|
477
552
|
info = {
|
|
478
553
|
"python": sys.version,
|
|
479
554
|
"platform": platform.platform(),
|
|
480
555
|
"pymmcore-plus": getattr(pymmcore_plus, "__version__", "err"),
|
|
481
|
-
"pymmcore": getattr(pymmcore, "__version__", "err"),
|
|
482
556
|
}
|
|
557
|
+
try:
|
|
558
|
+
import pymmcore
|
|
559
|
+
|
|
560
|
+
info["pymmcore"] = getattr(pymmcore, "__version__", "err")
|
|
561
|
+
except ImportError:
|
|
562
|
+
info["pymmcore"] = ""
|
|
563
|
+
try:
|
|
564
|
+
import pymmcore_nano
|
|
565
|
+
|
|
566
|
+
info["pymmcore-nano"] = getattr(pymmcore_nano, "__version__", "err")
|
|
567
|
+
except ImportError:
|
|
568
|
+
info["pymmcore-nano"] = ""
|
|
483
569
|
|
|
484
570
|
with suppress(Exception):
|
|
485
571
|
core = pymmcore_plus.CMMCorePlus.instance()
|
|
@@ -511,3 +597,24 @@ def system_info() -> dict[str, str]:
|
|
|
511
597
|
info["qt"] = f"{API_NAME} {QT_VERSION}"
|
|
512
598
|
|
|
513
599
|
return info
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
if sys.version_info < (3, 11):
|
|
603
|
+
|
|
604
|
+
def _utcnow() -> datetime.datetime:
|
|
605
|
+
return datetime.datetime.utcnow()
|
|
606
|
+
else:
|
|
607
|
+
|
|
608
|
+
def _utcnow() -> datetime.datetime:
|
|
609
|
+
return datetime.datetime.now(datetime.UTC)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def timestamp() -> str:
|
|
613
|
+
"""Return the current timestamp, try using local timezone, in ISO format.
|
|
614
|
+
|
|
615
|
+
YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM
|
|
616
|
+
"""
|
|
617
|
+
now = _utcnow()
|
|
618
|
+
with suppress(Exception):
|
|
619
|
+
now = now.astimezone()
|
|
620
|
+
return now.isoformat()
|
pymmcore_plus/core/__init__.py
CHANGED
|
@@ -15,8 +15,11 @@ __all__ = [
|
|
|
15
15
|
"FocusDirection",
|
|
16
16
|
"Keyword",
|
|
17
17
|
"Metadata",
|
|
18
|
+
"PixelFormat",
|
|
18
19
|
"PortType",
|
|
19
20
|
"PropertyType",
|
|
21
|
+
"SequencedEvent",
|
|
22
|
+
"iter_sequenced_events",
|
|
20
23
|
]
|
|
21
24
|
|
|
22
25
|
from ._adapter import DeviceAdapter
|
|
@@ -32,6 +35,7 @@ from ._constants import (
|
|
|
32
35
|
DeviceType,
|
|
33
36
|
FocusDirection,
|
|
34
37
|
Keyword,
|
|
38
|
+
PixelFormat,
|
|
35
39
|
PortType,
|
|
36
40
|
PropertyType,
|
|
37
41
|
)
|
|
@@ -39,3 +43,4 @@ from ._device import Device
|
|
|
39
43
|
from ._metadata import Metadata
|
|
40
44
|
from ._mmcore_plus import CMMCorePlus
|
|
41
45
|
from ._property import DeviceProperty
|
|
46
|
+
from ._sequencing import SequencedEvent, iter_sequenced_events
|
pymmcore_plus/core/_config.py
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections import defaultdict
|
|
6
|
-
from typing import TYPE_CHECKING, Any,
|
|
6
|
+
from typing import TYPE_CHECKING, Any, overload
|
|
7
7
|
|
|
8
|
-
import pymmcore
|
|
8
|
+
import pymmcore_plus._pymmcore as pymmcore
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Iterable, Iterator
|
|
12
|
+
|
|
11
13
|
from typing_extensions import TypeAlias # py310
|
|
12
14
|
|
|
13
|
-
DevPropValueTuple: TypeAlias =
|
|
14
|
-
DevPropTuple: TypeAlias =
|
|
15
|
+
DevPropValueTuple: TypeAlias = tuple[str, str, str]
|
|
16
|
+
DevPropTuple: TypeAlias = tuple[str, str]
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class Configuration(pymmcore.Configuration):
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Iterator, MutableMapping
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Literal, overload
|
|
4
5
|
|
|
5
|
-
import pymmcore
|
|
6
|
+
import pymmcore_plus._pymmcore as pymmcore
|
|
6
7
|
|
|
7
8
|
from ._config import Configuration
|
|
8
9
|
from ._property import DeviceProperty
|
|
@@ -72,7 +73,7 @@ class ConfigGroup(MutableMapping[str, Configuration]):
|
|
|
72
73
|
def __getitem__(self, configName: str) -> Configuration:
|
|
73
74
|
try:
|
|
74
75
|
return self._mmc.getConfigData(self._name, configName)
|
|
75
|
-
except ValueError as e:
|
|
76
|
+
except (ValueError, RuntimeError) as e:
|
|
76
77
|
if configName not in self:
|
|
77
78
|
raise KeyError(
|
|
78
79
|
f"Group {self._name!r} does not have a config {configName!r}"
|
pymmcore_plus/core/_constants.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from enum import Enum, IntEnum
|
|
3
|
+
from enum import Enum, IntEnum, auto
|
|
4
|
+
from typing import Any, Literal
|
|
4
5
|
|
|
5
|
-
import pymmcore
|
|
6
|
+
import pymmcore_plus._pymmcore as pymmcore
|
|
6
7
|
|
|
7
8
|
# NOTE: by using pymmcore.attributes, we guarantee that the values are the same
|
|
8
9
|
# however, we also risk AttributeErrors in the future.
|
|
@@ -156,6 +157,7 @@ class PropertyType(IntEnum):
|
|
|
156
157
|
String = pymmcore.String
|
|
157
158
|
Float = pymmcore.Float
|
|
158
159
|
Integer = pymmcore.Integer
|
|
160
|
+
Boolean = auto() # not supported in pymmcore
|
|
159
161
|
|
|
160
162
|
def to_python(self) -> type | None:
|
|
161
163
|
return {0: None, 1: str, 2: float, 3: int}[self]
|
|
@@ -163,8 +165,31 @@ class PropertyType(IntEnum):
|
|
|
163
165
|
def to_json(self) -> str:
|
|
164
166
|
return {0: "null", 1: "string", 2: "number", 3: "integer"}[self]
|
|
165
167
|
|
|
166
|
-
def __repr__(self) -> str:
|
|
167
|
-
return getattr(self.to_python(), "__name__", "
|
|
168
|
+
def __repr__(self) -> Literal["undefined", "float", "int", "str"]:
|
|
169
|
+
return getattr(self.to_python(), "__name__", "undefined")
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def create(cls, value: Any) -> PropertyType:
|
|
173
|
+
if isinstance(value, PropertyType):
|
|
174
|
+
return value
|
|
175
|
+
if value is None:
|
|
176
|
+
return PropertyType.Undef
|
|
177
|
+
if isinstance(value, str):
|
|
178
|
+
return PropertyType[value.lower().capitalize()]
|
|
179
|
+
if isinstance(value, type):
|
|
180
|
+
if value is float:
|
|
181
|
+
return PropertyType.Float
|
|
182
|
+
elif value is int:
|
|
183
|
+
return PropertyType.Integer
|
|
184
|
+
elif value is str:
|
|
185
|
+
return PropertyType.String
|
|
186
|
+
elif value is bool:
|
|
187
|
+
return PropertyType.Boolean
|
|
188
|
+
|
|
189
|
+
raise TypeError(
|
|
190
|
+
f"Property type must be a PropertyType enum member, "
|
|
191
|
+
f"a string, or a type. Got: {type(value)}"
|
|
192
|
+
)
|
|
168
193
|
|
|
169
194
|
|
|
170
195
|
class ActionType(IntEnum):
|
|
@@ -185,13 +210,13 @@ class PortType(IntEnum):
|
|
|
185
210
|
|
|
186
211
|
|
|
187
212
|
class FocusDirection(IntEnum):
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
213
|
+
Unknown = pymmcore.FocusDirectionUnknown
|
|
214
|
+
TowardSample = pymmcore.FocusDirectionTowardSample
|
|
215
|
+
AwayFromSample = pymmcore.FocusDirectionAwayFromSample
|
|
191
216
|
# aliases
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
217
|
+
FocusDirectionUnknown = Unknown
|
|
218
|
+
FocusDirectionTowardSample = TowardSample
|
|
219
|
+
FocusDirectionAwayFromSample = AwayFromSample
|
|
195
220
|
|
|
196
221
|
|
|
197
222
|
class DeviceNotification(IntEnum):
|
|
@@ -222,6 +247,12 @@ class DeviceInitializationState(IntEnum):
|
|
|
222
247
|
|
|
223
248
|
|
|
224
249
|
class PixelType(str, Enum):
|
|
250
|
+
"""These are pixel types, as used in MMStudio and MMCoreJ wrapper.
|
|
251
|
+
|
|
252
|
+
They are only here for supporting the legacy (and probably to-be-deprecated)
|
|
253
|
+
taggedImages.
|
|
254
|
+
"""
|
|
255
|
+
|
|
225
256
|
UNKNOWN = ""
|
|
226
257
|
GRAY8 = "GRAY8"
|
|
227
258
|
GRAY16 = "GRAY16"
|
|
@@ -239,3 +270,97 @@ class PixelType(str, Enum):
|
|
|
239
270
|
depth, cls.UNKNOWN
|
|
240
271
|
)
|
|
241
272
|
return cls.GRAY32 if n_comp == 1 else cls.RGB32
|
|
273
|
+
|
|
274
|
+
def to_pixel_format(self) -> PixelFormat:
|
|
275
|
+
return {
|
|
276
|
+
self.GRAY8: PixelFormat.MONO8,
|
|
277
|
+
self.GRAY16: PixelFormat.MONO16,
|
|
278
|
+
self.GRAY32: PixelFormat.MONO32,
|
|
279
|
+
self.RGB32: PixelFormat.RGB8,
|
|
280
|
+
self.RGB64: PixelFormat.RGB16,
|
|
281
|
+
}[self]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class PixelFormat(str, Enum):
|
|
285
|
+
"""Subset of GeniCam Pixel Format names used by pymmcore-plus.
|
|
286
|
+
|
|
287
|
+
(This is similar to PixelType, but follows GeniCam standards.)
|
|
288
|
+
|
|
289
|
+
See <https://docs.baslerweb.com/pixel-format#unpacked-and-packed-pixel-formats>
|
|
290
|
+
for helpful clarifications. Note that **unpacked** pixel formats (like
|
|
291
|
+
Mono8, Mono12, Mono16) are always 8-bit aligned. Meaning Mono12 is actually
|
|
292
|
+
a 16-bit buffer.
|
|
293
|
+
|
|
294
|
+
Attributes
|
|
295
|
+
----------
|
|
296
|
+
MONO8 : str
|
|
297
|
+
8-bit (unpacked) monochrome pixel format.
|
|
298
|
+
MONO10 : str
|
|
299
|
+
10-bit (unpacked) monochrome pixel format. (16-bit buffer)
|
|
300
|
+
MONO12 : str
|
|
301
|
+
12-bit (unpacked) monochrome pixel format. (16-bit buffer)
|
|
302
|
+
MONO14 : str
|
|
303
|
+
14-bit (unpacked) monochrome pixel format. (16-bit buffer)
|
|
304
|
+
MONO16 : str
|
|
305
|
+
16-bit (unpacked) monochrome pixel format
|
|
306
|
+
MONO32 : str
|
|
307
|
+
32-bit (unpacked) monochrome pixel format
|
|
308
|
+
RGB8 : str
|
|
309
|
+
8-bit RGB pixel format. (24-bit buffer)
|
|
310
|
+
RGB10 : str
|
|
311
|
+
10-bit RGB pixel format. (48-bit buffer)
|
|
312
|
+
RGB12 : str
|
|
313
|
+
12-bit RGB pixel format. (48-bit buffer)
|
|
314
|
+
RGB14 : str
|
|
315
|
+
14-bit RGB pixel format. (48-bit buffer)
|
|
316
|
+
RGB16 : str
|
|
317
|
+
16-bit RGB pixel format. (48-bit buffer)
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
MONO8 = "Mono8"
|
|
321
|
+
MONO10 = "Mono10"
|
|
322
|
+
MONO12 = "Mono12"
|
|
323
|
+
MONO14 = "Mono14"
|
|
324
|
+
MONO16 = "Mono16"
|
|
325
|
+
MONO32 = "Mono32"
|
|
326
|
+
RGB8 = "RGB8"
|
|
327
|
+
RGB10 = "RGB10"
|
|
328
|
+
RGB12 = "RGB12"
|
|
329
|
+
RGB14 = "RGB14"
|
|
330
|
+
RGB16 = "RGB16"
|
|
331
|
+
|
|
332
|
+
@classmethod
|
|
333
|
+
def pick(cls, bit_depth: int, n_comp: int = 1) -> PixelFormat:
|
|
334
|
+
try:
|
|
335
|
+
return PIXEL_FORMATS[n_comp][bit_depth]
|
|
336
|
+
except KeyError as e:
|
|
337
|
+
raise NotImplementedError(
|
|
338
|
+
f"Unsupported Pixel Format {bit_depth=} {n_comp=}"
|
|
339
|
+
) from e
|
|
340
|
+
|
|
341
|
+
@classmethod
|
|
342
|
+
def for_current_camera(cls, core: pymmcore.CMMCore) -> PixelFormat:
|
|
343
|
+
n_comp = core.getNumberOfComponents()
|
|
344
|
+
if n_comp == 4:
|
|
345
|
+
n_comp = 3
|
|
346
|
+
return cls.pick(core.getImageBitDepth(), n_comp)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# map of {number of components: {bit depth: PixelFormat}}
|
|
350
|
+
PIXEL_FORMATS: dict[int, dict[int, PixelFormat]] = {
|
|
351
|
+
1: {
|
|
352
|
+
8: PixelFormat.MONO8,
|
|
353
|
+
10: PixelFormat.MONO10,
|
|
354
|
+
12: PixelFormat.MONO12,
|
|
355
|
+
14: PixelFormat.MONO14,
|
|
356
|
+
16: PixelFormat.MONO16,
|
|
357
|
+
32: PixelFormat.MONO32,
|
|
358
|
+
},
|
|
359
|
+
3: {
|
|
360
|
+
8: PixelFormat.RGB8,
|
|
361
|
+
10: PixelFormat.RGB10,
|
|
362
|
+
12: PixelFormat.RGB12,
|
|
363
|
+
14: PixelFormat.RGB14,
|
|
364
|
+
16: PixelFormat.RGB16,
|
|
365
|
+
},
|
|
366
|
+
}
|
pymmcore_plus/core/_device.py
CHANGED
|
@@ -43,12 +43,12 @@ class Device:
|
|
|
43
43
|
>>> device.schema() # JSON schema of device properties
|
|
44
44
|
"""
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
UNASSIGNED = "__UNASSIGNED__"
|
|
47
47
|
propertyChanged: PSignalInstance
|
|
48
48
|
|
|
49
49
|
def __init__(
|
|
50
50
|
self,
|
|
51
|
-
device_label: str =
|
|
51
|
+
device_label: str = UNASSIGNED,
|
|
52
52
|
mmcore: CMMCorePlus | None = None,
|
|
53
53
|
adapter_name: str = "",
|
|
54
54
|
device_name: str = "",
|
|
@@ -162,7 +162,7 @@ class Device:
|
|
|
162
162
|
raise TypeError("Must specify device_name")
|
|
163
163
|
if device_label:
|
|
164
164
|
self.label = device_label
|
|
165
|
-
elif self.label == self.
|
|
165
|
+
elif self.label == self.UNASSIGNED:
|
|
166
166
|
self.label = f"{adapter_name}-{device_name}"
|
|
167
167
|
|
|
168
168
|
self._mmc.loadDevice(self.label, adapter_name, device_name)
|
|
@@ -208,7 +208,7 @@ class Device:
|
|
|
208
208
|
def __repr__(self) -> str:
|
|
209
209
|
if self.isLoaded():
|
|
210
210
|
n = len(self.propertyNames())
|
|
211
|
-
props = f
|
|
211
|
+
props = f"{n} {'properties' if n > 1 else 'property'}"
|
|
212
212
|
lib = f"({self.library()}::{self.name()}) "
|
|
213
213
|
else:
|
|
214
214
|
props = "NOT LOADED"
|
pymmcore_plus/core/_metadata.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"""pythonic wrapper on pymmcore.Metadata object."""
|
|
2
2
|
|
|
3
|
-
from collections.abc import Mapping
|
|
3
|
+
from collections.abc import ItemsView, Iterator, KeysView, Mapping, ValuesView
|
|
4
4
|
from types import new_class
|
|
5
|
-
from typing import Any,
|
|
5
|
+
from typing import Any, cast
|
|
6
6
|
|
|
7
|
-
import pymmcore
|
|
7
|
+
import pymmcore_plus._pymmcore as pymmcore
|
|
8
8
|
|
|
9
9
|
_NULL = object()
|
|
10
10
|
|