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/napari/viewer.py ADDED
@@ -0,0 +1,405 @@
1
+ """Open converted twopy recordings in napari.
2
+
3
+ Inputs: converted ``recording_data.h5`` paths, optional ROI masks, and optional
4
+ movie preview frame ranges.
5
+ Outputs: napari image/label layers plus the twopy control dock.
6
+
7
+ This module owns viewer/layer creation only. It does not query the database,
8
+ read source MATLAB/TIFF files, or perform analysis.
9
+ """
10
+
11
+ from pathlib import Path
12
+ from typing import Protocol, cast
13
+
14
+ import numpy as np
15
+ import numpy.typing as npt
16
+
17
+ from twopy.converted import ConvertedMovie, load_converted_recording
18
+ from twopy.napari.controls import add_twopy_magicgui_controls
19
+ from twopy.napari.display import (
20
+ display_image_from_movie_image,
21
+ display_metadata_for_spatial_crop,
22
+ )
23
+ from twopy.napari.layout import place_response_options_after_recording_list
24
+ from twopy.napari.movie import exclusive_stop, resolve_movie_frame_range
25
+ from twopy.napari.plotting import (
26
+ add_twopy_response_options_widget,
27
+ add_twopy_response_plot_widget,
28
+ )
29
+ from twopy.napari.protocols import NapariViewer
30
+ from twopy.napari.roi import resolve_roi_save_file, roi_label_image_for_display
31
+ from twopy.napari.types import NapariRecordingView
32
+ from twopy.roi import RoiSet
33
+ from twopy.spatial import SpatialCrop
34
+
35
+ __all__ = ["open_recording_in_napari"]
36
+
37
+
38
+ class _LabelsLayerWithBrushSize(Protocol):
39
+ """Small layer shape needed to configure ROI painting defaults."""
40
+
41
+ brush_size: int
42
+
43
+
44
+ class _ImageLayerWithContrastRange(Protocol):
45
+ """Small layer shape needed to configure contrast slider bounds."""
46
+
47
+ contrast_limits_range: tuple[float, float]
48
+
49
+
50
+ def open_recording_in_napari(
51
+ recording_data_path: Path,
52
+ *,
53
+ roi_set: RoiSet | Path | None = None,
54
+ movie_path: Path | None = None,
55
+ viewer: NapariViewer | None = None,
56
+ movie_frame_range: tuple[int, int | None] | None = None,
57
+ mean_image_layer_name: str = "mean image",
58
+ movie_layer_name: str = "aligned movie",
59
+ roi_layer_name: str = "rois",
60
+ add_roi_labels_layer: bool = True,
61
+ add_controls: bool = True,
62
+ roi_save_file: Path | None = None,
63
+ ) -> NapariRecordingView:
64
+ """Open a converted recording in napari.
65
+
66
+ Args:
67
+ recording_data_path: Path to converted ``recording_data.h5``.
68
+ roi_set: Optional ``RoiSet`` or saved ROI HDF5 path to display.
69
+ movie_path: Optional explicit ``aligned_movie.h5`` path. Folder-based
70
+ loading passes this when the movie file is beside the recording
71
+ manifest.
72
+ viewer: Optional existing napari viewer. When omitted, a new viewer is
73
+ created.
74
+ movie_frame_range: Optional ``(start, end)`` frame range to load as a
75
+ movie preview. ``end=None`` means the final movie frame. ``None``
76
+ avoids loading movie frames and shows only the mean image.
77
+ mean_image_layer_name: Napari layer name for the mean image.
78
+ movie_layer_name: Napari layer name for the optional movie preview.
79
+ roi_layer_name: Napari layer name for ROI labels.
80
+ add_roi_labels_layer: Whether to add a napari Labels layer for ROI
81
+ drawing or editing. When ``roi_set`` is omitted, the layer starts as
82
+ an empty integer image with the movie spatial shape.
83
+ add_controls: Whether to add the small magicgui twopy control panel.
84
+ roi_save_file: Default ROI HDF5 path used by the Save ROIs button.
85
+ When omitted, twopy uses ``rois.h5`` beside ``recording_data.h5``.
86
+
87
+ Returns:
88
+ ``NapariRecordingView`` with the loaded recording and created layers.
89
+
90
+ Converted movies are usually small enough for direct interactive loading.
91
+ Analysis code still uses chunked readers; this viewer path favors simple,
92
+ auditable GUI behavior.
93
+ """
94
+ recording = load_converted_recording(recording_data_path, movie_path=movie_path)
95
+ resolved_viewer = create_viewer() if viewer is None else viewer
96
+ display_crop = recording.alignment_valid_crop
97
+ layer_metadata = display_metadata_for_spatial_crop(display_crop)
98
+ mean_image = display_crop.crop_image(recording.mean_image)
99
+ mean_layer = resolved_viewer.add_image(
100
+ display_image_from_movie_image(mean_image),
101
+ name=mean_image_layer_name,
102
+ colormap="gray",
103
+ opacity=0.5,
104
+ gamma=1.3,
105
+ contrast_limits=contrast_limits_from_data_range(
106
+ mean_image,
107
+ minimum_fraction=0.10,
108
+ maximum_fraction=1.0,
109
+ ),
110
+ metadata=layer_metadata,
111
+ )
112
+ set_image_contrast_limits_range(mean_layer, data=mean_image)
113
+
114
+ movie_layer = None
115
+ if movie_frame_range is not None:
116
+ start_frame, end_frame = resolve_movie_frame_range(
117
+ start_frame=movie_frame_range[0],
118
+ end_frame=movie_frame_range[1],
119
+ frame_count=recording.movie.shape[0],
120
+ )
121
+ movie = recording.movie.read_frames(
122
+ start_frame,
123
+ exclusive_stop(end_frame),
124
+ spatial_crop=display_crop,
125
+ )
126
+ movie_layer = resolved_viewer.add_image(
127
+ display_image_from_movie_image(movie),
128
+ name=movie_layer_name,
129
+ colormap="gray",
130
+ blending="additive",
131
+ contrast_limits=sampled_movie_auto_contrast_limits(
132
+ recording.movie,
133
+ spatial_crop=display_crop,
134
+ ),
135
+ metadata=layer_metadata,
136
+ )
137
+ set_image_contrast_limits_range(movie_layer, data=movie)
138
+
139
+ roi_layer = None
140
+ if add_roi_labels_layer:
141
+ roi_layer = resolved_viewer.add_labels(
142
+ roi_label_image_for_display(
143
+ roi_set,
144
+ recording,
145
+ spatial_crop=display_crop,
146
+ ),
147
+ name=roi_layer_name,
148
+ opacity=0.5,
149
+ blending="additive",
150
+ metadata=layer_metadata,
151
+ )
152
+ set_labels_brush_size(roi_layer, brush_size=6)
153
+ controls_widget = None
154
+ controls_dock = None
155
+ loaded_recordings_widget = None
156
+ loaded_recordings_dock = None
157
+ save_rois_widget = None
158
+ save_rois_dock = None
159
+ response_plot_widget = None
160
+ response_plot_dock = None
161
+ response_options_widget = None
162
+ response_options_dock = None
163
+ if add_controls:
164
+ resolved_roi_save_file = resolve_roi_save_file(
165
+ recording_data_path=recording.path,
166
+ roi_set=roi_set,
167
+ explicit_roi_save_file=roi_save_file,
168
+ )
169
+ response_plot_widget, response_plot_dock = add_twopy_response_plot_widget(
170
+ resolved_viewer,
171
+ recording=recording,
172
+ roi_labels_layer=roi_layer,
173
+ roi_save_file=resolved_roi_save_file,
174
+ )
175
+ control_docks = add_twopy_magicgui_controls(
176
+ resolved_viewer,
177
+ roi_labels_layer=roi_layer,
178
+ roi_save_file=resolved_roi_save_file,
179
+ recording=recording,
180
+ response_plot_widget=response_plot_widget,
181
+ mean_image_layer=mean_layer,
182
+ movie_layer=movie_layer,
183
+ )
184
+ controls_widget = control_docks.load_widget
185
+ controls_dock = control_docks.load_dock_widget
186
+ loaded_recordings_widget = control_docks.loaded_recordings_widget
187
+ loaded_recordings_dock = control_docks.loaded_recordings_dock_widget
188
+ save_rois_widget = control_docks.save_rois_widget
189
+ save_rois_dock = control_docks.save_rois_dock_widget
190
+ response_options_widget, response_options_dock = (
191
+ add_twopy_response_options_widget(
192
+ resolved_viewer,
193
+ response_plot_widget,
194
+ )
195
+ )
196
+ place_response_options_after_recording_list(
197
+ resolved_viewer,
198
+ loaded_recordings_dock,
199
+ response_options_dock,
200
+ )
201
+
202
+ return NapariRecordingView(
203
+ viewer=resolved_viewer,
204
+ recording=recording,
205
+ mean_image_layer=mean_layer,
206
+ movie_layer=movie_layer,
207
+ roi_labels_layer=roi_layer,
208
+ controls_widget=controls_widget,
209
+ controls_dock_widget=controls_dock,
210
+ loaded_recordings_widget=loaded_recordings_widget,
211
+ loaded_recordings_dock_widget=loaded_recordings_dock,
212
+ save_rois_widget=save_rois_widget,
213
+ save_rois_dock_widget=save_rois_dock,
214
+ response_plot_widget=response_plot_widget,
215
+ response_plot_dock_widget=response_plot_dock,
216
+ response_options_widget=response_options_widget,
217
+ response_options_dock_widget=response_options_dock,
218
+ )
219
+
220
+
221
+ def create_viewer() -> NapariViewer:
222
+ """Create a napari viewer lazily.
223
+
224
+ Args:
225
+ None.
226
+
227
+ Returns:
228
+ A napari viewer object exposing the small viewer protocol used here.
229
+ """
230
+ import napari
231
+
232
+ return cast(NapariViewer, napari.Viewer())
233
+
234
+
235
+ def set_labels_brush_size(layer: object, *, brush_size: int) -> None:
236
+ """Set the brush size on a napari Labels layer after creation.
237
+
238
+ Args:
239
+ layer: Labels layer returned by ``viewer.add_labels``.
240
+ brush_size: Brush diameter used by the ROI painting tool.
241
+
242
+ Returns:
243
+ None.
244
+
245
+ Napari exposes ``brush_size`` as a layer property, not a
246
+ ``Viewer.add_labels`` argument in all supported versions. Setting it here
247
+ keeps ROI drawing defaults explicit without making recording loading depend
248
+ on version-specific layer-constructor keywords.
249
+ """
250
+ labels_layer = cast(_LabelsLayerWithBrushSize, layer)
251
+ labels_layer.brush_size = brush_size
252
+
253
+
254
+ def set_image_contrast_limits_range(layer: object, *, data: npt.ArrayLike) -> None:
255
+ """Allow napari contrast sliders to cover the full loaded data range.
256
+
257
+ Args:
258
+ layer: Image layer returned by ``viewer.add_image``.
259
+ data: Image or movie values shown in that layer.
260
+
261
+ Returns:
262
+ None.
263
+
264
+ ``contrast_limits`` sets the initial slider handles. Napari initializes the
265
+ slider's allowed range from those handles, so we reset
266
+ ``contrast_limits_range`` afterward to keep the full data range available.
267
+ """
268
+ image_layer = cast(_ImageLayerWithContrastRange, layer)
269
+ image_layer.contrast_limits_range = contrast_limits_from_data_range(
270
+ data,
271
+ minimum_fraction=0.0,
272
+ maximum_fraction=1.0,
273
+ )
274
+
275
+
276
+ def sampled_movie_auto_contrast_limits(
277
+ movie: ConvertedMovie,
278
+ *,
279
+ spatial_crop: SpatialCrop,
280
+ sample_count: int = 10,
281
+ edge_fraction: float = 0.10,
282
+ random_seed: int = 0,
283
+ ) -> tuple[float, float]:
284
+ """Estimate movie display contrast from random interior frames.
285
+
286
+ Args:
287
+ movie: Lazy converted aligned-movie reader.
288
+ spatial_crop: Spatial crop shown in napari.
289
+ sample_count: Maximum number of frames to sample.
290
+ edge_fraction: Fraction of recording edges excluded before sampling.
291
+ random_seed: Seed used for deterministic frame sampling.
292
+
293
+ Returns:
294
+ ``(minimum, maximum)`` contrast limits for the movie layer.
295
+
296
+ The napari movie layer can be a short preview range, but display contrast
297
+ should reflect the recording rather than the first frames a user happened
298
+ to load. We sample a few frames away from recording edges, compute simple
299
+ per-frame auto contrast limits, then average those limits. The fixed seed
300
+ keeps the same recording visually stable across launches.
301
+ """
302
+ frame_indices = interior_random_frame_indices(
303
+ frame_count=movie.shape[0],
304
+ sample_count=sample_count,
305
+ edge_fraction=edge_fraction,
306
+ random_seed=random_seed,
307
+ )
308
+ limits = [
309
+ contrast_limits_from_data_range(
310
+ movie.read_frames(
311
+ int(frame_index),
312
+ int(frame_index) + 1,
313
+ spatial_crop=spatial_crop,
314
+ )[0],
315
+ minimum_fraction=0.0,
316
+ maximum_fraction=1.0,
317
+ )
318
+ for frame_index in frame_indices
319
+ ]
320
+ if len(limits) == 0:
321
+ return (0.0, 1.0)
322
+ lower = float(np.mean([limit[0] for limit in limits]))
323
+ upper = float(np.mean([limit[1] for limit in limits]))
324
+ if upper <= lower:
325
+ return contrast_limits_from_data_range(
326
+ movie.read_frames(0, 1, spatial_crop=spatial_crop)[0],
327
+ minimum_fraction=0.0,
328
+ maximum_fraction=1.0,
329
+ )
330
+ return (lower, upper)
331
+
332
+
333
+ def interior_random_frame_indices(
334
+ *,
335
+ frame_count: int,
336
+ sample_count: int,
337
+ edge_fraction: float,
338
+ random_seed: int,
339
+ ) -> npt.NDArray[np.int64]:
340
+ """Return deterministic random frame indices away from recording edges.
341
+
342
+ Args:
343
+ frame_count: Number of frames in the aligned movie.
344
+ sample_count: Maximum number of frames to return.
345
+ edge_fraction: Fraction excluded from both start and end.
346
+ random_seed: Seed used by NumPy's random generator.
347
+
348
+ Returns:
349
+ Sorted frame indices.
350
+
351
+ Very short recordings may not have enough frames to remove both edge
352
+ regions. In that case this falls back to all frames, because a stable
353
+ display estimate is better than failing the viewer load.
354
+ """
355
+ if frame_count <= 0 or sample_count <= 0:
356
+ return np.asarray((), dtype=np.int64)
357
+ edge_count = int(np.floor(frame_count * edge_fraction))
358
+ start = edge_count
359
+ stop = frame_count - edge_count
360
+ if start >= stop:
361
+ start = 0
362
+ stop = frame_count
363
+ candidates = np.arange(start, stop, dtype=np.int64)
364
+ if candidates.size <= sample_count:
365
+ return candidates
366
+ rng = np.random.default_rng(random_seed)
367
+ sampled = rng.choice(candidates, size=sample_count, replace=False)
368
+ return np.sort(sampled)
369
+
370
+
371
+ def contrast_limits_from_data_range(
372
+ data: npt.ArrayLike,
373
+ *,
374
+ minimum_fraction: float,
375
+ maximum_fraction: float,
376
+ ) -> tuple[float, float]:
377
+ """Return contrast limits at fractions of one data range.
378
+
379
+ Args:
380
+ data: Image or movie values shown in napari.
381
+ minimum_fraction: Fraction between the finite data minimum and maximum
382
+ used for the lower contrast limit.
383
+ maximum_fraction: Fraction between the finite data minimum and maximum
384
+ used for the upper contrast limit.
385
+
386
+ Returns:
387
+ ``(minimum, maximum)`` contrast limits.
388
+
389
+ This keeps the viewer display easier to inspect without changing the
390
+ underlying converted data. Fractions are range positions, not percentiles,
391
+ so the result is deterministic and cheap for already-loaded preview arrays.
392
+ """
393
+ values = np.asarray(data, dtype=np.float64)
394
+ finite_values = values[np.isfinite(values)]
395
+ if finite_values.size == 0:
396
+ return (0.0, 1.0)
397
+ minimum = float(np.min(finite_values))
398
+ maximum = float(np.max(finite_values))
399
+ if maximum <= minimum:
400
+ return (minimum, maximum)
401
+ value_range = maximum - minimum
402
+ return (
403
+ minimum + value_range * minimum_fraction,
404
+ minimum + value_range * maximum_fraction,
405
+ )
@@ -0,0 +1,30 @@
1
+ """Read-only parity helpers for comparing twopy outputs to prior analyses.
2
+
3
+ Inputs: optional lab-generated reference outputs.
4
+ Outputs: small typed objects for audit scripts and tests.
5
+
6
+ Normal twopy analysis should not import this package. Analysis starts from
7
+ converted twopy objects; parity helpers only read prior outputs to check that
8
+ the converted-object analysis matches historical results when needed.
9
+ """
10
+
11
+ from twopy.parity.dff import (
12
+ SavedAnalysisDeltaFOverFComparison,
13
+ compare_saved_analysis_delta_f_over_f,
14
+ saved_analysis_epoch_windows,
15
+ )
16
+ from twopy.parity.roi import SavedAnalysisRoiSet, roi_set_from_saved_analysis
17
+ from twopy.parity.saved_analysis import (
18
+ SavedAnalysisLastRoi,
19
+ load_saved_analysis_last_roi,
20
+ )
21
+
22
+ __all__ = [
23
+ "SavedAnalysisDeltaFOverFComparison",
24
+ "SavedAnalysisLastRoi",
25
+ "SavedAnalysisRoiSet",
26
+ "compare_saved_analysis_delta_f_over_f",
27
+ "load_saved_analysis_last_roi",
28
+ "roi_set_from_saved_analysis",
29
+ "saved_analysis_epoch_windows",
30
+ ]