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,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))