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,223 @@
|
|
|
1
|
+
"""Map stimulus epochs onto imaging frames by interpolation.
|
|
2
|
+
|
|
3
|
+
Inputs: converted stimulus data, high-rate photodiode bounds, and acquisition
|
|
4
|
+
frame rate.
|
|
5
|
+
Outputs: per-frame epoch numbers and stimulus-labeled frame windows.
|
|
6
|
+
|
|
7
|
+
This module reproduces the lab analysis timing contract for frame movies: use
|
|
8
|
+
the photodiode to find the stimulus-bounded imaging frames, then interpolate the
|
|
9
|
+
stimulus-computer epoch column onto those imaging frames. It does not extract
|
|
10
|
+
ROIs or compute responses.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import numpy.typing as npt
|
|
17
|
+
|
|
18
|
+
from twopy._segments import constant_segments
|
|
19
|
+
from twopy.analysis.trials import EpochFrameWindow, FrameWindow
|
|
20
|
+
from twopy.converted import RecordingData
|
|
21
|
+
from twopy.photodiode import stimulus_frame_bounds_from_photodiode
|
|
22
|
+
from twopy.stimulus import stimulus_column_index, stimulus_epoch_names_by_number
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"InterpolatedEpochMapping",
|
|
26
|
+
"interpolate_stimulus_epochs_to_frame_windows",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class InterpolatedEpochMapping:
|
|
32
|
+
"""Stimulus epochs interpolated onto imaging frames.
|
|
33
|
+
|
|
34
|
+
Inputs: converted stimulus rows and photodiode-derived frame bounds.
|
|
35
|
+
Outputs: the absolute frame range, one epoch value per frame, and contiguous
|
|
36
|
+
epoch windows with names.
|
|
37
|
+
|
|
38
|
+
Keeping the per-frame list beside the windows makes timing easy to audit
|
|
39
|
+
against saved analysis outputs.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
start_frame: int
|
|
43
|
+
stop_frame: int
|
|
44
|
+
epoch_numbers: npt.NDArray[np.int64]
|
|
45
|
+
windows: tuple[EpochFrameWindow, ...]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def interpolate_stimulus_epochs_to_frame_windows(
|
|
49
|
+
recording: RecordingData,
|
|
50
|
+
) -> InterpolatedEpochMapping:
|
|
51
|
+
"""Interpolate stimulus epoch values onto stimulus-bounded movie frames.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
recording: Loaded converted recording with stimulus data, acquisition
|
|
55
|
+
metadata, and high-rate photodiode signal.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
``InterpolatedEpochMapping`` with one epoch value per imaging frame and
|
|
59
|
+
one window per contiguous positive epoch run.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: If required columns or frame-rate metadata are missing.
|
|
63
|
+
|
|
64
|
+
The stimulus computer owns epoch times. The imaging computer owns movie
|
|
65
|
+
frames. The photodiode defines the stimulus-bounded movie frame range, and
|
|
66
|
+
nearest-neighbor interpolation assigns the stimulus epoch number to each
|
|
67
|
+
frame inside that range.
|
|
68
|
+
"""
|
|
69
|
+
frame_rate_hz = _required_float_metadata(recording, "acq.frameRate")
|
|
70
|
+
start_frame, stop_frame = stimulus_frame_bounds_from_photodiode(
|
|
71
|
+
recording.high_res_pd,
|
|
72
|
+
movie_frame_count=recording.movie.shape[0],
|
|
73
|
+
frame_rate_hz=frame_rate_hz,
|
|
74
|
+
)
|
|
75
|
+
time_values = recording.stimulus_data[
|
|
76
|
+
:,
|
|
77
|
+
stimulus_column_index(recording, "time_seconds"),
|
|
78
|
+
]
|
|
79
|
+
epoch_values = recording.stimulus_data[
|
|
80
|
+
:,
|
|
81
|
+
stimulus_column_index(recording, "epoch_number"),
|
|
82
|
+
]
|
|
83
|
+
epoch_numbers = _interpolate_epoch_numbers(
|
|
84
|
+
time_values=time_values,
|
|
85
|
+
epoch_values=epoch_values,
|
|
86
|
+
frame_count=stop_frame - start_frame,
|
|
87
|
+
stimulus_duration_seconds=(stop_frame - start_frame) / frame_rate_hz,
|
|
88
|
+
)
|
|
89
|
+
windows = _epoch_windows_from_frame_values(
|
|
90
|
+
epoch_numbers=epoch_numbers,
|
|
91
|
+
start_frame=start_frame,
|
|
92
|
+
epoch_names=stimulus_epoch_names_by_number(recording),
|
|
93
|
+
)
|
|
94
|
+
return InterpolatedEpochMapping(
|
|
95
|
+
start_frame=start_frame,
|
|
96
|
+
stop_frame=stop_frame,
|
|
97
|
+
epoch_numbers=epoch_numbers,
|
|
98
|
+
windows=windows,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _interpolate_epoch_numbers(
|
|
103
|
+
*,
|
|
104
|
+
time_values: npt.NDArray[np.float64],
|
|
105
|
+
epoch_values: npt.NDArray[np.float64],
|
|
106
|
+
frame_count: int,
|
|
107
|
+
stimulus_duration_seconds: float,
|
|
108
|
+
) -> npt.NDArray[np.int64]:
|
|
109
|
+
"""Interpolate positive stimulus epochs onto imaging-frame slots.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
time_values: Stimulus-computer times in seconds.
|
|
113
|
+
epoch_values: Stimulus epoch numbers from the same rows.
|
|
114
|
+
frame_count: Number of imaging frames to label.
|
|
115
|
+
stimulus_duration_seconds: Stimulus-bounded movie duration.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Integer epoch number per imaging frame.
|
|
119
|
+
"""
|
|
120
|
+
if frame_count < 1:
|
|
121
|
+
msg = f"frame_count must be positive; got {frame_count}"
|
|
122
|
+
raise ValueError(msg)
|
|
123
|
+
|
|
124
|
+
positive = epoch_values != 0
|
|
125
|
+
times = np.asarray(time_values[positive], dtype=np.float64)
|
|
126
|
+
epochs = np.asarray(epoch_values[positive], dtype=np.float64)
|
|
127
|
+
keep = times <= stimulus_duration_seconds
|
|
128
|
+
times = times[keep]
|
|
129
|
+
epochs = epochs[keep]
|
|
130
|
+
if times.size == 0:
|
|
131
|
+
msg = "stimulus/data has no positive epoch rows inside the stimulus window"
|
|
132
|
+
raise ValueError(msg)
|
|
133
|
+
if np.any(np.diff(times) < 0):
|
|
134
|
+
msg = "stimulus/data time_seconds must be sorted for epoch interpolation"
|
|
135
|
+
raise ValueError(msg)
|
|
136
|
+
|
|
137
|
+
target_times = np.linspace(times[0], times[-1], frame_count)
|
|
138
|
+
nearest_indices = _nearest_time_indices(times, target_times)
|
|
139
|
+
return np.rint(epochs[nearest_indices]).astype(np.int64)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _nearest_time_indices(
|
|
143
|
+
source_times: npt.NDArray[np.float64],
|
|
144
|
+
target_times: npt.NDArray[np.float64],
|
|
145
|
+
) -> npt.NDArray[np.int64]:
|
|
146
|
+
"""Find nearest source row for each target time.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
source_times: Sorted source times.
|
|
150
|
+
target_times: Times that need source-row labels.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Integer source index per target time.
|
|
154
|
+
"""
|
|
155
|
+
right = np.searchsorted(source_times, target_times, side="left")
|
|
156
|
+
right = np.clip(right, 0, source_times.size - 1)
|
|
157
|
+
left = np.clip(right - 1, 0, source_times.size - 1)
|
|
158
|
+
choose_left = np.abs(target_times - source_times[left]) <= np.abs(
|
|
159
|
+
source_times[right] - target_times
|
|
160
|
+
)
|
|
161
|
+
return np.where(choose_left, left, right).astype(np.int64)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _epoch_windows_from_frame_values(
|
|
165
|
+
*,
|
|
166
|
+
epoch_numbers: npt.NDArray[np.int64],
|
|
167
|
+
start_frame: int,
|
|
168
|
+
epoch_names: dict[int, str],
|
|
169
|
+
) -> tuple[EpochFrameWindow, ...]:
|
|
170
|
+
"""Build contiguous epoch windows from per-frame epoch labels.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
epoch_numbers: Epoch value per stimulus-bounded imaging frame.
|
|
174
|
+
start_frame: Absolute movie frame for ``epoch_numbers[0]``.
|
|
175
|
+
epoch_names: Epoch-number to name mapping.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
One window per positive contiguous epoch run.
|
|
179
|
+
"""
|
|
180
|
+
windows: list[EpochFrameWindow] = []
|
|
181
|
+
for start, stop in constant_segments(epoch_numbers):
|
|
182
|
+
epoch_number = int(epoch_numbers[start])
|
|
183
|
+
if epoch_number <= 0:
|
|
184
|
+
continue
|
|
185
|
+
epoch_name = epoch_names.get(epoch_number, f"epoch_{epoch_number}")
|
|
186
|
+
absolute_start = start_frame + start
|
|
187
|
+
absolute_stop = start_frame + stop
|
|
188
|
+
windows.append(
|
|
189
|
+
EpochFrameWindow(
|
|
190
|
+
window=FrameWindow(
|
|
191
|
+
index=len(windows),
|
|
192
|
+
start_frame=absolute_start,
|
|
193
|
+
stop_frame=absolute_stop,
|
|
194
|
+
label=f"epoch_{epoch_number}:{epoch_name}",
|
|
195
|
+
),
|
|
196
|
+
epoch_number=epoch_number,
|
|
197
|
+
epoch_name=epoch_name,
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
return tuple(windows)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _required_float_metadata(recording: RecordingData, key: str) -> float:
|
|
204
|
+
"""Read one required numeric acquisition metadata field.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
recording: Loaded converted recording.
|
|
208
|
+
key: Acquisition metadata key.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Metadata value as a float.
|
|
212
|
+
"""
|
|
213
|
+
try:
|
|
214
|
+
value = recording.acquisition_metadata[key]
|
|
215
|
+
except KeyError as error:
|
|
216
|
+
msg = f"Recording metadata is missing required field {key!r}"
|
|
217
|
+
raise ValueError(msg) from error
|
|
218
|
+
if isinstance(value, str | bytes | int | float | np.generic):
|
|
219
|
+
return float(value)
|
|
220
|
+
|
|
221
|
+
value_type = type(value).__name__
|
|
222
|
+
msg = f"Recording metadata field {key!r} must be numeric, got {value_type}"
|
|
223
|
+
raise ValueError(msg)
|
twopy/analysis/motion.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Motion-artifact masking for ROI response traces.
|
|
2
|
+
|
|
3
|
+
Inputs: converted per-frame motion masks and computed dF/F traces.
|
|
4
|
+
Outputs: dF/F traces with high-motion frames marked as NaN.
|
|
5
|
+
|
|
6
|
+
Motion masking happens after dF/F because the mask describes frames that should
|
|
7
|
+
not contribute to downstream response statistics. It does not change ROI masks,
|
|
8
|
+
background correction, or baseline fitting.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from twopy.analysis.dff import RoiDeltaFOverF
|
|
14
|
+
from twopy.converted import RecordingData
|
|
15
|
+
|
|
16
|
+
__all__ = ["apply_motion_artifact_mask_to_delta_f_over_f"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def apply_motion_artifact_mask_to_delta_f_over_f(
|
|
20
|
+
dff: RoiDeltaFOverF,
|
|
21
|
+
recording: RecordingData,
|
|
22
|
+
) -> RoiDeltaFOverF:
|
|
23
|
+
"""Mask high-motion dF/F frames with NaN.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
dff: ROI dF/F result to mask.
|
|
27
|
+
recording: Loaded converted recording with ``motion_artifact_mask``.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
A new ``RoiDeltaFOverF`` with ``values`` copied and high-motion frames
|
|
31
|
+
set to ``NaN``.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ValueError: If the dF/F frame range is outside the converted recording
|
|
35
|
+
mask.
|
|
36
|
+
|
|
37
|
+
The mask is sliced by absolute frame coordinates, so cropped trace results
|
|
38
|
+
and full-recording results use the same converted per-frame evidence.
|
|
39
|
+
"""
|
|
40
|
+
mask = recording.motion_artifact_mask
|
|
41
|
+
if dff.start_frame < 0 or dff.stop_frame > mask.shape[0]:
|
|
42
|
+
msg = (
|
|
43
|
+
"dF/F frame range is outside motion artifact mask: "
|
|
44
|
+
f"[{dff.start_frame}, {dff.stop_frame}) for {mask.shape[0]} frames"
|
|
45
|
+
)
|
|
46
|
+
raise ValueError(msg)
|
|
47
|
+
|
|
48
|
+
local_mask = mask[dff.start_frame : dff.stop_frame]
|
|
49
|
+
masked_values = dff.values.copy()
|
|
50
|
+
masked_values[local_mask, :] = np.nan
|
|
51
|
+
metadata = dict(dff.metadata)
|
|
52
|
+
metadata["motion_artifact_masked_frame_count"] = int(np.count_nonzero(local_mask))
|
|
53
|
+
|
|
54
|
+
return RoiDeltaFOverF(
|
|
55
|
+
fluorescence=dff.fluorescence,
|
|
56
|
+
baseline=dff.baseline,
|
|
57
|
+
values=masked_values,
|
|
58
|
+
labels=dff.labels,
|
|
59
|
+
start_frame=dff.start_frame,
|
|
60
|
+
stop_frame=dff.stop_frame,
|
|
61
|
+
tau=dff.tau,
|
|
62
|
+
amplitudes=dff.amplitudes,
|
|
63
|
+
interleave_frame_numbers=dff.interleave_frame_numbers,
|
|
64
|
+
interleave_fluorescence=dff.interleave_fluorescence,
|
|
65
|
+
metadata=metadata,
|
|
66
|
+
)
|