TwoPy 0.1.8__tar.gz → 0.1.9__tar.gz
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-0.1.8 → twopy-0.1.9}/PKG-INFO +1 -1
- {twopy-0.1.8 → twopy-0.1.9}/pyproject.toml +1 -1
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/__init__.py +36 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/__init__.py +40 -0
- twopy-0.1.9/src/twopy/analysis/group_matching.py +463 -0
- twopy-0.1.9/src/twopy/analysis/response_maps.py +726 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis_cache.py +10 -2
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/database/__init__.py +2 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/database/search.py +34 -1
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/filenames.py +4 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/controls.py +396 -67
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/database_search.py +51 -6
- twopy-0.1.9/src/twopy/napari/group_matching.py +585 -0
- twopy-0.1.9/src/twopy/napari/group_matching_images.py +283 -0
- twopy-0.1.9/src/twopy/napari/group_matching_roi.py +1147 -0
- twopy-0.1.9/src/twopy/napari/latest_worker.py +144 -0
- twopy-0.1.9/src/twopy/napari/loaded_recordings_csv.py +110 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/loading.py +50 -2
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/data.py +94 -1
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/docks/options_panel.py +18 -0
- twopy-0.1.9/src/twopy/napari/plotting/docks/response_map_area.py +450 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/docks/response_plot_widget.py +182 -9
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/docks/save_actions.py +22 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/export.py +133 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/export_controls.py +20 -3
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/processing_options.py +27 -0
- twopy-0.1.9/src/twopy/napari/plotting/response_map_colors.py +24 -0
- twopy-0.1.9/src/twopy/napari/plotting/response_map_display.py +131 -0
- twopy-0.1.9/src/twopy/napari/plotting/response_map_options.py +350 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/responses.py +5 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/session.py +20 -11
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/sidebar.py +82 -4
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy.egg-info/PKG-INFO +1 -1
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy.egg-info/SOURCES.txt +14 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_api.py +3 -2
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_background_subtraction.py +13 -13
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_config.py +14 -13
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_conversion.py +13 -13
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_converted.py +10 -10
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_database.py +55 -12
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_filenames.py +2 -0
- twopy-0.1.9/tests/test_group_matching.py +180 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_hdf5_utils.py +2 -2
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_inspection.py +3 -3
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_matlab.py +5 -5
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_napari.py +1563 -134
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_parity.py +3 -3
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_parity_psycho5.py +5 -5
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_persistence.py +9 -9
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_pixel_calibration.py +5 -4
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_real_fixture.py +2 -2
- twopy-0.1.9/tests/test_response_map_area.py +98 -0
- twopy-0.1.9/tests/test_response_maps.py +311 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_response_roi_extraction.py +5 -5
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_roi.py +7 -7
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_saved_analysis.py +2 -2
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_session.py +7 -6
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_stimulus.py +6 -6
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_synchronization.py +2 -2
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_workflow.py +7 -7
- {twopy-0.1.8 → twopy-0.1.9}/README.md +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/setup.cfg +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/_segments.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/_version.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/background_subtraction.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/dff.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/dff_options.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/epoch_mapping.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/motion.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/persistence.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/response_context.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/response_processing/__init__.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/response_processing/apply.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/response_processing/options.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/response_processing/persistence.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/response_processing/qc.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/response_processing/signals.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/response_window_options.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/responses.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/trials.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/analysis/workflow.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/api.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/calibrations/pixel_calibration_profiles.csv +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/calibrations/pixel_size.csv +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/config.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/conversion/__init__.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/conversion/alignment_crop.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/conversion/api.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/conversion/hdf5_writing.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/conversion/matlab_values.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/conversion/source_loading.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/conversion/stimulus_code.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/conversion/types.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/converted.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/database/cache.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/database/catalog.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/database/connection.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/database/generic.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/database/modeled.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/database/sql.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/database/types.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/frame_ranges.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/hdf5_utils.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/inspection.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/matlab.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/__init__.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/__main__.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/dims.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/display.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/display_paths.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/errors.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/interactive.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/launcher.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/live_analysis.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/movie.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/paths.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/__init__.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/dff_options.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/docks/__init__.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/docks/api.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/docks/dynamic_options.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/docks/paths.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/docks/plot_area.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/docks/visibility_state.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/form_controls.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/label_visibility.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/normalization_options.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/options.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/panels.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/response_window_options.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/roi_generation/__init__.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/roi_generation/actions.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/roi_generation/controls.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/roi_generation/options.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/plotting/widgets.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/protocols.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/roi.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/state.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/text.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/trial_timeline.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/types.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/napari/viewer.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/parity/__init__.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/parity/dff.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/parity/psycho5.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/parity/roi.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/parity/roi_extraction.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/parity/saved_analysis.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/photodiode.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/photodiode_classification.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/pixel_calibration.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/pixel_calibration_profiles.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/response_roi_extraction.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/roi.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/roi_extraction.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/roi_mask_cleanup.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/session.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/spatial.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/stimulus.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/synchronization.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy/typing_guards.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy.egg-info/dependency_links.txt +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy.egg-info/entry_points.txt +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy.egg-info/requires.txt +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/src/twopy.egg-info/top_level.txt +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_dff.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_epoch_mapping.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_frame_ranges.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_import.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_motion.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_packaging.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_parity_roi_extraction.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_photodiode_classification.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_pixel_calibration_profiles.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_response_processing.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_response_window_options.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_responses.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_roi_extraction.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_roi_mask_cleanup.py +0 -0
- {twopy-0.1.8 → twopy-0.1.9}/tests/test_trials.py +0 -0
|
@@ -18,12 +18,19 @@ from twopy.analysis import (
|
|
|
18
18
|
DeltaFOverFFitMode,
|
|
19
19
|
DeltaFOverFOptions,
|
|
20
20
|
EpochFrameWindow,
|
|
21
|
+
EpochResponseMap,
|
|
21
22
|
FrameWindow,
|
|
22
23
|
GroupedRoiResponses,
|
|
23
24
|
GroupedRoiResponseSummary,
|
|
24
25
|
InterpolatedEpochMapping,
|
|
25
26
|
LoadedAnalysisOutputs,
|
|
26
27
|
LowPassFilterOptions,
|
|
28
|
+
ManualFovGroupRow,
|
|
29
|
+
ManualRoiMatchRow,
|
|
30
|
+
ManualRoiMatchStatus,
|
|
31
|
+
ResponseMapData,
|
|
32
|
+
ResponseMapMode,
|
|
33
|
+
ResponseMapOptions,
|
|
27
34
|
ResponseProcessingOptions,
|
|
28
35
|
RoiCorrelationScores,
|
|
29
36
|
RoiDeltaFOverF,
|
|
@@ -32,7 +39,9 @@ from twopy.analysis import (
|
|
|
32
39
|
SmoothingOptions,
|
|
33
40
|
WindowedRoiResponse,
|
|
34
41
|
analyze_recording_responses,
|
|
42
|
+
append_manual_roi_match_rows,
|
|
35
43
|
apply_motion_artifact_mask_to_delta_f_over_f,
|
|
44
|
+
compute_recording_response_maps,
|
|
36
45
|
compute_recording_responses,
|
|
37
46
|
compute_roi_delta_f_over_f,
|
|
38
47
|
default_baseline_epoch_number,
|
|
@@ -43,11 +52,20 @@ from twopy.analysis import (
|
|
|
43
52
|
interpolate_stimulus_epochs_to_frame_windows,
|
|
44
53
|
is_baseline_epoch_name,
|
|
45
54
|
load_analysis_outputs,
|
|
55
|
+
load_manual_fov_group_rows,
|
|
56
|
+
load_manual_roi_match_rows,
|
|
57
|
+
load_response_map_data,
|
|
46
58
|
make_frame_windows,
|
|
59
|
+
make_manual_fov_group_rows,
|
|
60
|
+
make_manual_roi_match_rows,
|
|
47
61
|
map_stimulus_epochs_to_frame_windows,
|
|
62
|
+
next_group_cell_id,
|
|
48
63
|
no_baseline_epoch_frame_windows,
|
|
49
64
|
resolve_baseline_frame_windows,
|
|
50
65
|
save_analysis_outputs,
|
|
66
|
+
save_manual_fov_group_rows,
|
|
67
|
+
save_manual_roi_match_rows,
|
|
68
|
+
save_response_map_data,
|
|
51
69
|
select_baseline_frame_windows,
|
|
52
70
|
split_traces_by_frame_windows,
|
|
53
71
|
summarize_epoch_roi_responses,
|
|
@@ -145,6 +163,7 @@ __all__ = [
|
|
|
145
163
|
"AnalysisResponseComputation",
|
|
146
164
|
"analyze_recording_responses",
|
|
147
165
|
"add_twopy_magicgui_controls",
|
|
166
|
+
"append_manual_roi_match_rows",
|
|
148
167
|
"apply_motion_artifact_mask_to_delta_f_over_f",
|
|
149
168
|
"BackgroundCorrectedRoiTraces",
|
|
150
169
|
"BackgroundCorrectionMethod",
|
|
@@ -153,6 +172,7 @@ __all__ = [
|
|
|
153
172
|
"ClassifiedStimulusWindow",
|
|
154
173
|
"classify_recording_photodiode_events",
|
|
155
174
|
"compute_roi_delta_f_over_f",
|
|
175
|
+
"compute_recording_response_maps",
|
|
156
176
|
"compute_recording_responses",
|
|
157
177
|
"CorrelationFilterOptions",
|
|
158
178
|
"convert_recording_to_twopy",
|
|
@@ -167,6 +187,7 @@ __all__ = [
|
|
|
167
187
|
"DeltaFOverFBaselineMode",
|
|
168
188
|
"DeltaFOverFFitMode",
|
|
169
189
|
"DeltaFOverFOptions",
|
|
190
|
+
"EpochResponseMap",
|
|
170
191
|
"EpochFrameWindow",
|
|
171
192
|
"extract_background_corrected_roi_traces",
|
|
172
193
|
"extract_response_watershed_rois",
|
|
@@ -187,6 +208,9 @@ __all__ = [
|
|
|
187
208
|
"is_baseline_epoch_name",
|
|
188
209
|
"launch_napari",
|
|
189
210
|
"load_analysis_outputs",
|
|
211
|
+
"load_response_map_data",
|
|
212
|
+
"load_manual_fov_group_rows",
|
|
213
|
+
"load_manual_roi_match_rows",
|
|
190
214
|
"load_converted_recording",
|
|
191
215
|
"load_pixel_calibrations",
|
|
192
216
|
"load_pixel_calibration_profile_mappings",
|
|
@@ -194,10 +218,16 @@ __all__ = [
|
|
|
194
218
|
"load_source_conversion_inputs",
|
|
195
219
|
"LoadedAnalysisOutputs",
|
|
196
220
|
"LowPassFilterOptions",
|
|
221
|
+
"ManualFovGroupRow",
|
|
222
|
+
"ManualRoiMatchRow",
|
|
223
|
+
"ManualRoiMatchStatus",
|
|
197
224
|
"make_roi_set_from_label_image",
|
|
198
225
|
"make_frame_windows",
|
|
226
|
+
"make_manual_fov_group_rows",
|
|
227
|
+
"make_manual_roi_match_rows",
|
|
199
228
|
"make_roi_set",
|
|
200
229
|
"map_stimulus_epochs_to_frame_windows",
|
|
230
|
+
"next_group_cell_id",
|
|
201
231
|
"no_baseline_epoch_frame_windows",
|
|
202
232
|
"map_stimulus_specific_column",
|
|
203
233
|
"NapariRecordingView",
|
|
@@ -218,6 +248,9 @@ __all__ = [
|
|
|
218
248
|
"PixelCalibrationRow",
|
|
219
249
|
"ResponseNormalization",
|
|
220
250
|
"ResponsePolarity",
|
|
251
|
+
"ResponseMapData",
|
|
252
|
+
"ResponseMapMode",
|
|
253
|
+
"ResponseMapOptions",
|
|
221
254
|
"ResponseProcessingOptions",
|
|
222
255
|
"ResponseStatistic",
|
|
223
256
|
"ResponseWatershedExtraction",
|
|
@@ -238,6 +271,9 @@ __all__ = [
|
|
|
238
271
|
"roi_label_image_from_layer",
|
|
239
272
|
"roi_set_to_label_image",
|
|
240
273
|
"save_analysis_outputs",
|
|
274
|
+
"save_response_map_data",
|
|
275
|
+
"save_manual_fov_group_rows",
|
|
276
|
+
"save_manual_roi_match_rows",
|
|
241
277
|
"save_napari_label_rois",
|
|
242
278
|
"save_roi_set",
|
|
243
279
|
"select_baseline_frame_windows",
|
|
@@ -23,6 +23,19 @@ from twopy.analysis.epoch_mapping import (
|
|
|
23
23
|
InterpolatedEpochMapping,
|
|
24
24
|
interpolate_stimulus_epochs_to_frame_windows,
|
|
25
25
|
)
|
|
26
|
+
from twopy.analysis.group_matching import (
|
|
27
|
+
ManualFovGroupRow,
|
|
28
|
+
ManualRoiMatchRow,
|
|
29
|
+
ManualRoiMatchStatus,
|
|
30
|
+
append_manual_roi_match_rows,
|
|
31
|
+
load_manual_fov_group_rows,
|
|
32
|
+
load_manual_roi_match_rows,
|
|
33
|
+
make_manual_fov_group_rows,
|
|
34
|
+
make_manual_roi_match_rows,
|
|
35
|
+
next_group_cell_id,
|
|
36
|
+
save_manual_fov_group_rows,
|
|
37
|
+
save_manual_roi_match_rows,
|
|
38
|
+
)
|
|
26
39
|
from twopy.analysis.motion import apply_motion_artifact_mask_to_delta_f_over_f
|
|
27
40
|
from twopy.analysis.persistence import (
|
|
28
41
|
ANALYSIS_OUTPUT_FILE_FORMAT,
|
|
@@ -33,6 +46,15 @@ from twopy.analysis.persistence import (
|
|
|
33
46
|
write_response_summary_trials_csv,
|
|
34
47
|
)
|
|
35
48
|
from twopy.analysis.response_context import default_recording_baseline_epoch_number
|
|
49
|
+
from twopy.analysis.response_maps import (
|
|
50
|
+
EpochResponseMap,
|
|
51
|
+
ResponseMapData,
|
|
52
|
+
ResponseMapMode,
|
|
53
|
+
ResponseMapOptions,
|
|
54
|
+
compute_recording_response_maps,
|
|
55
|
+
load_response_map_data,
|
|
56
|
+
save_response_map_data,
|
|
57
|
+
)
|
|
36
58
|
from twopy.analysis.response_processing import (
|
|
37
59
|
CorrelationFilterOptions,
|
|
38
60
|
LowPassFilterOptions,
|
|
@@ -85,12 +107,14 @@ __all__ = [
|
|
|
85
107
|
"BackgroundCorrectionMethod",
|
|
86
108
|
"compute_roi_delta_f_over_f",
|
|
87
109
|
"compute_recording_responses",
|
|
110
|
+
"compute_recording_response_maps",
|
|
88
111
|
"CorrelationFilterOptions",
|
|
89
112
|
"default_baseline_epoch_number",
|
|
90
113
|
"default_recording_baseline_epoch_number",
|
|
91
114
|
"DeltaFOverFBaselineMode",
|
|
92
115
|
"DeltaFOverFOptions",
|
|
93
116
|
"DeltaFOverFFitMode",
|
|
117
|
+
"EpochResponseMap",
|
|
94
118
|
"EpochFrameWindow",
|
|
95
119
|
"extract_background_corrected_roi_traces",
|
|
96
120
|
"frame_windows_from_photodiode_alignment",
|
|
@@ -102,18 +126,34 @@ __all__ = [
|
|
|
102
126
|
"interpolate_stimulus_epochs_to_frame_windows",
|
|
103
127
|
"is_baseline_epoch_name",
|
|
104
128
|
"load_analysis_outputs",
|
|
129
|
+
"load_response_map_data",
|
|
105
130
|
"LoadedAnalysisOutputs",
|
|
106
131
|
"LowPassFilterOptions",
|
|
132
|
+
"ManualFovGroupRow",
|
|
133
|
+
"ManualRoiMatchRow",
|
|
134
|
+
"ManualRoiMatchStatus",
|
|
107
135
|
"make_frame_windows",
|
|
136
|
+
"make_manual_fov_group_rows",
|
|
137
|
+
"make_manual_roi_match_rows",
|
|
108
138
|
"map_stimulus_epochs_to_frame_windows",
|
|
109
139
|
"no_baseline_epoch_frame_windows",
|
|
140
|
+
"next_group_cell_id",
|
|
110
141
|
"RoiDeltaFOverF",
|
|
142
|
+
"ResponseMapData",
|
|
143
|
+
"ResponseMapMode",
|
|
144
|
+
"ResponseMapOptions",
|
|
111
145
|
"ResponseProcessingOptions",
|
|
112
146
|
"RoiCorrelationScores",
|
|
113
147
|
"RoiResponseSummary",
|
|
114
148
|
"RoiResponseTrial",
|
|
115
149
|
"resolve_baseline_frame_windows",
|
|
150
|
+
"append_manual_roi_match_rows",
|
|
151
|
+
"load_manual_fov_group_rows",
|
|
152
|
+
"load_manual_roi_match_rows",
|
|
116
153
|
"save_analysis_outputs",
|
|
154
|
+
"save_response_map_data",
|
|
155
|
+
"save_manual_fov_group_rows",
|
|
156
|
+
"save_manual_roi_match_rows",
|
|
117
157
|
"select_baseline_frame_windows",
|
|
118
158
|
"split_traces_by_frame_windows",
|
|
119
159
|
"SmoothingOptions",
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""Persist manual ROI matches across recordings.
|
|
2
|
+
|
|
3
|
+
Inputs: user-authored ROI match decisions from a group-analysis review.
|
|
4
|
+
Outputs: plain CSV rows that identify which ROI labels belong to the same
|
|
5
|
+
cell, or which ROI labels were reviewed and left unmatched.
|
|
6
|
+
|
|
7
|
+
This module is GUI-independent on purpose. Napari can help users make visual
|
|
8
|
+
decisions, but the scientific audit trail is the explicit match table written
|
|
9
|
+
here.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import csv
|
|
13
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Literal
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ManualFovGroupRow",
|
|
20
|
+
"ManualRoiMatchRow",
|
|
21
|
+
"ManualRoiMatchStatus",
|
|
22
|
+
"append_manual_roi_match_rows",
|
|
23
|
+
"load_manual_fov_group_rows",
|
|
24
|
+
"load_manual_roi_match_rows",
|
|
25
|
+
"make_manual_fov_group_rows",
|
|
26
|
+
"make_manual_roi_match_rows",
|
|
27
|
+
"next_group_cell_id",
|
|
28
|
+
"save_manual_fov_group_rows",
|
|
29
|
+
"save_manual_roi_match_rows",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
ManualRoiMatchStatus = Literal["matched", "unmatched", "ambiguous", "ignored"]
|
|
33
|
+
|
|
34
|
+
_FIELDNAMES = (
|
|
35
|
+
"fov_group_id",
|
|
36
|
+
"group_cell_id",
|
|
37
|
+
"recording_path",
|
|
38
|
+
"roi_label",
|
|
39
|
+
"status",
|
|
40
|
+
"note",
|
|
41
|
+
)
|
|
42
|
+
_FOV_FIELDNAMES = ("fov_group_id", "recording_path", "note")
|
|
43
|
+
_STATUSES: tuple[ManualRoiMatchStatus, ...] = (
|
|
44
|
+
"matched",
|
|
45
|
+
"unmatched",
|
|
46
|
+
"ambiguous",
|
|
47
|
+
"ignored",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class ManualFovGroupRow:
|
|
53
|
+
"""One row assigning a recording to a manually reviewed FOV group.
|
|
54
|
+
|
|
55
|
+
Inputs: one converted recording path and the FOV group label chosen by the
|
|
56
|
+
user.
|
|
57
|
+
Outputs: a typed row that can be saved beside downstream group-analysis
|
|
58
|
+
artifacts.
|
|
59
|
+
|
|
60
|
+
FOV grouping is deliberately separate from ROI matching. A recording can be
|
|
61
|
+
visually assigned to a FOV before any cell-level match decisions are made.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
fov_group_id: str
|
|
65
|
+
recording_path: Path
|
|
66
|
+
note: str = ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class ManualRoiMatchRow:
|
|
71
|
+
"""One row in a manual cross-recording ROI match table.
|
|
72
|
+
|
|
73
|
+
Inputs: one recording path, one ROI label, and the group-cell decision that
|
|
74
|
+
the user made for it.
|
|
75
|
+
Outputs: a typed row that can be written to CSV or loaded from CSV.
|
|
76
|
+
|
|
77
|
+
``group_cell_id`` is scoped to the saved match file. Rows that share the
|
|
78
|
+
same ``group_cell_id`` and ``status="matched"`` are treated as the same
|
|
79
|
+
visually assigned cell across recordings.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
fov_group_id: str
|
|
83
|
+
group_cell_id: int
|
|
84
|
+
recording_path: Path
|
|
85
|
+
roi_label: str
|
|
86
|
+
status: ManualRoiMatchStatus
|
|
87
|
+
note: str = ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def make_manual_fov_group_rows(
|
|
91
|
+
recording_groups: Mapping[Path, str],
|
|
92
|
+
*,
|
|
93
|
+
note: str = "",
|
|
94
|
+
notes: Mapping[Path, str] | None = None,
|
|
95
|
+
) -> tuple[ManualFovGroupRow, ...]:
|
|
96
|
+
"""Create validated FOV-group rows from recording assignments.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
recording_groups: Mapping from recording path to non-empty FOV group
|
|
100
|
+
id.
|
|
101
|
+
note: Optional fallback free-text note stored on each row.
|
|
102
|
+
notes: Optional per-recording free-text notes stored on matching rows.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Tuple of FOV group rows sorted by recording path.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
ValueError: If no recordings are assigned, a path is empty, or a FOV id
|
|
109
|
+
is empty.
|
|
110
|
+
"""
|
|
111
|
+
if len(recording_groups) == 0:
|
|
112
|
+
msg = "Manual FOV group rows require at least one recording assignment."
|
|
113
|
+
raise ValueError(msg)
|
|
114
|
+
return tuple(
|
|
115
|
+
ManualFovGroupRow(
|
|
116
|
+
fov_group_id=_validate_fov_group_id(fov_group_id),
|
|
117
|
+
recording_path=_validate_recording_path(recording_path),
|
|
118
|
+
note=notes.get(recording_path, note) if notes is not None else note,
|
|
119
|
+
)
|
|
120
|
+
for recording_path, fov_group_id in sorted(
|
|
121
|
+
recording_groups.items(),
|
|
122
|
+
key=lambda item: str(item[0]),
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def make_manual_roi_match_rows(
|
|
128
|
+
recording_rois: Mapping[Path, str],
|
|
129
|
+
*,
|
|
130
|
+
group_cell_id: int,
|
|
131
|
+
fov_group_id: str = "",
|
|
132
|
+
status: ManualRoiMatchStatus = "matched",
|
|
133
|
+
note: str = "",
|
|
134
|
+
) -> tuple[ManualRoiMatchRow, ...]:
|
|
135
|
+
"""Create validated match-table rows for one user decision.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
recording_rois: Mapping from recording path to ROI label.
|
|
139
|
+
group_cell_id: Positive cell-group identifier for this decision.
|
|
140
|
+
fov_group_id: Optional user-facing FOV group identifier.
|
|
141
|
+
status: Decision status for every emitted row.
|
|
142
|
+
note: Optional free-text note stored on each row.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Tuple of manual ROI match rows sorted by recording path.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
ValueError: If the group id, status, paths, or ROI labels are invalid.
|
|
149
|
+
"""
|
|
150
|
+
_validate_group_cell_id(group_cell_id)
|
|
151
|
+
_validate_status(status)
|
|
152
|
+
if len(recording_rois) == 0:
|
|
153
|
+
msg = "Manual ROI match rows require at least one recording ROI."
|
|
154
|
+
raise ValueError(msg)
|
|
155
|
+
|
|
156
|
+
rows: list[ManualRoiMatchRow] = []
|
|
157
|
+
for recording_path, roi_label in sorted(
|
|
158
|
+
recording_rois.items(),
|
|
159
|
+
key=lambda item: str(item[0]),
|
|
160
|
+
):
|
|
161
|
+
rows.append(
|
|
162
|
+
ManualRoiMatchRow(
|
|
163
|
+
fov_group_id=fov_group_id,
|
|
164
|
+
group_cell_id=group_cell_id,
|
|
165
|
+
recording_path=_validate_recording_path(recording_path),
|
|
166
|
+
roi_label=_validate_roi_label(roi_label),
|
|
167
|
+
status=status,
|
|
168
|
+
note=note,
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
return tuple(rows)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def save_manual_fov_group_rows(
|
|
175
|
+
rows: Iterable[ManualFovGroupRow],
|
|
176
|
+
path: Path,
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Write manual FOV grouping rows to a CSV file.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
rows: Manual FOV group rows to persist.
|
|
182
|
+
path: Destination CSV path.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
None.
|
|
186
|
+
"""
|
|
187
|
+
_write_csv_rows(
|
|
188
|
+
path,
|
|
189
|
+
fieldnames=_FOV_FIELDNAMES,
|
|
190
|
+
rows=(_fov_row_to_csv(row) for row in rows),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def load_manual_fov_group_rows(path: Path) -> tuple[ManualFovGroupRow, ...]:
|
|
195
|
+
"""Load manual FOV grouping rows from a CSV file.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
path: CSV file written by ``save_manual_fov_group_rows``.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Tuple of validated FOV grouping rows.
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
ValueError: If the file contains missing columns or invalid values.
|
|
205
|
+
"""
|
|
206
|
+
csv_rows = _read_required_csv_rows(
|
|
207
|
+
path,
|
|
208
|
+
fieldnames=_FOV_FIELDNAMES,
|
|
209
|
+
table_name="Manual FOV group file",
|
|
210
|
+
)
|
|
211
|
+
return tuple(_fov_row_from_csv(row) for row in csv_rows)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def append_manual_roi_match_rows(
|
|
215
|
+
path: Path,
|
|
216
|
+
rows: Sequence[ManualRoiMatchRow],
|
|
217
|
+
) -> tuple[ManualRoiMatchRow, ...]:
|
|
218
|
+
"""Append manual ROI match rows to an existing CSV file.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
path: CSV match table path. Missing files are created.
|
|
222
|
+
rows: Rows to append after the existing rows.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
All rows written to the file.
|
|
226
|
+
"""
|
|
227
|
+
existing_rows = load_manual_roi_match_rows(path) if path.exists() else ()
|
|
228
|
+
all_rows = (*existing_rows, *tuple(rows))
|
|
229
|
+
save_manual_roi_match_rows(all_rows, path)
|
|
230
|
+
return all_rows
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def save_manual_roi_match_rows(
|
|
234
|
+
rows: Iterable[ManualRoiMatchRow],
|
|
235
|
+
path: Path,
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Write manual ROI match rows to a CSV file.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
rows: Manual match rows to persist.
|
|
241
|
+
path: Destination CSV path.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
None.
|
|
245
|
+
|
|
246
|
+
The CSV file is intentionally plain text so group-analysis decisions can be
|
|
247
|
+
inspected, diffed, and edited without twopy.
|
|
248
|
+
"""
|
|
249
|
+
_write_csv_rows(
|
|
250
|
+
path,
|
|
251
|
+
fieldnames=_FIELDNAMES,
|
|
252
|
+
rows=(_row_to_csv(row) for row in rows),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def load_manual_roi_match_rows(path: Path) -> tuple[ManualRoiMatchRow, ...]:
|
|
257
|
+
"""Load manual ROI match rows from a CSV file.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
path: CSV file written by ``save_manual_roi_match_rows``.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Tuple of validated manual ROI match rows.
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
ValueError: If the file contains missing columns or invalid values.
|
|
267
|
+
"""
|
|
268
|
+
input_path = path.expanduser()
|
|
269
|
+
csv_rows = _read_required_csv_rows(
|
|
270
|
+
input_path,
|
|
271
|
+
fieldnames=_FIELDNAMES,
|
|
272
|
+
table_name="Manual ROI match file",
|
|
273
|
+
)
|
|
274
|
+
return tuple(_row_from_csv(row, input_path=input_path) for row in csv_rows)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def next_group_cell_id(rows: Sequence[ManualRoiMatchRow]) -> int:
|
|
278
|
+
"""Return the next positive group-cell id for a match table.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
rows: Existing manual match rows.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
One plus the highest existing group-cell id, or ``1`` for an empty
|
|
285
|
+
match table.
|
|
286
|
+
"""
|
|
287
|
+
if len(rows) == 0:
|
|
288
|
+
return 1
|
|
289
|
+
return max(row.group_cell_id for row in rows) + 1
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _write_csv_rows(
|
|
293
|
+
path: Path,
|
|
294
|
+
*,
|
|
295
|
+
fieldnames: Sequence[str],
|
|
296
|
+
rows: Iterable[Mapping[str, str]],
|
|
297
|
+
) -> None:
|
|
298
|
+
"""Write validated dictionaries to a required-header CSV file."""
|
|
299
|
+
output_path = path.expanduser()
|
|
300
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
301
|
+
with output_path.open("w", encoding="utf-8", newline="") as csv_file:
|
|
302
|
+
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
|
|
303
|
+
writer.writeheader()
|
|
304
|
+
writer.writerows(rows)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _read_required_csv_rows(
|
|
308
|
+
path: Path,
|
|
309
|
+
*,
|
|
310
|
+
fieldnames: Sequence[str],
|
|
311
|
+
table_name: str,
|
|
312
|
+
) -> tuple[dict[str, str], ...]:
|
|
313
|
+
"""Read rows from a CSV table that must contain known columns."""
|
|
314
|
+
input_path = path.expanduser()
|
|
315
|
+
with input_path.open("r", encoding="utf-8", newline="") as csv_file:
|
|
316
|
+
reader = csv.DictReader(csv_file)
|
|
317
|
+
if reader.fieldnames is None:
|
|
318
|
+
msg = f"{table_name} is empty: {input_path}"
|
|
319
|
+
raise ValueError(msg)
|
|
320
|
+
missing = tuple(field for field in fieldnames if field not in reader.fieldnames)
|
|
321
|
+
if len(missing) > 0:
|
|
322
|
+
msg = f"{table_name} missing columns {missing}: {input_path}"
|
|
323
|
+
raise ValueError(msg)
|
|
324
|
+
return tuple(
|
|
325
|
+
{
|
|
326
|
+
field: _required_csv_cell(row, field=field, input_path=input_path)
|
|
327
|
+
for field in fieldnames
|
|
328
|
+
}
|
|
329
|
+
for row in reader
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _required_csv_cell(
|
|
334
|
+
row: Mapping[str, str | None],
|
|
335
|
+
*,
|
|
336
|
+
field: str,
|
|
337
|
+
input_path: Path,
|
|
338
|
+
) -> str:
|
|
339
|
+
"""Return one required CSV cell value."""
|
|
340
|
+
value = row[field]
|
|
341
|
+
if value is None:
|
|
342
|
+
msg = f"Manual group CSV row missing {field!r} value: {input_path}"
|
|
343
|
+
raise ValueError(msg)
|
|
344
|
+
return value
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _fov_row_to_csv(row: ManualFovGroupRow) -> dict[str, str]:
|
|
348
|
+
"""Convert one FOV group row into CSV strings."""
|
|
349
|
+
return {
|
|
350
|
+
"fov_group_id": _validate_fov_group_id(row.fov_group_id),
|
|
351
|
+
"recording_path": str(_validate_recording_path(row.recording_path)),
|
|
352
|
+
"note": row.note,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _fov_row_from_csv(values: Mapping[str, str]) -> ManualFovGroupRow:
|
|
357
|
+
"""Parse one FOV grouping CSV row."""
|
|
358
|
+
return ManualFovGroupRow(
|
|
359
|
+
fov_group_id=_validate_fov_group_id(values["fov_group_id"]),
|
|
360
|
+
recording_path=_recording_path_from_csv(values["recording_path"]),
|
|
361
|
+
note=values["note"],
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _row_to_csv(row: ManualRoiMatchRow) -> dict[str, str]:
|
|
366
|
+
"""Convert one validated row into CSV strings."""
|
|
367
|
+
_validate_group_cell_id(row.group_cell_id)
|
|
368
|
+
_validate_status(row.status)
|
|
369
|
+
return {
|
|
370
|
+
"fov_group_id": row.fov_group_id,
|
|
371
|
+
"group_cell_id": str(row.group_cell_id),
|
|
372
|
+
"recording_path": str(_validate_recording_path(row.recording_path)),
|
|
373
|
+
"roi_label": _validate_roi_label(row.roi_label),
|
|
374
|
+
"status": row.status,
|
|
375
|
+
"note": row.note,
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _row_from_csv(
|
|
380
|
+
values: Mapping[str, str],
|
|
381
|
+
*,
|
|
382
|
+
input_path: Path,
|
|
383
|
+
) -> ManualRoiMatchRow:
|
|
384
|
+
"""Parse and validate one CSV row."""
|
|
385
|
+
try:
|
|
386
|
+
group_cell_id = int(values["group_cell_id"])
|
|
387
|
+
except ValueError as error:
|
|
388
|
+
msg = (
|
|
389
|
+
"Manual ROI match row has non-integer group_cell_id "
|
|
390
|
+
f"{values['group_cell_id']!r}: {input_path}"
|
|
391
|
+
)
|
|
392
|
+
raise ValueError(msg) from error
|
|
393
|
+
status = _parse_status(values["status"], input_path=input_path)
|
|
394
|
+
return ManualRoiMatchRow(
|
|
395
|
+
fov_group_id=values["fov_group_id"],
|
|
396
|
+
group_cell_id=_validate_group_cell_id(group_cell_id),
|
|
397
|
+
recording_path=_recording_path_from_csv(values["recording_path"]),
|
|
398
|
+
roi_label=_validate_roi_label(values["roi_label"]),
|
|
399
|
+
status=status,
|
|
400
|
+
note=values["note"],
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _parse_status(value: str, *, input_path: Path) -> ManualRoiMatchStatus:
|
|
405
|
+
"""Return a typed status from CSV text."""
|
|
406
|
+
if value == "matched":
|
|
407
|
+
return "matched"
|
|
408
|
+
if value == "unmatched":
|
|
409
|
+
return "unmatched"
|
|
410
|
+
if value == "ambiguous":
|
|
411
|
+
return "ambiguous"
|
|
412
|
+
if value == "ignored":
|
|
413
|
+
return "ignored"
|
|
414
|
+
msg = f"Manual ROI match status {value!r} is invalid: {input_path}"
|
|
415
|
+
raise ValueError(msg)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _validate_status(status: ManualRoiMatchStatus) -> ManualRoiMatchStatus:
|
|
419
|
+
"""Return a status after validating it is one of the saved literals."""
|
|
420
|
+
if status not in _STATUSES:
|
|
421
|
+
msg = f"Manual ROI match status {status!r} is invalid."
|
|
422
|
+
raise ValueError(msg)
|
|
423
|
+
return status
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _validate_group_cell_id(group_cell_id: int) -> int:
|
|
427
|
+
"""Return a positive group-cell id."""
|
|
428
|
+
if group_cell_id < 1:
|
|
429
|
+
msg = f"Manual ROI group_cell_id must be positive; got {group_cell_id}."
|
|
430
|
+
raise ValueError(msg)
|
|
431
|
+
return group_cell_id
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _validate_recording_path(recording_path: Path) -> Path:
|
|
435
|
+
"""Return a non-empty recording path."""
|
|
436
|
+
if str(recording_path) == "":
|
|
437
|
+
msg = "Manual group recording_path cannot be empty."
|
|
438
|
+
raise ValueError(msg)
|
|
439
|
+
return recording_path.expanduser()
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _recording_path_from_csv(value: str) -> Path:
|
|
443
|
+
"""Return a validated recording path from raw CSV text."""
|
|
444
|
+
if value.strip() == "":
|
|
445
|
+
msg = "Manual group recording_path cannot be empty."
|
|
446
|
+
raise ValueError(msg)
|
|
447
|
+
return _validate_recording_path(Path(value))
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _validate_fov_group_id(fov_group_id: str) -> str:
|
|
451
|
+
"""Return a non-empty FOV group id."""
|
|
452
|
+
if fov_group_id == "":
|
|
453
|
+
msg = "Manual FOV group id cannot be empty."
|
|
454
|
+
raise ValueError(msg)
|
|
455
|
+
return fov_group_id
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _validate_roi_label(roi_label: str) -> str:
|
|
459
|
+
"""Return a non-empty ROI label."""
|
|
460
|
+
if roi_label == "":
|
|
461
|
+
msg = "Manual ROI match roi_label cannot be empty."
|
|
462
|
+
raise ValueError(msg)
|
|
463
|
+
return roi_label
|