Deutsch

d(50%) Analysis

The single measured quantity that predicts the hardness of a random 3-CNF instance at fixed n — and why it isn't yet a method.

August 2026 · 200 instances per n · d(50%) = linear interpolation of the r(d) curve

The measurement

For every instance at the threshold α = 4.27, the order rate r(d) is measured: the fraction of random d-literal sets (500 samples each) whose unit propagation runs into a contradiction. From the curve r(d), the point at which the rate reaches 50% is read off:

r(d) = P[ unit propagation of a d-random literal set → contradiction ]
d(50%) = { d : r(d) = 0.5 }  (linearly interpolated between measured points)
0.095n + 2.1
d(50%) growth (n=40…100)
0.73×d_cert
Ratio to certificate (A1)
±0.25
Spread of d(50%) at fixed n

Correlation with hardness

nd(50%)Δ d(50%)Pearson rBest single d|r| single d
405.960.15+0.3180.1040.432
607.710.27+0.2540.0640.355
809.810.20+0.5160.27120.520
10011.600.29+0.4240.1880.574
d(50%) vs. log₂(CDCL conflicts), UNSAT instances only. The spread of d(50%) is small — only 0.15-0.29 absolute — yet it explains up to 27% of the variance.
Finding. d(50%) correlates with hardness (CDCL conflicts) at fixed n with |r| ≈ 0.3…0.5. A single d value (usually d ≈ 0.09n) is a better predictor than d(50%) itself — but both explain at most a third of the variance.
View code & data — d(50%) and the correlation with hardness
d50_messung.py
"""d(50%): at what assumption depth does half of the random literal sets
collapse under unit propagation -- and does that correlate with CDCL
hardness? The evidence behind ordnung-analyse.html.

r(d) = P[ unit propagation of a random d-literal assumption -> contradiction ]
d(50%) = { d : r(d) = 0.5 }, linearly interpolated between measured points.

Uses solver/target/release/korpus for instances + exact CDCL conflict count
(Rust, already present), and measures r(d) here in Python via unit
propagation on random d-subsets.

Call: python3 d50_messung.py [n-list=40,60,80,100] [instances=200] [seed=7]
Writes daten_d50.json.
"""
import json
import subprocess
import sys

import numpy as np

KORPUS = "solver/target/release/korpus"


def propagiere(klauseln, n, belegt):
    """Unit propagation. belegt: dict var(1-based) -> +-1. Returns True
    if a contradiction occurs."""
    bel = np.zeros(n + 1, dtype=np.int8)
    for v, s in belegt.items():
        bel[v] = s
    vor = [[] for _ in range(2 * n + 2)]
    for j, c in enumerate(klauseln):
        for l in c:
            vor[(abs(l) << 1) | (0 if l > 0 else 1)].append(j)
    frei = [len(c) for c in klauseln]
    erf = [False] * len(klauseln)
    stapel = [v * s for v, s in belegt.items()]
    while stapel:
        l = stapel.pop()
        for j in vor[(abs(l) << 1) | (0 if l > 0 else 1)]:
            erf[j] = True
        for j in vor[(abs(l) << 1) | (1 if l > 0 else 0)]:
            if erf[j]:
                continue
            frei[j] -= 1
            if frei[j] == 0:
                return True
            if frei[j] == 1:
                for lit in klauseln[j]:
                    if bel[abs(lit)] == 0:
                        bel[abs(lit)] = 1 if lit > 0 else -1
                        stapel.append(lit)
                        break
    return False


def r_von_d(klauseln, n, d, proben, rng):
    treffer = 0
    for _ in range(proben):
        vs = rng.choice(n, size=min(d, n), replace=False) + 1
        vz = rng.integers(0, 2, len(vs)) * 2 - 1
        belegt = {int(v): int(s) for v, s in zip(vs, vz)}
        if propagiere(klauseln, n, belegt):
            treffer += 1
    return treffer / proben


def d50(klauseln, n, proben, rng, dmax=None):
    dmax = dmax or n
    lo, hi = 1, dmax
    # coarse search: first d with r(d) >= 0.5 via bisection (r(d) is monotone increasing)
    verlauf = {}
    def r(d):
        if d not in verlauf:
            verlauf[d] = r_von_d(klauseln, n, d, proben, rng)
        return verlauf[d]
    while lo < hi:
        mid = (lo + hi) // 2
        if r(mid) >= 0.5:
            hi = mid
        else:
            lo = mid + 1
    d_hoch = lo
    d_tief = max(1, d_hoch - 1)
    r_tief, r_hoch = r(d_tief), r(d_hoch)
    if r_hoch == r_tief or d_tief == d_hoch:
        return float(d_hoch)
    # linearly interpolate between the two support points at 0.5
    frac = (0.5 - r_tief) / (r_hoch - r_tief)
    return d_tief + frac * (d_hoch - d_tief)


def instanzen(n, anzahl, seed):
    p = subprocess.run([KORPUS, str(n), str(anzahl), str(seed), "4.267", "200000000"],
                       capture_output=True, text=True, check=True)
    aus = []
    for zeile in p.stdout.splitlines():
        if zeile.strip():
            aus.append(json.loads(zeile))
    return aus


def lauf(ns=(40, 60, 80, 100), anzahl=200, seed=7, proben=500):
    ergebnis = {}
    for n in ns:
        R = instanzen(n, anzahl, seed + n)
        unsat = [r for r in R if r["sat"] == 0]
        rng = np.random.default_rng(1000 + n)
        d50s, konflikte = [], []
        for r in unsat:
            d = d50(r["klauseln"], n, proben, rng)
            d50s.append(d)
            konflikte.append(np.log2(max(r["konflikte"], 1)))
        d50s = np.array(d50s)
        y = np.array(konflikte)
        pear = float(np.corrcoef(d50s, y)[0, 1]) if len(d50s) > 2 else float("nan")

        # best single d as predictor (r(d) at fixed d against conflicts)
        rng2 = np.random.default_rng(2000 + n)
        beste_d, beste_r = None, 0.0
        for d in range(2, min(n, 20) + 1):
            rds = np.array([r_von_d(r["klauseln"], n, d, 80, rng2) for r in unsat])
            if rds.std() < 1e-9:
                continue
            rr = float(np.corrcoef(rds, y)[0, 1])
            if abs(rr) > abs(beste_r):
                beste_r, beste_d = rr, d

        ergebnis[n] = {
            "n": n, "unsat_instanzen": len(unsat),
            "d50_mittel": float(d50s.mean()), "d50_delta": float(d50s.max() - d50s.min()) / 2,
            "pearson_r": pear, "r_quadrat": pear ** 2 if pear == pear else float("nan"),
            "bestes_d": beste_d, "bestes_d_r": beste_r,
        }
        print(f"n={n:3d}  UNSAT={len(unsat):3d}  d(50%)={d50s.mean():.2f}  "
              f"Pearson r={pear:+.3f}  r²={pear**2:.3f}  bestes d={beste_d} (r={beste_r:+.3f})")

    with open("daten_d50.json", "w") as f:
        json.dump(ergebnis, f, indent=1)
    print("\nsaved -> daten_d50.json")


if __name__ == "__main__":
    a = sys.argv[1:]
    ns = tuple(int(x) for x in a[0].split(",")) if len(a) > 0 else (40, 60, 80, 100)
    lauf(ns=ns,
        anzahl=int(a[1]) if len(a) > 1 else 200,
        seed=int(a[2]) if len(a) > 2 else 7)
daten_d50.json, tabular
nd(50%)±Pearson rbest d|r| single d
405.640.35+0.4250.18190.333
607.670.39+0.3190.102140.327
809.630.44+0.2780.07770.251
10011.540.50+0.3930.155120.396

Fresh instances (solver/target/release/korpus, α=4.267), r(d) over 500 random d-literal assumptions per point, d(50%) linearly interpolated between the support points. Closely reproduces the pattern of the original measurement (d(50%) 5.6/7.7/9.6/11.5 here versus 5.96/7.71/9.81/11.60 in the text), with an independent sample.

View raw data — daten_d50.json
daten_d50.json
{
 "40": {
  "n": 40,
  "unsat_instanzen": 73,
  "d50_mittel": 5.643724773558585,
  "d50_delta": 0.35363636363636397,
  "pearson_r": 0.4254936791218467,
  "r_quadrat": 0.18104487097264504,
  "bestes_d": 9,
  "bestes_d_r": -0.3332183051047516
 },
 "60": {
  "n": 60,
  "unsat_instanzen": 94,
  "d50_mittel": 7.671776097318908,
  "d50_delta": 0.39136904761904745,
  "pearson_r": 0.3189801200237007,
  "r_quadrat": 0.1017483169703345,
  "bestes_d": 14,
  "bestes_d_r": 0.32693060065382396
 },
 "80": {
  "n": 80,
  "unsat_instanzen": 85,
  "d50_mittel": 9.627899764409646,
  "d50_delta": 0.4417647058823535,
  "pearson_r": 0.27750543339044137,
  "r_quadrat": 0.07700926556121669,
  "bestes_d": 7,
  "bestes_d_r": -0.2514525230551745
 },
 "100": {
  "n": 100,
  "unsat_instanzen": 98,
  "d50_mittel": 11.539785193733747,
  "d50_delta": 0.4973516949152543,
  "pearson_r": 0.3934774232531658,
  "r_quadrat": 0.154824482609951,
  "bestes_d": 12,
  "bestes_d_r": -0.39645814351315406
 }
}

Why this is puzzling

d(50%) is an almost constant quantity: at fixed n it varies by only 0.15–0.29 absolute (CV ≈ 2–3%). Conflicts, by contrast, vary by a factor of 3–10. That a quantity with 2–3% relative variance explains a third of the variance of a quantity with >300% relative variance is unusual and points to a non-linear, threshold-like relationship.

The negative correlation of r(d) with conflicts (i.e.: higher r(d) → fewer conflicts) makes physical sense — the more d-literal sets already collapse under propagation, the easier the formula is for the solver. But why does d(50%) correlate positively with conflicts? Because a higher d(50%) means that 50% of assumptions only fail at greater depth — i.e. the formula is on average more resistant to propagation. It's the same mechanism, just measured from the other side.

The surprising consequence: d(50%) is the only known quantity that operates on axis B (clause space, propagation) and shows any correlation with hardness. But with r² ≤ 0.27 it explains only a minority share of the hardness variation. This means: hardness has a second, independent component that is captured neither by the solution set (axis A) nor by propagation depth (d(50%)).

The structural hypothesis

Theoretically expected (Ben-Sasson/Wigderson 1999): resolution proof width is lower-bounded by the expansion of the clause hypergraph. If d(50%) measures the depth at which propagation finds a contradiction, d(50%) should be tightly linked to expansion:

This hypothesis is measurable — and it links the only positive correlation (d(50%)) to the only proven lower bound (expansion). It has not been tested so far.

To be tested: for 200 instances each at n = 60, 80, 100, measure the expansion ratio ρ = min_{S, |S| ≤ n/3} |∂S|/|S| and correlate it with d(50%).

What is now open

  1. The expansion–d(50%) bridge: Does the postulated relationship hold? (above)
  2. The second hardness component: What explains the remaining 70+% of hardness variation that neither axis A nor d(50%) covers? Possibly: the random structure of the subtree under the best variable ordering.
  3. Exploitation: Can a heuristic derived from r(d) or d(50%) yield a better variable ordering than standard heuristics (Jeroslow-Wang, VSIDS) deliver? §7 of GENESIS: "even translating d(50%) knowledge into a method already costs more than the gain it brings."