Skip to main content

molframe_core/coords/
sample.rs

1//! Borrowed Cartesian samples evaluate one atom in O(1), without allocating a coordinate column.
2
3use num_traits::ToPrimitive;
4
5/// A topology-aligned linear sample of two retained coordinate columns.
6/// Periodic reconstruction and alignment belong to the caller before sampling.
7#[derive(Clone, Copy, Debug)]
8pub struct CoordinateSample<'a> {
9    start: &'a [[f32; 3]],
10    end: &'a [[f32; 3]],
11    weight: f32,
12}
13
14/// Invalid coordinate-sample metadata.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
16pub enum CoordinateSampleError {
17    /// Both columns must address the same original atom rows.
18    #[error("coordinate sample requires equal-length columns")]
19    Shape,
20    /// Extrapolation and nonfinite interpolation weights are refused.
21    #[error("coordinate sample weight must be finite and between zero and one")]
22    Weight,
23}
24
25impl<'a> CoordinateSample<'a> {
26    /// Validates metadata in O(1) while borrowing both original columns.
27    /// Individual coordinate triples are checked when read with [`Self::position`].
28    ///
29    /// # Errors
30    /// Refuses mismatched columns or an invalid interpolation weight.
31    pub fn new(
32        start: &'a [[f32; 3]],
33        end: &'a [[f32; 3]],
34        weight: f32,
35    ) -> Result<Self, CoordinateSampleError> {
36        if start.len() != end.len() {
37            return Err(CoordinateSampleError::Shape);
38        }
39        if !weight.is_finite() || !(0.0..=1.0).contains(&weight) {
40            return Err(CoordinateSampleError::Weight);
41        }
42        Ok(Self { start, end, weight })
43    }
44
45    /// Number of original atom addresses in this sample.
46    #[must_use]
47    pub const fn len(self) -> usize {
48        self.start.len()
49    }
50
51    /// Whether the sample contains any original atom addresses.
52    #[must_use]
53    pub const fn is_empty(self) -> bool {
54        self.start.is_empty()
55    }
56
57    /// Reads one interpolated atom, refusing an absent row or nonfinite input.
58    /// Wider intermediate arithmetic keeps opposite extreme finite positions
59    /// from overflowing while preserving exact coordinates at both endpoints.
60    #[must_use]
61    pub fn position(self, row: usize) -> Option<[f32; 3]> {
62        let start = *self.start.get(row)?;
63        let end = *self.end.get(row)?;
64        if !start.iter().chain(&end).all(|value| value.is_finite()) {
65            return None;
66        }
67        if self.weight.to_bits() == 0.0f32.to_bits() || self.weight.to_bits() == (-0.0f32).to_bits()
68        {
69            return Some(start);
70        }
71        if self.weight.to_bits() == 1.0f32.to_bits() {
72            return Some(end);
73        }
74        let weight = f64::from(self.weight);
75        let mut output = [0.0; 3];
76        for axis in 0..3 {
77            output[axis] = (f64::from(start[axis]) * (1.0 - weight)
78                + f64::from(end[axis]) * weight)
79                .to_f32()?;
80        }
81        Some(output)
82    }
83}
84
85#[cfg(test)]
86#[path = "sample_tests.rs"]
87mod tests;