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/protocols.py ADDED
@@ -0,0 +1,378 @@
1
+ """
2
+ Protocol definitions for hardware instruments and data management.
3
+
4
+ This module defines the core interfaces that standardize behavior across
5
+ the mesofield project, allowing for interoperability between different
6
+ hardware instruments, data producers, and data consumers.
7
+
8
+ Protocol Implementation Notes
9
+ -----------------------------
10
+ When implementing these protocols, there are two approaches:
11
+
12
+ 1. **Direct inheritance** (for regular classes without metaclass conflicts):
13
+
14
+ .. code-block:: python
15
+
16
+ class MySensor(DataAcquisitionDevice):
17
+ def __init__(self):
18
+ self._init_logger()
19
+ # Implement required methods and attributes
20
+
21
+ 2. **Duck typing** (for classes with existing inheritance or metaclass
22
+ conflicts, e.g. ``QThread``):
23
+
24
+ .. code-block:: python
25
+
26
+ class MyQThreadSensor(QThread):
27
+ def __init__(self):
28
+ super().__init__()
29
+ self._init_logger()
30
+ self.device_type = "sensor"
31
+ self.device_id = "my_sensor"
32
+
33
+ The second approach is necessary for Qt classes (``QObject``, ``QThread``,
34
+ ``QWidget``) or any class that already uses a metaclass. Protocol checking
35
+ uses duck typing internally, so both approaches work with our system.
36
+ """
37
+
38
+ import threading
39
+ from typing import Dict, List, Any, Optional, Protocol, TypeVar, Generic, runtime_checkable
40
+
41
+ from typing import TYPE_CHECKING
42
+ if TYPE_CHECKING:
43
+ from mesofield.hardware import HardwareManager
44
+ from mesofield.config import ExperimentConfig
45
+
46
+ T = TypeVar('T')
47
+
48
+ # These are the Protocol definitions - they are useful for static type checking
49
+ # and documentation, but should not be used for inheritance with classes that
50
+ # already have a metaclass (like QThread)
51
+
52
+
53
+ class Procedure(Protocol):
54
+ """Protocol defining the standard interface for experiment procedures."""
55
+
56
+ protocol: str
57
+ experimenter: str
58
+ config: "ExperimentConfig"
59
+ data_dir: str
60
+
61
+ def initialize_hardware(self) -> None:
62
+ """Setup the experiment procedure.
63
+
64
+ """
65
+ ...
66
+
67
+ def setup_configuration(self, json_config: Optional[str]) -> None:
68
+ """Set up the configuration for the experiment procedure.
69
+
70
+ Args:
71
+ json_config: Path to a JSON configuration file (.json)
72
+ """
73
+ ...
74
+
75
+ def run(self) -> None:
76
+ """Run the experiment procedure."""
77
+ ...
78
+
79
+ def save_data(self) -> None:
80
+ """Save data from the experiment."""
81
+ ...
82
+
83
+ def cleanup(self) -> None:
84
+ """Clean up after the experiment procedure."""
85
+ ...
86
+
87
+ @runtime_checkable
88
+ class HardwareDevice(Protocol):
89
+ """Protocol defining the standard interface for all hardware devices.
90
+
91
+ Lifecycle: ``initialize`` -> ``arm`` -> ``start`` -> ``stop`` -> ``shutdown``.
92
+ Every device exposes ``self.signals`` (a :class:`mesofield.signals.DeviceSignals`)
93
+ carrying ``started``, ``finished``, and ``data`` emitters.
94
+ """
95
+
96
+ device_type: str
97
+ device_id: str
98
+ signals: Any # DeviceSignals; typed as Any to avoid circular import
99
+
100
+ def initialize(self) -> bool:
101
+ """One-time setup (open ports, load configs). Idempotent."""
102
+ ...
103
+
104
+ def arm(self, config: "ExperimentConfig") -> None:
105
+ """Per-run preparation (writers, output paths, sequence build).
106
+
107
+ Called by ``HardwareManager.arm_all`` immediately before
108
+ ``start_all``. Devices without per-run prep may no-op.
109
+ """
110
+ ...
111
+
112
+ def stop(self):
113
+ """Stop the device after a run."""
114
+ ...
115
+
116
+ def shutdown(self) -> None:
117
+ """Close and clean up resources."""
118
+ ...
119
+
120
+ def status(self) -> Dict[str, Any]:
121
+ """Get the current status of the device."""
122
+ ...
123
+
124
+ @property
125
+ def metadata(self) -> Dict[str, Any]:
126
+ """Return metadata about the hardware."""
127
+ ...
128
+
129
+
130
+
131
+ @runtime_checkable
132
+ class DataProducer(HardwareDevice, Protocol):
133
+ """Protocol for hardware that produces data streamed to the DataQueue."""
134
+
135
+ sampling_rate: float # in Hz
136
+ data_type: str
137
+ file_type: str
138
+ bids_type: Optional[str] = None
139
+ is_active: bool
140
+ output_path: str
141
+ metadata_path: Optional[str] = None
142
+
143
+ def start(self) -> bool:
144
+ """Start data acquisition. Should emit ``signals.started``."""
145
+ ...
146
+
147
+ def stop(self) -> bool:
148
+ """Stop data acquisition. Should emit ``signals.finished``."""
149
+ ...
150
+
151
+ def save_data(self, path: Optional[str] = None):
152
+ """Persist the captured data."""
153
+ ...
154
+
155
+ def get_data(self) -> Optional[Any]:
156
+ """Return the latest data."""
157
+ ...
158
+
159
+
160
+ @runtime_checkable
161
+ class FrameProcessor(Protocol):
162
+ """Protocol for optional real-time per-frame consumers.
163
+
164
+ A FrameProcessor subscribes to a :class:`DataProducer` camera's
165
+ ``signals.frame`` (carrying ``(img, idx, device_ts)``) and emits a
166
+ scalar result on its own ``signals.data`` and on a Qt-compatible
167
+ ``valueUpdated(time, value)`` signal. See
168
+ :mod:`mesofield.processors` for the threaded reference base class.
169
+ """
170
+
171
+ name: str
172
+ data_type: str
173
+ sampling_rate: float
174
+
175
+ def attach(self, camera: "DataProducer") -> None:
176
+ """Subscribe to ``camera.signals.frame`` and start processing."""
177
+ ...
178
+
179
+ def detach(self) -> None:
180
+ """Disconnect and stop the worker."""
181
+ ...
182
+
183
+ def compute(self, img: Any, idx: Any, ts: Any) -> Optional[float]:
184
+ """Return a scalar for this frame, or ``None`` to skip."""
185
+ ...
186
+
187
+
188
+ @runtime_checkable
189
+ class StimulusDevice(HardwareDevice, Protocol):
190
+ """Protocol for stimulus-presentation devices (e.g. PsychoPy).
191
+
192
+ Like :class:`HardwareDevice` but explicitly *not* a data producer:
193
+ consumers should not expect ``data`` signal emissions and should not
194
+ call ``save_data``/``get_data``.
195
+ """
196
+
197
+ def start(self) -> bool:
198
+ """Begin stimulus presentation."""
199
+ ...
200
+
201
+
202
+
203
+
204
+ @runtime_checkable
205
+ class DataConsumer(Protocol):
206
+ """Protocol defining the interface for data-consuming components."""
207
+
208
+ @property
209
+ def name(self) -> str:
210
+ """Return the name of the data consumer."""
211
+ ...
212
+
213
+ @property
214
+ def get_supported_data_types(self) -> List[str]:
215
+ """Return the types of data this consumer can process."""
216
+ ...
217
+
218
+ def process_data(self, data: Any, metadata: Dict[str, Any]) -> bool:
219
+ """Process data with metadata.
220
+
221
+ Args:
222
+ data: The data to process.
223
+ metadata: Metadata about the data, including source, timestamp, etc.
224
+
225
+ Returns:
226
+ bool: True if data was processed successfully, False otherwise.
227
+ """
228
+ ...
229
+
230
+
231
+ # ---------------------------------------------------------------------------
232
+ # Threading mixins
233
+ # ---------------------------------------------------------------------------
234
+
235
+ class ThreadedHardwareDevice:
236
+ """
237
+ Mixin for implementing the HardwareDevice protocol with Python's threading.
238
+
239
+ This mixin provides the basic structure for a hardware device that uses
240
+ Python's threading module. It handles the thread creation, starting, and stopping.
241
+
242
+ Example:
243
+ .. code-block:: python
244
+
245
+ class MySensor(ThreadedHardwareDevice):
246
+ device_type = "sensor"
247
+ device_id = "my_sensor"
248
+
249
+ def __init__(self, config=None):
250
+ super().__init__()
251
+ self.config = config or {}
252
+
253
+ def initialize(self):
254
+ pass
255
+
256
+ def _run(self):
257
+ while not self._stop_event.is_set():
258
+ # Do work
259
+ pass
260
+
261
+ def get_status(self):
262
+ return {"active": not self._stop_event.is_set()}
263
+ """
264
+
265
+ def __init__(self):
266
+ self._thread = None
267
+ self._stop_event = threading.Event()
268
+ self._active = False
269
+
270
+ def start(self) -> bool:
271
+ """Start the device thread."""
272
+ if self._active:
273
+ return True
274
+
275
+ self._stop_event.clear()
276
+ self._thread = threading.Thread(target=self._run)
277
+ self._thread.daemon = True
278
+ self._thread.start()
279
+ self._active = True
280
+ return True
281
+
282
+ def stop(self) -> bool:
283
+ """Stop the device thread."""
284
+ if not self._active:
285
+ return True
286
+
287
+ self._stop_event.set()
288
+ if self._thread and self._thread.is_alive():
289
+ self._thread.join(timeout=1.0)
290
+ self._active = False
291
+ return True
292
+
293
+ def close(self) -> None:
294
+ """Close the device and clean up resources."""
295
+ self.stop()
296
+
297
+ def _run(self) -> None:
298
+ """
299
+ Main thread method to be overridden by subclasses.
300
+
301
+ This method runs in a separate thread when start() is called.
302
+ It should check self._stop_event periodically and exit if it's set.
303
+ """
304
+ raise NotImplementedError("Subclasses must implement _run()")
305
+
306
+
307
+ class AsyncioHardwareDevice:
308
+ """
309
+ Mixin for implementing the HardwareDevice protocol with asyncio.
310
+
311
+ This mixin provides the basic structure for a hardware device that uses
312
+ Python's asyncio module. It handles the task creation, starting, and cancellation.
313
+
314
+ Example:
315
+ .. code-block:: python
316
+
317
+ class MySensor(AsyncioHardwareDevice):
318
+ device_type = "sensor"
319
+ device_id = "my_sensor"
320
+
321
+ def __init__(self, loop=None, config=None):
322
+ super().__init__(loop)
323
+ self.config = config or {}
324
+
325
+ def initialize(self):
326
+ pass
327
+
328
+ async def _run(self):
329
+ while True:
330
+ if self._should_stop():
331
+ break
332
+ await asyncio.sleep(0.01)
333
+
334
+ def get_status(self):
335
+ return {"active": self._task is not None and not self._task.done()}
336
+ """
337
+
338
+ def __init__(self, loop=None):
339
+ import asyncio
340
+ self._loop = loop or asyncio.get_event_loop()
341
+ self._task = None
342
+ self._stop_requested = False
343
+
344
+ def start(self) -> bool:
345
+ """Start the device task."""
346
+ import asyncio
347
+ if self._task is not None and not self._task.done():
348
+ return True
349
+
350
+ self._stop_requested = False
351
+ self._task = asyncio.create_task(self._run())
352
+ return True
353
+
354
+ def stop(self) -> bool:
355
+ """Stop the device task."""
356
+ if self._task is None or self._task.done():
357
+ return True
358
+
359
+ self._stop_requested = True
360
+ self._task.cancel()
361
+ return True
362
+
363
+ def close(self) -> None:
364
+ """Close the device and clean up resources."""
365
+ self.stop()
366
+
367
+ def _should_stop(self) -> bool:
368
+ """Check if the task should stop."""
369
+ return self._stop_requested
370
+
371
+ async def _run(self) -> None:
372
+ """
373
+ Main coroutine to be overridden by subclasses.
374
+
375
+ This coroutine runs as a task when start() is called.
376
+ It should check self._should_stop() periodically and exit if it returns True.
377
+ """
378
+ raise NotImplementedError("Subclasses must implement _run()")
@@ -0,0 +1,34 @@
1
+ """Scaffolding for new experiments and machine-level rig configurations.
2
+
3
+ Both halves are "get a machine/experiment ready to use" concerns:
4
+
5
+ - :func:`scaffold_experiment` generates a runnable experiment directory.
6
+ - :mod:`mesofield.scaffold.rigs` keeps a per-machine store of canonical
7
+ ``hardware.yaml`` files that ``mesofield init`` copies into new experiments.
8
+ """
9
+
10
+ from mesofield.scaffold.experiment import (
11
+ scaffold_experiment,
12
+ hardware_yaml_template,
13
+ )
14
+ from mesofield.scaffold.rigs import (
15
+ rigs_dir,
16
+ list_rigs,
17
+ rig_path,
18
+ rig_devices,
19
+ add_rig,
20
+ new_rig,
21
+ remove_rig,
22
+ )
23
+
24
+ __all__ = [
25
+ "scaffold_experiment",
26
+ "hardware_yaml_template",
27
+ "rigs_dir",
28
+ "list_rigs",
29
+ "rig_path",
30
+ "rig_devices",
31
+ "add_rig",
32
+ "new_rig",
33
+ "remove_rig",
34
+ ]