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/parity/dff.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""Compare twopy ROI dF/F outputs against prior saved-analysis traces.
|
|
2
|
+
|
|
3
|
+
Inputs: a converted ``RecordingData`` object and a read-only saved-analysis
|
|
4
|
+
object.
|
|
5
|
+
Outputs: computed twopy dF/F, saved traces, and simple error metrics.
|
|
6
|
+
|
|
7
|
+
This module is an audit layer. It does not make saved-analysis files part of
|
|
8
|
+
normal analysis; it only checks whether twopy analysis over converted objects
|
|
9
|
+
matches prior saved outputs for the same ROI masks and frame windows.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import numpy.typing as npt
|
|
16
|
+
|
|
17
|
+
from twopy.analysis.background_subtraction import (
|
|
18
|
+
BackgroundCorrectionMethod,
|
|
19
|
+
extract_background_corrected_roi_traces,
|
|
20
|
+
)
|
|
21
|
+
from twopy.analysis.dff import (
|
|
22
|
+
DeltaFOverFFitMode,
|
|
23
|
+
RoiDeltaFOverF,
|
|
24
|
+
compute_roi_delta_f_over_f,
|
|
25
|
+
)
|
|
26
|
+
from twopy.analysis.epoch_mapping import interpolate_stimulus_epochs_to_frame_windows
|
|
27
|
+
from twopy.analysis.trials import FrameWindow
|
|
28
|
+
from twopy.converted import RecordingData
|
|
29
|
+
from twopy.parity.roi import SavedAnalysisRoiSet, roi_set_from_saved_analysis
|
|
30
|
+
from twopy.parity.saved_analysis import SavedAnalysisLastRoi
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"SavedAnalysisDeltaFOverFComparison",
|
|
34
|
+
"compare_saved_analysis_delta_f_over_f",
|
|
35
|
+
"saved_analysis_epoch_windows",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class SavedAnalysisDeltaFOverFComparison:
|
|
41
|
+
"""Computed dF/F beside saved reference traces and error metrics.
|
|
42
|
+
|
|
43
|
+
Inputs: twopy-computed dF/F and saved-analysis ROI traces with the same
|
|
44
|
+
frame and ROI shape.
|
|
45
|
+
Outputs: absolute errors, per-ROI correlations, and summary metrics.
|
|
46
|
+
|
|
47
|
+
Keeping the full arrays beside the summaries makes a parity failure
|
|
48
|
+
inspectable: callers can plot one ROI, inspect one frame window, or report
|
|
49
|
+
aggregate errors without recomputing extraction.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
computed: RoiDeltaFOverF
|
|
53
|
+
saved_values: npt.NDArray[np.float64]
|
|
54
|
+
absolute_error: npt.NDArray[np.float64]
|
|
55
|
+
correlation_by_roi: npt.NDArray[np.float64]
|
|
56
|
+
roi_set: SavedAnalysisRoiSet
|
|
57
|
+
interleave_windows: tuple[FrameWindow, ...]
|
|
58
|
+
mean_absolute_error: float
|
|
59
|
+
median_absolute_error: float
|
|
60
|
+
max_absolute_error: float
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def compare_saved_analysis_delta_f_over_f(
|
|
64
|
+
recording: RecordingData,
|
|
65
|
+
saved_analysis: SavedAnalysisLastRoi,
|
|
66
|
+
*,
|
|
67
|
+
saved_trace_index: int = 0,
|
|
68
|
+
interleave_epoch_number: int = 1,
|
|
69
|
+
background_method: BackgroundCorrectionMethod = "movie_global_percentile",
|
|
70
|
+
seconds_interleave_use: float | None = 1.0,
|
|
71
|
+
fit_mode: DeltaFOverFFitMode = "source_bounds",
|
|
72
|
+
trace_start_frame: int | None = None,
|
|
73
|
+
trace_stop_frame: int | None = None,
|
|
74
|
+
data_rate_hz: float | None = None,
|
|
75
|
+
chunk_frames: int = 128,
|
|
76
|
+
) -> SavedAnalysisDeltaFOverFComparison:
|
|
77
|
+
"""Compute twopy dF/F and compare it to one saved trace matrix.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
recording: Loaded converted recording.
|
|
81
|
+
saved_analysis: Read-only saved-analysis data.
|
|
82
|
+
saved_trace_index: Which saved trace cell to compare.
|
|
83
|
+
interleave_epoch_number: One-based saved epoch number used as baseline.
|
|
84
|
+
background_method: twopy background method used before dF/F.
|
|
85
|
+
seconds_interleave_use: Seconds from the end of each interleave window.
|
|
86
|
+
``None`` uses each full interleave window.
|
|
87
|
+
fit_mode: dF/F exponential fit mode.
|
|
88
|
+
trace_start_frame: Optional absolute movie frame for saved row zero.
|
|
89
|
+
When omitted, twopy infers the stimulus-bounded frame range from
|
|
90
|
+
converted photodiode and stimulus data.
|
|
91
|
+
trace_stop_frame: Optional exclusive absolute stop frame. When omitted
|
|
92
|
+
with an explicit start, the saved trace length defines the stop.
|
|
93
|
+
data_rate_hz: Optional imaging frame rate. When omitted, twopy reads
|
|
94
|
+
``acq.frameRate`` from converted acquisition metadata.
|
|
95
|
+
chunk_frames: Movie frames to read per HDF5 chunk during ROI extraction.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
``SavedAnalysisDeltaFOverFComparison`` with computed values and metrics.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
ValueError: If saved traces, ROI masks, or windows do not agree with the
|
|
102
|
+
converted recording frame contract.
|
|
103
|
+
|
|
104
|
+
The comparison uses saved ROI masks and saved interleave windows, then runs
|
|
105
|
+
the normal twopy analysis path over converted HDF5 data.
|
|
106
|
+
"""
|
|
107
|
+
saved_values = _saved_trace_values(saved_analysis, saved_trace_index)
|
|
108
|
+
frame_start, frame_stop = _saved_trace_frame_range(
|
|
109
|
+
recording=recording,
|
|
110
|
+
saved_frame_count=saved_values.shape[0],
|
|
111
|
+
trace_start_frame=trace_start_frame,
|
|
112
|
+
trace_stop_frame=trace_stop_frame,
|
|
113
|
+
)
|
|
114
|
+
roi_set = roi_set_from_saved_analysis(saved_analysis, recording)
|
|
115
|
+
_validate_saved_trace_roi_count(saved_values, roi_set)
|
|
116
|
+
|
|
117
|
+
traces = extract_background_corrected_roi_traces(
|
|
118
|
+
recording,
|
|
119
|
+
roi_set.roi_set,
|
|
120
|
+
method=background_method,
|
|
121
|
+
start_frame=frame_start,
|
|
122
|
+
stop_frame=frame_stop,
|
|
123
|
+
chunk_frames=chunk_frames,
|
|
124
|
+
)
|
|
125
|
+
interleave_windows = saved_analysis_epoch_windows(
|
|
126
|
+
saved_analysis,
|
|
127
|
+
epoch_number=interleave_epoch_number,
|
|
128
|
+
trace_start_frame=frame_start,
|
|
129
|
+
trace_stop_frame=frame_stop,
|
|
130
|
+
)
|
|
131
|
+
computed = compute_roi_delta_f_over_f(
|
|
132
|
+
traces,
|
|
133
|
+
interleave_windows,
|
|
134
|
+
data_rate_hz=(
|
|
135
|
+
_recording_frame_rate_hz(recording)
|
|
136
|
+
if data_rate_hz is None
|
|
137
|
+
else float(data_rate_hz)
|
|
138
|
+
),
|
|
139
|
+
seconds_interleave_use=seconds_interleave_use,
|
|
140
|
+
fit_mode=fit_mode,
|
|
141
|
+
)
|
|
142
|
+
_validate_comparison_shapes(computed.values, saved_values)
|
|
143
|
+
|
|
144
|
+
absolute_error = np.abs(computed.values - saved_values)
|
|
145
|
+
finite_errors = absolute_error[np.isfinite(absolute_error)]
|
|
146
|
+
return SavedAnalysisDeltaFOverFComparison(
|
|
147
|
+
computed=computed,
|
|
148
|
+
saved_values=saved_values,
|
|
149
|
+
absolute_error=absolute_error,
|
|
150
|
+
correlation_by_roi=_correlation_by_roi(computed.values, saved_values),
|
|
151
|
+
roi_set=roi_set,
|
|
152
|
+
interleave_windows=interleave_windows,
|
|
153
|
+
mean_absolute_error=float(np.mean(finite_errors)),
|
|
154
|
+
median_absolute_error=float(np.median(finite_errors)),
|
|
155
|
+
max_absolute_error=float(np.max(finite_errors)),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def saved_analysis_epoch_windows(
|
|
160
|
+
saved_analysis: SavedAnalysisLastRoi,
|
|
161
|
+
*,
|
|
162
|
+
epoch_number: int,
|
|
163
|
+
trace_start_frame: int,
|
|
164
|
+
trace_stop_frame: int,
|
|
165
|
+
) -> tuple[FrameWindow, ...]:
|
|
166
|
+
"""Convert saved epoch starts and durations to movie-frame windows.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
saved_analysis: Read-only saved-analysis data.
|
|
170
|
+
epoch_number: One-based saved epoch number.
|
|
171
|
+
trace_start_frame: Absolute movie frame for saved trace row zero.
|
|
172
|
+
trace_stop_frame: Exclusive absolute movie frame after the saved trace.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Tuple of ``FrameWindow`` objects in absolute movie coordinates.
|
|
176
|
+
|
|
177
|
+
Saved epoch starts are one-based row numbers inside the saved trace matrix.
|
|
178
|
+
twopy analysis uses zero-based half-open movie-frame windows, so each saved
|
|
179
|
+
start is shifted by ``trace_start_frame - 1`` and each duration defines the
|
|
180
|
+
exclusive stop.
|
|
181
|
+
"""
|
|
182
|
+
epoch_index = epoch_number - 1
|
|
183
|
+
if epoch_index < 0 or epoch_index >= len(saved_analysis.epoch_start_times):
|
|
184
|
+
msg = (
|
|
185
|
+
f"Saved analysis has {len(saved_analysis.epoch_start_times)} epochs; "
|
|
186
|
+
f"cannot read epoch {epoch_number}"
|
|
187
|
+
)
|
|
188
|
+
raise ValueError(msg)
|
|
189
|
+
|
|
190
|
+
starts = saved_analysis.epoch_start_times[epoch_index]
|
|
191
|
+
durations = saved_analysis.epoch_durations[epoch_index]
|
|
192
|
+
if starts.shape != durations.shape:
|
|
193
|
+
msg = (
|
|
194
|
+
f"Saved epoch {epoch_number} starts and durations differ: "
|
|
195
|
+
f"{starts.shape} versus {durations.shape}"
|
|
196
|
+
)
|
|
197
|
+
raise ValueError(msg)
|
|
198
|
+
|
|
199
|
+
windows: list[FrameWindow] = []
|
|
200
|
+
for index, (start, duration) in enumerate(zip(starts, durations, strict=True)):
|
|
201
|
+
local_start = int(start) - 1
|
|
202
|
+
duration_frames = int(duration)
|
|
203
|
+
absolute_start = trace_start_frame + local_start
|
|
204
|
+
absolute_stop = absolute_start + duration_frames
|
|
205
|
+
if local_start < 0 or duration_frames <= 0 or absolute_stop > trace_stop_frame:
|
|
206
|
+
msg = (
|
|
207
|
+
f"Saved epoch {epoch_number} window {index} is outside trace "
|
|
208
|
+
f"range [{trace_start_frame}, {trace_stop_frame}): "
|
|
209
|
+
f"start={int(start)}, duration={duration_frames}"
|
|
210
|
+
)
|
|
211
|
+
raise ValueError(msg)
|
|
212
|
+
windows.append(
|
|
213
|
+
FrameWindow(
|
|
214
|
+
index=index,
|
|
215
|
+
start_frame=absolute_start,
|
|
216
|
+
stop_frame=absolute_stop,
|
|
217
|
+
label=f"saved_epoch_{epoch_number:04d}_{index + 1:04d}",
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
return tuple(windows)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _saved_trace_values(
|
|
224
|
+
saved_analysis: SavedAnalysisLastRoi,
|
|
225
|
+
saved_trace_index: int,
|
|
226
|
+
) -> npt.NDArray[np.float64]:
|
|
227
|
+
"""Return one saved trace matrix as float64 values.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
saved_analysis: Read-only saved-analysis data.
|
|
231
|
+
saved_trace_index: Trace cell index to load.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Saved trace matrix shaped ``(frames, rois)``.
|
|
235
|
+
"""
|
|
236
|
+
if saved_trace_index < 0 or saved_trace_index >= len(
|
|
237
|
+
saved_analysis.time_by_rois_initial,
|
|
238
|
+
):
|
|
239
|
+
msg = (
|
|
240
|
+
f"Saved analysis has {len(saved_analysis.time_by_rois_initial)} "
|
|
241
|
+
f"trace matrices; cannot read index {saved_trace_index}"
|
|
242
|
+
)
|
|
243
|
+
raise ValueError(msg)
|
|
244
|
+
values = np.asarray(
|
|
245
|
+
saved_analysis.time_by_rois_initial[saved_trace_index],
|
|
246
|
+
dtype=np.float64,
|
|
247
|
+
)
|
|
248
|
+
if values.ndim != 2:
|
|
249
|
+
msg = f"Saved trace matrix must have shape (frames, rois); got {values.shape}"
|
|
250
|
+
raise ValueError(msg)
|
|
251
|
+
return values
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _saved_trace_frame_range(
|
|
255
|
+
*,
|
|
256
|
+
recording: RecordingData,
|
|
257
|
+
saved_frame_count: int,
|
|
258
|
+
trace_start_frame: int | None,
|
|
259
|
+
trace_stop_frame: int | None,
|
|
260
|
+
) -> tuple[int, int]:
|
|
261
|
+
"""Resolve the absolute movie-frame range for saved trace rows.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
recording: Converted recording.
|
|
265
|
+
saved_frame_count: Number of saved trace rows.
|
|
266
|
+
trace_start_frame: Optional explicit start frame.
|
|
267
|
+
trace_stop_frame: Optional explicit stop frame.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
``(start, stop)`` in absolute movie-frame coordinates.
|
|
271
|
+
"""
|
|
272
|
+
if trace_start_frame is None:
|
|
273
|
+
if trace_stop_frame is not None:
|
|
274
|
+
msg = "trace_stop_frame requires trace_start_frame"
|
|
275
|
+
raise ValueError(msg)
|
|
276
|
+
mapping = interpolate_stimulus_epochs_to_frame_windows(recording)
|
|
277
|
+
start = mapping.start_frame
|
|
278
|
+
stop = mapping.stop_frame
|
|
279
|
+
if stop - start != saved_frame_count:
|
|
280
|
+
msg = (
|
|
281
|
+
"Saved trace length does not match interpolated stimulus frame "
|
|
282
|
+
f"range: saved={saved_frame_count}, inferred={stop - start}"
|
|
283
|
+
)
|
|
284
|
+
raise ValueError(msg)
|
|
285
|
+
return start, stop
|
|
286
|
+
|
|
287
|
+
start = int(trace_start_frame)
|
|
288
|
+
stop = (
|
|
289
|
+
start + saved_frame_count if trace_stop_frame is None else int(trace_stop_frame)
|
|
290
|
+
)
|
|
291
|
+
if stop - start != saved_frame_count:
|
|
292
|
+
msg = (
|
|
293
|
+
"Saved trace frame range length must match saved rows: "
|
|
294
|
+
f"range={stop - start}, saved={saved_frame_count}"
|
|
295
|
+
)
|
|
296
|
+
raise ValueError(msg)
|
|
297
|
+
if start < 0 or stop > recording.movie.shape[0] or start >= stop:
|
|
298
|
+
msg = (
|
|
299
|
+
f"Saved trace frame range [{start}, {stop}) is invalid for "
|
|
300
|
+
f"{recording.movie.shape[0]} movie frames"
|
|
301
|
+
)
|
|
302
|
+
raise ValueError(msg)
|
|
303
|
+
return start, stop
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _validate_saved_trace_roi_count(
|
|
307
|
+
saved_values: npt.NDArray[np.float64],
|
|
308
|
+
roi_set: SavedAnalysisRoiSet,
|
|
309
|
+
) -> None:
|
|
310
|
+
"""Confirm saved traces and saved ROI masks describe the same ROI count.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
saved_values: Saved trace matrix.
|
|
314
|
+
roi_set: ROI set derived from the saved mask.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
None.
|
|
318
|
+
"""
|
|
319
|
+
roi_count = roi_set.roi_set.masks.shape[0]
|
|
320
|
+
if saved_values.shape[1] != roi_count:
|
|
321
|
+
msg = (
|
|
322
|
+
"Saved trace ROI count does not match saved ROI mask labels: "
|
|
323
|
+
f"traces={saved_values.shape[1]}, masks={roi_count}"
|
|
324
|
+
)
|
|
325
|
+
raise ValueError(msg)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _validate_comparison_shapes(
|
|
329
|
+
computed_values: npt.NDArray[np.float64],
|
|
330
|
+
saved_values: npt.NDArray[np.float64],
|
|
331
|
+
) -> None:
|
|
332
|
+
"""Confirm computed and saved dF/F matrices can be compared.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
computed_values: twopy-computed dF/F values.
|
|
336
|
+
saved_values: Saved trace matrix.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
None.
|
|
340
|
+
"""
|
|
341
|
+
if computed_values.shape != saved_values.shape:
|
|
342
|
+
msg = (
|
|
343
|
+
"Computed dF/F shape does not match saved traces: "
|
|
344
|
+
f"computed={computed_values.shape}, saved={saved_values.shape}"
|
|
345
|
+
)
|
|
346
|
+
raise ValueError(msg)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _recording_frame_rate_hz(recording: RecordingData) -> float:
|
|
350
|
+
"""Read the imaging frame rate from converted recording metadata.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
recording: Converted recording with acquisition metadata.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Frame rate in hertz.
|
|
357
|
+
"""
|
|
358
|
+
try:
|
|
359
|
+
value = recording.acquisition_metadata["acq.frameRate"]
|
|
360
|
+
except KeyError as error:
|
|
361
|
+
msg = "Recording metadata is missing required field 'acq.frameRate'"
|
|
362
|
+
raise ValueError(msg) from error
|
|
363
|
+
if isinstance(value, str | bytes | int | float | np.generic):
|
|
364
|
+
return float(value)
|
|
365
|
+
|
|
366
|
+
value_type = type(value).__name__
|
|
367
|
+
msg = f"Recording metadata field 'acq.frameRate' must be numeric, got {value_type}"
|
|
368
|
+
raise ValueError(msg)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _correlation_by_roi(
|
|
372
|
+
computed_values: npt.NDArray[np.float64],
|
|
373
|
+
saved_values: npt.NDArray[np.float64],
|
|
374
|
+
) -> npt.NDArray[np.float64]:
|
|
375
|
+
"""Compute one Pearson correlation per ROI.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
computed_values: twopy-computed dF/F matrix.
|
|
379
|
+
saved_values: Saved dF/F matrix.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
One correlation per ROI, with ``NaN`` when either trace is constant or
|
|
383
|
+
has fewer than two finite samples.
|
|
384
|
+
"""
|
|
385
|
+
correlations = np.full(computed_values.shape[1], np.nan, dtype=np.float64)
|
|
386
|
+
for roi_index in range(computed_values.shape[1]):
|
|
387
|
+
computed = computed_values[:, roi_index]
|
|
388
|
+
saved = saved_values[:, roi_index]
|
|
389
|
+
finite = np.isfinite(computed) & np.isfinite(saved)
|
|
390
|
+
if np.count_nonzero(finite) < 2:
|
|
391
|
+
continue
|
|
392
|
+
computed_finite = computed[finite]
|
|
393
|
+
saved_finite = saved[finite]
|
|
394
|
+
if np.std(computed_finite) == 0 or np.std(saved_finite) == 0:
|
|
395
|
+
continue
|
|
396
|
+
correlations[roi_index] = float(
|
|
397
|
+
np.corrcoef(computed_finite, saved_finite)[0, 1],
|
|
398
|
+
)
|
|
399
|
+
return correlations
|
twopy/parity/roi.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Convert prior saved ROI masks into twopy ROI sets for parity checks.
|
|
2
|
+
|
|
3
|
+
Inputs: read-only saved-analysis ROI label images and converted recording
|
|
4
|
+
metadata.
|
|
5
|
+
Outputs: normal ``RoiSet`` objects that twopy analysis functions can consume.
|
|
6
|
+
|
|
7
|
+
This module is only for parity work. It lets audit scripts reuse historical ROI
|
|
8
|
+
masks while keeping normal analysis inputs in twopy-owned objects.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import numpy.typing as npt
|
|
15
|
+
|
|
16
|
+
from twopy.converted import RecordingData
|
|
17
|
+
from twopy.parity.saved_analysis import SavedAnalysisLastRoi
|
|
18
|
+
from twopy.roi import RoiSet, make_roi_set_from_label_image
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"SavedAnalysisRoiSet",
|
|
22
|
+
"roi_set_from_saved_analysis",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class SavedAnalysisRoiSet:
|
|
28
|
+
"""A twopy ROI set plus provenance from a saved ROI label image.
|
|
29
|
+
|
|
30
|
+
Inputs: one saved ROI label image and one converted recording.
|
|
31
|
+
Outputs: full-frame ``RoiSet`` masks, original numeric ROI labels, and a
|
|
32
|
+
note describing whether the source mask was already full-frame or embedded
|
|
33
|
+
from the alignment-valid crop.
|
|
34
|
+
|
|
35
|
+
The provenance fields make parity comparisons auditable without changing
|
|
36
|
+
the normal ``RoiSet`` contract.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
roi_set: RoiSet
|
|
40
|
+
source_label_values: tuple[int, ...]
|
|
41
|
+
source_shape: tuple[int, int]
|
|
42
|
+
spatial_source: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def roi_set_from_saved_analysis(
|
|
46
|
+
saved_analysis: SavedAnalysisLastRoi,
|
|
47
|
+
recording: RecordingData,
|
|
48
|
+
) -> SavedAnalysisRoiSet:
|
|
49
|
+
"""Build a full-frame twopy ROI set from a saved ``lastRoi`` mask.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
saved_analysis: Loaded saved-analysis object with ``roi_mask_initial``.
|
|
53
|
+
recording: Converted recording that defines the movie shape and saved
|
|
54
|
+
alignment-valid crop.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
``SavedAnalysisRoiSet`` with masks in full movie coordinates.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
ValueError: If the label image shape is not the movie shape or the
|
|
61
|
+
alignment-valid crop shape, or if ROI labels are not positive
|
|
62
|
+
integer-like values.
|
|
63
|
+
|
|
64
|
+
Historical ROI masks from the example saved outputs are crop-domain label
|
|
65
|
+
images. twopy ROI masks are full-frame, so crop-shaped masks are embedded
|
|
66
|
+
back into the converted movie coordinates before analysis.
|
|
67
|
+
"""
|
|
68
|
+
label_image = _integer_label_image(saved_analysis.roi_mask_initial)
|
|
69
|
+
full_frame_labels, spatial_source = _label_image_in_movie_coordinates(
|
|
70
|
+
label_image,
|
|
71
|
+
recording,
|
|
72
|
+
)
|
|
73
|
+
label_values = tuple(
|
|
74
|
+
int(value) for value in np.unique(full_frame_labels) if value > 0
|
|
75
|
+
)
|
|
76
|
+
roi_set = make_roi_set_from_label_image(
|
|
77
|
+
full_frame_labels,
|
|
78
|
+
labels=tuple(f"saved_roi_{label_value:04d}" for label_value in label_values),
|
|
79
|
+
)
|
|
80
|
+
return SavedAnalysisRoiSet(
|
|
81
|
+
roi_set=roi_set,
|
|
82
|
+
source_label_values=label_values,
|
|
83
|
+
source_shape=saved_analysis.roi_mask_initial.shape,
|
|
84
|
+
spatial_source=spatial_source,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _integer_label_image(label_image: npt.NDArray[np.float64]) -> npt.NDArray[np.int64]:
|
|
89
|
+
"""Return a saved ROI label image as integer labels.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
label_image: Two-dimensional numeric ROI label image.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Integer label image with nonfinite values treated as background.
|
|
96
|
+
"""
|
|
97
|
+
values = np.asarray(label_image, dtype=np.float64)
|
|
98
|
+
if values.ndim != 2:
|
|
99
|
+
msg = f"Saved ROI mask must be two-dimensional; got {values.shape}"
|
|
100
|
+
raise ValueError(msg)
|
|
101
|
+
|
|
102
|
+
finite = np.isfinite(values)
|
|
103
|
+
rounded = np.rint(values)
|
|
104
|
+
if not np.allclose(values[finite], rounded[finite]):
|
|
105
|
+
msg = "Saved ROI mask labels must be integer-like values"
|
|
106
|
+
raise ValueError(msg)
|
|
107
|
+
labels = np.zeros(values.shape, dtype=np.int64)
|
|
108
|
+
labels[finite] = rounded[finite].astype(np.int64)
|
|
109
|
+
if np.any(labels < 0):
|
|
110
|
+
msg = "Saved ROI mask labels cannot be negative"
|
|
111
|
+
raise ValueError(msg)
|
|
112
|
+
return labels
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _label_image_in_movie_coordinates(
|
|
116
|
+
label_image: npt.NDArray[np.int64],
|
|
117
|
+
recording: RecordingData,
|
|
118
|
+
) -> tuple[npt.NDArray[np.int64], str]:
|
|
119
|
+
"""Return a label image in full movie coordinates.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
label_image: Integer saved ROI label image.
|
|
123
|
+
recording: Converted recording that defines full-frame and crop shapes.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Full-frame label image plus the source coordinate description.
|
|
127
|
+
"""
|
|
128
|
+
movie_shape = recording.movie.shape[1:]
|
|
129
|
+
if label_image.shape == movie_shape:
|
|
130
|
+
return label_image, "full_frame"
|
|
131
|
+
|
|
132
|
+
crop = recording.alignment_valid_crop
|
|
133
|
+
if label_image.shape == crop.shape:
|
|
134
|
+
full_frame = np.zeros(movie_shape, dtype=np.int64)
|
|
135
|
+
full_frame[
|
|
136
|
+
crop.axis0_start : crop.axis0_stop,
|
|
137
|
+
crop.axis1_start : crop.axis1_stop,
|
|
138
|
+
] = label_image
|
|
139
|
+
return full_frame, "alignment_valid_crop"
|
|
140
|
+
|
|
141
|
+
msg = (
|
|
142
|
+
"Saved ROI mask shape must match the movie frame or alignment-valid "
|
|
143
|
+
f"crop; got {label_image.shape}, movie={movie_shape}, crop={crop.shape}"
|
|
144
|
+
)
|
|
145
|
+
raise ValueError(msg)
|