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.
Files changed (111) hide show
  1. docs/_static/custom.css +40 -0
  2. docs/_static/favicon.png +0 -0
  3. docs/_static/logo.png +0 -0
  4. docs/api/index.md +70 -0
  5. docs/conf.py +200 -0
  6. docs/developer_guide.md +303 -0
  7. docs/index.md +25 -0
  8. docs/tutorial.md +4 -0
  9. docs/user_guide.md +172 -0
  10. examples/teensy_pulse_generator.py +320 -0
  11. experiments/pipeline_demo/experiment.json +24 -0
  12. experiments/pipeline_demo/hardware.yaml +23 -0
  13. experiments/pipeline_demo/procedure.py +50 -0
  14. experiments/two_cam_demo/experiment.json +24 -0
  15. experiments/two_cam_demo/hardware.yaml +58 -0
  16. experiments/two_cam_demo/load_dataset.py +213 -0
  17. experiments/two_cam_demo/procedure.py +87 -0
  18. external/video-codecs/openh264-1.8.0-win64.dll +0 -0
  19. mesofield/__init__.py +45 -0
  20. mesofield/__main__.py +11 -0
  21. mesofield/_version.py +24 -0
  22. mesofield/base.py +750 -0
  23. mesofield/cli/__init__.py +57 -0
  24. mesofield/cli/_richhelp.py +100 -0
  25. mesofield/cli/acquire.py +254 -0
  26. mesofield/cli/datakit.py +165 -0
  27. mesofield/cli/process.py +376 -0
  28. mesofield/cli/rig.py +108 -0
  29. mesofield/cli/tools.py +347 -0
  30. mesofield/config.py +751 -0
  31. mesofield/data/__init__.py +23 -0
  32. mesofield/data/batch.py +633 -0
  33. mesofield/data/manager.py +388 -0
  34. mesofield/data/writer.py +289 -0
  35. mesofield/datakit/__init__.py +44 -0
  36. mesofield/datakit/__main__.py +35 -0
  37. mesofield/datakit/_utils/_logger.py +5 -0
  38. mesofield/datakit/_version.py +141 -0
  39. mesofield/datakit/config.py +50 -0
  40. mesofield/datakit/core.py +783 -0
  41. mesofield/datakit/datamodel.py +200 -0
  42. mesofield/datakit/discover.py +124 -0
  43. mesofield/datakit/explore.py +651 -0
  44. mesofield/datakit/notebooks/pupil_dlc.ipynb +2445 -0
  45. mesofield/datakit/profile.py +535 -0
  46. mesofield/datakit/shell.py +83 -0
  47. mesofield/datakit/sources/__init__.py +65 -0
  48. mesofield/datakit/sources/analysis/mesomap.py +194 -0
  49. mesofield/datakit/sources/analysis/mesoscope.py +77 -0
  50. mesofield/datakit/sources/analysis/pupil.py +246 -0
  51. mesofield/datakit/sources/behavior/__init__.py +0 -0
  52. mesofield/datakit/sources/behavior/dataqueue.py +281 -0
  53. mesofield/datakit/sources/behavior/psychopy.py +364 -0
  54. mesofield/datakit/sources/behavior/treadmill.py +323 -0
  55. mesofield/datakit/sources/behavior/wheel.py +277 -0
  56. mesofield/datakit/sources/camera/mesoscope.py +32 -0
  57. mesofield/datakit/sources/camera/metadata_json.py +130 -0
  58. mesofield/datakit/sources/camera/pupil.py +28 -0
  59. mesofield/datakit/sources/camera/suite2p.py +547 -0
  60. mesofield/datakit/sources/register.py +204 -0
  61. mesofield/datakit/sources/session/config.py +130 -0
  62. mesofield/datakit/sources/session/notes.py +63 -0
  63. mesofield/datakit/sources/session/timestamps.py +58 -0
  64. mesofield/datakit/timeline.py +306 -0
  65. mesofield/devices/__init__.py +42 -0
  66. mesofield/devices/base.py +498 -0
  67. mesofield/devices/base_camera.py +295 -0
  68. mesofield/devices/cameras.py +740 -0
  69. mesofield/devices/daq.py +151 -0
  70. mesofield/devices/encoder.py +384 -0
  71. mesofield/devices/mocks.py +275 -0
  72. mesofield/devices/psychopy_device.py +455 -0
  73. mesofield/devices/subprocesses/__init__.py +0 -0
  74. mesofield/devices/subprocesses/psychopy.py +133 -0
  75. mesofield/devices/treadmill.py +318 -0
  76. mesofield/engines.py +380 -0
  77. mesofield/gui/Mesofield_icon.png +0 -0
  78. mesofield/gui/__init__.py +76 -0
  79. mesofield/gui/config_wizard.py +724 -0
  80. mesofield/gui/controller.py +535 -0
  81. mesofield/gui/dynamic_controller.py +78 -0
  82. mesofield/gui/maingui.py +427 -0
  83. mesofield/gui/mdagui.py +285 -0
  84. mesofield/gui/qt_device_adapter.py +109 -0
  85. mesofield/gui/speedplotter.py +152 -0
  86. mesofield/gui/theme.py +445 -0
  87. mesofield/gui/tiff_viewer.py +1050 -0
  88. mesofield/gui/viewer.py +691 -0
  89. mesofield/hardware.py +549 -0
  90. mesofield/playback.py +1298 -0
  91. mesofield/processing/__init__.py +12 -0
  92. mesofield/processing/runner.py +237 -0
  93. mesofield/processors/__init__.py +13 -0
  94. mesofield/processors/base.py +287 -0
  95. mesofield/processors/frame_mean.py +19 -0
  96. mesofield/protocols.py +378 -0
  97. mesofield/scaffold/__init__.py +34 -0
  98. mesofield/scaffold/experiment.py +400 -0
  99. mesofield/scaffold/rigs.py +121 -0
  100. mesofield/signals.py +85 -0
  101. mesofield/utils/__init__.py +0 -0
  102. mesofield/utils/_logger.py +156 -0
  103. mesofield/utils/retrofit.py +309 -0
  104. mesofield/utils/utils.py +217 -0
  105. mesofield-0.3.2b0.dist-info/METADATA +178 -0
  106. mesofield-0.3.2b0.dist-info/RECORD +111 -0
  107. mesofield-0.3.2b0.dist-info/WHEEL +5 -0
  108. mesofield-0.3.2b0.dist-info/entry_points.txt +2 -0
  109. mesofield-0.3.2b0.dist-info/licenses/LICENSE +21 -0
  110. mesofield-0.3.2b0.dist-info/top_level.txt +6 -0
  111. 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])