#!/usr/bin/env python3
"""
PROVENANCE: PROOF

Computation 193 -- One condition, two faces: the Born condition on T's contingent content
and non-balance are both the non-degeneracy of T's contingent content, each an open
dense condition whose exceptional set is closed of positive codimension
=========================================================================================
THE ITEM.  After comp 190 the structural front carries two conditions that are not
derivations.  The first, Born agreement, is a theorem on the distinction-graph side and
on T's side asks that the contingent per-configuration content of T not tie: the law of
|T(C)| under mu must carry no atom but the forced one at zero (comp 187), which is item
1's residual freedom.  The second, non-balance, asks that the elementary directions not
cancel, sum_a v(a) != 0 (comps 152, 169, 178).  This file shows the two have the same
form, are two faces of one premise, and are logically independent of each other, so the
register can carry one condition with two faces rather than two conditions.

THE CONTINGENT CONTENT OF T, as the book states it.  An admissible T pairs a MAGNITUDE
with a DIRECTION (sec:asymmetric-tension).  The magnitude is any order-preserving
function t on the configuration lattice 2^D that vanishes exactly on the distinction-
free configurations, the independent sets of (D, delta) (eq:pos, eq:mon; comp 126): the
admissible magnitudes form a cone A of dimension 2^|D| - i(G), i(G) the number of
independent sets, with T_ex one point of it.  The direction is inherited from the
elementary directions v in (S^6)^|D| that P1 assigns.  So the contingent content is a
point (t, v) of A x (S^6)^|D|, and PST fixes no measure on either factor.

THE TWO FACES, and the one shape.
  Magnitude face (Born).  The law of |T(C)| under mu has an atom off zero iff two
  non-independent configurations tie, t_C = t_C'.  The tie set Tie is a finite union of
  hyperplanes in A, closed, of codimension 1; off it the only atom is the forced one,
  of mass i(G)/2^|D| (comp 187), and the distance to a tie is exactly
  min_{C != C'} |t_C - t_C'| / sqrt 2.
  Direction face (non-balance).  The balance set B = {sum_a v(a) = 0} is closed of
  codimension 7 in (S^6)^|D| (comp 152; 6 at |D| = 2), and the distance to it is exactly
  |sum_a v(a)| / sqrt|D|.
  So in both faces the condition is that the contingent content avoid a closed set of
  positive codimension: an open dense condition, stability rather than probability, as
  comp 152 already read non-balance.  Under any law with atomless conditionals both
  exceptional sets are null (comp 169's lemma for balance; a hyperplane is null for any
  law with a density on A).  Under DISCRETE content both have positive mass: the integer
  exemplar T_ex ties by pigeonhole, having at most |E| + 1 values on 2^|D| configurations
  (comp 187, backlog C.15), and the all-parallel law mu_pm balances with probability
  C(|D|, |D|/2) 2^-|D| (comp 178).  The parallel is exact: each face holds for continuous
  contingent content and fails for discrete content, and the theory fixes neither.

  And the faces are INDEPENDENT.  A generic t with a balanced v has no unforced atom and
  balances; T_ex with a generic v ties and does not balance.  Neither face implies the
  other, so this is one premise with two faces and not one face derived from the other.

WHAT THIS CHANGES.  Nothing is derived here and the register does not shrink by a
theorem: the front still carries five derivations, one underdetermination and one
condition.  What changes is the statement of the condition.  It is the non-degeneracy
of T's contingent content, (t, v) off a closed nowhere-dense exceptional set; its
magnitude face is item 1's own residual, which is why items 1 and 7 were one residue
counted twice; its direction face is non-balance.  What would close it is one argument,
that P3's maximum-entropy sublimation instantiates non-degenerate content, the same
kind of typicality statement that supplies A3; what would refute it is discrete content,
which P2's V_7-valued T excludes on the magnitude face (|T(C)| is real) and which the
book leaves open on the direction face (comp 152's quantised-direction scope).

CHECKS.
  1.  At |D| = 6, 8, 10 on three graphs, generic points of A (strictly increasing along
      inclusion, positive off the independent sets, drawn with continuous increments):
      largest atom of |T| under mu equals i(G)/2^|D| exactly, 200 draws each, no
      unforced atom; T_ex on the same graphs has unforced atoms, with at most |E| + 1
      distinct values.  The number of tie hyperplanes is C(2^|D| - i(G), 2).
  2.  Codimension 7 measured: for v uniform on (S^6)^|D|, the fraction with
      |sum v| < r scales as r^7 for r small against sqrt|D| (fitted exponent within 0.3
      of 7 between r = 0.45 and 1 at |D| = 8, four million draws); for
      mu_pm the balance fraction matches C(|D|, |D|/2) 2^-|D| and is 0 at odd |D|.
  3.  Independence: the 2 x 2 table (t generic or T_ex) x (v continuous or mu_pm):
      unforced atoms depend on the first factor only, balance on the second only.
  4.  The distance to degeneracy, min of the two face distances, is positive on every
      generic draw and zero on every degenerate one, and it is the quantity whose
      positivity is the condition.
A "pass" is all four.
"""
import itertools
import math
import numpy as np


# --- graphs and the configuration lattice -------------------------------------------
def graphs(n, rng):
    """three distinction graphs on n properties: a cycle, an Erdos-Renyi draw, and
    K_4 plus isolated properties (one of comp 180's failing families, reduced or not
    does not matter here: the forced atom is the subject, not its size)."""
    cyc = [(i, (i + 1) % n) for i in range(n)]
    er = [e for e in itertools.combinations(range(n), 2) if rng.random() < 0.35]
    k4 = list(itertools.combinations(range(4), 2))
    return {"cycle": cyc, "Erdos-Renyi": er, "K4+isolated": k4}

def bits(n):
    m = np.arange(1 << n)
    return np.array([(m >> a) & 1 for a in range(n)], dtype=np.int8)

def independent_mask(n, edges, bt):
    bad = np.zeros(1 << n, dtype=np.int8)
    for u, v in edges:
        bad |= bt[u] & bt[v]
    return bad == 0

def t_ex(n, edges, bt):
    out = np.zeros(1 << n)
    for u, v in edges:
        out += bt[u] & bt[v]
    return out

def generic_t(n, indep, rng):
    """a generic point of the admissible cone: t = 0 on independent sets, and on every
    other configuration C, t_C = max over C minus one property of t, plus a continuous
    positive increment.  Strictly increasing along inclusion off the independent sets,
    so eq:pos and eq:mon hold, and the increments make ties measure zero."""
    N = 1 << n
    t = np.zeros(N)
    for C in range(N):               # subsets in increasing order contain all their subsets below
        if indep[C]:
            continue
        best = 0.0
        for a in range(n):
            if C >> a & 1:
                best = max(best, t[C & ~(1 << a)])
        t[C] = best + rng.uniform(0.1, 1.0)
    return t

def largest_atom(t):
    vals, counts = np.unique(np.round(t, 12), return_counts=True)
    return counts.max() / t.size

def unforced_atom(t, indep):
    """largest atom among the values taken on non-independent configurations"""
    vals, counts = np.unique(np.round(t[~indep], 12), return_counts=True)
    return counts.max() / t.size if counts.size else 0.0


# --- directions ----------------------------------------------------------------------
def unit_dirs(n, rng, size):
    v = rng.normal(size=(size, n, 7))
    return v / np.linalg.norm(v, axis=2, keepdims=True)

def balance_distance(v):
    return np.linalg.norm(v.sum(axis=1), axis=1) / math.sqrt(v.shape[1])


# --- parts ---------------------------------------------------------------------------
def part1():
    print("\n  (1) THE MAGNITUDE FACE: generic admissible t has only the forced atom; T_ex ties by pigeonhole")
    rng = np.random.default_rng(193)
    ok = True
    print(f"      {'|D|':>4} {'graph':>13} {'i(G)/2^n':>9} {'generic: max atom':>18} {'unforced':>9} {'T_ex: max atom':>15} {'unforced':>9} {'#values':>8} {'|E|+1':>6} {'tie planes':>11}")
    for n in (6, 8, 10):
        bt = bits(n)
        for name, edges in graphs(n, rng).items():
            indep = independent_mask(n, edges, bt)
            forced = indep.sum() / (1 << n)
            atoms, unf = [], []
            for _ in range(200):
                t = generic_t(n, indep, rng)
                atoms.append(largest_atom(t)); unf.append(unforced_atom(t, indep))
            tx = t_ex(n, edges, bt)
            nvals = np.unique(tx).size
            planes = math.comb((1 << n) - int(indep.sum()), 2)
            g_ok = max(atoms) == forced and max(unf) <= 1 / (1 << n) + 1e-15
            x_ok = unforced_atom(tx, indep) > 1 / (1 << n) and nvals <= len(edges) + 1
            ok &= g_ok and x_ok
            print(f"      {n:4d} {name:>13} {forced:9.4f} {max(atoms):18.4f} {max(unf):9.4f} {largest_atom(tx):15.4f} {unforced_atom(tx, indep):9.4f} {nvals:8d} {len(edges)+1:6d} {planes:11d}")
    print(f"      generic: largest atom = i(G)/2^n and no unforced atom; T_ex: unforced atoms, at most |E|+1 values: {ok}")
    return ok

def part2():
    print("\n  (2) THE DIRECTION FACE: codimension 7 measured, and the discrete law's binomial")
    rng = np.random.default_rng(1930)
    ok = True
    n = 8
    d = np.concatenate([np.linalg.norm(unit_dirs(n, rng, 200000).sum(axis=1), axis=1) for _ in range(20)])
    rs = np.array([0.45, 0.55, 0.65, 0.8, 1.0])      # small against the typical |sum v| ~ sqrt(n), where the r^7 law holds
    frac = np.array([(d < r).mean() for r in rs])
    slope = float(np.polyfit(np.log(rs), np.log(frac), 1)[0])
    ok &= abs(slope - 7) < 0.3
    print(f"      |D| = {n}, v uniform on (S^6)^|D|, 4e6 draws: P(|sum v| < r) for r = {rs.tolist()}: {np.round(frac, 7).tolist()}")
    print(f"      fitted exponent {slope:.2f} against the codimension 7 (the law is local: at r ~ sqrt n it bends over); exact balance never occurs (min |sum v| = {d.min():.2e})")
    print(f"      {'|D|':>4} {'mu_pm balance':>14} {'binomial':>9}")
    for n in (4, 5, 6, 7, 8):
        eps = rng.choice([-1.0, 1.0], size=(200000, n))
        frac_b = (np.abs(eps.sum(axis=1)) < 0.5).mean()
        pred = math.comb(n, n // 2) / 2 ** n if n % 2 == 0 else 0.0
        ok &= abs(frac_b - pred) < 0.01
        print(f"      {n:4d} {frac_b:14.4f} {pred:9.4f}")
    return ok

def part3():
    print("\n  (3) THE FACES ARE INDEPENDENT: a 2 x 2 table")
    rng = np.random.default_rng(1931)
    n = 6
    bt = bits(n); edges = graphs(n, rng)["Erdos-Renyi"]
    indep = independent_mask(n, edges, bt)
    tg, tx = generic_t(n, indep, rng), t_ex(n, edges, bt)
    vc = unit_dirs(n, rng, 1)[0]
    u = rng.normal(size=7); u /= np.linalg.norm(u)
    vb = np.array([u, -u, u, -u, u, -u])          # mu_pm with a balanced sign vector
    rows = []
    print(f"      {'magnitude':>10} {'direction':>12} {'unforced atom':>14} {'|sum v|/sqrt n':>15}")
    for tname, t in (("generic", tg), ("T_ex", tx)):
        for vname, v in (("continuous", vc), ("mu_pm balanced", vb)):
            ua = unforced_atom(t, indep); bd = float(np.linalg.norm(v.sum(axis=0)) / math.sqrt(n))
            rows.append((tname, vname, ua, bd))
            print(f"      {tname:>10} {vname:>12} {ua:14.4f} {bd:15.4f}")
    ok = (rows[0][2] <= 1 / 64 + 1e-15 and rows[1][2] <= 1 / 64 + 1e-15 and rows[2][2] > 1 / 64 and rows[3][2] > 1 / 64
          and rows[0][3] > 0.1 and rows[2][3] > 0.1 and rows[1][3] < 1e-12 and rows[3][3] < 1e-12)
    print(f"      unforced atoms follow the magnitude column only, balance the direction column only: {ok}")
    return ok

def part4():
    print("\n  (4) THE DISTANCE TO DEGENERACY, one number for one condition")
    rng = np.random.default_rng(1932)
    n = 8
    bt = bits(n); edges = graphs(n, rng)["cycle"]
    indep = independent_mask(n, edges, bt)
    def dist(t, v):
        s = np.sort(t[~indep])
        dt = np.min(np.diff(s)) / math.sqrt(2) if s.size > 1 else np.inf
        dv = float(np.linalg.norm(v.sum(axis=0)) / math.sqrt(n))
        return dt, dv, min(dt, dv)
    ok = True
    print(f"      {'draw':>22} {'tie distance':>13} {'balance distance':>17} {'min':>10}")
    for i in range(5):
        t = generic_t(n, indep, rng); v = unit_dirs(n, rng, 1)[0]
        dt, dv, d = dist(t, v); ok &= d > 0
        print(f"      {'generic ' + str(i + 1):>22} {dt:13.3e} {dv:17.3e} {d:10.3e}")
    u = rng.normal(size=7); u /= np.linalg.norm(u)
    vb = np.array([u if i % 2 == 0 else -u for i in range(n)])
    for name, t, v in (("T_ex, continuous v", t_ex(n, edges, bt), unit_dirs(n, rng, 1)[0]),
                       ("generic t, balanced v", generic_t(n, indep, rng), vb)):
        dt, dv, d = dist(t, v); ok &= d < 1e-12
        print(f"      {name:>22} {dt:13.3e} {dv:17.3e} {d:10.3e}")
    print(f"      positive on every generic draw, zero on every degenerate one: {ok}")
    return ok


def main():
    print("=" * 100)
    print("  Computation 193 -- One condition, two faces: non-degeneracy of T's contingent content")
    print("=" * 100)
    checks = [
        ("generic magnitudes carry only the forced atom; the integer exemplar ties by pigeonhole", part1()),
        ("the balance set has codimension 7 and the discrete law balances binomially", part2()),
        ("the two faces are logically independent", part3()),
        ("the distance to degeneracy is positive exactly off the exceptional set", part4()),
    ]
    print("\n" + "=" * 100)
    for name, ok in checks:
        print(f"  {'ok  ' if ok else 'FAIL'}  {name}")
    print("=" * 100)
    print("  The Born condition on T and non-balance are the two faces of one condition, that the contingent")
    print("  content (t, v) of T is non-degenerate: off a closed set of codimension 1 in the magnitudes and")
    print("  7 in the directions.  Open and dense, null under continuous content, positive under discrete")
    print("  content, and fixed by the theory on neither face.  One premise, carried once.")
    print("  PASS" if all(ok for _, ok in checks) else "  FAIL")

main()
