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,213 @@
1
+ """Build a pandas pickle from this experiment's session outputs.
2
+
3
+ Reads each `manifest.json` produced by `procedure.py`, ingests each
4
+ declared producer (`wheel` CSV, `meso` / `pupil` OME-TIFF + frame-metadata
5
+ sidecars), and writes a single `processed/dataset.pkl` you can load with
6
+ ``pd.read_pickle(...)``.
7
+
8
+ The dataset has the shape databench consumes:
9
+
10
+ Subject Session Task | Source Signal
11
+ -----------------------------------------------------
12
+ DEMO 01 freeview | wheel timestamp, payload
13
+ | meso frame_index, TimeReceivedByCore
14
+ | pupil frame_index, TimeReceivedByCore
15
+
16
+ The mock producers don't write columns that match datakit's real
17
+ SOURCE_REGISTRY parsers (the wheel parser expects ``Clicks, Time,
18
+ Speed``; the mesoscope parser globs ``*_mesoscope.ome.tiff…``). We
19
+ therefore use the AcquisitionManifest as the source of truth and do a
20
+ manifest-driven ingest -- which is the pattern lab programmers will
21
+ write against their own producers.
22
+
23
+ The script does, however, import freely from ``mesofield.datakit`` and
24
+ ``mesokit_schema``: the manifest models, the loader's
25
+ ``ExperimentStore`` (only for the final pickle write convention),
26
+ ``hash_file`` for provenance, etc. The point is the datakit module is
27
+ the toolkit; the parsers shipped with it are one option among many.
28
+
29
+ Run::
30
+
31
+ python load_dataset.py # uses ./data
32
+ python load_dataset.py --root ./ # explicit
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import argparse
38
+ import json
39
+ from datetime import datetime, timezone
40
+ from pathlib import Path
41
+ from typing import Iterable
42
+
43
+ import numpy as np
44
+ import pandas as pd
45
+
46
+ # mesokit-schema: the typed manifest contract.
47
+ from mesokit_schema import (
48
+ AcquisitionManifest,
49
+ DatasetManifest,
50
+ ProducerEntry,
51
+ SourceVersion,
52
+ TimeBasis,
53
+ )
54
+ from mesokit_schema.dataset import hash_file
55
+
56
+ # mesofield.datakit: the ingest toolkit. We don't use SOURCE_REGISTRY's
57
+ # real-hardware parsers here (mock filenames don't match their globs),
58
+ # but we lean on the datakit module's package surface for everything else.
59
+ import mesofield.datakit # noqa: F401 — ensures datakit's logger is wired
60
+
61
+
62
+ def discover_sessions(root: Path) -> list[Path]:
63
+ """Find every `data/sub-*/ses-*/manifest.json` under `root`."""
64
+ data_root = root / "data"
65
+ if not data_root.exists():
66
+ return []
67
+ return sorted(data_root.glob("sub-*/ses-*/manifest.json"))
68
+
69
+
70
+ def load_producer_table(session_dir: Path, producer: ProducerEntry) -> pd.DataFrame:
71
+ """Read one producer's output (or its sidecar) into a DataFrame."""
72
+ if producer.file_type.endswith("csv"):
73
+ return pd.read_csv(session_dir / producer.output_path)
74
+
75
+ if producer.file_type == "ome.tiff":
76
+ # The pixel data lives in the TIFF (inspect with ImageJ); the
77
+ # time-axis metadata we merge into the dataset comes from the
78
+ # frame-metadata sidecar.
79
+ if not producer.metadata_path:
80
+ raise RuntimeError(
81
+ f"Camera producer {producer.device_id!r} has no metadata_path"
82
+ )
83
+ sidecar = json.loads((session_dir / producer.metadata_path).read_text())
84
+ return pd.DataFrame(sidecar.get("p0", []))
85
+
86
+ raise RuntimeError(
87
+ f"load_dataset.py: unknown file_type {producer.file_type!r} for "
88
+ f"producer {producer.device_id!r}; extend this loader for new types."
89
+ )
90
+
91
+
92
+ def build_session_table(session_dir: Path, manifest: AcquisitionManifest) -> pd.DataFrame:
93
+ """Multi-source DataFrame for a single session.
94
+
95
+ Anchored to the mesoscope (primary) frame count; shorter sources
96
+ are NaN-padded, longer ones truncated. Trades temporal precision
97
+ for an inspectable, regular table.
98
+ """
99
+ primary = next(
100
+ (p for p in manifest.producers if p.data_type == "frames" and p.bids_type == "func"),
101
+ manifest.producers[0],
102
+ )
103
+ primary_df = load_producer_table(session_dir, primary)
104
+ anchor_rows = len(primary_df)
105
+
106
+ columns: dict[tuple[str, str], list] = {}
107
+
108
+ def _add(source: str, df: pd.DataFrame) -> None:
109
+ for col in df.columns:
110
+ values = df[col].iloc[:anchor_rows].tolist()
111
+ if len(values) < anchor_rows:
112
+ values = values + [None] * (anchor_rows - len(values))
113
+ columns[(source, col)] = values
114
+
115
+ for producer in manifest.producers:
116
+ df = load_producer_table(session_dir, producer)
117
+ # Use device_id as the source key so two cameras (meso / pupil)
118
+ # appear as distinct sources even though both share data_type=frames.
119
+ _add(producer.device_id, df)
120
+
121
+ table = pd.DataFrame(columns)
122
+ table.columns = pd.MultiIndex.from_tuples(table.columns, names=["Source", "Signal"])
123
+
124
+ session = manifest.session
125
+ task = session.task or "default"
126
+ table.index = pd.MultiIndex.from_tuples(
127
+ [(session.subject, session.session, task)] * len(table),
128
+ names=["Subject", "Session", "Task"],
129
+ )
130
+ return table
131
+
132
+
133
+ def build_dataset(root: Path) -> tuple[Path, Path, pd.DataFrame]:
134
+ """Ingest every session under `root/data/`. Returns (pickle, manifest, df)."""
135
+ manifest_paths = discover_sessions(root)
136
+ if not manifest_paths:
137
+ raise FileNotFoundError(
138
+ f"No AcquisitionManifests found under {root / 'data'}. "
139
+ f"Run `python procedure.py` first."
140
+ )
141
+
142
+ tables: list[pd.DataFrame] = []
143
+ acq_hashes: list[str] = []
144
+ for mpath in manifest_paths:
145
+ acq = AcquisitionManifest.read(mpath)
146
+ if not acq.acquisition_complete:
147
+ print(f"[skip] {mpath} reports acquisition_complete=False")
148
+ continue
149
+ tables.append(build_session_table(mpath.parent, acq))
150
+ acq_hashes.append(acq.content_hash())
151
+ print(f"[ok] {acq.session.subject}/{acq.session.session}: "
152
+ f"{len(tables[-1])} rows × {len(tables[-1].columns)} cols")
153
+
154
+ if not tables:
155
+ raise RuntimeError("No complete sessions to ingest.")
156
+
157
+ dataset = pd.concat(tables, axis=0).sort_index()
158
+ out_dir = root / "processed"
159
+ out_dir.mkdir(exist_ok=True)
160
+ pkl_path = out_dir / "dataset.pkl"
161
+ dataset.to_pickle(pkl_path)
162
+
163
+ ds_manifest = DatasetManifest(
164
+ datakit_version="manifest-driven-ingest",
165
+ built_at=datetime.now(timezone.utc),
166
+ upstream_acquisition_hash=acq_hashes[0] if len(acq_hashes) == 1 else None,
167
+ data_file="dataset.pkl",
168
+ data_content_hash=hash_file(pkl_path),
169
+ time_basis=TimeBasis(
170
+ clock_source="derived",
171
+ description="Anchored on mesoscope frame count per session.",
172
+ ),
173
+ source_versions=[
174
+ SourceVersion(
175
+ tag="multi-source",
176
+ version="0.1.0",
177
+ parser_class="load_dataset.build_session_table",
178
+ )
179
+ ],
180
+ columns=[(s, c) for (s, c) in dataset.columns.tolist()],
181
+ )
182
+ ds_manifest_path = out_dir / "dataset_manifest.json"
183
+ ds_manifest.write(ds_manifest_path)
184
+
185
+ return pkl_path, ds_manifest_path, dataset
186
+
187
+
188
+ def main() -> int:
189
+ parser = argparse.ArgumentParser(description=__doc__)
190
+ parser.add_argument(
191
+ "--root", type=Path, default=Path(__file__).resolve().parent,
192
+ help="Experiment root containing data/ (default: this file's dir)",
193
+ )
194
+ args = parser.parse_args()
195
+
196
+ pkl, ds_manifest_path, df = build_dataset(args.root.resolve())
197
+ print()
198
+ print(f"Wrote {pkl}")
199
+ print(f"Wrote {ds_manifest_path}")
200
+ print()
201
+ print("--- first 3 rows ---")
202
+ print(df.head(3))
203
+ print()
204
+ print(f"--- columns ({len(df.columns)} total) ---")
205
+ for col in df.columns:
206
+ print(f" {col}")
207
+ print()
208
+ print("Load in Python: pd.read_pickle({})".format(repr(str(pkl))))
209
+ return 0
210
+
211
+
212
+ if __name__ == "__main__":
213
+ raise SystemExit(main())
@@ -0,0 +1,87 @@
1
+ """Two-camera + encoder demo: 10-second headless acquisition.
2
+
3
+ Subclasses :class:`mesofield.base.Procedure` with the standard duration-
4
+ timer pattern so the run terminates cleanly without a primary device's
5
+ hardware finished-signal. The AcquisitionManifest is written
6
+ automatically by the base Procedure's cleanup hook.
7
+
8
+ Run:
9
+
10
+ python procedure.py # headless (no Qt)
11
+ mesofield launch experiment.json # GUI
12
+
13
+ The headless path is what `mesofield init` scaffolds; cf. TUTORIAL.md.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import threading
19
+
20
+ from mesofield import DeviceRegistry
21
+ from mesofield.base import Procedure
22
+ from mesofield.devices.mocks import MockFrameProducer
23
+ from mesofield.devices.mocks import MockEncoderDevice
24
+ from mesofield.processors import FrameMean
25
+
26
+
27
+ DeviceRegistry._registry.setdefault("mock_wheel", MockEncoderDevice)
28
+ DeviceRegistry._registry.setdefault("mock_camera", MockFrameProducer)
29
+
30
+
31
+ class TwoCamDemoProcedure(Procedure):
32
+ """Mock 2-camera + encoder procedure with duration-gated cleanup."""
33
+
34
+ def __init__(self, *args, **kwargs):
35
+ super().__init__(*args, **kwargs)
36
+ # Worked example of the procedure-authored processor API:
37
+ # construct, assign, done. Auto-attached, auto-registered with the
38
+ # DataManager, and auto-plotted in the GUI when `plot=True`.
39
+ self.frame_mean = FrameMean(
40
+ camera=self.hardware.primary,
41
+ plot=True,
42
+ label="Frame Mean",
43
+ value_label="Mean intensity",
44
+ y_range=(0, 255),
45
+ )
46
+
47
+ def on_started(self) -> None:
48
+ duration = self.config.get("duration")
49
+ if duration:
50
+ self.logger.info(f"Duration cap armed: {duration}s")
51
+ self._duration_timer = threading.Timer(float(duration), self.cleanup)
52
+ self._duration_timer.daemon = True
53
+ self._duration_timer.start()
54
+
55
+ def on_finished(self) -> None:
56
+ super().on_finished()
57
+ timer = getattr(self, "_duration_timer", None)
58
+ if timer is not None:
59
+ timer.cancel()
60
+ self._duration_timer = None
61
+
62
+
63
+ def main() -> int:
64
+ import sys
65
+ from pathlib import Path
66
+
67
+ cfg = Path(__file__).parent / "experiment.json"
68
+ proc = TwoCamDemoProcedure(str(cfg))
69
+ finished = proc.run_until_finished(timeout=30.0)
70
+ if not finished:
71
+ print("Procedure did not finish in time.", file=sys.stderr)
72
+ return 1
73
+
74
+ session_dir = (
75
+ Path(proc.data_dir)
76
+ / f"sub-{proc.config.subject}"
77
+ / f"ses-{proc.config.session}"
78
+ )
79
+ print(f"\nAcquisition complete.")
80
+ print(f" Session dir: {session_dir}")
81
+ print(f" Manifest: {session_dir / 'manifest.json'}")
82
+ print(f"\nNext: python load_dataset.py")
83
+ return 0
84
+
85
+
86
+ if __name__ == "__main__":
87
+ raise SystemExit(main())
mesofield/__init__.py ADDED
@@ -0,0 +1,45 @@
1
+ """Mesofield top-level package.
2
+
3
+ This module exposes the :class:`DeviceRegistry` decorator used by hardware
4
+ adapters to declare themselves under a YAML ``type:`` key. The remaining
5
+ subpackages are not auto-imported; pull them in explicitly:
6
+
7
+ .. code-block:: python
8
+
9
+ from mesofield.base import Procedure
10
+ from mesofield.config import ExperimentConfig
11
+ from mesofield.hardware import HardwareManager
12
+ """
13
+
14
+ from typing import Type, Callable, Dict, Optional, Any, TypeVar
15
+
16
+ T = TypeVar("T")
17
+
18
+ class DeviceRegistry:
19
+ """Registry mapping YAML ``type:`` strings to device classes.
20
+
21
+ Hardware adapters register themselves via the
22
+ :meth:`~DeviceRegistry.register` decorator; the
23
+ :class:`~mesofield.hardware.HardwareManager` looks them up by string
24
+ when materialising devices from a YAML file.
25
+ """
26
+
27
+ _registry: Dict[str, Type[Any]] = {}
28
+
29
+ @classmethod
30
+ def register(cls, device_type: str) -> Callable[[Type[T]], Type[T]]:
31
+ """Register a device class for a specific device type.
32
+
33
+ The decorator also stamps ``registry_key`` onto the class so any
34
+ device instance can report its YAML ``type:`` for hardware export.
35
+ """
36
+ def decorator(device_class: Type[T]) -> Type[T]:
37
+ cls._registry[device_type] = device_class
38
+ device_class.registry_key = device_type
39
+ return device_class
40
+ return decorator
41
+
42
+ @classmethod
43
+ def get_class(cls, device_type: str) -> Optional[Type[Any]]:
44
+ """Get the device class for a specific device type."""
45
+ return cls._registry.get(device_type)
mesofield/__main__.py ADDED
@@ -0,0 +1,11 @@
1
+ """``python -m mesofield`` — entry point for the Mesofield CLI.
2
+
3
+ The CLI itself lives in :mod:`mesofield.cli`; this module just exposes the
4
+ root group so both ``python -m mesofield`` and the ``mesofield`` console
5
+ script resolve to the same place.
6
+ """
7
+
8
+ from mesofield.cli import cli
9
+
10
+ if __name__ == "__main__":
11
+ cli()
mesofield/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.3.2b0'
22
+ __version_tuple__ = version_tuple = (0, 3, 2, 'b0')
23
+
24
+ __commit_id__ = commit_id = None