use crate::prelude::*;
use nalgebra::{Const, Matrix, storage::Storage};
use thiserror::Error;
#[derive(Debug, Default, Clone)]
pub struct PeriodicBox {
matrix: Matrix3f,
inv: Matrix3f,
tric_corrections: Vec<Vector3f>,
}
fn build_tric_corrections(m: &Matrix3f) -> Vec<Vector3f> {
if m[(0, 1)] == 0.0
&& m[(0, 2)] == 0.0
&& m[(1, 0)] == 0.0
&& m[(1, 2)] == 0.0
&& m[(2, 0)] == 0.0
&& m[(2, 1)] == 0.0
{
return Vec::new();
}
let a: Vector3f = m.column(0).into();
let b: Vector3f = m.column(1).into();
let c: Vector3f = m.column(2).into();
let half_diag = 0.5
* (a + b + c)
.norm()
.max((a + b - c).norm())
.max((a - b + c).norm())
.max((-a + b + c).norm());
let bound2 = (2.0 * half_diag).powi(2);
let mut out = Vec::with_capacity(26);
for i in -1_i32..=1 {
for j in -1_i32..=1 {
for k in -1_i32..=1 {
if i == 0 && j == 0 && k == 0 {
continue;
}
let s: Vector3f = (i as f32) * a + (j as f32) * b + (k as f32) * c;
if s.norm_squared() < bound2 {
out.push(s);
}
}
}
}
out
}
#[derive(Debug,PartialEq,Clone,Copy)]
pub struct PbcDims(u8);
impl PbcDims {
pub fn set_dim(&mut self, n: usize, val: bool) {
if n>2 {
panic!("pbc has only 3 dimentions")
}
if val {
self.0 |= 1 << n;
} else {
self.0 &= !(1 << n);
}
}
pub fn new(x: bool, y: bool, z: bool) -> Self {
let mut ret = Self(0);
ret.set_dim(0, x);
ret.set_dim(1, y);
ret.set_dim(2, z);
ret
}
pub fn get_dim(&self, n: usize) -> bool {
if n>2 {
panic!("pbc has only 3 dimentions")
}
(self.0 & (1 << n)) != 0
}
pub fn any(&self) -> bool {
(self.0 & (1 << 0)) != 0
||
(self.0 & (1 << 1)) != 0
||
(self.0 & (1 << 2)) != 0
}
}
pub const PBC_FULL: PbcDims = PbcDims(0b0000_0111);
pub const PBC_NONE: PbcDims = PbcDims(0b0000_0000);
#[derive(Error,Debug)]
pub enum PeriodicBoxError {
#[error("pbc operation withon periodic box")]
NoPbc,
#[error("zero length box vector")]
ZeroLengthVector,
#[error("box matrix inverse failed")]
InverseFailed,
#[error("box angle is <60 deg")]
AngleTooSmall,
}
impl PeriodicBox {
pub fn from_matrix<S>(matrix: Matrix<f32,Const<3>,Const<3>,S>) -> Result<Self, PeriodicBoxError>
where S: Storage<f32, Const<3>, Const<3>>
{
for col in matrix.column_iter() {
if col.norm() == 0.0 {
Err(PeriodicBoxError::ZeroLengthVector)?
}
}
let matrix = matrix.clone_owned();
let inv = matrix
.try_inverse()
.ok_or_else(|| PeriodicBoxError::InverseFailed)?;
let tric_corrections = build_tric_corrections(&matrix);
Ok(Self {
matrix,
inv,
tric_corrections,
})
}
pub fn from_vectors_angles(
a: f32,
b: f32,
c: f32,
alpha: f32,
beta: f32,
gamma: f32,
) -> Result<Self, PeriodicBoxError> {
let mut m = Matrix3f::zeros();
if a == 0.0 || b == 0.0 || c == 0.0 {
Err(PeriodicBoxError::ZeroLengthVector)?;
}
if alpha < 60.0 || beta < 60.0 || gamma < 60.0 {
Err(PeriodicBoxError::AngleTooSmall)?;
}
m[(0, 0)] = a;
if alpha != 90.0 || beta != 90.0 || gamma != 90.0 {
let cosa = if alpha != 90.0 {
alpha.to_radians().cos()
} else {
0.0
};
let cosb = if beta != 90.0 {
beta.to_radians().cos()
} else {
0.0
};
let (sing, cosg) = if gamma != 90.0 {
gamma.to_radians().sin_cos()
} else {
(1.0, 0.0)
};
m[(0, 1)] = b * cosg;
m[(1, 1)] = b * sing;
m[(0, 2)] = c * cosb;
m[(1, 2)] = c * (cosa - cosb * cosg) / sing;
m[(2, 2)] = (c * c - m[(0, 2)].powf(2.0) - m[(1, 2)].powf(2.0)).sqrt();
} else {
m[(1, 1)] = b;
m[(2, 2)] = c;
}
Self::from_matrix(m)
}
pub fn to_vectors_angles(&self) -> (Vector3f, Vector3f) {
let mut vectors = Vector3f::zeros();
let mut angles = Vector3f::zeros();
let vx = self.matrix.column(0);
let vy = self.matrix.column(1);
let vz = self.matrix.column(2);
angles[0] = if vy.norm_squared() * vz.norm_squared() != 0.0 {
vy.angle(&vz).to_degrees()
} else {
90.0
};
angles[1] = if vx.norm_squared() * vz.norm_squared() != 0.0 {
vx.angle(&vz).to_degrees()
} else {
90.0
};
angles[2] = if vx.norm_squared() * vy.norm_squared() != 0.0 {
vx.angle(&vy).to_degrees()
} else {
90.0
};
vectors[0] = vx.norm();
vectors[1] = vy.norm();
vectors[2] = vz.norm();
(vectors, angles)
}
#[inline(always)]
pub fn shortest_vector<S>(&self, vec: &nalgebra::Vector<f32,Const<3>,S>) -> Vector3f
where S: Storage<f32, Const<3>>,
{
self.shortest_vector_dims(vec, PBC_FULL)
}
#[inline(always)]
pub fn shortest_vector_dims<S>(&self, vec: &nalgebra::Vector<f32,Const<3>,S>, pbc_dims: PbcDims) -> Vector3f
where S: Storage<f32, Const<3>>,
{
let mut box_vec = self.inv * vec;
for i in 0..3 {
if pbc_dims.get_dim(i) {
box_vec[i] -= box_vec[i].round();
}
}
let start = self.matrix * box_vec;
if self.tric_corrections.is_empty() || pbc_dims != PBC_FULL {
return start;
}
let mut best = start;
let mut best2 = start.norm_squared();
for s in &self.tric_corrections {
let cand = start + s;
let n2 = cand.norm_squared();
if n2 < best2 {
best2 = n2;
best = cand;
}
}
best
}
#[inline(always)]
pub fn closest_image(&self, point: &Pos, target: &Pos) -> Pos {
target + self.shortest_vector(&(point - target))
}
#[inline(always)]
pub fn closest_image_dims(&self, point: &Pos, target: &Pos, pbc_dims: PbcDims) -> Pos {
target + self.shortest_vector_dims(&(point - target), pbc_dims)
}
#[inline(always)]
pub fn get_matrix(&self) -> Matrix3f {
self.matrix
}
#[inline(always)]
pub fn to_box_coords<S>(&self, vec: &nalgebra::Vector<f32,Const<3>,S>) -> Vector3f
where S: Storage<f32, Const<3>>,
{
self.inv * vec
}
#[inline(always)]
pub fn is_inside(&self, point: &Pos) -> bool {
let v = self.inv * point;
v[0]<1.0 && v[1]<1.0 && v[2]<1.0
&& v[0]>=0.0 && v[1]>=0.0 && v[2]>=0.0
}
#[inline(always)]
pub fn to_lab_coords<S>(&self, vec: &nalgebra::Vector<f32,Const<3>,S>) -> Vector3f
where S: Storage<f32, Const<3>>,
{
self.matrix * vec
}
#[inline(always)]
pub fn get_box_extents(&self) -> Vector3f {
Vector3f::from_iterator(self.matrix.column_iter().map(|c| c.norm()))
}
pub fn get_lab_extents(&self) -> Vector3f {
Vector3f::new(
self.matrix[(0, 0)] + self.matrix[(0, 1)] + self.matrix[(0, 2)],
self.matrix[(1, 0)] + self.matrix[(1, 1)] + self.matrix[(1, 2)],
self.matrix[(2, 0)] + self.matrix[(2, 1)] + self.matrix[(2, 2)],
)
}
#[inline(always)]
pub fn distance_squared(&self, p1: &Pos, p2: &Pos, pbc_dims: PbcDims) -> f32 {
self.shortest_vector_dims(&(p2 - p1), pbc_dims).norm_squared()
}
#[inline(always)]
pub fn distance(&self, p1: &Pos, p2: &Pos, pbc: PbcDims) -> f32 {
self.distance_squared(p1, p2, pbc).sqrt()
}
pub fn is_triclinic(&self) -> bool {
self.matrix[(0,1)]!=0.0 || self.matrix[(0,2)]!=0.0 ||
self.matrix[(1,0)]!=0.0 || self.matrix[(1,2)]!=0.0 ||
self.matrix[(2,0)]!=0.0 || self.matrix[(2,1)]!=0.0
}
pub(crate) fn scale_vectors(&mut self, scale_factors: [f32; 3]) -> Result<(),PeriodicBoxError> {
for c in 0..3 {
self.matrix.column_mut(c).apply(|el| *el *= scale_factors[c]);
}
self.inv = self.matrix
.try_inverse()
.ok_or_else(|| PeriodicBoxError::InverseFailed)?;
Ok(())
}
#[inline(always)]
pub fn wrap_point(&self, p: &Pos) -> Pos {
let mut bv = self.inv * p;
for i in 0..3 {
bv[i] = bv[i].fract();
if bv[i] < 0.0 {
bv[i] = 1.0 - bv[i];
}
}
return self.matrix * bv;
}
#[inline(always)]
pub fn wrap_vec<S>(&self, vec: &nalgebra::Vector<f32,Const<3>,S>) -> Vector3f
where S: Storage<f32, Const<3>>,
{
let mut bv = self.inv * vec;
for i in 0..3 {
bv[i] = bv[i].fract();
if bv[i] < 0.0 {
bv[i] = 1.0 - bv[i];
}
}
return self.matrix * bv;
}
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
use super::PeriodicBox;
const EPSILON: f32 = 1e-6;
fn assert_vec_eq(v1: &Vector3f, v2: &Vector3f) {
assert!((v1 - v2).norm() < EPSILON, "Vectors not equal: {:?} != {:?}", v1, v2);
}
#[test]
#[should_panic]
fn invalid_from_vec_ang() {
let _b = PeriodicBox::from_vectors_angles(
10.0,0.2,15.0, 90.0, 9.0, 90.0
).unwrap();
}
#[test]
fn test_shortest_vector_dims_no_pbc() {
let box_matrix = Matrix3f::from_diagonal(&Vector3f::new(10.0, 10.0, 10.0));
let pbox = PeriodicBox::from_matrix(box_matrix).unwrap();
let test_vec = Vector3f::new(8.0, 8.0, 8.0);
let result = pbox.shortest_vector_dims(&test_vec, PBC_NONE);
assert_vec_eq(&result, &test_vec);
}
#[test]
fn test_shortest_vector_dims_full_pbc() {
let box_matrix = Matrix3f::from_diagonal(&Vector3f::new(10.0, 10.0, 10.0));
let pbox = PeriodicBox::from_matrix(box_matrix).unwrap();
let test_vec = Vector3f::new(8.0, 8.0, 8.0);
let result = pbox.shortest_vector_dims(&test_vec, PBC_FULL);
assert_vec_eq(&result, &Vector3f::new(-2.0, -2.0, -2.0));
}
#[test]
fn test_shortest_vector_dims_x_only() {
let box_matrix = Matrix3f::from_diagonal(&Vector3f::new(10.0, 10.0, 10.0));
let pbox = PeriodicBox::from_matrix(box_matrix).unwrap();
let test_vec = Vector3f::new(8.0, 8.0, 8.0);
let pbc_x = PbcDims::new(true, false, false);
let result = pbox.shortest_vector_dims(&test_vec, pbc_x);
assert_vec_eq(&result, &Vector3f::new(-2.0, 8.0, 8.0));
}
#[test]
fn test_shortest_vector_dims_xy_only() {
let box_matrix = Matrix3f::from_diagonal(&Vector3f::new(10.0, 10.0, 10.0));
let pbox = PeriodicBox::from_matrix(box_matrix).unwrap();
let test_vec = Vector3f::new(8.0, 8.0, 8.0);
let pbc_xy = PbcDims::new(true, true, false);
let result = pbox.shortest_vector_dims(&test_vec, pbc_xy);
assert_vec_eq(&result, &Vector3f::new(-2.0, -2.0, 8.0));
}
#[test]
fn test_closest_image_no_pbc() {
let box_matrix = Matrix3f::from_diagonal(&Vector3f::new(10.0, 10.0, 10.0));
let pbox = PeriodicBox::from_matrix(box_matrix).unwrap();
let point = Pos::new(8.0, 8.0, 8.0);
let target = Pos::origin();
let result = pbox.closest_image_dims(&point, &target, PBC_NONE);
assert_vec_eq(&result.coords, &point.coords);
}
#[test]
fn test_closest_image_full_pbc() {
let box_matrix = Matrix3f::from_diagonal(&Vector3f::new(10.0, 10.0, 10.0));
let pbox = PeriodicBox::from_matrix(box_matrix).unwrap();
let point = Pos::new(8.0, 8.0, 8.0);
let target = Pos::origin();
let result = pbox.closest_image_dims(&point, &target, PBC_FULL);
assert_vec_eq(&result.coords, &Pos::new(-2.0, -2.0, -2.0).coords);
}
#[test]
fn test_closest_image_x_only() {
let box_matrix = Matrix3f::from_diagonal(&Vector3f::new(10.0, 10.0, 10.0));
let pbox = PeriodicBox::from_matrix(box_matrix).unwrap();
let point = Pos::new(8.0, 8.0, 8.0);
let target = Pos::origin();
let pbc_x = PbcDims::new(true, false, false);
let result = pbox.closest_image_dims(&point, &target, pbc_x);
assert_vec_eq(&result.coords, &Pos::new(-2.0, 8.0, 8.0).coords);
}
#[test]
fn test_closest_image_xy_only() {
let box_matrix = Matrix3f::from_diagonal(&Vector3f::new(10.0, 10.0, 10.0));
let pbox = PeriodicBox::from_matrix(box_matrix).unwrap();
let point = Pos::new(8.0, 8.0, 8.0);
let target = Pos::origin();
let pbc_xy = PbcDims::new(true, true, false);
let result = pbox.closest_image_dims(&point, &target, pbc_xy);
assert_vec_eq(&result.coords, &Pos::new(-2.0, -2.0, 8.0).coords);
}
#[test]
fn test_orthogonal_has_no_tric_corrections() {
let box_matrix = Matrix3f::from_diagonal(&Vector3f::new(10.0, 20.0, 30.0));
let pbox = PeriodicBox::from_matrix(box_matrix).unwrap();
assert!(pbox.tric_corrections.is_empty());
}
#[test]
fn test_triclinic_mdtraj_box_matches_brute_force() {
let box_matrix = Matrix3f::new(
10.0, 4.0, -4.0,
0.0, 10.0, 0.0,
0.0, 0.0, 10.0,
);
let pbox = PeriodicBox::from_matrix(box_matrix).unwrap();
let p1 = Pos::new( 38.9214, 40.0078, -34.0795);
let p2 = Pos::new(-26.6187, 40.8926, 30.9709);
let d = pbox.distance(&p1, &p2, PBC_FULL);
assert!(
(d - 5.353627).abs() < 1e-3,
"expected ~5.353627, got {d} (prior buggy value ~5.597546)"
);
}
#[test]
fn test_triclinic_corner_matches_brute_force() {
let box_matrix = Matrix3f::new(
6.0, 0.0, 3.0,
0.0, 6.0, 3.0,
0.0, 0.0, 6.0,
);
let pbox = PeriodicBox::from_matrix(box_matrix).unwrap();
let dx = Vector3f::new(2.9, 2.9, 2.9);
let a: Vector3f = box_matrix.column(0).into();
let b: Vector3f = box_matrix.column(1).into();
let c: Vector3f = box_matrix.column(2).into();
let mut best = f32::INFINITY;
for i in -2..=2i32 { for j in -2..=2i32 { for k in -2..=2i32 {
let cand = dx + (i as f32)*a + (j as f32)*b + (k as f32)*c;
best = best.min(cand.norm());
}}}
let got = pbox.shortest_vector(&dx).norm();
assert!((got - best).abs() < 1e-5, "expected {best}, got {got}");
}
#[test]
fn test_triclinic_far_apart_reduction() {
let box_matrix = Matrix3f::new(
10.0, 4.0, -4.0,
0.0, 10.0, 0.0,
0.0, 0.0, 10.0,
);
let pbox = PeriodicBox::from_matrix(box_matrix).unwrap();
let p1 = Pos::new(0.1, 0.2, 0.3);
let p2 = Pos::new(60.1, 0.2, 0.3);
let d = pbox.distance(&p1, &p2, PBC_FULL);
assert!(d < 1e-4, "pure a-vector shift should collapse to ~0, got {d}");
}
}