Skip to main content

logicaffeine_proof/
sweep.rs

1//! Combinational equivalence: two circuits' outputs proven equal, or told apart by an input assignment that replays,
2//! every equality carried by a lemma whose LRAT proof a small checker re-validates.
3//!
4//! Two circuits are joined in one and-inverter graph by [`miter`]: inputs of the same name shared, outputs paired by
5//! name. Structural hashing already merges every gate both compute the same way. [`decide`] then sweeps the rest
6//! (Kuehlmann, *Dynamic transition relation simplification for bounded property checking*, ICCAD 2004; Mishchenko
7//! et al., *FRAIGs*, 2005): random simulation groups nodes by their value signatures, each candidate pair is put to
8//! the CDCL solver in topological order, a refuted difference becomes a lemma that later queries may use, and a
9//! counterexample is added to the simulation patterns to split the classes it disproves. Each output pair is then
10//! equal by the lemmas, or decided by one more query.
11//!
12//! A lemma's formula is [`query_with_cut`]: the Tseitin clauses of the two literals' cone, cut at the nodes the
13//! lemma names (left free, their own fan-in not encoded), the two clauses of each earlier lemma lying wholly inside
14//! that cone, and the difference itself. Leaving a cut free only removes constraints, so a refutation of the cut
15//! formula refutes the whole cone's; a candidate is first put to the solver a few levels deep, where an equality
16//! implied by its fan-ins' lemmas refutes at once, and only widened to the whole cone when the narrow formula has a
17//! model. The formula is a pure function of the graph, the cut and the lemmas before it, so [`check`] rebuilds it,
18//! replays the lemma's LRAT proof with [`crate::lrat::check`], and accepts a claimed equality only when the lemmas
19//! join its two literals, with matching phase, in a union-find. A solver whose UNSAT does not replay yields
20//! [`Verdict::Unknown`], never an equality; a model that does not separate the two literals under [`Aig::eval`]
21//! likewise.
22//!
23//! [`distinguish`] tells a family of circuits apart: the members that compute one function, each equality certified,
24//! and a few input assignments on which every two members that differ give different outputs, with each class's
25//! outputs there: what names the member a black box computes from its measured outputs.
26
27use std::collections::{HashMap, HashSet};
28
29use crate::aig::{Aig, AigLit, Leaf, Node};
30use crate::cdcl::{BudgetedResult, Lit, Solver};
31use crate::lrat::{self, Lrat};
32
33/// A proven equivalence `a ≡ b`: its proof refutes [`query_with_cut`]`(a, b, cut, earlier lemmas)`.
34#[derive(Clone, Debug)]
35pub struct Lemma {
36    pub a: AigLit,
37    pub b: AigLit,
38    /// The nodes the formula leaves free, in increasing order; empty for the whole cone.
39    pub cut: Vec<u32>,
40    pub proof: Lrat,
41}
42
43/// Lemmas in proof order; a lemma's query includes the lemmas before it.
44#[derive(Clone, Debug, Default)]
45pub struct Certificate {
46    pub lemmas: Vec<Lemma>,
47}
48
49/// What [`decide`] found for one pair of literals.
50#[derive(Clone, Debug, PartialEq, Eq)]
51pub enum Verdict {
52    Equal,
53    /// An assignment, of the inputs by index and the latches by index, under which the two literals differ.
54    Differ { inputs: Vec<bool>, latches: Vec<bool> },
55    /// No verdict: the conflict budget ran out, or the solver's answer did not survive its own check.
56    Unknown(String),
57}
58
59/// How [`decide`] searches.
60#[derive(Clone, Debug)]
61pub struct Options {
62    /// Sweep internal nodes before deciding the pairs.
63    pub sweep: bool,
64    /// Random simulation words (64 patterns each) the first signatures are drawn from.
65    pub sim_words: usize,
66    pub seed: u64,
67    /// Conflicts a sweeping query may spend; 0 is unlimited.
68    pub sweep_budget: u64,
69    /// Conflicts a pair's own query may spend; 0 is unlimited.
70    pub final_budget: u64,
71    /// What to report the search's cost to as it goes, if anything.
72    pub progress: Option<Progress>,
73}
74
75/// A report of a long search: `report` is handed the cost so far once every `every` candidates put to the solver.
76#[derive(Clone, Copy, Debug)]
77pub struct Progress {
78    pub every: usize,
79    pub report: fn(&Stats),
80}
81
82impl Default for Options {
83    fn default() -> Options {
84        Options { sweep: true, sim_words: 8, seed: 0x5eed_c0de_f00d_a16e, sweep_budget: 20_000, final_budget: 0, progress: None }
85    }
86}
87
88/// What a decision cost.
89#[derive(Clone, Debug, Default, PartialEq, Eq)]
90pub struct Stats {
91    pub sat_calls: usize,
92    pub merges: usize,
93    pub counterexamples: usize,
94    pub unknown: usize,
95    /// The queries that took the most conflicts, most first, at most [`HARDEST`].
96    pub hardest: Vec<Hard>,
97    /// Every query's cost by where it was cut: at each of [`CUT_DEPTHS`] in order, where the two cones meet, then over
98    /// the whole cone.
99    pub by_cut: [CutCost; CUT_KINDS],
100    /// The nodes of the cone the sweep runs over, and how many it has put to classes.
101    pub cone: usize,
102    pub swept: usize,
103}
104
105/// The queries put at one kind of cut, their verdicts and what they cost, summed.
106#[derive(Clone, Debug, Default, PartialEq, Eq)]
107pub struct CutCost {
108    pub queries: usize,
109    pub refuted: usize,
110    pub models: usize,
111    pub budget: usize,
112    pub vars: u64,
113    pub clauses: u64,
114    pub conflicts: u64,
115}
116
117/// One query's cost: the pair, the cut it was put with (empty for the whole cone), its formula's size and the
118/// conflicts its solve took.
119#[derive(Clone, Debug, PartialEq, Eq)]
120pub struct Hard {
121    pub a: AigLit,
122    pub b: AigLit,
123    pub cut: usize,
124    pub vars: usize,
125    pub conflicts: u64,
126    pub verdict: &'static str,
127}
128
129/// How many of the hardest queries [`Stats`] keeps.
130pub const HARDEST: usize = 16;
131
132impl Stats {
133    fn note(&mut self, h: Hard) {
134        let at = self.hardest.partition_point(|x| x.conflicts >= h.conflicts);
135        if at < HARDEST {
136            self.hardest.insert(at, h);
137            self.hardest.truncate(HARDEST);
138        }
139    }
140}
141
142/// A lemma's formula: variable `k` is the `k`-th node of `cone` (the two literals' cone, in increasing order), then
143/// of `extra` (nodes outside the cone an earlier lemma relates to a node inside it, free, in increasing order).
144#[derive(Clone, Debug)]
145pub struct Query {
146    pub cone: Vec<u32>,
147    pub extra: Vec<u32>,
148    pub num_vars: usize,
149    pub clauses: Vec<Vec<Lit>>,
150}
151
152/// The cone of `roots`, a node of `cut` kept as a leaf, in increasing order.
153fn cone_within(aig: &Aig, roots: &[AigLit], cut: &[u32]) -> Vec<u32> {
154    let mut seen: HashSet<u32> = HashSet::new();
155    let mut stack: Vec<u32> = roots.iter().map(|l| l.node()).collect();
156    let mut out = Vec::new();
157    while let Some(n) = stack.pop() {
158        if !seen.insert(n) {
159            continue;
160        }
161        out.push(n);
162        if cut.binary_search(&n).is_ok() {
163            continue;
164        }
165        if let Node::And(x, y) = aig.node(n) {
166            stack.push(x.node());
167            stack.push(y.node());
168        }
169    }
170    out.sort_unstable();
171    out
172}
173
174/// Lemmas in proof order, each node of one indexed by the lemmas that relate it: the lemmas touching a cone found in
175/// time proportional to the cone, not to every lemma, in lemma order.
176#[derive(Clone, Debug, Default)]
177pub struct Earlier {
178    pairs: Vec<(AigLit, AigLit)>,
179    by_node: HashMap<u32, Vec<u32>>,
180}
181
182impl Earlier {
183    pub fn new() -> Earlier {
184        Earlier::default()
185    }
186
187    /// Add the lemma `a ≡ b` after every one before it.
188    pub fn push(&mut self, a: AigLit, b: AigLit) {
189        let k = self.pairs.len() as u32;
190        self.pairs.push((a, b));
191        self.by_node.entry(a.node()).or_default().push(k);
192        if b.node() != a.node() {
193            self.by_node.entry(b.node()).or_default().push(k);
194        }
195    }
196
197    pub fn pairs(&self) -> &[(AigLit, AigLit)] {
198        &self.pairs
199    }
200
201    /// The lemmas with a literal on a node of `cone`, in lemma order.
202    fn touching(&self, cone: &[u32]) -> Vec<(AigLit, AigLit)> {
203        let mut at: Vec<u32> = cone.iter().filter_map(|n| self.by_node.get(n)).flatten().copied().collect();
204        at.sort_unstable();
205        at.dedup();
206        at.into_iter().map(|k| self.pairs[k as usize]).collect()
207    }
208}
209
210impl From<&[(AigLit, AigLit)]> for Earlier {
211    fn from(pairs: &[(AigLit, AigLit)]) -> Earlier {
212        let mut e = Earlier::new();
213        for &(a, b) in pairs {
214            e.push(a, b);
215        }
216        e
217    }
218}
219
220/// [`query_with_cut`] over the whole cone.
221pub fn query(aig: &Aig, a: AigLit, b: AigLit, earlier: &[(AigLit, AigLit)]) -> Query {
222    query_with_cut(aig, a, b, &[], earlier)
223}
224
225/// The Tseitin clauses of the cone of `a` and `b` in node order, a node of `cut` (sorted) left free, then the two
226/// clauses of each earlier lemma with a literal in that cone, in lemma order (its other node, when outside the cone,
227/// a free variable of its own: lemmas joined through a representative the cone does not reach still join), then
228/// `a ≠ b` as `(a ∨ b) ∧ (¬a ∨ ¬b)`.
229pub fn query_with_cut(aig: &Aig, a: AigLit, b: AigLit, cut: &[u32], earlier: &[(AigLit, AigLit)]) -> Query {
230    query_indexed(aig, a, b, cut, &Earlier::from(earlier))
231}
232
233/// [`query_with_cut`] with the earlier lemmas indexed.
234fn query_indexed(aig: &Aig, a: AigLit, b: AigLit, cut: &[u32], earlier: &Earlier) -> Query {
235    let cone = cone_within(aig, &[a, b], cut);
236    let inside = |n: u32| cone.binary_search(&n).is_ok();
237    let touching = earlier.touching(&cone);
238    let mut extra: Vec<u32> =
239        touching.iter().flat_map(|(p, q)| [p.node(), q.node()]).filter(|&n| !inside(n)).collect();
240    extra.sort_unstable();
241    extra.dedup();
242    let var = |n: u32| match cone.binary_search(&n) {
243        Ok(k) => k,
244        Err(_) => cone.len() + extra.binary_search(&n).expect("a node of the cone or of a lemma touching it"),
245    };
246    let lit = |l: AigLit| Lit::new(var(l.node()) as u32, !l.is_complemented());
247    let mut clauses = Vec::with_capacity(3 * cone.len() + 2 * touching.len() + 2);
248    for (k, &n) in cone.iter().enumerate() {
249        if cut.binary_search(&n).is_ok() {
250            continue;
251        }
252        match aig.node(n) {
253            Node::False => clauses.push(vec![Lit::neg(k as u32)]),
254            Node::And(x, y) => {
255                let o = Lit::pos(k as u32);
256                clauses.push(vec![o.negated(), lit(x)]);
257                clauses.push(vec![o.negated(), lit(y)]);
258                clauses.push(vec![o, lit(x).negated(), lit(y).negated()]);
259            }
260            Node::Input(_) | Node::Latch(_) => {}
261        }
262    }
263    for (p, q) in touching {
264        clauses.push(vec![lit(p).negated(), lit(q)]);
265        clauses.push(vec![lit(p), lit(q).negated()]);
266    }
267    clauses.push(vec![lit(a), lit(b)]);
268    clauses.push(vec![lit(a).negated(), lit(b).negated()]);
269    Query { num_vars: cone.len() + extra.len(), cone, extra, clauses }
270}
271
272/// The And nodes first reached `depth` levels back from the roots, in increasing order: the cut that keeps a query
273/// that many levels deep. Empty when the cone is shallower.
274fn frontier(aig: &Aig, roots: &[AigLit], depth: usize) -> Vec<u32> {
275    let mut level: HashMap<u32, usize> = HashMap::new();
276    let mut current: Vec<u32> = roots.iter().map(|l| l.node()).collect();
277    for &n in &current {
278        level.insert(n, 0);
279    }
280    for d in 1..=depth {
281        let mut next = Vec::new();
282        for &n in &current {
283            if let Node::And(x, y) = aig.node(n) {
284                for c in [x.node(), y.node()] {
285                    if !level.contains_key(&c) {
286                        level.insert(c, d);
287                        next.push(c);
288                    }
289                }
290            }
291        }
292        current = next;
293    }
294    let mut cut: Vec<u32> = current.into_iter().filter(|&n| matches!(aig.node(n), Node::And(..))).collect();
295    cut.sort_unstable();
296    cut
297}
298
299pub(crate) enum Outcome {
300    Equal(Lrat),
301    Differ(Vec<bool>, Vec<bool>),
302    Unknown(String),
303}
304
305/// [`query`] decided: refuted with a checked proof, a model that separates the pair under [`Aig::eval`], or neither.
306pub(crate) fn prove(aig: &Aig, a: AigLit, b: AigLit, earlier: &[(AigLit, AigLit)], budget: u64) -> Outcome {
307    prove_noted(aig, a, b, &[], &Earlier::from(earlier), budget, None)
308}
309
310/// `a ≡ b` decided over the cone cut at `cut`: refuted with a checked proof, a model that separates the pair under
311/// [`Aig::eval`], or neither; its cost noted in `noted`'s stats, under the kind of cut given there, when given.
312fn prove_noted(
313    aig: &Aig,
314    a: AigLit,
315    b: AigLit,
316    cut: &[u32],
317    earlier: &Earlier,
318    budget: u64,
319    noted: Option<(&mut Stats, usize)>,
320) -> Outcome {
321    let q = query_indexed(aig, a, b, cut, earlier);
322    let mut solver = Solver::new(q.num_vars);
323    for c in &q.clauses {
324        solver.add_clause(c.clone());
325    }
326    let result = solver.solve_budgeted(budget);
327    if let Some((stats, kind)) = noted {
328        let cost = &mut stats.by_cut[kind];
329        cost.queries += 1;
330        cost.vars += q.num_vars as u64;
331        cost.clauses += q.clauses.len() as u64;
332        cost.conflicts += solver.conflicts();
333        let verdict = match &result {
334            BudgetedResult::Sat(_) => {
335                cost.models += 1;
336                "model"
337            }
338            BudgetedResult::Unsat => {
339                cost.refuted += 1;
340                "refuted"
341            }
342            BudgetedResult::Budget => {
343                cost.budget += 1;
344                "budget"
345            }
346        };
347        stats.note(Hard { a, b, cut: cut.len(), vars: q.num_vars, conflicts: solver.conflicts(), verdict });
348    }
349    match result {
350        BudgetedResult::Sat(model) => {
351            let mut inputs = vec![false; aig.inputs().len()];
352            let mut latches = vec![false; aig.latches().len()];
353            for (k, &n) in q.cone.iter().enumerate() {
354                match aig.node(n) {
355                    Node::Input(i) => inputs[i] = model[k],
356                    Node::Latch(j) => latches[j] = model[k],
357                    _ => {}
358                }
359            }
360            let v = aig.eval(&inputs, &latches);
361            if Aig::value(&v, a) != Aig::value(&v, b) {
362                Outcome::Differ(inputs, latches)
363            } else {
364                Outcome::Unknown("the solver's model does not separate the pair".into())
365            }
366        }
367        BudgetedResult::Unsat => {
368            let learned: Vec<Vec<Lit>> = solver.learned().iter().map(|c| c.lits.clone()).collect();
369            match lrat::refutation(q.num_vars, &q.clauses, &learned) {
370                Some(proof) if lrat::check(q.num_vars, &q.clauses, &proof) => Outcome::Equal(proof),
371                _ => Outcome::Unknown("the solver's refutation does not replay".into()),
372            }
373        }
374        BudgetedResult::Budget => Outcome::Unknown(format!("no verdict within {budget} conflicts")),
375    }
376}
377
378/// The depths a candidate is first tried at before its whole cone.
379pub const CUT_DEPTHS: [usize; 2] = [6, 16];
380
381/// The kinds of cut a query is put at: each of [`CUT_DEPTHS`], where the cones meet, and none.
382pub const CUT_KINDS: usize = CUT_DEPTHS.len() + 2;
383
384/// Where the cones of `a` and `b` meet, as each first reaches it: an And node whose class (by `repr`, the classes
385/// proven so far) both cones hold a member of, whether the node itself is in both or a proven equal of it is. A
386/// rewrite of the same logic, a pipeline of it or a mapping of it differs only above where its two sides meet, so a
387/// query cut there sees only what differs. Empty when the cones never meet.
388fn meeting(aig: &Aig, a: AigLit, b: AigLit, repr: &[AigLit]) -> Vec<u32> {
389    let class = |n: u32| repr[n as usize].node();
390    let classes = |r: AigLit| -> HashSet<u32> { cone_within(aig, &[r], &[]).into_iter().map(class).collect() };
391    let (left, right) = (classes(a), classes(b));
392    let met = |n: u32| {
393        let c = class(n);
394        left.contains(&c) && right.contains(&c)
395    };
396    let mut cut = Vec::new();
397    let mut seen: HashSet<u32> = HashSet::new();
398    let mut stack = vec![a.node(), b.node()];
399    while let Some(n) = stack.pop() {
400        if !seen.insert(n) {
401            continue;
402        }
403        let Node::And(x, y) = aig.node(n) else { continue };
404        if n != a.node() && n != b.node() && met(n) {
405            cut.push(n);
406            continue;
407        }
408        stack.push(x.node());
409        stack.push(y.node());
410    }
411    cut.sort_unstable();
412    cut
413}
414
415/// `a ≡ b` decided narrow first: at each of [`CUT_DEPTHS`] (a model there may be the free cut's alone, so it only
416/// widens the search), then where the two cones [`meeting`] meet, then over the whole cone. An equality comes with the
417/// cut its proof refutes. Each cut is found only once every narrower one has failed: where the cones meet costs both
418/// whole cones to find.
419fn prove_local(
420    aig: &Aig,
421    a: AigLit,
422    b: AigLit,
423    earlier: &Earlier,
424    repr: &[AigLit],
425    budget: u64,
426    stats: &mut Stats,
427) -> (Outcome, Vec<u32>) {
428    let mut tried: Vec<Vec<u32>> = Vec::new();
429    for (kind, &depth) in CUT_DEPTHS.iter().enumerate() {
430        let cut = frontier(aig, &[a, b], depth);
431        if cut.is_empty() {
432            break;
433        }
434        match prove_noted(aig, a, b, &cut, earlier, budget, Some((&mut *stats, kind))) {
435            Outcome::Equal(proof) => return (Outcome::Equal(proof), cut),
436            Outcome::Differ(inputs, latches) => return (Outcome::Differ(inputs, latches), Vec::new()),
437            Outcome::Unknown(_) => tried.push(cut),
438        }
439    }
440    let met = meeting(aig, a, b, repr);
441    if !met.is_empty() && !tried.contains(&met) {
442        match prove_noted(aig, a, b, &met, earlier, budget, Some((&mut *stats, CUT_DEPTHS.len()))) {
443            Outcome::Equal(proof) => return (Outcome::Equal(proof), met),
444            Outcome::Differ(inputs, latches) => return (Outcome::Differ(inputs, latches), Vec::new()),
445            Outcome::Unknown(_) => {}
446        }
447    }
448    (prove_noted(aig, a, b, &[], earlier, budget, Some((stats, CUT_KINDS - 1))), Vec::new())
449}
450
451/// Hand the cost so far to `opts`'s progress report, when one is due.
452fn tick(stats: &Stats, opts: &Options) {
453    if let Some(p) = opts.progress {
454        if p.every > 0 && stats.sat_calls % p.every == 0 {
455            (p.report)(stats);
456        }
457    }
458}
459
460/// An input and latch assignment under which `lit` is true, or `None` when it never is (or the budget ran out first:
461/// `Err`). Uncertified: a search for a witness, whose finding is replayed by evaluation before it is returned.
462pub(crate) fn witness(aig: &Aig, lit: AigLit, budget: u64) -> Result<Option<(Vec<bool>, Vec<bool>)>, String> {
463    let q = query(aig, lit, AigLit::FALSE, &[]);
464    let mut solver = Solver::new(q.num_vars);
465    for c in &q.clauses {
466        solver.add_clause(c.clone());
467    }
468    match solver.solve_budgeted(budget) {
469        BudgetedResult::Sat(model) => {
470            let mut inputs = vec![false; aig.inputs().len()];
471            let mut latches = vec![false; aig.latches().len()];
472            for (k, &n) in q.cone.iter().enumerate() {
473                match aig.node(n) {
474                    Node::Input(i) => inputs[i] = model[k],
475                    Node::Latch(j) => latches[j] = model[k],
476                    _ => {}
477                }
478            }
479            let v = aig.eval(&inputs, &latches);
480            if Aig::value(&v, lit) {
481                Ok(Some((inputs, latches)))
482            } else {
483                Err("the solver's model does not make the literal true".into())
484            }
485        }
486        BudgetedResult::Unsat => Ok(None),
487        BudgetedResult::Budget => Err(format!("no verdict within {budget} conflicts")),
488    }
489}
490
491/// [`decide`]'s verdict for each pair, the certificate behind its equalities, and its cost.
492#[derive(Clone, Debug)]
493pub struct Decided {
494    pub verdicts: Vec<Verdict>,
495    pub certificate: Certificate,
496    pub stats: Stats,
497}
498
499/// Decide `a ≡ b` for every pair of literals of `aig`, latches read as free values.
500pub fn decide(aig: &Aig, pairs: &[(AigLit, AigLit)], opts: &Options) -> Decided {
501    let mut s = Sweeper {
502        aig,
503        repr: (0..aig.num_nodes() as u32).map(|n| AigLit::new(n, false)).collect(),
504        lemmas: Vec::new(),
505        earlier: Earlier::new(),
506        stats: Stats::default(),
507    };
508    if opts.sweep {
509        s.sweep(pairs, opts);
510    }
511    let mut verdicts = Vec::with_capacity(pairs.len());
512    for &(a, b) in pairs {
513        if s.resolve(a) == s.resolve(b) {
514            verdicts.push(Verdict::Equal);
515            continue;
516        }
517        s.stats.sat_calls += 1;
518        let (outcome, cut) = prove_local(aig, a, b, &s.earlier, &s.repr, opts.final_budget, &mut s.stats);
519        tick(&s.stats, opts);
520        verdicts.push(match outcome {
521            Outcome::Equal(proof) => {
522                s.learn(a, b, cut, proof);
523                Verdict::Equal
524            }
525            Outcome::Differ(inputs, latches) => Verdict::Differ { inputs, latches },
526            Outcome::Unknown(why) => {
527                s.stats.unknown += 1;
528                Verdict::Unknown(why)
529            }
530        });
531    }
532    Decided { verdicts, certificate: Certificate { lemmas: s.lemmas }, stats: s.stats }
533}
534
535/// How many times a node is put to the solver against a class before it stays apart.
536const ATTEMPTS: usize = 8;
537
538struct Sweeper<'a> {
539    aig: &'a Aig,
540    /// Each node's representative literal: itself, or the class member it was proven equal to.
541    repr: Vec<AigLit>,
542    lemmas: Vec<Lemma>,
543    earlier: Earlier,
544    stats: Stats,
545}
546
547/// Simulation patterns: per input and per latch, `words` words, the last of them filling with counterexamples.
548struct Patterns {
549    inputs: Vec<u64>,
550    latches: Vec<u64>,
551    words: usize,
552    /// Patterns of the last word already taken by counterexamples (64 once it is full).
553    used: usize,
554}
555
556impl Patterns {
557    fn random(aig: &Aig, words: usize, seed: u64) -> Patterns {
558        let mut state = seed;
559        let mut next = || {
560            state = state.wrapping_add(0x9e37_79b9_7f4a_7c15);
561            let mut z = state;
562            z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
563            z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
564            z ^ (z >> 31)
565        };
566        let inputs = (0..aig.inputs().len() * words).map(|_| next()).collect();
567        let latches = (0..aig.latches().len() * words).map(|_| next()).collect();
568        Patterns { inputs, latches, words, used: 64 }
569    }
570
571    /// Add one assignment: a fresh word carrying it in every pattern, or the next free pattern of the last word.
572    fn add(&mut self, inputs: &[bool], latches: &[bool]) {
573        if self.used == 64 {
574            let grow = |old: &[u64], values: &[bool], words: usize| -> Vec<u64> {
575                let mut out = Vec::with_capacity(values.len() * (words + 1));
576                for (i, &v) in values.iter().enumerate() {
577                    out.extend_from_slice(&old[i * words..(i + 1) * words]);
578                    out.push(if v { !0 } else { 0 });
579                }
580                out
581            };
582            self.inputs = grow(&self.inputs, inputs, self.words);
583            self.latches = grow(&self.latches, latches, self.words);
584            self.words += 1;
585            self.used = 1;
586            return;
587        }
588        let (words, bit) = (self.words, self.used);
589        for (store, values) in [(&mut self.inputs, inputs), (&mut self.latches, latches)] {
590            for (i, &v) in values.iter().enumerate() {
591                let cell = &mut store[i * words + words - 1];
592                *cell = (*cell & !(1 << bit)) | ((v as u64) << bit);
593            }
594        }
595        self.used += 1;
596    }
597}
598
599/// A node's signature with its phase normalized: complemented so the first pattern reads 0.
600fn signature(sim: &[u64], words: usize, n: u32) -> (Vec<u64>, bool) {
601    let row = &sim[n as usize * words..(n as usize + 1) * words];
602    let phase = row[0] & 1 == 1;
603    (row.iter().map(|&w| if phase { !w } else { w }).collect(), phase)
604}
605
606impl Sweeper<'_> {
607    fn resolve(&self, l: AigLit) -> AigLit {
608        self.repr[l.node() as usize].with_phase(l.is_complemented())
609    }
610
611    fn learn(&mut self, a: AigLit, b: AigLit, cut: Vec<u32>, proof: Lrat) {
612        self.lemmas.push(Lemma { a, b, cut, proof });
613        self.earlier.push(a, b);
614    }
615
616    fn sweep(&mut self, pairs: &[(AigLit, AigLit)], opts: &Options) {
617        let aig = self.aig;
618        let roots: Vec<AigLit> = pairs.iter().flat_map(|&(a, b)| [a, b]).collect();
619        let mut cone = aig.cone(&roots);
620        if cone.first() != Some(&0) {
621            cone.insert(0, 0);
622        }
623        // A node both sides reach is one node to both: merging it proves nothing the pairs need, and an invariant of
624        // shared arithmetic (a carry that never rises) can cost a solver everything. It stays a representative.
625        let left: HashSet<u32> = aig.cone(&pairs.iter().map(|p| p.0).collect::<Vec<_>>()).into_iter().collect();
626        let right: HashSet<u32> = aig.cone(&pairs.iter().map(|p| p.1).collect::<Vec<_>>()).into_iter().collect();
627        let mut patterns = Patterns::random(aig, opts.sim_words.max(1), opts.seed);
628        let mut sim = aig.simulate(patterns.words, &patterns.inputs, &patterns.latches);
629        let mut classes: HashMap<Vec<u64>, u32> = HashMap::new();
630        let mut apart: Vec<u32> = Vec::new();
631        self.stats.cone = cone.len();
632        for &n in &cone {
633            self.stats.swept += 1;
634            let is_and = matches!(aig.node(n), Node::And(..)) && !(left.contains(&n) && right.contains(&n));
635            let mut attempts = 0;
636            loop {
637                let (key, phase) = signature(&sim, patterns.words, n);
638                let Some(&r) = classes.get(&key) else {
639                    classes.insert(key, n);
640                    break;
641                };
642                if !is_and || attempts == ATTEMPTS {
643                    break;
644                }
645                attempts += 1;
646                let (_, phase_r) = signature(&sim, patterns.words, r);
647                let target = AigLit::new(r, phase ^ phase_r);
648                self.stats.sat_calls += 1;
649                let (outcome, cut) =
650                    prove_local(aig, AigLit::new(n, false), target, &self.earlier, &self.repr, opts.sweep_budget, &mut self.stats);
651                tick(&self.stats, opts);
652                match outcome {
653                    Outcome::Equal(proof) => {
654                        self.learn(AigLit::new(n, false), target, cut, proof);
655                        self.repr[n as usize] = target;
656                        self.stats.merges += 1;
657                        break;
658                    }
659                    Outcome::Differ(inputs, latches) => {
660                        self.stats.counterexamples += 1;
661                        patterns.add(&inputs, &latches);
662                        sim = aig.simulate(patterns.words, &patterns.inputs, &patterns.latches);
663                        classes.clear();
664                        for &m in &apart {
665                            classes.entry(signature(&sim, patterns.words, m).0).or_insert(m);
666                        }
667                    }
668                    Outcome::Unknown(_) => {
669                        self.stats.unknown += 1;
670                        break;
671                    }
672                }
673            }
674            if self.repr[n as usize] == AigLit::new(n, false) {
675                apart.push(n);
676            }
677        }
678    }
679}
680
681/// Union-find over nodes with the parity of each node to its root: `value(n) = value(root) ⊕ parity`.
682struct Parity {
683    parent: Vec<u32>,
684    flip: Vec<bool>,
685}
686
687impl Parity {
688    fn new(n: usize) -> Parity {
689        Parity { parent: (0..n as u32).collect(), flip: vec![false; n] }
690    }
691
692    fn find(&mut self, n: u32) -> (u32, bool) {
693        let mut path = Vec::new();
694        let mut x = n;
695        while self.parent[x as usize] != x {
696            path.push(x);
697            x = self.parent[x as usize];
698        }
699        let root = x;
700        for &p in path.iter().rev() {
701            let up = self.parent[p as usize];
702            if up != root {
703                self.flip[p as usize] ^= self.flip[up as usize];
704            }
705            self.parent[p as usize] = root;
706        }
707        (root, self.flip[n as usize] && n != root)
708    }
709
710    fn lit(&mut self, l: AigLit) -> (u32, bool) {
711        let (r, p) = self.find(l.node());
712        (r, p ^ l.is_complemented())
713    }
714
715    fn union(&mut self, a: AigLit, b: AigLit) {
716        let (ra, pa) = self.lit(a);
717        let (rb, pb) = self.lit(b);
718        if ra != rb {
719            self.parent[ra as usize] = rb;
720            self.flip[ra as usize] = pa ^ pb;
721        }
722    }
723}
724
725/// Whether `certificate` proves every claim: each lemma's proof refutes its query in order, and each claimed pair is
726/// one literal, or two literals the lemmas join with matching phase.
727pub fn check(aig: &Aig, certificate: &Certificate, claims: &[(AigLit, AigLit)]) -> bool {
728    let n = aig.num_nodes();
729    let in_graph = |l: AigLit| (l.node() as usize) < n;
730    let mut earlier = Earlier::new();
731    for lemma in &certificate.lemmas {
732        if !in_graph(lemma.a) || !in_graph(lemma.b) {
733            return false;
734        }
735        if lemma.cut.windows(2).any(|w| w[0] >= w[1]) || lemma.cut.iter().any(|&c| c as usize >= n) {
736            return false;
737        }
738        let q = query_indexed(aig, lemma.a, lemma.b, &lemma.cut, &earlier);
739        if !lrat::check(q.num_vars, &q.clauses, &lemma.proof) {
740            return false;
741        }
742        earlier.push(lemma.a, lemma.b);
743    }
744    let mut uf = Parity::new(n);
745    for &(a, b) in earlier.pairs() {
746        uf.union(a, b);
747    }
748    claims.iter().all(|&(a, b)| in_graph(a) && in_graph(b) && (a == b || uf.lit(a) == uf.lit(b)))
749}
750
751/// Two circuits in one graph: inputs of the same name shared (unnamed and one-sided inputs kept apart), each named
752/// output of the left paired with the right's output of that name, and an output a pair's difference.
753#[derive(Clone, Debug)]
754pub struct Miter {
755    pub aig: Aig,
756    /// Each output's name, its literal in the left circuit and in the right.
757    pub pairs: Vec<(String, AigLit, AigLit)>,
758}
759
760/// Why two circuits cannot be paired.
761#[derive(Clone, Debug, PartialEq, Eq)]
762pub enum MiterError {
763    /// A circuit holds latches: unroll it first ([`crate::seq_equiv`]).
764    Sequential,
765    /// An output has no name to pair it by.
766    Unnamed,
767    /// An output name one circuit has and the other lacks.
768    Unmatched(String),
769    /// An output name a circuit uses twice.
770    Duplicate(String),
771}
772
773pub fn miter(left: &Aig, right: &Aig) -> Result<Miter, MiterError> {
774    if !left.latches().is_empty() || !right.latches().is_empty() {
775        return Err(MiterError::Sequential);
776    }
777    for g in [left, right] {
778        let mut seen = std::collections::HashSet::new();
779        for o in g.outputs() {
780            let name = o.name.as_ref().ok_or(MiterError::Unnamed)?;
781            if !seen.insert(name) {
782                return Err(MiterError::Duplicate(name.clone()));
783            }
784        }
785    }
786    let mut aig = Aig::new();
787    let mut by_name: HashMap<String, AigLit> = HashMap::new();
788    let left_inputs: Vec<AigLit> = left
789        .inputs()
790        .iter()
791        .map(|p| {
792            let l = aig.add_input(p.name.clone());
793            if let Some(n) = &p.name {
794                by_name.insert(n.clone(), l);
795            }
796            l
797        })
798        .collect();
799    let right_inputs: Vec<AigLit> = right
800        .inputs()
801        .iter()
802        .map(|p| match p.name.as_ref().and_then(|n| by_name.get(n)) {
803            Some(&l) => l,
804            None => aig.add_input(p.name.clone()),
805        })
806        .collect();
807    let leaf = |inputs: &[AigLit], l: Leaf| match l {
808        Leaf::Input(i) => inputs[i],
809        Leaf::Latch(_) => unreachable!("a miter's circuits are combinational"),
810    };
811    let lmap = aig.import(left, &mut |l| leaf(&left_inputs, l));
812    let rmap = aig.import(right, &mut |l| leaf(&right_inputs, l));
813    let at = |map: &[AigLit], l: AigLit| map[l.node() as usize].with_phase(l.is_complemented());
814    let mut pairs = Vec::with_capacity(left.outputs().len());
815    for o in left.outputs() {
816        let name = o.name.clone().expect("named above");
817        let k = right.output_index(&name).ok_or_else(|| MiterError::Unmatched(name.clone()))?;
818        pairs.push((name, at(&lmap, o.lit), at(&rmap, right.outputs()[k].lit)));
819    }
820    if let Some(o) = right.outputs().iter().find(|o| left.output_index(o.name.as_deref().unwrap()).is_none()) {
821        return Err(MiterError::Unmatched(o.name.clone().unwrap()));
822    }
823    for (name, a, b) in &pairs {
824        let d = aig.xor(*a, *b);
825        aig.add_output(d, Some(name.clone()));
826    }
827    // Only the outputs' cones decide anything: what neither side's outputs read is left out.
828    let operands: Vec<AigLit> = pairs.iter().flat_map(|(_, a, b)| [*a, *b]).collect();
829    let (aig, map) = aig.compact_keeping(&operands);
830    let moved = |l: AigLit| map[l.node() as usize].expect("an operand is kept").with_phase(l.is_complemented());
831    let pairs = pairs.into_iter().map(|(name, a, b)| (name, moved(a), moved(b))).collect();
832    Ok(Miter { aig, pairs })
833}
834
835/// A decided miter: a verdict per output name, the certificate behind the equalities, and the search's cost.
836#[derive(Clone, Debug)]
837pub struct Report {
838    pub miter: Miter,
839    pub verdicts: Vec<(String, Verdict)>,
840    pub certificate: Certificate,
841    pub stats: Stats,
842}
843
844impl Report {
845    /// Every output proven equal.
846    pub fn all_equal(&self) -> bool {
847        self.verdicts.iter().all(|(_, v)| *v == Verdict::Equal)
848    }
849
850    /// The literal pairs of the outputs proven equal.
851    pub fn equal_pairs(&self) -> Vec<(AigLit, AigLit)> {
852        self.miter
853            .pairs
854            .iter()
855            .zip(&self.verdicts)
856            .filter(|(_, (_, v))| *v == Verdict::Equal)
857            .map(|((_, a, b), _)| (*a, *b))
858            .collect()
859    }
860
861    /// The certificate proves every equality, and every difference's assignment separates its pair.
862    pub fn check(&self) -> bool {
863        let differences_replay = self.miter.pairs.iter().zip(&self.verdicts).all(|((_, a, b), (_, v))| match v {
864            Verdict::Differ { inputs, latches } => {
865                let values = self.miter.aig.eval(inputs, latches);
866                Aig::value(&values, *a) != Aig::value(&values, *b)
867            }
868            _ => true,
869        });
870        differences_replay && check(&self.miter.aig, &self.certificate, &self.equal_pairs())
871    }
872
873    /// An assignment of the miter's inputs, by name (unnamed inputs left out).
874    pub fn named_inputs(&self, inputs: &[bool]) -> Vec<(String, bool)> {
875        self.miter
876            .aig
877            .inputs()
878            .iter()
879            .zip(inputs)
880            .filter_map(|(p, &v)| p.name.clone().map(|n| (n, v)))
881            .collect()
882    }
883}
884
885/// An input assignment on which some pair of `left`'s and `right`'s outputs differ, found by one search over every
886/// difference at once and replayed before it is returned, with the miter it is an assignment of; `Ok(None)` when the
887/// search shows they never differ. A search, not a certificate: an equality is claimed only by [`equivalent`].
888pub fn separating_input(left: &Aig, right: &Aig, budget: u64) -> Result<Option<(Miter, Vec<bool>)>, String> {
889    let mut m = miter(left, right).map_err(|e| format!("{e:?}"))?;
890    let differences: Vec<AigLit> = m.aig.outputs().iter().map(|o| o.lit).collect();
891    let any = differences.into_iter().fold(AigLit::FALSE, |acc, d| m.aig.or(acc, d));
892    match witness(&m.aig, any, budget)? {
893        Some((inputs, _)) => Ok(Some((m, inputs))),
894        None => Ok(None),
895    }
896}
897
898/// Pair `left` and `right` by output name and decide every pair.
899pub fn equivalent(left: &Aig, right: &Aig, opts: &Options) -> Result<Report, MiterError> {
900    let miter = miter(left, right)?;
901    let pairs: Vec<(AigLit, AigLit)> = miter.pairs.iter().map(|(_, a, b)| (*a, *b)).collect();
902    let decided = decide(&miter.aig, &pairs, opts);
903    let verdicts = miter.pairs.iter().map(|p| p.0.clone()).zip(decided.verdicts).collect();
904    Ok(Report { miter, verdicts, certificate: decided.certificate, stats: decided.stats })
905}
906
907/// A family of circuits told apart by [`distinguish`].
908#[derive(Clone, Debug)]
909pub struct Distinction {
910    /// Members that compute one function, by index: each class in member order, the classes by their first member.
911    pub classes: Vec<Vec<usize>>,
912    /// For each member after the first of its class: that first member, the member, and the report proving them equal.
913    pub proofs: Vec<(usize, usize, Report)>,
914    /// The input names a vector assigns, in order of first appearance across the members.
915    pub inputs: Vec<String>,
916    /// The output names, in the first member's order.
917    pub outputs: Vec<String>,
918    /// The seeds, then every input assignment found to separate two members, over `inputs`.
919    pub vectors: Vec<Vec<bool>>,
920    /// Vectors, by index, on one of which every two classes differ.
921    pub cover: Vec<usize>,
922    /// Each class's outputs on each cover vector, over `outputs`.
923    pub table: Vec<Vec<Vec<bool>>>,
924}
925
926impl Distinction {
927    /// The classes partition the members, every proof checks and binds the two members it names, every two classes
928    /// differ on a cover vector, and the table is what each class's members compute there.
929    pub fn check(&self, members: &[Aig]) -> bool {
930        let mut seen = vec![false; members.len()];
931        for &m in self.classes.iter().flatten() {
932            if m >= members.len() || std::mem::replace(&mut seen[m], true) {
933                return false;
934            }
935        }
936        if seen.contains(&false) || self.classes.iter().any(|c| c.is_empty()) {
937            return false;
938        }
939        let proven: HashSet<(usize, usize)> = self.proofs.iter().map(|(r, m, _)| (*r, *m)).collect();
940        if self.classes.iter().any(|c| c[1..].iter().any(|&m| !proven.contains(&(c[0], m)))) {
941            return false;
942        }
943        for (rep, m, report) in &self.proofs {
944            let bound = miter(&members[*rep], &members[*m]).is_ok_and(|again| again.aig.to_binary() == report.miter.aig.to_binary());
945            if !bound || !report.all_equal() || !report.check() {
946                return false;
947            }
948        }
949        let Ok(seen) = Outputs::of(members, &self.inputs, &self.outputs) else { return false };
950        if self.table.len() != self.classes.len() || self.cover.iter().any(|&v| v >= self.vectors.len()) {
951            return false;
952        }
953        let on: Vec<Vec<Vec<bool>>> = self.classes.iter().map(|c| self.cover.iter().map(|&v| seen.eval(c[0], &self.vectors[v])).collect()).collect();
954        let agree = self.classes.iter().zip(&on).all(|(c, rows)| c.iter().all(|&m| self.cover.iter().zip(rows).all(|(&v, row)| seen.eval(m, &self.vectors[v]) == *row)));
955        let apart = (0..on.len()).all(|i| (i + 1..on.len()).all(|j| on[i] != on[j]));
956        agree && apart && on == self.table
957    }
958}
959
960/// The members' inputs and outputs by name: what a vector over `inputs` gives each member's outputs over `outputs`.
961struct Outputs<'a> {
962    members: &'a [Aig],
963    /// For each member, each of its inputs' place in `inputs`; `None` for one without a name, which no output reads.
964    places: Vec<Vec<Option<usize>>>,
965    /// For each member, its literal for each name of `outputs`.
966    lits: Vec<Vec<AigLit>>,
967}
968
969impl<'a> Outputs<'a> {
970    fn of(members: &'a [Aig], inputs: &[String], outputs: &[String]) -> Result<Outputs<'a>, String> {
971        let mut places = Vec::with_capacity(members.len());
972        let mut lits = Vec::with_capacity(members.len());
973        for (k, g) in members.iter().enumerate() {
974            if !g.latches().is_empty() {
975                return Err(format!("member {k} holds latches"));
976            }
977            let read: HashSet<u32> = g.cone(&g.outputs().iter().map(|o| o.lit).collect::<Vec<_>>()).into_iter().collect();
978            let place = g
979                .inputs()
980                .iter()
981                .map(|p| match &p.name {
982                    Some(n) => inputs.iter().position(|i| i == n).map(Some).ok_or_else(|| format!("member {k}: input {n} is not among the inputs")),
983                    None if read.contains(&p.lit.node()) => Err(format!("member {k}: an input without a name that an output reads, which no vector can assign")),
984                    None => Ok(None),
985                })
986                .collect::<Result<Vec<Option<usize>>, String>>()?;
987            if g.outputs().len() != outputs.len() {
988                return Err(format!("member {k} has {} outputs, the first {}", g.outputs().len(), outputs.len()));
989            }
990            let lit = outputs
991                .iter()
992                .map(|o| g.output_index(o).map(|i| g.outputs()[i].lit).ok_or_else(|| format!("member {k} has no output {o}")))
993                .collect::<Result<Vec<AigLit>, String>>()?;
994            places.push(place);
995            lits.push(lit);
996        }
997        Ok(Outputs { members, places, lits })
998    }
999
1000    fn eval(&self, m: usize, vector: &[bool]) -> Vec<bool> {
1001        let inputs: Vec<bool> = self.places[m].iter().map(|&p| p.is_some_and(|p| vector[p])).collect();
1002        let values = self.members[m].eval(&inputs, &[]);
1003        self.lits[m].iter().map(|&l| Aig::value(&values, l)).collect()
1004    }
1005
1006    /// Member `m`'s outputs on every vector, sixty-four vectors a word, output by output.
1007    fn signature(&self, m: usize, vectors: &[Vec<bool>]) -> Vec<u64> {
1008        let words = vectors.len().div_ceil(64);
1009        if words == 0 {
1010            return Vec::new();
1011        }
1012        let g = &self.members[m];
1013        let mut inputs = vec![0u64; g.inputs().len() * words];
1014        for (k, v) in vectors.iter().enumerate() {
1015            for (i, &p) in self.places[m].iter().enumerate() {
1016                if p.is_some_and(|p| v[p]) {
1017                    inputs[i * words + k / 64] |= 1 << (k % 64);
1018                }
1019            }
1020        }
1021        let sim = g.simulate(words, &inputs, &[]);
1022        let tail = if vectors.len() % 64 == 0 { u64::MAX } else { (1u64 << (vectors.len() % 64)) - 1 };
1023        self.lits[m]
1024            .iter()
1025            .flat_map(|&l| (0..words).map(move |w| (l, w)))
1026            .map(|(l, w)| Aig::word(&sim, words, l, w) & if w + 1 == words { tail } else { u64::MAX })
1027            .collect()
1028    }
1029}
1030
1031/// Tell a family of circuits apart: which members compute the same function, each such equality proven and
1032/// certified ([`equivalent`]), and input assignments on which every two members that differ give different outputs.
1033/// The members share input and output names; an input without a name is refused unless no output reads it. Simulation on the vectors so far groups the members by their outputs;
1034/// within a group each member is put against the group's first, and an input that separates them ([`separating_input`],
1035/// `budget` conflicts) becomes a vector that splits the group, while none found makes it proven equal. The cover is
1036/// chosen greedily from the `seeds` and the found vectors, each pick the vector that tells the most pairs of classes
1037/// apart that no earlier pick does; the table is each class's outputs there, what names the class of a black box
1038/// whose outputs on the cover are measured.
1039pub fn distinguish(members: &[Aig], seeds: &[Vec<(String, bool)>], budget: u64, opts: &Options) -> Result<Distinction, String> {
1040    let mut inputs: Vec<String> = Vec::new();
1041    for g in members {
1042        for n in g.inputs().iter().filter_map(|p| p.name.as_ref()) {
1043            if !inputs.contains(n) {
1044                inputs.push(n.clone());
1045            }
1046        }
1047    }
1048    let outputs: Vec<String> = match members.first() {
1049        Some(g) => g.outputs().iter().map(|o| o.name.clone().ok_or("an output without a name")).collect::<Result<_, _>>()?,
1050        None => Vec::new(),
1051    };
1052    let seen = Outputs::of(members, &inputs, &outputs)?;
1053    let over = |named: &[(String, bool)]| -> Vec<bool> { inputs.iter().map(|i| named.iter().any(|(n, v)| n == i && *v)).collect() };
1054    let mut vectors: Vec<Vec<bool>> = seeds.iter().map(|s| over(s)).collect();
1055    let mut proofs: Vec<(usize, usize, Report)> = Vec::new();
1056    let mut proven = vec![false; members.len()];
1057    let groups = loop {
1058        let signatures: Vec<Vec<u64>> = (0..members.len()).map(|m| seen.signature(m, &vectors)).collect();
1059        let mut groups: Vec<Vec<usize>> = Vec::new();
1060        for m in 0..members.len() {
1061            match groups.iter_mut().find(|g| signatures[g[0]] == signatures[m]) {
1062                Some(g) => g.push(m),
1063                None => groups.push(vec![m]),
1064            }
1065        }
1066        let mut split = None;
1067        'groups: for g in &groups {
1068            for &m in &g[1..] {
1069                if proven[m] {
1070                    continue;
1071                }
1072                match separating_input(&members[g[0]], &members[m], budget)? {
1073                    Some((found, assignment)) => {
1074                        let named: Vec<(String, bool)> =
1075                            found.aig.inputs().iter().zip(&assignment).filter_map(|(p, &v)| p.name.clone().map(|n| (n, v))).collect();
1076                        split = Some(over(&named));
1077                        break 'groups;
1078                    }
1079                    None => {
1080                        let report = equivalent(&members[g[0]], &members[m], opts).map_err(|e| format!("{e:?}"))?;
1081                        if !report.all_equal() || !report.check() {
1082                            return Err(format!("members {} and {m}: no input separates them, yet no certified proof", g[0]));
1083                        }
1084                        proven[m] = true;
1085                        proofs.push((g[0], m, report));
1086                    }
1087                }
1088            }
1089        }
1090        match split {
1091            Some(v) => vectors.push(v),
1092            None => break groups,
1093        }
1094    };
1095    let on: Vec<Vec<Vec<bool>>> = groups.iter().map(|g| vectors.iter().map(|v| seen.eval(g[0], v)).collect()).collect();
1096    let mut apart: Vec<(usize, usize)> = (0..groups.len()).flat_map(|i| (i + 1..groups.len()).map(move |j| (i, j))).collect();
1097    let mut cover: Vec<usize> = Vec::new();
1098    while !apart.is_empty() {
1099        let tells = |v: usize| apart.iter().filter(|&&(i, j)| on[i][v] != on[j][v]).count();
1100        let best = (0..vectors.len()).max_by_key(|&v| (tells(v), std::cmp::Reverse(v))).filter(|&v| tells(v) > 0);
1101        let Some(best) = best else { return Err("two groups the vectors do not tell apart".into()) };
1102        cover.push(best);
1103        apart.retain(|&(i, j)| on[i][best] == on[j][best]);
1104    }
1105    let table = on.iter().map(|rows| cover.iter().map(|&v| rows[v].clone()).collect()).collect();
1106    Ok(Distinction { classes: groups, proofs, inputs, outputs, vectors, cover, table })
1107}
1108
1109#[cfg(test)]
1110#[path = "../../logicaffeine_tests/unit/logicaffeine_proof/sweep/tests.rs"]
1111mod tests;