use std::collections::HashMap;
use super::{
AngleType, BondType, DihedralType, ForceField, ImproperType, PairType, Params, StyleDefs,
};
use crate::ff::typifier::mmff::{MMFFAtomProp, MMFFParams};
use crate::ff::typifier::opls::{OplsTypeRow, OplsTypingMeta};
pub fn read_forcefield_xml(path: &str) -> Result<ForceField, String> {
let xml = std::fs::read_to_string(path).map_err(|e| format!("read {}: {}", path, e))?;
read_forcefield_xml_str(&xml)
}
pub fn read_forcefield_xml_str(xml: &str) -> Result<ForceField, String> {
let doc = roxmltree::Document::parse(xml).map_err(|e| format!("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 name = root.attribute("name").unwrap_or("unnamed");
let mut ff = ForceField::new(name);
for child in root.children().filter(|n| n.is_element()) {
match child.tag_name().name() {
"BondStyle" => parse_generic_style(&mut ff, &child, "bond")?,
"AngleStyle" => parse_generic_style(&mut ff, &child, "angle")?,
"DihedralStyle" => parse_generic_style(&mut ff, &child, "dihedral")?,
"ImproperStyle" => parse_generic_style(&mut ff, &child, "improper")?,
"PairStyle" => parse_generic_pair_style(&mut ff, &child)?,
"BondStretchParams" => parse_mmff_bonds(&mut ff, &child)?,
"AngleBendParams" => parse_mmff_angles(&mut ff, &child)?,
"StretchBendParams" => parse_mmff_stbn(&mut ff, &child)?,
"TorsionParams" => parse_mmff_torsions(&mut ff, &child)?,
"OutOfPlaneParams" => parse_mmff_oop(&mut ff, &child)?,
"VdWParams" => parse_mmff_vdw(&mut ff, &child)?,
"AtomTypes"
| "AtomProperties"
| "EquivalenceTable"
| "BondChargeIncrements"
| "PartialBondChargeIncrements"
| "DefaultStretchBend"
| "EmpiricalBondRules" => {}
_ => {} }
}
Ok(ff)
}
pub fn read_mmff_params_xml_str(xml: &str) -> Result<MMFFParams, String> {
let doc = roxmltree::Document::parse(xml).map_err(|e| format!("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 props = HashMap::new();
for child in root.children().filter(|n| n.is_element()) {
if child.tag_name().name() == "AtomProperties" {
for prop_node in child
.children()
.filter(|n| n.is_element() && n.tag_name().name() == "Prop")
{
let p = parse_atom_prop(&prop_node)?;
props.insert(p.type_id, p);
}
}
}
if props.is_empty() {
return Err("No <AtomProperties> found in XML".to_string());
}
Ok(MMFFParams::new(props))
}
pub fn read_opls_typing_xml_str(xml: &str) -> Result<OplsTypingMeta, String> {
let doc = roxmltree::Document::parse(xml).map_err(|e| format!("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 meta = OplsTypingMeta::new();
for child in root.children().filter(|n| n.is_element()) {
if child.tag_name().name() != "AtomTypes" {
continue;
}
for t in child
.children()
.filter(|n| n.is_element() && n.tag_name().name() == "Type")
{
let name = attr_str(&t, "name")?.to_owned();
let class = attr_str(&t, "class")?.to_owned();
let def = t.attribute("def").map(str::to_owned);
let overrides = t
.attribute("overrides")
.map(parse_overrides)
.unwrap_or_default();
let priority = match t.attribute("priority") {
None => None,
Some(s) => Some(s.parse::<i64>().map_err(|_| {
format!(
"<Type name={:?}> attribute 'priority' is not an integer: {:?}",
name, s
)
})?),
};
let layer = match t.attribute("layer") {
None => 0,
Some(s) => s.parse::<u32>().map_err(|_| {
format!(
"<Type name={:?}> attribute 'layer' is not an integer: {:?}",
name, s
)
})?,
};
meta.insert(
name,
OplsTypeRow {
class,
def,
overrides,
priority,
layer,
},
);
}
}
Ok(meta)
}
fn parse_overrides(raw: &str) -> Vec<String> {
raw.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
}
fn attr_str<'a>(node: &'a roxmltree::Node, name: &str) -> Result<&'a str, String> {
node.attribute(name)
.ok_or_else(|| format!("<{}> missing attribute '{}'", node.tag_name().name(), name))
}
fn attr_f64(node: &roxmltree::Node, name: &str) -> Result<f64, String> {
let s = attr_str(node, name)?;
s.parse::<f64>().map_err(|e| {
format!(
"<{}> attribute '{}' = {:?}: {}",
node.tag_name().name(),
name,
s,
e
)
})
}
fn attr_u32(node: &roxmltree::Node, name: &str) -> Result<u32, String> {
let s = attr_str(node, name)?;
s.parse::<u32>().map_err(|e| {
format!(
"<{}> attribute '{}' = {:?}: {}",
node.tag_name().name(),
name,
s,
e
)
})
}
fn opt_attr_f64(node: &roxmltree::Node, name: &str) -> Option<f64> {
node.attribute(name).and_then(|s| s.parse::<f64>().ok())
}
fn children_named<'a>(
parent: &'a roxmltree::Node<'a, 'a>,
tag: &'a str,
) -> impl Iterator<Item = roxmltree::Node<'a, 'a>> {
parent
.children()
.filter(move |n| n.is_element() && n.tag_name().name() == tag)
}
fn numeric_attrs<'a>(node: &'a roxmltree::Node, skip: &[&str]) -> Vec<(&'a str, f64)> {
let mut result = Vec::new();
for attr in node.attributes() {
if skip.contains(&attr.name()) {
continue;
}
if let Ok(v) = attr.value().parse::<f64>() {
result.push((attr.name(), v));
}
}
result
}
fn parse_atom_prop(node: &roxmltree::Node) -> Result<MMFFAtomProp, String> {
Ok(MMFFAtomProp {
type_id: attr_u32(node, "type")?,
atno: attr_u32(node, "atno")?,
crd: attr_u32(node, "crd")?,
val: attr_u32(node, "val")?,
pilp: attr_u32(node, "pilp")?,
mltb: attr_u32(node, "mltb")?,
arom: attr_u32(node, "arom")?,
linh: attr_u32(node, "linh")?,
sbmb: attr_u32(node, "sbmb")?,
})
}
fn parse_generic_style(
ff: &mut ForceField,
node: &roxmltree::Node,
category: &str,
) -> Result<(), String> {
let style_name = attr_str(node, "name")?;
let style = match category {
"bond" => ff.def_bondstyle(style_name),
"angle" => ff.def_anglestyle(style_name),
"dihedral" => ff.def_dihedralstyle(style_name),
"improper" => ff.def_improperstyle(style_name),
_ => return Err(format!("unknown category: {}", category)),
};
for type_node in children_named(node, "Type") {
let name = attr_str(&type_node, "name")?;
let params = numeric_attrs(&type_node, &["name"]);
style.def_type(name, ¶ms);
}
Ok(())
}
fn parse_generic_pair_style(ff: &mut ForceField, node: &roxmltree::Node) -> Result<(), String> {
let style_name = attr_str(node, "name")?;
let style_params = numeric_attrs(node, &["name"]);
let style = ff.def_pairstyle(style_name, &style_params);
for type_node in children_named(node, "Type") {
let name = attr_str(&type_node, "name")?;
let params = numeric_attrs(&type_node, &["name"]);
style.def_type(name, ¶ms);
}
Ok(())
}
fn parse_mmff_bonds(ff: &mut ForceField, node: &roxmltree::Node) -> Result<(), String> {
let style = ff.def_bondstyle("mmff_bond");
let StyleDefs::Bond(types) = &mut style.defs else {
unreachable!();
};
for bond in children_named(node, "Bond") {
let bt = attr_f64(&bond, "bond_type")?;
let t1 = attr_f64(&bond, "type1")?;
let t2 = attr_f64(&bond, "type2")?;
let kb = attr_f64(&bond, "kb")?;
let r0 = attr_f64(&bond, "r0")?;
types.push(BondType {
name: format!("{}_{}_{}", bt as u32, t1 as u32, t2 as u32),
itom: format!("{}", t1 as u32),
jtom: format!("{}", t2 as u32),
params: Params::from_pairs(&[("kb", kb), ("r0", r0), ("bond_type", bt)]),
});
}
Ok(())
}
fn parse_mmff_angles(ff: &mut ForceField, node: &roxmltree::Node) -> Result<(), String> {
let style = ff.def_anglestyle("mmff_angle");
let StyleDefs::Angle(types) = &mut style.defs else {
unreachable!();
};
for angle in children_named(node, "Angle") {
let at = attr_f64(&angle, "angle_type")?;
let t1 = attr_f64(&angle, "type1")?;
let t2 = attr_f64(&angle, "type2")?;
let t3 = attr_f64(&angle, "type3")?;
let ka = attr_f64(&angle, "ka")?;
let theta0 = attr_f64(&angle, "theta0")?.to_radians();
types.push(AngleType {
name: format!("{}_{}_{}_{}", at as u32, t1 as u32, t2 as u32, t3 as u32),
itom: format!("{}", t1 as u32),
jtom: format!("{}", t2 as u32),
ktom: format!("{}", t3 as u32),
params: Params::from_pairs(&[("ka", ka), ("theta0", theta0), ("angle_type", at)]),
});
}
Ok(())
}
fn parse_mmff_stbn(ff: &mut ForceField, node: &roxmltree::Node) -> Result<(), String> {
let style = ff.def_anglestyle("mmff_stbn");
let StyleDefs::Angle(types) = &mut style.defs else {
unreachable!();
};
for sb in children_named(node, "StretchBend") {
let sbt = attr_f64(&sb, "stbn_type")?;
let t1 = attr_f64(&sb, "type1")?;
let t2 = attr_f64(&sb, "type2")?;
let t3 = attr_f64(&sb, "type3")?;
let kba_ijk = attr_f64(&sb, "kba_ijk")?;
let kba_kji = attr_f64(&sb, "kba_kji")?;
types.push(AngleType {
name: format!("{}_{}_{}_{}", sbt as u32, t1 as u32, t2 as u32, t3 as u32),
itom: format!("{}", t1 as u32),
jtom: format!("{}", t2 as u32),
ktom: format!("{}", t3 as u32),
params: Params::from_pairs(&[
("kba_ijk", kba_ijk),
("kba_kji", kba_kji),
("stbn_type", sbt),
]),
});
}
Ok(())
}
fn parse_mmff_torsions(ff: &mut ForceField, node: &roxmltree::Node) -> Result<(), String> {
let style = ff.def_dihedralstyle("mmff_torsion");
let StyleDefs::Dihedral(types) = &mut style.defs else {
unreachable!();
};
for tor in children_named(node, "Torsion") {
let tt = attr_f64(&tor, "tor_type")?;
let t1 = attr_f64(&tor, "type1")?;
let t2 = attr_f64(&tor, "type2")?;
let t3 = attr_f64(&tor, "type3")?;
let t4 = attr_f64(&tor, "type4")?;
let v1 = attr_f64(&tor, "v1")?;
let v2 = attr_f64(&tor, "v2")?;
let v3 = attr_f64(&tor, "v3")?;
types.push(DihedralType {
name: format!(
"{}_{}_{}_{}_{}",
tt as u32, t1 as u32, t2 as u32, t3 as u32, t4 as u32
),
itom: format!("{}", t1 as u32),
jtom: format!("{}", t2 as u32),
ktom: format!("{}", t3 as u32),
ltom: format!("{}", t4 as u32),
params: Params::from_pairs(&[("v1", v1), ("v2", v2), ("v3", v3), ("tor_type", tt)]),
});
}
Ok(())
}
fn parse_mmff_oop(ff: &mut ForceField, node: &roxmltree::Node) -> Result<(), String> {
let style = ff.def_improperstyle("mmff_oop");
let StyleDefs::Improper(types) = &mut style.defs else {
unreachable!();
};
for oop in children_named(node, "Oop") {
let t1 = attr_f64(&oop, "type1")?;
let t2 = attr_f64(&oop, "type2")?;
let t3 = attr_f64(&oop, "type3")?;
let t4 = attr_f64(&oop, "type4")?;
let koop = attr_f64(&oop, "koop")?;
types.push(ImproperType {
name: format!("{}_{}_{}_{}", t1 as u32, t2 as u32, t3 as u32, t4 as u32),
itom: format!("{}", t1 as u32),
jtom: format!("{}", t2 as u32),
ktom: format!("{}", t3 as u32),
ltom: format!("{}", t4 as u32),
params: Params::from_pairs(&[("koop", koop)]),
});
}
Ok(())
}
fn parse_mmff_vdw(ff: &mut ForceField, node: &roxmltree::Node) -> Result<(), String> {
let mut style_params: Vec<(&str, f64)> = Vec::new();
for attr_name in ["B", "Beta", "DARAD", "DAEPS"] {
if let Some(v) = opt_attr_f64(node, attr_name) {
style_params.push((attr_name, v));
}
}
let style = ff.def_pairstyle("mmff_vdw", &style_params);
let StyleDefs::Pair(types) = &mut style.defs else {
unreachable!();
};
for vdw in children_named(node, "VdW") {
let atype = attr_f64(&vdw, "type")?;
let alpha = attr_f64(&vdw, "alpha")?;
let n_eff = attr_f64(&vdw, "n_eff")?;
let a_i = attr_f64(&vdw, "a_i")?;
let g_i = attr_f64(&vdw, "g_i")?;
let type_name = format!("{}", atype as u32);
types.push(PairType {
name: type_name.clone(),
itom: type_name.clone(),
jtom: type_name,
params: Params::from_pairs(&[
("alpha", alpha),
("n_eff", n_eff),
("a_i", a_i),
("g_i", g_i),
("type", atype),
]),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generic_bond_style() {
let xml = r#"
<ForceField name="test">
<BondStyle name="harmonic">
<Type name="CT-OH" k0="300.0" r0="1.4" />
<Type name="CT-CT" k0="268.0" r0="1.529" />
</BondStyle>
</ForceField>
"#;
let ff = read_forcefield_xml_str(xml).unwrap();
assert_eq!(ff.name, "test");
assert_eq!(ff.get_bondtypes().len(), 2);
let style = ff.get_style("bond", "harmonic").unwrap();
let bt = style.get_bondtype("CT", "OH").unwrap();
assert_eq!(bt.params.get("k0"), Some(300.0));
assert_eq!(bt.params.get("r0"), Some(1.4));
}
#[test]
fn test_generic_angle_style() {
let xml = r#"
<ForceField name="test">
<AngleStyle name="harmonic">
<Type name="HW-OW-HW" k0="55.0" theta0="104.52" />
</AngleStyle>
</ForceField>
"#;
let ff = read_forcefield_xml_str(xml).unwrap();
let types = ff.get_angletypes();
assert_eq!(types.len(), 1);
assert_eq!(types[0].params.get("theta0"), Some(104.52));
}
#[test]
fn test_generic_pair_style() {
let xml = r#"
<ForceField name="test">
<PairStyle name="lj/cut" cutoff="10.0">
<Type name="CT" epsilon="0.066" sigma="3.5" />
</PairStyle>
</ForceField>
"#;
let ff = read_forcefield_xml_str(xml).unwrap();
let style = ff.get_style("pair", "lj/cut").unwrap();
assert_eq!(style.params.get("cutoff"), Some(10.0));
assert_eq!(ff.get_pairtypes().len(), 1);
}
#[test]
fn test_generic_dihedral_style() {
let xml = r#"
<ForceField name="test">
<DihedralStyle name="opls">
<Type name="HC-CT-CT-HC" k1="0.0" k2="0.0" k3="0.3" />
</DihedralStyle>
</ForceField>
"#;
let ff = read_forcefield_xml_str(xml).unwrap();
let styles = ff.get_styles("dihedral");
assert_eq!(styles.len(), 1);
}
#[test]
fn test_mmff_bond_stretch() {
let xml = r#"
<ForceField name="MMFF94">
<BondStretchParams>
<Bond bond_type="0" type1="1" type2="1" kb="4.258" r0="1.508" />
<Bond bond_type="0" type1="1" type2="2" kb="4.539" r0="1.482" />
</BondStretchParams>
</ForceField>
"#;
let ff = read_forcefield_xml_str(xml).unwrap();
let types = ff.get_bondtypes();
assert_eq!(types.len(), 2);
assert_eq!(types[0].name, "0_1_1");
assert_eq!(types[0].params.get("kb"), Some(4.258));
assert_eq!(types[0].params.get("r0"), Some(1.508));
assert_eq!(types[0].params.get("bond_type"), Some(0.0));
}
#[test]
fn test_mmff_angle_bend() {
let xml = r#"
<ForceField name="MMFF94">
<AngleBendParams>
<Angle angle_type="0" type1="1" type2="1" type3="1" ka="0.608" theta0="109.608" />
</AngleBendParams>
</ForceField>
"#;
let ff = read_forcefield_xml_str(xml).unwrap();
let types = ff.get_angletypes();
assert_eq!(types.len(), 1);
assert_eq!(types[0].params.get("ka"), Some(0.608));
assert_eq!(
types[0].params.get("theta0"),
Some(109.608_f64.to_radians())
);
}
#[test]
fn test_mmff_torsion() {
let xml = r#"
<ForceField name="MMFF94">
<TorsionParams>
<Torsion tor_type="0" type1="0" type2="1" type3="1" type4="0" v1="0.0" v2="0.0" v3="0.300" />
</TorsionParams>
</ForceField>
"#;
let ff = read_forcefield_xml_str(xml).unwrap();
let styles = ff.get_styles("dihedral");
assert_eq!(styles.len(), 1);
assert_eq!(styles[0].name, "mmff_torsion");
}
#[test]
fn test_mmff_oop() {
let xml = r#"
<ForceField name="MMFF94">
<OutOfPlaneParams>
<Oop type1="0" type2="2" type3="0" type4="0" koop="0.020" />
</OutOfPlaneParams>
</ForceField>
"#;
let ff = read_forcefield_xml_str(xml).unwrap();
let styles = ff.get_styles("improper");
assert_eq!(styles.len(), 1);
}
#[test]
fn test_mmff_vdw() {
let xml = r#"
<ForceField name="MMFF94">
<VdWParams B="0.12" Beta="12.0" DARAD="0.8" DAEPS="0.5">
<VdW type="1" alpha="1.050" n_eff="2.490" a_i="3.890" g_i="1.282" />
</VdWParams>
</ForceField>
"#;
let ff = read_forcefield_xml_str(xml).unwrap();
let style = ff.get_style("pair", "mmff_vdw").unwrap();
assert_eq!(style.params.get("B"), Some(0.12));
assert_eq!(style.params.get("Beta"), Some(12.0));
let types = ff.get_pairtypes();
assert_eq!(types.len(), 1);
assert_eq!(types[0].params.get("alpha"), Some(1.05));
}
#[test]
fn test_invalid_root() {
let xml = r#"<NotForceField name="test" />"#;
let err = read_forcefield_xml_str(xml).unwrap_err();
assert!(err.contains("Root element must be <ForceField>"));
}
#[test]
fn test_missing_attribute() {
let xml = r#"
<ForceField name="test">
<BondStyle>
<Type name="A-B" k0="1.0" />
</BondStyle>
</ForceField>
"#;
let err = read_forcefield_xml_str(xml).unwrap_err();
assert!(err.contains("missing attribute 'name'"));
}
#[test]
fn test_full_mmff_xml() {
let ff = read_forcefield_xml_str(molrs::data::MMFF94_XML).unwrap();
assert_eq!(ff.name, "MMFF94");
assert!(!ff.get_bondtypes().is_empty());
assert!(!ff.get_angletypes().is_empty());
assert!(!ff.get_pairtypes().is_empty());
}
#[test]
fn test_mmff_params_xml() {
let params = read_mmff_params_xml_str(molrs::data::MMFF94_XML).unwrap();
assert!(params.get_prop(1).is_some());
}
#[test]
fn test_opls_typing_overrides_and_layer_defaults() {
let xml = r#"<ForceField name="OPLS-AA">
<AtomTypes>
<Type name="opls_135" class="CT" element="C" mass="12.011" def="[C;X4](C)(H)(H)H"/>
<Type name="opls_146" class="HA" element="H" mass="1.008" def="[H][c]" overrides="opls_144, opls_140"/>
<Type name="opls_001" class="opls_001" element="C" mass="12.011"/>
</AtomTypes>
</ForceField>"#;
let meta = read_opls_typing_xml_str(xml).unwrap();
let r135 = meta.get("opls_135").unwrap();
assert_eq!(r135.class, "CT");
assert_eq!(r135.def.as_deref(), Some("[C;X4](C)(H)(H)H"));
assert!(r135.overrides.is_empty());
assert_eq!(r135.layer, 0);
assert_eq!(r135.priority, None);
let r146 = meta.get("opls_146").unwrap();
assert_eq!(
r146.overrides,
vec!["opls_144".to_string(), "opls_140".to_string()]
);
let r001 = meta.get("opls_001").unwrap();
assert_eq!(r001.def, None);
}
#[test]
fn test_opls_typing_explicit_priority_and_layer() {
let xml = r#"<ForceField name="OPLS-AA">
<AtomTypes>
<Type name="opls_x" class="CT" def="[C]" priority="7" layer="2"/>
</AtomTypes>
</ForceField>"#;
let meta = read_opls_typing_xml_str(xml).unwrap();
let r = meta.get("opls_x").unwrap();
assert_eq!(r.priority, Some(7));
assert_eq!(r.layer, 2);
}
#[test]
fn test_opls_typing_missing_class_errors() {
let xml = r#"<ForceField name="OPLS-AA">
<AtomTypes><Type name="opls_135" def="[C]"/></AtomTypes>
</ForceField>"#;
let err = read_opls_typing_xml_str(xml).unwrap_err();
assert!(err.contains("class"), "err: {err}");
}
#[test]
fn test_opls_typing_bad_priority_errors() {
let xml = r#"<ForceField name="OPLS-AA">
<AtomTypes><Type name="opls_135" class="CT" priority="high"/></AtomTypes>
</ForceField>"#;
let err = read_opls_typing_xml_str(xml).unwrap_err();
assert!(err.contains("priority"), "err: {err}");
}
#[test]
fn test_opls_typing_wrong_root_errors() {
let err = read_opls_typing_xml_str(r#"<System name="x"/>"#).unwrap_err();
assert!(err.contains("ForceField"), "err: {err}");
}
}