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.
Files changed (84) hide show
  1. twopy/__init__.py +168 -0
  2. twopy/_segments.py +53 -0
  3. twopy/analysis/__init__.py +101 -0
  4. twopy/analysis/background_subtraction.py +672 -0
  5. twopy/analysis/dff.py +524 -0
  6. twopy/analysis/epoch_mapping.py +223 -0
  7. twopy/analysis/motion.py +66 -0
  8. twopy/analysis/persistence.py +975 -0
  9. twopy/analysis/response_processing/__init__.py +55 -0
  10. twopy/analysis/response_processing/apply.py +298 -0
  11. twopy/analysis/response_processing/options.py +192 -0
  12. twopy/analysis/response_processing/qc.py +270 -0
  13. twopy/analysis/response_processing/signals.py +200 -0
  14. twopy/analysis/responses.py +345 -0
  15. twopy/analysis/trials.py +341 -0
  16. twopy/analysis/workflow.py +457 -0
  17. twopy/api.py +78 -0
  18. twopy/config.py +200 -0
  19. twopy/conversion/__init__.py +40 -0
  20. twopy/conversion/alignment_crop.py +329 -0
  21. twopy/conversion/api.py +113 -0
  22. twopy/conversion/frame_ranges.py +35 -0
  23. twopy/conversion/hdf5_writing.py +362 -0
  24. twopy/conversion/matlab_values.py +123 -0
  25. twopy/conversion/source_loading.py +403 -0
  26. twopy/conversion/stimulus_code.py +248 -0
  27. twopy/conversion/types.py +295 -0
  28. twopy/converted.py +681 -0
  29. twopy/database/__init__.py +33 -0
  30. twopy/database/cache.py +166 -0
  31. twopy/database/catalog.py +90 -0
  32. twopy/database/connection.py +27 -0
  33. twopy/database/generic.py +254 -0
  34. twopy/database/modeled.py +419 -0
  35. twopy/database/sql.py +46 -0
  36. twopy/database/types.py +116 -0
  37. twopy/inspection.py +492 -0
  38. twopy/matlab.py +398 -0
  39. twopy/napari/__init__.py +27 -0
  40. twopy/napari/__main__.py +10 -0
  41. twopy/napari/constants.py +13 -0
  42. twopy/napari/controls.py +739 -0
  43. twopy/napari/display.py +181 -0
  44. twopy/napari/display_paths.py +197 -0
  45. twopy/napari/interactive.py +310 -0
  46. twopy/napari/launcher.py +175 -0
  47. twopy/napari/layout.py +147 -0
  48. twopy/napari/loading.py +207 -0
  49. twopy/napari/movie.py +71 -0
  50. twopy/napari/paths.py +181 -0
  51. twopy/napari/plotting/__init__.py +21 -0
  52. twopy/napari/plotting/data.py +266 -0
  53. twopy/napari/plotting/docks.py +1052 -0
  54. twopy/napari/plotting/export.py +803 -0
  55. twopy/napari/plotting/export_controls.py +209 -0
  56. twopy/napari/plotting/label_visibility.py +260 -0
  57. twopy/napari/plotting/options.py +269 -0
  58. twopy/napari/plotting/panels.py +119 -0
  59. twopy/napari/plotting/widgets.py +767 -0
  60. twopy/napari/protocols.py +90 -0
  61. twopy/napari/responses.py +91 -0
  62. twopy/napari/roi.py +238 -0
  63. twopy/napari/session.py +381 -0
  64. twopy/napari/state.py +106 -0
  65. twopy/napari/text.py +26 -0
  66. twopy/napari/types.py +39 -0
  67. twopy/napari/viewer.py +405 -0
  68. twopy/parity/__init__.py +30 -0
  69. twopy/parity/dff.py +399 -0
  70. twopy/parity/roi.py +145 -0
  71. twopy/parity/saved_analysis.py +174 -0
  72. twopy/photodiode.py +375 -0
  73. twopy/photodiode_classification.py +484 -0
  74. twopy/roi.py +462 -0
  75. twopy/session.py +167 -0
  76. twopy/spatial.py +154 -0
  77. twopy/stimulus.py +233 -0
  78. twopy/synchronization.py +235 -0
  79. twopy/typing_guards.py +255 -0
  80. twopy-0.1.0.dist-info/METADATA +311 -0
  81. twopy-0.1.0.dist-info/RECORD +84 -0
  82. twopy-0.1.0.dist-info/WHEEL +5 -0
  83. twopy-0.1.0.dist-info/entry_points.txt +2 -0
  84. 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)
@@ -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
+ )