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,484 @@
|
|
|
1
|
+
"""Classify photodiode events into stimulus timing objects.
|
|
2
|
+
|
|
3
|
+
Inputs: converted stimulus data plus paired high-rate and imaging-frame
|
|
4
|
+
photodiode events.
|
|
5
|
+
Outputs: classified start, trial-transition, and end events plus imaging-frame
|
|
6
|
+
windows with stimulus epoch metadata.
|
|
7
|
+
|
|
8
|
+
This module is the semantic layer above raw photodiode event detection. It
|
|
9
|
+
cross-checks detected photodiode events against the converted
|
|
10
|
+
``stimulus/data`` ``photodiode_flash`` column before producing trial windows,
|
|
11
|
+
so response analysis does not rely on event order alone.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Literal
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from twopy._segments import constant_segments, true_segments
|
|
20
|
+
from twopy.converted import RecordingData
|
|
21
|
+
from twopy.stimulus import stimulus_column_index, stimulus_epoch_names_by_number
|
|
22
|
+
from twopy.synchronization import AlignedPhotodiodeEvent, PhotodiodeAlignment
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"ClassifiedPhotodiodeEvent",
|
|
26
|
+
"ClassifiedStimulusTiming",
|
|
27
|
+
"ClassifiedStimulusWindow",
|
|
28
|
+
"PhotodiodeDurationClass",
|
|
29
|
+
"PhotodiodeEventType",
|
|
30
|
+
"classify_recording_photodiode_events",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
PhotodiodeEventType = Literal[
|
|
34
|
+
"stimulus_start",
|
|
35
|
+
"trial_transition",
|
|
36
|
+
"stimulus_end",
|
|
37
|
+
]
|
|
38
|
+
PhotodiodeDurationClass = Literal["short", "long"]
|
|
39
|
+
PhotodiodeClassificationMetadataValue = str | int | float | bool
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class ClassifiedPhotodiodeEvent:
|
|
44
|
+
"""One photodiode event with a stimulus-timing role.
|
|
45
|
+
|
|
46
|
+
Inputs: one paired photodiode event and its matching
|
|
47
|
+
``stimulus/data`` flash segment.
|
|
48
|
+
Outputs: event type, duration class, imaging frame, and stimulus row audit
|
|
49
|
+
fields.
|
|
50
|
+
|
|
51
|
+
``stimulus_flash_start_row`` and ``stimulus_flash_stop_row`` point back to
|
|
52
|
+
the stimulus-computer table rows that requested the flash. The aligned
|
|
53
|
+
event carries the measured high-rate and frame-resolution photodiode
|
|
54
|
+
evidence.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
index: int
|
|
58
|
+
event_type: PhotodiodeEventType
|
|
59
|
+
duration_class: PhotodiodeDurationClass
|
|
60
|
+
aligned_event: AlignedPhotodiodeEvent
|
|
61
|
+
imaging_frame: int
|
|
62
|
+
stimulus_flash_start_row: int
|
|
63
|
+
stimulus_flash_stop_row: int
|
|
64
|
+
stimulus_flash_duration_rows: int
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class ClassifiedStimulusWindow:
|
|
69
|
+
"""One stimulus epoch bounded by classified photodiode events.
|
|
70
|
+
|
|
71
|
+
Inputs: neighboring classified photodiode events and one contiguous
|
|
72
|
+
positive epoch run from ``stimulus/data``.
|
|
73
|
+
Outputs: imaging frame bounds plus the stimulus epoch number/name and row
|
|
74
|
+
bounds.
|
|
75
|
+
|
|
76
|
+
This is the object later response code should use for trial or epoch
|
|
77
|
+
slicing. It carries both imaging-frame coordinates and stimulus-table
|
|
78
|
+
evidence.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
index: int
|
|
82
|
+
start_event: ClassifiedPhotodiodeEvent
|
|
83
|
+
stop_event: ClassifiedPhotodiodeEvent
|
|
84
|
+
start_frame: int
|
|
85
|
+
stop_frame: int
|
|
86
|
+
epoch_number: int
|
|
87
|
+
epoch_name: str
|
|
88
|
+
stimulus_start_row: int
|
|
89
|
+
stimulus_stop_row: int
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class ClassifiedStimulusTiming:
|
|
94
|
+
"""Classified photodiode timing for one converted recording.
|
|
95
|
+
|
|
96
|
+
Inputs: one converted recording and one photodiode alignment.
|
|
97
|
+
Outputs: all classified events, all stimulus windows, and audit metadata.
|
|
98
|
+
|
|
99
|
+
``events`` has one item per detected flash. ``windows`` has one fewer item:
|
|
100
|
+
each window spans the interval between neighboring events.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
events: tuple[ClassifiedPhotodiodeEvent, ...]
|
|
104
|
+
windows: tuple[ClassifiedStimulusWindow, ...]
|
|
105
|
+
metadata: dict[str, PhotodiodeClassificationMetadataValue]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class _StimulusFlashSegment:
|
|
110
|
+
"""One contiguous positive run in the photodiode flash column.
|
|
111
|
+
|
|
112
|
+
Inputs: ``stimulus/data`` flash column.
|
|
113
|
+
Outputs: half-open row bounds for one requested flash.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
start_row: int
|
|
117
|
+
stop_row: int
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True)
|
|
121
|
+
class _StimulusEpochRun:
|
|
122
|
+
"""One contiguous positive stimulus epoch run.
|
|
123
|
+
|
|
124
|
+
Inputs: ``stimulus/data`` epoch column.
|
|
125
|
+
Outputs: epoch number and half-open row bounds.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
epoch_number: int
|
|
129
|
+
start_row: int
|
|
130
|
+
stop_row: int
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def classify_recording_photodiode_events(
|
|
134
|
+
recording: RecordingData,
|
|
135
|
+
alignment: PhotodiodeAlignment,
|
|
136
|
+
*,
|
|
137
|
+
flash_threshold: float = 0.5,
|
|
138
|
+
long_flash_factor: float = 1.25,
|
|
139
|
+
) -> ClassifiedStimulusTiming:
|
|
140
|
+
"""Classify paired photodiode events into stimulus timing windows.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
recording: Loaded converted recording with ``stimulus/data``.
|
|
144
|
+
alignment: Paired high-rate and imaging-frame photodiode events.
|
|
145
|
+
flash_threshold: Values above this threshold in ``photodiode_flash``
|
|
146
|
+
count as stimulus-computer flash requests.
|
|
147
|
+
long_flash_factor: High-rate photodiode events at least this multiple
|
|
148
|
+
of the typical transition duration are marked ``"long"``.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
``ClassifiedStimulusTiming`` with typed events and epoch windows.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
ValueError: If the photodiode, flash-column, and epoch-run contracts do
|
|
155
|
+
not agree.
|
|
156
|
+
|
|
157
|
+
The current classifier handles boundary-flash protocols: one flash marks
|
|
158
|
+
stimulus start, each following flash marks an epoch/trial transition, and
|
|
159
|
+
the final flash marks stimulus end. Extra within-epoch flashes should fail
|
|
160
|
+
here until we add protocol-specific classification rules for them.
|
|
161
|
+
"""
|
|
162
|
+
if long_flash_factor <= 1.0:
|
|
163
|
+
msg = f"long_flash_factor must be greater than 1; got {long_flash_factor}"
|
|
164
|
+
raise ValueError(msg)
|
|
165
|
+
|
|
166
|
+
flash_segments = _stimulus_flash_segments(
|
|
167
|
+
recording=recording,
|
|
168
|
+
flash_threshold=flash_threshold,
|
|
169
|
+
)
|
|
170
|
+
_validate_event_and_flash_counts(
|
|
171
|
+
paired_events=alignment.paired_events,
|
|
172
|
+
flash_segments=flash_segments,
|
|
173
|
+
)
|
|
174
|
+
classified_events = _classify_events(
|
|
175
|
+
paired_events=alignment.paired_events,
|
|
176
|
+
flash_segments=flash_segments,
|
|
177
|
+
long_flash_factor=long_flash_factor,
|
|
178
|
+
)
|
|
179
|
+
epoch_runs = _stimulus_epoch_runs(recording)
|
|
180
|
+
_validate_event_and_epoch_counts(
|
|
181
|
+
event_count=len(classified_events),
|
|
182
|
+
epoch_runs=epoch_runs,
|
|
183
|
+
)
|
|
184
|
+
windows = _classified_windows(
|
|
185
|
+
events=classified_events,
|
|
186
|
+
epoch_runs=epoch_runs,
|
|
187
|
+
epoch_names=stimulus_epoch_names_by_number(recording),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return ClassifiedStimulusTiming(
|
|
191
|
+
events=classified_events,
|
|
192
|
+
windows=windows,
|
|
193
|
+
metadata={
|
|
194
|
+
"event_count": len(classified_events),
|
|
195
|
+
"window_count": len(windows),
|
|
196
|
+
"stimulus_flash_segment_count": len(flash_segments),
|
|
197
|
+
"stimulus_epoch_run_count": len(epoch_runs),
|
|
198
|
+
"flash_threshold": float(flash_threshold),
|
|
199
|
+
"long_flash_factor": float(long_flash_factor),
|
|
200
|
+
"typical_transition_high_res_duration_samples": (
|
|
201
|
+
_typical_transition_duration(alignment.paired_events)
|
|
202
|
+
),
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _stimulus_flash_segments(
|
|
208
|
+
*,
|
|
209
|
+
recording: RecordingData,
|
|
210
|
+
flash_threshold: float,
|
|
211
|
+
) -> tuple[_StimulusFlashSegment, ...]:
|
|
212
|
+
"""Segment the converted stimulus photodiode-flash column.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
recording: Loaded converted recording.
|
|
216
|
+
flash_threshold: Values above this threshold are active flash rows.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
One segment per contiguous active flash run.
|
|
220
|
+
"""
|
|
221
|
+
flash_column = stimulus_column_index(recording, "photodiode_flash")
|
|
222
|
+
flash_values = recording.stimulus_data[:, flash_column]
|
|
223
|
+
active = np.asarray(flash_values > flash_threshold, dtype=np.bool_)
|
|
224
|
+
segments = true_segments(active)
|
|
225
|
+
if len(segments) == 0:
|
|
226
|
+
msg = "stimulus/data photodiode_flash column has no active flash segments"
|
|
227
|
+
raise ValueError(msg)
|
|
228
|
+
return tuple(
|
|
229
|
+
_StimulusFlashSegment(start_row=start, stop_row=stop)
|
|
230
|
+
for start, stop in segments
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _stimulus_epoch_runs(recording: RecordingData) -> tuple[_StimulusEpochRun, ...]:
|
|
235
|
+
"""Find contiguous positive stimulus epoch runs.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
recording: Loaded converted recording.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
One run per contiguous positive epoch number.
|
|
242
|
+
"""
|
|
243
|
+
epoch_column = stimulus_column_index(recording, "epoch_number")
|
|
244
|
+
epoch_numbers = recording.stimulus_data[:, epoch_column].astype(np.int64)
|
|
245
|
+
all_segments = constant_segments(epoch_numbers)
|
|
246
|
+
epoch_runs = tuple(
|
|
247
|
+
_StimulusEpochRun(
|
|
248
|
+
epoch_number=int(epoch_numbers[start]),
|
|
249
|
+
start_row=start,
|
|
250
|
+
stop_row=stop,
|
|
251
|
+
)
|
|
252
|
+
for start, stop in all_segments
|
|
253
|
+
if epoch_numbers[start] > 0
|
|
254
|
+
)
|
|
255
|
+
if len(epoch_runs) == 0:
|
|
256
|
+
msg = "stimulus/data does not contain any positive epoch runs"
|
|
257
|
+
raise ValueError(msg)
|
|
258
|
+
return epoch_runs
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _validate_event_and_flash_counts(
|
|
262
|
+
*,
|
|
263
|
+
paired_events: tuple[AlignedPhotodiodeEvent, ...],
|
|
264
|
+
flash_segments: tuple[_StimulusFlashSegment, ...],
|
|
265
|
+
) -> None:
|
|
266
|
+
"""Check that measured flashes match stimulus-computer flash requests.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
paired_events: Paired photodiode events detected from the recording.
|
|
270
|
+
flash_segments: Flash request segments from ``stimulus/data``.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
None.
|
|
274
|
+
"""
|
|
275
|
+
if len(paired_events) < 2:
|
|
276
|
+
msg = f"At least two photodiode events are required; got {len(paired_events)}"
|
|
277
|
+
raise ValueError(msg)
|
|
278
|
+
if len(paired_events) != len(flash_segments):
|
|
279
|
+
msg = (
|
|
280
|
+
"Detected photodiode events do not match stimulus flash requests: "
|
|
281
|
+
f"paired_events={len(paired_events)}, "
|
|
282
|
+
f"stimulus_flash_segments={len(flash_segments)}"
|
|
283
|
+
)
|
|
284
|
+
raise ValueError(msg)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _validate_event_and_epoch_counts(
|
|
288
|
+
*,
|
|
289
|
+
event_count: int,
|
|
290
|
+
epoch_runs: tuple[_StimulusEpochRun, ...],
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Check that neighboring events can bound every stimulus epoch run.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
event_count: Number of classified photodiode events.
|
|
296
|
+
epoch_runs: Contiguous positive epoch runs.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
None.
|
|
300
|
+
"""
|
|
301
|
+
expected_windows = event_count - 1
|
|
302
|
+
if len(epoch_runs) != expected_windows:
|
|
303
|
+
msg = (
|
|
304
|
+
"Classified photodiode windows do not match stimulus epoch runs: "
|
|
305
|
+
f"windows={expected_windows}, stimulus_epoch_runs={len(epoch_runs)}"
|
|
306
|
+
)
|
|
307
|
+
raise ValueError(msg)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _classify_events(
|
|
311
|
+
*,
|
|
312
|
+
paired_events: tuple[AlignedPhotodiodeEvent, ...],
|
|
313
|
+
flash_segments: tuple[_StimulusFlashSegment, ...],
|
|
314
|
+
long_flash_factor: float,
|
|
315
|
+
) -> tuple[ClassifiedPhotodiodeEvent, ...]:
|
|
316
|
+
"""Assign stimulus roles and duration classes to paired events.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
paired_events: Ordered high-rate-to-frame photodiode events.
|
|
320
|
+
flash_segments: Ordered stimulus-computer flash segments.
|
|
321
|
+
long_flash_factor: Multiple used to call a high-rate event long.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Classified events in recording order.
|
|
325
|
+
"""
|
|
326
|
+
typical_duration = _typical_transition_duration(paired_events)
|
|
327
|
+
return tuple(
|
|
328
|
+
ClassifiedPhotodiodeEvent(
|
|
329
|
+
index=index,
|
|
330
|
+
event_type=_event_type_for_index(index, len(paired_events)),
|
|
331
|
+
duration_class=_duration_class(
|
|
332
|
+
event.high_res_event.duration_samples,
|
|
333
|
+
typical_duration=typical_duration,
|
|
334
|
+
long_flash_factor=long_flash_factor,
|
|
335
|
+
),
|
|
336
|
+
aligned_event=event,
|
|
337
|
+
imaging_frame=event.imaging_frame,
|
|
338
|
+
stimulus_flash_start_row=segment.start_row,
|
|
339
|
+
stimulus_flash_stop_row=segment.stop_row,
|
|
340
|
+
stimulus_flash_duration_rows=segment.stop_row - segment.start_row,
|
|
341
|
+
)
|
|
342
|
+
for index, (event, segment) in enumerate(
|
|
343
|
+
zip(paired_events, flash_segments, strict=True),
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _classified_windows(
|
|
349
|
+
*,
|
|
350
|
+
events: tuple[ClassifiedPhotodiodeEvent, ...],
|
|
351
|
+
epoch_runs: tuple[_StimulusEpochRun, ...],
|
|
352
|
+
epoch_names: dict[int, str],
|
|
353
|
+
) -> tuple[ClassifiedStimulusWindow, ...]:
|
|
354
|
+
"""Build stimulus windows from neighboring classified events.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
events: Classified photodiode events.
|
|
358
|
+
epoch_runs: Contiguous positive epoch runs.
|
|
359
|
+
epoch_names: Epoch number to name mapping.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
One window per epoch run.
|
|
363
|
+
"""
|
|
364
|
+
windows: list[ClassifiedStimulusWindow] = []
|
|
365
|
+
for index, epoch_run in enumerate(epoch_runs):
|
|
366
|
+
start_event = events[index]
|
|
367
|
+
stop_event = events[index + 1]
|
|
368
|
+
_validate_window_contract(
|
|
369
|
+
start_event=start_event,
|
|
370
|
+
stop_event=stop_event,
|
|
371
|
+
epoch_run=epoch_run,
|
|
372
|
+
)
|
|
373
|
+
windows.append(
|
|
374
|
+
ClassifiedStimulusWindow(
|
|
375
|
+
index=index,
|
|
376
|
+
start_event=start_event,
|
|
377
|
+
stop_event=stop_event,
|
|
378
|
+
start_frame=start_event.imaging_frame,
|
|
379
|
+
stop_frame=stop_event.imaging_frame,
|
|
380
|
+
epoch_number=epoch_run.epoch_number,
|
|
381
|
+
epoch_name=epoch_names.get(
|
|
382
|
+
epoch_run.epoch_number,
|
|
383
|
+
f"epoch_{epoch_run.epoch_number}",
|
|
384
|
+
),
|
|
385
|
+
stimulus_start_row=epoch_run.start_row,
|
|
386
|
+
stimulus_stop_row=epoch_run.stop_row,
|
|
387
|
+
),
|
|
388
|
+
)
|
|
389
|
+
return tuple(windows)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _validate_window_contract(
|
|
393
|
+
*,
|
|
394
|
+
start_event: ClassifiedPhotodiodeEvent,
|
|
395
|
+
stop_event: ClassifiedPhotodiodeEvent,
|
|
396
|
+
epoch_run: _StimulusEpochRun,
|
|
397
|
+
) -> None:
|
|
398
|
+
"""Validate one stimulus epoch run against its bounding flashes.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
start_event: Classified event at the epoch start.
|
|
402
|
+
stop_event: Classified event at the next boundary.
|
|
403
|
+
epoch_run: Stimulus data epoch run.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
None.
|
|
407
|
+
"""
|
|
408
|
+
if stop_event.imaging_frame <= start_event.imaging_frame:
|
|
409
|
+
msg = (
|
|
410
|
+
"Classified event frames must be increasing; "
|
|
411
|
+
f"got {start_event.imaging_frame} then {stop_event.imaging_frame}"
|
|
412
|
+
)
|
|
413
|
+
raise ValueError(msg)
|
|
414
|
+
if epoch_run.start_row != start_event.stimulus_flash_start_row:
|
|
415
|
+
msg = (
|
|
416
|
+
"Stimulus epoch run does not start at its photodiode flash: "
|
|
417
|
+
f"epoch_start_row={epoch_run.start_row}, "
|
|
418
|
+
f"flash_start_row={start_event.stimulus_flash_start_row}"
|
|
419
|
+
)
|
|
420
|
+
raise ValueError(msg)
|
|
421
|
+
if epoch_run.stop_row != stop_event.stimulus_flash_start_row:
|
|
422
|
+
msg = (
|
|
423
|
+
"Stimulus epoch run does not stop at the next photodiode flash: "
|
|
424
|
+
f"epoch_stop_row={epoch_run.stop_row}, "
|
|
425
|
+
f"next_flash_start_row={stop_event.stimulus_flash_start_row}"
|
|
426
|
+
)
|
|
427
|
+
raise ValueError(msg)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _event_type_for_index(index: int, event_count: int) -> PhotodiodeEventType:
|
|
431
|
+
"""Return the stimulus timing role for one ordered event.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
index: Zero-based event index.
|
|
435
|
+
event_count: Total event count.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Event type label.
|
|
439
|
+
"""
|
|
440
|
+
if index == 0:
|
|
441
|
+
return "stimulus_start"
|
|
442
|
+
if index == event_count - 1:
|
|
443
|
+
return "stimulus_end"
|
|
444
|
+
return "trial_transition"
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _duration_class(
|
|
448
|
+
duration_samples: int,
|
|
449
|
+
*,
|
|
450
|
+
typical_duration: float,
|
|
451
|
+
long_flash_factor: float,
|
|
452
|
+
) -> PhotodiodeDurationClass:
|
|
453
|
+
"""Classify one high-rate photodiode duration as short or long.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
duration_samples: Measured high-rate event duration.
|
|
457
|
+
typical_duration: Typical transition duration.
|
|
458
|
+
long_flash_factor: Multiple used to call a duration long.
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Duration class label.
|
|
462
|
+
"""
|
|
463
|
+
if duration_samples >= typical_duration * long_flash_factor:
|
|
464
|
+
return "long"
|
|
465
|
+
return "short"
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _typical_transition_duration(
|
|
469
|
+
paired_events: tuple[AlignedPhotodiodeEvent, ...],
|
|
470
|
+
) -> float:
|
|
471
|
+
"""Estimate the typical high-rate transition-flash duration.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
paired_events: Ordered high-rate-to-frame photodiode events.
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
Median duration in high-rate samples.
|
|
478
|
+
"""
|
|
479
|
+
transition_events = paired_events[1:-1] if len(paired_events) > 2 else paired_events
|
|
480
|
+
durations = np.asarray(
|
|
481
|
+
[event.high_res_event.duration_samples for event in transition_events],
|
|
482
|
+
dtype=np.float64,
|
|
483
|
+
)
|
|
484
|
+
return float(np.median(durations))
|