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