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.
- pymmcore_plus/__init__.py +4 -1
- pymmcore_plus/_build.py +2 -0
- pymmcore_plus/_cli.py +49 -14
- 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 +69 -49
- pymmcore_plus/mda/__init__.py +2 -2
- pymmcore_plus/mda/_engine.py +151 -102
- 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 +36 -2
- pymmcore_plus/model/_pixel_size_config.py +26 -4
- {pymmcore_plus-0.10.2.dist-info → pymmcore_plus-0.11.1.dist-info}/METADATA +6 -5
- pymmcore_plus-0.11.1.dist-info/RECORD +59 -0
- {pymmcore_plus-0.10.2.dist-info → pymmcore_plus-0.11.1.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.1.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.10.2.dist-info → pymmcore_plus-0.11.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -31,6 +31,7 @@ from psygnal import SignalInstance
|
|
|
31
31
|
from pymmcore_plus._logger import current_logfile, logger
|
|
32
32
|
from pymmcore_plus._util import find_micromanager, print_tabular_data
|
|
33
33
|
from pymmcore_plus.mda import MDAEngine, MDARunner, PMDAEngine
|
|
34
|
+
from pymmcore_plus.metadata.functions import summary_metadata
|
|
34
35
|
|
|
35
36
|
from ._adapter import DeviceAdapter
|
|
36
37
|
from ._config import Configuration
|
|
@@ -39,6 +40,7 @@ from ._constants import (
|
|
|
39
40
|
DeviceDetectionStatus,
|
|
40
41
|
DeviceInitializationState,
|
|
41
42
|
DeviceType,
|
|
43
|
+
FocusDirection,
|
|
42
44
|
PixelType,
|
|
43
45
|
PropertyType,
|
|
44
46
|
)
|
|
@@ -46,7 +48,6 @@ from ._device import Device
|
|
|
46
48
|
from ._metadata import Metadata
|
|
47
49
|
from ._property import DeviceProperty
|
|
48
50
|
from ._sequencing import can_sequence_events
|
|
49
|
-
from ._state import core_state
|
|
50
51
|
from .events import CMMCoreSignaler, PCoreSignaler, _get_auto_core_callback_class
|
|
51
52
|
|
|
52
53
|
if TYPE_CHECKING:
|
|
@@ -56,8 +57,7 @@ if TYPE_CHECKING:
|
|
|
56
57
|
from useq import MDAEvent
|
|
57
58
|
|
|
58
59
|
from pymmcore_plus.mda._runner import SingleOutput
|
|
59
|
-
|
|
60
|
-
from ._state import StateDict
|
|
60
|
+
from pymmcore_plus.metadata.schema import SummaryMetaV1
|
|
61
61
|
|
|
62
62
|
_T = TypeVar("_T")
|
|
63
63
|
_F = TypeVar("_F", bound=Callable[..., Any])
|
|
@@ -193,6 +193,9 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
193
193
|
if _instance is None:
|
|
194
194
|
_instance = self
|
|
195
195
|
|
|
196
|
+
if hasattr("self", "enableFeature"):
|
|
197
|
+
self.enableFeature("StrictInitializationChecks", True)
|
|
198
|
+
|
|
196
199
|
# TODO: test this on windows ... writing to the same file may be an issue there
|
|
197
200
|
if logfile := current_logfile(logger):
|
|
198
201
|
self.setPrimaryLogFile(str(logfile))
|
|
@@ -400,6 +403,14 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
400
403
|
"""
|
|
401
404
|
return DeviceType(super().getDeviceType(label))
|
|
402
405
|
|
|
406
|
+
def getFocusDirection(self, stageLabel: str) -> FocusDirection:
|
|
407
|
+
"""Return device type for a given device.
|
|
408
|
+
|
|
409
|
+
**Why Override?** The returned [`pymmcore_plus.FocusDirection`][] enum is more
|
|
410
|
+
interpretable than the raw `int` returned by `pymmcore`
|
|
411
|
+
"""
|
|
412
|
+
return FocusDirection(super().getFocusDirection(stageLabel))
|
|
413
|
+
|
|
403
414
|
def getPropertyType(self, label: str, propName: str) -> PropertyType:
|
|
404
415
|
"""Return the intrinsic property type for a given device and property.
|
|
405
416
|
|
|
@@ -621,7 +632,7 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
621
632
|
|
|
622
633
|
@synchronized(_lock)
|
|
623
634
|
def popNextImageAndMD(
|
|
624
|
-
self, channel: int
|
|
635
|
+
self, channel: int = 0, slice: int = 0, *, fix: bool = True
|
|
625
636
|
) -> tuple[np.ndarray, Metadata]:
|
|
626
637
|
"""Gets and removes the next image (and metadata) from the circular buffer.
|
|
627
638
|
|
|
@@ -651,10 +662,7 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
651
662
|
Image and metadata
|
|
652
663
|
"""
|
|
653
664
|
md = Metadata()
|
|
654
|
-
|
|
655
|
-
img = super().popNextImageMD(channel, slice, md)
|
|
656
|
-
else:
|
|
657
|
-
img = super().popNextImageMD(md)
|
|
665
|
+
img = super().popNextImageMD(channel, slice, md)
|
|
658
666
|
return (self.fixImage(img) if fix else img, md)
|
|
659
667
|
|
|
660
668
|
@synchronized(_lock)
|
|
@@ -1470,7 +1478,11 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1470
1478
|
old_engine = self.mda.set_engine(engine)
|
|
1471
1479
|
self.events.mdaEngineRegistered.emit(engine, old_engine)
|
|
1472
1480
|
|
|
1473
|
-
def fixImage(
|
|
1481
|
+
def fixImage(
|
|
1482
|
+
self,
|
|
1483
|
+
img: np.ndarray,
|
|
1484
|
+
ncomponents: int | None = None,
|
|
1485
|
+
) -> np.ndarray:
|
|
1474
1486
|
"""Fix img shape/dtype based on `self.getNumberOfComponents()`.
|
|
1475
1487
|
|
|
1476
1488
|
:sparkles: *This method is new in `CMMCorePlus`.*
|
|
@@ -1498,6 +1510,28 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1498
1510
|
img = img[..., [2, 1, 0]] # Convert from BGRA to RGB
|
|
1499
1511
|
return img
|
|
1500
1512
|
|
|
1513
|
+
def getPhysicalCameraDevice(self, channel_index: int = 0) -> str:
|
|
1514
|
+
"""Return the name of the actual camera device for a given channel index.
|
|
1515
|
+
|
|
1516
|
+
:sparkles: *This method is new in `CMMCorePlus`.* It provides a convenience
|
|
1517
|
+
for accessing the name of the actual camera device when using the multi-camera
|
|
1518
|
+
utility.
|
|
1519
|
+
"""
|
|
1520
|
+
cam_dev = self.getCameraDevice()
|
|
1521
|
+
# best as I can tell, this is a hard-coded string in Utilities/MultiCamera.cpp
|
|
1522
|
+
# (it also appears in ArduinoCounter.cpp). This appears to be "the way"
|
|
1523
|
+
# to get at the original camera when using the multi-camera utility.
|
|
1524
|
+
prop_name = f"Physical Camera {channel_index+1}"
|
|
1525
|
+
if self.hasProperty(cam_dev, prop_name):
|
|
1526
|
+
return self.getProperty(cam_dev, prop_name)
|
|
1527
|
+
if channel_index > 0:
|
|
1528
|
+
warnings.warn(
|
|
1529
|
+
f"Camera {cam_dev} does not have a property {prop_name}. "
|
|
1530
|
+
f"Cannot get channel_index={channel_index}",
|
|
1531
|
+
stacklevel=2,
|
|
1532
|
+
)
|
|
1533
|
+
return cam_dev
|
|
1534
|
+
|
|
1501
1535
|
def getTaggedImage(self, channel_index: int = 0) -> TaggedImage:
|
|
1502
1536
|
"""Return getImage as named tuple with metadata.
|
|
1503
1537
|
|
|
@@ -1560,7 +1594,7 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1560
1594
|
|
|
1561
1595
|
try:
|
|
1562
1596
|
channel_group = self.getPropertyFromCache("Core", "ChannelGroup")
|
|
1563
|
-
channel = self.getCurrentConfigFromCache(channel_group)
|
|
1597
|
+
channel: str = self.getCurrentConfigFromCache(channel_group)
|
|
1564
1598
|
except Exception:
|
|
1565
1599
|
channel = "Default"
|
|
1566
1600
|
tags["Channel"] = channel
|
|
@@ -1570,15 +1604,9 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1570
1604
|
tags["Binning"] = self.getProperty(self.getCameraDevice(), "Binning")
|
|
1571
1605
|
|
|
1572
1606
|
if channel_index is not None:
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
if "Camera" not in tags:
|
|
1577
|
-
core_cam = tags.get("Core-Camera")
|
|
1578
|
-
phys_cam_key = f"{core_cam}-Physical Camera {channel_index+1}"
|
|
1579
|
-
if phys_cam_key in tags:
|
|
1580
|
-
tags["Camera"] = tags[phys_cam_key]
|
|
1581
|
-
# tags["Channel"] = tags[phys_cam_key] # ?? why did MMCoreJ do this?
|
|
1607
|
+
tags["CameraChannelIndex"] = channel_index
|
|
1608
|
+
tags["ChannelIndex"] = channel_index
|
|
1609
|
+
tags["Camera"] = self.getPhysicalCameraDevice(channel_index)
|
|
1582
1610
|
|
|
1583
1611
|
# these are added by AcqEngJ
|
|
1584
1612
|
# yyyy-MM-dd HH:mm:ss.mmmmmm # NOTE AcqEngJ omits microseconds
|
|
@@ -1982,7 +2010,7 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1982
2010
|
|
|
1983
2011
|
:sparkles: *This method is new in `CMMCorePlus`.*
|
|
1984
2012
|
"""
|
|
1985
|
-
_current = {
|
|
2013
|
+
_current: dict[str, str] = {
|
|
1986
2014
|
self.getCameraDevice(): "Camera",
|
|
1987
2015
|
self.getXYStageDevice(): "XYStage",
|
|
1988
2016
|
self.getFocusDevice(): "Focus",
|
|
@@ -2009,35 +2037,17 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
2009
2037
|
print_tabular_data(data, sort=sort)
|
|
2010
2038
|
|
|
2011
2039
|
def state(
|
|
2012
|
-
self,
|
|
2013
|
-
|
|
2014
|
-
devices: bool = True,
|
|
2015
|
-
image: bool = True,
|
|
2016
|
-
system_info: bool = False,
|
|
2017
|
-
system_status: bool = False,
|
|
2018
|
-
config_groups: bool | Sequence[str] = True,
|
|
2019
|
-
position: bool = False,
|
|
2020
|
-
autofocus: bool = False,
|
|
2021
|
-
pixel_size_configs: bool = False,
|
|
2022
|
-
device_types: bool = False,
|
|
2023
|
-
cached: bool = True,
|
|
2024
|
-
error_value: Any = None,
|
|
2025
|
-
) -> StateDict:
|
|
2040
|
+
self, *, cached: bool = True, include_time: bool = False, **_kwargs: Any
|
|
2041
|
+
) -> SummaryMetaV1:
|
|
2026
2042
|
"""Return info on the current state of the core."""
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
autofocus=autofocus,
|
|
2036
|
-
pixel_size_configs=pixel_size_configs,
|
|
2037
|
-
device_types=device_types,
|
|
2038
|
-
cached=cached,
|
|
2039
|
-
error_value=error_value,
|
|
2040
|
-
)
|
|
2043
|
+
if _kwargs:
|
|
2044
|
+
keys = ", ".join(_kwargs.keys())
|
|
2045
|
+
warnings.warn(
|
|
2046
|
+
f"CMMCorePlus.state no longer takes arguments: {keys}. Ignoring."
|
|
2047
|
+
"Please update your code as this may be an error in the future.",
|
|
2048
|
+
stacklevel=2,
|
|
2049
|
+
)
|
|
2050
|
+
return summary_metadata(self, include_time=include_time, cached=cached)
|
|
2041
2051
|
|
|
2042
2052
|
@contextmanager
|
|
2043
2053
|
def _property_change_emission_ensured(
|
|
@@ -2061,8 +2071,18 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
2061
2071
|
and self.getDeviceType(device) is DeviceType.StateDevice
|
|
2062
2072
|
):
|
|
2063
2073
|
properties = STATE_PROPS
|
|
2074
|
+
try:
|
|
2075
|
+
before = [self.getProperty(device, p) for p in properties]
|
|
2076
|
+
except Exception as e:
|
|
2077
|
+
logger.error(
|
|
2078
|
+
"Error getting properties %s on %s: %s. Cannot ensure signal emission",
|
|
2079
|
+
properties,
|
|
2080
|
+
device,
|
|
2081
|
+
e,
|
|
2082
|
+
)
|
|
2083
|
+
yield
|
|
2084
|
+
return
|
|
2064
2085
|
|
|
2065
|
-
before = [self.getProperty(device, p) for p in properties]
|
|
2066
2086
|
with _blockSignal(self.events, self.events.propertyChanged):
|
|
2067
2087
|
yield
|
|
2068
2088
|
after = [self.getProperty(device, p) for p in properties]
|
pymmcore_plus/mda/__init__.py
CHANGED
pymmcore_plus/mda/_engine.py
CHANGED
|
@@ -2,14 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
4
|
from contextlib import suppress
|
|
5
|
-
from
|
|
5
|
+
from itertools import product
|
|
6
6
|
from typing import (
|
|
7
7
|
TYPE_CHECKING,
|
|
8
|
-
Any,
|
|
9
8
|
Iterable,
|
|
10
9
|
Iterator,
|
|
11
|
-
Mapping,
|
|
12
10
|
NamedTuple,
|
|
11
|
+
Sequence,
|
|
13
12
|
cast,
|
|
14
13
|
)
|
|
15
14
|
|
|
@@ -17,40 +16,25 @@ from useq import HardwareAutofocus, MDAEvent, MDASequence
|
|
|
17
16
|
|
|
18
17
|
from pymmcore_plus._logger import logger
|
|
19
18
|
from pymmcore_plus._util import retry
|
|
20
|
-
from pymmcore_plus.core._constants import
|
|
19
|
+
from pymmcore_plus.core._constants import Keyword
|
|
21
20
|
from pymmcore_plus.core._sequencing import SequencedEvent
|
|
21
|
+
from pymmcore_plus.metadata import (
|
|
22
|
+
FrameMetaV1,
|
|
23
|
+
PropertyValue,
|
|
24
|
+
SummaryMetaV1,
|
|
25
|
+
frame_metadata,
|
|
26
|
+
summary_metadata,
|
|
27
|
+
)
|
|
22
28
|
|
|
23
29
|
from ._protocol import PMDAEngine
|
|
24
30
|
|
|
25
31
|
if TYPE_CHECKING:
|
|
26
|
-
from typing import TypedDict
|
|
27
|
-
|
|
28
32
|
from numpy.typing import NDArray
|
|
29
33
|
|
|
30
|
-
from pymmcore_plus.core import CMMCorePlus
|
|
34
|
+
from pymmcore_plus.core import CMMCorePlus
|
|
31
35
|
|
|
32
36
|
from ._protocol import PImagePayload
|
|
33
37
|
|
|
34
|
-
# currently matching keys from metadata from AcqEngJ
|
|
35
|
-
SummaryMetadata = TypedDict(
|
|
36
|
-
"SummaryMetadata",
|
|
37
|
-
{
|
|
38
|
-
"DateAndTime": str,
|
|
39
|
-
"PixelType": str,
|
|
40
|
-
"PixelSize_um": float,
|
|
41
|
-
"PixelSizeAffine": str,
|
|
42
|
-
"Core-XYStage": str,
|
|
43
|
-
"Core-Focus": str,
|
|
44
|
-
"Core-Autofocus": str,
|
|
45
|
-
"Core-Camera": str,
|
|
46
|
-
"Core-Galvo": str,
|
|
47
|
-
"Core-ImageProcessor": str,
|
|
48
|
-
"Core-SLM": str,
|
|
49
|
-
"Core-Shutter": str,
|
|
50
|
-
"AffineTransform": str,
|
|
51
|
-
},
|
|
52
|
-
)
|
|
53
|
-
|
|
54
38
|
|
|
55
39
|
class MDAEngine(PMDAEngine):
|
|
56
40
|
"""The default MDAengine that ships with pymmcore-plus.
|
|
@@ -58,6 +42,9 @@ class MDAEngine(PMDAEngine):
|
|
|
58
42
|
This implements the [`PMDAEngine`][pymmcore_plus.mda.PMDAEngine] protocol, and
|
|
59
43
|
uses a [`CMMCorePlus`][pymmcore_plus.CMMCorePlus] instance to control the hardware.
|
|
60
44
|
|
|
45
|
+
It may be subclassed to provide custom behavior, or to override specific methods.
|
|
46
|
+
<https://pymmcore-plus.github.io/pymmcore-plus/guides/custom_engine/>
|
|
47
|
+
|
|
61
48
|
Attributes
|
|
62
49
|
----------
|
|
63
50
|
mmcore: CMMCorePlus
|
|
@@ -67,12 +54,11 @@ class MDAEngine(PMDAEngine):
|
|
|
67
54
|
attempt to combine MDAEvents into a single `SequencedEvent` if
|
|
68
55
|
[`core.canSequenceEvents()`][pymmcore_plus.CMMCorePlus.canSequenceEvents]
|
|
69
56
|
reports that the events can be sequenced. This can be set after instantiation.
|
|
70
|
-
By default, this is `
|
|
71
|
-
|
|
72
|
-
set to `True` to improve performance.
|
|
57
|
+
By default, this is `True`, however in various testing and demo scenarios, you
|
|
58
|
+
may wish to set it to `False` in order to avoid unexpected behavior.
|
|
73
59
|
"""
|
|
74
60
|
|
|
75
|
-
def __init__(self, mmc: CMMCorePlus, use_hardware_sequencing: bool =
|
|
61
|
+
def __init__(self, mmc: CMMCorePlus, use_hardware_sequencing: bool = True) -> None:
|
|
76
62
|
self._mmc = mmc
|
|
77
63
|
self.use_hardware_sequencing = use_hardware_sequencing
|
|
78
64
|
|
|
@@ -91,6 +77,13 @@ class MDAEngine(PMDAEngine):
|
|
|
91
77
|
# Note: getAutoShutter() is True when no config is loaded at all
|
|
92
78
|
self._autoshutter_was_set: bool = self._mmc.getAutoShutter()
|
|
93
79
|
|
|
80
|
+
# -----
|
|
81
|
+
# The following values are stored during setup_sequence simply to speed up
|
|
82
|
+
# retrieval of metadata during each frame.
|
|
83
|
+
# sequence of (device, property) of all properties used in any of the presets
|
|
84
|
+
# in the channel group.
|
|
85
|
+
self._config_device_props: dict[str, Sequence[tuple[str, str]]] = {}
|
|
86
|
+
|
|
94
87
|
@property
|
|
95
88
|
def mmcore(self) -> CMMCorePlus:
|
|
96
89
|
"""The `CMMCorePlus` instance to use for hardware control."""
|
|
@@ -98,7 +91,7 @@ class MDAEngine(PMDAEngine):
|
|
|
98
91
|
|
|
99
92
|
# ===================== Protocol Implementation =====================
|
|
100
93
|
|
|
101
|
-
def setup_sequence(self, sequence: MDASequence) ->
|
|
94
|
+
def setup_sequence(self, sequence: MDASequence) -> SummaryMetaV1 | None:
|
|
102
95
|
"""Setup the hardware for the entire sequence."""
|
|
103
96
|
# clear z_correction for new sequence
|
|
104
97
|
self._z_correction.clear()
|
|
@@ -108,6 +101,7 @@ class MDAEngine(PMDAEngine):
|
|
|
108
101
|
|
|
109
102
|
self._mmc = CMMCorePlus.instance()
|
|
110
103
|
|
|
104
|
+
self._update_config_device_props()
|
|
111
105
|
# get if the autofocus is engaged at the start of the sequence
|
|
112
106
|
self._af_was_engaged = self._mmc.isContinuousFocusLocked()
|
|
113
107
|
|
|
@@ -115,30 +109,10 @@ class MDAEngine(PMDAEngine):
|
|
|
115
109
|
self._update_grid_fov_sizes(px_size, sequence)
|
|
116
110
|
|
|
117
111
|
self._autoshutter_was_set = self._mmc.getAutoShutter()
|
|
118
|
-
return self.get_summary_metadata()
|
|
112
|
+
return self.get_summary_metadata(mda_sequence=sequence)
|
|
119
113
|
|
|
120
|
-
def get_summary_metadata(self) ->
|
|
121
|
-
|
|
122
|
-
pt = PixelType.for_bytes(
|
|
123
|
-
self._mmc.getBytesPerPixel(), self._mmc.getNumberOfComponents()
|
|
124
|
-
)
|
|
125
|
-
affine = self._mmc.getPixelSizeAffine(True) # true == cached
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
"DateAndTime": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"),
|
|
129
|
-
"PixelType": str(pt),
|
|
130
|
-
"PixelSize_um": self._mmc.getPixelSizeUm(),
|
|
131
|
-
"PixelSizeAffine": ";".join(str(x) for x in affine),
|
|
132
|
-
"Core-XYStage": self._mmc.getXYStageDevice(),
|
|
133
|
-
"Core-Focus": self._mmc.getFocusDevice(),
|
|
134
|
-
"Core-Autofocus": self._mmc.getAutoFocusDevice(),
|
|
135
|
-
"Core-Camera": self._mmc.getCameraDevice(),
|
|
136
|
-
"Core-Galvo": self._mmc.getGalvoDevice(),
|
|
137
|
-
"Core-ImageProcessor": self._mmc.getImageProcessorDevice(),
|
|
138
|
-
"Core-SLM": self._mmc.getSLMDevice(),
|
|
139
|
-
"Core-Shutter": self._mmc.getShutterDevice(),
|
|
140
|
-
"AffineTransform": "Undefined",
|
|
141
|
-
}
|
|
114
|
+
def get_summary_metadata(self, mda_sequence: MDASequence | None) -> SummaryMetaV1:
|
|
115
|
+
return summary_metadata(self._mmc, mda_sequence=mda_sequence)
|
|
142
116
|
|
|
143
117
|
def _update_grid_fov_sizes(self, px_size: float, sequence: MDASequence) -> None:
|
|
144
118
|
*_, x_size, y_size = self._mmc.getROI()
|
|
@@ -250,6 +224,7 @@ class MDAEngine(PMDAEngine):
|
|
|
250
224
|
|
|
251
225
|
if event.channel is not None:
|
|
252
226
|
try:
|
|
227
|
+
# possible speedup by setting manually.
|
|
253
228
|
self._mmc.setConfig(event.channel.group, event.channel.config)
|
|
254
229
|
except Exception as e:
|
|
255
230
|
logger.warning("Failed to set channel. %s", e)
|
|
@@ -281,46 +256,50 @@ class MDAEngine(PMDAEngine):
|
|
|
281
256
|
"""
|
|
282
257
|
try:
|
|
283
258
|
self._mmc.snapImage()
|
|
259
|
+
# taking event time after snapImage includes exposure time
|
|
260
|
+
# not sure that's what we want, but it's currently consistent with the
|
|
261
|
+
# timing of the sequenced event runner (where Elapsed_Time_ms is taken after
|
|
262
|
+
# the image is acquired, not before the exposure starts)
|
|
263
|
+
t0 = event.metadata.get("runner_t0") or time.perf_counter()
|
|
264
|
+
event_time_ms = (time.perf_counter() - t0) * 1000
|
|
284
265
|
except Exception as e:
|
|
285
266
|
logger.warning("Failed to snap image. %s", e)
|
|
286
267
|
return
|
|
287
268
|
if not event.keep_shutter_open:
|
|
288
269
|
self._mmc.setShutterOpen(False)
|
|
289
|
-
|
|
270
|
+
|
|
271
|
+
# most cameras will only have a single channel
|
|
272
|
+
# but Multi-camera may have multiple, and we need to retrieve a buffer for each
|
|
273
|
+
for cam in range(self._mmc.getNumberOfCameraChannels()):
|
|
274
|
+
meta = self.get_frame_metadata(
|
|
275
|
+
event,
|
|
276
|
+
runner_time_ms=event_time_ms,
|
|
277
|
+
camera_device=self._mmc.getPhysicalCameraDevice(cam),
|
|
278
|
+
)
|
|
279
|
+
# Note, the third element is actually a MutableMapping, but mypy doesn't
|
|
280
|
+
# see TypedDict as a subclass of MutableMapping yet.
|
|
281
|
+
# https://github.com/python/mypy/issues/4976
|
|
282
|
+
yield ImagePayload(self._mmc.getImage(cam), event, meta) # type: ignore[misc]
|
|
290
283
|
|
|
291
284
|
def get_frame_metadata(
|
|
292
|
-
self,
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
# these are added by AcqEngJ
|
|
312
|
-
# yyyy-MM-dd HH:mm:ss.mmmmmm # NOTE AcqEngJ omits microseconds
|
|
313
|
-
tags["Time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
314
|
-
tags["PixelSizeUm"] = self._mmc.getPixelSizeUm(True) # true == cached
|
|
315
|
-
with suppress(RuntimeError):
|
|
316
|
-
tags["XPositionUm"] = self._mmc.getXPosition()
|
|
317
|
-
tags["YPositionUm"] = self._mmc.getYPosition()
|
|
318
|
-
with suppress(RuntimeError):
|
|
319
|
-
tags["ZPositionUm"] = self._mmc.getZPosition()
|
|
320
|
-
|
|
321
|
-
# used by Runner
|
|
322
|
-
tags["PerfCounter"] = time.perf_counter()
|
|
323
|
-
return tags
|
|
285
|
+
self,
|
|
286
|
+
event: MDAEvent,
|
|
287
|
+
prop_values: tuple[PropertyValue, ...] | None = None,
|
|
288
|
+
runner_time_ms: float = 0.0,
|
|
289
|
+
camera_device: str | None = None,
|
|
290
|
+
) -> FrameMetaV1:
|
|
291
|
+
if prop_values is None and (ch := event.channel):
|
|
292
|
+
prop_values = self._get_current_props(ch.group)
|
|
293
|
+
else:
|
|
294
|
+
prop_values = ()
|
|
295
|
+
return frame_metadata(
|
|
296
|
+
self._mmc,
|
|
297
|
+
cached=True,
|
|
298
|
+
runner_time_ms=runner_time_ms,
|
|
299
|
+
camera_device=camera_device,
|
|
300
|
+
property_values=prop_values,
|
|
301
|
+
mda_event=event,
|
|
302
|
+
)
|
|
324
303
|
|
|
325
304
|
def teardown_event(self, event: MDAEvent) -> None:
|
|
326
305
|
"""Teardown state of system (hardware, etc.) after `event`."""
|
|
@@ -434,9 +413,11 @@ class MDAEngine(PMDAEngine):
|
|
|
434
413
|
`exec_event`, which *is* part of the protocol), but it is made public
|
|
435
414
|
in case a user wants to subclass this engine and override this method.
|
|
436
415
|
"""
|
|
437
|
-
# TODO: add support for multiple camera devices
|
|
438
416
|
n_events = len(event.events)
|
|
439
417
|
|
|
418
|
+
t0 = event.metadata.get("runner_t0") or time.perf_counter()
|
|
419
|
+
event_t0_ms = (time.perf_counter() - t0) * 1000
|
|
420
|
+
|
|
440
421
|
# Start sequence
|
|
441
422
|
# Note that the overload of startSequenceAcquisition that takes a camera
|
|
442
423
|
# label does NOT automatically initialize a circular buffer. So if this call
|
|
@@ -446,15 +427,17 @@ class MDAEngine(PMDAEngine):
|
|
|
446
427
|
0, # intervalMS # TODO: add support for this
|
|
447
428
|
True, # stopOnOverflow
|
|
448
429
|
)
|
|
449
|
-
|
|
450
430
|
self.post_sequence_started(event)
|
|
451
431
|
|
|
432
|
+
n_channels = self._mmc.getNumberOfCameraChannels()
|
|
452
433
|
count = 0
|
|
453
|
-
iter_events =
|
|
434
|
+
iter_events = product(event.events, range(n_channels))
|
|
454
435
|
# block until the sequence is done, popping images in the meantime
|
|
455
436
|
while self._mmc.isSequenceRunning():
|
|
456
|
-
if self._mmc.getRemainingImageCount():
|
|
457
|
-
yield self.
|
|
437
|
+
if remaining := self._mmc.getRemainingImageCount():
|
|
438
|
+
yield self._next_seqimg_payload(
|
|
439
|
+
*next(iter_events), remaining=remaining - 1, event_t0=event_t0_ms
|
|
440
|
+
)
|
|
458
441
|
count += 1
|
|
459
442
|
else:
|
|
460
443
|
time.sleep(0.001)
|
|
@@ -462,23 +445,60 @@ class MDAEngine(PMDAEngine):
|
|
|
462
445
|
if self._mmc.isBufferOverflowed(): # pragma: no cover
|
|
463
446
|
raise MemoryError("Buffer overflowed")
|
|
464
447
|
|
|
465
|
-
while self._mmc.getRemainingImageCount():
|
|
466
|
-
yield self.
|
|
448
|
+
while remaining := self._mmc.getRemainingImageCount():
|
|
449
|
+
yield self._next_seqimg_payload(
|
|
450
|
+
*next(iter_events), remaining=remaining - 1, event_t0=event_t0_ms
|
|
451
|
+
)
|
|
467
452
|
count += 1
|
|
468
453
|
|
|
469
|
-
|
|
454
|
+
# necessary?
|
|
455
|
+
expected_images = n_events * n_channels
|
|
456
|
+
if count != expected_images:
|
|
470
457
|
logger.warning(
|
|
471
458
|
"Unexpected number of images returned from sequence. "
|
|
472
459
|
"Expected %s, got %s",
|
|
473
|
-
|
|
460
|
+
expected_images,
|
|
474
461
|
count,
|
|
475
462
|
)
|
|
476
463
|
|
|
477
|
-
def
|
|
464
|
+
def _next_seqimg_payload(
|
|
465
|
+
self,
|
|
466
|
+
event: MDAEvent,
|
|
467
|
+
channel: int = 0,
|
|
468
|
+
*,
|
|
469
|
+
event_t0: float = 0.0,
|
|
470
|
+
remaining: int = 0,
|
|
471
|
+
) -> PImagePayload:
|
|
478
472
|
"""Grab next image from the circular buffer and return it as an ImagePayload."""
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
473
|
+
_slice = 0 # ?
|
|
474
|
+
img, mm_meta = self._mmc.popNextImageAndMD(channel, _slice)
|
|
475
|
+
try:
|
|
476
|
+
seq_time = float(mm_meta.get(Keyword.Elapsed_Time_ms))
|
|
477
|
+
except Exception:
|
|
478
|
+
seq_time = 0.0
|
|
479
|
+
try:
|
|
480
|
+
# note, when present in circular buffer meta, this key is called "Camera".
|
|
481
|
+
# It's NOT actually Keyword.CoreCamera (but it's the same value)
|
|
482
|
+
# it is hardcoded in various places in mmCoreAndDevices, see:
|
|
483
|
+
# see: https://github.com/micro-manager/mmCoreAndDevices/pull/468
|
|
484
|
+
camera_device = mm_meta.GetSingleTag("Camera").GetValue()
|
|
485
|
+
except Exception:
|
|
486
|
+
camera_device = self._mmc.getPhysicalCameraDevice(channel)
|
|
487
|
+
|
|
488
|
+
# TODO: determine whether we want to try to populate changing property values
|
|
489
|
+
# during the course of a triggered sequence
|
|
490
|
+
meta = self.get_frame_metadata(
|
|
491
|
+
event,
|
|
492
|
+
prop_values=(),
|
|
493
|
+
runner_time_ms=event_t0 + seq_time,
|
|
494
|
+
camera_device=camera_device,
|
|
495
|
+
)
|
|
496
|
+
meta["hardware_triggered"] = True
|
|
497
|
+
meta["images_remaining_in_buffer"] = remaining
|
|
498
|
+
meta["camera_metadata"] = dict(mm_meta)
|
|
499
|
+
|
|
500
|
+
# https://github.com/python/mypy/issues/4976
|
|
501
|
+
return ImagePayload(img, event, meta) # type: ignore[return-value]
|
|
482
502
|
|
|
483
503
|
# ===================== EXTRA =====================
|
|
484
504
|
|
|
@@ -528,8 +548,37 @@ class MDAEngine(PMDAEngine):
|
|
|
528
548
|
correction = self._z_correction.setdefault(p_idx, 0.0)
|
|
529
549
|
self._mmc.setZPosition(cast("float", event.z_pos) + correction)
|
|
530
550
|
|
|
551
|
+
def _update_config_device_props(self) -> None:
|
|
552
|
+
# store devices/props that make up each config group for faster lookup
|
|
553
|
+
self._config_device_props.clear()
|
|
554
|
+
for grp in self._mmc.getAvailableConfigGroups():
|
|
555
|
+
for preset in self._mmc.getAvailableConfigs(grp):
|
|
556
|
+
# ordered/unique list of (device, property) tuples for each group
|
|
557
|
+
self._config_device_props[grp] = tuple(
|
|
558
|
+
{(i[0], i[1]): None for i in self._mmc.getConfigData(grp, preset)}
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
def _get_current_props(self, *groups: str) -> tuple[PropertyValue, ...]:
|
|
562
|
+
"""Faster version of core.getConfigGroupState(group).
|
|
563
|
+
|
|
564
|
+
MMCore does some excess iteration that we want to avoid here. It calls
|
|
565
|
+
GetAvailableConfigs and then calls getConfigData for *every* preset in the
|
|
566
|
+
group, (not only the one being requested). We go straight to cached data
|
|
567
|
+
for the group we want.
|
|
568
|
+
"""
|
|
569
|
+
return tuple(
|
|
570
|
+
{
|
|
571
|
+
"dev": dev,
|
|
572
|
+
"prop": prop,
|
|
573
|
+
"val": self._mmc.getPropertyFromCache(dev, prop),
|
|
574
|
+
}
|
|
575
|
+
for group in groups
|
|
576
|
+
if (dev_props := self._config_device_props.get(group))
|
|
577
|
+
for dev, prop in dev_props
|
|
578
|
+
)
|
|
579
|
+
|
|
531
580
|
|
|
532
581
|
class ImagePayload(NamedTuple):
|
|
533
582
|
image: NDArray
|
|
534
583
|
event: MDAEvent
|
|
535
|
-
metadata:
|
|
584
|
+
metadata: FrameMetaV1 | SummaryMetaV1
|
pymmcore_plus/mda/_protocol.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from abc import abstractmethod
|
|
4
|
-
from typing import TYPE_CHECKING,
|
|
4
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
5
5
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
7
7
|
from typing import Iterable, Iterator
|
|
@@ -9,7 +9,9 @@ if TYPE_CHECKING:
|
|
|
9
9
|
from numpy.typing import NDArray
|
|
10
10
|
from useq import MDAEvent, MDASequence
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
from pymmcore_plus.metadata.schema import FrameMetaV1, SummaryMetaV1
|
|
13
|
+
|
|
14
|
+
PImagePayload = tuple[NDArray, MDAEvent, FrameMetaV1]
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
# NOTE: This whole thing could potentially go in useq-schema
|
|
@@ -21,7 +23,7 @@ class PMDAEngine(Protocol):
|
|
|
21
23
|
"""Protocol that all MDA engines must implement."""
|
|
22
24
|
|
|
23
25
|
@abstractmethod
|
|
24
|
-
def setup_sequence(self, sequence: MDASequence) ->
|
|
26
|
+
def setup_sequence(self, sequence: MDASequence) -> SummaryMetaV1 | None:
|
|
25
27
|
"""Setup state of system (hardware, etc.) before an MDA is run.
|
|
26
28
|
|
|
27
29
|
This method is called once at the beginning of a sequence.
|