LogicSpace: A Dictionary-Based SAT Knowledge Compiler

Charles Dana
Tulane University • Monce SAS • ORCID: 0000-0002-0364-5379
April 2026

Abstract. We introduce LogicSpace, a polynomial-time preprocessor that compiles a CNF formula into a dictionary of implications, then exports learned clauses that reduce industrial solver conflicts by up to 94% on structured instances (integer factoring circuits). The DualTree extension compiles a binary implication tree in $O(n \cdot m)$ using only BCP propagation, producing a JSON-serializable knowledge artifact. On factoring circuits, the DualTree achieves 100% conflict elimination (0 remaining conflicts) by discovering 82–157 backbone variables purely from propagation under assumptions. We provide a live API for submitting instances, querying the dictionary, compiling DualTrees, and exporting optimized CNFs. Modern solvers with built-in inprocessing (CaDiCaL/Kissat) are not helped — they already discover the same implications internally.

Executive Summary

WHAT IT IS A SAT knowledge compiler that produces two outputs from one CNF: optimal_cnf() original + learned clauses ≥ |F| more knowledge simplified_cnf() original − learned knowledge ≤ |F| smaller, isomorphic Three compilation stages, each polynomial, each additive: 1. DualTree BCP under log(n) vocabulary O(n · m) ~1ms/100vars 2. Vivify enriched BCP contradicts → drop lit O(m · k) ~5ms 3. PolySAT 16 techniques by tier (0-5) O(n²) to O(n²m) ~80ms ┌─────────────────┐ │ CNF formula F │ n vars, m clauses └────────┬────────┘ │ ▼ ┌─────────────────┐ achilles picks log(n) vocabulary variables │ DualTree.compile │ BCP tree: 2n nodes, each O(m) propagate │ O(n · m) │ backbones + lifted clauses + dead branches └────────┬────────┘ │ ├── optimal_cnf() F + learned (feed to Glucose/Kissat) ├── simplified_cnf() F vivified against enriched BCP oracle │ ▼ ┌─────────────────┐ 16 polynomial techniques on simplified │ polysat_simplify │ BCP → 2-SAT → XOR → Gauss → BCE → BVE → ... │ tier 0-5 │ each fires, logs, further reduces └────────┬────────┘ │ ├── simplified_cnf() now further reduced (76% on factoring) ├── shrunk = true/false (synthesis: did it compress?) │ ▼ ┌─────────────────┐ if shrunk: recurse with simplified as input │ Λ worker │ vocabulary passed from coordinator │ recursive call │ each child gets a strictly smaller formula └─────────────────┘ depth = log(n), cost = O(n · poly(n))
THE LIBRARY pip install logicspace (or deploy as Lambda layer) from logicspace import DualTree # Compile — O(n·m), ~1ms per 100 vars tree = DualTree.compile(n_vars, clauses, vocabulary=vocab) # Two precomputed outputs, O(1) tree.optimal_cnf() # feed to solver tree.simplified_cnf() # feed to next worker tree.shrunk # recurse or stop? # PolySAT by tier — caller controls budget tree.polysat_simplify(max_tier=2) # + BCE, HBR, TSER # JSON portable — load anywhere without pysat tree.to_json("compiled.json") tree = DualTree.from_json("compiled.json") # Lambda recursive deployment handler receives: (clauses, vocabulary, depth, max_tier) handler runs: DualTree.compile + polysat_simplify handler returns: simplified_cnf + learned + shrunk handler recurses: if shrunk, invoke Λ(simplified, depth+1)

Experimental Results

Circuit SAT (Integer Factoring)

Binary multiplication circuit $N = p \times q$. CHAIN compiles carry propagation into binary clauses. SEED+CHAIN preprocessing discovers 356–858 backbone variables and exports binary implications that eliminate most solver conflicts.

v1 (SEED+CHAIN only):

$N$bitsvars$T_{\text{raw}}$$C_{\text{raw}}$ $T_{\text{opt}}$$C_{\text{opt}}$BBSolve speedup$C$ reduction
32399080.7ms1240.1ms83566.4×94%
3,599121,6434.0ms3540.9ms925644.5×74%
10,403142,25311.6ms8544.6ms3267052.5×62%
46,657162,95920.9ms1,4801.4ms19585814.9×87%

v2 (DualTree): On factoring circuits, the DualTree discovers enough backbones via BCP alone to eliminate all solver conflicts:

$N$bitsvarsCompileNodesDeadBB found$C_{\text{opt}}$$C$ reduction
323990877ms449223820100%
3,599121,6431.4s3,6151,8061240100%
10,403142,25311.7s8,1257881570100%

The DualTree's compile time is higher than v1 preprocessing (the full binary tree has thousands of nodes), but the result is strictly stronger: the solver has nothing left to search. Half the tree nodes are dead (contradictions), confirming that the carry chain structure creates deep propagation chains that BCP under assumptions can fully exploit.

Conflict reduction: v1 vs DualTree (factoring) v1 SEED+CHAIN (323) ████████████████████████████░░ 94% v2 DualTree (323) ██████████████████████████████ 100% v1 SEED+CHAIN (3599) ██████████████████████░░░░░░░░ 74% v2 DualTree (3599) ██████████████████████████████ 100% v1 SEED+CHAIN (10403) ██████████████████░░░░░░░░░░░░ 62% v2 DualTree (10403) ██████████████████████████████ 100%

The Golden Test: $T_{\text{pre}} + T_{\text{opt}} < T_{\text{raw}}$

Does preprocessing pay for itself? On hard random 3-SAT ($n \geq 250$). These timings were measured on a t3.small EC2 (eu-west-3, 2 vCPU, 2GB RAM) with Glucose3 via pysat. Conflict counts are deterministic (same seed = same conflicts); wall-clock times vary by hardware.

$n$seed$T_{\text{raw}}$$C_{\text{raw}}$ $T_{\text{pre}}$$T_{\text{opt}}$$T_{\text{total}}$ $C_{\text{opt}}$Total speedup
250423,027ms177,315 21ms2,584ms2,606ms164,7271.16× WIN
275772,720ms167,080 20ms2,333ms2,352ms151,3331.16× WIN
3004212,878ms476,984 22ms11,960ms11,982ms448,3061.07× WIN
3001232,031ms119,180 26ms286ms312ms28,2416.5× WIN
Result. Preprocessing cost is 20–27ms regardless of instance hardness (polynomial in clause count). Conflict reduction is consistent across hardware: 7–76% fewer conflicts on these instances. Wall-clock speedup is machine-dependent and varies with Glucose3 version — the conflict reduction is the reproducible metric. Best case: 76% conflict reduction (119,180 → 28,241 conflicts).

Multi-Solver Comparison

Does it help all solvers? Tested on hard 3-SAT ($n=300$, seed=123) on t3.small EC2. Wall-clock speedups are machine-specific; conflict reduction is deterministic.

Solver$T_{\text{raw}}$$T_{\text{pre}}+T_{\text{opt}}$Total speedup$C$ reduction
Glucose32,110ms325ms6.5× WIN76%
MapleChrono3,188ms1,003ms3.2× WIN51%
CaDiCaL (Kissat-family)981ms2,667ms0.4×−125%
Who benefits? ✓ Glucose3 — lacks inprocessing, benefits from learned clauses ✓ MapleChrono — same, benefits from learned clauses ✗ CaDiCaL/Kissat — built-in inprocessing already discovers the same implications; our clauses are redundant for them ✓ Circuit SAT — long implication chains, many backbones (strongest) ✓ Hard random — when T_raw > 1s, preprocessing cost is amortized ✗ Easy random — T_raw < 100ms, preprocessing overhead exceeds savings ✗ Symmetric — pigeonhole: no backbones, nothing to compile

Theory

Core Data Structure

Definition (Entry). An entry is $A \to B$ where $A, B \subseteq \text{Lits}$, $A \subset B$. Semantics: if all literals in $A$ hold, all literals in $B$ hold. The gain is $B \setminus A$. The leverage is $|B|/|A|$.
Definition (Dictionary). $D$ maps assumptions to maximal deductions: $D[A] = B$ with $O(1)$ lookup. The baseline $D[\emptyset]$ contains all backbones.

Operators

SEED. Clause $C = (l_1 \vee \cdots \vee l_k)$: for each $l_i$, emit $\{-l_j : j \neq i\} \to \{-l_j : j \neq i\} \cup \{l_i\}$.

CHAIN. $D[A]=B,\; D[C]=E,\; C \subseteq B \;\Longrightarrow\; D[A] := B \cup E$. Compiles multi-step BCP into single lookups.

CONFLICT. $D[A] \supseteq \{l, -l\} \;\Longrightarrow\;$ learn clause $(\neg a_1 \vee \cdots \vee \neg a_{|A|})$, SEED it.

EXPORT. Entry $\{a_1, \ldots, a_m\} \to B$ with gain $G = B \setminus A$ emits $|G|$ clauses: $$\forall\, g \in G: \quad (\neg a_1 \vee \cdots \vee \neg a_m \vee g)$$

The Optimal CNF

$$\mathcal{F}^* = \underbrace{\{(l) : l \in \mathcal{B}\}}_{\text{backbone units}} \;\cup\; \underbrace{\mathcal{F}_{\text{surviving}}}_{\text{pruned originals}} \;\cup\; \underbrace{\bigcup_{A \to B \in D} \{(\neg A \vee g) : g \in B \setminus A\}}_{\text{learned implications}}$$
Proposition (Soundness). Every learned clause is implied by the original CNF. $\mathcal{F}^*$ is equisatisfiable with $\mathcal{F}$. The learned clauses are redundant but helpful — they encode pre-compiled propagation chains.

Tension

The tension $\tau(D)$ measures dictionary completeness:

$$\tau(D) = \frac{|\text{backbones}(D)| + \sum_{A \to B \in D} |B \setminus A|}{n \cdot (n + 1)}$$

$\tau = 0$: knows nothing. $\tau = 1$: complete propagation table.

Prove, Extend, Prune

Every operation maps to a SAT call on the optimal CNF:

OperationSAT instanceWhat D learns
prove(L, R)$\mathcal{F}^* \wedge L \wedge \neg r$ — UNSAT?$(\neg L \vee r)$ clause
extend(L)$\mathcal{F}^* \wedge L \wedge v$ per varForced lits under $L$
prune(entry)$\mathcal{F}^* \wedge (L \setminus \{l\}) \wedge \neg g$ per $l$Smaller key, same gain

The Dictionary as a Knowledge Base

Beyond preprocessing: a queryable knowledge base ┌─────────────────────────────────────────────────────────┐ │ prove(left, right) — is this implication valid? │ │ extend(left) — what's forced from here? │ │ prune(entry) — can we remove an assumption? │ │ achilles() — which variable matters most? │ │ entries(top_n) — strongest implications │ │ preprocess() — export optimal CNF │ │ export/annotated — CNF with solution + backbones │ └─────────────────────────────────────────────────────────┘ The flywheel: D identifies what to prove → solver proves it → D stores it → D identifies the next thing → solver proves it → ... Each result enriches D. The optimal CNF gets richer. Next query is faster because D is richer.

Architecture

CNF instance │ ▼ ┌─────────────────┐ │ LogicSpace │ SEED + CHAIN (polynomial, ~20ms) │ Dictionary D │ → backbones, implications, entries └────────┬────────┘ │ │ export optimal CNF │ (original - subsumed + backbone units + learned clauses) │ ▼ ┌─────────────────┐ │ Industrial │ Glucose / CaDiCaL / Kissat │ SAT Solver │ → SAT/UNSAT in reduced search space └────────┬────────┘ │ │ learned clauses flow back ▼ ┌─────────────────┐ │ Dictionary D │ prove(), extend(), prune() │ (enriched) │ each = one solver.solve(assumptions=...) └────────┬────────┘ │ ▼ next query (incremental) or export

For large instances (e.g. SHA-256 circuit SAT): the dictionary identifies which assumptions are worth probing via the achilles heuristic, and dispatches targeted solver.solve(assumptions=[...]) calls. Multiple solver threads can work different assumptions concurrently, all writing to the same dictionary.

The Dual Implication Tree

An implication in LogicSpace v2 is a two-output object:

$$(\bigwedge l_i) \;\longrightarrow\; \left(\;\bigwedge l_j\;,\;\; \mathcal{F}'\right)$$

The left output is the standard forced-literal set (AND). The right output is the reduced instance $\mathcal{F}' = \text{BCP}(\mathcal{F} \mid l_i)$: the formula simplified under the assumptions.

Definition (Dual Entry). A dual entry maps assumptions to both forced literals and a simplified sub-instance. Implications discovered in $\mathcal{F}'$ with fewer assumptions propagate uphill to the parent dictionary — shorter keys, more general knowledge.

Construction: $O(n \log n)$

The DualTree is compiled in three steps:

  1. Vocabulary. The achilles heuristic scores each variable by $\text{score}(v) = \max(\text{pos}, \text{neg}) \times (\text{pos} + \text{neg})$, where pos/neg are literal occurrence counts. This produces a full permutation of unassigned variables — computed once, constant, passed downhill.
  2. Binary tree. Depth $= \lceil\log_2 n\rceil$. At each node, branch on the next vocabulary variable. Each node calls solver.propagate(assumptions) — pysat's BCP in C, $O(m)$ per call. No search, no decisions, no backtracking.
  3. Lift. Every literal forced by BCP under assumptions $\{a_1,\ldots,a_k\}$ lifts to root via the deduction theorem: $$\mathcal{F} \wedge a_1 \wedge \cdots \wedge a_k \vdash l \quad\Longrightarrow\quad \mathcal{F} \vdash (\neg a_1 \vee \cdots \vee \neg a_k \vee l)$$ If both children of a node force the same literal, it lifts with one fewer assumption (AND-lift). Dead branches (contradiction) force the opposite polarity.
DualTree compilation (O(n · log n)) achilles: score(v) = max(pos,neg) × (pos+neg) ← O(m), once vocabulary = [v7, v3, v12, v1, ...] ← fixed permutation depth 0: branch v7 ← 1 node │ ├─── v7=T: BCP → forced lits → lift ← O(m) BCP └─── v7=F: BCP → forced lits → lift ← O(m) BCP depth 1: branch v3 (on each surviving child) ← ≤2 nodes depth 2: branch v12 ← ≤4 nodes ... depth log(n): ← ≤n nodes Total: ≤2n nodes × O(m) BCP = O(n · m) ⊆ O(n · log n) Dead branches prune the tree — structured instances lose most branches early, effective nodes << 2n.
Proposition (Soundness). Every clause lifted by the DualTree is implied by $\mathcal{F}$. Proof: BCP under assumptions is sound. The deduction theorem is sound. Composition of sound operations is sound. $\square$
Proposition (Complexity). DualTree compilation runs in $O(n \cdot m)$ time. Proof: achilles is $O(m)$. Depth $= \lceil\log_2 n\rceil$ gives at most $2n-1$ nodes. Each node calls propagate() once: $O(m)$ via watch-list BCP. Total: $O(m) + O(2n \cdot m) = O(n \cdot m)$. For $k$-SAT with $m = O(n)$: $O(n^2)$. In practice, dead branches reduce effective nodes well below $2n$. $\square$

Vocabulary determines quality

At depth $d$, a unit clause discovered by BCP lifts to a clause of width $d+1$ at root. The branching order determines how constrained each sub-instance is. A good vocabulary produces sub-instances where BCP finds many forced literals — lifting short, powerful clauses. A bad vocabulary produces sub-instances where BCP finds nothing.

The achilles heuristic selects variables that are both frequent (high occurrence) and polarized (one polarity dominates). Branching on such variables maximizes the asymmetry between the two children, which is when BCP discovers the most.

On-demand deepening: $O(n^2)$

The base compile uses only BCP (fast, shallow). For nodes that deserve richer analysis, deepen() runs the full dictionary pipeline (SEED+CHAIN+CONFLICT) on the sub-instance. This is $O(n^2)$ per node but discovers implications invisible to BCP alone.

Two-tier DualTree Tier 1: compile() O(n · log n) always ───────────────────────────────────── pysat propagate (BCP in C) microseconds per node full vocabulary permutation dead-branch pruning Tier 2: deepen() O(n²) per leaf on demand ───────────────────────────────────── full dictionary build (SEED+CHAIN+CONFLICT) small-key entries lift as short clauses call selectively on high-value leaves

Performance

$n$$m$CompileNodesLiveDeadLearned
502130.8ms815625288
1004261.4ms20719017695
5002,13014ms1,0231,02304,485
1,0004,26015ms2,0472,04704,636

Distributed compilation via recursive Lambda workers

The DualTree is embarrassingly parallel: each node is independent once its assumptions are fixed. This maps directly to a recursive Lambda architecture where each invocation is a full knowledge compiler — not just BCP, but the entire polynomial toolkit. Workers spawn workers, achieving spot exponential branching with pay-per-node pricing.

Recursive Lambda architecture Coordinator (API server) │ │ invoke with (clauses, vocab, assumptions=[], depth=0) ▼ Lambda worker depth=0, branch on vocab[0] │ ├── simplify: F' = F | assumptions (remove satisfied, shorten) │ ├── compile: DualTree.compile(F') ← O(n · log n) │ full BCP tree on sub-instance │ discovers backbones, forced lits │ ├── deepen: tree.deepen_all() ← O(n²), optional │ SEED+CHAIN+CONFLICT per leaf │ polynomial techniques on reduced instance │ ├── lift: learned clauses → root via deduction theorem │ ├── if depth < max_depth AND not dead: │ invoke Lambda(assumptions + [+v]) concurrent │ invoke Lambda(assumptions + [-v]) concurrent │ └── return {forced, learned, tree_insights, children} Each worker runs the full polynomial toolkit on its sub-instance: — O(n · log n) DualTree compile (BCP propagation tree) — O(n²) dictionary enrichment (SEED+CHAIN+CONFLICT) — Backbone detection, implication lifting, clause shortening Dead branches don't recurse → structured instances prune 50%+ of nodes Depth log(n) → at most n concurrent Lambdas at the deepest level
# Lambda handler — each invocation is a full knowledge compiler
def handler(event, context):
    clauses     = event["clauses"]
    vocab       = event["vocabulary"]
    assumptions = event["assumptions"]
    depth       = event["depth"]
    max_depth   = event["max_depth"]
    n_vars      = event["n_vars"]

    # ── Phase 1: Simplify under assumptions ── O(m)
    assigned = set(assumptions)
    sub_clauses = []
    for clause in clauses:
        if any(lit in assigned for lit in clause):
            continue  # satisfied
        reduced = [lit for lit in clause if -lit not in assigned]
        if not reduced:
            return {"dead": True,
                    "learned": [[-a for a in assumptions[:-1]] + [-assumptions[-1]]]}
        sub_clauses.append(reduced)

    # ── Phase 2: Full DualTree compile ── O(n · log n)
    tree = DualTree.compile(n_vars, sub_clauses)
    forced = tree.backbones
    learned = [[-a for a in assumptions] + [l] for l in forced]

    # Lift all short clauses from sub-tree to root
    for clause in tree.optimal_cnf():
        if len(clause) <= 3 and clause not in sub_clauses:
            lifted = [-a for a in assumptions] + clause
            learned.append(lifted)

    # ── Phase 3: Deepen (optional) ── O(n²)
    tree.deepen_all()
    for lit in tree.backbones:
        if lit not in forced:
            learned.append([-a for a in assumptions] + [lit])

    if depth >= max_depth:
        return {"dead": False, "forced": forced,
                "learned": learned, "insights": tree.insights()}

    # ── Phase 4: Recurse — spawn two child Lambdas ──
    assigned_vars = {abs(l) for l in forced} | {abs(a) for a in assumptions}
    var = next((v for v in vocab if v not in assigned_vars), None)
    if var is None:
        return {"dead": False, "forced": forced, "learned": learned}

    import boto3, json, concurrent.futures
    client = boto3.client("lambda")

    def invoke_child(polarity):
        resp = client.invoke(
            FunctionName="logicspace-dual-worker",
            InvocationType="RequestResponse",
            Payload=json.dumps({
                "clauses": clauses, "vocabulary": vocab,
                "assumptions": assumptions + [polarity],
                "depth": depth + 1, "max_depth": max_depth,
                "n_vars": n_vars,
            }),
        )
        return json.loads(resp["Payload"].read())

    # Both children run concurrently
    with concurrent.futures.ThreadPoolExecutor(2) as ex:
        child_pos, child_neg = ex.submit(invoke_child, var), ex.submit(invoke_child, -var)
        children = [child_pos.result(), child_neg.result()]

    # AND-lift: both children force same lit → forced at parent depth
    if not children[0].get("dead") and not children[1].get("dead"):
        left_f = set(children[0].get("forced", []))
        right_f = set(children[1].get("forced", []))
        for lit in left_f & right_f:
            learned.append([-a for a in assumptions] + [lit])

    # Dead branch → opposite polarity forced
    for child in children:
        learned.extend(child.get("learned", []))
        if child.get("dead") and child.get("learned"):
            learned.extend(child["learned"])

    return {"dead": False, "forced": forced,
            "learned": learned, "children": children}

Each worker is a full compiler. Not a thin BCP probe — every Lambda runs the complete polynomial toolkit on its sub-instance: $O(n \cdot \log n)$ DualTree compilation (BCP propagation tree with vocabulary-ordered branching), then optionally $O(n^2)$ dictionary enrichment (SEED+CHAIN+CONFLICT). The sub-instance is already simplified under the parent's assumptions, so the effective $n$ shrinks at each depth. A worker at depth $d$ with $k$ backbones from its parent operates on $n - d - k$ effective variables.

Cost model. At depth $d = \lceil\log_2 n\rceil$, the tree has at most $2n - 1$ nodes. Each Lambda runs the full compile + deepen pipeline in ~100–500ms with 256MB. On structured instances, 50%+ of branches are dead (no recursion). For $n = 1000$: ~1000 Lambda invocations × 500ms × $0.0000067/GB-s = ~$0.001 per full compile. Wall-clock time = depth × Lambda overhead: $\lceil\log_2 1000\rceil \times 500\text{ms} \approx 5\text{s}$ end-to-end for a 1000-variable instance with full polynomial analysis at every node.

Why this works. The branching factor is 2 and each node is stateless (receives clauses + assumptions, returns learned clauses). No shared memory, no coordination. The parent waits for both children (concurrent invocations), merges their lifted clauses, and returns uphill. The deduction theorem guarantees soundness at every level. The vocabulary (achilles permutation) is computed once by the coordinator and passed to all workers — deterministic branching, reproducible results.

DualTree.compile() accepts a vocabulary parameter: the coordinator computes it once via achilles, every Lambda worker receives it and branches deterministically without recomputing. Same pattern as Snake v6's oppose() literal passed to child workers.

Rephrasing into $\log n$ vocabulary

At depth $d = \lceil\log_2 n\rceil$, the tree branches on $d$ vocabulary variables. At every node, the enriched BCP oracle vivifies the original clauses under the current assumptions. Even if a branch is only locally UNSAT (contradicts under specific assumptions), the vivification shortens the original clause — removing a literal that was provably redundant in that context.

After the full tree completes, every local contradiction at every depth has been used to vivify the original formula. The result: the problem is rephrased in terms of the remaining variables — the ones the vocabulary didn't branch on. If achilles picked the $\log n$ most discriminative variables, the remaining variables are the least connected, the easiest to resolve.

Rephrasing into log(n) vocabulary Input: F with n variables, m clauses of width k Vocabulary: achilles picks log(n) most discriminative variables [v1, v2, ..., vlog n] Tree: depth = log(n), nodes ≤ 2n, cost = O(n · m) At each node (assumptions A, branch variable vi): For each original clause (l1 ∨ l2 ∨ ... ∨ lk): assume (¬l1, ..., ¬lk-1) on the enriched solver BCP contradicts? → lk is redundant → clause shortened Even locally UNSAT branches contribute: branch A ∪ {vi} contradicts → clause containing vi loses vi The contradiction was local, but the vivification is global. Output: simplified_cnf() — same satisfiability as original — total_lits(simplified) ≤ total_lits(original) — every clause is a subset of some original clause — vocabulary variables resolved: problem rephrased in terms of the remaining n − log(n) variables Cost: O(n · m) for log(n) depth = 2n nodes polynomial total, exponential branching is bounded by n The depth log(n) gives 2log(n) = n leaf nodes. That's the budget: n polynomial-time workers, each vivifying the original against the enriched oracle. The vocabulary is the bridge between exponential branching and polynomial cost.
Observation. The DualTree at depth $\lceil\log_2 n\rceil$ rephrases an $n$-variable CNF into a simplified formula over $n - \lceil\log_2 n\rceil - k$ effective variables, where $k$ is the number of backbone variables discovered by vivification. The cost is $O(n \cdot m)$: at most $2n$ nodes, each running BCP vivification in $O(m \cdot k)$ where $k$ is the clause width. The shrunk boolean reports whether the rephrasing achieved any compression. If shrunk = false, the vocabulary was not discriminative enough — achilles should be re-run on the enriched formula for a second pass.

Simplified CNF

The DualTree produces two CNF outputs from one compile:

OutputWhatSizeUse
optimal_cnf()original + all learned clauses≥ originalFeed to solver — more knowledge, faster search
simplified_cnf()original − what we learned≤ originalFeed to next worker — smaller, isomorphic instance

The simplified CNF applies everything the tree discovered to reduce the original:

  1. Global BCP. Load original + all learned clauses into pysat. propagate() with no assumptions discovers forced literals invisible to BCP on the original alone. Remove satisfied clauses, shorten others.
  2. Vivification. For each surviving clause $(l_1 \vee l_2 \vee \cdots \vee l_k)$: assume the negation of all literals except one on the enriched solver. If BCP contradicts, that literal is redundant — the clause is shortened. The original formula doesn't know this, but the enriched oracle does.
  3. Cascade. Shortened clauses may become units. Propagate iteratively until fixpoint. Deduplicate.
Vivification via enriched BCP Original clause: (a ∨ b ∨ c ∨ d) On original only: assume(-a, -b, -c) → BCP propagates → no contradiction d is NOT provably redundant On enriched: assume(-a, -b, -c) → BCP propagates → contradiction! the learned clauses create chains that BCP follows to ⊥ d IS provably redundant Simplified clause: (a ∨ b ∨ c) ← one literal shorter still in original vocabulary still sound (same satisfiability)

Result: isomorphic to the original, fewer clauses, shorter clauses, same satisfiability. The learned clauses are never in the output — they are the lens, not the picture.

InstanceClausesLitsSimplifiedLitsLit reductionShrunk
Factor 323 (9-bit)2,8007,0261,5953,96243.6%true
Factor 3,599 (12-bit)5,10112,8253,0477,62540.5%true
Random 3-SAT (n=50)2136392106212.8%true
Random 3-SAT (n=100)4261,2784261,2780.0%false
Invariants. The simplified CNF satisfies three guarantees: (1) total_lits(simplified) ≤ total_lits(original) — the formula never grows. (2) Every output clause is a subset of some original clause — no new literals. (3) SAT(simplified) ↔ SAT(original) — isomorphic. The boolean shrunk reports whether any actual reduction occurred.

Why vivification on the enriched formula is strictly stronger than on the original. The enriched formula contains learned binary and ternary clauses that create BCP propagation chains absent from the original. When we assume the negation of literals in an original clause, BCP on the enriched formula follows these chains further — reaching contradictions the original cannot see. Every contradiction proves a literal redundant, shortening the clause. This is vivification, but with a polynomial-time compiled oracle (the DualTree's learned clauses) providing the extra propagation power.

On structured instances (factoring), vivification shortens clauses from width 3 to width 2 and removes 40–44% of total literals. On random 3-SAT (n=50), the enriched oracle finds enough to vivify 9 clauses. On larger random instances, shrunk = false — honest. The synthesis boolean tells the recursive Lambda architecture whether to pass the simplified formula downhill: if shrunk = false, further recursion cannot help.

The dual output DualTree.compile(F) │ ├── optimal_cnf() F + learned ≥ |F|, more knowledge │ feed to Glucose/Kissat │ └── simplified_cnf() F − learned ≤ |F|, isomorphic, smaller feed to next Lambda worker or to DualTree.compile() again The recursive pattern: worker receives simplified_cnf from parent runs DualTree.compile() on it produces a FURTHER simplified_cnf passes it to children Each level of recursion starts from a strictly smaller instance.

PolySAT integration: 16 techniques by tier

After the DualTree compile (BCP) and vivification, polysat_simplify(max_tier) runs the full PolySAT polynomial pipeline on the simplified formula. Each tier adds techniques at increasing cost:

TierTechniquesComplexity
0BCP, pure literal, 2-SAT$O(n \cdot m)$
1XOR extraction, GF(2) Gauss$O(c \log c)$
2BCE, paired resolve, HBR, TSER$O(c^2 \cdot k)$
3Equivalent lits, subsumption, BVE$O(m^2 \cdot k)$
4Vivification, failed literal probe$O(n^2 \cdot m)$
5Isomorphic transform, rephrase$O(\text{iter} \cdot n^2 \cdot \text{BCP})$

Factoring 323 (9-bit): original 2,800 clauses / 7,026 literals.

StageTimeClausesLitsReductionWhat fired
DualTree compile77ms1,5953,96243.6%BCP vivification
+ Tier 1+6ms1,1572,64662.3%XOR + GF(2) Gauss + pure
+ Tier 2+74ms7181,66776.3%+ BCE×15, TSER

76.3% literal reduction — from 7,026 to 1,667 literals. DualTree compile provides the BCP foundation (43.6%), XOR+Gauss finds parity constraints in the carry chain (62.3%), BCE eliminates blocked clauses (76.3%). Each tier is logged with per-technique fire counts.

tree = DualTree.compile(n_vars, clauses, vocabulary=vocab)

# O(1) — precomputed by compile
tree.simplified_cnf()             # original - BCP knowledge
tree.optimal_cnf()                # original + learned
tree.backbones                    # forced variables
tree.shrunk                       # did it compress?

# On demand — polynomial by tier
tree.polysat_simplify(max_tier=2) # + BCE, HBR, TSER
tree.polysat_simplify(max_tier=4) # + vivify, probe

# Portable
tree.to_json("model.json")
tree = DualTree.from_json("model.json")
Lambda worker with full polynomial toolkit def handler(event, context): clauses = event["clauses"] # simplified from parent vocab = event["vocabulary"] # achilles permutation, fixed depth = event["depth"] # Phase 1: DualTree compile — O(n · m) tree = DualTree.compile(n, clauses, vocabulary=vocab) # Phase 2: PolySAT by tier — caller chooses budget tree.polysat_simplify(max_tier=event.get("tier", 2)) # Phase 3: recurse if shrunk if tree.shrunk and depth < max_depth: var = next(v for v in vocab if v not in assigned) invoke(Λ, clauses=tree.simplified_cnf(), ...) return {learned, simplified, insights}

The compiled tree is self-documenting: every node records its assumptions, forced literals, and lifted clauses. The JSON artifact can be loaded on any machine without pysat — both the optimal and simplified CNFs are precomputed and served in $O(1)$.

API

POST /instances/clauses              submit CNF as JSON
POST /instances/dimacs               submit CNF as DIMACS text
POST /instances/{id}/preprocess      export optimal CNF (~20ms)
POST /instances/{id}/solve           solve with industrial solver
POST /instances/{id}/prove           verify left => right
POST /instances/{id}/deduce          propagate partial assignment
POST /instances/{id}/entry/detail    inspect an implication
POST /instances/{id}/entry/prune     shrink assumption set
POST /instances/{id}/entry/extend_right  grow deduction set
GET  /instances/{id}/entries         top implications by leverage
GET  /instances/{id}/backbones       forced literals
GET  /instances/{id}/achilles        highest-value variables
GET  /instances/{id}/kpis            tension, coverage, compute
GET  /instances/{id}/export/annotated  optimal CNF with comments
POST /instances/{id}/dual/compile    compile DualTree (O(n log n))
GET  /instances/{id}/dual/insights   tree summary & statistics
GET  /instances/{id}/dual/optimal    optimal CNF from DualTree
POST /instances/{id}/dual/deepen     enrich leaves (O(n^2) on demand)
GET  /instances/{id}/dual/tree       export DualTree as JSON
GET  /ui/{id}                        live resolution dashboard

Discussion

What is new. LogicSpace v1 compiles multi-step BCP into single $O(1)$ lookups, reducing solver conflicts by 62–94% on integer factoring circuits (reproduced). The DualTree (v2) goes further: every implication is a two-output object producing both forced literals and a reduced sub-instance. The $O(n \cdot m)$ compilation via pysat BCP produces a JSON-serializable knowledge artifact that serves the optimal CNF in $O(1)$.

The deduction theorem lift. The key theoretical insight: anything discovered under assumptions lifts to the root as a short clause. At depth $d$, a BCP-forced unit becomes a $(d+1)$-clause at root. Both children forcing the same literal yields the AND-lift — the literal is forced at the parent's depth. Dead branches force the opposite polarity. Every lifted clause is sound by construction.

Structured vs. random. The DualTree's power depends on instance structure. On factoring circuits, BCP under assumptions propagates deeply through carry chains — the DualTree eliminates 100% of conflicts with 82–157 backbones. On hard random 3-SAT, BCP under assumptions finds little — the conflict reduction is modest (7–17%). The DualTree excels where the instance has long implication chains that BCP can exploit. Random 3-SAT at the phase transition has short, tangled chains where search dominates.

Limitations. CaDiCaL/Kissat already do internally what CHAIN does. The DualTree's BCP-only tier discovers what propagation discovers — it cannot find implications that require search. The on-demand deepen() tier adds full dictionary power but at $O(n^2)$ cost per leaf. The depth parameter trades clause width (quality) for coverage (quantity). DualTree compile time scales as $O(n \cdot m)$; on large structured instances (2000+ vars), the tree can have thousands of nodes and produce millions of learned clauses — the compile is still polynomial but wall-clock time grows to seconds.

Reproducibility. The DualTree performance table (nodes, learned counts) is fully deterministic and reproduces exactly across machines. The factoring conflict reductions (94%, 74%, 62%, 87%) reproduce. Wall-clock speedups are hardware-dependent: the factoring table was measured on Apple Silicon, the random 3-SAT table on t3.small EC2. Conflict counts are the reproducible metric; timing ratios vary by ±2× across hardware.

The unique contribution is the compiled tree as a portable artifact. A DualTree JSON file can be loaded without pysat, without the original solver, on any machine. The optimal CNF is precomputed. The insights are immediate. No existing SAT tool produces a self-documenting, JSON-serializable knowledge base from a CNF formula.

Connection to AUMA. LogicSpace is the SAT-domain specialization of AUMA's universal maximization framework (Dana, 2023). The Dana Theorem guarantees polynomial SAT encoding; LogicSpace exploits the structure for fast inference. The dictionary's entries are the SAT analogue of AUMA's Fourier coefficients. The DualTree's depth parameter is the analogue of AUMA's exponent $a$: polynomial budget per node, binary branching on a fixed vocabulary, sound by construction.

Ecosystem

LogicSpace is a library. It runs on a potato. Pure Python, zero native dependencies beyond pysat (optional). pip install it, call DualTree.compile(), get a simplified CNF. The theory is self-contained: DualTree + 16 PolySAT techniques + vivification via enriched BCP. Every operation is polynomial, every result is sound, every output is a valid DIMACS CNF.

npdollars.aws.monce.ai is where LogicSpace scales. The npdollars solver uses LogicSpace as its compilation engine inside recursive Lambda workers. Depth escalation ($d=1$ cheapest first, escalate on stall) controls the cost dial. Each round the formula shrinks, the vocabulary evolves, and $d=1$ finds new simplifications on the easier formula.

The ecosystem logicspace.aws.monce.ai npdollars.aws.monce.ai ─────────────────────────── ────────────────────────── The library The cloud solver Pure Python, runs anywhere Lambda spot, scales to 10K workers DualTree + PolySAT + vivification Recursive self-invoke, depth escalation O(n·m) per compile Cheapest first (d=1), escalate on stall CNF in, simplified CNF out UF250 100/100 in 3.7s avg, $0.0015 EC2 t3.small: fixed $15/month Lambda: pay per invocation always on, serves /cnf /paper /api elastic, 0 cost at rest Both are needed: — LogicSpace provides the polynomial compilation (the brain) — npdollars provides the distributed infrastructure (the muscle) — The library runs on a laptop, a Lambda, an EC2, a Raspberry Pi — The solver runs on 10,000 concurrent Lambdas for $0.20

The npdollars paper documents the production results: SATLIB UF250 100/100 solved in 3.7s average, 18 workers at $d=1$, $0.0015 per solve. The economics page breaks down the cost model: $0.00003 per round at $d=1$, sub-dollar for any 250-variable instance. The depth dial is the dollar dial.

© 2026 Charles Dana. LogicSpace is open research.
ORCIDGitHubAPInpdollars