Skip to main content

holos_tda/bipersistence/
circular.rs

1//! Checked circular-coordinate families over class-extension atlases.
2
3use crate::circular::{
4    CircularCoordinate, CircularCoordinateFailure, CircularCoordinateParams,
5    circular_coordinate_with_failure_stage, validate_params,
6};
7use crate::{Error, Result};
8
9use super::{BipersistenceModule, BipersistenceTerm, ClassExtensionKind, CohomologyClassAtlas};
10
11/// Computational outcome for one class extension.
12#[derive(Debug, Clone, PartialEq)]
13pub enum CircularCoordinateFamilyStatus {
14    /// No circular-coordinate computation was attempted.
15    NotAttempted,
16    /// Automatic integral lifting did not produce a checked lift.
17    ///
18    /// This status reports a bounded computation result. It does not prove
19    /// that the class has no integral lift.
20    LiftFailed,
21    /// A checked lift was available, but harmonic coordinate construction failed.
22    SolveFailed,
23    /// The checked circular coordinate was computed.
24    Success(Box<CircularCoordinate>),
25}
26
27impl CircularCoordinateFamilyStatus {
28    /// Return the coordinate when computation succeeded.
29    pub fn coordinate(&self) -> Option<&CircularCoordinate> {
30        match self {
31            Self::Success(coordinate) => Some(coordinate),
32            Self::NotAttempted | Self::LiftFailed | Self::SolveFailed => None,
33        }
34    }
35}
36
37/// One entry in a checked circular-coordinate family.
38#[derive(Debug, Clone, PartialEq)]
39pub struct CircularCoordinateFamilyEntry {
40    /// Grid node.
41    pub grade: super::Bigrade,
42    /// Exact extension classification.
43    pub extension: ClassExtensionKind,
44    /// Computational outcome for this extension.
45    pub status: CircularCoordinateFamilyStatus,
46}
47
48/// Checked circle-valued coordinates and outcomes for one class extension family.
49#[derive(Debug, Clone, PartialEq)]
50pub struct CircularCoordinateFamily {
51    /// Grade of the selected class.
52    pub base_grade: super::Bigrade,
53    /// Selected class in the base node's canonical basis.
54    pub base_class: Vec<BipersistenceTerm>,
55    /// One entry at every grade in the upper parameter cone.
56    pub entries: Vec<CircularCoordinateFamilyEntry>,
57}
58
59impl BipersistenceModule {
60    /// Compute checked circular coordinates for every unique atlas extension.
61    ///
62    /// Ambiguous and absent extensions have status [`CircularCoordinateFamilyStatus::NotAttempted`].
63    /// Every unique extension has a lift failure, solve failure, or success
64    /// status. A failed computation is reported at its node, and does not
65    /// discard coordinates computed at other nodes.
66    /// Automatic family construction requires an odd prime coefficient modulus.
67    /// Modulus two requires a caller-supplied integral lift and is rejected
68    /// before any node computation.
69    pub fn circular_coordinate_family(
70        &self,
71        atlas: &CohomologyClassAtlas,
72        params: CircularCoordinateParams,
73    ) -> Result<CircularCoordinateFamily> {
74        validate_params(params)?;
75        if self.modulus() == 2 {
76            return Err(Error::InvalidInput(
77                "automatic circular families require an odd prime modulus".into(),
78            ));
79        }
80        if atlas.base_grade
81            != atlas
82                .extensions
83                .first()
84                .map(|extension| extension.grade)
85                .unwrap_or(atlas.base_grade)
86        {
87            return Err(Error::InvalidInput(
88                "the class atlas is not in canonical grid order".into(),
89            ));
90        }
91        let rebuilt = self.class_atlas(atlas.base_grade, &atlas.base_class)?;
92        if rebuilt != *atlas {
93            return Err(Error::InvalidInput(
94                "the class atlas belongs to a different bipersistence module".into(),
95            ));
96        }
97        let mut entries = Vec::with_capacity(atlas.extensions.len());
98        for extension in &atlas.extensions {
99            let status = if extension.kind == ClassExtensionKind::Unique {
100                self.coordinate_status(extension, params)?
101            } else {
102                CircularCoordinateFamilyStatus::NotAttempted
103            };
104            entries.push(CircularCoordinateFamilyEntry {
105                grade: extension.grade,
106                extension: extension.kind,
107                status,
108            });
109        }
110        Ok(CircularCoordinateFamily {
111            base_grade: atlas.base_grade,
112            base_class: atlas.base_class.clone(),
113            entries,
114        })
115    }
116
117    fn coordinate_status(
118        &self,
119        extension: &super::ClassExtension,
120        params: CircularCoordinateParams,
121    ) -> Result<CircularCoordinateFamilyStatus> {
122        let position = self.node_index(extension.grade);
123        let coordinates = extension
124            .class
125            .iter()
126            .map(|term| (term.basis_index, term.coefficient))
127            .collect::<Vec<_>>();
128        let rows = self.spaces[position]
129            .cocycle_from_coordinates(&coordinates)?
130            .into_iter()
131            .map(|term| (term.simplex[0], term.simplex[1], term.coefficient))
132            .collect::<Vec<_>>();
133        let cocycle =
134            crate::cocycle_from_ripser_terms(&self.graphs[position], self.modulus(), 0.0, &rows)?;
135        let status = match circular_coordinate_with_failure_stage(
136            &self.graphs[position],
137            &cocycle,
138            params,
139        ) {
140            Ok(coordinate) => CircularCoordinateFamilyStatus::Success(Box::new(coordinate)),
141            Err(CircularCoordinateFailure::Lift(_)) => CircularCoordinateFamilyStatus::LiftFailed,
142            Err(CircularCoordinateFailure::Solve(_)) => CircularCoordinateFamilyStatus::SolveFailed,
143            Err(CircularCoordinateFailure::Other(error)) => return Err(error),
144        };
145        Ok(status)
146    }
147}
148
149#[cfg(test)]
150#[path = "circular_tests.rs"]
151mod tests;