mod angle;
mod combined;
mod dihedral;
mod distance;
mod histogram1d;
mod observable;
pub use angle::AngleObservable;
pub use combined::{
AnyObservable, AxisSpec, CombinedDistribution, CombinedDistributionResult, KB_KCAL_PER_MOL_K,
};
pub use dihedral::DihedralObservable;
pub use distance::DistanceObservable;
pub use histogram1d::{Histogram1d, renormalize_density};
pub use observable::{AtomGroups, Observable};
use molrs::store::frame_access::FrameAccess;
use molrs::types::F;
use ndarray::Array1;
use crate::compute::error::ComputeError;
use crate::compute::result::ComputeResult;
use crate::compute::traits::Compute;
#[derive(Debug, Clone)]
pub struct DistributionFunction<O: Observable> {
observable: O,
n_bins: usize,
min: F,
max: F,
}
impl<O: Observable> DistributionFunction<O> {
pub fn new(observable: O, n_bins: usize, min: F, max: F) -> Result<Self, ComputeError> {
if n_bins == 0 {
return Err(ComputeError::OutOfRange {
field: "DistributionFunction::n_bins",
value: "0".to_string(),
});
}
if max <= min || !min.is_finite() || !max.is_finite() {
return Err(ComputeError::OutOfRange {
field: "DistributionFunction::range",
value: format!("min={min}, max={max}"),
});
}
Ok(Self {
observable,
n_bins,
min,
max,
})
}
pub fn over_natural_range(observable: O, n_bins: usize) -> Result<Self, ComputeError> {
let (min, max) = observable.natural_range().ok_or(ComputeError::OutOfRange {
field: "DistributionFunction::natural_range",
value: "observable has no natural range".to_string(),
})?;
Self::new(observable, n_bins, min, max)
}
fn accumulate<'a, FA: FrameAccess + Sync + 'a>(
&self,
frames: &[&'a FA],
groups: &AtomGroups,
) -> Result<(Histogram1d, usize), ComputeError>
where
O: Sync,
{
#[cfg(feature = "rayon")]
const PAR_THRESHOLD: usize = 4;
#[cfg(feature = "rayon")]
if frames.len() >= PAR_THRESHOLD {
use rayon::prelude::*;
return frames
.par_iter()
.try_fold(
|| {
(
Histogram1d::new(self.n_bins, self.min, self.max),
0usize,
Vec::<F>::new(),
)
},
|(mut hist, mut nrs, mut scratch), frame| {
self.observable.sample_into(*frame, groups, &mut scratch)?;
nrs += scratch.len();
for &s in &scratch {
hist.add(s);
}
Ok((hist, nrs, scratch))
},
)
.map(|r| r.map(|(hist, nrs, _)| (hist, nrs)))
.try_reduce(
|| (Histogram1d::new(self.n_bins, self.min, self.max), 0usize),
|(mut ha, nra), (hb, nrb)| {
ha.merge(&hb);
Ok((ha, nra + nrb))
},
);
}
let mut hist = Histogram1d::new(self.n_bins, self.min, self.max);
let mut n_raw_samples: usize = 0;
let mut scratch: Vec<F> = Vec::new();
for frame in frames {
self.observable.sample_into(*frame, groups, &mut scratch)?;
n_raw_samples += scratch.len();
for &s in &scratch {
hist.add(s);
}
}
Ok((hist, n_raw_samples))
}
}
impl<O: Observable + Sync> Compute for DistributionFunction<O> {
type Args<'a> = &'a AtomGroups;
type Output = DistributionResult;
fn compute<'a, FA: FrameAccess + Sync + 'a>(
&self,
frames: &[&'a FA],
groups: &'a AtomGroups,
) -> Result<DistributionResult, ComputeError> {
if frames.is_empty() {
return Err(ComputeError::EmptyInput);
}
if groups.arity() != self.observable.arity() {
return Err(ComputeError::BadShape {
expected: format!("arity {}", self.observable.arity()),
got: format!("arity {}", groups.arity()),
});
}
let (hist, n_raw_samples) = if groups.is_empty() {
(Histogram1d::new(self.n_bins, self.min, self.max), 0usize)
} else {
self.accumulate(frames, groups)?
};
let mut result = DistributionResult {
bin_centers: hist.centers(),
bin_edges: hist.edges(),
counts: hist.counts().clone(),
density: Array1::zeros(self.n_bins),
density_sin_corrected: None,
bin_width: hist.bin_width(),
n_binned: hist.binned(),
n_raw_samples,
n_frames: frames.len(),
angular: self.observable.is_angular(),
finalized: false,
};
result.finalize();
Ok(result)
}
}
#[derive(Debug, Clone)]
pub struct DistributionResult {
pub bin_centers: Array1<F>,
pub bin_edges: Array1<F>,
pub counts: Array1<F>,
pub density: Array1<F>,
pub density_sin_corrected: Option<Array1<F>>,
pub bin_width: F,
pub n_binned: F,
pub n_raw_samples: usize,
pub n_frames: usize,
pub angular: bool,
finalized: bool,
}
impl ComputeResult for DistributionResult {
fn finalize(&mut self) {
if self.finalized {
return;
}
if self.n_binned > 0.0 {
let denom = self.n_binned * self.bin_width;
self.density = self.counts.mapv(|c| c / denom);
} else {
self.density = Array1::zeros(self.counts.len());
}
if self.angular {
let mut w = Array1::<F>::zeros(self.counts.len());
for i in 0..self.counts.len() {
let s = self.bin_centers[i].sin();
w[i] = if s.abs() > 1e-12 {
self.density[i] / s
} else {
0.0
};
}
self.density_sin_corrected = Some(renormalize_density(&w, self.bin_width));
}
self.finalized = true;
}
}