torch-reconstruct-tomogram 0.6.0__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.
- torch_reconstruct_tomogram-0.6.0/.gitignore +115 -0
- torch_reconstruct_tomogram-0.6.0/LICENSE +29 -0
- torch_reconstruct_tomogram-0.6.0/PKG-INFO +71 -0
- torch_reconstruct_tomogram-0.6.0/README.md +39 -0
- torch_reconstruct_tomogram-0.6.0/pyproject.toml +195 -0
- torch_reconstruct_tomogram-0.6.0/src/torch_reconstruct_tomogram/__init__.py +26 -0
- torch_reconstruct_tomogram-0.6.0/src/torch_reconstruct_tomogram/projection.py +77 -0
- torch_reconstruct_tomogram-0.6.0/src/torch_reconstruct_tomogram/py.typed +5 -0
- torch_reconstruct_tomogram-0.6.0/src/torch_reconstruct_tomogram/reconstruct.py +294 -0
- torch_reconstruct_tomogram-0.6.0/tests/test_torch_reconstruct_tomogram.py +242 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
env/
|
|
12
|
+
build/
|
|
13
|
+
develop-eggs/
|
|
14
|
+
dist/
|
|
15
|
+
downloads/
|
|
16
|
+
eggs/
|
|
17
|
+
.eggs/
|
|
18
|
+
lib/
|
|
19
|
+
lib64/
|
|
20
|
+
parts/
|
|
21
|
+
sdist/
|
|
22
|
+
var/
|
|
23
|
+
wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
|
|
28
|
+
.DS_Store
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
*.manifest
|
|
32
|
+
*.spec
|
|
33
|
+
|
|
34
|
+
# Installer logs
|
|
35
|
+
pip-log.txt
|
|
36
|
+
pip-delete-this-directory.txt
|
|
37
|
+
|
|
38
|
+
# Unit test / coverage reports
|
|
39
|
+
htmlcov/
|
|
40
|
+
.tox/
|
|
41
|
+
.coverage
|
|
42
|
+
.coverage.*
|
|
43
|
+
.cache
|
|
44
|
+
nosetests.xml
|
|
45
|
+
coverage.xml
|
|
46
|
+
*.cover
|
|
47
|
+
.hypothesis/
|
|
48
|
+
.pytest_cache/
|
|
49
|
+
|
|
50
|
+
# Files downloaded for unit tests
|
|
51
|
+
**/tests/tmp/
|
|
52
|
+
|
|
53
|
+
# Translations
|
|
54
|
+
*.mo
|
|
55
|
+
*.pot
|
|
56
|
+
|
|
57
|
+
# Django stuff:
|
|
58
|
+
*.log
|
|
59
|
+
local_settings.py
|
|
60
|
+
|
|
61
|
+
# Flask stuff:
|
|
62
|
+
instance/
|
|
63
|
+
.webassets-cache
|
|
64
|
+
|
|
65
|
+
# Scrapy stuff:
|
|
66
|
+
.scrapy
|
|
67
|
+
|
|
68
|
+
# Sphinx documentation
|
|
69
|
+
docs/_build/
|
|
70
|
+
|
|
71
|
+
# PyBuilder
|
|
72
|
+
target/
|
|
73
|
+
|
|
74
|
+
# Jupyter Notebook
|
|
75
|
+
.ipynb_checkpoints
|
|
76
|
+
|
|
77
|
+
# dotenv
|
|
78
|
+
.env
|
|
79
|
+
|
|
80
|
+
# virtualenv
|
|
81
|
+
.venv
|
|
82
|
+
venv/
|
|
83
|
+
ENV/
|
|
84
|
+
|
|
85
|
+
# Spyder project settings
|
|
86
|
+
.spyderproject
|
|
87
|
+
.spyproject
|
|
88
|
+
|
|
89
|
+
# Rope project settings
|
|
90
|
+
.ropeproject
|
|
91
|
+
|
|
92
|
+
# mkdocs documentation
|
|
93
|
+
/site
|
|
94
|
+
|
|
95
|
+
# mypy
|
|
96
|
+
.mypy_cache/
|
|
97
|
+
|
|
98
|
+
# ruff
|
|
99
|
+
.ruff_cache/
|
|
100
|
+
|
|
101
|
+
# IDEs
|
|
102
|
+
.idea/
|
|
103
|
+
.vscode/
|
|
104
|
+
|
|
105
|
+
# Mojo extension compile cache (experimental torch-fourier-slice kernels)
|
|
106
|
+
__mojocache__/
|
|
107
|
+
*.mojopkg
|
|
108
|
+
# experimental demo outputs
|
|
109
|
+
packages/primitives/torch-fourier-slice/examples/*.npz
|
|
110
|
+
packages/primitives/torch-fourier-slice/examples/*.png
|
|
111
|
+
packages/primitives/torch-fourier-slice/examples/*.gif
|
|
112
|
+
# Personal local notes (not for commit)
|
|
113
|
+
notes/*.local.md
|
|
114
|
+
|
|
115
|
+
lightning_logs/
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020, TeamTomo
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: torch-reconstruct-tomogram
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: (sub-)tomogram reconstruction and subtilt extraction for cryoET.
|
|
5
|
+
Project-URL: homepage, https://github.com/teamtomo/teamtomo
|
|
6
|
+
Project-URL: repository, https://github.com/teamtomo/teamtomo
|
|
7
|
+
Project-URL: documentation, https://github.com/teamtomo/teamtomo#readme
|
|
8
|
+
Project-URL: changelog, https://github.com/teamtomo/teamtomo#changelog
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/teamtomo/teamtomo/issues
|
|
10
|
+
Project-URL: Source Code, https://github.com/teamtomo/teamtomo
|
|
11
|
+
Author-email: Marten Chaillet <martenchaillet@gmail.com>, Davide Torre <davidetorre99@gmail.com>
|
|
12
|
+
License: BSD-3-Clause
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: einops
|
|
24
|
+
Requires-Dist: torch
|
|
25
|
+
Requires-Dist: torch-ctf
|
|
26
|
+
Requires-Dist: torch-fourier-rescale
|
|
27
|
+
Requires-Dist: torch-fourier-slice
|
|
28
|
+
Requires-Dist: torch-grid-utils>=0.0.8
|
|
29
|
+
Requires-Dist: torch-subpixel-crop
|
|
30
|
+
Requires-Dist: torch-tilt-series[io]
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# torch-reconstruct-tomogram
|
|
34
|
+
|
|
35
|
+
[](https://github.com/teamtomo/torch-reconstruct-tomogram/raw/main/LICENSE)
|
|
36
|
+
[](https://pypi.org/project/torch-reconstruct-tomogram)
|
|
37
|
+
[](https://python.org)
|
|
38
|
+
[](https://github.com/teamtomo/torch-reconstruct-tomogram/actions/workflows/ci.yml)
|
|
39
|
+
[](https://codecov.io/gh/teamtomo/torch-reconstruct-tomogram)
|
|
40
|
+
|
|
41
|
+
(sub-)Tomogram reconstruction and subtilt extraction for cryo-ET.
|
|
42
|
+
|
|
43
|
+
## Overview
|
|
44
|
+
|
|
45
|
+
This package provides (sub-)tomogram reconstruction and subtilt extraction driven entirely from a [`torch-tilt-series`](https://github.com/teamtomo/torch-tilt-series) `TiltSeries`. It supports
|
|
46
|
+
|
|
47
|
+
* `extract_particle_tilt_series()`: extract a subtilt-series at 3D location(s) in the sample
|
|
48
|
+
* `reconstruct_subvolume()`: rank-polymorphic reconstruction of 3D patch(es) at location(s) in the sample
|
|
49
|
+
* `reconstruct_tomogram()`: full volume reconstruction by tiling reconstructed patches in 3D
|
|
50
|
+
|
|
51
|
+
`TiltSeries` holds alignment geometry (in Angstroms) plus `image_path`/`image_indices`/`pixel_spacing` metadata. The functions above take a `TiltSeries`, and load and (by default) preprocess the matching raw images internally via `torch_tilt_series.load_tilt_series_images()` / `preprocess_tilt_series_images()` (by default: plane subtraction, a DC-excluding bandpass with no low-pass, i.e. up to Nyquist, and central-crop normalization); pass `**preprocessing_kwargs` to override any of its defaults (`low=`, `high=`, `falloff=`, `bandpass_padding=`, `subtract_background=`, `normalize=`) - see `preprocess_tilt_series_images()`'s docstring for details. `output_pixel_spacing` lets both local (`reconstruct_subvolume`) and global (`reconstruct_tomogram`) reconstruction target an arbitrary output voxel size. Reconstruction happens at the input pixel spacing and is Fourier-rescaled to the requested output size. Reconstruction is performed in Fourier space using central slice insertion. Positions are in `zyx` coordinates, in Angstroms, relative to the tomogram center.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install torch-reconstruct-tomogram
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
To load a tilt series from AreTomo or ETOMO output, also install the IO dependencies for [`torch-tilt-series`](https://github.com/teamtomo/torch-tilt-series):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install torch-tilt-series[io]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Examples
|
|
66
|
+
|
|
67
|
+
See the [`examples/`](examples/) folder for scripts showing how to load a tilt series, reconstruct subvolumes and tomograms, and save the result.
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
This project is licensed under the BSD 3-Clause License - see the LICENSE file for details.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# torch-reconstruct-tomogram
|
|
2
|
+
|
|
3
|
+
[](https://github.com/teamtomo/torch-reconstruct-tomogram/raw/main/LICENSE)
|
|
4
|
+
[](https://pypi.org/project/torch-reconstruct-tomogram)
|
|
5
|
+
[](https://python.org)
|
|
6
|
+
[](https://github.com/teamtomo/torch-reconstruct-tomogram/actions/workflows/ci.yml)
|
|
7
|
+
[](https://codecov.io/gh/teamtomo/torch-reconstruct-tomogram)
|
|
8
|
+
|
|
9
|
+
(sub-)Tomogram reconstruction and subtilt extraction for cryo-ET.
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
This package provides (sub-)tomogram reconstruction and subtilt extraction driven entirely from a [`torch-tilt-series`](https://github.com/teamtomo/torch-tilt-series) `TiltSeries`. It supports
|
|
14
|
+
|
|
15
|
+
* `extract_particle_tilt_series()`: extract a subtilt-series at 3D location(s) in the sample
|
|
16
|
+
* `reconstruct_subvolume()`: rank-polymorphic reconstruction of 3D patch(es) at location(s) in the sample
|
|
17
|
+
* `reconstruct_tomogram()`: full volume reconstruction by tiling reconstructed patches in 3D
|
|
18
|
+
|
|
19
|
+
`TiltSeries` holds alignment geometry (in Angstroms) plus `image_path`/`image_indices`/`pixel_spacing` metadata. The functions above take a `TiltSeries`, and load and (by default) preprocess the matching raw images internally via `torch_tilt_series.load_tilt_series_images()` / `preprocess_tilt_series_images()` (by default: plane subtraction, a DC-excluding bandpass with no low-pass, i.e. up to Nyquist, and central-crop normalization); pass `**preprocessing_kwargs` to override any of its defaults (`low=`, `high=`, `falloff=`, `bandpass_padding=`, `subtract_background=`, `normalize=`) - see `preprocess_tilt_series_images()`'s docstring for details. `output_pixel_spacing` lets both local (`reconstruct_subvolume`) and global (`reconstruct_tomogram`) reconstruction target an arbitrary output voxel size. Reconstruction happens at the input pixel spacing and is Fourier-rescaled to the requested output size. Reconstruction is performed in Fourier space using central slice insertion. Positions are in `zyx` coordinates, in Angstroms, relative to the tomogram center.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install torch-reconstruct-tomogram
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
To load a tilt series from AreTomo or ETOMO output, also install the IO dependencies for [`torch-tilt-series`](https://github.com/teamtomo/torch-tilt-series):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install torch-tilt-series[io]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Examples
|
|
34
|
+
|
|
35
|
+
See the [`examples/`](examples/) folder for scripts showing how to load a tilt series, reconstruct subvolumes and tomograms, and save the result.
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
This project is licensed under the BSD 3-Clause License - see the LICENSE file for details.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# https://peps.python.org/pep-0517/
|
|
2
|
+
[build-system]
|
|
3
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
4
|
+
build-backend = "hatchling.build"
|
|
5
|
+
|
|
6
|
+
# https://hatch.pypa.io/latest/config/metadata/
|
|
7
|
+
[tool.hatch.version]
|
|
8
|
+
source = "vcs"
|
|
9
|
+
tag-pattern = "^torch-reconstruct-tomogram@v(?P<version>.+)$"
|
|
10
|
+
fallback-version = "0.5.0"
|
|
11
|
+
|
|
12
|
+
[tool.hatch.version.raw-options]
|
|
13
|
+
search_parent_directories = true
|
|
14
|
+
tag_regex = "^torch-reconstruct-tomogram@v(?P<version>\\d+\\.\\d+\\.\\d+.*)$"
|
|
15
|
+
# Constrain git-describe so it only considers TeamTomo's own tags, not other workspace tags.
|
|
16
|
+
# See https://github.com/ofek/hatch-vcs/issues/71
|
|
17
|
+
git_describe_command = "git describe --dirty --tags --long --match 'torch-reconstruct-tomogram@v[0-9]*.[0-9]*.[0-9]*'"
|
|
18
|
+
|
|
19
|
+
# read more about configuring hatch at:
|
|
20
|
+
# https://hatch.pypa.io/latest/config/build/
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
only-include = ["src"]
|
|
23
|
+
sources = ["src"]
|
|
24
|
+
|
|
25
|
+
# https://peps.python.org/pep-0621/
|
|
26
|
+
[project]
|
|
27
|
+
name = "torch-reconstruct-tomogram"
|
|
28
|
+
dynamic = ["version"]
|
|
29
|
+
description = "(sub-)tomogram reconstruction and subtilt extraction for cryoET."
|
|
30
|
+
readme = "README.md"
|
|
31
|
+
requires-python = ">=3.11"
|
|
32
|
+
license = { text = "BSD-3-Clause" }
|
|
33
|
+
authors = [
|
|
34
|
+
{ name = "Marten Chaillet", email = "martenchaillet@gmail.com" },
|
|
35
|
+
{ name = "Davide Torre", email = "davidetorre99@gmail.com" },
|
|
36
|
+
]
|
|
37
|
+
# https://pypi.org/classifiers/
|
|
38
|
+
classifiers = [
|
|
39
|
+
"Development Status :: 3 - Alpha",
|
|
40
|
+
"License :: OSI Approved :: BSD License",
|
|
41
|
+
"Programming Language :: Python :: 3",
|
|
42
|
+
"Programming Language :: Python :: 3.11",
|
|
43
|
+
"Programming Language :: Python :: 3.12",
|
|
44
|
+
"Programming Language :: Python :: 3.13",
|
|
45
|
+
"Programming Language :: Python :: 3.14",
|
|
46
|
+
"Typing :: Typed",
|
|
47
|
+
]
|
|
48
|
+
# add your package dependencies here
|
|
49
|
+
dependencies = [
|
|
50
|
+
"torch",
|
|
51
|
+
"einops",
|
|
52
|
+
"torch-fourier-slice",
|
|
53
|
+
"torch-fourier-rescale",
|
|
54
|
+
"torch-ctf",
|
|
55
|
+
"torch-grid-utils>=0.0.8",
|
|
56
|
+
"torch-subpixel-crop",
|
|
57
|
+
"torch-tilt-series[io]",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# https://peps.python.org/pep-0621/#dependencies-optional-dependencies
|
|
61
|
+
# add dependencies for "extra" features here. Not dev dependencies.
|
|
62
|
+
# [project.optional-dependencies]
|
|
63
|
+
# name = ["dependency"]
|
|
64
|
+
|
|
65
|
+
[project.urls]
|
|
66
|
+
homepage = "https://github.com/teamtomo/teamtomo"
|
|
67
|
+
repository = "https://github.com/teamtomo/teamtomo"
|
|
68
|
+
documentation = "https://github.com/teamtomo/teamtomo#readme"
|
|
69
|
+
changelog = "https://github.com/teamtomo/teamtomo#changelog"
|
|
70
|
+
"Bug Tracker" = "https://github.com/teamtomo/teamtomo/issues"
|
|
71
|
+
"Source Code" = "https://github.com/teamtomo/teamtomo"
|
|
72
|
+
# Entry points
|
|
73
|
+
# https://peps.python.org/pep-0621/#entry-points
|
|
74
|
+
# same as console_scripts entry point
|
|
75
|
+
# [project.scripts]
|
|
76
|
+
# torch-reconstruct-tomogram-cli = "torch_reconstruct_tomogram:main_cli"
|
|
77
|
+
|
|
78
|
+
# [project.entry-points."some.group"]
|
|
79
|
+
# tomatoes = "torch_reconstruct_tomogram:main_tomatoes"
|
|
80
|
+
|
|
81
|
+
# https://peps.python.org/pep-0735/
|
|
82
|
+
# setup with `uv sync` or `pip install -e . --group dev`
|
|
83
|
+
[dependency-groups]
|
|
84
|
+
test = ["pytest", "pytest-cov"]
|
|
85
|
+
dev = [
|
|
86
|
+
{ include-group = "test" },
|
|
87
|
+
"ipython",
|
|
88
|
+
"mypy",
|
|
89
|
+
"pdbpp", # https://github.com/pdbpp/pdbpp
|
|
90
|
+
"pre-commit-uv",
|
|
91
|
+
"rich", # https://github.com/Textualize/rich
|
|
92
|
+
"ruff",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
[tool.uv.sources]
|
|
96
|
+
torch-tilt-series = { workspace = true }
|
|
97
|
+
|
|
98
|
+
# https://docs.astral.sh/ruff
|
|
99
|
+
[tool.ruff]
|
|
100
|
+
line-length = 88
|
|
101
|
+
target-version = "py311"
|
|
102
|
+
src = ["src"]
|
|
103
|
+
fix = true
|
|
104
|
+
# unsafe-fixes = true
|
|
105
|
+
|
|
106
|
+
[tool.ruff.lint]
|
|
107
|
+
pydocstyle = { convention = "numpy" }
|
|
108
|
+
select = [
|
|
109
|
+
"E", # style errors
|
|
110
|
+
"W", # style warnings
|
|
111
|
+
"F", # flakes
|
|
112
|
+
"D", # pydocstyle
|
|
113
|
+
"D417", # Missing argument descriptions in Docstrings
|
|
114
|
+
"I", # isort
|
|
115
|
+
"UP", # pyupgrade
|
|
116
|
+
"C4", # flake8-comprehensions
|
|
117
|
+
"B", # flake8-bugbear
|
|
118
|
+
"A001", # flake8-builtins
|
|
119
|
+
"RUF", # ruff-specific rules
|
|
120
|
+
"TC", # flake8-type-checking
|
|
121
|
+
"TID", # flake8-tidy-imports
|
|
122
|
+
]
|
|
123
|
+
ignore = [
|
|
124
|
+
"D401", # First line should be in imperative mood (remove to opt in)
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
[tool.ruff.lint.per-file-ignores]
|
|
128
|
+
"tests/*.py" = ["D", "S"]
|
|
129
|
+
"docs/*.py" = ["D", "A"]
|
|
130
|
+
|
|
131
|
+
# https://docs.astral.sh/ruff/formatter/
|
|
132
|
+
[tool.ruff.format]
|
|
133
|
+
docstring-code-format = true
|
|
134
|
+
skip-magic-trailing-comma = false # default is false
|
|
135
|
+
|
|
136
|
+
# https://docs.pytest.org/
|
|
137
|
+
[tool.pytest.ini_options]
|
|
138
|
+
minversion = "7.0"
|
|
139
|
+
addopts = ["--color=yes"]
|
|
140
|
+
testpaths = ["tests"]
|
|
141
|
+
filterwarnings = [
|
|
142
|
+
"error",
|
|
143
|
+
# mrcfile 1.5.x triggers this when reading MRC headers with NumPy >= 2.5
|
|
144
|
+
"ignore:Setting the dtype on a NumPy array has been deprecated:DeprecationWarning",
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
# https://mypy.readthedocs.io/en/stable/config_file.html
|
|
148
|
+
[tool.mypy]
|
|
149
|
+
files = "src/**/"
|
|
150
|
+
strict = true
|
|
151
|
+
disallow_any_generics = false
|
|
152
|
+
disallow_subclassing_any = false
|
|
153
|
+
show_error_codes = true
|
|
154
|
+
pretty = true
|
|
155
|
+
# plugins = ["pydantic.mypy"]
|
|
156
|
+
|
|
157
|
+
# # module specific overrides
|
|
158
|
+
# [[tool.mypy.overrides]]
|
|
159
|
+
# module = ["numpy.*",]
|
|
160
|
+
# ignore_errors = true
|
|
161
|
+
|
|
162
|
+
# https://coverage.readthedocs.io/
|
|
163
|
+
[tool.coverage.report]
|
|
164
|
+
show_missing = true
|
|
165
|
+
exclude_lines = [
|
|
166
|
+
"pragma: no cover",
|
|
167
|
+
"if TYPE_CHECKING:",
|
|
168
|
+
"@overload",
|
|
169
|
+
"except ImportError",
|
|
170
|
+
"raise AssertionError",
|
|
171
|
+
"\\.\\.\\.",
|
|
172
|
+
"raise NotImplementedError()",
|
|
173
|
+
"pass",
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
[tool.coverage.run]
|
|
177
|
+
source = ["torch_reconstruct_tomogram"]
|
|
178
|
+
|
|
179
|
+
# https://github.com/mgedmin/check-manifest#configuration
|
|
180
|
+
# add files that you want check-manifest to explicitly ignore here
|
|
181
|
+
# (files that are in the repo but shouldn't go in the package)
|
|
182
|
+
[tool.check-manifest]
|
|
183
|
+
ignore = [
|
|
184
|
+
".pre-commit-config.yaml",
|
|
185
|
+
".ruff_cache/**/*",
|
|
186
|
+
"tests/**/*",
|
|
187
|
+
"uv.lock",
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
# https://github.com/crate-ci/typos/blob/master/docs/reference.md
|
|
191
|
+
[tool.typos.default]
|
|
192
|
+
extend-ignore-identifiers-re = []
|
|
193
|
+
|
|
194
|
+
[tool.typos.default.extend-words]
|
|
195
|
+
writeable = "writeable"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""(sub-)tomogram reconstruction and subtilt extraction for cryoET."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("torch-reconstruct-tomogram")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
__version__ = "uninstalled"
|
|
9
|
+
__author__ = "Marten Chaillet, Davide Torre"
|
|
10
|
+
__email__ = "martenchaillet@gmail.com, davidetorre99@gmail.com"
|
|
11
|
+
|
|
12
|
+
from torch_reconstruct_tomogram.projection import (
|
|
13
|
+
extract_particle_tilt_series,
|
|
14
|
+
project_points,
|
|
15
|
+
)
|
|
16
|
+
from torch_reconstruct_tomogram.reconstruct import (
|
|
17
|
+
reconstruct_subvolume,
|
|
18
|
+
reconstruct_tomogram,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"extract_particle_tilt_series",
|
|
23
|
+
"project_points",
|
|
24
|
+
"reconstruct_subvolume",
|
|
25
|
+
"reconstruct_tomogram",
|
|
26
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Project 3D points into tilt images and crop patches for reconstruction."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
from torch_grid_utils import dft_center
|
|
7
|
+
from torch_subpixel_crop import subpixel_crop_2d
|
|
8
|
+
from torch_tilt_series import (
|
|
9
|
+
TiltSeries,
|
|
10
|
+
load_tilt_series_images,
|
|
11
|
+
preprocess_tilt_series_images,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def project_points(tilt_series: TiltSeries, points_zyx: torch.Tensor) -> torch.Tensor:
|
|
16
|
+
"""Project 3D points to 2D image pixel coordinates.
|
|
17
|
+
|
|
18
|
+
- points are 3D zyx coordinates in Angstroms, relative to the tomogram center
|
|
19
|
+
- tilt_series supplies the projection geometry (`tilt_series.project_points`
|
|
20
|
+
works in Angstroms) and `tilt_series.pixel_spacing` (raises if unset),
|
|
21
|
+
used to convert the projected Angstrom positions to pixels
|
|
22
|
+
- projected 2D points are in pixels, relative to the center of each image
|
|
23
|
+
"""
|
|
24
|
+
return tilt_series.project_points(points_zyx) / tilt_series.pixel_spacing
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _extract_particle_tilt_series(
|
|
28
|
+
tilt_series: TiltSeries,
|
|
29
|
+
images: torch.Tensor,
|
|
30
|
+
points_zyx: torch.Tensor,
|
|
31
|
+
sidelength: int,
|
|
32
|
+
return_rfft: bool = True,
|
|
33
|
+
) -> torch.Tensor:
|
|
34
|
+
"""Extract a subtilt-series given already-loaded images."""
|
|
35
|
+
projected_yx = project_points(tilt_series, points_zyx)
|
|
36
|
+
projected_yx = projected_yx + dft_center(
|
|
37
|
+
images.shape[-2:], rfft=False, fftshift=True, device=images.device
|
|
38
|
+
)
|
|
39
|
+
return subpixel_crop_2d(
|
|
40
|
+
image=images,
|
|
41
|
+
positions=projected_yx,
|
|
42
|
+
sidelength=sidelength,
|
|
43
|
+
return_rfft=return_rfft,
|
|
44
|
+
decenter=return_rfft,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def extract_particle_tilt_series(
|
|
49
|
+
tilt_series: TiltSeries,
|
|
50
|
+
points_zyx: torch.Tensor,
|
|
51
|
+
sidelength: int,
|
|
52
|
+
return_rfft: bool = True,
|
|
53
|
+
preprocess: bool = True,
|
|
54
|
+
**preprocessing_kwargs: Any,
|
|
55
|
+
) -> torch.Tensor:
|
|
56
|
+
"""Extract a subtilt-series at 3D location(s) in the sample.
|
|
57
|
+
|
|
58
|
+
Loads (and, by default, preprocesses) the raw tilt images matching
|
|
59
|
+
`tilt_series` via `tilt_series.image_path`/`image_indices`. Preprocessing
|
|
60
|
+
(see `torch_tilt_series.preprocess_tilt_series_images`) by default applies
|
|
61
|
+
plane subtraction, a DC-excluding bandpass with no low-pass (i.e. up to
|
|
62
|
+
Nyquist), and central-crop normalization. `**preprocessing_kwargs` are
|
|
63
|
+
forwarded to `preprocess_tilt_series_images`, overriding any of its
|
|
64
|
+
defaults (`low`, `high`, `falloff`, `bandpass_padding`,
|
|
65
|
+
`subtract_background`, `normalize`) - see that function's docstring for
|
|
66
|
+
details.
|
|
67
|
+
"""
|
|
68
|
+
images = load_tilt_series_images(tilt_series)
|
|
69
|
+
if preprocess:
|
|
70
|
+
images = preprocess_tilt_series_images(images, **preprocessing_kwargs)
|
|
71
|
+
return _extract_particle_tilt_series(
|
|
72
|
+
tilt_series,
|
|
73
|
+
images,
|
|
74
|
+
points_zyx,
|
|
75
|
+
sidelength,
|
|
76
|
+
return_rfft=return_rfft,
|
|
77
|
+
)
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""(sub-)tomogram reconstruction in pytorch."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import einops
|
|
6
|
+
import numpy as np
|
|
7
|
+
import torch
|
|
8
|
+
import torch.nn.functional as F
|
|
9
|
+
from torch_fourier_rescale import fourier_rescale_rfft_2d
|
|
10
|
+
from torch_fourier_slice import insert_central_slices_rfft_3d_multichannel
|
|
11
|
+
from torch_grid_utils import fftfreq_grid
|
|
12
|
+
from torch_tilt_series import (
|
|
13
|
+
TiltSeries,
|
|
14
|
+
load_tilt_series_images,
|
|
15
|
+
preprocess_tilt_series_images,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from torch_reconstruct_tomogram.projection import _extract_particle_tilt_series
|
|
19
|
+
|
|
20
|
+
_PAD_FACTOR = 2.0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _writable(data):
|
|
24
|
+
if isinstance(data, np.ndarray) and not data.flags.writeable:
|
|
25
|
+
data = data.copy()
|
|
26
|
+
return data
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _reconstruct_subvolume(
|
|
30
|
+
tilt_series: TiltSeries,
|
|
31
|
+
images: torch.Tensor,
|
|
32
|
+
points_zyx: torch.Tensor,
|
|
33
|
+
sidelength: int,
|
|
34
|
+
output_pixel_spacing: float | None = None,
|
|
35
|
+
) -> torch.Tensor:
|
|
36
|
+
"""Reconstruct subvolume(s), given already-loaded images."""
|
|
37
|
+
device = images.device
|
|
38
|
+
input_pixel_spacing = tilt_series.pixel_spacing # raises if unset
|
|
39
|
+
if output_pixel_spacing is None:
|
|
40
|
+
output_pixel_spacing = input_pixel_spacing
|
|
41
|
+
|
|
42
|
+
points_zyx = torch.as_tensor(_writable(points_zyx), device=device).float()
|
|
43
|
+
points_zyx, ps = einops.pack([points_zyx], "* zyx")
|
|
44
|
+
|
|
45
|
+
# tomogram -> detector rotation: projection_matrices is sample -> detector
|
|
46
|
+
# only, so compose tomo2sample's rotation in first. Every patch's
|
|
47
|
+
# Fourier-insertion rotation must be expressed relative to the
|
|
48
|
+
# tomogram frame.
|
|
49
|
+
rotation_matrices = (
|
|
50
|
+
tilt_series.projection_matrices[:, :3, :3] @ tilt_series.tomo2sample[:3, :3]
|
|
51
|
+
)
|
|
52
|
+
rotation_matrices = torch.linalg.pinv(rotation_matrices)
|
|
53
|
+
|
|
54
|
+
sidelength_padded_output = int(_PAD_FACTOR * sidelength)
|
|
55
|
+
sidelength_padded_native = max(
|
|
56
|
+
1,
|
|
57
|
+
round(sidelength_padded_output * output_pixel_spacing / input_pixel_spacing),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
particle_tilt_series_rfft = _extract_particle_tilt_series(
|
|
61
|
+
tilt_series,
|
|
62
|
+
images,
|
|
63
|
+
points_zyx,
|
|
64
|
+
sidelength=sidelength_padded_native,
|
|
65
|
+
return_rfft=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
particle_tilt_series_rfft = torch.fft.fftshift(particle_tilt_series_rfft, dim=(-2,))
|
|
69
|
+
|
|
70
|
+
particle_tilt_series_rfft = fourier_rescale_rfft_2d(
|
|
71
|
+
dft=particle_tilt_series_rfft,
|
|
72
|
+
image_shape=(sidelength_padded_native, sidelength_padded_native),
|
|
73
|
+
target_shape=(sidelength_padded_output, sidelength_padded_output),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
particle_tilt_series_rfft = einops.rearrange(
|
|
77
|
+
particle_tilt_series_rfft,
|
|
78
|
+
"n_positions n_tilts h w_rfft -> n_tilts n_positions h w_rfft",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
patches_rfft, weights = insert_central_slices_rfft_3d_multichannel(
|
|
82
|
+
image_rfft=particle_tilt_series_rfft,
|
|
83
|
+
volume_shape=(sidelength_padded_output,) * 3,
|
|
84
|
+
rotation_matrices=rotation_matrices,
|
|
85
|
+
zyx_matrices=True,
|
|
86
|
+
fftfreq_max=0.5,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
valid_weights = weights > 1e-3
|
|
90
|
+
patches_rfft[:, valid_weights] /= weights[valid_weights]
|
|
91
|
+
|
|
92
|
+
patches_rfft = torch.fft.ifftshift(patches_rfft, dim=(-3, -2))
|
|
93
|
+
|
|
94
|
+
patches = torch.fft.irfftn(
|
|
95
|
+
patches_rfft,
|
|
96
|
+
s=(sidelength_padded_output,) * 3,
|
|
97
|
+
dim=(-3, -2, -1),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
patches = torch.fft.ifftshift(patches, dim=(-3, -2, -1))
|
|
101
|
+
|
|
102
|
+
grid = fftfreq_grid(
|
|
103
|
+
image_shape=(sidelength_padded_output,) * 3,
|
|
104
|
+
rfft=False,
|
|
105
|
+
fftshift=True,
|
|
106
|
+
norm=True,
|
|
107
|
+
device=device,
|
|
108
|
+
)
|
|
109
|
+
patches = patches / torch.sinc(grid) ** 2
|
|
110
|
+
|
|
111
|
+
p = (sidelength_padded_output - sidelength) // 2
|
|
112
|
+
patches = F.pad(patches, [-p] * 6)
|
|
113
|
+
|
|
114
|
+
[patches] = einops.unpack(patches, ps, "* d h w")
|
|
115
|
+
|
|
116
|
+
return patches
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def reconstruct_subvolume(
|
|
120
|
+
tilt_series: TiltSeries,
|
|
121
|
+
points_zyx: torch.Tensor,
|
|
122
|
+
sidelength: int,
|
|
123
|
+
output_pixel_spacing: float | None = None,
|
|
124
|
+
preprocess: bool = True,
|
|
125
|
+
**preprocessing_kwargs: Any,
|
|
126
|
+
) -> torch.Tensor:
|
|
127
|
+
"""Reconstruct 3D patch(es) at location(s) in the sample.
|
|
128
|
+
|
|
129
|
+
Rank-polymorphic: input (..., 3) -> output (..., d, h, w)
|
|
130
|
+
|
|
131
|
+
- tilt_series supplies the projection geometry,
|
|
132
|
+
- points_zyx are zyx coordinates in Angstroms, relative to the tomogram center
|
|
133
|
+
- sidelength is the output subvolume size in voxels
|
|
134
|
+
- output_pixel_spacing is the voxel size of the output in Angstroms
|
|
135
|
+
(defaults to `tilt_series.pixel_spacing`); the per-tilt 2D crops are
|
|
136
|
+
Fourier-rescaled to this pixel size before 3D reconstruction, so local
|
|
137
|
+
(subvolume) and global (tomogram) reconstructions can each target an
|
|
138
|
+
arbitrary output pixel size independent of the raw data's
|
|
139
|
+
- preprocess, if True (default), applies
|
|
140
|
+
`torch_tilt_series.preprocess_tilt_series_images` to the loaded images
|
|
141
|
+
before reconstruction - by default plane subtraction, a DC-excluding
|
|
142
|
+
bandpass with no low-pass, i.e. up to Nyquist, and central-crop
|
|
143
|
+
normalization
|
|
144
|
+
- `**preprocessing_kwargs` are forwarded to `preprocess_tilt_series_images`,
|
|
145
|
+
overriding any of its defaults (`low`, `high`, `falloff`,
|
|
146
|
+
`bandpass_padding`, `subtract_background`, `normalize`) - see that
|
|
147
|
+
function's docstring for details
|
|
148
|
+
"""
|
|
149
|
+
images = load_tilt_series_images(tilt_series)
|
|
150
|
+
if preprocess:
|
|
151
|
+
images = preprocess_tilt_series_images(images, **preprocessing_kwargs)
|
|
152
|
+
return _reconstruct_subvolume(
|
|
153
|
+
tilt_series,
|
|
154
|
+
images,
|
|
155
|
+
points_zyx,
|
|
156
|
+
sidelength,
|
|
157
|
+
output_pixel_spacing=output_pixel_spacing,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _cosine_taper_window(core_length: int, margin: int, device) -> torch.Tensor:
|
|
162
|
+
"""1D cosine-taper window, flat in the middle, tapered at the edges.
|
|
163
|
+
|
|
164
|
+
1.0 over the central `core_length` samples, cosine-tapered from 0 up to 1
|
|
165
|
+
(and back down to 0) over `margin` samples on each side. Total length is
|
|
166
|
+
core_length + 2 * margin.
|
|
167
|
+
"""
|
|
168
|
+
if margin == 0:
|
|
169
|
+
return torch.ones(core_length, device=device)
|
|
170
|
+
ramp = 0.5 * (1 - torch.cos(torch.linspace(0, torch.pi, margin, device=device)))
|
|
171
|
+
core = torch.ones(core_length, device=device)
|
|
172
|
+
return torch.cat([ramp, core, ramp.flip(0)])
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def reconstruct_tomogram(
|
|
176
|
+
tilt_series: TiltSeries,
|
|
177
|
+
volume_shape: tuple[int, int, int],
|
|
178
|
+
sidelength: int,
|
|
179
|
+
batch_size: int | None = None,
|
|
180
|
+
output_pixel_spacing: float | None = None,
|
|
181
|
+
preprocess: bool = True,
|
|
182
|
+
blend_margin: int | None = None,
|
|
183
|
+
**preprocessing_kwargs: Any,
|
|
184
|
+
) -> torch.Tensor:
|
|
185
|
+
"""Reconstruct the full tomogram by tiling reconstructed patches in 3D.
|
|
186
|
+
|
|
187
|
+
- tilt_series supplies the projection geometry, plus `image_path`/
|
|
188
|
+
`image_indices` used to load the raw images
|
|
189
|
+
- volume_shape is the (d, h, w) shape of the output tomogram, in voxels
|
|
190
|
+
- sidelength is the spacing between patch centers, in voxels; patches
|
|
191
|
+
are reconstructed on a grid tiling `volume_shape`
|
|
192
|
+
- batch_size, if set, reconstructs at most this many patches per chunk
|
|
193
|
+
(to bound memory usage); defaults to reconstructing all patches at once
|
|
194
|
+
- output_pixel_spacing is the voxel size of the output in Angstroms
|
|
195
|
+
(defaults to `tilt_series.pixel_spacing`)
|
|
196
|
+
- preprocess, if True (default), applies
|
|
197
|
+
`torch_tilt_series.preprocess_tilt_series_images` to the loaded images
|
|
198
|
+
before reconstruction - by default plane subtraction, a DC-excluding
|
|
199
|
+
bandpass with no low-pass, i.e. up to Nyquist, and central-crop
|
|
200
|
+
normalization
|
|
201
|
+
- blend_margin is the extra margin, in voxels, added around each patch
|
|
202
|
+
(total reconstructed patch size is `sidelength + 2 * blend_margin`);
|
|
203
|
+
overlapping patches are cosine-tapered and blended together over this
|
|
204
|
+
margin to avoid seams at patch boundaries. Defaults to
|
|
205
|
+
`sidelength // 4`
|
|
206
|
+
- `**preprocessing_kwargs` are forwarded to `preprocess_tilt_series_images`,
|
|
207
|
+
overriding any of its defaults (`low`, `high`, `falloff`,
|
|
208
|
+
`bandpass_padding`, `subtract_background`, `normalize`) - see that
|
|
209
|
+
function's docstring for details
|
|
210
|
+
"""
|
|
211
|
+
images = load_tilt_series_images(tilt_series)
|
|
212
|
+
if preprocess:
|
|
213
|
+
images = preprocess_tilt_series_images(images, **preprocessing_kwargs)
|
|
214
|
+
|
|
215
|
+
pixel_spacing = tilt_series.pixel_spacing # raises if unset
|
|
216
|
+
if output_pixel_spacing is None:
|
|
217
|
+
output_pixel_spacing = pixel_spacing
|
|
218
|
+
|
|
219
|
+
if blend_margin is None:
|
|
220
|
+
blend_margin = sidelength // 4
|
|
221
|
+
patch_sidelength = sidelength + 2 * blend_margin
|
|
222
|
+
half = patch_sidelength // 2
|
|
223
|
+
|
|
224
|
+
d, h, w = volume_shape
|
|
225
|
+
r = sidelength // 2
|
|
226
|
+
device = images.device
|
|
227
|
+
|
|
228
|
+
z_centers = torch.arange(start=r, end=d + r, step=sidelength, device=device)
|
|
229
|
+
y_centers = torch.arange(start=r, end=h + r, step=sidelength, device=device)
|
|
230
|
+
x_centers = torch.arange(start=r, end=w + r, step=sidelength, device=device)
|
|
231
|
+
# absolute 0-indexed voxel coordinates of each patch center
|
|
232
|
+
centers_voxel = torch.stack(
|
|
233
|
+
torch.meshgrid(z_centers, y_centers, x_centers, indexing="ij"), dim=-1
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
volume_center = torch.tensor([d, h, w], device=device) // 2
|
|
237
|
+
centers_zyx_ang = (centers_voxel - volume_center) * output_pixel_spacing
|
|
238
|
+
|
|
239
|
+
window_1d = _cosine_taper_window(sidelength, blend_margin, device="cpu")
|
|
240
|
+
window_3d = (
|
|
241
|
+
window_1d[:, None, None] * window_1d[None, :, None] * window_1d[None, None, :]
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
tomogram_sum = torch.zeros(volume_shape, dtype=torch.float32)
|
|
245
|
+
weight_sum = torch.zeros(volume_shape, dtype=torch.float32)
|
|
246
|
+
|
|
247
|
+
centers_flat, _ = einops.pack([centers_voxel], "* zyx")
|
|
248
|
+
centers_ang_flat, _ = einops.pack([centers_zyx_ang], "* zyx")
|
|
249
|
+
chunk_size = batch_size or len(centers_flat)
|
|
250
|
+
|
|
251
|
+
for start in range(0, len(centers_flat), chunk_size):
|
|
252
|
+
chunk_centers = centers_flat[start : start + chunk_size]
|
|
253
|
+
chunk_centers_ang = centers_ang_flat[start : start + chunk_size]
|
|
254
|
+
|
|
255
|
+
patches_batch = _reconstruct_subvolume(
|
|
256
|
+
tilt_series,
|
|
257
|
+
images,
|
|
258
|
+
chunk_centers_ang,
|
|
259
|
+
patch_sidelength,
|
|
260
|
+
output_pixel_spacing=output_pixel_spacing,
|
|
261
|
+
).cpu()
|
|
262
|
+
|
|
263
|
+
for j in range(len(patches_batch)):
|
|
264
|
+
cz, cy, cx = chunk_centers[j].tolist()
|
|
265
|
+
z0, y0, x0 = cz - half, cy - half, cx - half
|
|
266
|
+
z1, y1, x1 = (
|
|
267
|
+
z0 + patch_sidelength,
|
|
268
|
+
y0 + patch_sidelength,
|
|
269
|
+
x0 + patch_sidelength,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# clip the patch's placement to the volume bounds
|
|
273
|
+
cz0, cy0, cx0 = max(z0, 0), max(y0, 0), max(x0, 0)
|
|
274
|
+
cz1, cy1, cx1 = min(z1, d), min(y1, h), min(x1, w)
|
|
275
|
+
if cz0 >= cz1 or cy0 >= cy1 or cx0 >= cx1:
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
src = (
|
|
279
|
+
slice(cz0 - z0, cz1 - z0),
|
|
280
|
+
slice(cy0 - y0, cy1 - y0),
|
|
281
|
+
slice(cx0 - x0, cx1 - x0),
|
|
282
|
+
)
|
|
283
|
+
dst = (slice(cz0, cz1), slice(cy0, cy1), slice(cx0, cx1))
|
|
284
|
+
weight_block = window_3d[src]
|
|
285
|
+
tomogram_sum[dst] += patches_batch[j][src] * weight_block
|
|
286
|
+
weight_sum[dst] += weight_block
|
|
287
|
+
|
|
288
|
+
del patches_batch
|
|
289
|
+
if device.type != "cpu":
|
|
290
|
+
torch.cuda.empty_cache()
|
|
291
|
+
|
|
292
|
+
tomogram = tomogram_sum / weight_sum.clamp_min(1e-6)
|
|
293
|
+
|
|
294
|
+
return tomogram.to(device)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
import torch
|
|
4
|
+
from torch_tilt_series import TiltSeries
|
|
5
|
+
|
|
6
|
+
import torch_reconstruct_tomogram
|
|
7
|
+
from torch_reconstruct_tomogram import (
|
|
8
|
+
project_points,
|
|
9
|
+
reconstruct_subvolume,
|
|
10
|
+
reconstruct_tomogram,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
DEVICES = ["cpu"] + (["cuda"] if torch.cuda.is_available() else [])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def make_tilt_series(tmp_path, device="cpu", size=32):
|
|
17
|
+
mrcfile = pytest.importorskip("mrcfile")
|
|
18
|
+
tilt_angles = torch.tensor([-30.0, 0.0, 30.0])
|
|
19
|
+
images = np.zeros((3, size, size), dtype=np.float32)
|
|
20
|
+
c = size // 2
|
|
21
|
+
images[:, c - 2 : c + 2, c - 2 : c + 2] = 1.0
|
|
22
|
+
image_path = tmp_path / "images.mrc"
|
|
23
|
+
mrcfile.write(image_path, images, overwrite=True)
|
|
24
|
+
return TiltSeries(
|
|
25
|
+
tilt_angles=tilt_angles,
|
|
26
|
+
tilt_axis_angle=torch.tensor(0.0),
|
|
27
|
+
sample_translations=torch.zeros((3, 2)),
|
|
28
|
+
image_path=image_path,
|
|
29
|
+
pixel_spacing=1.0,
|
|
30
|
+
device=device,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_imports_with_version():
|
|
35
|
+
assert isinstance(torch_reconstruct_tomogram.__version__, str)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.parametrize("device", DEVICES)
|
|
39
|
+
def test_reconstruct_subvolume(device, tmp_path):
|
|
40
|
+
tilt_series = make_tilt_series(tmp_path, device)
|
|
41
|
+
point_zyx = torch.tensor([0.0, 0.0, 0.0], device=device)
|
|
42
|
+
subvolume = reconstruct_subvolume(tilt_series, point_zyx, sidelength=8)
|
|
43
|
+
assert subvolume.shape == (8, 8, 8)
|
|
44
|
+
assert subvolume.dtype == torch.float32
|
|
45
|
+
assert device in str(subvolume.device)
|
|
46
|
+
assert torch.isfinite(subvolume).all()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.parametrize("device", DEVICES)
|
|
50
|
+
def test_reconstruct_subvolume_rank_polymorphic(device, tmp_path):
|
|
51
|
+
tilt_series = make_tilt_series(tmp_path, device)
|
|
52
|
+
|
|
53
|
+
# single point (3,) -> (d, h, w)
|
|
54
|
+
point = torch.tensor([0.0, 0.0, 0.0], device=device)
|
|
55
|
+
assert reconstruct_subvolume(tilt_series, point, sidelength=8).shape == (8, 8, 8)
|
|
56
|
+
|
|
57
|
+
# batch (N, 3) -> (N, d, h, w)
|
|
58
|
+
points = torch.tensor([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]], device=device)
|
|
59
|
+
assert reconstruct_subvolume(tilt_series, points, sidelength=8).shape == (
|
|
60
|
+
2,
|
|
61
|
+
8,
|
|
62
|
+
8,
|
|
63
|
+
8,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# 2D grid (a, b, 3) -> (a, b, d, h, w)
|
|
67
|
+
grid_2d = torch.zeros(2, 3, 3, device=device)
|
|
68
|
+
assert reconstruct_subvolume(tilt_series, grid_2d, sidelength=8).shape == (
|
|
69
|
+
2,
|
|
70
|
+
3,
|
|
71
|
+
8,
|
|
72
|
+
8,
|
|
73
|
+
8,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.parametrize("device", DEVICES)
|
|
78
|
+
def test_reconstruct_subvolume_output_pixel_spacing(device, tmp_path):
|
|
79
|
+
tilt_series = make_tilt_series(tmp_path, device)
|
|
80
|
+
point = torch.tensor([0.0, 0.0, 0.0], device=device)
|
|
81
|
+
|
|
82
|
+
subvolume_default = reconstruct_subvolume(tilt_series, point, sidelength=8)
|
|
83
|
+
subvolume_explicit = reconstruct_subvolume(
|
|
84
|
+
tilt_series, point, sidelength=8, output_pixel_spacing=1.0
|
|
85
|
+
)
|
|
86
|
+
assert torch.allclose(subvolume_default, subvolume_explicit)
|
|
87
|
+
|
|
88
|
+
subvolume_coarse = reconstruct_subvolume(
|
|
89
|
+
tilt_series, point, sidelength=8, output_pixel_spacing=2.0
|
|
90
|
+
)
|
|
91
|
+
assert subvolume_coarse.shape == (8, 8, 8)
|
|
92
|
+
assert torch.isfinite(subvolume_coarse).all()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.mark.parametrize("device", DEVICES)
|
|
96
|
+
def test_reconstruct_subvolume_local_shifts(device, tmp_path):
|
|
97
|
+
tilt_series = make_tilt_series(tmp_path, device)
|
|
98
|
+
point = torch.tensor([0.0, 0.0, 0.0], device=device)
|
|
99
|
+
|
|
100
|
+
def zero_local_shifts(projected_yx):
|
|
101
|
+
return torch.zeros_like(projected_yx)
|
|
102
|
+
|
|
103
|
+
subvolume_default = reconstruct_subvolume(tilt_series, point, sidelength=8)
|
|
104
|
+
tilt_series.local_shifts_2d = zero_local_shifts
|
|
105
|
+
subvolume_with_hook = reconstruct_subvolume(tilt_series, point, sidelength=8)
|
|
106
|
+
assert torch.allclose(subvolume_default, subvolume_with_hook)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_project_points_local_shifts_are_angstroms_not_pixels():
|
|
110
|
+
# pixel_spacing != 1 so an Angstrom-space shift and a pixel-space shift
|
|
111
|
+
# would disagree if local_shifts were (still) being applied in pixels.
|
|
112
|
+
shift_ang = torch.tensor([5.0, -3.0])
|
|
113
|
+
|
|
114
|
+
def shift_fn(projected_yx_ang):
|
|
115
|
+
return shift_ang.expand_as(projected_yx_ang)
|
|
116
|
+
|
|
117
|
+
tilt_series_shifted = TiltSeries(
|
|
118
|
+
tilt_angles=torch.tensor([0.0]),
|
|
119
|
+
tilt_axis_angle=torch.tensor(0.0),
|
|
120
|
+
sample_translations=torch.zeros((1, 2)),
|
|
121
|
+
pixel_spacing=2.0,
|
|
122
|
+
local_shifts_2d=shift_fn,
|
|
123
|
+
)
|
|
124
|
+
tilt_series = TiltSeries(
|
|
125
|
+
tilt_angles=torch.tensor([0.0]),
|
|
126
|
+
tilt_axis_angle=torch.tensor(0.0),
|
|
127
|
+
sample_translations=torch.zeros((1, 2)),
|
|
128
|
+
pixel_spacing=2.0,
|
|
129
|
+
)
|
|
130
|
+
point = torch.tensor([[0.0, 0.0, 0.0]])
|
|
131
|
+
|
|
132
|
+
shifted_px = project_points(tilt_series_shifted, point)
|
|
133
|
+
unshifted_px = project_points(tilt_series, point)
|
|
134
|
+
# shift is applied in Angstroms, then the whole result is divided by
|
|
135
|
+
# pixel_spacing so the pixel-space delta is shift_ang / pixel_spacing,
|
|
136
|
+
# not shift_ang itself.
|
|
137
|
+
expected_delta_px = shift_ang / tilt_series.pixel_spacing
|
|
138
|
+
assert torch.allclose(
|
|
139
|
+
(shifted_px - unshifted_px)[0, 0], expected_delta_px, atol=1e-5
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@pytest.mark.parametrize("device", DEVICES)
|
|
144
|
+
def test_reconstruct_subvolume_preprocess_toggle(device, tmp_path):
|
|
145
|
+
tilt_series = make_tilt_series(tmp_path, device)
|
|
146
|
+
point = torch.tensor([0.0, 0.0, 0.0], device=device)
|
|
147
|
+
|
|
148
|
+
preprocessed = reconstruct_subvolume(
|
|
149
|
+
tilt_series, point, sidelength=8, preprocess=True
|
|
150
|
+
)
|
|
151
|
+
raw = reconstruct_subvolume(tilt_series, point, sidelength=8, preprocess=False)
|
|
152
|
+
assert not torch.allclose(preprocessed, raw)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@pytest.mark.parametrize("device", DEVICES)
|
|
156
|
+
def test_reconstruct_subvolume_preprocessing_kwargs_are_forwarded(device, tmp_path):
|
|
157
|
+
tilt_series = make_tilt_series(tmp_path, device)
|
|
158
|
+
point = torch.tensor([0.0, 0.0, 0.0], device=device)
|
|
159
|
+
|
|
160
|
+
default = reconstruct_subvolume(tilt_series, point, sidelength=8)
|
|
161
|
+
custom = reconstruct_subvolume(
|
|
162
|
+
tilt_series, point, sidelength=8, high=0.3, bandpass_padding=4
|
|
163
|
+
)
|
|
164
|
+
assert not torch.allclose(default, custom)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@pytest.mark.parametrize("device", DEVICES)
|
|
168
|
+
def test_reconstruct_tomogram_output_pixel_spacing(device, tmp_path):
|
|
169
|
+
tilt_series = make_tilt_series(tmp_path, device)
|
|
170
|
+
volume = reconstruct_tomogram(
|
|
171
|
+
tilt_series, (16, 16, 16), sidelength=8, output_pixel_spacing=2.0
|
|
172
|
+
)
|
|
173
|
+
assert volume.shape == (16, 16, 16)
|
|
174
|
+
assert torch.isfinite(volume).all()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@pytest.mark.parametrize("device", DEVICES)
|
|
178
|
+
def test_reconstruct_tomogram(device, tmp_path):
|
|
179
|
+
tilt_series = make_tilt_series(tmp_path, device)
|
|
180
|
+
volume = reconstruct_tomogram(tilt_series, (16, 16, 16), sidelength=8)
|
|
181
|
+
assert volume.shape == (16, 16, 16)
|
|
182
|
+
assert volume.dtype == torch.float32
|
|
183
|
+
assert device in str(volume.device)
|
|
184
|
+
assert torch.isfinite(volume).all()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@pytest.mark.parametrize("device", DEVICES)
|
|
188
|
+
def test_reconstruct_tomogram_non_cubic(device, tmp_path):
|
|
189
|
+
tilt_series = make_tilt_series(tmp_path, device)
|
|
190
|
+
# shape not divisible by sidelength is still cropped to the requested shape
|
|
191
|
+
volume = reconstruct_tomogram(tilt_series, (8, 24, 20), sidelength=8)
|
|
192
|
+
assert volume.shape == (8, 24, 20)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@pytest.mark.parametrize("device", DEVICES)
|
|
196
|
+
def test_reconstruct_tomogram_batch_size(device, tmp_path):
|
|
197
|
+
tilt_series = make_tilt_series(tmp_path, device)
|
|
198
|
+
recon_no_batch = reconstruct_tomogram(tilt_series, (16, 16, 16), sidelength=8)
|
|
199
|
+
recon_with_batch = reconstruct_tomogram(
|
|
200
|
+
tilt_series, (16, 16, 16), sidelength=8, batch_size=2
|
|
201
|
+
)
|
|
202
|
+
assert recon_no_batch.shape == (16, 16, 16)
|
|
203
|
+
assert recon_with_batch.shape == (16, 16, 16)
|
|
204
|
+
diff = torch.abs(recon_no_batch - recon_with_batch.to(recon_no_batch.device)).max()
|
|
205
|
+
assert diff == 0.0
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_reconstruct_subvolume_rotation_includes_tomo2sample(tmp_path, monkeypatch):
|
|
209
|
+
"""The Fourier-insertion rotation must be the tomogram -> detector
|
|
210
|
+
rotation (projection_matrices' rotation composed with tomo2sample's),
|
|
211
|
+
not just projection_matrices' sample -> detector rotation alone.
|
|
212
|
+
Otherwise every patch is reconstructed oriented to sample space while
|
|
213
|
+
being tiled into an axis-aligned tomogram-space grid, tearing apart at
|
|
214
|
+
patch boundaries whenever sample2tomo carries a rotation. No-op when
|
|
215
|
+
sample2tomo is the default identity, which is why this needs its own
|
|
216
|
+
explicit test rather than relying on the (identity-only) tests above.
|
|
217
|
+
"""
|
|
218
|
+
from torch_affine_utils.transforms_3d import Rz
|
|
219
|
+
|
|
220
|
+
import torch_reconstruct_tomogram.reconstruct as reconstruct_module
|
|
221
|
+
|
|
222
|
+
tilt_series = make_tilt_series(tmp_path)
|
|
223
|
+
tilt_series.levelled2tomo = Rz(torch.tensor(37.0), zyx=True, device="cpu")
|
|
224
|
+
|
|
225
|
+
captured = {}
|
|
226
|
+
real_insert = reconstruct_module.insert_central_slices_rfft_3d_multichannel
|
|
227
|
+
|
|
228
|
+
def spy(*args, **kwargs):
|
|
229
|
+
captured["rotation_matrices"] = kwargs["rotation_matrices"].clone()
|
|
230
|
+
return real_insert(*args, **kwargs)
|
|
231
|
+
|
|
232
|
+
monkeypatch.setattr(
|
|
233
|
+
reconstruct_module, "insert_central_slices_rfft_3d_multichannel", spy
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
reconstruct_subvolume(tilt_series, torch.tensor([0.0, 0.0, 0.0]), sidelength=8)
|
|
237
|
+
|
|
238
|
+
expected_forward = (
|
|
239
|
+
tilt_series.projection_matrices[:, :3, :3] @ tilt_series.tomo2sample[:3, :3]
|
|
240
|
+
)
|
|
241
|
+
expected = torch.linalg.pinv(expected_forward)
|
|
242
|
+
assert torch.allclose(captured["rotation_matrices"], expected, atol=1e-5)
|