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;
use super::observable::{AtomGroups, Observable};
use super::{AngleObservable, DihedralObservable, DistanceObservable, DistributionResult};
pub const KB_KCAL_PER_MOL_K: F = 1.987204e-3;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AxisSpec {
pub bins: usize,
pub min: F,
pub max: F,
pub sin_weight: bool,
}
impl AxisSpec {
pub fn new(bins: usize, min: F, max: F) -> Result<Self, ComputeError> {
if bins == 0 {
return Err(ComputeError::OutOfRange {
field: "AxisSpec::bins",
value: "0".to_string(),
});
}
if max <= min || !min.is_finite() || !max.is_finite() {
return Err(ComputeError::OutOfRange {
field: "AxisSpec::range",
value: format!("min={min}, max={max}"),
});
}
Ok(Self {
bins,
min,
max,
sin_weight: false,
})
}
pub fn with_sin_weight(mut self, on: bool) -> Self {
self.sin_weight = on;
self
}
fn bin_width(&self) -> F {
(self.max - self.min) / self.bins as F
}
fn fac(&self) -> F {
self.bins as F / (self.max - self.min)
}
fn cic(&self, d: F) -> Option<([usize; 2], [F; 2])> {
if d < self.min || d > self.max {
return None;
}
Some(super::histogram1d::cic_split(
d,
self.min,
self.fac(),
self.bins,
))
}
fn edges(&self) -> Array1<F> {
let w = self.bin_width();
Array1::from_iter((0..=self.bins).map(|i| self.min + i as F * w))
}
fn centers(&self) -> Array1<F> {
let w = self.bin_width();
Array1::from_iter((0..self.bins).map(|i| self.min + (i as F + 0.5) * w))
}
}
#[derive(Debug, Clone)]
pub enum AnyObservable {
Distance(DistanceObservable),
Angle(AngleObservable),
Dihedral(DihedralObservable),
}
impl AnyObservable {
pub fn from_kind(kind: &str) -> Result<(Self, usize), ComputeError> {
let obs = match kind {
"distance" => Self::Distance(DistanceObservable),
"angle" => Self::Angle(AngleObservable),
"dihedral" => Self::Dihedral(DihedralObservable),
other => {
return Err(ComputeError::OutOfRange {
field: "AnyObservable::kind",
value: other.to_string(),
});
}
};
let arity = obs.arity();
Ok((obs, arity))
}
fn arity(&self) -> usize {
match self {
Self::Distance(o) => o.arity(),
Self::Angle(o) => o.arity(),
Self::Dihedral(o) => o.arity(),
}
}
fn is_angular(&self) -> bool {
match self {
Self::Distance(o) => o.is_angular(),
Self::Angle(o) => o.is_angular(),
Self::Dihedral(o) => o.is_angular(),
}
}
fn sample_into<FA: FrameAccess>(
&self,
frame: &FA,
groups: &AtomGroups,
out: &mut Vec<F>,
) -> Result<(), ComputeError> {
match self {
Self::Distance(o) => o.sample_into(frame, groups, out),
Self::Angle(o) => o.sample_into(frame, groups, out),
Self::Dihedral(o) => o.sample_into(frame, groups, out),
}
}
}
impl From<DistanceObservable> for AnyObservable {
fn from(o: DistanceObservable) -> Self {
Self::Distance(o)
}
}
impl From<AngleObservable> for AnyObservable {
fn from(o: AngleObservable) -> Self {
Self::Angle(o)
}
}
impl From<DihedralObservable> for AnyObservable {
fn from(o: DihedralObservable) -> Self {
Self::Dihedral(o)
}
}
#[derive(Debug, Clone)]
pub struct CombinedDistribution {
observables: Vec<AnyObservable>,
axes: Vec<AxisSpec>,
}
impl CombinedDistribution {
pub fn new(observables: Vec<AnyObservable>, axes: Vec<AxisSpec>) -> Result<Self, ComputeError> {
let n = observables.len();
if !(2..=3).contains(&n) {
return Err(ComputeError::OutOfRange {
field: "CombinedDistribution::ndim",
value: format!("{n} (only 2-D and 3-D supported)"),
});
}
if axes.len() != n {
return Err(ComputeError::DimensionMismatch {
expected: n,
got: axes.len(),
what: "axis count vs observable count",
});
}
Ok(Self { observables, axes })
}
pub fn ndim(&self) -> usize {
self.axes.len()
}
fn strides(&self) -> Vec<usize> {
let mut s = vec![1usize; self.axes.len()];
for a in 1..self.axes.len() {
s[a] = s[a - 1] * self.axes[a - 1].bins;
}
s
}
fn total_bins(&self) -> usize {
self.axes.iter().map(|a| a.bins).product()
}
#[allow(clippy::too_many_arguments)]
fn accumulate_frame<FA: FrameAccess>(
&self,
frame: &FA,
groups: &[AtomGroups],
strides: &[usize],
n_tuples: usize,
cols: &mut Vec<Vec<F>>,
counts: &mut Array1<F>,
binned: &mut F,
n_raw_samples: &mut usize,
) -> Result<(), ComputeError> {
let n = self.observables.len();
if cols.len() < n {
cols.resize_with(n, Vec::new);
}
for (a, (obs, g)) in self.observables.iter().zip(groups.iter()).enumerate() {
obs.sample_into(frame, g, &mut cols[a])?;
}
for c in cols.iter().take(n) {
if c.len() != n_tuples {
return Err(ComputeError::DimensionMismatch {
expected: n_tuples,
got: c.len(),
what: "observable sample count",
});
}
}
*n_raw_samples += n_tuples;
#[allow(clippy::needless_range_loop)]
for t in 0..n_tuples {
let mut contribs: [([usize; 2], [F; 2]); 3] = [([0, 0], [0.0, 0.0]); 3];
let mut in_range = true;
for a in 0..n {
match self.axes[a].cic(cols[a][t]) {
Some(c) => contribs[a] = c,
None => {
in_range = false;
break;
}
}
}
if !in_range {
continue;
}
for corner in 0..(1usize << n) {
let mut flat = 0usize;
let mut w = 1.0;
for a in 0..n {
let pick = (corner >> a) & 1;
flat += contribs[a].0[pick] * strides[a];
w *= contribs[a].1[pick];
}
if w != 0.0 {
counts[flat] += w;
}
}
*binned += 1.0;
}
Ok(())
}
fn accumulate<'a, FA: FrameAccess + Sync + 'a>(
&self,
frames: &[&'a FA],
groups: &[AtomGroups],
strides: &[usize],
n_tuples: usize,
) -> Result<(Array1<F>, F, usize), ComputeError> {
let total_bins = self.total_bins();
if n_tuples == 0 {
return Ok((Array1::<F>::zeros(total_bins), 0.0, 0));
}
#[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(
|| (Array1::<F>::zeros(total_bins), 0.0 as F, 0usize, Vec::new()),
|(mut counts, mut binned, mut nrs, mut cols), frame| {
self.accumulate_frame(
*frame,
groups,
strides,
n_tuples,
&mut cols,
&mut counts,
&mut binned,
&mut nrs,
)?;
Ok((counts, binned, nrs, cols))
},
)
.map(|r| r.map(|(c, b, nrs, _)| (c, b, nrs)))
.try_reduce(
|| (Array1::<F>::zeros(total_bins), 0.0 as F, 0usize),
|(mut ca, ba, nra), (cb, bb, nrb)| {
ca += &cb;
Ok((ca, ba + bb, nra + nrb))
},
);
}
let mut counts = Array1::<F>::zeros(total_bins);
let mut binned: F = 0.0;
let mut n_raw_samples: usize = 0;
let mut cols: Vec<Vec<F>> = Vec::new();
for frame in frames {
self.accumulate_frame(
*frame,
groups,
strides,
n_tuples,
&mut cols,
&mut counts,
&mut binned,
&mut n_raw_samples,
)?;
}
Ok((counts, binned, n_raw_samples))
}
}
impl Compute for CombinedDistribution {
type Args<'a> = &'a [AtomGroups];
type Output = CombinedDistributionResult;
fn compute<'a, FA: FrameAccess + Sync + 'a>(
&self,
frames: &[&'a FA],
groups: &'a [AtomGroups],
) -> Result<CombinedDistributionResult, ComputeError> {
if frames.is_empty() {
return Err(ComputeError::EmptyInput);
}
let n = self.observables.len();
if groups.len() != n {
return Err(ComputeError::DimensionMismatch {
expected: n,
got: groups.len(),
what: "AtomGroups count vs observable count",
});
}
for (k, (obs, g)) in self.observables.iter().zip(groups.iter()).enumerate() {
if g.arity() != obs.arity() {
return Err(ComputeError::BadShape {
expected: format!("axis {k}: arity {}", obs.arity()),
got: format!("arity {}", g.arity()),
});
}
}
let n_tuples = groups[0].len();
for g in groups.iter() {
if g.len() != n_tuples {
return Err(ComputeError::DimensionMismatch {
expected: n_tuples,
got: g.len(),
what: "per-axis sample count (all axes must match axis 0)",
});
}
}
let strides = self.strides();
let (counts, binned, n_raw_samples) =
self.accumulate(frames, groups, &strides, n_tuples)?;
let bin_widths: Vec<F> = self.axes.iter().map(|a| a.bin_width()).collect();
let cell_volume: F = bin_widths.iter().product();
let mut result = CombinedDistributionResult {
axes: self.axes.clone(),
strides,
bin_widths,
cell_volume,
edges: self.axes.iter().map(|a| a.edges()).collect(),
centers: self.axes.iter().map(|a| a.centers()).collect(),
angular: self.observables.iter().map(|o| o.is_angular()).collect(),
counts,
density: Array1::zeros(self.total_bins()),
binned,
n_raw_samples,
n_frames: frames.len(),
finalized: false,
};
result.finalize();
Ok(result)
}
}
#[derive(Debug, Clone)]
pub struct CombinedDistributionResult {
axes: Vec<AxisSpec>,
strides: Vec<usize>,
bin_widths: Vec<F>,
cell_volume: F,
pub edges: Vec<Array1<F>>,
pub centers: Vec<Array1<F>>,
angular: Vec<bool>,
pub counts: Array1<F>,
pub density: Array1<F>,
pub binned: F,
pub n_raw_samples: usize,
pub n_frames: usize,
finalized: bool,
}
impl CombinedDistributionResult {
pub fn ndim(&self) -> usize {
self.axes.len()
}
pub fn flat_index(&self, idx: &[usize]) -> usize {
idx.iter()
.zip(self.strides.iter())
.map(|(&i, &s)| i * s)
.sum()
}
pub fn bin_width_product(&self) -> F {
self.cell_volume
}
pub fn marginal(&self, axis: usize) -> DistributionResult {
let bins = self.axes[axis].bins;
let mut counts = Array1::<F>::zeros(bins);
for (flat, &c) in self.counts.iter().enumerate() {
if c == 0.0 {
continue;
}
let ia = (flat / self.strides[axis]) % bins;
counts[ia] += c;
}
let bin_width = self.bin_widths[axis];
let n_binned: F = counts.sum();
let mut result = DistributionResult {
bin_centers: self.centers[axis].clone(),
bin_edges: self.edges[axis].clone(),
counts,
density: Array1::zeros(bins),
density_sin_corrected: None,
bin_width,
n_binned,
n_raw_samples: self.n_raw_samples,
n_frames: self.n_frames,
angular: self.angular[axis] || self.axes[axis].sin_weight,
finalized: false,
};
result.finalize();
result
}
pub fn free_energy(&self, temperature: F) -> Array1<F> {
let kt = KB_KCAL_PER_MOL_K * temperature;
let mut g = Array1::<F>::zeros(self.density.len());
let mut g_max = F::NEG_INFINITY;
for (i, &p) in self.density.iter().enumerate() {
if p > 0.0 {
let gi = -kt * p.ln();
g[i] = gi;
if gi > g_max {
g_max = gi;
}
}
}
let floor = if g_max.is_finite() { g_max } else { 0.0 };
for (i, &p) in self.density.iter().enumerate() {
if p <= 0.0 {
g[i] = floor;
}
}
g
}
}
impl ComputeResult for CombinedDistributionResult {
fn finalize(&mut self) {
if self.finalized {
return;
}
if self.binned > 0.0 {
let denom = self.binned * self.cell_volume;
self.density = self.counts.mapv(|c| c / denom);
} else {
self.density = Array1::zeros(self.counts.len());
}
self.finalized = true;
}
}