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/paths.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Path resolution helpers for the twopy napari adapter.
|
|
2
|
+
|
|
3
|
+
Inputs: user-selected files or folders.
|
|
4
|
+
Outputs: concrete converted-recording, movie, and ROI paths.
|
|
5
|
+
|
|
6
|
+
These helpers keep GUI callbacks small. A user can point napari at a converted
|
|
7
|
+
output folder, a source recording folder with ``twopy/`` output, or a direct
|
|
8
|
+
``recording_data.h5`` file.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from os import PathLike
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from twopy.napari.constants import (
|
|
16
|
+
ALIGNED_MOVIE_FILENAME,
|
|
17
|
+
DEFAULT_PATH_TEXT,
|
|
18
|
+
DEFAULT_ROI_FILENAME,
|
|
19
|
+
RECORDING_DATA_FILENAME,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"NapariRecordingPaths",
|
|
24
|
+
"PathInput",
|
|
25
|
+
"is_default_path",
|
|
26
|
+
"resolve_launch_recording_path",
|
|
27
|
+
"resolve_recording_paths",
|
|
28
|
+
"resolve_widget_output_path",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
PathInput = str | PathLike[str]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class NapariRecordingPaths:
|
|
36
|
+
"""Resolved files associated with one converted recording.
|
|
37
|
+
|
|
38
|
+
Inputs: a user-selected file or folder.
|
|
39
|
+
Outputs: paths that the viewer can load or save.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
recording_data_path: Path
|
|
43
|
+
movie_path: Path | None
|
|
44
|
+
roi_file_to_load: Path | None
|
|
45
|
+
roi_save_file: Path
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def resolve_recording_paths(path: PathInput) -> NapariRecordingPaths:
|
|
49
|
+
"""Resolve a widget-selected file or folder into twopy recording paths.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
path: User-selected folder, ``recording_data.h5`` file, or ``default``.
|
|
53
|
+
Magicgui may provide this as either a ``Path`` or plain string.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Existing recording data path plus optional movie and ROI paths.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ValueError: If the input does not identify a converted recording.
|
|
60
|
+
|
|
61
|
+
Folder selection is the intended GUI workflow. Direct file selection still
|
|
62
|
+
works for scripts and audits.
|
|
63
|
+
"""
|
|
64
|
+
selected = Path.cwd() if is_default_path(path) else Path(path).expanduser()
|
|
65
|
+
recording_path = _resolve_recording_data_path(selected)
|
|
66
|
+
recording_dir = recording_path.parent
|
|
67
|
+
movie_path = _optional_file(recording_dir / ALIGNED_MOVIE_FILENAME)
|
|
68
|
+
roi_path = _optional_file(recording_dir / DEFAULT_ROI_FILENAME)
|
|
69
|
+
return NapariRecordingPaths(
|
|
70
|
+
recording_data_path=recording_path,
|
|
71
|
+
movie_path=movie_path,
|
|
72
|
+
roi_file_to_load=roi_path,
|
|
73
|
+
roi_save_file=roi_path or recording_dir / DEFAULT_ROI_FILENAME,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def resolve_launch_recording_path(
|
|
78
|
+
recording_data_path: PathInput | None,
|
|
79
|
+
) -> Path | None:
|
|
80
|
+
"""Resolve an optional command-line recording path.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
recording_data_path: Optional caller path.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Existing ``recording_data.h5`` path, or ``None`` when no path was
|
|
87
|
+
supplied and no default recording exists.
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ValueError: If an explicit path was supplied and does not exist.
|
|
91
|
+
"""
|
|
92
|
+
if recording_data_path is not None:
|
|
93
|
+
return resolve_recording_paths(recording_data_path).recording_data_path
|
|
94
|
+
|
|
95
|
+
cwd = Path.cwd()
|
|
96
|
+
for candidate in (cwd / RECORDING_DATA_FILENAME, cwd / "twopy"):
|
|
97
|
+
try:
|
|
98
|
+
return resolve_recording_paths(candidate).recording_data_path
|
|
99
|
+
except ValueError:
|
|
100
|
+
continue
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def resolve_widget_output_path(path: PathInput, *, default: Path) -> Path:
|
|
105
|
+
"""Resolve an optional widget output path.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
path: Path value from a magicgui widget. Magicgui may provide this as
|
|
109
|
+
either a ``Path`` or plain string.
|
|
110
|
+
default: Default path to use when the widget says ``default``.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Expanded output path.
|
|
114
|
+
"""
|
|
115
|
+
if is_default_path(path):
|
|
116
|
+
return default.expanduser()
|
|
117
|
+
return Path(path).expanduser()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def is_default_path(path: PathInput) -> bool:
|
|
121
|
+
"""Return whether a widget path means "use twopy's default".
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
path: Path value from magicgui or a script.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
``True`` when the path is empty, ``.``, or ``default``.
|
|
128
|
+
"""
|
|
129
|
+
return str(path).strip().lower() in {"", ".", DEFAULT_PATH_TEXT}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _resolve_recording_data_path(path: Path) -> Path:
|
|
133
|
+
"""Find ``recording_data.h5`` from a selected file or folder.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
path: Expanded file or folder path.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Existing recording data file.
|
|
140
|
+
"""
|
|
141
|
+
candidates = _recording_data_candidates(path)
|
|
142
|
+
for candidate in candidates:
|
|
143
|
+
if candidate.is_file():
|
|
144
|
+
return candidate.resolve()
|
|
145
|
+
|
|
146
|
+
candidate_list = "\n".join(f"- {candidate}" for candidate in candidates)
|
|
147
|
+
msg = (
|
|
148
|
+
"Could not find recording_data.h5. Select a converted output folder, "
|
|
149
|
+
"a source recording folder with twopy output, or recording_data.h5.\n"
|
|
150
|
+
f"Checked:\n{candidate_list}"
|
|
151
|
+
)
|
|
152
|
+
raise ValueError(msg)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _recording_data_candidates(path: Path) -> tuple[Path, ...]:
|
|
156
|
+
"""Return the recording-data paths implied by one selected path.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
path: Expanded file or folder path.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Candidate paths in preferred order.
|
|
163
|
+
"""
|
|
164
|
+
if path.is_dir():
|
|
165
|
+
return (
|
|
166
|
+
path / RECORDING_DATA_FILENAME,
|
|
167
|
+
path / "twopy" / RECORDING_DATA_FILENAME,
|
|
168
|
+
)
|
|
169
|
+
return (path,)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _optional_file(path: Path) -> Path | None:
|
|
173
|
+
"""Return an optional file path when it exists.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
path: Candidate file path.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Resolved file path, or ``None``.
|
|
180
|
+
"""
|
|
181
|
+
return path.resolve() if path.is_file() else None
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Response plotting package for the twopy napari adapter.
|
|
2
|
+
|
|
3
|
+
Inputs: converted recordings, analysis outputs, ROI Labels layers, and plot
|
|
4
|
+
options.
|
|
5
|
+
Outputs: response plot dock widgets plus small data and option helpers.
|
|
6
|
+
|
|
7
|
+
This package keeps response plotting scoped under one napari subpackage. It
|
|
8
|
+
does not own conversion, ROI storage, or scientific analysis logic.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from twopy.napari.plotting.docks import (
|
|
12
|
+
add_twopy_response_options_widget,
|
|
13
|
+
add_twopy_response_plot_widget,
|
|
14
|
+
refresh_response_plot_widget,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"add_twopy_response_options_widget",
|
|
19
|
+
"add_twopy_response_plot_widget",
|
|
20
|
+
"refresh_response_plot_widget",
|
|
21
|
+
]
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Build plot-ready response data for the twopy napari adapter.
|
|
2
|
+
|
|
3
|
+
Inputs: grouped response analysis outputs and saved analysis HDF5 files.
|
|
4
|
+
Outputs: mean/SEM traces with direct response time vectors for plotting.
|
|
5
|
+
|
|
6
|
+
This module has no Qt imports. It keeps response-data preparation testable and
|
|
7
|
+
separate from the napari widgets that draw and control the plots.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import numpy.typing as npt
|
|
16
|
+
|
|
17
|
+
from twopy.analysis.dff import RoiDeltaFOverF
|
|
18
|
+
from twopy.analysis.persistence import load_analysis_outputs
|
|
19
|
+
from twopy.analysis.responses import (
|
|
20
|
+
GroupedRoiResponses,
|
|
21
|
+
RoiResponseTrial,
|
|
22
|
+
group_delta_f_over_f_by_epoch,
|
|
23
|
+
is_gray_epoch_name,
|
|
24
|
+
)
|
|
25
|
+
from twopy.analysis.trials import EpochFrameWindow
|
|
26
|
+
from twopy.analysis.workflow import (
|
|
27
|
+
DEFAULT_RESPONSE_POST_WINDOW_SECONDS,
|
|
28
|
+
DEFAULT_RESPONSE_PRE_WINDOW_SECONDS,
|
|
29
|
+
)
|
|
30
|
+
from twopy.converted import RecordingData
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"EpochResponsePlotData",
|
|
34
|
+
"ResponsePlotData",
|
|
35
|
+
"default_analysis_output_path",
|
|
36
|
+
"load_response_plot_data",
|
|
37
|
+
"response_plot_data_from_grouped",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class EpochResponsePlotData:
|
|
43
|
+
"""Mean and SEM traces for one stimulus epoch type.
|
|
44
|
+
|
|
45
|
+
Inputs: grouped response trials for one epoch.
|
|
46
|
+
Outputs: one mean and SEM trace per ROI on a shared time axis.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
epoch_name: str
|
|
50
|
+
epoch_number: int
|
|
51
|
+
roi_labels: tuple[str, ...]
|
|
52
|
+
time_seconds: npt.NDArray[np.float64]
|
|
53
|
+
mean_values: npt.NDArray[np.float64]
|
|
54
|
+
sem_values: npt.NDArray[np.float64]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class ResponsePlotData:
|
|
59
|
+
"""Plot-ready ROI responses grouped by stimulus epoch.
|
|
60
|
+
|
|
61
|
+
Inputs: grouped response analysis output.
|
|
62
|
+
Outputs: epoch plot data plus the source analysis path.
|
|
63
|
+
|
|
64
|
+
Each epoch carries its own direct ``time_seconds`` vector. The analysis HDF5
|
|
65
|
+
stores the same vector per trial, so the plot never has to infer time from
|
|
66
|
+
array position alone.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
source_path: Path | None
|
|
70
|
+
epochs: tuple[EpochResponsePlotData, ...]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def response_plot_data_from_grouped(
|
|
74
|
+
grouped: GroupedRoiResponses,
|
|
75
|
+
*,
|
|
76
|
+
source_path: Path | None = None,
|
|
77
|
+
) -> ResponsePlotData:
|
|
78
|
+
"""Summarize grouped responses into mean and SEM traces for plotting.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
grouped: Grouped response object from analysis.
|
|
82
|
+
source_path: Optional analysis output path used for display.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Plot-ready data with one item per stimulus epoch type.
|
|
86
|
+
"""
|
|
87
|
+
epoch_keys: list[tuple[int, str]] = []
|
|
88
|
+
trials_by_epoch: dict[tuple[int, str], list[RoiResponseTrial]] = {}
|
|
89
|
+
for trial in grouped.trials:
|
|
90
|
+
key = (trial.epoch_number, trial.epoch_name)
|
|
91
|
+
if key not in trials_by_epoch:
|
|
92
|
+
epoch_keys.append(key)
|
|
93
|
+
trials_by_epoch[key] = []
|
|
94
|
+
trials_by_epoch[key].append(trial)
|
|
95
|
+
|
|
96
|
+
epochs = tuple(
|
|
97
|
+
_epoch_plot_data(
|
|
98
|
+
epoch_number=epoch_number,
|
|
99
|
+
epoch_name=epoch_name,
|
|
100
|
+
trials=tuple(trials_by_epoch[(epoch_number, epoch_name)]),
|
|
101
|
+
roi_labels=grouped.roi_labels,
|
|
102
|
+
data_rate_hz=grouped.data_rate_hz,
|
|
103
|
+
)
|
|
104
|
+
for epoch_number, epoch_name in sorted(epoch_keys)
|
|
105
|
+
)
|
|
106
|
+
return ResponsePlotData(source_path=source_path, epochs=epochs)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def default_analysis_output_path(recording: RecordingData) -> Path:
|
|
110
|
+
"""Return the default analysis output path for one recording.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
recording: Loaded converted recording.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Path to ``analysis_outputs.h5`` beside ``recording_data.h5``.
|
|
117
|
+
"""
|
|
118
|
+
return recording.path.expanduser().parent / "analysis_outputs.h5"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def load_response_plot_data(path: Path) -> ResponsePlotData | str:
|
|
122
|
+
"""Load response plot data from an analysis output file.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
path: Candidate ``analysis_outputs.h5`` path.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Plot data, or a status message for the user.
|
|
129
|
+
"""
|
|
130
|
+
if not path.is_file():
|
|
131
|
+
return f"No analysis output found: {path}"
|
|
132
|
+
outputs = load_analysis_outputs(path)
|
|
133
|
+
if outputs.dff is not None and len(outputs.epoch_windows) > 0:
|
|
134
|
+
data_rate_hz = _analysis_output_data_rate_hz(
|
|
135
|
+
grouped=outputs.grouped_responses,
|
|
136
|
+
dff=outputs.dff,
|
|
137
|
+
)
|
|
138
|
+
if data_rate_hz is not None:
|
|
139
|
+
post_window_seconds = _response_post_window_seconds(outputs.epoch_windows)
|
|
140
|
+
grouped = group_delta_f_over_f_by_epoch(
|
|
141
|
+
outputs.dff,
|
|
142
|
+
outputs.epoch_windows,
|
|
143
|
+
data_rate_hz=data_rate_hz,
|
|
144
|
+
pre_window_seconds=DEFAULT_RESPONSE_PRE_WINDOW_SECONDS,
|
|
145
|
+
post_window_seconds=post_window_seconds,
|
|
146
|
+
)
|
|
147
|
+
return response_plot_data_from_grouped(grouped, source_path=outputs.path)
|
|
148
|
+
if outputs.grouped_responses is None:
|
|
149
|
+
return f"No grouped responses in: {path}"
|
|
150
|
+
return response_plot_data_from_grouped(
|
|
151
|
+
outputs.grouped_responses,
|
|
152
|
+
source_path=outputs.path,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _epoch_plot_data(
|
|
157
|
+
*,
|
|
158
|
+
epoch_number: int,
|
|
159
|
+
epoch_name: str,
|
|
160
|
+
trials: tuple[RoiResponseTrial, ...],
|
|
161
|
+
roi_labels: tuple[str, ...],
|
|
162
|
+
data_rate_hz: float,
|
|
163
|
+
) -> EpochResponsePlotData:
|
|
164
|
+
"""Build plot data for one epoch type.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
epoch_number: Stimulus epoch number.
|
|
168
|
+
epoch_name: Stimulus epoch name.
|
|
169
|
+
trials: Trial responses for this epoch type.
|
|
170
|
+
roi_labels: ROI labels in column order.
|
|
171
|
+
data_rate_hz: Imaging frame rate in hertz.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Mean and SEM traces with shape ``(rois, frames)``.
|
|
175
|
+
"""
|
|
176
|
+
time_seconds = _shared_time_axis(trials, data_rate_hz)
|
|
177
|
+
first_time = float(time_seconds[0])
|
|
178
|
+
max_frames = len(time_seconds)
|
|
179
|
+
trial_count = len(trials)
|
|
180
|
+
roi_count = len(roi_labels)
|
|
181
|
+
values = np.full((trial_count, max_frames, roi_count), np.nan, dtype=np.float64)
|
|
182
|
+
for trial_index, trial in enumerate(trials):
|
|
183
|
+
offsets = np.rint((trial.time_seconds - first_time) * data_rate_hz).astype(
|
|
184
|
+
np.int64,
|
|
185
|
+
copy=False,
|
|
186
|
+
)
|
|
187
|
+
values[trial_index, offsets, :] = trial.values
|
|
188
|
+
|
|
189
|
+
mean_values = np.nanmean(values, axis=0).T
|
|
190
|
+
valid_counts = np.sum(~np.isnan(values), axis=0).T
|
|
191
|
+
sem_values = np.zeros((roi_count, max_frames), dtype=np.float64)
|
|
192
|
+
variable_mask = valid_counts > 1
|
|
193
|
+
if np.any(variable_mask):
|
|
194
|
+
std_values = np.nanstd(values, axis=0, ddof=1).T
|
|
195
|
+
sem_values[variable_mask] = std_values[variable_mask] / np.sqrt(
|
|
196
|
+
valid_counts[variable_mask]
|
|
197
|
+
)
|
|
198
|
+
return EpochResponsePlotData(
|
|
199
|
+
epoch_name=epoch_name,
|
|
200
|
+
epoch_number=epoch_number,
|
|
201
|
+
roi_labels=roi_labels,
|
|
202
|
+
time_seconds=time_seconds,
|
|
203
|
+
mean_values=mean_values,
|
|
204
|
+
sem_values=sem_values,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _shared_time_axis(
|
|
209
|
+
trials: tuple[RoiResponseTrial, ...],
|
|
210
|
+
data_rate_hz: float,
|
|
211
|
+
) -> npt.NDArray[np.float64]:
|
|
212
|
+
"""Return one relative time axis that covers all trials in an epoch.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
trials: Trial responses for one epoch type.
|
|
216
|
+
data_rate_hz: Imaging frame rate in hertz.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Relative time in seconds, usually starting before stimulus onset and
|
|
220
|
+
ending after stimulus offset when surrounding gray frames are available.
|
|
221
|
+
"""
|
|
222
|
+
first_time = min(float(trial.time_seconds[0]) for trial in trials)
|
|
223
|
+
last_time = max(float(trial.time_seconds[-1]) for trial in trials)
|
|
224
|
+
frame_count = int(round((last_time - first_time) * data_rate_hz)) + 1
|
|
225
|
+
return first_time + (np.arange(frame_count, dtype=np.float64) / data_rate_hz)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _analysis_output_data_rate_hz(
|
|
229
|
+
*,
|
|
230
|
+
grouped: GroupedRoiResponses | None,
|
|
231
|
+
dff: RoiDeltaFOverF,
|
|
232
|
+
) -> float | None:
|
|
233
|
+
"""Return the saved response frame rate when available.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
grouped: Optional grouped responses loaded from analysis outputs.
|
|
237
|
+
dff: dF/F result with audit metadata.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Data rate in hertz, or ``None`` when unavailable.
|
|
241
|
+
"""
|
|
242
|
+
if grouped is not None:
|
|
243
|
+
return grouped.data_rate_hz
|
|
244
|
+
value = dff.metadata.get("data_rate_hz")
|
|
245
|
+
if isinstance(value, bool):
|
|
246
|
+
return None
|
|
247
|
+
if isinstance(value, int | float):
|
|
248
|
+
return float(value)
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _response_post_window_seconds(
|
|
253
|
+
epoch_windows: Sequence[EpochFrameWindow],
|
|
254
|
+
) -> float:
|
|
255
|
+
"""Return the default post-epoch plotting context from available windows.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
epoch_windows: Iterable of epoch-window objects with ``epoch_name``.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Two seconds when a gray interleave epoch exists, otherwise zero.
|
|
262
|
+
"""
|
|
263
|
+
for epoch_window in epoch_windows:
|
|
264
|
+
if is_gray_epoch_name(epoch_window.epoch_name):
|
|
265
|
+
return DEFAULT_RESPONSE_POST_WINDOW_SECONDS
|
|
266
|
+
return 0.0
|