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/viewer.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""Open converted twopy recordings in napari.
|
|
2
|
+
|
|
3
|
+
Inputs: converted ``recording_data.h5`` paths, optional ROI masks, and optional
|
|
4
|
+
movie preview frame ranges.
|
|
5
|
+
Outputs: napari image/label layers plus the twopy control dock.
|
|
6
|
+
|
|
7
|
+
This module owns viewer/layer creation only. It does not query the database,
|
|
8
|
+
read source MATLAB/TIFF files, or perform analysis.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Protocol, cast
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import numpy.typing as npt
|
|
16
|
+
|
|
17
|
+
from twopy.converted import ConvertedMovie, load_converted_recording
|
|
18
|
+
from twopy.napari.controls import add_twopy_magicgui_controls
|
|
19
|
+
from twopy.napari.display import (
|
|
20
|
+
display_image_from_movie_image,
|
|
21
|
+
display_metadata_for_spatial_crop,
|
|
22
|
+
)
|
|
23
|
+
from twopy.napari.layout import place_response_options_after_recording_list
|
|
24
|
+
from twopy.napari.movie import exclusive_stop, resolve_movie_frame_range
|
|
25
|
+
from twopy.napari.plotting import (
|
|
26
|
+
add_twopy_response_options_widget,
|
|
27
|
+
add_twopy_response_plot_widget,
|
|
28
|
+
)
|
|
29
|
+
from twopy.napari.protocols import NapariViewer
|
|
30
|
+
from twopy.napari.roi import resolve_roi_save_file, roi_label_image_for_display
|
|
31
|
+
from twopy.napari.types import NapariRecordingView
|
|
32
|
+
from twopy.roi import RoiSet
|
|
33
|
+
from twopy.spatial import SpatialCrop
|
|
34
|
+
|
|
35
|
+
__all__ = ["open_recording_in_napari"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _LabelsLayerWithBrushSize(Protocol):
|
|
39
|
+
"""Small layer shape needed to configure ROI painting defaults."""
|
|
40
|
+
|
|
41
|
+
brush_size: int
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _ImageLayerWithContrastRange(Protocol):
|
|
45
|
+
"""Small layer shape needed to configure contrast slider bounds."""
|
|
46
|
+
|
|
47
|
+
contrast_limits_range: tuple[float, float]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def open_recording_in_napari(
|
|
51
|
+
recording_data_path: Path,
|
|
52
|
+
*,
|
|
53
|
+
roi_set: RoiSet | Path | None = None,
|
|
54
|
+
movie_path: Path | None = None,
|
|
55
|
+
viewer: NapariViewer | None = None,
|
|
56
|
+
movie_frame_range: tuple[int, int | None] | None = None,
|
|
57
|
+
mean_image_layer_name: str = "mean image",
|
|
58
|
+
movie_layer_name: str = "aligned movie",
|
|
59
|
+
roi_layer_name: str = "rois",
|
|
60
|
+
add_roi_labels_layer: bool = True,
|
|
61
|
+
add_controls: bool = True,
|
|
62
|
+
roi_save_file: Path | None = None,
|
|
63
|
+
) -> NapariRecordingView:
|
|
64
|
+
"""Open a converted recording in napari.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
recording_data_path: Path to converted ``recording_data.h5``.
|
|
68
|
+
roi_set: Optional ``RoiSet`` or saved ROI HDF5 path to display.
|
|
69
|
+
movie_path: Optional explicit ``aligned_movie.h5`` path. Folder-based
|
|
70
|
+
loading passes this when the movie file is beside the recording
|
|
71
|
+
manifest.
|
|
72
|
+
viewer: Optional existing napari viewer. When omitted, a new viewer is
|
|
73
|
+
created.
|
|
74
|
+
movie_frame_range: Optional ``(start, end)`` frame range to load as a
|
|
75
|
+
movie preview. ``end=None`` means the final movie frame. ``None``
|
|
76
|
+
avoids loading movie frames and shows only the mean image.
|
|
77
|
+
mean_image_layer_name: Napari layer name for the mean image.
|
|
78
|
+
movie_layer_name: Napari layer name for the optional movie preview.
|
|
79
|
+
roi_layer_name: Napari layer name for ROI labels.
|
|
80
|
+
add_roi_labels_layer: Whether to add a napari Labels layer for ROI
|
|
81
|
+
drawing or editing. When ``roi_set`` is omitted, the layer starts as
|
|
82
|
+
an empty integer image with the movie spatial shape.
|
|
83
|
+
add_controls: Whether to add the small magicgui twopy control panel.
|
|
84
|
+
roi_save_file: Default ROI HDF5 path used by the Save ROIs button.
|
|
85
|
+
When omitted, twopy uses ``rois.h5`` beside ``recording_data.h5``.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
``NapariRecordingView`` with the loaded recording and created layers.
|
|
89
|
+
|
|
90
|
+
Converted movies are usually small enough for direct interactive loading.
|
|
91
|
+
Analysis code still uses chunked readers; this viewer path favors simple,
|
|
92
|
+
auditable GUI behavior.
|
|
93
|
+
"""
|
|
94
|
+
recording = load_converted_recording(recording_data_path, movie_path=movie_path)
|
|
95
|
+
resolved_viewer = create_viewer() if viewer is None else viewer
|
|
96
|
+
display_crop = recording.alignment_valid_crop
|
|
97
|
+
layer_metadata = display_metadata_for_spatial_crop(display_crop)
|
|
98
|
+
mean_image = display_crop.crop_image(recording.mean_image)
|
|
99
|
+
mean_layer = resolved_viewer.add_image(
|
|
100
|
+
display_image_from_movie_image(mean_image),
|
|
101
|
+
name=mean_image_layer_name,
|
|
102
|
+
colormap="gray",
|
|
103
|
+
opacity=0.5,
|
|
104
|
+
gamma=1.3,
|
|
105
|
+
contrast_limits=contrast_limits_from_data_range(
|
|
106
|
+
mean_image,
|
|
107
|
+
minimum_fraction=0.10,
|
|
108
|
+
maximum_fraction=1.0,
|
|
109
|
+
),
|
|
110
|
+
metadata=layer_metadata,
|
|
111
|
+
)
|
|
112
|
+
set_image_contrast_limits_range(mean_layer, data=mean_image)
|
|
113
|
+
|
|
114
|
+
movie_layer = None
|
|
115
|
+
if movie_frame_range is not None:
|
|
116
|
+
start_frame, end_frame = resolve_movie_frame_range(
|
|
117
|
+
start_frame=movie_frame_range[0],
|
|
118
|
+
end_frame=movie_frame_range[1],
|
|
119
|
+
frame_count=recording.movie.shape[0],
|
|
120
|
+
)
|
|
121
|
+
movie = recording.movie.read_frames(
|
|
122
|
+
start_frame,
|
|
123
|
+
exclusive_stop(end_frame),
|
|
124
|
+
spatial_crop=display_crop,
|
|
125
|
+
)
|
|
126
|
+
movie_layer = resolved_viewer.add_image(
|
|
127
|
+
display_image_from_movie_image(movie),
|
|
128
|
+
name=movie_layer_name,
|
|
129
|
+
colormap="gray",
|
|
130
|
+
blending="additive",
|
|
131
|
+
contrast_limits=sampled_movie_auto_contrast_limits(
|
|
132
|
+
recording.movie,
|
|
133
|
+
spatial_crop=display_crop,
|
|
134
|
+
),
|
|
135
|
+
metadata=layer_metadata,
|
|
136
|
+
)
|
|
137
|
+
set_image_contrast_limits_range(movie_layer, data=movie)
|
|
138
|
+
|
|
139
|
+
roi_layer = None
|
|
140
|
+
if add_roi_labels_layer:
|
|
141
|
+
roi_layer = resolved_viewer.add_labels(
|
|
142
|
+
roi_label_image_for_display(
|
|
143
|
+
roi_set,
|
|
144
|
+
recording,
|
|
145
|
+
spatial_crop=display_crop,
|
|
146
|
+
),
|
|
147
|
+
name=roi_layer_name,
|
|
148
|
+
opacity=0.5,
|
|
149
|
+
blending="additive",
|
|
150
|
+
metadata=layer_metadata,
|
|
151
|
+
)
|
|
152
|
+
set_labels_brush_size(roi_layer, brush_size=6)
|
|
153
|
+
controls_widget = None
|
|
154
|
+
controls_dock = None
|
|
155
|
+
loaded_recordings_widget = None
|
|
156
|
+
loaded_recordings_dock = None
|
|
157
|
+
save_rois_widget = None
|
|
158
|
+
save_rois_dock = None
|
|
159
|
+
response_plot_widget = None
|
|
160
|
+
response_plot_dock = None
|
|
161
|
+
response_options_widget = None
|
|
162
|
+
response_options_dock = None
|
|
163
|
+
if add_controls:
|
|
164
|
+
resolved_roi_save_file = resolve_roi_save_file(
|
|
165
|
+
recording_data_path=recording.path,
|
|
166
|
+
roi_set=roi_set,
|
|
167
|
+
explicit_roi_save_file=roi_save_file,
|
|
168
|
+
)
|
|
169
|
+
response_plot_widget, response_plot_dock = add_twopy_response_plot_widget(
|
|
170
|
+
resolved_viewer,
|
|
171
|
+
recording=recording,
|
|
172
|
+
roi_labels_layer=roi_layer,
|
|
173
|
+
roi_save_file=resolved_roi_save_file,
|
|
174
|
+
)
|
|
175
|
+
control_docks = add_twopy_magicgui_controls(
|
|
176
|
+
resolved_viewer,
|
|
177
|
+
roi_labels_layer=roi_layer,
|
|
178
|
+
roi_save_file=resolved_roi_save_file,
|
|
179
|
+
recording=recording,
|
|
180
|
+
response_plot_widget=response_plot_widget,
|
|
181
|
+
mean_image_layer=mean_layer,
|
|
182
|
+
movie_layer=movie_layer,
|
|
183
|
+
)
|
|
184
|
+
controls_widget = control_docks.load_widget
|
|
185
|
+
controls_dock = control_docks.load_dock_widget
|
|
186
|
+
loaded_recordings_widget = control_docks.loaded_recordings_widget
|
|
187
|
+
loaded_recordings_dock = control_docks.loaded_recordings_dock_widget
|
|
188
|
+
save_rois_widget = control_docks.save_rois_widget
|
|
189
|
+
save_rois_dock = control_docks.save_rois_dock_widget
|
|
190
|
+
response_options_widget, response_options_dock = (
|
|
191
|
+
add_twopy_response_options_widget(
|
|
192
|
+
resolved_viewer,
|
|
193
|
+
response_plot_widget,
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
place_response_options_after_recording_list(
|
|
197
|
+
resolved_viewer,
|
|
198
|
+
loaded_recordings_dock,
|
|
199
|
+
response_options_dock,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return NapariRecordingView(
|
|
203
|
+
viewer=resolved_viewer,
|
|
204
|
+
recording=recording,
|
|
205
|
+
mean_image_layer=mean_layer,
|
|
206
|
+
movie_layer=movie_layer,
|
|
207
|
+
roi_labels_layer=roi_layer,
|
|
208
|
+
controls_widget=controls_widget,
|
|
209
|
+
controls_dock_widget=controls_dock,
|
|
210
|
+
loaded_recordings_widget=loaded_recordings_widget,
|
|
211
|
+
loaded_recordings_dock_widget=loaded_recordings_dock,
|
|
212
|
+
save_rois_widget=save_rois_widget,
|
|
213
|
+
save_rois_dock_widget=save_rois_dock,
|
|
214
|
+
response_plot_widget=response_plot_widget,
|
|
215
|
+
response_plot_dock_widget=response_plot_dock,
|
|
216
|
+
response_options_widget=response_options_widget,
|
|
217
|
+
response_options_dock_widget=response_options_dock,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def create_viewer() -> NapariViewer:
|
|
222
|
+
"""Create a napari viewer lazily.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
None.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
A napari viewer object exposing the small viewer protocol used here.
|
|
229
|
+
"""
|
|
230
|
+
import napari
|
|
231
|
+
|
|
232
|
+
return cast(NapariViewer, napari.Viewer())
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def set_labels_brush_size(layer: object, *, brush_size: int) -> None:
|
|
236
|
+
"""Set the brush size on a napari Labels layer after creation.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
layer: Labels layer returned by ``viewer.add_labels``.
|
|
240
|
+
brush_size: Brush diameter used by the ROI painting tool.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
None.
|
|
244
|
+
|
|
245
|
+
Napari exposes ``brush_size`` as a layer property, not a
|
|
246
|
+
``Viewer.add_labels`` argument in all supported versions. Setting it here
|
|
247
|
+
keeps ROI drawing defaults explicit without making recording loading depend
|
|
248
|
+
on version-specific layer-constructor keywords.
|
|
249
|
+
"""
|
|
250
|
+
labels_layer = cast(_LabelsLayerWithBrushSize, layer)
|
|
251
|
+
labels_layer.brush_size = brush_size
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def set_image_contrast_limits_range(layer: object, *, data: npt.ArrayLike) -> None:
|
|
255
|
+
"""Allow napari contrast sliders to cover the full loaded data range.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
layer: Image layer returned by ``viewer.add_image``.
|
|
259
|
+
data: Image or movie values shown in that layer.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
None.
|
|
263
|
+
|
|
264
|
+
``contrast_limits`` sets the initial slider handles. Napari initializes the
|
|
265
|
+
slider's allowed range from those handles, so we reset
|
|
266
|
+
``contrast_limits_range`` afterward to keep the full data range available.
|
|
267
|
+
"""
|
|
268
|
+
image_layer = cast(_ImageLayerWithContrastRange, layer)
|
|
269
|
+
image_layer.contrast_limits_range = contrast_limits_from_data_range(
|
|
270
|
+
data,
|
|
271
|
+
minimum_fraction=0.0,
|
|
272
|
+
maximum_fraction=1.0,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def sampled_movie_auto_contrast_limits(
|
|
277
|
+
movie: ConvertedMovie,
|
|
278
|
+
*,
|
|
279
|
+
spatial_crop: SpatialCrop,
|
|
280
|
+
sample_count: int = 10,
|
|
281
|
+
edge_fraction: float = 0.10,
|
|
282
|
+
random_seed: int = 0,
|
|
283
|
+
) -> tuple[float, float]:
|
|
284
|
+
"""Estimate movie display contrast from random interior frames.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
movie: Lazy converted aligned-movie reader.
|
|
288
|
+
spatial_crop: Spatial crop shown in napari.
|
|
289
|
+
sample_count: Maximum number of frames to sample.
|
|
290
|
+
edge_fraction: Fraction of recording edges excluded before sampling.
|
|
291
|
+
random_seed: Seed used for deterministic frame sampling.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
``(minimum, maximum)`` contrast limits for the movie layer.
|
|
295
|
+
|
|
296
|
+
The napari movie layer can be a short preview range, but display contrast
|
|
297
|
+
should reflect the recording rather than the first frames a user happened
|
|
298
|
+
to load. We sample a few frames away from recording edges, compute simple
|
|
299
|
+
per-frame auto contrast limits, then average those limits. The fixed seed
|
|
300
|
+
keeps the same recording visually stable across launches.
|
|
301
|
+
"""
|
|
302
|
+
frame_indices = interior_random_frame_indices(
|
|
303
|
+
frame_count=movie.shape[0],
|
|
304
|
+
sample_count=sample_count,
|
|
305
|
+
edge_fraction=edge_fraction,
|
|
306
|
+
random_seed=random_seed,
|
|
307
|
+
)
|
|
308
|
+
limits = [
|
|
309
|
+
contrast_limits_from_data_range(
|
|
310
|
+
movie.read_frames(
|
|
311
|
+
int(frame_index),
|
|
312
|
+
int(frame_index) + 1,
|
|
313
|
+
spatial_crop=spatial_crop,
|
|
314
|
+
)[0],
|
|
315
|
+
minimum_fraction=0.0,
|
|
316
|
+
maximum_fraction=1.0,
|
|
317
|
+
)
|
|
318
|
+
for frame_index in frame_indices
|
|
319
|
+
]
|
|
320
|
+
if len(limits) == 0:
|
|
321
|
+
return (0.0, 1.0)
|
|
322
|
+
lower = float(np.mean([limit[0] for limit in limits]))
|
|
323
|
+
upper = float(np.mean([limit[1] for limit in limits]))
|
|
324
|
+
if upper <= lower:
|
|
325
|
+
return contrast_limits_from_data_range(
|
|
326
|
+
movie.read_frames(0, 1, spatial_crop=spatial_crop)[0],
|
|
327
|
+
minimum_fraction=0.0,
|
|
328
|
+
maximum_fraction=1.0,
|
|
329
|
+
)
|
|
330
|
+
return (lower, upper)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def interior_random_frame_indices(
|
|
334
|
+
*,
|
|
335
|
+
frame_count: int,
|
|
336
|
+
sample_count: int,
|
|
337
|
+
edge_fraction: float,
|
|
338
|
+
random_seed: int,
|
|
339
|
+
) -> npt.NDArray[np.int64]:
|
|
340
|
+
"""Return deterministic random frame indices away from recording edges.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
frame_count: Number of frames in the aligned movie.
|
|
344
|
+
sample_count: Maximum number of frames to return.
|
|
345
|
+
edge_fraction: Fraction excluded from both start and end.
|
|
346
|
+
random_seed: Seed used by NumPy's random generator.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Sorted frame indices.
|
|
350
|
+
|
|
351
|
+
Very short recordings may not have enough frames to remove both edge
|
|
352
|
+
regions. In that case this falls back to all frames, because a stable
|
|
353
|
+
display estimate is better than failing the viewer load.
|
|
354
|
+
"""
|
|
355
|
+
if frame_count <= 0 or sample_count <= 0:
|
|
356
|
+
return np.asarray((), dtype=np.int64)
|
|
357
|
+
edge_count = int(np.floor(frame_count * edge_fraction))
|
|
358
|
+
start = edge_count
|
|
359
|
+
stop = frame_count - edge_count
|
|
360
|
+
if start >= stop:
|
|
361
|
+
start = 0
|
|
362
|
+
stop = frame_count
|
|
363
|
+
candidates = np.arange(start, stop, dtype=np.int64)
|
|
364
|
+
if candidates.size <= sample_count:
|
|
365
|
+
return candidates
|
|
366
|
+
rng = np.random.default_rng(random_seed)
|
|
367
|
+
sampled = rng.choice(candidates, size=sample_count, replace=False)
|
|
368
|
+
return np.sort(sampled)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def contrast_limits_from_data_range(
|
|
372
|
+
data: npt.ArrayLike,
|
|
373
|
+
*,
|
|
374
|
+
minimum_fraction: float,
|
|
375
|
+
maximum_fraction: float,
|
|
376
|
+
) -> tuple[float, float]:
|
|
377
|
+
"""Return contrast limits at fractions of one data range.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
data: Image or movie values shown in napari.
|
|
381
|
+
minimum_fraction: Fraction between the finite data minimum and maximum
|
|
382
|
+
used for the lower contrast limit.
|
|
383
|
+
maximum_fraction: Fraction between the finite data minimum and maximum
|
|
384
|
+
used for the upper contrast limit.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
``(minimum, maximum)`` contrast limits.
|
|
388
|
+
|
|
389
|
+
This keeps the viewer display easier to inspect without changing the
|
|
390
|
+
underlying converted data. Fractions are range positions, not percentiles,
|
|
391
|
+
so the result is deterministic and cheap for already-loaded preview arrays.
|
|
392
|
+
"""
|
|
393
|
+
values = np.asarray(data, dtype=np.float64)
|
|
394
|
+
finite_values = values[np.isfinite(values)]
|
|
395
|
+
if finite_values.size == 0:
|
|
396
|
+
return (0.0, 1.0)
|
|
397
|
+
minimum = float(np.min(finite_values))
|
|
398
|
+
maximum = float(np.max(finite_values))
|
|
399
|
+
if maximum <= minimum:
|
|
400
|
+
return (minimum, maximum)
|
|
401
|
+
value_range = maximum - minimum
|
|
402
|
+
return (
|
|
403
|
+
minimum + value_range * minimum_fraction,
|
|
404
|
+
minimum + value_range * maximum_fraction,
|
|
405
|
+
)
|
twopy/parity/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Read-only parity helpers for comparing twopy outputs to prior analyses.
|
|
2
|
+
|
|
3
|
+
Inputs: optional lab-generated reference outputs.
|
|
4
|
+
Outputs: small typed objects for audit scripts and tests.
|
|
5
|
+
|
|
6
|
+
Normal twopy analysis should not import this package. Analysis starts from
|
|
7
|
+
converted twopy objects; parity helpers only read prior outputs to check that
|
|
8
|
+
the converted-object analysis matches historical results when needed.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from twopy.parity.dff import (
|
|
12
|
+
SavedAnalysisDeltaFOverFComparison,
|
|
13
|
+
compare_saved_analysis_delta_f_over_f,
|
|
14
|
+
saved_analysis_epoch_windows,
|
|
15
|
+
)
|
|
16
|
+
from twopy.parity.roi import SavedAnalysisRoiSet, roi_set_from_saved_analysis
|
|
17
|
+
from twopy.parity.saved_analysis import (
|
|
18
|
+
SavedAnalysisLastRoi,
|
|
19
|
+
load_saved_analysis_last_roi,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"SavedAnalysisDeltaFOverFComparison",
|
|
24
|
+
"SavedAnalysisLastRoi",
|
|
25
|
+
"SavedAnalysisRoiSet",
|
|
26
|
+
"compare_saved_analysis_delta_f_over_f",
|
|
27
|
+
"load_saved_analysis_last_roi",
|
|
28
|
+
"roi_set_from_saved_analysis",
|
|
29
|
+
"saved_analysis_epoch_windows",
|
|
30
|
+
]
|