Skip to main content

logicaffeine_proof/
pseudo_boolean.rs

1//! Pseudo-Boolean constraints + cutting planes — a proof system STRICTLY STRONGER than resolution.
2//!
3//! Resolution (and therefore CDCL — ours and Z3's) needs EXPONENTIALLY long refutations of the
4//! pigeonhole principle and of many counting/cardinality problems. **Cutting planes** refutes them
5//! in POLYNOMIAL size by reasoning about linear 0/1 inequalities directly: it p-simulates resolution
6//! and has short proofs where resolution provably cannot.
7//!
8//! This is the certified core: a normalized PB constraint `Σ aᵢ·ℓᵢ ≥ d` (coeffs > 0, each variable
9//! once, `ℓ` a literal `x` or `¬x`) and the four sound cutting-plane rules — **addition** (with
10//! literal cancellation via `x + ¬x = 1`), **multiplication**, **division-with-rounding** (the
11//! Gomory–Chvátal cut, sound only because variables are integral), and **saturation** — each of
12//! which is *implied by* its premises, so deriving the trivially-false `0 ≥ 1` refutes the inputs.
13//! Every rule is pinned against a brute-force oracle. The headline is [`php_refutation`]: the
14//! classic Cook–Coullard–Turán **linear-size** refutation of PHP — pure algebra where resolution
15//! explodes.
16
17use std::collections::BTreeMap;
18
19/// A normalized pseudo-Boolean constraint `Σ coeff·lit ≥ degree`: every `coeff > 0`, every variable
20/// appears at most once, and the literal sign rides the entry — `(coeff, positive)` with
21/// `positive == false` meaning the literal `¬x`. `degree` may be ≤ 0 (then the constraint is
22/// trivially true) or exceed the coefficient sum (then it is a contradiction).
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct PbConstraint {
25    terms: BTreeMap<usize, (i64, bool)>,
26    degree: i64,
27}
28
29impl PbConstraint {
30    /// `Σ ℓ ≥ k` over the given literals (each coefficient 1). Literals must be over distinct
31    /// variables (the caller's responsibility — true for clause/cardinality inputs).
32    pub fn at_least(lits: &[(usize, bool)], k: i64) -> PbConstraint {
33        let terms = lits.iter().map(|&(v, s)| (v, (1, s))).collect();
34        PbConstraint { terms, degree: k }
35    }
36
37    /// A CNF clause `ℓ₁ ∨ … ∨ ℓₖ` as the PB constraint `Σ ℓ ≥ 1` — the bridge that lets the cutting
38    /// plane engine consume ordinary SAT clauses (and p-simulate resolution on them).
39    pub fn clause(lits: &[(usize, bool)]) -> PbConstraint {
40        Self::at_least(lits, 1)
41    }
42
43    /// `Σ ℓ ≤ k`, normalized to `≥` form by flipping every literal: `Σ ¬ℓ ≥ n − k`.
44    pub fn at_most(lits: &[(usize, bool)], k: i64) -> PbConstraint {
45        let negated: Vec<(usize, bool)> = lits.iter().map(|&(v, s)| (v, !s)).collect();
46        Self::at_least(&negated, lits.len() as i64 - k)
47    }
48
49    /// The degree (right-hand side).
50    pub fn degree(&self) -> i64 {
51        self.degree
52    }
53
54    /// Number of (non-cancelled) terms.
55    pub fn len(&self) -> usize {
56        self.terms.len()
57    }
58
59    pub fn is_empty(&self) -> bool {
60        self.terms.is_empty()
61    }
62
63    /// **Addition** — `LHS₁ + LHS₂ ≥ d₁ + d₂`, then re-normalized. A variable that is `x` in one and
64    /// `¬x` in the other partially cancels (`a·x + b·¬x = (a−b)·x + b`), so the shared minimum
65    /// `min(a,b)` leaves the literal and is moved to the right, dropping the degree by `min(a,b)`.
66    /// Sound: the sum of two valid `≥` constraints is valid, and `x + ¬x = 1` is exact for 0/1.
67    pub fn add(&self, other: &PbConstraint) -> PbConstraint {
68        let mut terms = self.terms.clone();
69        let mut degree = self.degree + other.degree;
70        for (&v, &(c2, s2)) in &other.terms {
71            match terms.get(&v).copied() {
72                None => {
73                    terms.insert(v, (c2, s2));
74                }
75                Some((c1, s1)) if s1 == s2 => {
76                    terms.insert(v, (c1 + c2, s1));
77                }
78                Some((c1, _s1_opposite)) => {
79                    let cancel = c1.min(c2);
80                    degree -= cancel;
81                    match c1.cmp(&c2) {
82                        std::cmp::Ordering::Equal => {
83                            terms.remove(&v);
84                        }
85                        std::cmp::Ordering::Greater => {
86                            terms.insert(v, (c1 - c2, self.terms[&v].1));
87                        }
88                        std::cmp::Ordering::Less => {
89                            terms.insert(v, (c2 - c1, s2));
90                        }
91                    }
92                }
93            }
94        }
95        PbConstraint { terms, degree }
96    }
97
98    /// **Multiplication** by a positive integer — `c·LHS ≥ c·degree`. Trivially sound (scaling a
99    /// valid `≥` constraint by `c > 0`).
100    pub fn multiply(&self, c: i64) -> PbConstraint {
101        assert!(c > 0, "multiply by a positive integer");
102        PbConstraint {
103            terms: self
104                .terms
105                .iter()
106                .map(|(&v, &(coeff, s))| (v, (coeff * c, s)))
107                .collect(),
108            degree: self.degree * c,
109        }
110    }
111
112    /// **Division with rounding-up** (the Gomory–Chvátal cut) — `Σ ⌈aᵢ/d⌉·ℓᵢ ≥ ⌈degree/d⌉`. Sound
113    /// ONLY because variables are integral: `⌈aᵢ/d⌉ ≥ aᵢ/d` so the LHS only grows (≥ degree/d), and
114    /// being an integer it is `≥ ⌈degree/d⌉`. This rounding of the fractional slack is exactly what
115    /// makes cutting planes stronger than resolution.
116    pub fn divide_round(&self, d: i64) -> PbConstraint {
117        assert!(d > 0, "divide by a positive integer");
118        let ceil = |x: i64| x.div_euclid(d) + i64::from(x.rem_euclid(d) != 0);
119        PbConstraint {
120            terms: self
121                .terms
122                .iter()
123                .map(|(&v, &(c, s))| (v, (ceil(c), s)))
124                .collect(),
125            degree: ceil(self.degree),
126        }
127    }
128
129    /// **Saturation** — clamp every coefficient to the degree (`min(aᵢ, degree)`). A single 0/1
130    /// variable can contribute at most `degree` toward the bound, so the excess is redundant. Sound,
131    /// and it keeps coefficients from blowing up across many additions.
132    pub fn saturate(&self) -> PbConstraint {
133        let d = self.degree;
134        PbConstraint {
135            terms: self
136                .terms
137                .iter()
138                .map(|(&v, &(c, s))| (v, (c.min(d), s)))
139                .collect(),
140            degree: d,
141        }
142    }
143
144    /// A constraint NO 0/1 assignment can satisfy: the maximum possible LHS (every coefficient
145    /// counted) is still below the degree. The terminal of a refutation — e.g. `0 ≥ 1`.
146    pub fn is_contradiction(&self) -> bool {
147        let max_lhs: i64 = self.terms.values().map(|&(c, _)| c).sum();
148        self.degree > max_lhs
149    }
150
151    /// Evaluate under `assign` (a variable → bool map; unmentioned variables are false): does
152    /// `Σ coeff·[literal true] ≥ degree` hold? The oracle the rule soundness tests check against.
153    pub fn is_satisfied(&self, assign: &dyn Fn(usize) -> bool) -> bool {
154        let lhs: i64 = self
155            .terms
156            .iter()
157            .map(|(&v, &(c, s))| if assign(v) == s { c } else { 0 })
158            .sum();
159        lhs >= self.degree
160    }
161
162    /// `Σ coeffᵢ·litᵢ ≥ degree` directly from weighted terms `(var, coeff, positive)` — for constraints
163    /// whose coefficients are not all 1.
164    pub fn new_weighted(terms: &[(usize, i64, bool)], degree: i64) -> PbConstraint {
165        PbConstraint {
166            terms: terms.iter().map(|&(v, c, s)| (v, (c, s))).collect(),
167            degree,
168        }
169    }
170
171    /// The `(coeff, positive)` term on variable `v`, or `None` if `v` does not occur.
172    pub fn term(&self, v: usize) -> Option<(i64, bool)> {
173        self.terms.get(&v).copied()
174    }
175
176    /// Iterate the constraint's terms as `(variable, coeff, positive)` — the public view of the
177    /// otherwise-private normalized map, so a live theory can read a constraint without owning it.
178    pub fn terms(&self) -> impl Iterator<Item = (usize, i64, bool)> + '_ {
179        self.terms.iter().map(|(&v, &(c, s))| (v, c, s))
180    }
181}
182
183/// Does the variable permutation `perm` preserve every constraint? (`term(perm[v]) == term(v)` for all
184/// `v` — a relabelling that fixes each constraint's coefficient/sign structure, hence its solution set.)
185pub fn is_pb_symmetry(constraints: &[PbConstraint], perm: &[usize]) -> bool {
186    constraints
187        .iter()
188        .all(|c| (0..perm.len()).all(|v| c.term(perm[v]) == c.term(v)))
189}
190
191/// The **coefficient-symmetry** generators of a pseudo-Boolean system: adjacent transpositions of variables
192/// that share a *coefficient profile* — the same `(coeff, sign)` (or absence) in every constraint. Such
193/// variables are interchangeable because each constraint is a weighted sum, blind to which equal-weight
194/// variable carries which value. Each generator is a genuine symmetry ([`is_pb_symmetry`]); together they
195/// generate the full product of symmetric groups on the profile classes. Variables in no constraint are
196/// excluded (free, not a constraint symmetry).
197pub fn coeff_symmetry_generators(num_vars: usize, constraints: &[PbConstraint]) -> Vec<Vec<usize>> {
198    let profile =
199        |v: usize| -> Vec<Option<(i64, bool)>> { constraints.iter().map(|c| c.term(v)).collect() };
200    let mut classes: BTreeMap<Vec<Option<(i64, bool)>>, Vec<usize>> = BTreeMap::new();
201    for v in 0..num_vars {
202        let p = profile(v);
203        if p.iter().all(|t| t.is_none()) {
204            continue; // appears in no constraint
205        }
206        classes.entry(p).or_default().push(v);
207    }
208    let mut gens = Vec::new();
209    for vars in classes.values() {
210        for w in vars.windows(2) {
211            let mut p: Vec<usize> = (0..num_vars).collect();
212            p.swap(w[0], w[1]);
213            gens.push(p);
214        }
215    }
216    gens
217}
218
219/// Refute the pigeonhole principle PHP(`n`, `n−1`) with the classic Cook–Coullard–Turán cutting-
220/// plane proof: sum all `n` "each pigeon in ≥1 hole" constraints with all `n−1` "each hole holds
221/// ≤1 pigeon" constraints. Every `x` meets its `¬x` and cancels, collapsing the whole system to the
222/// trivially-false `0 ≥ 1` in just `2n−1` additions — **linear** size, where resolution needs
223/// exponentially many steps. Returns the final derived constraint (a contradiction).
224pub fn php_refutation(n: usize) -> PbConstraint {
225    let holes = n - 1;
226    let var = |i: usize, h: usize| i * holes + h;
227    let mut acc: Option<PbConstraint> = None;
228    let mut absorb = |c: PbConstraint| {
229        acc = Some(match acc.take() {
230            None => c,
231            Some(a) => a.add(&c),
232        });
233    };
234    // Each pigeon occupies at least one hole: Σ_h x_{i,h} ≥ 1.
235    for i in 0..n {
236        let lits: Vec<(usize, bool)> = (0..holes).map(|h| (var(i, h), true)).collect();
237        absorb(PbConstraint::clause(&lits));
238    }
239    // Each hole holds at most one pigeon: Σ_i x_{i,h} ≤ 1.
240    for h in 0..holes {
241        let lits: Vec<(usize, bool)> = (0..n).map(|i| (var(i, h), true)).collect();
242        absorb(PbConstraint::at_most(&lits, 1));
243    }
244    acc.expect("PHP has at least one constraint")
245}
246
247// ─────────────────────────────────────────────────────────────────────────────
248// Wiring into the general solver: refute a cardinality/pigeonhole CNF by cutting planes.
249// ─────────────────────────────────────────────────────────────────────────────
250
251use crate::ProofExpr;
252use std::collections::HashMap;
253
254/// Refute `e` by CUTTING PLANES when it is a cardinality/pigeonhole-shaped CNF. The pairwise
255/// encoding loses the cardinality structure, so we RECOVER it: the binary exclusions `¬(a∧b)` form
256/// conflict cliques, each a genuine "at most one" group; summing those `at_most(group,1)` cardinality
257/// constraints with the positive "at least one" clauses cancels every literal against its negation
258/// and collapses a pigeonhole-shaped system to `0 ≥ 1` — POLYNOMIAL, where resolution/CDCL explode.
259///
260/// Returns `true` ONLY on a genuine cutting-plane contradiction (a sound UNSAT proof); `false` (the
261/// caller falls through) for any formula that does not collapse. Never a false `true`: each step is
262/// a sound cutting-plane inference, and the at-most-one constraints are *implied* by the verified
263/// exclusion cliques.
264pub fn refute_clausal(e: &ProofExpr) -> bool {
265    let Some((rows, exclusions, nvars)) = extract_clausal(e) else {
266        return false;
267    };
268    if rows.is_empty() {
269        return false;
270    }
271    // Recover at-most-one groups: connected components of the exclusion graph that are full cliques.
272    let mut uf = UnionFind::new(nvars);
273    for &(a, b) in &exclusions {
274        uf.union(a, b);
275    }
276    let excl_set: std::collections::HashSet<(usize, usize)> = exclusions
277        .iter()
278        .map(|&(a, b)| (a.min(b), a.max(b)))
279        .collect();
280    let mut comps: HashMap<usize, Vec<usize>> = HashMap::new();
281    for v in 0..nvars {
282        comps.entry(uf.find(v)).or_default().push(v);
283    }
284    let mut sum: Option<PbConstraint> = None;
285    let mut absorb = |c: PbConstraint| {
286        sum = Some(match sum.take() {
287            None => c,
288            Some(s) => s.add(&c),
289        });
290    };
291    for row in &rows {
292        let lits: Vec<(usize, bool)> = row.iter().map(|&v| (v, true)).collect();
293        absorb(PbConstraint::at_least(&lits, 1));
294    }
295    for members in comps.values() {
296        if members.len() < 2 {
297            continue;
298        }
299        // Only a FULL clique is a sound at-most-one (else two members could share a slot).
300        let is_clique = members.iter().enumerate().all(|(i, &a)| {
301            members[i + 1..]
302                .iter()
303                .all(|&b| excl_set.contains(&(a.min(b), a.max(b))))
304        });
305        if !is_clique {
306            return false; // incomplete at-most-one → can't soundly refute this way
307        }
308        let lits: Vec<(usize, bool)> = members.iter().map(|&v| (v, true)).collect();
309        absorb(PbConstraint::at_most(&lits, 1));
310    }
311    match sum {
312        Some(c) => c.is_contradiction() || c.saturate().is_contradiction(),
313        None => false,
314    }
315}
316
317/// Flatten `e` into `(positive-clause variable lists, binary exclusion pairs, var count)` over dense
318/// variable indices, or `None` if any top-level conjunct is neither an all-positive disjunction nor
319/// a binary mutual-exclusion. (A standalone reader so the PB path is independent of the others.)
320fn extract_clausal(e: &ProofExpr) -> Option<(Vec<Vec<usize>>, Vec<(usize, usize)>, usize)> {
321    let mut conjuncts = Vec::new();
322    flatten_and(e, &mut conjuncts);
323    let mut idx: HashMap<String, usize> = HashMap::new();
324    let mut var = |name: &str, idx: &mut HashMap<String, usize>| -> usize {
325        let n = idx.len();
326        *idx.entry(name.to_string()).or_insert(n)
327    };
328    let mut rows = Vec::new();
329    let mut excl = Vec::new();
330    for c in conjuncts {
331        if let Some(atoms) = positive_disjunction(c) {
332            rows.push(atoms.iter().map(|a| var(a, &mut idx)).collect());
333        } else if let Some((a, b)) = exclusion_pair(c) {
334            excl.push((var(&a, &mut idx), var(&b, &mut idx)));
335        } else {
336            return None;
337        }
338    }
339    let nvars = idx.len();
340    Some((rows, excl, nvars))
341}
342
343fn flatten_and<'a>(e: &'a ProofExpr, out: &mut Vec<&'a ProofExpr>) {
344    // Iterative worklist rather than recursion: a flat CNF clausifies into a left-nested `And` spine
345    // whose depth is the clause count, so a recursive walk overflows the stack on a few-thousand-clause
346    // formula. The explicit stack handles any depth. Pushing `r` before `l` pops `l` first, preserving
347    // the left-to-right clause order the recursive version produced.
348    let mut stack = vec![e];
349    while let Some(node) = stack.pop() {
350        match node {
351            ProofExpr::And(l, r) => {
352                stack.push(r);
353                stack.push(l);
354            }
355            other => out.push(other),
356        }
357    }
358}
359
360fn positive_disjunction(e: &ProofExpr) -> Option<Vec<String>> {
361    fn walk(e: &ProofExpr, out: &mut Vec<String>) -> bool {
362        match e {
363            ProofExpr::Or(l, r) => walk(l, out) && walk(r, out),
364            ProofExpr::Atom(a) => {
365                out.push(a.clone());
366                true
367            }
368            _ => false,
369        }
370    }
371    let mut atoms = Vec::new();
372    walk(e, &mut atoms).then_some(atoms)
373}
374
375fn exclusion_pair(e: &ProofExpr) -> Option<(String, String)> {
376    match e {
377        ProofExpr::Not(inner) => match inner.as_ref() {
378            ProofExpr::And(a, b) => match (a.as_ref(), b.as_ref()) {
379                (ProofExpr::Atom(a), ProofExpr::Atom(b)) => Some((a.clone(), b.clone())),
380                _ => None,
381            },
382            _ => None,
383        },
384        ProofExpr::Or(l, r) => match (l.as_ref(), r.as_ref()) {
385            (ProofExpr::Not(a), ProofExpr::Not(b)) => match (a.as_ref(), b.as_ref()) {
386                (ProofExpr::Atom(a), ProofExpr::Atom(b)) => Some((a.clone(), b.clone())),
387                _ => None,
388            },
389            _ => None,
390        },
391        _ => None,
392    }
393}
394
395struct UnionFind {
396    parent: Vec<usize>,
397}
398impl UnionFind {
399    fn new(n: usize) -> Self {
400        UnionFind {
401            parent: (0..n).collect(),
402        }
403    }
404    fn find(&mut self, x: usize) -> usize {
405        if self.parent[x] != x {
406            let r = self.find(self.parent[x]);
407            self.parent[x] = r;
408        }
409        self.parent[x]
410    }
411    fn union(&mut self, a: usize, b: usize) {
412        let (ra, rb) = (self.find(a), self.find(b));
413        if ra != rb {
414            self.parent[ra] = rb;
415        }
416    }
417}
418
419use crate::cdcl::Lit;
420
421/// A **live cardinality theory** for the CDCL engine ([`crate::cdcl::Theory`]) — the trail-driven twin of
422/// the static cutting-plane refuter [`refute_clausal`]. It propagates and conflicts on unit-coefficient
423/// cardinality constraints (`Σ ℓ ≥ k`, covering clauses, at-least-`k`, and at-most-`k`) directly on the
424/// solver's trail (stateless: it rebuilds the partial assignment each call, so it can never desync), so it
425/// FUSES with a GF(2) parity theory ([`crate::xor_engine::XorEngine`]) under one
426/// [`crate::cdcl::Solver::solve_with`]: parity and counting reason *together* on the shared trail. That is
427/// exactly the structure that defeats either alone — e.g. minimal-disagreement parity, a parity core plus a
428/// residual cardinality core, where Gaussian elimination handles the parity and cardinality propagation
429/// handles the counting, but neither is a refutation by itself.
430///
431/// Pair it with the stateless [`crate::xor_engine::XorEngine`], not the incremental
432/// [`crate::xor_engine::IncXor`]: `IncXor`'s trail-sync matches by variable but not value, so a
433/// backtrack-then-flip leaves its matrix stale — correct whenever a clausal XOR encoding backs it (the
434/// dispatcher's case), but unsound in a pure-theory fusion with no Boolean clauses to mask it.
435///
436/// **Soundness** rests on the cardinality reason clause: for `Σ ℓ ≥ k` over `n` literals, ANY `n − k + 1`
437/// of them form a clause entailed by the constraint (at most `n − k` of the literals can be false, so any
438/// `n − k + 1` contain a true one). Hence when more than `n − k` are false, `n − k + 1` false literals are
439/// an all-false entailed clause — a **conflict**; when exactly `n − k` are false, every unassigned literal
440/// is forced true with reason `{the false literals} ∨ ℓ` (size `n − k + 1`, entailed, currently unit).
441/// Every returned clause is a logical consequence of the constraint, so `Solver::solve_with` may carry it
442/// into the learned database soundly.
443pub struct CardinalityTheory {
444    num_vars: usize,
445    /// Each constraint as `(literals, k)` in `Σ ℓ ≥ k` form. `at_most`/`clause` normalize into this.
446    constraints: Vec<(Vec<(usize, bool)>, i64)>,
447}
448
449impl CardinalityTheory {
450    /// Build from normalized [`PbConstraint`]s. Every input MUST be unit-coefficient (a cardinality
451    /// constraint); a weighted term is rejected (`panic`) rather than risk an unsound weighted reason —
452    /// general weighted PB belongs to the static cutting-plane engine ([`refute_clausal`]), not this live
453    /// clausal-reason theory.
454    pub fn new(num_vars: usize, constraints: &[PbConstraint]) -> Self {
455        let constraints = constraints
456            .iter()
457            .map(|pb| {
458                let lits: Vec<(usize, bool)> = pb
459                    .terms()
460                    .map(|(v, c, s)| {
461                        assert_eq!(
462                            c, 1,
463                            "CardinalityTheory requires unit coefficients (got {c} on var {v})"
464                        );
465                        (v, s)
466                    })
467                    .collect();
468                (lits, pb.degree())
469            })
470            .collect();
471        CardinalityTheory {
472            num_vars,
473            constraints,
474        }
475    }
476}
477
478impl crate::cdcl::Theory for CardinalityTheory {
479    fn propagate(&mut self, trail: &[Lit]) -> Vec<Vec<Lit>> {
480        let mut a: Vec<Option<bool>> = vec![None; self.num_vars];
481        for &l in trail {
482            a[l.var() as usize] = Some(l.is_positive());
483        }
484        let mut out: Vec<Vec<Lit>> = Vec::new();
485        for (lits, k) in &self.constraints {
486            let k = *k;
487            if k <= 0 {
488                continue; // trivially satisfied
489            }
490            let n = lits.len() as i64;
491            if k > n {
492                out.push(Vec::new()); // Σ ℓ ≥ k > n is unsatisfiable — an unconditional contradiction
493                continue;
494            }
495            let mut false_lits: Vec<(usize, bool)> = Vec::new();
496            let mut unassigned: Vec<(usize, bool)> = Vec::new();
497            let mut true_count = 0i64;
498            for &(v, s) in lits {
499                match a[v] {
500                    Some(val) if val == s => true_count += 1,
501                    Some(_) => false_lits.push((v, s)),
502                    None => unassigned.push((v, s)),
503                }
504            }
505            if true_count >= k {
506                continue; // already satisfied
507            }
508            let max_false = n - k; // the constraint tolerates at most this many false literals
509            let fc = false_lits.len() as i64;
510            if fc > max_false {
511                // CONFLICT: any (max_false + 1) of the false literals are an entailed, all-false clause.
512                let take = (max_false + 1) as usize;
513                out.push(
514                    false_lits[..take]
515                        .iter()
516                        .map(|&(v, s)| Lit::new(v as u32, s))
517                        .collect(),
518                );
519            } else if fc == max_false && !unassigned.is_empty() {
520                // Every unassigned literal is forced true: reason = {the false literals} ∨ ℓ.
521                let base: Vec<Lit> = false_lits
522                    .iter()
523                    .map(|&(v, s)| Lit::new(v as u32, s))
524                    .collect();
525                for &(v, s) in &unassigned {
526                    let mut c = base.clone();
527                    c.push(Lit::new(v as u32, s));
528                    out.push(c);
529                }
530            }
531        }
532        out
533    }
534}
535
536#[cfg(test)]
537#[path = "../../logicaffeine_tests/unit/logicaffeine_proof/pseudo_boolean/tests.rs"]
538mod tests;