.ncnf manifesto

Indexed CNF. Polynomial skeleton + taboos.

Charles Dana · Monce SAS · April 2026

Problem → Solution set.
Same grammar, both sides of the arrow.

0. The arrow

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

problem.ncnf  →  solution-set.ncnf

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.

1. Why DIMACS is not enough

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.

2. What .ncnf brings to the table

  1. Compression. Sudoku 100×100 in ~1 KB. PHP(n, n+1) in three lines. Any n. Deterministic expansion.
  2. Symmetry for free. The template ∀ i, j: … is the symmetry group. No recovery pass needed.
  3. Lifted BCP. One template propagation step absorbs O(nk) ground steps. Same work, nk leverage.
  4. Exponentially shorter UNSAT proofs. PHP(n, n+1) is 2Ω(n) in ground resolution (Haken 1985). In .ncnf it is one counting taboo. The proof fits on a napkin.
  5. Transfer across sizes. A lifted clause learned on n=9 replays on n=100 without retraining. Ground clauses can never transfer — they reference specific literals. Template clauses reference positions, which exist at every size.
  6. Human-readable reasoning. Naked pairs, X-wing, swordfish: these are not heuristics. They are lifted learned clauses that humans discovered by hand. .ncnf lets a solver output them in the same language.

3. The file format (sketch)

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.

4. PHP(n, n+1) — the catpiss case

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.

5. Completion — .ncnf → .ncnf

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.

solve : .ncnf × budget → .ncnf

Three solution-shapes, all expressed in the same format:

VerdictOutput .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.
UNSAToriginal + the refutation taboo that fired. For PHP(n, n+1) this is the one-line counting taboo. The proof is .ncnf.
REDUCEDoriginal + 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.

6. Complexity budget

StepWorkCharged in
PARSE + INSTANTIATEO(|F|)milliseconds
MATCH (per region)O(narity(R))polynomial in n
REWRITE (lifted BCP)O(size of match set)polynomial
CNF-CHECK per regionO(atom budget)Kissat seconds
GROUND residualO(|residual|)linear in uncovered part
SWARM residual— exponential —dollars (npdollars)
TABOO inductionO(|new-clause| × |L|)amortized, one-shot
time(solve) = poly(n, |L|)   +   $(residual)

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.

7. Divide and conquer via pattern recognition

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.

8. Connection to the monce stack

9. What we commit to building

  1. .ncnf grammar (params, vars, quantified clauses, taboos, units) — one page of EBNF.
  2. Grounder: .ncnf + n → DIMACS. Dogfoods the format against current /cnf/solve.
  3. Seed corpus: Sudoku 9×9, 16×16, 25×25; PHP(n, n+1) for n∈{4, 8, 16}; graph coloring k-COL.
  4. Template matcher: a lifted unifier between formula fragments and taboo LHS.
  5. Taboo library: a versioned, append-only artifact stored next to the session on S3. Every solve exports the taboos it found.
  6. POST /ncnf/solve: parse → peel → /cnf/solve on residual → induce taboos → persist.

9*. The flex — sum of a million bits in 1.2 KB

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 descriptionGround DIMACS (clauses)CompressionKissat
100~810 B5 2072 ms SAT
1 000~900 B73 01078×18 ms SAT
10 000~1 020 B1 010 014960×258 ms SAT
100 000~1 110 B12 200 017~10 700×65 s TIMEOUT
1 000 000~1.2 KB140 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):

|vars|(N) ∼ N1.12    r²=1.000
|clauses|(N) ∼ N1.13    r²=1.000
kissat(N) ∼ N1.20    r²=0.932
|.ncnf| ∼ O(1) — constant

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.

9a. cnf → ncnf — the inverse arrow

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):

Problemn_vars / n_clLift (ms)TemplatesPartial taboosCov.Round-trip
PHP(5,6)30 / 812.560100%
PHP(8,9)72 / 2979.490100%
PHP(20,21)420 / 4 221163100%
Ordering GT(5)25 / 855.02462%
Mutilated(2n=4)20 / 5615.542611%
Tseitin grid 3×312 / 3210.6161050%
Coloring (20, 40, 3)60 / 2008232340%
Sudoku 4×464 / 400639456%

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.

9b. Benchmark — seven problems, real numbers

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.

Problemn vars / clausesBaselineEnrichedSelf×InducedTransfer
PHP(10, 11) → PHP(12, 13) UNSAT 110 / 5611220 ms1.9 ms642× 131 583× (60 s TIMEOUT → 1.9 ms)
Sudoku 9×9 SAT 729 / 11 7452.5 ms2.6 ms0.96× 0
Pythagorean(200) → (400) SAT 200 / 2541.7 ms1.7 ms1.00× 01.06×
Graph coloring (120 / 360 / k=3) UNSAT 360 / 1 5607.9 ms1.8 ms4.4× 25.0×
Mutilated chessboard 10×10 → 12×12 UNSAT 176 / 57227.5 ms2.0 ms13.8× 17224×
Tseitin on 6×6 grid → 7×7 UNSAT 60 / 2003.9 ms1.7 ms2.3× 79.2×
Ordering principle GT(12) → GT(15) UNSAT 144 / 1 4645.7 ms2.3 ms2.5× 584.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:

10. The claim

DIMACS is the assembly language of SAT.
.ncnf is the first source language that a solver can read — and the first solution format it can write.

problem → solution-set, in the same grammar.

Charles Dana · Monce SAS · npdollars.aws.monce.ai/ncnf · April 2026
Co-Authored-By: Claude (Anthropic)