Skip to main content

use_hyperplane/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use use_coordinate::GeometryError;
5
6/// An n-dimensional hyperplane represented by `coefficients.dot(point) + offset = 0`.
7#[derive(Debug, Clone, PartialEq)]
8pub struct Hyperplane {
9    coefficients: Vec<f64>,
10    offset: f64,
11}
12
13impl Hyperplane {
14    /// Creates a validated hyperplane from finite coefficients and finite offset.
15    ///
16    /// # Errors
17    ///
18    /// Returns a [`GeometryError`] when coefficients are empty, non-finite, all zero,
19    /// or when the offset is non-finite.
20    pub fn try_new(coefficients: Vec<f64>, offset: f64) -> Result<Self, GeometryError> {
21        if coefficients.is_empty() || coefficients.iter().all(|value| *value == 0.0) {
22            return Err(GeometryError::ZeroDirectionVector);
23        }
24
25        for value in &coefficients {
26            if !value.is_finite() {
27                return Err(GeometryError::non_finite_component(
28                    "Hyperplane",
29                    "coefficient",
30                    *value,
31                ));
32            }
33        }
34
35        if !offset.is_finite() {
36            return Err(GeometryError::non_finite_component(
37                "Hyperplane",
38                "offset",
39                offset,
40            ));
41        }
42
43        Ok(Self {
44            coefficients,
45            offset,
46        })
47    }
48
49    /// Returns the hyperplane dimension.
50    #[must_use]
51    pub fn dimension(&self) -> usize {
52        self.coefficients.len()
53    }
54
55    /// Returns the coefficient vector.
56    #[must_use]
57    pub fn coefficients(&self) -> &[f64] {
58        &self.coefficients
59    }
60
61    /// Returns the offset.
62    #[must_use]
63    pub const fn offset(&self) -> f64 {
64        self.offset
65    }
66
67    /// Evaluates the hyperplane equation at `point` when dimensions match.
68    #[must_use]
69    pub fn evaluate(&self, point: &[f64]) -> Option<f64> {
70        if point.len() != self.coefficients.len() {
71            return None;
72        }
73
74        Some(
75            self.coefficients
76                .iter()
77                .zip(point)
78                .map(|(coefficient, coordinate)| coefficient * coordinate)
79                .sum::<f64>()
80                + self.offset,
81        )
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::Hyperplane;
88    use use_coordinate::GeometryError;
89
90    #[test]
91    fn evaluates_hyperplanes() {
92        let hyperplane = Hyperplane::try_new(vec![1.0, 0.0, 0.0], -2.0).expect("valid hyperplane");
93
94        assert_eq!(hyperplane.dimension(), 3);
95        assert_eq!(hyperplane.coefficients(), &[1.0, 0.0, 0.0]);
96        assert_eq!(hyperplane.offset(), -2.0);
97        assert_eq!(hyperplane.evaluate(&[2.0, 4.0, 5.0]), Some(0.0));
98        assert_eq!(hyperplane.evaluate(&[2.0, 4.0]), None);
99    }
100
101    #[test]
102    fn rejects_empty_or_zero_coefficients() {
103        assert_eq!(
104            Hyperplane::try_new(Vec::new(), 0.0),
105            Err(GeometryError::ZeroDirectionVector)
106        );
107        assert_eq!(
108            Hyperplane::try_new(vec![0.0, 0.0], 0.0),
109            Err(GeometryError::ZeroDirectionVector)
110        );
111    }
112}