1use std::collections::{BTreeMap, BTreeSet};
2
3use super::Result;
4use super::model::{
5 EdgeWeightEdit, H1Intervention, InterventionArtifact, InterventionBudget, InterventionStatus,
6};
7use super::primitives::maximum_edit;
8use crate::{
9 EdgeKey, Error, IntervalGroupId, PersistenceProgram, ProgramTraceArtifact, ProgramUpdateMode,
10 SparseDistanceMatrix, VerifiedProgramTrace,
11};
12
13impl PersistenceProgram {
14 pub fn kill_h1_before(
23 &self,
24 target: IntervalGroupId,
25 target_scale: f64,
26 budget: InterventionBudget,
27 ) -> Result<H1Intervention> {
28 let space = validate_intervention_request(self, target, target_scale)?;
29 if budget.max_candidates == 0 {
30 return Ok(budget_limited_intervention(target, target_scale));
31 }
32 search_intervention(self, space, target, target_scale)
33 }
34}
35
36fn validate_intervention_request(
37 program: &PersistenceProgram,
38 target: IntervalGroupId,
39 target_scale: f64,
40) -> Result<&crate::PersistentClassSpace> {
41 if !target_scale.is_finite() || target_scale < 0.0 {
42 return Err(Error::InvalidInput(format!(
43 "intervention target scale must be non-negative and finite, got {target_scale}"
44 )));
45 }
46 let space = program
47 .result()
48 .spaces
49 .iter()
50 .find(|space| space.id == target)
51 .ok_or_else(|| Error::InvalidInput(format!("unknown class space {target}")))?;
52 check_intervention_interval(space, target_scale)?;
53 Ok(space)
54}
55
56fn check_intervention_interval(
57 space: &crate::PersistentClassSpace,
58 target_scale: f64,
59) -> Result<()> {
60 if space.interval.is_essential() {
61 return Err(Error::InvalidInput(
62 "the finite-death intervention does not support an essential class space".into(),
63 ));
64 }
65 if target_scale <= space.interval.birth || target_scale >= space.interval.death {
66 return Err(Error::InvalidInput(format!(
67 "target scale must lie strictly inside ({}, {})",
68 space.interval.birth, space.interval.death
69 )));
70 }
71 Ok(())
72}
73
74fn budget_limited_intervention(target: IntervalGroupId, target_scale: f64) -> H1Intervention {
75 H1Intervention {
76 target,
77 target_scale,
78 status: InterventionStatus::BudgetLimited,
79 lower_bound: 0.0,
80 upper_bound: None,
81 edits: Vec::new(),
82 result: None,
83 artifact: None,
84 }
85}
86
87fn search_intervention(
88 program: &PersistenceProgram,
89 space: &crate::PersistentClassSpace,
90 target: IntervalGroupId,
91 target_scale: f64,
92) -> Result<H1Intervention> {
93 let edits = destroyer_edits(program.current_graph(), space, target_scale)?;
94 if edits.is_empty() {
95 return Err(Error::InvalidInput(
96 "declared destroyer triangles need no edge edit".into(),
97 ));
98 }
99 let updated = apply_edits(program.current_graph(), &edits)?;
100 let trace = ProgramTraceArtifact::build(
101 program.current_graph(),
102 std::slice::from_ref(&updated),
103 program.params(),
104 program.limits(),
105 )?;
106 let verified = trace.verify(program.limits())?;
107 if !continued_space_dies_by(&verified, target, target_scale) {
108 return Ok(budget_limited_intervention(target, target_scale));
109 }
110 finish_intervention(program, space, target, target_scale, edits, trace, verified)
111}
112
113fn finish_intervention(
114 program: &PersistenceProgram,
115 space: &crate::PersistentClassSpace,
116 target: IntervalGroupId,
117 target_scale: f64,
118 edits: Vec<EdgeWeightEdit>,
119 trace: ProgramTraceArtifact,
120 verified: VerifiedProgramTrace,
121) -> Result<H1Intervention> {
122 let upper_bound = maximum_edit(&edits);
123 let reused = verified.steps[0].mode == ProgramUpdateMode::Reused;
124 let lower_bound = if reused {
125 space.interval.death - target_scale
126 } else {
127 0.0
128 };
129 let status = if reused && lower_bound.to_bits() == upper_bound.to_bits() {
130 InterventionStatus::Optimal
131 } else {
132 InterventionStatus::BoundedGap
133 };
134 let artifact = InterventionArtifact {
135 target,
136 target_scale,
137 status,
138 lower_bound,
139 upper_bound,
140 edits: edits.clone(),
141 trace,
142 };
143 artifact.verify(program.limits())?;
144 Ok(H1Intervention {
145 target,
146 target_scale,
147 status,
148 lower_bound,
149 upper_bound: Some(upper_bound),
150 edits,
151 result: Some(verified.final_program.result().clone()),
152 artifact: Some(artifact),
153 })
154}
155pub(super) fn destroyer_edits(
156 graph: &SparseDistanceMatrix,
157 space: &crate::PersistentClassSpace,
158 target_scale: f64,
159) -> Result<Vec<EdgeWeightEdit>> {
160 let mut edges = BTreeSet::new();
161 for pair in &space.critical_pairs {
162 let death = pair.death.as_ref().ok_or_else(|| {
163 Error::InvalidInput("finite class space has no destroyer triangle".into())
164 })?;
165 let [u, v, w]: [usize; 3] = death
166 .vertices
167 .as_slice()
168 .try_into()
169 .map_err(|_| Error::InvalidInput("destroyer is not a triangle".into()))?;
170 for edge in [EdgeKey::new(u, v), EdgeKey::new(u, w), EdgeKey::new(v, w)] {
171 if graph.get(edge.u, edge.v) > target_scale {
172 edges.insert(edge);
173 }
174 }
175 }
176 Ok(edges
177 .into_iter()
178 .map(|edge| EdgeWeightEdit {
179 edge,
180 before: graph.get(edge.u, edge.v),
181 after: target_scale,
182 })
183 .collect())
184}
185
186pub(super) fn apply_edits(
187 graph: &SparseDistanceMatrix,
188 edits: &[EdgeWeightEdit],
189) -> Result<SparseDistanceMatrix> {
190 let by_edge: BTreeMap<_, _> = edits.iter().map(|edit| (edit.edge, edit)).collect();
191 let triplets: Vec<_> = graph
192 .edges()
193 .map(|(u, v, value)| {
194 let edge = EdgeKey::new(u, v);
195 let value = by_edge.get(&edge).map_or(value, |edit| edit.after);
196 (u, v, value)
197 })
198 .collect();
199 if by_edge
200 .keys()
201 .any(|edge| graph.get(edge.u, edge.v).is_infinite())
202 {
203 return Err(Error::InvalidInput(
204 "intervention edit names an absent edge".into(),
205 ));
206 }
207 SparseDistanceMatrix::from_triplets(graph.len(), &triplets)
208}
209
210pub(super) fn continued_space_dies_by(
211 verified: &VerifiedProgramTrace,
212 target: IntervalGroupId,
213 target_scale: f64,
214) -> bool {
215 let Some(initial) = verified
216 .initial_result
217 .spaces
218 .iter()
219 .find(|space| space.id == target)
220 else {
221 return false;
222 };
223 let Some(step) = verified.steps.first() else {
224 return false;
225 };
226 let final_by_basis: BTreeMap<_, _> = verified
227 .final_program
228 .result()
229 .spaces
230 .iter()
231 .flat_map(|space| space.basis.iter().map(move |class| (class.id, space)))
232 .collect();
233 let mapped: BTreeMap<_, _> = step
234 .continuation
235 .iter()
236 .filter(|record| record.old_spaces.contains(&target))
237 .flat_map(|record| record.transport.iter().map(|term| (term.old, term.new)))
238 .collect();
239 initial.basis.iter().all(|class| {
240 mapped
241 .get(&class.id)
242 .and_then(|new| final_by_basis.get(new))
243 .is_some_and(|space| space.interval.death <= target_scale)
244 })
245}