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/inspection.py ADDED
@@ -0,0 +1,492 @@
1
+ """Inspect all recognized files in a two-photon recording session.
2
+
3
+ Inputs: a session folder that follows the twopy data layout.
4
+ Outputs: typed summaries for MATLAB, TIFF, text, CSV, ZIP, and other files.
5
+
6
+ The goal is fast orientation: users and tests can see what the recording
7
+ contains without loading the full movie or committing to an analysis method.
8
+ """
9
+
10
+ import csv
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Literal, cast
14
+ from zipfile import ZipFile
15
+
16
+ import tifffile
17
+
18
+ from twopy.matlab import MatlabFileSummary, inspect_mat_file
19
+
20
+ __all__ = [
21
+ "FileInspection",
22
+ "SessionInspection",
23
+ "TIFF_METADATA_CSV_COLUMNS",
24
+ "TiffSummary",
25
+ "inspect_recording_files",
26
+ "write_tiff_metadata_csv",
27
+ ]
28
+
29
+ FileKind = Literal["matlab", "tiff", "text", "csv", "zip", "other"]
30
+
31
+ TIFF_METADATA_CSV_COLUMNS = (
32
+ "relative_path",
33
+ "size_bytes",
34
+ "series_count",
35
+ "series_shapes",
36
+ "series_dtypes",
37
+ "page_count",
38
+ "first_page_shape",
39
+ "first_page_dtype",
40
+ "image_width",
41
+ "image_length",
42
+ "bits_per_sample",
43
+ "samples_per_pixel",
44
+ "x_resolution",
45
+ "y_resolution",
46
+ "resolution_unit",
47
+ "image_description_chars",
48
+ "scanimage_config_name",
49
+ "scanimage_software_version",
50
+ "scanimage_acq_lines_per_frame",
51
+ "scanimage_acq_pixels_per_line",
52
+ "scanimage_acq_number_of_frames",
53
+ "scanimage_acq_number_of_channels_save",
54
+ "scanimage_acq_frame_rate",
55
+ "scanimage_acq_zoom_factor",
56
+ "scanimage_acq_pixel_time",
57
+ "scanimage_acq_ms_per_line",
58
+ "scanimage_acq_z_step_size",
59
+ "scanimage_acq_scan_angle_multiplier_fast",
60
+ "scanimage_acq_scan_angle_multiplier_slow",
61
+ "scanimage_acq_scan_rotation",
62
+ "scanimage_acq_scan_shift_fast",
63
+ "scanimage_acq_scan_shift_slow",
64
+ "scanimage_acq_x_step",
65
+ "scanimage_acq_y_step",
66
+ "scanimage_motor_abs_x_position",
67
+ "scanimage_motor_abs_y_position",
68
+ "scanimage_motor_abs_z_position",
69
+ )
70
+
71
+ _SCANIMAGE_CSV_KEY_BY_COLUMN = {
72
+ "scanimage_config_name": "configName",
73
+ "scanimage_software_version": "software.version",
74
+ "scanimage_acq_lines_per_frame": "acq.linesPerFrame",
75
+ "scanimage_acq_pixels_per_line": "acq.pixelsPerLine",
76
+ "scanimage_acq_number_of_frames": "acq.numberOfFrames",
77
+ "scanimage_acq_number_of_channels_save": "acq.numberOfChannelsSave",
78
+ "scanimage_acq_frame_rate": "acq.frameRate",
79
+ "scanimage_acq_zoom_factor": "acq.zoomFactor",
80
+ "scanimage_acq_pixel_time": "acq.pixelTime",
81
+ "scanimage_acq_ms_per_line": "acq.msPerLine",
82
+ "scanimage_acq_z_step_size": "acq.zStepSize",
83
+ "scanimage_acq_scan_angle_multiplier_fast": "acq.scanAngleMultiplierFast",
84
+ "scanimage_acq_scan_angle_multiplier_slow": "acq.scanAngleMultiplierSlow",
85
+ "scanimage_acq_scan_rotation": "acq.scanRotation",
86
+ "scanimage_acq_scan_shift_fast": "acq.scanShiftFast",
87
+ "scanimage_acq_scan_shift_slow": "acq.scanShiftSlow",
88
+ "scanimage_acq_x_step": "acq.xstep",
89
+ "scanimage_acq_y_step": "acq.ystep",
90
+ "scanimage_motor_abs_x_position": "motor.absXPosition",
91
+ "scanimage_motor_abs_y_position": "motor.absYPosition",
92
+ "scanimage_motor_abs_z_position": "motor.absZPosition",
93
+ }
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class TiffSummary:
98
+ """Small metadata summary for one TIFF movie.
99
+
100
+ Inputs: TIFF file path.
101
+ Outputs: TIFF layout, tags, and parsed ScanImage metadata.
102
+
103
+ This avoids loading image pixels while still proving the movie can be opened
104
+ and inspected by Python. ``scanimage_state`` is a flat dictionary keyed by
105
+ paths such as ``acq.zoomFactor`` for easy script access.
106
+ """
107
+
108
+ series_count: int
109
+ series_shapes: tuple[tuple[int, ...], ...]
110
+ series_dtypes: tuple[str, ...]
111
+ page_count: int
112
+ first_page_shape: tuple[int, ...]
113
+ first_page_dtype: str
114
+ tags: dict[str, str]
115
+ scanimage_state: dict[str, str]
116
+
117
+
118
+ @dataclass(frozen=True)
119
+ class FileInspection:
120
+ """Inspection result for one file inside a recording.
121
+
122
+ Inputs: one file path under the session directory.
123
+ Outputs: file kind, size, and type-specific summary fields.
124
+
125
+ Optional fields stay ``None`` unless they apply to that file kind. This keeps
126
+ the object simple and avoids a hierarchy before the data model is stable.
127
+ """
128
+
129
+ relative_path: Path
130
+ kind: FileKind
131
+ size_bytes: int
132
+ matlab: MatlabFileSummary | None
133
+ tiff: TiffSummary | None
134
+ text_preview: tuple[str, ...] | None
135
+ zip_entries: tuple[str, ...] | None
136
+
137
+
138
+ @dataclass(frozen=True)
139
+ class SessionInspection:
140
+ """Inspection result for all recognized files in a recording.
141
+
142
+ Inputs: session directory.
143
+ Outputs: sorted file inspection entries.
144
+
145
+ This is the Python object that the GUI can use to show what twopy found.
146
+ """
147
+
148
+ session_dir: Path
149
+ files: tuple[FileInspection, ...]
150
+
151
+
152
+ def inspect_recording_files(
153
+ session_dir: Path,
154
+ *,
155
+ tiff_metadata_csv: Path | None = None,
156
+ ) -> SessionInspection:
157
+ """Inspect all files below one recording session directory.
158
+
159
+ Args:
160
+ session_dir: Recording directory to inspect recursively.
161
+ tiff_metadata_csv: Optional CSV path that receives one row per TIFF
162
+ movie with selected TIFF tags and ScanImage metadata.
163
+
164
+ Returns:
165
+ A sorted session inspection with one entry per file.
166
+
167
+ Raises:
168
+ FileNotFoundError: If ``session_dir`` is missing.
169
+
170
+ The function inspects metadata only: MATLAB variable summaries, TIFF series
171
+ metadata, short text previews, and ZIP entry names.
172
+ """
173
+ root = session_dir.expanduser()
174
+ if not root.is_dir():
175
+ msg = f"Missing recording directory: {root}"
176
+ raise FileNotFoundError(msg)
177
+
178
+ inspections = tuple(
179
+ _inspect_file(root, path)
180
+ for path in sorted(
181
+ candidate for candidate in root.rglob("*") if candidate.is_file()
182
+ )
183
+ )
184
+
185
+ inspection = SessionInspection(session_dir=root, files=inspections)
186
+ if tiff_metadata_csv is not None:
187
+ write_tiff_metadata_csv(inspection, tiff_metadata_csv)
188
+ return inspection
189
+
190
+
191
+ def write_tiff_metadata_csv(inspection: SessionInspection, path: Path) -> None:
192
+ """Write selected TIFF and ScanImage metadata to a CSV file.
193
+
194
+ Args:
195
+ inspection: Session inspection returned by ``inspect_recording_files``.
196
+ path: CSV file path to write.
197
+
198
+ Returns:
199
+ None. The function writes a human-readable metadata table to disk.
200
+
201
+ The CSV keeps the high-value fields stable while the full parsed ScanImage
202
+ dictionary remains available on each ``TiffSummary`` Python object.
203
+ """
204
+ output_path = path.expanduser()
205
+ output_path.parent.mkdir(parents=True, exist_ok=True)
206
+
207
+ with output_path.open("w", encoding="utf-8", newline="") as csv_file:
208
+ writer = csv.DictWriter(csv_file, fieldnames=TIFF_METADATA_CSV_COLUMNS)
209
+ writer.writeheader()
210
+ for file in inspection.files:
211
+ if file.tiff is not None:
212
+ writer.writerow(_tiff_csv_row(file))
213
+
214
+
215
+ def _inspect_file(root: Path, path: Path) -> FileInspection:
216
+ """Inspect one file using the simplest reader for its suffix.
217
+
218
+ Args:
219
+ root: Session root used to compute relative paths.
220
+ path: File to inspect.
221
+
222
+ Returns:
223
+ A file inspection with metadata for recognized file types.
224
+
225
+ The suffix switch is intentionally boring and explicit. It is easier to
226
+ audit than a registry while the project has only a handful of file types.
227
+ """
228
+ suffix = path.suffix.lower()
229
+ size_bytes = path.stat().st_size
230
+
231
+ if suffix == ".mat":
232
+ return FileInspection(
233
+ relative_path=path.relative_to(root),
234
+ kind="matlab",
235
+ size_bytes=size_bytes,
236
+ matlab=inspect_mat_file(path),
237
+ tiff=None,
238
+ text_preview=None,
239
+ zip_entries=None,
240
+ )
241
+
242
+ if suffix in {".tif", ".tiff"}:
243
+ return FileInspection(
244
+ relative_path=path.relative_to(root),
245
+ kind="tiff",
246
+ size_bytes=size_bytes,
247
+ matlab=None,
248
+ tiff=_inspect_tiff(path),
249
+ text_preview=None,
250
+ zip_entries=None,
251
+ )
252
+
253
+ if suffix == ".zip":
254
+ return FileInspection(
255
+ relative_path=path.relative_to(root),
256
+ kind="zip",
257
+ size_bytes=size_bytes,
258
+ matlab=None,
259
+ tiff=None,
260
+ text_preview=None,
261
+ zip_entries=_inspect_zip(path),
262
+ )
263
+
264
+ if suffix in {".txt", ".csv", ".batch"}:
265
+ return FileInspection(
266
+ relative_path=path.relative_to(root),
267
+ kind="csv" if suffix == ".csv" else "text",
268
+ size_bytes=size_bytes,
269
+ matlab=None,
270
+ tiff=None,
271
+ text_preview=_text_preview(path),
272
+ zip_entries=None,
273
+ )
274
+
275
+ return FileInspection(
276
+ relative_path=path.relative_to(root),
277
+ kind="other",
278
+ size_bytes=size_bytes,
279
+ matlab=None,
280
+ tiff=None,
281
+ text_preview=None,
282
+ zip_entries=None,
283
+ )
284
+
285
+
286
+ def _inspect_tiff(path: Path) -> TiffSummary:
287
+ """Inspect TIFF series metadata without reading the full movie.
288
+
289
+ Args:
290
+ path: TIFF movie path.
291
+
292
+ Returns:
293
+ Series count, shape, and dtype metadata.
294
+
295
+ tifffile exposes series metadata lazily, which is enough for an initial GUI
296
+ summary and avoids loading hundreds of megabytes into memory.
297
+ """
298
+ with tifffile.TiffFile(path) as tif:
299
+ shapes = tuple(
300
+ tuple(int(length) for length in series.shape) for series in tif.series
301
+ )
302
+ dtypes = tuple(str(series.dtype) for series in tif.series)
303
+ first_page = cast(tifffile.TiffPage, tif.pages[0])
304
+ tags = _selected_tiff_tags(first_page)
305
+ return TiffSummary(
306
+ series_count=len(tif.series),
307
+ series_shapes=shapes,
308
+ series_dtypes=dtypes,
309
+ page_count=len(tif.pages),
310
+ first_page_shape=tuple(int(length) for length in first_page.shape),
311
+ first_page_dtype=str(first_page.dtype),
312
+ tags=tags,
313
+ scanimage_state=_parse_scanimage_description(
314
+ tags.get("ImageDescription", ""),
315
+ ),
316
+ )
317
+
318
+
319
+ def _selected_tiff_tags(page: tifffile.TiffPage) -> dict[str, str]:
320
+ """Read the TIFF tags that are useful for imaging metadata.
321
+
322
+ Args:
323
+ page: First TIFF page from the movie.
324
+
325
+ Returns:
326
+ Mapping from TIFF tag name to string value.
327
+
328
+ ScanImage writes the same metadata onto each page in the observed files, so
329
+ the first page is enough and avoids iterating thousands of frames.
330
+ """
331
+ tag_names = (
332
+ "ImageWidth",
333
+ "ImageLength",
334
+ "BitsPerSample",
335
+ "SamplesPerPixel",
336
+ "XResolution",
337
+ "YResolution",
338
+ "ResolutionUnit",
339
+ "ImageDescription",
340
+ "Software",
341
+ "DateTime",
342
+ )
343
+ tags: dict[str, str] = {}
344
+ for name in tag_names:
345
+ tag = page.tags.get(name)
346
+ if tag is not None:
347
+ tags[name] = _metadata_value_to_text(cast(object, tag.value))
348
+ return tags
349
+
350
+
351
+ def _parse_scanimage_description(description: str) -> dict[str, str]:
352
+ """Parse ScanImage ``state.*`` lines from a TIFF ImageDescription.
353
+
354
+ Args:
355
+ description: Raw TIFF ``ImageDescription`` tag value.
356
+
357
+ Returns:
358
+ Flat metadata dictionary without the leading ``state.`` prefix.
359
+
360
+ ScanImage stores one assignment per line. Keeping values as strings avoids
361
+ guessing units or numeric intent too early while still making fields easy to
362
+ search and write to CSV.
363
+ """
364
+ state: dict[str, str] = {}
365
+ for raw_line in description.splitlines():
366
+ line = raw_line.strip()
367
+ if not line.startswith("state.") or "=" not in line:
368
+ continue
369
+ key, value = line.removeprefix("state.").split("=", 1)
370
+ state[key] = _clean_scanimage_value(value)
371
+ return state
372
+
373
+
374
+ def _clean_scanimage_value(value: str) -> str:
375
+ """Normalize one ScanImage assignment value for Python and CSV use.
376
+
377
+ Args:
378
+ value: Raw value text from one ``state.*`` line.
379
+
380
+ Returns:
381
+ Value with surrounding whitespace and simple MATLAB string quotes removed.
382
+ """
383
+ stripped = value.strip()
384
+ if len(stripped) >= 2 and stripped[0] == "'" and stripped[-1] == "'":
385
+ return stripped[1:-1]
386
+ return stripped
387
+
388
+
389
+ def _metadata_value_to_text(value: object) -> str:
390
+ """Convert TIFF tag values into stable text.
391
+
392
+ Args:
393
+ value: TIFF tag value returned by tifffile.
394
+
395
+ Returns:
396
+ Compact string representation for Python summaries and CSV output.
397
+ """
398
+ if isinstance(value, bytes):
399
+ return value.decode("utf-8", errors="replace")
400
+ if isinstance(value, tuple):
401
+ return " ".join(str(part) for part in value)
402
+ return str(value)
403
+
404
+
405
+ def _tiff_csv_row(file: FileInspection) -> dict[str, str]:
406
+ """Convert one TIFF file inspection into a CSV row.
407
+
408
+ Args:
409
+ file: File inspection with a non-``None`` TIFF summary.
410
+
411
+ Returns:
412
+ Dictionary keyed by ``TIFF_METADATA_CSV_COLUMNS``.
413
+ """
414
+ if file.tiff is None:
415
+ msg = "Expected TIFF summary when writing TIFF metadata CSV"
416
+ raise ValueError(msg)
417
+
418
+ tiff = file.tiff
419
+ row = {
420
+ "relative_path": str(file.relative_path),
421
+ "size_bytes": str(file.size_bytes),
422
+ "series_count": str(tiff.series_count),
423
+ "series_shapes": ";".join(
424
+ _shape_to_text(shape) for shape in tiff.series_shapes
425
+ ),
426
+ "series_dtypes": ";".join(tiff.series_dtypes),
427
+ "page_count": str(tiff.page_count),
428
+ "first_page_shape": _shape_to_text(tiff.first_page_shape),
429
+ "first_page_dtype": tiff.first_page_dtype,
430
+ "image_width": tiff.tags.get("ImageWidth", ""),
431
+ "image_length": tiff.tags.get("ImageLength", ""),
432
+ "bits_per_sample": tiff.tags.get("BitsPerSample", ""),
433
+ "samples_per_pixel": tiff.tags.get("SamplesPerPixel", ""),
434
+ "x_resolution": tiff.tags.get("XResolution", ""),
435
+ "y_resolution": tiff.tags.get("YResolution", ""),
436
+ "resolution_unit": tiff.tags.get("ResolutionUnit", ""),
437
+ "image_description_chars": str(len(tiff.tags.get("ImageDescription", ""))),
438
+ }
439
+ for column, key in _SCANIMAGE_CSV_KEY_BY_COLUMN.items():
440
+ row[column] = tiff.scanimage_state.get(key, "")
441
+ return row
442
+
443
+
444
+ def _shape_to_text(shape: tuple[int, ...]) -> str:
445
+ """Format an array shape for one CSV cell.
446
+
447
+ Args:
448
+ shape: Tuple of array dimensions.
449
+
450
+ Returns:
451
+ Shape dimensions joined by ``x``.
452
+ """
453
+ return "x".join(str(length) for length in shape)
454
+
455
+
456
+ def _text_preview(path: Path, line_count: int = 5) -> tuple[str, ...]:
457
+ """Read the first few lines of a text-like file.
458
+
459
+ Args:
460
+ path: Text file path.
461
+ line_count: Maximum number of lines to return.
462
+
463
+ Returns:
464
+ Preview lines without trailing newline characters.
465
+
466
+ A short preview is enough to recognize metadata files without treating them
467
+ as a stable parsed format too early.
468
+ """
469
+ lines: list[str] = []
470
+ with path.open("r", encoding="utf-8", errors="replace") as text_file:
471
+ for _ in range(line_count):
472
+ line = text_file.readline()
473
+ if line == "":
474
+ break
475
+ lines.append(line.rstrip("\n"))
476
+ return tuple(lines)
477
+
478
+
479
+ def _inspect_zip(path: Path) -> tuple[str, ...]:
480
+ """List files contained inside a ZIP archive.
481
+
482
+ Args:
483
+ path: ZIP archive path.
484
+
485
+ Returns:
486
+ Sorted entry names stored in the archive.
487
+
488
+ The archive is not extracted. Listing entries is enough for inspection and
489
+ avoids writing data back into the source recording folder.
490
+ """
491
+ with ZipFile(path) as archive:
492
+ return tuple(sorted(archive.namelist()))