use std::{
error::Error,
fmt::{self, Display, Formatter},
};
use crate::{ComputationDevice, MdState, snapshot::Snapshot};
pub const SOFT_CORE_ALPHA: f32 = 0.5;
pub const SOFT_CORE_POWER: i32 = 1;
pub const SOFT_CORE_SIGMA_MIN: f32 = 3.0;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DecouplingSchedule {
pub coulomb_scale: f32,
pub coulomb_dscale_dlambda: f32,
pub lj_lambda: f32,
pub lj_dlambda_dlambda: f32,
}
pub fn staged_decoupling_schedule(lambda: f64) -> DecouplingSchedule {
let lambda = (lambda as f32).clamp(0.0, 1.0);
if lambda < 0.5 {
DecouplingSchedule {
coulomb_scale: 1.0 - 2.0 * lambda,
coulomb_dscale_dlambda: -2.0,
lj_lambda: 0.0,
lj_dlambda_dlambda: 0.0,
}
} else {
DecouplingSchedule {
coulomb_scale: 0.0,
coulomb_dscale_dlambda: 0.0,
lj_lambda: 2.0 * lambda - 1.0,
lj_dlambda_dlambda: 2.0,
}
}
}
#[derive(Default)]
pub struct StateAlchemical {
pub mol_idx: Option<usize>,
pub lambda: f64,
pub dh_dl: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub enum AlchemicalError {
EmptySnapshots,
MissingDhDl,
InvalidLambda(f64),
InvalidTemperature(f64),
InvalidFreeEnergy(f64),
NotEnoughWindows(usize),
NonFiniteMeanDhDl { lambda: f64, mean_dh_dl: f64 },
UnsortedWindows { previous: f64, next: f64 },
InvalidMoleculeIndex { mol_idx: usize, mol_count: usize },
AlchemicalMoleculeNotSet,
UnsupportedDevice(&'static str),
}
impl Display for AlchemicalError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::EmptySnapshots => write!(f, "snapshot slice is empty"),
Self::MissingDhDl => write!(f, "no dh/dlambda values were recorded in the snapshots"),
Self::InvalidLambda(lambda) => {
write!(f, "lambda must be finite and in [0, 1], got {lambda}")
}
Self::InvalidTemperature(temperature) => {
write!(
f,
"temperature must be finite and positive, got {temperature}"
)
}
Self::InvalidFreeEnergy(dg) => write!(f, "free energy must be finite, got {dg}"),
Self::NotEnoughWindows(n) => {
write!(f, "at least two lambda windows are required, got {n}")
}
Self::NonFiniteMeanDhDl { lambda, mean_dh_dl } => write!(
f,
"mean dh/dlambda must be finite for lambda {lambda}, got {mean_dh_dl}"
),
Self::UnsortedWindows { previous, next } => write!(
f,
"lambda windows must be strictly increasing, got {previous} followed by {next}"
),
Self::InvalidMoleculeIndex { mol_idx, mol_count } => write!(
f,
"alchemical molecule index {mol_idx} is out of range for {mol_count} molecules"
),
Self::AlchemicalMoleculeNotSet => {
write!(f, "no alchemical molecule has been configured")
}
Self::UnsupportedDevice(message) => write!(f, "{message}"),
}
}
}
impl Error for AlchemicalError {}
#[derive(Clone, Debug)]
pub struct LambdaWindow {
pub lambda: f64,
pub mean_dh_dl: f64,
pub sem_dh_dl: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct TiEstimate {
pub free_energy: f64,
pub standard_error: f64,
}
pub fn collect_window(
lambda: f64,
snapshots: &[Snapshot],
) -> Result<LambdaWindow, AlchemicalError> {
let dh_dl: Vec<f64> = snapshots
.iter()
.filter_map(|s| s.energy_data.as_ref()?.dh_dl.map(f64::from))
.collect();
if dh_dl.is_empty() {
return Err(AlchemicalError::MissingDhDl);
}
for &value in &dh_dl {
if !value.is_finite() {
return Err(AlchemicalError::NonFiniteMeanDhDl {
lambda,
mean_dh_dl: value,
});
}
}
let n = dh_dl.len() as f64;
let mean = dh_dl.iter().sum::<f64>() / n;
let sem = {
let variance = dh_dl.iter().map(|v| (*v - mean).powi(2)).sum::<f64>() / (n - 1.0);
(variance / n).sqrt()
};
Ok(LambdaWindow {
lambda,
mean_dh_dl: mean,
sem_dh_dl: sem,
})
}
pub fn free_energy_ti(windows: &[LambdaWindow]) -> Result<f64, AlchemicalError> {
if windows.len() < 2 {
return Err(AlchemicalError::NotEnoughWindows(windows.len()));
}
for window in windows {
if !window.mean_dh_dl.is_finite() {
return Err(AlchemicalError::NonFiniteMeanDhDl {
lambda: window.lambda,
mean_dh_dl: window.mean_dh_dl,
});
}
}
for pair in windows.windows(2) {
if pair[1].lambda <= pair[0].lambda {
return Err(AlchemicalError::UnsortedWindows {
previous: pair[0].lambda,
next: pair[1].lambda,
});
}
}
Ok(windows
.windows(2)
.map(|w| 0.5 * (w[0].mean_dh_dl + w[1].mean_dh_dl) * (w[1].lambda - w[0].lambda))
.sum())
}
pub fn free_energy_ti_with_sem(windows: &[LambdaWindow]) -> Result<TiEstimate, AlchemicalError> {
Ok(TiEstimate {
free_energy: free_energy_ti(windows)?,
standard_error: integrate_ti_sem(windows).unwrap(),
})
}
pub fn mean_coupled_interaction_kcal(snapshots: &[Snapshot]) -> Option<f32> {
let mut sum = 0.0;
let mut count = 0;
for dh_dl in snapshots
.iter()
.filter_map(|snap| snap.energy_data.as_ref()?.dh_dl)
.filter(|dh_dl| dh_dl.is_finite() && *dh_dl != 0.0)
{
sum += -dh_dl;
count += 1;
}
(count > 0).then_some(sum / count as f32)
}
fn integrate_ti_sem(windows: &[LambdaWindow]) -> Option<f64> {
let mut variance = 0.0;
for pair in windows.windows(2) {
let delta_lambda = pair[1].lambda - pair[0].lambda;
let prefactor = 0.5 * delta_lambda;
let sem_0 = pair[0].sem_dh_dl;
let sem_1 = pair[1].sem_dh_dl;
variance += prefactor.powi(2) * (sem_0.powi(2) + sem_1.powi(2));
}
Some(variance.sqrt())
}
impl MdState {
pub fn configure_alchemical_window(
&mut self,
dev: &ComputationDevice,
mol_idx: usize,
lambda: f64,
) -> Result<(), AlchemicalError> {
if !lambda.is_finite() || !(0.0..=1.0).contains(&lambda) {
return Err(AlchemicalError::InvalidLambda(lambda));
}
let mol_count = self.mol_start_indices.len();
if mol_idx >= mol_count {
return Err(AlchemicalError::InvalidMoleculeIndex { mol_idx, mol_count });
}
self.alchemical.mol_idx = Some(mol_idx);
self.alchemical.lambda = lambda;
self.alchemical.dh_dl = 0.0;
self.spme_force_prev = None;
self.build_all_neighbors(dev);
Ok(())
}
pub fn clear_alchemical_window(&mut self, dev: &ComputationDevice) {
self.alchemical.mol_idx = None;
self.alchemical.lambda = 0.0;
self.alchemical.dh_dl = 0.0;
self.spme_force_prev = None;
self.build_all_neighbors(dev);
}
pub(crate) fn alchemical_atom_range(&self) -> Option<(usize, usize)> {
let mol_idx = self.alchemical.mol_idx?;
let start = *self.mol_start_indices.get(mol_idx)?;
let end = self
.mol_start_indices
.get(mol_idx + 1)
.copied()
.unwrap_or(self.atoms.len());
Some((start, end))
}
}