use serde::{Deserialize, Serialize};
use super::AnisotropicEnergy;
use crate::univariate::UnivariateEnergy;
use hoomd_vector::{InnerProduct, Rotate, Unit, Vector};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Patch<V> {
pub director: Unit<V>,
pub cos_delta: f64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AngularMask<E, V> {
pub isotropic: E,
pub masks_i: Vec<Patch<V>>,
pub masks_j: Vec<Patch<V>>,
}
impl<E, V> AngularMask<E, V>
where
V: Vector,
{
#[inline]
#[must_use]
pub fn new<I>(isotropic: E, masks: I) -> Self
where
I: IntoIterator<Item = Patch<V>>,
{
let masks = Vec::from_iter(masks);
Self {
isotropic,
masks_i: masks.clone(),
masks_j: masks,
}
}
}
impl<E, V, R> AnisotropicEnergy<V, R> for AngularMask<E, V>
where
E: UnivariateEnergy,
V: InnerProduct,
R: Rotate<V> + Into<R::Matrix> + Copy,
{
#[inline]
fn energy(&self, r_ij: &V, o_ij: &R) -> f64 {
let o_ij_matrix: R::Matrix = (*o_ij).into();
let (unit_r_ij, r_ij_norm) = r_ij.to_unit_unchecked();
let unit_r_ji = -(*unit_r_ij.get());
for mask_j in &self.masks_j {
let d_j = o_ij_matrix.rotate(mask_j.director.get());
for mask_i in &self.masks_i {
if mask_i.director.get().dot(unit_r_ij.get()) >= mask_i.cos_delta
&& d_j.dot(&unit_r_ji) >= mask_j.cos_delta
{
return self.isotropic.energy(r_ij_norm);
}
}
}
0.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use approxim::assert_relative_eq;
use rstest::*;
use std::f64::consts::PI;
use crate::univariate::{Boxcar, LennardJones};
use hoomd_vector::{Angle, Cartesian, InnerProduct, Versor};
#[test]
fn single_patch_2d() {
let epsilon = 1.125;
let boxcar = Boxcar {
epsilon,
left: 0.0,
right: 1000.0,
};
let mask = [Patch {
director: [1.0, 0.0]
.try_into()
.expect("hard-coded vector should have non-zero length"),
cos_delta: (PI / 8.0).cos(),
}];
let angular_mask = AngularMask::new(boxcar.clone(), mask);
assert_eq!(
angular_mask.energy(&Cartesian::from([1.0, 0.0]), &Angle::from(0.0)),
0.0
);
assert_eq!(
angular_mask.energy(&Cartesian::from([1.0, 0.0]), &Angle::from(PI)),
epsilon
);
assert_eq!(
angular_mask.energy(
&Cartesian::from([1.0, 0.0]),
&Angle::from(PI + PI / 8.0 - 0.001)
),
epsilon
);
assert_eq!(
angular_mask.energy(
&Cartesian::from([1.0, 0.0]),
&Angle::from(PI + PI / 8.0 + 0.001)
),
0.0
);
assert_eq!(
angular_mask.energy(
&Cartesian::from([1.0, 0.0]),
&Angle::from(PI + PI / 8.0 + 0.001)
),
0.0
);
for theta in (0..100).map(|x| f64::from(x) * 2.0 * PI / 100.0) {
assert_eq!(
angular_mask.energy(&Cartesian::from([0.0, 1.0]), &Angle::from(theta)),
0.0
);
}
let mask = [Patch {
director: [1.0, 1.0]
.try_into()
.expect("hard-coded vector should have non-zero length"),
cos_delta: (PI / 3.0).cos(),
}];
let angular_mask = AngularMask::new(boxcar, mask);
assert_eq!(
angular_mask.energy(&Cartesian::from([1.0, 1.0]), &Angle::from(0.0)),
0.0
);
assert_eq!(
angular_mask.energy(&Cartesian::from([1.0, 1.0]), &Angle::from(PI)),
epsilon
);
assert_eq!(
angular_mask.energy(
&Cartesian::from([1.0, 1.0]),
&Angle::from(PI + PI / 3.0 - 0.001)
),
epsilon
);
assert_eq!(
angular_mask.energy(
&Cartesian::from([1.0, 1.0]),
&Angle::from(PI + PI / 3.0 + 0.001)
),
0.0
);
assert_eq!(
angular_mask.energy(
&Cartesian::from([1.0, 1.0]),
&Angle::from(PI + PI / 3.0 + 0.001)
),
0.0
);
assert_eq!(
angular_mask.energy(
&Cartesian::from([1.0, 1.0]),
&Angle::from(PI + PI / 3.0 + 0.001)
),
0.0
);
assert_eq!(
angular_mask.energy(&Cartesian::from([0.0, 1.0]), &Angle::from(0.0)),
0.0
);
assert_eq!(
angular_mask.energy(&Cartesian::from([0.0, 1.0]), &Angle::from(-3.0 * PI / 4.0)),
epsilon
);
}
#[rstest]
#[case([0.0, 1.0].into(), 0.0, 1.0)]
#[case([0.0, 1.0].into(), PI / 2.0, 0.0)]
#[case([0.0, 1.0].into(), PI, 1.0)]
#[case([0.0, -1.0].into(), 0.0, 1.0)]
#[case([0.0, -1.0].into(), PI / 2.0, 0.0)]
#[case([0.0, -1.0].into(), PI, 1.0)]
#[case([1.0, 0.0].into(), 0.0, 0.0)]
#[case([1.0, 0.0].into(), PI / 2.0, 1.0)]
#[case([1.0, 0.0].into(), PI, 0.0)]
#[case([-1.0, 0.0].into(), 0.0, 0.0)]
#[case([-1.0, 0.0].into(), PI / 2.0, 1.0)]
#[case([-1.0, 0.0].into(), PI, 0.0)]
fn multiple_patches_2d(#[case] r_ij: Cartesian<2>, #[case] theta: f64, #[case] expected: f64) {
let epsilon = 1.0;
let boxcar = Boxcar {
epsilon,
left: 0.0,
right: 1000.0,
};
let masks_i = vec![
Patch {
director: [0.0, 1.0]
.try_into()
.expect("hard-coded vector should have non-zero length"),
cos_delta: (PI / 8.0).cos(),
},
Patch {
director: [0.0, -1.0]
.try_into()
.expect("hard-coded vector should have non-zero length"),
cos_delta: (PI / 8.0).cos(),
},
Patch {
director: [1.0, 0.0]
.try_into()
.expect("hard-coded vector should have non-zero length"),
cos_delta: (PI / 8.0).cos(),
},
Patch {
director: [-1.0, 0.0]
.try_into()
.expect("hard-coded vector should have non-zero length"),
cos_delta: (PI / 8.0).cos(),
},
];
let masks_j = vec![
Patch {
director: [0.0, 1.0]
.try_into()
.expect("hard-coded vector should have non-zero length"),
cos_delta: (PI / 8.0).cos(),
},
Patch {
director: [0.0, -1.0]
.try_into()
.expect("hard-coded vector should have non-zero length"),
cos_delta: (PI / 8.0).cos(),
},
];
let angular_mask = AngularMask {
isotropic: boxcar,
masks_i,
masks_j,
};
assert_eq!(angular_mask.energy(&r_ij, &Angle::from(theta)), expected);
}
#[rstest]
fn smooth_potential(#[values(0.9, 1.1, 1.2, 3.0)] r: f64) {
let epsilon = 1.0;
let sigma = 1.0;
let lj: LennardJones = LennardJones { epsilon, sigma };
let mask = [Patch {
director: [1.0, 0.0]
.try_into()
.expect("hard-coded vector should have non-zero length"),
cos_delta: (PI).cos(),
}];
let angular_mask = AngularMask::new(lj.clone(), mask);
for theta in (0..100).map(|x| f64::from(x) * 2.0 * PI / 100.0) {
let r_ij = Angle::from(theta).rotate(&Cartesian::from([0.0, r]));
assert_relative_eq!(
angular_mask.energy(&r_ij, &Angle::from(0.0)),
lj.energy(r),
epsilon = 1e-12
);
}
}
#[test]
fn single_patch_3d() {
let epsilon = 1.125;
let boxcar = Boxcar {
epsilon,
left: 0.0,
right: 1000.0,
};
let mask = [Patch {
director: [0.0, 0.0, 1.0]
.try_into()
.expect("hard-coded vector should have non-zero length"),
cos_delta: (PI / 8.0).cos(),
}];
let angular_mask = AngularMask::new(boxcar, mask);
let (x_axis, _) = Cartesian::from([1.0, 0.0, 0.0]).to_unit_unchecked();
let (y_axis, _) = Cartesian::from([1.0, 0.0, 0.0]).to_unit_unchecked();
assert_eq!(
angular_mask.energy(
&Cartesian::from([0.0, 0.0, 1.0]),
&Versor::from_axis_angle(x_axis, 0.0)
),
0.0
);
assert_eq!(
angular_mask.energy(
&Cartesian::from([0.0, 0.0, 1.0]),
&Versor::from_axis_angle(y_axis, PI)
),
epsilon
);
assert_eq!(
angular_mask.energy(
&Cartesian::from([0.0, 0.0, 1.0]),
&Versor::from_axis_angle(x_axis, PI + PI / 8.0 - 0.001)
),
epsilon
);
assert_eq!(
angular_mask.energy(
&Cartesian::from([0.0, 0.0, 1.0]),
&Versor::from_axis_angle(y_axis, PI + PI / 8.0 + 0.001)
),
0.0
);
assert_eq!(
angular_mask.energy(
&Cartesian::from([0.0, 0.0, 1.0]),
&Versor::from_axis_angle(x_axis, PI + PI / 8.0 + 0.001)
),
0.0
);
for theta in (0..100).map(|x| f64::from(x) * 2.0 * PI / 100.0) {
assert_eq!(
angular_mask.energy(
&Cartesian::from([0.0, 1.0, 0.0]),
&Versor::from_axis_angle(x_axis, theta)
),
0.0
);
}
}
}