use crate::{utils, voxel_map::EncodedImage};
use std::cmp::Ordering::Less;
pub struct Atoms {
pub lattice: Lattice,
pub positions: Vec<[f64; 3]>,
pub text: String,
pub reduced_positions: Vec<[f64; 3]>,
}
impl Atoms {
pub fn new(
lattice: Lattice,
positions: Vec<[f64; 3]>,
text: String,
) -> Self {
let reduced_positions = positions
.iter()
.map(|p| lattice.cartesian_to_reduced(*p))
.collect::<Vec<[f64; 3]>>();
Self {
lattice,
positions,
text,
reduced_positions,
}
}
}
pub struct Lattice {
pub cartesian_shift_matrix: [[f64; 3]; 27],
pub to_fractional: [[f64; 3]; 3],
pub to_cartesian: [[f64; 3]; 3],
pub reduced_cartesian_shift_matrix: [[f64; 3]; 27],
pub reduced_grid_shift_matrix: Vec<Vec<usize>>,
pub reduced_to_fractional: [[f64; 3]; 3],
pub reduced_to_cartesian: [[f64; 3]; 3],
pub volume: f64,
}
impl Lattice {
pub fn new(lattice: [[f64; 3]; 3]) -> Self {
let cartesian_shift_matrix =
Lattice::create_cartesian_shift_matrix(&lattice);
let to_fractional = match utils::invert_lattice(&lattice) {
Some(inv) => inv,
None => panic!("Supplied lattice does not span 3d space."),
};
let reduced_lattice = lll_lattice(lattice);
let reduced_cartesian_shift_matrix =
Lattice::create_cartesian_shift_matrix(&reduced_lattice);
let reduced_to_fractional =
match utils::invert_lattice(&reduced_lattice) {
Some(inv) => inv,
None => panic!("Supplied lattice does not span 3d space."),
};
let reduced_grid_shift_matrix = Lattice::create_grid_shift_matrix(
&reduced_cartesian_shift_matrix,
&reduced_to_fractional,
);
let volume =
utils::vdot(lattice[0], utils::cross(lattice[1], lattice[2])).abs();
let to_cartesian = lattice;
let reduced_to_cartesian = reduced_lattice;
Self {
cartesian_shift_matrix,
to_fractional,
to_cartesian,
reduced_cartesian_shift_matrix,
reduced_grid_shift_matrix,
reduced_to_fractional,
reduced_to_cartesian,
volume,
}
}
pub fn fractional_to_reduced(&self, p: [f64; 3]) -> [f64; 3] {
self.cartesian_to_reduced(utils::dot(p, self.to_cartesian))
}
pub fn fractional_to_cartesian(&self, p: [f64; 3]) -> [f64; 3] {
utils::dot(p, self.to_cartesian)
}
pub fn cartesian_to_reduced(&self, p: [f64; 3]) -> [f64; 3] {
let pn = utils::dot(p, self.reduced_to_fractional)
.iter()
.map(|p| p - p.floor())
.collect::<Vec<f64>>()
.try_into()
.unwrap();
utils::dot(pn, self.reduced_to_cartesian)
}
pub fn minimum_distance(
&self,
a: [f64; 3],
b: [f64; 3],
min_dist: Option<f64>,
) -> f64 {
let mut min_dist = min_dist.unwrap_or(f64::INFINITY);
for periodic_shift in self.reduced_cartesian_shift_matrix.iter() {
let distance = {
(a[0] - (b[0] + periodic_shift[0])).powi(2)
+ (a[1] - (b[1] + periodic_shift[1])).powi(2)
+ (a[2] - (b[2] + periodic_shift[2])).powi(2)
};
if distance < min_dist {
min_dist = distance;
}
}
min_dist
}
pub fn closest_image(&self, a: [f64; 3], b: [f64; 3]) -> EncodedImage {
let mut min_dist = f64::INFINITY;
let mut position = [0.0; 3];
for periodic_shift in self.reduced_cartesian_shift_matrix.iter() {
let image_position = b
.iter()
.zip(periodic_shift)
.map(|(f, image)| *f + *image)
.collect::<Vec<f64>>();
let distance = a
.iter()
.zip(&image_position)
.fold(0.0, |acc, (f, p)| acc + (f - p).powi(2));
if distance < min_dist {
min_dist = distance;
position[0] = image_position[0];
position[1] = image_position[1];
position[2] = image_position[2];
}
}
let image = utils::dot(position, self.to_fractional);
EncodedImage::new([
image[0].floor() as i8,
image[1].floor() as i8,
image[2].floor() as i8,
])
}
fn create_cartesian_shift_matrix(
lattice: &[[f64; 3]; 3],
) -> [[f64; 3]; 27] {
let x = lattice[0];
let y = lattice[1];
let z = lattice[2];
[
[
-x[0] - y[0] - z[0],
-x[1] - y[1] - z[1],
-x[2] - y[2] - z[2],
],
[-x[0] - y[0], -x[1] - y[1], -x[2] - y[2]],
[
-x[0] - y[0] + z[0],
-x[1] - y[1] + z[1],
-x[2] - y[2] + z[2],
],
[-x[0] - z[0], -x[1] - z[1], -x[2] - z[2]],
[-x[0], -x[1], -x[2]],
[-x[0] + z[0], -x[1] + z[1], -x[2] + z[2]],
[
-x[0] + y[0] - z[0],
-x[1] + y[1] - z[1],
-x[2] + y[2] - z[2],
],
[-x[0] + y[0], -x[1] + y[1], -x[2] + y[2]],
[
-x[0] + y[0] + z[0],
-x[1] + y[1] + z[1],
-x[2] + y[2] + z[2],
],
[-y[0] - z[0], -y[1] - z[1], -y[2] - z[2]],
[-y[0], -y[1], -y[2]],
[-y[0] + z[0], -y[1] + z[1], -y[2] + z[2]],
[-z[0], -z[1], -z[2]],
[0.0, 0.0, 0.0],
[z[0], z[1], z[2]],
[y[0] - z[0], y[1] - z[1], y[2] - z[2]],
[y[0], y[1], y[2]],
[y[0] + z[0], y[1] + z[1], y[2] + z[2]],
[x[0] - y[0] - z[0], x[1] - y[1] - z[1], x[2] - y[2] - z[2]],
[x[0] - y[0], x[1] - y[1], x[2] - y[2]],
[x[0] - y[0] + z[0], x[1] - y[1] + z[1], x[2] - y[2] + z[2]],
[x[0] - z[0], x[1] - z[1], x[2] - z[2]],
[x[0], x[1], x[2]],
[x[0] + z[0], x[1] + z[1], x[2] + z[2]],
[x[0] + y[0] - z[0], x[1] + y[1] - z[1], x[2] + y[2] - z[2]],
[x[0] + y[0], x[1] + y[1], x[2] + y[2]],
[x[0] + y[0] + z[0], x[1] + y[1] + z[1], x[2] + y[2] + z[2]],
]
}
fn create_grid_shift_matrix(
shift_matrix: &[[f64; 3]; 27],
to_fractional: &[[f64; 3]; 3],
) -> Vec<Vec<usize>> {
shift_matrix
.iter()
.map(|c_shift| {
let shift = utils::idot(*c_shift, *to_fractional);
let max = shift.iter().map(|x| x.abs()).max().unwrap();
(0..max)
.map(|i| {
let out = shift
.iter()
.map(|s| {
if let Less = (s.abs() - i).cmp(&1) {
0
} else {
s.signum()
}
})
.collect::<Vec<isize>>();
(out[0] * 9 + out[1] * 3 + out[2] + 13) as usize
})
.collect()
})
.collect()
}
}
#[allow(clippy::needless_range_loop)] pub fn lll_lattice(lattice: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
let delta = 0.75;
let mut a = lattice;
let (mut b, mut mu) = gram_schmidt(&a);
let mut i = 1usize;
while i <= 2 {
for j in (0..i).rev() {
match mu[i][j] {
q if q.abs() <= 0.5 => (),
q => {
for k in 0..3 {
a[i][k] -= q.round() * a[j][k];
}
let (b_temp, mu_temp) = gram_schmidt(&a);
b = b_temp;
mu = mu_temp;
}
}
}
if utils::vdot(b[i], b[i])
>= (delta - mu[i][i - 1].powi(2)) * utils::vdot(b[i - 1], b[i - 1])
{
i += 1;
} else {
for j in 0..3 {
b[0][0] = a[i][j];
a[i][j] = a[i - 1][j];
a[i - 1][j] = b[0][0];
}
let (b_temp, mu_temp) = gram_schmidt(&a);
b = b_temp;
mu = mu_temp;
i = 1usize.max(i - 1);
}
}
a
}
fn gram_schmidt(v: &[[f64; 3]; 3]) -> ([[f64; 3]; 3], [[f64; 3]; 3]) {
let mut u = [[0f64; 3]; 3];
let mut mu = [[0f64; 3]; 3];
u[0] = [v[0][0], v[0][1], v[0][2]];
mu[1][0] = utils::vdot(v[1], u[0]) / utils::vdot(u[0], u[0]);
for i in 0..3 {
u[1][i] = v[1][i] - (mu[1][0] * u[0][i]);
}
mu[2][0] = utils::vdot(v[2], u[0]) / utils::vdot(u[0], u[0]);
mu[2][1] = utils::vdot(v[2], u[1]) / utils::vdot(u[1], u[1]);
for i in 0..3 {
u[2][i] = v[2][i] - (mu[2][0] * u[0][i]) - (mu[2][1] * u[1][i]);
}
(u, mu)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils;
#[test]
fn test_lattice_cubic() {
let matrix = [[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]];
let lattice = Lattice::new(matrix);
assert_eq!(lattice.volume, 1000.0);
assert_eq!(lattice.to_cartesian, lattice.reduced_to_cartesian);
}
#[test]
#[should_panic(expected = "Supplied lattice does not span 3d space")]
fn test_lattice_singular_panic() {
Lattice::new([[1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
}
#[test]
fn test_coordinate_transformations() {
let matrix = [[2.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 2.0]];
let lattice = Lattice::new(matrix);
let frac = [0.5, 0.5, 0.5];
let cart = lattice.fractional_to_cartesian(frac);
assert_eq!(cart, [1.0, 1.0, 1.0]);
let p_cart = [1.0, 1.0, 1.0];
let p_red = lattice.cartesian_to_reduced(p_cart);
assert_eq!(p_red, [1.0, 1.0, 1.0]);
let p_outside = [3.0, 1.0, 1.0];
let p_wrapped = lattice.cartesian_to_reduced(p_outside);
assert!((p_wrapped[0] - 1.0).abs() < 1e-6);
}
#[test]
fn test_minimum_distance_pbc() {
let matrix = [[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]];
let lattice = Lattice::new(matrix);
let p1 = [1.0, 1.0, 1.0];
let p2 = [9.0, 1.0, 1.0];
let dist = lattice.minimum_distance(p1, p2, None).powf(0.5);
assert!(
(dist - 2.0).abs() < 1e-6,
"PBC distance failed, got {}",
dist
);
}
#[test]
fn test_lll_reduction_wikipedia() {
let input_lattice = [
[1.0, 1.0, 1.0], [-1.0, 0.0, 2.0], [3.0, 5.0, 6.0], ];
let lattice = Lattice::new(input_lattice);
let reduced = lattice.reduced_to_cartesian;
let expected_rows =
vec![[0.0, 1.0, 0.0], [1.0, 0.0, 1.0], [-1.0, 0.0, 2.0]];
for row in reduced.iter() {
let found = expected_rows.iter().any(|exp| {
let diff_pos = utils::subtract(*row, *exp);
let diff_neg =
utils::subtract(*row, [-exp[0], -exp[1], -exp[2]]);
utils::vdot(diff_pos, diff_pos) < 1e-6
|| utils::vdot(diff_neg, diff_neg) < 1e-6
});
assert!(found, "Unexpected reduced vector: {:?}", row);
}
assert!((lattice.volume - 3.0).abs() < 1e-6);
}
#[test]
fn test_atoms_initialisation() {
let matrix = [[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]];
let lattice = Lattice::new(matrix);
let positions = vec![[15.0, 5.0, 5.0]];
let text = "Test Atom".to_string();
let atoms = Atoms::new(lattice, positions.clone(), text.clone());
assert_eq!(atoms.positions, positions);
let red = atoms.reduced_positions[0];
assert!((red[0] - 5.0).abs() < 1e-6);
assert!((red[1] - 5.0).abs() < 1e-6);
assert!((red[2] - 5.0).abs() < 1e-6);
}
}