use molrs::store::frame_access::FrameAccess;
use molrs::store::keys;
use molrs::types::{F, U};
use crate::compute::error::ComputeError;
use crate::compute::util::{MicHelper, Positions, get_positions_ref};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AtomGroups {
arity: usize,
flat: Vec<u32>,
}
impl AtomGroups {
pub fn new(arity: usize, flat: Vec<u32>) -> Result<Self, ComputeError> {
if arity == 0 {
return Err(ComputeError::OutOfRange {
field: "AtomGroups::arity",
value: "0".to_string(),
});
}
if !flat.len().is_multiple_of(arity) {
return Err(ComputeError::BadShape {
expected: format!("multiple of arity {arity}"),
got: format!("{} indices", flat.len()),
});
}
Ok(Self { arity, flat })
}
pub fn from_frame<FA: FrameAccess>(
frame: &FA,
block: &'static str,
arity: usize,
) -> Result<Self, ComputeError> {
if arity == 0 || arity > keys::ENDPOINTS.len() {
return Err(ComputeError::OutOfRange {
field: "AtomGroups::arity",
value: arity.to_string(),
});
}
if !frame.contains_block(block) {
return Err(ComputeError::MissingBlock { name: block });
}
let mut columns: Vec<Vec<U>> = Vec::with_capacity(arity);
for &col in &keys::ENDPOINTS[..arity] {
let view = frame
.get_uint(block, col)
.ok_or(ComputeError::MissingColumn { block, col })?;
columns.push(view.iter().copied().collect());
}
let n = columns[0].len();
if columns.iter().any(|c| c.len() != n) {
return Err(ComputeError::BadShape {
expected: format!("{arity} endpoint columns of equal length"),
got: "mismatched endpoint column lengths".to_string(),
});
}
let mut flat = Vec::with_capacity(n * arity);
for row in 0..n {
for col in &columns {
flat.push(col[row]);
}
}
Self::new(arity, flat)
}
pub fn pairs(tuples: &[(u32, u32)]) -> Self {
let mut flat = Vec::with_capacity(tuples.len() * 2);
for &(i, j) in tuples {
flat.push(i);
flat.push(j);
}
Self { arity: 2, flat }
}
pub fn triples(tuples: &[(u32, u32, u32)]) -> Self {
let mut flat = Vec::with_capacity(tuples.len() * 3);
for &(i, j, k) in tuples {
flat.push(i);
flat.push(j);
flat.push(k);
}
Self { arity: 3, flat }
}
pub fn quads(tuples: &[(u32, u32, u32, u32)]) -> Self {
let mut flat = Vec::with_capacity(tuples.len() * 4);
for &(i, j, k, l) in tuples {
flat.push(i);
flat.push(j);
flat.push(k);
flat.push(l);
}
Self { arity: 4, flat }
}
pub fn arity(&self) -> usize {
self.arity
}
pub fn len(&self) -> usize {
self.flat.len() / self.arity
}
pub fn is_empty(&self) -> bool {
self.flat.is_empty()
}
pub fn tuple(&self, i: usize) -> &[u32] {
&self.flat[i * self.arity..(i + 1) * self.arity]
}
}
pub trait Observable {
fn arity(&self) -> usize;
fn is_angular(&self) -> bool {
false
}
fn natural_range(&self) -> Option<(F, F)> {
None
}
fn sample<FA: FrameAccess>(
&self,
frame: &FA,
groups: &AtomGroups,
) -> Result<Vec<F>, ComputeError>;
fn sample_into<FA: FrameAccess>(
&self,
frame: &FA,
groups: &AtomGroups,
out: &mut Vec<F>,
) -> Result<(), ComputeError> {
out.clear();
out.extend(self.sample(frame, groups)?);
Ok(())
}
}
pub(crate) type PosCols<'a> = (Positions<'a>, Positions<'a>, Positions<'a>);
pub(crate) fn positions<FA: FrameAccess>(frame: &FA) -> Result<PosCols<'_>, ComputeError> {
let (xp, yp, zp) = get_positions_ref(frame)?;
let n = xp.slice().len();
if yp.slice().len() != n || zp.slice().len() != n {
return Err(ComputeError::DimensionMismatch {
expected: n,
got: yp.slice().len().min(zp.slice().len()),
what: "atoms x/y/z length",
});
}
Ok((xp, yp, zp))
}
#[inline]
pub(crate) fn displacement(
mic: &MicHelper,
xs: &[F],
ys: &[F],
zs: &[F],
a: usize,
b: usize,
) -> [F; 3] {
let from = [xs[a], ys[a], zs[a]];
let to = [xs[b], ys[b], zs[b]];
mic.disp(from, to)
}
pub(crate) fn norm(v: [F; 3]) -> F {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
pub(crate) fn dot(a: [F; 3], b: [F; 3]) -> F {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
pub(crate) fn cross(a: [F; 3], b: [F; 3]) -> [F; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn atomgroups_validates_arity_multiple() {
assert!(AtomGroups::new(3, vec![0, 1, 2, 3]).is_err());
assert!(AtomGroups::new(0, vec![]).is_err());
let g = AtomGroups::new(2, vec![0, 1, 2, 3]).unwrap();
assert_eq!(g.len(), 2);
assert_eq!(g.tuple(1), &[2, 3]);
}
#[test]
fn empty_groups_report_empty() {
let g = AtomGroups::pairs(&[]);
assert!(g.is_empty());
assert_eq!(g.len(), 0);
}
#[test]
fn from_frame_reads_topology_block() {
use molrs::store::block::Block;
use molrs::store::frame::Frame;
use ndarray::Array1;
let mut frame = Frame::new();
let mut angles = Block::new();
angles
.insert("atomi", Array1::from_vec(vec![0 as U, 3]).into_dyn())
.unwrap();
angles
.insert("atomj", Array1::from_vec(vec![1 as U, 4]).into_dyn())
.unwrap();
angles
.insert("atomk", Array1::from_vec(vec![2 as U, 5]).into_dyn())
.unwrap();
frame.insert("angles", angles);
let g = AtomGroups::from_frame(&frame, "angles", 3).unwrap();
assert_eq!(g.len(), 2);
assert_eq!(g.tuple(0), &[0, 1, 2]);
assert_eq!(g.tuple(1), &[3, 4, 5]);
assert!(AtomGroups::from_frame(&frame, "bonds", 2).is_err());
assert!(AtomGroups::from_frame(&frame, "angles", 4).is_err());
}
}