Skip to main content

holos_tda/coverage/
model.rs

1use std::collections::BTreeSet;
2
3use num_rational::BigRational;
4
5use crate::field::{MODULUS_LIMIT, is_prime};
6use crate::{Error, KineticEdgeKey, Result};
7
8/// Resource limits for one relative coverage calculation.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub struct CoverageLimits {
12    /// Largest accepted vertex count.
13    pub max_vertices: usize,
14    /// Largest accepted active edge count.
15    pub max_edges: usize,
16    /// Largest accepted active triangle count.
17    pub max_triangles: usize,
18    /// Largest accepted dense linear-system entry count.
19    pub max_matrix_entries: usize,
20    /// Largest accepted state count.
21    pub max_states: usize,
22    /// Largest accepted action count.
23    pub max_actions: usize,
24    /// Largest failure set count checked in one plan evaluation.
25    pub max_failure_sets: usize,
26}
27
28impl Default for CoverageLimits {
29    fn default() -> Self {
30        Self {
31            max_vertices: 1_000_000,
32            max_edges: 20_000_000,
33            max_triangles: 1_000_000,
34            max_matrix_entries: 100_000_000,
35            max_states: 4_096,
36            max_actions: 65_536,
37            max_failure_sets: 10_000_000,
38        }
39    }
40}
41
42/// Declared geometric contract for the planar controlled-boundary theorem.
43///
44/// The constructor checks the exact radius inequality from assumption A2.
45/// The caller declares A3: nodes lie in one compact connected planar domain.
46/// The caller declares geometric A4: the fence cycle maps to that domain's
47/// connected piecewise-linear boundary. The checker validates the graph,
48/// unique labels, fence edges, radii, and relative chain. It cannot recover
49/// the domain from connectivity data.
50#[derive(Debug, Clone, Copy, PartialEq)]
51pub struct PlanarCoverageModel {
52    broadcast_radius: f64,
53    sensing_radius: f64,
54}
55
56impl PlanarCoverageModel {
57    /// Construct the controlled-boundary radius contract.
58    ///
59    /// Both radii must be finite and positive. The exact dyadic values must
60    /// satisfy `3 * sensing_radius^2 >= broadcast_radius^2`.
61    pub fn new(broadcast_radius: f64, sensing_radius: f64) -> Result<Self> {
62        if !broadcast_radius.is_finite()
63            || !sensing_radius.is_finite()
64            || broadcast_radius <= 0.0
65            || sensing_radius <= 0.0
66        {
67            return Err(Error::InvalidInput(
68                "coverage radii must be finite and positive".into(),
69            ));
70        }
71        let broadcast = rational(broadcast_radius);
72        let sensing = rational(sensing_radius);
73        if BigRational::from_integer(3.into()) * &sensing * sensing < broadcast.clone() * broadcast
74        {
75            return Err(Error::InvalidInput(
76                "coverage radii violate 3 * sensing_radius^2 >= broadcast_radius^2".into(),
77            ));
78        }
79        Ok(Self {
80            broadcast_radius,
81            sensing_radius,
82        })
83    }
84
85    /// Radius used to construct the communication graph.
86    pub fn broadcast_radius(self) -> f64 {
87        self.broadcast_radius
88    }
89
90    /// Radius of each declared sensing disc.
91    pub fn sensing_radius(self) -> f64 {
92        self.sensing_radius
93    }
94}
95
96/// A simple oriented fence cycle in global vertex labels.
97///
98/// Construction chooses one canonical rotation and orientation. The cycle
99/// remains one-dimensional even when the Rips complex contains fence chords
100/// or triangles.
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct CoverageFence {
103    vertices: Vec<usize>,
104}
105
106impl CoverageFence {
107    /// Construct a canonical fence from its cyclic vertex order.
108    pub fn new(vertices: Vec<usize>) -> Result<Self> {
109        if vertices.len() < 3 {
110            return Err(Error::InvalidInput(
111                "coverage fence requires at least three vertices".into(),
112            ));
113        }
114        if vertices.iter().copied().collect::<BTreeSet<_>>().len() != vertices.len() {
115            return Err(Error::InvalidInput(
116                "coverage fence repeats a vertex".into(),
117            ));
118        }
119        let forward = rotate_to_minimum(&vertices);
120        let mut reversed = vertices;
121        reversed.reverse();
122        let reversed = rotate_to_minimum(&reversed);
123        Ok(Self {
124            vertices: forward.min(reversed),
125        })
126    }
127
128    /// Canonical cyclic vertex order.
129    pub fn vertices(&self) -> &[usize] {
130        &self.vertices
131    }
132
133    pub(crate) fn edges(&self) -> Vec<KineticEdgeKey> {
134        cycle_pairs(&self.vertices)
135            .map(|(u, v)| KineticEdgeKey::new(u, v))
136            .collect()
137    }
138}
139
140/// One nonzero coefficient of a relative coverage two-chain.
141#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
142pub struct CoverageTriangleTerm {
143    /// First triangle vertex.
144    pub a: usize,
145    /// Second triangle vertex.
146    pub b: usize,
147    /// Third triangle vertex.
148    pub c: usize,
149    /// Coefficient in `1..modulus`.
150    pub coefficient: u32,
151}
152
153/// Exact result of one controlled-boundary criterion calculation.
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct CoverageEvaluation {
156    /// True when the canonical fence cycle bounds in the active Rips complex.
157    pub criterion_holds: bool,
158    /// Canonical solution with every free variable set to zero.
159    pub witness: Vec<CoverageTriangleTerm>,
160    /// Active vertex count used by the calculation.
161    pub active_vertices: usize,
162    /// Active edge count used by the calculation.
163    pub active_edges: usize,
164    /// Active triangle count used by the calculation.
165    pub active_triangles: usize,
166}
167
168pub(crate) fn cycle_pairs(vertices: &[usize]) -> impl Iterator<Item = (usize, usize)> + '_ {
169    vertices
170        .iter()
171        .copied()
172        .zip(vertices.iter().copied().cycle().skip(1))
173        .take(vertices.len())
174}
175
176fn rotate_to_minimum(values: &[usize]) -> Vec<usize> {
177    let position = values
178        .iter()
179        .enumerate()
180        .min_by_key(|(_, value)| **value)
181        .map(|(position, _)| position)
182        .unwrap_or(0);
183    values[position..]
184        .iter()
185        .chain(&values[..position])
186        .copied()
187        .collect()
188}
189
190pub(crate) fn validate_field(modulus: u32) -> Result<()> {
191    if u64::from(modulus) >= MODULUS_LIMIT || !is_prime(u64::from(modulus)) {
192        return Err(Error::InvalidInput(
193            "coverage modulus must be a supported prime".into(),
194        ));
195    }
196    Ok(())
197}
198
199fn rational(value: f64) -> BigRational {
200    BigRational::from_float(value).expect("validated finite f64 has an exact rational form")
201}