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.
- twopy/__init__.py +168 -0
- twopy/_segments.py +53 -0
- twopy/analysis/__init__.py +101 -0
- twopy/analysis/background_subtraction.py +672 -0
- twopy/analysis/dff.py +524 -0
- twopy/analysis/epoch_mapping.py +223 -0
- twopy/analysis/motion.py +66 -0
- twopy/analysis/persistence.py +975 -0
- twopy/analysis/response_processing/__init__.py +55 -0
- twopy/analysis/response_processing/apply.py +298 -0
- twopy/analysis/response_processing/options.py +192 -0
- twopy/analysis/response_processing/qc.py +270 -0
- twopy/analysis/response_processing/signals.py +200 -0
- twopy/analysis/responses.py +345 -0
- twopy/analysis/trials.py +341 -0
- twopy/analysis/workflow.py +457 -0
- twopy/api.py +78 -0
- twopy/config.py +200 -0
- twopy/conversion/__init__.py +40 -0
- twopy/conversion/alignment_crop.py +329 -0
- twopy/conversion/api.py +113 -0
- twopy/conversion/frame_ranges.py +35 -0
- twopy/conversion/hdf5_writing.py +362 -0
- twopy/conversion/matlab_values.py +123 -0
- twopy/conversion/source_loading.py +403 -0
- twopy/conversion/stimulus_code.py +248 -0
- twopy/conversion/types.py +295 -0
- twopy/converted.py +681 -0
- twopy/database/__init__.py +33 -0
- twopy/database/cache.py +166 -0
- twopy/database/catalog.py +90 -0
- twopy/database/connection.py +27 -0
- twopy/database/generic.py +254 -0
- twopy/database/modeled.py +419 -0
- twopy/database/sql.py +46 -0
- twopy/database/types.py +116 -0
- twopy/inspection.py +492 -0
- twopy/matlab.py +398 -0
- twopy/napari/__init__.py +27 -0
- twopy/napari/__main__.py +10 -0
- twopy/napari/constants.py +13 -0
- twopy/napari/controls.py +739 -0
- twopy/napari/display.py +181 -0
- twopy/napari/display_paths.py +197 -0
- twopy/napari/interactive.py +310 -0
- twopy/napari/launcher.py +175 -0
- twopy/napari/layout.py +147 -0
- twopy/napari/loading.py +207 -0
- twopy/napari/movie.py +71 -0
- twopy/napari/paths.py +181 -0
- twopy/napari/plotting/__init__.py +21 -0
- twopy/napari/plotting/data.py +266 -0
- twopy/napari/plotting/docks.py +1052 -0
- twopy/napari/plotting/export.py +803 -0
- twopy/napari/plotting/export_controls.py +209 -0
- twopy/napari/plotting/label_visibility.py +260 -0
- twopy/napari/plotting/options.py +269 -0
- twopy/napari/plotting/panels.py +119 -0
- twopy/napari/plotting/widgets.py +767 -0
- twopy/napari/protocols.py +90 -0
- twopy/napari/responses.py +91 -0
- twopy/napari/roi.py +238 -0
- twopy/napari/session.py +381 -0
- twopy/napari/state.py +106 -0
- twopy/napari/text.py +26 -0
- twopy/napari/types.py +39 -0
- twopy/napari/viewer.py +405 -0
- twopy/parity/__init__.py +30 -0
- twopy/parity/dff.py +399 -0
- twopy/parity/roi.py +145 -0
- twopy/parity/saved_analysis.py +174 -0
- twopy/photodiode.py +375 -0
- twopy/photodiode_classification.py +484 -0
- twopy/roi.py +462 -0
- twopy/session.py +167 -0
- twopy/spatial.py +154 -0
- twopy/stimulus.py +233 -0
- twopy/synchronization.py +235 -0
- twopy/typing_guards.py +255 -0
- twopy-0.1.0.dist-info/METADATA +311 -0
- twopy-0.1.0.dist-info/RECORD +84 -0
- twopy-0.1.0.dist-info/WHEEL +5 -0
- twopy-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|