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
|
@@ -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
|
+
}
|