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
@@ -0,0 +1,275 @@
1
+ """Synthetic devices for headless / GUI-only iteration.
2
+
3
+ These mock devices produce realistic data on daemon threads without any
4
+ serial or camera hardware. They are used by the scaffold ``--rig dev``
5
+ template and by the test suite.
6
+
7
+ .. rubric:: Mock encoder
8
+
9
+ Produces random click counts via :class:`BaseDataProducer`.
10
+
11
+ Usage in ``hardware.yaml``::
12
+
13
+ encoder:
14
+ type: mock
15
+ sample_interval_ms: 100
16
+
17
+ Or programmatically::
18
+
19
+ from mesofield.devices.mocks import MockEncoderDevice
20
+ dev = MockEncoderDevice({"id": "encoder", "sample_interval_ms": 50})
21
+ dev.start()
22
+ ...
23
+ dev.stop()
24
+
25
+ .. rubric:: Mock camera
26
+
27
+ Subclasses :class:`BaseCamera` for the cosmetic + manifest surface every
28
+ camera shares, and :class:`BaseDataProducer` for the buffer + record()
29
+ mechanism every queue-pushing producer needs.
30
+
31
+ YAML registration via ``type: mock_camera``::
32
+
33
+ camera:
34
+ type: mock_camera
35
+ primary: true
36
+ width: 64
37
+ height: 64
38
+ frame_interval_ms: 50
39
+ output:
40
+ suffix: meso
41
+ file_type: ome.tiff
42
+ bids_type: func
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import random
48
+ import threading
49
+ import time
50
+ from datetime import datetime, timezone
51
+ from pathlib import Path
52
+ from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional
53
+
54
+ import numpy as np
55
+
56
+ from mesofield.devices.base import BaseDataProducer
57
+ from mesofield.devices.base_camera import BaseCamera
58
+
59
+ if TYPE_CHECKING:
60
+ from mesofield.config import ExperimentConfig
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Mock encoder
65
+ # ---------------------------------------------------------------------------
66
+
67
+
68
+ class MockEncoderDevice(BaseDataProducer):
69
+ """Synthetic encoder that records random click counts."""
70
+
71
+ device_type: ClassVar[str] = "encoder"
72
+ file_type: ClassVar[str] = "csv"
73
+ bids_type: ClassVar[Optional[str]] = "beh"
74
+
75
+ # Declare the dataqueue payload contract so the test_pipeline e2e can
76
+ # round-trip it and prove the schema reaches the manifest.
77
+ dataqueue_payload_schema: ClassVar[Optional[dict]] = {
78
+ "device_id": "wheel",
79
+ "payload_format": "scalar",
80
+ "payload_fields": {},
81
+ "description": "Mock click count pushed by _run_loop().",
82
+ }
83
+
84
+ def __init__(self, cfg: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None:
85
+ super().__init__(cfg, **kwargs)
86
+ self.sample_interval_s: float = float(
87
+ self.cfg.get("sample_interval_ms", 100)
88
+ ) / 1000.0
89
+ self._stop_event = threading.Event()
90
+ self._thread: Optional[threading.Thread] = None
91
+
92
+ def _run_loop(self) -> None:
93
+ while not self._stop_event.is_set():
94
+ self.record(random.randint(1, 10))
95
+ self._stop_event.wait(self.sample_interval_s)
96
+
97
+ def start(self) -> bool:
98
+ if self._thread is not None and self._thread.is_alive():
99
+ return False
100
+ self._stop_event.clear()
101
+ self._thread = threading.Thread(
102
+ target=self._run_loop,
103
+ name=f"MockEncoder-{self.device_id}",
104
+ daemon=True,
105
+ )
106
+ self._thread.start()
107
+ return super().start()
108
+
109
+ def stop(self) -> bool:
110
+ self._stop_event.set()
111
+ thread = self._thread
112
+ if thread is not None:
113
+ thread.join(timeout=2.0)
114
+ self._thread = None
115
+ return super().stop()
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Mock camera
120
+ # ---------------------------------------------------------------------------
121
+
122
+
123
+ class MockFrameProducer(BaseCamera, BaseDataProducer):
124
+ """Synthetic camera producing real OME-TIFF + frame metadata JSON."""
125
+
126
+ device_type: ClassVar[str] = "camera"
127
+ file_type: ClassVar[str] = "ome.tiff"
128
+ bids_type: ClassVar[Optional[str]] = "func"
129
+ data_type: ClassVar[str] = "frames"
130
+ clock_source: ClassVar[str] = "wall_unix_s"
131
+
132
+ def __init__(self, cfg: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None:
133
+ BaseDataProducer.__init__(self, cfg, **kwargs)
134
+ self._init_camera_surface(self.cfg, backend="mock")
135
+ self.width: int = int(self.cfg.get("width", 32))
136
+ self.height: int = int(self.cfg.get("height", 32))
137
+ self.frame_interval_s: float = float(self.cfg.get("frame_interval_ms", 50)) / 1000.0
138
+ if self.frame_interval_s:
139
+ self.sampling_rate = 1.0 / self.frame_interval_s
140
+ self._stop_event = threading.Event()
141
+ self._thread: Optional[threading.Thread] = None
142
+ self._frames: list[np.ndarray] = []
143
+ self._frames_lock = threading.Lock()
144
+ self._frame_records: list[dict[str, Any]] = []
145
+ self._qt_image_adapter = None
146
+ self.image_ready = None
147
+ try:
148
+ from mesofield.gui.qt_device_adapter import QtImageAdapter
149
+
150
+ self._qt_image_adapter = QtImageAdapter()
151
+ self.image_ready = self._qt_image_adapter.image_ready
152
+ except Exception:
153
+ self.logger.debug("QtImageAdapter unavailable; running headless.")
154
+
155
+ # ---- lifecycle ------------------------------------------------------
156
+ def arm(self, config: "ExperimentConfig") -> None:
157
+ BaseDataProducer.arm(self, config)
158
+ self.set_sequence(config.build_sequence)
159
+ self._frames.clear()
160
+ self._frame_records.clear()
161
+
162
+ def start(self) -> bool:
163
+ if self._thread is not None and self._thread.is_alive():
164
+ return False
165
+ self._stop_event.clear()
166
+ self._thread = threading.Thread(
167
+ target=self._run_loop,
168
+ name=f"MockCamera-{self.device_id}",
169
+ daemon=True,
170
+ )
171
+ self._thread.start()
172
+ return BaseDataProducer.start(self)
173
+
174
+ def stop(self) -> bool:
175
+ self._stop_event.set()
176
+ thread = self._thread
177
+ if thread is not None:
178
+ thread.join(timeout=2.0)
179
+ self._thread = None
180
+ return BaseDataProducer.stop(self)
181
+
182
+ def _run_loop(self) -> None:
183
+ rng = np.random.default_rng(seed=0)
184
+ while not self._stop_event.is_set():
185
+ ts = time.time()
186
+ frame = self._synthesise_frame(rng, len(self._frames))
187
+ with self._frames_lock:
188
+ self._frames.append(frame)
189
+ self._frame_records.append({
190
+ "frame_index": len(self._frames) - 1,
191
+ "TimeReceivedByCore": datetime.fromtimestamp(ts, tz=timezone.utc)
192
+ .isoformat().replace("+00:00", "Z"),
193
+ })
194
+ self.record({"frame_index": len(self._frames) - 1}, ts=ts)
195
+ if self._qt_image_adapter is not None:
196
+ self._qt_image_adapter.emit_frame(frame)
197
+ self._stop_event.wait(self.frame_interval_s)
198
+
199
+ # --- live preview contract (BaseCamera abstract methods) ------------
200
+ def snap(self) -> np.ndarray:
201
+ rng = np.random.default_rng(seed=0)
202
+ frame = self._synthesise_frame(rng, 0)
203
+ if self._qt_image_adapter is not None:
204
+ self._qt_image_adapter.emit_frame(frame)
205
+ return frame
206
+
207
+ def start_live(self) -> None:
208
+ self.start()
209
+
210
+ def stop_live(self) -> None:
211
+ self.stop()
212
+
213
+ def _synthesise_frame(self, rng: np.random.Generator, frame_index: int) -> np.ndarray:
214
+ h, w = self.height, self.width
215
+ frame = np.zeros((h, w), dtype=np.uint16)
216
+ half_h, half_w = h // 2, w // 2
217
+ baselines = (1000, 2000, 3000, 4000)
218
+ slow_drift = int(50 * np.sin(frame_index / 5.0))
219
+ frame[:half_h, :half_w] = baselines[0] + slow_drift
220
+ frame[:half_h, half_w:] = baselines[1] + slow_drift
221
+ frame[half_h:, :half_w] = baselines[2] + slow_drift
222
+ frame[half_h:, half_w:] = baselines[3] + slow_drift
223
+ frame += rng.integers(0, 50, size=(h, w), dtype=np.uint16)
224
+ return frame
225
+
226
+ # ---- save -----------------------------------------------------------
227
+ def save_data(self, path: Optional[str] = None) -> Optional[str]:
228
+ """Replay buffered frames through the standard mesofield writer."""
229
+ target = path or self.output_path
230
+ if not target:
231
+ self.logger.debug("save_data: no path for %s", self.device_id)
232
+ return None
233
+ Path(target).parent.mkdir(parents=True, exist_ok=True)
234
+
235
+ with self._frames_lock:
236
+ frames = list(self._frames)
237
+ records = list(self._frame_records)
238
+
239
+ if not frames:
240
+ self.logger.warning("MockFrameProducer captured 0 frames")
241
+ return None
242
+
243
+ self.output_path = str(target)
244
+ self.writer = self._make_writer(target)
245
+ self.metadata_path = getattr(
246
+ self.writer, "_frame_metadata_filename", str(target) + "_frame_metadata.json",
247
+ )
248
+
249
+ import useq
250
+
251
+ seq = useq.MDASequence(
252
+ time_plan={"loops": len(frames), "interval": self.frame_interval_s},
253
+ )
254
+ self.writer.sequenceStarted(seq, {})
255
+ try:
256
+ for i, (frame, record) in enumerate(zip(frames, records)):
257
+ event = useq.MDAEvent(index={"t": i})
258
+ self.writer.frameReady(frame, event, record)
259
+ finally:
260
+ self.writer.sequenceFinished(seq)
261
+
262
+ self.logger.info(
263
+ "Wrote %d frames to %s via %s",
264
+ len(frames), target, type(self.writer).__name__,
265
+ )
266
+ return target
267
+
268
+ # ---- manifest hint --------------------------------------------------
269
+ @property
270
+ def calibration(self) -> Dict[str, Any]:
271
+ return {
272
+ "width": self.width,
273
+ "height": self.height,
274
+ "frame_interval_ms": int(self.frame_interval_s * 1000),
275
+ }