use crate::prelude::*;
use bse::lut::{element_Z_from_sym, element_sym_from_Z_with_normalize};
use serde::{Deserialize, Serialize};
pub const BOHR_TO_ANG: f64 = 0.529177210544;
pub const ANG_TO_BOHR: f64 = 1.0 / BOHR_TO_ANG;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Unit {
#[default]
#[serde(alias = "ANG", alias = "Angstrom", alias = "ang", alias = "angstrom")]
Angstrom,
#[serde(alias = "AU", alias = "A.U.", alias = "au", alias = "a.u.", alias = "BOHR", alias = "Bohr", alias = "bohr")]
Bohr,
}
impl From<&str> for Unit {
fn from(s: &str) -> Self {
match s.to_uppercase().as_str() {
"ANG" | "ANGSTROM" => Unit::Angstrom,
"BOHR" | "AU" | "A.U." => Unit::Bohr,
_ => Unit::Angstrom, }
}
}
impl From<String> for Unit {
fn from(s: String) -> Self {
Unit::from(s.as_str())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AtomInfo {
pub raw: String,
pub label: String,
pub identifier: String,
pub symbol: String,
pub charge: i32,
pub is_ghost: bool,
pub coords: [f64; 3],
}
impl AtomInfo {
pub fn new(raw: &str, coords: [f64; 3], unit: Unit) -> Result<Self, CIntError> {
let (label, identifier, symbol, is_ghost) = parse_atom_symbol(raw)?;
let charge = if is_ghost { 0 } else { symbol_to_charge(&symbol)? };
let coords_bohr =
if unit == Unit::Angstrom { [coords[0] * ANG_TO_BOHR, coords[1] * ANG_TO_BOHR, coords[2] * ANG_TO_BOHR] } else { coords };
Ok(AtomInfo { raw: raw.to_string(), label, identifier, symbol, charge, is_ghost, coords: coords_bohr })
}
}
pub fn parse_atom_symbol(raw: &str) -> Result<(String, String, String, bool), CIntError> {
let label = raw.trim().to_ascii_uppercase();
if let Ok(z) = label.parse::<i32>() {
let sym = charge_to_symbol(z)?;
return Ok((sym.clone(), sym.clone(), sym, false));
}
let (identifier, is_ghost) = if let Some(stripped) = label.strip_prefix("GHOST") {
(stripped.trim_start_matches(|c: char| !c.is_alphanumeric()), true)
} else if let Some(stripped) = label.strip_prefix("X") {
(stripped.trim_start_matches(|c: char| !c.is_alphanumeric()), true)
} else {
(label.as_str(), false)
};
let symbol = identifier.trim_end_matches(|c: char| !c.is_alphabetic()).to_string();
let _ = symbol_to_charge(&symbol)?;
Ok((label.to_string(), identifier.to_string(), symbol, is_ghost))
}
pub fn symbol_to_charge(symbol: &str) -> Result<i32, CIntError> {
element_Z_from_sym(symbol).ok_or_else(|| cint_error!(ParseError, "Unknown element: {symbol}"))
}
fn charge_to_symbol(z: i32) -> Result<String, CIntError> {
element_sym_from_Z_with_normalize(z).ok_or_else(|| cint_error!(ParseError, "Invalid atomic number: {z}"))
}
pub fn parse_atom_string(s: &str, unit: Unit) -> Result<Vec<AtomInfo>, CIntError> {
let s = s.replace(';', "\n").replace(',', " ");
let atoms: Vec<AtomInfo> = s
.lines()
.filter(|line| {
let trimmed = line.trim();
!trimmed.is_empty() && !trimmed.starts_with('#')
})
.map(|line| parse_atom_line(line.trim(), unit))
.collect::<Result<Vec<AtomInfo>, CIntError>>()?;
Ok(atoms)
}
fn parse_atom_line(line: &str, default_unit: Unit) -> Result<AtomInfo, CIntError> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 4 {
return cint_raise!(ParseError, "Invalid atom line: '{line}'. Expected at least 4 parts.");
}
let raw = parts[0];
let x: f64 = parse_float(parts[1], "x coordinate")?;
let y: f64 = parse_float(parts[2], "y coordinate")?;
let z: f64 = parse_float(parts[3], "z coordinate")?;
let unit = if parts.len() > 4 {
match parts[4].to_uppercase().as_str() {
"ANG" | "ANGSTROM" => Unit::Angstrom,
"BOHR" | "AU" | "A.U." => Unit::Bohr,
_ => default_unit,
}
} else {
default_unit
};
AtomInfo::new(raw, [x, y, z], unit)
}
pub fn parse_zmatrix(s: &str, unit: Unit) -> Result<Vec<AtomInfo>, CIntError> {
let (entries, labels) = parse_zmatrix_to_entries(s)?;
if entries.is_empty() {
return Ok(vec![]);
}
let coords = zmat_entries_to_cart(&entries)?;
let atoms: Vec<AtomInfo> = labels
.iter()
.zip(coords.iter())
.map(|(label, coord)| AtomInfo::new(label, *coord, unit))
.collect::<Result<Vec<AtomInfo>, CIntError>>()?;
Ok(atoms)
}
fn parse_zmatrix_to_entries(s: &str) -> Result<(Vec<ZmatEntry>, Vec<String>), CIntError> {
let s = s.replace(';', "\n");
let lines: Vec<&str> = s
.lines()
.filter(|line| {
let trimmed = line.trim();
!trimmed.is_empty() && !trimmed.starts_with('#')
})
.collect();
if lines.is_empty() {
return Ok((vec![], vec![]));
}
let mut entries: Vec<ZmatEntry> = vec![];
let mut labels: Vec<String> = vec![];
for (idx, line) in lines.iter().enumerate() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
continue;
}
let label = parts[0].to_string();
labels.push(label.clone());
let entry = match idx {
0 => {
if parts.len() > 1 {
eprintln!("Warning: Extra parameters in z-matrix line '{line}' will be ignored for first atom.");
}
ZmatEntry::first()
},
1 => {
match parts.len() {
..3 => return cint_raise!(ParseError, "Invalid z-matrix line: '{line}'. Need bond length."),
3 => (),
4.. => eprintln!("Warning: Extra parameters in z-matrix line '{line}' will be ignored for second atom."),
};
let bond_ref = find_atom_index(&labels[..idx], parts[1])?;
let bond: f64 = parse_float(parts[2], "bond length")?;
ZmatEntry { bond_ref, bond, angle_ref: 0, angle: 0.0, dihedral_ref: 0, dihedral: 0.0 }
},
2 => {
match parts.len() {
..5 => return cint_raise!(ParseError, "Invalid z-matrix line: '{line}'. Need bond and angle."),
5 => (),
6.. => eprintln!("Warning: Extra parameters in z-matrix line '{line}' will be ignored for third atom."),
};
let bond_ref = find_atom_index(&labels[..idx], parts[1])?;
let bond: f64 = parse_float(parts[2], "bond length")?;
let angle_ref = find_atom_index(&labels[..idx], parts[3])?;
let angle: f64 = parse_float(parts[4], "angle")?;
ZmatEntry { bond_ref, bond, angle_ref, angle, dihedral_ref: 0, dihedral: 0.0 }
},
_ => {
match parts.len() {
..7 => return cint_raise!(ParseError, "Invalid z-matrix line: '{line}'. Need bond, angle, and dihedral."),
7 => (),
8.. => eprintln!("Warning: Extra parameters in z-matrix line '{line}' will be ignored for fourth+ atom."),
};
let bond_ref = find_atom_index(&labels[..idx], parts[1])?;
let bond: f64 = parse_float(parts[2], "bond length")?;
let angle_ref = find_atom_index(&labels[..idx], parts[3])?;
let angle: f64 = parse_float(parts[4], "angle")?;
let dihedral_ref = find_atom_index(&labels[..idx], parts[5])?;
let dihedral: f64 = parse_float(parts[6], "dihedral")?;
ZmatEntry { bond_ref, bond, angle_ref, angle, dihedral_ref, dihedral }
},
};
entries.push(entry);
}
Ok((entries, labels))
}
fn zmat_entries_to_cart(entries: &[ZmatEntry]) -> Result<Vec<[f64; 3]>, CIntError> {
const TOL: f64 = 1e-7;
if entries.is_empty() {
return Ok(vec![]);
}
let mut coords = vec![[0.0, 0.0, 0.0]];
for (i, entry) in entries.iter().enumerate().skip(1) {
let bond = entry.bond;
let coord = if i == 1 {
let ref_pos = coords[entry.bond_ref];
[ref_pos[0] + bond, ref_pos[1], ref_pos[2]]
} else if i == 2 {
let bonda_pos = coords[entry.bond_ref];
let anga_pos = coords[entry.angle_ref];
let angle_rad = entry.angle * std::f64::consts::PI / 180.0;
let v1 = [anga_pos[0] - bonda_pos[0], anga_pos[1] - bonda_pos[1], anga_pos[2] - bonda_pos[2]];
let v1_norm = norm3(v1);
if v1_norm < TOL {
return cint_raise!(ParseError, "Reference atoms for z-matrix are too close");
}
let vecn = if !((v1[0].abs() < TOL) && (v1[1].abs() < TOL)) { cross3(v1, [0.0, 0.0, 1.0]) } else { [0.0, 0.0, 1.0] };
let rmat = rotation_mat(vecn, angle_rad);
let c = mat_vec_mul(&rmat, v1);
let scale = bond / v1_norm;
[bonda_pos[0] + c[0] * scale, bonda_pos[1] + c[1] * scale, bonda_pos[2] + c[2] * scale]
} else {
let r1_pos = coords[entry.bond_ref];
let a1_pos = coords[entry.angle_ref];
let d1_pos = coords[entry.dihedral_ref];
let angle_rad = entry.angle * std::f64::consts::PI / 180.0;
let dihedral_rad = entry.dihedral * std::f64::consts::PI / 180.0;
zmatrix_to_cartesian(r1_pos, a1_pos, d1_pos, bond, angle_rad, dihedral_rad)?
};
coords.push(coord);
}
Ok(coords)
}
fn parse_float(s: &str, context: &str) -> Result<f64, CIntError> {
s.parse().map_err(|e| cint_error!(ParseError, "Failed to parse {context} '{s}': {e}"))
}
fn find_atom_index(order: &[String], name: &str) -> Result<usize, CIntError> {
for (i, atom_name) in order.iter().enumerate() {
if atom_name == name {
return Ok(i);
}
}
if let Ok(idx) = name.parse::<usize>() {
if idx > 0 && idx <= order.len() {
return Ok(idx - 1);
}
}
cint_raise!(ParseError, "Atom '{name}' not found in z-matrix")
}
fn zmatrix_to_cartesian(
r1: [f64; 3], a1: [f64; 3], d1: [f64; 3], bond: f64,
angle: f64,
dihedral: f64,
) -> Result<[f64; 3], CIntError> {
const TOL: f64 = 1e-7;
let v1 = [a1[0] - r1[0], a1[1] - r1[1], a1[2] - r1[2]];
let v1_norm = norm3(v1);
if v1_norm < TOL {
return cint_raise!(ParseError, "Reference atoms for z-matrix ({r1:?}, {a1:?}) are too close");
}
let v1_unit = [v1[0] / v1_norm, v1[1] / v1_norm, v1[2] / v1_norm];
if angle < TOL {
return Ok([r1[0] + bond * v1_unit[0], r1[1] + bond * v1_unit[1], r1[2] + bond * v1_unit[2]]);
}
if std::f64::consts::PI - angle < TOL {
return Ok([r1[0] - bond * v1_unit[0], r1[1] - bond * v1_unit[1], r1[2] - bond * v1_unit[2]]);
}
let v2 = [d1[0] - a1[0], d1[1] - a1[1], d1[2] - a1[2]];
let neg_v1 = [-v1_unit[0], -v1_unit[1], -v1_unit[2]];
let vecn = cross3(v2, neg_v1);
let vecn_norm = norm3(vecn);
if vecn_norm < TOL {
let vecn_alt = if !((v1_unit[0].abs() < TOL) && (v1_unit[1].abs() < TOL)) {
cross3(v1_unit, [0.0, 0.0, 1.0])
} else {
cross3(v1_unit, [1.0, 0.0, 0.0])
};
let vecn_alt_norm = norm3(vecn_alt);
if vecn_alt_norm < TOL {
return cint_raise!(ParseError, "Cannot determine rotation axis for z-matrix");
}
let vecn_unit = [vecn_alt[0] / vecn_alt_norm, vecn_alt[1] / vecn_alt_norm, vecn_alt[2] / vecn_alt_norm];
let rmat = rotation_mat(vecn_unit, angle);
let c = mat_vec_mul(&rmat, v1_unit);
Ok([r1[0] + bond * c[0], r1[1] + bond * c[1], r1[2] + bond * c[2]])
} else {
let vecn_unit = [vecn[0] / vecn_norm, vecn[1] / vecn_norm, vecn[2] / vecn_norm];
let rmat_dih = rotation_mat(v1_unit, -dihedral);
let vecn_rot = mat_vec_mul(&rmat_dih, vecn_unit);
let rmat_ang = rotation_mat(vecn_rot, angle);
let c = mat_vec_mul(&rmat_ang, v1_unit);
Ok([r1[0] + bond * c[0], r1[1] + bond * c[1], r1[2] + bond * c[2]])
}
}
fn rotation_mat(vec: [f64; 3], theta: f64) -> [[f64; 3]; 3] {
let norm = norm3(vec);
let k = [vec[0] / norm, vec[1] / norm, vec[2] / norm];
let c = theta.cos();
let s = theta.sin();
let uu = [[k[0] * k[0], k[0] * k[1], k[0] * k[2]], [k[1] * k[0], k[1] * k[1], k[1] * k[2]], [k[2] * k[0], k[2] * k[1], k[2] * k[2]]];
let ux = [[0.0, -k[2], k[1]], [k[2], 0.0, -k[0]], [-k[1], k[0], 0.0]];
[
[c + (1.0 - c) * uu[0][0] + s * ux[0][0], (1.0 - c) * uu[0][1] + s * ux[0][1], (1.0 - c) * uu[0][2] + s * ux[0][2]],
[(1.0 - c) * uu[1][0] + s * ux[1][0], c + (1.0 - c) * uu[1][1] + s * ux[1][1], (1.0 - c) * uu[1][2] + s * ux[1][2]],
[(1.0 - c) * uu[2][0] + s * ux[2][0], (1.0 - c) * uu[2][1] + s * ux[2][1], c + (1.0 - c) * uu[2][2] + s * ux[2][2]],
]
}
fn mat_vec_mul(m: &[[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
[
m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
]
}
fn norm3(v: [f64; 3]) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
}
fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[derive(Debug, Clone, PartialEq)]
pub struct ZmatEntry {
pub bond_ref: usize,
pub bond: f64,
pub angle_ref: usize,
pub angle: f64,
pub dihedral_ref: usize,
pub dihedral: f64,
}
impl ZmatEntry {
pub fn first() -> Self {
ZmatEntry { bond_ref: 0, bond: 0.0, angle_ref: 0, angle: 0.0, dihedral_ref: 0, dihedral: 0.0 }
}
pub fn second(bond: f64) -> Self {
ZmatEntry { bond_ref: 0, bond, angle_ref: 0, angle: 0.0, dihedral_ref: 0, dihedral: 0.0 }
}
pub fn third(bond: f64, angle: f64) -> Self {
ZmatEntry { bond_ref: 0, bond, angle_ref: 1, angle, dihedral_ref: 0, dihedral: 0.0 }
}
pub fn full(bond_ref: usize, bond: f64, angle_ref: usize, angle: f64, dihedral_ref: usize, dihedral: f64) -> Self {
ZmatEntry { bond_ref, bond, angle_ref, angle, dihedral_ref, dihedral }
}
}
pub fn cart2zmat(coords: &[[f64; 3]]) -> Vec<ZmatEntry> {
const TOL: f64 = 1e-7;
if coords.is_empty() {
return vec![];
}
let mut zmat = vec![ZmatEntry::first()];
if coords.len() > 1 {
let r1 = [coords[1][0] - coords[0][0], coords[1][1] - coords[0][1], coords[1][2] - coords[0][2]];
let bond = norm3(r1);
zmat.push(ZmatEntry::second(bond));
}
if coords.len() > 2 {
let r1 = [coords[1][0] - coords[0][0], coords[1][1] - coords[0][1], coords[1][2] - coords[0][2]];
let nr1 = norm3(r1);
let r2 = [coords[2][0] - coords[0][0], coords[2][1] - coords[0][1], coords[2][2] - coords[0][2]];
let nr2 = norm3(r2);
let cos_a = dot3(r1, r2) / (nr1 * nr2);
let angle = cos_a.acos() * 180.0 / std::f64::consts::PI;
zmat.push(ZmatEntry::third(nr2, angle));
}
if coords.len() > 3 {
let o0 = coords[0];
let o1 = coords[1];
let mut o2 = coords[2];
let mut p2: usize = 2;
for (k, c) in coords.iter().enumerate().skip(3) {
let r0 = [c[0] - o0[0], c[1] - o0[1], c[2] - o0[2]];
let nr0 = norm3(r0);
let r1 = [o1[0] - o0[0], o1[1] - o0[1], o1[2] - o0[2]];
let nr1 = norm3(r1);
let cos_a1 = dot3(r0, r1) / (nr0 * nr1);
let a1 = cos_a1.acos() * 180.0 / std::f64::consts::PI;
let b0 = cross3(r0, r1);
let nb0 = norm3(b0);
if nb0 < TOL {
zmat.push(ZmatEntry::full(0, nr0, 1, a1, p2, 0.0));
} else {
let b1 = cross3([o2[0] - o0[0], o2[1] - o0[1], o2[2] - o0[2]], r1);
let nb1 = norm3(b1);
if nb1 < TOL {
zmat.push(ZmatEntry::full(0, nr0, 1, a1, p2, 0.0));
o2 = *c;
p2 = 3 + k; } else {
let cos_a2 = dot3(b1, b0) / (nb0 * nb1);
let cross_b = cross3(b1, b0);
let sign = dot3(cross_b, r1);
let a2 = if sign < 0.0 {
cos_a2.acos() * 180.0 / std::f64::consts::PI
} else {
-cos_a2.acos() * 180.0 / std::f64::consts::PI
};
zmat.push(ZmatEntry::full(0, nr0, 1, a1, p2, a2));
}
}
}
}
zmat
}
pub fn zmat2cart(zmat: &[ZmatEntry], unit: Unit) -> Result<Vec<[f64; 3]>, CIntError> {
if zmat.is_empty() {
return Ok(vec![]);
}
let mut coords = vec![[0.0, 0.0, 0.0]];
for (i, entry) in zmat.iter().enumerate() {
if i == 0 {
continue; }
let bond_bohr = if unit == Unit::Angstrom { entry.bond * ANG_TO_BOHR } else { entry.bond };
let coord = match i {
1 => {
[bond_bohr, 0.0, 0.0]
},
2 => {
let bonda_pos = coords[entry.bond_ref];
let anga_pos = coords[entry.angle_ref];
let angle_rad = entry.angle * std::f64::consts::PI / 180.0;
let v1 = [anga_pos[0] - bonda_pos[0], anga_pos[1] - bonda_pos[1], anga_pos[2] - bonda_pos[2]];
let v1_norm = norm3(v1);
if v1_norm < 1e-7 {
return cint_raise!(ParseError, "Reference atoms for z-matrix are too close");
}
let vecn = if !((v1[0].abs() < 1e-7) && (v1[1].abs() < 1e-7)) { cross3(v1, [0.0, 0.0, 1.0]) } else { [0.0, 0.0, 1.0] };
let rmat = rotation_mat(vecn, angle_rad);
let c = mat_vec_mul(&rmat, v1);
let scale = bond_bohr / v1_norm;
[bonda_pos[0] + c[0] * scale, bonda_pos[1] + c[1] * scale, bonda_pos[2] + c[2] * scale]
},
_ => {
let r1_idx = entry.bond_ref;
let a1_idx = entry.angle_ref;
let d1_idx = entry.dihedral_ref;
if r1_idx >= coords.len() || a1_idx >= coords.len() || d1_idx >= coords.len() {
return cint_raise!(ParseError, "Invalid reference index in z-matrix");
}
let angle_rad = entry.angle * std::f64::consts::PI / 180.0;
let dihedral_rad = entry.dihedral * std::f64::consts::PI / 180.0;
zmatrix_to_cartesian(coords[r1_idx], coords[a1_idx], coords[d1_idx], bond_bohr, angle_rad, dihedral_rad)?
},
};
coords.push(coord);
}
Ok(coords)
}
#[cfg(test)]
mod tests_llm_assist {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_parse_atom_symbol() {
assert_eq!(parse_atom_symbol("H").unwrap(), ("H".to_string(), "H".to_string(), "H".to_string(), false));
assert_eq!(parse_atom_symbol("H1").unwrap(), ("H1".to_string(), "H1".to_string(), "H".to_string(), false));
assert_eq!(parse_atom_symbol("O@2").unwrap(), ("O@2".to_string(), "O@2".to_string(), "O".to_string(), false));
assert_eq!(parse_atom_symbol("GHOST-H").unwrap(), ("GHOST-H".to_string(), "H".to_string(), "H".to_string(), true));
assert_eq!(parse_atom_symbol("GHOST_O").unwrap(), ("GHOST_O".to_string(), "O".to_string(), "O".to_string(), true));
assert_eq!(parse_atom_symbol("X-H").unwrap(), ("X-H".to_string(), "H".to_string(), "H".to_string(), true));
assert_eq!(parse_atom_symbol("X_O").unwrap(), ("X_O".to_string(), "O".to_string(), "O".to_string(), true));
assert_eq!(parse_atom_symbol("H@").unwrap(), ("H@".to_string(), "H@".to_string(), "H".to_string(), false));
assert_eq!(parse_atom_symbol("Au1").unwrap(), ("AU1".to_string(), "AU1".to_string(), "AU".to_string(), false));
}
#[test]
fn test_element_charge() {
assert_eq!(symbol_to_charge("H").unwrap(), 1);
assert_eq!(symbol_to_charge("O").unwrap(), 8);
assert_eq!(symbol_to_charge("Au").unwrap(), 79);
assert!(symbol_to_charge("X").is_err());
}
#[test]
fn test_parse_atom_string() {
let atoms = parse_atom_string("H 0 0 0; O 0 0 1.2", Unit::Angstrom).unwrap();
assert_eq!(atoms.len(), 2);
assert_eq!(atoms[0].symbol, "H");
assert_eq!(atoms[0].charge, 1);
assert_relative_eq!(atoms[0].coords[2], 0.0);
assert_eq!(atoms[1].symbol, "O");
assert_eq!(atoms[1].charge, 8);
assert_relative_eq!(atoms[1].coords[2], 1.2 * ANG_TO_BOHR);
}
#[test]
fn test_parse_atom_string_ghost() {
let atoms = parse_atom_string("H 0 0 0; GHOST-O 0 0 1.2", Unit::Angstrom).unwrap();
assert_eq!(atoms.len(), 2);
assert!(!atoms[0].is_ghost);
assert!(atoms[1].is_ghost);
assert_eq!(atoms[1].charge, 0);
}
#[test]
fn test_parse_atom_ghost_formats() {
let atoms1 = parse_atom_string("GHOST-O 0 0 1.2", Unit::Angstrom).unwrap();
assert_eq!(atoms1.len(), 1);
assert!(atoms1[0].is_ghost);
assert_eq!(atoms1[0].charge, 0);
let atoms2 = parse_atom_string("GHOST-O 0 0 1.2; O 0 0 0", Unit::Angstrom).unwrap();
assert_eq!(atoms2.len(), 2);
assert!(atoms2[0].is_ghost);
assert!(!atoms2[1].is_ghost);
assert_eq!(atoms2[0].charge, 0);
assert_eq!(atoms2[1].charge, 8);
let atoms3 = parse_atom_string("X_H 0 0 1; H 0 0 0", Unit::Angstrom).unwrap();
assert_eq!(atoms3.len(), 2);
assert!(atoms3[0].is_ghost);
assert!(!atoms3[1].is_ghost);
assert_eq!(atoms3[0].charge, 0);
assert_eq!(atoms3[1].charge, 1);
let atoms4 = parse_atom_string("ghost.H 0 0 1; H 0 0 0", Unit::Angstrom).unwrap();
assert_eq!(atoms4.len(), 2);
assert!(atoms4[0].is_ghost);
assert!(!atoms4[1].is_ghost);
assert_eq!(atoms4[0].charge, 0);
assert_eq!(atoms4[1].charge, 1);
let atoms5 = parse_atom_string("GHOST-O 0 0 0; X_H 0 0 1; ghost.H 0 0 2; O 0 0 3; H 0 0 4; H 0 0 5", Unit::Angstrom).unwrap();
assert_eq!(atoms5.len(), 6);
assert!(atoms5[0].is_ghost); assert!(atoms5[1].is_ghost); assert!(atoms5[2].is_ghost); assert!(!atoms5[3].is_ghost); assert!(!atoms5[4].is_ghost); assert!(!atoms5[5].is_ghost); }
#[test]
fn test_parse_atom_ghost_dot_format() {
let atoms = parse_atom_string("ghost.H 0 0 1", Unit::Angstrom).unwrap();
assert_eq!(atoms.len(), 1);
assert!(atoms[0].is_ghost);
assert_eq!(atoms[0].charge, 0);
assert_eq!(atoms[0].symbol, "H");
}
#[test]
fn test_parse_atom_string_labeled() {
let atoms = parse_atom_string("H1 0 0 0; H2 0 0 1", Unit::Bohr).unwrap();
assert_eq!(atoms.len(), 2);
assert_eq!(atoms[0].label, "H1");
assert_eq!(atoms[0].symbol, "H");
assert_eq!(atoms[1].label, "H2");
assert_eq!(atoms[1].symbol, "H");
}
#[test]
fn test_parse_zmatrix_water() {
let atoms = parse_zmatrix("O\nH 1 0.94\nH 1 0.94 2 104.5", Unit::Angstrom).unwrap();
assert_eq!(atoms.len(), 3);
assert_eq!(atoms[0].symbol, "O");
assert_relative_eq!(atoms[1].coords[0], 0.94 * ANG_TO_BOHR, epsilon = 1e-6);
assert_relative_eq!(atoms[1].coords[1], 0.0, epsilon = 1e-6);
assert_relative_eq!(atoms[1].coords[2], 0.0, epsilon = 1e-6);
let bond_bohr = 0.94 * ANG_TO_BOHR;
let angle_rad = 104.5 * std::f64::consts::PI / 180.0;
let expected_x = bond_bohr * angle_rad.cos(); let expected_z = bond_bohr * angle_rad.sin(); assert_relative_eq!(atoms[2].coords[0], expected_x, epsilon = 1e-6);
assert_relative_eq!(atoms[2].coords[1], 0.0, epsilon = 1e-6);
assert_relative_eq!(atoms[2].coords[2], expected_z, epsilon = 1e-6);
}
#[test]
fn test_charge_to_symbol() {
assert_eq!(charge_to_symbol(1).unwrap(), "H");
assert_eq!(charge_to_symbol(8).unwrap(), "O");
assert_eq!(charge_to_symbol(79).unwrap(), "Au");
assert!(charge_to_symbol(0).is_err());
}
#[test]
fn test_unit_conversion() {
let coords_ang = [1.0, 2.0, 3.0];
let atom = AtomInfo::new("H", coords_ang, Unit::Angstrom).unwrap();
assert_relative_eq!(atom.coords[0], coords_ang[0] * ANG_TO_BOHR);
assert_relative_eq!(atom.coords[1], coords_ang[1] * ANG_TO_BOHR);
assert_relative_eq!(atom.coords[2], coords_ang[2] * ANG_TO_BOHR);
let atom_bohr = AtomInfo::new("H", coords_ang, Unit::Bohr).unwrap();
assert_relative_eq!(atom_bohr.coords[0], 1.0);
}
}
#[cfg(test)]
mod test_zmat {
use approx::assert_relative_eq;
use super::*;
#[test]
fn test_parse_zmatrix() {
let token = r#"
H
H 1 2.67247631453057
H 1 4.22555607338457 2 50.7684795164077
H 1 2.90305235726773 2 79.3904651036893 3 6.20854462618583
"#;
let atoms = parse_zmatrix(token, Unit::Bohr).unwrap();
let expected_coords =
[[0.0, 0.0, 0.0], [2.67247631, 0.0, 0.0], [2.67247631, 0.0, 3.27310166], [0.53449526, 0.30859098, 2.83668811]];
for (i, atom) in atoms.iter().enumerate() {
println!("Atom {}: symbol={}, coords={:?}", i + 1, atom.symbol, atom.coords);
assert_relative_eq!(atom.coords[0], expected_coords[i][0], epsilon = 1e-6);
assert_relative_eq!(atom.coords[1], expected_coords[i][1], epsilon = 1e-6);
assert_relative_eq!(atom.coords[2], expected_coords[i][2], epsilon = 1e-6);
}
}
#[test]
fn test_parse_zmatrix_collinear() {
let token = "H\nH 1 1.0\nH 1 2.0 2 0.0\nH 1 1.5 2 90.0 3 45.0";
let atoms = parse_zmatrix(token, Unit::Angstrom).unwrap();
let expected_coords_angstrom = [
[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0], [0.0, 0.0, 1.5], ];
for (i, atom) in atoms.iter().enumerate() {
println!("Atom {}: symbol={}, coords={:?}", i + 1, atom.symbol, atom.coords);
assert_relative_eq!(atom.coords[0], expected_coords_angstrom[i][0] * ANG_TO_BOHR, epsilon = 1e-6);
assert_relative_eq!(atom.coords[1], expected_coords_angstrom[i][1] * ANG_TO_BOHR, epsilon = 1e-6);
assert_relative_eq!(atom.coords[2], expected_coords_angstrom[i][2] * ANG_TO_BOHR, epsilon = 1e-6);
}
}
#[test]
fn test_parse_zmatrix_linear_molecule() {
let token = "C\nO 1 1.16\nO 1 1.16 2 180.0";
let atoms = parse_zmatrix(token, Unit::Angstrom).unwrap();
let expected_coords_angstrom = [
[0.0, 0.0, 0.0], [1.16, 0.0, 0.0], [-1.16, 0.0, 0.0], ];
for (i, atom) in atoms.iter().enumerate() {
println!("Atom {}: symbol={}, coords={:?}", i + 1, atom.symbol, atom.coords);
assert_relative_eq!(atom.coords[0], expected_coords_angstrom[i][0] * ANG_TO_BOHR, epsilon = 1e-6);
assert_relative_eq!(atom.coords[1], expected_coords_angstrom[i][1] * ANG_TO_BOHR, epsilon = 1e-6);
assert_relative_eq!(atom.coords[2], expected_coords_angstrom[i][2] * ANG_TO_BOHR, epsilon = 1e-6);
}
}
#[test]
fn test_parse_zmatrix_angle_zero() {
let token = "H\nH 1 1.0\nH 1 2.0 2 0.0\nH 1 3.0 2 0.0 3 0.0";
let atoms = parse_zmatrix(token, Unit::Angstrom).unwrap();
let expected_coords_angstrom = [
[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0], [3.0, 0.0, 0.0], ];
for (i, atom) in atoms.iter().enumerate() {
println!("Atom {}: symbol={}, coords={:?}", i + 1, atom.symbol, atom.coords);
assert_relative_eq!(atom.coords[0], expected_coords_angstrom[i][0] * ANG_TO_BOHR, epsilon = 1e-6);
assert_relative_eq!(atom.coords[1], expected_coords_angstrom[i][1] * ANG_TO_BOHR, epsilon = 1e-6);
assert_relative_eq!(atom.coords[2], expected_coords_angstrom[i][2] * ANG_TO_BOHR, epsilon = 1e-6);
}
}
#[test]
fn test_parse_zmatrix_angle_pi() {
let token = "H\nH 1 1.0\nH 1 1.0 2 180.0";
let atoms = parse_zmatrix(token, Unit::Angstrom).unwrap();
let expected_coords_angstrom = [
[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], ];
for (i, atom) in atoms.iter().enumerate() {
println!("Atom {}: symbol={}, coords={:?}", i + 1, atom.symbol, atom.coords);
assert_relative_eq!(atom.coords[0], expected_coords_angstrom[i][0] * ANG_TO_BOHR, epsilon = 1e-6);
assert_relative_eq!(atom.coords[1], expected_coords_angstrom[i][1] * ANG_TO_BOHR, epsilon = 1e-6);
assert_relative_eq!(atom.coords[2], expected_coords_angstrom[i][2] * ANG_TO_BOHR, epsilon = 1e-6);
}
}
#[test]
fn test_parse_zmatrix_dihedral_zero() {
let token = "H\nH 1 1.0\nH 1 1.0 2 90.0\nH 1 1.0 2 90.0 3 0.0";
let atoms = parse_zmatrix(token, Unit::Angstrom).unwrap();
let expected_coords_angstrom = [
[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0], ];
for (i, atom) in atoms.iter().enumerate() {
println!("Atom {}: symbol={}, coords={:?}", i + 1, atom.symbol, atom.coords);
assert_relative_eq!(atom.coords[0], expected_coords_angstrom[i][0] * ANG_TO_BOHR, epsilon = 1e-6);
assert_relative_eq!(atom.coords[1], expected_coords_angstrom[i][1] * ANG_TO_BOHR, epsilon = 1e-6);
assert_relative_eq!(atom.coords[2], expected_coords_angstrom[i][2] * ANG_TO_BOHR, epsilon = 1e-6);
}
}
#[test]
fn test_parse_zmatrix_dihedral_180() {
let token = "H\nH 1 1.0\nH 1 1.0 2 90.0\nH 1 1.0 2 90.0 3 180.0";
let atoms = parse_zmatrix(token, Unit::Angstrom).unwrap();
let expected_coords_angstrom = [
[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], ];
for (i, atom) in atoms.iter().enumerate() {
println!("Atom {}: symbol={}, coords={:?}", i + 1, atom.symbol, atom.coords);
assert_relative_eq!(atom.coords[0], expected_coords_angstrom[i][0] * ANG_TO_BOHR, epsilon = 1e-6);
assert_relative_eq!(atom.coords[1], expected_coords_angstrom[i][1] * ANG_TO_BOHR, epsilon = 1e-6);
assert_relative_eq!(atom.coords[2], expected_coords_angstrom[i][2] * ANG_TO_BOHR, epsilon = 1e-6);
}
}
#[test]
fn test_parse_zmatrix_bohr_unit() {
let token = "H\nH 1 1.0\nH 1 1.0 2 90.0";
let atoms = parse_zmatrix(token, Unit::Bohr).unwrap();
let expected_coords_bohr = [
[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0], ];
for (i, atom) in atoms.iter().enumerate() {
println!("Atom {}: symbol={}, coords={:?}", i + 1, atom.symbol, atom.coords);
assert_relative_eq!(atom.coords[0], expected_coords_bohr[i][0], epsilon = 1e-6);
assert_relative_eq!(atom.coords[1], expected_coords_bohr[i][1], epsilon = 1e-6);
assert_relative_eq!(atom.coords[2], expected_coords_bohr[i][2], epsilon = 1e-6);
}
}
#[test]
fn test_cart2zmat_basic() {
let coords: Vec<[f64; 3]> = vec![
[0.0, 1.889726124565, 0.0],
[0.0, 0.0, -1.889726124565],
[1.889726124565, -1.889726124565, 0.0],
[1.889726124565, 0.0, 1.133835674739],
];
let zmat = cart2zmat(&coords);
assert_eq!(zmat.len(), 4);
assert_eq!(zmat[0].bond_ref, 0);
assert_eq!(zmat[1].bond_ref, 0);
assert_relative_eq!(zmat[1].bond, 2.67247631453057, epsilon = 1e-6);
assert_eq!(zmat[2].bond_ref, 0);
assert_relative_eq!(zmat[2].bond, 4.22555607338457, epsilon = 1e-6);
assert_eq!(zmat[2].angle_ref, 1);
assert_relative_eq!(zmat[2].angle, 50.7684795164077, epsilon = 1e-6);
assert_eq!(zmat[3].bond_ref, 0);
assert_relative_eq!(zmat[3].bond, 2.90305235726773, epsilon = 1e-6);
assert_eq!(zmat[3].angle_ref, 1);
assert_relative_eq!(zmat[3].angle, 79.3904651036893, epsilon = 1e-6);
assert_eq!(zmat[3].dihedral_ref, 2);
assert_relative_eq!(zmat[3].dihedral.abs(), 6.20854462618583, epsilon = 1e-5);
}
#[test]
fn test_cart2zmat_roundtrip_non_pyscf_convention() {
let coords: Vec<[f64; 3]> = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0], [0.5, 0.5, 1.0],
];
let zmat = cart2zmat(&coords);
let recovered = zmat2cart(&zmat, Unit::Bohr).unwrap();
println!("Original coords: {:?}", coords);
println!("Recovered coords: {:?}", recovered);
assert_relative_eq!(coords[0][0], recovered[0][0], epsilon = 1e-6);
assert_relative_eq!(coords[0][1], recovered[0][1], epsilon = 1e-6);
assert_relative_eq!(coords[0][2], recovered[0][2], epsilon = 1e-6);
assert_relative_eq!(coords[1][0], recovered[1][0], epsilon = 1e-6);
assert_relative_eq!(coords[1][1], recovered[1][1], epsilon = 1e-6);
assert_relative_eq!(coords[1][2], recovered[1][2], epsilon = 1e-6);
let orig_dist = norm3([coords[2][0] - coords[0][0], coords[2][1] - coords[0][1], coords[2][2] - coords[0][2]]);
let rec_dist = norm3([recovered[2][0] - recovered[0][0], recovered[2][1] - recovered[0][1], recovered[2][2] - recovered[0][2]]);
assert_relative_eq!(orig_dist, rec_dist, epsilon = 1e-6);
assert_relative_eq!(recovered[2][1], 0.0, epsilon = 1e-6);
println!("Zmat entries for this case:");
for (i, entry) in zmat.iter().enumerate() {
println!(
"Atom {}: bond_ref={}, bond={}, angle_ref={}, angle={}, dihedral_ref={}, dihedral={}",
i + 1,
entry.bond_ref,
entry.bond,
entry.angle_ref,
entry.angle,
entry.dihedral_ref,
entry.dihedral
);
}
}
#[test]
fn test_cart2zmat_roundtrip() {
let coords: Vec<[f64; 3]> = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 1.0], [0.5, 0.5, 1.0]];
let zmat = cart2zmat(&coords);
let recovered = zmat2cart(&zmat, Unit::Bohr).unwrap();
for (orig, rec) in coords.iter().zip(recovered.iter()) {
println!("Original: {:?}, Recovered: {:?}", orig, rec);
assert_relative_eq!(orig[0], rec[0], epsilon = 1e-6);
assert_relative_eq!(orig[1], rec[1], epsilon = 1e-6);
assert_relative_eq!(orig[2], rec[2], epsilon = 1e-6);
}
}
#[test]
fn test_cart2zmat_roundtrip_water() {
let coords: Vec<[f64; 3]> = vec![
[0.0, 0.0, 0.0], [0.94 * ANG_TO_BOHR, 0.0, 0.0], [0.94 * ANG_TO_BOHR * 104.5_f64.to_radians().cos(), 0.0, 0.94 * ANG_TO_BOHR * 104.5_f64.to_radians().sin()], ];
let zmat = cart2zmat(&coords);
let recovered = zmat2cart(&zmat, Unit::Bohr).unwrap();
for (orig, rec) in coords.iter().zip(recovered.iter()) {
assert_relative_eq!(orig[0], rec[0], epsilon = 1e-6);
assert_relative_eq!(orig[1], rec[1], epsilon = 1e-6);
assert_relative_eq!(orig[2], rec[2], epsilon = 1e-6);
}
}
#[test]
fn test_cart2zmat_roundtrip_complex() {
let coords: Vec<[f64; 3]> =
vec![[0.0, 0.0, 0.0], [2.67247631, 0.0, 0.0], [2.67247631, 0.0, 3.27310166], [0.53449526, 0.30859098, 2.83668811]];
let zmat = cart2zmat(&coords);
println!("Zmat entries:");
for (i, entry) in zmat.iter().enumerate() {
println!(
"Atom {}: bond_ref={}, bond={}, angle_ref={}, angle={}, dihedral_ref={}, dihedral={}",
i + 1,
entry.bond_ref,
entry.bond,
entry.angle_ref,
entry.angle,
entry.dihedral_ref,
entry.dihedral
);
}
let recovered = zmat2cart(&zmat, Unit::Bohr).unwrap();
for (i, (orig, rec)) in coords.iter().zip(recovered.iter()).enumerate() {
println!("Atom {}: Original {:?}, Recovered {:?}", i + 1, orig, rec);
assert_relative_eq!(orig[0], rec[0], epsilon = 1e-5);
assert_relative_eq!(orig[1], rec[1], epsilon = 1e-5);
assert_relative_eq!(orig[2], rec[2], epsilon = 1e-5);
}
}
#[test]
fn test_cart2zmat_roundtrip_angstrom() {
let coords_bohr: Vec<[f64; 3]> = vec![
[0.0, 0.0, 0.0],
[1.89, 0.0, 0.0], [1.89, 0.0, 1.89], ];
let zmat = cart2zmat(&coords_bohr);
let zmat_ang: Vec<ZmatEntry> = zmat
.iter()
.map(|e| ZmatEntry {
bond_ref: e.bond_ref,
bond: e.bond / ANG_TO_BOHR, angle_ref: e.angle_ref,
angle: e.angle,
dihedral_ref: e.dihedral_ref,
dihedral: e.dihedral,
})
.collect();
let recovered = zmat2cart(&zmat_ang, Unit::Angstrom).unwrap();
for (orig, rec) in coords_bohr.iter().zip(recovered.iter()) {
assert_relative_eq!(orig[0], rec[0], epsilon = 1e-6);
assert_relative_eq!(orig[1], rec[1], epsilon = 1e-6);
assert_relative_eq!(orig[2], rec[2], epsilon = 1e-6);
}
}
#[test]
fn test_cart2zmat_collinear() {
let coords: Vec<[f64; 3]> = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[2.0, 0.0, 0.0], [0.0, 1.0, 0.0], ];
let zmat = cart2zmat(&coords);
assert_eq!(zmat.len(), 4);
assert_relative_eq!(zmat[2].angle, 0.0, epsilon = 1e-6);
println!("Fourth atom zmat: {:?}", zmat[3]);
}
#[test]
fn test_parse_zmatrix_with_zmat_entry() {
let zmat = vec![
ZmatEntry::first(),
ZmatEntry::second(2.67247631453057),
ZmatEntry::third(4.22555607338457, 50.7684795164077),
ZmatEntry::full(0, 2.90305235726773, 1, 79.3904651036893, 2, 6.20854462618583),
];
let coords = zmat2cart(&zmat, Unit::Angstrom).unwrap();
let expected: Vec<[f64; 3]> = vec![
[0.0, 0.0, 0.0],
[2.67247631 * ANG_TO_BOHR, 0.0, 0.0],
[2.67247631 * ANG_TO_BOHR, 0.0, 3.27310166 * ANG_TO_BOHR],
[0.53449526 * ANG_TO_BOHR, 0.30859098 * ANG_TO_BOHR, 2.83668811 * ANG_TO_BOHR],
];
for (rec, exp) in coords.iter().zip(expected.iter()) {
println!("Recovered {:?}, Expected {:?}", rec, exp);
assert_relative_eq!(rec[0], exp[0], epsilon = 1e-5);
assert_relative_eq!(rec[1], exp[1], epsilon = 1e-5);
assert_relative_eq!(rec[2], exp[2], epsilon = 1e-5);
}
}
}