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
twopy/napari/session.py
ADDED
|
@@ -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
|