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

Computation 195 -- Maximum entropy supplies non-degeneracy: under the symmetry package
the maximum-entropy law on T's contingent content has a density, both exceptional sets
are null under it, and the one thing the argument needs is that P3's typicality clause
reach the content of T and not only the configuration
=========================================================================================
THE ITEM.  comp 193 left the structural front with one condition, the non-degeneracy of
T's contingent content (t, v): the magnitude t must avoid the tie set, closed of
codimension one in the admissible cone, and the directions v must avoid the balance
set, closed of codimension seven in (S^6)^|D|.  Both faces fail only on closed
nowhere-dense sets and both fail with positive probability only under DISCRETE content.
sec:non-balance-status says what would close the condition: "an argument about
correlation rather than about direction", since comp 152's all-parallel law mu_pm is
invariant under everything the postulates assert (relabelling of D, per-property
complementation, the diagonal G_2 action) and still balances 31% of the time at
|D| = 6.  comp 193 said the same thing from the other side: one argument, that P3's
maximum-entropy sublimation instantiates non-degenerate content, would close both faces.

THE ARGUMENT, which is a theorem of probability and not of PST.  Fix the invariant
reference measure nu on the content space: the uniform product on (S^6)^|D| for the
directions, Lebesgue measure on the admissible cone (with a scale constraint, since the
cone is unbounded) for the magnitudes.  The Gibbs variational principle says that for
every law P the relative entropy D(P || nu) is non-negative with equality only at
P = nu, and that under any finite set of moment constraints the maximum-entropy law is
an exponential family with a density against nu.  So the maximum-entropy law on the
content, with or without constraints of that kind, is ABSOLUTELY CONTINUOUS, and every
closed nowhere-dense set, the tie set and the balance set among them, is null under
it.  mu_pm is on the other side of the same principle: it is supported on a
seven-dimensional subset of a 6|D|-dimensional space, so its relative entropy is
+infinity, and it is the FARTHEST law from maximum entropy that the symmetry package
admits, not a competitor to it.  The symmetry package does not force non-degeneracy
(comp 152); the symmetry package plus maximum entropy does.

WHAT THE ARGUMENT NEEDS, stated exactly.  P3's clause as the book states it reads:
"among the substrate configurations that cross the threshold, the realised one carries
no unforced density structure beyond what T determines", a maximum-entropy selection
of the CONFIGURATION, consistent with the tension data, that supplies A3.  The content
(t, v) of T is that tension data: the input to the selection, not its output.  So the
argument above closes the condition if and only if the typicality clause is read as
reaching the contingent content of T as well as the configuration.  The book does not
make that reading on its own; this file records that it is the one thing missing, that
it is a reading of P3 and not a new postulate, and that under it the condition would
have exactly the standing A3 has.

CHECKS.
  1.  Directions.  A one-parameter family inside the symmetry package: v(a) drawn from
      the von Mises-Fisher law of concentration kappa about +-u, u uniform, signs
      i.i.d. fair (the signs comp 178 showed complementation forces).  Its entropy,
      in closed form from the Bessel functions, decreases strictly in kappa, with its
      maximum at kappa = 0, the uniform product.  Exact balance has probability zero at
      every finite kappa (a steep power law, slope between 6 and 7 at r = 0.5 to 0.75 on a
      million draws at kappa = 0 and 4, approaching the codimension 7 as r -> 0, which
      comp 193 measured at 6.7 on four million draws), and the binomial
      mass C(|D|, |D|/2) 2^-|D| only at kappa = infinity, which is mu_pm.
  2.  Magnitudes.  The maximum-entropy law on the admissible cone with a scale
      constraint has exponential increments along inclusion (Gibbs); 200 draws on three
      graphs carry only the forced atom.  A lattice of increments with L levels is the
      discrete tamper: its tie fraction falls as L grows and is 1 at L = 1, which is T_ex.
  3.  The reference measure is load-bearing: with the fourteen octonion units as the
      only directions, comp 152's quantised scope, the maximum-entropy law is uniform on
      a finite set and balances with probability 1/14 at |D| = 2 and positively at every
      even |D|.  The continuum of directions P1 supplies is what makes the exceptional
      set null.
  4.  The two faces under one law: a joint maximum-entropy draw of (t, v) satisfies both
      faces on every one of 500 draws, with the distance to degeneracy of comp 193
      positive each time.
A "pass" is all four.
"""
import itertools
import math
import numpy as np
from scipy.special import iv, gammaln


# --- directions: von Mises-Fisher on S^6 --------------------------------------------
P = 7                                  # ambient dimension of V_7
LOG_AREA_S6 = math.log(16 * math.pi ** 3 / 15)

def vmf_entropy(kappa):
    """differential entropy of the vMF law on S^6 against the surface measure, exact."""
    if kappa == 0.0:
        return LOG_AREA_S6
    nu = P / 2 - 1                                                  # 5/2
    log_C = nu * math.log(kappa) - (P / 2) * math.log(2 * math.pi) - math.log(iv(nu, kappa))
    A = iv(P / 2, kappa) / iv(nu, kappa)                            # E[u . x]
    return -log_C - kappa * A

def sample_vmf(kappa, mu, n, rng):
    """Wood's algorithm for vMF on S^{P-1}; kappa = 0 gives the uniform law."""
    if kappa == 0.0:
        v = rng.normal(size=(n, P)); return v / np.linalg.norm(v, axis=1, keepdims=True)
    b = (-2 * kappa + math.sqrt(4 * kappa ** 2 + (P - 1) ** 2)) / (P - 1)
    x0 = (1 - b) / (1 + b)
    c = kappa * x0 + (P - 1) * math.log(1 - x0 ** 2)
    out = np.empty((n, P)); k = 0
    while k < n:
        m = n - k
        z = rng.beta((P - 1) / 2, (P - 1) / 2, size=m)
        w = (1 - (1 + b) * z) / (1 - (1 - b) * z)
        u = rng.uniform(size=m)
        acc = kappa * w + (P - 1) * np.log(1 - x0 * w) - c >= np.log(u)
        w = w[acc]
        if w.size == 0:
            continue
        t = rng.normal(size=(w.size, P)); t -= np.outer(t @ mu, mu); t /= np.linalg.norm(t, axis=1, keepdims=True)
        v = np.sqrt(1 - w[:, None] ** 2) * t + w[:, None] * mu
        take = min(v.shape[0], n - k); out[k:k + take] = v[:take]; k += take
    return out

def draw_family(kappa, n, draws, rng):
    """draws clouds of n directions: v(a) ~ vMF(kappa, eps_a u), u uniform, eps fair."""
    out = np.empty((draws, n, P))
    for d in range(draws):
        u = rng.normal(size=P); u /= np.linalg.norm(u)
        eps = rng.choice([-1.0, 1.0], size=n)
        if np.isinf(kappa):
            out[d] = eps[:, None] * u
        else:
            vs = sample_vmf(kappa, u, n, rng)
            out[d] = eps[:, None] * vs
    return out


def part1():
    print("\n  (1) DIRECTIONS: entropy is maximal at the uniform product, and balance is null at every finite kappa")
    rng = np.random.default_rng(195)
    kappas = [0.0, 0.5, 1.0, 2.0, 4.0, 8.0, 16.0]
    H = [vmf_entropy(k) for k in kappas]
    mono = all(H[i] > H[i + 1] for i in range(len(H) - 1))
    print("      kappa:   " + "".join(f"{k:9.1f}" for k in kappas))
    print("      entropy: " + "".join(f"{h:9.4f}" for h in H) + f"   (log area S^6 = {LOG_AREA_S6:.4f}; strictly decreasing: {mono})")
    n = 8; ok = mono
    print(f"      {'kappa':>8} {'exact balance':>14} {'P(|sum|<0.5)':>13} {'P(|sum|<0.75)':>14} {'slope':>7}")
    for k in (0.0, 4.0, np.inf):
        s = np.concatenate([np.linalg.norm(draw_family(k, n, 200000, rng).sum(axis=1), axis=1) for _ in range(5 if not np.isinf(k) else 1)])
        exact = (s < 1e-9).mean()
        p1, p2 = (s < 0.5).mean(), (s < 0.75).mean()
        slope = math.log(p2 / p1) / math.log(1.5) if p1 > 0 else float('nan')
        if np.isinf(k):
            ok &= abs(exact - math.comb(n, n // 2) / 2 ** n) < 0.01
        else:
            ok &= exact == 0.0 and slope > 5.5
        print(f"      {str(k):>8} {exact:14.4f} {p1:13.6f} {p2:13.6f} {slope:7.2f}")
    print(f"      binomial at kappa = inf is {math.comb(n, n // 2) / 2 ** n:.4f}; mu_pm is the kappa = inf point, relative entropy +inf")
    return ok


# --- magnitudes: exponential increments on the admissible cone -----------------------
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 maxent_t(n, indep, rng, levels=None):
    """t = 0 on independent sets; elsewhere the maximum over C minus one property plus an
    increment: exponential (the maximum-entropy increment under a mean constraint) or,
    for the tamper, uniform on a lattice of `levels` values."""
    N = 1 << n; t = np.zeros(N)
    for C in range(N):
        if indep[C]:
            continue
        best = max(t[C & ~(1 << a)] for a in range(n) if C >> a & 1)
        inc = rng.exponential(1.0) if levels is None else rng.integers(1, levels + 1) / levels
        t[C] = best + inc
    return t

def unforced_atom(t, indep):
    vals, counts = np.unique(np.round(t[~indep], 12), return_counts=True)
    return counts.max() / t.size

def part2():
    print("\n  (2) MAGNITUDES: exponential increments carry only the forced atom; a lattice of increments ties")
    rng = np.random.default_rng(1950)
    ok = True
    for n, edges in ((6, [(i, (i + 1) % 6) for i in range(6)]), (8, [e for e in itertools.combinations(range(8), 2) if rng.random() < 0.35]), (8, list(itertools.combinations(range(4), 2)))):
        bt = bits(n); indep = independent_mask(n, edges, bt)
        worst = max(unforced_atom(maxent_t(n, indep, rng), indep) for _ in range(200))
        ok &= worst <= 1 / (1 << n) + 1e-15
        print(f"      |D| = {n}, {len(edges):2d} distinctions: largest unforced atom over 200 maximum-entropy draws = {worst:.4f} (one configuration is {1/(1<<n):.4f})")
    n = 8; edges = [(i, (i + 1) % 8) for i in range(8)]; bt = bits(n); indep = independent_mask(n, edges, bt)
    print(f"      {'levels L':>9} {'mean tie mass':>14}")
    prev = None
    for L in (1, 2, 4, 16, 256):
        m = float(np.mean([unforced_atom(maxent_t(n, indep, rng, levels=L), indep) for _ in range(50)]))
        ok &= (prev is None or m <= prev + 1e-12) and (L > 1 or m > 0.2)
        prev = m
        print(f"      {L:9d} {m:14.4f}" + ("   (L = 1 is T_ex: every increment 1, the edge count)" if L == 1 else ""))
    return ok


def part3():
    print("\n  (3) THE REFERENCE MEASURE IS LOAD-BEARING: quantised directions balance under maximum entropy")
    rng = np.random.default_rng(1951)
    units = np.vstack([np.eye(P), -np.eye(P)])                    # the fourteen octonion imaginary units
    ok = True
    print(f"      {'|D|':>4} {'balance, uniform on 14 units':>29} {'exact':>8}")
    for n in (2, 4, 6):
        idx = rng.integers(0, 14, size=(400000, n))
        s = units[idx].sum(axis=1)
        frac = (np.abs(s).sum(axis=1) < 1e-9).mean()
        exact = 1 / 14 if n == 2 else None
        if exact is not None:
            ok &= abs(frac - exact) < 0.005
        ok &= frac > 0
        print(f"      {n:4d} {frac:29.4f} {('1/14' if n == 2 else '> 0'):>8}")
    print("      maximum entropy on a finite set is uniform on it and has no density: the continuum P1 supplies does the work")
    return ok


def part4():
    print("\n  (4) THE TWO FACES UNDER ONE MAXIMUM-ENTROPY LAW: 500 joint draws")
    rng = np.random.default_rng(1952)
    n = 8; edges = [(i, (i + 1) % 8) for i in range(8)]; bt = bits(n); indep = independent_mask(n, edges, bt)
    dmin = np.inf; ok = True
    for _ in range(500):
        t = maxent_t(n, indep, rng); v = sample_vmf(0.0, None, n, rng)
        s = np.sort(t[~indep]); dt = np.min(np.diff(s)) / math.sqrt(2)
        dv = float(np.linalg.norm(v.sum(axis=0)) / math.sqrt(n))
        d = min(dt, dv); dmin = min(dmin, d); ok &= d > 0
    print(f"      smallest distance to degeneracy over 500 draws: {dmin:.3e} (> 0 on every draw: {ok})")
    return ok


def main():
    print("=" * 100)
    print("  Computation 195 -- Maximum entropy supplies non-degeneracy, if P3's typicality reaches the content of T")
    print("=" * 100)
    checks = [
        ("directions: entropy maximal at the uniform product, balance null at every finite kappa, binomial only at mu_pm", part1()),
        ("magnitudes: exponential increments carry only the forced atom, lattice increments tie", part2()),
        ("quantised directions balance under maximum entropy: the continuum reference measure is load-bearing", part3()),
        ("both faces hold on every joint maximum-entropy draw", part4()),
    ]
    print("\n" + "=" * 100)
    for name, ok in checks:
        print(f"  {'ok  ' if ok else 'FAIL'}  {name}")
    print("=" * 100)
    print("  Under maximum entropy relative to the invariant reference measure the content of T has a density,")
    print("  so the tie set and the balance set are null and both faces of the condition hold almost surely.")
    print("  mu_pm is the farthest law from maximum entropy the package admits.  What the argument needs is")
    print("  that P3's typicality clause reach the content of T and not only the configuration: a reading of")
    print("  P3, not a new postulate, and the book has not made it.")
    print("  PASS" if all(ok for _, ok in checks) else "  FAIL")

main()
