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,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"]