#![allow(non_camel_case_types)]
pub mod basis_sets;
pub mod charges;
pub mod dynamics;
pub mod geom;
pub mod method;
mod plots;
pub mod scf;
pub mod solvation;
use std::{
fmt::Display,
fs,
fs::File,
io::{self, ErrorKind, Write},
path::Path,
process::Command,
};
use basis_sets::BasisSet;
use lin_alg::f64::Vec3;
use method::{Method, MethodSection};
use scf::Scf;
use solvation::{Solvator, SolvatorImplicit};
use crate::{
AtomGeneric,
orca::{
charges::{ChargesOutput, MbisChargesCfg},
dynamics::{Dynamics, DynamicsOutput},
geom::Geom,
plots::Plots,
},
};
const TEMP_DIR: &str = "orca_temp";
fn make_inp_block(block_name: &str, contents: &[(&str, String)], keywords: &[&str]) -> String {
let mut r = String::new();
r.push('%');
r.push_str(block_name);
for kw in keywords {
r.push(' ');
r.push_str(kw);
}
r.push('\n');
for (k, v) in contents {
r.push_str(&format!(" {} {}\n", k, v));
}
r.push_str("end");
r
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum GcpOption {
HfMinis,
HfSv,
Hf631Gd,
HfSvp,
HfTz,
DftMinis,
DftSv,
Dft631Gd,
DftLanl,
DftVsP_,
DftSvp,
DftTz,
File,
}
impl GcpOption {
pub fn keyword(self) -> String {
match self {
Self::HfMinis => "hf/minis",
Self::HfSv => "hf/sv",
Self::Hf631Gd => "hf/631gd",
Self::HfSvp => "hf/svp",
Self::HfTz => "hf/tz",
Self::DftMinis => "dft/minis",
Self::DftSv => "dft/sv",
Self::Dft631Gd => "dft/631gd",
Self::DftLanl => "dft/lanl",
Self::DftVsP_ => "dft/sv(p)",
Self::DftSvp => "dft/svp",
Self::DftTz => "dft/tz",
Self::File => "file",
}
.to_string()
}
}
#[derive(Clone, Copy, Default, PartialEq, Debug)]
pub enum GeomOptThresh {
Loose,
#[default]
Opt,
Tight,
VeryTight,
}
impl GeomOptThresh {
pub fn keyword(self) -> String {
match self {
Self::Loose => "LooseOpt".to_string(),
Self::Opt => "Opt".to_string(),
Self::Tight => "TightOpt".to_string(),
Self::VeryTight => "VeryTightOpt".to_string(),
}
}
}
impl Display for GeomOptThresh {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let v = match self {
Self::Loose => "Loose",
Self::Opt => "Opt",
Self::Tight => "Tight",
Self::VeryTight => "Very tight",
};
write!(f, "{}", v)
}
}
#[derive(Clone, Debug, Default)]
pub enum Task {
#[default]
SinglePoint,
GeometryOptimization((GeomOptThresh, Option<Geom>)),
MbisCharges(MbisChargesCfg),
MolDynamics(Dynamics),
}
impl Display for Task {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let v = match self {
Self::SinglePoint => "Single point energy",
Self::GeometryOptimization(_) => "Optimize geometry",
Self::MbisCharges(_) => "MBIS charges",
Self::MolDynamics(_) => "Mol dynamics (Ab-initio)",
};
write!(f, "{v}")
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Keyword {
Freq,
NumericalGradient,
D4Dispersion,
ConformerSearch,
Gcp(GcpOption),
UseSymmetry,
AnFreq,
NumFreq,
}
impl Keyword {
pub fn keyword(self) -> String {
match self {
Self::Freq => "FREQ".to_string(),
Self::NumericalGradient => "NUMGRAD".to_string(),
Self::D4Dispersion => "D4".to_string(),
Self::ConformerSearch => "GOAT".to_string(),
Self::Gcp(option) => format!("GCP({}", option.keyword()),
Self::UseSymmetry => "UseSymmetry".to_string(),
Self::AnFreq => "AnFreq".to_string(),
Self::NumFreq => "NumFreq".to_string(),
}
}
}
#[derive(Clone, Copy, PartialEq, Debug, Default)]
pub enum LocalizationMethod {
#[default]
PipekMezey,
FosterBoys,
}
impl LocalizationMethod {
pub fn keyword(self) -> String {
match self {
Self::PipekMezey => "PM",
Self::FosterBoys => "FB",
}
.to_owned()
}
}
#[derive(Clone, Debug)]
pub struct BondLocalization {
pub method: LocalizationMethod,
}
#[derive(Clone, Debug, Default)]
pub struct Symmetry {
pub sym_thresh: Option<f32>,
pub prefer_c2v: Option<bool>,
pub point_group: Option<String>,
}
impl Symmetry {
pub fn make_inp(&self) -> String {
let mut contents = vec![("UseSymmetry", "true".to_owned())];
if let Some(v) = self.sym_thresh {
contents.push(("SymThresh", format!("{v:.6}")));
}
if let Some(v) = self.prefer_c2v {
contents.push(("PreferC2v", format!("{v:?}")));
}
if let Some(v) = &self.point_group {
contents.push(("PreferC2v", v.clone()));
}
make_inp_block("sym", &contents, &[])
}
}
#[derive(Debug, Clone, Default)]
pub struct OrcaInput {
pub task: Task,
pub method: Method,
pub method_section: Option<MethodSection>, pub basis_set: BasisSet,
pub keywords: Vec<Keyword>,
pub atoms: Vec<AtomGeneric>,
pub solvator: Option<Solvator>,
pub solvator_implicit: Option<SolvatorImplicit>,
pub bond_localization: Option<BondLocalization>,
pub scf: Option<Scf>,
pub symmetry: Option<Symmetry>, pub plots: Option<Plots>,
}
impl OrcaInput {
pub fn new(method: Method, basis_set: BasisSet, atoms: &[AtomGeneric]) -> Self {
Self {
method,
basis_set,
atoms: atoms.to_vec(),
..Self::default()
}
}
pub fn make_inp(&self) -> String {
let mut result = String::new();
result.push_str(&format!("!{}", self.method.keyword()));
if !self.method.is_composite() {
result.push_str(&format!(" {}", self.basis_set.keyword()));
}
match &self.task {
Task::SinglePoint => {} Task::GeometryOptimization((thresh, geom)) => {
result.push_str(&format!(" {}", thresh.keyword()));
if let Some(v) = geom {
result.push('\n');
result.push_str(&v.make_inp());
}
}
Task::MbisCharges(cfg) => {
result.push_str(" MBIS");
if cfg.octopole || cfg.quadrupole || cfg.dipole {
result.push('\n');
result.push_str(&cfg.make_inp());
}
}
Task::MolDynamics(md) => {
result.push_str(&format!(" {}", md.make_inp()));
}
}
for kw in &self.keywords {
if kw == &Keyword::D4Dispersion && self.method.is_composite() {
continue;
}
result.push_str(&format!(" {}", kw.keyword()));
}
if let Some(v) = &self.method_section {
result.push('\n');
result.push_str(&v.make_inp());
}
if let Some(v) = &self.solvator {
result.push('\n');
result.push_str(&v.make_inp());
}
if let Some(v) = &self.solvator_implicit {
result.push('\n');
result.push_str(&v.make_inp());
}
if let Some(v) = &self.bond_localization {
result.push('\n');
result.push_str(&make_inp_block(
"loc",
&[("locmet", v.method.keyword())],
&[],
));
}
if let Some(v) = &self.scf {
result.push('\n');
result.push_str(&v.make_inp());
}
if let Some(v) = &self.symmetry {
result.push('\n');
result.push_str(&v.make_inp());
}
if let Some(v) = &self.plots {
result.push('\n');
result.push_str(&v.make_inp());
}
result.push_str("\n\n* xyz 0 1\n");
for atom in &self.atoms {
result.push_str(&format!(
"{:<2} {:>12.5} {:>12.5} {:>12.5}\n",
atom.element.to_letter(),
atom.posit.x,
atom.posit.y,
atom.posit.z
));
}
result.push('*');
result
}
pub fn save(&self, path: &Path) -> io::Result<()> {
let mut file = File::create(path)?;
let text = self.make_inp();
write!(file, "{text}")
}
pub fn run(&self) -> io::Result<OrcaOutput> {
let dir = Path::new(TEMP_DIR);
fs::create_dir_all(dir)?;
let inp_fname = "temp_orca_input.inp";
let inp_path = dir.join(Path::new(inp_fname));
self.save(&inp_path)?;
let cmd_out = match Command::new("orca")
.current_dir(dir)
.args([inp_fname])
.output()
{
Ok(out) => out,
Err(e) if e.kind() == ErrorKind::NotFound => {
fs::remove_dir_all(dir)?;
return Err(io::Error::new(
ErrorKind::NotFound,
"`orca` executable not found in the system PATH",
));
}
Err(e) => return Err(e),
};
if !cmd_out.status.success() {
let stderr_str = String::from_utf8_lossy(&cmd_out.stderr);
fs::remove_dir_all(dir)?;
return Err(io::Error::other(format!(
"Problem reading out temporary ORCA file: {}",
stderr_str
)));
}
let result_text = String::from_utf8(cmd_out.stdout)
.map_err(|e| io::Error::new(ErrorKind::InvalidData, e))?;
if !result_text.contains("****ORCA TERMINATED NORMALLY****") {
return Err(io::Error::other(format!(
"ORCA terminated abnormally: {result_text}"
)));
}
let result = match &self.task {
Task::SinglePoint => {
OrcaOutput::Text(result_text)
}
Task::MolDynamics(md) => {
let out = dir.join(&md.traj_out_dir);
let out = DynamicsOutput::new(&out, result_text)?;
OrcaOutput::Dynamics(out)
}
Task::MbisCharges(_) => {
let out = ChargesOutput::new(result_text)?;
OrcaOutput::Charges(out)
}
Task::GeometryOptimization(_) => {
let out = GeometryOutput::new(result_text)?;
OrcaOutput::Geometry(out)
}
};
fs::remove_dir_all(dir)?;
Ok(result)
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum TerminationStatus {
Normal,
Error, }
#[derive(Debug, Clone)]
pub enum OrcaOutput {
Text(String),
Dynamics(DynamicsOutput),
Charges(ChargesOutput),
Geometry(GeometryOutput),
}
#[derive(Debug, Clone)]
pub struct GeometryOutput {
pub text: String,
pub posits: Vec<Vec3>,
}
impl GeometryOutput {
pub fn new(text: String) -> io::Result<Self> {
let mut posits = Vec::new();
let final_eval_marker = "*** FINAL ENERGY EVALUATION AT THE STATIONARY POINT ***";
let section_start = text.find(final_eval_marker).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"Final stationary point not reached yet",
)
})?;
let remaining_text = &text[section_start..];
let coord_header = "CARTESIAN COORDINATES (ANGSTROEM)";
let header_pos = remaining_text.find(coord_header).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"Could not find coordinates in final section",
)
})?;
let mut lines = remaining_text[header_pos..].lines();
lines.next();
lines.next();
for line in lines {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('-') {
break;
}
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() >= 4 {
let x = parts[1]
.parse::<f64>()
.map_err(|e| io::Error::new(ErrorKind::InvalidData, e))?;
let y = parts[2]
.parse::<f64>()
.map_err(|e| io::Error::new(ErrorKind::InvalidData, e))?;
let z = parts[3]
.parse::<f64>()
.map_err(|e| io::Error::new(ErrorKind::InvalidData, e))?;
posits.push(Vec3 { x, y, z });
}
}
if posits.is_empty() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"Coordinate block was empty or malformed",
));
}
Ok(Self { text, posits })
}
}