Aetheria/docs/research/gravity-well-law/replace.py
Meta Vens fbe3156556 Research: well law and generator co-designed, graded as nested basins (r3), with minimap renders
Generator rules read from the band law's reach (Roche margin, cusp bound, window ending
before the parent's fade, sibling spacing, capacity folding into the parent), applied as a
re-placement of today's zones; basin metrics for ruling wells-read-as-nested-basins (cusp,
blob, nesting, saddle curvature, dead ground, steep share, lanes) and a numpy port of
IsolinesUI.shader rendering four zones per candidate. Stable series branch for the band
potential when the softening is comparable to the band (tau > 1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 07:32:58 +02:00

106 lines
6.6 KiB
Python

# Generator rules co-designed with the band law (aetheria-gravity, imagination-ag3-well-law, r3 pass, 2026-10-15).
# Re-places the bodies of one generated zone (same tree, same masses) so every family sits where the law is exact
# and rails are honest. The rules, each read from the law (well_law.py constants), none respelled:
# reach Lam_C = lam * D_C for a child of a body (the law's nominal reach); ring members RING_REACH * D.
# lower bound a child sits at least ROCHE_MARGIN x its parent's Roche limit out (the 234 in-core children go).
# upper bound a child's whole family ends before its parent's fade begins: BETA D_C <= GAMMA lam D_F and
# D_C (1 + lam) <= GAMMA lam D_F (B_F <= GAMMA Lam_F, the band law's split-invariance condition);
# a root's children: 0.75 Z (today's).
# spacing siblings at least a reach apart: D_(i+1) >= D_i / (1 - lam), so no sibling's term ever reaches
# another sibling's rail (phase-free: |D_i - D_j| >= max(Lam_i, Lam_j)). spacing=s < 1 loosens it to
# D_i / (1 - s lam), letting reaches overlap neighbouring rails.
# capacity children are laid inside-out at max(lower bound, previous x 1/(1-lam)); while the last overflows the
# window the lightest family is folded into the parent's body (family mass, so the far field is unchanged).
# placement kept children keep their order and are stretched geometrically so the outermost sits at hi.
# cusp (opt) cusp=k: a child also sits where its Plummer peak pull 0.5 c_c/eps_c is k x its parent's pull c_p/D,
# i.e. D >= 2 k eps_c c_p / c_c, so its own basin keeps a saddle against the parent (wells-read-as-nested-basins).
# bodyless a ring keeps its radius when it is a root (rosette geometry is rosette-generation-r3's), else shrinks
# to fit its node's reach; members' children sit within 0.2 x the nearest-partner chord; outer
# ("captured") children sit beyond the ring group's support (1 + RING_REACH) x D_ring. A pair whose
# members cannot sit outside each other's Roche limit inside its node's reach becomes one body.
import math
import numpy as np
import well_law as WL
ROCHE_MARGIN = 1.5 # v >= 0.83 sqrt(c) at the innermost child under the Plummer core
ROCHE = lambda m: 7.32 * np.maximum(m, 1e-12) ** 0.25 # PlanetSettings.RocheLimit (Settings.asset 121-124)
def replace_zone(par, D, m, body, cls, Z, lam, cusp=0.0, spacing=1.0):
CUSP = cusp
n = len(par)
kids = [[j for j in range(n) if par[j] == i] for i in range(n)]
depth = np.zeros(n, int)
for j in range(n):
a = par[j]
while a >= 0: depth[j] += 1; a = par[a]
def subtree(i):
out = [i]
for j in kids[i]: out += subtree(j)
return out
mb = np.where(body, m, 0.0).astype(float)
newD = D.astype(float).copy(); keep = np.ones(n, bool); m2 = m.astype(float).copy(); body2 = body.copy()
stats = {'folded': 0, 'folded_massive': 0, 'kept': 0}
q = 1 / (1 - spacing * lam)
def fam_mass(i): return sum(mb[j] for j in subtree(i) if keep[j])
def fold(i, into):
for j in subtree(i):
if keep[j]:
keep[j] = False; stats['folded'] += 1; stats['folded_massive'] += int(mb[j] > 0)
m2[into] += mb[j]; mb[into] += mb[j]
cfun = lambda mm: 45.08 * np.maximum(mm, 1e-12) ** 0.2 # PlanetSettings.WellStrength (Settings.asset 117-120)
def place(ch, lo, hi, into):
"""Greedy inside-out: each child at max(its lower bound, previous x q); fold the lightest family until the
last fits under hi; then stretch geometrically so the outermost sits at hi (ratios only grow)."""
hi = hi * (1 - 1e-6)
ch = sorted([j for j in ch if keep[j]], key=lambda j: D[j])
def lo_of(j):
if not CUSP or mb[j] <= 0 or not body2[into] or mb[into] <= 0: return lo
# the child's Plummer peak pull 0.5 c_c / eps_c must beat CUSP x the parent's pull c_p / D at the child
return max(lo, 2 * CUSP * ROCHE(m2[j]) * cfun(m2[into]) / cfun(m2[j]))
while ch:
pos = []; prev = 0.0
for j in ch:
prev = max(lo_of(j), prev * q) if pos else lo_of(j); pos.append(prev)
if pos[-1] <= hi: break
light = min(ch, key=lambda j: fam_mass(j)); fold(light, into)
ch = [j for j in ch if keep[j]]
if not ch: return
if len(ch) == 1:
newD[ch[0]] = max(pos[0], math.sqrt(lo * hi)) if pos[0] <= hi else pos[0]
return
g = (hi / pos[-1]) ** (1 / (len(ch) - 1))
for k, j in enumerate(ch): newD[j] = pos[k] * g ** k
for i in sorted(range(n), key=lambda i: depth[i]):
if not keep[i] or not kids[i]: continue
root = par[i] < 0
# B_F <= GAMMA Lam_F: the family's own support ends before its fade starts, so a merged body has the same terms
def hi_of(Dn): return 0.75 * Z if root else min(WL.GAMMA * lam * Dn / WL.BETA, WL.GAMMA * lam * Dn / (1 + lam))
if body2[i]:
place(kids[i], ROCHE_MARGIN * ROCHE(m2[i]), hi_of(newD[i]), i)
continue
massive = [j for j in kids[i] if keep[j] and mb[j] > 0]
if not massive:
continue
dmin = min(D[j] for j in massive)
ring = [j for j in massive if abs(D[j] - dmin) <= 0.1]
if len(ring) < 2: ring = [min(massive, key=lambda j: D[j])]
heavy = max(ring, key=lambda j: mb[j])
Dr = D[ring[0]]
if not root:
Dr = min(Dr, WL.GAMMA * lam * newD[i] / (WL.BETA ** 2 * (1 + WL.RING_REACH)))
if Dr < ROCHE_MARGIN * ROCHE(m2[heavy]):
# the pair cannot fit its node's reach with its members outside each other's Roche limit: it is one body
for j in list(kids[i]):
if keep[j]: fold(j, i)
body2[i] = True; stats['binaries_merged'] = stats.get('binaries_merged', 0) + 1
continue
for j in ring: newD[j] = Dr
chord = 2 * Dr * math.sin(math.pi / len(ring)) if len(ring) >= 2 else Dr
for j in ring:
place(kids[j], ROCHE_MARGIN * ROCHE(m2[j]), 0.2 * chord, j)
outer = [j for j in kids[i] if keep[j] and j not in ring]
place(outer, (1 + WL.RING_REACH) * Dr * WL.BETA, hi_of(newD[i]), heavy)
idx = np.where(keep)[0]; remap = {j: k for k, j in enumerate(idx)}
par2 = np.array([remap[par[j]] if par[j] >= 0 else -1 for j in idx])
stats['kept'] = int(keep.sum()); stats['idx'] = idx
return par2, newD[idx], m2[idx], body2[idx], [cls[j] for j in idx], stats