A SAT solver, traditionally, is a function formula → assignment
(or ⊥). That signature is lossy in both directions. The input throws
away structure (DIMACS forgets how the formula was generated). The
output throws away structure (one witness, no description of the
solution set, no explanation of unsatisfiability).
.ncnf fixes both ends with one move: input and output live in the same grammar. A solver is a total function
The output is never a naked assignment and never a naked ⊥. It is an .ncnf file describing all models — as ground units if the model is unique, as residual templates if the solution set is parametric, as a refutation taboo if the problem is UNSAT, as a strictly-reduced .ncnf if the budget ran out. Solutions compose because solutions and problems are the same object.
A 9×9 Sudoku in DIMACS is p cnf 729 11988. A 100×100
Sudoku is tens of millions of clauses. But the generator in both
cases is ten lines of code. DIMACS discards every bit of that structure
the moment it grounds. Modern solvers then spend their life
re-discovering symmetry they were never told about.
Symmetry breakers (Shatter, BreakID), lifted input languages (Essence, MiniZinc, ASP), lazy grounding — all of them are trying to claw back, post hoc, the structure that DIMACS threw away. .ncnf is the file format that does not throw it away.
A SAT instance is not a list of clauses. It is a program that emits clauses under an index.
∀ i, j: …
is the symmetry group. No recovery pass needed.
p ncnf name=sudoku
param n=9
var x[i:1..n, j:1..n, v:1..n]
# polynomial skeleton (axioms)
∀ i,j: ∨_v x[i,j,v] # a cell has a value
∀ i,j, v<w: ¬x[i,j,v] ∨ ¬x[i,j,w] # at most one
∀ i,v, j<k: ¬x[i,j,v] ∨ ¬x[i,k,v] # row
∀ j,v, i<k: ¬x[i,j,v] ∨ ¬x[k,j,v] # col
∀ br,bc,v, (i1,j1)<(i2,j2) ∈ box(br,bc):
¬x[i1,j1,v] ∨ ¬x[i2,j2,v] # box
# taboos (learned / asserted)
taboo naked_pair:
∀ i, j<k, v<w:
pair_in_cells(i, {j,k}, {v,w})
⇒ ∀ l ∉ {j,k}: ¬x[i,l,v] ∧ ¬x[i,l,w]
# givens
unit x[1,1,5] x[1,2,3] …
Three regions: param, var / skeleton, taboo / unit. A skeleton entry is a quantified clause template. A taboo is a quantified learned clause — a conditional exclusion pattern. Units are ground assertions.
p ncnf name=php
param n
var p[i:1..n+1, j:1..n]
∀ i: ∨_j p[i,j] # every pigeon lands
∀ j, i<k: ¬p[i,j] ∨ ¬p[k,j] # hole is exclusive
taboo counting:
|{ i : ∃j p[i,j] }| = n+1 ∧ |{ j }| = n
⇒ ⊥
Three lines. The taboo is the pigeonhole principle. Classical resolution needs 2Ω(n) ground steps. A lifted solver fires the counting taboo once. The cost of UNSAT drops from exponential to constant.
Closure property. A solver is a transformation from .ncnf to .ncnf. The input describes the problem; the output describes the solution set. Never a single assignment, never just a verdict. Solutions compose because inputs and outputs share the same grammar.
Three solution-shapes, all expressed in the same format:
| Verdict | Output .ncnf |
|---|---|
| SAT (unique) | original skeleton +
unit for every ground literal. No free templates left. |
| SAT (family) | original skeleton + residual templates over the still-free indices. The solution set is parametric and the .ncnf is its parameterization. |
| UNSAT | original + the refutation taboo that fired. For PHP(n, n+1) this is the one-line counting taboo. The proof is .ncnf. |
| REDUCED | original + all taboos learned + all units proven. Strictly easier than the input. Feedable to the next solver pass or to the ground swarm. |
Completion is a peeling loop that accumulates the output .ncnf and shrinks the residual. Each step is a bounded CNF computation.
solve(ncnf F, budget B, taboo library L) → ncnf F':
F' ← empty .ncnf with the same (params, vars) as F.
residual ← F.
1. PARSE F → (params, vars, skeleton S, taboos T, units U).
2. INSTANTIATE for chosen n: S, T stay lifted. U is ground.
3. FOR each region R in S ∪ T, by ascending cost:
a. MATCH find occurrences of R’s LHS in residual.
b. REWRITE apply R’s RHS — lifted BCP. Accumulate any forced
ground literals as new unit entries in F'.
c. DISCHARGE emit a DIMACS fragment for R under the current
partial model and call Kissat with an atom budget.
— forced partial witness : append its units to F'.
— region has a free parameter : append the residual
template (with narrowed index) to F'. No enumeration.
— UNSAT locally : a taboo fires — append the
taboo to F' and re-enter step 3.
d. UPDATE residual ← residual minus the part of R that was
discharged. Residual is monotonically shrinking.
4. GROUND what remains of residual — the sub-formula not
covered by any matched template or taboo — to DIMACS.
5. SWARM hand the ground residual to /cnf/solve:
kissat direct → v6 race → v7 Lambda swarm.
The swarm returns a model or a refutation.
6. LIFT BACK translate the swarm’s ground result into .ncnf form:
— SAT model → unit entries in F'.
— UNSAT certificate → refutation taboo in F'.
7. INDUCE scan F' for patterns the inducer can generalize
(repeated learned clauses → a single lifted taboo).
Append to the taboo library L for future instances.
8. RETURN F'.
Every step is accumulative, not enumerative. Step 3c never
has to all-SAT a region — it either forces units, keeps the
region symbolic, or fires a taboo. The only place enumeration happens
is step 5, which is exactly where npdollars already pays dollars. The
output F' is always valid .ncnf: you can feed it back into
solve and get a fixpoint.
.ncnf moves the polynomial/exponential frontier as far right as the taboo library permits. Everything left of the frontier is compiled into the output .ncnf for free.
| Step | Work | Charged in |
|---|---|---|
| PARSE + INSTANTIATE | O(|F|) | milliseconds |
| MATCH (per region) | O(narity(R)) | polynomial in n |
| REWRITE (lifted BCP) | O(size of match set) | polynomial |
| CNF-CHECK per region | O(atom budget) | Kissat seconds |
| GROUND residual | O(|residual|) | linear in uncovered part |
| SWARM residual | — exponential — | dollars (npdollars) |
| TABOO induction | O(|new-clause| × |L|) | amortized, one-shot |
The library L is capital. Each dollar spent on a
novel instance pays for a taboo that is free for every future instance
at every size. PHP(10, 11) is paid once; PHP(100, 101), PHP(1000, 1001)
are catpiss ever after.
Classical CDCL branching is exponential in the worst case because it splits on variables. .ncnf splits on structure:
residual = full formula
while residual is non-empty:
find the template region R ∈ (S ∪ T) that matches the largest
sub-formula of residual at the lowest cost.
if match found:
discharge R (lifted BCP or CNF-check)
residual ← residual − matched
else:
break # genuinely novel structure
swarm(residual)
This is structural divide-and-conquer. Every peel is polynomial. The swarm only ever touches what the library cannot yet explain. The dictionary of LogicSpace is the ground analogue; .ncnf is the lifted analogue.
.ncnf grammar (params, vars, quantified clauses,
taboos, units) — one page of EBNF..ncnf + n → DIMACS. Dogfoods the format
against current /cnf/solve.POST /ncnf/solve: parse → peel → /cnf/solve on
residual → induce taboos → persist.Want to state “the sum of these N binary variables equals target” as a SAT instance? In DIMACS that’s a ripple adder chain: ~7 · N · ⌈log₂(N+1)⌉ clauses, scaling to ~140 million at N=1M. In .ncnf it’s five templates plus a few unit clauses for the target — constant size, independent of N.
| N | .ncnf description | Ground DIMACS (clauses) | Compression | Kissat |
|---|---|---|---|---|
| 100 | ~810 B | 5 207 | 6× | 2 ms SAT |
| 1 000 | ~900 B | 73 010 | 78× | 18 ms SAT |
| 10 000 | ~1 020 B | 1 010 014 | 960× | 258 ms SAT |
| 100 000 | ~1 110 B | 12 200 017 | ~10 700× | 65 s TIMEOUT |
| 1 000 000 | ~1.2 KB | 140 000 000 | ~117 000× | unreasonable to ground |
p ncnf name=adder_sum param N=1000000 param B=20 var b[i:1..N] var s[i:0..N, k:0..B-1] var c[i:1..N, k:0..B] ∀ k : ¬s[0,k] # init s ∀ i : b[i] ↔ c[i,0] # init c ∀ i, k : s[i,k] = s[i-1,k] XOR c[i,k] # sum ∀ i, k : c[i,k+1] = s[i-1,k] AND c[i,k] # carry ∀ i : ¬c[i,B] # no overflow unit s[N,*] = target-in-binary # the goal
A SAT instance over 140 million ground clauses fits in 1.2 KB of .ncnf. It is the same instance — grounding recovers every clause exactly. The scaling laws (measured, EC2, April 2026):
POST /ncnf/adder builds the instance live:
curl -X POST https://npdollars.aws.monce.ai/ncnf/adder \
-H "Content-Type: application/json" \
-d '{"N": 10000, "target": 3333, "solve": true}'
→ {"result": "SAT", "kissat_ms": ~260,
"description.syntactic_bytes_estimate": ~1020,
"compression_vs_dimacs": ~18000 }
That is the principle of .ncnf in one example: the description stays tiny while the ground formula stays exact. SAT solvers have always paid per clause; .ncnf lets you pay per quantifier.
Every flat DIMACS can be lifted into a .ncnf given a
schema (variable index layout) and an instance size. The lift is
polynomial: O(n_clauses × max_arity × orbit_size),
with both arity and orbit size bounded by user-supplied caps.
POST /ncnf/lift
{"dimacs": "p cnf 42 133\n1 2 3 4 5 6 0\n…",
"schema": [{"name":"p","domains":[[1,7],[1,6]]}], # optional
"instance_size": {"n": 6}, # optional hint
"max_arity": 4,
"budget_ms": 5000,
"verify_round_trip": true}
|
v
ncnf.lift.cnf_to_ncnf
1. parse DIMACS
2. pick schema (given, or infer by factoring n_vars with hint)
3. decode each clause -> (var, idx, sign) tuples
4. shape-cluster (arity, (var,sign)*, offsets)
5. for each shape:
full orbit present? -> emit template Clause (∀-quantified)
partial orbit? -> emit ground clauses as a taboo
7. round-trip verifier re-grounds the lifted .ncnf and checks the
clause set matches the input exactly.
Soundness. A clause template is emitted only when every
quantifier-valid instantiation of the shape is already present in the
input, so the lifted form grounds back to the exact same clause set.
Partial orbits become origin:"lifted:partial" taboos —
ground clauses kept as-is, still monotone under the closure property.
Measured lift times (single-threaded Python, MacBook M):
| Problem | n_vars / n_cl | Lift (ms) | Templates | Partial taboos | Cov. | Round-trip |
|---|---|---|---|---|---|---|
| PHP(5,6) | 30 / 81 | 2.5 | 6 | 0 | 100% | ✓ |
| PHP(8,9) | 72 / 297 | 9.4 | 9 | 0 | 100% | ✓ |
| PHP(20,21) | 420 / 4 221 | 163 | — | — | 100% | ✓ |
| Ordering GT(5) | 25 / 85 | 5.0 | 2 | 46 | 2% | ✓ |
| Mutilated(2n=4) | 20 / 56 | 15.5 | 4 | 26 | 11% | ✓ |
| Tseitin grid 3×3 | 12 / 32 | 10.6 | 16 | 10 | 50% | ✓ |
| Coloring (20, 40, 3) | 60 / 200 | 82 | 3 | 23 | 40% | ✓ |
| Sudoku 4×4 | 64 / 400 | 63 | 9 | 4 | 56% | ✓ |
All seven round-trip exactly — grounding the lifted .ncnf reproduces the input clause set byte-for-byte. Pure ∀ problems (PHP) lift to 100% templates; mixed problems with index conditions (GT’s i < j, Sudoku’s box cells) split between templates and ground taboos, but round-trip is preserved.
Running ncnf.solve on seven instances with kissat 4.0.4
as the ground oracle. Baseline = median kissat on the raw
skeleton. Enriched = median kissat on the same skeleton after
appending the taboos we induced from one prior UNSAT proof. Transfer
= the enriched run on a larger instance of the same family that
shares the taboo structure.
| Problem | n vars / clauses | Baseline | Enriched | Self× | Induced | Transfer |
|---|---|---|---|---|---|---|
| PHP(10, 11) → PHP(12, 13) UNSAT | 110 / 561 | 1220 ms | 1.9 ms | 642× | 1 | 31 583× (60 s TIMEOUT → 1.9 ms) |
| Sudoku 9×9 SAT | 729 / 11 745 | 2.5 ms | 2.6 ms | 0.96× | 0 | — |
| Pythagorean(200) → (400) SAT | 200 / 254 | 1.7 ms | 1.7 ms | 1.00× | 0 | 1.06× |
| Graph coloring (120 / 360 / k=3) UNSAT | 360 / 1 560 | 7.9 ms | 1.8 ms | 4.4× | 2 | 5.0× |
| Mutilated chessboard 10×10 → 12×12 UNSAT | 176 / 572 | 27.5 ms | 2.0 ms | 13.8× | 17 | 224× |
| Tseitin on 6×6 grid → 7×7 UNSAT | 60 / 200 | 3.9 ms | 1.7 ms | 2.3× | 7 | 9.2× |
| Ordering principle GT(12) → GT(15) UNSAT | 144 / 1 464 | 5.7 ms | 2.3 ms | 2.5× | 58 | 4.3× |
MacBook M-series, kissat 4.0.4 Python subprocess, median of 3 runs.
Full reproducer at ncnf/bench/run_big.py in the repo.
POST /ncnf/solve {"problem": "php", "params": {"n": 8}}
returns the live envelope.
Reading the numbers honestly:
problem → solution-set, in the same grammar.
Charles Dana · Monce SAS · npdollars.aws.monce.ai/ncnf · April 2026
Co-Authored-By: Claude (Anthropic)