use crate::constants::{
AIR_FORCE_CONSTANT, DESOLV_CUTOFF, ELEC_CUTOFF, ELEC_MIN_DISTANCE, SOFTCORE_ALPHA, VDW_CUTOFF,
};
use crate::restraints;
use crate::structure;
use crate::structure::Atom;
pub fn calc_clashes(molecule1: &structure::Molecule, molecule2: &structure::Molecule) -> f64 {
let mut clashes = 0;
let mut total_atoms = 0;
for atom1 in &molecule1.0 {
for atom2 in &molecule2.0 {
total_atoms += 1;
let dist = structure::distance(atom1, atom2);
let vdw_dist = atom1.vdw_radius + atom2.vdw_radius;
if dist < vdw_dist {
clashes += 1;
}
}
}
clashes as f64 / total_atoms as f64
}
fn softcore_lj_potential(atom1: &Atom, atom2: &Atom, distance: f64, alpha: f64) -> f64 {
let epsilon_ij = (atom1.epsilon * atom2.epsilon).sqrt();
let rmin_ij = atom1.rmin2 + atom2.rmin2;
let rmin6 = rmin_ij.powi(6);
let r6 = distance.powi(6);
let r_eff6 = r6 + alpha * rmin6;
let ratio = rmin6 / r_eff6;
epsilon_ij * (ratio.powi(2) - 2.0 * ratio)
}
pub fn vdw_energy(receptor: &structure::Molecule, ligand: &structure::Molecule) -> f64 {
let mut energy = 0.0;
for atom1 in &receptor.0 {
for atom2 in &ligand.0 {
if atom1.serial != atom2.serial {
let dist = structure::distance(atom1, atom2);
if dist > 0.0 && dist < VDW_CUTOFF {
let vdw = softcore_lj_potential(atom1, atom2, dist, SOFTCORE_ALPHA);
let capped_vdw = vdw.clamp(-50.0, 500.0);
energy += capped_vdw;
}
}
}
}
energy
}
pub fn desolv_energy(receptor: &structure::Molecule, ligand: &structure::Molecule) -> f64 {
let mut energy = 0.0;
let get_asp = |element: &str| -> f64 {
match element.trim() {
"C" => 0.012, "N" => -0.160, "O" => -0.160, "S" => 0.012, "H" => 0.0, _ => 0.0,
}
};
for atom1 in &receptor.0 {
let asp1 = get_asp(&atom1.element);
if asp1.abs() < 0.001 {
continue; }
let mut burial_count = 0;
for atom2 in &ligand.0 {
let dist = structure::distance(atom1, atom2);
if dist < DESOLV_CUTOFF {
burial_count += 1;
}
}
let burial = (burial_count as f64 / 10.0).min(1.0);
energy -= asp1 * burial * atom1.vdw_radius * atom1.vdw_radius * 4.0 * std::f64::consts::PI;
}
for atom2 in &ligand.0 {
let asp2 = get_asp(&atom2.element);
if asp2.abs() < 0.001 {
continue;
}
let mut burial_count = 0;
for atom1 in &receptor.0 {
let dist = structure::distance(atom1, atom2);
if dist < DESOLV_CUTOFF {
burial_count += 1;
}
}
let burial = (burial_count as f64 / 10.0).min(1.0);
energy -= asp2 * burial * atom2.vdw_radius * atom2.vdw_radius * 4.0 * std::f64::consts::PI;
}
energy
}
pub fn air_energy(
restraints: &[crate::restraints::Restraint],
receptor: &structure::Molecule,
ligand: &structure::Molecule,
) -> f64 {
let mut energy = 0.0;
let lower_bound = 0.0; let upper_bound = 7.0;
for restraint in restraints {
let ca_receptor = receptor
.0
.iter()
.find(|a| a.resseq == restraint.0.resseq && a.name.trim() == "CA");
let ca_ligand = ligand
.0
.iter()
.find(|a| a.resseq == restraint.1.resseq && a.name.trim() == "CA");
if let (Some(ca1), Some(ca2)) = (ca_receptor, ca_ligand) {
let dist = structure::distance(ca1, ca2);
let violation = if dist < lower_bound {
lower_bound - dist } else if dist > upper_bound {
dist - upper_bound } else {
0.0 };
energy += AIR_FORCE_CONSTANT * violation * violation;
}
}
energy
}
pub fn satisfaction_ratio(
restraints: &[restraints::Restraint],
receptor: &structure::Molecule,
ligand: &structure::Molecule,
) -> f64 {
let satisfied = restraints
.iter()
.filter(|&x| x.is_satisfied(receptor, ligand))
.count();
let total = restraints.len();
if total > 0 {
satisfied as f64 / total as f64
} else {
0.0
}
}
pub fn elec_energy(receptor: &structure::Molecule, ligand: &structure::Molecule) -> f64 {
let k = 332.0636;
let mut energy = 0.0;
for atom1 in &receptor.0 {
for atom2 in &ligand.0 {
let dist = structure::distance(atom1, atom2);
if dist > ELEC_MIN_DISTANCE && dist < ELEC_CUTOFF {
let charge1 = atom1.charge;
let charge2 = atom2.charge;
energy += k * (charge1 * charge2) / (dist * dist);
}
}
}
energy
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_atom(x: f64, y: f64, z: f64, element: &str, charge: f64) -> structure::Atom {
structure::Atom {
serial: 1,
name: "CA".to_string(),
altloc: ' ',
resname: "ALA".to_string(),
chainid: 'A',
resseq: 1,
icode: ' ',
x,
y,
z,
occupancy: 1.0,
tempfactor: 0.0,
element: element.to_string(),
charge,
vdw_radius: 1.7,
epsilon: -0.1,
rmin2: 2.0,
eps_1_4: -0.1,
rmin2_1_4: 1.9,
}
}
#[test]
fn test_calc_clashes_no_clashes() {
let mut mol1 = structure::Molecule::new();
mol1.0.push(create_test_atom(0.0, 0.0, 0.0, "C", 0.0));
let mut mol2 = structure::Molecule::new();
mol2.0.push(create_test_atom(10.0, 0.0, 0.0, "C", 0.0));
let ratio = calc_clashes(&mol1, &mol2);
assert_eq!(ratio, 0.0);
}
#[test]
fn test_calc_clashes_with_clashes() {
let mut mol1 = structure::Molecule::new();
mol1.0.push(create_test_atom(0.0, 0.0, 0.0, "C", 0.0));
let mut mol2 = structure::Molecule::new();
mol2.0.push(create_test_atom(1.0, 0.0, 0.0, "C", 0.0));
let ratio = calc_clashes(&mol1, &mol2);
assert!(ratio > 0.0, "Should detect clash");
assert!(ratio <= 1.0, "Clash ratio should be <= 1.0");
}
#[test]
fn test_vdw_energy_far_atoms() {
let mut receptor = structure::Molecule::new();
receptor.0.push(create_test_atom(0.0, 0.0, 0.0, "C", 0.0));
let mut ligand = structure::Molecule::new();
ligand.0.push(create_test_atom(20.0, 0.0, 0.0, "C", 0.0));
let energy = vdw_energy(&receptor, &ligand);
assert_eq!(energy, 0.0, "Energy should be zero beyond cutoff");
}
#[test]
fn test_vdw_energy_close_atoms() {
let mut receptor = structure::Molecule::new();
receptor.0.push(create_test_atom(0.0, 0.0, 0.0, "C", 0.0));
let mut ligand = structure::Molecule::new();
ligand.0.push(create_test_atom(5.0, 0.0, 0.0, "C", 0.0));
let energy = vdw_energy(&receptor, &ligand);
assert!(energy.is_finite(), "Energy should be finite");
}
#[test]
fn test_vdw_energy_very_close_atoms() {
let mut receptor = structure::Molecule::new();
receptor.0.push(create_test_atom(0.0, 0.0, 0.0, "C", 0.0));
let mut ligand = structure::Molecule::new();
ligand.0.push(create_test_atom(1.0, 0.0, 0.0, "C", 0.0));
let energy = vdw_energy(&receptor, &ligand);
assert!(energy.is_finite(), "Soft-core should prevent infinities");
assert!(energy <= 500.0, "Energy should be capped at 500.0");
}
#[test]
fn test_elec_energy_neutral_atoms() {
let mut receptor = structure::Molecule::new();
receptor.0.push(create_test_atom(0.0, 0.0, 0.0, "C", 0.0));
let mut ligand = structure::Molecule::new();
ligand.0.push(create_test_atom(5.0, 0.0, 0.0, "C", 0.0));
let energy = elec_energy(&receptor, &ligand);
assert_eq!(energy, 0.0, "Energy should be zero for neutral atoms");
}
#[test]
fn test_elec_energy_charged_atoms() {
let mut receptor = structure::Molecule::new();
receptor.0.push(create_test_atom(0.0, 0.0, 0.0, "C", 1.0));
let mut ligand = structure::Molecule::new();
ligand.0.push(create_test_atom(5.0, 0.0, 0.0, "C", -1.0));
let energy = elec_energy(&receptor, &ligand);
assert!(
energy < 0.0,
"Opposite charges should give attractive (negative) energy"
);
assert!(energy.is_finite(), "Energy should be finite");
}
#[test]
fn test_elec_energy_same_charge() {
let mut receptor = structure::Molecule::new();
receptor.0.push(create_test_atom(0.0, 0.0, 0.0, "C", 1.0));
let mut ligand = structure::Molecule::new();
ligand.0.push(create_test_atom(5.0, 0.0, 0.0, "C", 1.0));
let energy = elec_energy(&receptor, &ligand);
assert!(
energy > 0.0,
"Same charges should give repulsive (positive) energy"
);
}
#[test]
fn test_desolv_energy_hydrophobic_burial() {
let mut receptor = structure::Molecule::new();
receptor.0.push(create_test_atom(0.0, 0.0, 0.0, "C", 0.0));
let mut ligand = structure::Molecule::new();
for i in 0..15 {
ligand.0.push(create_test_atom(
3.0 * (i as f64).cos(),
3.0 * (i as f64).sin(),
0.0,
"C",
0.0,
));
}
let energy = desolv_energy(&receptor, &ligand);
assert!(
energy < 0.0,
"Burying hydrophobic carbon should be favorable"
);
}
#[test]
fn test_desolv_energy_hydrophilic_burial() {
let mut receptor = structure::Molecule::new();
receptor.0.push(create_test_atom(0.0, 0.0, 0.0, "O", 0.0));
let mut ligand = structure::Molecule::new();
for i in 0..15 {
ligand.0.push(create_test_atom(
3.0 * (i as f64).cos(),
3.0 * (i as f64).sin(),
0.0,
"C",
0.0,
));
}
let energy = desolv_energy(&receptor, &ligand);
assert!(
energy > 0.0,
"Burying hydrophilic oxygen should be unfavorable"
);
}
#[test]
fn test_air_energy_satisfied_restraints() {
let mut receptor = structure::Molecule::new();
let mut atom1 = create_test_atom(0.0, 0.0, 0.0, "C", 0.0);
atom1.name = "CA".to_string();
atom1.resseq = 1;
receptor.0.push(atom1.clone());
let mut ligand = structure::Molecule::new();
let mut atom2 = create_test_atom(5.0, 0.0, 0.0, "C", 0.0); atom2.name = "CA".to_string();
atom2.resseq = 10;
ligand.0.push(atom2.clone());
let restraints = vec![restraints::Restraint(atom1, atom2)];
let energy = air_energy(&restraints, &receptor, &ligand);
assert_eq!(energy, 0.0, "Satisfied restraints should have zero penalty");
}
#[test]
fn test_air_energy_violated_restraints() {
let mut receptor = structure::Molecule::new();
let mut atom1 = create_test_atom(0.0, 0.0, 0.0, "C", 0.0);
atom1.name = "CA".to_string();
atom1.resseq = 1;
receptor.0.push(atom1.clone());
let mut ligand = structure::Molecule::new();
let mut atom2 = create_test_atom(15.0, 0.0, 0.0, "C", 0.0); atom2.name = "CA".to_string();
atom2.resseq = 10;
ligand.0.push(atom2.clone());
let restraints = vec![restraints::Restraint(atom1, atom2)];
let energy = air_energy(&restraints, &receptor, &ligand);
assert!(energy > 0.0, "Violated restraints should have penalty");
}
#[test]
fn test_satisfaction_ratio_all_satisfied() {
let mut receptor = structure::Molecule::new();
let mut atom1 = create_test_atom(0.0, 0.0, 0.0, "C", 0.0);
atom1.name = "CA".to_string();
atom1.resseq = 1;
receptor.0.push(atom1.clone());
let mut ligand = structure::Molecule::new();
let mut atom2 = create_test_atom(5.0, 0.0, 0.0, "C", 0.0);
atom2.name = "CA".to_string();
atom2.resseq = 10;
ligand.0.push(atom2.clone());
let restraints = vec![restraints::Restraint(atom1, atom2)];
let ratio = satisfaction_ratio(&restraints, &receptor, &ligand);
assert_eq!(ratio, 1.0, "All restraints satisfied should give ratio 1.0");
}
#[test]
fn test_satisfaction_ratio_none_satisfied() {
let mut receptor = structure::Molecule::new();
let mut atom1 = create_test_atom(0.0, 0.0, 0.0, "C", 0.0);
atom1.name = "CA".to_string();
atom1.resseq = 1;
receptor.0.push(atom1.clone());
let mut ligand = structure::Molecule::new();
let mut atom2 = create_test_atom(15.0, 0.0, 0.0, "C", 0.0);
atom2.name = "CA".to_string();
atom2.resseq = 10;
ligand.0.push(atom2.clone());
let restraints = vec![restraints::Restraint(atom1, atom2)];
let ratio = satisfaction_ratio(&restraints, &receptor, &ligand);
assert_eq!(ratio, 0.0, "No restraints satisfied should give ratio 0.0");
}
#[test]
fn test_satisfaction_ratio_empty_restraints() {
let receptor = structure::Molecule::new();
let ligand = structure::Molecule::new();
let restraints = vec![];
let ratio = satisfaction_ratio(&restraints, &receptor, &ligand);
assert_eq!(ratio, 0.0, "Empty restraints should give ratio 0.0");
}
#[test]
fn test_satisfaction_ratio_partial() {
let mut receptor = structure::Molecule::new();
let mut atom1 = create_test_atom(0.0, 0.0, 0.0, "C", 0.0);
atom1.name = "CA".to_string();
atom1.resseq = 1;
let mut atom3 = create_test_atom(0.0, 0.0, 0.0, "C", 0.0);
atom3.name = "CA".to_string();
atom3.resseq = 2;
receptor.0.push(atom1.clone());
receptor.0.push(atom3.clone());
let mut ligand = structure::Molecule::new();
let mut atom2 = create_test_atom(5.0, 0.0, 0.0, "C", 0.0); atom2.name = "CA".to_string();
atom2.resseq = 10;
let mut atom4 = create_test_atom(15.0, 0.0, 0.0, "C", 0.0); atom4.name = "CA".to_string();
atom4.resseq = 11;
ligand.0.push(atom2.clone());
ligand.0.push(atom4.clone());
let restraints = vec![
restraints::Restraint(atom1, atom2),
restraints::Restraint(atom3, atom4),
];
let ratio = satisfaction_ratio(&restraints, &receptor, &ligand);
assert_eq!(ratio, 0.5, "1 of 2 satisfied should give ratio 0.5");
}
}