Skip to main content

ferritin_plms/esmfold2/
model.rs

1//! ESMFold2Model — wires ESMC-6B backbone + structure-head layers.
2
3use super::config::ESMFold2Config;
4#[allow(unused_imports)]
5use super::layers::{
6    atom_encoder::AtomEncoder, // TODO: add atom_encoder field when AtomEncoder is complete
7    confidence_head::{ConfidenceHead, bins_to_scalar},
8    diffusion::DiffusionModule,
9    folding_trunk::FoldingTrunk,
10    lm_encoder::LMEncoder,
11};
12use super::output::ESMFold2Output;
13use candle_core::{DType, Device, Result, Tensor};
14use candle_nn::VarBuilder;
15
16/// Full ESMFold2-Fast structure prediction model.
17///
18/// The ESMC-6B backbone (frozen) is loaded separately; this struct holds only
19/// the structure-head components that are fine-tuned for structure prediction.
20pub struct ESMFold2Model {
21    config: ESMFold2Config,
22    lm_encoder: LMEncoder,
23    folding_trunk: FoldingTrunk,
24    diffusion: DiffusionModule,
25    confidence_head: ConfidenceHead,
26    device: Device,
27}
28
29impl ESMFold2Model {
30    /// Load the ESMFold2-Fast structure head from a `VarBuilder`.
31    ///
32    /// The `VarBuilder` should be rooted at the structure-head safetensors
33    /// (`biohub/ESMFold2-Fast model.safetensors`). The ESMC-6B backbone is
34    /// loaded separately.
35    pub fn load(vb: VarBuilder, config: ESMFold2Config) -> Result<Self> {
36        let device = vb.device().clone();
37
38        let lm_encoder = LMEncoder::load(
39            vb.pp("lm_encoder"),
40            config.lm_d_model,
41            config.d_single,
42            config.lm_encoder_n_layers,
43        )?;
44
45        let folding_trunk = FoldingTrunk::load(
46            vb.pp("folding_trunk"),
47            config.trunk_n_layers,
48            config.d_single,
49            config.d_pair,
50            config.trunk_n_heads,
51        )?;
52
53        let diffusion = DiffusionModule::load(
54            vb.pp("structure_head"),
55            config.c_token,
56            config.c_atom,
57            config.d_single,
58            config.d_pair,
59            config.token_num_blocks,
60            config.token_num_heads,
61            config.fourier_dim,
62        )?;
63
64        let confidence_head = ConfidenceHead::load(
65            vb.pp("confidence_head"),
66            config.d_single,
67            config.d_pair,
68            config.num_plddt_bins,
69            config.num_pae_bins,
70            config.num_pde_bins,
71            config.distogram_bins,
72        )?;
73
74        Ok(Self {
75            config,
76            lm_encoder,
77            folding_trunk,
78            diffusion,
79            confidence_head,
80            device,
81        })
82    }
83
84    /// Run the full ESMFold2 structure-head forward pass.
85    ///
86    /// # Arguments
87    /// * `hidden_states`    — ESMC-6B backbone output `[B, L, 2560]`
88    /// * `residue_indices`  — residue positions `[B, L]`, any numeric dtype
89    /// * `chain_ids`        — chain identifiers `[B, L]`, any numeric dtype
90    /// * `num_loops`        — number of recycling iterations (typically 3)
91    /// * `num_steps`        — diffusion denoising steps (14 fast / 50 quality)
92    ///
93    /// # Returns
94    /// [`ESMFold2Output`] with coordinates, pLDDT, and optional PAE/distogram.
95    pub fn forward(
96        &self,
97        hidden_states: &Tensor,
98        residue_indices: &Tensor,
99        chain_ids: &Tensor,
100        num_loops: usize,
101        num_steps: usize,
102    ) -> Result<ESMFold2Output> {
103        let (b, l, _) = hidden_states.dims3()?;
104
105        // LMEncoder: [B, L, 2560] → [B, L, d_single=384]
106        let mut single = self.lm_encoder.forward(hidden_states)?;
107
108        // Initialise pair from sequence metadata: [B, L, L, d_pair=256]
109        let mut pair = self
110            .folding_trunk
111            .init_pair(&single, residue_indices, chain_ids)?;
112
113        // Recycling loops through FoldingTrunk
114        for _ in 0..num_loops {
115            let (s, p) = self.folding_trunk.forward(&single, &pair)?;
116            single = s;
117            pair = p;
118        }
119
120        // Diffusion: [B, L, 3] token-level Cα coordinates
121        let coords = self.diffusion.forward(&single, &pair, l, num_steps)?;
122
123        // Confidence head: pLDDT, pAE, distogram
124        let conf = self.confidence_head.forward(&single, &pair)?;
125
126        // Convert pAE logits [B, N, N, 64] → scalar PAE [B, N, N] in Å (0..32)
127        let pae = conf
128            .pae_logits
129            .map(|t| bins_to_scalar(&t, 0.0, 32.0))
130            .transpose()?;
131
132        // Stub ptm/iptm as zeros [B] — requires frame-aligned point error calc
133        let ptm = Tensor::zeros(b, DType::F32, &self.device)?;
134        let iptm = Tensor::zeros(b, DType::F32, &self.device)?;
135
136        Ok(ESMFold2Output {
137            sample_atom_coords: coords,
138            plddt: conf.plddt,
139            ptm,
140            iptm,
141            pae,
142            distogram_logits: conf.distogram_logits,
143        })
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use candle_core::Device;
151
152    fn make_model(device: &Device) -> ESMFold2Model {
153        use super::super::config::ESMFold2Config;
154        let cfg = ESMFold2Config {
155            lm_d_model: 64,
156            lm_encoder_n_layers: 1,
157            d_single: 32,
158            d_pair: 16,
159            trunk_n_layers: 1,
160            trunk_n_heads: 2,
161            c_token: 32,
162            c_atom: 16,
163            token_num_blocks: 1,
164            token_num_heads: 2,
165            fourier_dim: 16,
166            num_plddt_bins: 50,
167            num_pae_bins: 64,
168            num_pde_bins: 64,
169            distogram_bins: 39,
170            ..ESMFold2Config::fast()
171        };
172        let vb = VarBuilder::zeros(DType::F32, device);
173        ESMFold2Model::load(vb, cfg).unwrap()
174    }
175
176    #[test]
177    fn test_forward_output_shapes() {
178        let device = Device::Cpu;
179        let model = make_model(&device);
180
181        let b = 1usize;
182        let l = 8usize;
183        let hidden = Tensor::zeros(&[b, l, 64], DType::F32, &device).unwrap();
184        let res_idx: Vec<f32> = (0..l).map(|i| i as f32).collect();
185        let residue_indices = Tensor::from_vec(res_idx, &[b, l], &device).unwrap();
186        let chain_ids = Tensor::zeros(&[b, l], DType::F32, &device).unwrap();
187
188        let out = model
189            .forward(&hidden, &residue_indices, &chain_ids, 1, 2)
190            .unwrap();
191
192        assert_eq!(out.sample_atom_coords.dims(), &[b, l, 3]);
193        assert_eq!(out.plddt.dims(), &[b, l]);
194        assert_eq!(out.ptm.dims(), &[b]);
195        assert_eq!(out.iptm.dims(), &[b]);
196    }
197}