pytorch-frustratometer 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytorch_frustratometer/__init__.py +50 -0
- pytorch_frustratometer/__main__.py +3 -0
- pytorch_frustratometer/algorithm/__init__.py +32 -0
- pytorch_frustratometer/algorithm/awsem.py +575 -0
- pytorch_frustratometer/algorithm/configurational.py +529 -0
- pytorch_frustratometer/algorithm/decoy_statistics.py +74 -0
- pytorch_frustratometer/algorithm/mj.py +180 -0
- pytorch_frustratometer/algorithm/mutational.py +1059 -0
- pytorch_frustratometer/algorithm/singleresidue.py +1862 -0
- pytorch_frustratometer/analysis/__init__.py +3 -0
- pytorch_frustratometer/analysis/dynamic_clusters.py +613 -0
- pytorch_frustratometer/base.py +543 -0
- pytorch_frustratometer/cli.py +658 -0
- pytorch_frustratometer/coefficients.py +203 -0
- pytorch_frustratometer/data/__init__.py +2 -0
- pytorch_frustratometer/data/burial_gamma.dat +20 -0
- pytorch_frustratometer/data/dataset.py +439 -0
- pytorch_frustratometer/data/io.py +1761 -0
- pytorch_frustratometer/data/shard_formats.py +778 -0
- pytorch_frustratometer/data/shard_io.py +48 -0
- pytorch_frustratometer/data/shard_writer.py +183 -0
- pytorch_frustratometer/data/water_gamma.dat +421 -0
- pytorch_frustratometer/frustratometer.py +1432 -0
- pytorch_frustratometer/original_code/AWSEM.py +369 -0
- pytorch_frustratometer/original_code/GenerateVisualizations.pl +82 -0
- pytorch_frustratometer/original_code/Pdb2Gro.py +106 -0
- pytorch_frustratometer/original_code/atomisitc_frustration.py +312 -0
- pytorch_frustratometer/original_code/draw_links.py +124 -0
- pytorch_frustratometer/original_code/fix_backbone.cpp +8232 -0
- pytorch_frustratometer/original_code/fix_backbone.h +527 -0
- pytorch_frustratometer/original_code/functions.R +1507 -0
- pytorch_frustratometer/original_code/smart_matrix_lib.h +652 -0
- pytorch_frustratometer/plots/__init__.py +0 -0
- pytorch_frustratometer/plots/common.py +355 -0
- pytorch_frustratometer/plots/debug/__init__.py +0 -0
- pytorch_frustratometer/plots/debug/configurational_outliers.py +186 -0
- pytorch_frustratometer/plots/debug/frustration_results.py +279 -0
- pytorch_frustratometer/plots/debug/mutational_outliers.py +133 -0
- pytorch_frustratometer/plots/debug/offdiagonal_analysis.py +390 -0
- pytorch_frustratometer/plots/debug/pairwise_results.py +475 -0
- pytorch_frustratometer/plots/debug/singleresidue_outliers.py +128 -0
- pytorch_frustratometer/plots/debug/structural_outlier_map.py +291 -0
- pytorch_frustratometer/plots/mutations.py +1534 -0
- pytorch_frustratometer/plots/mvsj.py +134 -0
- pytorch_frustratometer/plots/paper/__init__.py +0 -0
- pytorch_frustratometer/plots/paper/fig1_kde.py +136 -0
- pytorch_frustratometer/plots/paper/frustaevo_comparison.py +339 -0
- pytorch_frustratometer/plots/paper/frustrapy_comparison.py +489 -0
- pytorch_frustratometer/plots/paper/frustration_results.py +279 -0
- pytorch_frustratometer/plots/paper/msfa.py +321 -0
- pytorch_frustratometer/plots/paper/pairwise_results.py +475 -0
- pytorch_frustratometer/plots/paper/paper_figures.py +2216 -0
- pytorch_frustratometer/plots/paper/plot_funfam_comparison.py +460 -0
- pytorch_frustratometer/plots/paper/validation_figures.py +629 -0
- pytorch_frustratometer/plots/per_protein.py +2029 -0
- pytorch_frustratometer/plots/plot_self_consistency.py +263 -0
- pytorch_frustratometer/plots/singleresidue.py +457 -0
- pytorch_frustratometer/structure/__init__.py +6 -0
- pytorch_frustratometer/structure/geometry.py +467 -0
- pytorch_frustratometer/structure/ideal_residues.py +206 -0
- pytorch_frustratometer/structure/load_structure.py +205 -0
- pytorch_frustratometer/structure/parse.py +507 -0
- pytorch_frustratometer/structure/place_ideal_atoms.py +1031 -0
- pytorch_frustratometer/structure/protein.py +372 -0
- pytorch_frustratometer/structure/residue_constants.py +751 -0
- pytorch_frustratometer/utils/__init__.py +1 -0
- pytorch_frustratometer/utils/density.py +144 -0
- pytorch_frustratometer/utils/utils.py +138 -0
- pytorch_frustratometer/utils/weights.py +125 -0
- pytorch_frustratometer-0.1.0.data/data/pytorch_frustratometer/data/burial_gamma.dat +20 -0
- pytorch_frustratometer-0.1.0.data/data/pytorch_frustratometer/data/water_gamma.dat +421 -0
- pytorch_frustratometer-0.1.0.dist-info/METADATA +612 -0
- pytorch_frustratometer-0.1.0.dist-info/RECORD +76 -0
- pytorch_frustratometer-0.1.0.dist-info/WHEEL +4 -0
- pytorch_frustratometer-0.1.0.dist-info/entry_points.txt +2 -0
- pytorch_frustratometer-0.1.0.dist-info/licenses/AUTHORS.md +18 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from .coefficients import Coefficients
|
|
2
|
+
import torch
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def __getattr__(name: str):
|
|
6
|
+
if name == "analysis":
|
|
7
|
+
from . import analysis
|
|
8
|
+
return analysis
|
|
9
|
+
if name in ("FrustratometerDataset", "collate_frustratometer"):
|
|
10
|
+
from .data.dataset import FrustratometerDataset, collate_frustratometer
|
|
11
|
+
if name == "FrustratometerDataset":
|
|
12
|
+
return FrustratometerDataset
|
|
13
|
+
return collate_frustratometer
|
|
14
|
+
if name == "Frustratometer3":
|
|
15
|
+
from .frustratometer import Frustratometer3
|
|
16
|
+
return Frustratometer3
|
|
17
|
+
if name == "MJFrustratometer":
|
|
18
|
+
from .algorithm.mj import MJFrustratometer
|
|
19
|
+
return MJFrustratometer
|
|
20
|
+
if name == "SingleResidueFrustratometer":
|
|
21
|
+
from .algorithm.singleresidue import _get_SingleResidueFrustratometer
|
|
22
|
+
return _get_SingleResidueFrustratometer()
|
|
23
|
+
if name == "MutationalFrustratometer":
|
|
24
|
+
from .algorithm.mutational import _get_MutationalFrustratometer
|
|
25
|
+
return _get_MutationalFrustratometer()
|
|
26
|
+
if name == "ConfigurationalFrustratometer":
|
|
27
|
+
from .algorithm.configurational import _get_ConfigurationalFrustratometer
|
|
28
|
+
return _get_ConfigurationalFrustratometer()
|
|
29
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def Frustratometer(mode, coefficients=None, energy_function="awsem", output_dir=None,
|
|
33
|
+
dtype=torch.float32, **coeff_kwargs):
|
|
34
|
+
"""Build the model for ``mode``: one mode, ``"all"`` or a list of modes.
|
|
35
|
+
|
|
36
|
+
Returns ``Frustratometer3``, the model the CLI uses, for the AWSEM energy function and
|
|
37
|
+
``MJFrustratometer`` for ``energy_function="mj"``. Given ``coefficients`` are copied with ``mode`` set.
|
|
38
|
+
"""
|
|
39
|
+
import dataclasses
|
|
40
|
+
if coefficients is None:
|
|
41
|
+
coefficients = Coefficients.from_dict({"mode": mode, **coeff_kwargs})
|
|
42
|
+
else:
|
|
43
|
+
coefficients = dataclasses.replace(coefficients, mode=mode)
|
|
44
|
+
|
|
45
|
+
if energy_function == "mj":
|
|
46
|
+
from .algorithm.mj import MJFrustratometer
|
|
47
|
+
return MJFrustratometer(coefficients, output_dir=output_dir, dtype=dtype)
|
|
48
|
+
|
|
49
|
+
from .frustratometer import Frustratometer3
|
|
50
|
+
return Frustratometer3(coefficients, output_dir=output_dir, dtype=dtype)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from .awsem import AWSEM, _register_mode_mixins # noqa: F401
|
|
2
|
+
from .decoy_statistics import ( # noqa: F401
|
|
3
|
+
decoy_aa_frequencies, decoy_aa_weights, decoy_statistics,
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def __getattr__(name: str):
|
|
8
|
+
# Lazy-load lightning-dependent classes to avoid paying the import cost
|
|
9
|
+
# at package init time.
|
|
10
|
+
if name in ("FrustratometerBase", "_FromPDBMixin"):
|
|
11
|
+
import importlib
|
|
12
|
+
mod = importlib.import_module("..base", __name__)
|
|
13
|
+
return getattr(mod, name)
|
|
14
|
+
if name in ("Frustratometer3",):
|
|
15
|
+
from ..frustratometer import Frustratometer3
|
|
16
|
+
return Frustratometer3
|
|
17
|
+
if name == "SingleResidueFrustratometer":
|
|
18
|
+
from .singleresidue import _get_SingleResidueFrustratometer
|
|
19
|
+
return _get_SingleResidueFrustratometer()
|
|
20
|
+
if name == "MutationalFrustratometer":
|
|
21
|
+
from .mutational import _get_MutationalFrustratometer
|
|
22
|
+
return _get_MutationalFrustratometer()
|
|
23
|
+
if name == "ConfigurationalFrustratometer":
|
|
24
|
+
from .configurational import _get_ConfigurationalFrustratometer
|
|
25
|
+
return _get_ConfigurationalFrustratometer()
|
|
26
|
+
# Fall back to awsem module for any other names (e.g., compute_* functions)
|
|
27
|
+
try:
|
|
28
|
+
from . import awsem as _awsem
|
|
29
|
+
return getattr(_awsem, name)
|
|
30
|
+
except AttributeError:
|
|
31
|
+
pass
|
|
32
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional, Union
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
import torch
|
|
6
|
+
import torch.nn.functional as F
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from ..structure.residue_constants import CHARGES as charges
|
|
10
|
+
from ..utils.utils import frustration_loss as _frustration_loss
|
|
11
|
+
|
|
12
|
+
# Shared utilities live in _utils.py; re-exported here for backward compatibility.
|
|
13
|
+
from ..utils.utils import (
|
|
14
|
+
index_to_pseudo_logits,
|
|
15
|
+
compute_frustration_metrics,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AWSEM(torch.nn.Module):
|
|
20
|
+
"""
|
|
21
|
+
PyTorch implementation of the Frustratometer energy model.
|
|
22
|
+
|
|
23
|
+
This class computes four types of frustration metrics for protein
|
|
24
|
+
structures using AWSEM-style statistical potentials:
|
|
25
|
+
|
|
26
|
+
- Single-residue frustration
|
|
27
|
+
- Mutational frustration
|
|
28
|
+
- Configurational frustration
|
|
29
|
+
- Smooth frustration
|
|
30
|
+
|
|
31
|
+
The implementation is fully vectorized and supports batch
|
|
32
|
+
decoy evaluation for efficient computation.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
aa_type : torch.IntTensor
|
|
37
|
+
Residue identity indices for the protein sequence.
|
|
38
|
+
Shape: [N_res]. Values must be in [0, 19].
|
|
39
|
+
|
|
40
|
+
edge_index : torch.IntTensor
|
|
41
|
+
Pairwise contact edges used for interaction energies.
|
|
42
|
+
Shape: [2, N_edges].
|
|
43
|
+
|
|
44
|
+
distances : torch.FloatTensor
|
|
45
|
+
Pairwise distances corresponding to edges.
|
|
46
|
+
Shape: [N_edges] or [N_res, N_res].
|
|
47
|
+
|
|
48
|
+
phospho_sites : Optional[torch.IntTensor]
|
|
49
|
+
Indices of phosphorylated residues.
|
|
50
|
+
|
|
51
|
+
water_flag : bool
|
|
52
|
+
If True, use water-mediated interaction potentials.
|
|
53
|
+
|
|
54
|
+
n_decoys : int
|
|
55
|
+
Number of sampled decoys: configurational frustration, single-residue with
|
|
56
|
+
use_c_rand and mutational frustration with use_c_rand.
|
|
57
|
+
|
|
58
|
+
mode : list[str] | tuple[str] | str
|
|
59
|
+
Frustration modes to compute. Options are:
|
|
60
|
+
"singleresidue", "mutational", "configurational", "smooth".
|
|
61
|
+
|
|
62
|
+
run_dmfs : bool
|
|
63
|
+
If True, return decoy amino acid identities together with results.
|
|
64
|
+
"""
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
coefficients,
|
|
68
|
+
dtype: torch.dtype = torch.float32,
|
|
69
|
+
):
|
|
70
|
+
super().__init__()
|
|
71
|
+
self.coefficients = coefficients
|
|
72
|
+
self._dtype = dtype
|
|
73
|
+
|
|
74
|
+
self.all_modes = ["singleresidue", "mutational", "configurational", "configurational_exact", "smooth"]
|
|
75
|
+
|
|
76
|
+
mode = self.coefficients.mode
|
|
77
|
+
if isinstance(mode, str):
|
|
78
|
+
if mode == "all":
|
|
79
|
+
mode = ["singleresidue", "mutational", "configurational"]
|
|
80
|
+
else:
|
|
81
|
+
mode = [mode]
|
|
82
|
+
|
|
83
|
+
assert isinstance(mode, (list, tuple))
|
|
84
|
+
assert all(m in self.all_modes for m in mode)
|
|
85
|
+
|
|
86
|
+
self.mode = mode
|
|
87
|
+
self.run_dmfs = self.coefficients.run_dmfs
|
|
88
|
+
self.n_decoys = self.coefficients.tert_frust_ndecoys
|
|
89
|
+
self.water_flag = True
|
|
90
|
+
self.phospho_flag = False
|
|
91
|
+
self.use_c_rand = False # set to True to exactly reproduce C++ output (slow); forces float64
|
|
92
|
+
|
|
93
|
+
data_dir = Path(__file__).parent.parent / "data"
|
|
94
|
+
|
|
95
|
+
AWSEM_ORDER = ['A','R','N','D','C','Q','E','G','H','I','L','K','M','F','P','S','T','W','Y','V']
|
|
96
|
+
YOUR_ORDER = ['A','C','D','E','F','G','H','I','K','L','M','N','P','Q','R','S','T','V','W','Y']
|
|
97
|
+
|
|
98
|
+
# --- Mapping from YOUR order → AWSEM index ---
|
|
99
|
+
your_to_awsem = torch.tensor([AWSEM_ORDER.index(aa) for aa in YOUR_ORDER], dtype=torch.long)
|
|
100
|
+
|
|
101
|
+
burial_gamma_raw = np.loadtxt(str(data_dir / "burial_gamma.dat"))
|
|
102
|
+
burial_gamma_raw = torch.from_numpy(burial_gamma_raw).to(dtype)
|
|
103
|
+
|
|
104
|
+
# Remap to YOUR alphabetical order
|
|
105
|
+
burial_gamma = torch.ones((21,3)).to(dtype)
|
|
106
|
+
burial_gamma[:20, :] = burial_gamma_raw[your_to_awsem, :]
|
|
107
|
+
self.register_buffer("burial_gamma", burial_gamma)
|
|
108
|
+
|
|
109
|
+
# Water interaction gamma
|
|
110
|
+
if self.water_flag:
|
|
111
|
+
water_gamma_raw = np.loadtxt(str(data_dir / "water_gamma.dat")) # shape (420, 2)
|
|
112
|
+
|
|
113
|
+
water_gamma_raw = torch.from_numpy(water_gamma_raw).reshape(self.coefficients.n_wells, 210, 2).to(dtype)
|
|
114
|
+
water_gamma_raw *= self.coefficients.k_water
|
|
115
|
+
|
|
116
|
+
# Build upper triangle indices
|
|
117
|
+
i_idx, j_idx = torch.triu_indices(20, 20) # each length 210
|
|
118
|
+
|
|
119
|
+
water_gamma_20 = torch.zeros((self.coefficients.n_wells, 20, 20, 2)).to(dtype)
|
|
120
|
+
water_gamma_20[:, i_idx, j_idx, :] = water_gamma_raw # upper triangle
|
|
121
|
+
water_gamma_20[:, j_idx, i_idx, :] = water_gamma_raw # mirror to lower
|
|
122
|
+
|
|
123
|
+
# Remap rows and columns to YOUR alphabetical order
|
|
124
|
+
water_gamma_20 = water_gamma_20[:, your_to_awsem, :][:, :, your_to_awsem, :]
|
|
125
|
+
|
|
126
|
+
water_gamma = torch.ones((self.coefficients.n_wells, 21, 21, 2)).to(dtype)
|
|
127
|
+
water_gamma[:, :20, :20, :] = water_gamma_20
|
|
128
|
+
else:
|
|
129
|
+
water_gamma = torch.ones((self.coefficients.n_wells, 21, 21, 2)).to(dtype)
|
|
130
|
+
|
|
131
|
+
self.register_buffer("water_gamma", water_gamma)
|
|
132
|
+
|
|
133
|
+
# Register charge lookup as buffer so it moves with .to(device)
|
|
134
|
+
self.register_buffer("charge_lookup", torch.IntTensor(charges))
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def dtype(self) -> torch.dtype:
|
|
138
|
+
"""float64 when use_c_rand is set (required for C++ reproducibility), else self._dtype."""
|
|
139
|
+
return torch.float64 if self.use_c_rand else self._dtype
|
|
140
|
+
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
# Helpers
|
|
143
|
+
# ------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
def _b_idx(self, B: int, E: int, device) -> torch.Tensor:
|
|
146
|
+
"""[B, E] batch index tensor for advanced indexing."""
|
|
147
|
+
return torch.arange(B, device=device)[:, None].expand(-1, E)
|
|
148
|
+
|
|
149
|
+
def _gather_node(
|
|
150
|
+
self,
|
|
151
|
+
node_features: torch.Tensor, # [B, L] or [B, L, D]
|
|
152
|
+
edge_index: torch.Tensor, # [B, 2, E]
|
|
153
|
+
):
|
|
154
|
+
"""
|
|
155
|
+
Gather node features for src and dst of each edge.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
src_feats: [B, E] or [B, E, D]
|
|
159
|
+
dst_feats: [B, E] or [B, E, D]
|
|
160
|
+
"""
|
|
161
|
+
B, _, E = edge_index.shape
|
|
162
|
+
b_idx = self._b_idx(B, E, edge_index.device)
|
|
163
|
+
src = edge_index[:, 0, :] # [B, E]
|
|
164
|
+
dst = edge_index[:, 1, :] # [B, E]
|
|
165
|
+
return node_features[b_idx, src], node_features[b_idx, dst]
|
|
166
|
+
|
|
167
|
+
def _gather_edge_distances(
|
|
168
|
+
self,
|
|
169
|
+
distance: torch.Tensor, # [B, L, L]
|
|
170
|
+
edge_index: torch.Tensor, # [B, 2, E]
|
|
171
|
+
) -> torch.Tensor: # [B, E]
|
|
172
|
+
B, _, E = edge_index.shape
|
|
173
|
+
b_idx = self._b_idx(B, E, edge_index.device)
|
|
174
|
+
return distance[b_idx, edge_index[:, 0, :], edge_index[:, 1, :]]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def forward(self, batch):
|
|
178
|
+
"""
|
|
179
|
+
Compute requested frustration modes.
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
dict
|
|
184
|
+
Dictionary containing requested frustration metrics.
|
|
185
|
+
Keys may include:
|
|
186
|
+
|
|
187
|
+
- "singleresidue"
|
|
188
|
+
- "mutational"
|
|
189
|
+
- "configurational"
|
|
190
|
+
- "smooth"
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
result = {}
|
|
194
|
+
with torch.no_grad():
|
|
195
|
+
if "singleresidue" in self.mode or "smooth" in self.mode:
|
|
196
|
+
fi, nat_e, stats, sr_metrics, sr_losses, sr_mutations = \
|
|
197
|
+
self.compute_tert_frust_singleresidue(batch)
|
|
198
|
+
# smooth is derived from the single-residue FI, so keep it in the result even when
|
|
199
|
+
# only "smooth" was requested: the structure (.pdb with B-factors) and the .pml
|
|
200
|
+
# files that load it are written by the single-residue writer.
|
|
201
|
+
if "singleresidue" in self.mode or "smooth" in self.mode:
|
|
202
|
+
result["singleresidue"] = fi
|
|
203
|
+
result["_singleresidue_native_energy"] = nat_e
|
|
204
|
+
result["_singleresidue_decoy_stats"] = stats
|
|
205
|
+
if sr_metrics is not None:
|
|
206
|
+
result["_singleresidue_metrics"] = sr_metrics
|
|
207
|
+
if sr_losses is not None:
|
|
208
|
+
result["_singleresidue_losses"] = sr_losses
|
|
209
|
+
if sr_mutations is not None:
|
|
210
|
+
result["_singleresidue_mutations"] = sr_mutations
|
|
211
|
+
if "smooth" in self.mode:
|
|
212
|
+
# F_ij^smooth = -(Fi + Fj) (Guan et al. 2024, PNAS doi:10.1073/pnas.2410662121)
|
|
213
|
+
fi_flat = fi.squeeze(-1) if fi.dim() == 3 else fi # [B, L]
|
|
214
|
+
fi_src, fi_dst = self._gather_node(fi_flat, batch["edge_index"])
|
|
215
|
+
result["smooth"] = -(fi_src + fi_dst)
|
|
216
|
+
if "configurational" in self.mode or "configurational_exact" in self.mode or "mutational" in self.mode:
|
|
217
|
+
result.update(self.compute_tert_frust(batch))
|
|
218
|
+
return result
|
|
219
|
+
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
# Mutational / configurational frustration
|
|
222
|
+
# ------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
def compute_tert_frust(self, batch):
|
|
225
|
+
"""
|
|
226
|
+
Compute pairwise frustration metrics.
|
|
227
|
+
|
|
228
|
+
This function evaluates mutational and configurational frustration
|
|
229
|
+
for all interacting residue pairs.
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
dict
|
|
234
|
+
Dictionary containing frustration indices for each pair.
|
|
235
|
+
"""
|
|
236
|
+
native_energy = self.compute_native_ixn(batch)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
decoy_results = self.compute_decoy_ixns(batch)
|
|
240
|
+
|
|
241
|
+
# Native aa types at each edge: [B, E]
|
|
242
|
+
aa_src, aa_dst = self._gather_node(batch["aa_type"], batch["edge_index"])
|
|
243
|
+
base_aa = torch.stack([aa_src, aa_dst], dim=0) # [2, B, E]
|
|
244
|
+
|
|
245
|
+
out = {}
|
|
246
|
+
if "mutational" in self.mode:
|
|
247
|
+
fi_mut, nat_mut, mut_stats, mut_metrics, mut_losses, mut_mutations = \
|
|
248
|
+
self.compute_tert_frust_mutational(batch)
|
|
249
|
+
out["_mutational_native_energy"] = nat_mut
|
|
250
|
+
out["_mutational_decoy_stats"] = mut_stats
|
|
251
|
+
out["mutational"] = fi_mut
|
|
252
|
+
if mut_mutations is not None:
|
|
253
|
+
out["_mutational_mutations"] = mut_mutations
|
|
254
|
+
if mut_metrics is not None:
|
|
255
|
+
out["_mutational_metrics"] = mut_metrics
|
|
256
|
+
if mut_losses is not None:
|
|
257
|
+
out["_mutational_losses"] = mut_losses
|
|
258
|
+
|
|
259
|
+
if "configurational_exact" in decoy_results:
|
|
260
|
+
conf_stats = decoy_results["configurational_exact"]["stats"] # [B, 2]
|
|
261
|
+
fi_conf_exact = self.compute_frustration_index(
|
|
262
|
+
native_energy["configurational_exact"], conf_stats.unsqueeze(1)
|
|
263
|
+
)
|
|
264
|
+
out["_configurational_exact_native_energy"] = native_energy["configurational_exact"]
|
|
265
|
+
out["_configurational_exact_decoy_stats"] = conf_stats
|
|
266
|
+
out["configurational_exact"] = fi_conf_exact
|
|
267
|
+
|
|
268
|
+
if "configurational" in decoy_results:
|
|
269
|
+
conf_stats = decoy_results["configurational"]["stats"] # [B, 2]
|
|
270
|
+
# Unsqueeze to [B, 1, 2] so mean/std broadcast over the E edge dimension.
|
|
271
|
+
fi_conf = self.compute_frustration_index(native_energy["configurational"], conf_stats.unsqueeze(1))
|
|
272
|
+
|
|
273
|
+
out["_configurational_native_energy"] = native_energy["configurational"]
|
|
274
|
+
out["_configurational_decoy_stats"] = conf_stats
|
|
275
|
+
|
|
276
|
+
out["configurational"] = fi_conf
|
|
277
|
+
if self.run_dmfs:
|
|
278
|
+
conf_mut = decoy_results["configurational"]
|
|
279
|
+
if "energies" in conf_mut:
|
|
280
|
+
out["_configurational_mutations"] = {
|
|
281
|
+
"energies": conf_mut["energies"],
|
|
282
|
+
"res_type": conf_mut["res_type"],
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if "configurational_frust_true" in batch and "configurational_frust_mask" in batch:
|
|
286
|
+
true_conf = batch["configurational_frust_true"].to(fi_conf.device)
|
|
287
|
+
mask_conf = batch["configurational_frust_mask"].to(fi_conf.device).bool()
|
|
288
|
+
conf_metrics = compute_frustration_metrics(
|
|
289
|
+
fi_conf, true_conf, mask_conf,
|
|
290
|
+
threshold=self.coefficients.frustration_thresholds[1],
|
|
291
|
+
)
|
|
292
|
+
out["_configurational_metrics"] = conf_metrics
|
|
293
|
+
# FrustrAI-Fold-compatible losses
|
|
294
|
+
pseudo_logits_conf = index_to_pseudo_logits(
|
|
295
|
+
fi_conf, self.coefficients.frustration_thresholds[1]
|
|
296
|
+
) # [B, E, 3]
|
|
297
|
+
welltype_conf = batch.get("configurational_welltype")
|
|
298
|
+
idx_loss_conf, cls_loss_conf = _frustration_loss(
|
|
299
|
+
mode=2,
|
|
300
|
+
pred_frustration_index=fi_conf.unsqueeze(-1),
|
|
301
|
+
pred_frustration_class=pseudo_logits_conf,
|
|
302
|
+
true_frustration_index=true_conf,
|
|
303
|
+
seq_mask=mask_conf,
|
|
304
|
+
welltype=welltype_conf,
|
|
305
|
+
stage="test",
|
|
306
|
+
)
|
|
307
|
+
out["_configurational_losses"] = (idx_loss_conf.item(), cls_loss_conf.item())
|
|
308
|
+
|
|
309
|
+
return out
|
|
310
|
+
|
|
311
|
+
def compute_native_ixn(self, batch):
|
|
312
|
+
"""
|
|
313
|
+
Compute native interaction energies.
|
|
314
|
+
|
|
315
|
+
Returns
|
|
316
|
+
-------
|
|
317
|
+
dict
|
|
318
|
+
Dictionary with native energies for each frustration mode.
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
result = {}
|
|
322
|
+
B, _, E = batch["edge_index"].shape
|
|
323
|
+
device = batch["edge_index"].device
|
|
324
|
+
|
|
325
|
+
# Gather per-edge quantities
|
|
326
|
+
aa_src, aa_dst = self._gather_node(batch["aa_type"], batch["edge_index"]) # [B, E]
|
|
327
|
+
rho_src, rho_dst = self._gather_node(batch["residue_density"], batch["edge_index"]) # [B, E]
|
|
328
|
+
edge_distances = self._gather_edge_distances(batch["distance"], batch["edge_index"]) # [B, E]
|
|
329
|
+
|
|
330
|
+
aa_type = torch.stack([aa_src, aa_dst], dim=0) # [2, B, E]
|
|
331
|
+
density = torch.stack([rho_src, rho_dst], dim=0) # [2, B, E]
|
|
332
|
+
|
|
333
|
+
src = batch["edge_index"][:, 0, :] # [B, E]
|
|
334
|
+
dst = batch["edge_index"][:, 1, :] # [B, E]
|
|
335
|
+
|
|
336
|
+
electrostatics = self.calculate_electrostatics(aa_src, aa_dst, edge_distances) # [B, E]
|
|
337
|
+
|
|
338
|
+
if "configurational" in self.mode or "configurational_exact" in self.mode:
|
|
339
|
+
nat_conf = self.compute_configurational_ixn(
|
|
340
|
+
batch,
|
|
341
|
+
aa_type,
|
|
342
|
+
edge_distances,
|
|
343
|
+
density,
|
|
344
|
+
electrostatics,
|
|
345
|
+
return_decoy_energies=True, # return raw [B, E] energy
|
|
346
|
+
return_stats=False,
|
|
347
|
+
) # [B, E]
|
|
348
|
+
if "configurational" in self.mode:
|
|
349
|
+
result["configurational"] = nat_conf
|
|
350
|
+
if "configurational_exact" in self.mode:
|
|
351
|
+
result["configurational_exact"] = nat_conf
|
|
352
|
+
|
|
353
|
+
return result
|
|
354
|
+
|
|
355
|
+
def compute_decoy_ixns(self, batch):
|
|
356
|
+
"""
|
|
357
|
+
Generate decoy interactions and compute their energies.
|
|
358
|
+
|
|
359
|
+
Returns
|
|
360
|
+
-------
|
|
361
|
+
dict
|
|
362
|
+
Dictionary containing decoy statistics and sampled amino acids.
|
|
363
|
+
"""
|
|
364
|
+
from .configurational import sample_configurational_decoys
|
|
365
|
+
|
|
366
|
+
results = {}
|
|
367
|
+
|
|
368
|
+
if "configurational" in self.mode:
|
|
369
|
+
decoy_distances, decoy_density, decoy_aa = sample_configurational_decoys(
|
|
370
|
+
distance_matrix=batch["distance"],
|
|
371
|
+
residue_density=batch["residue_density"],
|
|
372
|
+
aa_type=batch["aa_type"],
|
|
373
|
+
seq_mask=batch["seq_mask"],
|
|
374
|
+
n_decoys=self.n_decoys,
|
|
375
|
+
cutoff=self.coefficients.tert_frust_cutoff,
|
|
376
|
+
use_c_rand=self.use_c_rand,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
decoy_elec = self.calculate_electrostatics(decoy_aa[0], decoy_aa[1], decoy_distances)
|
|
380
|
+
|
|
381
|
+
conf_stats = self._compute_energy(
|
|
382
|
+
batch, decoy_aa, decoy_distances, decoy_density, decoy_elec,
|
|
383
|
+
return_decoy_energies=self.run_dmfs, return_stats=True
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
if self.run_dmfs:
|
|
387
|
+
results["configurational"] = {
|
|
388
|
+
"stats": conf_stats[1], "energies": conf_stats[0], "res_type": decoy_aa
|
|
389
|
+
}
|
|
390
|
+
else:
|
|
391
|
+
results["configurational"] = {"stats": conf_stats}
|
|
392
|
+
|
|
393
|
+
if "configurational_exact" in self.mode:
|
|
394
|
+
from .configurational_exact import enumerate_configurational_decoys
|
|
395
|
+
exact_stats = enumerate_configurational_decoys(self, batch)
|
|
396
|
+
results["configurational_exact"] = {"stats": exact_stats}
|
|
397
|
+
|
|
398
|
+
return results
|
|
399
|
+
|
|
400
|
+
# ------------------------------------------------------------------
|
|
401
|
+
# Energy terms
|
|
402
|
+
# ------------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
def compute_frustration_index(self, energy, stats):
|
|
405
|
+
"""stats: [..., 2] where [..., 0]=mean, [..., 1]=std."""
|
|
406
|
+
std = stats[..., 1]
|
|
407
|
+
fi = (stats[..., 0] - energy) / std.clamp(min=1e-8)
|
|
408
|
+
# std==0 means all decoys gave identical energy → no information → FI=0 (neutral)
|
|
409
|
+
return torch.where(std < 1e-8, torch.zeros_like(fi), fi)
|
|
410
|
+
|
|
411
|
+
def compute_burial_energy(
|
|
412
|
+
self,
|
|
413
|
+
batch,
|
|
414
|
+
res_type,
|
|
415
|
+
rho
|
|
416
|
+
):
|
|
417
|
+
"""
|
|
418
|
+
Args:
|
|
419
|
+
res_type: [B, L] or [2, B, E] or [B, n_decoys]
|
|
420
|
+
rho: same shape as res_type
|
|
421
|
+
Returns:
|
|
422
|
+
burial_energy: same shape as res_type (float64)
|
|
423
|
+
"""
|
|
424
|
+
device = res_type.device
|
|
425
|
+
rho = rho.to(self.dtype)
|
|
426
|
+
|
|
427
|
+
t_min = torch.tanh(self.coefficients.burial_kappa * (rho.unsqueeze(-1) - self.coefficients.burial_ro_min.to(device, dtype=self.dtype))) # [..., L, 3]
|
|
428
|
+
t_max = torch.tanh(self.coefficients.burial_kappa * (self.coefficients.burial_ro_max.to(device, dtype=self.dtype) - rho.unsqueeze(-1))) # [..., L, 3]
|
|
429
|
+
|
|
430
|
+
# C++ uses (t_min + t_max), NOT 0.25*(1+t_min)*(1+t_max)
|
|
431
|
+
theta = t_min + t_max # [..., L, 3]
|
|
432
|
+
|
|
433
|
+
burial_gamma = self.get_burial_gamma(batch, res_type).to(device, dtype=self.dtype) # [..., L, 3]
|
|
434
|
+
burial_energy = -0.5 * self.coefficients.k_burial * (burial_gamma * theta).sum(dim=-1)
|
|
435
|
+
|
|
436
|
+
return burial_energy
|
|
437
|
+
|
|
438
|
+
def compute_water_energy(
|
|
439
|
+
self,
|
|
440
|
+
batch,
|
|
441
|
+
distances,
|
|
442
|
+
density1,
|
|
443
|
+
density2,
|
|
444
|
+
aa_type1,
|
|
445
|
+
aa_type2
|
|
446
|
+
):
|
|
447
|
+
"""
|
|
448
|
+
Fully vectorized water energy computation for all edges at once.
|
|
449
|
+
|
|
450
|
+
edge_index: [2, N_edges] tensor of interacting residue indices
|
|
451
|
+
distances: [N_edges] tensor of distances
|
|
452
|
+
residue_density: [N_res] tensor of per-residue ρ
|
|
453
|
+
aa_type: [N_res] tensor of residue types
|
|
454
|
+
water_gamma: [n_wells, 20, 20, 2] gamma table
|
|
455
|
+
"""
|
|
456
|
+
device = distances.device
|
|
457
|
+
distances = distances.to(self.dtype)
|
|
458
|
+
density1 = density1.to(self.dtype)
|
|
459
|
+
density2 = density2.to(self.dtype)
|
|
460
|
+
|
|
461
|
+
# Get leading dimensions from aa_type1
|
|
462
|
+
leading_shape = aa_type1.shape[:-1] # Everything except last dim (edges)
|
|
463
|
+
n_edges = aa_type1.shape[-1]
|
|
464
|
+
|
|
465
|
+
sigma_wat_i = (1.0 - torch.tanh(self.coefficients.water_kappa_sigma * (density1 - self.coefficients.threshold)))
|
|
466
|
+
sigma_wat_j = (1.0 - torch.tanh(self.coefficients.water_kappa_sigma * (density2 - self.coefficients.threshold)))
|
|
467
|
+
sigma_wat = 0.25 * sigma_wat_i * sigma_wat_j
|
|
468
|
+
sigma_prot = 1.0 - sigma_wat
|
|
469
|
+
|
|
470
|
+
# Water gamma for each pair
|
|
471
|
+
water_gamma = self.get_water_gamma(batch) # [n_wells, 20, 20, 2]
|
|
472
|
+
|
|
473
|
+
# Flatten leading dims for indexing, then reshape back
|
|
474
|
+
aa_type1_flat = aa_type1.reshape(-1, n_edges) # [D*B, N_edges]
|
|
475
|
+
aa_type2_flat = aa_type2.reshape(-1, n_edges)
|
|
476
|
+
|
|
477
|
+
# Index and reshape to preserve leading dims
|
|
478
|
+
water_gamma_direct_flat = water_gamma[0, aa_type1_flat, aa_type2_flat, :].mean(dim=-1) # [D*B, N_edges]
|
|
479
|
+
water_gamma_prot_mediated_flat = water_gamma[1, aa_type1_flat, aa_type2_flat, 0] # [D*B, N_edges]
|
|
480
|
+
water_gamma_wat_mediated_flat = water_gamma[1, aa_type1_flat, aa_type2_flat, 1] # [D*B, N_edges]
|
|
481
|
+
|
|
482
|
+
# Reshape back to include leading dims
|
|
483
|
+
water_gamma_direct = water_gamma_direct_flat.reshape(*leading_shape, n_edges)
|
|
484
|
+
water_gamma_prot_mediated = water_gamma_prot_mediated_flat.reshape(*leading_shape, n_edges)
|
|
485
|
+
water_gamma_wat_mediated = water_gamma_wat_mediated_flat.reshape(*leading_shape, n_edges)
|
|
486
|
+
|
|
487
|
+
sigma_gamma_direct = water_gamma_direct
|
|
488
|
+
sigma_gamma_mediated = sigma_prot * water_gamma_prot_mediated + sigma_wat * water_gamma_wat_mediated
|
|
489
|
+
|
|
490
|
+
# Theta functions for direct and mediated wells
|
|
491
|
+
# Direct well
|
|
492
|
+
t_min_direct = torch.tanh(self.coefficients.water_kappa * (distances - self.coefficients.well_rmin[0].to(device, dtype=self.dtype)))
|
|
493
|
+
t_max_direct = torch.tanh(self.coefficients.water_kappa * (self.coefficients.well_rmax[0].to(device, dtype=self.dtype) - distances))
|
|
494
|
+
theta_direct = 0.25 * (1 + t_min_direct) * (1 + t_max_direct)
|
|
495
|
+
|
|
496
|
+
# Mediated well
|
|
497
|
+
t_min_mediated = torch.tanh(self.coefficients.water_kappa * (distances - self.coefficients.well_rmin[1].to(device, dtype=self.dtype)))
|
|
498
|
+
t_max_mediated = torch.tanh(self.coefficients.water_kappa * (self.coefficients.well_rmax[1].to(device, dtype=self.dtype) - distances))
|
|
499
|
+
theta_mediated = 0.25 * (1 + t_min_mediated) * (1 + t_max_mediated)
|
|
500
|
+
|
|
501
|
+
# Total water energy
|
|
502
|
+
water_energy = -(sigma_gamma_direct * theta_direct + sigma_gamma_mediated * theta_mediated)
|
|
503
|
+
|
|
504
|
+
return water_energy
|
|
505
|
+
|
|
506
|
+
def calculate_electrostatics(
|
|
507
|
+
self,
|
|
508
|
+
aa_type1,
|
|
509
|
+
aa_type2,
|
|
510
|
+
distances,
|
|
511
|
+
):
|
|
512
|
+
"""
|
|
513
|
+
Args:
|
|
514
|
+
aa_type1/2: int tensors, any shape
|
|
515
|
+
distances: float tensor, same shape
|
|
516
|
+
Returns:
|
|
517
|
+
V: same shape as distances
|
|
518
|
+
"""
|
|
519
|
+
if not self.coefficients.use_electrostatics:
|
|
520
|
+
return torch.zeros_like(distances, dtype=self.dtype)
|
|
521
|
+
|
|
522
|
+
q_i = self.charge_lookup[aa_type1]
|
|
523
|
+
q_j = self.charge_lookup[aa_type2]
|
|
524
|
+
|
|
525
|
+
charged_mask = (q_i != 0) & (q_j != 0)
|
|
526
|
+
|
|
527
|
+
k = self.get_k_ij(q_i, q_j)
|
|
528
|
+
# Clamp to avoid 0/0=NaN for padded entries (distance=0, q=0).
|
|
529
|
+
# charged_mask zeros the result for neutral pairs anyway.
|
|
530
|
+
distances_d = distances.to(self.dtype).clamp(min=1e-6)
|
|
531
|
+
|
|
532
|
+
V = k * q_i.to(self.dtype) * q_j.to(self.dtype) / distances_d * \
|
|
533
|
+
torch.exp(-self.coefficients.k_screening * distances_d / self.coefficients.screening_length)
|
|
534
|
+
|
|
535
|
+
return V * charged_mask.to(self.dtype)
|
|
536
|
+
|
|
537
|
+
def get_k_ij(self, q_i, q_j):
|
|
538
|
+
if (self.coefficients.k_PlusMinus == self.coefficients.k_PlusPlus
|
|
539
|
+
== self.coefficients.k_MinusMinus):
|
|
540
|
+
return self.coefficients.k_PlusMinus
|
|
541
|
+
|
|
542
|
+
k = torch.full_like(q_i, self.coefficients.k_PlusMinus) # default opposite sign
|
|
543
|
+
k = torch.where((q_i > 0) & (q_j > 0),
|
|
544
|
+
torch.full_like(k, self.coefficients.k_PlusPlus), k)
|
|
545
|
+
k = torch.where((q_i < 0) & (q_j < 0),
|
|
546
|
+
torch.full_like(k, self.coefficients.k_MinusMinus), k)
|
|
547
|
+
return k
|
|
548
|
+
|
|
549
|
+
def get_burial_gamma(self, batch, res_type):
|
|
550
|
+
if not self.phospho_flag or "phospho_map" not in batch:
|
|
551
|
+
return self.burial_gamma[res_type.long()]
|
|
552
|
+
else:
|
|
553
|
+
res_type_ = res_type.clone()
|
|
554
|
+
res_type_[batch["phopho_map"]] = 6 # use burial gamma for glutamine instead of serine
|
|
555
|
+
return self.burial_gamma[res_type_]
|
|
556
|
+
|
|
557
|
+
def get_water_gamma(self, batch):
|
|
558
|
+
if not self.phospho_flag or "phospho_map" not in batch:
|
|
559
|
+
return self.water_gamma
|
|
560
|
+
else:
|
|
561
|
+
water_gamma = self.water_gamma.clone()
|
|
562
|
+
water_gamma[batch["phopho_map"]] = self.coefficients.phosph_water_gamma[batch["phopho_map"]]
|
|
563
|
+
return water_gamma
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _register_mode_mixins():
|
|
567
|
+
from .singleresidue import SingleResidueMixin
|
|
568
|
+
from .configurational import ConfigurationalMixin
|
|
569
|
+
from .mutational import MutationalMixin
|
|
570
|
+
import torch.nn as nn
|
|
571
|
+
AWSEM.__bases__ = (SingleResidueMixin, ConfigurationalMixin, MutationalMixin, nn.Module)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
_register_mode_mixins()
|
|
575
|
+
|