1pub const SOLVER_ABI_IDENTITY: &str = "joint-solver/decompose-dp-bb/1";
18
19#[derive(Debug, PartialEq, Eq)]
23pub enum SolverError {
24 ResourceExhausted { fuel_spent: u64, fuel_limit: u64 },
26}
27
28impl std::fmt::Display for SolverError {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 SolverError::ResourceExhausted {
32 fuel_spent,
33 fuel_limit,
34 } => write!(
35 f,
36 "solver fuel exhausted: spent {fuel_spent} of {fuel_limit} node expansions"
37 ),
38 }
39 }
40}
41
42impl std::error::Error for SolverError {}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum Feasibility {
50 None,
51 One,
52 Many,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum SolveStrategy {
60 ExactDp,
61 BranchAndBound,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct Component {
69 pub variables: Vec<u32>,
70 pub edges: Vec<(u32, u32)>,
71 pub width_bound: u32,
72}
73
74impl Component {
75 pub fn strategy(&self, pinned_width: u32) -> SolveStrategy {
79 if self.width_bound <= pinned_width {
80 SolveStrategy::ExactDp
81 } else {
82 SolveStrategy::BranchAndBound
83 }
84 }
85}
86
87pub struct ConstraintGraph {
92 num_variables: u32,
93 edges: Vec<(u32, u32)>,
94}
95
96impl ConstraintGraph {
97 pub fn new(num_variables: u32, edges: impl IntoIterator<Item = (u32, u32)>) -> Self {
98 let edges: Vec<(u32, u32)> = edges
99 .into_iter()
100 .map(|(a, b)| {
101 assert!(
102 a < num_variables && b < num_variables,
103 "constraint edge ({a}, {b}) references a variable outside 0..{num_variables}"
104 );
105 assert!(a != b, "self-loop constraint edge on variable {a}");
106 (a.min(b), a.max(b))
107 })
108 .collect();
109 Self {
110 num_variables,
111 edges,
112 }
113 }
114
115 pub fn decompose(&self) -> Vec<Component> {
121 let n = self.num_variables as usize;
122 let mut parent: Vec<u32> = (0..self.num_variables).collect();
123
124 fn find(parent: &mut [u32], x: u32) -> u32 {
125 let mut root = x;
126 while parent[root as usize] != root {
127 root = parent[root as usize];
128 }
129 let mut cur = x;
130 while parent[cur as usize] != root {
131 let next = parent[cur as usize];
132 parent[cur as usize] = root;
133 cur = next;
134 }
135 root
136 }
137
138 for &(a, b) in &self.edges {
139 let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
140 if ra != rb {
141 let (lo, hi) = (ra.min(rb), ra.max(rb));
144 parent[hi as usize] = lo;
145 }
146 }
147
148 let mut members: Vec<Vec<u32>> = vec![Vec::new(); n];
149 for v in 0..self.num_variables {
150 let root = find(&mut parent, v);
151 members[root as usize].push(v);
152 }
153 let mut component_edges: Vec<Vec<(u32, u32)>> = vec![Vec::new(); n];
154 for &(a, b) in &self.edges {
155 let root = find(&mut parent, a);
156 component_edges[root as usize].push((a, b));
157 }
158
159 (0..n)
160 .filter(|&root| !members[root].is_empty())
161 .map(|root| {
162 let variables = members[root].clone();
163 let mut edges = component_edges[root].clone();
164 edges.sort_unstable();
165 edges.dedup();
166 let width_bound = elimination_width_bound(&variables, &edges);
167 Component {
168 variables,
169 edges,
170 width_bound,
171 }
172 })
173 .collect()
174 }
175}
176
177fn elimination_width_bound(variables: &[u32], edges: &[(u32, u32)]) -> u32 {
182 use std::collections::{BTreeMap, BTreeSet};
183
184 let mut adj: BTreeMap<u32, BTreeSet<u32>> =
185 variables.iter().map(|&v| (v, BTreeSet::new())).collect();
186 for &(a, b) in edges {
187 adj.get_mut(&a).unwrap().insert(b);
188 adj.get_mut(&b).unwrap().insert(a);
189 }
190
191 let mut width = 0u32;
192 while !adj.is_empty() {
193 let (&v, _) = adj
195 .iter()
196 .min_by_key(|(idx, neigh)| (neigh.len(), **idx))
197 .expect("non-empty adjacency");
198 let neighbors: Vec<u32> = adj[&v].iter().copied().collect();
199 width = width.max(neighbors.len() as u32);
200 for &n in &neighbors {
201 let set = adj.get_mut(&n).expect("neighbor present");
202 set.remove(&v);
203 for &m in &neighbors {
204 if m != n {
205 set.insert(m);
206 }
207 }
208 }
209 adj.remove(&v);
210 }
211 width
212}
213
214pub fn candidate_components(num_entities: u32, pairs: &[(u32, u32)]) -> (Vec<u32>, Vec<u32>) {
222 let n = pairs.len();
223 let mut parent: Vec<u32> = (0..n as u32).collect();
224
225 fn find(parent: &mut [u32], x: u32) -> u32 {
226 let mut root = x;
227 while parent[root as usize] != root {
228 root = parent[root as usize];
229 }
230 let mut cur = x;
231 while parent[cur as usize] != root {
232 let next = parent[cur as usize];
233 parent[cur as usize] = root;
234 cur = next;
235 }
236 root
237 }
238
239 let mut entity_owner: Vec<Option<u32>> = vec![None; num_entities as usize];
240 for (i, &(head, tail)) in pairs.iter().enumerate() {
241 for entity in [head, tail] {
242 assert!(
243 entity < num_entities,
244 "candidate {i} references entity {entity} outside 0..{num_entities}"
245 );
246 match entity_owner[entity as usize] {
247 None => entity_owner[entity as usize] = Some(i as u32),
248 Some(owner) => {
249 let (ra, rb) = (find(&mut parent, i as u32), find(&mut parent, owner));
250 if ra != rb {
251 let (lo, hi) = (ra.min(rb), ra.max(rb));
252 parent[hi as usize] = lo;
253 }
254 }
255 }
256 }
257 }
258
259 let mut members: Vec<Vec<u32>> = vec![Vec::new(); n];
260 for cand in 0..n as u32 {
261 let root = find(&mut parent, cand);
262 members[root as usize].push(cand);
263 }
264 let mut offsets = Vec::new();
265 let mut indices = Vec::new();
266 offsets.push(0u32);
267 for group in members.into_iter().filter(|g| !g.is_empty()) {
268 indices.extend_from_slice(&group);
269 offsets.push(indices.len() as u32);
270 }
271 (offsets, indices)
272}
273
274#[derive(Debug)]
281pub struct FuelMeter {
282 limit: u64,
283 spent: u64,
284}
285
286impl FuelMeter {
287 pub fn new(limit: u64) -> Self {
288 Self { limit, spent: 0 }
289 }
290
291 pub fn spent(&self) -> u64 {
292 self.spent
293 }
294
295 pub fn remaining(&self) -> u64 {
297 self.limit - self.spent
298 }
299
300 pub fn refund(&mut self, expansions: u64) {
305 self.spent = self.spent.saturating_sub(expansions);
306 }
307
308 pub fn charge(&mut self, expansions: u64) -> Result<(), SolverError> {
312 let new_spent = self.spent.saturating_add(expansions);
313 if new_spent > self.limit {
314 return Err(SolverError::ResourceExhausted {
315 fuel_spent: self.spent,
316 fuel_limit: self.limit,
317 });
318 }
319 self.spent = new_spent;
320 Ok(())
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn decomposition_is_deterministic_under_edge_order() {
330 let edges = [(3, 1), (7, 5), (1, 0), (5, 6)];
331 let mut reversed = edges;
332 reversed.reverse();
333 let a = ConstraintGraph::new(9, edges).decompose();
334 let b = ConstraintGraph::new(9, reversed).decompose();
335 assert_eq!(a, b, "edge input order must not change the decomposition");
336 }
337
338 #[test]
339 fn every_variable_lands_in_exactly_one_component() {
340 let graph = ConstraintGraph::new(6, [(0, 1), (4, 5)]);
341 let components = graph.decompose();
342 let mut seen: Vec<u32> = components
343 .iter()
344 .flat_map(|c| c.variables.iter().copied())
345 .collect();
346 seen.sort_unstable();
347 assert_eq!(seen, vec![0, 1, 2, 3, 4, 5]);
348 assert_eq!(components.len(), 4);
351 assert!(components
352 .iter()
353 .any(|c| c.variables == vec![2] && c.edges.is_empty()));
354 }
355
356 #[test]
357 fn components_are_canonical_and_ordered_by_minimum_variable() {
358 let graph = ConstraintGraph::new(7, [(6, 4), (2, 0), (4, 5)]);
359 let components = graph.decompose();
360 let mins: Vec<u32> = components.iter().map(|c| c.variables[0]).collect();
361 let mut sorted = mins.clone();
362 sorted.sort_unstable();
363 assert_eq!(mins, sorted, "components ordered by minimum variable");
364 for c in &components {
365 let mut vars = c.variables.clone();
366 vars.sort_unstable();
367 assert_eq!(vars, c.variables, "variables ascending");
368 let mut edges = c.edges.clone();
369 edges.sort_unstable();
370 assert_eq!(edges, c.edges, "edges normalized and sorted");
371 assert!(c.edges.iter().all(|(a, b)| a < b), "edges are (low, high)");
372 }
373 }
374
375 #[test]
376 fn width_bound_matches_known_graphs() {
377 let path = ConstraintGraph::new(4, [(0, 1), (1, 2), (2, 3)]).decompose();
379 assert_eq!(path[0].width_bound, 1);
380 let star = ConstraintGraph::new(5, [(0, 1), (0, 2), (0, 3), (0, 4)]).decompose();
382 assert_eq!(star[0].width_bound, 1);
383 let k4 =
385 ConstraintGraph::new(4, [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).decompose();
386 assert_eq!(k4[0].width_bound, 3);
387 }
388
389 #[test]
390 fn strategy_splits_on_the_pinned_envelope() {
391 let k4 =
392 ConstraintGraph::new(4, [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]).decompose();
393 assert_eq!(k4[0].strategy(3), SolveStrategy::ExactDp);
394 assert_eq!(k4[0].strategy(2), SolveStrategy::BranchAndBound);
395 }
396
397 #[test]
398 fn candidate_components_join_on_shared_entities_deterministically() {
399 let pairs = [(0, 1), (1, 2), (3, 4)];
401 let (offsets, indices) = candidate_components(5, &pairs);
402 assert_eq!(offsets, vec![0, 2, 3]);
403 assert_eq!(indices, vec![0, 1, 2]);
404
405 let flipped = [(1, 0), (2, 1), (4, 3)];
409 let (offsets_f, indices_f) = candidate_components(5, &flipped);
410 assert_eq!((offsets_f, indices_f), (offsets, indices));
411
412 let (offsets, indices) = candidate_components(3, &[(0, 1), (1, 2), (0, 2)]);
414 assert_eq!(offsets, vec![0, 3]);
415 let mut sorted = indices.clone();
416 sorted.sort_unstable();
417 assert_eq!(sorted, vec![0, 1, 2]);
418 }
419
420 #[test]
421 fn fuel_refuses_typed_at_the_boundary_and_saturates() {
422 let mut fuel = FuelMeter::new(10);
423 fuel.charge(10).expect("exactly the budget is legal");
424 let err = fuel.charge(1).expect_err("beyond fuel must refuse");
425 assert_eq!(
426 err,
427 SolverError::ResourceExhausted {
428 fuel_spent: 10,
429 fuel_limit: 10
430 }
431 );
432 assert_eq!(fuel.spent(), 10);
435 assert_eq!(
436 fuel.charge(1).expect_err("still refused"),
437 SolverError::ResourceExhausted {
438 fuel_spent: 10,
439 fuel_limit: 10
440 }
441 );
442 }
443}