use roxmltree::Node;
use super::ForceFieldReader;
use crate::ff::forcefield::{ForceField, SpecialBonds};
const KJ_PER_KCAL: f64 = 4.184;
const NM_TO_ANGSTROM: f64 = 10.0;
#[derive(Debug, Default, Clone, Copy)]
pub struct OplsXmlReader;
impl OplsXmlReader {
pub fn new() -> Self {
Self
}
}
impl ForceFieldReader for OplsXmlReader {
fn read_str(&self, text: &str) -> Result<ForceField, String> {
let doc =
roxmltree::Document::parse(text).map_err(|e| format!("OPLS XML parse error: {}", e))?;
let root = doc.root_element();
if root.tag_name().name() != "ForceField" {
return Err(format!(
"root element must be <ForceField>, got <{}>",
root.tag_name().name()
));
}
let mut ff = ForceField::new(root.attribute("name").unwrap_or("OPLS-AA"));
let mut masses: Vec<(String, f64)> = Vec::new();
let mut nonbonded: Vec<NonbondedRow> = Vec::new();
let mut coulomb14 = 0.5_f64;
let mut lj14 = 0.5_f64;
for sec in root.children().filter(Node::is_element) {
match sec.tag_name().name() {
"AtomTypes" => {
for t in sec.children().filter(Node::is_element) {
require_tag(&t, "Type")?;
let name = require_str(&t, "name")?.to_owned();
let mass = opt_f64(&t, "mass")?.unwrap_or(0.0);
masses.push((name, mass));
}
}
"HarmonicBondForce" => parse_bonds(&mut ff, &sec)?,
"HarmonicAngleForce" => parse_angles(&mut ff, &sec)?,
"RBTorsionForce" => parse_dihedrals(&mut ff, &sec)?,
"NonbondedForce" => {
coulomb14 = opt_f64(&sec, "coulomb14scale")?.unwrap_or(0.5);
lj14 = opt_f64(&sec, "lj14scale")?.unwrap_or(0.5);
for a in sec.children().filter(Node::is_element) {
require_tag(&a, "Atom")?;
nonbonded.push(NonbondedRow {
ty: require_str(&a, "type")?.to_owned(),
charge: opt_f64(&a, "charge")?.unwrap_or(0.0),
sigma: require_f64(&a, "sigma")? * NM_TO_ANGSTROM,
epsilon: require_f64(&a, "epsilon")? / KJ_PER_KCAL,
});
}
}
other => {
return Err(format!("unknown OPLS section <{}>", other));
}
}
}
build_nonbonded(&mut ff, &masses, &nonbonded);
ff.set_special_bonds(SpecialBonds {
lj: [0.0, 0.0, lj14],
coul: [0.0, 0.0, coulomb14],
});
Ok(ff)
}
}
struct NonbondedRow {
ty: String,
charge: f64,
sigma: f64,
epsilon: f64,
}
fn build_nonbonded(ff: &mut ForceField, masses: &[(String, f64)], nonbonded: &[NonbondedRow]) {
if !masses.is_empty() {
let atom = ff.def_atomstyle("full");
for (name, mass) in masses {
let charge = nonbonded
.iter()
.find(|r| &r.ty == name)
.map(|r| r.charge)
.unwrap_or(0.0);
atom.def_atomtype(name, &[("mass", *mass), ("charge", charge)]);
}
}
if !nonbonded.is_empty() {
let lj = ff.def_pairstyle("lj/cut", &[]);
for r in nonbonded {
lj.def_pairtype(&r.ty, None, &[("epsilon", r.epsilon), ("sigma", r.sigma)]);
}
ff.def_pairstyle("coul/cut", &[]);
}
}
fn parse_bonds(ff: &mut ForceField, sec: &Node) -> Result<(), String> {
let style = ff.def_bondstyle("harmonic");
for b in sec.children().filter(Node::is_element) {
require_tag(&b, "Bond")?;
let c1 = require_str(&b, "class1")?;
let c2 = require_str(&b, "class2")?;
let r0 = require_f64(&b, "length")? * NM_TO_ANGSTROM;
let k0 = require_f64(&b, "k")? / (KJ_PER_KCAL * 100.0);
style.def_bondtype(c1, c2, &[("k0", k0), ("r0", r0)]);
}
Ok(())
}
fn parse_angles(ff: &mut ForceField, sec: &Node) -> Result<(), String> {
let style = ff.def_anglestyle("harmonic");
for a in sec.children().filter(Node::is_element) {
require_tag(&a, "Angle")?;
let c1 = require_str(&a, "class1")?;
let c2 = require_str(&a, "class2")?;
let c3 = require_str(&a, "class3")?;
let theta0 = require_f64(&a, "angle")?; let k0 = require_f64(&a, "k")? / KJ_PER_KCAL; style.def_angletype(c1, c2, c3, &[("k0", k0), ("theta0", theta0)]);
}
Ok(())
}
fn parse_dihedrals(ff: &mut ForceField, sec: &Node) -> Result<(), String> {
let style = ff.def_dihedralstyle("opls");
for d in sec.children().filter(Node::is_element) {
require_tag(&d, "Proper")?;
let c1 = require_str(&d, "class1")?;
let c2 = require_str(&d, "class2")?;
let c3 = require_str(&d, "class3")?;
let c4 = require_str(&d, "class4")?;
let rb = [
opt_f64(&d, "c0")?.unwrap_or(0.0),
opt_f64(&d, "c1")?.unwrap_or(0.0),
opt_f64(&d, "c2")?.unwrap_or(0.0),
opt_f64(&d, "c3")?.unwrap_or(0.0),
opt_f64(&d, "c4")?.unwrap_or(0.0),
opt_f64(&d, "c5")?.unwrap_or(0.0),
];
let [f1, f2, f3, f4] = rb_to_opls(rb);
style.def_dihedraltype(
c1,
c2,
c3,
c4,
&[("f1", f1), ("f2", f2), ("f3", f3), ("f4", f4)],
);
}
Ok(())
}
fn rb_to_opls([_c0, c1, c2, c3, c4, _c5]: [f64; 6]) -> [f64; 4] {
let f1 = -2.0 * c1 - 1.5 * c3;
let f2 = -c2 - c4;
let f3 = -0.5 * c3;
let f4 = -0.25 * c4;
[
f1 / KJ_PER_KCAL,
f2 / KJ_PER_KCAL,
f3 / KJ_PER_KCAL,
f4 / KJ_PER_KCAL,
]
}
fn require_tag(node: &Node, expect: &str) -> Result<(), String> {
let got = node.tag_name().name();
if got == expect {
Ok(())
} else {
Err(format!("expected <{}>, got <{}>", expect, got))
}
}
fn require_str<'a>(node: &'a Node, attr: &str) -> Result<&'a str, String> {
node.attribute(attr).ok_or_else(|| {
format!(
"<{}> missing required attribute `{}`",
node.tag_name().name(),
attr
)
})
}
fn require_f64(node: &Node, attr: &str) -> Result<f64, String> {
let raw = require_str(node, attr)?;
raw.parse::<f64>().map_err(|_| {
format!(
"<{}> attribute `{}` is not a number: {:?}",
node.tag_name().name(),
attr,
raw
)
})
}
fn opt_f64(node: &Node, attr: &str) -> Result<Option<f64>, String> {
match node.attribute(attr) {
None => Ok(None),
Some(raw) => raw.parse::<f64>().map(Some).map_err(|_| {
format!(
"<{}> attribute `{}` is not a number: {:?}",
node.tag_name().name(),
attr,
raw
)
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
const MINI: &str = r#"<ForceField name="OPLS-AA" combining_rule="geometric">
<AtomTypes>
<Type name="opls_001" class="opls_001" element="C" mass="12.011"/>
<Type name="opls_002" class="opls_002" element="O" mass="15.9994"/>
</AtomTypes>
<HarmonicBondForce>
<Bond class1="OW" class2="HW" length="0.09572" k="502080.0"/>
</HarmonicBondForce>
<HarmonicAngleForce>
<Angle class1="HW" class2="OW" class3="HW" angle="1.91113553093" k="627.6"/>
</HarmonicAngleForce>
<RBTorsionForce>
<Proper class1="Br" class2="C" class3="CT" class4="HC" c0="0.75312" c1="2.25936" c2="0.0" c3="-3.01248" c4="0.0" c5="0.0"/>
</RBTorsionForce>
<NonbondedForce coulomb14scale="0.5" lj14scale="0.5">
<Atom type="opls_001" charge="0.5" sigma="0.375" epsilon="0.43932"/>
<Atom type="opls_002" charge="-0.5" sigma="0.296" epsilon="0.87864"/>
</NonbondedForce>
</ForceField>"#;
#[test]
fn rb_to_opls_matches_gromacs_inversion() {
let [f1, f2, f3, f4] = rb_to_opls([0.75312, 2.25936, 0.0, -3.01248, 0.0, 0.0]);
assert!((f1 - 0.0).abs() < 1e-12, "f1 {f1}");
assert!((f2 - 0.0).abs() < 1e-12, "f2 {f2}");
assert!((f3 - (1.50624 / 4.184)).abs() < 1e-12, "f3 {f3}");
assert!((f4 - 0.0).abs() < 1e-12, "f4 {f4}");
}
#[test]
fn reads_all_sections_with_molrs_units() {
let ff = OplsXmlReader::new().read_str(MINI).unwrap();
let bond = ff.get_style("bond", "harmonic").unwrap();
let bt = bond.get_bondtype("OW", "HW").unwrap();
assert!((bt.params.get("r0").unwrap() - 0.9572).abs() < 1e-9);
assert!((bt.params.get("k0").unwrap() - 502080.0 / 418.4).abs() < 1e-6);
let angle = ff.get_style("angle", "harmonic").unwrap();
let at = &angle_types(angle)[0];
assert!((at.params.get("theta0").unwrap() - 1.91113553093).abs() < 1e-9);
assert!((at.params.get("k0").unwrap() - 627.6 / 4.184).abs() < 1e-9);
let dih = ff.get_style("dihedral", "opls").unwrap();
assert!(dihedral_types(dih)[0].params.get("f3").is_some());
let lj = ff.get_style("pair", "lj/cut").unwrap();
let pt = lj.get_pairtype("opls_001", None).unwrap();
assert!((pt.params.get("sigma").unwrap() - 3.75).abs() < 1e-9);
assert!((pt.params.get("epsilon").unwrap() - 0.43932 / 4.184).abs() < 1e-9);
assert!(ff.get_style("pair", "coul/cut").is_some());
let sb = ff.special_bonds();
assert_eq!(sb.lj, [0.0, 0.0, 0.5]);
assert_eq!(sb.coul, [0.0, 0.0, 0.5]);
let atom = ff.get_style("atom", "full").unwrap();
let a1 = atom.get_atomtype("opls_001").unwrap();
assert!((a1.params.get("mass").unwrap() - 12.011).abs() < 1e-9);
assert!((a1.params.get("charge").unwrap() - 0.5).abs() < 1e-12);
let a2 = atom.get_atomtype("opls_002").unwrap();
assert!((a2.params.get("charge").unwrap() + 0.5).abs() < 1e-12);
}
#[test]
fn missing_required_attr_errors() {
let xml = r#"<ForceField name="x"><HarmonicBondForce>
<Bond class1="OW" class2="HW" length="0.1"/>
</HarmonicBondForce></ForceField>"#;
let err = OplsXmlReader::new().read_str(xml).unwrap_err();
assert!(err.contains('k'), "err: {err}");
}
#[test]
fn non_numeric_attr_errors() {
let xml = r#"<ForceField name="x"><HarmonicBondForce>
<Bond class1="OW" class2="HW" length="oops" k="1.0"/>
</HarmonicBondForce></ForceField>"#;
let err = OplsXmlReader::new().read_str(xml).unwrap_err();
assert!(err.contains("not a number"), "err: {err}");
}
#[test]
fn wrong_root_errors() {
let err = OplsXmlReader::new()
.read_str(r#"<System name="x"/>"#)
.unwrap_err();
assert!(err.contains("ForceField"), "err: {err}");
}
#[test]
fn unknown_section_errors() {
let xml = r#"<ForceField name="x"><MysteryForce/></ForceField>"#;
let err = OplsXmlReader::new().read_str(xml).unwrap_err();
assert!(err.contains("unknown OPLS section"), "err: {err}");
}
use crate::ff::forcefield::{AngleType, DihedralType, Style, StyleDefs};
fn angle_types(s: &Style) -> &[AngleType] {
match &s.defs {
StyleDefs::Angle(v) => v,
_ => unreachable!(),
}
}
fn dihedral_types(s: &Style) -> &[DihedralType] {
match &s.defs {
StyleDefs::Dihedral(v) => v,
_ => unreachable!(),
}
}
}