#!/usr/bin/env python3
"""
Example 102 — The Nodal Flow on Numbers: Primes as Equilibria, Not Attractors
============================================================================

Runs the ACTUAL nodal dynamics ∂EPI/∂t = νf·ΔNFR on the arithmetic
network to settle the one dynamical question left open by Examples 100 and
101: does the flow make composites move TOWARD primes (the theory's §7.1
reading "primes act as sinks that attract nearby composites toward
equilibrium")? The measured answer is a precise refinement: primes are the
EQUILIBRIA of the flow (the §4 primality theorem in motion), but they are
NOT dynamical attractors — composites do not flow toward them.

Physics
-------
The nodal equation ∂EPI/∂t = νf·ΔNFR has fixed points exactly where the
forcing vanishes: νf > 0 ⟹ a node is at rest ⟺ ΔNFR = 0. Two distinct
ΔNFR's live on the arithmetic network (Example 101 kept them separate):

  • ΔNFR_arith — the per-node arithmetic pressure from (Ω, τ, σ); = 0 ⟺
    prime (the §4 theorem). It is a FIXED label, independent of EPI.
  • ΔNFR_graph — the canonical diffusion coupling (neighbour_mean − self)
    = −(L_rw·EPI); it evolves and relaxes (Example 99).

Running each flow answers the §7.1 question directly and measurably.

The measured result (all verified below, N = 140)
-------------------------------------------------
FLOW 1 (arithmetic pressure ∂EPI/∂t = νf·ΔNFR_arith):
  • mean|ΔNFR| is CONSTANT — there is no relaxation; it is a fixed forcing.
  • Every prime is FROZEN (drift = 0, exactly): primes are the zero-velocity
    rest points. This is the §4 primality theorem in motion: ΔNFR = 0 ⟺
    EPI frozen ⟺ prime.
  • Composites DRIFT, at a rate graded by Ω: r(drift, Ω) ≈ 0.93. They move
    AWAY from — not toward — the prime rest state.
  • The prime fixed points are MARGINAL (neutral): ΔNFR_arith does not
    depend on EPI, so there is no restoring force and no basin. A prime is
    a rest point for ANY EPI value.

FLOW 2 (graph diffusion ∂EPI/∂t = νf·ΔNFR_graph):
  • This flow DOES relax — to the degree-weighted uniform state (the
    conserved attractor of Example 99).
  • That attractor is the high-degree = COMPOSITE bulk. Primes are pulled
    UP toward the composites (the OPPOSITE of §7.1's "attract composites").
  • Isolated large primes (Example 101) are frozen at their seed — they do
    not participate at all.

Honest scope
------------
- This REFINES theory §7.1. The STATIC half is correct (Example 101: primes
  sit at ≈ 16× lower |Φ_s| — they are potential-field "sinks" in the
  geometric sense). The DYNAMICAL half ("attract nearby composites toward
  equilibrium") is NOT realized by the nodal flow: composites either drift
  away (arithmetic flow) or pull primes up (diffusion flow). Low static
  potential does not produce a dynamical basin here.
- The strong POSITIVE result is the §4 theorem made dynamical: primes are
  exactly the equilibria (zero-forcing rest points) of the arithmetic
  nodal flow. That is a faithful TNFR statement, not a new theorem — it is
  ΔNFR = 0 read as ∂EPI/∂t = 0.
- ΔNFR_arith (per-node, from Ω,τ,σ) is NOT the graph-diffusion Laplacian;
  the two flows are genuinely different (Example 101's caveat). Neither
  produces attraction toward primes.
- This extends the Example 101 inversion ("primes are inert isolates, not
  central attractors") from the static picture to the dynamical one.

References
----------
- theory/TNFR_NUMBER_THEORY.md §4 (ΔNFR=0 theorem), §7.1 (Φ_s sinks claim)
- examples/07_number_theory/101_numbers_as_coupled_network.py (static: Ω-graded periphery)
- examples/08_emergent_geometry/99_structural_diffusion.py (diffusion relaxes to degree-uniform)
- src/tnfr/dynamics/canonical.py (compute_canonical_nodal_derivative)
- src/tnfr/physics/structural_diffusion.py (structural_diffusion_operator)
- AGENTS.md §"Foundational Physics" (the nodal equation, fixed points)
"""

import os
import statistics
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))

import networkx as nx
import numpy as np
from sympy import factorint, isprime

from tnfr.dynamics.canonical import compute_canonical_nodal_derivative
from tnfr.mathematics.number_theory import ArithmeticTNFRNetwork
from tnfr.physics.structural_diffusion import structural_diffusion_operator

N = 140


def _build():
    """Arithmetic network with seed EPI/νf and the fixed arithmetic ΔNFR."""
    net = ArithmeticTNFRNetwork(max_number=N)
    G = net.graph.to_undirected()
    nodes = sorted(G.nodes())
    props = {n: net.get_tnfr_properties(n) for n in nodes}
    epi0 = {n: float(props[n]["EPI"]) for n in nodes}
    vf = {n: float(props[n]["nu_f"]) for n in nodes}
    dnfr = {n: float(props[n]["DELTA_NFR"]) for n in nodes}
    omega = {n: sum(factorint(n).values()) for n in nodes}
    return net, G, nodes, epi0, vf, dnfr, omega


# ============================================================================
# EXPERIMENT 1: Arithmetic flow — primes are the fixed points (§4 in motion)
# ============================================================================
def experiment_1_arithmetic_flow(nodes, epi0, vf, dnfr, omega):
    """∂EPI/∂t = νf·ΔNFR_arith: primes frozen, composites Ω-graded drift."""
    print("=" * 72)
    print("EXPERIMENT 1: Arithmetic Flow — Primes Are the Fixed Points")
    print("=" * 72)
    print()
    print("∂EPI/∂t = νf·ΔNFR_arith.  ΔNFR_arith is a FIXED per-node property")
    print("(from Ω,τ,σ) and = 0 ⟺ prime (§4). Integrate and watch who moves.")
    print()

    primes = [n for n in nodes if isprime(n)]
    comps = [n for n in nodes if not isprime(n)]
    dt = 0.05
    epi = dict(epi0)
    p0 = statistics.mean(abs(dnfr[n]) for n in nodes)
    for _ in range(40):
        for n in nodes:
            d = compute_canonical_nodal_derivative(
                vf[n], dnfr[n], validate_units=False
            ).derivative
            epi[n] += dt * d
    p1 = statistics.mean(abs(dnfr[n]) for n in nodes)
    drift = {n: abs(epi[n] - epi0[n]) for n in nodes}
    frozen = sum(1 for p in primes if drift[p] < 1e-12)
    om = np.array([omega[n] for n in nodes], float)
    dr = np.array([drift[n] for n in nodes], float)

    print(
        f"  mean|ΔNFR|:  t=0 {p0:.4f}  ->  t=end {p1:.4f}   "
        f"(CONSTANT: a fixed forcing, no relaxation)"
    )
    print(f"  primes FROZEN (drift < 1e-12): {frozen}/{len(primes)}")
    print(
        f"  mean EPI drift:  primes {statistics.mean(drift[p] for p in primes):.4f}"
        f"   composites {statistics.mean(drift[c] for c in comps):.4f}"
    )
    print(
        f"  r(drift, Ω) = {np.corrcoef(dr, om)[0, 1]:.3f}  "
        f"(composite drift is Ω-graded)"
    )
    print()
    print("VERDICT: primes are EXACTLY the zero-velocity rest points — the §4")
    print("theorem in motion (ΔNFR=0 ⟺ ∂EPI/∂t=0 ⟺ prime). Composites drift")
    print("AWAY at an Ω-graded rate; they do NOT flow toward the primes.")
    print()


# ============================================================================
# EXPERIMENT 2: Stability — the prime equilibria are marginal (no basin)
# ============================================================================
def experiment_2_marginal_stability(net, nodes, vf, dnfr):
    """ΔNFR_arith is independent of EPI ⟹ primes are neutral fixed points."""
    print("=" * 72)
    print("EXPERIMENT 2: The Prime Equilibria Are Marginal (No Restoring Force)")
    print("=" * 72)
    print()
    print("Is a prime an ATTRACTOR? Linearize: ∂(δEPI)/∂t = νf·(∂ΔNFR/∂EPI)·δEPI.")
    print("ΔNFR_arith depends on (Ω,τ,σ), NOT on EPI, so ∂ΔNFR/∂EPI = 0.")
    print()

    primes = [n for n in nodes if isprime(n)]
    p = primes[3]
    # perturb EPI of a prime; its ΔNFR (and hence velocity) is unchanged
    base_v = compute_canonical_nodal_derivative(
        vf[p], dnfr[p], validate_units=False
    ).derivative
    print(f"  prime {p}: ΔNFR_arith = {dnfr[p]:.2e}  ->  ∂EPI/∂t = {base_v:.2e}")
    print(f"  perturbing EPI({p}) by any amount leaves ΔNFR (and velocity) at 0")
    print("  -> no restoring force, no basin: a continuum of rest states")
    print()
    print("VERDICT: the prime fixed points are MARGINAL (neutral), not")
    print("attracting. There is no dynamical pull toward a prime — the flow")
    print("simply VANISHES there. 'Equilibrium' ≠ 'attractor'.")
    print()


# ============================================================================
# EXPERIMENT 3: Diffusion flow — the attractor is the composite bulk
# ============================================================================
def experiment_3_diffusion_flow(G, nodes, epi0, vf):
    """∂EPI/∂t = νf·ΔNFR_graph relaxes to degree-uniform; primes pulled up."""
    print("=" * 72)
    print("EXPERIMENT 3: Diffusion Flow — the Attractor Is the Composite Bulk")
    print("=" * 72)
    print()
    print("∂EPI/∂t = νf·(neighbour_mean − self) = νf·(−L_rw·EPI). This flow")
    print("DOES relax (Example 99) — to the degree-weighted uniform state.")
    print()

    giant = G.subgraph(max(nx.connected_components(G), key=len)).copy()
    gnodes = sorted(giant.nodes())
    gidx = {n: i for i, n in enumerate(gnodes)}
    _, lrw = structural_diffusion_operator(giant)
    epi_v = np.array([epi0[n] for n in gnodes])
    vf_v = np.array([vf[n] for n in gnodes])
    gp = [n for n in gnodes if isprime(n)]
    gc = [n for n in gnodes if not isprime(n)]
    deg = dict(giant.degree())

    mp0 = epi_v[[gidx[p] for p in gp]].mean()
    mc0 = epi_v[[gidx[c] for c in gc]].mean()
    dt = 0.1
    for _ in range(300):
        epi_v = epi_v + dt * vf_v * (-(lrw @ epi_v))
    mp1 = epi_v[[gidx[p] for p in gp]].mean()
    mc1 = epi_v[[gidx[c] for c in gc]].mean()
    degw = np.array([deg[n] for n in gnodes], float)
    eq = (degw * np.array([epi0[n] for n in gnodes])).sum() / degw.sum()
    iso = [n for n in nodes if G.degree(n) == 0]

    print(f"  giant core: {len(gnodes)} nodes ({len(gp)} primes, {len(gc)} comps)")
    print(f"  mean EPI prime:     t0 {mp0:.3f}  ->  t_end {mp1:.3f}")
    print(f"  mean EPI composite: t0 {mc0:.3f}  ->  t_end {mc1:.3f}")
    print(f"  degree-weighted equilibrium (the attractor) = {eq:.3f}")
    print(
        f"  isolated large primes (frozen at seed, never participate): " f"{len(iso)}"
    )
    print()
    print("  -> primes are pulled UP toward the composite bulk (the high-")
    print("     degree attractor), the OPPOSITE of §7.1's 'primes attract")
    print("     composites'. The bulk wins; the primes are dragged along.")
    print()


# ============================================================================
# EXPERIMENT 4: Synthesis — §7.1 refined
# ============================================================================
def experiment_4_synthesis():
    """Static sink: yes. Dynamical attractor: no."""
    print("=" * 72)
    print("EXPERIMENT 4: Synthesis — Refining the §7.1 'Sink' Reading")
    print("=" * 72)
    print()
    print("  §7.1 claim: 'primes act as sinks in the potential field — they")
    print("  attract nearby composites toward equilibrium.' Split it in two:")
    print()
    print("  STATIC half  (potential geometry):  CORRECT (Example 101)")
    print("    primes sit at ≈ 16× lower |Φ_s| — sinks in the geometric sense.")
    print("  DYNAMICAL half (attraction/basin):  NOT REALIZED (this example)")
    print("    arithmetic flow: composites drift AWAY (Ω-graded), primes are")
    print("      marginal rest points — no basin;")
    print("    diffusion flow:  the attractor is the composite bulk; primes")
    print("      are pulled UP toward it (the opposite direction).")
    print()
    print("  So the correct dynamical statement is:")
    print("    primes are the EQUILIBRIA of the nodal flow (ΔNFR=0 = §4),")
    print("    but NOT its attractors. Low static potential ≠ dynamical pull.")
    print()


def main():
    print()
    print("  TNFR Example 102: The Nodal Flow on Numbers")
    print("  Primes as equilibria, not attractors")
    print("  ===========================================")
    print()
    net, G, nodes, epi0, vf, dnfr, omega = _build()
    experiment_1_arithmetic_flow(nodes, epi0, vf, dnfr, omega)
    experiment_2_marginal_stability(net, nodes, vf, dnfr)
    experiment_3_diffusion_flow(G, nodes, epi0, vf)
    experiment_4_synthesis()
    print("=" * 72)
    print("WHAT THIS ESTABLISHES")
    print("=" * 72)
    print()
    print("Running the actual nodal equation ∂EPI/∂t = νf·ΔNFR on the")
    print("arithmetic network settles the §7.1 question. The strong positive")
    print("result is the §4 primality theorem made dynamical: primes are")
    print("EXACTLY the equilibria (zero-forcing rest points) of the")
    print("arithmetic flow — frozen, while composites drift away at an")
    print("Ω-graded rate. But primes are NOT attractors: the arithmetic")
    print("equilibria are marginal (no basin), and the diffusion flow")
    print("relaxes to the composite bulk, pulling primes UP. The §7.1")
    print("'primes attract composites' is a static potential-geometry fact")
    print("(primes at low Φ_s, true) overstated as a dynamical attraction")
    print("the flow does not produce — extending the Example 101 inversion")
    print("to the dynamical level. Numbers reflect TNFR dynamics deeply, but")
    print("the honest reflection is 'primes = inert equilibria', not")
    print("'primes = attractors'.")
    print()


if __name__ == "__main__":
    main()
