TwoPy 0.1.0__tar.gz → 0.1.2__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.
Files changed (120) hide show
  1. {twopy-0.1.0 → twopy-0.1.2}/PKG-INFO +33 -40
  2. {twopy-0.1.0 → twopy-0.1.2}/README.md +32 -39
  3. {twopy-0.1.0 → twopy-0.1.2}/pyproject.toml +1 -1
  4. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/__init__.py +11 -2
  5. twopy-0.1.2/src/twopy/_version.py +14 -0
  6. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/analysis/__init__.py +12 -0
  7. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/analysis/persistence.py +37 -0
  8. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/analysis/response_processing/__init__.py +6 -0
  9. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/analysis/response_processing/apply.py +75 -12
  10. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/analysis/response_processing/options.py +30 -6
  11. twopy-0.1.2/src/twopy/analysis/response_processing/persistence.py +261 -0
  12. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/analysis/response_processing/signals.py +70 -1
  13. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/analysis/workflow.py +64 -0
  14. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/interactive.py +65 -7
  15. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/launcher.py +13 -3
  16. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/plotting/data.py +31 -7
  17. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/plotting/docks.py +133 -20
  18. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/plotting/options.py +19 -19
  19. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/plotting/panels.py +0 -20
  20. twopy-0.1.2/src/twopy/napari/plotting/processing_options.py +390 -0
  21. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/responses.py +14 -1
  22. {twopy-0.1.0 → twopy-0.1.2}/src/twopy.egg-info/PKG-INFO +33 -40
  23. {twopy-0.1.0 → twopy-0.1.2}/src/twopy.egg-info/SOURCES.txt +3 -0
  24. {twopy-0.1.0 → twopy-0.1.2}/tests/test_import.py +2 -1
  25. {twopy-0.1.0 → twopy-0.1.2}/tests/test_napari.py +311 -1
  26. twopy-0.1.2/tests/test_packaging.py +63 -0
  27. {twopy-0.1.0 → twopy-0.1.2}/tests/test_persistence.py +69 -0
  28. {twopy-0.1.0 → twopy-0.1.2}/tests/test_real_fixture.py +31 -0
  29. {twopy-0.1.0 → twopy-0.1.2}/tests/test_response_processing.py +36 -0
  30. twopy-0.1.0/tests/test_packaging.py +0 -32
  31. {twopy-0.1.0 → twopy-0.1.2}/setup.cfg +0 -0
  32. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/_segments.py +0 -0
  33. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/analysis/background_subtraction.py +0 -0
  34. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/analysis/dff.py +0 -0
  35. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/analysis/epoch_mapping.py +0 -0
  36. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/analysis/motion.py +0 -0
  37. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/analysis/response_processing/qc.py +0 -0
  38. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/analysis/responses.py +0 -0
  39. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/analysis/trials.py +0 -0
  40. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/api.py +0 -0
  41. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/config.py +0 -0
  42. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/conversion/__init__.py +0 -0
  43. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/conversion/alignment_crop.py +0 -0
  44. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/conversion/api.py +0 -0
  45. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/conversion/frame_ranges.py +0 -0
  46. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/conversion/hdf5_writing.py +0 -0
  47. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/conversion/matlab_values.py +0 -0
  48. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/conversion/source_loading.py +0 -0
  49. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/conversion/stimulus_code.py +0 -0
  50. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/conversion/types.py +0 -0
  51. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/converted.py +0 -0
  52. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/database/__init__.py +0 -0
  53. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/database/cache.py +0 -0
  54. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/database/catalog.py +0 -0
  55. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/database/connection.py +0 -0
  56. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/database/generic.py +0 -0
  57. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/database/modeled.py +0 -0
  58. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/database/sql.py +0 -0
  59. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/database/types.py +0 -0
  60. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/inspection.py +0 -0
  61. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/matlab.py +0 -0
  62. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/__init__.py +0 -0
  63. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/__main__.py +0 -0
  64. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/constants.py +0 -0
  65. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/controls.py +0 -0
  66. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/display.py +0 -0
  67. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/display_paths.py +0 -0
  68. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/layout.py +0 -0
  69. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/loading.py +0 -0
  70. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/movie.py +0 -0
  71. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/paths.py +0 -0
  72. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/plotting/__init__.py +0 -0
  73. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/plotting/export.py +0 -0
  74. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/plotting/export_controls.py +0 -0
  75. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/plotting/label_visibility.py +0 -0
  76. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/plotting/widgets.py +0 -0
  77. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/protocols.py +0 -0
  78. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/roi.py +0 -0
  79. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/session.py +0 -0
  80. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/state.py +0 -0
  81. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/text.py +0 -0
  82. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/types.py +0 -0
  83. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/napari/viewer.py +0 -0
  84. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/parity/__init__.py +0 -0
  85. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/parity/dff.py +0 -0
  86. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/parity/roi.py +0 -0
  87. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/parity/saved_analysis.py +0 -0
  88. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/photodiode.py +0 -0
  89. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/photodiode_classification.py +0 -0
  90. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/roi.py +0 -0
  91. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/session.py +0 -0
  92. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/spatial.py +0 -0
  93. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/stimulus.py +0 -0
  94. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/synchronization.py +0 -0
  95. {twopy-0.1.0 → twopy-0.1.2}/src/twopy/typing_guards.py +0 -0
  96. {twopy-0.1.0 → twopy-0.1.2}/src/twopy.egg-info/dependency_links.txt +0 -0
  97. {twopy-0.1.0 → twopy-0.1.2}/src/twopy.egg-info/entry_points.txt +0 -0
  98. {twopy-0.1.0 → twopy-0.1.2}/src/twopy.egg-info/requires.txt +0 -0
  99. {twopy-0.1.0 → twopy-0.1.2}/src/twopy.egg-info/top_level.txt +0 -0
  100. {twopy-0.1.0 → twopy-0.1.2}/tests/test_api.py +0 -0
  101. {twopy-0.1.0 → twopy-0.1.2}/tests/test_background_subtraction.py +0 -0
  102. {twopy-0.1.0 → twopy-0.1.2}/tests/test_config.py +0 -0
  103. {twopy-0.1.0 → twopy-0.1.2}/tests/test_conversion.py +0 -0
  104. {twopy-0.1.0 → twopy-0.1.2}/tests/test_converted.py +0 -0
  105. {twopy-0.1.0 → twopy-0.1.2}/tests/test_database.py +0 -0
  106. {twopy-0.1.0 → twopy-0.1.2}/tests/test_dff.py +0 -0
  107. {twopy-0.1.0 → twopy-0.1.2}/tests/test_epoch_mapping.py +0 -0
  108. {twopy-0.1.0 → twopy-0.1.2}/tests/test_inspection.py +0 -0
  109. {twopy-0.1.0 → twopy-0.1.2}/tests/test_matlab.py +0 -0
  110. {twopy-0.1.0 → twopy-0.1.2}/tests/test_motion.py +0 -0
  111. {twopy-0.1.0 → twopy-0.1.2}/tests/test_parity.py +0 -0
  112. {twopy-0.1.0 → twopy-0.1.2}/tests/test_photodiode_classification.py +0 -0
  113. {twopy-0.1.0 → twopy-0.1.2}/tests/test_responses.py +0 -0
  114. {twopy-0.1.0 → twopy-0.1.2}/tests/test_roi.py +0 -0
  115. {twopy-0.1.0 → twopy-0.1.2}/tests/test_saved_analysis.py +0 -0
  116. {twopy-0.1.0 → twopy-0.1.2}/tests/test_session.py +0 -0
  117. {twopy-0.1.0 → twopy-0.1.2}/tests/test_stimulus.py +0 -0
  118. {twopy-0.1.0 → twopy-0.1.2}/tests/test_synchronization.py +0 -0
  119. {twopy-0.1.0 → twopy-0.1.2}/tests/test_trials.py +0 -0
  120. {twopy-0.1.0 → twopy-0.1.2}/tests/test_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: twopy
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Two-photon imaging analysis tool with a napari interface.
5
5
  Author-email: Gustavo Madeira Santana <fromweb1@gumadeiras.com>
6
6
  License-Expression: MIT
@@ -22,6 +22,8 @@ Requires-Dist: ty<0.1,>=0.0.34
22
22
 
23
23
  Two-photon imaging analysis tool for the Clark Lab output format.
24
24
 
25
+ ![twopy napari interface](docs/assets/twopy-napari.png)
26
+
25
27
  ## Getting Started
26
28
 
27
29
  twopy lets you open two-photon recordings, draw ROIs, plot responses in real time,
@@ -36,25 +38,24 @@ the converted files, so the original source files remain separate from twopy's o
36
38
 
37
39
  Examples use micromamba, but any conda-compatible environment manager should work.
38
40
 
39
- Run this once from the twopy folder to install everything you need:
41
+ Preferred setup is a fresh Python 3.13 environment with the published PyPI
42
+ package:
40
43
 
41
44
  ```sh
42
- micromamba env create -f environment.yml
43
- micromamba activate twopy
44
- micromamba run -n twopy pre-commit install
45
- cp config.example.yml config.yml
45
+ micromamba create -n twopy -c conda-forge python=3.13 pip -y && micromamba run -n twopy python -m pip install twopy
46
46
  ```
47
47
 
48
- Then edit `config.yml` so the paths match your computer. The example file
49
- explains each setting in plain language. `config.yml` stays local to your
50
- machine and is not tracked by git.
51
-
52
- If the `twopy` command is missing after setup, refresh the editable install:
48
+ Verify the install:
53
49
 
54
50
  ```sh
55
- micromamba run -n twopy python -m pip install -e .
51
+ micromamba run -n twopy python -c 'import twopy; print(twopy.__name__)'
56
52
  ```
57
53
 
54
+ If you are working from a source checkout, copy `config.example.yml` to
55
+ `config.yml`, then edit `config.yml` so the paths match your computer. The
56
+ example file explains each setting in plain language. `config.yml` stays local
57
+ to your machine and is not tracked by git.
58
+
58
59
  ### Start The GUI
59
60
 
60
61
  Start napari from the twopy environment:
@@ -96,9 +97,18 @@ Basic GUI flow:
96
97
 
97
98
  ## Setup Details
98
99
 
99
- The environment installs twopy as an editable package, so the `twopy` terminal
100
- command is available after activating the environment. If the environment
101
- already existed before the command was added, refresh the editable install:
100
+ For development, install from the repository so local code edits are used:
101
+
102
+ ```sh
103
+ micromamba env create -f environment.yml
104
+ micromamba activate twopy
105
+ micromamba run -n twopy pre-commit install
106
+ ```
107
+
108
+ The development environment installs twopy as an editable package, so the
109
+ `twopy` terminal command is available after activating the environment. If the
110
+ environment already existed before the command was added, refresh the editable
111
+ install:
102
112
 
103
113
  ```sh
104
114
  micromamba run -n twopy python -m pip install -e .
@@ -113,31 +123,6 @@ micromamba run -n twopy pre-commit run --all-files
113
123
  The installed pre-commit hook runs ruff, ty, and the unit tests before each
114
124
  commit.
115
125
 
116
- ## Release
117
-
118
- PyPI publishing uses GitHub Actions Trusted Publishing. No PyPI API token secret
119
- is required.
120
-
121
- One-time setup in PyPI:
122
-
123
- 1. Add a trusted publisher for the `twopy` project.
124
- 2. Use owner `gumadeiras`, repository `twopy`, workflow
125
- `publish-to-pypi.yml`, and environment `pypi`.
126
- 3. In GitHub, create the `pypi` environment and require manual approval.
127
-
128
- Release flow:
129
-
130
- 1. Update `project.version` in `pyproject.toml`.
131
- 2. Run `micromamba run -n twopy pre-commit run --all-files`.
132
- 3. Commit the version change.
133
- 4. Create a GitHub release whose tag is the same version, with or without a
134
- leading `v`.
135
- 5. Publish the release.
136
-
137
- The release workflow checks that the tag matches `pyproject.toml`, builds the
138
- wheel and source distribution, checks package metadata, and publishes to PyPI
139
- after the `pypi` environment approval.
140
-
141
126
  ## Find Recordings
142
127
 
143
128
  ```python
@@ -241,6 +226,14 @@ shared exponential tau and one amplitude per ROI. The default dF/F fit mode is
241
226
  `robust`; pass `fit_mode="source_bounds"` when you need original source-bound
242
227
  behavior for audit comparisons.
243
228
 
229
+ The script and napari workflows also accept `ResponseProcessingOptions` for
230
+ post-dF/F response processing. Smoothing and low-pass filters are applied to
231
+ continuous dF/F before trial grouping, while correlation filtering scores
232
+ grouped trial responses and stores the selected settings plus QC scores in the
233
+ analysis HDF5 output. Smoothing supports moving-average and Savitzky-Golay
234
+ methods; Savitzky-Golay defaults to a seven-frame window with polynomial order
235
+ two.
236
+
244
237
  ## Open In Napari
245
238
 
246
239
  From a converted output directory that contains `recording_data.h5`, or from a
@@ -2,6 +2,8 @@
2
2
 
3
3
  Two-photon imaging analysis tool for the Clark Lab output format.
4
4
 
5
+ ![twopy napari interface](docs/assets/twopy-napari.png)
6
+
5
7
  ## Getting Started
6
8
 
7
9
  twopy lets you open two-photon recordings, draw ROIs, plot responses in real time,
@@ -16,25 +18,24 @@ the converted files, so the original source files remain separate from twopy's o
16
18
 
17
19
  Examples use micromamba, but any conda-compatible environment manager should work.
18
20
 
19
- Run this once from the twopy folder to install everything you need:
21
+ Preferred setup is a fresh Python 3.13 environment with the published PyPI
22
+ package:
20
23
 
21
24
  ```sh
22
- micromamba env create -f environment.yml
23
- micromamba activate twopy
24
- micromamba run -n twopy pre-commit install
25
- cp config.example.yml config.yml
25
+ micromamba create -n twopy -c conda-forge python=3.13 pip -y && micromamba run -n twopy python -m pip install twopy
26
26
  ```
27
27
 
28
- Then edit `config.yml` so the paths match your computer. The example file
29
- explains each setting in plain language. `config.yml` stays local to your
30
- machine and is not tracked by git.
31
-
32
- If the `twopy` command is missing after setup, refresh the editable install:
28
+ Verify the install:
33
29
 
34
30
  ```sh
35
- micromamba run -n twopy python -m pip install -e .
31
+ micromamba run -n twopy python -c 'import twopy; print(twopy.__name__)'
36
32
  ```
37
33
 
34
+ If you are working from a source checkout, copy `config.example.yml` to
35
+ `config.yml`, then edit `config.yml` so the paths match your computer. The
36
+ example file explains each setting in plain language. `config.yml` stays local
37
+ to your machine and is not tracked by git.
38
+
38
39
  ### Start The GUI
39
40
 
40
41
  Start napari from the twopy environment:
@@ -76,9 +77,18 @@ Basic GUI flow:
76
77
 
77
78
  ## Setup Details
78
79
 
79
- The environment installs twopy as an editable package, so the `twopy` terminal
80
- command is available after activating the environment. If the environment
81
- already existed before the command was added, refresh the editable install:
80
+ For development, install from the repository so local code edits are used:
81
+
82
+ ```sh
83
+ micromamba env create -f environment.yml
84
+ micromamba activate twopy
85
+ micromamba run -n twopy pre-commit install
86
+ ```
87
+
88
+ The development environment installs twopy as an editable package, so the
89
+ `twopy` terminal command is available after activating the environment. If the
90
+ environment already existed before the command was added, refresh the editable
91
+ install:
82
92
 
83
93
  ```sh
84
94
  micromamba run -n twopy python -m pip install -e .
@@ -93,31 +103,6 @@ micromamba run -n twopy pre-commit run --all-files
93
103
  The installed pre-commit hook runs ruff, ty, and the unit tests before each
94
104
  commit.
95
105
 
96
- ## Release
97
-
98
- PyPI publishing uses GitHub Actions Trusted Publishing. No PyPI API token secret
99
- is required.
100
-
101
- One-time setup in PyPI:
102
-
103
- 1. Add a trusted publisher for the `twopy` project.
104
- 2. Use owner `gumadeiras`, repository `twopy`, workflow
105
- `publish-to-pypi.yml`, and environment `pypi`.
106
- 3. In GitHub, create the `pypi` environment and require manual approval.
107
-
108
- Release flow:
109
-
110
- 1. Update `project.version` in `pyproject.toml`.
111
- 2. Run `micromamba run -n twopy pre-commit run --all-files`.
112
- 3. Commit the version change.
113
- 4. Create a GitHub release whose tag is the same version, with or without a
114
- leading `v`.
115
- 5. Publish the release.
116
-
117
- The release workflow checks that the tag matches `pyproject.toml`, builds the
118
- wheel and source distribution, checks package metadata, and publishes to PyPI
119
- after the `pypi` environment approval.
120
-
121
106
  ## Find Recordings
122
107
 
123
108
  ```python
@@ -221,6 +206,14 @@ shared exponential tau and one amplitude per ROI. The default dF/F fit mode is
221
206
  `robust`; pass `fit_mode="source_bounds"` when you need original source-bound
222
207
  behavior for audit comparisons.
223
208
 
209
+ The script and napari workflows also accept `ResponseProcessingOptions` for
210
+ post-dF/F response processing. Smoothing and low-pass filters are applied to
211
+ continuous dF/F before trial grouping, while correlation filtering scores
212
+ grouped trial responses and stores the selected settings plus QC scores in the
213
+ analysis HDF5 output. Smoothing supports moving-average and Savitzky-Golay
214
+ methods; Savitzky-Golay defaults to a seven-frame window with polynomial order
215
+ two.
216
+
224
217
  ## Open In Napari
225
218
 
226
219
  From a converted output directory that contains `recording_data.h5`, or from a
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "twopy"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Two-photon imaging analysis tool with a napari interface."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.14"
@@ -5,6 +5,7 @@ Inputs: none.
5
5
  Outputs: package constants and helper functions imported by users and scripts.
6
6
  """
7
7
 
8
+ from twopy._version import __version__
8
9
  from twopy.analysis import (
9
10
  DEFAULT_RESPONSE_POST_WINDOW_SECONDS,
10
11
  DEFAULT_RESPONSE_PRE_WINDOW_SECONDS,
@@ -12,6 +13,7 @@ from twopy.analysis import (
12
13
  AnalysisResponseRun,
13
14
  BackgroundCorrectedRoiTraces,
14
15
  BackgroundCorrectionMethod,
16
+ CorrelationFilterOptions,
15
17
  DeltaFOverFFitMode,
16
18
  EpochFrameWindow,
17
19
  FrameWindow,
@@ -19,9 +21,13 @@ from twopy.analysis import (
19
21
  GroupedRoiResponseSummary,
20
22
  InterpolatedEpochMapping,
21
23
  LoadedAnalysisOutputs,
24
+ LowPassFilterOptions,
25
+ ResponseProcessingOptions,
26
+ RoiCorrelationScores,
22
27
  RoiDeltaFOverF,
23
28
  RoiResponseSummary,
24
29
  RoiResponseTrial,
30
+ SmoothingOptions,
25
31
  WindowedRoiResponse,
26
32
  analyze_recording_responses,
27
33
  apply_motion_artifact_mask_to_delta_f_over_f,
@@ -104,6 +110,7 @@ __all__ = [
104
110
  "classify_recording_photodiode_events",
105
111
  "compute_roi_delta_f_over_f",
106
112
  "compute_recording_responses",
113
+ "CorrelationFilterOptions",
107
114
  "convert_recording_to_twopy",
108
115
  "DEFAULT_RESPONSE_POST_WINDOW_SECONDS",
109
116
  "DEFAULT_RESPONSE_PRE_WINDOW_SECONDS",
@@ -129,6 +136,7 @@ __all__ = [
129
136
  "load_roi_set",
130
137
  "load_source_conversion_inputs",
131
138
  "LoadedAnalysisOutputs",
139
+ "LowPassFilterOptions",
132
140
  "make_roi_set_from_label_image",
133
141
  "make_frame_windows",
134
142
  "make_roi_set",
@@ -143,7 +151,9 @@ __all__ = [
143
151
  "PhotodiodeEvent",
144
152
  "PhotodiodeEventType",
145
153
  "PhotodiodeEventSet",
154
+ "ResponseProcessingOptions",
146
155
  "RoiDeltaFOverF",
156
+ "RoiCorrelationScores",
147
157
  "RoiResponseSummary",
148
158
  "RoiResponseTrial",
149
159
  "RoiSet",
@@ -155,6 +165,7 @@ __all__ = [
155
165
  "save_roi_set",
156
166
  "select_epoch_frame_windows",
157
167
  "split_traces_by_frame_windows",
168
+ "SmoothingOptions",
158
169
  "SpatialCrop",
159
170
  "SpatialDomain",
160
171
  "StimulusSpecificColumnMapping",
@@ -164,5 +175,3 @@ __all__ = [
164
175
  "write_response_summary_grouped_csv",
165
176
  "write_response_summary_trials_csv",
166
177
  ]
167
-
168
- __version__ = "0.1.0"
@@ -0,0 +1,14 @@
1
+ """Package-version metadata for twopy.
2
+
3
+ Inputs: installed Python package metadata.
4
+ Outputs: the current twopy package version string.
5
+
6
+ Keeping this in one small module prevents the public Python API and command-line
7
+ interface from drifting when ``pyproject.toml`` changes.
8
+ """
9
+
10
+ from importlib.metadata import version
11
+
12
+ __all__ = ["__version__"]
13
+
14
+ __version__ = version("twopy")
@@ -31,6 +31,13 @@ from twopy.analysis.persistence import (
31
31
  write_response_summary_grouped_csv,
32
32
  write_response_summary_trials_csv,
33
33
  )
34
+ from twopy.analysis.response_processing import (
35
+ CorrelationFilterOptions,
36
+ LowPassFilterOptions,
37
+ ResponseProcessingOptions,
38
+ RoiCorrelationScores,
39
+ SmoothingOptions,
40
+ )
34
41
  from twopy.analysis.responses import (
35
42
  GroupedRoiResponses,
36
43
  GroupedRoiResponseSummary,
@@ -72,6 +79,7 @@ __all__ = [
72
79
  "BackgroundCorrectionMethod",
73
80
  "compute_roi_delta_f_over_f",
74
81
  "compute_recording_responses",
82
+ "CorrelationFilterOptions",
75
83
  "DeltaFOverFFitMode",
76
84
  "EpochFrameWindow",
77
85
  "extract_background_corrected_roi_traces",
@@ -85,14 +93,18 @@ __all__ = [
85
93
  "is_gray_epoch_name",
86
94
  "load_analysis_outputs",
87
95
  "LoadedAnalysisOutputs",
96
+ "LowPassFilterOptions",
88
97
  "make_frame_windows",
89
98
  "map_stimulus_epochs_to_frame_windows",
90
99
  "RoiDeltaFOverF",
100
+ "ResponseProcessingOptions",
101
+ "RoiCorrelationScores",
91
102
  "RoiResponseSummary",
92
103
  "RoiResponseTrial",
93
104
  "save_analysis_outputs",
94
105
  "select_epoch_frame_windows",
95
106
  "split_traces_by_frame_windows",
107
+ "SmoothingOptions",
96
108
  "summarize_epoch_roi_responses",
97
109
  "summarize_grouped_responses",
98
110
  "WindowedRoiResponse",
@@ -23,6 +23,14 @@ from twopy.analysis.background_subtraction import (
23
23
  BackgroundCorrectionMethod,
24
24
  )
25
25
  from twopy.analysis.dff import RoiDeltaFOverF
26
+ from twopy.analysis.response_processing import (
27
+ ResponseProcessingOptions,
28
+ RoiCorrelationScores,
29
+ )
30
+ from twopy.analysis.response_processing.persistence import (
31
+ read_response_processing_group,
32
+ write_response_processing_group,
33
+ )
26
34
  from twopy.analysis.responses import (
27
35
  GroupedRoiResponses,
28
36
  RoiResponseTrial,
@@ -87,6 +95,8 @@ class LoadedAnalysisOutputs:
87
95
  dff: RoiDeltaFOverF | None
88
96
  epoch_windows: tuple[EpochFrameWindow, ...]
89
97
  grouped_responses: GroupedRoiResponses | None
98
+ response_processing_options: ResponseProcessingOptions | None
99
+ correlation_scores: RoiCorrelationScores | None
90
100
 
91
101
 
92
102
  def save_analysis_outputs(
@@ -97,6 +107,8 @@ def save_analysis_outputs(
97
107
  dff: RoiDeltaFOverF | None = None,
98
108
  epoch_windows: Sequence[EpochFrameWindow] = (),
99
109
  grouped_responses: GroupedRoiResponses | None = None,
110
+ response_processing_options: ResponseProcessingOptions | None = None,
111
+ correlation_scores: RoiCorrelationScores | None = None,
100
112
  response_summary_trials_csv: Path | None = None,
101
113
  response_summary_grouped_csv: Path | None = None,
102
114
  ) -> None:
@@ -109,6 +121,9 @@ def save_analysis_outputs(
109
121
  dff: Optional ROI dF/F result.
110
122
  epoch_windows: Optional stimulus windows used for response grouping.
111
123
  grouped_responses: Optional grouped trial responses.
124
+ response_processing_options: Optional smoothing, low-pass, and
125
+ correlation-QC settings used for these outputs.
126
+ correlation_scores: Optional ROI-level correlation QC scores.
112
127
  response_summary_trials_csv: Optional CSV path for one row per trial
113
128
  and ROI, with one column per relative response timepoint.
114
129
  response_summary_grouped_csv: Optional CSV path for one row per epoch,
@@ -129,6 +144,9 @@ def save_analysis_outputs(
129
144
  if any(path is not None for path in summary_outputs) and grouped_responses is None:
130
145
  msg = "response summary CSV outputs require grouped_responses"
131
146
  raise ValueError(msg)
147
+ if correlation_scores is not None and response_processing_options is None:
148
+ msg = "correlation_scores require response_processing_options"
149
+ raise ValueError(msg)
132
150
 
133
151
  output_path = path.expanduser()
134
152
  output_path.parent.mkdir(parents=True, exist_ok=True)
@@ -147,6 +165,12 @@ def save_analysis_outputs(
147
165
  h5_file.create_group("responses"),
148
166
  grouped_responses,
149
167
  )
168
+ if response_processing_options is not None:
169
+ write_response_processing_group(
170
+ h5_file.create_group("response_processing"),
171
+ options=response_processing_options,
172
+ correlation_scores=correlation_scores,
173
+ )
150
174
 
151
175
  if grouped_responses is not None:
152
176
  if response_summary_trials_csv is not None:
@@ -177,6 +201,11 @@ def load_analysis_outputs(path: Path) -> LoadedAnalysisOutputs:
177
201
  input_path = path.expanduser()
178
202
  with h5py.File(input_path, "r") as h5_file:
179
203
  _require_analysis_format(h5_file, input_path)
204
+ response_processing = (
205
+ read_response_processing_group(h5_file["response_processing"])
206
+ if "response_processing" in h5_file
207
+ else None
208
+ )
180
209
  return LoadedAnalysisOutputs(
181
210
  path=input_path,
182
211
  roi_set=(
@@ -198,6 +227,14 @@ def load_analysis_outputs(path: Path) -> LoadedAnalysisOutputs:
198
227
  if "responses" in h5_file
199
228
  else None
200
229
  ),
230
+ response_processing_options=(
231
+ response_processing.options if response_processing is not None else None
232
+ ),
233
+ correlation_scores=(
234
+ response_processing.correlation_scores
235
+ if response_processing is not None
236
+ else None
237
+ ),
201
238
  )
202
239
 
203
240
 
@@ -12,6 +12,8 @@ how to save the resulting audit trail.
12
12
 
13
13
  from twopy.analysis.response_processing.apply import (
14
14
  ResponseProcessingResult,
15
+ apply_correlation_filter_to_grouped_roi_responses,
16
+ mask_grouped_roi_responses_by_included_rois,
15
17
  process_grouped_roi_responses,
16
18
  process_roi_delta_f_over_f,
17
19
  )
@@ -33,6 +35,7 @@ from twopy.analysis.response_processing.qc import (
33
35
  from twopy.analysis.response_processing.signals import (
34
36
  butterworth_low_pass,
35
37
  nan_aware_moving_average,
38
+ nan_aware_savgol_filter,
36
39
  )
37
40
 
38
41
  __all__ = [
@@ -42,7 +45,10 @@ __all__ = [
42
45
  "CorrelationWindowSeconds",
43
46
  "LowPassFilterMethod",
44
47
  "LowPassFilterOptions",
48
+ "apply_correlation_filter_to_grouped_roi_responses",
49
+ "mask_grouped_roi_responses_by_included_rois",
45
50
  "nan_aware_moving_average",
51
+ "nan_aware_savgol_filter",
46
52
  "process_grouped_roi_responses",
47
53
  "process_roi_delta_f_over_f",
48
54
  "ResponseProcessingOptions",
@@ -25,10 +25,13 @@ from twopy.analysis.response_processing.qc import (
25
25
  from twopy.analysis.response_processing.signals import (
26
26
  butterworth_low_pass,
27
27
  nan_aware_moving_average,
28
+ nan_aware_savgol_filter,
28
29
  )
29
30
  from twopy.analysis.responses import GroupedRoiResponses, RoiResponseTrial
30
31
 
31
32
  __all__ = [
33
+ "apply_correlation_filter_to_grouped_roi_responses",
34
+ "mask_grouped_roi_responses_by_included_rois",
32
35
  "ResponseProcessingResult",
33
36
  "process_grouped_roi_responses",
34
37
  "process_roi_delta_f_over_f",
@@ -122,18 +125,10 @@ def process_grouped_roi_responses(
122
125
  validate_response_processing_options(options, data_rate_hz=grouped.data_rate_hz)
123
126
  _validate_grouped_for_processing(grouped)
124
127
  processed = _copy_grouped_with_processed_values(grouped, options=options)
125
- correlation_scores = None
126
- if options.correlation_filter.reference in {"epoch_mean", "epoch_peak"}:
127
- correlation_scores = score_roi_correlations(
128
- processed,
129
- minimum_correlation=options.correlation_filter.minimum_correlation,
130
- reference=options.correlation_filter.reference,
131
- window_seconds=options.correlation_filter.window_seconds,
132
- )
133
- processed = _mask_excluded_rois(
134
- processed,
135
- included_mask=correlation_scores.included_mask,
136
- )
128
+ processed, correlation_scores = apply_correlation_filter_to_grouped_roi_responses(
129
+ processed,
130
+ options=options,
131
+ )
137
132
  return ResponseProcessingResult(
138
133
  grouped_responses=processed,
139
134
  options=options,
@@ -141,6 +136,65 @@ def process_grouped_roi_responses(
141
136
  )
142
137
 
143
138
 
139
+ def apply_correlation_filter_to_grouped_roi_responses(
140
+ grouped: GroupedRoiResponses,
141
+ *,
142
+ options: ResponseProcessingOptions,
143
+ ) -> tuple[GroupedRoiResponses, RoiCorrelationScores | None]:
144
+ """Apply only the correlation filter from a processing option object.
145
+
146
+ Args:
147
+ grouped: Grouped ROI responses to score and optionally mask.
148
+ options: Complete processing options. Smoothing and low-pass settings
149
+ are validated but not re-applied here.
150
+
151
+ Returns:
152
+ ``(grouped_responses, scores)``. ``scores`` is ``None`` when the
153
+ correlation filter is disabled.
154
+
155
+ This helper lets the standard workflow process continuous dF/F first, then
156
+ perform trial-level quality control after grouping without accidentally
157
+ smoothing or filtering the same traces a second time.
158
+ """
159
+ validate_response_processing_options(options, data_rate_hz=grouped.data_rate_hz)
160
+ _validate_grouped_for_processing(grouped)
161
+ if options.correlation_filter.reference not in {"epoch_mean", "epoch_peak"}:
162
+ return grouped, None
163
+ correlation_scores = score_roi_correlations(
164
+ grouped,
165
+ minimum_correlation=options.correlation_filter.minimum_correlation,
166
+ reference=options.correlation_filter.reference,
167
+ window_seconds=options.correlation_filter.window_seconds,
168
+ )
169
+ return (
170
+ mask_grouped_roi_responses_by_included_rois(
171
+ grouped,
172
+ included_mask=correlation_scores.included_mask,
173
+ ),
174
+ correlation_scores,
175
+ )
176
+
177
+
178
+ def mask_grouped_roi_responses_by_included_rois(
179
+ grouped: GroupedRoiResponses,
180
+ *,
181
+ included_mask: npt.NDArray[np.bool_],
182
+ ) -> GroupedRoiResponses:
183
+ """Return grouped responses with excluded ROI columns set to NaN.
184
+
185
+ Args:
186
+ grouped: Grouped ROI responses to mask.
187
+ included_mask: Boolean vector matching ``grouped.roi_labels``.
188
+
189
+ Returns:
190
+ New grouped responses with the same shape and labels.
191
+
192
+ The mask is public because persisted QC scores should be able to hide ROIs
193
+ during plot reloads without recomputing the scientific QC decision.
194
+ """
195
+ return _mask_excluded_rois(grouped, included_mask=included_mask)
196
+
197
+
144
198
  def _process_values(
145
199
  values: npt.NDArray[np.float64],
146
200
  *,
@@ -154,6 +208,12 @@ def _process_values(
154
208
  processed,
155
209
  window_frames=options.smoothing.window_frames,
156
210
  )
211
+ if options.smoothing.method == "savgol":
212
+ processed = nan_aware_savgol_filter(
213
+ processed,
214
+ window_frames=options.smoothing.window_frames,
215
+ polynomial_order=options.smoothing.polynomial_order,
216
+ )
157
217
  if options.low_pass.method == "butterworth":
158
218
  if options.low_pass.cutoff_hz is None:
159
219
  msg = "enabled low-pass filtering requires cutoff_hz"
@@ -272,6 +332,9 @@ def _processing_metadata(
272
332
  return {
273
333
  "response_processing_smoothing_method": options.smoothing.method,
274
334
  "response_processing_smoothing_window_frames": options.smoothing.window_frames,
335
+ "response_processing_smoothing_polynomial_order": (
336
+ options.smoothing.polynomial_order
337
+ ),
275
338
  "response_processing_low_pass_method": options.low_pass.method,
276
339
  "response_processing_low_pass_cutoff_hz": (
277
340
  "none"
@@ -25,7 +25,7 @@ __all__ = [
25
25
  "validate_response_processing_options",
26
26
  ]
27
27
 
28
- SmoothingMethod = Literal["none", "moving_average"]
28
+ SmoothingMethod = Literal["none", "moving_average", "savgol"]
29
29
  LowPassFilterMethod = Literal["none", "butterworth"]
30
30
  CorrelationFilterReference = Literal["none", "epoch_mean", "epoch_peak"]
31
31
  CorrelationWindowSeconds = tuple[float | None, float | None]
@@ -33,17 +33,21 @@ CorrelationWindowSeconds = tuple[float | None, float | None]
33
33
 
34
34
  @dataclass(frozen=True)
35
35
  class SmoothingOptions:
36
- """Moving-average smoothing settings.
36
+ """Response-trace smoothing settings.
37
37
 
38
38
  Inputs: a smoothing method and window length in imaging frames.
39
39
  Outputs: a typed contract consumed by signal-processing kernels.
40
40
 
41
- ``method="none"`` leaves values unchanged. ``window_frames`` is still kept
42
- explicit so saved options can report exactly what the user selected.
41
+ ``method="none"`` leaves values unchanged. ``window_frames`` and
42
+ ``polynomial_order`` are still kept explicit so saved options can report
43
+ exactly what the user selected. ``method="savgol"`` defaults to a
44
+ seven-frame, second-order Savitzky-Golay smoother because that preserves
45
+ local peak shape better than a moving average.
43
46
  """
44
47
 
45
48
  method: SmoothingMethod = "none"
46
- window_frames: int = 1
49
+ window_frames: int = 7
50
+ polynomial_order: int = 2
47
51
 
48
52
 
49
53
  @dataclass(frozen=True)
@@ -124,12 +128,32 @@ def validate_response_processing_options(
124
128
 
125
129
  def _validate_smoothing_options(options: SmoothingOptions) -> None:
126
130
  """Validate smoothing options."""
127
- if options.method not in {"none", "moving_average"}:
131
+ if options.method not in {"none", "moving_average", "savgol"}:
128
132
  msg = f"Unknown smoothing method {options.method!r}"
129
133
  raise ValueError(msg)
130
134
  if options.window_frames < 1:
131
135
  msg = f"smoothing window_frames must be at least 1; got {options.window_frames}"
132
136
  raise ValueError(msg)
137
+ if options.polynomial_order < 0:
138
+ msg = (
139
+ "smoothing polynomial_order must be non-negative; "
140
+ f"got {options.polynomial_order}"
141
+ )
142
+ raise ValueError(msg)
143
+ if options.method == "savgol":
144
+ if options.window_frames % 2 == 0:
145
+ msg = (
146
+ "Savitzky-Golay smoothing window_frames must be odd; "
147
+ f"got {options.window_frames}"
148
+ )
149
+ raise ValueError(msg)
150
+ if options.polynomial_order >= options.window_frames:
151
+ msg = (
152
+ "Savitzky-Golay polynomial_order must be below window_frames; "
153
+ f"got order {options.polynomial_order} and window "
154
+ f"{options.window_frames}"
155
+ )
156
+ raise ValueError(msg)
133
157
 
134
158
 
135
159
  def _validate_low_pass_options(