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