pymmcore-plus 0.10.2__py3-none-any.whl → 0.11.1__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 (33) hide show
  1. pymmcore_plus/__init__.py +4 -1
  2. pymmcore_plus/_build.py +2 -0
  3. pymmcore_plus/_cli.py +49 -14
  4. pymmcore_plus/_util.py +99 -9
  5. pymmcore_plus/core/__init__.py +2 -0
  6. pymmcore_plus/core/_constants.py +109 -8
  7. pymmcore_plus/core/_mmcore_plus.py +69 -49
  8. pymmcore_plus/mda/__init__.py +2 -2
  9. pymmcore_plus/mda/_engine.py +151 -102
  10. pymmcore_plus/mda/_protocol.py +5 -3
  11. pymmcore_plus/mda/_runner.py +16 -21
  12. pymmcore_plus/mda/events/_protocol.py +10 -2
  13. pymmcore_plus/mda/handlers/_5d_writer_base.py +25 -13
  14. pymmcore_plus/mda/handlers/_img_sequence_writer.py +9 -5
  15. pymmcore_plus/mda/handlers/_ome_tiff_writer.py +7 -3
  16. pymmcore_plus/mda/handlers/_ome_zarr_writer.py +9 -4
  17. pymmcore_plus/mda/handlers/_tensorstore_handler.py +19 -19
  18. pymmcore_plus/metadata/__init__.py +36 -0
  19. pymmcore_plus/metadata/functions.py +343 -0
  20. pymmcore_plus/metadata/schema.py +471 -0
  21. pymmcore_plus/metadata/serialize.py +116 -0
  22. pymmcore_plus/model/_config_file.py +2 -4
  23. pymmcore_plus/model/_config_group.py +29 -3
  24. pymmcore_plus/model/_device.py +20 -1
  25. pymmcore_plus/model/_microscope.py +36 -2
  26. pymmcore_plus/model/_pixel_size_config.py +26 -4
  27. {pymmcore_plus-0.10.2.dist-info → pymmcore_plus-0.11.1.dist-info}/METADATA +6 -5
  28. pymmcore_plus-0.11.1.dist-info/RECORD +59 -0
  29. {pymmcore_plus-0.10.2.dist-info → pymmcore_plus-0.11.1.dist-info}/WHEEL +1 -1
  30. pymmcore_plus/core/_state.py +0 -244
  31. pymmcore_plus-0.10.2.dist-info/RECORD +0 -56
  32. {pymmcore_plus-0.10.2.dist-info → pymmcore_plus-0.11.1.dist-info}/entry_points.txt +0 -0
  33. {pymmcore_plus-0.10.2.dist-info → pymmcore_plus-0.11.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,343 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import suppress
4
+ from typing import TYPE_CHECKING, Any, TypedDict
5
+
6
+ import pymmcore_plus
7
+ from pymmcore_plus._util import timestamp
8
+ from pymmcore_plus.core._constants import DeviceType, PixelFormat
9
+
10
+ if TYPE_CHECKING:
11
+ from typing import Unpack
12
+
13
+ import useq
14
+
15
+ from pymmcore_plus.core import CMMCorePlus
16
+
17
+ from .schema import (
18
+ ConfigGroup,
19
+ DeviceInfo,
20
+ FrameMetaV1,
21
+ ImageInfo,
22
+ PixelSizeConfigPreset,
23
+ Position,
24
+ PropertyInfo,
25
+ PropertyValue,
26
+ StagePosition,
27
+ SummaryMetaV1,
28
+ SystemInfo,
29
+ )
30
+
31
+ class _OptionalFrameMetaKwargs(TypedDict, total=False):
32
+ """Additional optional fields for frame metadata."""
33
+
34
+ mda_event: useq.MDAEvent
35
+ hardware_triggered: bool
36
+ images_remaining_in_buffer: int
37
+ camera_metadata: dict[str, Any]
38
+ extra: dict[str, Any]
39
+
40
+ # -----------------------------------------------------------------
41
+ # These are the two main functions that are called from the outside
42
+ # -----------------------------------------------------------------
43
+
44
+
45
+ def summary_metadata(
46
+ core: CMMCorePlus,
47
+ *,
48
+ mda_sequence: useq.MDASequence | None = None,
49
+ cached: bool = True,
50
+ include_time: bool = True,
51
+ ) -> SummaryMetaV1:
52
+ """Return a summary metadata for the current state of the system.
53
+
54
+ See [pymmcore_plus.metadata.SummaryMetaV1][] for a description of the
55
+ dictionary format.
56
+ """
57
+ summary: SummaryMetaV1 = {
58
+ "format": "summary-dict",
59
+ "version": "1.0",
60
+ "devices": devices_info(core, cached=cached),
61
+ "system_info": system_info(core),
62
+ "image_infos": image_infos(core),
63
+ "position": position(core),
64
+ "config_groups": config_groups(core),
65
+ "pixel_size_configs": pixel_size_configs(core),
66
+ }
67
+ if include_time:
68
+ summary["datetime"] = timestamp()
69
+ if mda_sequence:
70
+ summary["mda_sequence"] = mda_sequence
71
+ return summary
72
+
73
+
74
+ def frame_metadata(
75
+ core: CMMCorePlus,
76
+ *,
77
+ cached: bool = True,
78
+ runner_time_ms: float = -1,
79
+ camera_device: str | None = None,
80
+ property_values: tuple[PropertyValue, ...] = (),
81
+ **kwargs: Unpack[_OptionalFrameMetaKwargs],
82
+ ) -> FrameMetaV1:
83
+ """Return metadata for the current frame."""
84
+ return {
85
+ "format": "frame-dict",
86
+ "version": "1.0",
87
+ "runner_time_ms": runner_time_ms,
88
+ "camera_device": camera_device or core.getPhysicalCameraDevice(),
89
+ "property_values": property_values,
90
+ "exposure_ms": core.getExposure(),
91
+ "pixel_size_um": core.getPixelSizeUm(cached),
92
+ "position": position(core),
93
+ **kwargs,
94
+ }
95
+
96
+
97
+ # ----------------------------------------------
98
+ # supporting functions
99
+ # ----------------------------------------------
100
+
101
+
102
+ def device_info(core: CMMCorePlus, *, label: str, cached: bool = True) -> DeviceInfo:
103
+ """Return information about a specific device label."""
104
+ devtype = core.getDeviceType(label)
105
+ info: DeviceInfo = {
106
+ "label": label,
107
+ "library": core.getDeviceLibrary(label),
108
+ "name": core.getDeviceName(label),
109
+ "type": devtype.name,
110
+ "description": core.getDeviceDescription(label),
111
+ "properties": properties(core, device=label, cached=cached),
112
+ }
113
+ if parent := core.getParentLabel(label):
114
+ info["parent_label"] = parent
115
+ with suppress(RuntimeError):
116
+ if devtype == DeviceType.Hub:
117
+ info["child_names"] = core.getInstalledDevices(label)
118
+ if devtype == DeviceType.State:
119
+ info["labels"] = core.getStateLabels(label)
120
+ elif devtype == DeviceType.Stage:
121
+ info["is_sequenceable"] = core.isStageSequenceable(label)
122
+ info["is_continuous_focus_drive"] = core.isContinuousFocusDrive(label)
123
+ with suppress(RuntimeError):
124
+ info["focus_direction"] = core.getFocusDirection(label).name # type: ignore[typeddict-item]
125
+ elif devtype == DeviceType.XYStage:
126
+ info["is_sequenceable"] = core.isXYStageSequenceable(label)
127
+ elif devtype == DeviceType.Camera:
128
+ info["is_sequenceable"] = core.isExposureSequenceable(label)
129
+ elif devtype == DeviceType.SLM:
130
+ info["is_sequenceable"] = core.getSLMSequenceMaxLength(label) > 0
131
+ return info
132
+
133
+
134
+ def system_info(core: CMMCorePlus) -> SystemInfo:
135
+ """Return general system information."""
136
+ return {
137
+ "pymmcore_version": pymmcore_plus.__version__,
138
+ "pymmcore_plus_version": pymmcore_plus.__version__,
139
+ "mmcore_version": core.getVersionInfo(),
140
+ "device_api_version": core.getAPIVersionInfo(),
141
+ "device_adapter_search_paths": core.getDeviceAdapterSearchPaths(),
142
+ "system_configuration_file": core.systemConfigurationFile(),
143
+ "primary_log_file": core.getPrimaryLogFile(),
144
+ "sequence_buffer_size_mb": core.getCircularBufferMemoryFootprint(),
145
+ "continuous_focus_enabled": core.isContinuousFocusEnabled(),
146
+ "continuous_focus_locked": core.isContinuousFocusLocked(),
147
+ "auto_shutter": core.getAutoShutter(),
148
+ "timeout_ms": core.getTimeoutMs(),
149
+ }
150
+
151
+
152
+ def image_info(core: CMMCorePlus) -> ImageInfo:
153
+ """Return information about the current camera image properties."""
154
+ w = core.getImageWidth()
155
+ h = core.getImageHeight()
156
+ n_comp = core.getNumberOfComponents()
157
+ plane_shape: tuple[int, int] | tuple[int, int, int] = (h, w)
158
+ if n_comp == 1:
159
+ plane_shape = (h, w)
160
+ elif n_comp == 4:
161
+ plane_shape = (h, w, 3)
162
+ else:
163
+ plane_shape = (h, w, n_comp)
164
+ bpp = core.getBytesPerPixel()
165
+ dtype = f"uint{(bpp // n_comp) * 8}"
166
+
167
+ info: ImageInfo = {
168
+ "camera_label": core.getCameraDevice(),
169
+ "plane_shape": plane_shape,
170
+ "dtype": dtype,
171
+ "height": h,
172
+ "width": w,
173
+ "pixel_format": PixelFormat.for_current_camera(core).value,
174
+ "pixel_size_um": core.getPixelSizeUm(True),
175
+ "pixel_size_config_name": core.getCurrentPixelSizeConfig(),
176
+ }
177
+
178
+ # if (n_channels := core.getNumberOfCameraChannels()) > 1:
179
+ # info["num_camera_adapter_channels"] = n_channels
180
+ if (mag_factor := core.getMagnificationFactor()) != 1.0:
181
+ info["magnification_factor"] = mag_factor
182
+ if (affine := core.getPixelSizeAffine(True)) != (1.0, 0.0, 0.0, 0.0, 1.0, 0.0):
183
+ info["pixel_size_affine"] = affine
184
+
185
+ with suppress(RuntimeError):
186
+ if (roi := core.getROI()) != [0, 0, w, h]:
187
+ info["roi"] = tuple(roi) # type: ignore [typeddict-item]
188
+ with suppress(RuntimeError):
189
+ if any(rois := core.getMultiROI()):
190
+ info["multi_roi"] = rois
191
+ return info
192
+
193
+
194
+ def image_infos(core: CMMCorePlus) -> tuple[ImageInfo, ...]:
195
+ """Return information about the current image properties for all cameras."""
196
+ if not (selected := core.getCameraDevice()):
197
+ return ()
198
+ # currently selected device is always first
199
+ infos: list[ImageInfo] = [image_info(core)]
200
+ try:
201
+ # set every other camera and get the image info
202
+ for cam in core.getLoadedDevicesOfType(DeviceType.Camera):
203
+ if cam != selected:
204
+ with suppress(RuntimeError):
205
+ core.setCameraDevice(cam)
206
+ infos.append(image_info(core))
207
+ finally:
208
+ # set the camera back to the originally selected device
209
+ core.setCameraDevice(selected)
210
+ return tuple(infos)
211
+
212
+
213
+ def position(core: CMMCorePlus, all_stages: bool = False) -> Position:
214
+ """Return current position of active (and, optionally, all) stages."""
215
+ position: Position = {}
216
+ with suppress(Exception):
217
+ position["x"] = core.getXPosition()
218
+ with suppress(Exception):
219
+ position["y"] = core.getYPosition()
220
+ with suppress(Exception):
221
+ position["z"] = core.getPosition()
222
+ if all_stages:
223
+ pos_list: list[StagePosition] = []
224
+ for stage in core.getLoadedDevicesOfType(DeviceType.Stage):
225
+ with suppress(Exception):
226
+ pos_list.append(
227
+ {
228
+ "device_label": stage,
229
+ "position": core.getPosition(stage),
230
+ }
231
+ )
232
+ for stage in core.getLoadedDevicesOfType(DeviceType.XYStage):
233
+ with suppress(Exception):
234
+ pos_list.append(
235
+ {
236
+ "device_label": stage,
237
+ "position": tuple(core.getXYPosition(stage)), # type: ignore
238
+ }
239
+ )
240
+ position["all_stages"] = pos_list
241
+ return position
242
+
243
+
244
+ def config_group(core: CMMCorePlus, *, group_name: str) -> ConfigGroup:
245
+ """Return a dictionary of configuration presets for a specific group."""
246
+ return {
247
+ "name": group_name,
248
+ "presets": tuple(
249
+ {
250
+ "name": preset_name,
251
+ "settings": tuple(
252
+ {"dev": dev, "prop": prop, "val": val}
253
+ for dev, prop, val in core.getConfigData(group_name, preset_name)
254
+ ),
255
+ }
256
+ for preset_name in core.getAvailableConfigs(group_name)
257
+ ),
258
+ }
259
+
260
+
261
+ def config_groups(core: CMMCorePlus) -> tuple[ConfigGroup, ...]:
262
+ """Return all configuration groups."""
263
+ return tuple(
264
+ config_group(core, group_name=group_name)
265
+ for group_name in core.getAvailableConfigGroups()
266
+ )
267
+
268
+
269
+ def pixel_size_config(core: CMMCorePlus, *, config_name: str) -> PixelSizeConfigPreset:
270
+ """Return info for a specific pixel size preset for a specific."""
271
+ info: PixelSizeConfigPreset = {
272
+ "name": config_name,
273
+ "pixel_size_um": core.getPixelSizeUmByID(config_name),
274
+ "settings": tuple(
275
+ {"dev": dev, "prop": prop, "val": val}
276
+ for dev, prop, val in core.getPixelSizeConfigData(config_name)
277
+ ),
278
+ }
279
+ affine = core.getPixelSizeAffineByID(config_name)
280
+ if affine != (1.0, 0.0, 0.0, 0.0, 1.0, 0.0):
281
+ info["pixel_size_affine"] = affine
282
+ return info
283
+
284
+
285
+ def devices_info(core: CMMCorePlus, cached: bool = True) -> tuple[DeviceInfo, ...]:
286
+ """Return a dictionary of device information for all loaded devices."""
287
+ return tuple(
288
+ device_info(core, label=lbl, cached=cached) for lbl in core.getLoadedDevices()
289
+ )
290
+
291
+
292
+ def property_info(
293
+ core: CMMCorePlus,
294
+ device: str,
295
+ prop: str,
296
+ *,
297
+ cached: bool = True,
298
+ ) -> PropertyInfo:
299
+ """Return information on a specific device property."""
300
+ try:
301
+ if cached:
302
+ value = core.getPropertyFromCache(device, prop)
303
+ else: # pragma: no cover
304
+ value = core.getProperty(device, prop)
305
+ except Exception: # pragma: no cover
306
+ value = None
307
+ info: PropertyInfo = {
308
+ "name": prop,
309
+ "value": value,
310
+ "data_type": core.getPropertyType(device, prop).__repr__(),
311
+ "allowed_values": core.getAllowedPropertyValues(device, prop),
312
+ "is_read_only": core.isPropertyReadOnly(device, prop),
313
+ }
314
+ if core.isPropertyPreInit(device, prop):
315
+ info["is_pre_init"] = True
316
+ if core.isPropertySequenceable(device, prop):
317
+ info["sequenceable"] = True
318
+ info["sequence_max_length"] = core.getPropertySequenceMaxLength(device, prop)
319
+ if core.hasPropertyLimits(device, prop):
320
+ info["limits"] = (
321
+ core.getPropertyLowerLimit(device, prop),
322
+ core.getPropertyUpperLimit(device, prop),
323
+ )
324
+ return info
325
+
326
+
327
+ def properties(
328
+ core: CMMCorePlus, device: str, *, cached: bool = True
329
+ ) -> tuple[PropertyInfo, ...]:
330
+ """Return a dictionary of device properties values for all loaded devices."""
331
+ # this actually appears to be faster than getSystemStateCache
332
+ return tuple(
333
+ property_info(core, device, prop, cached=cached)
334
+ for prop in core.getDevicePropertyNames(device)
335
+ )
336
+
337
+
338
+ def pixel_size_configs(core: CMMCorePlus) -> tuple[PixelSizeConfigPreset, ...]:
339
+ """Return a dictionary of pixel size configurations."""
340
+ return tuple(
341
+ pixel_size_config(core, config_name=config_name)
342
+ for config_name in core.getAvailablePixelSizeConfigs()
343
+ )