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
@@ -0,0 +1,381 @@
1
+ """Loaded-recording session helpers for the twopy napari adapter.
2
+
3
+ Inputs: loaded converted recordings and the napari layers created for them.
4
+ Outputs: a small Qt list panel plus helper functions for selecting and
5
+ unloading recordings.
6
+
7
+ This module owns GUI session bookkeeping only. It does not load files, compute
8
+ responses, or save ROIs; it records which viewer layers belong to which loaded
9
+ recording so the controls can switch context cleanly.
10
+ """
11
+
12
+ from collections.abc import Callable
13
+ from dataclasses import dataclass, replace
14
+ from pathlib import Path
15
+ from typing import Protocol, cast
16
+
17
+ from qtpy.QtWidgets import (
18
+ QLabel,
19
+ QListWidget,
20
+ QPushButton,
21
+ QVBoxLayout,
22
+ QWidget,
23
+ )
24
+
25
+ from twopy.converted import RecordingData
26
+ from twopy.napari.plotting import refresh_response_plot_widget
27
+ from twopy.napari.protocols import NapariViewer
28
+ from twopy.napari.types import NapariRecordingView
29
+
30
+ __all__ = [
31
+ "LoadedNapariRecording",
32
+ "LoadedRecordingsPanel",
33
+ "record_loaded_view",
34
+ "remove_loaded_recording_layers",
35
+ "render_loaded_recordings_panel",
36
+ "select_loaded_recording",
37
+ "set_loaded_recording_visibility",
38
+ "unload_loaded_recording",
39
+ "update_selected_roi_save_file",
40
+ ]
41
+
42
+
43
+ class NapariSessionState(Protocol):
44
+ """State fields required by loaded-recording session helpers."""
45
+
46
+ viewer: NapariViewer
47
+ roi_labels_layer: object | None
48
+ roi_save_file: Path
49
+ recording: RecordingData | None
50
+ response_plot_widget: object | None
51
+ loaded_recordings: list["LoadedNapariRecording"]
52
+ selected_recording_index: int | None
53
+ loaded_recordings_panel: "LoadedRecordingsPanel | None"
54
+
55
+
56
+ class _LayerWithVisibility(Protocol):
57
+ """Small protocol for napari layers whose visibility can be toggled."""
58
+
59
+ visible: bool
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class LoadedNapariRecording:
64
+ """One recording loaded into a shared napari viewer.
65
+
66
+ Inputs: converted recording object plus the layers created for that
67
+ recording.
68
+ Outputs: one auditable record that links a path in the list panel to the
69
+ napari layers and ROI save default that belong to it.
70
+ """
71
+
72
+ display_path: Path
73
+ recording: RecordingData
74
+ roi_save_file: Path
75
+ mean_image_layer: object
76
+ movie_layer: object | None
77
+ roi_labels_layer: object | None
78
+
79
+ def layers(self) -> tuple[object, ...]:
80
+ """Return all napari layers owned by this loaded recording.
81
+
82
+ Args:
83
+ None.
84
+
85
+ Returns:
86
+ Tuple containing the mean image, optional movie, and optional ROI
87
+ Labels layer.
88
+ """
89
+ layers: list[object] = [self.mean_image_layer]
90
+ if self.movie_layer is not None:
91
+ layers.append(self.movie_layer)
92
+ if self.roi_labels_layer is not None:
93
+ layers.append(self.roi_labels_layer)
94
+ return tuple(layers)
95
+
96
+
97
+ class LoadedRecordingsPanel(QWidget):
98
+ """Qt panel listing recordings loaded in the current napari session."""
99
+
100
+ def __init__(
101
+ self,
102
+ *,
103
+ on_select: Callable[[int | None], None],
104
+ on_unload: Callable[[int], None],
105
+ ) -> None:
106
+ """Create an initially empty loaded-recordings panel.
107
+
108
+ Args:
109
+ on_select: Callback receiving the selected row, or ``None`` when
110
+ no recording is selected.
111
+ on_unload: Callback receiving the row to unload.
112
+ """
113
+ super().__init__()
114
+ self._on_select = on_select
115
+ self._on_unload = on_unload
116
+ self._list = QListWidget()
117
+ self._list.currentRowChanged.connect(self._select_row)
118
+ unload_button = QPushButton("Unload Recording")
119
+ unload_button.clicked.connect(self._unload_selected)
120
+
121
+ layout = QVBoxLayout()
122
+ layout.addWidget(QLabel("Loaded Recordings"))
123
+ layout.addWidget(self._list)
124
+ layout.addWidget(unload_button)
125
+ self.setLayout(layout)
126
+
127
+ def set_recordings(
128
+ self,
129
+ recordings: tuple[LoadedNapariRecording, ...],
130
+ *,
131
+ selected_index: int | None,
132
+ ) -> None:
133
+ """Replace the list contents without changing analysis state.
134
+
135
+ Args:
136
+ recordings: Loaded recordings to show.
137
+ selected_index: Row that should be visually selected.
138
+
139
+ Returns:
140
+ None.
141
+ """
142
+ self._list.blockSignals(True)
143
+ try:
144
+ self._list.clear()
145
+ for recording in recordings:
146
+ self._list.addItem(str(recording.display_path))
147
+ if selected_index is None:
148
+ self._list.setCurrentRow(-1)
149
+ else:
150
+ self._list.setCurrentRow(selected_index)
151
+ finally:
152
+ self._list.blockSignals(False)
153
+
154
+ def selected_index(self) -> int | None:
155
+ """Return the current selected row.
156
+
157
+ Args:
158
+ None.
159
+
160
+ Returns:
161
+ Selected row, or ``None`` when no row is selected.
162
+ """
163
+ row = self._list.currentRow()
164
+ return row if row >= 0 else None
165
+
166
+ def _select_row(self, row: int) -> None:
167
+ """Forward list selection changes to the control state."""
168
+ self._on_select(row if row >= 0 else None)
169
+
170
+ def _unload_selected(self) -> None:
171
+ """Ask the controls to unload the currently selected recording."""
172
+ selected = self.selected_index()
173
+ if selected is not None:
174
+ self._on_unload(selected)
175
+
176
+
177
+ def remove_loaded_recording_layers(
178
+ viewer: object,
179
+ loaded_recording: LoadedNapariRecording,
180
+ ) -> None:
181
+ """Remove all viewer layers owned by one loaded recording.
182
+
183
+ Args:
184
+ viewer: napari viewer that owns the layers.
185
+ loaded_recording: Loaded recording whose layers should be removed.
186
+
187
+ Returns:
188
+ None.
189
+
190
+ Real napari exposes a ``viewer.layers`` collection. Tests use small fake
191
+ image/label lists, so the helper supports both shapes without moving layer
192
+ ownership into the tests.
193
+ """
194
+ for layer in loaded_recording.layers():
195
+ _remove_layer(viewer, layer)
196
+
197
+
198
+ def record_loaded_view(
199
+ state: NapariSessionState,
200
+ *,
201
+ view: NapariRecordingView,
202
+ roi_save_file: Path,
203
+ replace_selected: bool,
204
+ ) -> None:
205
+ """Register a loaded recording view and make it the active context.
206
+
207
+ Args:
208
+ state: Mutable napari session state.
209
+ view: Viewer/layer objects returned by ``open_recording_in_napari``.
210
+ roi_save_file: Default ROI file for this recording.
211
+ replace_selected: Whether to replace the selected loaded recording
212
+ instead of appending a new list entry.
213
+
214
+ Returns:
215
+ None.
216
+ """
217
+ loaded = LoadedNapariRecording(
218
+ display_path=view.recording.path.parent,
219
+ recording=view.recording,
220
+ roi_save_file=roi_save_file,
221
+ mean_image_layer=view.mean_image_layer,
222
+ movie_layer=view.movie_layer,
223
+ roi_labels_layer=view.roi_labels_layer,
224
+ )
225
+ if replace_selected and state.selected_recording_index is not None:
226
+ index = state.selected_recording_index
227
+ remove_loaded_recording_layers(state.viewer, state.loaded_recordings[index])
228
+ state.loaded_recordings[index] = loaded
229
+ else:
230
+ state.loaded_recordings.append(loaded)
231
+ index = len(state.loaded_recordings) - 1
232
+ select_loaded_recording(state, index)
233
+
234
+
235
+ def select_loaded_recording(
236
+ state: NapariSessionState,
237
+ index: int | None,
238
+ ) -> None:
239
+ """Make one loaded recording the active save/plot context.
240
+
241
+ Args:
242
+ state: Mutable napari session state.
243
+ index: Loaded-recording list index, or ``None`` to clear selection.
244
+
245
+ Returns:
246
+ None.
247
+ """
248
+ if index is None or index < 0 or index >= len(state.loaded_recordings):
249
+ state.selected_recording_index = None
250
+ state.recording = None
251
+ state.roi_labels_layer = None
252
+ set_loaded_recording_visibility(state.loaded_recordings, selected_index=None)
253
+ refresh_response_plot_widget(
254
+ state.response_plot_widget,
255
+ recording=None,
256
+ roi_labels_layer=None,
257
+ roi_save_file=None,
258
+ )
259
+ render_loaded_recordings_panel(state)
260
+ return
261
+
262
+ selected = state.loaded_recordings[index]
263
+ state.selected_recording_index = index
264
+ state.recording = selected.recording
265
+ state.roi_labels_layer = selected.roi_labels_layer
266
+ state.roi_save_file = selected.roi_save_file
267
+ set_loaded_recording_visibility(state.loaded_recordings, selected_index=index)
268
+ refresh_response_plot_widget(
269
+ state.response_plot_widget,
270
+ recording=selected.recording,
271
+ roi_labels_layer=selected.roi_labels_layer,
272
+ roi_save_file=selected.roi_save_file,
273
+ )
274
+ render_loaded_recordings_panel(state)
275
+
276
+
277
+ def unload_loaded_recording(state: NapariSessionState, index: int) -> None:
278
+ """Unload one recording and remove its layers from napari.
279
+
280
+ Args:
281
+ state: Mutable napari session state.
282
+ index: Loaded-recording list index to remove.
283
+
284
+ Returns:
285
+ None.
286
+ """
287
+ if index < 0 or index >= len(state.loaded_recordings):
288
+ return
289
+ removed = state.loaded_recordings.pop(index)
290
+ remove_loaded_recording_layers(state.viewer, removed)
291
+ if len(state.loaded_recordings) == 0:
292
+ select_loaded_recording(state, None)
293
+ return
294
+ next_index = min(index, len(state.loaded_recordings) - 1)
295
+ select_loaded_recording(state, next_index)
296
+
297
+
298
+ def render_loaded_recordings_panel(state: NapariSessionState) -> None:
299
+ """Sync the loaded-recordings Qt panel with session state.
300
+
301
+ Args:
302
+ state: Mutable napari session state.
303
+
304
+ Returns:
305
+ None.
306
+ """
307
+ if state.loaded_recordings_panel is None:
308
+ return
309
+ state.loaded_recordings_panel.set_recordings(
310
+ tuple(state.loaded_recordings),
311
+ selected_index=state.selected_recording_index,
312
+ )
313
+
314
+
315
+ def update_selected_roi_save_file(
316
+ state: NapariSessionState,
317
+ roi_save_file: Path,
318
+ ) -> None:
319
+ """Store a changed ROI save path on the selected recording.
320
+
321
+ Args:
322
+ state: Mutable napari session state.
323
+ roi_save_file: New default ROI save file.
324
+
325
+ Returns:
326
+ None.
327
+ """
328
+ index = state.selected_recording_index
329
+ if index is None or index < 0 or index >= len(state.loaded_recordings):
330
+ return
331
+ state.loaded_recordings[index] = replace(
332
+ state.loaded_recordings[index],
333
+ roi_save_file=roi_save_file,
334
+ )
335
+
336
+
337
+ def set_loaded_recording_visibility(
338
+ loaded_recordings: list[LoadedNapariRecording],
339
+ *,
340
+ selected_index: int | None,
341
+ ) -> None:
342
+ """Show selected recording layers and hide layers from other recordings.
343
+
344
+ Args:
345
+ loaded_recordings: Recordings currently loaded in the viewer.
346
+ selected_index: Recording index that should be visible, or ``None`` to
347
+ hide all loaded recording layers.
348
+
349
+ Returns:
350
+ None.
351
+ """
352
+ for index, loaded_recording in enumerate(loaded_recordings):
353
+ visible = selected_index is not None and index == selected_index
354
+ for layer in loaded_recording.layers():
355
+ if hasattr(layer, "visible"):
356
+ cast(_LayerWithVisibility, layer).visible = visible
357
+
358
+
359
+ def _remove_layer(viewer: object, layer: object) -> None:
360
+ """Remove one layer from napari or from test doubles.
361
+
362
+ Args:
363
+ viewer: Real or fake napari viewer.
364
+ layer: Layer object to remove.
365
+
366
+ Returns:
367
+ None.
368
+ """
369
+ layer_collection = getattr(viewer, "layers", None)
370
+ if layer_collection is not None:
371
+ try:
372
+ layer_collection.remove(layer)
373
+ return
374
+ except (AttributeError, TypeError, ValueError):
375
+ pass
376
+
377
+ for collection_name in ("images", "labels"):
378
+ collection = getattr(viewer, collection_name, None)
379
+ if isinstance(collection, list) and layer in collection:
380
+ collection.remove(layer)
381
+ return
twopy/napari/state.py ADDED
@@ -0,0 +1,106 @@
1
+ """Local napari UI state for twopy.
2
+
3
+ Inputs: small local JSON file in the user's home directory.
4
+ Outputs: remembered GUI defaults such as the last recording folder.
5
+
6
+ This module stores convenience state only. It is never used for analysis
7
+ decisions, and missing or corrupt state simply falls back to defaults.
8
+ """
9
+
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+
14
+ __all__ = [
15
+ "read_last_recording_folder",
16
+ "recording_folder_for_state",
17
+ "write_last_recording_folder",
18
+ ]
19
+
20
+ STATE_FILE_ENV_VAR = "TWOPY_NAPARI_STATE_FILE"
21
+
22
+
23
+ def read_last_recording_folder(*, state_file: Path | None = None) -> Path | None:
24
+ """Read the last recording folder selected in napari.
25
+
26
+ Args:
27
+ state_file: Optional state file override for tests.
28
+
29
+ Returns:
30
+ Existing folder path, or ``None`` when no usable state exists.
31
+ """
32
+ path = _state_file_path(state_file)
33
+ if not path.is_file():
34
+ return None
35
+ try:
36
+ data = json.loads(path.read_text())
37
+ except (OSError, json.JSONDecodeError):
38
+ return None
39
+ folder_text = data.get("last_recording_folder")
40
+ if not isinstance(folder_text, str):
41
+ return None
42
+ folder = Path(folder_text).expanduser()
43
+ return folder if folder.is_dir() else None
44
+
45
+
46
+ def write_last_recording_folder(
47
+ folder: Path,
48
+ *,
49
+ state_file: Path | None = None,
50
+ ) -> None:
51
+ """Remember the last recording folder selected in napari.
52
+
53
+ Args:
54
+ folder: Folder to store.
55
+ state_file: Optional state file override for tests.
56
+
57
+ Returns:
58
+ None.
59
+ """
60
+ resolved_folder = folder.expanduser().resolve()
61
+ path = _state_file_path(state_file)
62
+ try:
63
+ path.parent.mkdir(parents=True, exist_ok=True)
64
+ path.write_text(
65
+ json.dumps(
66
+ {"last_recording_folder": str(resolved_folder)},
67
+ indent=2,
68
+ sort_keys=True,
69
+ )
70
+ + "\n",
71
+ )
72
+ except OSError:
73
+ return
74
+
75
+
76
+ def recording_folder_for_state(selected_path: Path, recording_data_path: Path) -> Path:
77
+ """Choose which folder should be remembered after a successful load.
78
+
79
+ Args:
80
+ selected_path: Path selected by the user.
81
+ recording_data_path: Resolved ``recording_data.h5`` path.
82
+
83
+ Returns:
84
+ Folder path to store.
85
+ """
86
+ expanded = selected_path.expanduser()
87
+ if expanded.is_dir():
88
+ return expanded
89
+ return recording_data_path.expanduser().parent
90
+
91
+
92
+ def _state_file_path(state_file: Path | None) -> Path:
93
+ """Return the JSON state file path.
94
+
95
+ Args:
96
+ state_file: Optional explicit path.
97
+
98
+ Returns:
99
+ Path to the napari state JSON file.
100
+ """
101
+ if state_file is not None:
102
+ return state_file.expanduser()
103
+ env_path = os.environ.get(STATE_FILE_ENV_VAR)
104
+ if env_path:
105
+ return Path(env_path).expanduser()
106
+ return Path.home() / ".config" / "twopy" / "napari_state.json"
twopy/napari/text.py ADDED
@@ -0,0 +1,26 @@
1
+ """Small text-formatting helpers for napari status messages.
2
+
3
+ Inputs: counts and noun labels.
4
+ Outputs: short human-readable phrases for GUI status text.
5
+
6
+ Keeping this tiny helper in napari scope avoids repeating plural rules across
7
+ separate dock widgets.
8
+ """
9
+
10
+ __all__ = ["counted_noun"]
11
+
12
+
13
+ def counted_noun(count: int, singular: str, plural: str | None = None) -> str:
14
+ """Return a count plus the correct singular or plural noun.
15
+
16
+ Args:
17
+ count: Number being displayed.
18
+ singular: Noun to use when ``count`` is one.
19
+ plural: Optional plural noun. When omitted, ``s`` is appended to
20
+ ``singular``.
21
+
22
+ Returns:
23
+ Text such as ``"1 ROI"`` or ``"2 files"``.
24
+ """
25
+ noun = singular if count == 1 else plural or f"{singular}s"
26
+ return f"{count} {noun}"
twopy/napari/types.py ADDED
@@ -0,0 +1,39 @@
1
+ """Typed data objects returned by the twopy napari adapter.
2
+
3
+ Inputs: loaded recordings and napari layer/widget objects.
4
+ Outputs: dataclasses that let scripts inspect the GUI objects twopy created.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+
9
+ from twopy.converted import RecordingData
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class NapariRecordingView:
14
+ """Objects created when a converted recording is opened in napari.
15
+
16
+ Inputs: a converted recording path and optional ROI set.
17
+ Outputs: viewer, loaded recording, and the layer objects added to the
18
+ viewer.
19
+
20
+ Keeping these references makes script-driven napari sessions auditable:
21
+ callers can inspect which recording was loaded and where each layer came
22
+ from.
23
+ """
24
+
25
+ viewer: object
26
+ recording: RecordingData
27
+ mean_image_layer: object
28
+ movie_layer: object | None
29
+ roi_labels_layer: object | None
30
+ controls_widget: object | None
31
+ controls_dock_widget: object | None
32
+ loaded_recordings_widget: object | None
33
+ loaded_recordings_dock_widget: object | None
34
+ save_rois_widget: object | None
35
+ save_rois_dock_widget: object | None
36
+ response_plot_widget: object | None
37
+ response_plot_dock_widget: object | None
38
+ response_options_widget: object | None
39
+ response_options_dock_widget: object | None