pymmcore-plus 0.16.0__py3-none-any.whl → 0.17.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/_ipy_completion.py +1 -1
- pymmcore_plus/_logger.py +2 -2
- pymmcore_plus/core/_device.py +37 -6
- pymmcore_plus/core/_mmcore_plus.py +4 -15
- pymmcore_plus/core/_property.py +1 -1
- pymmcore_plus/core/_sequencing.py +2 -0
- pymmcore_plus/experimental/simulate/__init__.py +88 -0
- pymmcore_plus/experimental/simulate/_objects.py +670 -0
- pymmcore_plus/experimental/simulate/_render.py +510 -0
- pymmcore_plus/experimental/simulate/_sample.py +156 -0
- pymmcore_plus/experimental/unicore/__init__.py +2 -0
- pymmcore_plus/experimental/unicore/_device_manager.py +26 -0
- pymmcore_plus/experimental/unicore/core/_config.py +706 -0
- pymmcore_plus/experimental/unicore/core/_unicore.py +830 -17
- pymmcore_plus/experimental/unicore/devices/_device_base.py +13 -0
- pymmcore_plus/experimental/unicore/devices/_hub.py +50 -0
- pymmcore_plus/experimental/unicore/devices/_stage.py +46 -1
- pymmcore_plus/experimental/unicore/devices/_state.py +6 -0
- pymmcore_plus/mda/handlers/_5d_writer_base.py +16 -5
- pymmcore_plus/mda/handlers/_tensorstore_handler.py +7 -1
- pymmcore_plus/metadata/_ome.py +75 -21
- pymmcore_plus/metadata/functions.py +2 -1
- {pymmcore_plus-0.16.0.dist-info → pymmcore_plus-0.17.0.dist-info}/METADATA +5 -3
- {pymmcore_plus-0.16.0.dist-info → pymmcore_plus-0.17.0.dist-info}/RECORD +27 -21
- {pymmcore_plus-0.16.0.dist-info → pymmcore_plus-0.17.0.dist-info}/WHEEL +1 -1
- {pymmcore_plus-0.16.0.dist-info → pymmcore_plus-0.17.0.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.16.0.dist-info → pymmcore_plus-0.17.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
"""Config file loading and saving for UniMMCore.
|
|
2
|
+
|
|
3
|
+
This module provides Python-owned config file loading/saving that supports
|
|
4
|
+
both C++ and Python devices. Python device lines use a `#py ` prefix so they
|
|
5
|
+
are treated as comments by upstream C++/pymmcore implementations.
|
|
6
|
+
|
|
7
|
+
Format example:
|
|
8
|
+
# C++ devices
|
|
9
|
+
Device,Camera,DemoCamera,DCam
|
|
10
|
+
Property,Core,Initialize,1
|
|
11
|
+
|
|
12
|
+
# Python devices (hidden from upstream via comment prefix)
|
|
13
|
+
#py pyDevice,PyCamera,mypackage.cameras,MyCameraClass
|
|
14
|
+
#py Property,PyCamera,Exposure,50.0
|
|
15
|
+
#py Property,Core,Camera,PyCamera
|
|
16
|
+
|
|
17
|
+
# Config groups can mix both
|
|
18
|
+
ConfigGroup,Channel,DAPI,Dichroic,Label,400DCLP
|
|
19
|
+
#py ConfigGroup,Channel,DAPI,PyFilter,Position,Blue
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import warnings
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
27
|
+
|
|
28
|
+
from pymmcore_plus import CFGCommand, CFGGroup, DeviceType, Keyword
|
|
29
|
+
from pymmcore_plus._util import timestamp
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from collections.abc import Iterable, Sequence
|
|
33
|
+
|
|
34
|
+
from ._unicore import UniMMCore
|
|
35
|
+
|
|
36
|
+
__all__ = ["load_system_configuration", "save_system_configuration"]
|
|
37
|
+
|
|
38
|
+
# Prefix for Python device lines (treated as comment by C++/regular pymmcore)
|
|
39
|
+
PY_PREFIX = "#py "
|
|
40
|
+
# Custom command names (not in CFGCommand enum)
|
|
41
|
+
_PY_DEVICE_CMD = "pyDevice"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# Loading
|
|
46
|
+
# =============================================================================
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_system_configuration(core: UniMMCore, filename: str | Path) -> None:
|
|
50
|
+
"""Load system configuration from a file.
|
|
51
|
+
|
|
52
|
+
This is a Python implementation of MMCore::loadSystemConfigurationImpl
|
|
53
|
+
that supports both C++ and Python devices. Lines prefixed with `#py `
|
|
54
|
+
are processed as Python device commands.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
core : UniMMCore
|
|
59
|
+
The core instance to configure.
|
|
60
|
+
filename : str | Path
|
|
61
|
+
Path to the configuration file.
|
|
62
|
+
"""
|
|
63
|
+
path = Path(filename).expanduser().resolve()
|
|
64
|
+
if not path.exists(): # pragma: no cover
|
|
65
|
+
raise FileNotFoundError(f"Configuration file not found: {path}")
|
|
66
|
+
|
|
67
|
+
load_from_string(core, path.read_text(), str(path))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_from_string(core: UniMMCore, text: str, source: str = "<string>") -> None:
|
|
71
|
+
"""Load system configuration from a string.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
core : UniMMCore
|
|
76
|
+
The core instance to configure.
|
|
77
|
+
text : str
|
|
78
|
+
The configuration text.
|
|
79
|
+
source : str
|
|
80
|
+
Source identifier for error messages.
|
|
81
|
+
"""
|
|
82
|
+
for line_num, line in enumerate(text.splitlines(), start=1):
|
|
83
|
+
# Strip Windows CR if present
|
|
84
|
+
if not (line := line.rstrip("\r").strip()):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
# strip #py prefix (python devices)
|
|
89
|
+
if line.startswith(PY_PREFIX):
|
|
90
|
+
line = line[len(PY_PREFIX) :].lstrip()
|
|
91
|
+
|
|
92
|
+
if line.startswith("#"):
|
|
93
|
+
# Regular comment - skip
|
|
94
|
+
continue
|
|
95
|
+
else:
|
|
96
|
+
# Standard command
|
|
97
|
+
_run_command(core, line)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
raise RuntimeError(
|
|
100
|
+
f"Error in configuration file {source!r} at line {line_num}: "
|
|
101
|
+
f"{line!r}\n{e}"
|
|
102
|
+
) from e
|
|
103
|
+
|
|
104
|
+
# File parsing finished, apply startup configuration if defined
|
|
105
|
+
if core.isConfigDefined(CFGGroup.System, CFGGroup.System_Startup):
|
|
106
|
+
# Build system state cache before setConfig to avoid failures
|
|
107
|
+
core.waitForSystem()
|
|
108
|
+
core.updateSystemStateCache()
|
|
109
|
+
core.setConfig(CFGGroup.System, CFGGroup.System_Startup)
|
|
110
|
+
|
|
111
|
+
# Final sync after all configuration is applied
|
|
112
|
+
core.waitForSystem()
|
|
113
|
+
core.updateSystemStateCache()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _run_command(core: UniMMCore, line: str) -> None:
|
|
117
|
+
"""Execute a single configuration command.
|
|
118
|
+
|
|
119
|
+
Mirrors MMCore::loadSystemConfigurationImpl command processing.
|
|
120
|
+
"""
|
|
121
|
+
tokens = line.split(CFGCommand.FieldDelimiters)
|
|
122
|
+
if not tokens: # pragma: no cover
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
cmd_name, *args = tokens
|
|
126
|
+
|
|
127
|
+
# Handle custom commands first
|
|
128
|
+
if cmd_name == _PY_DEVICE_CMD:
|
|
129
|
+
_exec_device(core, args)
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
command = CFGCommand(cmd_name)
|
|
134
|
+
except ValueError: # pragma: no cover
|
|
135
|
+
raise ValueError(f"Unknown configuration command: {cmd_name!r}") from None
|
|
136
|
+
|
|
137
|
+
if command == CFGCommand.Configuration: # pragma: no cover
|
|
138
|
+
warnings.warn(
|
|
139
|
+
f"Obsolete command {cmd_name!r} ignored in configuration file",
|
|
140
|
+
UserWarning,
|
|
141
|
+
stacklevel=3,
|
|
142
|
+
)
|
|
143
|
+
return
|
|
144
|
+
if command == CFGCommand.Equipment: # pragma: no cover
|
|
145
|
+
raise ValueError("Equipment command has been removed from config format")
|
|
146
|
+
if command == CFGCommand.ImageSynchro: # pragma: no cover
|
|
147
|
+
raise ValueError("ImageSynchro command has been removed from config format")
|
|
148
|
+
|
|
149
|
+
executor = _COMMAND_EXECUTORS.get(command)
|
|
150
|
+
if executor is None: # pragma: no cover
|
|
151
|
+
# Unknown command in our executor map - should not happen for valid CFGCommand
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
executor(core, args)
|
|
156
|
+
except Exception as e: # pragma: no cover
|
|
157
|
+
raise RuntimeError(
|
|
158
|
+
f"Error executing command {cmd_name!r} with arguments {args!r}: {e}"
|
|
159
|
+
) from e
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# =============================================================================
|
|
163
|
+
# Command Executors
|
|
164
|
+
# =============================================================================
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _exec_device(core: UniMMCore, args: Sequence[str]) -> None:
|
|
168
|
+
"""Load a device: Device,<label>,<library>,<device_name>."""
|
|
169
|
+
if len(args) != 3: # pragma: no cover
|
|
170
|
+
raise ValueError(f"Device command requires 3 arguments, got {len(args)}")
|
|
171
|
+
label, library, device_name = args
|
|
172
|
+
core.loadDevice(label, library, device_name)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _exec_property(core: UniMMCore, args: Sequence[str]) -> None:
|
|
176
|
+
"""Set a property: Property,<device>,<property>,<value>."""
|
|
177
|
+
if len(args) not in (2, 3): # pragma: no cover
|
|
178
|
+
raise ValueError(f"Property command requires 2-3 arguments, got {len(args)}")
|
|
179
|
+
|
|
180
|
+
device, prop = args[0], args[1]
|
|
181
|
+
value = args[2] if len(args) > 2 else ""
|
|
182
|
+
|
|
183
|
+
# Special case: Core device properties
|
|
184
|
+
if device == Keyword.CoreDevice:
|
|
185
|
+
if prop == Keyword.CoreInitialize:
|
|
186
|
+
try:
|
|
187
|
+
init_val = int(value)
|
|
188
|
+
except (ValueError, TypeError): # pragma: no cover
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"Initialize value must be integer, got {value!r}"
|
|
191
|
+
) from None
|
|
192
|
+
if init_val == 0:
|
|
193
|
+
# Unload all devices (reset state for fresh config load)
|
|
194
|
+
core.unloadAllDevices()
|
|
195
|
+
elif init_val == 1:
|
|
196
|
+
core.initializeAllDevices()
|
|
197
|
+
return
|
|
198
|
+
elif prop == Keyword.CoreCamera:
|
|
199
|
+
core.setCameraDevice(value)
|
|
200
|
+
return
|
|
201
|
+
elif prop == Keyword.CoreShutter:
|
|
202
|
+
core.setShutterDevice(value)
|
|
203
|
+
return
|
|
204
|
+
elif prop == Keyword.CoreFocus:
|
|
205
|
+
core.setFocusDevice(value)
|
|
206
|
+
return
|
|
207
|
+
elif prop == Keyword.CoreXYStage:
|
|
208
|
+
core.setXYStageDevice(value)
|
|
209
|
+
return
|
|
210
|
+
elif prop == Keyword.CoreAutoFocus:
|
|
211
|
+
core.setAutoFocusDevice(value)
|
|
212
|
+
return
|
|
213
|
+
elif prop == Keyword.CoreSLM:
|
|
214
|
+
core.setSLMDevice(value)
|
|
215
|
+
return
|
|
216
|
+
elif prop == Keyword.CoreGalvo:
|
|
217
|
+
core.setGalvoDevice(value)
|
|
218
|
+
return
|
|
219
|
+
elif prop == Keyword.CoreChannelGroup:
|
|
220
|
+
core.setChannelGroup(value)
|
|
221
|
+
return
|
|
222
|
+
elif prop == Keyword.CoreAutoShutter:
|
|
223
|
+
core.setAutoShutter(bool(int(value)))
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
core.setProperty(device, prop, value)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _exec_delay(core: UniMMCore, args: Sequence[str]) -> None:
|
|
230
|
+
"""Set device delay: Delay,<device>,<delay_ms>."""
|
|
231
|
+
if len(args) != 2: # pragma: no cover
|
|
232
|
+
raise ValueError(f"Delay command requires 2 arguments, got {len(args)}")
|
|
233
|
+
device, delay_str = args
|
|
234
|
+
try:
|
|
235
|
+
delay_ms = float(delay_str)
|
|
236
|
+
except ValueError: # pragma: no cover
|
|
237
|
+
raise ValueError(f"Delay must be a number, got {delay_str!r}") from None
|
|
238
|
+
core.setDeviceDelayMs(device, delay_ms)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _exec_focus_direction(core: UniMMCore, args: Sequence[str]) -> None:
|
|
242
|
+
"""Set focus direction: FocusDirection,<device>,<direction>."""
|
|
243
|
+
if len(args) != 2: # pragma: no cover
|
|
244
|
+
raise ValueError(
|
|
245
|
+
f"FocusDirection command requires 2 arguments, got {len(args)}"
|
|
246
|
+
)
|
|
247
|
+
device, direction_str = args
|
|
248
|
+
try:
|
|
249
|
+
direction = int(direction_str)
|
|
250
|
+
except ValueError: # pragma: no cover
|
|
251
|
+
raise ValueError(
|
|
252
|
+
f"FocusDirection must be an integer, got {direction_str!r}"
|
|
253
|
+
) from None
|
|
254
|
+
core.setFocusDirection(device, direction)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _exec_label(core: UniMMCore, args: Sequence[str]) -> None:
|
|
258
|
+
"""Define state label: Label,<device>,<state>,<label>."""
|
|
259
|
+
if len(args) != 3: # pragma: no cover
|
|
260
|
+
raise ValueError(f"Label command requires 3 arguments, got {len(args)}")
|
|
261
|
+
device, state_str, label = args
|
|
262
|
+
try:
|
|
263
|
+
state = int(state_str)
|
|
264
|
+
except ValueError: # pragma: no cover
|
|
265
|
+
raise ValueError(f"State must be an integer, got {state_str!r}") from None
|
|
266
|
+
core.defineStateLabel(device, state, label)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _exec_config_group(core: UniMMCore, args: Sequence[str]) -> None:
|
|
270
|
+
"""Define config group/preset."""
|
|
271
|
+
if len(args) < 1:
|
|
272
|
+
raise ValueError("ConfigGroup command requires at least 1 argument")
|
|
273
|
+
|
|
274
|
+
group_name = args[0]
|
|
275
|
+
|
|
276
|
+
if len(args) == 1:
|
|
277
|
+
# Just define an empty group
|
|
278
|
+
core.defineConfigGroup(group_name)
|
|
279
|
+
elif len(args) in (4, 5):
|
|
280
|
+
# Define a config setting
|
|
281
|
+
preset_name = args[1]
|
|
282
|
+
device = args[2]
|
|
283
|
+
prop = args[3]
|
|
284
|
+
value = args[4] if len(args) > 4 else ""
|
|
285
|
+
core.defineConfig(group_name, preset_name, device, prop, value)
|
|
286
|
+
else: # pragma: no cover
|
|
287
|
+
raise ValueError(
|
|
288
|
+
f"ConfigGroup command requires 1, 4, or 5 arguments, got {len(args)}"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _exec_config_pixel_size(core: UniMMCore, args: Sequence[str]) -> None:
|
|
293
|
+
"""Define pixel size config: ConfigPixelSize,<preset>,<device>,<prop>,<value>."""
|
|
294
|
+
if len(args) != 4: # pragma: no cover
|
|
295
|
+
raise ValueError(
|
|
296
|
+
f"ConfigPixelSize command requires 4 arguments, got {len(args)}"
|
|
297
|
+
)
|
|
298
|
+
preset_name, device, prop, value = args
|
|
299
|
+
core.definePixelSizeConfig(preset_name, device, prop, value)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _exec_pixel_size_um(core: UniMMCore, args: Sequence[str]) -> None:
|
|
303
|
+
"""Set pixel size: PixelSize_um,<preset>,<size>."""
|
|
304
|
+
if len(args) != 2: # pragma: no cover
|
|
305
|
+
raise ValueError(f"PixelSize_um command requires 2 arguments, got {len(args)}")
|
|
306
|
+
preset_name, size_str = args
|
|
307
|
+
try:
|
|
308
|
+
size = float(size_str)
|
|
309
|
+
except ValueError: # pragma: no cover
|
|
310
|
+
raise ValueError(f"Pixel size must be a number, got {size_str!r}") from None
|
|
311
|
+
core.setPixelSizeUm(preset_name, size)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _exec_pixel_size_affine(core: UniMMCore, args: Sequence[str]) -> None:
|
|
315
|
+
"""Set pixel size affine transform."""
|
|
316
|
+
if len(args) != 7: # pragma: no cover
|
|
317
|
+
raise ValueError(
|
|
318
|
+
f"PixelSizeAffine command requires 7 arguments, got {len(args)}"
|
|
319
|
+
)
|
|
320
|
+
preset_name = args[0]
|
|
321
|
+
try:
|
|
322
|
+
affine = [float(x) for x in args[1:]]
|
|
323
|
+
except ValueError: # pragma: no cover
|
|
324
|
+
raise ValueError(f"Affine values must be numbers, got {args[1:]!r}") from None
|
|
325
|
+
core.setPixelSizeAffine(preset_name, affine)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _exec_parent_id(core: UniMMCore, args: Sequence[str]) -> None:
|
|
329
|
+
"""Set parent hub: Parent,<device>,<parent_hub>."""
|
|
330
|
+
if len(args) != 2: # pragma: no cover
|
|
331
|
+
raise ValueError(f"Parent command requires 2 arguments, got {len(args)}")
|
|
332
|
+
device, parent = args
|
|
333
|
+
core.setParentLabel(device, parent)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# Map commands to their executors
|
|
337
|
+
# Commands not in this map are silently ignored (Equipment, ImageSynchro, etc.)
|
|
338
|
+
_COMMAND_EXECUTORS: dict[CFGCommand, Callable[[UniMMCore, Sequence[str]], None]] = {
|
|
339
|
+
CFGCommand.Device: _exec_device,
|
|
340
|
+
CFGCommand.Property: _exec_property,
|
|
341
|
+
CFGCommand.Delay: _exec_delay,
|
|
342
|
+
CFGCommand.FocusDirection: _exec_focus_direction,
|
|
343
|
+
CFGCommand.Label: _exec_label,
|
|
344
|
+
CFGCommand.ConfigGroup: _exec_config_group,
|
|
345
|
+
CFGCommand.ConfigPixelSize: _exec_config_pixel_size,
|
|
346
|
+
CFGCommand.PixelSize_um: _exec_pixel_size_um,
|
|
347
|
+
CFGCommand.PixelSizeAffine: _exec_pixel_size_affine,
|
|
348
|
+
CFGCommand.ParentID: _exec_parent_id,
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# Add optional pixel size commands if available
|
|
352
|
+
if hasattr(CFGCommand, "PixelSize_dxdz"):
|
|
353
|
+
|
|
354
|
+
def _exec_pixel_size_dxdz(core: UniMMCore, args: Sequence[str]) -> None:
|
|
355
|
+
if len(args) != 2: # pragma: no cover
|
|
356
|
+
raise ValueError(
|
|
357
|
+
f"PixelSize_dxdz command requires 2 arguments, got {len(args)}"
|
|
358
|
+
)
|
|
359
|
+
preset_name, value_str = args
|
|
360
|
+
core.setPixelSizedxdz(preset_name, float(value_str))
|
|
361
|
+
|
|
362
|
+
_COMMAND_EXECUTORS[CFGCommand.PixelSize_dxdz] = _exec_pixel_size_dxdz
|
|
363
|
+
|
|
364
|
+
if hasattr(CFGCommand, "PixelSize_dydz"):
|
|
365
|
+
|
|
366
|
+
def _exec_pixel_size_dydz(core: UniMMCore, args: Sequence[str]) -> None:
|
|
367
|
+
if len(args) != 2: # pragma: no cover
|
|
368
|
+
raise ValueError(
|
|
369
|
+
f"PixelSize_dydz command requires 2 arguments, got {len(args)}"
|
|
370
|
+
)
|
|
371
|
+
preset_name, value_str = args
|
|
372
|
+
core.setPixelSizedydz(preset_name, float(value_str))
|
|
373
|
+
|
|
374
|
+
_COMMAND_EXECUTORS[CFGCommand.PixelSize_dydz] = _exec_pixel_size_dydz
|
|
375
|
+
|
|
376
|
+
if hasattr(CFGCommand, "PixelSize_OptimalZUm"):
|
|
377
|
+
|
|
378
|
+
def _exec_pixel_size_optimal_z(core: UniMMCore, args: Sequence[str]) -> None:
|
|
379
|
+
if len(args) != 2: # pragma: no cover
|
|
380
|
+
raise ValueError(
|
|
381
|
+
f"PixelSize_OptimalZUm command requires 2 arguments, got {len(args)}"
|
|
382
|
+
)
|
|
383
|
+
preset_name, value_str = args
|
|
384
|
+
core.setPixelSizeOptimalZUm(preset_name, float(value_str))
|
|
385
|
+
|
|
386
|
+
_COMMAND_EXECUTORS[CFGCommand.PixelSize_OptimalZUm] = _exec_pixel_size_optimal_z
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# =============================================================================
|
|
390
|
+
# Saving
|
|
391
|
+
# =============================================================================
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def save_system_configuration(
|
|
395
|
+
core: UniMMCore, filename: str | Path, *, prefix_py_devices: bool = True
|
|
396
|
+
) -> None:
|
|
397
|
+
"""Save the current system configuration to a file.
|
|
398
|
+
|
|
399
|
+
This saves both C++ and Python devices.
|
|
400
|
+
|
|
401
|
+
Parameters
|
|
402
|
+
----------
|
|
403
|
+
core : UniMMCore
|
|
404
|
+
The core instance to save configuration from.
|
|
405
|
+
filename : str | Path
|
|
406
|
+
Path to save the configuration file.
|
|
407
|
+
prefix_py_devices : bool, optional
|
|
408
|
+
If True (default), Python device lines are prefixed with `#py ` so they
|
|
409
|
+
are ignored by upstream C++/pymmcore implementations. If False, Python
|
|
410
|
+
device lines are saved without the prefix (config will only be loadable
|
|
411
|
+
by UniMMCore).
|
|
412
|
+
"""
|
|
413
|
+
path = Path(filename).expanduser().resolve()
|
|
414
|
+
|
|
415
|
+
with open(path, "w") as f:
|
|
416
|
+
for section, lines in _iter_config_sections(core, prefix_py_devices):
|
|
417
|
+
f.write(f"# {section}\n")
|
|
418
|
+
for line in lines:
|
|
419
|
+
f.write(line + "\n")
|
|
420
|
+
f.write("\n")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _serialize(
|
|
424
|
+
*args: Any, py_device: bool = False, prefix_py_devices: bool = True
|
|
425
|
+
) -> str:
|
|
426
|
+
"""Create a config line from arguments."""
|
|
427
|
+
line = CFGCommand.FieldDelimiters.join(str(a) for a in args)
|
|
428
|
+
return f"{PY_PREFIX}{line}" if (py_device and prefix_py_devices) else line
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _iter_config_sections(
|
|
432
|
+
core: UniMMCore, prefix_py_devices: bool = True
|
|
433
|
+
) -> Iterable[tuple[str, Iterable[str]]]:
|
|
434
|
+
"""Iterate over config sections, yielding (header, lines) tuples."""
|
|
435
|
+
pfx = prefix_py_devices # shorthand
|
|
436
|
+
yield f"Generated by pymmcore-plus UniMMCore on {timestamp()}", []
|
|
437
|
+
|
|
438
|
+
# Reset command
|
|
439
|
+
yield (
|
|
440
|
+
"Unload all devices",
|
|
441
|
+
[
|
|
442
|
+
_serialize(
|
|
443
|
+
CFGCommand.Property, Keyword.CoreDevice, Keyword.CoreInitialize, 0
|
|
444
|
+
)
|
|
445
|
+
],
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Load devices
|
|
449
|
+
yield "Load devices", list(_iter_devices(core, pfx))
|
|
450
|
+
|
|
451
|
+
# Pre-initialization properties
|
|
452
|
+
yield "Pre-initialization properties", list(_iter_pre_init_props(core, pfx))
|
|
453
|
+
|
|
454
|
+
# Hub references
|
|
455
|
+
yield "Hub references", list(_iter_hub_refs(core))
|
|
456
|
+
|
|
457
|
+
# Initialize command
|
|
458
|
+
yield (
|
|
459
|
+
"Initialize",
|
|
460
|
+
[
|
|
461
|
+
_serialize(
|
|
462
|
+
CFGCommand.Property, Keyword.CoreDevice, Keyword.CoreInitialize, 1
|
|
463
|
+
)
|
|
464
|
+
],
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Delays
|
|
468
|
+
yield "Delays", list(_iter_delays(core, pfx))
|
|
469
|
+
|
|
470
|
+
# Focus directions
|
|
471
|
+
yield "Stage focus directions", list(_iter_focus_directions(core, pfx))
|
|
472
|
+
|
|
473
|
+
# Labels
|
|
474
|
+
yield "Labels", list(_iter_labels(core, pfx))
|
|
475
|
+
|
|
476
|
+
# Config groups
|
|
477
|
+
yield "Configuration presets", list(_iter_config_groups(core, pfx))
|
|
478
|
+
|
|
479
|
+
# Pixel size configs
|
|
480
|
+
yield "PixelSize settings", list(_iter_pixel_size_configs(core, pfx))
|
|
481
|
+
|
|
482
|
+
# Core device roles
|
|
483
|
+
yield "Roles", list(_iter_roles(core, pfx))
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _iter_devices(core: UniMMCore, prefix_py_devices: bool = True) -> Iterable[str]:
|
|
487
|
+
"""Iterate over device load commands."""
|
|
488
|
+
for label in core.getLoadedDevices():
|
|
489
|
+
if label == Keyword.CoreDevice: # type: ignore[comparison-overlap]
|
|
490
|
+
continue
|
|
491
|
+
is_py = core.isPyDevice(label)
|
|
492
|
+
library = core.getDeviceLibrary(label)
|
|
493
|
+
device_name = core.getDeviceName(label)
|
|
494
|
+
# Use pyDevice command for Python devices, Device for C++ devices
|
|
495
|
+
cmd = _PY_DEVICE_CMD if is_py else CFGCommand.Device
|
|
496
|
+
yield _serialize(
|
|
497
|
+
cmd,
|
|
498
|
+
label,
|
|
499
|
+
library,
|
|
500
|
+
device_name,
|
|
501
|
+
py_device=is_py,
|
|
502
|
+
prefix_py_devices=prefix_py_devices,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _iter_pre_init_props(
|
|
507
|
+
core: UniMMCore, prefix_py_devices: bool = True
|
|
508
|
+
) -> Iterable[str]:
|
|
509
|
+
"""Iterate over pre-initialization property commands."""
|
|
510
|
+
for label in core.getLoadedDevices():
|
|
511
|
+
if label == Keyword.CoreDevice: # type: ignore[comparison-overlap]
|
|
512
|
+
continue
|
|
513
|
+
is_py = core.isPyDevice(label)
|
|
514
|
+
for prop_name in core.getDevicePropertyNames(label):
|
|
515
|
+
if core.isPropertyPreInit(label, prop_name):
|
|
516
|
+
value = core.getProperty(label, prop_name)
|
|
517
|
+
yield _serialize(
|
|
518
|
+
CFGCommand.Property,
|
|
519
|
+
label,
|
|
520
|
+
prop_name,
|
|
521
|
+
value,
|
|
522
|
+
py_device=is_py,
|
|
523
|
+
prefix_py_devices=prefix_py_devices,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _iter_hub_refs(core: UniMMCore) -> Iterable[str]:
|
|
528
|
+
"""Iterate over parent hub reference commands."""
|
|
529
|
+
for label in core.getLoadedDevices():
|
|
530
|
+
if label == Keyword.CoreDevice: # type: ignore[comparison-overlap]
|
|
531
|
+
continue
|
|
532
|
+
is_py = core.isPyDevice(label)
|
|
533
|
+
# Python devices don't have parent labels (yet)
|
|
534
|
+
if is_py:
|
|
535
|
+
continue
|
|
536
|
+
try:
|
|
537
|
+
parent = core.getParentLabel(label)
|
|
538
|
+
except RuntimeError:
|
|
539
|
+
continue
|
|
540
|
+
if parent:
|
|
541
|
+
yield _serialize(CFGCommand.ParentID, label, parent, py_device=is_py)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _iter_delays(core: UniMMCore, prefix_py_devices: bool = True) -> Iterable[str]:
|
|
545
|
+
"""Iterate over device delay commands."""
|
|
546
|
+
for label in core.getLoadedDevices():
|
|
547
|
+
if label == Keyword.CoreDevice: # type: ignore[comparison-overlap]
|
|
548
|
+
continue
|
|
549
|
+
delay = core.getDeviceDelayMs(label)
|
|
550
|
+
if delay > 0:
|
|
551
|
+
yield _serialize(
|
|
552
|
+
CFGCommand.Delay,
|
|
553
|
+
label,
|
|
554
|
+
delay,
|
|
555
|
+
py_device=core.isPyDevice(label),
|
|
556
|
+
prefix_py_devices=prefix_py_devices,
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _iter_focus_directions(
|
|
561
|
+
core: UniMMCore, prefix_py_devices: bool = True
|
|
562
|
+
) -> Iterable[str]:
|
|
563
|
+
"""Iterate over focus direction commands."""
|
|
564
|
+
for label in core.getLoadedDevicesOfType(DeviceType.Stage):
|
|
565
|
+
is_py = core.isPyDevice(label)
|
|
566
|
+
direction = core.getFocusDirection(label)
|
|
567
|
+
yield _serialize(
|
|
568
|
+
CFGCommand.FocusDirection,
|
|
569
|
+
label,
|
|
570
|
+
direction.value,
|
|
571
|
+
py_device=is_py,
|
|
572
|
+
prefix_py_devices=prefix_py_devices,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _iter_labels(core: UniMMCore, prefix_py_devices: bool = True) -> Iterable[str]:
|
|
577
|
+
"""Iterate over state device label commands."""
|
|
578
|
+
for label in core.getLoadedDevicesOfType(DeviceType.State):
|
|
579
|
+
is_py = core.isPyDevice(label)
|
|
580
|
+
labels = core.getStateLabels(label)
|
|
581
|
+
for state, state_label in enumerate(labels):
|
|
582
|
+
if state_label:
|
|
583
|
+
yield _serialize(
|
|
584
|
+
CFGCommand.Label,
|
|
585
|
+
label,
|
|
586
|
+
state,
|
|
587
|
+
state_label,
|
|
588
|
+
py_device=is_py,
|
|
589
|
+
prefix_py_devices=prefix_py_devices,
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _iter_config_groups(
|
|
594
|
+
core: UniMMCore, prefix_py_devices: bool = True
|
|
595
|
+
) -> Iterable[str]:
|
|
596
|
+
"""Iterate over config group commands."""
|
|
597
|
+
for group_name in core.getAvailableConfigGroups():
|
|
598
|
+
presets = core.getAvailableConfigs(group_name)
|
|
599
|
+
if not presets:
|
|
600
|
+
# Empty group
|
|
601
|
+
yield _serialize(CFGCommand.ConfigGroup, group_name)
|
|
602
|
+
continue
|
|
603
|
+
|
|
604
|
+
for preset_name in presets:
|
|
605
|
+
config = core.getConfigData(group_name, preset_name, native=True)
|
|
606
|
+
for i in range(config.size()):
|
|
607
|
+
setting = config.getSetting(i)
|
|
608
|
+
device = setting.getDeviceLabel()
|
|
609
|
+
prop = setting.getPropertyName()
|
|
610
|
+
value = setting.getPropertyValue()
|
|
611
|
+
is_py = core.isPyDevice(device)
|
|
612
|
+
yield _serialize(
|
|
613
|
+
CFGCommand.ConfigGroup,
|
|
614
|
+
group_name,
|
|
615
|
+
preset_name,
|
|
616
|
+
device,
|
|
617
|
+
prop,
|
|
618
|
+
value,
|
|
619
|
+
py_device=is_py,
|
|
620
|
+
prefix_py_devices=prefix_py_devices,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _iter_pixel_size_configs(
|
|
625
|
+
core: UniMMCore, prefix_py_devices: bool = True
|
|
626
|
+
) -> Iterable[str]:
|
|
627
|
+
"""Iterate over pixel size config commands."""
|
|
628
|
+
for preset_name in core.getAvailablePixelSizeConfigs():
|
|
629
|
+
config = core.getPixelSizeConfigData(preset_name, native=True)
|
|
630
|
+
for i in range(config.size()):
|
|
631
|
+
setting = config.getSetting(i)
|
|
632
|
+
device = setting.getDeviceLabel()
|
|
633
|
+
prop = setting.getPropertyName()
|
|
634
|
+
value = setting.getPropertyValue()
|
|
635
|
+
# Pixel size configs typically only reference C++ devices
|
|
636
|
+
is_py = core.isPyDevice(device)
|
|
637
|
+
yield _serialize(
|
|
638
|
+
CFGCommand.ConfigPixelSize,
|
|
639
|
+
preset_name,
|
|
640
|
+
device,
|
|
641
|
+
prop,
|
|
642
|
+
value,
|
|
643
|
+
py_device=is_py,
|
|
644
|
+
prefix_py_devices=prefix_py_devices,
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
# Pixel size
|
|
648
|
+
size = core.getPixelSizeUmByID(preset_name)
|
|
649
|
+
yield _serialize(CFGCommand.PixelSize_um, preset_name, size)
|
|
650
|
+
|
|
651
|
+
# Affine transform
|
|
652
|
+
affine = core.getPixelSizeAffineByID(preset_name)
|
|
653
|
+
if affine and any(v != 0 for v in affine):
|
|
654
|
+
yield _serialize(CFGCommand.PixelSizeAffine, preset_name, *affine)
|
|
655
|
+
|
|
656
|
+
# Optional extended pixel size properties
|
|
657
|
+
if hasattr(core, "getPixelSizedxdz"):
|
|
658
|
+
dxdz = core.getPixelSizedxdz(preset_name)
|
|
659
|
+
if dxdz:
|
|
660
|
+
yield _serialize(CFGCommand.PixelSize_dxdz, preset_name, dxdz)
|
|
661
|
+
if hasattr(core, "getPixelSizedydz"):
|
|
662
|
+
dydz = core.getPixelSizedydz(preset_name)
|
|
663
|
+
if dydz:
|
|
664
|
+
yield _serialize(CFGCommand.PixelSize_dydz, preset_name, dydz)
|
|
665
|
+
if hasattr(core, "getPixelSizeOptimalZUm"):
|
|
666
|
+
optimal_z = core.getPixelSizeOptimalZUm(preset_name)
|
|
667
|
+
if optimal_z:
|
|
668
|
+
yield _serialize(
|
|
669
|
+
CFGCommand.PixelSize_OptimalZUm, preset_name, optimal_z
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _iter_roles(core: UniMMCore, prefix_py_devices: bool = True) -> Iterable[str]:
|
|
674
|
+
"""Iterate over core device role commands."""
|
|
675
|
+
roles = [
|
|
676
|
+
(Keyword.CoreCamera, core.getCameraDevice),
|
|
677
|
+
(Keyword.CoreShutter, core.getShutterDevice),
|
|
678
|
+
(Keyword.CoreFocus, core.getFocusDevice),
|
|
679
|
+
(Keyword.CoreXYStage, core.getXYStageDevice),
|
|
680
|
+
(Keyword.CoreAutoFocus, core.getAutoFocusDevice),
|
|
681
|
+
(Keyword.CoreSLM, core.getSLMDevice),
|
|
682
|
+
(Keyword.CoreGalvo, core.getGalvoDevice),
|
|
683
|
+
]
|
|
684
|
+
|
|
685
|
+
for role_keyword, getter in roles:
|
|
686
|
+
try:
|
|
687
|
+
if device := getter():
|
|
688
|
+
yield _serialize(
|
|
689
|
+
CFGCommand.Property,
|
|
690
|
+
Keyword.CoreDevice,
|
|
691
|
+
role_keyword,
|
|
692
|
+
device,
|
|
693
|
+
py_device=core.isPyDevice(device),
|
|
694
|
+
prefix_py_devices=prefix_py_devices,
|
|
695
|
+
)
|
|
696
|
+
except Exception: # pragma: no cover
|
|
697
|
+
# Some getters may not be available
|
|
698
|
+
pass
|
|
699
|
+
|
|
700
|
+
# AutoShutter setting
|
|
701
|
+
yield _serialize(
|
|
702
|
+
CFGCommand.Property,
|
|
703
|
+
Keyword.CoreDevice,
|
|
704
|
+
Keyword.CoreAutoShutter,
|
|
705
|
+
int(core.getAutoShutter()),
|
|
706
|
+
)
|