pymmcore-plus 0.10.2__py3-none-any.whl → 0.11.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 +4 -1
- pymmcore_plus/_build.py +2 -0
- pymmcore_plus/_cli.py +47 -12
- pymmcore_plus/_util.py +99 -9
- pymmcore_plus/core/__init__.py +2 -0
- pymmcore_plus/core/_constants.py +109 -8
- pymmcore_plus/core/_mmcore_plus.py +67 -47
- pymmcore_plus/mda/__init__.py +2 -2
- pymmcore_plus/mda/_engine.py +148 -98
- pymmcore_plus/mda/_protocol.py +5 -3
- pymmcore_plus/mda/_runner.py +16 -21
- pymmcore_plus/mda/events/_protocol.py +10 -2
- pymmcore_plus/mda/handlers/_5d_writer_base.py +25 -13
- pymmcore_plus/mda/handlers/_img_sequence_writer.py +9 -5
- pymmcore_plus/mda/handlers/_ome_tiff_writer.py +7 -3
- pymmcore_plus/mda/handlers/_ome_zarr_writer.py +9 -4
- pymmcore_plus/mda/handlers/_tensorstore_handler.py +19 -19
- pymmcore_plus/metadata/__init__.py +36 -0
- pymmcore_plus/metadata/functions.py +343 -0
- pymmcore_plus/metadata/schema.py +471 -0
- pymmcore_plus/metadata/serialize.py +116 -0
- pymmcore_plus/model/_config_file.py +2 -4
- pymmcore_plus/model/_config_group.py +29 -3
- pymmcore_plus/model/_device.py +20 -1
- pymmcore_plus/model/_microscope.py +35 -1
- pymmcore_plus/model/_pixel_size_config.py +25 -3
- {pymmcore_plus-0.10.2.dist-info → pymmcore_plus-0.11.0.dist-info}/METADATA +4 -3
- pymmcore_plus-0.11.0.dist-info/RECORD +59 -0
- {pymmcore_plus-0.10.2.dist-info → pymmcore_plus-0.11.0.dist-info}/WHEEL +1 -1
- pymmcore_plus/core/_state.py +0 -244
- pymmcore_plus-0.10.2.dist-info/RECORD +0 -56
- {pymmcore_plus-0.10.2.dist-info → pymmcore_plus-0.11.0.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.10.2.dist-info → pymmcore_plus-0.11.0.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 # type: ignore [typeddict-item]
|
|
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 # type: ignore [typeddict-item]
|
|
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
|
+
)
|