Deutsch

The Judo Throw

So far every path has pointed toward more structure. Randomness takes structure away — so try the reverse: push structurelessness to the extreme and see whether something else appears.

August 25/26, 2026 · 3,506 instances, 76,501 + 3,551 candidates, calibrated against permutation

The move is not new — and that is the good news

Reversal has won in several fields. Ramsey theory: “complete disorder is impossible”[1] — push a coloring toward randomness and structure emerges regardless. Szemerédi's regularity lemma: every graph decomposes into a bounded number of pieces that look random. Erdős's probabilistic method: construct objects by showing that a random one will do. Concentration of measure: in high dimension randomness turns rigid again.

But the case that matters here happened on exactly this subject. Mézard, Parisi and Zecchina did not search for structure in the formula — they asked what the disorder looks like[2]. Answer: the solution space shatters at αd ≈ 3.86 into exponentially many clusters[3], long before the threshold at αs ≈ 4.267[4]. This anti-structure was the structure, and Survey Propagation used it to solve random 3-SAT with a million variables near the threshold, when nothing else could.

And the caveat was already fixed before the run began. Survey Propagation is an aggregate over the assignment space — marginal distributions with a joker state. By the two-axis separation, such aggregates cannot tell SAT from UNSAT, and correspondingly SP is one-sided: it finds solutions, it never proves unsatisfiability. The handbook measures the same thing from the other side: cavity/SP separates with AUC 0.86 but does not predict hardness (r ≤ 0.18).

So the judo throw is permitted — but it inherits a known half-blindness. That is no reason not to make it. It is the reason to give it a stopping criterion beforehand instead of a hope.

Why the disorder moves into the instrument, not the instance

All the examples above put the randomness in the instance. Here it moves into the measuring instrument instead — and the instrument was already standing ready: SAT in phase space measures exactly this flow, its fractal basin boundaries, and the relation α = κ/λ. There it was understood on one instance; here the question is whether it knows something about hardness across many instances. A process is run on the formula, and what is measured is how disorderly it behaves. That follows a pattern this project has already measured twice on its own:

Observation. Every dimension that has ever carried weight in complexity theory — treewidth, circuit depth, VC dimension, bond dimension, proof width — is a property of a process on the object, never an intrinsic coordinate of the object.

The same thing happened twice in this repo: the blunt coordinate — the formula as its own location — died; the front of the current-counting, a process quantity, lives. These are not two independent findings but one pattern confirming itself.

The instrument panel

Six probes, sharing the decisive property: none of them calls a solver. That is a condition, not a coincidence — in the handbook, every good method separates SAT from UNSAT only because it asks satisfiability questions, and is thereby circular.

ProbeWhat it measures
A  ChaosThe continuous dynamics of Ercsey-Ravasz/Toroczkai[5] (phase space). On UNSAT it provably has no attractor — what is measured is the transient, with a fixed budget instead of running to a solution.
B  SpectrumEigenvalue spacing ratio[6]. Poisson 0.3863 (ordered) versus Wigner-Dyson 0.5307 (chaotic)[7] — the canonical order/chaos probe of random matrix theory.
C  LocalizationParticipation ratio of the clause weights: does the dynamics concentrate the blame on a few clauses or smear it out?
D  OverlapThe q-distribution of spin glasses, from truncated random walks — budget 12n, while a solution needs a median of 1,990 flips. It must never decide.
E  CompressionDisorder as incompressibility, split by scaffold and sign.
F  PercolationResponse of unit propagation to a perturbation — see below.

The interim finding: don't hold a parameter fixed

The first version of probe F set one variable and measured the avalanche of unit propagation. Result: always exactly 1, with no spread at all. That was not a bug in the code but a finding — at α = 4.267, random 3-SAT sits far below the percolation threshold of unit propagation. Starting from one variable, nothing runs.

Instead of holding the parameter fixed, it was swept. And then something appears:

1 3 5 4.93 at ρ = 0.16 .02 .10 .16 .24 .34 .46 Seed fraction ρ Gain
Swept variables per seed variable, against the fraction of randomly set variables. A sharp transition at ρ ≈ 0.16. Location and sharpness of this transition are process quantities of the instance, not counts.

How to keep the search from fooling itself

This is the actual substance of the run. Measure 76,501 candidates and report the best one, and you are reporting the maximum of a sample — and that is clearly nonzero even when every single candidate was pure noise.

Three barriers against confounders

What is measured is the partial rank correlation against log2(conflicts), with {n, σdeg2, sat} regressed out. Degree spread carries 42 % of the hardness spread in this project; regressing out sat is the sharpest barrier, because that UNSAT is more expensive is already known — rediscovering only that is finding nothing.

The permutation null over the whole chain

The same search was run eight times, complete, on shuffled labels. The threshold for a finding is the maximum of those, not zero.

StageMeanMaximum
Stage 1  (76,501 candidates, lern only)0.0940.117
Stage 2  (best 600 on pruef)0.0720.086
greedy  (4 terms, forward selection)0.1010.130
View code & data — the permutation null, 76 501 candidates
judo/sieb.py — rank correlation, confounder projection, greedy combination
def korr(v, H, zr):
    """|Rank correlation| of v with the already residualized target zr."""
    r = B.rang(v)
    r = r - H @ r
    s = r.std()
    if s < 1e-12:
        return 0.0
    return abs(float(np.dot(r, zr) / (len(r) * s * zr.std())))


# ------------------------------------------------------------ Search space

def paare(Q, hoechstens=None, rng=None):
    ij = [(i, j) for i in range(Q) for j in range(i + 1, Q)]
    if hoechstens and len(ij) > hoechstens:
        rng = rng or np.random.default_rng(0)
        pick = rng.choice(len(ij), hoechstens, replace=False)
        ij = [ij[t] for t in pick]
    return ij


def verbinde(X, i, j, art):
    a, b = X[:, i], X[:, j]
    if art == "quot":
        n = np.abs(b)
        return a / np.where(n < 1e-9, 1e-9, np.where(b < 0, -n, n))
    if art == "prod":
        return a * b
    if art == "diff":                     # standardized difference
        za = (a - a.mean()) / (a.std() + 1e-12)
        zb = (b - b.mean()) / (b.std() + 1e-12)
        return za - zb
    raise ValueError(art)


ARTEN = ("quot", "prod", "diff")


def stufe1(X, H, zr, ij, arten=ARTEN, mitsingles=True):
    """Rank all candidates on `lern`. Returns list (score, description)."""
    aus = []
    if mitsingles:
        for i in range(X.shape[1]):
            aus.append((korr(X[:, i], H, zr), ("einzeln", i, -1)))
    for (i, j) in ij:
        for art in arten:
            aus.append((korr(verbinde(X, i, j, art), H, zr), (art, i, j)))
    return aus


def baue(X, bez):
    art, i, j = bez
    return X[:, i] if art == "einzeln" else verbinde(X, i, j, art)


# ------------------------------------------------------------------ Run

def sammle_bank(substrate):
    with Pool(min(22, os.cpu_count() or 4)) as p:
        reihen = p.map(BK.bank, substrate, chunksize=16)
    namen = sorted(set().union(*[set(r) for r in reihen]))
    X = np.array([[r.get(k, 0.0) for k in namen] for r in reihen], np.float64)
    X[~np.isfinite(X)] = 0.0
    return namen, X


def z(v):
    s = v.std()
    return (v - v.mean()) / s if s > 1e-12 else np.zeros_like(v)


def gierig(Xl, Hl, zrl, Xp, Hp, zrp, bezuege, tiefe=4):
    """Forward selection: standardized sums of several expressions.

    Rank correlation is blind to monotone transformations, so a single
    expression can no longer be improved -- gains only come from FURTHER
    terms. A term is taken only if it raises min(lern,pruef): a term that
    only helps on lern is overfitting and is not taken.
    """
    if not bezuege:
        return []
    vl = {b: z(baue(Xl, b)) for b in bezuege}
    vp = {b: z(baue(Xp, b)) for b in bezuege}
    start = bezuege[0]
    kette, sl, sp = [start], vl[start].copy(), vp[start].copy()
    bestwert = min(korr(sl, Hl, zrl), korr(sp, Hp, zrp))
    zeichen = [1.0]
    spur = [(bestwert, tuple(kette), tuple(zeichen))]
    for _ in range(tiefe - 1):
        bester, bwert, bvz = None, bestwert, 1.0
        for b in bezuege:
            if b in kette:
                continue
            for vz in (1.0, -1.0):
                w = min(korr(sl + vz * vl[b], Hl, zrl),
                        korr(sp + vz * vp[b], Hp, zrp))
                if w > bwert + 1e-4:
                    bester, bwert, bvz = b, w, vz
        if bester is None:
            break
        kette.append(bester)
        zeichen.append(bvz)
        sl += bvz * vl[bester]
        sp += bvz * vp[bester]
        bestwert = bwert
        spur.append((bestwert, tuple(kette), tuple(zeichen)))
    return spur
judo/daten/sieb.json — 8 permutations, all candidates recomputed
Threshold 0.13010.07320.09830.12640.10450.13060.09270.09780.091Run · value of the greedy stage under shuffled labels

Each point: the same search over 76,501 candidates, but the hardness label shuffled within each cell. The maximum of eight runs is the threshold — not zero.

View raw data — judo/daten/sieb.json (threshold + null values)
judo/daten/sieb.json (threshold + null values)
{
 "schwelle": 0.1301589930874898,
 "s1schwelle": 0.11654299343656545,
 "nullwerte": [
  0.07307985475037881,
  0.09803691914137302,
  0.12620280753198415,
  0.10369664996358074,
  0.1301589930874898,
  0.09224358597393721,
  0.09736080529329685,
  0.09094165030533212
 ],
 "kandidaten": 76501,
 "bank": 226
}
Notable: the greedy stage drives the null, not the pair search. The reason is built in — forward selection maximizes min(lern, pruef) directly, so pruef is no longer an independent sample for it, but a target quantity. Had the greedy stage not been priced in, the threshold would sit at 0.086, about fifteen percent too low.

The strict test

The pooled value overestimates: n has four levels and sat two, and what rank-linear regression removes leaves a nonlinear dependency standing. So every candidate is additionally recomputed within each fixed (n, sat) cell, where neither one can reach through. Also reported is in how many of the eight cells the sign is the same — a real feature points the same direction everywhere, an artifact flips.

Checked that the apparatus works

On synthetic data with a planted signal, before the actual run:

best candidateon testthresholdverdict
signal planted0.8750.8970.131found, and recognized as a ratio
no signal0.1260.0170.133correctly rejected

The second row is the more important one: on pure noise the search reaches 0.126 — and the threshold catches it.

The finding

Thirty candidates lay above the threshold. The strict test leaves one of them standing as the best:

The expression. cSchuld.entropie × cTeil.teilnahme — the entropy of the clause weights times the participation ratio of the trajectories. Strictly measured r = 0.259, same sign in all eight cells. Both building blocks are localization measures of the chaotic dynamics: how strongly the process concentrates the blame on a few clauses.
featurestrictrange (8 cells)sign
cSchuld.entropie alone−0.043[−0.332, +0.314]4/8
cTeil.teilnahme alone+0.192[−0.018, +0.465]7/8
product of the two+0.259[+0.117, +0.481]8/8

The product carries more than its parts. The entropy alone is worthless and flips sign in half the cells; the participation ratio alone sits at 0.192. The information does not sit in either coordinate but in their combination — exactly what a search over ratios and products is built for.

What turned out to be an artifact

The larger part of the thirty was confounder recombination, and the strict test found it:

building blockr with nr with sat
kGeruest (compression of the scaffold)+0.938−0.022
kUeberhang−0.937+0.020
cSaettigung (saturation of the cube)−0.086+0.762
kGeruest is a pure n-proxy, cSaettigung a pure sat-proxy. Their difference looks like 0.305 when pooled and drops to −0.031 in the strict test, with the sign flipping. That is exactly the deception the test was built against.

More terms buy nothing real

The greedy stage combines several expressions into a signed sum. The result is the most instructive part of the run:

termspooled teststrictcells matching
10.332+0.2598/8
20.392+0.2487/8
30.388+0.2607/8
40.407+0.2366/8
50.405+0.2486/8
View code & data — why more terms buy nothing real
judo/streng_je_term.py — the same chain, with a persisted cell-wise control
"""The strict (cell-wise) metric for every stage of the greedy combination,
persisted -- previously computed only in a throwaway script, never
saved anywhere. That contradicts the project's own rule: every measured
claim on the page must lead to a file that can be viewed and rechecked.

Reads sieb.json (field "gierig", the identifiers of the individual terms)
and the bank afresh, rebuilds every stage as a signed sum, and computes
at every stage the control from HANDBUCH.md/JUDO.md: partial rank
correlation *within* each fixed (n, sat) cell, averaged, with a count of
the cells that point in the same sign.
"""
import json, pickle, sys, os
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import bewerten as B
import sieb as SB

HIER = os.path.dirname(os.path.abspath(__file__))
DATEN = os.path.join(HIER, "daten")


def streng(v, nn, sat, gv, y, mindest=60):
    ws, ns = [], []
    for x in sorted(set(nn)):
        for s_ in (0.0, 1.0):
            m = (nn == x) & (sat == s_)
            if m.sum() < mindest:
                continue
            ws.append(B.partiell(v[m], y[m], [gv[m]]))
            ns.append(int(m.sum()))
    if not ws:
        return 0.0, 0, 0
    ws, ns = np.array(ws), np.array(ns)
    mit = float(np.average(ws, weights=ns))
    return mit, int((np.sign(ws) == np.sign(mit)).sum()), len(ws)


import lauf as L

_TEIL_CACHE = {}
def _bauen(X, R, menge):
    if not _TEIL_CACHE:
        substrate = pickle.load(open(os.path.join(DATEN, "substrat.pkl"), "rb"))
        satz, subs, idx = L.teile(R, substrate)
        P = B.Pruefstand(satz)
        for k in ("lern", "pruef", "test"):
            Xk = X[idx[k]]
            H = SB.hut(P.stoer[k], len(Xk))
            zr0 = B.rang(P.ziel[k])
            _TEIL_CACHE[k] = (Xk, H, zr0 - H @ zr0)
    return _TEIL_CACHE[menge]


def lauf():
    d = np.load(os.path.join(DATEN, "bank.npz"), allow_pickle=True)
    X, namen = d["X"], list(d["namen"])
    ni = {n: i for i, n in enumerate(namen)}
    R = pickle.load(open(os.path.join(DATEN, "korpus.pkl"), "rb"))
    sb = json.load(open(os.path.join(DATEN, "sieb.json")))

    nn = np.array([r["n"] for r in R], float)
    sat = np.array([r["sat"] for r in R], float)
    gv = np.array([r["gradvar"] for r in R], float)
    y = np.log2(np.maximum([r["konflikte"] for r in R], 1)).astype(float)    # IMPORTANT: sieb.json stores "gierig" only with tiefe=4 and the pool of
    # the best 40 stage-2 candidates (internal call in sieb.py::eine_suche).
    # But the table on judo.html has 5 stages from a wider pool --
    # all 30 candidates from sb["ergebnis"] instead of just the best 40 from
    # stage 2 -- and tiefe=5. To reproduce the exact numbers shown, the same
    # (wider) pool and the same depth must be used here.
    bezuege = [(e["art"], ni[e["a"]], ni[e["b"]]) for e in sb["ergebnis"]]
    Xl_, Hl_, zrl_ = _bauen(X, R, "lern")
    Xp_, Hp_, zrp_ = _bauen(X, R, "pruef")
    Xt_, Ht_, zrt_ = _bauen(X, R, "test")
    spur = SB.gierig(Xl_, Hl_, zrl_, Xp_, Hp_, zrp_, bezuege, tiefe=5)

    aus = []
    for wert, kette, zeichen in spur:
        sl = sum(v * SB.z(SB.baue(Xl_, b)) for v, b in zip(zeichen, kette))
        sp = sum(v * SB.z(SB.baue(Xp_, b)) for v, b in zip(zeichen, kette))
        st = sum(v * SB.z(SB.baue(Xt_, b)) for v, b in zip(zeichen, kette))
        rl, rp, rt = SB.korr(sl, Hl_, zrl_), SB.korr(sp, Hp_, zrp_), SB.korr(st, Ht_, zrt_)
        ganz = sum(v * SB.z(SB.baue(X, b)) for v, b in zip(zeichen, kette))
        sg, gl, zz = streng(ganz, nn, sat, gv, y)
        txt = " ".join(("+" if v > 0 else "-") +
                       (namen[i] if a == "einzeln" else f"({namen[i]} {a} {namen[j]})")
                       for v, (a, i, j) in zip(zeichen, kette))
        aus.append({"terme": len(kette), "lern": rl, "pruef": rp, "test": rt,
                    "streng": sg, "zellen_gleich": gl, "zellen": zz, "ausdruck": txt})    print(f"  {len(kette)} terms: lern {rl:.3f} pruef {rp:.3f} test {rt:.3f}  "
              f"streng {sg:+.3f} {gl}/{zz}")

    with open(os.path.join(DATEN, "streng_je_term.json"), "w") as f:
        json.dump(aus, f, indent=1)    print(f"\\nsaved -> {os.path.join(DATEN, 'streng_je_term.json')}")


if __name__ == "__main__":
    lauf()
judo/daten/streng_je_term.json, tabulated
termslernpruefteststrictcells
10.3120.3100.332+0.2598/8
20.3450.3550.392+0.2487/8
30.3580.3770.388+0.2607/8
40.3780.3900.407+0.2366/8
50.3790.3930.405+0.2486/8
judo/daten/streng_je_term.json — 5 stages of forward selection
123450.00.10.20.30.4Number of terms in the combination
strict (the real metric)lernprueftest (pooled)

The three pooled curves rise with every term. The red one — the only one computed against n, degree spread and sat, and cell-wise — stays flat and even falls. That is the overfitting, not the finding.

View raw data — judo/daten/streng_je_term.json
judo/daten/streng_je_term.json
[
 {
  "terme": 1,
  "lern": 0.3121949716325861,
  "pruef": 0.30971683757420543,
  "test": 0.3324133787235236,
  "streng": 0.25930672858491144,
  "zellen_gleich": 8,
  "zellen": 8,
  "ausdruck": "+(cSchuld.entropie prod cTeil.teilnahme)"
 },
 {
  "terme": 2,
  "lern": 0.3446945783258858,
  "pruef": 0.3547529368331009,
  "test": 0.3919760659956465,
  "streng": 0.24807174490592482,
  "zellen_gleich": 7,
  "zellen": 8,
  "ausdruck": "+(cSchuld.entropie prod cTeil.teilnahme) -(cBogen.median diff cSchuldStreu.median)"
 },
 {
  "terme": 3,
  "lern": 0.35832565991185183,
  "pruef": 0.37746030194027413,
  "test": 0.38782528426719703,
  "streng": 0.26004803841288204,
  "zellen_gleich": 7,
  "zellen": 8,
  "ausdruck": "+(cSchuld.entropie prod cTeil.teilnahme) -(cBogen.median diff cSchuldStreu.median) -(cTeil.streu diff sL.teilnahme)"
 },
 {
  "terme": 4,
  "lern": 0.3776439765673545,
  "pruef": 0.389714897199669,
  "test": 0.407362902209816,
  "streng": 0.23569579486940836,
  "zellen_gleich": 6,
  "zellen": 8,
  "ausdruck": "+(cSchuld.entropie prod cTeil.teilnahme) -(cBogen.median diff cSchuldStreu.median) -(cTeil.streu diff sL.teilnahme) +(cK.teilnahme quot kUeberhang)"
 },
 {
  "terme": 5,
  "lern": 0.3794966148490839,
  "pruef": 0.39318464523725827,
  "test": 0.404828812015732,
  "streng": 0.24841559524734894,
  "zellen_gleich": 6,
  "zellen": 8,
  "ausdruck": "+(cSchuld.entropie prod cTeil.teilnahme) -(cBogen.median diff cSchuldStreu.median) -(cTeil.streu diff sL.teilnahme) +(cK.teilnahme quot kUeberhang) -(cSchuld.streu quot cSpurEnde.teilnahme)"
 }
]
The pooled number rises from 0.33 to 0.41, the strict one sits at 0.25, and the cell consistency falls from 8/8 to 6/8. The extra terms buy no hardness information, only residual structure of the confounders — clearly visible in the fourth term, which contains kUeberhang, the n-proxy with r = 0.94.

Anyone who had optimized for the pooled number here would have reported a four-term expression at 0.41 that knows less than the one-term expression at 0.33. Pooled and strict measures diverge under greedy search, and only the strict one is reliable.

The injection on the iGPU

The original plan was to run a local language model as a mutation operator. Measured: the Radeon 890M has 512 MiB of its own memory, everything beyond that runs over GTT and thus over the same DDR5 bus as the CPU. No bandwidth advantage, 12 to 15 characters per second — independent of model size, a 4B model was as slow as a 35B one.

So it was rebuilt: the breadth comes from the sieve (all pairs, exhaustively), the model gets the part that suits it — one line per suggestion instead of a code block, and only combinations of three or more names, whose space is too large to enumerate. In three hours: 3,551 candidates, 3,514 of them after deduplication.

Best expression from the injection. cSchuldStreu.schiefe × wE.rate_frueh × sLAbst.median / cTeil.streu — strict 0.283, 8/8. Four building blocks from three different instruments: blame-spread skewness and participation spread (chaos), early decay rate of the walk energy (truncated random walks), median of the Laplacian eigenvalue spacings (spectrum).

The injection's own permutation null — the same 3,514 expressions against a label shuffled within the cells — sits at 0.067, with zero false hits across four runs. The strict statistic with cell consistency is a markedly sharper instrument than the pooled one.

Putting it in context, without rounding up

Against the handbook, Part IV: the solver-free approaches recorded there sit at r ≤ 0.19; d(50%) reaches 0.56 and needs a solver. The finding thus exceeds everything solver-free so far — with the caveat that it has not been checked whether the table's numbers were computed against the same confounders.

A practically usable hardness estimator, 0.28 is not. It explains roughly eight percent of the hardness spread.

What the judo throw has shown: the disorder of a process on the formula knows something about hardness that no aggregate over the solution space knows. Little — but measurable, solver-free, and pointing the same direction in all eight cells.

The fresh test

One question remained open, and it was the most uncomfortable one. The injection was steered over 497 rounds by a leaderboard ranked by exactly the quantity it is ultimately evaluated on — and that quantity ran over all instances. So there was no held-out set that would have broken the feedback loop.

The permutation test does not answer this. It shuffles the label and recomputes a fixed set of candidates; it thereby prices “is the value of this expression real”, not “did the steering drive it up”. The latter can only be answered with instances that did not exist during the search.

So: 900 fresh instances, different seeds, the same substrate, the same expressions — and no more tuning. Three sizes n instead of four, hence six cells instead of eight.

expressionfreshcellsbefore
sieve, best pair expression+0.2666/6+0.259
injection, best+0.2386/6+0.283
injection, second-best+0.2226/6+0.273
injection, third+0.2256/6+0.272
cTeil.teilnahme alone+0.2116/6+0.192
cSchuldStreu.schiefe alone−0.1234/6
control: n-proxy+0.0093/6+0.013
control: sat-proxy−0.0373/6−0.040

Three things are established by this.

First: the sieve's finding holds. 0.259 → 0.266, same sign in all six cells. No inflation — the sieve had separated sets and a threshold from permutation, and both held up.
Second: the injection was inflated by about 0.05. 0.283 → 0.238, and the same for the next two (0.273 → 0.222, 0.272 → 0.225). The amount is remarkably uniform, roughly seventeen percent, and it is exactly the price of the feedback loop having run over all instances. So the injection only appeared to win. The best expression remains the sieve's.
Third: the controls are dead on fresh data, as they should be — the n-proxy at +0.009 and the sat-proxy at −0.037, both with flipping sign (3/6). The confounder correction is working.

Notable in passing: cSchuldStreu.schiefe, the building block present in eight of the ten best injection expressions, is alone weak and inconsistent (−0.123 at 4/6). The same pattern as with the sieve — the information sits in the combination, not the building block.

The final result

The finding, confirmed on fresh instances:

cSchuld.entropie × cTeil.teilnahme  →  r = 0.266

Solver-free, controlled against n, degree spread and satisfiability, pointing the same direction in all six cells, confirmed on 900 never-seen instances. Noise threshold of the search: 0.130.

Against the handbook, Part IV: the solver-free approaches recorded there sit at r ≤ 0.19; d(50%) reaches 0.56 and needs a solver. A practically usable hardness estimator, 0.27 is not — it explains roughly seven percent of the spread.

View code & data — the confirmation on 900 never-seen instances
judo/frischprobe.py
"""The final test: a corpus that nothing has ever seen.

WHY IT IS NEEDED. The injection was steered over 497 rounds by a
leaderboard ranked by exactly the quantity it is ultimately evaluated
on -- and that quantity ran over ALL instances. So there was no held-out
set that would have broken the feedback loop.

The permutation test does not answer this. It shuffles the label and
recomputes a FIXED set of candidates; it thereby prices "is the value of
this expression real", not "did the steering drive it up". The latter can
only be answered with instances that did not exist during the search.

So: fresh instances, a different seed, the same substrate, the same
expressions -- and no more tuning.
"""
import glob, json, os, pickle, sys, time
from multiprocessing import Pool
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import bank as BK, bewerten as B, einspritzung as E, substrat as S

HIER = os.path.dirname(os.path.abspath(__file__))

def eine(a):
    i, r = a
    return i, BK.bank(S.alles(r["klauseln"], r["n"], saat=7000 + i))

def lauf():
    R = []
    for f in sorted(glob.glob(os.path.join(HIER, "frisch", "korpus_n*.jsonl"))):
        for z in open(f):
            z = z.strip()
            if z:
                try: R.append(json.loads(z))
                except json.JSONDecodeError: pass
    for r in R:
        c = np.zeros(r["n"], np.int64)
        for k in r["klauseln"]:
            for l in k: c[abs(l) - 1] += 1
        r["gradvar"] = float(c.var())
    print(f"# {len(R)} frische Instanzen, SAT-Anteil {np.mean([r['sat'] for r in R]):.3f}",
          flush=True)

    t0 = time.time()
    aus = [None] * len(R)
    with Pool(min(22, os.cpu_count() or 4)) as p:
        for i, b in p.imap_unordered(eine, list(enumerate(R)), chunksize=4):
            aus[i] = b
    print(f"# Substrat+Bank in {(time.time()-t0)/60:.1f} min", flush=True)

    alt = np.load(os.path.join(HIER, "daten", "bank.npz"), allow_pickle=True)
    namen = list(alt["namen"])
    X = np.array([[b.get(k, 0.0) for k in namen] for b in aus], np.float64)
    X[~np.isfinite(X)] = 0.0
    ni = {n: i for i, n in enumerate(namen)}

    nn = np.array([r["n"] for r in R], float)
    sat = np.array([r["sat"] for r in R], float)
    gv = np.array([r["gradvar"] for r in R], float)
    y = np.log2(np.maximum([r["konflikte"] for r in R], 1)).astype(float)
    zellen = [m for m in ((nn == x) & (sat == s)
                          for x in sorted(set(nn)) for s in (0.0, 1.0))
              if m.sum() >= 40]
    gr = np.array([m.sum() for m in zellen], float)

    def streng(v):
        w = np.array([B.partiell(v[m], y[m], [gv[m]]) for m in zellen])
        mit = float((w * gr).sum() / gr.sum())
        return mit, int((np.sign(w) == np.sign(mit)).sum()), len(w)

    KAND = [
      ("Sieb, bester Paarausdruck",
       "cSchuld.entropie * cTeil.teilnahme", 0.259),
      ("Einspritzung, bester",
       "cSchuldStreu.schiefe * wE.rate_frueh * sLAbst.median / cTeil.streu", 0.283),
      ("Einspritzung, zweitbester",
       "cSchuldStreu.schiefe * wE.rate_frueh / cTeil.streu", 0.273),
      ("Einspritzung, dritter",
       "cSchuldStreu.schiefe * wE.rate_frueh * sLAbst.median / cTeil.vk", 0.272),
      ("nur der Hauptbaustein", "cSchuldStreu.schiefe", None),
      ("nur cTeil.teilnahme", "cTeil.teilnahme", 0.192),
      ("Artefakt zur Kontrolle (n-Stellvertreter)", "kGeruest", 0.013),
      ("Artefakt zur Kontrolle (sat-Stellvertreter)", "cSaettigung", -0.040),
    ]
    print(f"\n  {'':44s} {'frisch':>8s} {'Zellen':>7s}   {'alt':>7s}")
    for nam, ausdr, alt_ in KAND:
        try:
            v = E.werte_aus(ausdr, X, ni)
        except Exception as ex:
            print(f"  {nam:44s}  -- {ex}"); continue
        m, g, z = streng(v)
        av = f"{alt_:+.3f}" if alt_ is not None else "   -  "
        print(f"  {nam:44s} {m:+8.3f}  {g}/{z:<4d}  {av}")
    print(f"\n  (Zellen = wieviele der {len(zellen)} (n,sat)-Zellen dasselbe "
          f"Vorzeichen zeigen)")

if __name__ == "__main__":
    lauf()
judo/daten/frischprobe.log, tabulated
expressionfreshcellssieve/old result
sieve, best pair expression+0.2666/6+0.259
injection, best+0.2386/6+0.283
injection, second-best+0.2226/6+0.273
injection, third+0.2256/6+0.272
main building block only−0.1234/6
cTeil.teilnahme only+0.2116/6+0.192
control: n-proxy+0.0093/6+0.013
control: sat-proxy−0.0373/6−0.040

900 instances with fresh seeds, SAT fraction 0.509, substrate recomputed in 14.9 min. “cells” = how many of the 6 (n, sat) cells show the same sign.

View raw data — judo/daten/frischprobe.log
judo/daten/frischprobe.log
# 900 frische Instanzen, SAT-Anteil 0.509
# Substrat+Bank in 14.9 min

                                                 frisch  Zellen       alt
  Sieb, bester Paarausdruck                      +0.266  6/6     +0.259
  Einspritzung, bester                           +0.238  6/6     +0.283
  Einspritzung, zweitbester                      +0.222  6/6     +0.273
  Einspritzung, dritter                          +0.225  6/6     +0.272
  nur der Hauptbaustein                          -0.123  4/6        -  
  nur cTeil.teilnahme                            +0.211  6/6     +0.192
  Artefakt zur Kontrolle (n-Stellvertreter)      +0.009  3/6     +0.013
  Artefakt zur Kontrolle (sat-Stellvertreter)    -0.037  3/6     -0.040

  (Zellen = wieviele der 6 (n,sat)-Zellen dasselbe Vorzeichen zeigen)
What the judo throw has shown: the disorder of a process on the formula knows something about hardness that no aggregate over the solution space knows. Little — but measurable, solver-free, calibrated against permutation, and repeated on fresh instances.
And what it showed about measuring itself: three times in this run a number looked bigger than it was — pooled instead of cell-wise (0.33 versus 0.26), with more terms instead of fewer (0.41 versus 0.33), and with feedback over all the data instead of over a subset (0.28 versus 0.24). Each time the same counter-check caught it: compute it within the cells, and compute it on instances nobody has seen yet.

Sources

  1. The phrase “complete disorder is impossible” traces back to Theodore S. Motzkin and is cited as a maxim of Ramsey theory, among others in Graham, Rothschild & Spencer, Ramsey Theory, Wiley, 2nd ed. 1990. Biography (MacTutor)
  2. M. Mézard, G. Parisi, R. Zecchina: Analytic and Algorithmic Solution of Random Satisfiability Problems. Science 297, 812–815 (2002). doi:10.1126/science.1073287 · companion paper with the algorithm: M. Mézard, R. Zecchina, The random K-satisfiability problem: from an analytic solution to an efficient algorithm, Phys. Rev. E 66, 056126 (2002), arXiv:cond-mat/0207194
  3. F. Krzakała, A. Montanari, F. Ricci-Tersenghi, G. Semerjian, L. Zdeborová: Gibbs states and the set of solutions of random constraint satisfaction problems. PNAS 104, 10318–10323 (2007). doi:10.1073/pnas.0703685104 · arXiv:cond-mat/0612365
  4. S. Mertens, M. Mézard, R. Zecchina: Threshold values of random K-SAT from the cavity method. Random Structures & Algorithms 28, 340–373 (2006). arXiv:cs/0309020
  5. M. Ercsey-Ravasz, Z. Toroczkai: Optimization hardness as transient chaos in an analog approach to constraint satisfaction. Nature Physics 7, 966–970 (2011). doi:10.1038/nphys2105
  6. V. Oganesyan, D. A. Huse: Localization of interacting fermions at high temperature. Phys. Rev. B 75, 155111 (2007) — introduces the spacing ratio. doi:10.1103/PhysRevB.75.155111 · arXiv:cond-mat/0610854
  7. Y. Y. Atas, E. Bogomolny, O. Giraud, G. Roux: Distribution of the Ratio of Consecutive Level Spacings in Random Matrix Ensembles. Phys. Rev. Lett. 110, 084101 (2013) — hence the values 0.3863 (Poisson) and 0.5307 (GOE). doi:10.1103/PhysRevLett.110.084101 · arXiv:1212.5611

The measurements on this page come from judo/ in the project archive: substrat.py (probes), bank.py (feature bank), sieb.py (search and permutation null), bewerten.py (confounders), frischprobe.py (the fresh instances). The full text is included as JUDO.md.