use crate::{twobody::IsotropicTwobodyEnergy, Cutoff};
use coulomb::{
pairwise::MultipoleEnergy,
permittivity::{ConstantPermittivity, RelativePermittivity},
};
#[cfg(feature = "serde")]
use serde::Serialize;
use std::fmt::{Debug, Display};
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct IonIon<T: MultipoleEnergy> {
#[cfg_attr(feature = "serde", serde(rename = "z₁z₂", alias = "z1z2"))]
charge_product: f64,
#[cfg_attr(feature = "serde", serde(skip))]
scheme: T,
permittivity: ConstantPermittivity,
}
impl<T: MultipoleEnergy + Display> Display for IonIon<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}, {}", self.scheme, self.permittivity)
}
}
impl<T: MultipoleEnergy> coulomb::DebyeLength for IonIon<T> {
fn kappa(&self) -> Option<f64> {
self.scheme.kappa()
}
fn set_debye_length(&mut self, debye_length: Option<f64>) -> Result<(), coulomb::Error> {
self.scheme.set_debye_length(debye_length)
}
fn debye_length(&self) -> Option<f64> {
self.scheme.debye_length()
}
}
impl<T: MultipoleEnergy> RelativePermittivity for IonIon<T> {
fn permittivity(&self, _temperature: f64) -> Result<f64, coulomb::Error> {
Ok(f64::from(self.permittivity))
}
fn set_permittivity(&mut self, permittivity: f64) -> Result<(), coulomb::Error> {
self.permittivity = ConstantPermittivity::new(permittivity);
Ok(())
}
}
impl<T: MultipoleEnergy> IonIon<T> {
pub const fn new(charge_product: f64, permittivity: ConstantPermittivity, scheme: T) -> Self {
Self {
charge_product,
permittivity,
scheme,
}
}
}
impl<T: MultipoleEnergy + Debug + Clone + PartialEq + Send + Sync> IsotropicTwobodyEnergy
for IonIon<T>
{
fn isotropic_twobody_energy(&self, distance_squared: f64) -> f64 {
coulomb::TO_CHEMISTRY_UNIT / f64::from(self.permittivity)
* self
.scheme
.ion_ion_energy(self.charge_product, 1.0, distance_squared.sqrt())
}
}
impl<T: MultipoleEnergy + Cutoff> Cutoff for IonIon<T> {
fn cutoff(&self) -> f64 {
self.scheme.cutoff()
}
fn lower_cutoff(&self) -> f64 {
self.scheme.lower_cutoff()
}
}
pub type IonIonYukawa = IonIon<coulomb::pairwise::Yukawa>;
pub type IonIonPlain = IonIon<coulomb::pairwise::Plain>;
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct IonIonPolar<T: MultipoleEnergy> {
pub ionion: IonIon<T>,
charges: (f64, f64),
polarizabilities: (f64, f64),
}
impl<T: MultipoleEnergy> IonIonPolar<T> {
pub const fn new(ionion: IonIon<T>, charges: (f64, f64), polarizabilities: (f64, f64)) -> Self {
Self {
ionion,
charges,
polarizabilities,
}
}
}
impl<T: MultipoleEnergy + Debug + Clone + PartialEq + Send + Sync> IsotropicTwobodyEnergy
for IonIonPolar<T>
{
fn isotropic_twobody_energy(&self, distance_squared: f64) -> f64 {
let r = distance_squared.sqrt();
let scheme = &self.ionion.scheme;
let ion_ion = scheme.ion_ion_energy(self.ionion.charge_product, 1.0, r);
let rv = [r, 0.0, 0.0];
let neg_rv = [-r, 0.0, 0.0];
let ion_induced_dipole =
scheme.ion_induced_dipole_energy(self.charges.0, self.polarizabilities.1, rv)
+ scheme.ion_induced_dipole_energy(self.charges.1, self.polarizabilities.0, neg_rv);
let to_kjmol = coulomb::TO_CHEMISTRY_UNIT / f64::from(self.ionion.permittivity);
to_kjmol * (ion_ion + ion_induced_dipole)
}
}
impl<T: MultipoleEnergy + Cutoff> Cutoff for IonIonPolar<T> {
fn cutoff(&self) -> f64 {
self.ionion.cutoff()
}
fn lower_cutoff(&self) -> f64 {
self.ionion.lower_cutoff()
}
}
impl<T: MultipoleEnergy + Display> Display for IonIonPolar<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"IonIonPolar({}, {:?})",
self.ionion, self.polarizabilities
)
}
}
#[cfg(test)]
mod tests {
use core::f64;
use approx::assert_relative_eq;
use super::*;
use coulomb::{pairwise::Plain, DebyeLength};
#[test]
fn test_ion_ion() {
let permittivity = ConstantPermittivity::new(80.0);
let r: f64 = 7.0;
let cutoff = f64::INFINITY;
let scheme = Plain::new(cutoff, None);
let ionion = IonIon::new(1.0, permittivity, scheme);
let unscreened_energy = ionion.isotropic_twobody_energy(r.powi(2));
assert_relative_eq!(unscreened_energy, 2.48099031507825);
let debye_length = 30.0;
let scheme = Plain::new(cutoff, Some(debye_length));
let ionion = IonIon::new(1.0, permittivity, scheme);
let screened_energy = ionion.isotropic_twobody_energy(r.powi(2));
assert_relative_eq!(
screened_energy,
unscreened_energy * (-r / debye_length).exp()
);
}
#[test]
fn test_ion_ion_polar() {
let permittivity = ConstantPermittivity::new(80.0);
let r: f64 = 8.0;
let charges = (1.0, -1.0);
let cutoff = f64::INFINITY;
let polarizabilities = (100.0, 80.0);
let scheme = Plain::new(cutoff, None);
let ionion = IonIon::new(charges.0 * charges.1, permittivity, scheme);
let ionion_polar = IonIonPolar::new(ionion.clone(), charges, polarizabilities);
let energy = ionion_polar.isotropic_twobody_energy(r.powi(2));
assert_relative_eq!(energy, -2.5524641571630236);
let induced_energy = energy - ionion.isotropic_twobody_energy(r.powi(2));
assert_relative_eq!(induced_energy, -0.38159763146955505);
assert_relative_eq!(
induced_energy,
-(100.0 + 80.0) * coulomb::TO_CHEMISTRY_UNIT
/ (2.0 * f64::from(permittivity) * r.powi(4))
);
let scheme = Plain::new(cutoff, Some(30.0));
let ionion = IonIon::new(charges.0 * charges.1, permittivity, scheme.clone());
let ionion_polar = IonIonPolar::new(ionion.clone(), charges, polarizabilities);
let energy = ionion_polar.isotropic_twobody_energy(r.powi(2));
assert_relative_eq!(energy, -2.021903629249571);
let induced_energy = energy - ionion.isotropic_twobody_energy(r.powi(2));
let kappa = 1.0 / scheme.debye_length().unwrap();
assert_relative_eq!(induced_energy, -0.3591754384137349);
assert_relative_eq!(
induced_energy,
-(polarizabilities.0 + polarizabilities.1) * coulomb::TO_CHEMISTRY_UNIT
/ (2.0 * f64::from(permittivity))
* (f64::exp(-r * kappa) * (1.0 / r.powi(2) + kappa / r)).powi(2),
epsilon = 1e-7
);
assert_relative_eq!(
induced_energy,
-(polarizabilities.0 + polarizabilities.1) * coulomb::TO_CHEMISTRY_UNIT
/ (2.0 * f64::from(permittivity))
* f64::exp(-2.0 * r * kappa)
* (1.0 / r.powi(2) + kappa / r).powi(2),
epsilon = 1e-7
);
}
}