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,535 @@
|
|
|
1
|
+
"""Memory / storage profiler for materialized datakit datasets.
|
|
2
|
+
|
|
3
|
+
Profiles a materialized ``pandas.DataFrame`` (or a saved ``.pkl`` / ``.h5``
|
|
4
|
+
file containing one), with first-class support for the nested object
|
|
5
|
+
payloads typical of datakit outputs:
|
|
6
|
+
|
|
7
|
+
- Object-dtype columns containing embedded ``pandas.DataFrame`` instances
|
|
8
|
+
(recursed into, per inner column).
|
|
9
|
+
- Object-dtype columns containing ``numpy.ndarray`` payloads.
|
|
10
|
+
- ``meta`` columns (or any object cell) holding nested ``dict`` payloads —
|
|
11
|
+
recursively profiled key by key.
|
|
12
|
+
- Arbitrary Python objects, sized via ``sys.getsizeof`` with cycle-safe
|
|
13
|
+
recursion into containers (``list``/``tuple``/``set``/``dict``).
|
|
14
|
+
|
|
15
|
+
The profiler leans on the pandas ``memory_usage(deep=True)`` API for
|
|
16
|
+
contiguous-dtype columns and the index, and only falls back to recursive
|
|
17
|
+
sizing for ``object`` payloads where pandas reports only the pointer cost.
|
|
18
|
+
|
|
19
|
+
Output
|
|
20
|
+
------
|
|
21
|
+
:func:`profile_materialized` returns a :class:`MaterializedMemoryReport`
|
|
22
|
+
that can render:
|
|
23
|
+
|
|
24
|
+
- ``summary()`` — concise human-readable summary string
|
|
25
|
+
- ``verbose()`` — detailed human-readable breakdown
|
|
26
|
+
- ``to_dict()`` — JSON-serialisable nested dict
|
|
27
|
+
- ``to_json(path)``— write JSON file to disk
|
|
28
|
+
|
|
29
|
+
CLI usage::
|
|
30
|
+
|
|
31
|
+
mesofield datakit profile path/to/materialized.pkl --verbose
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import json
|
|
37
|
+
import sys
|
|
38
|
+
from collections import Counter, defaultdict
|
|
39
|
+
from dataclasses import dataclass, field
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Any, Union
|
|
42
|
+
|
|
43
|
+
import numpy as np
|
|
44
|
+
import pandas as pd
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Sizing helpers
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
# Cap on recursion depth for nested container sizing to avoid pathological
|
|
52
|
+
# graphs. Most datakit payloads are shallow (df-of-df, dict-of-arrays).
|
|
53
|
+
_MAX_DEPTH = 32
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _is_missing(value: Any) -> bool:
|
|
57
|
+
if value is None:
|
|
58
|
+
return True
|
|
59
|
+
try:
|
|
60
|
+
result = pd.isna(value)
|
|
61
|
+
except Exception:
|
|
62
|
+
return False
|
|
63
|
+
if isinstance(result, (bool, np.bool_)):
|
|
64
|
+
return bool(result)
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _deep_size(value: Any, seen: set[int] | None = None, depth: int = 0) -> int:
|
|
69
|
+
"""Recursive, cycle-safe size estimate in bytes for arbitrary objects.
|
|
70
|
+
|
|
71
|
+
Uses pandas / numpy native sizing where available, falls back to
|
|
72
|
+
``sys.getsizeof`` + container recursion otherwise.
|
|
73
|
+
"""
|
|
74
|
+
if seen is None:
|
|
75
|
+
seen = set()
|
|
76
|
+
if depth > _MAX_DEPTH:
|
|
77
|
+
return int(sys.getsizeof(value))
|
|
78
|
+
obj_id = id(value)
|
|
79
|
+
if obj_id in seen:
|
|
80
|
+
return 0
|
|
81
|
+
seen.add(obj_id)
|
|
82
|
+
|
|
83
|
+
if value is None:
|
|
84
|
+
return 0
|
|
85
|
+
if isinstance(value, np.ndarray):
|
|
86
|
+
return int(value.nbytes + sys.getsizeof(value))
|
|
87
|
+
if isinstance(value, pd.DataFrame):
|
|
88
|
+
return int(value.memory_usage(index=True, deep=True).sum())
|
|
89
|
+
if isinstance(value, pd.Series):
|
|
90
|
+
return int(value.memory_usage(index=True, deep=True))
|
|
91
|
+
if isinstance(value, pd.Index):
|
|
92
|
+
return int(value.memory_usage(deep=True))
|
|
93
|
+
if isinstance(value, (str, bytes, bytearray, memoryview)):
|
|
94
|
+
return int(sys.getsizeof(value))
|
|
95
|
+
if isinstance(value, dict):
|
|
96
|
+
size = sys.getsizeof(value)
|
|
97
|
+
for k, v in value.items():
|
|
98
|
+
size += _deep_size(k, seen, depth + 1)
|
|
99
|
+
size += _deep_size(v, seen, depth + 1)
|
|
100
|
+
return int(size)
|
|
101
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
102
|
+
size = sys.getsizeof(value)
|
|
103
|
+
for item in value:
|
|
104
|
+
size += _deep_size(item, seen, depth + 1)
|
|
105
|
+
return int(size)
|
|
106
|
+
if hasattr(value, "__dict__"):
|
|
107
|
+
return int(sys.getsizeof(value) + _deep_size(vars(value), seen, depth + 1))
|
|
108
|
+
return int(sys.getsizeof(value))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _flatten_dict_sizes(
|
|
112
|
+
d: dict, prefix: str = "", out: dict[str, int] | None = None, depth: int = 0
|
|
113
|
+
) -> dict[str, int]:
|
|
114
|
+
"""Flatten a nested dict into ``{dotted.key: bytes}`` for profiling."""
|
|
115
|
+
if out is None:
|
|
116
|
+
out = {}
|
|
117
|
+
if depth > _MAX_DEPTH:
|
|
118
|
+
out[prefix or "<root>"] = _deep_size(d)
|
|
119
|
+
return out
|
|
120
|
+
for k, v in d.items():
|
|
121
|
+
key = f"{prefix}.{k}" if prefix else str(k)
|
|
122
|
+
if isinstance(v, dict):
|
|
123
|
+
_flatten_dict_sizes(v, key, out, depth + 1)
|
|
124
|
+
else:
|
|
125
|
+
out[key] = _deep_size(v)
|
|
126
|
+
return out
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# Report dataclasses
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class ColumnMemory:
|
|
136
|
+
column: str
|
|
137
|
+
source: str
|
|
138
|
+
feature: str
|
|
139
|
+
dtype: str
|
|
140
|
+
n_total: int
|
|
141
|
+
n_non_null: int
|
|
142
|
+
n_null: int
|
|
143
|
+
pandas_deep_bytes: int
|
|
144
|
+
pointer_array_bytes: int
|
|
145
|
+
object_payload_bytes: int
|
|
146
|
+
estimated_total_bytes: int
|
|
147
|
+
avg_non_null_cell_bytes: int
|
|
148
|
+
max_non_null_cell_bytes: int
|
|
149
|
+
value_type_counts: dict[str, int] = field(default_factory=dict)
|
|
150
|
+
nested_dataframe_inner_bytes: dict[str, int] = field(default_factory=dict)
|
|
151
|
+
nested_dict_key_bytes: dict[str, int] = field(default_factory=dict)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass
|
|
155
|
+
class CellMemory:
|
|
156
|
+
row: str
|
|
157
|
+
column: str
|
|
158
|
+
source: str
|
|
159
|
+
feature: str
|
|
160
|
+
value_type: str
|
|
161
|
+
estimated_bytes: int
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class MaterializedMemoryReport:
|
|
166
|
+
source_path: str | None
|
|
167
|
+
shape: tuple[int, int]
|
|
168
|
+
index_names: tuple[str, ...]
|
|
169
|
+
column_levels: tuple[str, ...]
|
|
170
|
+
index_bytes: int
|
|
171
|
+
columns_index_bytes: int
|
|
172
|
+
pandas_deep_total_bytes: int
|
|
173
|
+
estimated_total_bytes: int
|
|
174
|
+
columns: list[ColumnMemory]
|
|
175
|
+
by_source_bytes: dict[str, int]
|
|
176
|
+
by_source_feature_bytes: dict[str, int] # "source.feature" -> bytes
|
|
177
|
+
largest_cells: list[CellMemory]
|
|
178
|
+
|
|
179
|
+
# --- Rendering ---------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def _fmt_bytes(n: int) -> str:
|
|
183
|
+
n = int(n)
|
|
184
|
+
for unit in ("B", "KB", "MB", "GB", "TB"):
|
|
185
|
+
if abs(n) < 1024.0 or unit == "TB":
|
|
186
|
+
return f"{n:,.1f} {unit}" if unit != "B" else f"{n:,} B"
|
|
187
|
+
n /= 1024.0
|
|
188
|
+
return f"{n:.1f} TB"
|
|
189
|
+
|
|
190
|
+
def summary(self) -> str:
|
|
191
|
+
lines = []
|
|
192
|
+
lines.append("=" * 60)
|
|
193
|
+
lines.append("MATERIALIZED DATASET MEMORY PROFILE")
|
|
194
|
+
lines.append("=" * 60)
|
|
195
|
+
if self.source_path:
|
|
196
|
+
lines.append(f" Source: {self.source_path}")
|
|
197
|
+
lines.append(f" Shape: {self.shape[0]} rows x {self.shape[1]} columns")
|
|
198
|
+
lines.append(f" Index levels: {', '.join(self.index_names) or '-'}")
|
|
199
|
+
lines.append(f" Column levels: {', '.join(self.column_levels) or '-'}")
|
|
200
|
+
lines.append(f" Index bytes: {self._fmt_bytes(self.index_bytes)}")
|
|
201
|
+
lines.append(f" Columns bytes: {self._fmt_bytes(self.columns_index_bytes)}")
|
|
202
|
+
lines.append(
|
|
203
|
+
f" Pandas deep: {self._fmt_bytes(self.pandas_deep_total_bytes)} "
|
|
204
|
+
"(pointer-only for object dtypes)"
|
|
205
|
+
)
|
|
206
|
+
lines.append(
|
|
207
|
+
f" Estimated: {self._fmt_bytes(self.estimated_total_bytes)} "
|
|
208
|
+
"(recursive object payload sizing)"
|
|
209
|
+
)
|
|
210
|
+
lines.append("")
|
|
211
|
+
lines.append("TOP SOURCES BY ESTIMATED SIZE")
|
|
212
|
+
lines.append("-" * 40)
|
|
213
|
+
top_sources = sorted(self.by_source_bytes.items(), key=lambda kv: kv[1], reverse=True)[:10]
|
|
214
|
+
for src, b in top_sources:
|
|
215
|
+
lines.append(f" {src:<30} {self._fmt_bytes(b):>12}")
|
|
216
|
+
return "\n".join(lines)
|
|
217
|
+
|
|
218
|
+
def verbose(self, *, top_n_cells: int = 20) -> str:
|
|
219
|
+
lines = [self.summary(), ""]
|
|
220
|
+
|
|
221
|
+
lines.append("PER-COLUMN BREAKDOWN")
|
|
222
|
+
lines.append("-" * 40)
|
|
223
|
+
header = (
|
|
224
|
+
f" {'source.feature':<40} {'dtype':<12} {'non_null':>8} "
|
|
225
|
+
f"{'pandas':>12} {'estimated':>12}"
|
|
226
|
+
)
|
|
227
|
+
lines.append(header)
|
|
228
|
+
sorted_cols = sorted(self.columns, key=lambda c: c.estimated_total_bytes, reverse=True)
|
|
229
|
+
for c in sorted_cols:
|
|
230
|
+
lines.append(
|
|
231
|
+
f" {c.column:<40} {c.dtype:<12} {c.n_non_null:>8} "
|
|
232
|
+
f"{self._fmt_bytes(c.pandas_deep_bytes):>12} "
|
|
233
|
+
f"{self._fmt_bytes(c.estimated_total_bytes):>12}"
|
|
234
|
+
)
|
|
235
|
+
if c.value_type_counts:
|
|
236
|
+
types = ", ".join(f"{t}={n}" for t, n in c.value_type_counts.items())
|
|
237
|
+
lines.append(f" types: {types}")
|
|
238
|
+
if c.nested_dataframe_inner_bytes:
|
|
239
|
+
lines.append(" nested DataFrame inner columns:")
|
|
240
|
+
inner_sorted = sorted(
|
|
241
|
+
c.nested_dataframe_inner_bytes.items(), key=lambda kv: kv[1], reverse=True
|
|
242
|
+
)
|
|
243
|
+
for name, b in inner_sorted:
|
|
244
|
+
lines.append(f" {name:<32} {self._fmt_bytes(b):>12}")
|
|
245
|
+
if c.nested_dict_key_bytes:
|
|
246
|
+
lines.append(" nested dict keys (aggregated across rows):")
|
|
247
|
+
key_sorted = sorted(
|
|
248
|
+
c.nested_dict_key_bytes.items(), key=lambda kv: kv[1], reverse=True
|
|
249
|
+
)
|
|
250
|
+
for name, b in key_sorted[:20]:
|
|
251
|
+
lines.append(f" {name:<32} {self._fmt_bytes(b):>12}")
|
|
252
|
+
if len(key_sorted) > 20:
|
|
253
|
+
lines.append(f" ... and {len(key_sorted) - 20} more keys")
|
|
254
|
+
|
|
255
|
+
if self.largest_cells:
|
|
256
|
+
lines.append("")
|
|
257
|
+
lines.append(f"TOP {min(top_n_cells, len(self.largest_cells))} LARGEST CELLS")
|
|
258
|
+
lines.append("-" * 40)
|
|
259
|
+
lines.append(
|
|
260
|
+
f" {'row':<40} {'source.feature':<30} {'type':<14} {'size':>12}"
|
|
261
|
+
)
|
|
262
|
+
for cm in self.largest_cells[:top_n_cells]:
|
|
263
|
+
lines.append(
|
|
264
|
+
f" {cm.row:<40} {cm.column:<30} {cm.value_type:<14} "
|
|
265
|
+
f"{self._fmt_bytes(cm.estimated_bytes):>12}"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return "\n".join(lines)
|
|
269
|
+
|
|
270
|
+
# --- Serialisation -----------------------------------------------------
|
|
271
|
+
|
|
272
|
+
def to_dict(self) -> dict[str, Any]:
|
|
273
|
+
return {
|
|
274
|
+
"source_path": self.source_path,
|
|
275
|
+
"shape": list(self.shape),
|
|
276
|
+
"index_names": list(self.index_names),
|
|
277
|
+
"column_levels": list(self.column_levels),
|
|
278
|
+
"index_bytes": int(self.index_bytes),
|
|
279
|
+
"columns_index_bytes": int(self.columns_index_bytes),
|
|
280
|
+
"pandas_deep_total_bytes": int(self.pandas_deep_total_bytes),
|
|
281
|
+
"estimated_total_bytes": int(self.estimated_total_bytes),
|
|
282
|
+
"by_source_bytes": {k: int(v) for k, v in self.by_source_bytes.items()},
|
|
283
|
+
"by_source_feature_bytes": {
|
|
284
|
+
k: int(v) for k, v in self.by_source_feature_bytes.items()
|
|
285
|
+
},
|
|
286
|
+
"columns": [
|
|
287
|
+
{
|
|
288
|
+
"column": c.column,
|
|
289
|
+
"source": c.source,
|
|
290
|
+
"feature": c.feature,
|
|
291
|
+
"dtype": c.dtype,
|
|
292
|
+
"n_total": c.n_total,
|
|
293
|
+
"n_non_null": c.n_non_null,
|
|
294
|
+
"n_null": c.n_null,
|
|
295
|
+
"pandas_deep_bytes": c.pandas_deep_bytes,
|
|
296
|
+
"pointer_array_bytes": c.pointer_array_bytes,
|
|
297
|
+
"object_payload_bytes": c.object_payload_bytes,
|
|
298
|
+
"estimated_total_bytes": c.estimated_total_bytes,
|
|
299
|
+
"avg_non_null_cell_bytes": c.avg_non_null_cell_bytes,
|
|
300
|
+
"max_non_null_cell_bytes": c.max_non_null_cell_bytes,
|
|
301
|
+
"value_type_counts": c.value_type_counts,
|
|
302
|
+
"nested_dataframe_inner_bytes": c.nested_dataframe_inner_bytes,
|
|
303
|
+
"nested_dict_key_bytes": c.nested_dict_key_bytes,
|
|
304
|
+
}
|
|
305
|
+
for c in self.columns
|
|
306
|
+
],
|
|
307
|
+
"largest_cells": [
|
|
308
|
+
{
|
|
309
|
+
"row": cm.row,
|
|
310
|
+
"column": cm.column,
|
|
311
|
+
"source": cm.source,
|
|
312
|
+
"feature": cm.feature,
|
|
313
|
+
"value_type": cm.value_type,
|
|
314
|
+
"estimated_bytes": cm.estimated_bytes,
|
|
315
|
+
}
|
|
316
|
+
for cm in self.largest_cells
|
|
317
|
+
],
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
def to_json(self, path: Union[str, Path], *, indent: int = 2) -> Path:
|
|
321
|
+
out = Path(path).expanduser().resolve()
|
|
322
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
323
|
+
with out.open("w", encoding="utf-8") as f:
|
|
324
|
+
json.dump(self.to_dict(), f, indent=indent, default=str)
|
|
325
|
+
return out
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ---------------------------------------------------------------------------
|
|
329
|
+
# Core profiler
|
|
330
|
+
# ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _col_path(col_key: Any) -> str:
|
|
334
|
+
if isinstance(col_key, tuple):
|
|
335
|
+
return ".".join(str(x) for x in col_key)
|
|
336
|
+
return str(col_key)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _source_feature(col_key: Any) -> tuple[str, str]:
|
|
340
|
+
if isinstance(col_key, tuple):
|
|
341
|
+
src = str(col_key[0]) if len(col_key) > 0 else ""
|
|
342
|
+
feat = str(col_key[1]) if len(col_key) > 1 else ""
|
|
343
|
+
return src, feat
|
|
344
|
+
return str(col_key), ""
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _row_path(row_key: Any) -> str:
|
|
348
|
+
if isinstance(row_key, tuple):
|
|
349
|
+
return ".".join(str(x) for x in row_key)
|
|
350
|
+
return str(row_key)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def profile_materialized(
|
|
354
|
+
target: Union[pd.DataFrame, str, Path],
|
|
355
|
+
*,
|
|
356
|
+
top_n_cells: int = 20,
|
|
357
|
+
source_path: str | None = None,
|
|
358
|
+
) -> MaterializedMemoryReport:
|
|
359
|
+
"""Build a :class:`MaterializedMemoryReport` from a DataFrame or saved file.
|
|
360
|
+
|
|
361
|
+
Parameters
|
|
362
|
+
----------
|
|
363
|
+
target
|
|
364
|
+
A materialized ``pandas.DataFrame`` or a path-like pointing to a
|
|
365
|
+
``.pkl`` / ``.pickle`` file produced by datakit.
|
|
366
|
+
top_n_cells
|
|
367
|
+
How many of the largest individual object cells to keep in the
|
|
368
|
+
report.
|
|
369
|
+
source_path
|
|
370
|
+
Optional override for the path recorded in the report (useful when
|
|
371
|
+
passing an already-loaded DataFrame).
|
|
372
|
+
"""
|
|
373
|
+
if isinstance(target, (str, Path)):
|
|
374
|
+
p = Path(target).expanduser().resolve()
|
|
375
|
+
if not p.is_file():
|
|
376
|
+
raise FileNotFoundError(f"Pickle file not found: {p}")
|
|
377
|
+
df = pd.read_pickle(p)
|
|
378
|
+
resolved_source = str(p)
|
|
379
|
+
elif isinstance(target, pd.DataFrame):
|
|
380
|
+
df = target
|
|
381
|
+
resolved_source = source_path
|
|
382
|
+
else:
|
|
383
|
+
raise TypeError(
|
|
384
|
+
f"Expected pandas DataFrame or path to pickle, got {type(target).__name__}"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if not isinstance(df, pd.DataFrame):
|
|
388
|
+
raise TypeError(
|
|
389
|
+
f"Loaded object is not a DataFrame, got {type(df).__name__}"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
index_bytes = int(df.index.memory_usage(deep=True))
|
|
393
|
+
columns_index_bytes = int(df.columns.memory_usage(deep=True))
|
|
394
|
+
pandas_deep_total_bytes = int(df.memory_usage(index=True, deep=True).sum())
|
|
395
|
+
|
|
396
|
+
column_reports: list[ColumnMemory] = []
|
|
397
|
+
by_source_bytes: dict[str, int] = defaultdict(int)
|
|
398
|
+
by_source_feature_bytes: dict[str, int] = defaultdict(int)
|
|
399
|
+
all_cells: list[CellMemory] = []
|
|
400
|
+
|
|
401
|
+
n_rows = int(len(df))
|
|
402
|
+
|
|
403
|
+
for col_key in df.columns:
|
|
404
|
+
series = df[col_key]
|
|
405
|
+
path = _col_path(col_key)
|
|
406
|
+
source, feature = _source_feature(col_key)
|
|
407
|
+
non_null = int(series.notna().sum())
|
|
408
|
+
null_count = int(n_rows - non_null)
|
|
409
|
+
dtype = str(series.dtype)
|
|
410
|
+
pandas_deep_bytes = int(series.memory_usage(index=False, deep=True))
|
|
411
|
+
|
|
412
|
+
nested_df_inner: dict[str, int] = defaultdict(int)
|
|
413
|
+
nested_dict_keys: dict[str, int] = defaultdict(int)
|
|
414
|
+
type_counter: Counter[str] = Counter()
|
|
415
|
+
|
|
416
|
+
if dtype != "object":
|
|
417
|
+
pointer_array_bytes = pandas_deep_bytes
|
|
418
|
+
object_payload_bytes = 0
|
|
419
|
+
estimated_total_bytes = pandas_deep_bytes
|
|
420
|
+
avg_cell = int(pandas_deep_bytes / non_null) if non_null else 0
|
|
421
|
+
max_cell = int(series.dtype.itemsize) if hasattr(series.dtype, "itemsize") else avg_cell
|
|
422
|
+
type_counter[dtype] = non_null
|
|
423
|
+
else:
|
|
424
|
+
pointer_array_bytes = int(series.memory_usage(index=False, deep=False))
|
|
425
|
+
object_payload_bytes = 0
|
|
426
|
+
max_cell = 0
|
|
427
|
+
|
|
428
|
+
for row_key, value in series.items():
|
|
429
|
+
if _is_missing(value):
|
|
430
|
+
continue
|
|
431
|
+
|
|
432
|
+
type_name = type(value).__name__
|
|
433
|
+
type_counter[type_name] += 1
|
|
434
|
+
|
|
435
|
+
if isinstance(value, pd.DataFrame):
|
|
436
|
+
cell_bytes = int(value.memory_usage(index=True, deep=True).sum())
|
|
437
|
+
nested_df_inner["<index>"] += int(value.index.memory_usage(deep=True))
|
|
438
|
+
for inner_col in value.columns:
|
|
439
|
+
inner_series = value[inner_col]
|
|
440
|
+
nested_df_inner[str(inner_col)] += int(
|
|
441
|
+
inner_series.memory_usage(index=False, deep=True)
|
|
442
|
+
)
|
|
443
|
+
elif isinstance(value, pd.Series):
|
|
444
|
+
cell_bytes = int(value.memory_usage(index=True, deep=True))
|
|
445
|
+
elif isinstance(value, np.ndarray):
|
|
446
|
+
cell_bytes = int(value.nbytes + sys.getsizeof(value))
|
|
447
|
+
elif isinstance(value, dict):
|
|
448
|
+
cell_bytes = _deep_size(value)
|
|
449
|
+
for k, b in _flatten_dict_sizes(value).items():
|
|
450
|
+
nested_dict_keys[k] += int(b)
|
|
451
|
+
else:
|
|
452
|
+
cell_bytes = _deep_size(value)
|
|
453
|
+
|
|
454
|
+
object_payload_bytes += cell_bytes
|
|
455
|
+
if cell_bytes > max_cell:
|
|
456
|
+
max_cell = cell_bytes
|
|
457
|
+
|
|
458
|
+
all_cells.append(
|
|
459
|
+
CellMemory(
|
|
460
|
+
row=_row_path(row_key),
|
|
461
|
+
column=path,
|
|
462
|
+
source=source,
|
|
463
|
+
feature=feature,
|
|
464
|
+
value_type=type_name,
|
|
465
|
+
estimated_bytes=int(cell_bytes),
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
estimated_total_bytes = int(pointer_array_bytes + object_payload_bytes)
|
|
470
|
+
avg_cell = int(object_payload_bytes / non_null) if non_null else 0
|
|
471
|
+
|
|
472
|
+
by_source_bytes[source] += int(estimated_total_bytes)
|
|
473
|
+
by_source_feature_bytes[path] += int(estimated_total_bytes)
|
|
474
|
+
|
|
475
|
+
column_reports.append(
|
|
476
|
+
ColumnMemory(
|
|
477
|
+
column=path,
|
|
478
|
+
source=source,
|
|
479
|
+
feature=feature,
|
|
480
|
+
dtype=dtype,
|
|
481
|
+
n_total=n_rows,
|
|
482
|
+
n_non_null=non_null,
|
|
483
|
+
n_null=null_count,
|
|
484
|
+
pandas_deep_bytes=pandas_deep_bytes,
|
|
485
|
+
pointer_array_bytes=int(pointer_array_bytes),
|
|
486
|
+
object_payload_bytes=int(object_payload_bytes),
|
|
487
|
+
estimated_total_bytes=int(estimated_total_bytes),
|
|
488
|
+
avg_non_null_cell_bytes=int(avg_cell),
|
|
489
|
+
max_non_null_cell_bytes=int(max_cell),
|
|
490
|
+
value_type_counts=dict(type_counter),
|
|
491
|
+
nested_dataframe_inner_bytes=dict(nested_df_inner),
|
|
492
|
+
nested_dict_key_bytes=dict(nested_dict_keys),
|
|
493
|
+
)
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
estimated_values_total = sum(c.estimated_total_bytes for c in column_reports)
|
|
497
|
+
estimated_total_bytes = int(index_bytes + columns_index_bytes + estimated_values_total)
|
|
498
|
+
|
|
499
|
+
all_cells.sort(key=lambda cm: cm.estimated_bytes, reverse=True)
|
|
500
|
+
largest_cells = all_cells[:top_n_cells]
|
|
501
|
+
|
|
502
|
+
raw_index_names = tuple(df.index.names) if hasattr(df.index, "names") else ()
|
|
503
|
+
index_names = tuple(
|
|
504
|
+
str(n) if n is not None else f"level_{i}" for i, n in enumerate(raw_index_names)
|
|
505
|
+
)
|
|
506
|
+
if isinstance(df.columns, pd.MultiIndex):
|
|
507
|
+
column_levels = tuple(
|
|
508
|
+
str(n) if n is not None else f"level_{i}"
|
|
509
|
+
for i, n in enumerate(df.columns.names)
|
|
510
|
+
)
|
|
511
|
+
else:
|
|
512
|
+
column_levels = ("columns",)
|
|
513
|
+
|
|
514
|
+
return MaterializedMemoryReport(
|
|
515
|
+
source_path=resolved_source,
|
|
516
|
+
shape=tuple(df.shape),
|
|
517
|
+
index_names=index_names,
|
|
518
|
+
column_levels=column_levels,
|
|
519
|
+
index_bytes=index_bytes,
|
|
520
|
+
columns_index_bytes=columns_index_bytes,
|
|
521
|
+
pandas_deep_total_bytes=pandas_deep_total_bytes,
|
|
522
|
+
estimated_total_bytes=estimated_total_bytes,
|
|
523
|
+
columns=column_reports,
|
|
524
|
+
by_source_bytes=dict(by_source_bytes),
|
|
525
|
+
by_source_feature_bytes=dict(by_source_feature_bytes),
|
|
526
|
+
largest_cells=largest_cells,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
__all__ = [
|
|
531
|
+
"CellMemory",
|
|
532
|
+
"ColumnMemory",
|
|
533
|
+
"MaterializedMemoryReport",
|
|
534
|
+
"profile_materialized",
|
|
535
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Interactive shell helper for datakit.
|
|
2
|
+
|
|
3
|
+
Opens an embedded IPython (falling back to the stdlib ``code`` REPL) with a
|
|
4
|
+
datakit object pre-loaded into the namespace. Shared by the
|
|
5
|
+
``mesofield datakit shell`` CLI command and ``python -m mesofield.datakit``.
|
|
6
|
+
|
|
7
|
+
Depending on the ``target`` the shell is seeded with:
|
|
8
|
+
|
|
9
|
+
- a directory -> ``dataset`` (a :class:`~mesofield.datakit.Dataset`) plus
|
|
10
|
+
its ``inventory`` and a ``report`` from :func:`~mesofield.datakit.explore`
|
|
11
|
+
- a ``.pkl`` / ``.h5`` file -> ``df`` (the materialized DataFrame loaded via
|
|
12
|
+
:func:`~mesofield.datakit.load_dataset`) plus its ``report``
|
|
13
|
+
- nothing -> just the datakit package bound as ``datakit``
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Optional, Union
|
|
20
|
+
|
|
21
|
+
PathLike = Union[str, Path]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_namespace(
|
|
25
|
+
target: Optional[PathLike] = None,
|
|
26
|
+
*,
|
|
27
|
+
hdf_key: str = "dataset",
|
|
28
|
+
) -> tuple[dict[str, Any], str]:
|
|
29
|
+
"""Build the ``(namespace, header)`` pair for an interactive datakit shell."""
|
|
30
|
+
import mesofield.datakit as datakit
|
|
31
|
+
from .core import Dataset, load_dataset
|
|
32
|
+
from .explore import explore
|
|
33
|
+
|
|
34
|
+
namespace: dict[str, Any] = {"datakit": datakit, "explore": explore}
|
|
35
|
+
|
|
36
|
+
if target is None:
|
|
37
|
+
header = "Embedded datakit shell. Available: datakit, explore"
|
|
38
|
+
return namespace, header
|
|
39
|
+
|
|
40
|
+
p = Path(target).expanduser().resolve()
|
|
41
|
+
if p.is_dir():
|
|
42
|
+
dataset = Dataset.from_directory(p)
|
|
43
|
+
report = explore(dataset, print_output=False)
|
|
44
|
+
namespace.update(dataset=dataset, inventory=dataset.inventory, report=report)
|
|
45
|
+
header = (
|
|
46
|
+
f"Embedded datakit shell for {p}\n"
|
|
47
|
+
"Available: dataset, inventory, report, datakit, explore"
|
|
48
|
+
)
|
|
49
|
+
elif p.is_file():
|
|
50
|
+
df = load_dataset(p, hdf_key=hdf_key)
|
|
51
|
+
report = explore(df, print_output=False)
|
|
52
|
+
namespace.update(df=df, report=report)
|
|
53
|
+
header = (
|
|
54
|
+
f"Embedded datakit shell for {p}\n"
|
|
55
|
+
f"Loaded materialized dataset as 'df' "
|
|
56
|
+
f"({df.shape[0]} rows x {df.shape[1]} cols)\n"
|
|
57
|
+
"Available: df, report, datakit, explore"
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
raise FileNotFoundError(f"Path does not exist: {p}")
|
|
61
|
+
|
|
62
|
+
return namespace, header
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def open_shell(
|
|
66
|
+
target: Optional[PathLike] = None,
|
|
67
|
+
*,
|
|
68
|
+
hdf_key: str = "dataset",
|
|
69
|
+
) -> int:
|
|
70
|
+
"""Open an interactive shell pre-loaded with a datakit object.
|
|
71
|
+
|
|
72
|
+
Returns a process exit code (``0`` on success).
|
|
73
|
+
"""
|
|
74
|
+
namespace, header = build_namespace(target, hdf_key=hdf_key)
|
|
75
|
+
try:
|
|
76
|
+
from IPython import embed
|
|
77
|
+
|
|
78
|
+
embed(header=header, user_ns=namespace)
|
|
79
|
+
except ImportError:
|
|
80
|
+
from code import interact
|
|
81
|
+
|
|
82
|
+
interact(header, local=namespace)
|
|
83
|
+
return 0
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Explicit registry of datakit data sources."""
|
|
2
|
+
|
|
3
|
+
from .camera.mesoscope import MesoMetadataSource
|
|
4
|
+
from .camera.pupil import PupilMetadataSource
|
|
5
|
+
from .camera.suite2p import Suite2pV2
|
|
6
|
+
from .behavior.treadmill import TreadmillSource
|
|
7
|
+
from .behavior.dataqueue import DataqueueSource
|
|
8
|
+
from .behavior.wheel import WheelEncoder
|
|
9
|
+
from .behavior.psychopy import Psychopy
|
|
10
|
+
from .analysis.mesoscope import MesoMeanSource, MesoDFFSource
|
|
11
|
+
from .analysis.mesomap import MesoMapSource
|
|
12
|
+
from .analysis.pupil import PupilDLCSource
|
|
13
|
+
from .session.config import SessionConfigSource
|
|
14
|
+
from .session.notes import SessionNotesSource
|
|
15
|
+
from .session.timestamps import SessionTimestampsSource
|
|
16
|
+
from .register import DataSource
|
|
17
|
+
|
|
18
|
+
SOURCE_REGISTRY: dict[str, type[DataSource]] = {
|
|
19
|
+
"meso_metadata": MesoMetadataSource,
|
|
20
|
+
"pupil_metadata": PupilMetadataSource,
|
|
21
|
+
"suite2p": Suite2pV2,
|
|
22
|
+
"treadmill": TreadmillSource,
|
|
23
|
+
"dataqueue": DataqueueSource,
|
|
24
|
+
"wheel": WheelEncoder,
|
|
25
|
+
"psychopy": Psychopy,
|
|
26
|
+
"meso_mean": MesoMeanSource,
|
|
27
|
+
"meso_dff": MesoDFFSource,
|
|
28
|
+
"mesomap": MesoMapSource,
|
|
29
|
+
"pupil_dlc": PupilDLCSource,
|
|
30
|
+
"session_config": SessionConfigSource,
|
|
31
|
+
"notes": SessionNotesSource,
|
|
32
|
+
"timestamps": SessionTimestampsSource,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_source_class(tag: str) -> type[DataSource]:
|
|
37
|
+
"""Return the source class for a tag."""
|
|
38
|
+
if tag not in SOURCE_REGISTRY:
|
|
39
|
+
raise KeyError(f"No source registered for tag '{tag}'")
|
|
40
|
+
return SOURCE_REGISTRY[tag]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def available_tags() -> tuple[str, ...]:
|
|
44
|
+
"""Return registered source tags in sorted order."""
|
|
45
|
+
return tuple(sorted(SOURCE_REGISTRY.keys()))
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"MesoMetadataSource",
|
|
49
|
+
"PupilMetadataSource",
|
|
50
|
+
"Suite2pV2",
|
|
51
|
+
"TreadmillSource",
|
|
52
|
+
"DataqueueSource",
|
|
53
|
+
"WheelEncoder",
|
|
54
|
+
"Psychopy",
|
|
55
|
+
"MesoMeanSource",
|
|
56
|
+
"MesoDFFSource",
|
|
57
|
+
"MesoMapSource",
|
|
58
|
+
"PupilDLCSource",
|
|
59
|
+
"SessionConfigSource",
|
|
60
|
+
"SessionNotesSource",
|
|
61
|
+
"SessionTimestampsSource",
|
|
62
|
+
"SOURCE_REGISTRY",
|
|
63
|
+
"get_source_class",
|
|
64
|
+
"available_tags",
|
|
65
|
+
]
|