use super::geometry::{distance, image_bounds};
use super::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TrajectoryFrame {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub delta_e0: Vec<f64>,
pub step: usize,
pub structures: Vec<WeightedStructure>,
pub score: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DistanceDistribution {
pub edges: Vec<f64>,
pub counts_per_absorber: Vec<f64>,
pub coordination: f64,
pub mean: Option<f64>,
pub variance: Option<f64>,
}
pub fn distance_distribution(
configuration: &Configuration,
absorbers: &[usize],
neighbor_element: Option<u8>,
edges: &[f64],
) -> Result<DistanceDistribution, RmcError> {
configuration.validate()?;
require(
!absorbers.is_empty()
&& absorbers.iter().all(|&i| i < configuration.atoms.len())
&& absorbers
.iter()
.collect::<std::collections::HashSet<_>>()
.len()
== absorbers.len(),
"invalid distribution absorber indices",
)?;
require(
neighbor_element.is_none_or(|z| crate::structure::Element::from_z(z).is_some()),
"invalid neighbor element",
)?;
require(
edges.len() >= 2
&& edges.len() <= 1_000_001
&& edges.iter().all(|v| v.is_finite() && *v >= 0.)
&& edges.windows(2).all(|v| v[1] > v[0]),
"distance bin edges must be finite, nonnegative and increasing",
)?;
let mut counts = vec![0.; edges.len() - 1];
let mut n = 0usize;
let mut mean = 0.;
let mut m2 = 0.;
for &i in absorbers {
visit_neighbors(configuration, i, *edges.last().unwrap(), |j, r| {
if neighbor_element.is_none_or(|z| configuration.atoms[j].atomic_number == z)
&& r >= edges[0]
{
let bin = edges.partition_point(|&v| v <= r) - 1;
counts[bin] += 1.;
n += 1;
let delta = r - mean;
mean += delta / n as f64;
m2 += delta * (r - mean);
}
true
})?;
}
for v in &mut counts {
*v /= absorbers.len() as f64;
}
Ok(DistanceDistribution {
edges: edges.to_vec(),
counts_per_absorber: counts,
coordination: n as f64 / absorbers.len() as f64,
mean: (n > 0).then_some(mean),
variance: (n > 0).then_some(m2 / n.max(1) as f64),
})
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RadialDistribution {
pub neighbors: DistanceDistribution,
pub g_r: Option<Vec<f64>>,
pub density: Option<f64>,
pub independent_radius: Option<f64>,
}
pub fn radial_distribution(
configuration: &Configuration,
centers: &[usize],
neighbor_element: Option<u8>,
edges: &[f64],
) -> Result<RadialDistribution, RmcError> {
let neighbors = distance_distribution(configuration, centers, neighbor_element, edges)?;
let (g_r, density, independent_radius) = if let Some(lattice) = configuration.lattice()? {
let count = configuration
.atoms
.iter()
.filter(|atom| neighbor_element.is_none_or(|z| atom.atomic_number == z))
.count();
require(
count > 0,
"radial distribution needs a nonzero neighbor-species density",
)?;
let density = count as f64 / lattice.volume();
require(
density.is_finite() && density > 0.,
"invalid periodic species density",
)?;
require(
edges.windows(2).all(|r| {
let shell = (4. * std::f64::consts::PI / 3.) * (r[1].powi(3) - r[0].powi(3));
shell.is_finite() && shell > 0.
}),
"spherical shell volumes must be finite and positive",
)?;
let values = neighbors
.counts_per_absorber
.iter()
.zip(edges.windows(2))
.map(|(count, r)| {
count / (density * (4. * std::f64::consts::PI / 3.) * (r[1].powi(3) - r[0].powi(3)))
})
.collect();
let radius = (0..3)
.map(|i| lattice.interplanar_spacing(i) / 2.)
.fold(f64::INFINITY, f64::min);
(Some(values), Some(density), Some(radius))
} else {
(None, None, None)
};
Ok(RadialDistribution {
neighbors,
g_r,
density,
independent_radius,
})
}
pub(super) fn visit_neighbors(
configuration: &Configuration,
center: usize,
cutoff: f64,
mut visit: impl FnMut(usize, f64) -> bool,
) -> Result<bool, RmcError> {
let a = configuration.atoms[center].position;
if let Some(lattice) = configuration.lattice()? {
let bounds = image_bounds(&lattice, cutoff)?;
require(
bounds
.iter()
.map(|n| (2 * n + 1) as usize)
.product::<usize>()
.saturating_mul(configuration.atoms.len())
<= 10_000_000,
"neighbor enumeration exceeds ten million candidate images per absorber",
)?;
for (j, b) in configuration.atoms.iter().enumerate() {
let frac = lattice
.to_frac(std::array::from_fn(|axis| b.position[axis] - a[axis]))
.map(|v| v - v.round());
for x in -bounds[0]..=bounds[0] {
for y in -bounds[1]..=bounds[1] {
for z in -bounds[2]..=bounds[2] {
if center == j && [x, y, z] == [0, 0, 0] {
continue;
}
let r = distance(
lattice.to_cart([
frac[0] + x as f64,
frac[1] + y as f64,
frac[2] + z as f64,
]),
[0.; 3],
);
if r < cutoff && !visit(j, r) {
return Ok(false);
}
}
}
}
}
} else {
for (j, b) in configuration.atoms.iter().enumerate() {
if j != center {
let r = distance(a, b.position);
if r < cutoff && !visit(j, r) {
return Ok(false);
}
}
}
}
Ok(true)
}
pub(super) fn bond_distance(c: &Configuration, a: usize, b: usize) -> Result<f64, RmcError> {
let direct = distance(c.atoms[a].position, c.atoms[b].position);
if c.cell.is_none() {
return Ok(direct);
}
let lattice = c.lattice()?.unwrap();
let frac = lattice
.to_frac(std::array::from_fn(|i| {
c.atoms[b].position[i] - c.atoms[a].position[i]
}))
.map(|v| v - v.round());
let mut best = distance(lattice.to_cart(frac), [0.; 3]);
visit_neighbors(c, a, best + 1e-10, |j, r| {
if j == b {
best = best.min(r);
}
true
})?;
Ok(best)
}