pymmcore-plus 0.9.4__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.
Files changed (68) hide show
  1. pymmcore_plus/__init__.py +7 -4
  2. pymmcore_plus/_benchmark.py +203 -0
  3. pymmcore_plus/_build.py +6 -1
  4. pymmcore_plus/_cli.py +131 -31
  5. pymmcore_plus/_logger.py +19 -10
  6. pymmcore_plus/_pymmcore.py +12 -0
  7. pymmcore_plus/_util.py +133 -30
  8. pymmcore_plus/core/__init__.py +5 -0
  9. pymmcore_plus/core/_config.py +6 -4
  10. pymmcore_plus/core/_config_group.py +4 -3
  11. pymmcore_plus/core/_constants.py +135 -10
  12. pymmcore_plus/core/_device.py +4 -4
  13. pymmcore_plus/core/_metadata.py +3 -3
  14. pymmcore_plus/core/_mmcore_plus.py +254 -170
  15. pymmcore_plus/core/_property.py +6 -6
  16. pymmcore_plus/core/_sequencing.py +370 -233
  17. pymmcore_plus/core/events/__init__.py +6 -6
  18. pymmcore_plus/core/events/_device_signal_view.py +8 -6
  19. pymmcore_plus/core/events/_norm_slot.py +2 -4
  20. pymmcore_plus/core/events/_prop_event_mixin.py +7 -4
  21. pymmcore_plus/core/events/_protocol.py +5 -2
  22. pymmcore_plus/core/events/_psygnal.py +2 -2
  23. pymmcore_plus/experimental/__init__.py +0 -0
  24. pymmcore_plus/experimental/unicore/__init__.py +14 -0
  25. pymmcore_plus/experimental/unicore/_device_manager.py +173 -0
  26. pymmcore_plus/experimental/unicore/_proxy.py +127 -0
  27. pymmcore_plus/experimental/unicore/_unicore.py +703 -0
  28. pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
  29. pymmcore_plus/experimental/unicore/devices/_device.py +269 -0
  30. pymmcore_plus/experimental/unicore/devices/_properties.py +400 -0
  31. pymmcore_plus/experimental/unicore/devices/_stage.py +221 -0
  32. pymmcore_plus/install.py +16 -11
  33. pymmcore_plus/mda/__init__.py +1 -1
  34. pymmcore_plus/mda/_engine.py +320 -148
  35. pymmcore_plus/mda/_protocol.py +6 -4
  36. pymmcore_plus/mda/_runner.py +62 -51
  37. pymmcore_plus/mda/_thread_relay.py +5 -3
  38. pymmcore_plus/mda/events/__init__.py +2 -2
  39. pymmcore_plus/mda/events/_protocol.py +10 -2
  40. pymmcore_plus/mda/events/_psygnal.py +2 -2
  41. pymmcore_plus/mda/handlers/_5d_writer_base.py +106 -15
  42. pymmcore_plus/mda/handlers/__init__.py +7 -1
  43. pymmcore_plus/mda/handlers/_img_sequence_writer.py +11 -6
  44. pymmcore_plus/mda/handlers/_ome_tiff_writer.py +8 -4
  45. pymmcore_plus/mda/handlers/_ome_zarr_writer.py +82 -9
  46. pymmcore_plus/mda/handlers/_tensorstore_handler.py +374 -0
  47. pymmcore_plus/mda/handlers/_util.py +1 -1
  48. pymmcore_plus/metadata/__init__.py +36 -0
  49. pymmcore_plus/metadata/functions.py +353 -0
  50. pymmcore_plus/metadata/schema.py +472 -0
  51. pymmcore_plus/metadata/serialize.py +120 -0
  52. pymmcore_plus/mocks.py +51 -0
  53. pymmcore_plus/model/_config_file.py +5 -6
  54. pymmcore_plus/model/_config_group.py +29 -2
  55. pymmcore_plus/model/_core_device.py +12 -1
  56. pymmcore_plus/model/_core_link.py +2 -1
  57. pymmcore_plus/model/_device.py +39 -8
  58. pymmcore_plus/model/_microscope.py +39 -3
  59. pymmcore_plus/model/_pixel_size_config.py +27 -4
  60. pymmcore_plus/model/_property.py +13 -3
  61. pymmcore_plus/seq_tester.py +1 -1
  62. {pymmcore_plus-0.9.4.dist-info → pymmcore_plus-0.13.0.dist-info}/METADATA +22 -11
  63. pymmcore_plus-0.13.0.dist-info/RECORD +71 -0
  64. {pymmcore_plus-0.9.4.dist-info → pymmcore_plus-0.13.0.dist-info}/WHEEL +1 -1
  65. pymmcore_plus/core/_state.py +0 -244
  66. pymmcore_plus-0.9.4.dist-info/RECORD +0 -55
  67. {pymmcore_plus-0.9.4.dist-info → pymmcore_plus-0.13.0.dist-info}/entry_points.txt +0 -0
  68. {pymmcore_plus-0.9.4.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 typing import Any, Callable, Iterator, Literal, TypeVar
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", "retry", "no_stdout", "signals_backend"]
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 `Micro-Manager*` folder in the `pymmcore-plus` user data directory
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
- 3. A `Micro-Manager*` folder in the `pymmcore_plus` package directory (this is the
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
- 4. The default micro-manager install location:
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.append(env_path)
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
- full_list.extend([str(x) for x in user_install])
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
- full_list.extend([str(x) for x in local_install])
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.append(str(pth))
141
- return full_list
159
+ full_list[str(pth)] = None
160
+ return list(full_list)
142
161
 
143
162
 
144
- def _qt_app_is_running() -> bool:
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
- try:
148
- # in broken environments modname can be a namespace package...
149
- # and QtWidgets will still be unavailable
150
- QtWidgets = importlib.import_module(".QtWidgets", modname)
151
- except ImportError: # pragma: no cover
152
- continue
153
- return QtWidgets.QApplication.instance() is not None
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 _qt_app_is_running() else "psygnal"
248
+ return "qt" if qt_app_running else "psygnal"
173
249
  if env_var == "qt":
174
- if _qt_app_is_running():
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
- try:
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
- except ValueError: # pragma: no cover
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
 
@@ -474,16 +547,25 @@ def system_info() -> dict[str, str]:
474
547
 
475
548
  This backs the `mmcore info` command in the CLI.
476
549
  """
477
- import pymmcore
478
-
479
550
  import pymmcore_plus
480
551
 
481
552
  info = {
482
553
  "python": sys.version,
483
554
  "platform": platform.platform(),
484
555
  "pymmcore-plus": getattr(pymmcore_plus, "__version__", "err"),
485
- "pymmcore": getattr(pymmcore, "__version__", "err"),
486
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"] = ""
487
569
 
488
570
  with suppress(Exception):
489
571
  core = pymmcore_plus.CMMCorePlus.instance()
@@ -515,3 +597,24 @@ def system_info() -> dict[str, str]:
515
597
  info["qt"] = f"{API_NAME} {QT_VERSION}"
516
598
 
517
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()
@@ -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
@@ -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, Iterable, Iterator, Tuple, overload
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 = Tuple[str, str, str]
14
- DevPropTuple: TypeAlias = Tuple[str, str]
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 typing import TYPE_CHECKING, Any, Iterator, Literal, MutableMapping, overload
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}"
@@ -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__", "None")
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
- FocusDirectionUnknown = pymmcore.FocusDirectionUnknown
189
- FocusDirectionTowardSample = pymmcore.FocusDirectionTowardSample
190
- FocusDirectionAwayFromSample = pymmcore.FocusDirectionAwayFromSample
213
+ Unknown = pymmcore.FocusDirectionUnknown
214
+ TowardSample = pymmcore.FocusDirectionTowardSample
215
+ AwayFromSample = pymmcore.FocusDirectionAwayFromSample
191
216
  # aliases
192
- Unknown = FocusDirectionUnknown
193
- TowardSample = FocusDirectionTowardSample
194
- AwayFromSample = FocusDirectionAwayFromSample
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
+ }
@@ -43,12 +43,12 @@ class Device:
43
43
  >>> device.schema() # JSON schema of device properties
44
44
  """
45
45
 
46
- UNASIGNED = "__UNASIGNED__"
46
+ UNASSIGNED = "__UNASSIGNED__"
47
47
  propertyChanged: PSignalInstance
48
48
 
49
49
  def __init__(
50
50
  self,
51
- device_label: str = UNASIGNED,
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.UNASIGNED:
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'{n} {"properties" if n>1 else "property"}'
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"
@@ -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, ItemsView, Iterator, KeysView, ValuesView, cast
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