TwoPy 0.1.0__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 (84) hide show
  1. twopy/__init__.py +168 -0
  2. twopy/_segments.py +53 -0
  3. twopy/analysis/__init__.py +101 -0
  4. twopy/analysis/background_subtraction.py +672 -0
  5. twopy/analysis/dff.py +524 -0
  6. twopy/analysis/epoch_mapping.py +223 -0
  7. twopy/analysis/motion.py +66 -0
  8. twopy/analysis/persistence.py +975 -0
  9. twopy/analysis/response_processing/__init__.py +55 -0
  10. twopy/analysis/response_processing/apply.py +298 -0
  11. twopy/analysis/response_processing/options.py +192 -0
  12. twopy/analysis/response_processing/qc.py +270 -0
  13. twopy/analysis/response_processing/signals.py +200 -0
  14. twopy/analysis/responses.py +345 -0
  15. twopy/analysis/trials.py +341 -0
  16. twopy/analysis/workflow.py +457 -0
  17. twopy/api.py +78 -0
  18. twopy/config.py +200 -0
  19. twopy/conversion/__init__.py +40 -0
  20. twopy/conversion/alignment_crop.py +329 -0
  21. twopy/conversion/api.py +113 -0
  22. twopy/conversion/frame_ranges.py +35 -0
  23. twopy/conversion/hdf5_writing.py +362 -0
  24. twopy/conversion/matlab_values.py +123 -0
  25. twopy/conversion/source_loading.py +403 -0
  26. twopy/conversion/stimulus_code.py +248 -0
  27. twopy/conversion/types.py +295 -0
  28. twopy/converted.py +681 -0
  29. twopy/database/__init__.py +33 -0
  30. twopy/database/cache.py +166 -0
  31. twopy/database/catalog.py +90 -0
  32. twopy/database/connection.py +27 -0
  33. twopy/database/generic.py +254 -0
  34. twopy/database/modeled.py +419 -0
  35. twopy/database/sql.py +46 -0
  36. twopy/database/types.py +116 -0
  37. twopy/inspection.py +492 -0
  38. twopy/matlab.py +398 -0
  39. twopy/napari/__init__.py +27 -0
  40. twopy/napari/__main__.py +10 -0
  41. twopy/napari/constants.py +13 -0
  42. twopy/napari/controls.py +739 -0
  43. twopy/napari/display.py +181 -0
  44. twopy/napari/display_paths.py +197 -0
  45. twopy/napari/interactive.py +310 -0
  46. twopy/napari/launcher.py +175 -0
  47. twopy/napari/layout.py +147 -0
  48. twopy/napari/loading.py +207 -0
  49. twopy/napari/movie.py +71 -0
  50. twopy/napari/paths.py +181 -0
  51. twopy/napari/plotting/__init__.py +21 -0
  52. twopy/napari/plotting/data.py +266 -0
  53. twopy/napari/plotting/docks.py +1052 -0
  54. twopy/napari/plotting/export.py +803 -0
  55. twopy/napari/plotting/export_controls.py +209 -0
  56. twopy/napari/plotting/label_visibility.py +260 -0
  57. twopy/napari/plotting/options.py +269 -0
  58. twopy/napari/plotting/panels.py +119 -0
  59. twopy/napari/plotting/widgets.py +767 -0
  60. twopy/napari/protocols.py +90 -0
  61. twopy/napari/responses.py +91 -0
  62. twopy/napari/roi.py +238 -0
  63. twopy/napari/session.py +381 -0
  64. twopy/napari/state.py +106 -0
  65. twopy/napari/text.py +26 -0
  66. twopy/napari/types.py +39 -0
  67. twopy/napari/viewer.py +405 -0
  68. twopy/parity/__init__.py +30 -0
  69. twopy/parity/dff.py +399 -0
  70. twopy/parity/roi.py +145 -0
  71. twopy/parity/saved_analysis.py +174 -0
  72. twopy/photodiode.py +375 -0
  73. twopy/photodiode_classification.py +484 -0
  74. twopy/roi.py +462 -0
  75. twopy/session.py +167 -0
  76. twopy/spatial.py +154 -0
  77. twopy/stimulus.py +233 -0
  78. twopy/synchronization.py +235 -0
  79. twopy/typing_guards.py +255 -0
  80. twopy-0.1.0.dist-info/METADATA +311 -0
  81. twopy-0.1.0.dist-info/RECORD +84 -0
  82. twopy-0.1.0.dist-info/WHEEL +5 -0
  83. twopy-0.1.0.dist-info/entry_points.txt +2 -0
  84. twopy-0.1.0.dist-info/top_level.txt +1 -0
twopy/matlab.py ADDED
@@ -0,0 +1,398 @@
1
+ """Inspect MATLAB files used by two-photon microscope recordings.
2
+
3
+ Inputs: MATLAB ``.mat`` files from a recording or prior lab analysis.
4
+ Outputs: typed summaries of variables, shapes, dtypes, and storage format.
5
+
6
+ This module is intentionally an inspection layer first. It lets twopy understand
7
+ what MATLAB files contain before later code converts selected variables into
8
+ analysis-specific Python objects.
9
+ """
10
+
11
+ from collections.abc import Iterable
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Literal, cast
15
+
16
+ import h5py
17
+ import scipy.io
18
+
19
+ __all__ = [
20
+ "MatlabHdf5Group",
21
+ "MatlabFileSummary",
22
+ "MatlabLoadedFile",
23
+ "MatlabVariableSummary",
24
+ "inspect_mat_file",
25
+ "load_mat_file",
26
+ ]
27
+
28
+ MatlabFileFormat = Literal["hdf5-mat", "scipy-mat"]
29
+ ScipyLoadAction = Literal["inspect", "load"]
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class MatlabVariableSummary:
34
+ """Human-readable summary of one MATLAB variable.
35
+
36
+ Inputs: one variable loaded from or discovered inside a MATLAB file.
37
+ Outputs: name, Python-facing type, shape, dtype, and whether it is a group.
38
+
39
+ This object keeps inspection results small and auditable; large arrays stay
40
+ on disk unless later conversion code explicitly asks to load them.
41
+ """
42
+
43
+ name: str
44
+ python_type: str
45
+ shape: tuple[int, ...] | None
46
+ dtype: str | None
47
+ is_group: bool
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class MatlabFileSummary:
52
+ """Summary of one MATLAB file.
53
+
54
+ Inputs: path to a MATLAB file.
55
+ Outputs: detected storage format plus summaries of visible variables.
56
+
57
+ The summary is designed for logs and GUI inspection, not numerical analysis.
58
+ """
59
+
60
+ path: Path
61
+ file_format: MatlabFileFormat
62
+ variables: tuple[MatlabVariableSummary, ...]
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class MatlabHdf5Group:
67
+ """Reference-like summary for an HDF5 group inside a MATLAB file.
68
+
69
+ Inputs: group name and its child names.
70
+ Outputs: a small Python object that says the group exists without loading it.
71
+
72
+ HDF5 groups can contain nested references and large data. Keeping them as a
73
+ summary avoids pretending every MATLAB value is a simple array.
74
+ """
75
+
76
+ name: str
77
+ child_names: tuple[str, ...]
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class MatlabLoadedFile:
82
+ """Loaded MATLAB variables as Python objects.
83
+
84
+ Inputs: MATLAB file path and optional variable names to load.
85
+ Outputs: a mapping from variable names to Python objects.
86
+
87
+ For older MAT files, SciPy returns Python and NumPy objects. For HDF5-backed
88
+ MAT files, datasets are loaded when requested and groups are summarized.
89
+ """
90
+
91
+ path: Path
92
+ file_format: MatlabFileFormat
93
+ variables: dict[str, object]
94
+
95
+
96
+ def inspect_mat_file(path: Path) -> MatlabFileSummary:
97
+ """Inspect one MATLAB file without loading more data than needed.
98
+
99
+ Args:
100
+ path: MATLAB ``.mat`` file to inspect.
101
+
102
+ Returns:
103
+ A summary describing the file format and visible variables.
104
+
105
+ Raises:
106
+ FileNotFoundError: If the path does not exist as a file.
107
+ ValueError: If neither h5py nor SciPy can read the file.
108
+
109
+ h5py is tried first because HDF5-backed MAT files can be inspected lazily.
110
+ Older MAT files then fall back to SciPy.
111
+ """
112
+ mat_path = path.expanduser()
113
+ if not mat_path.is_file():
114
+ msg = f"Missing MATLAB file: {mat_path}"
115
+ raise FileNotFoundError(msg)
116
+
117
+ try:
118
+ return _inspect_hdf5_mat_file(mat_path)
119
+ except OSError:
120
+ return _inspect_scipy_mat_file(mat_path)
121
+
122
+
123
+ def load_mat_file(
124
+ path: Path,
125
+ variable_names: Iterable[str] | None = None,
126
+ ) -> MatlabLoadedFile:
127
+ """Load variables from one MATLAB file into Python objects.
128
+
129
+ Args:
130
+ path: MATLAB ``.mat`` file to read.
131
+ variable_names: Optional variable names to load. ``None`` means load all
132
+ visible top-level variables.
133
+
134
+ Returns:
135
+ Loaded variables keyed by MATLAB variable name.
136
+
137
+ Raises:
138
+ FileNotFoundError: If the path does not exist as a file.
139
+ KeyError: If a requested variable is absent.
140
+ ValueError: If neither h5py nor SciPy can read the file.
141
+
142
+ Use explicit ``variable_names`` for large HDF5-backed files, such as aligned
143
+ movies, so callers control memory use.
144
+ """
145
+ mat_path = path.expanduser()
146
+ if not mat_path.is_file():
147
+ msg = f"Missing MATLAB file: {mat_path}"
148
+ raise FileNotFoundError(msg)
149
+
150
+ wanted = tuple(variable_names) if variable_names is not None else None
151
+
152
+ try:
153
+ return _load_hdf5_mat_file(mat_path, wanted)
154
+ except OSError:
155
+ return _load_scipy_mat_file(mat_path, wanted)
156
+
157
+
158
+ def _inspect_hdf5_mat_file(path: Path) -> MatlabFileSummary:
159
+ """Inspect an HDF5-backed MATLAB file with lazy dataset metadata.
160
+
161
+ Args:
162
+ path: MATLAB file that h5py can open.
163
+
164
+ Returns:
165
+ A summary of top-level HDF5 groups and datasets.
166
+
167
+ HDF5 stores array shape and dtype in metadata, so this path avoids loading
168
+ large movie arrays while still showing what is inside.
169
+ """
170
+ variables: list[MatlabVariableSummary] = []
171
+
172
+ with h5py.File(path, "r") as mat_file:
173
+ for name, item in sorted(mat_file.items()):
174
+ if isinstance(item, h5py.Dataset):
175
+ variables.append(
176
+ MatlabVariableSummary(
177
+ name=name,
178
+ python_type="h5py.Dataset",
179
+ shape=tuple(int(length) for length in item.shape),
180
+ dtype=str(item.dtype),
181
+ is_group=False,
182
+ ),
183
+ )
184
+ else:
185
+ variables.append(
186
+ MatlabVariableSummary(
187
+ name=name,
188
+ python_type=type(item).__name__,
189
+ shape=None,
190
+ dtype=None,
191
+ is_group=True,
192
+ ),
193
+ )
194
+
195
+ return MatlabFileSummary(
196
+ path=path, file_format="hdf5-mat", variables=tuple(variables)
197
+ )
198
+
199
+
200
+ def _load_hdf5_mat_file(
201
+ path: Path,
202
+ variable_names: tuple[str, ...] | None,
203
+ ) -> MatlabLoadedFile:
204
+ """Load requested top-level variables from an HDF5-backed MATLAB file.
205
+
206
+ Args:
207
+ path: MATLAB file that h5py can open.
208
+ variable_names: Variable names to load, or ``None`` for all top-level
209
+ entries.
210
+
211
+ Returns:
212
+ Loaded dataset values and group summaries.
213
+
214
+ HDF5 datasets are read with ``[()]`` because that is h5py's direct way to
215
+ materialize one dataset as a Python/NumPy object.
216
+ """
217
+ variables: dict[str, object] = {}
218
+
219
+ with h5py.File(path, "r") as mat_file:
220
+ names = variable_names if variable_names is not None else tuple(mat_file.keys())
221
+ _ensure_hdf5_names_exist(mat_file, names)
222
+
223
+ for name in names:
224
+ item = mat_file[name]
225
+ if isinstance(item, h5py.Dataset):
226
+ variables[name] = item[()]
227
+ else:
228
+ variables[name] = MatlabHdf5Group(
229
+ name=name,
230
+ child_names=tuple(sorted(str(child) for child in item)),
231
+ )
232
+
233
+ return MatlabLoadedFile(path=path, file_format="hdf5-mat", variables=variables)
234
+
235
+
236
+ def _inspect_scipy_mat_file(path: Path) -> MatlabFileSummary:
237
+ """Inspect an older MATLAB file through SciPy.
238
+
239
+ Args:
240
+ path: MATLAB file that SciPy can load.
241
+
242
+ Returns:
243
+ A summary of non-private MATLAB variables.
244
+
245
+ SciPy loads older MAT files into Python objects. We use it only for files
246
+ that h5py cannot open, which avoids accidentally loading large HDF5 arrays.
247
+ """
248
+ visible = _load_visible_scipy_variables(path, action="inspect")
249
+
250
+ variables = tuple(
251
+ _summarize_loaded_variable(name, value)
252
+ for name, value in sorted(visible.items())
253
+ )
254
+
255
+ return MatlabFileSummary(path=path, file_format="scipy-mat", variables=variables)
256
+
257
+
258
+ def _load_scipy_mat_file(
259
+ path: Path,
260
+ variable_names: tuple[str, ...] | None,
261
+ ) -> MatlabLoadedFile:
262
+ """Load variables from an older MATLAB file through SciPy.
263
+
264
+ Args:
265
+ path: MATLAB file that SciPy can read.
266
+ variable_names: Variable names to keep, or ``None`` for all visible
267
+ variables.
268
+
269
+ Returns:
270
+ Loaded variables keyed by MATLAB variable name.
271
+
272
+ SciPy loads the file into memory. That is acceptable for the small metadata
273
+ files observed so far, but large files should use HDF5-backed loading.
274
+ """
275
+ visible = _load_visible_scipy_variables(path, action="load")
276
+
277
+ if variable_names is None:
278
+ return MatlabLoadedFile(path=path, file_format="scipy-mat", variables=visible)
279
+
280
+ missing = tuple(name for name in variable_names if name not in visible)
281
+ if missing:
282
+ msg = f"Missing MATLAB variable(s) in {path}: {', '.join(missing)}"
283
+ raise KeyError(msg)
284
+
285
+ return MatlabLoadedFile(
286
+ path=path,
287
+ file_format="scipy-mat",
288
+ variables={name: visible[name] for name in variable_names},
289
+ )
290
+
291
+
292
+ def _load_visible_scipy_variables(
293
+ path: Path,
294
+ *,
295
+ action: ScipyLoadAction,
296
+ ) -> dict[str, object]:
297
+ """Load visible variables from an older MATLAB file through SciPy.
298
+
299
+ Args:
300
+ path: MATLAB file that SciPy can read.
301
+ action: Caller intent used only for the error message.
302
+
303
+ Returns:
304
+ Non-private MATLAB variables keyed by name.
305
+
306
+ SciPy adds private ``__header__``-style entries. twopy hides those because
307
+ they are file bookkeeping, not recording data.
308
+ """
309
+ try:
310
+ loaded = scipy.io.loadmat(path, struct_as_record=False, squeeze_me=False)
311
+ except (NotImplementedError, OSError, ValueError) as error:
312
+ msg = f"Could not {action} MATLAB file {path}: {error}"
313
+ raise ValueError(msg) from error
314
+
315
+ return {
316
+ str(name): cast(object, value)
317
+ for name, value in loaded.items()
318
+ if not name.startswith("__")
319
+ }
320
+
321
+
322
+ def _summarize_loaded_variable(name: str, value: object) -> MatlabVariableSummary:
323
+ """Summarize one SciPy-loaded MATLAB variable.
324
+
325
+ Args:
326
+ name: MATLAB variable name.
327
+ value: Python object returned by ``scipy.io.loadmat``.
328
+
329
+ Returns:
330
+ Shape and dtype information when available, plus the Python type name.
331
+
332
+ This function uses structural checks instead of assuming every MATLAB value
333
+ is a NumPy array. MATLAB structs, cells, and scalars can arrive differently.
334
+ """
335
+ shape = _shape_tuple(value)
336
+ dtype = _dtype_name(value)
337
+
338
+ return MatlabVariableSummary(
339
+ name=name,
340
+ python_type=type(value).__name__,
341
+ shape=shape,
342
+ dtype=dtype,
343
+ is_group=False,
344
+ )
345
+
346
+
347
+ def _shape_tuple(value: object) -> tuple[int, ...] | None:
348
+ """Return an object's shape as a tuple of ints when it has one.
349
+
350
+ Args:
351
+ value: Python object from a MATLAB loader.
352
+
353
+ Returns:
354
+ The object's shape or ``None`` when shape metadata is absent.
355
+
356
+ The conversion to plain ``int`` keeps summaries stable across NumPy scalar
357
+ integer implementations.
358
+ """
359
+ shape = getattr(value, "shape", None)
360
+ if not isinstance(shape, tuple):
361
+ return None
362
+ return tuple(int(length) for length in shape)
363
+
364
+
365
+ def _dtype_name(value: object) -> str | None:
366
+ """Return an object's dtype name when it has one.
367
+
368
+ Args:
369
+ value: Python object from a MATLAB loader.
370
+
371
+ Returns:
372
+ String dtype name or ``None`` when dtype metadata is absent.
373
+
374
+ The result is text because the inspection layer is for display and logging.
375
+ """
376
+ dtype = getattr(value, "dtype", None)
377
+ if dtype is None:
378
+ return None
379
+ return str(cast(object, dtype))
380
+
381
+
382
+ def _ensure_hdf5_names_exist(mat_file: h5py.File, names: tuple[str, ...]) -> None:
383
+ """Raise a clear error when requested HDF5 variables are absent.
384
+
385
+ Args:
386
+ mat_file: Open HDF5-backed MATLAB file.
387
+ names: Requested top-level variable names.
388
+
389
+ Returns:
390
+ None when every requested name exists.
391
+
392
+ Raises:
393
+ KeyError: If one or more names are absent.
394
+ """
395
+ missing = tuple(name for name in names if name not in mat_file)
396
+ if missing:
397
+ msg = f"Missing MATLAB variable(s) in {mat_file.filename}: {', '.join(missing)}"
398
+ raise KeyError(msg)
@@ -0,0 +1,27 @@
1
+ """Napari adapter for twopy.
2
+
3
+ Inputs: converted twopy recordings, ROI labels, and optional launch settings.
4
+ Outputs: napari viewers, image layers, Labels ROI layers, and small magicgui
5
+ controls.
6
+
7
+ The package keeps GUI code scoped by responsibility. Core conversion,
8
+ database, ROI, and analysis modules stay independent so a future napari plugin
9
+ can wrap these helpers instead of owning scientific logic.
10
+ """
11
+
12
+ from twopy.napari.controls import NapariControlDocks, add_twopy_magicgui_controls
13
+ from twopy.napari.launcher import launch_napari, main
14
+ from twopy.napari.roi import roi_label_image_from_layer, save_napari_label_rois
15
+ from twopy.napari.types import NapariRecordingView
16
+ from twopy.napari.viewer import open_recording_in_napari
17
+
18
+ __all__ = [
19
+ "NapariRecordingView",
20
+ "NapariControlDocks",
21
+ "add_twopy_magicgui_controls",
22
+ "launch_napari",
23
+ "main",
24
+ "open_recording_in_napari",
25
+ "roi_label_image_from_layer",
26
+ "save_napari_label_rois",
27
+ ]
@@ -0,0 +1,10 @@
1
+ """Command-line module entry point for ``python -m twopy.napari``.
2
+
3
+ Inputs: command-line arguments parsed by ``twopy.napari.launcher``.
4
+ Outputs: a napari app session.
5
+ """
6
+
7
+ from twopy.napari.launcher import main
8
+
9
+ if __name__ == "__main__":
10
+ main()
@@ -0,0 +1,13 @@
1
+ """Shared constants for the twopy napari adapter.
2
+
3
+ Inputs: none.
4
+ Outputs: small GUI defaults used by launcher and controls.
5
+
6
+ Keeping defaults in one file prevents launcher and widget behavior from
7
+ drifting as the napari surface grows.
8
+ """
9
+
10
+ DEFAULT_PATH_TEXT = "default"
11
+ DEFAULT_ROI_FILENAME = "rois.h5"
12
+ RECORDING_DATA_FILENAME = "recording_data.h5"
13
+ ALIGNED_MOVIE_FILENAME = "aligned_movie.h5"