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,269 @@
1
+ """Qt option widgets for twopy response plots.
2
+
3
+ Inputs: axis limits, ROI or epoch labels, visibility state, and optional ROI
4
+ colors.
5
+ Outputs: compact Qt controls for plot customization.
6
+
7
+ This module owns option-panel widgets only. It does not draw response traces,
8
+ load analysis files, or compute responses.
9
+ """
10
+
11
+ from collections.abc import Callable, Hashable, Mapping
12
+ from typing import cast
13
+
14
+ from qtpy.QtCore import QSignalBlocker
15
+ from qtpy.QtGui import QColor
16
+ from qtpy.QtWidgets import (
17
+ QCheckBox,
18
+ QDoubleSpinBox,
19
+ QGridLayout,
20
+ QHBoxLayout,
21
+ QLabel,
22
+ QPushButton,
23
+ QScrollArea,
24
+ QVBoxLayout,
25
+ QWidget,
26
+ )
27
+
28
+ __all__ = [
29
+ "axis_options_widget",
30
+ "visibility_options_widget",
31
+ ]
32
+
33
+ type CallableAxisBounds = Callable[..., None]
34
+ type CallableVisibility = Callable[[Hashable, bool], None]
35
+ type CallableVisibilityBatch = Callable[[dict[Hashable, bool]], None]
36
+ type VisibilityKey = int | str
37
+ type VisibilityState = Mapping[int, bool] | Mapping[str, bool]
38
+
39
+
40
+ def axis_options_widget(
41
+ *,
42
+ x_min: float,
43
+ x_max: float,
44
+ y_min: float,
45
+ y_max: float,
46
+ on_change: object,
47
+ ) -> QWidget:
48
+ """Create manual axis-bound controls.
49
+
50
+ Args:
51
+ x_min: Initial x-axis minimum.
52
+ x_max: Initial x-axis maximum.
53
+ y_min: Initial y-axis minimum.
54
+ y_max: Initial y-axis maximum.
55
+ on_change: Callback receiving named axis bounds.
56
+
57
+ Returns:
58
+ Qt widget containing four numeric inputs.
59
+ """
60
+ callback = cast(CallableAxisBounds, on_change)
61
+ widget = QWidget()
62
+ layout = QGridLayout()
63
+ layout.addWidget(QLabel("Axis limits"), 0, 0, 1, 2)
64
+ x_min_spin = _axis_spin_box(x_min)
65
+ x_max_spin = _axis_spin_box(x_max)
66
+ y_min_spin = _axis_spin_box(y_min)
67
+ y_max_spin = _axis_spin_box(y_max)
68
+ layout.addWidget(QLabel("x min"), 1, 0)
69
+ layout.addWidget(x_min_spin, 1, 1)
70
+ layout.addWidget(QLabel("x max"), 2, 0)
71
+ layout.addWidget(x_max_spin, 2, 1)
72
+ layout.addWidget(QLabel("y min"), 3, 0)
73
+ layout.addWidget(y_min_spin, 3, 1)
74
+ layout.addWidget(QLabel("y max"), 4, 0)
75
+ layout.addWidget(y_max_spin, 4, 1)
76
+
77
+ def update_bounds() -> None:
78
+ """Pass current spin-box values back to the response widget."""
79
+ callback(
80
+ x_min=x_min_spin.value(),
81
+ x_max=x_max_spin.value(),
82
+ y_min=y_min_spin.value(),
83
+ y_max=y_max_spin.value(),
84
+ )
85
+
86
+ x_min_spin.valueChanged.connect(update_bounds)
87
+ x_max_spin.valueChanged.connect(update_bounds)
88
+ y_min_spin.valueChanged.connect(update_bounds)
89
+ y_max_spin.valueChanged.connect(update_bounds)
90
+ widget.setLayout(layout)
91
+ return widget
92
+
93
+
94
+ def visibility_options_widget(
95
+ *,
96
+ title: str,
97
+ labels: tuple[str, ...],
98
+ visibility: VisibilityState,
99
+ on_change: object,
100
+ on_change_batch: object | None = None,
101
+ keys: tuple[VisibilityKey, ...] | None = None,
102
+ colors: tuple[QColor, ...] | None = None,
103
+ ) -> QWidget:
104
+ """Create select-all/select-none checkboxes for plot visibility.
105
+
106
+ Args:
107
+ title: Section label.
108
+ labels: Checkbox labels.
109
+ visibility: Current visibility by label or by ``keys`` when keys are
110
+ provided.
111
+ on_change: Callback receiving ``(label, visible)``.
112
+ on_change_batch: Optional callback receiving all labels changed by
113
+ Select all or None. This prevents one bulk click from redrawing the
114
+ plots once per checkbox.
115
+ keys: Optional stable callback keys matching ``labels``. Labels stay
116
+ human-readable; keys prevent duplicate display names from toggling
117
+ the wrong underlying item.
118
+ colors: Optional swatch colors matching ``labels`` by position.
119
+
120
+ Returns:
121
+ Qt widget containing controls.
122
+ """
123
+ callback = cast(CallableVisibility, on_change)
124
+ batch_callback = (
125
+ cast(CallableVisibilityBatch, on_change_batch)
126
+ if on_change_batch is not None
127
+ else None
128
+ )
129
+ widget = QWidget()
130
+ visibility_lookup = dict(visibility.items())
131
+ layout = QVBoxLayout()
132
+ layout.addWidget(QLabel(title))
133
+ button_row = QHBoxLayout()
134
+ all_button = QPushButton("Select all")
135
+ none_button = QPushButton("None")
136
+ button_row.addWidget(all_button)
137
+ button_row.addWidget(none_button)
138
+ layout.addLayout(button_row)
139
+
140
+ list_widget = QWidget()
141
+ list_layout = QVBoxLayout()
142
+ list_layout.setContentsMargins(0, 0, 0, 0)
143
+ checkboxes: list[QCheckBox] = []
144
+ checkbox_keys: list[VisibilityKey] = []
145
+ for index, label in enumerate(labels):
146
+ key = keys[index] if keys is not None and index < len(keys) else label
147
+ checkbox = QCheckBox(label)
148
+ checkbox.setChecked(visibility_lookup.get(key, True))
149
+ checkbox.stateChanged.connect(
150
+ lambda _state, item=key, box=checkbox: callback(item, box.isChecked())
151
+ )
152
+ checkboxes.append(checkbox)
153
+ checkbox_keys.append(key)
154
+ list_layout.addWidget(_visibility_row(checkbox, _color_at(colors, index)))
155
+ list_widget.setLayout(list_layout)
156
+
157
+ scroll = QScrollArea()
158
+ scroll.setWidgetResizable(True)
159
+ scroll.setWidget(list_widget)
160
+ scroll.setMaximumHeight(_visibility_list_height(checkboxes, visible_rows=8))
161
+ layout.addWidget(scroll)
162
+
163
+ def set_all(checked: bool) -> None:
164
+ """Set all checkboxes and visibility flags together."""
165
+ blockers = [QSignalBlocker(checkbox) for checkbox in checkboxes]
166
+ for checkbox in checkboxes:
167
+ checkbox.setChecked(checked)
168
+ del blockers
169
+ if batch_callback is not None:
170
+ batch_callback({key: checked for key in checkbox_keys})
171
+ return
172
+ for key in checkbox_keys:
173
+ callback(key, checked)
174
+
175
+ all_button.clicked.connect(lambda: set_all(True))
176
+ none_button.clicked.connect(lambda: set_all(False))
177
+ widget.setLayout(layout)
178
+ return widget
179
+
180
+
181
+ def _axis_spin_box(value: float) -> QDoubleSpinBox:
182
+ """Create one numeric axis-limit input.
183
+
184
+ Args:
185
+ value: Initial value.
186
+
187
+ Returns:
188
+ Configured spin box.
189
+ """
190
+ spin_box = QDoubleSpinBox()
191
+ spin_box.setRange(-1_000_000.0, 1_000_000.0)
192
+ spin_box.setDecimals(3)
193
+ spin_box.setSingleStep(0.1)
194
+ spin_box.setValue(value)
195
+ return spin_box
196
+
197
+
198
+ def _visibility_row(checkbox: QCheckBox, color: QColor | None) -> QWidget:
199
+ """Create one compact checkbox row, with an optional color swatch.
200
+
201
+ Args:
202
+ checkbox: Visibility checkbox.
203
+ color: Optional ROI color.
204
+
205
+ Returns:
206
+ Row widget for the visibility list.
207
+ """
208
+ row = QWidget()
209
+ layout = QHBoxLayout()
210
+ layout.setContentsMargins(0, 0, 0, 0)
211
+ if color is not None:
212
+ layout.addWidget(_color_swatch(color))
213
+ layout.addWidget(checkbox)
214
+ layout.addStretch(1)
215
+ row.setLayout(layout)
216
+ return row
217
+
218
+
219
+ def _color_swatch(color: QColor) -> QWidget:
220
+ """Create the small color square shown beside one ROI label.
221
+
222
+ Args:
223
+ color: ROI color.
224
+
225
+ Returns:
226
+ Fixed-size Qt widget styled as a color square.
227
+ """
228
+ swatch = QWidget()
229
+ swatch.setFixedSize(12, 12)
230
+ swatch.setStyleSheet(
231
+ f"background-color: {color.name()}; border: 1px solid #cfd6df;"
232
+ )
233
+ return swatch
234
+
235
+
236
+ def _color_at(colors: tuple[QColor, ...] | None, index: int) -> QColor | None:
237
+ """Return one optional swatch color.
238
+
239
+ Args:
240
+ colors: Optional colors.
241
+ index: Requested label index.
242
+
243
+ Returns:
244
+ Matching color, or ``None``.
245
+ """
246
+ if colors is None or index >= len(colors):
247
+ return None
248
+ return colors[index]
249
+
250
+
251
+ def _visibility_list_height(
252
+ checkboxes: list[QCheckBox],
253
+ *,
254
+ visible_rows: int,
255
+ ) -> int:
256
+ """Return a compact maximum height for a checkbox list.
257
+
258
+ Args:
259
+ checkboxes: Checkbox widgets in one visibility section.
260
+ visible_rows: Number of rows to show before scrolling.
261
+
262
+ Returns:
263
+ Pixel height for the scroll area.
264
+ """
265
+ if len(checkboxes) == 0:
266
+ return 0
267
+ row_height = max(checkbox.sizeHint().height() for checkbox in checkboxes)
268
+ rows = min(len(checkboxes), visible_rows)
269
+ return row_height * rows + 8
@@ -0,0 +1,119 @@
1
+ """Small Qt panel builders for response plotting widgets.
2
+
3
+ Inputs: already-created buttons, labels, layouts, and plot widgets.
4
+ Outputs: composed Qt widgets used by the napari response docks.
5
+
6
+ This module owns generic panel layout only. It does not load data, compute
7
+ responses, or export figures.
8
+ """
9
+
10
+ from qtpy.QtWidgets import (
11
+ QHBoxLayout,
12
+ QLabel,
13
+ QPushButton,
14
+ QScrollArea,
15
+ QSpinBox,
16
+ QVBoxLayout,
17
+ QWidget,
18
+ )
19
+
20
+ from twopy.napari.plotting.widgets import EpochPlotWidget
21
+
22
+ __all__ = [
23
+ "epoch_plot_panel",
24
+ "plot_size_widget",
25
+ "response_update_tab",
26
+ "scrolling_tab",
27
+ ]
28
+
29
+
30
+ def epoch_plot_panel(*, title: str, plot: EpochPlotWidget) -> QWidget:
31
+ """Create one titled plot panel for the horizontal response strip.
32
+
33
+ Args:
34
+ title: Plot title.
35
+ plot: Epoch response plot widget.
36
+
37
+ Returns:
38
+ Qt widget containing the title and plot.
39
+ """
40
+ panel = QWidget()
41
+ layout = QVBoxLayout()
42
+ layout.addWidget(QLabel(title))
43
+ layout.addWidget(plot)
44
+ panel.setLayout(layout)
45
+ return panel
46
+
47
+
48
+ def response_update_tab(
49
+ *,
50
+ reload_saved_button: QPushButton,
51
+ recompute_preview_button: QPushButton,
52
+ save_analysis_button: QPushButton,
53
+ recording_summary_label: QLabel,
54
+ analysis_output_label: QLabel,
55
+ roi_output_label: QLabel,
56
+ status_label: QLabel,
57
+ ) -> QWidget:
58
+ """Create the tab that owns response recompute/reload actions.
59
+
60
+ Args:
61
+ reload_saved_button: Button that reloads persisted response outputs.
62
+ recompute_preview_button: Button that computes responses from current
63
+ Labels ROIs.
64
+ save_analysis_button: Button that saves ROIs and persisted analysis.
65
+ recording_summary_label: Label showing root, genotype, stimulus, and
66
+ recording time for the selected recording.
67
+ analysis_output_label: Label showing the analysis output path.
68
+ roi_output_label: Label showing the ROI output path.
69
+ status_label: Label showing the latest save or reload status.
70
+
71
+ Returns:
72
+ Qt widget for the Update tab.
73
+ """
74
+ tab = QWidget()
75
+ layout = QVBoxLayout()
76
+ layout.addWidget(save_analysis_button)
77
+ layout.addWidget(recompute_preview_button)
78
+ layout.addWidget(reload_saved_button)
79
+ layout.addWidget(recording_summary_label)
80
+ layout.addWidget(analysis_output_label)
81
+ layout.addWidget(roi_output_label)
82
+ layout.addWidget(status_label)
83
+ layout.addStretch(1)
84
+ tab.setLayout(layout)
85
+ return tab
86
+
87
+
88
+ def plot_size_widget(spin_box: QSpinBox) -> QWidget:
89
+ """Create the compact plot-size control row.
90
+
91
+ Args:
92
+ spin_box: Numeric plot-size input.
93
+
94
+ Returns:
95
+ Qt widget containing the label and spin box.
96
+ """
97
+ widget = QWidget()
98
+ layout = QHBoxLayout()
99
+ layout.addWidget(QLabel("Plot size"))
100
+ layout.addWidget(spin_box)
101
+ widget.setLayout(layout)
102
+ return widget
103
+
104
+
105
+ def scrolling_tab(layout: QVBoxLayout) -> QScrollArea:
106
+ """Create a scrollable options tab around one layout.
107
+
108
+ Args:
109
+ layout: Layout that callers will populate with controls.
110
+
111
+ Returns:
112
+ Scroll area suitable for a tab body.
113
+ """
114
+ content = QWidget()
115
+ content.setLayout(layout)
116
+ scroll = QScrollArea()
117
+ scroll.setWidgetResizable(True)
118
+ scroll.setWidget(content)
119
+ return scroll