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,209 @@
1
+ """Qt controls that trigger response-plot exports.
2
+
3
+ Inputs: a callable that returns the current response export state.
4
+ Outputs: one Export tab with buttons for recording images, ROI overlays, and
5
+ response plots.
6
+
7
+ This module owns export button wiring only. It keeps the response dock focused
8
+ on live plot state while the pure export functions write the actual figures.
9
+ """
10
+
11
+ from collections.abc import Callable
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+
15
+ from qtpy.QtWidgets import QLabel, QPushButton, QVBoxLayout, QWidget
16
+
17
+ from twopy.converted import RecordingData
18
+ from twopy.napari.display_paths import format_output_folder
19
+ from twopy.napari.plotting.data import ResponsePlotData
20
+ from twopy.napari.plotting.export import (
21
+ export_epoch_plots,
22
+ export_epoch_roi_overlay_plots,
23
+ export_recording_roi_overlay,
24
+ export_recording_view,
25
+ export_roi_view,
26
+ )
27
+ from twopy.napari.text import counted_noun
28
+
29
+ __all__ = ["ResponseExportState", "create_response_export_tab"]
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class ResponseExportState:
34
+ """Current napari and response-plot state needed for export actions.
35
+
36
+ Inputs: viewer, loaded recording, ROI layer, plot data, visible selections,
37
+ axis bounds, and output folder.
38
+ Outputs: immutable state object consumed by export buttons.
39
+ """
40
+
41
+ viewer: object | None
42
+ recording: RecordingData | None
43
+ roi_labels_layer: object | None
44
+ plot_data: ResponsePlotData | None
45
+ output_dir: Path
46
+ roi_label_values: tuple[int, ...]
47
+ roi_colors: tuple[str, ...]
48
+ epoch_indices: tuple[int, ...]
49
+ roi_indices: tuple[int, ...]
50
+ show_sem: bool
51
+ time_bounds: tuple[float, float]
52
+ value_bounds: tuple[float, float]
53
+
54
+
55
+ def create_response_export_tab(
56
+ get_state: Callable[[], ResponseExportState],
57
+ ) -> QWidget:
58
+ """Create the response export tab.
59
+
60
+ Args:
61
+ get_state: Callback returning current response and napari state.
62
+
63
+ Returns:
64
+ Qt widget with export buttons and status text.
65
+ """
66
+ status_label = QLabel("Exports save beside the recording.")
67
+ status_label.setWordWrap(True)
68
+ tab = QWidget()
69
+ layout = QVBoxLayout()
70
+ for text, callback in (
71
+ ("Save recording view", _save_recording_view),
72
+ ("Save ROI view", _save_roi_view),
73
+ ("Save recording ROI overlay", _save_recording_roi_overlay),
74
+ ("Save plots", _save_plots),
75
+ ("Save plots with ROIs", _save_plots_with_rois),
76
+ ):
77
+ layout.addWidget(_button(text, get_state, status_label, callback))
78
+ layout.addWidget(status_label)
79
+ layout.addStretch(1)
80
+ tab.setLayout(layout)
81
+ return tab
82
+
83
+
84
+ def _save_recording_view(state: ResponseExportState) -> tuple[Path, ...] | str:
85
+ """Export the current movie frame or mean-image fallback."""
86
+ if state.recording is None:
87
+ return "No recording loaded."
88
+ return export_recording_view(
89
+ viewer=state.viewer,
90
+ recording=state.recording,
91
+ output_dir=state.output_dir,
92
+ )
93
+
94
+
95
+ def _save_roi_view(state: ResponseExportState) -> tuple[Path, ...] | str:
96
+ """Export current ROI contours without a recording image."""
97
+ if state.recording is None:
98
+ return "No recording loaded."
99
+ return export_roi_view(
100
+ roi_labels_layer=state.roi_labels_layer,
101
+ recording=state.recording,
102
+ output_dir=state.output_dir,
103
+ roi_label_values=state.roi_label_values,
104
+ roi_colors=state.roi_colors,
105
+ )
106
+
107
+
108
+ def _save_recording_roi_overlay(
109
+ state: ResponseExportState,
110
+ ) -> tuple[Path, ...] | str:
111
+ """Export current recording image with ROI contours overlaid."""
112
+ if state.recording is None:
113
+ return "No recording loaded."
114
+ return export_recording_roi_overlay(
115
+ viewer=state.viewer,
116
+ recording=state.recording,
117
+ roi_labels_layer=state.roi_labels_layer,
118
+ output_dir=state.output_dir,
119
+ roi_label_values=state.roi_label_values,
120
+ roi_colors=state.roi_colors,
121
+ )
122
+
123
+
124
+ def _save_plots(state: ResponseExportState) -> tuple[Path, ...] | str:
125
+ """Export each visible epoch response plot."""
126
+ if state.plot_data is None:
127
+ return "No responses available."
128
+ return export_epoch_plots(
129
+ plot_data=state.plot_data,
130
+ output_dir=state.output_dir,
131
+ epoch_indices=state.epoch_indices,
132
+ roi_indices=state.roi_indices,
133
+ roi_colors=state.roi_colors,
134
+ show_sem=state.show_sem,
135
+ time_bounds=state.time_bounds,
136
+ value_bounds=state.value_bounds,
137
+ )
138
+
139
+
140
+ def _save_plots_with_rois(state: ResponseExportState) -> tuple[Path, ...] | str:
141
+ """Export two-column ROI overlay plus response plot figures."""
142
+ if state.recording is None:
143
+ return "No recording loaded."
144
+ if state.plot_data is None:
145
+ return "No responses available."
146
+ return export_epoch_roi_overlay_plots(
147
+ viewer=state.viewer,
148
+ recording=state.recording,
149
+ roi_labels_layer=state.roi_labels_layer,
150
+ plot_data=state.plot_data,
151
+ output_dir=state.output_dir,
152
+ epoch_indices=state.epoch_indices,
153
+ roi_indices=state.roi_indices,
154
+ roi_label_values=state.roi_label_values,
155
+ roi_colors=state.roi_colors,
156
+ show_sem=state.show_sem,
157
+ time_bounds=state.time_bounds,
158
+ value_bounds=state.value_bounds,
159
+ )
160
+
161
+
162
+ def _button(
163
+ text: str,
164
+ get_state: Callable[[], ResponseExportState],
165
+ status_label: QLabel,
166
+ callback: Callable[[ResponseExportState], tuple[Path, ...] | str],
167
+ ) -> QPushButton:
168
+ """Create one export button.
169
+
170
+ Args:
171
+ text: Button label.
172
+ get_state: Callback returning current export state.
173
+ status_label: Label updated after the button runs.
174
+ callback: Export action for this button.
175
+
176
+ Returns:
177
+ Configured Qt button.
178
+ """
179
+ button = QPushButton(text)
180
+
181
+ def run_export() -> None:
182
+ """Run the export action and show a short status."""
183
+ state = get_state()
184
+ result = callback(state)
185
+ status_label.setText(_export_status(result, state.recording))
186
+
187
+ button.clicked.connect(run_export)
188
+ return button
189
+
190
+
191
+ def _export_status(
192
+ result: tuple[Path, ...] | str,
193
+ recording: RecordingData | None,
194
+ ) -> str:
195
+ """Return a compact export status message.
196
+
197
+ Args:
198
+ result: Either an error/status string or written files.
199
+ recording: Optional selected recording for compact folder display.
200
+
201
+ Returns:
202
+ User-facing status string.
203
+ """
204
+ if isinstance(result, str):
205
+ return result
206
+ if len(result) == 0:
207
+ return "No files exported."
208
+ folder = format_output_folder(result[0].parent, recording)
209
+ return f"Saved {counted_noun(len(result), 'file')} to {folder}"
@@ -0,0 +1,260 @@
1
+ """Synchronize plot ROI visibility with the napari Labels overlay.
2
+
3
+ Inputs: the current napari Labels layer, ROI labels, ROI visibility state, and
4
+ ROI colors.
5
+ Outputs: a Labels-layer colormap where deselected ROIs are dimmed.
6
+
7
+ This module only changes how labels are displayed. It never edits the label
8
+ image, so hiding an ROI in the plot options cannot delete ROI data.
9
+ """
10
+
11
+ from collections.abc import Mapping
12
+ from typing import Any, Protocol, cast
13
+
14
+ from qtpy.QtGui import QColor
15
+
16
+ __all__ = [
17
+ "apply_roi_visibility_to_labels_layer",
18
+ "roi_label_value_from_label",
19
+ "roi_label_values_from_labels",
20
+ ]
21
+
22
+ _LABEL_COLOR_LOOKAHEAD = 4096
23
+ _BASE_LABEL_COLORMAP_METADATA_KEY = "twopy_base_label_colormap"
24
+ _DESELECTED_ROI_ALPHA = 0.2
25
+ type VisibilityKey = int | str
26
+ type VisibilityState = Mapping[int, bool] | Mapping[str, bool]
27
+
28
+
29
+ class _LabelsLayerWithColormap(Protocol):
30
+ """Small protocol for napari Labels layers with settable colormaps."""
31
+
32
+ colormap: object
33
+ metadata: dict[str, object]
34
+
35
+
36
+ class _LabelColormapLike(Protocol):
37
+ """Small protocol for napari label colormaps used for color sampling."""
38
+
39
+ colors: object
40
+ seed: object
41
+ background_value: object
42
+
43
+
44
+ def apply_roi_visibility_to_labels_layer(
45
+ layer: object | None,
46
+ *,
47
+ roi_labels: tuple[str, ...],
48
+ visibility: VisibilityState,
49
+ colors: tuple[QColor, ...],
50
+ keys: tuple[VisibilityKey, ...] | None = None,
51
+ ) -> None:
52
+ """Apply ROI visibility state to the napari Labels layer display.
53
+
54
+ Args:
55
+ layer: Current napari Labels layer, if available.
56
+ roi_labels: ROI names in analysis/plot order.
57
+ visibility: Visibility by ROI name, or by ``keys`` when keys are
58
+ provided.
59
+ colors: ROI colors in the same order as ``roi_labels``.
60
+ keys: Optional stable visibility keys matching ``roi_labels``.
61
+
62
+ Returns:
63
+ None.
64
+
65
+ Deselected ROIs keep their label pixels in ``layer.data``; only their
66
+ display alpha is reduced. Future labels stay drawable because the direct
67
+ colormap contains napari-style colors for a broad range of label values
68
+ instead of a single fallback color.
69
+ """
70
+ if layer is None or not hasattr(layer, "colormap"):
71
+ return
72
+
73
+ try:
74
+ from napari.utils.colormaps import direct_colormap
75
+ except ImportError:
76
+ return
77
+
78
+ labels_layer = cast(_LabelsLayerWithColormap, layer)
79
+ visibility_lookup = dict(visibility.items())
80
+ color_dict = _visible_label_color_dict(
81
+ labels_layer,
82
+ roi_labels=roi_labels,
83
+ )
84
+ for roi_index, roi_label in enumerate(roi_labels):
85
+ label_value = roi_label_value_from_label(
86
+ roi_label,
87
+ fallback=roi_index + 1,
88
+ )
89
+ key = (
90
+ keys[roi_index] if keys is not None and roi_index < len(keys) else roi_label
91
+ )
92
+ color = colors[roi_index] if roi_index < len(colors) else QColor("#4cc9f0")
93
+ color_dict[label_value] = _qcolor_rgba(color, visibility_lookup.get(key, True))
94
+
95
+ labels_layer.colormap = direct_colormap(color_dict)
96
+
97
+
98
+ def roi_label_value_from_label(label: str, *, fallback: int) -> int:
99
+ """Return the napari integer label represented by one ROI label.
100
+
101
+ Args:
102
+ label: ROI label from analysis output, usually ``roi_0001``.
103
+ fallback: One-based ROI position used when the label has no numeric
104
+ suffix.
105
+
106
+ Returns:
107
+ Positive integer label value for the napari Labels layer.
108
+
109
+ napari lets users draw label ``10`` without drawing labels ``1`` through
110
+ ``9``. ``make_roi_set_from_label_image`` preserves that value in labels like
111
+ ``roi_0010``, so visibility needs to target label ``10`` instead of the
112
+ first plotted ROI position.
113
+ """
114
+ suffix = label.rsplit("_", maxsplit=1)[-1]
115
+ if suffix.isdecimal():
116
+ return int(suffix)
117
+ return fallback
118
+
119
+
120
+ def roi_label_values_from_labels(roi_labels: tuple[str, ...]) -> tuple[int, ...]:
121
+ """Return napari integer Labels values for plot ROI labels.
122
+
123
+ Args:
124
+ roi_labels: ROI labels in plot order.
125
+
126
+ Returns:
127
+ Integer Labels-layer values in the same order.
128
+ """
129
+ return tuple(
130
+ roi_label_value_from_label(roi_label, fallback=index + 1)
131
+ for index, roi_label in enumerate(roi_labels)
132
+ )
133
+
134
+
135
+ def _qcolor_rgba(color: QColor, visible: bool) -> tuple[float, float, float, float]:
136
+ """Return a normalized RGBA tuple for one ROI label.
137
+
138
+ Args:
139
+ color: ROI color.
140
+ visible: Whether the ROI should be fully visible in the Labels overlay.
141
+
142
+ Returns:
143
+ RGBA tuple in napari's expected ``0`` to ``1`` float range.
144
+ """
145
+ return (
146
+ color.redF(),
147
+ color.greenF(),
148
+ color.blueF(),
149
+ 1.0 if visible else _DESELECTED_ROI_ALPHA,
150
+ )
151
+
152
+
153
+ def _visible_label_color_dict(
154
+ layer: _LabelsLayerWithColormap,
155
+ *,
156
+ roi_labels: tuple[str, ...],
157
+ ) -> dict[int | None, object]:
158
+ """Return visible colors for known and future Labels values.
159
+
160
+ Args:
161
+ layer: napari Labels layer.
162
+ roi_labels: ROI names in analysis/plot order.
163
+
164
+ Returns:
165
+ Mapping from label values to RGBA colors for napari's direct colormap.
166
+ """
167
+ color_count = max(
168
+ _LABEL_COLOR_LOOKAHEAD,
169
+ _max_roi_label_value(roi_labels) + 512,
170
+ )
171
+ color_dict: dict[int | None, object] = {0: "transparent"}
172
+ for label_value in range(1, color_count + 1):
173
+ color_dict[label_value] = _base_label_rgba(layer, label_value)
174
+ color_dict[None] = color_dict[color_count]
175
+ return color_dict
176
+
177
+
178
+ def _max_roi_label_value(roi_labels: tuple[str, ...]) -> int:
179
+ """Return the largest integer label represented by ROI labels.
180
+
181
+ Args:
182
+ roi_labels: ROI names in analysis/plot order.
183
+
184
+ Returns:
185
+ Maximum positive Labels value, or ``0`` when there are no ROIs.
186
+ """
187
+ if len(roi_labels) == 0:
188
+ return 0
189
+ return max(
190
+ roi_label_value_from_label(roi_label, fallback=index + 1)
191
+ for index, roi_label in enumerate(roi_labels)
192
+ )
193
+
194
+
195
+ def _base_label_rgba(
196
+ layer: _LabelsLayerWithColormap,
197
+ label_value: int,
198
+ ) -> tuple[float, float, float, float]:
199
+ """Return napari's cyclic label color for one label value.
200
+
201
+ Args:
202
+ layer: napari Labels layer.
203
+ label_value: Positive integer label value.
204
+
205
+ Returns:
206
+ RGBA tuple in ``0`` to ``1`` float range.
207
+ """
208
+ try:
209
+ from napari.utils.colormaps import label_colormap
210
+ except ImportError:
211
+ return _qcolor_rgba(QColor("#4cc9f0"), True)
212
+
213
+ params = _base_label_colormap_params(layer)
214
+ colormap = label_colormap(
215
+ num_colors=params["num_colors"],
216
+ seed=params["seed"],
217
+ background_value=params["background_value"],
218
+ )
219
+ rgba = colormap.map(label_value)
220
+ return (
221
+ float(rgba[0]),
222
+ float(rgba[1]),
223
+ float(rgba[2]),
224
+ float(rgba[3]),
225
+ )
226
+
227
+
228
+ def _base_label_colormap_params(
229
+ layer: _LabelsLayerWithColormap,
230
+ ) -> dict[str, int | float]:
231
+ """Return stable cyclic-colormap parameters for one Labels layer.
232
+
233
+ Args:
234
+ layer: napari Labels layer.
235
+
236
+ Returns:
237
+ Plain parameters for reconstructing napari's cyclic label colormap.
238
+
239
+ The first call records the original cyclic colormap. Later calls may see a
240
+ direct visibility colormap, so reading from metadata preserves the original
241
+ color cycle while users toggle ROI visibility.
242
+ """
243
+ stored = layer.metadata.get(_BASE_LABEL_COLORMAP_METADATA_KEY)
244
+ if isinstance(stored, dict):
245
+ stored_params = cast(dict[str, object], stored)
246
+ return {
247
+ "num_colors": int(cast(Any, stored_params["num_colors"])),
248
+ "seed": float(cast(Any, stored_params["seed"])),
249
+ "background_value": int(cast(Any, stored_params["background_value"])),
250
+ }
251
+
252
+ colormap = cast(_LabelColormapLike, layer.colormap)
253
+ colors = getattr(colormap, "colors", ())
254
+ params = {
255
+ "num_colors": max(len(colors) - 1, 1),
256
+ "seed": float(getattr(colormap, "seed", 0.5)),
257
+ "background_value": int(getattr(colormap, "background_value", 0)),
258
+ }
259
+ layer.metadata[_BASE_LABEL_COLORMAP_METADATA_KEY] = params
260
+ return params