|
1 | 1 | """Build static axes-topology metadata for frame covariance calculations. |
2 | 2 |
|
3 | 3 | This module caches topology-only atom-index relationships needed by customised |
4 | | -united-atom axes calculations. The cache avoids repeated MDAnalysis selection |
5 | | -parsing inside the frame-local covariance loop while preserving frame-dependent |
6 | | -positions, forces, centres, axes, torques, and moments of inertia. |
| 4 | +axes calculations. The cache avoids repeated MDAnalysis selection parsing inside |
| 5 | +the frame-local covariance loop while preserving frame-dependent positions, |
| 6 | +forces, centres, axes, torques, and moments of inertia. |
7 | 7 | """ |
8 | 8 |
|
9 | 9 | from __future__ import annotations |
|
17 | 17 | logger = logging.getLogger(__name__) |
18 | 18 |
|
19 | 19 | UAKey = tuple[int, int, int] |
| 20 | +ResidueKey = tuple[int, int] |
20 | 21 |
|
21 | 22 |
|
22 | 23 | @dataclass(frozen=True) |
@@ -44,16 +45,35 @@ class UAAxesTopology: |
44 | 45 | residue_ua_masses: np.ndarray |
45 | 46 |
|
46 | 47 |
|
| 48 | +@dataclass(frozen=True) |
| 49 | +class ResidueAxesTopology: |
| 50 | + """Static topology required to compute customised residue axes. |
| 51 | +
|
| 52 | + Attributes: |
| 53 | + residue_heavy_indices: Heavy atom indices in the residue. |
| 54 | + residue_ua_masses: UA masses for heavy atoms in the residue. |
| 55 | + has_neighbor_bonds: Whether the residue is bonded to a neighbouring |
| 56 | + residue according to the original customised residue-axis selection. |
| 57 | + """ |
| 58 | + |
| 59 | + residue_heavy_indices: np.ndarray |
| 60 | + residue_ua_masses: np.ndarray |
| 61 | + has_neighbor_bonds: bool |
| 62 | + |
| 63 | + |
47 | 64 | @dataclass(frozen=True) |
48 | 65 | class AxesTopology: |
49 | 66 | """Cached axes topology for frame covariance calculations. |
50 | 67 |
|
51 | 68 | Attributes: |
52 | 69 | ua: Mapping from ``(mol_id, local_residue_id, ua_id)`` to cached |
53 | 70 | united-atom axes topology. |
| 71 | + residue: Mapping from ``(mol_id, local_residue_id)`` to cached |
| 72 | + residue axes topology. |
54 | 73 | """ |
55 | 74 |
|
56 | 75 | ua: dict[UAKey, UAAxesTopology] = field(default_factory=dict) |
| 76 | + residue: dict[ResidueKey, ResidueAxesTopology] = field(default_factory=dict) |
57 | 77 |
|
58 | 78 |
|
59 | 79 | class BuildAxesTopologyNode: |
@@ -86,24 +106,76 @@ def run(self, shared_data: dict[str, Any]) -> dict[str, Any]: |
86 | 106 | beads = shared_data["beads"] |
87 | 107 |
|
88 | 108 | ua_topology: dict[UAKey, UAAxesTopology] = {} |
| 109 | + residue_topology: dict[ResidueKey, ResidueAxesTopology] = {} |
89 | 110 | fragments = u.atoms.fragments |
90 | 111 |
|
91 | 112 | for mol_id, level_list in enumerate(levels): |
92 | | - if "united_atom" not in level_list: |
93 | | - continue |
| 113 | + mol = fragments[mol_id] |
| 114 | + |
| 115 | + if "residue" in level_list: |
| 116 | + self._add_residue_topology( |
| 117 | + mol=mol, |
| 118 | + mol_id=mol_id, |
| 119 | + beads=beads, |
| 120 | + out=residue_topology, |
| 121 | + ) |
94 | 122 |
|
95 | | - self._add_ua_topology( |
96 | | - u=u, |
97 | | - mol=fragments[mol_id], |
98 | | - mol_id=mol_id, |
99 | | - beads=beads, |
100 | | - out=ua_topology, |
101 | | - ) |
| 123 | + if "united_atom" in level_list: |
| 124 | + self._add_ua_topology( |
| 125 | + u=u, |
| 126 | + mol=mol, |
| 127 | + mol_id=mol_id, |
| 128 | + beads=beads, |
| 129 | + out=ua_topology, |
| 130 | + ) |
102 | 131 |
|
103 | | - topology = AxesTopology(ua=ua_topology) |
| 132 | + topology = AxesTopology(ua=ua_topology, residue=residue_topology) |
104 | 133 | shared_data["axes_topology"] = topology |
105 | 134 | return {"axes_topology": topology} |
106 | 135 |
|
| 136 | + def _add_residue_topology( |
| 137 | + self, |
| 138 | + *, |
| 139 | + mol: Any, |
| 140 | + mol_id: int, |
| 141 | + beads: dict[Any, list[np.ndarray]], |
| 142 | + out: dict[ResidueKey, ResidueAxesTopology], |
| 143 | + ) -> None: |
| 144 | + """Cache static residue axes topology for one molecule. |
| 145 | +
|
| 146 | + Args: |
| 147 | + mol: Molecule AtomGroup. |
| 148 | + mol_id: Molecule index. |
| 149 | + beads: Bead-index mapping produced by ``BuildBeadsNode``. |
| 150 | + out: Output residue topology mapping mutated in place. |
| 151 | + """ |
| 152 | + bead_key = (mol_id, "residue") |
| 153 | + bead_idx_list = beads.get(bead_key, []) |
| 154 | + if not bead_idx_list: |
| 155 | + return |
| 156 | + |
| 157 | + for local_res_i, residue in enumerate(mol.residues): |
| 158 | + if local_res_i >= len(bead_idx_list): |
| 159 | + continue |
| 160 | + |
| 161 | + residue_atoms = residue.atoms |
| 162 | + residue_heavy = residue_atoms.select_atoms("mass 2 to 999") |
| 163 | + residue_heavy_indices = residue_heavy.indices.astype(int, copy=True) |
| 164 | + residue_ua_masses = np.asarray( |
| 165 | + self._get_ua_masses_from_topology(residue_atoms), |
| 166 | + dtype=float, |
| 167 | + ) |
| 168 | + has_neighbor_bonds = self._has_neighbor_bonds( |
| 169 | + mol=mol, |
| 170 | + local_res_i=local_res_i, |
| 171 | + ) |
| 172 | + |
| 173 | + out[(mol_id, local_res_i)] = ResidueAxesTopology( |
| 174 | + residue_heavy_indices=residue_heavy_indices, |
| 175 | + residue_ua_masses=residue_ua_masses, |
| 176 | + has_neighbor_bonds=has_neighbor_bonds, |
| 177 | + ) |
| 178 | + |
107 | 179 | def _add_ua_topology( |
108 | 180 | self, |
109 | 181 | *, |
@@ -177,6 +249,27 @@ def _add_ua_topology( |
177 | 249 | residue_ua_masses=residue_ua_masses, |
178 | 250 | ) |
179 | 251 |
|
| 252 | + @staticmethod |
| 253 | + def _has_neighbor_bonds(*, mol: Any, local_res_i: int) -> bool: |
| 254 | + """Return whether a residue is bonded to neighbouring residues. |
| 255 | +
|
| 256 | + Args: |
| 257 | + mol: Molecule AtomGroup used for the original bonded-neighbour |
| 258 | + selection. |
| 259 | + local_res_i: Residue index local to ``mol``. |
| 260 | +
|
| 261 | + Returns: |
| 262 | + True when the residue has bonded atoms in the previous or next |
| 263 | + residue according to the original customised residue-axis query. |
| 264 | + """ |
| 265 | + index_prev = local_res_i - 1 |
| 266 | + index_next = local_res_i + 1 |
| 267 | + atom_set = mol.select_atoms( |
| 268 | + f"(resindex {index_prev} or resindex {index_next}) " |
| 269 | + f"and bonded resid {local_res_i}" |
| 270 | + ) |
| 271 | + return len(atom_set) > 0 |
| 272 | + |
180 | 273 | @staticmethod |
181 | 274 | def _split_bonded_atoms(atom: Any) -> tuple[Any, Any]: |
182 | 275 | """Return bonded heavy and light atoms for one atom. |
|
0 commit comments