mesofield 0.3.2b0__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.
- docs/_static/custom.css +40 -0
- docs/_static/favicon.png +0 -0
- docs/_static/logo.png +0 -0
- docs/api/index.md +70 -0
- docs/conf.py +200 -0
- docs/developer_guide.md +303 -0
- docs/index.md +25 -0
- docs/tutorial.md +4 -0
- docs/user_guide.md +172 -0
- examples/teensy_pulse_generator.py +320 -0
- experiments/pipeline_demo/experiment.json +24 -0
- experiments/pipeline_demo/hardware.yaml +23 -0
- experiments/pipeline_demo/procedure.py +50 -0
- experiments/two_cam_demo/experiment.json +24 -0
- experiments/two_cam_demo/hardware.yaml +58 -0
- experiments/two_cam_demo/load_dataset.py +213 -0
- experiments/two_cam_demo/procedure.py +87 -0
- external/video-codecs/openh264-1.8.0-win64.dll +0 -0
- mesofield/__init__.py +45 -0
- mesofield/__main__.py +11 -0
- mesofield/_version.py +24 -0
- mesofield/base.py +750 -0
- mesofield/cli/__init__.py +57 -0
- mesofield/cli/_richhelp.py +100 -0
- mesofield/cli/acquire.py +254 -0
- mesofield/cli/datakit.py +165 -0
- mesofield/cli/process.py +376 -0
- mesofield/cli/rig.py +108 -0
- mesofield/cli/tools.py +347 -0
- mesofield/config.py +751 -0
- mesofield/data/__init__.py +23 -0
- mesofield/data/batch.py +633 -0
- mesofield/data/manager.py +388 -0
- mesofield/data/writer.py +289 -0
- mesofield/datakit/__init__.py +44 -0
- mesofield/datakit/__main__.py +35 -0
- mesofield/datakit/_utils/_logger.py +5 -0
- mesofield/datakit/_version.py +141 -0
- mesofield/datakit/config.py +50 -0
- mesofield/datakit/core.py +783 -0
- mesofield/datakit/datamodel.py +200 -0
- mesofield/datakit/discover.py +124 -0
- mesofield/datakit/explore.py +651 -0
- mesofield/datakit/notebooks/pupil_dlc.ipynb +2445 -0
- mesofield/datakit/profile.py +535 -0
- mesofield/datakit/shell.py +83 -0
- mesofield/datakit/sources/__init__.py +65 -0
- mesofield/datakit/sources/analysis/mesomap.py +194 -0
- mesofield/datakit/sources/analysis/mesoscope.py +77 -0
- mesofield/datakit/sources/analysis/pupil.py +246 -0
- mesofield/datakit/sources/behavior/__init__.py +0 -0
- mesofield/datakit/sources/behavior/dataqueue.py +281 -0
- mesofield/datakit/sources/behavior/psychopy.py +364 -0
- mesofield/datakit/sources/behavior/treadmill.py +323 -0
- mesofield/datakit/sources/behavior/wheel.py +277 -0
- mesofield/datakit/sources/camera/mesoscope.py +32 -0
- mesofield/datakit/sources/camera/metadata_json.py +130 -0
- mesofield/datakit/sources/camera/pupil.py +28 -0
- mesofield/datakit/sources/camera/suite2p.py +547 -0
- mesofield/datakit/sources/register.py +204 -0
- mesofield/datakit/sources/session/config.py +130 -0
- mesofield/datakit/sources/session/notes.py +63 -0
- mesofield/datakit/sources/session/timestamps.py +58 -0
- mesofield/datakit/timeline.py +306 -0
- mesofield/devices/__init__.py +42 -0
- mesofield/devices/base.py +498 -0
- mesofield/devices/base_camera.py +295 -0
- mesofield/devices/cameras.py +740 -0
- mesofield/devices/daq.py +151 -0
- mesofield/devices/encoder.py +384 -0
- mesofield/devices/mocks.py +275 -0
- mesofield/devices/psychopy_device.py +455 -0
- mesofield/devices/subprocesses/__init__.py +0 -0
- mesofield/devices/subprocesses/psychopy.py +133 -0
- mesofield/devices/treadmill.py +318 -0
- mesofield/engines.py +380 -0
- mesofield/gui/Mesofield_icon.png +0 -0
- mesofield/gui/__init__.py +76 -0
- mesofield/gui/config_wizard.py +724 -0
- mesofield/gui/controller.py +535 -0
- mesofield/gui/dynamic_controller.py +78 -0
- mesofield/gui/maingui.py +427 -0
- mesofield/gui/mdagui.py +285 -0
- mesofield/gui/qt_device_adapter.py +109 -0
- mesofield/gui/speedplotter.py +152 -0
- mesofield/gui/theme.py +445 -0
- mesofield/gui/tiff_viewer.py +1050 -0
- mesofield/gui/viewer.py +691 -0
- mesofield/hardware.py +549 -0
- mesofield/playback.py +1298 -0
- mesofield/processing/__init__.py +12 -0
- mesofield/processing/runner.py +237 -0
- mesofield/processors/__init__.py +13 -0
- mesofield/processors/base.py +287 -0
- mesofield/processors/frame_mean.py +19 -0
- mesofield/protocols.py +378 -0
- mesofield/scaffold/__init__.py +34 -0
- mesofield/scaffold/experiment.py +400 -0
- mesofield/scaffold/rigs.py +121 -0
- mesofield/signals.py +85 -0
- mesofield/utils/__init__.py +0 -0
- mesofield/utils/_logger.py +156 -0
- mesofield/utils/retrofit.py +309 -0
- mesofield/utils/utils.py +217 -0
- mesofield-0.3.2b0.dist-info/METADATA +178 -0
- mesofield-0.3.2b0.dist-info/RECORD +111 -0
- mesofield-0.3.2b0.dist-info/WHEEL +5 -0
- mesofield-0.3.2b0.dist-info/entry_points.txt +2 -0
- mesofield-0.3.2b0.dist-info/licenses/LICENSE +21 -0
- mesofield-0.3.2b0.dist-info/top_level.txt +6 -0
- scripts/bench_frame_processor.py +103 -0
mesofield/engines.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""Custom MicroManager ``MDAEngine`` subclasses used by mesofield.
|
|
2
|
+
|
|
3
|
+
Three flavours are provided, all subclasses of
|
|
4
|
+
:class:`pymmcore_plus.mda.MDAEngine`:
|
|
5
|
+
|
|
6
|
+
``MesoEngine``
|
|
7
|
+
Drives the mesoscope camera; loads an LED sequence on the Arduino
|
|
8
|
+
switch in :meth:`MesoEngine.setup_sequence` and streams images out of
|
|
9
|
+
the circular buffer in :meth:`MesoEngine.exec_sequenced_event`.
|
|
10
|
+
|
|
11
|
+
``PupilEngine``
|
|
12
|
+
Drives the pupil camera; optionally pulses an NI-DAQ line at
|
|
13
|
+
sequence start (used for synchronisation with downstream
|
|
14
|
+
instruments).
|
|
15
|
+
|
|
16
|
+
``DevEngine``
|
|
17
|
+
Hardware-free development engine. Streams whatever the camera
|
|
18
|
+
backend produces with no Arduino/NIDAQ interaction; intended for
|
|
19
|
+
mock cameras and CI.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import nidaqmx.system
|
|
23
|
+
import useq
|
|
24
|
+
import time
|
|
25
|
+
from itertools import product
|
|
26
|
+
|
|
27
|
+
from typing import TYPE_CHECKING, Iterable, Mapping, Sequence
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from pymmcore_plus.core._sequencing import SequencedEvent
|
|
30
|
+
from pymmcore_plus.mda.metadata import FrameMetaV1 # type: ignore
|
|
31
|
+
from numpy.typing import NDArray
|
|
32
|
+
from useq import MDAEvent
|
|
33
|
+
PImagePayload = tuple[NDArray, MDAEvent, FrameMetaV1]
|
|
34
|
+
|
|
35
|
+
from pymmcore_plus.metadata import SummaryMetaV1
|
|
36
|
+
from pymmcore_plus.mda import MDAEngine
|
|
37
|
+
import nidaqmx
|
|
38
|
+
from mesofield.utils._logger import get_logger
|
|
39
|
+
|
|
40
|
+
logger = get_logger(__name__)
|
|
41
|
+
|
|
42
|
+
from typing import TYPE_CHECKING
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from mesofield.config import ExperimentConfig
|
|
45
|
+
from pymmcore_plus import CMMCorePlus
|
|
46
|
+
|
|
47
|
+
class MesoEngine(MDAEngine):
|
|
48
|
+
"""MDA engine for the mesoscope camera.
|
|
49
|
+
|
|
50
|
+
Loads an LED switching sequence onto an Arduino driver in
|
|
51
|
+
:meth:`setup_sequence`, then streams images out of the camera's
|
|
52
|
+
circular buffer in :meth:`exec_sequenced_event`. Subclasses
|
|
53
|
+
:class:`pymmcore_plus.mda.MDAEngine`.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, mmc, use_hardware_sequencing: bool = True) -> None:
|
|
57
|
+
super().__init__(mmc)
|
|
58
|
+
self.logger = get_logger(f'{__name__}.{self.__class__.__name__}')
|
|
59
|
+
self._mmc: CMMCorePlus = mmc
|
|
60
|
+
self.use_hardware_sequencing = use_hardware_sequencing
|
|
61
|
+
self._wheel_data = None
|
|
62
|
+
# TODO: add triggerable parameter
|
|
63
|
+
|
|
64
|
+
def set_config(self, cfg) -> None:
|
|
65
|
+
"""Bind a live :class:`~mesofield.config.ExperimentConfig` to this engine.
|
|
66
|
+
|
|
67
|
+
Called by the :class:`~mesofield.base.Procedure` orchestrator after
|
|
68
|
+
hardware is up so the engine can reach the configured encoder.
|
|
69
|
+
"""
|
|
70
|
+
self._config: ExperimentConfig = cfg
|
|
71
|
+
self._encoder = cfg.hardware.encoder
|
|
72
|
+
|
|
73
|
+
def setup_sequence(self, sequence: useq.MDASequence) -> SummaryMetaV1 | None:
|
|
74
|
+
"""Perform setup required before the sequence is executed."""
|
|
75
|
+
|
|
76
|
+
led_sequence = sequence.metadata.get('led_sequence')
|
|
77
|
+
if not led_sequence and hasattr(self, '_config'):
|
|
78
|
+
led_sequence = self._config.led_pattern
|
|
79
|
+
if not led_sequence:
|
|
80
|
+
raise ValueError('Missing led_sequence in MDASequence metadata and ExperimentConfig')
|
|
81
|
+
|
|
82
|
+
camera = getattr(self, 'camera', None)
|
|
83
|
+
self.logger.debug(
|
|
84
|
+
f"setup_sequence: engine.camera={'set' if camera is not None else 'None'} "
|
|
85
|
+
f"has_start_led={hasattr(camera, 'start_led_sequence') if camera else False}"
|
|
86
|
+
)
|
|
87
|
+
if camera is not None and hasattr(camera, 'start_led_sequence'):
|
|
88
|
+
camera.start_led_sequence(led_sequence)
|
|
89
|
+
else:
|
|
90
|
+
self._mmc.getPropertyObject('Arduino-Switch', 'State').loadSequence(led_sequence)
|
|
91
|
+
self._mmc.getPropertyObject('Arduino-Switch', 'State').setValue(4) # seems essential to initiate serial communication
|
|
92
|
+
self._mmc.getPropertyObject('Arduino-Switch', 'State').startSequence()
|
|
93
|
+
|
|
94
|
+
self.logger.debug(f'setup_sequence loaded LED sequence at time: {time.time()}')
|
|
95
|
+
|
|
96
|
+
return super().setup_sequence(sequence)
|
|
97
|
+
|
|
98
|
+
def exec_sequenced_event(self, event: 'SequencedEvent') -> Iterable['PImagePayload']:
|
|
99
|
+
"""Execute a sequenced (triggered) event and return the image data.
|
|
100
|
+
|
|
101
|
+
This method is not part of the PMDAEngine protocol (it is called by
|
|
102
|
+
`exec_event`, which *is* part of the protocol), but it is made public
|
|
103
|
+
in case a user wants to subclass this engine and override this method.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
n_events = len(event.events)
|
|
107
|
+
|
|
108
|
+
t0 = event.metadata.get("runner_t0") or time.perf_counter()
|
|
109
|
+
event_t0_ms = (time.perf_counter() - t0) * 1000
|
|
110
|
+
|
|
111
|
+
# Start sequence
|
|
112
|
+
# Note that the overload of startSequenceAcquisition that takes a camera
|
|
113
|
+
# label does NOT automatically initialize a circular buffer. So if this call
|
|
114
|
+
# is changed to accept the camera in the future, that should be kept in mind.
|
|
115
|
+
self._mmc.startSequenceAcquisition(
|
|
116
|
+
n_events,
|
|
117
|
+
0, # intervalMS
|
|
118
|
+
True, # stopOnOverflow
|
|
119
|
+
)
|
|
120
|
+
self.logger.debug(f'exec_sequenced_event with {n_events} events at t0 {t0}')
|
|
121
|
+
self.post_sequence_started(event)
|
|
122
|
+
|
|
123
|
+
n_channels = self._mmc.getNumberOfCameraChannels()
|
|
124
|
+
count = 0
|
|
125
|
+
iter_events = product(event.events, range(n_channels))
|
|
126
|
+
# block until the sequence is done, popping images in the meantime
|
|
127
|
+
while self._mmc.isSequenceRunning():
|
|
128
|
+
if remaining := self._mmc.getRemainingImageCount():
|
|
129
|
+
img, mm_meta = self._mmc.popNextImageAndMD()
|
|
130
|
+
ev, ch = next(iter_events)
|
|
131
|
+
yield self._create_seqimg_payload_from_popped(
|
|
132
|
+
img, mm_meta, event=ev, channel=ch, remaining=remaining - 1, event_t0=event_t0_ms
|
|
133
|
+
)
|
|
134
|
+
count += 1
|
|
135
|
+
else:
|
|
136
|
+
if count == n_events:
|
|
137
|
+
self.logger.debug(f'stopped MDA with {count} events and {remaining} remaining with {self._mmc.getRemainingImageCount()} images in buffer')
|
|
138
|
+
break
|
|
139
|
+
#self._mmc.stopSequenceAcquisition() Might be source of early cutoff by not allowing engine to save the rest of image in buffer
|
|
140
|
+
#time.sleep(0.001) #does not seem to optimize performance either way
|
|
141
|
+
|
|
142
|
+
if self._mmc.isBufferOverflowed(): # pragma: no cover
|
|
143
|
+
self.logger.warning(f'OVERFLOW MDA: {self._mmc} with {count} events and {remaining} remaining with {self._mmc.getRemainingImageCount()} images in buffer')
|
|
144
|
+
raise MemoryError("Buffer overflowed")
|
|
145
|
+
|
|
146
|
+
while remaining := self._mmc.getRemainingImageCount():
|
|
147
|
+
self.logger.debug(f'Saving Remaining Images in buffer {self._mmc} with {count} events and {remaining} remaining with {self._mmc.getRemainingImageCount()} images in buffer')
|
|
148
|
+
img, mm_meta = self._mmc.popNextImageAndMD()
|
|
149
|
+
ev, ch = next(iter_events)
|
|
150
|
+
yield self._create_seqimg_payload_from_popped(
|
|
151
|
+
img, mm_meta, event=ev, channel=ch, remaining=remaining - 1, event_t0=event_t0_ms
|
|
152
|
+
)
|
|
153
|
+
count += 1
|
|
154
|
+
|
|
155
|
+
def teardown_sequence(self, sequence: useq.MDASequence) -> None:
|
|
156
|
+
"""Perform any teardown required after the sequence has been executed."""
|
|
157
|
+
self.logger.debug(f'teardown_sequence at time: {time.time()}')
|
|
158
|
+
|
|
159
|
+
# Stop the Arduino LED Sequence
|
|
160
|
+
camera = getattr(self, 'camera', None)
|
|
161
|
+
if camera is not None and hasattr(camera, 'stop_led_sequence'):
|
|
162
|
+
camera.stop_led_sequence()
|
|
163
|
+
else:
|
|
164
|
+
self._mmc.getPropertyObject('Arduino-Switch', 'State').stopSequence()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class PupilEngine(MDAEngine):
|
|
168
|
+
"""MDA engine for the pupil camera with optional NI-DAQ triggering.
|
|
169
|
+
|
|
170
|
+
On :meth:`setup_sequence` an NI-DAQ object may be loaded from the
|
|
171
|
+
sequence metadata; on :meth:`exec_sequenced_event` that NI-DAQ is
|
|
172
|
+
pulsed at sequence start so other instruments can synchronise.
|
|
173
|
+
Subclasses :class:`pymmcore_plus.mda.MDAEngine`.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def __init__(self, mmc, use_hardware_sequencing: bool = True) -> None:
|
|
177
|
+
super().__init__(mmc)
|
|
178
|
+
self._mmc: CMMCorePlus = mmc
|
|
179
|
+
self.use_hardware_sequencing = use_hardware_sequencing
|
|
180
|
+
self._wheel_data = None
|
|
181
|
+
# TODO: add triggerable parameter
|
|
182
|
+
self.logger = get_logger(f'{__name__}.{self.__class__.__name__}')
|
|
183
|
+
|
|
184
|
+
def set_config(self, cfg: 'ExperimentConfig') -> None:
|
|
185
|
+
"""Bind a live :class:`~mesofield.config.ExperimentConfig`.
|
|
186
|
+
|
|
187
|
+
Caches the configured NI-DAQ device and (when more than one
|
|
188
|
+
``CMMCorePlus`` is in play) a reference to the primary core.
|
|
189
|
+
"""
|
|
190
|
+
self._config = cfg
|
|
191
|
+
self.nidaq = cfg.hardware.nidaq
|
|
192
|
+
if len(self._config._cores) > 1:
|
|
193
|
+
self._mmc1 = cfg._cores[0]
|
|
194
|
+
else:
|
|
195
|
+
self._mmc1 = None
|
|
196
|
+
|
|
197
|
+
def setup_sequence(self, sequence: useq.MDASequence) -> SummaryMetaV1 | None:
|
|
198
|
+
"""Resolve the NI-DAQ from sequence metadata and reset it for the run."""
|
|
199
|
+
self.nidaq = sequence.metadata.get("nidaq")
|
|
200
|
+
if self.nidaq is not None:
|
|
201
|
+
self.nidaq.reset()
|
|
202
|
+
self.logger.debug(f'setup_sequence loaded Nidaq: {self.nidaq}')
|
|
203
|
+
return super().setup_sequence(sequence)
|
|
204
|
+
|
|
205
|
+
def exec_sequenced_event(self, event: 'SequencedEvent') -> Iterable['PImagePayload']:
|
|
206
|
+
"""Execute a sequenced (triggered) event and return the image data.
|
|
207
|
+
|
|
208
|
+
This method is not part of the PMDAEngine protocol (it is called by
|
|
209
|
+
`exec_event`, which *is* part of the protocol), but it is made public
|
|
210
|
+
in case a user wants to subclass this engine and override this method.
|
|
211
|
+
"""
|
|
212
|
+
n_events = len(event.events)
|
|
213
|
+
|
|
214
|
+
t0 = event.metadata.get("runner_t0") or time.perf_counter()
|
|
215
|
+
event_t0_ms = (time.perf_counter() - t0) * 1000
|
|
216
|
+
|
|
217
|
+
#https://github.com/pymmcore-plus/useq-schema/issues/213
|
|
218
|
+
if self.nidaq is not None:# and self.io_type == "DO":
|
|
219
|
+
self.nidaq.start()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Start sequence
|
|
223
|
+
# Note that the overload of startSequenceAcquisition that takes a camera
|
|
224
|
+
# label does NOT automatically initialize a circular buffer. So if this call
|
|
225
|
+
# is changed to accept the camera in the future, that should be kept in mind.
|
|
226
|
+
self._mmc.startSequenceAcquisition(
|
|
227
|
+
n_events,
|
|
228
|
+
0, # intervalMS # TODO: add support for this
|
|
229
|
+
True, # stopOnOverflow
|
|
230
|
+
)
|
|
231
|
+
self.logger.debug(f'exec_sequenced_event with {n_events} events at t0 {t0}')
|
|
232
|
+
self.post_sequence_started(event)
|
|
233
|
+
|
|
234
|
+
n_channels = self._mmc.getNumberOfCameraChannels()
|
|
235
|
+
count = 0
|
|
236
|
+
iter_events = product(event.events, range(n_channels))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# block until the sequence is done, popping images in the meantime
|
|
240
|
+
while True:
|
|
241
|
+
if self._mmc.isSequenceRunning():
|
|
242
|
+
if remaining := self._mmc.getRemainingImageCount():
|
|
243
|
+
img, mm_meta = self._mmc.popNextImageAndMD()
|
|
244
|
+
ev, ch = next(iter_events)
|
|
245
|
+
yield self._create_seqimg_payload_from_popped(
|
|
246
|
+
img, mm_meta, event=ev, channel=ch, remaining=remaining - 1, event_t0=event_t0_ms
|
|
247
|
+
)
|
|
248
|
+
count += 1
|
|
249
|
+
else:
|
|
250
|
+
if count == n_events: # or self._mmc1 is not None and self._mmc1.isSequenceRunning() is not True:
|
|
251
|
+
self.logger.debug(f'stopped MDA with {count} events and {remaining} remaining with {self._mmc.getRemainingImageCount()} images in buffer')
|
|
252
|
+
self._mmc.stopSequenceAcquisition()
|
|
253
|
+
break
|
|
254
|
+
time.sleep(0.001)
|
|
255
|
+
else:
|
|
256
|
+
break
|
|
257
|
+
|
|
258
|
+
if self._mmc.isBufferOverflowed(): # pragma: no cover
|
|
259
|
+
self.logger.warning(f'OVERFLOW MDA: {self._mmc} with {count} events and {remaining} remaining with {self._mmc.getRemainingImageCount()} images in buffer')
|
|
260
|
+
raise MemoryError("Buffer overflowed")
|
|
261
|
+
|
|
262
|
+
while remaining := self._mmc.getRemainingImageCount():
|
|
263
|
+
self.logger.debug(f'Saving Remaining Images in buffer {self._mmc} with {count} events and {remaining} remaining with {self._mmc.getRemainingImageCount()} images in buffer')
|
|
264
|
+
img, mm_meta = self._mmc.popNextImageAndMD()
|
|
265
|
+
ev, ch = next(iter_events)
|
|
266
|
+
yield self._create_seqimg_payload_from_popped(
|
|
267
|
+
img, mm_meta, event=ev, channel=ch, remaining=remaining - 1, event_t0=event_t0_ms
|
|
268
|
+
)
|
|
269
|
+
count += 1
|
|
270
|
+
|
|
271
|
+
def teardown_sequence(self, sequence: useq.MDASequence) -> None:
|
|
272
|
+
"""Perform any teardown required after the sequence has been executed."""
|
|
273
|
+
self.logger.debug(f'teardown_sequence at time: {time.time()}')
|
|
274
|
+
# self.nidaq.stop()
|
|
275
|
+
# # Save exposure times from nidaq
|
|
276
|
+
# times = self.nidaq.get_exposure_times()
|
|
277
|
+
# path = self._config.make_path('nidaq_timestamps', 'txt', 'func')
|
|
278
|
+
# with open(path, 'w') as f:
|
|
279
|
+
# for t in times:
|
|
280
|
+
# f.write(f"{t}\n")
|
|
281
|
+
|
|
282
|
+
#self._encoder.stop()
|
|
283
|
+
# Get and store the encoder data
|
|
284
|
+
#self._wheel_data = self._encoder.get_data()
|
|
285
|
+
#self._config.save_wheel_encoder_data(self._wheel_data)
|
|
286
|
+
#self._config.save_configuration()
|
|
287
|
+
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class DevEngine(MDAEngine):
|
|
294
|
+
"""Hardware-free development engine for mock cameras and CI.
|
|
295
|
+
|
|
296
|
+
Streams images out of the camera's circular buffer with no Arduino
|
|
297
|
+
or NI-DAQ interaction. Use this engine for development and tests
|
|
298
|
+
that don't have access to physical rig hardware.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
def __init__(self, mmc, use_hardware_sequencing: bool = True) -> None:
|
|
302
|
+
super().__init__(mmc)
|
|
303
|
+
self._mmc = mmc
|
|
304
|
+
self.use_hardware_sequencing = use_hardware_sequencing
|
|
305
|
+
self._config = None
|
|
306
|
+
self.logger = get_logger(f'{__name__}.{self.__class__.__name__}')
|
|
307
|
+
|
|
308
|
+
self._encoder: SerialWorker = None
|
|
309
|
+
|
|
310
|
+
def set_config(self, cfg) -> None:
|
|
311
|
+
"""Bind a live :class:`~mesofield.config.ExperimentConfig` to this engine."""
|
|
312
|
+
self._config = cfg
|
|
313
|
+
|
|
314
|
+
def exec_sequenced_event(self, event: 'SequencedEvent') -> Iterable['PImagePayload']:
|
|
315
|
+
"""Execute a sequenced (triggered) event and return the image data.
|
|
316
|
+
|
|
317
|
+
This method is not part of the PMDAEngine protocol (it is called by
|
|
318
|
+
`exec_event`, which *is* part of the protocol), but it is made public
|
|
319
|
+
in case a user wants to subclass this engine and override this method.
|
|
320
|
+
"""
|
|
321
|
+
n_events = len(event.events)
|
|
322
|
+
|
|
323
|
+
t0 = event.metadata.get("runner_t0") or time.perf_counter()
|
|
324
|
+
event_t0_ms = (time.perf_counter() - t0) * 1000
|
|
325
|
+
# Start sequence
|
|
326
|
+
# Note that the overload of startSequenceAcquisition that takes a camera
|
|
327
|
+
# label does NOT automatically initialize a circular buffer. So if this call
|
|
328
|
+
# is changed to accept the camera in the future, that should be kept in mind.
|
|
329
|
+
self._mmc.startSequenceAcquisition(
|
|
330
|
+
n_events,
|
|
331
|
+
0, # intervalMS
|
|
332
|
+
True, # stopOnOverflow
|
|
333
|
+
)
|
|
334
|
+
self.logger.info(f'exec_sequenced_event with {n_events} events at t0 {t0}')
|
|
335
|
+
self.post_sequence_started(event)
|
|
336
|
+
|
|
337
|
+
n_channels = self._mmc.getNumberOfCameraChannels()
|
|
338
|
+
count = 0
|
|
339
|
+
iter_events = product(event.events, range(n_channels))
|
|
340
|
+
# block until the sequence is done, popping images in the meantime
|
|
341
|
+
while True:
|
|
342
|
+
if self._mmc.isSequenceRunning():
|
|
343
|
+
if remaining := self._mmc.getRemainingImageCount():
|
|
344
|
+
img, mm_meta = self._mmc.popNextImageAndMD()
|
|
345
|
+
ev, ch = next(iter_events)
|
|
346
|
+
yield self._create_seqimg_payload_from_popped(
|
|
347
|
+
img, mm_meta, event=ev, channel=ch, remaining=remaining - 1, event_t0=event_t0_ms
|
|
348
|
+
)
|
|
349
|
+
count += 1
|
|
350
|
+
else:
|
|
351
|
+
if count == n_events:
|
|
352
|
+
self.logger.debug(f'stopped MDA with {count} events and {remaining} remaining with {self._mmc.getRemainingImageCount()} images in buffer')
|
|
353
|
+
self._mmc.stopSequenceAcquisition()
|
|
354
|
+
break
|
|
355
|
+
time.sleep(0.001)
|
|
356
|
+
else:
|
|
357
|
+
break
|
|
358
|
+
|
|
359
|
+
if self._mmc.isBufferOverflowed(): # pragma: no cover
|
|
360
|
+
self.logger.warning(f'OVERFLOW MDA: {self._mmc} with {count} events and {remaining} remaining with {self._mmc.getRemainingImageCount()} images in buffer')
|
|
361
|
+
raise MemoryError("Buffer overflowed")
|
|
362
|
+
|
|
363
|
+
while remaining := self._mmc.getRemainingImageCount():
|
|
364
|
+
self.logger.debug(f'Saving Remaining Images in buffer {self._mmc} with {count} events and {remaining} remaining with {self._mmc.getRemainingImageCount()} images in buffer')
|
|
365
|
+
img, mm_meta = self._mmc.popNextImageAndMD()
|
|
366
|
+
ev, ch = next(iter_events)
|
|
367
|
+
yield self._create_seqimg_payload_from_popped(
|
|
368
|
+
img, mm_meta, event=ev, channel=ch, remaining=remaining - 1, event_t0=event_t0_ms
|
|
369
|
+
)
|
|
370
|
+
count += 1
|
|
371
|
+
|
|
372
|
+
def teardown_sequence(self, sequence: useq.MDASequence) -> None:
|
|
373
|
+
"""Perform any teardown required after the sequence has been executed."""
|
|
374
|
+
self.logger.info(f'teardown_sequence at time: {time.time()}')
|
|
375
|
+
# self._encoder.stop()
|
|
376
|
+
# Get and store the encoder data
|
|
377
|
+
# self._wheel_data = self._encoder.get_data()
|
|
378
|
+
# self._config.save_wheel_encoder_data(self._wheel_data)
|
|
379
|
+
# self._config.save_configuration()
|
|
380
|
+
pass
|
|
Binary file
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Mesofield Qt GUI package.
|
|
2
|
+
|
|
3
|
+
Defines the desktop application widgets used by ``mesofield launch``:
|
|
4
|
+
the main window, acquisition view, dynamic device controls, config
|
|
5
|
+
wizard, image preview, and serial / encoder plotters.
|
|
6
|
+
|
|
7
|
+
This top-level module exposes :class:`ConfigTableModel`, a thin
|
|
8
|
+
``QAbstractTableModel`` that presents a :class:`ConfigRegister` as a
|
|
9
|
+
table of (key, value) rows so the GUI can edit experiment parameters
|
|
10
|
+
live.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Any, List
|
|
14
|
+
|
|
15
|
+
from PyQt6.QtCore import Qt, QAbstractTableModel, QModelIndex, QVariant
|
|
16
|
+
|
|
17
|
+
class ConfigTableModel(QAbstractTableModel):
|
|
18
|
+
"""A table model that presents ConfigRegister as rows of (key, value)."""
|
|
19
|
+
def __init__(self, registry):
|
|
20
|
+
super().__init__()
|
|
21
|
+
self._registry = registry
|
|
22
|
+
self._keys: List[str] = self._registry.keys()
|
|
23
|
+
# Listen to external changes
|
|
24
|
+
for key in self._keys:
|
|
25
|
+
self._registry.register_callback(key, self._on_config_changed)
|
|
26
|
+
|
|
27
|
+
def rowCount(self, parent=QModelIndex()):
|
|
28
|
+
return len(self._keys)
|
|
29
|
+
|
|
30
|
+
def columnCount(self, parent=QModelIndex()):
|
|
31
|
+
return 2 # "Parameter" and "Value"
|
|
32
|
+
|
|
33
|
+
def data(self, index: QModelIndex, role=Qt.ItemDataRole.DisplayRole):
|
|
34
|
+
if not index.isValid():
|
|
35
|
+
return QVariant()
|
|
36
|
+
key = self._keys[index.row()]
|
|
37
|
+
|
|
38
|
+
if role == Qt.ItemDataRole.DisplayRole:
|
|
39
|
+
if index.column() == 0:
|
|
40
|
+
return key
|
|
41
|
+
elif index.column() == 1:
|
|
42
|
+
return str(self._registry.get(key))
|
|
43
|
+
return QVariant()
|
|
44
|
+
|
|
45
|
+
def headerData(self, section: int, orientation, role=Qt.ItemDataRole.DisplayRole):
|
|
46
|
+
if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
|
|
47
|
+
return ["Parameter", "Value"][section]
|
|
48
|
+
return super().headerData(section, orientation, role)
|
|
49
|
+
|
|
50
|
+
def flags(self, index: QModelIndex):
|
|
51
|
+
base = super().flags(index)
|
|
52
|
+
if index.column() == 1:
|
|
53
|
+
return base | Qt.ItemFlag.ItemIsEditable
|
|
54
|
+
return base
|
|
55
|
+
|
|
56
|
+
def setData(self, index: QModelIndex, value: Any, role=Qt.ItemDataRole.EditRole):
|
|
57
|
+
if index.isValid() and index.column() == 1 and role == Qt.ItemDataRole.EditRole:
|
|
58
|
+
key = self._keys[index.row()]
|
|
59
|
+
try:
|
|
60
|
+
# Attempt to set the new value (will auto-convert via ConfigRegister)
|
|
61
|
+
self._registry.set(key, value)
|
|
62
|
+
self.dataChanged.emit(index, index, [Qt.ItemDataRole.DisplayRole])
|
|
63
|
+
return True
|
|
64
|
+
except TypeError as e:
|
|
65
|
+
# you could pop up a QMessageBox here
|
|
66
|
+
print(f"Type error: {e}")
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def _on_config_changed(self, key: str, new_val: Any):
|
|
70
|
+
"""Called by ConfigRegister whenever a key changes externally."""
|
|
71
|
+
try:
|
|
72
|
+
row = self._keys.index(key)
|
|
73
|
+
except ValueError:
|
|
74
|
+
return
|
|
75
|
+
idx = self.index(row, 1)
|
|
76
|
+
self.dataChanged.emit(idx, idx, [Qt.ItemDataRole.DisplayRole])
|