mesofield 0.3.2b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docs/_static/custom.css +40 -0
- docs/_static/favicon.png +0 -0
- docs/_static/logo.png +0 -0
- docs/api/index.md +70 -0
- docs/conf.py +200 -0
- docs/developer_guide.md +303 -0
- docs/index.md +25 -0
- docs/tutorial.md +4 -0
- docs/user_guide.md +172 -0
- examples/teensy_pulse_generator.py +320 -0
- experiments/pipeline_demo/experiment.json +24 -0
- experiments/pipeline_demo/hardware.yaml +23 -0
- experiments/pipeline_demo/procedure.py +50 -0
- experiments/two_cam_demo/experiment.json +24 -0
- experiments/two_cam_demo/hardware.yaml +58 -0
- experiments/two_cam_demo/load_dataset.py +213 -0
- experiments/two_cam_demo/procedure.py +87 -0
- external/video-codecs/openh264-1.8.0-win64.dll +0 -0
- mesofield/__init__.py +45 -0
- mesofield/__main__.py +11 -0
- mesofield/_version.py +24 -0
- mesofield/base.py +750 -0
- mesofield/cli/__init__.py +57 -0
- mesofield/cli/_richhelp.py +100 -0
- mesofield/cli/acquire.py +254 -0
- mesofield/cli/datakit.py +165 -0
- mesofield/cli/process.py +376 -0
- mesofield/cli/rig.py +108 -0
- mesofield/cli/tools.py +347 -0
- mesofield/config.py +751 -0
- mesofield/data/__init__.py +23 -0
- mesofield/data/batch.py +633 -0
- mesofield/data/manager.py +388 -0
- mesofield/data/writer.py +289 -0
- mesofield/datakit/__init__.py +44 -0
- mesofield/datakit/__main__.py +35 -0
- mesofield/datakit/_utils/_logger.py +5 -0
- mesofield/datakit/_version.py +141 -0
- mesofield/datakit/config.py +50 -0
- mesofield/datakit/core.py +783 -0
- mesofield/datakit/datamodel.py +200 -0
- mesofield/datakit/discover.py +124 -0
- mesofield/datakit/explore.py +651 -0
- mesofield/datakit/notebooks/pupil_dlc.ipynb +2445 -0
- mesofield/datakit/profile.py +535 -0
- mesofield/datakit/shell.py +83 -0
- mesofield/datakit/sources/__init__.py +65 -0
- mesofield/datakit/sources/analysis/mesomap.py +194 -0
- mesofield/datakit/sources/analysis/mesoscope.py +77 -0
- mesofield/datakit/sources/analysis/pupil.py +246 -0
- mesofield/datakit/sources/behavior/__init__.py +0 -0
- mesofield/datakit/sources/behavior/dataqueue.py +281 -0
- mesofield/datakit/sources/behavior/psychopy.py +364 -0
- mesofield/datakit/sources/behavior/treadmill.py +323 -0
- mesofield/datakit/sources/behavior/wheel.py +277 -0
- mesofield/datakit/sources/camera/mesoscope.py +32 -0
- mesofield/datakit/sources/camera/metadata_json.py +130 -0
- mesofield/datakit/sources/camera/pupil.py +28 -0
- mesofield/datakit/sources/camera/suite2p.py +547 -0
- mesofield/datakit/sources/register.py +204 -0
- mesofield/datakit/sources/session/config.py +130 -0
- mesofield/datakit/sources/session/notes.py +63 -0
- mesofield/datakit/sources/session/timestamps.py +58 -0
- mesofield/datakit/timeline.py +306 -0
- mesofield/devices/__init__.py +42 -0
- mesofield/devices/base.py +498 -0
- mesofield/devices/base_camera.py +295 -0
- mesofield/devices/cameras.py +740 -0
- mesofield/devices/daq.py +151 -0
- mesofield/devices/encoder.py +384 -0
- mesofield/devices/mocks.py +275 -0
- mesofield/devices/psychopy_device.py +455 -0
- mesofield/devices/subprocesses/__init__.py +0 -0
- mesofield/devices/subprocesses/psychopy.py +133 -0
- mesofield/devices/treadmill.py +318 -0
- mesofield/engines.py +380 -0
- mesofield/gui/Mesofield_icon.png +0 -0
- mesofield/gui/__init__.py +76 -0
- mesofield/gui/config_wizard.py +724 -0
- mesofield/gui/controller.py +535 -0
- mesofield/gui/dynamic_controller.py +78 -0
- mesofield/gui/maingui.py +427 -0
- mesofield/gui/mdagui.py +285 -0
- mesofield/gui/qt_device_adapter.py +109 -0
- mesofield/gui/speedplotter.py +152 -0
- mesofield/gui/theme.py +445 -0
- mesofield/gui/tiff_viewer.py +1050 -0
- mesofield/gui/viewer.py +691 -0
- mesofield/hardware.py +549 -0
- mesofield/playback.py +1298 -0
- mesofield/processing/__init__.py +12 -0
- mesofield/processing/runner.py +237 -0
- mesofield/processors/__init__.py +13 -0
- mesofield/processors/base.py +287 -0
- mesofield/processors/frame_mean.py +19 -0
- mesofield/protocols.py +378 -0
- mesofield/scaffold/__init__.py +34 -0
- mesofield/scaffold/experiment.py +400 -0
- mesofield/scaffold/rigs.py +121 -0
- mesofield/signals.py +85 -0
- mesofield/utils/__init__.py +0 -0
- mesofield/utils/_logger.py +156 -0
- mesofield/utils/retrofit.py +309 -0
- mesofield/utils/utils.py +217 -0
- mesofield-0.3.2b0.dist-info/METADATA +178 -0
- mesofield-0.3.2b0.dist-info/RECORD +111 -0
- mesofield-0.3.2b0.dist-info/WHEEL +5 -0
- mesofield-0.3.2b0.dist-info/entry_points.txt +2 -0
- mesofield-0.3.2b0.dist-info/licenses/LICENSE +21 -0
- mesofield-0.3.2b0.dist-info/top_level.txt +6 -0
- scripts/bench_frame_processor.py +103 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Common data structures shared across the ``datakit`` pipeline.
|
|
2
|
+
|
|
3
|
+
The module groups the frozen dataclasses that represent discovered files,
|
|
4
|
+
aligned timelines, and high-level dataset bundles so that explorers can see
|
|
5
|
+
"what flows through the system" in one place.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, Iterable, Mapping, Optional, Sequence, Tuple
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
StreamKind = str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class StreamPayload:
|
|
24
|
+
"""Typed wrapper around arbitrary stream values.
|
|
25
|
+
|
|
26
|
+
Each payload records its ``kind`` (table/array/mapping/sequence/scalar), the
|
|
27
|
+
raw ``data`` object, and lightweight ``attrs`` that describe shape or column
|
|
28
|
+
metadata. Persisting this metadata makes it easier to reconstruct the
|
|
29
|
+
original Python object on reload.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
kind: StreamKind
|
|
33
|
+
data: Any
|
|
34
|
+
attrs: Dict[str, Any] = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
def __post_init__(self) -> None:
|
|
37
|
+
object.__setattr__(self, "attrs", dict(self.attrs or {}))
|
|
38
|
+
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
# Constructors
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
@classmethod
|
|
43
|
+
def table(cls, frame: pd.DataFrame, attrs: Optional[Dict[str, Any]] = None) -> "StreamPayload":
|
|
44
|
+
base_attrs = {
|
|
45
|
+
"columns": list(frame.columns),
|
|
46
|
+
"index_name": frame.index.name,
|
|
47
|
+
}
|
|
48
|
+
if attrs:
|
|
49
|
+
base_attrs.update(attrs)
|
|
50
|
+
return cls(kind="table", data=frame, attrs=base_attrs)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def array(cls, array: np.ndarray, attrs: Optional[Dict[str, Any]] = None) -> "StreamPayload":
|
|
54
|
+
base_attrs = {
|
|
55
|
+
"dtype": str(array.dtype),
|
|
56
|
+
"shape": tuple(int(x) for x in array.shape),
|
|
57
|
+
}
|
|
58
|
+
if attrs:
|
|
59
|
+
base_attrs.update(attrs)
|
|
60
|
+
return cls(kind="array", data=array, attrs=base_attrs)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def mapping(cls, mapping: Mapping[str, Any], attrs: Optional[Dict[str, Any]] = None) -> "StreamPayload":
|
|
64
|
+
base_attrs = {
|
|
65
|
+
"keys": list(mapping.keys()),
|
|
66
|
+
}
|
|
67
|
+
if attrs:
|
|
68
|
+
base_attrs.update(attrs)
|
|
69
|
+
return cls(kind="mapping", data=dict(mapping), attrs=base_attrs)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def sequence(cls, seq: Sequence[Any], attrs: Optional[Dict[str, Any]] = None) -> "StreamPayload":
|
|
73
|
+
base_attrs = {
|
|
74
|
+
"length": len(seq),
|
|
75
|
+
}
|
|
76
|
+
if attrs:
|
|
77
|
+
base_attrs.update(attrs)
|
|
78
|
+
return cls(kind="sequence", data=list(seq), attrs=base_attrs)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def scalar(cls, value: Any, attrs: Optional[Dict[str, Any]] = None) -> "StreamPayload":
|
|
82
|
+
return cls(kind="scalar", data=value, attrs=dict(attrs or {}))
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
# Introspection helpers
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
@property
|
|
88
|
+
def is_table(self) -> bool:
|
|
89
|
+
return self.kind == "table"
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def is_array(self) -> bool:
|
|
93
|
+
return self.kind == "array"
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def is_mapping(self) -> bool:
|
|
97
|
+
return self.kind == "mapping"
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def is_sequence(self) -> bool:
|
|
101
|
+
return self.kind == "sequence"
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def ensure(cls, value: Any, *, kind: Optional[StreamKind] = None, attrs: Optional[Dict[str, Any]] = None) -> "StreamPayload":
|
|
105
|
+
if isinstance(value, StreamPayload):
|
|
106
|
+
if attrs:
|
|
107
|
+
merged = dict(value.attrs)
|
|
108
|
+
merged.update(attrs)
|
|
109
|
+
return StreamPayload(kind=value.kind, data=value.data, attrs=merged)
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
detected_kind = kind
|
|
113
|
+
|
|
114
|
+
if detected_kind is None:
|
|
115
|
+
if isinstance(value, pd.DataFrame):
|
|
116
|
+
detected_kind = "table"
|
|
117
|
+
elif isinstance(value, np.ndarray):
|
|
118
|
+
detected_kind = "array"
|
|
119
|
+
elif isinstance(value, Mapping):
|
|
120
|
+
detected_kind = "mapping"
|
|
121
|
+
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
|
122
|
+
detected_kind = "sequence"
|
|
123
|
+
else:
|
|
124
|
+
detected_kind = "scalar"
|
|
125
|
+
|
|
126
|
+
if detected_kind == "table":
|
|
127
|
+
frame = value if isinstance(value, pd.DataFrame) else pd.DataFrame(value)
|
|
128
|
+
return cls.table(frame, attrs)
|
|
129
|
+
if detected_kind == "array":
|
|
130
|
+
array = value if isinstance(value, np.ndarray) else np.asarray(value)
|
|
131
|
+
return cls.array(array, attrs)
|
|
132
|
+
if detected_kind == "mapping":
|
|
133
|
+
if not isinstance(value, Mapping):
|
|
134
|
+
raise TypeError("Cannot coerce value to mapping payload")
|
|
135
|
+
return cls.mapping(dict(value), attrs)
|
|
136
|
+
if detected_kind == "sequence":
|
|
137
|
+
sequence = value if isinstance(value, Sequence) else list(value)
|
|
138
|
+
return cls.sequence(sequence, attrs)
|
|
139
|
+
return cls.scalar(value, attrs)
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def data_view(self) -> Any:
|
|
143
|
+
"""Return the canonical data representation (DataFrame, ndarray, etc)."""
|
|
144
|
+
|
|
145
|
+
if self.kind == "table" and not isinstance(self.data, pd.DataFrame):
|
|
146
|
+
return pd.DataFrame(self.data)
|
|
147
|
+
if self.kind == "array" and not isinstance(self.data, np.ndarray):
|
|
148
|
+
return np.asarray(self.data)
|
|
149
|
+
if self.kind == "mapping" and not isinstance(self.data, dict):
|
|
150
|
+
return dict(self.data)
|
|
151
|
+
if self.kind == "sequence" and not isinstance(self.data, list):
|
|
152
|
+
return list(self.data)
|
|
153
|
+
return self.data
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass(frozen=True)
|
|
157
|
+
class ManifestEntry:
|
|
158
|
+
"""Single file discovered during manifest building."""
|
|
159
|
+
|
|
160
|
+
tag: str # e.g. "meso_metadata"
|
|
161
|
+
path: str
|
|
162
|
+
origin: str # "data" | "processed"
|
|
163
|
+
subject: str # e.g. "STREHAB07"
|
|
164
|
+
session: str # e.g. "05"
|
|
165
|
+
task: str | None = None # e.g. "widefield"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(frozen=True)
|
|
169
|
+
class LoadedStream:
|
|
170
|
+
"""Hydrated data stream with timestamps and metadata."""
|
|
171
|
+
|
|
172
|
+
tag: str
|
|
173
|
+
t: np.ndarray # seconds, float64, strictly increasing
|
|
174
|
+
value: object # array or domain object
|
|
175
|
+
meta: dict
|
|
176
|
+
_payload: StreamPayload = field(init=False, repr=False, compare=False)
|
|
177
|
+
|
|
178
|
+
def __post_init__(self) -> None:
|
|
179
|
+
payload = StreamPayload.ensure(self.value)
|
|
180
|
+
object.__setattr__(self, "t", np.asarray(self.t, dtype=np.float64))
|
|
181
|
+
object.__setattr__(self, "meta", dict(self.meta or {}))
|
|
182
|
+
object.__setattr__(self, "_payload", payload)
|
|
183
|
+
object.__setattr__(self, "value", payload.data_view)
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def payload(self) -> StreamPayload:
|
|
187
|
+
return self._payload
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def data(self) -> Any:
|
|
191
|
+
return self._payload.data_view
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass(frozen=True)
|
|
195
|
+
class Manifest:
|
|
196
|
+
"""Manifest of files discovered for an experiment root."""
|
|
197
|
+
|
|
198
|
+
root: Path
|
|
199
|
+
entries: list[ManifestEntry]
|
|
200
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""File-system discovery helpers that produce machine-readable manifests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ._utils._logger import get_logger
|
|
9
|
+
from .datamodel import Manifest, ManifestEntry
|
|
10
|
+
from .sources import SOURCE_REGISTRY
|
|
11
|
+
|
|
12
|
+
_BIDS_COMPONENT_PATTERN = re.compile(r"(sub|ses|task)-([A-Za-z0-9]+)", re.IGNORECASE)
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def discover_manifest(experiment_dir: Path | str) -> Manifest:
|
|
18
|
+
"""Return a :class:`Manifest` for ``experiment_dir`` by scanning registered sources."""
|
|
19
|
+
|
|
20
|
+
root = Path(experiment_dir).resolve()
|
|
21
|
+
entries = _discover_entries(root)
|
|
22
|
+
if not entries:
|
|
23
|
+
raise ValueError(f"No files discovered for {root}")
|
|
24
|
+
entries.sort(key=lambda entry: (entry.tag, entry.path))
|
|
25
|
+
return Manifest(root=root, entries=entries)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _discover_entries(root: Path) -> list[ManifestEntry]:
|
|
29
|
+
logger.info("Starting file discovery", extra={"phase": "discover", "experiment_dir": str(root)})
|
|
30
|
+
|
|
31
|
+
entries: list[ManifestEntry] = []
|
|
32
|
+
for tag in sorted(SOURCE_REGISTRY.keys()):
|
|
33
|
+
logger.debug("Discovering files", extra={"phase": "discover", "tag": tag})
|
|
34
|
+
source_class = SOURCE_REGISTRY[tag]
|
|
35
|
+
|
|
36
|
+
for pattern in getattr(source_class, "patterns", ()): # type: ignore[arg-type]
|
|
37
|
+
for origin in ("processed", "data"):
|
|
38
|
+
search_root = root / origin
|
|
39
|
+
if not search_root.exists():
|
|
40
|
+
continue
|
|
41
|
+
for file_path in search_root.glob(pattern):
|
|
42
|
+
rel_path = file_path.relative_to(root)
|
|
43
|
+
subject, session, task = _parse_components(rel_path, tag)
|
|
44
|
+
entries.append(
|
|
45
|
+
ManifestEntry(
|
|
46
|
+
tag=tag,
|
|
47
|
+
path=rel_path.as_posix(),
|
|
48
|
+
origin=origin,
|
|
49
|
+
subject=subject,
|
|
50
|
+
session=session,
|
|
51
|
+
task=task,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
logger.debug(
|
|
55
|
+
"Found file",
|
|
56
|
+
extra={
|
|
57
|
+
"phase": "discover",
|
|
58
|
+
"tag": tag,
|
|
59
|
+
"path": rel_path.as_posix(),
|
|
60
|
+
"origin": origin,
|
|
61
|
+
"task": task,
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
logger.info("Discovery complete", extra={"phase": "discover", "total_files": len(entries)})
|
|
66
|
+
return entries
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _parse_components(relative_path: Path, tag: str) -> tuple[str, str, str | None]:
|
|
70
|
+
captures: dict[str, str] = {}
|
|
71
|
+
normalized_path = relative_path.as_posix()
|
|
72
|
+
for label, value in _BIDS_COMPONENT_PATTERN.findall(normalized_path):
|
|
73
|
+
captures.setdefault(label.lower(), value)
|
|
74
|
+
|
|
75
|
+
parts = list(relative_path.parts)
|
|
76
|
+
if parts and parts[0] in {"data", "processed"}:
|
|
77
|
+
parts = parts[1:]
|
|
78
|
+
|
|
79
|
+
subject = captures.get("sub")
|
|
80
|
+
if subject is None and parts:
|
|
81
|
+
candidate = parts[0]
|
|
82
|
+
if candidate.startswith("sub-"):
|
|
83
|
+
_, _, remainder = candidate.partition("-")
|
|
84
|
+
subject = remainder or None
|
|
85
|
+
else:
|
|
86
|
+
subject = candidate
|
|
87
|
+
|
|
88
|
+
session_value = captures.get("ses")
|
|
89
|
+
session_token: str | None = None
|
|
90
|
+
if session_value is not None:
|
|
91
|
+
session_token = f"ses-{session_value}"
|
|
92
|
+
else:
|
|
93
|
+
for component in parts:
|
|
94
|
+
if component.startswith("ses-"):
|
|
95
|
+
session_token = component
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
task_value = captures.get("task")
|
|
99
|
+
task_token: str | None = None
|
|
100
|
+
if task_value is not None:
|
|
101
|
+
task_token = f"task-{task_value}"
|
|
102
|
+
else:
|
|
103
|
+
match = re.search(r"task-([A-Za-z0-9]+)", normalized_path)
|
|
104
|
+
if match:
|
|
105
|
+
task_token = f"task-{match.group(1)}"
|
|
106
|
+
else:
|
|
107
|
+
task_token = "task-session"
|
|
108
|
+
|
|
109
|
+
missing: list[str] = []
|
|
110
|
+
if not subject:
|
|
111
|
+
missing.append("subject")
|
|
112
|
+
if not session_token:
|
|
113
|
+
missing.append("session")
|
|
114
|
+
if missing:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"Missing {', '.join(missing)} token(s) in path '{normalized_path}' for tag '{tag}'"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
assert subject is not None
|
|
120
|
+
assert session_token is not None
|
|
121
|
+
return subject, session_token, task_token
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
__all__ = ["discover_manifest"]
|