use super::geometry::distance;
use super::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StructuralDistribution {
pub edges: Vec<f64>,
pub counts: Vec<usize>,
pub underflow: usize,
pub overflow: usize,
pub count: usize,
pub mean: Option<f64>,
pub variance: Option<f64>,
}
pub(super) fn distribution(
values: impl IntoIterator<Item = f64>,
edges: &[f64],
) -> Result<StructuralDistribution, RmcError> {
require(
edges.len() >= 2
&& edges.len() <= 1_000_001
&& edges.iter().all(|v| v.is_finite())
&& edges.windows(2).all(|w| w[1] > w[0]),
"invalid structural histogram edges",
)?;
let mut result = StructuralDistribution {
edges: edges.to_vec(),
counts: vec![0; edges.len() - 1],
underflow: 0,
overflow: 0,
count: 0,
mean: None,
variance: None,
};
let mut mean = 0.;
let mut m2 = 0.;
for value in values {
require(value.is_finite(), "nonfinite structural observation")?;
result.count += 1;
let delta = value - mean;
mean += delta / result.count as f64;
m2 += delta * (value - mean);
if value < edges[0] {
result.underflow += 1;
} else if value >= *edges.last().unwrap() {
result.overflow += 1;
} else {
result.counts[edges.partition_point(|&v| v <= value) - 1] += 1;
}
}
if result.count > 0 {
result.mean = Some(mean);
result.variance = Some(m2 / result.count as f64);
}
Ok(result)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DisplacementReport {
pub atoms: usize,
pub translation: [f64; 3],
pub second_moment: [[f64; 3]; 3],
pub mean_square: f64,
pub rms: f64,
pub maximum: f64,
pub translation_removed: bool,
}
pub fn displacement_report(
reference: &Configuration,
current: &Configuration,
atoms: &[usize],
remove_translation: bool,
) -> Result<DisplacementReport, RmcError> {
reference.validate()?;
current.validate()?;
require(
reference.cell == current.cell
&& reference.atoms.len() == current.atoms.len()
&& reference
.atoms
.iter()
.zip(¤t.atoms)
.all(|(a, b)| a.atomic_number == b.atomic_number),
"displacement report changed topology",
)?;
indices(atoms, current.atoms.len())?;
let delta: Vec<[f64; 3]> = atoms
.iter()
.map(|&a| {
std::array::from_fn(|i| current.atoms[a].position[i] - reference.atoms[a].position[i])
})
.collect();
let translation =
std::array::from_fn(|i| delta.iter().map(|d| d[i]).sum::<f64>() / atoms.len() as f64);
let mut second_moment = [[0.; 3]; 3];
let mut maximum: f64 = 0.;
for d in delta {
let d: [f64; 3] = std::array::from_fn(|i| {
d[i] - if remove_translation {
translation[i]
} else {
0.
}
});
maximum = maximum.max(distance(d, [0.; 3]));
for i in 0..3 {
for j in 0..3 {
second_moment[i][j] += d[i] * d[j] / atoms.len() as f64;
}
}
}
let mean_square = (0..3).map(|i| second_moment[i][i]).sum::<f64>();
Ok(DisplacementReport {
atoms: atoms.len(),
translation,
second_moment,
mean_square,
rms: mean_square.sqrt(),
maximum,
translation_removed: remove_translation,
})
}
pub(super) fn indices(atoms: &[usize], len: usize) -> Result<(), RmcError> {
require(
!atoms.is_empty()
&& atoms.iter().all(|&i| i < len)
&& atoms
.iter()
.collect::<std::collections::BTreeSet<_>>()
.len()
== atoms.len(),
"atom selection must be nonempty, distinct and in range",
)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PathGeometryReport {
pub path: ScatteringPath,
pub half_length: f64,
pub leg_lengths: Vec<f64>,
pub angles: Vec<f64>,
}
pub fn path_geometry_report(
c: &Configuration,
path: &ScatteringPath,
) -> Result<PathGeometryReport, RmcError> {
c.validate()?;
let p = path.positions(c)?;
let legs = p.len() - 1;
let leg_lengths: Vec<_> = p.windows(2).map(|v| distance(v[0], v[1])).collect();
require(
leg_lengths.iter().all(|v| *v > 1e-8),
"path report contains coincident consecutive vertices",
)?;
let angles = (0..legs)
.map(|i| angle(p[(i + legs - 1) % legs], p[i], p[(i + 1) % legs]))
.collect::<Result<Vec<_>, _>>()?;
Ok(PathGeometryReport {
path: path.clone(),
half_length: leg_lengths.iter().sum::<f64>() / 2.,
leg_lengths,
angles,
})
}
pub fn path_length_distribution(
c: &Configuration,
paths: &[ScatteringPath],
edges: &[f64],
) -> Result<StructuralDistribution, RmcError> {
c.validate()?;
distribution(
paths
.iter()
.map(|p| p.half_length(c))
.collect::<Result<Vec<_>, _>>()?,
edges,
)
}
pub fn angle_distribution(
c: &Configuration,
centers: &[usize],
elements: Option<[u8; 2]>,
cutoff: f64,
edges: &[f64],
) -> Result<StructuralDistribution, RmcError> {
c.validate()?;
indices(centers, c.atoms.len())?;
require(
cutoff.is_finite()
&& cutoff > 0.
&& elements.is_none_or(|z| {
z.iter()
.all(|&z| crate::structure::Element::from_z(z).is_some())
}),
"invalid angle cutoff or elements",
)?;
let mut angles = Vec::new();
let mut pairs = 0usize;
for ¢er in centers {
let origin = c.atoms[center].position;
let neighbors = super::paths::neighbor_images(c, center, cutoff)?;
for (i, a) in neighbors.iter().enumerate() {
let pa = a.position(c)?;
if distance(pa, origin) >= cutoff {
continue;
}
for b in &neighbors[i + 1..] {
pairs += 1;
require(
pairs <= 1_000_000,
"angle report exceeds one million neighbor pairs",
)?;
let pb = b.position(c)?;
if distance(pb, origin) >= cutoff {
continue;
}
let z = [c.atoms[a.atom].atomic_number, c.atoms[b.atom].atomic_number];
if elements.is_none_or(|e| e == z || e == [z[1], z[0]]) {
angles.push(angle(pa, origin, pb)?);
}
}
}
}
distribution(angles, edges)
}
fn angle(a: [f64; 3], center: [f64; 3], b: [f64; 3]) -> Result<f64, RmcError> {
let norm = distance(a, center) * distance(b, center);
require(
norm > 1e-16 && norm.is_finite(),
"angle has coincident vertices",
)?;
Ok(((0..3)
.map(|i| (a[i] - center[i]) * (b[i] - center[i]))
.sum::<f64>()
/ norm)
.clamp(-1., 1.)
.acos()
.to_degrees())
}