Files
rustytorch/demos/rtx-alphafold-demo/src/structure_module.rs
T
osobhandClaude Opus 4.6 02d382d5f6 style: apply rustfmt across all crates and demos
Consistent formatting pass: line wrapping, import sorting, trailing
whitespace removal, let-chain indentation, merged derive attributes,
and unsafe block reformatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-12 07:01:58 -07:00

390 lines
12 KiB
Rust

//! Structure module for 3D coordinate prediction.
//!
//! Implements SE(3)-equivariant attention (Invariant Point Attention)
//! for predicting backbone atom coordinates.
use crate::encoder::SequenceEmbedding;
use alphafold_shared::{AtomCoord, AtomName};
/// Number of IPA iterations.
pub const NUM_IPA_ITERATIONS: usize = 8;
/// Backbone frame representation.
#[derive(Debug, Clone, Copy)]
pub struct BackboneFrame {
/// Rotation matrix (3x3)
pub rotation: [[f32; 3]; 3],
/// Translation vector
pub translation: [f32; 3],
}
impl Default for BackboneFrame {
fn default() -> Self {
Self {
rotation: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
translation: [0.0, 0.0, 0.0],
}
}
}
impl BackboneFrame {
/// Apply frame transformation to a point.
#[must_use]
pub fn apply(&self, point: [f32; 3]) -> [f32; 3] {
let rotated = [
self.rotation[0][0] * point[0]
+ self.rotation[0][1] * point[1]
+ self.rotation[0][2] * point[2],
self.rotation[1][0] * point[0]
+ self.rotation[1][1] * point[1]
+ self.rotation[1][2] * point[2],
self.rotation[2][0] * point[0]
+ self.rotation[2][1] * point[1]
+ self.rotation[2][2] * point[2],
];
[
rotated[0] + self.translation[0],
rotated[1] + self.translation[1],
rotated[2] + self.translation[2],
]
}
/// Compose two frames: self * other.
#[must_use]
pub fn compose(&self, other: &BackboneFrame) -> BackboneFrame {
// R_new = R_self * R_other
let mut rotation = [[0.0_f32; 3]; 3];
for i in 0..3 {
for j in 0..3 {
for k in 0..3 {
rotation[i][j] += self.rotation[i][k] * other.rotation[k][j];
}
}
}
// t_new = R_self * t_other + t_self
let translation = self.apply(other.translation);
BackboneFrame {
rotation,
translation,
}
}
/// Create frame from axis-angle rotation.
#[must_use]
pub fn from_axis_angle(axis: [f32; 3], angle: f32) -> Self {
let c = angle.cos();
let s = angle.sin();
let t = 1.0 - c;
let rotation = [
[
t * axis[0] * axis[0] + c,
t * axis[0] * axis[1] - s * axis[2],
t * axis[0] * axis[2] + s * axis[1],
],
[
t * axis[0] * axis[1] + s * axis[2],
t * axis[1] * axis[1] + c,
t * axis[1] * axis[2] - s * axis[0],
],
[
t * axis[0] * axis[2] - s * axis[1],
t * axis[1] * axis[2] + s * axis[0],
t * axis[2] * axis[2] + c,
],
];
BackboneFrame {
rotation,
translation: [0.0, 0.0, 0.0],
}
}
}
/// Invariant Point Attention module.
#[derive(Debug, Clone)]
pub struct InvariantPointAttention {
/// Number of query points
num_query_points: usize,
/// Number of attention heads
num_heads: usize,
/// Head dimension
head_dim: usize,
}
impl Default for InvariantPointAttention {
fn default() -> Self {
Self::new(4, 8, 16)
}
}
impl InvariantPointAttention {
/// Create a new IPA module.
#[must_use]
pub fn new(num_query_points: usize, num_heads: usize, head_dim: usize) -> Self {
Self {
num_query_points,
num_heads,
head_dim,
}
}
/// Apply IPA to update sequence embeddings and frames.
pub fn forward(
&self,
seq_emb: &SequenceEmbedding,
frames: &mut [BackboneFrame],
) -> Vec<Vec<f32>> {
let seq_len = seq_emb.sequence_length;
let mut outputs = vec![vec![0.0_f32; seq_emb.embedding_dim]; seq_len];
// Simplified IPA: compute attention based on spatial distance
for i in 0..seq_len {
for j in 0..seq_len {
// Compute spatial distance between frames
let dist = distance(&frames[i].translation, &frames[j].translation);
// Distance-based attention (simplified)
let attention = (-dist / 10.0).exp();
// Accumulate weighted embeddings
if let Some(emb_j) = seq_emb.get(j) {
for (k, &v) in emb_j.iter().enumerate() {
outputs[i][k] += attention * v;
}
}
}
// Normalize
let norm: f32 = outputs[i].iter().map(|x| x * x).sum::<f32>().sqrt() + 1e-6;
for x in &mut outputs[i] {
*x /= norm;
}
}
outputs
}
}
/// Structure module that predicts 3D coordinates.
#[derive(Debug, Clone)]
pub struct StructureModule {
/// IPA layers
ipa_layers: Vec<InvariantPointAttention>,
/// Number of recycles
num_recycles: usize,
}
impl StructureModule {
/// Create a new structure module.
#[must_use]
pub fn new(num_layers: usize, num_recycles: usize) -> Self {
let ipa_layers = (0..num_layers)
.map(|_| InvariantPointAttention::default())
.collect();
Self {
ipa_layers,
num_recycles,
}
}
/// Predict 3D structure from sequence embeddings.
#[must_use]
pub fn predict(
&self,
seq_emb: &SequenceEmbedding,
sequence: &str,
) -> (Vec<AtomCoord>, Vec<f32>) {
let seq_len = seq_emb.sequence_length;
// Initialize frames (identity)
let mut frames: Vec<BackboneFrame> =
(0..seq_len).map(|_| BackboneFrame::default()).collect();
// Initialize frame positions along a line
for (i, frame) in frames.iter_mut().enumerate() {
frame.translation = [i as f32 * 3.8, 0.0, 0.0]; // ~3.8Å per residue
}
// Recycle through IPA layers
for _ in 0..self.num_recycles {
for ipa in &self.ipa_layers {
let updates = ipa.forward(seq_emb, &mut frames);
// Update frames based on IPA output
for (i, update) in updates.iter().enumerate() {
// Convert update to frame adjustment (simplified)
let angle_x = update.first().copied().unwrap_or(0.0) * 0.1;
let angle_y = update.get(1).copied().unwrap_or(0.0) * 0.1;
let delta_frame = BackboneFrame::from_axis_angle([1.0, 0.0, 0.0], angle_x);
let delta_frame2 = BackboneFrame::from_axis_angle([0.0, 1.0, 0.0], angle_y);
frames[i] = frames[i].compose(&delta_frame).compose(&delta_frame2);
}
}
}
// Convert frames to atom coordinates
self.frames_to_atoms(&frames, sequence)
}
/// Convert backbone frames to atom coordinates.
fn frames_to_atoms(
&self,
frames: &[BackboneFrame],
sequence: &str,
) -> (Vec<AtomCoord>, Vec<f32>) {
// Standard backbone atom positions in local frame (Angstroms)
let n_local = [-1.458, 0.0, 0.0];
let ca_local = [0.0, 0.0, 0.0];
let c_local = [1.523, 0.0, 0.0];
let o_local = [2.0, 1.0, 0.0];
let cb_local = [-0.5, 1.5, 0.0];
let mut atoms = Vec::with_capacity(frames.len() * 5);
let mut plddt_scores = Vec::with_capacity(frames.len());
for (i, (frame, residue)) in frames.iter().zip(sequence.chars()).enumerate() {
// Generate pLDDT based on frame stability (simplified)
let stability = frame.rotation[0][0] + frame.rotation[1][1] + frame.rotation[2][2];
let plddt = (50.0 + stability * 15.0).clamp(20.0, 100.0);
plddt_scores.push(plddt);
// Transform backbone atoms
let n_pos = frame.apply(n_local);
atoms.push(AtomCoord {
residue_idx: i,
atom_name: AtomName::N,
x: n_pos[0],
y: n_pos[1],
z: n_pos[2],
b_factor: plddt,
});
let ca_pos = frame.apply(ca_local);
atoms.push(AtomCoord {
residue_idx: i,
atom_name: AtomName::Ca,
x: ca_pos[0],
y: ca_pos[1],
z: ca_pos[2],
b_factor: plddt,
});
let c_pos = frame.apply(c_local);
atoms.push(AtomCoord {
residue_idx: i,
atom_name: AtomName::C,
x: c_pos[0],
y: c_pos[1],
z: c_pos[2],
b_factor: plddt,
});
let o_pos = frame.apply(o_local);
atoms.push(AtomCoord {
residue_idx: i,
atom_name: AtomName::O,
x: o_pos[0],
y: o_pos[1],
z: o_pos[2],
b_factor: plddt,
});
// CB for non-glycine
if residue != 'G' {
let cb_pos = frame.apply(cb_local);
atoms.push(AtomCoord {
residue_idx: i,
atom_name: AtomName::Cb,
x: cb_pos[0],
y: cb_pos[1],
z: cb_pos[2],
b_factor: plddt,
});
}
}
(atoms, plddt_scores)
}
}
/// Compute Euclidean distance between two points.
fn distance(a: &[f32; 3], b: &[f32; 3]) -> f32 {
let dx = a[0] - b[0];
let dy = a[1] - b[1];
let dz = a[2] - b[2];
(dx * dx + dy * dy + dz * dz).sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backbone_frame_default() {
let frame = BackboneFrame::default();
let point = [1.0, 2.0, 3.0];
let transformed = frame.apply(point);
assert_eq!(transformed, point); // Identity transform
}
#[test]
fn test_frame_translation() {
let mut frame = BackboneFrame::default();
frame.translation = [1.0, 2.0, 3.0];
let point = [0.0, 0.0, 0.0];
let transformed = frame.apply(point);
assert_eq!(transformed, [1.0, 2.0, 3.0]);
}
#[test]
fn test_frame_composition() {
let frame1 = BackboneFrame {
rotation: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
translation: [1.0, 0.0, 0.0],
};
let frame2 = BackboneFrame {
rotation: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
translation: [0.0, 1.0, 0.0],
};
let composed = frame1.compose(&frame2);
assert!((composed.translation[0] - 1.0).abs() < 0.001);
assert!((composed.translation[1] - 1.0).abs() < 0.001);
}
#[test]
fn test_structure_module() {
use crate::encoder::ProteinEncoder;
let encoder = ProteinEncoder::new();
let seq_emb = encoder.encode("ACDEF");
let structure_module = StructureModule::new(2, 1);
let (atoms, plddt) = structure_module.predict(&seq_emb, "ACDEF");
assert!(!atoms.is_empty());
assert_eq!(plddt.len(), 5);
}
#[test]
fn test_ipa() {
use crate::encoder::ProteinEncoder;
let encoder = ProteinEncoder::new();
let seq_emb = encoder.encode("ACDEF");
let mut frames: Vec<BackboneFrame> = (0..5).map(|_| BackboneFrame::default()).collect();
let ipa = InvariantPointAttention::default();
let output = ipa.forward(&seq_emb, &mut frames);
assert_eq!(output.len(), 5);
}
}