torch-structure-manipulation 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.
Files changed (19) hide show
  1. torch_structure_manipulation-0.6.0/.gitignore +115 -0
  2. torch_structure_manipulation-0.6.0/LICENSE +29 -0
  3. torch_structure_manipulation-0.6.0/PKG-INFO +64 -0
  4. torch_structure_manipulation-0.6.0/README.md +38 -0
  5. torch_structure_manipulation-0.6.0/pyproject.toml +95 -0
  6. torch_structure_manipulation-0.6.0/src/torch_structure_manipulation/__init__.py +54 -0
  7. torch_structure_manipulation-0.6.0/src/torch_structure_manipulation/atomic_structure.py +228 -0
  8. torch_structure_manipulation-0.6.0/src/torch_structure_manipulation/bonding.py +260 -0
  9. torch_structure_manipulation-0.6.0/src/torch_structure_manipulation/bonding_data.json +679 -0
  10. torch_structure_manipulation-0.6.0/src/torch_structure_manipulation/py.typed +1 -0
  11. torch_structure_manipulation-0.6.0/src/torch_structure_manipulation/structure_transforms/__init__.py +45 -0
  12. torch_structure_manipulation-0.6.0/src/torch_structure_manipulation/structure_transforms/center_molecule.py +180 -0
  13. torch_structure_manipulation-0.6.0/src/torch_structure_manipulation/structure_transforms/rotate_molecule.py +118 -0
  14. torch_structure_manipulation-0.6.0/src/torch_structure_manipulation/structure_transforms/select_atoms.py +172 -0
  15. torch_structure_manipulation-0.6.0/src/torch_structure_manipulation/structure_transforms/translate_molecule.py +79 -0
  16. torch_structure_manipulation-0.6.0/src/torch_structure_manipulation/structure_transforms/utils.py +110 -0
  17. torch_structure_manipulation-0.6.0/tests/test_atomic_structure.py +151 -0
  18. torch_structure_manipulation-0.6.0/tests/test_bonding.py +98 -0
  19. torch_structure_manipulation-0.6.0/tests/test_structure_transforms.py +436 -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,64 @@
1
+ Metadata-Version: 2.5
2
+ Name: torch-structure-manipulation
3
+ Version: 0.6.0
4
+ Summary: Typed molecular structure data, bonding annotations, and tensor transforms
5
+ Project-URL: homepage, https://github.com/teamtomo/teamtomo
6
+ Project-URL: repository, https://github.com/teamtomo/teamtomo
7
+ Author: TeamTomo Developers
8
+ Author-email: Davide Torre <davidetorre99@gmail.com>
9
+ License: BSD-3-Clause
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: gemmi
21
+ Requires-Dist: numpy
22
+ Requires-Dist: pandas
23
+ Requires-Dist: roma
24
+ Requires-Dist: torch
25
+ Description-Content-Type: text/markdown
26
+
27
+ # torch-structure-manipulation
28
+
29
+ Typed, in-memory utilities for atomic structures:
30
+
31
+ - `AtomicStructure.from_dataframe` converts mmdf-compatible DataFrames into
32
+ device-aware tensors, with positions stored in `(z, y, x)` array order.
33
+ - `AtomicStructure.from_annotated_dataframe` annotates bonding metadata and
34
+ then constructs the structure in one step.
35
+ - `annotate_bonding_environments` adds template-derived bonding and
36
+ `protein`/`rna`/`other` molecule annotations without reading files.
37
+ - `classify_structure_composition` and `get_scattering_provider_keys` expose
38
+ aggregate composition labels and per-atom Peng provider keys respectively.
39
+ - Centering, rotation, translation, atom selection, and coordinate conversion
40
+ helpers are re-exported from the package root (also available under
41
+ ``structure_transforms``).
42
+
43
+ ```python
44
+ import mmdf
45
+
46
+ from torch_structure_manipulation import (
47
+ AtomicStructure,
48
+ annotate_bonding_environments,
49
+ center_structure,
50
+ )
51
+
52
+ # File I/O belongs to the caller; mmdf is not a runtime dependency.
53
+ atoms = mmdf.read("structure.cif")
54
+ structure = AtomicStructure.from_annotated_dataframe(atoms, include_hydrogens=False)
55
+
56
+ # Or annotate first when you still need the DataFrame:
57
+ annotated = annotate_bonding_environments(atoms)
58
+ centered = center_structure(annotated, center_point=(0.0, 0.0, 0.0), zyx=False)
59
+ structure = AtomicStructure.from_dataframe(centered)
60
+ ```
61
+
62
+ ## License
63
+
64
+ This project is licensed under the BSD 3-Clause License; see `LICENSE`.
@@ -0,0 +1,38 @@
1
+ # torch-structure-manipulation
2
+
3
+ Typed, in-memory utilities for atomic structures:
4
+
5
+ - `AtomicStructure.from_dataframe` converts mmdf-compatible DataFrames into
6
+ device-aware tensors, with positions stored in `(z, y, x)` array order.
7
+ - `AtomicStructure.from_annotated_dataframe` annotates bonding metadata and
8
+ then constructs the structure in one step.
9
+ - `annotate_bonding_environments` adds template-derived bonding and
10
+ `protein`/`rna`/`other` molecule annotations without reading files.
11
+ - `classify_structure_composition` and `get_scattering_provider_keys` expose
12
+ aggregate composition labels and per-atom Peng provider keys respectively.
13
+ - Centering, rotation, translation, atom selection, and coordinate conversion
14
+ helpers are re-exported from the package root (also available under
15
+ ``structure_transforms``).
16
+
17
+ ```python
18
+ import mmdf
19
+
20
+ from torch_structure_manipulation import (
21
+ AtomicStructure,
22
+ annotate_bonding_environments,
23
+ center_structure,
24
+ )
25
+
26
+ # File I/O belongs to the caller; mmdf is not a runtime dependency.
27
+ atoms = mmdf.read("structure.cif")
28
+ structure = AtomicStructure.from_annotated_dataframe(atoms, include_hydrogens=False)
29
+
30
+ # Or annotate first when you still need the DataFrame:
31
+ annotated = annotate_bonding_environments(atoms)
32
+ centered = center_structure(annotated, center_point=(0.0, 0.0, 0.0), zyx=False)
33
+ structure = AtomicStructure.from_dataframe(centered)
34
+ ```
35
+
36
+ ## License
37
+
38
+ This project is licensed under the BSD 3-Clause License; see `LICENSE`.
@@ -0,0 +1,95 @@
1
+ [build-system]
2
+ requires = ["hatchling", "hatch-vcs"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [tool.hatch.version]
6
+ source = "vcs"
7
+ tag-pattern = "^torch-structure-manipulation@v(?P<version>.+)$"
8
+ fallback-version = "0.1.0"
9
+
10
+ [tool.hatch.version.raw-options]
11
+ search_parent_directories = true
12
+ tag_regex = "^torch-structure-manipulation@v(?P<version>\\d+\\.\\d+\\.\\d+.*)$"
13
+ git_describe_command = "git describe --dirty --tags --long --match 'torch-structure-manipulation@v[0-9]*.[0-9]*.[0-9]*'"
14
+
15
+ [tool.hatch.build.targets.wheel]
16
+ only-include = ["src"]
17
+ sources = ["src"]
18
+
19
+ [project]
20
+ name = "torch-structure-manipulation"
21
+ dynamic = ["version"]
22
+ description = "Typed molecular structure data, bonding annotations, and tensor transforms"
23
+ readme = "README.md"
24
+ requires-python = ">=3.11"
25
+ license = { text = "BSD-3-Clause" }
26
+ authors = [
27
+ { name = "Davide Torre", email = "davidetorre99@gmail.com" },
28
+ { name = "TeamTomo Developers" },
29
+ ]
30
+ classifiers = [
31
+ "Development Status :: 3 - Alpha",
32
+ "License :: OSI Approved :: BSD License",
33
+ "Programming Language :: Python :: 3",
34
+ "Programming Language :: Python :: 3.11",
35
+ "Programming Language :: Python :: 3.12",
36
+ "Programming Language :: Python :: 3.13",
37
+ "Programming Language :: Python :: 3.14",
38
+ "Typing :: Typed",
39
+ ]
40
+ dependencies = ["gemmi", "numpy", "pandas", "roma", "torch"]
41
+
42
+ [project.urls]
43
+ homepage = "https://github.com/teamtomo/teamtomo"
44
+ repository = "https://github.com/teamtomo/teamtomo"
45
+
46
+ [dependency-groups]
47
+ test = ["pytest", "pytest-cov"]
48
+ dev = [
49
+ { include-group = "test" },
50
+ "ipython",
51
+ "mypy",
52
+ "pdbpp",
53
+ "rich",
54
+ "ruff",
55
+ ]
56
+
57
+ [tool.ruff]
58
+ line-length = 88
59
+ target-version = "py311"
60
+ src = ["src"]
61
+
62
+ [tool.ruff.lint]
63
+ pydocstyle = { convention = "numpy" }
64
+ select = ["E", "W", "F", "D", "D417", "I", "UP", "C4", "B", "A001", "RUF", "TC", "TID"]
65
+ ignore = ["D401"]
66
+
67
+ [tool.ruff.lint.per-file-ignores]
68
+ "tests/*.py" = ["D", "S"]
69
+
70
+ [tool.mypy]
71
+ files = "src/torch_structure_manipulation"
72
+ strict = true
73
+ disallow_any_generics = false
74
+ disallow_subclassing_any = false
75
+ show_error_codes = true
76
+ pretty = true
77
+
78
+ [[tool.mypy.overrides]]
79
+ module = ["pandas", "pandas.*"]
80
+ ignore_missing_imports = true
81
+
82
+ [[tool.mypy.overrides]]
83
+ module = ["roma", "roma.*"]
84
+ ignore_missing_imports = true
85
+
86
+ [tool.pytest.ini_options]
87
+ minversion = "7.0"
88
+ testpaths = ["tests"]
89
+ filterwarnings = ["error"]
90
+
91
+ [tool.coverage.run]
92
+ source = ["torch_structure_manipulation"]
93
+
94
+ [tool.check-manifest]
95
+ ignore = [".ruff_cache/**/*", "tests/**/*"]
@@ -0,0 +1,54 @@
1
+ """Atomic structure data, bonding annotations, and structure transforms."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ from .atomic_structure import AtomicStructure
6
+ from .bonding import (
7
+ annotate_bonding_environments,
8
+ classify_structure_composition,
9
+ get_scattering_provider_keys,
10
+ )
11
+ from .structure_transforms import (
12
+ apply_rotation,
13
+ apply_rotation_to_coords,
14
+ apply_translation,
15
+ apply_translation_to_coords,
16
+ ball_query_atoms,
17
+ calculate_center_from_tensors,
18
+ center_structure,
19
+ center_structure_from_coords,
20
+ df_to_atomxyz,
21
+ df_to_atomzyx,
22
+ find_atoms_in_ball,
23
+ get_nucleic_acid_residues,
24
+ get_protein_residues,
25
+ remove_sidechains,
26
+ separate_protein_rna,
27
+ )
28
+
29
+ try:
30
+ __version__ = version("torch-structure-manipulation")
31
+ except PackageNotFoundError:
32
+ __version__ = "uninstalled"
33
+
34
+ __all__ = [
35
+ "AtomicStructure",
36
+ "annotate_bonding_environments",
37
+ "apply_rotation",
38
+ "apply_rotation_to_coords",
39
+ "apply_translation",
40
+ "apply_translation_to_coords",
41
+ "ball_query_atoms",
42
+ "calculate_center_from_tensors",
43
+ "center_structure",
44
+ "center_structure_from_coords",
45
+ "classify_structure_composition",
46
+ "df_to_atomxyz",
47
+ "df_to_atomzyx",
48
+ "find_atoms_in_ball",
49
+ "get_nucleic_acid_residues",
50
+ "get_protein_residues",
51
+ "get_scattering_provider_keys",
52
+ "remove_sidechains",
53
+ "separate_protein_rna",
54
+ ]
@@ -0,0 +1,228 @@
1
+ """Tensor-backed atomic structure data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, replace
6
+ from typing import TYPE_CHECKING
7
+
8
+ import gemmi
9
+ import torch
10
+
11
+ if TYPE_CHECKING:
12
+ import pandas as pd
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class AtomicStructure:
17
+ """Lightweight numerical representation of an atomic structure.
18
+
19
+ Positions use array-friendly ``(z, y, x)`` order. Numerical fields may have
20
+ arbitrary broadcast-compatible batch dimensions before their atom dimension.
21
+ Text metadata is shared across batches and held in immutable tuples.
22
+
23
+ Bonding metadata (``bonded_environments``, ``molecule_types``) is not batched:
24
+ one tuple is shared by every batch member. That matches ensemble-of-poses use
25
+ cases (same chemistry, different coordinates) but not batched structures with
26
+ different chemistry. See :meth:`from_annotated_dataframe` and the bonded-factor
27
+ notes on
28
+ :func:`torch_calculate_electrostatic_potential.potential_from_structure_3d`.
29
+ """
30
+
31
+ positions_zyx: torch.Tensor
32
+ atomic_numbers: torch.Tensor
33
+ elements: tuple[str, ...]
34
+ atom_names: tuple[str, ...]
35
+ b_factors: torch.Tensor
36
+ occupancies: torch.Tensor
37
+ bonded_environments: tuple[str, ...] | None = None
38
+ molecule_types: tuple[str, ...] | None = None
39
+
40
+ def __post_init__(self) -> None:
41
+ """Validate atom dimensions and batch broadcasting."""
42
+ if self.positions_zyx.ndim < 2 or self.positions_zyx.shape[-1] != 3:
43
+ raise ValueError("positions_zyx must have shape (..., n_atoms, 3)")
44
+ n_atoms = self.positions_zyx.shape[-2]
45
+ if self.atomic_numbers.ndim < 1 or self.atomic_numbers.shape[-1] != n_atoms:
46
+ raise ValueError(
47
+ "atomic_numbers must have shape (..., n_atoms) with the same "
48
+ "number of atoms as positions_zyx"
49
+ )
50
+ if len(self.elements) != n_atoms or len(self.atom_names) != n_atoms:
51
+ raise ValueError("text metadata must have one value per atom")
52
+ numerical_fields = {
53
+ "b_factors": self.b_factors,
54
+ "occupancies": self.occupancies,
55
+ }
56
+ for name, value in numerical_fields.items():
57
+ if value.ndim > 0 and value.shape[-1] != n_atoms:
58
+ raise ValueError(f"{name} must be scalar or have shape (..., n_atoms)")
59
+ batch_shapes = [
60
+ self.positions_zyx.shape[:-2],
61
+ self.atomic_numbers.shape[:-1],
62
+ *[
63
+ value.shape[:-1] if value.ndim > 0 else ()
64
+ for value in numerical_fields.values()
65
+ ],
66
+ ]
67
+ try:
68
+ torch.broadcast_shapes(*batch_shapes) # type: ignore[no-untyped-call]
69
+ except RuntimeError as error:
70
+ raise ValueError(
71
+ "numerical AtomicStructure fields have incompatible batch shapes"
72
+ ) from error
73
+ if (
74
+ self.bonded_environments is not None
75
+ and len(self.bonded_environments) != n_atoms
76
+ ):
77
+ raise ValueError("bonded_environments must have one value per atom")
78
+ if self.molecule_types is not None and len(self.molecule_types) != n_atoms:
79
+ raise ValueError("molecule_types must have one value per atom")
80
+
81
+ @classmethod
82
+ def from_dataframe(
83
+ cls,
84
+ df: pd.DataFrame,
85
+ *,
86
+ device: torch.device | str | None = None,
87
+ dtype: torch.dtype = torch.float32,
88
+ ) -> AtomicStructure:
89
+ """Construct from an mmdf-compatible DataFrame.
90
+
91
+ Required columns are ``x``, ``y``, ``z``, and ``element``. Atom names
92
+ come from ``atom`` when present. ``b_isotropic`` and ``occupancy``
93
+ default to zero and one, respectively. Optional ``bonded_environments``
94
+ and ``molecule_type`` columns are preserved when present.
95
+ """
96
+ required = {"x", "y", "z", "element"}
97
+ missing = sorted(required.difference(df.columns))
98
+ if missing:
99
+ raise ValueError(f"missing required structure columns: {missing}")
100
+
101
+ elements = tuple(str(value).strip().upper() for value in df["element"])
102
+ if "atomic_number" in df:
103
+ atomic_number_values = [int(value) for value in df["atomic_number"]]
104
+ else:
105
+ atomic_number_values = [
106
+ gemmi.Element(element).atomic_number for element in elements
107
+ ]
108
+ unknown = sorted(
109
+ element
110
+ for element, atomic_number in zip(
111
+ elements, atomic_number_values, strict=True
112
+ )
113
+ if atomic_number == 0
114
+ )
115
+ if unknown:
116
+ raise ValueError(f"unknown element symbols: {unknown}")
117
+
118
+ positions = torch.as_tensor(
119
+ df.loc[:, ["z", "y", "x"]].to_numpy(copy=True),
120
+ dtype=dtype,
121
+ device=device,
122
+ )
123
+ atomic_numbers = torch.tensor(
124
+ atomic_number_values,
125
+ dtype=torch.int64,
126
+ device=device,
127
+ )
128
+ b_values = df["b_isotropic"] if "b_isotropic" in df else [0.0] * len(df)
129
+ occupancy_values = df["occupancy"] if "occupancy" in df else [1.0] * len(df)
130
+ atom_names = (
131
+ tuple(str(value).strip() for value in df["atom"])
132
+ if "atom" in df
133
+ else ("",) * len(df)
134
+ )
135
+ bonded = (
136
+ tuple(str(value) for value in df["bonded_environments"])
137
+ if "bonded_environments" in df
138
+ else None
139
+ )
140
+ molecule_types = (
141
+ tuple(str(value) for value in df["molecule_type"])
142
+ if "molecule_type" in df
143
+ else None
144
+ )
145
+ return cls(
146
+ positions_zyx=positions,
147
+ atomic_numbers=atomic_numbers,
148
+ elements=elements,
149
+ atom_names=atom_names,
150
+ b_factors=torch.as_tensor(b_values, dtype=dtype, device=device),
151
+ occupancies=torch.as_tensor(occupancy_values, dtype=dtype, device=device),
152
+ bonded_environments=bonded,
153
+ molecule_types=molecule_types,
154
+ )
155
+
156
+ @classmethod
157
+ def from_annotated_dataframe(
158
+ cls,
159
+ df: pd.DataFrame,
160
+ *,
161
+ include_hydrogens: bool = True,
162
+ device: torch.device | str | None = None,
163
+ dtype: torch.dtype = torch.float32,
164
+ ) -> AtomicStructure:
165
+ """Annotate bonding metadata, then construct from the result.
166
+
167
+ This is the usual entry point for Peng bonded scattering factors: it
168
+ calls :func:`~torch_structure_manipulation.annotate_bonding_environments`
169
+ to add ``bonded_environments`` and ``molecule_type`` columns, then
170
+ delegates to :meth:`from_dataframe`.
171
+
172
+ The input must include ``chain``, ``residue_id``, ``residue``, ``atom``,
173
+ and ``element`` in addition to the coordinate columns required by
174
+ :meth:`from_dataframe`.
175
+ """
176
+ from .bonding import annotate_bonding_environments
177
+
178
+ annotated = annotate_bonding_environments(
179
+ df, include_hydrogens=include_hydrogens
180
+ )
181
+ return cls.from_dataframe(annotated, device=device, dtype=dtype)
182
+
183
+ @property
184
+ def num_atoms(self) -> int:
185
+ """Number of atoms in each structure."""
186
+ return self.positions_zyx.shape[-2]
187
+
188
+ @property
189
+ def batch_shape(self) -> torch.Size:
190
+ """Broadcasted batch shape of all numerical fields."""
191
+ batch_shapes = [
192
+ self.positions_zyx.shape[:-2],
193
+ self.atomic_numbers.shape[:-1],
194
+ self.b_factors.shape[:-1] if self.b_factors.ndim > 0 else (),
195
+ self.occupancies.shape[:-1] if self.occupancies.ndim > 0 else (),
196
+ ]
197
+ return torch.Size(
198
+ torch.broadcast_shapes(*batch_shapes) # type: ignore[no-untyped-call]
199
+ )
200
+
201
+ @property
202
+ def device(self) -> torch.device:
203
+ """Device containing the atomic positions."""
204
+ return self.positions_zyx.device
205
+
206
+ def with_positions(self, positions_zyx: torch.Tensor) -> AtomicStructure:
207
+ """Return a copy with replacement, broadcast-compatible positions."""
208
+ if positions_zyx.ndim < 2 or positions_zyx.shape[-2:] != (self.num_atoms, 3):
209
+ raise ValueError(
210
+ "replacement positions must have shape (..., n_atoms, 3) with the "
211
+ "same number of atoms"
212
+ )
213
+ return replace(self, positions_zyx=positions_zyx)
214
+
215
+ def to(
216
+ self,
217
+ device: torch.device | str | None = None,
218
+ dtype: torch.dtype | None = None,
219
+ ) -> AtomicStructure:
220
+ """Return a copy with numerical tensors moved to a device and dtype."""
221
+ floating_dtype = self.positions_zyx.dtype if dtype is None else dtype
222
+ return replace(
223
+ self,
224
+ positions_zyx=self.positions_zyx.to(device=device, dtype=floating_dtype),
225
+ atomic_numbers=self.atomic_numbers.to(device=device),
226
+ b_factors=self.b_factors.to(device=device, dtype=floating_dtype),
227
+ occupancies=self.occupancies.to(device=device, dtype=floating_dtype),
228
+ )