use molrs::Atomistic;
use crate::ff::forcefield::ForceField;
use super::layered::LayeredTypingEngine;
use super::meta::OplsTypingMeta;
pub fn typify_atoms(
mol: &Atomistic,
meta: &OplsTypingMeta,
ff: &ForceField,
) -> Result<Atomistic, String> {
let engine = LayeredTypingEngine::build(meta)?;
let assignments = engine.typify(mol);
let charge_of = |type_name: &str| -> Option<f64> {
ff.get_style("atom", "full")?
.get_atomtype(type_name)?
.params
.get("charge")
};
let mut out = mol.clone();
for (atom_id, type_name) in &assignments {
out.set_atom(*atom_id, "type", type_name.clone())
.map_err(|e| e.to_string())?;
if let Some(row) = meta.get(type_name) {
out.set_atom(*atom_id, "class", row.class.clone())
.map_err(|e| e.to_string())?;
}
if let Some(q) = charge_of(type_name) {
out.set_atom(*atom_id, "charge", q)
.map_err(|e| e.to_string())?;
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ff::typifier::opls::meta::OplsTypeRow;
use molrs::Atom;
fn ethane() -> Atomistic {
let mut g = Atomistic::new();
let c0 = g.add_atom(Atom::xyz("C", 0.0, 0.0, 0.0));
let c1 = g.add_atom(Atom::xyz("C", 1.5, 0.0, 0.0));
g.add_bond(c0, c1).unwrap();
for c in [c0, c1] {
for k in 0..3 {
let h = g.add_atom(Atom::xyz("H", 0.3 * (k as f64 + 1.0), 0.8, 0.0));
g.add_bond(c, h).unwrap();
}
}
g
}
fn meta_with(rows: &[(&str, OplsTypeRow)]) -> OplsTypingMeta {
let mut m = OplsTypingMeta::new();
for (name, row) in rows {
m.insert(*name, row.clone());
}
m
}
fn row(class: &str, def: Option<&str>, overrides: &[&str]) -> OplsTypeRow {
OplsTypeRow {
class: class.to_string(),
def: def.map(str::to_string),
overrides: overrides.iter().map(|s| s.to_string()).collect(),
priority: None,
layer: 0,
}
}
#[test]
fn typing_assigns_carbon_and_hydrogen() {
let m = meta_with(&[
("opls_135", row("CT", Some("[C;X4](C)(H)(H)H"), &[])),
("opls_140", row("HC", Some("H[C;X4]"), &[])),
]);
let ff = ForceField::new("OPLS-AA");
let typed = typify_atoms(ðane(), &m, &ff).unwrap();
let mut n_ct = 0;
let mut n_hc = 0;
for (_, a) in typed.atoms() {
match a.get_str("type") {
Some("opls_135") => {
n_ct += 1;
assert_eq!(a.get_str("class"), Some("CT"));
}
Some("opls_140") => n_hc += 1,
_ => {}
}
}
assert_eq!(n_ct, 2, "both methyl carbons typed CT");
assert_eq!(n_hc, 6, "all six H typed HC");
}
#[test]
fn higher_priority_overrides_wins() {
let m = meta_with(&[
("opls_generic", row("CG", Some("[C]"), &[])),
("opls_special", row("CS", Some("[C;X4]"), &["opls_generic"])),
]);
let ff = ForceField::new("OPLS-AA");
let typed = typify_atoms(ðane(), &m, &ff).unwrap();
for (id, a) in typed.atoms() {
if matches!(a.get_str("element"), Some("C")) {
assert_eq!(
a.get_str("type"),
Some("opls_special"),
"carbon {id:?} should take the higher-priority override"
);
}
}
}
#[test]
fn charge_written_from_forcefield() {
let m = meta_with(&[("opls_140", row("HC", Some("H[C;X4]"), &[]))]);
let mut ff = ForceField::new("OPLS-AA");
ff.def_atomstyle("full")
.def_atomtype("opls_140", &[("mass", 1.008), ("charge", 0.06)]);
let typed = typify_atoms(ðane(), &m, &ff).unwrap();
let h = typed
.atoms()
.find(|(_, a)| a.get_str("type") == Some("opls_140"))
.expect("a hydrogen was typed");
assert_eq!(h.1.get_f64("charge"), Some(0.06));
}
#[test]
fn malformed_def_fails_fast() {
let m = meta_with(&[("opls_bad", row("X", Some("[C"), &[]))]);
let ff = ForceField::new("OPLS-AA");
let err = typify_atoms(ðane(), &m, &ff).unwrap_err();
assert!(err.contains("opls_bad"), "err names the type: {err}");
}
#[test]
fn recursive_dollar_def_matches() {
let m = meta_with(&[("opls_rec", row("CT", Some("[$([CX4][CX4])]"), &[]))]);
let ff = ForceField::new("OPLS-AA");
let typed = typify_atoms(ðane(), &m, &ff).unwrap();
let n = typed
.atoms()
.filter(|(_, a)| a.get_str("type") == Some("opls_rec"))
.count();
assert_eq!(n, 2, "recursive def should match both sp3 carbons");
}
#[test]
fn typed_atom_ref_def_without_dependency_types_nothing() {
let m = meta_with(&[("opls_ref", row("HA", Some("[H][C;%opls_145]"), &[]))]);
let ff = ForceField::new("OPLS-AA");
let typed = typify_atoms(ðane(), &m, &ff).unwrap();
assert!(typed.atoms().all(|(_, a)| a.get_str("type").is_none()));
}
#[test]
fn layered_dependency_def_types_after_its_dependency() {
let mut g = Atomistic::new();
let cm = g.add_atom(Atom::xyz("C", 0.0, 0.0, 0.0));
let ch = g.add_atom(Atom::xyz("C", 1.5, 0.0, 0.0));
let o = g.add_atom(Atom::xyz("O", 2.5, 0.0, 0.0));
let ho = g.add_atom(Atom::xyz("H", 3.3, 0.0, 0.0));
g.add_bond(cm, ch).unwrap();
g.add_bond(ch, o).unwrap();
g.add_bond(o, ho).unwrap();
for k in 0..3 {
let h = g.add_atom(Atom::xyz("H", 0.3 * (k as f64 + 1.0), 0.9, 0.0));
g.add_bond(cm, h).unwrap();
}
for k in 0..2 {
let h = g.add_atom(Atom::xyz("H", 1.5 + 0.3 * (k as f64), 0.9, 0.0));
g.add_bond(ch, h).unwrap();
}
let m = meta_with(&[
("opls_154", row("OH", Some("[O;X2](H)([!H])"), &[])),
("opls_155", row("HO", Some("H[O;%opls_154]"), &[])),
]);
let ff = ForceField::new("OPLS-AA");
let typed = typify_atoms(&g, &m, &ff).unwrap();
assert_eq!(
typed.get_atom(o).unwrap().get_str("type"),
Some("opls_154"),
"alcohol O typed opls_154"
);
assert_eq!(
typed.get_atom(ho).unwrap().get_str("type"),
Some("opls_155"),
"hydroxyl H typed opls_155 via the %opls_154 dependency"
);
}
}