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,40 @@
1
+ /* Wordmark font: Exo 2 — clean, slightly futuristic, pairs well with PyData. */
2
+ @import url("https://fonts.googleapis.com/css2?family=Exo+2:wght@400;500;600;700&display=swap");
3
+
4
+ /* PyData renders the brand as: .navbar-brand > img.logo__image + p.title */
5
+ .navbar-brand {
6
+ display: flex;
7
+ align-items: center;
8
+ gap: 0.6rem;
9
+ text-decoration: none;
10
+ }
11
+
12
+ /* Override pydata's higher-specificity rule on the logo image. */
13
+ .bd-header .navbar-brand img.logo__image,
14
+ .navbar-brand img.logo__image,
15
+ .navbar-brand-box img.logo__image {
16
+ height: 1.4rem !important;
17
+ max-height: 1.4rem !important;
18
+ width: auto !important;
19
+ object-fit: contain;
20
+ }
21
+
22
+ /* Stop the README's banner image from being stretched to content width.
23
+ pydata applies width:100% to images by default; cap to natural size. */
24
+ .bd-content img {
25
+ max-width: 100%;
26
+ width: auto;
27
+ height: auto;
28
+ object-fit: contain;
29
+ }
30
+
31
+ .navbar-brand p,
32
+ .navbar-brand .title {
33
+ font-family: "Exo 2", system-ui, sans-serif;
34
+ font-weight: 500;
35
+ font-size: 1.5rem;
36
+ letter-spacing: 0.08em;
37
+ text-transform: uppercase;
38
+ margin: 0;
39
+ line-height: 1;
40
+ }
Binary file
docs/_static/logo.png ADDED
Binary file
docs/api/index.md ADDED
@@ -0,0 +1,70 @@
1
+ # API Reference
2
+
3
+ Mesofield is organised into a small set of subpackages. Each one is documented below; start with the package you're working in.
4
+
5
+ ::::{grid} 1 2 2 2
6
+ :gutter: 2
7
+
8
+ :::{grid-item-card} `base` & `protocols`
9
+ :link: mesofield.base
10
+ :link-type: doc
11
+
12
+ The `Procedure` orchestrator and the device protocols every hardware adapter implements.
13
+ :::
14
+
15
+ :::{grid-item-card} `config` & `signals`
16
+ :link: mesofield.config
17
+ :link-type: doc
18
+
19
+ The `ExperimentConfig` registry and the lightweight psygnal-based event bus.
20
+ :::
21
+
22
+ :::{grid-item-card} `hardware` & `engines`
23
+ :link: mesofield.hardware
24
+ :link-type: doc
25
+
26
+ `HardwareManager` lifecycle and the MicroManager MDA engine integrations.
27
+ :::
28
+
29
+ :::{grid-item-card} `devices`
30
+ :link: mesofield.devices
31
+ :link-type: doc
32
+
33
+ Concrete hardware adapters: cameras, DAQs, encoders, treadmills, PsychoPy.
34
+ :::
35
+
36
+ :::{grid-item-card} `data` & `datakit`
37
+ :link: mesofield.data
38
+ :link-type: doc
39
+
40
+ Writers, batch managers, and the post-acquisition data exploration toolkit.
41
+ :::
42
+
43
+ :::{grid-item-card} `gui`
44
+ :link: mesofield.gui
45
+ :link-type: doc
46
+
47
+ The PyQt6 widgets and controllers that make up the desktop app.
48
+ :::
49
+
50
+ ::::
51
+
52
+ ```{toctree}
53
+ :hidden:
54
+ :maxdepth: 2
55
+
56
+ mesofield.base
57
+ mesofield.config
58
+ mesofield.protocols
59
+ mesofield.signals
60
+ mesofield.hardware
61
+ mesofield.engines
62
+ mesofield.devices
63
+ mesofield.data
64
+ mesofield.datakit
65
+ mesofield.processing
66
+ mesofield.processors
67
+ mesofield.scaffold
68
+ mesofield.gui
69
+ mesofield.utils
70
+ ```
docs/conf.py ADDED
@@ -0,0 +1,200 @@
1
+ """Sphinx configuration for the Mesofield docs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ ROOT = Path(__file__).resolve().parent.parent
10
+ sys.path.insert(0, str(ROOT))
11
+
12
+ # -- Project information ------------------------------------------------------
13
+
14
+ project = "Mesofield"
15
+ author = "Jacob Gronemeyer"
16
+ copyright = "2026, Sipe Laboratory, Penn State"
17
+
18
+ try:
19
+ from mesofield._version import version as _version
20
+ except Exception:
21
+ _version = "0.0.0"
22
+ release = _version
23
+ version = ".".join(_version.split(".")[:2])
24
+
25
+ # -- General configuration ----------------------------------------------------
26
+
27
+ extensions = [
28
+ "sphinx.ext.autodoc",
29
+ "sphinx.ext.napoleon",
30
+ "sphinx.ext.viewcode",
31
+ "sphinx.ext.intersphinx",
32
+ "myst_parser",
33
+ "sphinx_copybutton",
34
+ "sphinx_design",
35
+ ]
36
+
37
+ source_suffix = {
38
+ ".rst": "restructuredtext",
39
+ ".md": "markdown",
40
+ }
41
+
42
+ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "api/mesofield.rst"]
43
+
44
+ # -- Autodoc ------------------------------------------------------------------
45
+
46
+ autodoc_default_options = {
47
+ "members": True,
48
+ "show-inheritance": True,
49
+ "member-order": "bysource",
50
+ "exclude-members": "__weakref__,__init_subclass__,__subclasshook__",
51
+ }
52
+
53
+
54
+ def _skip_foreign_members(app, what, name, obj, skip, options):
55
+ """Hide classes/functions that live outside the ``mesofield`` package.
56
+
57
+ Re-exports like ``from psygnal import Signal`` would otherwise pull in
58
+ upstream docstrings (often markdown-formatted) and render them poorly.
59
+ """
60
+ if skip:
61
+ return skip
62
+ module = getattr(obj, "__module__", None)
63
+ if module and not module.startswith("mesofield"):
64
+ return True
65
+ return None
66
+ autodoc_typehints = "description"
67
+ autodoc_class_signature = "separated"
68
+ autodoc_preserve_defaults = True
69
+
70
+ # Mock platform-specific imports so the autodoc build can run anywhere.
71
+ autodoc_mock_imports = [
72
+ "pywin32",
73
+ "win32",
74
+ "win32com",
75
+ "winreg",
76
+ ]
77
+
78
+ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
79
+
80
+ # -- Napoleon (Google-style docstrings) ---------------------------------------
81
+
82
+ napoleon_google_docstring = True
83
+ napoleon_numpy_docstring = False
84
+ napoleon_include_init_with_doc = True
85
+ napoleon_use_admonition_for_examples = True
86
+ napoleon_use_admonition_for_notes = True
87
+ napoleon_use_param = True
88
+ napoleon_use_rtype = True
89
+
90
+ # -- MyST ---------------------------------------------------------------------
91
+
92
+ myst_enable_extensions = [
93
+ "colon_fence",
94
+ "deflist",
95
+ "fieldlist",
96
+ "tasklist",
97
+ "attrs_inline",
98
+ "smartquotes",
99
+ "substitution",
100
+ ]
101
+ myst_heading_anchors = 3
102
+
103
+ # -- Intersphinx --------------------------------------------------------------
104
+
105
+ intersphinx_mapping = {
106
+ "python": ("https://docs.python.org/3", None),
107
+ "numpy": ("https://numpy.org/doc/stable", None),
108
+ "pandas": ("https://pandas.pydata.org/docs", None),
109
+ }
110
+
111
+ # -- HTML output --------------------------------------------------------------
112
+
113
+ html_theme = "pydata_sphinx_theme"
114
+ html_title = "Mesofield"
115
+ html_static_path = ["_static"]
116
+ html_logo = "_static/logo.png"
117
+ html_favicon = "_static/favicon.png"
118
+ html_css_files = ["custom.css"]
119
+ html_theme_options = {
120
+ "logo": {
121
+ "text": "MESOFIELD",
122
+ "image_light": "_static/logo.png",
123
+ "image_dark": "_static/logo.png",
124
+ },
125
+ "github_url": "https://github.com/Gronemeyer/mesofield",
126
+ "use_edit_page_button": True,
127
+ "show_prev_next": False,
128
+ "navbar_align": "left",
129
+ "header_links_before_dropdown": 6,
130
+ "icon_links": [],
131
+ "pygment_light_style": "tango",
132
+ "pygment_dark_style": "monokai",
133
+ }
134
+ html_context = {
135
+ "github_user": "Gronemeyer",
136
+ "github_repo": "mesofield",
137
+ "github_version": "main",
138
+ "doc_path": "docs",
139
+ }
140
+
141
+
142
+ # -- sphinx-apidoc ------------------------------------------------------------
143
+
144
+ def _run_apidoc(_app) -> None:
145
+ """Generate one .rst per package/module under mesofield/ before build."""
146
+ from sphinx.ext import apidoc
147
+
148
+ out_dir = Path(__file__).parent / "api"
149
+ src_dir = ROOT / "mesofield"
150
+ excludes = [
151
+ str(src_dir / "_version.py"),
152
+ str(src_dir / "__main__.py"),
153
+ str(src_dir / "datakit" / "_version.py"),
154
+ str(src_dir / "datakit" / "__main__.py"),
155
+ str(src_dir / "datakit" / "_utils"),
156
+ ]
157
+ apidoc.main(
158
+ [
159
+ "--force",
160
+ "--separate",
161
+ "--module-first",
162
+ "--no-toc",
163
+ "--maxdepth",
164
+ "1",
165
+ "--output-dir",
166
+ str(out_dir),
167
+ str(src_dir),
168
+ *excludes,
169
+ ]
170
+ )
171
+
172
+ # Post-process apidoc output:
173
+ # 1. Strip the hardcoded ``:undoc-members:`` so undocumented members do
174
+ # not flood the rendered pages.
175
+ # 2. Rewrite the page title from ``mesofield.devices.base module`` to
176
+ # just ``base`` so the sidebar shows leaf names.
177
+ for rst in out_dir.glob("mesofield*.rst"):
178
+ lines = rst.read_text().splitlines()
179
+ if (
180
+ len(lines) >= 2
181
+ and lines[1]
182
+ and set(lines[1]) <= {"="}
183
+ and len(lines[1]) == len(lines[0])
184
+ ):
185
+ title = lines[0]
186
+ # Strip the "module"/"package" suffix and pick the last dotted part.
187
+ for suffix in (" module", " package"):
188
+ if title.endswith(suffix):
189
+ title = title[: -len(suffix)]
190
+ break
191
+ leaf = title.rsplit(".", 1)[-1] or title
192
+ lines[0] = leaf
193
+ lines[1] = "=" * len(leaf)
194
+ cleaned = [line for line in lines if line.strip() != ":undoc-members:"]
195
+ rst.write_text("\n".join(cleaned) + "\n")
196
+
197
+
198
+ def setup(app) -> None:
199
+ app.connect("builder-inited", _run_apidoc)
200
+ app.connect("autodoc-skip-member", _skip_foreign_members)
@@ -0,0 +1,303 @@
1
+ # Developer Guide
2
+
3
+ This guide is for **developers extending mesofield** — writing custom
4
+ device adapters, subclassing `Procedure`, building frame processors, or
5
+ contributing to the framework itself. If you're just running
6
+ acquisitions, see the [User Guide](user_guide.md) instead.
7
+
8
+ ## Architecture in one diagram
9
+
10
+ ```
11
+ experiment.json
12
+
13
+
14
+ ExperimentConfig ─── HardwareManager ─── Devices (BaseDevice subclasses)
15
+ │ │ │
16
+ │ │ ├── DataProducer.signals.data ──┐
17
+ │ │ │ ▼
18
+ │ └── DataManager ◀───── Queues ─────── DataSaver ───--- disk
19
+
20
+
21
+ Procedure (orchestrates lifecycle, emits signals, owns the run)
22
+
23
+
24
+ MainWindow (Qt GUI; binds widgets to procedure.events)
25
+ ```
26
+
27
+ The four backbone classes are:
28
+
29
+ | Class | Module | Owns |
30
+ |-------|--------|------|
31
+ | `Procedure` | [`mesofield.base`](api/generated/mesofield.base) | Run lifecycle (initialize → arm → start → finish), hooks, manifest |
32
+ | `ExperimentConfig` | [`mesofield.config`](api/generated/mesofield.config) | Parameter registry + JSON I/O + BIDS paths |
33
+ | `HardwareManager` | [`mesofield.hardware`](api/generated/mesofield.hardware) | YAML-driven device factory + lifecycle |
34
+ | `DataManager` | [`mesofield.data`](api/generated/mesofield.data) | Per-run data queues, notes, timestamps, manifest writes |
35
+
36
+ ## Procedure lifecycle
37
+
38
+ ```
39
+ 1. initialize_hardware — bring devices up (one-time)
40
+ 2. prerun — subclass hook (default: no-op)
41
+ 3. hardware.arm_all — per-run prep on every device
42
+ 4. connect primary.signals.finished -> _cleanup_procedure
43
+ 5. on_started — subclass hook
44
+ 6. hardware.start_all
45
+ 7. on_finished — subclass hook (after primary fires finished)
46
+ 8. save_data + cleanup
47
+ ```
48
+
49
+ Hooks `prerun`, `on_started`, `on_finished` are no-ops on `Procedure`
50
+ itself. Override them in your subclass under
51
+ `experiments/<name>/procedure.py`.
52
+
53
+ ```python
54
+ from mesofield.base import Procedure
55
+
56
+
57
+ class MyProcedure(Procedure):
58
+ def prerun(self):
59
+ self.logger.info(f"Subject {self.config.subject}, "
60
+ f"duration {self.config.duration}s")
61
+
62
+ def on_started(self):
63
+ # called after every device has started
64
+ pass
65
+
66
+ def on_finished(self):
67
+ # called after the primary camera signals finished
68
+ self.logger.info("Run complete; manifest will be written next")
69
+ ```
70
+
71
+ `load_procedure_from_config` is the discovery hook called by the CLI; it
72
+ reads the optional `procedure_file` and `procedure_class` fields from
73
+ `experiment.json` and instantiates your subclass.
74
+
75
+ ## Procedure signals (`procedure.events`)
76
+
77
+ `Procedure.events` is a [`ProcedureSignals`](api/generated/mesofield.base)
78
+ `QObject` exposing four `pyqtSignal`s:
79
+
80
+ | Signal | Payload | Fires when |
81
+ |--------|---------|-----------|
82
+ | `procedure_started` | — | After all devices have started |
83
+ | `hardware_initialized` | `bool` (success) | After `initialize_hardware` |
84
+ | `procedure_finished` | — | After cleanup completes successfully |
85
+ | `procedure_error` | `str` (message) | On any uncaught run-time error |
86
+ | `data_saved` | — | After `save_data` completes |
87
+
88
+ Connect from a Qt widget or from another device:
89
+
90
+ ```python
91
+ procedure.events.procedure_started.connect(self.lock_form)
92
+ procedure.events.procedure_finished.connect(self.unlock_form)
93
+ procedure.events.procedure_error.connect(self.show_error_dialog)
94
+ ```
95
+
96
+ ## Custom hardware devices
97
+
98
+ A hardware device is any class that satisfies the
99
+ [`HardwareDevice`](api/generated/mesofield.protocols) protocol. The
100
+ easiest path is to subclass one of the base classes:
101
+
102
+ | Base | Use when |
103
+ |------|----------|
104
+ | `BaseDevice` | Generic device with no streaming data |
105
+ | `BaseDataProducer` | Streaming source (timeseries / counts) with a buffer |
106
+ | `BaseSerialDevice` | Streaming source whose transport is a serial port |
107
+ | `BaseCamera` | Anything that produces frames + writes a writer file |
108
+
109
+ ### Minimal example — a serial sensor
110
+
111
+ ```python
112
+ from mesofield import DeviceRegistry
113
+ from mesofield.devices.base import BaseSerialDevice
114
+
115
+
116
+ @DeviceRegistry.register("thermal")
117
+ class ThermalSensor(BaseSerialDevice):
118
+ """One-byte-per-sample thermal probe over serial."""
119
+
120
+ file_type = "csv"
121
+ bids_type = "beh"
122
+ data_type = "thermal"
123
+
124
+ def parse_line(self, line: bytes):
125
+ """Parse one serial frame.
126
+
127
+ Returns:
128
+ ``(payload, timestamp_or_None)`` — the payload is whatever
129
+ you want fanned out on ``self.signals.data``; pass ``None``
130
+ for the timestamp to let the framework stamp it.
131
+ """
132
+ return float(line), None
133
+ ```
134
+
135
+ Then in `hardware.yaml`:
136
+
137
+ ```yaml
138
+ thermal:
139
+ type: thermal
140
+ port: /dev/ttyUSB1
141
+ baudrate: 115200
142
+ output:
143
+ suffix: thermal
144
+ file_type: csv
145
+ bids_type: beh
146
+ ```
147
+
148
+ `@DeviceRegistry.register("thermal")` is what binds the YAML `type:`
149
+ string to the class. The decorator also stamps `registry_key` onto the
150
+ class so any instance can report its YAML type for hardware export.
151
+
152
+ ### Camera-shaped devices
153
+
154
+ For anything that produces frames, subclass
155
+ [`BaseCamera`](api/generated/mesofield.devices.base_camera) — it
156
+ defaults to OME-TIFF output via `CustomWriter`, exposes a `snap()` /
157
+ `start_live()` / `stop_live()` contract for the GUI preview, and
158
+ plumbs frame metadata into the manifest. The
159
+ [`MMCamera`](api/generated/mesofield.devices.cameras) and
160
+ [`OpenCVCamera`](api/generated/mesofield.devices.cameras) classes
161
+ are the two concrete implementations to read for reference.
162
+
163
+ ## Threading models
164
+
165
+ Devices can use any concurrency model that respects the lifecycle:
166
+
167
+ ```python
168
+ # Qt-thread device (camera, GUI-driven serial)
169
+ from PyQt6.QtCore import QThread
170
+ from mesofield.protocols import HardwareDevice
171
+
172
+ class QtDevice(QThread):
173
+ device_type = "qt_device"
174
+ device_id = "qdev"
175
+ def run(self): ... # Qt thread loop
176
+
177
+
178
+ # Python threading device
179
+ from mesofield.protocols import ThreadedHardwareDevice
180
+
181
+ class ThreadedDevice(ThreadedHardwareDevice):
182
+ device_type = "thread_device"
183
+ device_id = "tdev"
184
+ def _run(self): ... # standard daemon thread
185
+
186
+
187
+ # asyncio device
188
+ from mesofield.protocols import AsyncioHardwareDevice
189
+
190
+ class AsyncDevice(AsyncioHardwareDevice):
191
+ device_type = "async_device"
192
+ device_id = "adev"
193
+ async def _run(self): ...
194
+ ```
195
+
196
+ Pick whichever fits your hardware best — the framework only cares about
197
+ the protocol, not the concurrency model.
198
+
199
+ ## Frame processors
200
+
201
+ For per-frame compute (mean intensity, ROI tracking, anything that
202
+ turns an ndarray into a scalar), use the `@processor` decorator on a
203
+ `Procedure` method:
204
+
205
+ ```python
206
+ from mesofield.base import Procedure, processor
207
+
208
+
209
+ class MyProcedure(Procedure):
210
+ @processor(camera="meso", plot=True, label="Frame mean", y_range=(0, 65535))
211
+ def frame_mean(self, img, idx, ts):
212
+ return float(img.mean())
213
+ ```
214
+
215
+ The framework wraps the function in a
216
+ [`FrameProcessor`](api/generated/mesofield.processors), attaches it to
217
+ the camera whose `device_id` matches `"meso"`, registers it with
218
+ `DataManager`, and (when `plot=True`) adds a live
219
+ [`SerialWidget`](api/generated/mesofield.gui.speedplotter) to the GUI.
220
+
221
+ Recognised `plot_kwargs`: `label`, `value_label`, `value_units`,
222
+ `y_range`, `value_scale`, `max_points`.
223
+
224
+ ## Scaffolding a new experiment
225
+
226
+ The CLI scaffold drops a fill-out template:
227
+
228
+ ```bash
229
+ mesofield new my-experiment --rig my-rig
230
+ cd my-experiment
231
+ ```
232
+
233
+ You get:
234
+
235
+ ```
236
+ my-experiment/
237
+ README.md
238
+ experiment.json # subjects, duration, DisplayKeys
239
+ hardware.yaml # copied from the selected rig
240
+ procedure.py # your Procedure subclass
241
+ devices/
242
+ __init__.py
243
+ thermal_example.py # annotated custom-device template
244
+ ```
245
+
246
+ `--rig` selects from `mesofield rig list`. Use `--hardware path/to/file`
247
+ to use an explicit YAML; omit both to enter an interactive picker.
248
+
249
+ ## Rig store
250
+
251
+ A `hardware.yaml` is rig-specific (COM ports, camera ids, MM `.cfg`
252
+ paths). Each machine keeps a small store of named canonical configs in
253
+ the OS config directory:
254
+
255
+ ```bash
256
+ mesofield rig new my-rig # writes a fill-out template
257
+ mesofield rig list # show registered rigs
258
+ mesofield rig add my-rig file.yaml # adopt an existing yaml
259
+ ```
260
+
261
+ The store lives at the platform-default config location (resolved by
262
+ `platformdirs`); `mesofield rig where` prints the path.
263
+
264
+ ## Logging
265
+
266
+ Use the project logger inside your code:
267
+
268
+ ```python
269
+ from mesofield.utils._logger import get_logger
270
+
271
+ logger = get_logger(__name__)
272
+ logger.info("...")
273
+ ```
274
+
275
+ Every device and processor should use a `__name__`-scoped logger so the
276
+ file traces are easy to grep.
277
+
278
+ ## Where to look for examples
279
+
280
+ - [`mesofield/devices/mocks.py`](api/generated/mesofield.devices.mocks)
281
+ — mock serial encoder + mock camera. Read these first; they're the
282
+ simplest concrete implementations of every base class.
283
+ - [`mesofield/devices/cameras.py`](api/generated/mesofield.devices.cameras)
284
+ — `MMCamera` (Micro-Manager backend) and `OpenCVCamera` (capture
285
+ thread + MP4 writer). The two shapes most custom cameras start from.
286
+ - [`mesofield/scaffold/experiment.py`](api/generated/mesofield.scaffold)
287
+ — what the `mesofield new` CLI emits. Reading the templates is a
288
+ shortcut to understanding the expected file shape.
289
+ - [`mesofield/processors/frame_mean.py`](api/generated/mesofield.processors)
290
+ — three-line frame processor. The minimum viable processor.
291
+
292
+ ## Retrofitting legacy sessions
293
+
294
+ Sessions acquired before manifests landed can be retrofitted:
295
+
296
+ ```bash
297
+ mesofield process retrofit-manifest /path/to/experiment
298
+ ```
299
+
300
+ This walks the BIDS tree, reads `timestamps.csv` / `configuration.csv`,
301
+ and synthesises an `AcquisitionManifest` per session. Calibration
302
+ constants aren't recoverable (they weren't written), but everything
303
+ else round-trips and downstream tools become happy again.
docs/index.md ADDED
@@ -0,0 +1,25 @@
1
+ ```{include} ../README.md
2
+ ```
3
+
4
+ ```{toctree}
5
+ :caption: Get started
6
+ :hidden:
7
+
8
+ Home <self>
9
+ tutorial
10
+ ```
11
+
12
+ ```{toctree}
13
+ :caption: Guides
14
+ :hidden:
15
+
16
+ user_guide
17
+ developer_guide
18
+ ```
19
+
20
+ ```{toctree}
21
+ :caption: Reference
22
+ :hidden:
23
+
24
+ api/index
25
+ ```
docs/tutorial.md ADDED
@@ -0,0 +1,4 @@
1
+ # Tutorial
2
+
3
+ ```{include} ../TUTORIAL.md
4
+ ```