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

Computation 175 -- colour and generation cannot be independent labels on
                   tau-perp, and the Dixon ambient does not change that
========================================================================
WHAT THIS SETTLES.  history/research/open_research.md item 3.6 was closed at v25.8 and
REOPENED at v32.64 when comp 106's block-diagonality claim was withdrawn.  The
closure said, and sec:gauge still said until this computation landed, that "PST
resolves this on the Dixon side by moving to the ambient C (x) H (x) O, where the
three generation sectors are orthogonal (comp 48)".

That inference is invalid, and this file shows it in three steps, each measured
rather than asserted.

  (1) ORTHOGONALITY IS NOT INVARIANCE.  The three Fano lines through tau-hat ARE
      mutually orthogonal, exactly: Tr(P_i P_j) = 0 to machine zero, each P is a
      genuine projector, and the ranks sum to dim(tau-perp) = 6.  comp 48's
      decomposition is real.  And colour still leaks out of them at O(1).  Both
      statements are true at once, because orthogonality of subspaces says
      nothing about whether a group preserves them.  The v25.8 closure used the
      first to claim the second.

  (2) THE OBSTRUCTION IS A THEOREM, NOT A DIMENSION COUNT.  The register states
      it as "3 (x) 3 needs real dimension >= 18 against tau-perp's 6".  True, but
      weaker than what actually holds.  The commutant of Stab_{G_2}(tau-hat) on
      tau-perp is computed here and is exactly 2-dimensional over R, i.e. C.  By
      Schur that makes tau-perp an IRREDUCIBLE complex 3-dimensional
      representation: the fundamental 3.  An irreducible representation has NO
      proper invariant subspace at all.  So there is no colour-invariant way to
      split tau-perp into generation sectors -- not merely none that anyone has
      found.  The dimension count rules out a tensor factorisation; irreducibility
      additionally rules out a direct-sum one, which is the case the Fano lines
      actually realise.

  (3) THE DIXON AMBIENT CHANGES NOTHING.  Colour acts on C (x) H (x) O only
      through the O factor, as I (x) g.  The sectors are G_k = (C (x) H) (x) L_k.
      Then

          ||(1 - I(x)P_k)(I(x)g)(I(x)P_k)|| = ||I (x) (1-P_k) g P_k||
                                            = ||(1-P_k) g P_k||

      in operator norm, so the leakage on the 64-real-dimensional ambient is
      IDENTICAL to the leakage on O alone.  Measured here to 0.00e+00 difference
      across trials.  The ambient supplies room (64 real dimensions against 8)
      and no invariance whatever.  Tensoring a slot with factors that colour does
      not act on cannot change how colour moves that slot.

WHAT SURVIVES, AND IT IS NOT NOTHING.  The COUNT three is untouched: tau-perp is
3-dimensional over C, robustly, and that is where N_gen = 3 and N_c = 3 both come
from.  What fails is reading the same three directions as two INDEPENDENT labels.
PST therefore derives one three, not two, and inherits the octonionic
programme's colour-versus-generation question rather than closing it.  That is
what sec:ngen-mechanism and the Summary now say, and what sec:gauge said the
opposite of until this file.

SCOPE.  This is a statement about tau-perp and about the Dixon ambient built over
it.  It does not say no resolution exists anywhere; it says the two specific
moves the book made -- orthogonal sectors, and the enlargement to C (x) H (x) O --
do not provide one, and that within tau-perp none can.
"""

import numpy as np
from scipy.linalg import expm

TAU = 7
TRIPLES = [(1, 2, 3), (1, 4, 5), (1, 7, 6), (2, 4, 6), (2, 5, 7), (3, 4, 7), (3, 6, 5)]


def rule():
    print("=" * 100)


def octonions():
    C = np.zeros((8, 8, 8))
    for i in range(8):
        C[0, i, i] = 1.0
        C[i, 0, i] = 1.0
    for i in range(1, 8):
        C[i, i, 0] = -1.0
    for (a, b, c) in TRIPLES:
        for (x, y, z) in [(a, b, c), (b, c, a), (c, a, b)]:
            C[x, y, z] += 1.0
            C[y, x, z] -= 1.0
    return C


C = octonions()
E = np.eye(8)


def mul(u, v):
    return np.einsum('i,j,ijk->k', u, v, C)


def stabiliser():
    """Der(O) = g_2, then the subalgebra annihilating tau-hat = su(3)."""
    rows = []
    for i in range(8):
        for j in range(8):
            blk = np.zeros((8, 64))
            eij = mul(E[i], E[j])
            for a in range(8):
                for b in range(8):
                    v = np.zeros(8)
                    v[a] = eij[b]
                    w = mul(E[a], E[j]) if b == i else np.zeros(8)
                    u = mul(E[i], E[a]) if b == j else np.zeros(8)
                    blk[:, a * 8 + b] = v - w - u
            rows.append(blk)
    _, s, Vt = np.linalg.svd(np.vstack(rows))
    der = [Vt[k].reshape(8, 8) for k in range(64) if k >= len(s) or s[k] < 1e-9]
    M = np.array([(d @ E[TAU]) for d in der]).T
    _, s2, Vt2 = np.linalg.svd(M)
    coef = [Vt2[k] for k in range(len(der)) if k >= len(s2) or s2[k] < 1e-9]
    return der, [sum(c[i] * der[i] for i in range(len(der))) for c in coef]


def fano_lines():
    return [tuple(sorted(set(t) - {TAU})) for t in TRIPLES if TAU in t]


# ---------------------------------------------------------------------------
def part1(der, stab):
    rule()
    print("  1.  THE ALGEBRA")
    rule()
    ok = len(der) == 14 and len(stab) == 8
    print(f"      dim Der(O)                = {len(der):2d}   (g_2 = 14)")
    print(f"      dim Stab_{{g_2}}(tau-hat)   = {len(stab):2d}   (su(3) = 8)")
    print(f"      -> the colour algebra is where the book says it is : {ok}")
    print()
    return ok


def part2(stab, lines):
    rule()
    print("  2.  ORTHOGONALITY IS NOT INVARIANCE")
    rule()
    P = [sum(np.outer(E[k], E[k]) for k in L) for L in lines]
    off = max(abs(np.trace(P[i] @ P[j])) for i in range(3) for j in range(3) if i != j)
    idem = max(np.linalg.norm(p @ p - p) for p in P)
    ranks = sum(int(round(np.trace(p))) for p in P)
    print(f"      the three Fano lines through tau-hat : {lines}")
    print(f"      max |Tr(P_i P_j)|, i != j            : {off:.2e}")
    print(f"      max ||P^2 - P||                      : {idem:.2e}")
    print(f"      ranks sum to                         : {ranks}  (= dim tau-perp)")
    orth = off < 1e-12 and idem < 1e-12 and ranks == 6
    print(f"      -> the sectors ARE exactly orthogonal, comp 48 stands : {orth}")
    print()
    rng = np.random.default_rng(20260929)
    worst = 0.0
    for _ in range(8):
        g = expm(sum(rng.normal() * S for S in stab))
        for p in P:
            worst = max(worst, float(np.linalg.norm((np.eye(8) - p) @ g @ p, 2)))
    print(f"      max leakage ||(1 - P) g P|| over random colour g : {worst:.3f}")
    leaks = worst > 0.1
    print(f"      -> and colour does NOT preserve them                 : {leaks}")
    print()
    print("      Both hold at once.  Orthogonality of subspaces is not invariance")
    print("      under a group.  The v25.8 closure inferred the second from the")
    print("      first, and that step is what reopened item 3.6.")
    print()
    return orth and leaks, P


def part3(stab):
    rule()
    print("  3.  SCHUR: tau-perp IS IRREDUCIBLE, SO NO GENERATION SPLITTING EXISTS")
    rule()
    idx = [1, 2, 3, 4, 5, 6]
    n = len(idx)
    R = [S[np.ix_(idx, idx)] for S in stab]
    blocks = []
    for A in R:
        B = np.zeros((n * n, n * n))
        for r in range(n):
            for c in range(n):
                Ers = np.zeros((n, n))
                Ers[r, c] = 1.0
                B[:, r * n + c] = (Ers @ A - A @ Ers).flatten()
        blocks.append(B)
    sv = np.linalg.svd(np.vstack(blocks), compute_uv=False)
    dim_comm = n * n - int((sv > 1e-9).sum())
    print(f"      dim of the real commutant of su(3) on tau-perp : {dim_comm}")
    print( "        1 -> real irreducible    2 -> COMPLEX irreducible (= C)    >2 -> reducible")
    ok = dim_comm == 2
    print(f"      -> tau-perp is the irreducible fundamental 3           : {ok}")
    print()
    print("      An irreducible representation has NO proper invariant subspace.")
    print("      So there is no colour-invariant way to split tau-perp into")
    print("      generation sectors at all: the Fano-line splitting exists as a")
    print("      decomposition of the vector space and is not preserved, which is")
    print("      exactly what part 2 measured.")
    print()
    print("      The dimension count in the register rules out a TENSOR")
    print("      factorisation: 3_colour (x) 3_gen would need 9 complex = 18 real")
    print("      dimensions against tau-perp's 3 complex = 6.  Irreducibility")
    print("      additionally rules out a DIRECT-SUM one, which is the case the")
    print("      Fano lines actually realise.  The obstruction is therefore a")
    print("      theorem, not a counting remark.")
    print()
    return ok


def part4(stab, P):
    rule()
    print("  4.  THE DIXON AMBIENT C (x) H (x) O CHANGES NOTHING")
    rule()
    CH = np.eye(8)      # C (x) H: real dim 8, colour acts trivially on it
    rng = np.random.default_rng(4242)
    print("      sectors G_k = (C (x) H) (x) L_k ; colour acts as I (x) g")
    print()
    print(f"      {'trial':>6}{'leak on O':>14}{'leak on the ambient':>22}{'difference':>14}")
    worst_diff = 0.0
    for t in range(5):
        g = expm(sum(rng.normal() * S for S in stab))
        G = np.kron(CH, g)
        a = float(np.linalg.norm((np.eye(8) - P[0]) @ g @ P[0], 2))
        Pk = np.kron(CH, P[0])
        b = float(np.linalg.norm((np.eye(64) - Pk) @ G @ Pk, 2))
        worst_diff = max(worst_diff, abs(a - b))
        print(f"      {t:>6}{a:>14.4f}{b:>22.4f}{abs(a-b):>14.1e}")
    print()
    print(f"      max difference across trials : {worst_diff:.2e}")
    ok = worst_diff < 1e-9
    print(f"      -> identical, so the ambient supplies no invariance     : {ok}")
    print()
    print("      And necessarily so:")
    print("        ||(1 - I(x)P)(I(x)g)(I(x)P)|| = ||I (x) (1-P)gP|| = ||(1-P)gP||.")
    print("      Tensoring a slot with factors colour does not act on cannot change")
    print("      how colour moves that slot.  The ambient gives ROOM, 64 real")
    print("      dimensions against 8, and no invariance whatever.")
    print()
    return ok


def part5():
    rule()
    print("  5.  WHAT SURVIVES")
    rule()
    print("      The COUNT three is untouched.  tau-perp is 3-dimensional over C,")
    print("      and that single fact is where BOTH N_c = 3 and N_gen = 3 come from.")
    print("      What fails is reading those same three directions as two")
    print("      INDEPENDENT labels.  PST derives one three, not two.")
    print()
    print("      So the honest status is: PST INHERITS the octonionic programme's")
    print("      colour-versus-generation question rather than closing it, and")
    print("      tree-level FCNC absence is not a structural theorem of PST.")
    print()
    return True


def part6(stab, P):
    rule()
    print("  6.  TAMPER TEST")
    rule()
    # The Cartan torus DOES preserve the lines.  If the leakage test could not
    # tell the two cases apart it would be measuring nothing.
    idx = [1, 2, 3, 4, 5, 6]
    blocks = []
    for Ln in [tuple(sorted(set(t) - {TAU})) for t in TRIPLES if TAU in t]:
        Pp = sum(np.outer(E[k], E[k]) for k in Ln)
        Q = np.eye(8) - Pp
        blocks.append(np.array([(Q @ S @ Pp).flatten() for S in stab]).T)
    sv = np.linalg.svd(np.vstack(blocks), compute_uv=False)
    dim_pres = len(stab) - int((sv > 1e-9).sum())
    rng = np.random.default_rng(99)
    # build a generic element of the preserving subalgebra and show it does NOT leak
    _, s, Vt = np.linalg.svd(np.vstack(blocks))
    null = [Vt[k] for k in range(len(stab)) if k >= len(s) or s[k] < 1e-9]
    X = sum(rng.normal() * sum(c[i] * stab[i] for i in range(len(stab))) for c in null)
    gC = expm(X)
    leak_cartan = max(float(np.linalg.norm((np.eye(8) - p) @ gC @ p, 2)) for p in P)
    print(f"      dim of the subalgebra preserving all three lines : {dim_pres}  (maximal torus)")
    print(f"      leakage for a generic element of THAT subalgebra  : {leak_cartan:.2e}")
    print(f"      leakage for a generic element of all of su(3)     : O(1), part 2")
    ok = dim_pres == 2 and leak_cartan < 1e-9
    print(f"      -> the test separates the two cases, so it measures something : {ok}")
    print()
    return ok


def main():
    print()
    rule()
    print("  COMPUTATION 175.  Colour and generation cannot be independent labels")
    rule()
    print("""
  Item 3.6 was closed at v25.8 on the ground that the Dixon ambient makes the
  three generation sectors orthogonal.  That is true and it does not resolve
  anything, for reasons measured below rather than asserted.
""")
    der, stab = stabiliser()
    r1 = part1(der, stab)
    r2, P = part2(stab, fano_lines())
    r3 = part3(stab)
    r4 = part4(stab, P)
    r5 = part5()
    r6 = part6(stab, P)

    rule()
    print("  ASSESSMENT")
    rule()
    print(f"  1. Der(O) = 14 and Stab(tau-hat) = 8                      : {r1}")
    print(f"  2. sectors orthogonal AND colour leaks out of them        : {r2}")
    print(f"  3. tau-perp irreducible, so no invariant splitting exists : {r3}")
    print(f"  4. the Dixon ambient leaves the leakage identical         : {r4}")
    print(f"  5. the count three survives; two independent labels do not: {r5}")
    print(f"  6. tamper test: the Cartan case is distinguished          : {r6}")
    print()
    print("""  RESULT.  Item 3.6 stays OPEN, and is now sharper than "unproven".  Within
  tau-perp a colour-invariant generation splitting is IMPOSSIBLE, by Schur: the
  representation is irreducible and has no proper invariant subspace.  The move
  to C (x) H (x) O, which the book offered as the resolution, changes the leakage
  by exactly zero, because colour acts only on the O factor.

  What survives is the count.  tau-perp is 3-dimensional over C, and that one
  fact is the source of both N_c = 3 and N_gen = 3.  PST derives ONE three.
  Reading it as two independent labels is what the octonionic programme leaves
  open, and PST inherits that rather than closing it.""")
    print()
    return all([r1, r2, r3, r4, r5, r6])


if __name__ == "__main__":
    import sys
    # Exit only on FAILURE.  The website runs these in the browser under Pyodide,
    # where SystemExit is fatal even at status 0: it tears the runtime down, and the
    # page prints ERROR after output that was in fact correct.  Falling off the end
    # hands the build gates the same status 0, and a failure still exits 1.
    if not main():
        sys.exit(1)
