holos_tda/coverage_synthesis/
specification.rs1use std::collections::BTreeMap;
4
5use crate::monotone_proof::ProofLimits;
6use crate::{
7 CoverageFence, CoverageLimits, Error, KineticEdge, KineticEdgeKey, KineticFiltration,
8 KineticLimits, PlanarCoverageModel, Result, SparseDistanceMatrix,
9};
10
11use super::super::evaluate::{find_set, union_sets, validate_actions};
12use super::types::{CoverageAction, CoverageComponent};
13
14pub(crate) const FORMAT_MAX_PROOF_NODES: usize = 10_000_000;
15pub(crate) const FORMAT_MAX_PROOF_TERMS: usize = 100_000_000;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19#[non_exhaustive]
20pub struct CoverageSynthesisLimits {
21 pub max_bytes: usize,
23 pub max_oracle_calls: usize,
25 pub max_search_nodes: usize,
27 pub max_proof_nodes: usize,
29 pub max_proof_depth: usize,
31 pub max_proof_terms: usize,
33 pub coverage: CoverageLimits,
35 pub kinetic: KineticLimits,
37}
38
39impl Default for CoverageSynthesisLimits {
40 fn default() -> Self {
41 Self {
42 max_bytes: 1 << 30,
43 max_oracle_calls: 2_000_000,
44 max_search_nodes: 2_000_000,
45 max_proof_nodes: 2_000_000,
46 max_proof_depth: 1_024,
47 max_proof_terms: 10_000_000,
48 coverage: CoverageLimits::default(),
49 kinetic: KineticLimits::default(),
50 }
51 }
52}
53
54impl CoverageSynthesisLimits {
55 #[must_use]
57 pub fn with_max_oracle_calls(mut self, maximum: usize) -> Self {
58 self.max_oracle_calls = maximum;
59 self
60 }
61
62 #[must_use]
64 pub fn with_max_search_nodes(mut self, maximum: usize) -> Self {
65 self.max_search_nodes = maximum;
66 self
67 }
68
69 pub(crate) fn proof(self) -> ProofLimits {
70 ProofLimits {
71 nodes: self.max_proof_nodes.min(FORMAT_MAX_PROOF_NODES),
72 depth: self.max_proof_depth,
73 terms: self.max_proof_terms.min(FORMAT_MAX_PROOF_TERMS),
74 checks: self.max_oracle_calls,
75 }
76 }
77}
78
79#[derive(Debug, Clone, PartialEq)]
81pub enum CoverageSource {
82 Finite,
84 Affine {
87 scenario: u64,
89 edges: Vec<KineticEdge>,
91 start: f64,
93 end: f64,
95 },
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct CoverageState {
101 pub(crate) scenario: u64,
102 pub(crate) step: u64,
103 pub(crate) base_vertices: Vec<usize>,
104 pub(crate) possible_edges: Vec<KineticEdgeKey>,
105}
106
107impl CoverageState {
108 pub fn new(
110 scenario: u64,
111 step: u64,
112 graph: &SparseDistanceMatrix,
113 mut base_vertices: Vec<usize>,
114 broadcast_radius: f64,
115 ) -> Result<Self> {
116 base_vertices.sort_unstable();
117 base_vertices.dedup();
118 if !broadcast_radius.is_finite()
119 || broadcast_radius <= 0.0
120 || base_vertices.iter().any(|vertex| *vertex >= graph.len())
121 {
122 return Err(Error::InvalidInput(
123 "coverage state has an invalid radius or base vertex".into(),
124 ));
125 }
126 Ok(Self {
127 scenario,
128 step,
129 base_vertices,
130 possible_edges: graph
131 .edges()
132 .filter(|edge| edge.2 <= broadcast_radius)
133 .map(|(u, v, _)| KineticEdgeKey::new(u, v))
134 .collect(),
135 })
136 }
137
138 pub fn scenario(&self) -> u64 {
140 self.scenario
141 }
142
143 pub fn step(&self) -> u64 {
145 self.step
146 }
147
148 pub fn base_vertices(&self) -> &[usize] {
150 &self.base_vertices
151 }
152
153 pub fn possible_edges(&self) -> &[KineticEdgeKey] {
155 &self.possible_edges
156 }
157}
158
159#[derive(Debug, Clone, PartialEq)]
161pub struct CoverageSpecification {
162 pub(crate) vertex_count: usize,
163 pub(crate) model: PlanarCoverageModel,
164 pub(crate) modulus: u32,
165 pub(crate) fence: CoverageFence,
166 pub(crate) failable_vertices: Vec<usize>,
167 pub(crate) failure_budget: usize,
168 pub(crate) source: CoverageSource,
169 pub(crate) states: Vec<CoverageState>,
170}
171
172impl CoverageSpecification {
173 #[allow(clippy::too_many_arguments)]
175 pub fn new(
176 vertex_count: usize,
177 model: PlanarCoverageModel,
178 modulus: u32,
179 fence: CoverageFence,
180 mut failable_vertices: Vec<usize>,
181 failure_budget: usize,
182 mut states: Vec<CoverageState>,
183 limits: CoverageLimits,
184 ) -> Result<Self> {
185 failable_vertices.sort_unstable();
186 failable_vertices.dedup();
187 states.sort_by_key(|state| (state.scenario, state.step));
188 let specification = Self {
189 vertex_count,
190 model,
191 modulus,
192 fence,
193 failable_vertices,
194 failure_budget,
195 source: CoverageSource::Finite,
196 states,
197 };
198 specification.validate(limits)?;
199 Ok(specification)
200 }
201
202 #[allow(clippy::too_many_arguments)]
204 pub fn from_kinetic(
205 filtration: &KineticFiltration,
206 scenario: u64,
207 model: PlanarCoverageModel,
208 modulus: u32,
209 fence: CoverageFence,
210 failable_vertices: Vec<usize>,
211 failure_budget: usize,
212 base_vertices: Vec<usize>,
213 limits: CoverageLimits,
214 ) -> Result<Self> {
215 let states = filtration
216 .critical_graphs(model.broadcast_radius())?
217 .into_iter()
218 .enumerate()
219 .map(|(step, state)| {
220 CoverageState::new(
221 scenario,
222 step as u64,
223 &state.graph,
224 base_vertices.clone(),
225 model.broadcast_radius(),
226 )
227 })
228 .collect::<Result<Vec<_>>>()?;
229 let mut specification = Self::new(
230 filtration.vertex_count(),
231 model,
232 modulus,
233 fence,
234 failable_vertices,
235 failure_budget,
236 states,
237 limits,
238 )?;
239 specification.source = CoverageSource::Affine {
240 scenario,
241 edges: filtration.edges().to_vec(),
242 start: filtration.start(),
243 end: filtration.end(),
244 };
245 Ok(specification)
246 }
247
248 pub fn vertex_count(&self) -> usize {
250 self.vertex_count
251 }
252
253 pub fn model(&self) -> PlanarCoverageModel {
255 self.model
256 }
257
258 pub fn modulus(&self) -> u32 {
260 self.modulus
261 }
262
263 pub fn fence(&self) -> &CoverageFence {
265 &self.fence
266 }
267
268 pub fn failable_vertices(&self) -> &[usize] {
270 &self.failable_vertices
271 }
272
273 pub fn failure_budget(&self) -> usize {
275 self.failure_budget
276 }
277
278 pub fn source(&self) -> &CoverageSource {
280 &self.source
281 }
282
283 pub fn states(&self) -> &[CoverageState] {
285 &self.states
286 }
287
288 pub(crate) fn validate(&self, limits: CoverageLimits) -> Result<()> {
289 PlanarCoverageModel::new(self.model.broadcast_radius(), self.model.sensing_radius())?;
290 self.validate_scope(limits)?;
291 if self
292 .states
293 .windows(2)
294 .any(|pair| (pair[0].scenario, pair[0].step) >= (pair[1].scenario, pair[1].step))
295 {
296 return Err(Error::InvalidInput(
297 "coverage states are not in canonical scenario and step order".into(),
298 ));
299 }
300 for state in &self.states {
301 self.validate_state(state, limits)?;
302 }
303 Ok(())
304 }
305
306 pub(super) fn validate_scope(&self, limits: CoverageLimits) -> Result<()> {
307 let invalid_failable = self
308 .failable_vertices
309 .iter()
310 .any(|vertex| *vertex >= self.vertex_count);
311 let failable_fence = self
312 .fence
313 .vertices()
314 .iter()
315 .any(|vertex| self.failable_vertices.binary_search(vertex).is_ok());
316 if self.vertex_count == 0
317 || self.vertex_count > limits.max_vertices
318 || self.states.is_empty()
319 || self.states.len() > limits.max_states
320 || self
321 .fence
322 .vertices()
323 .iter()
324 .any(|vertex| *vertex >= self.vertex_count)
325 || invalid_failable
326 || failable_fence
327 || self.failure_budget > self.failable_vertices.len()
328 {
329 Err(Error::InvalidInput(
330 "coverage specification has an invalid scope, fence, or failure model".into(),
331 ))
332 } else {
333 Ok(())
334 }
335 }
336
337 pub(super) fn validate_state(
338 &self,
339 state: &CoverageState,
340 limits: CoverageLimits,
341 ) -> Result<()> {
342 let invalid_edge = state
343 .possible_edges
344 .iter()
345 .any(|edge| edge.u >= edge.v || edge.v >= self.vertex_count);
346 let missing_fence = self
347 .fence
348 .vertices()
349 .iter()
350 .any(|vertex| state.base_vertices.binary_search(vertex).is_err());
351 if state
352 .base_vertices
353 .iter()
354 .any(|vertex| *vertex >= self.vertex_count)
355 || state.possible_edges.len() > limits.max_edges
356 || invalid_edge
357 || state
358 .possible_edges
359 .windows(2)
360 .any(|pair| pair[0] >= pair[1])
361 || missing_fence
362 {
363 Err(Error::InvalidInput(
364 "coverage state is not canonical or omits a fence vertex".into(),
365 ))
366 } else {
367 Ok(())
368 }
369 }
370
371 pub fn components(&self, actions: &[CoverageAction]) -> Result<Vec<CoverageComponent>> {
373 validate_actions(self, actions, CoverageLimits::default())?;
374 let offset = self.states.len();
375 let mut parent = (0..offset + actions.len()).collect::<Vec<_>>();
376 for (action, candidate) in actions.iter().enumerate() {
377 for &state in &candidate.states {
378 union_sets(&mut parent, state, offset + action);
379 }
380 }
381 let mut components = BTreeMap::<usize, CoverageComponent>::new();
382 for state in 0..self.states.len() {
383 let root = find_set(&mut parent, state);
384 components.entry(root).or_default().states.push(state);
385 }
386 for action in 0..actions.len() {
387 let root = find_set(&mut parent, offset + action);
388 components.entry(root).or_default().actions.push(action);
389 }
390 let mut output = components.into_values().collect::<Vec<_>>();
391 output.sort_by_key(|component| {
392 (
393 component.states.first().copied().unwrap_or(usize::MAX),
394 component.actions.first().copied().unwrap_or(usize::MAX),
395 )
396 });
397 Ok(output)
398 }
399}