CoolFace
Modelpublic

Synthyra/ESMFold2-Experimental-Cutoff2025

sourceHugging Facemitupdated 3d agoView on Hugging Face
0likes101downloads
esmfold2_processor.py356 linesDownload Raw Back to root
1import random
2from contextlib import contextmanager, nullcontext
3from pathlib import Path
4from typing import Any
5
6import numpy as np
7import torch
8
9from .esmfold2_conformers import load_ccd
10from .esmfold2_output import build_molecular_complex_from_features
11from .esmfold2_prepare_input import ChainInfo, prepare_esmfold2_input
12from .esmfold2_types import (
13    MSA,
14    Modification,
15    ProteinInput,
16    StructurePredictionInput,
17)
18from .esmfold2_molecular_complex import MolecularComplexResult
19
20
21@contextmanager
22def _seed_context(seed: int | None):
23    if seed is None:
24        yield
25        return
26    py_state = random.getstate()
27    np_state = np.random.get_state()
28    torch_state = torch.random.get_rng_state()
29    cuda_state = torch.cuda.get_rng_state_all() if torch.cuda.is_available() else None
30    random.seed(seed)
31    np.random.seed(seed)
32    torch.manual_seed(seed)
33    if torch.cuda.is_available():
34        torch.cuda.manual_seed_all(seed)
35    try:
36        yield
37    finally:
38        random.setstate(py_state)
39        np.random.set_state(np_state)
40        torch.random.set_rng_state(torch_state)
41        if cuda_state is not None:
42            torch.cuda.set_rng_state_all(cuda_state)
43
44
45def clean_esmfold2_input(input: StructurePredictionInput) -> StructurePredictionInput:
46    """Group identical protein sequences into the same ProteinInput with multiple ids.
47
48    Example: Passing a tetramer like [ProteinInput(id=["0"], seq="AAA|AAA|BBB|BBB")]
49    gets converted into [ProteinInput(id=["0_0", "0_1"], seq="AAA"),
50                         ProteinInput(id=["0_2", "0_3"], seq="BBB")]
51
52    Preserves the original order of unique sequences. Also converts "|" chainbreak
53    tokens to ":" in the sequence.
54    """
55    cleaned_sequences: list = []
56    chain_to_ids: dict[str, list[str]] = {}
57    chain_to_modifications: dict[str, list] = {}
58    chain_to_msa: dict[str, MSA | None] = {}
59
60    for item in input.sequences:
61        if isinstance(item, ProteinInput):
62            sequence = ":".join(item.sequence.split("|"))
63            if ":" not in sequence:
64                cleaned_sequences.append(item)
65                continue
66
67            if ":" in sequence and input.covalent_bonds is not None:
68                raise ValueError(
69                    "Covalent bonds are not supported when using chainbreaks. "
70                    "Chains must be separated into multiple ProteinInput objects."
71                )
72
73            base_id = item.id[0] if isinstance(item.id, list) else item.id
74            chain_to_ids = {}
75            chain_to_modifications = {}
76            chain_to_msa = {}
77            chains = sequence.split(":")
78
79            chain_start_positions = []
80            pos = 0
81            for chain in chains:
82                chain_start_positions.append(pos)
83                pos += len(chain) + 1
84
85            if item.modifications is not None:
86                for chain_idx, chain in enumerate(chains):
87                    chain_start = chain_start_positions[chain_idx]
88                    chain_end = chain_start + len(chain)
89                    chain_modifications = []
90                    for mod in item.modifications:
91                        if chain_start <= mod.position < chain_end:
92                            adjusted_mod = Modification(
93                                position=mod.position - chain_start, ccd=mod.ccd
94                            )
95                            chain_modifications.append(adjusted_mod)
96                    if chain not in chain_to_modifications:
97                        chain_to_modifications[chain] = chain_modifications
98                    else:
99                        chain_to_modifications[chain].extend(chain_modifications)
100
101            if item.msa is not None:
102                for chain_idx, chain in enumerate(chains):
103                    if chain not in chain_to_msa:
104                        chain_start = chain_start_positions[chain_idx]
105                        chain_end = chain_start + len(chain)
106                        chain_msa = item.msa.select_positions(  # type: ignore
107                            np.arange(chain_start, chain_end)
108                        )
109                        chain_to_msa[chain] = chain_msa
110
111            for i, chain in enumerate(chains):
112                chain_id = base_id + "_" + str(i)
113                if chain in chain_to_ids:
114                    chain_to_ids[chain].append(chain_id)
115                else:
116                    chain_to_ids[chain] = [chain_id]
117                    cleaned_sequences.append((item, chain))
118        else:
119            cleaned_sequences.append(item)
120
121    for i in range(len(cleaned_sequences)):
122        if isinstance(cleaned_sequences[i], tuple):
123            item, chain = cleaned_sequences[i]
124            chain_ids = chain_to_ids[chain]
125            chain_modifications = (
126                chain_to_modifications.get(chain) if item.modifications else None
127            )
128            chain_msa = chain_to_msa.get(chain) if item.msa else None
129            cleaned_sequences[i] = ProteinInput(
130                id=chain_ids,
131                sequence=chain,
132                msa=chain_msa,
133                modifications=chain_modifications,
134            )
135
136    return StructurePredictionInput(
137        sequences=cleaned_sequences,
138        distogram_conditioning=input.distogram_conditioning,
139        covalent_bonds=input.covalent_bonds,
140    )
141
142
143class ESMFold2InputBuilder:
144    def __init__(self, ccd_cache: Path | None = None):
145        load_ccd(ccd_cache)
146
147    def prepare_input(
148        self,
149        input: StructurePredictionInput,
150        seed: int | None = None,
151        device: torch.device | str | None = None,
152    ) -> tuple[dict, list[ChainInfo]]:
153        """Prepare raw input for the folding model.
154
155        Converts user-provided StructurePredictionInput into batched tensors
156        ready for model inference.
157
158        Parameters
159        ----------
160        input : StructurePredictionInput
161            Input specification (sequences, structures, constraints, etc.).
162        seed : int, optional
163            Random seed for reproducibility.
164        device : torch.device or str, optional
165            Target device for the returned tensors. Defaults to CPU; pass
166            ``model.device`` to skip a separate ``.to(...)`` step. ``fold()``
167            forwards ``model.device`` automatically.
168
169        Returns
170        -------
171        tuple[dict, list[ChainInfo]]
172            Batched input tensors and chain metadata for output processing.
173        """
174        structure_prediction_input = clean_esmfold2_input(input)
175        with _seed_context(seed) if seed is not None else nullcontext():
176            features, chain_infos = prepare_esmfold2_input(
177                structure_prediction_input, seed=seed
178            )
179            features = {
180                k: (v[None].to(device) if device is not None else v[None])
181                if isinstance(v, torch.Tensor)
182                else v
183                for k, v in features.items()
184            }
185
186        return features, chain_infos
187
188    def __call__(
189        self,
190        input: StructurePredictionInput,
191        seed: int | None = None,
192        device: torch.device | str | None = None,
193    ) -> tuple[dict, list[ChainInfo]]:
194        return self.prepare_input(input, seed=seed, device=device)
195
196    def decode(
197        self,
198        output: dict[str, torch.Tensor],
199        features: dict[str, torch.Tensor],
200        chain_infos: list[ChainInfo],
201        *,
202        num_diffusion_samples: int = 1,
203        complex_id: str = "pred",
204    ) -> MolecularComplexResult | list[MolecularComplexResult]:
205        """Convert raw model outputs into one MolecularComplexResult per sample.
206
207        Parameters
208        ----------
209        output : dict[str, Tensor]
210            Output dict returned by ESMFold2Model.forward.
211        features : dict[str, Tensor]
212            Feature dict from :meth:`prepare_input` (batched, on the model device).
213        chain_infos : list[ChainInfo]
214            Chain metadata returned alongside `features`.
215        num_diffusion_samples : int
216            Number of diffusion samples present in the output (Bm = B * num_diffusion_samples).
217        complex_id : str
218            Identifier assigned to each MolecularComplex.
219
220        Returns
221        -------
222        MolecularComplexResult or list[MolecularComplexResult]
223            A single result when num_diffusion_samples == 1, otherwise a list of length Bm.
224        """
225        atom_mask = features["atom_attention_mask"][0]
226        ref_element = features["ref_element"][0]
227        ref_atom_name_chars = features["ref_atom_name_chars"][0]
228
229        sample_coords = output["sample_atom_coords"]
230        plddts = output["plddt"]
231        Bm = sample_coords.shape[0]
232
233        ptm_t = output.get("ptm")
234        iptm_t = output.get("iptm")
235        pae_t = output.get("pae")
236        distogram_t = output.get("distogram_logits")
237        pair_chains_t = output.get("pair_chains_iptm")
238        residue_index_t = output.get("residue_index")
239        entity_id_t = output.get("entity_id")
240
241        results: list[MolecularComplexResult] = []
242        for i in range(Bm):
243            mc = build_molecular_complex_from_features(
244                coords=sample_coords[i],
245                plddt=plddts[i],
246                atom_mask=atom_mask,
247                ref_element=ref_element,
248                ref_atom_name_chars=ref_atom_name_chars,
249                chain_infos=chain_infos,
250                complex_id=complex_id,
251            )
252            results.append(
253                MolecularComplexResult(
254                    complex=mc,
255                    plddt=plddts[i].detach().cpu(),
256                    ptm=float(ptm_t[i].item()) if ptm_t is not None else None,
257                    iptm=float(iptm_t[i].item()) if iptm_t is not None else None,
258                    pae=pae_t[i].detach().cpu() if pae_t is not None else None,
259                    distogram=(
260                        distogram_t[0].detach().cpu()
261                        if distogram_t is not None
262                        else None
263                    ),
264                    pair_chains_iptm=(
265                        pair_chains_t[i].detach().cpu()
266                        if pair_chains_t is not None
267                        else None
268                    ),
269                    residue_index=(
270                        residue_index_t[0].detach().cpu()
271                        if residue_index_t is not None
272                        else None
273                    ),
274                    entity_id=(
275                        entity_id_t[0].detach().cpu()
276                        if entity_id_t is not None
277                        else None
278                    ),
279                )
280            )
281
282        if num_diffusion_samples == 1 and len(results) == 1:
283            return results[0]
284        return results
285
286    def fold(
287        self,
288        model: Any,
289        input: StructurePredictionInput,
290        *,
291        num_loops: int = 3,
292        num_sampling_steps: int = 200,
293        num_diffusion_samples: int = 1,
294        seed: int | None = None,
295        noise_scale: float | None = None,
296        step_scale: float | None = None,
297        max_inference_sigma: int | None = None,
298        early_exit: bool = False,
299        complex_id: str = "pred",
300    ) -> MolecularComplexResult | list[MolecularComplexResult]:
301        """Fold a structure end-to-end: encode → model → decode.
302
303        Parameters
304        ----------
305        model : ESMFold2Model
306            The folding model. Must already be on the target device and in eval mode.
307        input : StructurePredictionInput
308            User-facing input specification.
309        num_loops, num_sampling_steps, num_diffusion_samples : int
310            Inference knobs forwarded to the model.
311        seed : int, optional
312            Seeds both input prep (SMILES conformer generation) and diffusion sampling.
313        noise_scale, step_scale, max_inference_sigma, early_exit
314            Optional sampler overrides forwarded to the model when not None.
315        complex_id : str
316            Identifier assigned to the predicted MolecularComplex(es).
317
318        Returns
319        -------
320        MolecularComplexResult or list[MolecularComplexResult]
321            A single result when num_diffusion_samples == 1, otherwise a list.
322        """
323        features, chain_infos = self.prepare_input(
324            input, seed=seed, device=model.device
325        )
326
327        sampler_kwargs: dict[str, Any] = {}
328        if noise_scale is not None:
329            sampler_kwargs["noise_scale"] = noise_scale
330        if step_scale is not None:
331            sampler_kwargs["step_scale"] = step_scale
332        if max_inference_sigma is not None:
333            sampler_kwargs["max_inference_sigma"] = max_inference_sigma
334
335        with torch.no_grad():
336            with _seed_context(seed) if seed is not None else nullcontext():
337                output = model(
338                    **features,
339                    num_loops=num_loops,
340                    num_sampling_steps=num_sampling_steps,
341                    num_diffusion_samples=num_diffusion_samples,
342                    early_exit=early_exit,
343                    **sampler_kwargs,
344                )
345
346        return self.decode(
347            output,
348            features,
349            chain_infos,
350            num_diffusion_samples=num_diffusion_samples,
351            complex_id=complex_id,
352        )
353
354
355__all__ = ["ESMFold2InputBuilder", "clean_esmfold2_input"]
356