pymmcore-plus 0.12.0__tar.gz → 0.13.1__tar.gz
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-0.12.0 → pymmcore_plus-0.13.1}/.gitignore +1 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/PKG-INFO +14 -6
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/pyproject.toml +16 -8
- pymmcore_plus-0.13.1/src/pymmcore_plus/_benchmark.py +203 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/_cli.py +78 -13
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/_logger.py +10 -2
- pymmcore_plus-0.13.1/src/pymmcore_plus/_pymmcore.py +12 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/_util.py +16 -10
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/__init__.py +3 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/_config.py +1 -1
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/_config_group.py +2 -2
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/_constants.py +27 -3
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/_device.py +4 -4
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/_metadata.py +1 -1
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/_mmcore_plus.py +184 -118
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/_property.py +3 -5
- pymmcore_plus-0.13.1/src/pymmcore_plus/core/_sequencing.py +434 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/events/__init__.py +3 -3
- pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/unicore/__init__.py +14 -0
- pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/unicore/_device_manager.py +173 -0
- pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/unicore/_proxy.py +127 -0
- pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/unicore/_unicore.py +703 -0
- pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
- pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/unicore/devices/_device.py +269 -0
- pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/unicore/devices/_properties.py +400 -0
- pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/unicore/devices/_stage.py +221 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/install.py +10 -7
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/_engine.py +152 -43
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/_runner.py +8 -1
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/events/__init__.py +2 -2
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/handlers/_ome_zarr_writer.py +2 -2
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/handlers/_tensorstore_handler.py +6 -2
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/metadata/functions.py +18 -8
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/metadata/schema.py +6 -5
- pymmcore_plus-0.13.1/src/pymmcore_plus/mocks.py +49 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/_config_file.py +1 -1
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/_core_device.py +10 -1
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/_device.py +17 -6
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/_property.py +11 -2
- pymmcore_plus-0.13.1/src/pymmcore_plus/py.typed +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/seq_tester.py +1 -1
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/conftest.py +43 -9
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/io/test_zarr_writers.py +14 -2
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_adapter_class.py +1 -1
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_cli.py +36 -11
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_core.py +32 -78
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_events.py +26 -19
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_mda.py +75 -21
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_model.py +9 -1
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_pixel_config_class.py +1 -1
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_sequencing.py +15 -28
- pymmcore_plus-0.13.1/tests/test_slm_image.py +68 -0
- pymmcore_plus-0.13.1/tests/unicore/test_unicore.py +286 -0
- pymmcore_plus-0.13.1/tests/unicore/test_xy_stage.py +199 -0
- pymmcore_plus-0.12.0/src/pymmcore_plus/core/_sequencing.py +0 -299
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/LICENSE +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/README.md +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/__init__.py +3 -3
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/_build.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/_adapter.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/events/_device_signal_view.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/events/_norm_slot.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/events/_prop_event_mixin.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/events/_protocol.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/events/_psygnal.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/events/_qsignals.py +0 -0
- pymmcore_plus-0.12.0/src/pymmcore_plus/py.typed → pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/__init__.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/__init__.py +1 -1
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/_protocol.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/_thread_relay.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/events/_protocol.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/events/_psygnal.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/events/_qsignals.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/handlers/_5d_writer_base.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/handlers/__init__.py +1 -1
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/handlers/_img_sequence_writer.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/handlers/_ome_tiff_writer.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/handlers/_util.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/metadata/__init__.py +3 -3
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/metadata/serialize.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/__init__.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/_config_group.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/_core_link.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/_microscope.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/_pixel_size_config.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/__init__.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/io/test_image_sequence_writer.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/io/test_ome_tiff.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/local_config.cfg +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_bench.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_config_group_class.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_device_class.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_metadata.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_misc.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_property_class.py +0 -0
- {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_thread_relay.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pymmcore-plus
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.1
|
|
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
|
|
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
24
|
Classifier: Topic :: System :: Hardware
|
|
24
25
|
Classifier: Topic :: System :: Hardware :: Hardware Drivers
|
|
25
26
|
Classifier: Topic :: Utilities
|
|
@@ -29,11 +30,10 @@ Requires-Dist: platformdirs>=3.0.0
|
|
|
29
30
|
Requires-Dist: psygnal>=0.7
|
|
30
31
|
Requires-Dist: pymmcore>=10.7.0.71.0
|
|
31
32
|
Requires-Dist: rich>=10.2.0
|
|
32
|
-
Requires-Dist: tensorstore
|
|
33
|
+
Requires-Dist: tensorstore; python_version < '3.13'
|
|
33
34
|
Requires-Dist: typer>=0.4.2
|
|
34
35
|
Requires-Dist: typing-extensions
|
|
35
|
-
Requires-Dist: useq-schema>=0.
|
|
36
|
-
Requires-Dist: wrapt>=1.14
|
|
36
|
+
Requires-Dist: useq-schema>=0.6.2
|
|
37
37
|
Provides-Extra: cli
|
|
38
38
|
Requires-Dist: rich>=10.2.0; extra == 'cli'
|
|
39
39
|
Requires-Dist: typer>=0.4.2; extra == 'cli'
|
|
@@ -53,9 +53,17 @@ Requires-Dist: mkdocstrings==0.22.0; extra == 'docs'
|
|
|
53
53
|
Provides-Extra: io
|
|
54
54
|
Requires-Dist: tifffile>=2021.6.14; extra == 'io'
|
|
55
55
|
Requires-Dist: zarr<3,>=2.2; extra == 'io'
|
|
56
|
+
Provides-Extra: pyqt5
|
|
57
|
+
Requires-Dist: pyqt5>=5.15.4; extra == 'pyqt5'
|
|
58
|
+
Provides-Extra: pyqt6
|
|
59
|
+
Requires-Dist: pyqt6<6.8,>=6.4.2; extra == 'pyqt6'
|
|
60
|
+
Provides-Extra: pyside2
|
|
61
|
+
Requires-Dist: pyside2>=5.15; extra == 'pyside2'
|
|
62
|
+
Provides-Extra: pyside6
|
|
63
|
+
Requires-Dist: pyside6<6.8,>=6.4.0; extra == 'pyside6'
|
|
56
64
|
Provides-Extra: test
|
|
57
65
|
Requires-Dist: msgpack; extra == 'test'
|
|
58
|
-
Requires-Dist: msgspec; extra == 'test'
|
|
66
|
+
Requires-Dist: msgspec; (python_version < '3.13') and extra == 'test'
|
|
59
67
|
Requires-Dist: pytest-cov>=4; extra == 'test'
|
|
60
68
|
Requires-Dist: pytest-qt>=4; extra == 'test'
|
|
61
69
|
Requires-Dist: pytest>=7.3.2; extra == 'test'
|
|
@@ -28,6 +28,7 @@ classifiers = [
|
|
|
28
28
|
"Programming Language :: Python :: 3.10",
|
|
29
29
|
"Programming Language :: Python :: 3.11",
|
|
30
30
|
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Programming Language :: Python :: 3.13",
|
|
31
32
|
"Topic :: System :: Hardware",
|
|
32
33
|
"Topic :: System :: Hardware :: Hardware Drivers",
|
|
33
34
|
"Topic :: Utilities",
|
|
@@ -38,10 +39,9 @@ dependencies = [
|
|
|
38
39
|
"numpy >=1.17.3",
|
|
39
40
|
"psygnal >=0.7",
|
|
40
41
|
"pymmcore >=10.7.0.71.0",
|
|
41
|
-
"typing-extensions",
|
|
42
|
-
"useq-schema >=0.
|
|
43
|
-
"
|
|
44
|
-
"tensorstore",
|
|
42
|
+
"typing-extensions", # not actually required at runtime
|
|
43
|
+
"useq-schema >=0.6.2",
|
|
44
|
+
"tensorstore; python_version < '3.13'",
|
|
45
45
|
# cli requirements included by default for now
|
|
46
46
|
"typer >=0.4.2",
|
|
47
47
|
"rich >=10.2.0",
|
|
@@ -52,8 +52,12 @@ dependencies = [
|
|
|
52
52
|
[project.optional-dependencies]
|
|
53
53
|
cli = ["typer >=0.4.2", "rich >=10.2.0"]
|
|
54
54
|
io = ["tifffile >=2021.6.14", "zarr >=2.2,<3"]
|
|
55
|
+
PySide2 = ["PySide2 >=5.15"]
|
|
56
|
+
PySide6 = ["PySide6 >=6.4.0,<6.8"]
|
|
57
|
+
PyQt5 = ["PyQt5 >=5.15.4"]
|
|
58
|
+
PyQt6 = ["PyQt6 >=6.4.2,<6.8"]
|
|
55
59
|
test = [
|
|
56
|
-
"msgspec",
|
|
60
|
+
"msgspec; python_version < '3.13'",
|
|
57
61
|
"msgpack",
|
|
58
62
|
"pytest-cov >=4",
|
|
59
63
|
"pytest-qt >=4",
|
|
@@ -98,7 +102,7 @@ include = ["/src", "/tests"]
|
|
|
98
102
|
only-include = ["src"]
|
|
99
103
|
sources = ["src"]
|
|
100
104
|
|
|
101
|
-
# https://
|
|
105
|
+
# https://docs.astral.sh/ruff/rules/
|
|
102
106
|
[tool.ruff]
|
|
103
107
|
line-length = 88
|
|
104
108
|
target-version = "py39"
|
|
@@ -117,7 +121,7 @@ select = [
|
|
|
117
121
|
"A001", # flake8-builtins
|
|
118
122
|
"RUF", # ruff-specific rules
|
|
119
123
|
"TID", # tidy
|
|
120
|
-
"
|
|
124
|
+
"TC", # typecheck
|
|
121
125
|
"SLF", # private-access
|
|
122
126
|
]
|
|
123
127
|
ignore = [
|
|
@@ -141,6 +145,7 @@ docstring-code-format = true
|
|
|
141
145
|
minversion = "6.0"
|
|
142
146
|
testpaths = ["tests"]
|
|
143
147
|
filterwarnings = ["error", "ignore:Failed to disconnect::pytestqt"]
|
|
148
|
+
markers = ["run_last: mark a test to run last"]
|
|
144
149
|
|
|
145
150
|
# https://mypy.readthedocs.io/en/stable/config_file.html
|
|
146
151
|
[tool.mypy]
|
|
@@ -151,6 +156,9 @@ disallow_subclassing_any = false
|
|
|
151
156
|
show_error_codes = true
|
|
152
157
|
pretty = true
|
|
153
158
|
plugins = "pydantic.mypy"
|
|
159
|
+
# see https://github.com/python/mypy/issues/5374 and related discussions
|
|
160
|
+
# it causes more pain than it solves
|
|
161
|
+
disable_error_code = ["type-abstract"]
|
|
154
162
|
|
|
155
163
|
[[tool.mypy.overrides]]
|
|
156
164
|
module = ["tests.*"]
|
|
@@ -185,4 +193,4 @@ ignore = [
|
|
|
185
193
|
]
|
|
186
194
|
|
|
187
195
|
[tool.typos.default]
|
|
188
|
-
extend-ignore-identifiers-re = ["(?i)nd2?.*", "(?i)ome"]
|
|
196
|
+
extend-ignore-identifiers-re = ["(?i)nd2?.*", "(?i)ome", "anager"]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import timeit
|
|
4
|
+
import warnings
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from pymmcore_plus import CMMCorePlus, DeviceType
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Iterable, Iterator, Sequence
|
|
11
|
+
|
|
12
|
+
from pymmcore_plus.core._device import Device
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Benchmark:
|
|
16
|
+
device_type = DeviceType.Camera
|
|
17
|
+
|
|
18
|
+
def __init__(self, core: CMMCorePlus, label: str = "") -> None:
|
|
19
|
+
self.core = core
|
|
20
|
+
self.label = label
|
|
21
|
+
|
|
22
|
+
def setup(self) -> None:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
def device(self) -> Device | None:
|
|
26
|
+
if self.label is not None:
|
|
27
|
+
return self.core.getDeviceObject(self.label)
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
def run(self, number: int) -> Iterator[tuple[str, float | str]]:
|
|
31
|
+
# get methods in the order of definition, in reverse MRO order
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
self.setup()
|
|
35
|
+
except Exception as e: # pragma: no cover
|
|
36
|
+
warnings.warn(
|
|
37
|
+
f"Setup failed on device {self.label!r}: {e}",
|
|
38
|
+
RuntimeWarning,
|
|
39
|
+
stacklevel=2,
|
|
40
|
+
)
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
methods: list[str] = []
|
|
44
|
+
for base in reversed(type(self).mro()):
|
|
45
|
+
methods.extend(m for m in base.__dict__ if m.startswith("bench_"))
|
|
46
|
+
|
|
47
|
+
for method_name in methods:
|
|
48
|
+
try:
|
|
49
|
+
t = timeit.timeit(getattr(self, method_name), number=number)
|
|
50
|
+
result: float | str = round(1000 * t / number, 3)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
result = str(e)
|
|
53
|
+
yield method_name[6:], result
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CoreBenchmark(Benchmark):
|
|
57
|
+
device_type = DeviceType.Core
|
|
58
|
+
|
|
59
|
+
def bench_getDeviceAdapterNames(self) -> None:
|
|
60
|
+
self.core.getDeviceAdapterNames()
|
|
61
|
+
|
|
62
|
+
def bench_getLoadedDevices(self) -> None:
|
|
63
|
+
self.core.getLoadedDevices()
|
|
64
|
+
|
|
65
|
+
def bench_getSystemState(self) -> None:
|
|
66
|
+
self.core.getSystemState()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CameraBenchmark(Benchmark):
|
|
70
|
+
device_type = DeviceType.Camera
|
|
71
|
+
|
|
72
|
+
def setup(self) -> None:
|
|
73
|
+
self.core.setCameraDevice(self.label)
|
|
74
|
+
self.core.setExposure(self.label, 1)
|
|
75
|
+
|
|
76
|
+
def bench_getMultiROI(self) -> None:
|
|
77
|
+
self.core.getMultiROI()
|
|
78
|
+
|
|
79
|
+
def bench_getExposure(self) -> None:
|
|
80
|
+
self.core.getExposure(self.label)
|
|
81
|
+
|
|
82
|
+
def bench_snapImage(self) -> None:
|
|
83
|
+
self.core.snapImage()
|
|
84
|
+
|
|
85
|
+
def bench_getImage(self) -> None:
|
|
86
|
+
self.core.getImage()
|
|
87
|
+
|
|
88
|
+
def bench_getImageWidth(self) -> None:
|
|
89
|
+
self.core.getImageWidth()
|
|
90
|
+
|
|
91
|
+
def bench_getImageHeight(self) -> None:
|
|
92
|
+
self.core.getImageHeight()
|
|
93
|
+
|
|
94
|
+
def bench_getImageBufferSize(self) -> None:
|
|
95
|
+
self.core.getImageBufferSize()
|
|
96
|
+
|
|
97
|
+
def bench_getImageBitDepth(self) -> None:
|
|
98
|
+
self.core.getImageBitDepth()
|
|
99
|
+
|
|
100
|
+
def bench_getNumberOfComponents(self) -> None:
|
|
101
|
+
self.core.getNumberOfComponents()
|
|
102
|
+
|
|
103
|
+
def bench_getNumberOfCameraChannels(self) -> None:
|
|
104
|
+
self.core.getNumberOfCameraChannels()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class XYStageBenchmark(Benchmark):
|
|
108
|
+
device_type = DeviceType.XYStage
|
|
109
|
+
|
|
110
|
+
def setup(self) -> None:
|
|
111
|
+
self.core.setXYStageDevice(self.label)
|
|
112
|
+
self.position = self.core.getXYPosition(self.label)
|
|
113
|
+
|
|
114
|
+
def bench_getXYPosition(self) -> None:
|
|
115
|
+
self.core.getXYPosition(self.label)
|
|
116
|
+
|
|
117
|
+
def bench_getXPosition(self) -> None:
|
|
118
|
+
self.core.getXPosition(self.label)
|
|
119
|
+
|
|
120
|
+
def bench_getYPosition(self) -> None:
|
|
121
|
+
self.core.getYPosition(self.label)
|
|
122
|
+
|
|
123
|
+
def bench_setXYPosition(self) -> None:
|
|
124
|
+
self.core.setXYPosition(self.label, *self.position)
|
|
125
|
+
|
|
126
|
+
def bench_setRelativeXYPosition(self) -> None:
|
|
127
|
+
self.core.setRelativeXYPosition(self.label, 0, 0)
|
|
128
|
+
|
|
129
|
+
def bench_isXYStageSequenceable(self) -> None:
|
|
130
|
+
self.core.isXYStageSequenceable(self.label)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class StageBenchmark(Benchmark):
|
|
134
|
+
device_type = DeviceType.Stage
|
|
135
|
+
|
|
136
|
+
def setup(self) -> None:
|
|
137
|
+
self.position = self.core.getPosition(self.label)
|
|
138
|
+
|
|
139
|
+
def bench_getPosition(self) -> None:
|
|
140
|
+
self.core.getPosition(self.label)
|
|
141
|
+
|
|
142
|
+
def bench_setPosition(self) -> None:
|
|
143
|
+
self.core.setPosition(self.label, self.position)
|
|
144
|
+
|
|
145
|
+
def bench_setRelativePosition(self) -> None:
|
|
146
|
+
self.core.setRelativePosition(self.label, 0)
|
|
147
|
+
|
|
148
|
+
def bench_isStageSequenceable(self) -> None:
|
|
149
|
+
self.core.isStageSequenceable(self.label)
|
|
150
|
+
|
|
151
|
+
def bench_isStageLinearSequenceable(self) -> None:
|
|
152
|
+
self.core.isStageLinearSequenceable(self.label)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class StateBenchmark(Benchmark):
|
|
156
|
+
device_type = DeviceType.State
|
|
157
|
+
|
|
158
|
+
def setup(self) -> None:
|
|
159
|
+
self.initial_state = self.core.getState(self.label)
|
|
160
|
+
try:
|
|
161
|
+
self.labels: Sequence[str] = self.core.getStateLabels(self.label)
|
|
162
|
+
except Exception:
|
|
163
|
+
self.labels = []
|
|
164
|
+
|
|
165
|
+
def bench_getState(self) -> None:
|
|
166
|
+
self.core.getState(self.label)
|
|
167
|
+
|
|
168
|
+
def bench_setState(self) -> None:
|
|
169
|
+
self.core.setState(self.label, self.initial_state)
|
|
170
|
+
|
|
171
|
+
def bench_getNumberOfStates(self) -> None:
|
|
172
|
+
self.core.getNumberOfStates(self.label)
|
|
173
|
+
|
|
174
|
+
def bench_getStateLabel(self) -> None:
|
|
175
|
+
self.core.getStateLabel(self.label)
|
|
176
|
+
|
|
177
|
+
def bench_getStateFromLabel(self) -> None:
|
|
178
|
+
for label in self.labels:
|
|
179
|
+
self.core.getStateFromLabel(self.label, label)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def benchmark_core_and_devices(
|
|
183
|
+
core: CMMCorePlus, number: int = 100
|
|
184
|
+
) -> Iterable[Device | None | tuple[str, float | str]]:
|
|
185
|
+
"""Take an initialized core with devices and benchmark various methods.
|
|
186
|
+
|
|
187
|
+
Yields
|
|
188
|
+
------
|
|
189
|
+
Device | None | tuple[str, float | str]
|
|
190
|
+
If a `Device`, it is the device object being benchmarked.
|
|
191
|
+
If None, it is the core object being benchmarked.
|
|
192
|
+
If a tuple, it is the method name and the time taken to run it.
|
|
193
|
+
"""
|
|
194
|
+
for cls in Benchmark.__subclasses__():
|
|
195
|
+
if cls.device_type == DeviceType.Core:
|
|
196
|
+
bench = cls(core, "Core")
|
|
197
|
+
yield bench.device()
|
|
198
|
+
yield from bench.run(number)
|
|
199
|
+
else:
|
|
200
|
+
for dev in core.getLoadedDevicesOfType(cls.device_type):
|
|
201
|
+
bench = cls(core, dev)
|
|
202
|
+
yield bench.device()
|
|
203
|
+
yield from bench.run(number)
|
|
@@ -8,6 +8,9 @@ from contextlib import suppress
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Optional, Union, cast
|
|
10
10
|
|
|
11
|
+
from pymmcore_plus.core._device import Device
|
|
12
|
+
from pymmcore_plus.core._mmcore_plus import CMMCorePlus
|
|
13
|
+
|
|
11
14
|
try:
|
|
12
15
|
import typer
|
|
13
16
|
from rich import print
|
|
@@ -28,14 +31,31 @@ app = typer.Typer(name="mmcore", no_args_is_help=True)
|
|
|
28
31
|
|
|
29
32
|
def _show_version_and_exit(value: bool) -> None:
|
|
30
33
|
if value:
|
|
31
|
-
import pymmcore
|
|
32
|
-
|
|
33
34
|
typer.echo(f"pymmcore-plus v{pymmcore_plus.__version__}")
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
try:
|
|
36
|
+
import pymmcore_nano as pymmcore
|
|
37
|
+
|
|
38
|
+
typer.echo(f"pymmcore-nano v{pymmcore.__version__}")
|
|
39
|
+
except ImportError:
|
|
40
|
+
import pymmcore
|
|
41
|
+
|
|
42
|
+
typer.echo(f"pymmcore v{pymmcore.__version__}")
|
|
43
|
+
typer.echo(f"MMCore v{pymmcore.CMMCore().getVersionInfo()}")
|
|
44
|
+
typer.echo(f"{pymmcore.CMMCore().getAPIVersionInfo()}")
|
|
36
45
|
raise typer.Exit()
|
|
37
46
|
|
|
38
47
|
|
|
48
|
+
CONFIG_PARAM = typer.Option(
|
|
49
|
+
None,
|
|
50
|
+
"-c",
|
|
51
|
+
"--config",
|
|
52
|
+
dir_okay=False,
|
|
53
|
+
exists=True,
|
|
54
|
+
resolve_path=True,
|
|
55
|
+
help="Path to Micro-Manager system configuration file.",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
39
59
|
@app.callback()
|
|
40
60
|
def _main(
|
|
41
61
|
version: Optional[bool] = typer.Option(
|
|
@@ -168,15 +188,7 @@ def run(
|
|
|
168
188
|
resolve_path=True,
|
|
169
189
|
help="Path to useq-schema file.",
|
|
170
190
|
),
|
|
171
|
-
config: Optional[Path] =
|
|
172
|
-
None,
|
|
173
|
-
"-c",
|
|
174
|
-
"--config",
|
|
175
|
-
dir_okay=False,
|
|
176
|
-
exists=True,
|
|
177
|
-
resolve_path=True,
|
|
178
|
-
help="Path to Micro-Manager system configuration file.",
|
|
179
|
-
),
|
|
191
|
+
config: Optional[Path] = CONFIG_PARAM,
|
|
180
192
|
z_go_up: Optional[bool] = typer.Option(None, help="Acquire from bottom to top."),
|
|
181
193
|
z_top: Optional[float] = typer.Option(None, help="Top of z-stack."),
|
|
182
194
|
z_bottom: Optional[float] = typer.Option(None, help="Bottom of z-stack."),
|
|
@@ -423,5 +435,58 @@ def _tail_file(file_path: Union[str, Path], interval: float = 0.1) -> None:
|
|
|
423
435
|
time.sleep(1)
|
|
424
436
|
|
|
425
437
|
|
|
438
|
+
@app.command()
|
|
439
|
+
def bench(
|
|
440
|
+
config: Optional[Path] = CONFIG_PARAM,
|
|
441
|
+
number: int = typer.Option(
|
|
442
|
+
10, "-n", "--number", help="Number of iterations for each test."
|
|
443
|
+
),
|
|
444
|
+
) -> None:
|
|
445
|
+
"""Run a benchmark of Core and Devices loaded with `config` (or Demo)."""
|
|
446
|
+
from rich.console import Console
|
|
447
|
+
from rich.live import Live
|
|
448
|
+
from rich.table import Table
|
|
449
|
+
|
|
450
|
+
from pymmcore_plus._benchmark import benchmark_core_and_devices
|
|
451
|
+
|
|
452
|
+
console = Console()
|
|
453
|
+
|
|
454
|
+
core = CMMCorePlus()
|
|
455
|
+
if config is not None:
|
|
456
|
+
console.log(
|
|
457
|
+
f"Loading config {config} ...",
|
|
458
|
+
style="bright_blue",
|
|
459
|
+
end="",
|
|
460
|
+
)
|
|
461
|
+
core.loadSystemConfiguration(str(config))
|
|
462
|
+
else:
|
|
463
|
+
console.log("Loading DEMO configuration ...", style="bright_blue", end="")
|
|
464
|
+
core.loadSystemConfiguration()
|
|
465
|
+
console.log("Loaded.", style="bright_blue")
|
|
466
|
+
|
|
467
|
+
table = Table()
|
|
468
|
+
table.add_column("Method")
|
|
469
|
+
table.add_column("Time (ms)")
|
|
470
|
+
|
|
471
|
+
with Live(table, console=console, refresh_per_second=4):
|
|
472
|
+
for item in benchmark_core_and_devices(core, number):
|
|
473
|
+
if item is None:
|
|
474
|
+
table.add_row("Device: Core", "------", style="yellow")
|
|
475
|
+
elif isinstance(item, Device):
|
|
476
|
+
console.print(
|
|
477
|
+
f"Measuring ({item.type()}) Device: "
|
|
478
|
+
f"{item.label!r} <{item.library()}::{item.name()}>"
|
|
479
|
+
f": {item.description()}",
|
|
480
|
+
style="#333333",
|
|
481
|
+
)
|
|
482
|
+
table.add_row(f"Device: {item.label}", "------", style="yellow")
|
|
483
|
+
else:
|
|
484
|
+
method, time = item
|
|
485
|
+
if isinstance(time, float):
|
|
486
|
+
table.add_row(method, f"{time:.4f}")
|
|
487
|
+
else:
|
|
488
|
+
table.add_row(method, str(time), style="red")
|
|
489
|
+
|
|
490
|
+
|
|
426
491
|
def main() -> None: # pragma: no cover
|
|
427
492
|
app()
|
|
@@ -118,9 +118,17 @@ def configure_logging(
|
|
|
118
118
|
|
|
119
119
|
# automatically log to stderr
|
|
120
120
|
if log_to_stderr and sys.stderr:
|
|
121
|
-
|
|
121
|
+
# try to use rich for stderr logging
|
|
122
|
+
# fallback to plain text if rich is not installed
|
|
123
|
+
try:
|
|
124
|
+
from rich.logging import RichHandler
|
|
125
|
+
|
|
126
|
+
stderr_handler: logging.Handler = RichHandler()
|
|
127
|
+
except ImportError:
|
|
128
|
+
stderr_handler = logging.StreamHandler(sys.stderr)
|
|
129
|
+
stderr_handler.setFormatter(CustomFormatter())
|
|
130
|
+
|
|
122
131
|
stderr_handler.setLevel(stderr_level)
|
|
123
|
-
stderr_handler.setFormatter(CustomFormatter())
|
|
124
132
|
logger.addHandler(stderr_handler)
|
|
125
133
|
|
|
126
134
|
# automatically log to file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Internal module to choose between pymmcore and pymmcore-nano."""
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from pymmcore_nano import * # noqa F403
|
|
5
|
+
from pymmcore_nano import __version__
|
|
6
|
+
|
|
7
|
+
BACKEND = "pymmcore-nano"
|
|
8
|
+
except ImportError:
|
|
9
|
+
from pymmcore import * # noqa F403
|
|
10
|
+
from pymmcore import __version__ # noqa F401
|
|
11
|
+
|
|
12
|
+
BACKEND = "pymmcore"
|
|
@@ -39,7 +39,7 @@ except ImportError:
|
|
|
39
39
|
from contextlib import nullcontext as no_stdout
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
__all__ = ["find_micromanager", "
|
|
42
|
+
__all__ = ["find_micromanager", "no_stdout", "retry", "signals_backend"]
|
|
43
43
|
|
|
44
44
|
APP_NAME = "pymmcore-plus"
|
|
45
45
|
USER_DATA_DIR = Path(user_data_dir(appname=APP_NAME))
|
|
@@ -418,13 +418,10 @@ def _sorted_rows(data: dict, sort: str | None) -> list[tuple]:
|
|
|
418
418
|
"""Return a list of rows, sorted by the given column name."""
|
|
419
419
|
rows = list(zip(*data.values()))
|
|
420
420
|
if sort is not None:
|
|
421
|
-
|
|
421
|
+
with suppress(ValueError):
|
|
422
|
+
# silently ignore if the sort column is not found
|
|
422
423
|
sort_idx = [x.lower() for x in data].index(sort.lower())
|
|
423
|
-
|
|
424
|
-
raise ValueError(
|
|
425
|
-
f"invalid sort column: {sort!r}. Must be one of {list(data)}"
|
|
426
|
-
) from None
|
|
427
|
-
rows.sort(key=lambda x: x[sort_idx])
|
|
424
|
+
rows.sort(key=lambda x: x[sort_idx])
|
|
428
425
|
return rows
|
|
429
426
|
|
|
430
427
|
|
|
@@ -550,16 +547,25 @@ def system_info() -> dict[str, str]:
|
|
|
550
547
|
|
|
551
548
|
This backs the `mmcore info` command in the CLI.
|
|
552
549
|
"""
|
|
553
|
-
import pymmcore
|
|
554
|
-
|
|
555
550
|
import pymmcore_plus
|
|
556
551
|
|
|
557
552
|
info = {
|
|
558
553
|
"python": sys.version,
|
|
559
554
|
"platform": platform.platform(),
|
|
560
555
|
"pymmcore-plus": getattr(pymmcore_plus, "__version__", "err"),
|
|
561
|
-
"pymmcore": getattr(pymmcore, "__version__", "err"),
|
|
562
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"] = ""
|
|
563
569
|
|
|
564
570
|
with suppress(Exception):
|
|
565
571
|
core = pymmcore_plus.CMMCorePlus.instance()
|
|
@@ -18,6 +18,8 @@ __all__ = [
|
|
|
18
18
|
"PixelFormat",
|
|
19
19
|
"PortType",
|
|
20
20
|
"PropertyType",
|
|
21
|
+
"SequencedEvent",
|
|
22
|
+
"iter_sequenced_events",
|
|
21
23
|
]
|
|
22
24
|
|
|
23
25
|
from ._adapter import DeviceAdapter
|
|
@@ -41,3 +43,4 @@ from ._device import Device
|
|
|
41
43
|
from ._metadata import Metadata
|
|
42
44
|
from ._mmcore_plus import CMMCorePlus
|
|
43
45
|
from ._property import DeviceProperty
|
|
46
|
+
from ._sequencing import SequencedEvent, iter_sequenced_events
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import Iterator, MutableMapping
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Literal, overload
|
|
5
5
|
|
|
6
|
-
import pymmcore
|
|
6
|
+
import pymmcore_plus._pymmcore as pymmcore
|
|
7
7
|
|
|
8
8
|
from ._config import Configuration
|
|
9
9
|
from ._property import DeviceProperty
|
|
@@ -73,7 +73,7 @@ class ConfigGroup(MutableMapping[str, Configuration]):
|
|
|
73
73
|
def __getitem__(self, configName: str) -> Configuration:
|
|
74
74
|
try:
|
|
75
75
|
return self._mmc.getConfigData(self._name, configName)
|
|
76
|
-
except ValueError as e:
|
|
76
|
+
except (ValueError, RuntimeError) as e:
|
|
77
77
|
if configName not in self:
|
|
78
78
|
raise KeyError(
|
|
79
79
|
f"Group {self._name!r} does not have a config {configName!r}"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from enum import Enum, IntEnum
|
|
4
|
-
from typing import Literal
|
|
3
|
+
from enum import Enum, IntEnum, auto
|
|
4
|
+
from typing import Any, Literal
|
|
5
5
|
|
|
6
|
-
import pymmcore
|
|
6
|
+
import pymmcore_plus._pymmcore as pymmcore
|
|
7
7
|
|
|
8
8
|
# NOTE: by using pymmcore.attributes, we guarantee that the values are the same
|
|
9
9
|
# however, we also risk AttributeErrors in the future.
|
|
@@ -157,6 +157,7 @@ class PropertyType(IntEnum):
|
|
|
157
157
|
String = pymmcore.String
|
|
158
158
|
Float = pymmcore.Float
|
|
159
159
|
Integer = pymmcore.Integer
|
|
160
|
+
Boolean = auto() # not supported in pymmcore
|
|
160
161
|
|
|
161
162
|
def to_python(self) -> type | None:
|
|
162
163
|
return {0: None, 1: str, 2: float, 3: int}[self]
|
|
@@ -167,6 +168,29 @@ class PropertyType(IntEnum):
|
|
|
167
168
|
def __repr__(self) -> Literal["undefined", "float", "int", "str"]:
|
|
168
169
|
return getattr(self.to_python(), "__name__", "undefined")
|
|
169
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
|
+
)
|
|
193
|
+
|
|
170
194
|
|
|
171
195
|
class ActionType(IntEnum):
|
|
172
196
|
NoAction = pymmcore.NoAction
|
|
@@ -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"
|