Skip to main content

holos_tda/filtration/
grade.rs

1use std::fmt;
2
3/// Failure while constructing an explicit filtered complex or grade.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct FiltrationError {
6    message: String,
7}
8
9impl FiltrationError {
10    pub(crate) fn new(message: impl Into<String>) -> Self {
11        Self {
12            message: message.into(),
13        }
14    }
15
16    /// Description of the violated filtration rule.
17    pub fn message(&self) -> &str {
18        &self.message
19    }
20}
21
22impl fmt::Display for FiltrationError {
23    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
24        write!(formatter, "filtered complex: {}", self.message)
25    }
26}
27
28impl std::error::Error for FiltrationError {}
29
30/// A grade with a decidable filtration partial order.
31pub trait FiltrationGrade: Clone + Eq {
32    /// Return true when `self` is no later than `other` in the filtration.
33    fn precedes(&self, other: &Self) -> bool;
34}
35
36/// A filtration grade with one canonical total order.
37pub trait LinearFiltrationGrade: FiltrationGrade + Ord {}
38
39/// A finite, non-negative scalar filtration grade with a canonical total order.
40///
41/// The value is stored as canonical IEEE 754 bits. Negative zero is stored as
42/// positive zero, so equality and ordering agree.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
44pub struct ScalarGrade(u64);
45
46impl ScalarGrade {
47    /// Construct a checked scalar grade.
48    pub fn new(value: f64) -> Result<Self, FiltrationError> {
49        if !value.is_finite() || value < 0.0 {
50            return Err(FiltrationError::new(
51                "a scalar grade must be finite and non-negative",
52            ));
53        }
54        Ok(Self(if value == 0.0 {
55            0.0f64.to_bits()
56        } else {
57            value.to_bits()
58        }))
59    }
60
61    /// The scalar value.
62    pub fn value(self) -> f64 {
63        f64::from_bits(self.0)
64    }
65
66    /// Canonical IEEE 754 bits used by proof formats.
67    pub fn bits(self) -> u64 {
68        self.0
69    }
70}
71
72impl FiltrationGrade for ScalarGrade {
73    fn precedes(&self, other: &Self) -> bool {
74        self <= other
75    }
76}
77
78impl LinearFiltrationGrade for ScalarGrade {}
79
80/// A coordinatewise filtration grade with `N` parameters.
81///
82/// Grades use the product partial order. Incomparable grades have no total
83/// order. Scalar persistence requires a [`ScalarProjection`].
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85pub struct ProductGrade<const N: usize> {
86    coordinates: [ScalarGrade; N],
87}
88
89impl<const N: usize> ProductGrade<N> {
90    /// Construct a checked product grade from scalar coordinates.
91    pub fn new(coordinates: [f64; N]) -> Result<Self, FiltrationError> {
92        if N == 0 {
93            return Err(FiltrationError::new(
94                "a product grade requires at least one coordinate",
95            ));
96        }
97        let mut checked = [ScalarGrade(0); N];
98        for (index, value) in coordinates.into_iter().enumerate() {
99            checked[index] = ScalarGrade::new(value)?;
100        }
101        Ok(Self {
102            coordinates: checked,
103        })
104    }
105
106    /// Scalar coordinates in parameter order.
107    pub fn coordinates(&self) -> &[ScalarGrade; N] {
108        &self.coordinates
109    }
110}
111
112impl<const N: usize> FiltrationGrade for ProductGrade<N> {
113    fn precedes(&self, other: &Self) -> bool {
114        self.coordinates
115            .iter()
116            .zip(other.coordinates.iter())
117            .all(|(left, right)| left.precedes(right))
118    }
119}
120
121/// A declared monotone map from a grade into a scalar filtration.
122pub trait ScalarProjection<G: FiltrationGrade> {
123    /// Project one grade into the scalar filtration.
124    fn project(&self, grade: &G) -> Result<ScalarGrade, FiltrationError>;
125}
126
127/// Projection onto one coordinate of a product grade.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct CoordinateProjection {
130    coordinate: usize,
131}
132
133impl CoordinateProjection {
134    /// Select a zero-based coordinate.
135    pub fn new(coordinate: usize) -> Self {
136        Self { coordinate }
137    }
138}
139
140impl<const N: usize> ScalarProjection<ProductGrade<N>> for CoordinateProjection {
141    fn project(&self, grade: &ProductGrade<N>) -> Result<ScalarGrade, FiltrationError> {
142        grade
143            .coordinates
144            .get(self.coordinate)
145            .copied()
146            .ok_or_else(|| {
147                FiltrationError::new(format!(
148                    "coordinate {} is outside a {N}-parameter grade",
149                    self.coordinate
150                ))
151            })
152    }
153}