1use crate::cdcl::Lit;
18use crate::complexity::RankedRefutation;
19use crate::proof::{Perm, ProofStep, Witness};
20
21pub struct Reduction {
26 pub image: Vec<Lit>,
28 pub target_num_vars: usize,
29}
30
31impl Reduction {
32 pub fn apply_lit(&self, l: Lit) -> Lit {
34 let img = self.image[l.var() as usize];
35 if l.is_positive() {
36 img
37 } else {
38 img.negated()
39 }
40 }
41
42 pub fn apply_clause(&self, c: &[Lit]) -> Vec<Lit> {
44 c.iter().map(|&l| self.apply_lit(l)).collect()
45 }
46
47 fn conjugate(&self, sigma: &Perm) -> Perm {
51 let nv = self.target_num_vars;
52 let mut inv: Vec<Option<usize>> = vec![None; nv];
53 for (v, img) in self.image.iter().enumerate() {
54 inv[img.var() as usize] = Some(v);
55 }
56 let images: Vec<Lit> = (0..nv)
57 .map(|w| match inv[w] {
58 Some(v) => self.apply_lit(sigma.apply(Lit::pos(v as u32))),
59 None => Lit::pos(w as u32),
60 })
61 .collect();
62 Perm::from_images(images)
63 }
64
65 pub fn apply_step(&self, step: &ProofStep) -> ProofStep {
67 match step {
68 ProofStep::Rup(c) => ProofStep::Rup(self.apply_clause(c)),
69 ProofStep::Delete(c) => ProofStep::Delete(self.apply_clause(c)),
70 ProofStep::Pr { clause, witness } => {
71 let w = match witness {
72 Witness::Assignment(a) => Witness::Assignment(a.iter().map(|&l| self.apply_lit(l)).collect()),
73 Witness::Substitution(sigma) => Witness::Substitution(self.conjugate(sigma)),
74 };
75 ProofStep::Pr { clause: self.apply_clause(clause), witness: w }
76 }
77 }
78 }
79}
80
81impl Reduction {
82 pub fn is_bijective(&self) -> bool {
85 if self.image.len() != self.target_num_vars {
86 return false;
87 }
88 let mut vars: Vec<u32> = self.image.iter().map(|l| l.var()).collect();
89 vars.sort_unstable();
90 vars.dedup();
91 vars.len() == self.target_num_vars
92 }
93
94 pub fn inverse(&self) -> Option<Reduction> {
96 if !self.is_bijective() {
97 return None;
98 }
99 let nv = self.target_num_vars;
100 let mut inv = vec![Lit::pos(0); nv];
101 for (v, &img) in self.image.iter().enumerate() {
102 inv[img.var() as usize] = Lit::new(v as u32, img.is_positive());
103 }
104 Some(Reduction { image: inv, target_num_vars: nv })
105 }
106
107 pub fn compose(&self, other: &Reduction) -> Reduction {
109 Reduction {
110 image: (0..other.image.len()).map(|v| self.apply_lit(other.image[v])).collect(),
111 target_num_vars: self.target_num_vars,
112 }
113 }
114
115 pub fn is_identity(&self) -> bool {
117 self.image.len() == self.target_num_vars
118 && self.image.iter().enumerate().all(|(v, &l)| l == Lit::pos(v as u32))
119 }
120
121 pub fn is_loop_at(&self, formula: &[Vec<Lit>]) -> bool {
125 if !self.is_bijective() {
126 return false;
127 }
128 let mapped: Vec<Vec<Lit>> = formula.iter().map(|c| self.apply_clause(c)).collect();
129 canon_clauses(&mapped) == canon_clauses(formula)
130 }
131}
132
133fn canon_clauses(cs: &[Vec<Lit>]) -> Vec<Vec<u32>> {
136 let mut out: Vec<Vec<u32>> = cs
137 .iter()
138 .map(|c| {
139 let mut k: Vec<u32> = c.iter().map(|l| l.var() * 2 + u32::from(!l.is_positive())).collect();
140 k.sort_unstable();
141 k.dedup();
142 k
143 })
144 .collect();
145 out.sort();
146 out
147}
148
149pub fn transfer(
154 reduction: &Reduction,
155 source_steps: &[ProofStep],
156 ranks: &[u64],
157 target_formula: &[Vec<Lit>],
158) -> Option<RankedRefutation> {
159 let steps: Vec<ProofStep> = source_steps.iter().map(|s| reduction.apply_step(s)).collect();
160 if crate::pr::check_pr_refutation_fast(reduction.target_num_vars, target_formula, &steps) {
161 Some(RankedRefutation { refuted: true, steps, ranks: ranks.to_vec() })
162 } else {
163 None
164 }
165}
166
167pub fn refines(fine: &[u64], coarse: &[u64]) -> bool {
172 if fine.len() != coarse.len() {
173 return false;
174 }
175 let n = fine.len();
176 for i in 0..n {
177 for j in 0..n {
178 if fine[i] >= fine[j] && coarse[i] < coarse[j] {
179 return false;
180 }
181 }
182 }
183 true
184}
185
186pub fn iso(a: &[u64], b: &[u64]) -> bool {
189 refines(a, b) && refines(b, a)
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn refinement_is_a_thin_category_a_preorder() {
198 let mut state = 0x2C0A_7E60_1234_ABCDu64;
202 let mut next = || {
203 state = state.wrapping_add(0x9E3779B97F4A7C15);
204 let mut z = state;
205 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
206 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
207 z ^ (z >> 31)
208 };
209 for _ in 0..10_000 {
210 let len = 1 + (next() as usize % 10);
211 let mut a: Vec<u64> = (0..len).map(|_| next() % 8).collect();
213 a.sort_unstable_by(|x, y| y.cmp(x));
214 assert!(refines(&a, &a), "refinement is reflexive (identity)");
216 let b: Vec<u64> = a.iter().map(|&x| x / 2).collect();
218 let c: Vec<u64> = b.iter().map(|&x| x / 2).collect();
219 assert!(refines(&a, &b), "a ⟶ b (a refines its coarsening)");
220 assert!(refines(&b, &c), "b ⟶ c");
221 assert!(refines(&a, &c), "composition: a ⟶ b ⟶ c ⟹ a ⟶ c");
223 }
224 }
225
226 #[test]
227 fn the_linear_measure_is_the_initial_object() {
228 let (php, _) = crate::families::php(7);
233 let (_, ranked) =
234 crate::lyapunov::solve_by_measure_synthesis(php.num_vars, &php.clauses).unwrap();
235 let symmetry = ranked.ranks.clone();
236 let linear = crate::lyapunov::proof_induced_measure(symmetry.len());
237 assert!(refines(&linear, &symmetry), "linear (finest) ⟶ symmetry measure");
239 let coarse: Vec<u64> = symmetry.iter().map(|&r| if r > 1 { 1 } else { 0 }).collect();
241 assert!(refines(&symmetry, &coarse), "symmetry ⟶ its 2-level coarsening");
242 assert!(refines(&linear, &coarse), "linear ⟶ coarse (the composite)");
244 assert!(!iso(&symmetry, &coarse), "the coarsening is a strict morphism, not an isomorphism");
246 }
247
248 #[test]
249 fn transfer_functor_carries_a_collapse_along_a_reduction() {
250 let n = 6;
255 let (php, _) = crate::families::php(n);
256 let nv = php.num_vars;
257 let (_, source) =
258 crate::lyapunov::solve_by_measure_synthesis(nv, &php.clauses).unwrap();
259 let perm: Vec<usize> = (0..nv).map(|v| (v * 7 + 3) % nv).collect();
261 let is_perm = {
263 let mut seen = perm.clone();
264 seen.sort_unstable();
265 seen.dedup();
266 seen.len() == nv
267 };
268 let perm: Vec<usize> = if is_perm { perm } else { (0..nv).rev().collect() };
269 let reduction = Reduction {
270 image: (0..nv).map(|v| crate::cdcl::Lit::pos(perm[v] as u32)).collect(),
271 target_num_vars: nv,
272 };
273 let target: Vec<Vec<crate::cdcl::Lit>> =
274 php.clauses.iter().map(|c| reduction.apply_clause(c)).collect();
275 let transferred = transfer(&reduction, &source.steps, &source.ranks, &target)
276 .expect("the transferred collapse must refute the renamed formula");
277 assert!(
278 crate::pr::check_pr_refutation_fast(nv, &target, &transferred.steps),
279 "the transferred proof re-checks against the target F"
280 );
281 assert_eq!(transferred.ranks, source.ranks, "the functor preserves the measure object (ranks)");
282 }
283
284 #[test]
285 fn invertible_reductions_form_a_groupoid() {
286 let mut state = 0x9A17_0117_BEEF_0042u64;
289 let mut next = || {
290 state = state.wrapping_add(0x9E3779B97F4A7C15);
291 let mut z = state;
292 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
293 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
294 z ^ (z >> 31)
295 };
296 for _ in 0..3_000 {
297 let nv = 2 + (next() as usize % 8);
298 let mut perm: Vec<usize> = (0..nv).collect();
299 for i in (1..nv).rev() {
300 let j = next() as usize % (i + 1);
301 perm.swap(i, j);
302 }
303 let image: Vec<crate::cdcl::Lit> =
304 (0..nv).map(|v| crate::cdcl::Lit::new(perm[v] as u32, next() & 1 == 0)).collect();
305 let rho = Reduction { image, target_num_vars: nv };
306 assert!(rho.is_bijective(), "a renaming is an isomorphism");
307 let inv = rho.inverse().expect("isomorphisms invert");
308 assert!(rho.compose(&inv).is_identity(), "ρ∘ρ⁻¹ = id");
309 assert!(inv.compose(&rho).is_identity(), "ρ⁻¹∘ρ = id");
310 }
311 }
312
313 #[test]
314 fn pi_one_of_a_problem_is_its_symmetry_group() {
315 let n = 4;
320 let holes = n - 1;
321 let (php, _) = crate::families::php(n);
322 let nv = php.num_vars;
323 let var = |p: usize, h: usize| p * holes + h;
324 let swap_pigeons = |a: usize, b: usize| -> Reduction {
326 let image: Vec<crate::cdcl::Lit> = (0..nv)
327 .map(|v| {
328 let (p, h) = (v / holes, v % holes);
329 let np = if p == a { b } else if p == b { a } else { p };
330 crate::cdcl::Lit::pos(var(np, h) as u32)
331 })
332 .collect();
333 Reduction { image, target_num_vars: nv }
334 };
335 let s01 = swap_pigeons(0, 1);
336 let s12 = swap_pigeons(1, 2);
337 assert!(s01.is_loop_at(&php.clauses), "pigeon-swap is a loop = an automorphism = in π₁");
339 assert!(s12.is_loop_at(&php.clauses));
340 assert!(s01.compose(&s12).is_loop_at(&php.clauses), "composition of loops is a loop");
342 assert!(s01.inverse().unwrap().is_loop_at(&php.clauses), "inverse of a loop is a loop");
343 assert!(s01.compose(&s01).is_identity(), "a transposition squares to the identity loop");
344 let bad = {
346 let mut image: Vec<crate::cdcl::Lit> = (0..nv).map(|v| crate::cdcl::Lit::pos(v as u32)).collect();
347 image.swap(var(0, 0), var(1, 1)); Reduction { image, target_num_vars: nv }
349 };
350 assert!(!bad.is_loop_at(&php.clauses), "a non-symmetry renaming is not in π₁");
351 }
352
353 #[test]
354 fn the_2cells_are_the_group_relations_so_the_infinity_groupoid_is_BG() {
355 let n = 5;
365 let holes = n - 1;
366 let (php, _) = crate::families::php(n);
367 let nv = php.num_vars;
368 let var = |p: usize, h: usize| p * holes + h;
369 let s = |i: usize| -> Reduction {
370 let image: Vec<crate::cdcl::Lit> = (0..nv)
371 .map(|v| {
372 let (p, h) = (v / holes, v % holes);
373 let np = if p == i { i + 1 } else if p == i + 1 { i } else { p };
374 crate::cdcl::Lit::pos(var(np, h) as u32)
375 })
376 .collect();
377 Reduction { image, target_num_vars: nv }
378 };
379 for i in 0..n - 1 {
381 assert!(s(i).is_loop_at(&php.clauses), "generator s_{i} is a loop in π₁");
382 assert!(s(i).compose(&s(i)).is_identity(), "s_{i}² = id");
384 }
385 for i in 0..n - 2 {
387 let b = s(i).compose(&s(i + 1));
388 assert!(b.compose(&b).compose(&b).is_identity(), "braid relation (s_i·s_next)^3 = id at i={i}");
389 }
390 for i in 0..n - 1 {
392 for j in 0..n - 1 {
393 if (i as i32 - j as i32).abs() >= 2 {
394 assert_eq!(
395 s(i).compose(&s(j)).image,
396 s(j).compose(&s(i)).image,
397 "distant swaps commute at i={i} j={j}"
398 );
399 }
400 }
401 }
402 }
404
405 #[test]
406 fn transfer_is_functorial() {
407 let n = 5;
410 let (php, _) = crate::families::php(n);
411 let nv = php.num_vars;
412 let (_, source) = crate::lyapunov::solve_by_measure_synthesis(nv, &php.clauses).unwrap();
413
414 let id = Reduction { image: (0..nv).map(|v| crate::cdcl::Lit::pos(v as u32)).collect(), target_num_vars: nv };
416 let via_id: Vec<_> = source.steps.iter().map(|s| id.apply_step(s)).collect();
417 assert_eq!(via_id, source.steps, "transfer along identity is the identity functor");
418
419 let p_rho: Vec<usize> = (0..nv).rev().collect();
421 let p_tau: Vec<usize> = (0..nv).map(|v| (v + 1) % nv).collect();
422 let rho = Reduction { image: (0..nv).map(|v| crate::cdcl::Lit::pos(p_rho[v] as u32)).collect(), target_num_vars: nv };
423 let tau = Reduction { image: (0..nv).map(|v| crate::cdcl::Lit::pos(p_tau[v] as u32)).collect(), target_num_vars: nv };
424 let comp = Reduction {
426 image: (0..nv).map(|v| rho.apply_lit(tau.image[v])).collect(),
427 target_num_vars: nv,
428 };
429 for s in &source.steps {
431 let two_step = rho.apply_step(&tau.apply_step(s));
432 let one_step = comp.apply_step(s);
433 assert_eq!(two_step, one_step, "transfer along ρ∘τ = transfer(ρ) ∘ transfer(τ)");
434 }
435 }
436
437 #[test]
438 fn aut_F_acts_on_the_collapses_the_2group_crossed_module() {
439 let n = 6;
450 let holes = n - 1;
451 let (php, _) = crate::families::php(n);
452 let nv = php.num_vars;
453 let var = |p: usize, h: usize| p * holes + h;
454 let (_, source) = crate::lyapunov::solve_by_measure_synthesis(nv, &php.clauses).unwrap();
455
456 let sigma = {
458 let image: Vec<crate::cdcl::Lit> = (0..nv)
459 .map(|v| {
460 let (p, h) = (v / holes, v % holes);
461 let np = if p == 0 { 1 } else if p == 1 { 0 } else { p };
462 crate::cdcl::Lit::pos(var(np, h) as u32)
463 })
464 .collect();
465 Reduction { image, target_num_vars: nv }
466 };
467 assert!(sigma.is_loop_at(&php.clauses), "σ is a symmetry of F (a loop in π₁)");
468
469 let acted = transfer(&sigma, &source.steps, &source.ranks, &php.clauses)
471 .expect("a symmetry of F carries a collapse of F to a collapse of F");
472 assert!(crate::pr::check_pr_refutation_fast(nv, &php.clauses, &acted.steps), "the acted collapse refutes F");
473 assert_eq!(acted.ranks, source.ranks, "the action is by isomorphisms (ranks preserved)");
475 let id = Reduction { image: (0..nv).map(|v| crate::cdcl::Lit::pos(v as u32)).collect(), target_num_vars: nv };
477 let by_id: Vec<_> = source.steps.iter().map(|s| id.apply_step(s)).collect();
478 assert_eq!(by_id, source.steps, "the identity symmetry acts trivially (group-action unit law)");
479 assert_ne!(acted.steps, source.steps, "a non-identity symmetry genuinely permutes the collapses");
481 }
482
483 #[test]
484 fn non_uniqueness_gives_two_objects_in_the_category() {
485 let n = 7;
490 let (php, _) = crate::families::php(n);
491 let (_, ranked) =
492 crate::lyapunov::solve_by_measure_synthesis(php.num_vars, &php.clauses).unwrap();
493 let (cp_traj, _) = crate::lyapunov::cutting_planes_lyapunov(n);
494 assert_ne!(ranked.ranks.len(), cp_traj.len(), "symmetry and cutting-planes are distinct objects");
496 assert!(refines(&ranked.ranks, &ranked.ranks) && refines(&cp_traj, &cp_traj));
498 }
499}