TwoPy 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- twopy/__init__.py +168 -0
- twopy/_segments.py +53 -0
- twopy/analysis/__init__.py +101 -0
- twopy/analysis/background_subtraction.py +672 -0
- twopy/analysis/dff.py +524 -0
- twopy/analysis/epoch_mapping.py +223 -0
- twopy/analysis/motion.py +66 -0
- twopy/analysis/persistence.py +975 -0
- twopy/analysis/response_processing/__init__.py +55 -0
- twopy/analysis/response_processing/apply.py +298 -0
- twopy/analysis/response_processing/options.py +192 -0
- twopy/analysis/response_processing/qc.py +270 -0
- twopy/analysis/response_processing/signals.py +200 -0
- twopy/analysis/responses.py +345 -0
- twopy/analysis/trials.py +341 -0
- twopy/analysis/workflow.py +457 -0
- twopy/api.py +78 -0
- twopy/config.py +200 -0
- twopy/conversion/__init__.py +40 -0
- twopy/conversion/alignment_crop.py +329 -0
- twopy/conversion/api.py +113 -0
- twopy/conversion/frame_ranges.py +35 -0
- twopy/conversion/hdf5_writing.py +362 -0
- twopy/conversion/matlab_values.py +123 -0
- twopy/conversion/source_loading.py +403 -0
- twopy/conversion/stimulus_code.py +248 -0
- twopy/conversion/types.py +295 -0
- twopy/converted.py +681 -0
- twopy/database/__init__.py +33 -0
- twopy/database/cache.py +166 -0
- twopy/database/catalog.py +90 -0
- twopy/database/connection.py +27 -0
- twopy/database/generic.py +254 -0
- twopy/database/modeled.py +419 -0
- twopy/database/sql.py +46 -0
- twopy/database/types.py +116 -0
- twopy/inspection.py +492 -0
- twopy/matlab.py +398 -0
- twopy/napari/__init__.py +27 -0
- twopy/napari/__main__.py +10 -0
- twopy/napari/constants.py +13 -0
- twopy/napari/controls.py +739 -0
- twopy/napari/display.py +181 -0
- twopy/napari/display_paths.py +197 -0
- twopy/napari/interactive.py +310 -0
- twopy/napari/launcher.py +175 -0
- twopy/napari/layout.py +147 -0
- twopy/napari/loading.py +207 -0
- twopy/napari/movie.py +71 -0
- twopy/napari/paths.py +181 -0
- twopy/napari/plotting/__init__.py +21 -0
- twopy/napari/plotting/data.py +266 -0
- twopy/napari/plotting/docks.py +1052 -0
- twopy/napari/plotting/export.py +803 -0
- twopy/napari/plotting/export_controls.py +209 -0
- twopy/napari/plotting/label_visibility.py +260 -0
- twopy/napari/plotting/options.py +269 -0
- twopy/napari/plotting/panels.py +119 -0
- twopy/napari/plotting/widgets.py +767 -0
- twopy/napari/protocols.py +90 -0
- twopy/napari/responses.py +91 -0
- twopy/napari/roi.py +238 -0
- twopy/napari/session.py +381 -0
- twopy/napari/state.py +106 -0
- twopy/napari/text.py +26 -0
- twopy/napari/types.py +39 -0
- twopy/napari/viewer.py +405 -0
- twopy/parity/__init__.py +30 -0
- twopy/parity/dff.py +399 -0
- twopy/parity/roi.py +145 -0
- twopy/parity/saved_analysis.py +174 -0
- twopy/photodiode.py +375 -0
- twopy/photodiode_classification.py +484 -0
- twopy/roi.py +462 -0
- twopy/session.py +167 -0
- twopy/spatial.py +154 -0
- twopy/stimulus.py +233 -0
- twopy/synchronization.py +235 -0
- twopy/typing_guards.py +255 -0
- twopy-0.1.0.dist-info/METADATA +311 -0
- twopy-0.1.0.dist-info/RECORD +84 -0
- twopy-0.1.0.dist-info/WHEEL +5 -0
- twopy-0.1.0.dist-info/entry_points.txt +2 -0
- twopy-0.1.0.dist-info/top_level.txt +1 -0
twopy/typing_guards.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Runtime guards for narrowing untyped boundary values.
|
|
2
|
+
|
|
3
|
+
Inputs: objects decoded from HDF5, JSON, YAML, Qt, napari, or similar external
|
|
4
|
+
libraries.
|
|
5
|
+
Outputs: plain Python values whose runtime shape has been checked before type
|
|
6
|
+
narrowing.
|
|
7
|
+
|
|
8
|
+
These helpers keep ``typing.cast`` localized to the point where runtime
|
|
9
|
+
validation proves a value's contract.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from collections.abc import Collection
|
|
13
|
+
from typing import SupportsInt, cast
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import numpy.typing as npt
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"int_or_none",
|
|
20
|
+
"int_tuple_or_none",
|
|
21
|
+
"require_bool_array",
|
|
22
|
+
"require_float64_array",
|
|
23
|
+
"require_int64_array",
|
|
24
|
+
"string_key_mapping_or_none",
|
|
25
|
+
"require_string_choice",
|
|
26
|
+
"require_string_key_mapping",
|
|
27
|
+
"require_string_key_mapping_values",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def int_or_none(value: object) -> int | None:
|
|
32
|
+
"""Return an integer value when conversion is unambiguous.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
value: Candidate integer-like value from external metadata.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Plain ``int`` or ``None`` when conversion fails.
|
|
39
|
+
|
|
40
|
+
Boolean values are rejected even though ``bool`` subclasses ``int`` because
|
|
41
|
+
crop bounds and shapes should not silently treat flags as coordinates.
|
|
42
|
+
"""
|
|
43
|
+
if isinstance(value, bool):
|
|
44
|
+
return None
|
|
45
|
+
if not isinstance(value, str | bytes | bytearray) and not hasattr(value, "__int__"):
|
|
46
|
+
return None
|
|
47
|
+
try:
|
|
48
|
+
return int(cast(str | bytes | bytearray | SupportsInt, value))
|
|
49
|
+
except (TypeError, ValueError):
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def int_tuple_or_none(value: object, *, length: int) -> tuple[int, ...] | None:
|
|
54
|
+
"""Return a fixed-length tuple of integers when every item parses.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
value: Candidate sequence of integer-like values.
|
|
58
|
+
length: Required tuple length.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Integer tuple or ``None`` when the value is not a matching sequence.
|
|
62
|
+
"""
|
|
63
|
+
if not isinstance(value, tuple | list) or len(value) != length:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
parsed: list[int] = []
|
|
67
|
+
for item in value:
|
|
68
|
+
parsed_item = int_or_none(item)
|
|
69
|
+
if parsed_item is None:
|
|
70
|
+
return None
|
|
71
|
+
parsed.append(parsed_item)
|
|
72
|
+
return tuple(parsed)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def string_key_mapping_or_none(value: object) -> dict[str, object] | None:
|
|
76
|
+
"""Return a dictionary when every key is text.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
value: Candidate mapping-like object from external metadata.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Copy of the dictionary with string keys, or ``None`` when invalid.
|
|
83
|
+
"""
|
|
84
|
+
if not isinstance(value, dict):
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
result: dict[str, object] = {}
|
|
88
|
+
for key, item in value.items():
|
|
89
|
+
if not isinstance(key, str):
|
|
90
|
+
return None
|
|
91
|
+
result[key] = item
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def require_string_key_mapping(value: object, *, name: str) -> dict[str, object]:
|
|
96
|
+
"""Return a dictionary after validating that every key is text.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
value: Candidate dictionary from an external boundary.
|
|
100
|
+
name: Human-readable value name for error messages.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Copy of the dictionary with string keys.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
ValueError: If ``value`` is not a dictionary with string keys.
|
|
107
|
+
"""
|
|
108
|
+
result = string_key_mapping_or_none(value)
|
|
109
|
+
if result is None:
|
|
110
|
+
msg = f"{name} must be a dictionary with string keys"
|
|
111
|
+
raise ValueError(msg)
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def require_string_key_mapping_values(
|
|
116
|
+
value: object,
|
|
117
|
+
*,
|
|
118
|
+
name: str,
|
|
119
|
+
) -> dict[str, dict[str, object]]:
|
|
120
|
+
"""Return a dictionary whose values are string-key dictionaries.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
value: Candidate nested dictionary from an external boundary.
|
|
124
|
+
name: Human-readable value name for error messages.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Nested dictionary copied after validating both mapping levels.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
ValueError: If the top-level value or any nested value is malformed.
|
|
131
|
+
"""
|
|
132
|
+
outer = require_string_key_mapping(value, name=name)
|
|
133
|
+
result: dict[str, dict[str, object]] = {}
|
|
134
|
+
for key, item in outer.items():
|
|
135
|
+
result[key] = require_string_key_mapping(
|
|
136
|
+
item,
|
|
137
|
+
name=f"{name}[{key!r}]",
|
|
138
|
+
)
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def require_string_choice[TextChoice: str](
|
|
143
|
+
value: object,
|
|
144
|
+
*,
|
|
145
|
+
name: str,
|
|
146
|
+
allowed: Collection[TextChoice],
|
|
147
|
+
) -> TextChoice:
|
|
148
|
+
"""Return a text value after checking it is one of the allowed choices.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
value: Candidate text value from an external boundary.
|
|
152
|
+
name: Human-readable value name for error messages.
|
|
153
|
+
allowed: Valid string values.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
The selected allowed value narrowed to the literal type of ``allowed``.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
ValueError: If ``value`` is not one of the allowed strings.
|
|
160
|
+
"""
|
|
161
|
+
text = str(value)
|
|
162
|
+
if text in allowed:
|
|
163
|
+
return cast(TextChoice, text)
|
|
164
|
+
|
|
165
|
+
allowed_text = ", ".join(repr(item) for item in sorted(allowed))
|
|
166
|
+
msg = f"{name} must be one of {allowed_text}; got {text!r}"
|
|
167
|
+
raise ValueError(msg)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def require_float64_array(
|
|
171
|
+
value: object,
|
|
172
|
+
*,
|
|
173
|
+
name: str,
|
|
174
|
+
ndim: int | None = None,
|
|
175
|
+
) -> npt.NDArray[np.float64]:
|
|
176
|
+
"""Return a NumPy array after validating float64 dtype and rank.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
value: Candidate array value from an external boundary.
|
|
180
|
+
name: Human-readable dataset name for error messages.
|
|
181
|
+
ndim: Optional required number of dimensions.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
``float64`` NumPy array.
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
ValueError: If dtype or rank does not match.
|
|
188
|
+
"""
|
|
189
|
+
array = np.asarray(value)
|
|
190
|
+
_require_array_contract(array, name=name, dtype=np.dtype(np.float64), ndim=ndim)
|
|
191
|
+
return cast(npt.NDArray[np.float64], array)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def require_int64_array(
|
|
195
|
+
value: object,
|
|
196
|
+
*,
|
|
197
|
+
name: str,
|
|
198
|
+
ndim: int | None = None,
|
|
199
|
+
) -> npt.NDArray[np.int64]:
|
|
200
|
+
"""Return a NumPy array after validating int64 dtype and rank.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
value: Candidate array value from an external boundary.
|
|
204
|
+
name: Human-readable dataset name for error messages.
|
|
205
|
+
ndim: Optional required number of dimensions.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
``int64`` NumPy array.
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
ValueError: If dtype or rank does not match.
|
|
212
|
+
"""
|
|
213
|
+
array = np.asarray(value)
|
|
214
|
+
_require_array_contract(array, name=name, dtype=np.dtype(np.int64), ndim=ndim)
|
|
215
|
+
return cast(npt.NDArray[np.int64], array)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def require_bool_array(
|
|
219
|
+
value: object,
|
|
220
|
+
*,
|
|
221
|
+
name: str,
|
|
222
|
+
ndim: int | None = None,
|
|
223
|
+
) -> npt.NDArray[np.bool_]:
|
|
224
|
+
"""Return a NumPy array after validating bool dtype and rank.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
value: Candidate array value from an external boundary.
|
|
228
|
+
name: Human-readable dataset name for error messages.
|
|
229
|
+
ndim: Optional required number of dimensions.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Boolean NumPy array.
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
ValueError: If dtype or rank does not match.
|
|
236
|
+
"""
|
|
237
|
+
array = np.asarray(value)
|
|
238
|
+
_require_array_contract(array, name=name, dtype=np.dtype(np.bool_), ndim=ndim)
|
|
239
|
+
return cast(npt.NDArray[np.bool_], array)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _require_array_contract(
|
|
243
|
+
array: npt.NDArray[np.generic],
|
|
244
|
+
*,
|
|
245
|
+
name: str,
|
|
246
|
+
dtype: np.dtype[np.generic],
|
|
247
|
+
ndim: int | None,
|
|
248
|
+
) -> None:
|
|
249
|
+
"""Validate the dtype and rank expected for a persisted array."""
|
|
250
|
+
if array.dtype != dtype:
|
|
251
|
+
msg = f"{name} must have dtype {dtype.name}; got {array.dtype}"
|
|
252
|
+
raise ValueError(msg)
|
|
253
|
+
if ndim is not None and array.ndim != ndim:
|
|
254
|
+
msg = f"{name} must have {ndim} dimension(s); got shape {array.shape}"
|
|
255
|
+
raise ValueError(msg)
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: twopy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Two-photon imaging analysis tool with a napari interface.
|
|
5
|
+
Author-email: Gustavo Madeira Santana <fromweb1@gumadeiras.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: <3.14,>=3.13
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: h5py<4,>=3.16
|
|
10
|
+
Requires-Dist: matplotlib<4,>=3.10
|
|
11
|
+
Requires-Dist: napari[all]<0.8,>=0.7
|
|
12
|
+
Requires-Dist: numpy<3,>=2.3
|
|
13
|
+
Requires-Dist: pre-commit<5,>=4.6
|
|
14
|
+
Requires-Dist: qtpy<3,>=2.4
|
|
15
|
+
Requires-Dist: pyyaml<7,>=6.0.3
|
|
16
|
+
Requires-Dist: ruff>=0.15
|
|
17
|
+
Requires-Dist: scipy<2,>=1.17
|
|
18
|
+
Requires-Dist: tifffile<2027,>=2026.3
|
|
19
|
+
Requires-Dist: ty<0.1,>=0.0.34
|
|
20
|
+
|
|
21
|
+
# twopy
|
|
22
|
+
|
|
23
|
+
Two-photon imaging analysis tool for the Clark Lab output format.
|
|
24
|
+
|
|
25
|
+
## Getting Started
|
|
26
|
+
|
|
27
|
+
twopy lets you open two-photon recordings, draw ROIs, plot responses in real time,
|
|
28
|
+
process and analyze them, and save them.
|
|
29
|
+
|
|
30
|
+
When you first load a recording, twopy converts it to a standardized HDF5 format.
|
|
31
|
+
The converted format includes the aligned movie, mean image, stimulus tables,
|
|
32
|
+
photodiode signals, and recording metadata. Analysis and the GUI both work from
|
|
33
|
+
the converted files, so the original source files remain separate from twopy's outputs.
|
|
34
|
+
|
|
35
|
+
### Install
|
|
36
|
+
|
|
37
|
+
Examples use micromamba, but any conda-compatible environment manager should work.
|
|
38
|
+
|
|
39
|
+
Run this once from the twopy folder to install everything you need:
|
|
40
|
+
|
|
41
|
+
```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
|
|
46
|
+
```
|
|
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:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
micromamba run -n twopy python -m pip install -e .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Start The GUI
|
|
59
|
+
|
|
60
|
+
Start napari from the twopy environment:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
micromamba activate twopy
|
|
64
|
+
twopy
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or run it without activating the environment first:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
micromamba run -n twopy twopy
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
You can also open a recording directly:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
twopy /path/to/source/recording
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Or direct path to converted HDF5 files:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
twopy /path/to/recording_data.h5
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Inside napari, use the `twopy` panel to choose a recording folder or a
|
|
86
|
+
`recording_data.h5` file. If a source recording has not been converted yet,
|
|
87
|
+
twopy converts it first, then opens the converted files.
|
|
88
|
+
|
|
89
|
+
Basic GUI flow:
|
|
90
|
+
|
|
91
|
+
1. Start twopy.
|
|
92
|
+
2. Choose a recording.
|
|
93
|
+
3. Draw or edit ROIs in the `rois` Labels layer.
|
|
94
|
+
4. Click Save ROIs.
|
|
95
|
+
5. Use the response plot panel to update plots from the current ROIs.
|
|
96
|
+
|
|
97
|
+
## Setup Details
|
|
98
|
+
|
|
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:
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
micromamba run -n twopy python -m pip install -e .
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Check
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
micromamba run -n twopy pre-commit run --all-files
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The installed pre-commit hook runs ruff, ty, and the unit tests before each
|
|
114
|
+
commit.
|
|
115
|
+
|
|
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
|
+
## Find Recordings
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from twopy import find_recordings
|
|
145
|
+
|
|
146
|
+
recordings = find_recordings(
|
|
147
|
+
year=2023,
|
|
148
|
+
month=10,
|
|
149
|
+
day=17,
|
|
150
|
+
genotype="gh146",
|
|
151
|
+
stimulus="combo_stim",
|
|
152
|
+
sensor="g6f",
|
|
153
|
+
cell_type="ALPN",
|
|
154
|
+
hemisphere="right",
|
|
155
|
+
person="Gustavo",
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`config.yml` controls whether DB queries use mounted files directly or cached
|
|
160
|
+
local copies. The default is `database_access: copy` because database searches
|
|
161
|
+
over the network can be slow, while copying the DB file locally is usually fast.
|
|
162
|
+
|
|
163
|
+
## Convert Recording
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from pathlib import Path
|
|
167
|
+
|
|
168
|
+
from twopy import convert_recording_to_twopy
|
|
169
|
+
|
|
170
|
+
recording = Path("/path/to/recording")
|
|
171
|
+
|
|
172
|
+
converted = convert_recording_to_twopy(recording)
|
|
173
|
+
print(converted.path)
|
|
174
|
+
print(converted.movie_path)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Conversion writes `recording_data.h5` for metadata, stimulus tables,
|
|
178
|
+
photodiode signals, and the mean image. The large aligned movie is written
|
|
179
|
+
separately to `aligned_movie.h5`. By default the mean image uses the full movie;
|
|
180
|
+
pass `mean_start_frame` and `mean_stop_frame` to use a frame range. By default,
|
|
181
|
+
conversion writes to the location configured by `analysis_output`; pass
|
|
182
|
+
`output_dir` only when you want to override that for a specific call.
|
|
183
|
+
|
|
184
|
+
`config.yml` also controls analysis output routing. `analysis_output: source`
|
|
185
|
+
writes into `recording/twopy`; a path mirrors the recording directory structure
|
|
186
|
+
under that output root.
|
|
187
|
+
|
|
188
|
+
## Analyze Converted Data
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from pathlib import Path
|
|
192
|
+
|
|
193
|
+
import numpy as np
|
|
194
|
+
|
|
195
|
+
from twopy import (
|
|
196
|
+
classify_recording_photodiode_events,
|
|
197
|
+
compute_roi_delta_f_over_f,
|
|
198
|
+
detect_recording_photodiode_events,
|
|
199
|
+
extract_background_corrected_roi_traces,
|
|
200
|
+
load_converted_recording,
|
|
201
|
+
make_roi_set,
|
|
202
|
+
map_stimulus_epochs_to_frame_windows,
|
|
203
|
+
select_epoch_frame_windows,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
recording = load_converted_recording(Path("/path/to/recording_data.h5"))
|
|
207
|
+
mask_array = np.zeros((1, *recording.movie.shape[1:]), dtype=bool)
|
|
208
|
+
mask_array[0, :10, :10] = True
|
|
209
|
+
roi_set = make_roi_set(mask_array)
|
|
210
|
+
traces = extract_background_corrected_roi_traces(
|
|
211
|
+
recording,
|
|
212
|
+
roi_set,
|
|
213
|
+
method="movie_global_percentile",
|
|
214
|
+
)
|
|
215
|
+
alignment = detect_recording_photodiode_events(recording)
|
|
216
|
+
timing = classify_recording_photodiode_events(recording, alignment)
|
|
217
|
+
epoch_windows = map_stimulus_epochs_to_frame_windows(recording, alignment)
|
|
218
|
+
interleave_windows = select_epoch_frame_windows(
|
|
219
|
+
epoch_windows,
|
|
220
|
+
epoch_name="Gray Interleave",
|
|
221
|
+
)
|
|
222
|
+
dff = compute_roi_delta_f_over_f(
|
|
223
|
+
traces,
|
|
224
|
+
interleave_windows,
|
|
225
|
+
data_rate_hz=float(recording.acquisition_metadata["acq.frameRate"]),
|
|
226
|
+
fit_mode="robust",
|
|
227
|
+
)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Analysis starts from converted HDF5 files. ROI masks are GUI-independent, trace
|
|
231
|
+
extraction streams movie chunks, background correction stays explicit, and
|
|
232
|
+
stimulus epoch windows come from classified photodiode events instead of
|
|
233
|
+
nominal frame-rate assumptions. `timing.events` keeps the start, transition,
|
|
234
|
+
and end classifications auditable. Analysis trace extraction uses the saved
|
|
235
|
+
alignment-valid spatial crop by default, including `method="none"` when you
|
|
236
|
+
want uncorrected traces through the analysis path. ROI masks remain full-frame;
|
|
237
|
+
pass `spatial_domain="full_frame"` only when that is the intended audit path.
|
|
238
|
+
The lower-level `extract_roi_traces` helper is a full-frame raw primitive. ROI
|
|
239
|
+
dF/F uses corrected ROI fluorescence plus gray interleave windows to fit one
|
|
240
|
+
shared exponential tau and one amplitude per ROI. The default dF/F fit mode is
|
|
241
|
+
`robust`; pass `fit_mode="source_bounds"` when you need original source-bound
|
|
242
|
+
behavior for audit comparisons.
|
|
243
|
+
|
|
244
|
+
## Open In Napari
|
|
245
|
+
|
|
246
|
+
From a converted output directory that contains `recording_data.h5`, or from a
|
|
247
|
+
source recording directory:
|
|
248
|
+
|
|
249
|
+
```sh
|
|
250
|
+
twopy
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
If converted files are missing and the selected folder has the expected source
|
|
254
|
+
recording files, twopy runs conversion first and then opens the converted HDF5
|
|
255
|
+
files. If no recording is found, `twopy` still opens napari. Choose a recording
|
|
256
|
+
folder or `recording_data.h5` in the `twopy` dock panel; twopy loads it after
|
|
257
|
+
selection.
|
|
258
|
+
|
|
259
|
+
Or pass a source folder or converted recording explicitly:
|
|
260
|
+
|
|
261
|
+
```sh
|
|
262
|
+
twopy /path/to/source/recording
|
|
263
|
+
twopy /path/to/recording_data.h5
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
By default the launcher opens the mean image, the full movie, an editable
|
|
267
|
+
`rois` Labels layer, a top response-plot dock, a `twopy` loading dock, and a left
|
|
268
|
+
Save ROIs dock. Use `--no-movie` to skip the movie preview, or `--movie-start`
|
|
269
|
+
and `--movie-end` to choose a different preview range. Save ROIs writes
|
|
270
|
+
`rois.h5` beside the current recording by default. The response dock can reload
|
|
271
|
+
existing `analysis_outputs.h5` or update plots from the current Labels layer.
|
|
272
|
+
Saving analysis writes `rois.h5`, `analysis_outputs.h5`,
|
|
273
|
+
`exports/csvs/response_summary_trials.csv`, and
|
|
274
|
+
`exports/csvs/response_summary_grouped.csv` beside the converted recording.
|
|
275
|
+
Response plots share one y-axis across epochs and show two seconds before
|
|
276
|
+
stimulus onset and two seconds after stimulus offset by default, when gray
|
|
277
|
+
interleave frames are available in the grouped responses. Epoch plots are laid
|
|
278
|
+
out horizontally. Each saved response trial includes its own `time_seconds`
|
|
279
|
+
vector, so plots use direct response time values rather than inferring time from
|
|
280
|
+
array indices.
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
from pathlib import Path
|
|
284
|
+
|
|
285
|
+
from twopy import (
|
|
286
|
+
launch_napari,
|
|
287
|
+
open_recording_in_napari,
|
|
288
|
+
roi_label_image_from_layer,
|
|
289
|
+
save_napari_label_rois,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
launch_napari(Path("/path/to/recording_data.h5"))
|
|
293
|
+
|
|
294
|
+
view = open_recording_in_napari(
|
|
295
|
+
Path("/path/to/recording_data.h5"),
|
|
296
|
+
movie_frame_range=(0, 200),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# After drawing or editing the rois Labels layer:
|
|
300
|
+
label_image = roi_label_image_from_layer(view.roi_labels_layer)
|
|
301
|
+
roi_set = save_napari_label_rois(label_image, Path("/path/to/rois.h5"))
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Napari code is a thin adapter. It loads converted twopy files, displays the
|
|
305
|
+
mean image, optionally displays a bounded movie preview, creates an editable
|
|
306
|
+
ROI Labels layer, and adds small dock widgets for loading folders, saving ROIs,
|
|
307
|
+
and plotting responses. ROI saving writes the current Labels layer through the
|
|
308
|
+
core ROI HDF5 helpers. Response plotting calls the core analysis workflow when
|
|
309
|
+
updating from current ROIs. Pass `roi_set=Path("/path/to/rois.h5")` when
|
|
310
|
+
reopening existing ROIs. Napari code does not read source MATLAB/TIFF files or
|
|
311
|
+
own analysis decisions.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
twopy/__init__.py,sha256=dZ06g_2W0Diu2h41x5xQhm4N2glY4Y-_ZTAjpfr9pvY,5035
|
|
2
|
+
twopy/_segments.py,sha256=bVpeIYkXMVWb9XW0vC4ZGb3v89gRh6f_Ut1wdMNdxU4,1548
|
|
3
|
+
twopy/api.py,sha256=DhN-wFsr9ywwpvm0A1oFuydh5Ov6Fg-6aA8_uIYI1lA,2822
|
|
4
|
+
twopy/config.py,sha256=FH0AAY7AqCBMgYX0CvRFvwf3_NicfWN3BA9B8mVPQQI,6281
|
|
5
|
+
twopy/converted.py,sha256=un70rJBhPtPaSX3RtUEZYR4KvY8n2UStS223rfAY1Yc,22656
|
|
6
|
+
twopy/inspection.py,sha256=iDHhlipW7RzE9cdFT023JoQz5ENWc-etFothbZcnV0o,15759
|
|
7
|
+
twopy/matlab.py,sha256=tIx3RBskSyfWqUut3EX6A0C57_iCiZOJh3f8pPkKOpw,12254
|
|
8
|
+
twopy/photodiode.py,sha256=2MUc-yxKlCFHlr7VLBuDngkGqEIgrKD4scRqj4jjA2U,12147
|
|
9
|
+
twopy/photodiode_classification.py,sha256=yhLBlE1ExV7ufGz9c2AcXvOxg8wScsjecYEaVO5mHrU,15916
|
|
10
|
+
twopy/roi.py,sha256=dGRfpsHi3vdZuNxXirQYpNTLiZE79O-Njy6wbIP2Nso,14756
|
|
11
|
+
twopy/session.py,sha256=MCvg4xkyct9ARFhJRk4rXgJE5q72EpmIammu3drfZFo,5773
|
|
12
|
+
twopy/spatial.py,sha256=-3uUu5cOCxTLi5XbFzgoOBmL65ADwqRI-JMwG6XFBw4,4663
|
|
13
|
+
twopy/stimulus.py,sha256=2qnZVawUKx0QC3ZES0imlSXM-a706Gb6sxUbAfxdnQE,7903
|
|
14
|
+
twopy/synchronization.py,sha256=ltsCW6cDNcUcO_PaYhHbul-vvsrfvezChYhNw8uMvOA,8103
|
|
15
|
+
twopy/typing_guards.py,sha256=-dcYZZ4xK4mEIH7WdKQZnQWndYVW6PX56HvDWDhXEfk,7394
|
|
16
|
+
twopy/analysis/__init__.py,sha256=TfSKtHiK1RTKNbcklPLc2urdhGWGO9Z3vHzVGBr_Sn0,3154
|
|
17
|
+
twopy/analysis/background_subtraction.py,sha256=yPbKGpA7S5tkFLdzvJazc5x08en_wJ3sBmrwa0VO_Yo,24724
|
|
18
|
+
twopy/analysis/dff.py,sha256=1SOLSopTt2z84rdcTAxka19MYr41Tn-fEto-6LYNtXY,18737
|
|
19
|
+
twopy/analysis/epoch_mapping.py,sha256=cJMJ6XxJnJO2WrrCALBbWs3qixtbYqbYNiyj9YCbOfo,7728
|
|
20
|
+
twopy/analysis/motion.py,sha256=bXTlTt_n6oRHBzvo-nfic400VFY4xtnu_KHa8cdOBCQ,2250
|
|
21
|
+
twopy/analysis/persistence.py,sha256=5vD1XW6R3w5IjcB0YgQmMnA87qkIUUj82VbDoQA_RsM,30650
|
|
22
|
+
twopy/analysis/responses.py,sha256=i2TQNOJsUYi0hmLrawAk17MHy7mxaInA9zonU4BHQXU,11691
|
|
23
|
+
twopy/analysis/trials.py,sha256=bN9__XzFPzdemwHoVwLx1u60cIF77NVkZ-RFOk-HxXg,10626
|
|
24
|
+
twopy/analysis/workflow.py,sha256=7QZM1-jalGEbb0xBLMebT_P0iv5PdUxEhULbi5cplJI,17279
|
|
25
|
+
twopy/analysis/response_processing/__init__.py,sha256=nwyg6_HlK7gsz5vQ9k0q30Ng7cTIIwCNSptXYpGuCJQ,1686
|
|
26
|
+
twopy/analysis/response_processing/apply.py,sha256=qNj6qtWN8zddoWA8LY-XHpBorFve7GHofaBFtrd4MRg,10330
|
|
27
|
+
twopy/analysis/response_processing/options.py,sha256=E9J69KUXNXLBBbGMt9d0axRK6FD-U0cEP001d-fIAxo,6828
|
|
28
|
+
twopy/analysis/response_processing/qc.py,sha256=KJg2tK7LyRb2zSW2WEsC6Ol7XnTfZKDAf-yYAyF0078,10136
|
|
29
|
+
twopy/analysis/response_processing/signals.py,sha256=Ny46o9__Lichb8Qov21tFvv2ZM1KYWIUpQUhOan6sI4,6534
|
|
30
|
+
twopy/conversion/__init__.py,sha256=V133Oaq_h8xgKE-7y3vFPgwiI7iH7FhzcFryuS8OTkU,1142
|
|
31
|
+
twopy/conversion/alignment_crop.py,sha256=WZXRBJOd60e32WwFMaPtsPnj2hJeDawTXvyqSla_r64,10452
|
|
32
|
+
twopy/conversion/api.py,sha256=_JKQozpAiGpfQ7gE1dpS2tE77YDWJRzoYZDDne_jEWQ,3958
|
|
33
|
+
twopy/conversion/frame_ranges.py,sha256=LIKegKiBtR4d14e7DCJGCCYAGf0UbhwDGOAZ33XpG3w,1057
|
|
34
|
+
twopy/conversion/hdf5_writing.py,sha256=ZJ2-1VBuzCYuXmqTBIgaDeXCK_yLZSuYzUSGGuYMAXw,12756
|
|
35
|
+
twopy/conversion/matlab_values.py,sha256=ZmRIuZXnHebX8R_O8KfDZZOXwoCxWsutkBt9EPk5gT8,3518
|
|
36
|
+
twopy/conversion/source_loading.py,sha256=TcbNQXufB7sMnnV2ZPnVjRyqSpvH8HScuMXGz0PFpiU,12567
|
|
37
|
+
twopy/conversion/stimulus_code.py,sha256=_20gbcQ1W3GiMixaawavmMUqf-5kb_mJq6l619Pjzzw,7935
|
|
38
|
+
twopy/conversion/types.py,sha256=66OOzbl50xebE3KQIVQiAFZ0nEWE6RdFITtjZZbPPA4,9971
|
|
39
|
+
twopy/database/__init__.py,sha256=DC11J6iEgPrMMEsXOfK3xBXDlj1GWkB07MIF5fXV82o,915
|
|
40
|
+
twopy/database/cache.py,sha256=JJDcMdJTqh3EvbLGCboZMx3NZSEkLHY5X8pKsXkbh8A,4875
|
|
41
|
+
twopy/database/catalog.py,sha256=EzkoUkBYSTyOEfDvywVidFtIkNTD0TEX93teOEpDCyI,2763
|
|
42
|
+
twopy/database/connection.py,sha256=j2U_FMvzgTeAMFUzahA3KI_uepjKkVxbceCdnNLjp3c,756
|
|
43
|
+
twopy/database/generic.py,sha256=G4TaqSpbaQAjiLxqnVloQnTm4fNGyXfQyqGcF7iC3e8,8195
|
|
44
|
+
twopy/database/modeled.py,sha256=Nf4fmgt_Xfv5JT0921OvvJVo72Wtx11iZuVFOl-hItQ,14190
|
|
45
|
+
twopy/database/sql.py,sha256=m26wQroYW2JVp-ckhouxjVKrXlGuOwx6iaCwuYQyIGs,1417
|
|
46
|
+
twopy/database/types.py,sha256=kZA2c3Duv_boiEuxWRCzg5CNkxqwDqea4wwN1b_GuHk,3229
|
|
47
|
+
twopy/napari/__init__.py,sha256=XG3ny3KEz8m6VFUz6ZaKpSNesIo6G9GHgCaQ4wz-3YM,965
|
|
48
|
+
twopy/napari/__main__.py,sha256=NnZzUP0UCWU6xOiCzPoBfGBVfrRYi6jWXVJNZ1wNuK8,250
|
|
49
|
+
twopy/napari/constants.py,sha256=pP_b6W8IPTeoKUsX3bkP74XlNGRhP7-rmZBVpVgPgts,393
|
|
50
|
+
twopy/napari/controls.py,sha256=irmGkgsoRZLJmodrGkYuOHJMD_87B2KA0w8-u_GO1jQ,24917
|
|
51
|
+
twopy/napari/display.py,sha256=0t0VTDqqXY04Xe5RDKi6nYLPmJbEZAdSJFJVierFGK4,5888
|
|
52
|
+
twopy/napari/display_paths.py,sha256=OXnmNOEtYerVtDG_FhkpfUYAaGVoGAlRqQvmxV9tYEM,5930
|
|
53
|
+
twopy/napari/interactive.py,sha256=Mf9kB9u7q6i1Hm6kQR8wJAQ1_U9MI-14TD8NfdNU9jU,10697
|
|
54
|
+
twopy/napari/launcher.py,sha256=G_eUOcvsdUaYag9045veMtdChRcjIGW0R2JhaL9Z58w,5698
|
|
55
|
+
twopy/napari/layout.py,sha256=MLO0YP-gpYP54INOgQRn3pEfYXxntR3_ix19AbGMGoo,4148
|
|
56
|
+
twopy/napari/loading.py,sha256=FLo6yLeRd7DTRiGKryoOwrRpzXKggBjt6I7NoxJecOI,6620
|
|
57
|
+
twopy/napari/movie.py,sha256=BAwniak4TR-mk9gnXlwivfPRMb4jhrhQ3EwvgdO2MEY,2344
|
|
58
|
+
twopy/napari/paths.py,sha256=gFrmtO-YJqjNlpjW6Q6VyP0QV3BZ55qTJdM8q6CJDTM,5309
|
|
59
|
+
twopy/napari/protocols.py,sha256=FnUqCWrDYIW5XrKdHNaE6f75NzxfYW5Ycnw67eBil78,2275
|
|
60
|
+
twopy/napari/responses.py,sha256=Dsu6-U9jG_UI8JZbIV1A2XtUqtl-lT6iOg52F5Qkx4s,3109
|
|
61
|
+
twopy/napari/roi.py,sha256=S4Zb1wRIvrZgF3eWAPOyuBb8XnSjhaZiMectlqLuF3Q,7606
|
|
62
|
+
twopy/napari/session.py,sha256=G12Zf-Bhd3dbPRnKGL8gD1ryaBKiDYf-yehYLaDq_Bs,11866
|
|
63
|
+
twopy/napari/state.py,sha256=OtMZfk2ifdWlC7j3jmkqN1TNAApTmlZ4szGhH5ksgaY,2908
|
|
64
|
+
twopy/napari/text.py,sha256=kHS91G4CyPjMzKcPcqGlT7ASmgl4z4uX1jgQIbu0FME,800
|
|
65
|
+
twopy/napari/types.py,sha256=Hth2EVLTdg1S3OSr4Af4UJBTzFA4yTT2p3jh7PSJskc,1289
|
|
66
|
+
twopy/napari/viewer.py,sha256=5GjpNtqJVeO2glszdUdk7OAMzyqOwq1aFuVm3wCRBG8,14744
|
|
67
|
+
twopy/napari/plotting/__init__.py,sha256=CQW3sf1ujm2ZTXk4xKKhp6ZkabKZw6QsTSJ6iy84Twc,650
|
|
68
|
+
twopy/napari/plotting/data.py,sha256=otS24bVi11VW_5Mvun512oc8kc9ovn3wsFulIeoJTSU,8680
|
|
69
|
+
twopy/napari/plotting/docks.py,sha256=QajHsgFCbuqqc1hdH-wBHukoS-PDR7Q_0E1wj8ZuqiQ,37486
|
|
70
|
+
twopy/napari/plotting/export.py,sha256=Y9hvLAf7IzxE47jFp7kp9lFb_UDiuQ0i8AzUR0TXITE,25617
|
|
71
|
+
twopy/napari/plotting/export_controls.py,sha256=t7jLjQwd8HG5F-Zxyc0TLW0EFXn5m70diNFAawL6IRc,6695
|
|
72
|
+
twopy/napari/plotting/label_visibility.py,sha256=NzMdsiNAem0XNyl_X17qLWUUzGsbFJrKzQ1svxwp9_Q,8175
|
|
73
|
+
twopy/napari/plotting/options.py,sha256=tXqzyCp0cRZGOjm11LCi4JEoN1BToR6TIKMcpKq6tyQ,8147
|
|
74
|
+
twopy/napari/plotting/panels.py,sha256=lj9btXu-w6rHQROThWYH2FFbYsvHILuMzi4jhSK5wmI,3322
|
|
75
|
+
twopy/napari/plotting/widgets.py,sha256=pJUSOVQ0FiBG6qwKShNFruH9SxtfkjesHKo1bjUs0Rs,21874
|
|
76
|
+
twopy/parity/__init__.py,sha256=IfSiTYAP4FVNzgQVVrCtbpVF5SI9vSYlR3yNZN1UWes,1009
|
|
77
|
+
twopy/parity/dff.py,sha256=wBnkZ95kaywT2U7XtoZ5rbG-wD5Ergqs5cnLwVPc6-g,14446
|
|
78
|
+
twopy/parity/roi.py,sha256=Q3deH6xRGCmTXASTlPHQgzH7FhBWkdjRUgh9skyS8j0,4975
|
|
79
|
+
twopy/parity/saved_analysis.py,sha256=Kbpaxh1zapR5XxnHEkin8r4mEZX8kEtonudeZcwI7Dw,5597
|
|
80
|
+
twopy-0.1.0.dist-info/METADATA,sha256=HXTcVoHQZ_2YNXJoy-nG7QiYijSKn11k1W6lH5gzV8A,10173
|
|
81
|
+
twopy-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
82
|
+
twopy-0.1.0.dist-info/entry_points.txt,sha256=RnmRKOxEv5ZwLN-dArdSB678gM2iNxbE30VwRpwhp8o,44
|
|
83
|
+
twopy-0.1.0.dist-info/top_level.txt,sha256=8MB6QGpnHMTeTfEE543DOr676HcemeoELujrJkFcvak,6
|
|
84
|
+
twopy-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
twopy
|