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
docs/_static/custom.css
ADDED
|
@@ -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
|
+
}
|
docs/_static/favicon.png
ADDED
|
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)
|
docs/developer_guide.md
ADDED
|
@@ -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