#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub struct Atom {
pub symbol: String,
pub coords: [f64; 3],
pub atomic_number: u8,
}
impl Atom {
pub fn new(symbol: impl Into<String>, x: f64, y: f64, z: f64) -> Self {
Self {
symbol: symbol.into(),
coords: [x, y, z],
atomic_number: 0,
}
}
pub fn with_atomic_number(mut self, z: u8) -> Self {
self.atomic_number = z;
self
}
}
impl fmt::Display for Atom {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:<4} {:>12.6} {:>12.6} {:>12.6}",
self.symbol, self.coords[0], self.coords[1], self.coords[2]
)
}
}
#[derive(Debug, Clone)]
pub struct GaussianRouteSection {
pub method: String,
pub basis_set: String,
pub calc_type: String,
pub keywords: HashMap<String, String>,
pub extra_tokens: Vec<String>,
}
impl GaussianRouteSection {
pub fn new(
method: impl Into<String>,
basis_set: impl Into<String>,
calc_type: impl Into<String>,
) -> Self {
Self {
method: method.into(),
basis_set: basis_set.into(),
calc_type: calc_type.into(),
keywords: HashMap::new(),
extra_tokens: Vec::new(),
}
}
pub fn add_keyword(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.keywords.insert(key.into(), value.into());
}
pub fn render(&self) -> String {
let mut parts = vec![format!(
"# {}/{} {}",
self.method, self.basis_set, self.calc_type
)];
for (k, v) in &self.keywords {
if v.is_empty() {
parts.push(k.clone());
} else {
parts.push(format!("{}={}", k, v));
}
}
parts.extend(self.extra_tokens.iter().cloned());
parts.join(" ")
}
}
#[derive(Debug, Clone)]
pub struct GaussianInput {
pub link0: Vec<String>,
pub route: GaussianRouteSection,
pub title: String,
pub charge: i32,
pub multiplicity: u32,
pub atoms: Vec<Atom>,
pub z_matrix: Option<String>,
}
impl GaussianInput {
pub fn new(
route: GaussianRouteSection,
title: impl Into<String>,
charge: i32,
multiplicity: u32,
) -> Self {
Self {
link0: Vec::new(),
route,
title: title.into(),
charge,
multiplicity,
atoms: Vec::new(),
z_matrix: None,
}
}
pub fn add_atom(&mut self, atom: Atom) {
self.atoms.push(atom);
}
pub fn add_link0(&mut self, directive: impl Into<String>) {
self.link0.push(directive.into());
}
pub fn render(&self) -> String {
let mut out = String::new();
for l0 in &self.link0 {
out.push('%');
out.push_str(l0);
out.push('\n');
}
out.push_str(&self.route.render());
out.push_str("\n\n");
out.push_str(&self.title);
out.push_str("\n\n");
out.push_str(&format!("{} {}\n", self.charge, self.multiplicity));
if let Some(zmat) = &self.z_matrix {
out.push_str(zmat);
} else {
for atom in &self.atoms {
out.push_str(&atom.to_string());
out.push('\n');
}
}
out.push('\n');
out
}
pub fn num_atoms(&self) -> usize {
self.atoms.len()
}
}
#[derive(Debug, Clone)]
pub struct OptStep {
pub step: usize,
pub energy: f64,
pub max_force: f64,
pub rms_force: f64,
pub max_displacement: f64,
pub rms_displacement: f64,
pub converged: bool,
pub atoms: Vec<Atom>,
}
impl OptStep {
pub fn new(step: usize, energy: f64) -> Self {
Self {
step,
energy,
max_force: 0.0,
rms_force: 0.0,
max_displacement: 0.0,
rms_displacement: 0.0,
converged: false,
atoms: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct GaussianFrequency {
pub freq_cm: f64,
pub ir_intensity: f64,
pub raman_activity: Option<f64>,
pub reduced_mass: f64,
pub displacements: Vec<[f64; 3]>,
}
impl GaussianFrequency {
pub fn new(freq_cm: f64, ir_intensity: f64, reduced_mass: f64) -> Self {
Self {
freq_cm,
ir_intensity,
raman_activity: None,
reduced_mass,
displacements: Vec::new(),
}
}
pub fn is_imaginary(&self) -> bool {
self.freq_cm < 0.0
}
}
#[derive(Debug, Clone)]
pub struct NmrShielding {
pub atom_idx: usize,
pub isotropic: f64,
pub anisotropy: f64,
}
#[derive(Debug, Clone, Default)]
pub struct GaussianOutput {
pub scf_energy: Option<f64>,
pub opt_converged: bool,
pub opt_steps: Vec<OptStep>,
pub frequencies: Vec<GaussianFrequency>,
pub nmr_shieldings: Vec<NmrShielding>,
pub zero_point_energy: Option<f64>,
pub thermal_correction_h: Option<f64>,
pub thermal_correction_g: Option<f64>,
pub final_geometry: Vec<Atom>,
pub n_imaginary: usize,
}
impl GaussianOutput {
pub fn new() -> Self {
Self::default()
}
pub fn parse(content: &str) -> Self {
let mut out = GaussianOutput::new();
let mut current_opt_step: Option<OptStep> = None;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("SCF Done:")
&& let Some(e) = parse_after_eq(trimmed)
{
out.scf_energy = Some(e);
}
if trimmed.contains("Optimization completed.") {
out.opt_converged = true;
if let Some(step) = current_opt_step.take() {
out.opt_steps.push(step);
}
}
if trimmed.starts_with("Energy=")
&& let Some(e) = parse_value_after(trimmed, "Energy=")
{
let step_num = out.opt_steps.len() + 1;
current_opt_step = Some(OptStep::new(step_num, e));
}
if trimmed.starts_with("Frequencies --")
&& let Some(rest) = trimmed.strip_prefix("Frequencies --")
{
for tok in rest.split_whitespace() {
if let Ok(f) = tok.parse::<f64>() {
out.frequencies.push(GaussianFrequency::new(f, 0.0, 1.0));
if f < 0.0 {
out.n_imaginary += 1;
}
}
}
}
if trimmed.starts_with("IR Inten --")
&& let Some(rest) = trimmed.strip_prefix("IR Inten --")
{
let intensities: Vec<f64> = rest
.split_whitespace()
.filter_map(|t| t.parse().ok())
.collect();
let n_freqs = out.frequencies.len();
let start = n_freqs.saturating_sub(intensities.len());
for (i, &ir) in intensities.iter().enumerate() {
if start + i < n_freqs {
out.frequencies[start + i].ir_intensity = ir;
}
}
}
if trimmed.starts_with("Zero-point correction=")
&& let Some(v) = parse_value_after(trimmed, "Zero-point correction=")
{
out.zero_point_energy = Some(v);
}
if trimmed.starts_with("Thermal correction to Enthalpy=")
&& let Some(v) = parse_value_after(trimmed, "Thermal correction to Enthalpy=")
{
out.thermal_correction_h = Some(v);
}
if trimmed.starts_with("Thermal correction to Gibbs Free Energy=")
&& let Some(v) =
parse_value_after(trimmed, "Thermal correction to Gibbs Free Energy=")
{
out.thermal_correction_g = Some(v);
}
}
if let Some(step) = current_opt_step {
out.opt_steps.push(step);
}
out
}
pub fn lowest_frequency(&self) -> Option<f64> {
self.frequencies.iter().map(|f| f.freq_cm).reduce(f64::min)
}
pub fn n_frequencies(&self) -> usize {
self.frequencies.len()
}
}
fn parse_after_eq(s: &str) -> Option<f64> {
let idx = s.rfind('=')?;
s[idx + 1..].split_whitespace().next()?.parse().ok()
}
fn parse_value_after(s: &str, prefix: &str) -> Option<f64> {
s.strip_prefix(prefix)?
.split_whitespace()
.next()?
.parse()
.ok()
}
#[derive(Debug, Clone)]
pub struct OrcaSettings {
pub max_scf_iter: usize,
pub scf_conv: f64,
pub grad_conv: f64,
pub grid: u8,
pub rijcosx: bool,
pub aux_basis: Option<String>,
pub n_procs: usize,
pub mem_per_proc: usize,
}
impl OrcaSettings {
pub fn default_settings() -> Self {
Self {
max_scf_iter: 125,
scf_conv: 1e-8,
grad_conv: 1e-5,
grid: 5,
rijcosx: false,
aux_basis: None,
n_procs: 1,
mem_per_proc: 2048,
}
}
}
#[derive(Debug, Clone)]
pub struct OrcaInput {
pub keywords: Vec<String>,
pub charge: i32,
pub multiplicity: u32,
pub atoms: Vec<Atom>,
pub settings: OrcaSettings,
pub extra_blocks: Vec<String>,
}
impl OrcaInput {
pub fn new(charge: i32, multiplicity: u32) -> Self {
Self {
keywords: Vec::new(),
charge,
multiplicity,
atoms: Vec::new(),
settings: OrcaSettings::default_settings(),
extra_blocks: Vec::new(),
}
}
pub fn add_keyword(&mut self, kw: impl Into<String>) {
self.keywords.push(kw.into());
}
pub fn add_atom(&mut self, atom: Atom) {
self.atoms.push(atom);
}
pub fn render(&self) -> String {
let mut out = String::new();
if !self.keywords.is_empty() {
out.push_str("! ");
out.push_str(&self.keywords.join(" "));
out.push('\n');
}
if self.settings.n_procs > 1 {
out.push_str(&format!("%pal nprocs {} end\n", self.settings.n_procs));
}
out.push_str(&format!("%maxcore {}\n", self.settings.mem_per_proc));
out.push_str(&format!(
"%scf\n MaxIter {}\n ConvTol {:.2e}\nend\n",
self.settings.max_scf_iter, self.settings.scf_conv
));
for block in &self.extra_blocks {
out.push_str(block);
out.push('\n');
}
out.push_str(&format!("* xyz {} {}\n", self.charge, self.multiplicity));
for atom in &self.atoms {
out.push_str(&atom.to_string());
out.push('\n');
}
out.push_str("*\n");
out
}
}
#[derive(Debug, Clone)]
pub struct OrcaExcitedState {
pub state: usize,
pub energy_ev: f64,
pub oscillator_strength: f64,
pub symmetry: String,
}
impl OrcaExcitedState {
pub fn new(
state: usize,
energy_ev: f64,
oscillator_strength: f64,
symmetry: impl Into<String>,
) -> Self {
Self {
state,
energy_ev,
oscillator_strength,
symmetry: symmetry.into(),
}
}
}
#[derive(Debug, Clone)]
pub struct AtomGradient {
pub atom_idx: usize,
pub gradient: [f64; 3],
}
#[derive(Debug, Clone, Default)]
pub struct OrcaOutput {
pub electronic_energy: Option<f64>,
pub dispersion_correction: Option<f64>,
pub gradients: Vec<AtomGradient>,
pub excited_states: Vec<OrcaExcitedState>,
pub opt_converged: bool,
pub scf_iterations: usize,
pub version: Option<String>,
pub final_geometry: Vec<Atom>,
}
impl OrcaOutput {
pub fn new() -> Self {
Self::default()
}
pub fn parse(content: &str) -> Self {
let mut out = OrcaOutput::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("Program Version") {
out.version = Some(trimmed.to_string());
}
if trimmed.starts_with("FINAL SINGLE POINT ENERGY")
&& let Some(val) = trimmed
.split_whitespace()
.last()
.and_then(|s| s.parse().ok())
{
out.electronic_energy = Some(val);
}
if trimmed.starts_with("Dispersion correction")
&& let Some(val) = trimmed
.split_whitespace()
.last()
.and_then(|s| s.parse().ok())
{
out.dispersion_correction = Some(val);
}
if trimmed.contains("OPTIMIZATION HAS CONVERGED") {
out.opt_converged = true;
}
if trimmed.starts_with("SCF CONVERGED AFTER")
&& let Some(n) = trimmed
.split_whitespace()
.find(|t| t.parse::<usize>().is_ok())
.and_then(|t| t.parse().ok())
{
out.scf_iterations = n;
}
if trimmed.starts_with("STATE") && trimmed.contains("EXCITATION ENERGY") {
if let Some(ev) = parse_ev_from_state_line(trimmed) {
let osc = parse_f_from_state_line(trimmed).unwrap_or(0.0);
let state_num = out.excited_states.len() + 1;
out.excited_states
.push(OrcaExcitedState::new(state_num, ev, osc, "SINGLET"));
}
}
}
out
}
pub fn total_energy(&self) -> Option<f64> {
match (self.electronic_energy, self.dispersion_correction) {
(Some(e), Some(d)) => Some(e + d),
(Some(e), None) => Some(e),
_ => None,
}
}
pub fn absorption_centroid_nm(&self) -> Option<f64> {
let total_osc: f64 = self
.excited_states
.iter()
.map(|s| s.oscillator_strength)
.sum();
if total_osc < 1e-15 {
return None;
}
let weighted: f64 = self
.excited_states
.iter()
.map(|s| (1239.84 / s.energy_ev) * s.oscillator_strength)
.sum();
Some(weighted / total_osc)
}
}
fn parse_ev_from_state_line(line: &str) -> Option<f64> {
let tokens: Vec<&str> = line.split_whitespace().collect();
for (i, &tok) in tokens.iter().enumerate() {
if tok == "eV" && i > 0 {
return tokens[i - 1].parse().ok();
}
}
None
}
fn parse_f_from_state_line(line: &str) -> Option<f64> {
for part in line.split_whitespace() {
if let Some(val) = part.strip_prefix("f=") {
return val.parse().ok();
}
}
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Spin {
Alpha,
Beta,
}
#[derive(Debug, Clone)]
pub struct MolecularOrbital {
pub index: usize,
pub energy: f64,
pub occupation: f64,
pub spin: Spin,
pub symmetry: String,
pub coefficients: Vec<f64>,
}
impl MolecularOrbital {
pub fn new(index: usize, energy: f64, occupation: f64, spin: Spin) -> Self {
Self {
index,
energy,
occupation,
spin,
symmetry: String::new(),
coefficients: Vec::new(),
}
}
pub fn is_homo(&self, homo_idx: usize) -> bool {
self.index == homo_idx
}
pub fn is_lumo(&self, lumo_idx: usize) -> bool {
self.index == lumo_idx
}
pub fn coeff_norm(&self) -> f64 {
self.coefficients.iter().map(|c| c * c).sum::<f64>().sqrt()
}
}
#[derive(Debug, Clone, Default)]
pub struct MolecularOrbitalSet {
pub alpha_mos: Vec<MolecularOrbital>,
pub beta_mos: Vec<MolecularOrbital>,
pub n_alpha: usize,
pub n_beta: usize,
}
impl MolecularOrbitalSet {
pub fn new(n_alpha: usize, n_beta: usize) -> Self {
Self {
alpha_mos: Vec::new(),
beta_mos: Vec::new(),
n_alpha,
n_beta,
}
}
pub fn homo_idx(&self) -> Option<usize> {
if self.n_alpha == 0 {
None
} else {
Some(self.n_alpha - 1)
}
}
pub fn lumo_idx(&self) -> Option<usize> {
if self.n_alpha < self.alpha_mos.len() {
Some(self.n_alpha)
} else {
None
}
}
pub fn homo_lumo_gap_ev(&self) -> Option<f64> {
let homo = self.homo_idx()?;
let lumo = self.lumo_idx()?;
let e_homo = self.alpha_mos.get(homo)?.energy;
let e_lumo = self.alpha_mos.get(lumo)?.energy;
Some((e_lumo - e_homo) * 27.2114)
}
pub fn add_alpha(&mut self, mo: MolecularOrbital) {
self.alpha_mos.push(mo);
}
pub fn add_beta(&mut self, mo: MolecularOrbital) {
self.beta_mos.push(mo);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShellType {
S,
P,
D,
F,
G,
SP,
}
impl fmt::Display for ShellType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ShellType::S => write!(f, "S"),
ShellType::P => write!(f, "P"),
ShellType::D => write!(f, "D"),
ShellType::F => write!(f, "F"),
ShellType::G => write!(f, "G"),
ShellType::SP => write!(f, "SP"),
}
}
}
#[derive(Debug, Clone)]
pub struct GaussianShell {
pub shell_type: ShellType,
pub exponents: Vec<f64>,
pub coefficients_s: Vec<f64>,
pub coefficients_p: Vec<f64>,
}
impl GaussianShell {
pub fn new(shell_type: ShellType, exponents: Vec<f64>, coefficients: Vec<f64>) -> Self {
Self {
shell_type,
exponents,
coefficients_s: coefficients,
coefficients_p: Vec::new(),
}
}
pub fn new_sp(exponents: Vec<f64>, coeff_s: Vec<f64>, coeff_p: Vec<f64>) -> Self {
Self {
shell_type: ShellType::SP,
exponents,
coefficients_s: coeff_s,
coefficients_p: coeff_p,
}
}
pub fn n_primitives(&self) -> usize {
self.exponents.len()
}
pub fn n_basis_functions(&self) -> usize {
match self.shell_type {
ShellType::S => 1,
ShellType::P => 3,
ShellType::D => 6, ShellType::F => 10,
ShellType::G => 15,
ShellType::SP => 4, }
}
}
#[derive(Debug, Clone)]
pub struct ElementBasis {
pub element: String,
pub shells: Vec<GaussianShell>,
}
impl ElementBasis {
pub fn new(element: impl Into<String>) -> Self {
Self {
element: element.into(),
shells: Vec::new(),
}
}
pub fn add_shell(&mut self, shell: GaussianShell) {
self.shells.push(shell);
}
pub fn n_basis_functions(&self) -> usize {
self.shells.iter().map(|s| s.n_basis_functions()).sum()
}
}
#[derive(Debug, Clone)]
pub struct BasisSet {
pub name: String,
pub elements: HashMap<String, ElementBasis>,
}
impl BasisSet {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
elements: HashMap::new(),
}
}
pub fn add_element(&mut self, basis: ElementBasis) {
self.elements.insert(basis.element.clone(), basis);
}
pub fn total_basis_functions(&self, atom_symbols: &[&str]) -> usize {
atom_symbols
.iter()
.filter_map(|&sym| self.elements.get(sym))
.map(|eb| eb.n_basis_functions())
.sum()
}
}
#[derive(Debug, Clone)]
pub struct NormalModes {
pub n_atoms: usize,
pub frequencies: Vec<f64>,
pub ir_intensities: Vec<f64>,
pub raman_activities: Vec<f64>,
pub displacements: Vec<Vec<f64>>,
pub reduced_masses: Vec<f64>,
pub force_constants: Vec<f64>,
}
impl NormalModes {
pub fn new(n_atoms: usize) -> Self {
Self {
n_atoms,
frequencies: Vec::new(),
ir_intensities: Vec::new(),
raman_activities: Vec::new(),
displacements: Vec::new(),
reduced_masses: Vec::new(),
force_constants: Vec::new(),
}
}
pub fn expected_modes_nonlinear(&self) -> usize {
3 * self.n_atoms - 6
}
pub fn expected_modes_linear(&self) -> usize {
3 * self.n_atoms - 5
}
pub fn n_imaginary(&self) -> usize {
self.frequencies.iter().filter(|&&f| f < 0.0).count()
}
pub fn strongest_ir_band(&self) -> Option<(f64, f64)> {
self.ir_intensities
.iter()
.copied()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, intensity)| (self.frequencies[i], intensity))
}
pub fn zpve_cm(&self) -> f64 {
self.frequencies
.iter()
.filter(|&&f| f > 0.0)
.map(|&f| f * 0.5)
.sum()
}
}
#[derive(Debug, Clone)]
pub struct ElectronicStructure {
pub total_energy: f64,
pub zero_point_energy: f64,
pub thermal_energy: f64,
pub enthalpy_correction: f64,
pub entropy: f64,
pub temperature: f64,
pub gibbs_free_energy: f64,
pub translational_entropy: f64,
pub rotational_entropy: f64,
pub vibrational_entropy: f64,
}
impl ElectronicStructure {
pub fn new(total_energy: f64, temperature: f64) -> Self {
Self {
total_energy,
zero_point_energy: 0.0,
thermal_energy: 0.0,
enthalpy_correction: 0.0,
entropy: 0.0,
temperature,
gibbs_free_energy: 0.0,
translational_entropy: 0.0,
rotational_entropy: 0.0,
vibrational_entropy: 0.0,
}
}
pub fn e0_plus_zpe(&self) -> f64 {
self.total_energy + self.zero_point_energy
}
pub fn enthalpy(&self) -> f64 {
let kt = self.temperature / 315_775.13;
self.total_energy + self.zero_point_energy + self.thermal_energy + kt
}
pub fn free_energy(&self) -> f64 {
let s_hartree_per_k = self.entropy * 4.184 / (4.3597e-18 * 6.022e23);
self.enthalpy() - self.temperature * s_hartree_per_k
}
}
#[derive(Debug, Clone)]
pub struct ExcitedStateTransition {
pub state: usize,
pub energy_ev: f64,
pub oscillator_strength: f64,
pub dominant_transitions: Vec<(usize, usize, f64)>,
pub transition_dipole: [f64; 3],
pub multiplicity: String,
}
impl ExcitedStateTransition {
pub fn new(state: usize, energy_ev: f64, oscillator_strength: f64) -> Self {
Self {
state,
energy_ev,
oscillator_strength,
dominant_transitions: Vec::new(),
transition_dipole: [0.0; 3],
multiplicity: "Singlet".to_string(),
}
}
pub fn wavelength_nm(&self) -> f64 {
1239.84 / self.energy_ev
}
pub fn add_transition(&mut self, from: usize, to: usize, coeff: f64) {
self.dominant_transitions.push((from, to, coeff));
}
}
#[derive(Debug, Clone)]
pub struct ExcitedStates {
pub states: Vec<ExcitedStateTransition>,
pub method: String,
pub ground_energy: f64,
}
impl ExcitedStates {
pub fn new(method: impl Into<String>, ground_energy: f64) -> Self {
Self {
states: Vec::new(),
method: method.into(),
ground_energy,
}
}
pub fn add_state(&mut self, state: ExcitedStateTransition) {
self.states.push(state);
}
pub fn brightest_state(&self) -> Option<&ExcitedStateTransition> {
self.states.iter().max_by(|a, b| {
a.oscillator_strength
.partial_cmp(&b.oscillator_strength)
.unwrap_or(std::cmp::Ordering::Equal)
})
}
pub fn uv_vis_spectrum(
&self,
lambda_min: f64,
lambda_max: f64,
n_points: usize,
fwhm_nm: f64,
) -> (Vec<f64>, Vec<f64>) {
let gamma = fwhm_nm / 2.0;
let step = (lambda_max - lambda_min) / n_points.max(1) as f64;
let wavelengths: Vec<f64> = (0..n_points)
.map(|i| lambda_min + i as f64 * step)
.collect();
let intensities: Vec<f64> = wavelengths
.iter()
.map(|&lam| {
self.states
.iter()
.map(|s| {
let dl = lam - s.wavelength_nm();
s.oscillator_strength * gamma * gamma / (dl * dl + gamma * gamma)
})
.sum()
})
.collect();
(wavelengths, intensities)
}
}
#[derive(Debug, Clone)]
pub struct NaturalBondOrbital {
pub index: usize,
pub nbo_type: String,
pub atoms: Vec<usize>,
pub occupancy: f64,
pub energy: f64,
pub hybrids: Vec<(usize, String, f64)>,
}
impl NaturalBondOrbital {
pub fn new(index: usize, nbo_type: impl Into<String>, occupancy: f64, energy: f64) -> Self {
Self {
index,
nbo_type: nbo_type.into(),
atoms: Vec::new(),
occupancy,
energy,
hybrids: Vec::new(),
}
}
pub fn is_lone_pair(&self) -> bool {
self.nbo_type.starts_with("LP")
}
pub fn is_bonding(&self) -> bool {
self.nbo_type == "BD"
}
pub fn is_antibonding(&self) -> bool {
self.nbo_type == "BD*"
}
}
#[derive(Debug, Clone)]
pub struct WibergBondOrderMatrix {
pub n_atoms: usize,
data: Vec<f64>,
}
impl WibergBondOrderMatrix {
pub fn new(n_atoms: usize) -> Self {
let size = n_atoms * (n_atoms + 1) / 2;
Self {
n_atoms,
data: vec![0.0; size],
}
}
fn idx(&self, i: usize, j: usize) -> usize {
let (a, b) = if i <= j { (i, j) } else { (j, i) };
a * self.n_atoms - a * (a + 1) / 2 + b
}
pub fn set(&mut self, i: usize, j: usize, bo: f64) {
let idx = self.idx(i, j);
if idx < self.data.len() {
self.data[idx] = bo;
}
}
pub fn get(&self, i: usize, j: usize) -> f64 {
let idx = self.idx(i, j);
if idx < self.data.len() {
self.data[idx]
} else {
0.0
}
}
pub fn valence(&self, i: usize) -> f64 {
(0..self.n_atoms)
.filter(|&j| j != i)
.map(|j| self.get(i, j))
.sum()
}
}
#[derive(Debug, Clone)]
pub struct NboOutput {
pub nbos: Vec<NaturalBondOrbital>,
pub natural_charges: Vec<f64>,
pub bond_orders: WibergBondOrderMatrix,
pub second_order_interactions: Vec<(usize, usize, f64)>,
}
impl NboOutput {
pub fn new(n_atoms: usize) -> Self {
Self {
nbos: Vec::new(),
natural_charges: vec![0.0; n_atoms],
bond_orders: WibergBondOrderMatrix::new(n_atoms),
second_order_interactions: Vec::new(),
}
}
pub fn total_charge(&self) -> f64 {
self.natural_charges.iter().sum()
}
pub fn n_lone_pairs(&self) -> usize {
self.nbos.iter().filter(|n| n.is_lone_pair()).count()
}
pub fn n_bonding(&self) -> usize {
self.nbos.iter().filter(|n| n.is_bonding()).count()
}
pub fn strongest_interaction(&self) -> Option<&(usize, usize, f64)> {
self.second_order_interactions
.iter()
.max_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_atom_display() {
let a = Atom::new("C", 0.0, 0.0, 0.0);
let s = a.to_string();
assert!(s.contains("C"), "display should contain element: {s}");
}
#[test]
fn test_gaussian_input_render_route() {
let route = GaussianRouteSection::new("B3LYP", "6-31G*", "Opt");
let inp = GaussianInput::new(route, "Test molecule", 0, 1);
let rendered = inp.render();
assert!(rendered.contains("B3LYP/6-31G*"), "route: {rendered}");
assert!(rendered.contains("0 1"), "charge/mult: {rendered}");
}
#[test]
fn test_gaussian_input_num_atoms() {
let route = GaussianRouteSection::new("HF", "STO-3G", "SP");
let mut inp = GaussianInput::new(route, "H2", 0, 1);
inp.add_atom(Atom::new("H", 0.0, 0.0, 0.0));
inp.add_atom(Atom::new("H", 0.74, 0.0, 0.0));
assert_eq!(inp.num_atoms(), 2);
}
#[test]
fn test_gaussian_input_link0() {
let route = GaussianRouteSection::new("B3LYP", "6-31G*", "SP");
let mut inp = GaussianInput::new(route, "test", 0, 1);
inp.add_link0("chk=job.chk");
let rendered = inp.render();
assert!(rendered.contains("%chk=job.chk"), "link0: {rendered}");
}
#[test]
fn test_gaussian_route_keywords() {
let mut route = GaussianRouteSection::new("MP2", "cc-pVDZ", "SP");
route.add_keyword("SCF", "Tight");
let line = route.render();
assert!(line.contains("SCF=Tight"), "keywords: {line}");
}
#[test]
fn test_gaussian_output_parse_scf_energy() {
let log = "SCF Done: E(RB3LYP) = -78.5873 A.U. after 8 cycles\n";
let out = GaussianOutput::parse(log);
let e = out.scf_energy.expect("SCF energy should be parsed");
assert!((e + 78.5873).abs() < 1e-3, "e={e}");
}
#[test]
fn test_gaussian_output_parse_frequencies() {
let log = " Frequencies -- 1625.54 3050.12 3100.45\n IR Inten -- 10.20 0.50 5.30\n";
let out = GaussianOutput::parse(log);
assert_eq!(out.frequencies.len(), 3);
assert!((out.frequencies[0].freq_cm - 1625.54).abs() < 0.01);
}
#[test]
fn test_gaussian_output_imaginary_frequency() {
let log = " Frequencies -- -150.3 500.0\n";
let out = GaussianOutput::parse(log);
assert_eq!(out.n_imaginary, 1);
}
#[test]
fn test_gaussian_output_zpe() {
let log = "Zero-point correction= 0.054321 (Hartree/Particle)\n";
let out = GaussianOutput::parse(log);
let zpe = out.zero_point_energy.expect("ZPE");
assert!((zpe - 0.054321).abs() < 1e-6, "zpe={zpe}");
}
#[test]
fn test_gaussian_output_opt_converged() {
let log = "Optimization completed.\n";
let out = GaussianOutput::parse(log);
assert!(out.opt_converged);
}
#[test]
fn test_gaussian_output_lowest_frequency() {
let log = " Frequencies -- 300.0 500.0 100.0\n";
let out = GaussianOutput::parse(log);
let lo = out.lowest_frequency().expect("lowest freq");
assert!((lo - 100.0).abs() < 0.01);
}
#[test]
fn test_orca_input_render_keywords() {
let mut inp = OrcaInput::new(0, 1);
inp.add_keyword("B3LYP");
inp.add_keyword("def2-TZVP");
let rendered = inp.render();
assert!(rendered.contains("! B3LYP def2-TZVP"), "render: {rendered}");
}
#[test]
fn test_orca_input_coord_block() {
let mut inp = OrcaInput::new(0, 1);
inp.add_atom(Atom::new("O", 0.0, 0.0, 0.0));
inp.add_atom(Atom::new("H", 0.96, 0.0, 0.0));
let rendered = inp.render();
assert!(rendered.contains("* xyz 0 1"), "coord block: {rendered}");
assert!(rendered.contains("O"), "oxygen: {rendered}");
}
#[test]
fn test_orca_output_parse_energy() {
let log = "FINAL SINGLE POINT ENERGY -76.3456789\n";
let out = OrcaOutput::parse(log);
let e = out.electronic_energy.expect("energy");
assert!((e + 76.3456789).abs() < 1e-6, "e={e}");
}
#[test]
fn test_orca_output_total_energy_with_dispersion() {
let log =
"FINAL SINGLE POINT ENERGY -76.3456789\nDispersion correction -0.0012345\n";
let out = OrcaOutput::parse(log);
let total = out.total_energy().expect("total");
assert!((total + 76.3469134).abs() < 1e-4, "total={total}");
}
#[test]
fn test_orca_output_opt_converged() {
let log = "OPTIMIZATION HAS CONVERGED\n";
let out = OrcaOutput::parse(log);
assert!(out.opt_converged);
}
#[test]
fn test_mo_homo_lumo_gap() {
let mut mo_set = MolecularOrbitalSet::new(5, 5);
for i in 0..5 {
let e = -0.5 + i as f64 * 0.05;
mo_set.add_alpha(MolecularOrbital::new(i, e, 2.0, Spin::Alpha));
}
mo_set.add_alpha(MolecularOrbital::new(5, 0.1, 0.0, Spin::Alpha));
let gap = mo_set.homo_lumo_gap_ev().expect("gap");
let expected = 0.4 * 27.2114;
assert!((gap - expected).abs() < 0.01, "gap={gap}");
}
#[test]
fn test_mo_homo_idx() {
let mo_set = MolecularOrbitalSet::new(3, 3);
assert_eq!(mo_set.homo_idx(), Some(2));
}
#[test]
fn test_mo_coeff_norm() {
let mut mo = MolecularOrbital::new(0, -0.5, 2.0, Spin::Alpha);
mo.coefficients = vec![1.0, 0.0, 0.0, 0.0];
assert!((mo.coeff_norm() - 1.0).abs() < 1e-12);
}
#[test]
fn test_basis_set_total_functions() {
let mut bs = BasisSet::new("STO-3G");
let mut c_basis = ElementBasis::new("C");
c_basis.add_shell(GaussianShell::new(
ShellType::S,
vec![71.62, 13.04, 3.531],
vec![0.1543, 0.5353, 0.4446],
));
c_basis.add_shell(GaussianShell::new(
ShellType::SP,
vec![2.941, 0.6835, 0.2222],
vec![-0.09996, 0.3995, 0.7001],
));
bs.add_element(c_basis);
let total = bs.total_basis_functions(&["C", "C"]);
assert_eq!(total, 10, "total={total}");
}
#[test]
fn test_shell_n_primitives() {
let shell = GaussianShell::new(ShellType::D, vec![1.0, 2.0, 3.0], vec![0.1, 0.5, 0.4]);
assert_eq!(shell.n_primitives(), 3);
}
#[test]
fn test_normal_modes_n_imaginary() {
let mut nm = NormalModes::new(3);
nm.frequencies = vec![
-150.0, 500.0, 1000.0, 1500.0, 2000.0, 2500.0, 3000.0, 3500.0, 4000.0,
];
assert_eq!(nm.n_imaginary(), 1);
}
#[test]
fn test_normal_modes_zpve() {
let mut nm = NormalModes::new(2);
nm.frequencies = vec![1000.0, 2000.0];
let zpve = nm.zpve_cm();
assert!((zpve - 1500.0).abs() < 1e-6, "zpve={zpve}");
}
#[test]
fn test_normal_modes_strongest_ir() {
let mut nm = NormalModes::new(3);
nm.frequencies = vec![500.0, 1000.0, 1500.0];
nm.ir_intensities = vec![10.0, 250.0, 5.0];
let (freq, intensity) = nm.strongest_ir_band().expect("IR band");
assert!((freq - 1000.0).abs() < 1e-6);
assert!((intensity - 250.0).abs() < 1e-6);
}
#[test]
fn test_electronic_structure_e0_zpe() {
let mut es = ElectronicStructure::new(-76.4, 298.15);
es.zero_point_energy = 0.02;
assert!((es.e0_plus_zpe() + 76.38).abs() < 1e-6);
}
#[test]
fn test_electronic_structure_enthalpy_positive() {
let es = ElectronicStructure::new(-76.4, 298.15);
let h = es.enthalpy();
assert!(
(h - es.total_energy).abs() < 0.01,
"h-e={}",
h - es.total_energy
);
}
#[test]
fn test_excited_states_wavelength() {
let s = ExcitedStateTransition::new(1, 4.0, 0.3);
let wl = s.wavelength_nm();
assert!((wl - 309.96).abs() < 0.01, "wl={wl}");
}
#[test]
fn test_excited_states_brightest() {
let mut es = ExcitedStates::new("TDDFT", -76.4);
es.add_state(ExcitedStateTransition::new(1, 4.0, 0.05));
es.add_state(ExcitedStateTransition::new(2, 5.0, 0.80));
es.add_state(ExcitedStateTransition::new(3, 6.0, 0.10));
let brightest = es.brightest_state().expect("brightest");
assert_eq!(brightest.state, 2);
}
#[test]
fn test_excited_states_uv_vis_spectrum() {
let mut es = ExcitedStates::new("TDDFT", -76.4);
es.add_state(ExcitedStateTransition::new(1, 4.0, 1.0));
let (wls, ints) = es.uv_vis_spectrum(200.0, 600.0, 100, 10.0);
assert_eq!(wls.len(), 100);
assert_eq!(ints.len(), 100);
let max_idx = ints
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map(|(i, _)| i)
.unwrap();
assert!(
wls[max_idx] > 280.0 && wls[max_idx] < 340.0,
"peak at {}",
wls[max_idx]
);
}
#[test]
fn test_nbo_total_charge() {
let mut nbo = NboOutput::new(3);
nbo.natural_charges = vec![-0.5, 0.25, 0.25];
assert!((nbo.total_charge()).abs() < 1e-12);
}
#[test]
fn test_nbo_count_lone_pairs() {
let mut nbo = NboOutput::new(2);
nbo.nbos.push(NaturalBondOrbital::new(1, "LP", 1.98, -0.5));
nbo.nbos.push(NaturalBondOrbital::new(2, "BD", 1.95, -0.4));
nbo.nbos.push(NaturalBondOrbital::new(3, "LP", 1.97, -0.45));
assert_eq!(nbo.n_lone_pairs(), 2);
assert_eq!(nbo.n_bonding(), 1);
}
#[test]
fn test_wiberg_bond_order() {
let mut mat = WibergBondOrderMatrix::new(3);
mat.set(0, 1, 1.95);
mat.set(1, 2, 0.98);
assert!((mat.get(0, 1) - 1.95).abs() < 1e-12);
assert!((mat.get(1, 0) - 1.95).abs() < 1e-12); let val = mat.valence(1);
assert!((val - 2.93).abs() < 1e-6, "valence={val}");
}
#[test]
fn test_nbo_strongest_interaction() {
let mut nbo = NboOutput::new(4);
nbo.second_order_interactions = vec![(0, 1, 5.0), (1, 2, 30.0), (2, 3, 15.0)];
let strongest = nbo.strongest_interaction().expect("strongest");
assert!((strongest.2 - 30.0).abs() < 1e-12);
}
}