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]>
319 lines
9.8 KiB
Rust
319 lines
9.8 KiB
Rust
//! PDB and mmCIF export functionality.
|
|
|
|
use alphafold_shared::{AminoAcid, AtomName, ProteinStructure};
|
|
|
|
/// Export structure to PDB format.
|
|
#[must_use]
|
|
pub fn to_pdb(structure: &ProteinStructure) -> String {
|
|
let mut pdb = String::new();
|
|
|
|
// Header
|
|
pdb.push_str(&format!(
|
|
"HEADER PREDICTED STRUCTURE {}\n",
|
|
"01-JAN-26"
|
|
));
|
|
pdb.push_str(&format!("TITLE {}\n", structure.name.to_uppercase()));
|
|
pdb.push_str("REMARK 1 PREDICTED BY ALPHAFOLD-LITE (RUSTYTORCH++)\n");
|
|
pdb.push_str(&format!(
|
|
"REMARK 2 AVERAGE PLDDT: {:.1}\n",
|
|
structure.model_confidence.avg_plddt
|
|
));
|
|
pdb.push_str(&format!(
|
|
"REMARK 2 PTM SCORE: {:.3}\n",
|
|
structure.model_confidence.ptm_score
|
|
));
|
|
|
|
// Sequence
|
|
let seq_lines = structure.sequence.as_bytes().chunks(60);
|
|
for (i, chunk) in seq_lines.enumerate() {
|
|
let seq_str: String = chunk.iter().map(|&b| b as char).collect();
|
|
pdb.push_str(&format!(
|
|
"SEQRES {:3} A {:4} {}\n",
|
|
i + 1,
|
|
structure.num_residues,
|
|
seq_str
|
|
));
|
|
}
|
|
|
|
// Atoms
|
|
let mut atom_serial = 1;
|
|
let mut current_residue = usize::MAX;
|
|
let mut residue_name = "UNK";
|
|
|
|
for atom in &structure.atom_coords {
|
|
// Update residue info if changed
|
|
if atom.residue_idx != current_residue {
|
|
current_residue = atom.residue_idx;
|
|
if let Some(aa_char) = structure.sequence.chars().nth(atom.residue_idx)
|
|
&& let Some(aa) = AminoAcid::from_code(aa_char)
|
|
{
|
|
residue_name = aa.code3();
|
|
}
|
|
}
|
|
|
|
let atom_name = match atom.atom_name {
|
|
AtomName::N => " N ",
|
|
AtomName::Ca => " CA ",
|
|
AtomName::C => " C ",
|
|
AtomName::O => " O ",
|
|
AtomName::Cb => " CB ",
|
|
};
|
|
|
|
let element = match atom.atom_name {
|
|
AtomName::N => "N",
|
|
AtomName::Ca | AtomName::C | AtomName::Cb => "C",
|
|
AtomName::O => "O",
|
|
};
|
|
|
|
pdb.push_str(&format!(
|
|
"ATOM {:5} {}{:>3} A{:4} {:8.3}{:8.3}{:8.3}{:6.2}{:6.2} {:>2}\n",
|
|
atom_serial,
|
|
atom_name,
|
|
residue_name,
|
|
atom.residue_idx + 1,
|
|
atom.x,
|
|
atom.y,
|
|
atom.z,
|
|
1.00, // Occupancy
|
|
atom.b_factor, // B-factor (pLDDT)
|
|
element
|
|
));
|
|
|
|
atom_serial += 1;
|
|
}
|
|
|
|
// Terminal
|
|
pdb.push_str("TER\n");
|
|
pdb.push_str("END\n");
|
|
|
|
pdb
|
|
}
|
|
|
|
/// Export structure to mmCIF format.
|
|
#[must_use]
|
|
pub fn to_mmcif(structure: &ProteinStructure) -> String {
|
|
let mut cif = String::new();
|
|
|
|
// Data block
|
|
let safe_name = structure.name.replace(' ', "_").to_lowercase();
|
|
cif.push_str(&format!("data_{safe_name}\n#\n"));
|
|
|
|
// Entry
|
|
cif.push_str(&format!("_entry.id {safe_name}\n#\n"));
|
|
|
|
// Entity
|
|
cif.push_str("_entity.id 1\n");
|
|
cif.push_str("_entity.type polymer\n");
|
|
cif.push_str("_entity.src_method man\n");
|
|
cif.push_str("_entity.pdbx_description 'Predicted protein structure'\n");
|
|
cif.push_str("#\n");
|
|
|
|
// Entity poly
|
|
cif.push_str("_entity_poly.entity_id 1\n");
|
|
cif.push_str("_entity_poly.type polypeptide(L)\n");
|
|
cif.push_str(&format!(
|
|
"_entity_poly.pdbx_seq_one_letter_code\n;{}\n;\n",
|
|
structure.sequence
|
|
));
|
|
cif.push_str("#\n");
|
|
|
|
// Software
|
|
cif.push_str("_software.name 'AlphaFold-Lite'\n");
|
|
cif.push_str("_software.version '1.0'\n");
|
|
cif.push_str("_software.classification 'structure prediction'\n");
|
|
cif.push_str("#\n");
|
|
|
|
// Model quality
|
|
cif.push_str("loop_\n");
|
|
cif.push_str("_ma_qa_metric_global.id\n");
|
|
cif.push_str("_ma_qa_metric_global.model_id\n");
|
|
cif.push_str("_ma_qa_metric_global.metric_id\n");
|
|
cif.push_str("_ma_qa_metric_global.metric_value\n");
|
|
cif.push_str(&format!(
|
|
"1 1 1 {:.2}\n",
|
|
structure.model_confidence.avg_plddt
|
|
));
|
|
cif.push_str(&format!(
|
|
"2 1 2 {:.4}\n",
|
|
structure.model_confidence.ptm_score
|
|
));
|
|
cif.push_str("#\n");
|
|
|
|
// Atom sites
|
|
cif.push_str("loop_\n");
|
|
cif.push_str("_atom_site.group_PDB\n");
|
|
cif.push_str("_atom_site.id\n");
|
|
cif.push_str("_atom_site.type_symbol\n");
|
|
cif.push_str("_atom_site.label_atom_id\n");
|
|
cif.push_str("_atom_site.label_alt_id\n");
|
|
cif.push_str("_atom_site.label_comp_id\n");
|
|
cif.push_str("_atom_site.label_asym_id\n");
|
|
cif.push_str("_atom_site.label_entity_id\n");
|
|
cif.push_str("_atom_site.label_seq_id\n");
|
|
cif.push_str("_atom_site.pdbx_PDB_ins_code\n");
|
|
cif.push_str("_atom_site.Cartn_x\n");
|
|
cif.push_str("_atom_site.Cartn_y\n");
|
|
cif.push_str("_atom_site.Cartn_z\n");
|
|
cif.push_str("_atom_site.occupancy\n");
|
|
cif.push_str("_atom_site.B_iso_or_equiv\n");
|
|
cif.push_str("_atom_site.auth_seq_id\n");
|
|
cif.push_str("_atom_site.auth_asym_id\n");
|
|
cif.push_str("_atom_site.pdbx_PDB_model_num\n");
|
|
|
|
let mut current_residue = usize::MAX;
|
|
let mut residue_name = "UNK";
|
|
|
|
for (i, atom) in structure.atom_coords.iter().enumerate() {
|
|
if atom.residue_idx != current_residue {
|
|
current_residue = atom.residue_idx;
|
|
if let Some(aa_char) = structure.sequence.chars().nth(atom.residue_idx)
|
|
&& let Some(aa) = AminoAcid::from_code(aa_char)
|
|
{
|
|
residue_name = aa.code3();
|
|
}
|
|
}
|
|
|
|
let atom_name = atom.atom_name.as_pdb_str();
|
|
let element = match atom.atom_name {
|
|
AtomName::N => "N",
|
|
AtomName::Ca | AtomName::C | AtomName::Cb => "C",
|
|
AtomName::O => "O",
|
|
};
|
|
|
|
cif.push_str(&format!(
|
|
"ATOM {} {} {} . {} A 1 {} ? {:.3} {:.3} {:.3} 1.00 {:.2} {} A 1\n",
|
|
i + 1,
|
|
element,
|
|
atom_name,
|
|
residue_name,
|
|
atom.residue_idx + 1,
|
|
atom.x,
|
|
atom.y,
|
|
atom.z,
|
|
atom.b_factor,
|
|
atom.residue_idx + 1
|
|
));
|
|
}
|
|
|
|
cif.push_str("#\n");
|
|
|
|
cif
|
|
}
|
|
|
|
/// Generate a `PyMOL` script for visualization.
|
|
#[must_use]
|
|
pub fn to_pymol_script(structure: &ProteinStructure) -> String {
|
|
let mut script = String::new();
|
|
|
|
script.push_str("# PyMOL visualization script for AlphaFold-Lite prediction\n");
|
|
script.push_str(&format!("# Protein: {}\n\n", structure.name));
|
|
|
|
// Color by pLDDT
|
|
script.push_str("# Color by pLDDT confidence\n");
|
|
script.push_str("cmd.color('0x0053D6', 'b > 90') # Very high (blue)\n");
|
|
script.push_str("cmd.color('0x65CBF3', 'b > 70 and b <= 90') # High (cyan)\n");
|
|
script.push_str("cmd.color('0xFFDB13', 'b > 50 and b <= 70') # Low (yellow)\n");
|
|
script.push_str("cmd.color('0xFF7D45', 'b <= 50') # Very low (orange)\n\n");
|
|
|
|
// Visualization settings
|
|
script.push_str("# Visualization settings\n");
|
|
script.push_str("cmd.show('cartoon')\n");
|
|
script.push_str("cmd.set('cartoon_fancy_helices', 1)\n");
|
|
script.push_str("cmd.set('cartoon_smooth_loops', 1)\n");
|
|
script.push_str("cmd.bg_color('white')\n");
|
|
script.push_str("cmd.zoom()\n");
|
|
|
|
script
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use alphafold_shared::{
|
|
AtomCoord, ChainInfo, ConfidenceCategory, ModelConfidence, SecondaryStructure,
|
|
};
|
|
|
|
fn create_test_structure() -> ProteinStructure {
|
|
ProteinStructure {
|
|
name: "Test Protein".to_string(),
|
|
sequence: "ACD".to_string(),
|
|
num_residues: 3,
|
|
atom_coords: vec![
|
|
AtomCoord {
|
|
residue_idx: 0,
|
|
atom_name: AtomName::Ca,
|
|
x: 0.0,
|
|
y: 0.0,
|
|
z: 0.0,
|
|
b_factor: 90.0,
|
|
},
|
|
AtomCoord {
|
|
residue_idx: 1,
|
|
atom_name: AtomName::Ca,
|
|
x: 3.8,
|
|
y: 0.0,
|
|
z: 0.0,
|
|
b_factor: 85.0,
|
|
},
|
|
AtomCoord {
|
|
residue_idx: 2,
|
|
atom_name: AtomName::Ca,
|
|
x: 7.6,
|
|
y: 0.0,
|
|
z: 0.0,
|
|
b_factor: 80.0,
|
|
},
|
|
],
|
|
plddt_scores: vec![90.0, 85.0, 80.0],
|
|
pae_matrix: None,
|
|
model_confidence: ModelConfidence {
|
|
avg_plddt: 85.0,
|
|
ptm_score: 0.8,
|
|
iptm_score: None,
|
|
category: ConfidenceCategory::High,
|
|
},
|
|
secondary_structure: vec![
|
|
SecondaryStructure::Helix,
|
|
SecondaryStructure::Helix,
|
|
SecondaryStructure::Coil,
|
|
],
|
|
chains: vec![ChainInfo {
|
|
chain_id: 'A',
|
|
start_residue: 0,
|
|
end_residue: 3,
|
|
sequence: "ACD".to_string(),
|
|
}],
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_to_pdb() {
|
|
let structure = create_test_structure();
|
|
let pdb = to_pdb(&structure);
|
|
|
|
assert!(pdb.contains("HEADER"));
|
|
assert!(pdb.contains("ATOM"));
|
|
assert!(pdb.contains("END"));
|
|
assert!(pdb.contains("CA"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_to_mmcif() {
|
|
let structure = create_test_structure();
|
|
let cif = to_mmcif(&structure);
|
|
|
|
assert!(cif.contains("data_"));
|
|
assert!(cif.contains("_atom_site"));
|
|
assert!(cif.contains("ATOM"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_pymol_script() {
|
|
let structure = create_test_structure();
|
|
let script = to_pymol_script(&structure);
|
|
|
|
assert!(script.contains("cmd.color"));
|
|
assert!(script.contains("cartoon"));
|
|
}
|
|
}
|