Skip to main content

ferritin_plms/esmfold2/layers/
diffusion.rs

1//! AF3-style EDM diffusion module for all-atom coordinate generation.
2//!
3//! Architecture:
4//! 1. Single repr projected to token dim: `[B, N, d_single] → [B, N, c_token=768]`
5//! 2. Fourier noise embedding encodes σ_t → `[fourier_dim=256]`
6//! 3. 12 token-level transformer blocks (c_token=768, 16 heads, with pair bias)
7//! 4. 3 atom-level transformer blocks (c_atom=128, 4 heads) — TODO
8//! 5. Output projection: `[B, N, c_token] → [B, N*n_atoms_per_token, 3]`
9//!
10//! EDM noise schedule (inference):
11//! ```text
12//! sigma_t = s_max * (s_min / s_max)^((t / T)^p)
13//!   s_max = 160.0,  s_min = 0.0004,  T = num_steps,  p = 7.0
14//!
15//! Stochastic correction: gamma_t = min(gamma_0, sqrt(sigma_next/sigma_t) - 1)
16//! sigma_hat = sigma_t * (1 + gamma_t)
17//! x_hat = x + sqrt(sigma_hat^2 - sigma_t^2) * noise * noise_scale
18//! D = score_network(x_hat, sigma_hat)
19//! x = x_hat + step_scale * (D - x_hat) * (sigma_next / sigma_hat - 1)
20//! ```
21//!
22//! Weight layout (rooted at `structure_head`):
23//! ```text
24//! token_proj.*                            — d_single → c_token (no bias)
25//! noise_embedding.*                       — Fourier frequencies (learnable)
26//! noise_proj.*                            — fourier_dim → c_token (no bias)
27//! token_transformer.blocks.{0..11}.*     — 12 token transformer blocks
28//! token_transformer.blocks.{i}.norm.*
29//! token_transformer.blocks.{i}.attn.*    — MHA with pair bias
30//! token_transformer.blocks.{i}.ffn.*     — SwiGLU FFN
31//! out_proj.*                             — c_token → 3 (Cα coords, no bias)
32//! ```
33
34use candle_core::{D, DType, Result, Tensor};
35use candle_nn::{self as nn, LayerNorm, LayerNormConfig, Module, VarBuilder};
36
37// ── Fourier Noise Embedding ───────────────────────────────────────────────────
38
39/// Learnable Fourier embedding for the noise level σ.
40///
41/// Maps a scalar σ to `[fourier_dim]` features:
42/// `[sin(2π σ w_0), cos(2π σ w_0), ..., sin(2π σ w_{d/2-1}), cos(2π σ w_{d/2-1})]`
43/// where `w_i` are learnable scalar weights.
44struct FourierEmbedding {
45    weights: Tensor, // [fourier_dim / 2]
46    dim: usize,
47}
48
49impl FourierEmbedding {
50    fn load(vb: VarBuilder, fourier_dim: usize) -> Result<Self> {
51        let weights = vb.get((fourier_dim / 2,), "weights")?;
52        Ok(Self {
53            weights,
54            dim: fourier_dim,
55        })
56    }
57
58    /// `sigma` — scalar (f32); returns `[fourier_dim]`
59    fn embed(&self, sigma: f32) -> Result<Tensor> {
60        let device = self.weights.device();
61        let sigma_t = Tensor::from_vec(vec![sigma], 1, device)?.to_dtype(DType::F32)?;
62        // [1] * [d/2] → [d/2]  (broadcast multiply)
63        let x = sigma_t.broadcast_mul(&self.weights)?;
64        let x = (x * (2.0 * std::f64::consts::PI))?;
65        let sin_x = x.sin()?; // [d/2]
66        let cos_x = x.cos()?; // [d/2]
67        Tensor::cat(&[&sin_x, &cos_x], 0) // [fourier_dim]
68    }
69}
70
71// ── Token Transformer Block ───────────────────────────────────────────────────
72
73/// Pre-norm MHA block at token level, conditioned by the pair representation.
74///
75/// Follows the same pattern as LMEncoder blocks but with pair bias conditioning.
76struct TokenAttn {
77    norm: LayerNorm,
78    q_proj: nn::Linear,
79    k_proj: nn::Linear,
80    v_proj: nn::Linear,
81    pair_bias: nn::Linear, // d_pair → n_heads
82    gate: nn::Linear,
83    out_proj: nn::Linear,
84    n_heads: usize,
85    d_head: usize,
86}
87
88impl TokenAttn {
89    fn load(vb: VarBuilder, c_token: usize, n_heads: usize, d_pair: usize) -> Result<Self> {
90        let d_head = c_token / n_heads;
91        Ok(Self {
92            norm: nn::layer_norm(c_token, LayerNormConfig::from(1e-5), vb.pp("norm"))?,
93            q_proj: nn::linear_no_bias(c_token, n_heads * d_head, vb.pp("q_proj"))?,
94            k_proj: nn::linear_no_bias(c_token, n_heads * d_head, vb.pp("k_proj"))?,
95            v_proj: nn::linear_no_bias(c_token, n_heads * d_head, vb.pp("v_proj"))?,
96            pair_bias: nn::linear_no_bias(d_pair, n_heads, vb.pp("pair_bias"))?,
97            gate: nn::linear_no_bias(c_token, n_heads * d_head, vb.pp("gate"))?,
98            out_proj: nn::linear_no_bias(n_heads * d_head, c_token, vb.pp("out_proj"))?,
99            n_heads,
100            d_head,
101        })
102    }
103
104    /// `x`: `[B, N, c_token]`, `pair`: `[B, N, N, d_pair]`
105    fn forward(&self, x: &Tensor, pair: &Tensor) -> Result<Tensor> {
106        let (b, n, _) = x.dims3()?;
107        let (h, dh) = (self.n_heads, self.d_head);
108
109        let x_n = self.norm.forward(x)?;
110
111        let q = self.q_proj.forward(&x_n)?; // [B, N, H*dh]
112        let k = self.k_proj.forward(&x_n)?;
113        let v = self.v_proj.forward(&x_n)?;
114        let gate = nn::ops::sigmoid(&self.gate.forward(&x_n)?)?; // [B, N, H*dh]
115
116        // pair bias: [B, N, N, d_pair] → [B, N, N, H] → [B, H, N, N]
117        let pair_b = self
118            .pair_bias
119            .forward(pair)? // [B, N, N, H]
120            .permute((0, 3, 1, 2))?
121            .contiguous()?; // [B, H, N, N]
122
123        // → [B, H, N, dh]
124        let to_heads = |t: Tensor| -> Result<Tensor> {
125            t.reshape((b, n, h, dh))?
126                .permute((0, 2, 1, 3))?
127                .contiguous()
128        };
129        let q = to_heads(q)?;
130        let k = to_heads(k)?;
131        let v = to_heads(v)?;
132
133        let scale = (dh as f64).sqrt();
134        let scores = (q.matmul(&k.transpose(D::Minus2, D::Minus1)?.contiguous()?)? / scale)?;
135        // scores: [B, H, N, N]; pair_b: [B, H, N, N]
136        let scores = (scores + pair_b)?;
137        let attn = nn::ops::softmax(&scores, D::Minus1)?;
138        let out = attn.matmul(&v)?; // [B, H, N, dh]
139
140        // [B, H, N, dh] → [B, N, H*dh]
141        let out = out
142            .permute((0, 2, 1, 3))?
143            .contiguous()? // [B, N, H, dh]
144            .reshape((b, n, h * dh))?;
145
146        let out = (gate * out)?;
147        let out = self.out_proj.forward(&out)?;
148        x + &out
149    }
150}
151
152struct TokenFfn {
153    norm: LayerNorm,
154    gate_up: nn::Linear, // c_token → 2 * hidden
155    down: nn::Linear,    // hidden → c_token
156}
157
158impl TokenFfn {
159    fn load(vb: VarBuilder, c_token: usize) -> Result<Self> {
160        // SwiGLU hidden: nearest-256 multiple of (c_token * 8/3)
161        let hidden = ((8.0 / 3.0 * c_token as f64 + 255.0) / 256.0).floor() as usize * 256;
162        Ok(Self {
163            norm: nn::layer_norm(c_token, LayerNormConfig::from(1e-5), vb.pp("norm"))?,
164            gate_up: nn::linear_no_bias(c_token, hidden * 2, vb.pp("gate_up"))?,
165            down: nn::linear_no_bias(hidden, c_token, vb.pp("down"))?,
166        })
167    }
168
169    fn forward(&self, x: &Tensor) -> Result<Tensor> {
170        let h = self.gate_up.forward(&self.norm.forward(x)?)?;
171        let chunks = h.chunk(2, D::Minus1)?;
172        let act = (chunks[0].silu()? * &chunks[1])?;
173        let out = self.down.forward(&act)?;
174        x + &out
175    }
176}
177
178struct TokenBlock {
179    attn: TokenAttn,
180    ffn: TokenFfn,
181}
182
183impl TokenBlock {
184    fn load(vb: VarBuilder, c_token: usize, n_heads: usize, d_pair: usize) -> Result<Self> {
185        Ok(Self {
186            attn: TokenAttn::load(vb.pp("attn"), c_token, n_heads, d_pair)?,
187            ffn: TokenFfn::load(vb.pp("ffn"), c_token)?,
188        })
189    }
190
191    fn forward(&self, x: &Tensor, pair: &Tensor) -> Result<Tensor> {
192        let x = self.attn.forward(x, pair)?;
193        self.ffn.forward(&x)
194    }
195}
196
197// ── DiffusionModule ───────────────────────────────────────────────────────────
198
199/// AF3-style EDM diffusion module.
200///
201/// Denoises atom coordinates conditioned on single and pair representations
202/// produced by the folding trunk. Uses 12 token-level transformer blocks with
203/// pair bias conditioning, followed by coordinate output projection.
204pub struct DiffusionModule {
205    token_proj: nn::Linear, // d_single → c_token
206    noise_emb: FourierEmbedding,
207    noise_proj: nn::Linear,  // fourier_dim → c_token
208    blocks: Vec<TokenBlock>, // 12 blocks
209    out_proj: nn::Linear,    // c_token → 3 (Cα or token centroid)
210    // TODO: atom_transformer (3 blocks, c_atom=128) for all-atom output
211    c_token: usize,
212    d_single: usize,
213    // Noise schedule parameters
214    s_max: f64,
215    s_min: f64,
216    p: f64,
217    gamma_0: f64,
218    noise_scale: f64,
219    step_scale: f64,
220    device: candle_core::Device,
221}
222
223impl DiffusionModule {
224    /// Load the diffusion module from a `VarBuilder` rooted at `structure_head.*`.
225    ///
226    /// # Arguments
227    /// * `vb`       — builder rooted at `structure_head`
228    /// * `c_token`  — token channel dimension (768)
229    /// * `c_atom`   — atom channel dimension (128, reserved for atom transformer TODO)
230    /// * `d_single` — single repr dimension from folding trunk (384)
231    /// * `d_pair`   — pair repr dimension from folding trunk (256)
232    /// * `n_token_blocks` — number of token transformer blocks (12)
233    /// * `n_token_heads`  — number of heads per token block (16)
234    /// * `fourier_dim`    — Fourier noise embedding dimension (256)
235    pub fn load(
236        vb: VarBuilder,
237        c_token: usize,
238        c_atom: usize,
239        d_single: usize,
240        d_pair: usize,
241        n_token_blocks: usize,
242        n_token_heads: usize,
243        fourier_dim: usize,
244    ) -> Result<Self> {
245        let _ = c_atom; // reserved for atom transformer
246
247        let token_proj = nn::linear_no_bias(d_single, c_token, vb.pp("token_proj"))?;
248        let noise_emb = FourierEmbedding::load(vb.pp("noise_embedding"), fourier_dim)?;
249        let noise_proj = nn::linear_no_bias(fourier_dim, c_token, vb.pp("noise_proj"))?;
250
251        let blocks = (0..n_token_blocks)
252            .map(|i| {
253                TokenBlock::load(
254                    vb.pp(format!("token_transformer.blocks.{i}")),
255                    c_token,
256                    n_token_heads,
257                    d_pair,
258                )
259            })
260            .collect::<Result<Vec<_>>>()?;
261
262        let out_proj = nn::linear_no_bias(c_token, 3, vb.pp("out_proj"))?;
263        let device = vb.device().clone();
264
265        Ok(Self {
266            token_proj,
267            noise_emb,
268            noise_proj,
269            blocks,
270            out_proj,
271            c_token,
272            d_single,
273            s_max: 160.0,
274            s_min: 0.0004,
275            p: 7.0,
276            gamma_0: 0.8,
277            noise_scale: 1.003,
278            step_scale: 1.5,
279            device,
280        })
281    }
282
283    /// Compute sigma schedule: `s_max * (s_min/s_max)^((t/T)^p)` for t in 0..T.
284    fn sigma_schedule(&self, num_steps: usize) -> Vec<f32> {
285        (0..=num_steps)
286            .map(|t| {
287                let frac = t as f64 / num_steps as f64;
288                (self.s_max * (self.s_min / self.s_max).powf(frac.powf(self.p))) as f32
289            })
290            .collect()
291    }
292
293    /// Run the score network: projects single, embeds σ, runs transformer blocks.
294    ///
295    /// Input:  `noisy_coords [B, N, 3]`,  `single [B, N, d_single]`,  `pair [B, N, N, d_pair]`
296    /// Output: `denoised_coords [B, N, 3]`
297    fn score_network(
298        &self,
299        noisy: &Tensor,
300        sigma: f32,
301        single: &Tensor,
302        pair: &Tensor,
303    ) -> Result<Tensor> {
304        let (b, n, _) = single.dims3()?;
305
306        // Project single to token dim and inject noise level
307        let mut tok = self.token_proj.forward(single)?; // [B, N, c_token]
308        let noise_feat = self
309            .noise_proj
310            .forward(&self.noise_emb.embed(sigma)?.unsqueeze(0)?.unsqueeze(0)?)?; // [1, 1, c_token]
311        tok = tok.broadcast_add(&noise_feat)?;
312
313        // Run 12 transformer blocks
314        for block in &self.blocks {
315            tok = block.forward(&tok, pair)?;
316        }
317
318        // Project to 3D coordinates
319        let coords = self.out_proj.forward(&tok)?; // [B, N, 3]
320
321        // Rescale output: coords are expressed in the frame where sigma=1;
322        // multiply by sigma for correct scale  (simplified AF3 preconditioner)
323        let scale = Tensor::from_vec(vec![sigma], 1, &self.device)?.to_dtype(DType::F32)?;
324        let _ = (b, n, noisy); // noisy coords available if needed for skip connection
325        coords.broadcast_mul(&scale.reshape((1, 1, 1))?.broadcast_as(coords.shape())?)
326    }
327
328    /// Run the AF3-style EDM denoising loop.
329    ///
330    /// # Arguments
331    /// * `single`    — single representation `[B, N_tok, d_single]`
332    /// * `pair`      — pair representation `[B, N_tok, N_tok, d_pair]`
333    /// * `n_atoms`   — number of atoms (= N_tok for Cα-only, otherwise N_tok * atoms_per_tok)
334    /// * `num_steps` — number of denoising steps (14 fast / 50 quality)
335    ///
336    /// # Returns
337    /// Token-level coordinates `[B, N_tok, 3]` in Ångströms (Cα output).
338    pub fn forward(
339        &self,
340        single: &Tensor,
341        pair: &Tensor,
342        n_atoms: usize,
343        num_steps: usize,
344    ) -> Result<Tensor> {
345        let (b, _n_tok, _) = single.dims3()?;
346        let sigmas = self.sigma_schedule(num_steps);
347
348        // Initialise x ~ N(0, sigma_0^2 * I)
349        let sigma_0 = sigmas[0];
350        let mut x = Tensor::randn(0f32, sigma_0, (b, n_atoms, 3_usize), &self.device)?;
351
352        for step in 0..num_steps {
353            let sigma_t = sigmas[step];
354            let sigma_next = sigmas[step + 1];
355
356            // Stochastic correction factor (gamma from DDPM-flavored EDM)
357            let gamma = (self.gamma_0 as f32)
358                .min((sigma_next / sigma_t).sqrt() - 1.0)
359                .max(0.0);
360            let sigma_hat = sigma_t * (1.0 + gamma);
361
362            // Add stochastic noise when gamma > 0
363            let x_hat = if gamma > 0.0 {
364                let extra_noise_std =
365                    (sigma_hat * sigma_hat - sigma_t * sigma_t).sqrt() * self.noise_scale as f32;
366                let noise = Tensor::randn(0f32, extra_noise_std, x.shape(), &self.device)?;
367                (&x + &noise)?
368            } else {
369                x.clone()
370            };
371
372            // Score network forward
373            let d = self.score_network(&x_hat, sigma_hat, single, pair)?;
374
375            // DDPM update step
376            let ratio = sigma_next / sigma_hat;
377            let step_factor = self.step_scale * (ratio as f64 - 1.0); // f64 for Tensor::Mul<f64>
378            // x = x_hat + step_factor * (d - x_hat)
379            let diff = (d - &x_hat)?;
380            x = (x_hat + (diff * step_factor)?)?;
381        }
382
383        Ok(x)
384    }
385}
386
387// ── Tests ─────────────────────────────────────────────────────────────────────
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392    use candle_core::{Device, Tensor};
393
394    const B: usize = 1;
395    const N: usize = 6;
396    const D_SINGLE: usize = 64; // small for fast tests
397    const D_PAIR: usize = 32;
398    const C_TOKEN: usize = 64;
399    const C_ATOM: usize = 16;
400    const N_HEADS: usize = 4;
401    const FOURIER_DIM: usize = 16;
402    const N_BLOCKS: usize = 2; // use 2 blocks in tests (not 12)
403
404    fn make_module(device: &Device) -> DiffusionModule {
405        let vb = VarBuilder::zeros(DType::F32, device);
406        DiffusionModule::load(
407            vb,
408            C_TOKEN,
409            C_ATOM,
410            D_SINGLE,
411            D_PAIR,
412            N_BLOCKS,
413            N_HEADS,
414            FOURIER_DIM,
415        )
416        .unwrap()
417    }
418
419    #[test]
420    fn test_fourier_embedding_shape() {
421        let device = Device::Cpu;
422        let vb = VarBuilder::zeros(DType::F32, &device);
423        let emb = FourierEmbedding::load(vb, FOURIER_DIM).unwrap();
424        let out = emb.embed(1.0).unwrap();
425        assert_eq!(out.dims(), &[FOURIER_DIM]);
426    }
427
428    #[test]
429    fn test_sigma_schedule_length() {
430        let device = Device::Cpu;
431        let m = make_module(&device);
432        let sigmas = m.sigma_schedule(14);
433        assert_eq!(sigmas.len(), 15); // num_steps + 1 (includes sigma_T = s_min)
434        assert!(sigmas[0] > sigmas[14], "sigma should decrease");
435    }
436
437    #[test]
438    fn test_sigma_schedule_bounds() {
439        let device = Device::Cpu;
440        let m = make_module(&device);
441        let sigmas = m.sigma_schedule(14);
442        // sigma_0 ≈ s_max, sigma_T ≈ s_min
443        assert!(sigmas[0] > 100.0, "first sigma near s_max=160");
444        assert!(sigmas[14] < 1.0, "last sigma near s_min=0.0004");
445    }
446
447    #[test]
448    fn test_token_block_shape() {
449        let device = Device::Cpu;
450        let vb = VarBuilder::zeros(DType::F32, &device);
451        let block = TokenBlock::load(vb, C_TOKEN, N_HEADS, D_PAIR).unwrap();
452        let x = Tensor::zeros(&[B, N, C_TOKEN], DType::F32, &device).unwrap();
453        let pair = Tensor::zeros(&[B, N, N, D_PAIR], DType::F32, &device).unwrap();
454        let out = block.forward(&x, &pair).unwrap();
455        assert_eq!(out.dims(), &[B, N, C_TOKEN]);
456    }
457
458    #[test]
459    fn test_diffusion_forward_shape() {
460        let device = Device::Cpu;
461        let m = make_module(&device);
462        let single = Tensor::zeros(&[B, N, D_SINGLE], DType::F32, &device).unwrap();
463        let pair = Tensor::zeros(&[B, N, N, D_PAIR], DType::F32, &device).unwrap();
464        let out = m.forward(&single, &pair, N, 2).unwrap();
465        assert_eq!(out.dims(), &[B, N, 3]);
466    }
467
468    #[test]
469    fn test_diffusion_batch_shape() {
470        let device = Device::Cpu;
471        let m = make_module(&device);
472        let single = Tensor::zeros(&[2, N, D_SINGLE], DType::F32, &device).unwrap();
473        let pair = Tensor::zeros(&[2, N, N, D_PAIR], DType::F32, &device).unwrap();
474        let out = m.forward(&single, &pair, N, 2).unwrap();
475        assert_eq!(out.dims(), &[2, N, 3]);
476    }
477}