use crate::atoms::Atoms;
use crate::critical::{CriticalPoint, CriticalPointKind};
use crate::errors::MaximaError;
use crate::grid::Grid;
use crate::hash::IntMap;
use crate::progress::{Bar, HiddenBar, ProgressBar};
use crate::voxel_map::{
BlockingVoxelMap, EncodedAtom, EncodedWeight, Voxel, VoxelMap,
};
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::thread;
pub enum WeightResult {
Critical(Box<[EncodedWeight]>),
Interior(usize),
Boundary(Box<[EncodedWeight]>),
Maximum,
}
pub fn weight_step(
p: isize,
density: &[f64],
voxel_map: &BlockingVoxelMap,
weight_tolerance: f64,
) -> WeightResult {
let control = density[p as usize];
let grid = &voxel_map.grid;
let mut t_sum = 0.;
let mut weights = IntMap::<EncodedAtom, f64>::default();
let mut weight_count = 0;
grid.voronoi_shifts(p)
.into_iter()
.for_each(|((pt, image), alpha)| {
let charge_diff = density[pt as usize] - control;
if charge_diff > 0. {
let rho = charge_diff * alpha;
match voxel_map.voxel_get(pt) {
Voxel::Boundary(weight_map) => {
weight_count = weight_map.len().max(weight_count);
weight_map.into_iter().for_each(|(maxima, weight)| {
let maxima = match image.is_zero() {
true => maxima,
false => maxima.image_add(image),
};
let w = weights.entry(maxima).or_insert(0.);
*w += weight as f64 * rho
});
},
Voxel::Maxima(maxima) => {
let maxima = match image.is_zero() {
true => maxima,
false => maxima.image_add(image),
};
let w = weights.entry(maxima).or_insert(0.);
*w += rho
}
Voxel::Vacuum => panic!("Vacuum voxel found with higher charge density than the control voxel.")
};
t_sum += rho;
}
});
match weights.len().cmp(&1) {
std::cmp::Ordering::Greater => {
let mut total = 0.;
let weights = weights
.into_iter()
.filter_map(|(maxima, weight)| {
let weight = weight / t_sum;
if weight > weight_tolerance {
total += weight;
Some((maxima, weight as f32))
} else {
None
}
})
.collect::<Vec<(EncodedAtom, f32)>>();
if let std::cmp::Ordering::Greater = weights.len().cmp(&1) {
let weights = weights
.into_iter()
.map(|(maxima, w)| EncodedWeight::new(maxima, w))
.collect::<Box<[EncodedWeight]>>();
if weights.len() > weight_count {
WeightResult::Critical(weights)
} else {
WeightResult::Boundary(weights)
}
} else {
WeightResult::Interior(weights[0].0.0 as usize)
}
}
std::cmp::Ordering::Equal => {
WeightResult::Interior(weights.keys().next().unwrap().0 as usize)
}
std::cmp::Ordering::Less => WeightResult::Maximum,
}
}
pub fn weight(
density: &[f64],
voxel_map: &BlockingVoxelMap,
index: &[usize],
weight_tolerance: f64,
visible_bar: bool,
threads: usize,
) -> (Vec<CriticalPoint>, Vec<CriticalPoint>) {
let counter = Arc::new(AtomicUsize::new(0));
let mut critical_points = (vec![], vec![]);
let pbar: Box<dyn ProgressBar> = match visible_bar {
false => Box::new(HiddenBar {}),
true => {
Box::new(Bar::new(index.len(), String::from("Bader Partitioning")))
}
};
thread::scope(|s| {
let th = (0..threads)
.map(|_| {
s.spawn(|| {
let mut c_ps = (vec![], vec![]);
loop {
let p = {
let i = counter.fetch_add(
1,
std::sync::atomic::Ordering::Relaxed,
);
if i >= index.len() {
break;
};
index[i] as isize
};
match weight_step(
p,
density,
voxel_map,
weight_tolerance,
) {
WeightResult::Maximum => {}
WeightResult::Interior(maxima) => {
voxel_map.maxima_store(p, maxima as isize);
}
WeightResult::Boundary(weights) => {
voxel_map.weight_store(p, weights);
}
WeightResult::Critical(weights) => {
let atoms: Vec<EncodedAtom> = weights
.iter()
.map(|ed| ed.decode().0)
.collect();
voxel_map.weight_store(p, weights);
if atoms.len() < 3 {
c_ps.0.push(CriticalPoint::new(
p,
CriticalPointKind::Bond,
atoms.into(),
));
} else {
c_ps.1.push(CriticalPoint::new(
p,
CriticalPointKind::Ring,
atoms.into(),
));
}
}
}
pbar.tick();
}
c_ps
})
})
.collect::<Vec<_>>();
for thread in th {
if let Ok(c_ps) = thread.join() {
critical_points.0.extend(c_ps.0);
critical_points.1.extend(c_ps.1);
}
}
});
critical_points.0.shrink_to_fit();
critical_points.1.shrink_to_fit();
critical_points
}
pub fn maxima_finder(
index: &[usize],
density: &[f64],
voxel_map: &BlockingVoxelMap,
maximum_distance: &f64,
atoms: &Atoms,
threads: usize,
visible_bar: bool,
) -> Result<Vec<CriticalPoint>, MaximaError> {
let grid = &voxel_map.grid;
let mut bader_maxima = Vec::<CriticalPoint>::new();
let pbar: Box<dyn ProgressBar> = match visible_bar {
false => Box::new(HiddenBar {}),
true => Box::new(Bar::new(index.len(), String::from("Maxima Finding"))),
};
let index_len = index.len();
let chunk_size = (index_len / threads) + (index_len % threads).min(1);
thread::scope(|s| {
let th = index
.chunks(chunk_size)
.map(|chunk| {
s.spawn(|| {
chunk
.iter()
.filter_map(|p| {
pbar.tick();
let rho = density[*p];
for (pt, _) in voxel_map
.grid
.voronoi_shifts_nocheck(*p as isize)
{
if density[pt as usize] > rho {
return None;
}
}
Some(
assign_maximum(
*p as isize,
atoms,
grid,
maximum_distance,
)
.map(|atom| {
CriticalPoint::new(
*p as isize,
CriticalPointKind::Nuclei,
Box::new([
EncodedAtom::new_zero_image(
atom as u32,
),
]),
)
}),
)
})
.collect::<Result<Vec<CriticalPoint>, MaximaError>>()
})
})
.collect::<Vec<_>>();
for thread in th {
if let Ok(maxima_list) = thread.join() {
match maxima_list {
Ok(bm) => bader_maxima.extend(bm),
Err(e) => return Err(e),
}
} else {
panic!("Failed to join thread in manima finder.")
};
}
Ok(())
})?;
bader_maxima.shrink_to_fit();
Ok(bader_maxima)
}
pub fn minima_finder(
index: &[usize],
density: &[f64],
voxel_map: &VoxelMap,
threads: usize,
visible_bar: bool,
) -> Vec<CriticalPoint> {
let mut bader_minima = Vec::<CriticalPoint>::new();
let pbar: Box<dyn ProgressBar> = match visible_bar {
false => Box::new(HiddenBar {}),
true => Box::new(Bar::new(index.len(), String::from("Minima Finding"))),
};
let index_len = index.len();
let chunk_size = (index_len / threads) + (index_len % threads).min(1);
thread::scope(|s| {
let th = index
.chunks(chunk_size)
.map(|chunk| {
s.spawn(|| {
chunk
.iter()
.filter_map(|p| {
pbar.tick();
let rho = density[*p];
for (pt, _) in voxel_map
.grid
.voronoi_shifts_nocheck(*p as isize)
{
if density[pt as usize] < rho {
return None;
}
}
if let Voxel::Boundary(weights) =
voxel_map.voxel_get(*p as isize)
{
return Some(CriticalPoint::new(
*p as isize,
CriticalPointKind::Cage,
weights.into_keys().collect(),
));
}
None
})
.collect::<Vec<CriticalPoint>>()
})
})
.collect::<Vec<_>>();
for thread in th {
if let Ok(minima_list) = thread.join() {
bader_minima.extend(minima_list);
} else {
panic!("Failed to join thread in manima finder.")
};
}
});
bader_minima.shrink_to_fit();
bader_minima
}
pub fn assign_maximum(
maximum: isize,
atoms: &Atoms,
grid: &Grid,
maximum_distance: &f64,
) -> Result<usize, MaximaError> {
let m_cartesian = grid.to_cartesian(maximum);
let m_reduced_cartesian = atoms.lattice.cartesian_to_reduced(m_cartesian);
let mut atom_num = 0;
let mut min_distance = f64::INFINITY;
for (i, atom) in atoms.reduced_positions.iter().enumerate() {
for atom_shift in atoms.lattice.reduced_cartesian_shift_matrix.iter() {
let distance = {
(m_reduced_cartesian[0] - (atom[0] + atom_shift[0])).powi(2)
+ (m_reduced_cartesian[1] - (atom[1] + atom_shift[1]))
.powi(2)
+ (m_reduced_cartesian[2] - (atom[2] + atom_shift[2]))
.powi(2)
};
if distance < min_distance {
min_distance = distance;
atom_num = i;
}
}
}
if min_distance.powf(0.5) > *maximum_distance {
Err(MaximaError {
maximum: m_cartesian,
atom: atom_num,
distance: min_distance.powf(0.5),
})
} else {
Ok(atom_num)
}
}
pub fn laplacian(p: usize, density: &[f64], grid: &Grid) -> f64 {
let rho = density[p];
grid.voronoi_shifts_nocheck(p as isize)
.iter()
.fold(0.0, |acc, (pt, alpha)| {
acc + alpha * (density[*pt as usize] - rho)
})
/ grid.voronoi.volume
}
#[cfg(test)]
mod tests {
use super::*;
use crate::atoms::{Atoms, Lattice};
use crate::grid::Grid;
use crate::voxel_map::BlockingVoxelMap;
fn setup_env(dim: usize) -> (Grid, Atoms, BlockingVoxelMap) {
let lattice_mat = [[3.0, 0.0, 0.0], [0.0, 3.0, 0.0], [0.0, 0.0, 3.0]];
let origin = [0.0, 0.0, 0.0];
let grid = Grid::new([dim, dim, dim], lattice_mat, origin);
let atoms = Atoms::new(
Lattice::new(lattice_mat),
vec![[0.0, 0.0, 0.0], [1.5, 1.5, 1.5]],
String::from("Test"),
);
let map = BlockingVoxelMap::new([dim, dim, dim], lattice_mat, origin);
(grid, atoms, map)
}
#[test]
fn test_weight_step_maximum() {
let dim = 3;
let (_, _, map) = setup_env(dim);
let mut density = vec![0.0; 27];
density[13] = 10.0;
let result = weight_step(13, &density, &map, 1e-8);
match result {
WeightResult::Maximum => (),
_ => panic!("Expected Maximum, got {:?}", result_name(&result)),
}
}
#[test]
fn test_weight_step_interior() {
let dim = 3;
let (_, _, map) = setup_env(dim);
let mut density = vec![0.0; 27];
density[13] = 10.0;
density[14] = 5.0;
map.maxima_store(13, 100);
let result = weight_step(14, &density, &map, 1e-8);
match result {
WeightResult::Interior(atom_idx) => assert_eq!(atom_idx, 100),
_ => panic!("Expected Interior, got {:?}", result_name(&result)),
}
}
#[test]
fn test_weight_step_boundary() {
let dim = 3;
let (_, _, map) = setup_env(dim);
let mut density = vec![0.0; 27];
density[13] = 5.0; density[12] = 10.0; density[14] = 10.0;
map.maxima_store(12, EncodedAtom::new_zero_image(1).0 as isize);
map.maxima_store(14, EncodedAtom::new_zero_image(2).0 as isize);
let result = weight_step(13, &density, &map, 1e-8);
match result {
WeightResult::Boundary(weights)
| WeightResult::Critical(weights) => {
assert_eq!(weights.len(), 2);
let w1 = weights
.iter()
.find(|w| w.decode().0.atom_index() == 1)
.unwrap();
let w2 = weights
.iter()
.find(|w| w.decode().0.atom_index() == 2)
.unwrap();
let val1 = w1.decode().1;
let val2 = w2.decode().1;
assert!((val1 - 0.5).abs() < 0.1);
assert!((val2 - 0.5).abs() < 0.1);
}
_ => panic!(
"Expected Boundary/Critical, got {:?}",
result_name(&result)
),
}
}
#[test]
fn test_assign_maximum_nearest() {
let dim = 10;
let (grid, atoms, _) = setup_env(dim);
let max_dist = 1.0;
let atom_idx = assign_maximum(0, &atoms, &grid, &max_dist).unwrap();
assert_eq!(atom_idx, 0);
let center_idx = 5 * 100 + 5 * 10 + 5;
let atom_idx_2 =
assign_maximum(center_idx, &atoms, &grid, &max_dist).unwrap();
assert_eq!(atom_idx_2, 1);
}
#[test]
fn test_assign_maximum_cutoff() {
let dim = 10;
let (grid, atoms, _) = setup_env(dim);
let tight_cutoff = 0.05; let point_idx = 100;
let result = assign_maximum(point_idx, &atoms, &grid, &tight_cutoff);
assert!(result.is_err());
}
#[test]
fn test_laplacian_uniform() {
let dim = 3;
let (grid, _, _) = setup_env(dim);
let density = vec![1.0; 27];
let lap = laplacian(13, &density, &grid);
assert!(lap.abs() < 1e-9);
}
#[test]
fn test_laplacian_peak() {
let dim = 3;
let (grid, _, _) = setup_env(dim);
let mut density = vec![0.0; 27];
density[13] = 10.0;
let lap = laplacian(13, &density, &grid);
assert!(lap < 0.0);
}
fn result_name(w: &WeightResult) -> &'static str {
match w {
WeightResult::Maximum => "Maximum",
WeightResult::Interior(_) => "Interior",
WeightResult::Boundary(_) => "Boundary",
WeightResult::Critical(_) => "Critical",
}
}
}