TwoPy 0.1.2__tar.gz → 0.1.3__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.3/PKG-INFO +97 -0
- twopy-0.1.3/README.md +77 -0
- {twopy-0.1.2 → twopy-0.1.3}/pyproject.toml +1 -1
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/database/cache.py +2 -5
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/controls.py +43 -2
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/loading.py +53 -1
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/plotting/label_visibility.py +34 -8
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/viewer.py +4 -2
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/session.py +5 -1
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/stimulus.py +8 -4
- twopy-0.1.3/src/twopy.egg-info/PKG-INFO +97 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_napari.py +87 -1
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_session.py +24 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_stimulus.py +13 -0
- twopy-0.1.2/PKG-INFO +0 -304
- twopy-0.1.2/README.md +0 -284
- twopy-0.1.2/src/twopy.egg-info/PKG-INFO +0 -304
- {twopy-0.1.2 → twopy-0.1.3}/setup.cfg +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/__init__.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/_segments.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/_version.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/analysis/__init__.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/analysis/background_subtraction.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/analysis/dff.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/analysis/epoch_mapping.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/analysis/motion.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/analysis/persistence.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/analysis/response_processing/__init__.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/analysis/response_processing/apply.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/analysis/response_processing/options.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/analysis/response_processing/persistence.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/analysis/response_processing/qc.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/analysis/response_processing/signals.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/analysis/responses.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/analysis/trials.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/analysis/workflow.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/api.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/config.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/conversion/__init__.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/conversion/alignment_crop.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/conversion/api.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/conversion/frame_ranges.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/conversion/hdf5_writing.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/conversion/matlab_values.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/conversion/source_loading.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/conversion/stimulus_code.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/conversion/types.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/converted.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/database/__init__.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/database/catalog.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/database/connection.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/database/generic.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/database/modeled.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/database/sql.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/database/types.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/inspection.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/matlab.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/__init__.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/__main__.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/constants.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/display.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/display_paths.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/interactive.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/launcher.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/layout.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/movie.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/paths.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/plotting/__init__.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/plotting/data.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/plotting/docks.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/plotting/export.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/plotting/export_controls.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/plotting/options.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/plotting/panels.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/plotting/processing_options.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/plotting/widgets.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/protocols.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/responses.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/roi.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/session.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/state.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/text.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/napari/types.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/parity/__init__.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/parity/dff.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/parity/roi.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/parity/saved_analysis.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/photodiode.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/photodiode_classification.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/roi.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/spatial.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/synchronization.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy/typing_guards.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy.egg-info/SOURCES.txt +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy.egg-info/dependency_links.txt +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy.egg-info/entry_points.txt +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy.egg-info/requires.txt +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/src/twopy.egg-info/top_level.txt +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_api.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_background_subtraction.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_config.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_conversion.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_converted.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_database.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_dff.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_epoch_mapping.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_import.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_inspection.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_matlab.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_motion.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_packaging.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_parity.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_persistence.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_photodiode_classification.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_real_fixture.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_response_processing.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_responses.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_roi.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_saved_analysis.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_synchronization.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_trials.py +0 -0
- {twopy-0.1.2 → twopy-0.1.3}/tests/test_workflow.py +0 -0
twopy-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: twopy
|
|
3
|
+
Version: 0.1.3
|
|
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
|
+

|
|
26
|
+
|
|
27
|
+
## Getting Started
|
|
28
|
+
|
|
29
|
+
twopy lets you open two-photon recordings, draw ROIs, plot responses in real time,
|
|
30
|
+
process and analyze them, and save them.
|
|
31
|
+
|
|
32
|
+
When you first load a recording, twopy converts it to a standardized HDF5 format.
|
|
33
|
+
The converted format includes the aligned movie, mean image, stimulus tables,
|
|
34
|
+
photodiode signals, and recording metadata. Analysis and the GUI both work from
|
|
35
|
+
the converted files, so the original source files remain separate from twopy's outputs.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
Examples use micromamba, but any conda-compatible environment manager should work.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
micromamba create -n twopy -c conda-forge python=3.13 pip -y
|
|
44
|
+
micromamba run -n twopy python -m pip install twopy
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Verify the install:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
micromamba run -n twopy python -c 'import twopy; print(twopy.__name__)'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For a source checkout, copy `config.example.yml` to `config.yml` and edit the
|
|
54
|
+
paths for your computer. `config.yml` stays local and is not tracked by git.
|
|
55
|
+
|
|
56
|
+
## Start
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
micromamba activate twopy
|
|
60
|
+
twopy
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Open a source recording or converted recording directly:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
twopy /path/to/source/recording
|
|
67
|
+
twopy /path/to/recording_data.h5
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Basic GUI flow:
|
|
71
|
+
|
|
72
|
+
1. Choose a recording.
|
|
73
|
+
2. Draw or edit ROIs in the `rois` Labels layer.
|
|
74
|
+
3. Click Save ROIs.
|
|
75
|
+
4. Update response plots from the current ROIs.
|
|
76
|
+
5. Save analysis outputs.
|
|
77
|
+
|
|
78
|
+
When a source recording has not been converted yet, twopy converts it first.
|
|
79
|
+
Converted data includes the aligned movie, mean image, stimulus tables,
|
|
80
|
+
photodiode signals, and recording metadata.
|
|
81
|
+
|
|
82
|
+
## Docs
|
|
83
|
+
|
|
84
|
+
- [GUI guide](docs/gui.md): napari loading, ROI editing, plotting, and saved
|
|
85
|
+
outputs.
|
|
86
|
+
- [Python API guide](docs/python_api.md): find recordings, convert data, run
|
|
87
|
+
analysis, and open napari from scripts.
|
|
88
|
+
- [Development guide](docs/development.md): source setup and local checks.
|
|
89
|
+
- [Input data spec](docs/input_data_spec.md): short recording folder contract.
|
|
90
|
+
- [Recording file reference](docs/recording_file_schema.md): detailed source
|
|
91
|
+
and converted file schema.
|
|
92
|
+
|
|
93
|
+
## Check
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
micromamba run -n twopy pre-commit run --all-files
|
|
97
|
+
```
|
twopy-0.1.3/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# twopy
|
|
2
|
+
|
|
3
|
+
Two-photon imaging analysis tool for the Clark Lab output format.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Getting Started
|
|
8
|
+
|
|
9
|
+
twopy lets you open two-photon recordings, draw ROIs, plot responses in real time,
|
|
10
|
+
process and analyze them, and save them.
|
|
11
|
+
|
|
12
|
+
When you first load a recording, twopy converts it to a standardized HDF5 format.
|
|
13
|
+
The converted format includes the aligned movie, mean image, stimulus tables,
|
|
14
|
+
photodiode signals, and recording metadata. Analysis and the GUI both work from
|
|
15
|
+
the converted files, so the original source files remain separate from twopy's outputs.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Examples use micromamba, but any conda-compatible environment manager should work.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
micromamba create -n twopy -c conda-forge python=3.13 pip -y
|
|
24
|
+
micromamba run -n twopy python -m pip install twopy
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Verify the install:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
micromamba run -n twopy python -c 'import twopy; print(twopy.__name__)'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
For a source checkout, copy `config.example.yml` to `config.yml` and edit the
|
|
34
|
+
paths for your computer. `config.yml` stays local and is not tracked by git.
|
|
35
|
+
|
|
36
|
+
## Start
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
micromamba activate twopy
|
|
40
|
+
twopy
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Open a source recording or converted recording directly:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
twopy /path/to/source/recording
|
|
47
|
+
twopy /path/to/recording_data.h5
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Basic GUI flow:
|
|
51
|
+
|
|
52
|
+
1. Choose a recording.
|
|
53
|
+
2. Draw or edit ROIs in the `rois` Labels layer.
|
|
54
|
+
3. Click Save ROIs.
|
|
55
|
+
4. Update response plots from the current ROIs.
|
|
56
|
+
5. Save analysis outputs.
|
|
57
|
+
|
|
58
|
+
When a source recording has not been converted yet, twopy converts it first.
|
|
59
|
+
Converted data includes the aligned movie, mean image, stimulus tables,
|
|
60
|
+
photodiode signals, and recording metadata.
|
|
61
|
+
|
|
62
|
+
## Docs
|
|
63
|
+
|
|
64
|
+
- [GUI guide](docs/gui.md): napari loading, ROI editing, plotting, and saved
|
|
65
|
+
outputs.
|
|
66
|
+
- [Python API guide](docs/python_api.md): find recordings, convert data, run
|
|
67
|
+
analysis, and open napari from scripts.
|
|
68
|
+
- [Development guide](docs/development.md): source setup and local checks.
|
|
69
|
+
- [Input data spec](docs/input_data_spec.md): short recording folder contract.
|
|
70
|
+
- [Recording file reference](docs/recording_file_schema.md): detailed source
|
|
71
|
+
and converted file schema.
|
|
72
|
+
|
|
73
|
+
## Check
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
micromamba run -n twopy pre-commit run --all-files
|
|
77
|
+
```
|
|
@@ -11,9 +11,9 @@ import hashlib
|
|
|
11
11
|
import json
|
|
12
12
|
import shutil
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import cast
|
|
15
14
|
|
|
16
15
|
from twopy.database.types import DatabaseAccess
|
|
16
|
+
from twopy.typing_guards import string_key_mapping_or_none
|
|
17
17
|
|
|
18
18
|
__all__ = [
|
|
19
19
|
"DEFAULT_DATABASE_CACHE_DIR",
|
|
@@ -107,10 +107,7 @@ def _read_copy_manifest(manifest_path: Path) -> dict[str, object] | None:
|
|
|
107
107
|
except (OSError, json.JSONDecodeError):
|
|
108
108
|
return None
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
return None
|
|
112
|
-
|
|
113
|
-
return cast(dict[str, object], loaded)
|
|
110
|
+
return string_key_mapping_or_none(loaded)
|
|
114
111
|
|
|
115
112
|
|
|
116
113
|
def _manifest_metadata_matches(
|
|
@@ -272,9 +272,14 @@ def _make_twopy_load_widget(state: NapariControlState) -> object:
|
|
|
272
272
|
)
|
|
273
273
|
movie_range = None
|
|
274
274
|
if load_movie:
|
|
275
|
+
start_frame, end_frame = _widget_movie_frame_range_for_load(
|
|
276
|
+
movie_frame_range,
|
|
277
|
+
frame_count=loaded_recording.movie.shape[0],
|
|
278
|
+
preserve_current_range=replace_selected,
|
|
279
|
+
)
|
|
275
280
|
movie_range = resolve_movie_frame_range(
|
|
276
|
-
start_frame=
|
|
277
|
-
end_frame=
|
|
281
|
+
start_frame=start_frame,
|
|
282
|
+
end_frame=end_frame,
|
|
278
283
|
frame_count=loaded_recording.movie.shape[0],
|
|
279
284
|
zero_end_means_last=True,
|
|
280
285
|
)
|
|
@@ -717,6 +722,42 @@ def _default_movie_end_frame(recording: RecordingData | None) -> int:
|
|
|
717
722
|
return max(recording.movie.shape[0] - 1, 0)
|
|
718
723
|
|
|
719
724
|
|
|
725
|
+
def _widget_movie_frame_range_for_load(
|
|
726
|
+
movie_frame_range: tuple[int, int],
|
|
727
|
+
*,
|
|
728
|
+
frame_count: int,
|
|
729
|
+
preserve_current_range: bool,
|
|
730
|
+
) -> tuple[int, int]:
|
|
731
|
+
"""Choose a GUI movie preview range for the recording being loaded.
|
|
732
|
+
|
|
733
|
+
Args:
|
|
734
|
+
movie_frame_range: Current range-slider value.
|
|
735
|
+
frame_count: Number of movie frames in the recording being loaded.
|
|
736
|
+
preserve_current_range: Whether this is a reload of the selected
|
|
737
|
+
recording after an option change.
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
Inclusive preview range constrained to the recording length. New
|
|
741
|
+
recordings default to their full movie, while reloads preserve the
|
|
742
|
+
visible slider value where possible.
|
|
743
|
+
|
|
744
|
+
The frame-range widget keeps its previous value until after a recording
|
|
745
|
+
loads. That value is meaningful when reloading the same recording after an
|
|
746
|
+
option change, but stale when choosing a different recording.
|
|
747
|
+
"""
|
|
748
|
+
last_frame = max(frame_count - 1, 0)
|
|
749
|
+
if not preserve_current_range:
|
|
750
|
+
return (0, last_frame)
|
|
751
|
+
|
|
752
|
+
start_frame = int(movie_frame_range[0])
|
|
753
|
+
end_frame = int(movie_frame_range[1])
|
|
754
|
+
if start_frame > last_frame:
|
|
755
|
+
start_frame = last_frame
|
|
756
|
+
if end_frame > last_frame:
|
|
757
|
+
end_frame = last_frame
|
|
758
|
+
return (start_frame, end_frame)
|
|
759
|
+
|
|
760
|
+
|
|
720
761
|
def _load_recording_for_defaults(
|
|
721
762
|
*,
|
|
722
763
|
recording_data_path: Path,
|
|
@@ -193,6 +193,7 @@ def _first_source_recording_dir(candidates: tuple[Path | None, ...]) -> Path | N
|
|
|
193
193
|
Resolved source folder, or ``None``.
|
|
194
194
|
"""
|
|
195
195
|
seen: set[Path] = set()
|
|
196
|
+
validation_error: FileNotFoundError | ValueError | None = None
|
|
196
197
|
for candidate in candidates:
|
|
197
198
|
if candidate is None:
|
|
198
199
|
continue
|
|
@@ -202,6 +203,57 @@ def _first_source_recording_dir(candidates: tuple[Path | None, ...]) -> Path | N
|
|
|
202
203
|
seen.add(expanded)
|
|
203
204
|
try:
|
|
204
205
|
return discover_session_files(expanded).session_dir.resolve()
|
|
205
|
-
except (FileNotFoundError, ValueError):
|
|
206
|
+
except (FileNotFoundError, ValueError) as error:
|
|
207
|
+
if validation_error is None and _looks_like_source_recording_dir(expanded):
|
|
208
|
+
validation_error = error
|
|
206
209
|
continue
|
|
210
|
+
if validation_error is not None:
|
|
211
|
+
raise validation_error
|
|
207
212
|
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _looks_like_source_recording_dir(path: Path) -> bool:
|
|
216
|
+
"""Return whether a folder appears to be microscope source output.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
path: Candidate folder.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
``True`` when the folder contains stable source-recording markers.
|
|
223
|
+
|
|
224
|
+
Discovery errors from source-like folders should reach the user. Errors
|
|
225
|
+
from arbitrary folders should stay quiet so twopy can continue checking
|
|
226
|
+
other candidates such as a parent source folder.
|
|
227
|
+
"""
|
|
228
|
+
if not path.is_dir():
|
|
229
|
+
return False
|
|
230
|
+
fixed_markers = (
|
|
231
|
+
"stimulusData",
|
|
232
|
+
"alignedMovie.mat",
|
|
233
|
+
"defaultAlignChannel.txt",
|
|
234
|
+
"highResPd.mat",
|
|
235
|
+
"imageDescription.mat",
|
|
236
|
+
"imagingResPd.mat",
|
|
237
|
+
)
|
|
238
|
+
if any((path / marker).exists() for marker in fixed_markers):
|
|
239
|
+
return True
|
|
240
|
+
return any(_source_candidate_files(path, "*.tif")) or any(
|
|
241
|
+
_source_candidate_files(path, "*_alignment.txt")
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _source_candidate_files(path: Path, pattern: str) -> tuple[Path, ...]:
|
|
246
|
+
"""Return source files matching a flexible microscope filename pattern.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
path: Candidate source folder.
|
|
250
|
+
pattern: Glob pattern.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Matching files excluding macOS AppleDouble sidecars.
|
|
254
|
+
"""
|
|
255
|
+
return tuple(
|
|
256
|
+
candidate
|
|
257
|
+
for candidate in path.glob(pattern)
|
|
258
|
+
if candidate.is_file() and not candidate.name.startswith("._")
|
|
259
|
+
)
|
|
@@ -9,10 +9,12 @@ image, so hiding an ROI in the plot options cannot delete ROI data.
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from collections.abc import Mapping
|
|
12
|
-
from typing import
|
|
12
|
+
from typing import Protocol, cast
|
|
13
13
|
|
|
14
14
|
from qtpy.QtGui import QColor
|
|
15
15
|
|
|
16
|
+
from twopy.typing_guards import int_or_none, string_key_mapping_or_none
|
|
17
|
+
|
|
16
18
|
__all__ = [
|
|
17
19
|
"apply_roi_visibility_to_labels_layer",
|
|
18
20
|
"roi_label_value_from_label",
|
|
@@ -241,13 +243,11 @@ def _base_label_colormap_params(
|
|
|
241
243
|
color cycle while users toggle ROI visibility.
|
|
242
244
|
"""
|
|
243
245
|
stored = layer.metadata.get(_BASE_LABEL_COLORMAP_METADATA_KEY)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
"background_value": int(cast(Any, stored_params["background_value"])),
|
|
250
|
-
}
|
|
246
|
+
stored_params = string_key_mapping_or_none(stored)
|
|
247
|
+
if stored_params is not None:
|
|
248
|
+
params = _parsed_label_colormap_params(stored_params)
|
|
249
|
+
if params is not None:
|
|
250
|
+
return params
|
|
251
251
|
|
|
252
252
|
colormap = cast(_LabelColormapLike, layer.colormap)
|
|
253
253
|
colors = getattr(colormap, "colors", ())
|
|
@@ -258,3 +258,29 @@ def _base_label_colormap_params(
|
|
|
258
258
|
}
|
|
259
259
|
layer.metadata[_BASE_LABEL_COLORMAP_METADATA_KEY] = params
|
|
260
260
|
return params
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _parsed_label_colormap_params(
|
|
264
|
+
params: dict[str, object],
|
|
265
|
+
) -> dict[str, int | float] | None:
|
|
266
|
+
"""Return persisted label-colormap parameters when all fields parse."""
|
|
267
|
+
num_colors = int_or_none(params.get("num_colors"))
|
|
268
|
+
seed = _float_or_none(params.get("seed"))
|
|
269
|
+
background_value = int_or_none(params.get("background_value"))
|
|
270
|
+
if num_colors is None or seed is None or background_value is None:
|
|
271
|
+
return None
|
|
272
|
+
return {
|
|
273
|
+
"num_colors": num_colors,
|
|
274
|
+
"seed": seed,
|
|
275
|
+
"background_value": background_value,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _float_or_none(value: object) -> float | None:
|
|
280
|
+
"""Return a float for simple numeric metadata values."""
|
|
281
|
+
if isinstance(value, bool) or not isinstance(value, int | float | str):
|
|
282
|
+
return None
|
|
283
|
+
try:
|
|
284
|
+
return float(value)
|
|
285
|
+
except ValueError:
|
|
286
|
+
return None
|
|
@@ -32,7 +32,9 @@ from twopy.napari.types import NapariRecordingView
|
|
|
32
32
|
from twopy.roi import RoiSet
|
|
33
33
|
from twopy.spatial import SpatialCrop
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
APPLICATION_TITLE = "twopy"
|
|
36
|
+
|
|
37
|
+
__all__ = ["APPLICATION_TITLE", "create_viewer", "open_recording_in_napari"]
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
class _LabelsLayerWithBrushSize(Protocol):
|
|
@@ -229,7 +231,7 @@ def create_viewer() -> NapariViewer:
|
|
|
229
231
|
"""
|
|
230
232
|
import napari
|
|
231
233
|
|
|
232
|
-
return cast(NapariViewer, napari.Viewer())
|
|
234
|
+
return cast(NapariViewer, napari.Viewer(title=APPLICATION_TITLE))
|
|
233
235
|
|
|
234
236
|
|
|
235
237
|
def set_labels_brush_size(layer: object, *, brush_size: int) -> None:
|
|
@@ -154,7 +154,11 @@ def _find_one_file(session_dir: Path, pattern: str, label: str) -> Path:
|
|
|
154
154
|
The raw movie and alignment text include stimulus/run details in their
|
|
155
155
|
names. A strict one-match check avoids guessing when the folder is messy.
|
|
156
156
|
"""
|
|
157
|
-
matches = sorted(
|
|
157
|
+
matches = sorted(
|
|
158
|
+
path
|
|
159
|
+
for path in session_dir.glob(pattern)
|
|
160
|
+
if path.is_file() and not path.name.startswith("._")
|
|
161
|
+
)
|
|
158
162
|
|
|
159
163
|
if len(matches) != 1:
|
|
160
164
|
joined = ", ".join(str(path) for path in matches) if matches else "none"
|
|
@@ -10,9 +10,9 @@ per-``stimtype`` mapping explicit for scripts and analysis code.
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from dataclasses import dataclass
|
|
13
|
-
from typing import cast
|
|
14
13
|
|
|
15
14
|
from twopy.converted import RecordingData
|
|
15
|
+
from twopy.typing_guards import require_string_key_mapping
|
|
16
16
|
|
|
17
17
|
__all__ = [
|
|
18
18
|
"StimulusSpecificColumnMapping",
|
|
@@ -176,10 +176,14 @@ def _mappings_from_metadata(
|
|
|
176
176
|
|
|
177
177
|
mappings: list[StimulusSpecificColumnMapping] = []
|
|
178
178
|
for column in columns:
|
|
179
|
-
|
|
179
|
+
try:
|
|
180
|
+
column_metadata = require_string_key_mapping(
|
|
181
|
+
column,
|
|
182
|
+
name=f"stimulus metadata for stimtype {stimtype!r} column",
|
|
183
|
+
)
|
|
184
|
+
except ValueError as error:
|
|
180
185
|
msg = f"stimulus metadata for stimtype {stimtype!r} is malformed"
|
|
181
|
-
raise ValueError(msg)
|
|
182
|
-
column_metadata = cast(dict[str, object], column)
|
|
186
|
+
raise ValueError(msg) from error
|
|
183
187
|
if column_metadata.get("column_name") != stable_column_name:
|
|
184
188
|
continue
|
|
185
189
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: twopy
|
|
3
|
+
Version: 0.1.3
|
|
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
|
+

|
|
26
|
+
|
|
27
|
+
## Getting Started
|
|
28
|
+
|
|
29
|
+
twopy lets you open two-photon recordings, draw ROIs, plot responses in real time,
|
|
30
|
+
process and analyze them, and save them.
|
|
31
|
+
|
|
32
|
+
When you first load a recording, twopy converts it to a standardized HDF5 format.
|
|
33
|
+
The converted format includes the aligned movie, mean image, stimulus tables,
|
|
34
|
+
photodiode signals, and recording metadata. Analysis and the GUI both work from
|
|
35
|
+
the converted files, so the original source files remain separate from twopy's outputs.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
Examples use micromamba, but any conda-compatible environment manager should work.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
micromamba create -n twopy -c conda-forge python=3.13 pip -y
|
|
44
|
+
micromamba run -n twopy python -m pip install twopy
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Verify the install:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
micromamba run -n twopy python -c 'import twopy; print(twopy.__name__)'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For a source checkout, copy `config.example.yml` to `config.yml` and edit the
|
|
54
|
+
paths for your computer. `config.yml` stays local and is not tracked by git.
|
|
55
|
+
|
|
56
|
+
## Start
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
micromamba activate twopy
|
|
60
|
+
twopy
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Open a source recording or converted recording directly:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
twopy /path/to/source/recording
|
|
67
|
+
twopy /path/to/recording_data.h5
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Basic GUI flow:
|
|
71
|
+
|
|
72
|
+
1. Choose a recording.
|
|
73
|
+
2. Draw or edit ROIs in the `rois` Labels layer.
|
|
74
|
+
3. Click Save ROIs.
|
|
75
|
+
4. Update response plots from the current ROIs.
|
|
76
|
+
5. Save analysis outputs.
|
|
77
|
+
|
|
78
|
+
When a source recording has not been converted yet, twopy converts it first.
|
|
79
|
+
Converted data includes the aligned movie, mean image, stimulus tables,
|
|
80
|
+
photodiode signals, and recording metadata.
|
|
81
|
+
|
|
82
|
+
## Docs
|
|
83
|
+
|
|
84
|
+
- [GUI guide](docs/gui.md): napari loading, ROI editing, plotting, and saved
|
|
85
|
+
outputs.
|
|
86
|
+
- [Python API guide](docs/python_api.md): find recordings, convert data, run
|
|
87
|
+
analysis, and open napari from scripts.
|
|
88
|
+
- [Development guide](docs/development.md): source setup and local checks.
|
|
89
|
+
- [Input data spec](docs/input_data_spec.md): short recording folder contract.
|
|
90
|
+
- [Recording file reference](docs/recording_file_schema.md): detailed source
|
|
91
|
+
and converted file schema.
|
|
92
|
+
|
|
93
|
+
## Check
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
micromamba run -n twopy pre-commit run --all-files
|
|
97
|
+
```
|
|
@@ -90,7 +90,11 @@ from twopy.napari.plotting.widgets import (
|
|
|
90
90
|
)
|
|
91
91
|
from twopy.napari.state import write_last_recording_folder
|
|
92
92
|
from twopy.napari.text import counted_noun
|
|
93
|
-
from twopy.napari.viewer import
|
|
93
|
+
from twopy.napari.viewer import (
|
|
94
|
+
APPLICATION_TITLE,
|
|
95
|
+
create_viewer,
|
|
96
|
+
interior_random_frame_indices,
|
|
97
|
+
)
|
|
94
98
|
from twopy.spatial import SpatialCrop
|
|
95
99
|
|
|
96
100
|
|
|
@@ -400,6 +404,20 @@ class NapariAdapterTest(unittest.TestCase):
|
|
|
400
404
|
environ["TWOPY_NAPARI_STATE_FILE"] = self._previous_state_file
|
|
401
405
|
self._state_temp_dir.cleanup()
|
|
402
406
|
|
|
407
|
+
def test_create_viewer_uses_twopy_window_title(self) -> None:
|
|
408
|
+
"""Confirm the launcher brands the top-level napari window.
|
|
409
|
+
|
|
410
|
+
Inputs: patched napari Viewer constructor.
|
|
411
|
+
Outputs: viewer construction receives the twopy application title.
|
|
412
|
+
"""
|
|
413
|
+
viewer = _FakeViewer()
|
|
414
|
+
|
|
415
|
+
with patch("napari.Viewer", return_value=viewer) as viewer_constructor:
|
|
416
|
+
created = create_viewer()
|
|
417
|
+
|
|
418
|
+
self.assertIs(created, viewer)
|
|
419
|
+
viewer_constructor.assert_called_once_with(title=APPLICATION_TITLE)
|
|
420
|
+
|
|
403
421
|
def test_opens_empty_roi_labels_layer_by_default(self) -> None:
|
|
404
422
|
"""Confirm new recordings open with an editable empty ROI layer.
|
|
405
423
|
|
|
@@ -1311,6 +1329,42 @@ class NapariAdapterTest(unittest.TestCase):
|
|
|
1311
1329
|
self.assertEqual(load_widget.movie_frame_range.value, (0, 1))
|
|
1312
1330
|
self.assertEqual(len(viewer.images), 2)
|
|
1313
1331
|
|
|
1332
|
+
def test_loading_shorter_recording_clamps_stale_movie_range(self) -> None:
|
|
1333
|
+
"""Confirm old movie slider values do not block a shorter recording.
|
|
1334
|
+
|
|
1335
|
+
Inputs: two converted recordings where the first movie is longer.
|
|
1336
|
+
Outputs: the second recording loads and the slider fits its frame count.
|
|
1337
|
+
"""
|
|
1338
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
1339
|
+
root = Path(temp_dir)
|
|
1340
|
+
first = root / "first"
|
|
1341
|
+
second = root / "second"
|
|
1342
|
+
first.mkdir()
|
|
1343
|
+
second.mkdir()
|
|
1344
|
+
_write_converted_recording(
|
|
1345
|
+
first,
|
|
1346
|
+
movie_values=np.arange(20, dtype=np.float64).reshape(5, 2, 2),
|
|
1347
|
+
)
|
|
1348
|
+
_write_converted_recording(
|
|
1349
|
+
second,
|
|
1350
|
+
movie_values=np.arange(12, dtype=np.float64).reshape(3, 2, 2),
|
|
1351
|
+
)
|
|
1352
|
+
viewer = _FakeViewer()
|
|
1353
|
+
control_docks = add_twopy_magicgui_controls(
|
|
1354
|
+
viewer,
|
|
1355
|
+
roi_labels_layer=None,
|
|
1356
|
+
roi_save_file=Path("unused.h5"),
|
|
1357
|
+
)
|
|
1358
|
+
controls = cast(_ControlWidget, control_docks.load_widget)
|
|
1359
|
+
load_widget = cast(Any, controls[1])
|
|
1360
|
+
|
|
1361
|
+
load_widget(recording_folder=first)
|
|
1362
|
+
load_widget.movie_frame_range.value = (3, 4)
|
|
1363
|
+
load_widget(recording_folder=second)
|
|
1364
|
+
|
|
1365
|
+
self.assertEqual(load_widget.movie_frame_range.value, (0, 2))
|
|
1366
|
+
self.assertEqual(len(viewer.images), 4)
|
|
1367
|
+
|
|
1314
1368
|
def test_recording_path_resolution_reports_available_files(self) -> None:
|
|
1315
1369
|
"""Confirm folder resolution finds optional movie and ROI paths.
|
|
1316
1370
|
|
|
@@ -1380,6 +1434,20 @@ class NapariAdapterTest(unittest.TestCase):
|
|
|
1380
1434
|
(source_dir / "twopy" / "aligned_movie.h5").resolve(),
|
|
1381
1435
|
)
|
|
1382
1436
|
|
|
1437
|
+
def test_source_recording_validation_error_is_reported(self) -> None:
|
|
1438
|
+
"""Confirm malformed source folders do not look like missing HDF5.
|
|
1439
|
+
|
|
1440
|
+
Inputs: source-shaped recording folder with two real TIFF movies.
|
|
1441
|
+
Outputs: the source discovery error reaches the caller.
|
|
1442
|
+
"""
|
|
1443
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
1444
|
+
source_dir = Path(temp_dir)
|
|
1445
|
+
_write_source_recording_shape(source_dir)
|
|
1446
|
+
(source_dir / "second_movie.tif").touch()
|
|
1447
|
+
|
|
1448
|
+
with self.assertRaisesRegex(ValueError, "raw TIFF movie"):
|
|
1449
|
+
resolve_or_convert_recording(source_dir)
|
|
1450
|
+
|
|
1383
1451
|
def test_recording_path_resolution_repairs_missing_converted_movie(self) -> None:
|
|
1384
1452
|
"""Confirm incomplete source-local twopy output is regenerated in place.
|
|
1385
1453
|
|
|
@@ -1593,6 +1661,24 @@ class NapariAdapterTest(unittest.TestCase):
|
|
|
1593
1661
|
self.assertEqual(layer.get_color(1)[3], 1.0)
|
|
1594
1662
|
self.assertAlmostEqual(layer.get_color(2)[3], 0.2)
|
|
1595
1663
|
|
|
1664
|
+
def test_roi_visibility_ignores_malformed_colormap_metadata(self) -> None:
|
|
1665
|
+
"""Confirm malformed persisted label-colormap metadata is rebuilt.
|
|
1666
|
+
|
|
1667
|
+
Inputs: Labels layer with incomplete twopy colormap metadata.
|
|
1668
|
+
Outputs: visibility still applies instead of raising from metadata access.
|
|
1669
|
+
"""
|
|
1670
|
+
layer = Labels(np.array([[1]], dtype=np.int64))
|
|
1671
|
+
layer.metadata["twopy_base_label_colormap"] = {"num_colors": "invalid"}
|
|
1672
|
+
|
|
1673
|
+
apply_roi_visibility_to_labels_layer(
|
|
1674
|
+
layer,
|
|
1675
|
+
roi_labels=("roi_1",),
|
|
1676
|
+
visibility={"roi_1": False},
|
|
1677
|
+
colors=(QColor("#ff0000"),),
|
|
1678
|
+
)
|
|
1679
|
+
|
|
1680
|
+
self.assertAlmostEqual(layer.get_color(1)[3], 0.2)
|
|
1681
|
+
|
|
1596
1682
|
def test_display_helpers_transpose_movie_axes_for_napari(self) -> None:
|
|
1597
1683
|
"""Confirm movie axes are transposed only at the napari boundary.
|
|
1598
1684
|
|