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>
This commit is contained in:
Meta Vens 2026-10-16 07:32:58 +02:00
commit fbe3156556
29 changed files with 810 additions and 5 deletions

View file

@ -0,0 +1,118 @@
# The well law and the generator, co-designed; graded as nested basins
Imagination pass `imagination-ag3-well-law` (r3), campaign aetheria-gravity, session self-2026-10-15-ag3, 2026-10-16, read at master b71dfc21. It revises `gravity-well-law.md` (r2) for question `aetheria-gravity:question:tail-taming-law-r3`, under rulings `tame-tails-parent-only-rails`, `wells-read-as-nested-basins` and the lean `tail-taming-lean-conserving-compact`.
The operator, verbatim:
- "'reach is narrower than the generator's window' tells me that this exploration has not been taking full advantage of its permission to also treat zone generation (or at least the variables governing its behavior) as part of the solution space."
- "Hypotheses regarding the aesthetic quality of the proposed solution can be somewhat graded by running the minimap shader on the resulting heightfield: it presents both potential contour lines and shades steep slopes (which should be avoided by overencumbered freighters) with scary orange plan contours"
- (ruling `wells-read-as-nested-basins`) "What we'll want to see is clear cusps between bodies and defined wells ... Fields should flow into each other with enough curvature to create saddles to ride along, and parents should encompass their children."
New scripts, all in `gravity-well-law/`:
- `replace.py`: the generator rules, applied as a re-placement of each generated zone. It keeps the same tree and masses and folds whatever does not fit into its parent.
- `probe3.py`: gained `place=`, `cusp=` and `spacing=` arguments and a stricter split counter (same reach and same fade start).
- `probe4.py`: the basin metrics and the minimap port.
- `well_law.py`: gained a stable series branch for τ > 1 (§3).
Outputs are `probe3-r3-output.txt`, `probe4-output.txt` and `minimap/C*-*.png`.
## 1. The two pulls the law and the generator must balance
- **Rail honesty** wants the parent to own the child's orbit: no sibling's term may reach it. In the band law a body's term reaches Λ = λ·D. So siblings must stand at least a reach apart, D_(i+1) ≥ D_i/(1 − λ), and a child's whole family must end before its parent's fade starts (B_F ≤ ½Λ_F). Split invariance is exact under the same condition.
- **A visible cusp** wants the child to keep its own basin against the parent's slope. With the Plummer core the child's steepest pull is 0.5·c_c/ε_c, so a saddle exists on the child-parent line only if D ≥ 2·ε_c·c_p/c_c.
- For a planetoid moon of a median planet that is about 53 u. Today's generator puts moons from 2·PlanetSafetyRadius (about 18 u) upward, so 805 of 10,127 planetoid cusps vanish today (C0 below).
- The saddle must also lie inside the child's reach: r_saddle ≈ D·c_c/(c_c + c_p), 0.3-0.4 D for this mass range. So λ ≥ about 0.4.
- **The window between them.** For a child of parent P: [max(1.5·Roche_P, 3·ε_c·c_p/c_c), ½·λ·D_P/β], and the upper end is D_P/6 for λ = ½ and β = 1.5.
- It is about [45-80, 102] u for moons of a median planet at 610 u.
- For the root's children it is [120, 0.75·Z].
- **What it costs.** The window is narrow, so the honest generator holds fewer bodies than today's. That count is the price, measured below.
## 2. Candidates
Each candidate varies one thing from the one before. The phases are the same in every candidate (a hash of zone and node), and so are the zones. "Generator" means `replace.py` applied to today's generated zones.
| # | Law | Generator rule | Varies from previous |
|---|---|---|---|
| C0 | today (harmonic core, log tails), K 4.098 | today | control |
| C1 | band law, reach ½·D, K 2.049 | today | the law |
| C2 | band law | strict: Roche margin 1.5, window from the law's reach, siblings a full reach apart | the generator |
| C3 | band law | C2 plus the cusp bound D ≥ 3·ε_c·c_p/c_c | the cusp bound |
| C4 | band law | C3 with siblings half a reach apart, D_(i+1) ≥ D_i/(1 − λ/2) | the spacing |
**Hypotheses, written before the first render run and recorded unchanged:**
- **H0** (C0): zone-wide funnel contours round the sun; small bodies are blobs at the minimap's contour spacing; orange only at cores; low nesting.
- **H1** (C1): the funnel contours go and each family gets its own contour set. Blob share falls because the depth range shrinks. Steep share rises (K halved doubles the drawn slope). Dead ground stays near 0, because a parent's body slope persists under its children.
- **H2** (C2): about half the bodies remain. Each stands alone with plain parent slope between (the "self-contained spikes" risk). Nesting rises.
- **H3** (C3): moon cusps never vanish, and blob share falls further.
- **H4** (C4): body count recovers toward today. Rails loosen to about ±10%, and blob and nesting stay near C3.
## 3. Results
Basin metrics are over 60 zones (every 25th, all Z) at the minimap texel (15.625 u). Cusps are over every pair in all 1500 zones. Rails come from `probe3-r3-output.txt` (6 draws).
- **Nodes kept:** C2 13,447, C3 11,260, C4 20,768 (of 31,640).
- **Cusp vanished:** count / pairs for planets and planetoids.
- **Prominence:** cusp prominence over c_child, p50, planets and planetoids.
- **Blob:** bodies without their own closed contour at the minimap spacing DR/20.
- **Nested:** the child's basin spills into its parent's.
- **Saddle:** p50 of the smaller Hessian eigenvalue at spill saddles, 1/s².
- **Dead:** share of the disc with |F| < 0.05 u/s², a 10 u drift over a 20 s coast (about a hull length; 1% of a packed Djinni's 4 u/s², B25).
- **Steep:** share with the port's danger blend > 0.5 (freighter no-go).
- **Ratio:** rail honesty, p10-p90 across classes.
- **Split:** families whose field outside B is unchanged by merging them.
| # | Nodes | Wells/zone p50 | Cusp vanished (planet; planetoid) | Prominence/c (planet; planetoid) | Blob | Nested | Saddle | Dead p50/p90 | Steep p50/p90 | Lanes open | Ratio p10-p90 | Split exact | Far field |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| C0 | 31,640 | 17 | 358/6879; 805/10127 | 0.94; 0.73 | 0.876 | 0.459 | 0.022 | 0.000/0.001 | 0.026/0.146 | 0.97 | 0.95-2.47 | n/a | 5.54x |
| C1 | 31,640 | 17 | 261/6879; 499/10127 | 0.91; 0.57 | 0.585 | 0.542 | 0.013 | 0.001/0.005 | 0.029/0.125 | 1.00 | 0.83-1.24 | 1765/6976 | 1.00x |
| C2 | 13,447 | 8 | 203/3724; 164/1633 | 0.27; 0.16 | 0.729 | 0.528 | 0.017 | 0.001/0.003 | 0.016/0.078 | 1.00 | 0.99-1.07 | 3198/3199 | 1.00x |
| C3 | 11,260 | 7 | 30/2766; 3/890 | 0.34; 0.27 | 0.488 | 0.588 | 0.019 | 0.001/0.003 | 0.017/0.084 | 1.00 | 0.99-1.03 | 2308/2309 | 1.00x |
| C4 | 20,768 | 12 | 47/6177; 4/4894 | 0.54; 0.36 | 0.561 | 0.542 | 0.020 | 0.001/0.003 | 0.023/0.109 | 1.00 | 0.90-1.10 | 4107/4108 | 1.00x |
Every candidate keeps the r2 guarantees where the generator places it: far field exactly c(M_zone), zone depth 0.99x of one body of its mass (p90 1.29-1.34), planets round a median sun 99-100% in 12-18 u/s, and 0 children under half their parent's pull. That last one was 234 in C0 and C1: the Roche margin removes them. The one family per placed run that is not exactly split-invariant is a bodyless node's child; I did not chase it.
**Renders** (`minimap/C<n>-<zone>.png`). The zones: small (Z 1000, seed 1), binary (Z 2125, seed 10), rosette (Z 4182, seed 104), moons (Z 6846, seed 253, 25 moons today). Per render (steep, blob, nested) is in `probe4-output.txt`.
| Hyp. | Held / failed | Why |
|---|---|---|
| H0 | held | C0-moons shows 17 concentric zone contours round the sun with planets as orange specks inside them. Blob 34/34 there and 0.876 overall; nested 0.459. |
| H1 | held, but steep did not rise | The funnel contours are gone and every planet has its own ring set, with a cusp against the sun (C1-moons). Blob falls to 0.585. Steep *falls* slightly (p90 0.146 to 0.125): the funnel's summed slope near the sun outweighs the halved K. |
| H2 | half held | 42% of nodes remain. Nesting does not rise (0.528). Blob *rises* to 0.729: the strict window squeezes moons close to their parents, where the parent's slope eats their cusp (planetoid prominence 0.16). No dead plateaus appear (dead 0.001), so the "self-contained spikes" failure does not show as flat ground. It shows as small cusps. |
| H3 | held | Planetoid cusps vanish for 3/890 (C0 805/10127). Blob 0.488 is the best of the five, nested 0.588 the best. The price: 36% of nodes kept. |
| H4 | held | 66% of nodes kept and wells 12 per zone (C0 17). Cusps survive (planetoid 4/4894). Blob 0.561, nested 0.542, saddle curvature 0.020, close to today's 0.022. Rails 0.90-1.10. |
**What no candidate fixes.** Blob stays at 0.49 or above in every candidate, because the minimap spaces its 21 contours linearly over the deepest body's depth (`IsolinesUI.shader` line 178, `_DepthRange` = the deepest planet, ZoneRenderer.cs 622-626). With c ∝ m^0.2 a planetoid's own well is a few percent of a sun's depth, below one contour step under any law. Fixing it is a presentation choice (log-spaced or per-family contours) for grid-noise-presentation, not a law or generator one. The "rosette" render keeps 11/11 blobs in C2-C4 because the root rosette's members are deep and close; rosette geometry is `rosette-generation-r3`'s.
**Precision finding.** The band closed form cancels catastrophically when the softening is comparable to the band (τ = (a²+ε²)/(b²−a²) > 1). The float32 error is 6.9e-3 at τ = 3 and 12 at τ = 10 (`check-output.txt` §5). Today's generator produces such terms: children inside their parent's Roche limit, and binaries shrunk inside their members' cores. `well_law.band_potential` now sums the band as a series in (1−t)/(1+τ) when τ > 1 (float32 error 1e-7). Forces were never affected. Potentials, and so the r2 depth column, were, slightly; the r2 rows rerun here (w1 and w3 on today's generator) match r2 to two digits. The r2 split claim "2382/2427" used a counter that did not check the fade start; the corrected count for w3 is 2329/2427 in-window, 3425/6976 overall.
## 4. Recommendation and what the generator must do
**Recommended: C4.** Band law with reach ½·D, plus the generator rules below. It is the only candidate that holds all three of:
- the nested-basins bar near today's saddle curvature, with cusps nearly always present and 40% fewer blobs;
- rails within ±10% (today 0.95-2.47);
- two thirds of today's bodies.
It is exact where it matters: far field, merged families, and no child in a core.
**Alternative: C3.** It wins on every basin number and has exact rails, at 36% of the bodies. If the operator values honest rails and clean basins over body count, C3 is the choice; the rules differ by one constant (spacing).
**Generator rules (children-within-reach's scope).** Each rule reads the law's reach (WellTree.Reach / Support), never a respelling.
1. **Lower bound** D ≥ max(1.5·Roche_parent, 3·ε_child·c_parent/c_child), so there is no child in a core and every child keeps a cusp. Replaces PlanetSafetyRadius at ZoneGenerator.cs 407, 587, 632, 648.
2. **Upper bound:** the child's family ends before its parent's fade: 1.5·D_C ≤ ½·λ·D_P and D_C·(1+λ) ≤ ½·λ·D_P, i.e. D_C ≤ D_P/6 at λ = ½. A root's children keep 0.75·Z (line 406). Replaces ChildDistanceMaximum's neighbour rule (650-651) and the linear distance map (631-632).
3. **Spacing:** D_(i+1) ≥ D_i/(1 − λ/2). Replaces distMulMin/distMulMax (425-426, 440-441, 455-456, 497-498, 615) as the spacing owner. They may remain as random spread above the minimum.
4. **Capacity:** a requested count (452, 494, 422, 437) is a request. Children that do not fit are folded into the parent's body (family mass, so the field at range is unchanged).
5. **Pairs:** a binary whose members cannot sit outside each other's Roche limit inside its node's reach is made one body (486). Circumbinary children sit beyond (1 + 4)·D_ring·1.5.
6. **Rosettes** keep their own follow-up (twisted geometry). The law's ring group must then accept members on two radii.
**Dead or derived generator variables:**
- `PlanetSafetyRadius` (Settings.cs 113, Settings.asset 135-138): dead.
- `ChildDistanceMinimum` and `ChildDistanceMaximum`: derived from the law's reach, Roche and cusp bounds.
- `distMul*` as spacing: dead.
- `p0ChildDist` and `p1ChildDist` (573-574): derived (members' windows, 0.2 × the nearest-partner chord).
- Unchanged: counts as requests, `massMul*`, `jupiterJump`, `massFraction`, `MassFloor` and the final mass renormalisation (mass split); the m^p exponent and the WellStrength and Roche curves. Conservation removes the reason to retune p.
## Confidence
- Basin metrics: medium. They are measured at the minimap texel (15.6 u), so moons under two texels are basins of one or two cells, and "nested" is limited by that resolution, not by the law.
- Cusps (analytic line samples, all zones) and rails: high.
- The port: the frag logic of IsolinesUI.shader and the material's values. The background is a stand-in colour and the window is the whole zone at the minimap's texel. It is a port, not the shader; Self renders the chosen candidate in Unity.
- Hypotheses H0-H4 were written before the first render run.

View file

@ -15,3 +15,6 @@ median sun: c = 304.9, sqrt(c) = 17.46 u/s, eps = Roche = 79.8
float32 eps 79.8 band [1000.0, 1500.0]: max abs err 7.28e-06 (unit strength; depth 2.77)
float32 eps 17.0 band [40.0, 60.0]: max abs err 1.45e-05 (unit strength; depth 1.15)
float32 eps 79.8 band [12000.0, 25000.0]: max abs err 9.06e-07 (unit strength; depth 5.49)
tau 3.00: closed form float32 err 6.9e-03; series float32 err 3.1e-07; F = dphi/dr 3.5e-08
tau 10.00: closed form float32 err 1.2e+01; series float32 err 1.1e-07; F = dphi/dr 3.8e-08
tau 9.92: closed form float32 err 1.2e+01; series float32 err 1.1e-07; F = dphi/dr 2.1e-08

View file

@ -67,3 +67,24 @@ for eps, a, b in [(79.8, 1000.0, 1500.0), (17.0, 40.0, 60.0), (79.8, 12000.0, 25
r64 = np.linspace(0, b, 100001)
ref = band_potential(eps, a, b, r64)
print(f'float32 eps {eps} band [{a}, {b}]: max abs err {np.max(np.abs(band32(eps, a, b, r64) - ref)):.2e} (unit strength; depth {np.abs(ref).max():.2f})')
# 5. large tau (softening comparable to the band): the closed form cancels in float32; the series branch does not
def series32(eps, a, b, r):
f32 = np.float32
eps, a, b, r = f32(eps), f32(a), f32(b), r.astype(f32)
s = r * r; span = b * b - a * a; tau = (a * a + eps * eps) / span; w = f32(1) / (f32(1) + tau)
def bandv(t):
Y = (f32(1) - t).astype(f32); acc = np.zeros_like(t)
for k in range(40):
inner = np.zeros_like(t)
for j in range(4, len(SC)): inner = (inner + f32(SC[j]) * Y ** (j + k + 1) / f32(j + k + 1)).astype(f32)
acc = (acc + w ** (k + 1) * inner).astype(f32)
return f32(-.5) * acc
t = np.clip((s - a * a) / span, 0, 1).astype(f32)
inside = f32(.5) * np.log((s + eps * eps) / (a * a + eps * eps)).astype(f32) + bandv(np.zeros(1, f32))[0]
return np.where(s <= a * a, inside, np.where(s >= b * b, f32(0), bandv(t)))
for eps, a, b in [(1658.0, 1000.0, 1500.0), (3391.0, 1000.0, 1500.0), (59.0, 11.0, 22.0)]:
r64 = np.linspace(0, b, 20001); ref = band_potential(eps, a, b, r64)
tau = (a * a + eps * eps) / (b * b - a * a)
print(f'tau {tau:6.2f}: closed form float32 err {np.max(np.abs(band32(eps, a, b, r64) - ref)):.1e}; series float32 err '
f'{np.max(np.abs(series32(eps, a, b, r64) - ref)):.1e}; F = dphi/dr {np.max(np.abs(np.gradient(ref, r64) - band_pull(eps, a, b, r64))[3:-3]) / band_pull(eps, a, b, r64).max():.1e}')

BIN
docs/research/gravity-well-law/minimap/C0-binary.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C0-moons.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C0-rosette.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C0-small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C1-binary.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C1-moons.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C1-rosette.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C1-small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C2-binary.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C2-moons.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C2-rosette.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C2-small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C3-binary.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C3-moons.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C3-rosette.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C3-small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C4-binary.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C4-moons.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C4-rosette.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/research/gravity-well-law/minimap/C4-small.png (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,94 @@
# python probe3.py bodies.csv 6 <law> [place=... cusp=... spacing=...], one process per run, Starfire 2026-10-16 (stable band potential)
== [7] (w1) conserving compact, Lam = 0.5*D (856s, 6 draws)
Sun ratio p10/50/90/99 [0.86, 1.01, 1.24, 1.55] parent-only v p10/50/90 [21.99, 23.6, 24.62] n 2211
GasGiant ratio p10/50/90/99 [0.83, 1.0, 1.24, 1.63] parent-only v p10/50/90 [17.58, 22.08, 24.48] n 3566
Planet ratio p10/50/90/99 [0.87, 0.99, 1.14, 1.45] parent-only v p10/50/90 [14.9, 16.9, 23.2] n 7445
Planetoid ratio p10/50/90/99 [0.89, 0.99, 1.12, 1.42] parent-only v p10/50/90 [11.01, 13.75, 18.14] n 10200
Belt ratio p10/50/90/99 [0.87, 0.99, 1.15, 1.45] parent-only v p10/50/90 [11.05, 14.24, 19.45] n 3134
planets round a median sun: v p10/50/90 [16.07, 17.17, 17.78]; share in 12-18 0.99; n 1116
Sun-class children parent-only v p10/50/90 [21.99, 23.6, 24.62]
children with no parent pull: {}; parent pull < 0.5 c/D: {'Planetoid': 214, 'Belt': 11, 'Planet': 9} (total 234)
mid-zone inward pull by Z: 1000: 0.55, 2125: 0.39, 4182: 0.24, 6846: 0.17, 10000: 0.13
zone far-field c / c(M_zone) p10/50/90: at 2Z [1.0, 1.0, 1.0], at 10Z [1.0, 1.0, 1.0]
split invariance (family merged into one body), max |da|/|a| p50/p90/p99: at 1.05 B_f [0.52, 1.63, 5.34], at 2Z [0.0, 0.0, 0.0]; n 6976; families whose reach the merge leaves unchanged 1765/6976, of those inside the ruled half-tidal window 1165/2427
whole-zone depth over one body of the zone mass, p10/50/90: [0.99, 1.0, 1.32]
wells (bodies) per zone p50/p99: [17.0, 58.0]
containment violations (term outside an ancestor support): 0 of 70890
terms per zone p50/p99: [30.0, 114.0]
sim sample (uniform in the zone disc): terms in support p50/p99/mean [4.0, 14.0]/4.92; tree tests p50/p99 [10.0, 34.0]
shader texel (8192 u window): terms in support p50/p99 [3.0, 13.0], mean per texel p50/p99 over windows [2.88, 12.3]; terms meeting the window p50/p99 [21.0, 76.0]
== [9] (w3) conserving compact, Lam = 2*d_Hill (629s, 6 draws)
Sun ratio p10/50/90/99 [0.84, 1.09, 1.32, 1.62] parent-only v p10/50/90 [21.99, 23.6, 24.62] n 2211
GasGiant ratio p10/50/90/99 [0.77, 1.0, 1.29, 1.67] parent-only v p10/50/90 [17.58, 22.08, 24.48] n 3566
Planet ratio p10/50/90/99 [0.81, 0.97, 1.17, 1.5] parent-only v p10/50/90 [14.9, 16.9, 23.2] n 7445
Planetoid ratio p10/50/90/99 [0.84, 0.97, 1.15, 1.45] parent-only v p10/50/90 [11.01, 13.75, 18.14] n 10200
Belt ratio p10/50/90/99 [0.83, 0.98, 1.19, 1.48] parent-only v p10/50/90 [11.05, 14.24, 19.45] n 3134
planets round a median sun: v p10/50/90 [16.07, 17.17, 17.78]; share in 12-18 0.99; n 1116
Sun-class children parent-only v p10/50/90 [21.99, 23.6, 24.62]
children with no parent pull: {}; parent pull < 0.5 c/D: {'Planetoid': 214, 'Belt': 11, 'Planet': 9} (total 234)
mid-zone inward pull by Z: 1000: 0.60, 2125: 0.42, 4182: 0.27, 6846: 0.19, 10000: 0.14
zone far-field c / c(M_zone) p10/50/90: at 2Z [1.0, 1.0, 1.0], at 10Z [1.0, 1.0, 1.0]
split invariance (family merged into one body), max |da|/|a| p50/p90/p99: at 1.05 B_f [0.0, 0.91, 3.16], at 2Z [0.0, 0.0, 0.0]; n 6976; families whose reach the merge leaves unchanged 3425/6976, of those inside the ruled half-tidal window 2329/2427
whole-zone depth over one body of the zone mass, p10/50/90: [0.99, 1.01, 1.34]
wells (bodies) per zone p50/p99: [17.0, 58.0]
containment violations (term outside an ancestor support): 0 of 70890
terms per zone p50/p99: [30.0, 114.0]
sim sample (uniform in the zone disc): terms in support p50/p99/mean [4.0, 15.0]/5.62; tree tests p50/p99 [10.0, 34.0]
shader texel (8192 u window): terms in support p50/p99 [3.0, 14.0], mean per texel p50/p99 over windows [3.69, 12.61]; terms meeting the window p50/p99 [21.0, 76.0]
## placement lam=0.5 cusp=0.0 spacing=1.0: nodes 31640 -> 13447; folded 18193 (14992 massive) into their parents; grandchildren inside the half-tidal window 4278/4278
== [7] (w1) conserving compact, Lam = 0.5*D (300s, 6 draws)
Sun ratio p10/50/90/99 [1.0, 1.0, 1.0, 1.26] parent-only v p10/50/90 [20.21, 23.53, 24.6] n 2203
GasGiant ratio p10/50/90/99 [1.0, 1.0, 1.07, 1.36] parent-only v p10/50/90 [16.24, 19.97, 23.41] n 2895
Planet ratio p10/50/90/99 [1.0, 1.0, 1.01, 1.23] parent-only v p10/50/90 [13.52, 16.04, 18.48] n 3725
Planetoid ratio p10/50/90/99 [0.99, 1.0, 1.01, 1.2] parent-only v p10/50/90 [10.48, 12.71, 15.34] n 1633
Belt ratio p10/50/90/99 [0.99, 1.0, 1.01, 1.25] parent-only v p10/50/90 [9.97, 13.71, 16.83] n 408
planets round a median sun: v p10/50/90 [14.42, 16.66, 17.66]; share in 12-18 1.00; n 102
Sun-class children parent-only v p10/50/90 [20.21, 23.53, 24.6]
children with no parent pull: {}; parent pull < 0.5 c/D: {} (total 0)
mid-zone inward pull by Z: 1000: 0.51, 2125: 0.37, 4182: 0.22, 6846: 0.15, 10000: 0.11
zone far-field c / c(M_zone) p10/50/90: at 2Z [1.0, 1.0, 1.0], at 10Z [1.0, 1.0, 1.0]
split invariance (family merged into one body), max |da|/|a| p50/p90/p99: at 1.05 B_f [0.0, 0.0, 0.0], at 2Z [0.0, 0.0, 0.0]; n 3199; families whose reach the merge leaves unchanged 3198/3199, of those inside the ruled half-tidal window 3198/3199
whole-zone depth over one body of the zone mass, p10/50/90: [0.99, 0.99, 1.29]
wells (bodies) per zone p50/p99: [8.0, 24.0]
containment violations (term outside an ancestor support): 0 of 24097
terms per zone p50/p99: [14.0, 45.0]
sim sample (uniform in the zone disc): terms in support p50/p99/mean [3.0, 12.0]/3.98; tree tests p50/p99 [4.0, 22.0]
shader texel (8192 u window): terms in support p50/p99 [3.0, 11.0], mean per texel p50/p99 over windows [2.38, 10.96]; terms meeting the window p50/p99 [10.0, 36.0]
## placement lam=0.5 cusp=1.5 spacing=1.0: nodes 31640 -> 11260; folded 20380 (17097 massive) into their parents; grandchildren inside the half-tidal window 3002/3002
== [7] (w1) conserving compact, Lam = 0.5*D (400s, 6 draws)
Sun ratio p10/50/90/99 [1.0, 1.0, 1.0, 1.24] parent-only v p10/50/90 [21.7, 23.55, 24.6] n 2177
GasGiant ratio p10/50/90/99 [1.0, 1.0, 1.03, 1.34] parent-only v p10/50/90 [17.34, 20.43, 23.51] n 2489
Planet ratio p10/50/90/99 [1.0, 1.0, 1.0, 1.12] parent-only v p10/50/90 [14.74, 16.38, 18.64] n 2767
Planetoid ratio p10/50/90/99 [0.99, 1.0, 1.01, 1.02] parent-only v p10/50/90 [11.23, 13.81, 15.8] n 890
Belt ratio p10/50/90/99 [0.99, 1.0, 1.01, 1.16] parent-only v p10/50/90 [10.19, 13.22, 16.57] n 352
planets round a median sun: v p10/50/90 [15.96, 16.79, 17.75]; share in 12-18 1.00; n 57
Sun-class children parent-only v p10/50/90 [21.7, 23.55, 24.6]
children with no parent pull: {}; parent pull < 0.5 c/D: {} (total 0)
mid-zone inward pull by Z: 1000: 0.53, 2125: 0.36, 4182: 0.23, 6846: 0.15, 10000: 0.12
zone far-field c / c(M_zone) p10/50/90: at 2Z [1.0, 1.0, 1.0], at 10Z [1.0, 1.0, 1.0]
split invariance (family merged into one body), max |da|/|a| p50/p90/p99: at 1.05 B_f [0.0, 0.0, 0.0], at 2Z [0.0, 0.0, 0.0]; n 2309; families whose reach the merge leaves unchanged 2308/2309, of those inside the ruled half-tidal window 2308/2309
whole-zone depth over one body of the zone mass, p10/50/90: [0.99, 0.99, 1.32]
wells (bodies) per zone p50/p99: [7.0, 18.0]
containment violations (term outside an ancestor support): 0 of 17929
terms per zone p50/p99: [11.0, 36.0]
sim sample (uniform in the zone disc): terms in support p50/p99/mean [3.0, 12.0]/3.97; tree tests p50/p99 [4.0, 18.0]
shader texel (8192 u window): terms in support p50/p99 [3.0, 11.0], mean per texel p50/p99 over windows [2.38, 10.73]; terms meeting the window p50/p99 [9.0, 28.0]
## placement lam=0.5 cusp=1.5 spacing=0.5: nodes 31640 -> 20768; folded 10872 (9026 massive) into their parents; grandchildren inside the half-tidal window 7699/7703
== [7] (w1) conserving compact, Lam = 0.5*D (559s, 6 draws)
Sun ratio p10/50/90/99 [0.92, 1.0, 1.07, 1.24] parent-only v p10/50/90 [21.9, 23.57, 24.61] n 2201
GasGiant ratio p10/50/90/99 [0.9, 1.0, 1.1, 1.38] parent-only v p10/50/90 [17.75, 21.98, 24.35] n 3400
Planet ratio p10/50/90/99 [0.92, 1.0, 1.02, 1.14] parent-only v p10/50/90 [15.03, 17.03, 22.76] n 6180
Planetoid ratio p10/50/90/99 [0.93, 1.0, 1.01, 1.08] parent-only v p10/50/90 [11.7, 14.29, 20.02] n 4894
Belt ratio p10/50/90/99 [0.96, 1.0, 1.01, 1.08] parent-only v p10/50/90 [10.24, 13.81, 18.52] n 1476
planets round a median sun: v p10/50/90 [16.01, 16.91, 17.69]; share in 12-18 0.99; n 219
Sun-class children parent-only v p10/50/90 [21.9, 23.57, 24.61]
children with no parent pull: {}; parent pull < 0.5 c/D: {} (total 0)
mid-zone inward pull by Z: 1000: 0.52, 2125: 0.37, 4182: 0.23, 6846: 0.16, 10000: 0.12
zone far-field c / c(M_zone) p10/50/90: at 2Z [1.0, 1.0, 1.0], at 10Z [1.0, 1.0, 1.0]
split invariance (family merged into one body), max |da|/|a| p50/p90/p99: at 1.05 B_f [0.0, 0.0, 0.0], at 2Z [0.0, 0.0, 0.0]; n 4108; families whose reach the merge leaves unchanged 4107/4108, of those inside the ruled half-tidal window 4103/4104
whole-zone depth over one body of the zone mass, p10/50/90: [0.99, 0.99, 1.34]
wells (bodies) per zone p50/p99: [12.0, 28.0]
containment violations (term outside an ancestor support): 0 of 34357
terms per zone p50/p99: [19.0, 46.0]
sim sample (uniform in the zone disc): terms in support p50/p99/mean [3.0, 12.0]/4.07; tree tests p50/p99 [7.0, 25.0]
shader texel (8192 u window): terms in support p50/p99 [3.0, 11.0], mean per texel p50/p99 over windows [2.42, 10.88]; terms meeting the window p50/p99 [13.0, 35.0]

View file

@ -3,7 +3,8 @@
# paths), plus: split invariance (one family merged into one body of its mass), whole-zone merge depth, and
# evaluation cost (terms per sample for the sim, per texel for a 2048^2 / 4 u Surface Displacement window).
# Rows t1-t3 are the first draft (windows tapered from the centre), kept because they show what the band form buys.
# Usage: python probe3.py bodies.csv [draws] [law-index,...]
# Usage: python probe3.py bodies.csv [draws] [law-index,...] [place=LAM] [cusp=K] [spacing=S]
# place=LAM re-places every zone by replace.py's generator rules (same tree and masses) before measuring.
import csv, sys, os, collections, time
import numpy as np
HERE = os.path.dirname(os.path.abspath(__file__))
@ -176,12 +177,30 @@ LAWS = [
('(w1) conserving compact, Lam = 0.5*D', 'band', HALF),
('(w2) conserving compact, Lam = 1*D', 'band', lambda i, D, CF, cp: 1.0 * D),
('(w3) conserving compact, Lam = 2*d_Hill', 'band', TIDAL),
('(w4) conserving compact, Lam = D/3', 'band', lambda i, D, CF, cp: D / 3),
]
pick = [int(x) for x in sys.argv[3].split(',')] if len(sys.argv) > 3 else range(len(LAWS))
sunm = np.sort([float(r['mass']) for r in rows if r['type'] == 'Sun' and r['empty'] == '0'])
SUN_LO, SUN_HI = sunm[int(.4 * len(sunm))], sunm[int(.6 * len(sunm))]
pre = {k: zone_struct(z) for k, z in zones.items()}
PLACE = next((float(a.split('=')[1]) for a in sys.argv[4:] if a.startswith('place=')), None)
CUSPK = next((float(a.split('=')[1]) for a in sys.argv[4:] if a.startswith('cusp=')), 0.0)
SPACING = next((float(a.split('=')[1]) for a in sys.argv[4:] if a.startswith('spacing=')), 1.0)
if PLACE is not None:
import replace as RP
agg = collections.Counter(); inwin = [0, 0]; before = sum(len(v[0]) for v in pre.values())
for k, z in zones.items():
par, D, m, body, cls = pre[k]
par2, D2, m2, b2, c2, st = RP.replace_zone(par, D, m, body, cls, float(z[0]['zoneRadius']), PLACE, CUSPK, SPACING)
pre[k] = (par2, D2, m2, b2, c2); st.pop('idx'); agg.update(st)
cb = np.where(b2, strength(np.maximum(m2, 1e-12)), 0.0)
for j in range(len(par2)):
p = par2[j]
if p >= 0 and b2[p] and par2[p] >= 0 and b2[par2[p]]:
inwin[1] += 1; inwin[0] += int(D2[j] <= 0.5 * D2[p] * np.sqrt(cb[p] / (2 * cb[par2[p]])))
print(f'## placement lam={PLACE} cusp={CUSPK} spacing={SPACING}: nodes {before} -> {agg["kept"]}; folded {agg["folded"]} ({agg["folded_massive"]} massive) into their parents; '
f'grandchildren inside the half-tidal window {inwin[0]}/{inwin[1]}')
TH8 = np.arange(8) * np.pi / 4; RING8 = np.stack([np.cos(TH8), np.sin(TH8)], 1)
class ZoneLaw:
@ -278,7 +297,9 @@ for li in pick:
a1 = zn.field(pos, X); a2 = z2.field(pos2, X)
split_err[tag].append(np.max(np.linalg.norm(a1 - a2, axis=1) / np.maximum(np.linalg.norm(a2, axis=1), 1e-12)))
if kind in ('band', 'tapered'):
same = int(zn.Lam[f] == z2.Lam[remap[f]])
# same terms: same reach and same fade start (max(B_f, GAMMA Lam_f) against the merged body's GAMMA Lam_f)
fs = [t[3] for t in zn.T if t[0] == f and t[1] > 0 and t[4] == zn.Lam[f]]
same = int(zn.Lam[f] == z2.Lam[remap[f]] and (kind != 'band' or (fs and fs[0] == WL.GAMMA * zn.Lam[f])))
split_exact[1] += 1; split_exact[0] += same
# families the ruled generator window would hold: every descendant within half its parent's tidal
# radius (B25: d_H = D sqrt(c / 2 c_parent); a parent with no body parent has no window and is skipped)

View file

@ -0,0 +1,61 @@
# python probe4.py bodies.csv minimap <i> 25, one process per candidate, Starfire 2026-10-16
render zones: {'small': (('0', '1'), {'Z': 1000.0, 'moons': 3, 'rosette': False, 'binary': False, 'n': 15}), 'binary': (('0.25', '10'), {'Z': 2125.0, 'moons': 8, 'rosette': False, 'binary': True, 'n': 24}), 'rosette': (('0.5', '104'), {'Z': 4181.9805, 'moons': 27, 'rosette': True, 'binary': False, 'n': 41}), 'moons': (('0.75', '253'), {'Z': 6845.6714, 'moons': 25, 'rosette': False, 'binary': True, 'n': 40})}
== C0 today law, today generator
cusp Sun : vanished 17/2050; prominence/c p10/50 [0.715, 1.55]
cusp GasGiant : vanished 163/3305; prominence/c p10/50 [0.254, 1.209]
cusp Planet : vanished 358/6879; prominence/c p10/50 [0.274, 0.943]
cusp Planetoid: vanished 805/10127; prominence/c p10/50 [0.13, 0.733]
dead ground (|F| < 0.05) share of disc p50/p90 [0.0, 0.001]; steep share p50/p90 [0.026, 0.146]; lanes open in 0.97 of zones
blob share 0.876; nested share 0.459; saddle min |eigenvalue| p50 [0.022] 1/s^2 (n 854)
render small ('0', '1'): steep 0.141, dead 0.000, lanes True, blob 10/11, nested 5/11, bodies 12, depth range 441.7
render binary ('0.25', '10'): steep 0.071, dead 0.000, lanes True, blob 18/18, nested 6/14, bodies 19, depth range 786.2
render rosette ('0.5', '104'): steep 0.216, dead 0.000, lanes True, blob 35/35, nested 5/27, bodies 35, depth range 2080.2
render moons ('0.75', '253'): steep 0.036, dead 0.000, lanes True, blob 34/34, nested 15/29, bodies 35, depth range 2381.6
render zones: {'small': (('0', '1'), {'Z': 1000.0, 'moons': 3, 'rosette': False, 'binary': False, 'n': 15}), 'binary': (('0.25', '10'), {'Z': 2125.0, 'moons': 8, 'rosette': False, 'binary': True, 'n': 24}), 'rosette': (('0.5', '104'), {'Z': 4181.9805, 'moons': 27, 'rosette': True, 'binary': False, 'n': 41}), 'moons': (('0.75', '253'), {'Z': 6845.6714, 'moons': 25, 'rosette': False, 'binary': True, 'n': 40})}
== C1 band law (reach D/2), today generator
cusp Sun : vanished 5/2050; prominence/c p10/50 [0.853, 1.969]
cusp GasGiant : vanished 99/3305; prominence/c p10/50 [0.245, 1.344]
cusp Planet : vanished 261/6879; prominence/c p10/50 [0.214, 0.913]
cusp Planetoid: vanished 499/10127; prominence/c p10/50 [0.139, 0.571]
dead ground (|F| < 0.05) share of disc p50/p90 [0.001, 0.005]; steep share p50/p90 [0.029, 0.125]; lanes open in 1.00 of zones
blob share 0.585; nested share 0.542; saddle min |eigenvalue| p50 [0.013] 1/s^2 (n 895)
render small ('0', '1'): steep 0.122, dead 0.000, lanes True, blob 7/11, nested 8/11, bodies 12, depth range 356.9
render binary ('0.25', '10'): steep 0.091, dead 0.001, lanes True, blob 14/18, nested 6/14, bodies 19, depth range 591.6
render rosette ('0.5', '104'): steep 0.152, dead 0.000, lanes True, blob 35/35, nested 10/27, bodies 35, depth range 1935.9
render moons ('0.75', '253'): steep 0.019, dead 0.002, lanes True, blob 27/34, nested 19/29, bodies 35, depth range 1170.6
render zones: {'small': (('0', '1'), {'Z': 1000.0, 'moons': 3, 'rosette': False, 'binary': False, 'n': 15}), 'binary': (('0.25', '10'), {'Z': 2125.0, 'moons': 8, 'rosette': False, 'binary': True, 'n': 24}), 'rosette': (('0.5', '104'), {'Z': 4181.9805, 'moons': 27, 'rosette': True, 'binary': False, 'n': 41}), 'moons': (('0.75', '253'), {'Z': 6845.6714, 'moons': 25, 'rosette': False, 'binary': True, 'n': 40})}
== C2 band law + strict generator
cusp Sun : vanished 25/2168; prominence/c p10/50 [0.32, 1.28]
cusp GasGiant : vanished 170/2887; prominence/c p10/50 [0.056, 0.406]
cusp Planet : vanished 203/3724; prominence/c p10/50 [0.034, 0.269]
cusp Planetoid: vanished 164/1633; prominence/c p10/50 [0.027, 0.16]
dead ground (|F| < 0.05) share of disc p50/p90 [0.001, 0.003]; steep share p50/p90 [0.016, 0.078]; lanes open in 1.00 of zones
blob share 0.729; nested share 0.528; saddle min |eigenvalue| p50 [0.017] 1/s^2 (n 386)
render small ('0', '1'): steep 0.083, dead 0.000, lanes True, blob 6/6, nested 3/6, bodies 7, depth range 357.6
render binary ('0.25', '10'): steep 0.044, dead 0.000, lanes True, blob 4/6, nested 2/6, bodies 7, depth range 581.5
render rosette ('0.5', '104'): steep 0.101, dead 0.000, lanes True, blob 12/12, nested 2/4, bodies 12, depth range 1715.4
render moons ('0.75', '253'): steep 0.008, dead 0.001, lanes True, blob 6/9, nested 5/9, bodies 10, depth range 1004.0
render zones: {'small': (('0', '1'), {'Z': 1000.0, 'moons': 3, 'rosette': False, 'binary': False, 'n': 15}), 'binary': (('0.25', '10'), {'Z': 2125.0, 'moons': 8, 'rosette': False, 'binary': True, 'n': 24}), 'rosette': (('0.5', '104'), {'Z': 4181.9805, 'moons': 27, 'rosette': True, 'binary': False, 'n': 41}), 'moons': (('0.75', '253'), {'Z': 6845.6714, 'moons': 25, 'rosette': False, 'binary': True, 'n': 40})}
== C3 band law + strict generator + cusp bound
cusp Sun : vanished 16/2141; prominence/c p10/50 [0.299, 1.422]
cusp GasGiant : vanished 79/2481; prominence/c p10/50 [0.158, 0.578]
cusp Planet : vanished 30/2766; prominence/c p10/50 [0.134, 0.341]
cusp Planetoid: vanished 3/890; prominence/c p10/50 [0.109, 0.272]
dead ground (|F| < 0.05) share of disc p50/p90 [0.001, 0.003]; steep share p50/p90 [0.017, 0.084]; lanes open in 1.00 of zones
blob share 0.488; nested share 0.588; saddle min |eigenvalue| p50 [0.019] 1/s^2 (n 375)
render small ('0', '1'): steep 0.087, dead 0.000, lanes True, blob 2/4, nested 3/4, bodies 5, depth range 357.6
render binary ('0.25', '10'): steep 0.042, dead 0.000, lanes True, blob 4/5, nested 1/5, bodies 6, depth range 582.3
render rosette ('0.5', '104'): steep 0.101, dead 0.000, lanes True, blob 11/11, nested 1/3, bodies 11, depth range 1723.0
render moons ('0.75', '253'): steep 0.008, dead 0.001, lanes True, blob 5/8, nested 5/8, bodies 9, depth range 1004.0
render zones: {'small': (('0', '1'), {'Z': 1000.0, 'moons': 3, 'rosette': False, 'binary': False, 'n': 15}), 'binary': (('0.25', '10'), {'Z': 2125.0, 'moons': 8, 'rosette': False, 'binary': True, 'n': 24}), 'rosette': (('0.5', '104'), {'Z': 4181.9805, 'moons': 27, 'rosette': True, 'binary': False, 'n': 41}), 'moons': (('0.75', '253'), {'Z': 6845.6714, 'moons': 25, 'rosette': False, 'binary': True, 'n': 40})}
== C4 band law + loose spacing + cusp bound
cusp Sun : vanished 12/2155; prominence/c p10/50 [0.623, 1.751]
cusp GasGiant : vanished 75/3387; prominence/c p10/50 [0.229, 1.003]
cusp Planet : vanished 47/6177; prominence/c p10/50 [0.18, 0.54]
cusp Planetoid: vanished 4/4894; prominence/c p10/50 [0.125, 0.356]
dead ground (|F| < 0.05) share of disc p50/p90 [0.001, 0.003]; steep share p50/p90 [0.023, 0.109]; lanes open in 1.00 of zones
blob share 0.561; nested share 0.542; saddle min |eigenvalue| p50 [0.02] 1/s^2 (n 708)
render small ('0', '1'): steep 0.109, dead 0.000, lanes True, blob 4/8, nested 6/8, bodies 9, depth range 357.0
render binary ('0.25', '10'): steep 0.061, dead 0.000, lanes True, blob 6/11, nested 5/11, bodies 12, depth range 580.5
render rosette ('0.5', '104'): steep 0.101, dead 0.000, lanes True, blob 11/11, nested 1/3, bodies 11, depth range 1723.0
render moons ('0.75', '253'): steep 0.011, dead 0.002, lanes True, blob 11/17, nested 9/17, bodies 18, depth range 1000.4

View file

@ -0,0 +1,307 @@
# Basin readability probe and minimap renders (aetheria-gravity, imagination-ag3-well-law, r3 pass, 2026-10-15).
# For ruling wells-read-as-nested-basins. Per candidate (law + generator rule), over the same zones and phases:
# cusp every body-parent/child pair: the potential along the segment child -> parent has an interior saddle
# (a local maximum after the child's own minimum); prominence = (saddle - child minimum) / c_child.
# grid on a sample of zones, at the minimap's texel (15.625 u: ARPG.unity camera "Map Gravity" orthographic
# size 1000 at scene line 7812 over the 128^2 MinimapGravity.renderTexture, lines 15-16):
# dead ground share of the zone disc where |F| < 0.05 u/s^2 (a 10 u drift over a 20 s coast, about a
# hull length; 1% of a packed Djinni's 4 u/s^2, map B25);
# steep share where the port's danger blend > 0.5 (the orange "freighter no-go");
# lanes open the non-steep part of the disc is one region (islands under 0.5% of the disc ignored);
# basins steepest descent on the drawn height (8 neighbours): a body's basin is the one holding its
# cell; blob = its basin is shared with another body or its prominence (spill height - basin
# minimum) is under the minimap contour spacing DR/20; nested = it spills into its parent's
# basin; saddle curvature = Hessian eigenvalues of phi at the spill point.
# renders a numpy port of Assets/Shaders/IsolinesUI.shader (constants cited where used) on four zones.
# Usage: python probe4.py bodies.csv outdir [candidate-index,...] [sample stride, default 25 = 60 zones]
import os, sys, math, zlib, struct, collections, hashlib
import numpy as np
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
DATA, OUT = sys.argv[1], sys.argv[2]
ONLY = [int(x) for x in sys.argv[3].split(',')] if len(sys.argv) > 3 else None
STRIDE = int(sys.argv[4]) if len(sys.argv) > 4 else 25
os.makedirs(OUT, exist_ok=True)
sys.argv = ['probe3.py', DATA, '1', '0']
src = open(os.path.join(HERE, 'probe3.py'), encoding='utf-8').read()
g = {'__file__': os.path.join(HERE, 'probe3.py'), '__name__': 'probe3_defs'}
exec(src[:src.index('for li in pick:')], g) # definitions, data and zones only; the probe loop does not run
import replace as RP, well_law as WL
zones, pre, ZoneLaw, strength, core_radius, family = g['zones'], g['pre'], g['ZoneLaw'], g['strength'], g['core_radius'], g['family']
HALF = g['HALF']
TEXEL = 2000.0 / 128 # minimap texel in u (see header)
K_TODAY, K_BAND = 4.0983607, 2.0491803 # Settings.asset 125 today; halved for the Plummer peak (gravity-well-law.md)
FLAT = 0.05
CANDIDATES = [
# name, law kind, reach, K, placement (lam, cusp, spacing) or None
('C0 today law, today generator', 'today', None, K_TODAY, None),
('C1 band law (reach D/2), today generator', 'band', HALF, K_BAND, None),
('C2 band law + strict generator', 'band', HALF, K_BAND, (0.5, 0.0, 1.0)),
('C3 band law + strict generator + cusp bound', 'band', HALF, K_BAND, (0.5, 1.5, 1.0)),
('C4 band law + loose spacing + cusp bound', 'band', HALF, K_BAND, (0.5, 1.5, 0.5)),
]
def phases(key, ids):
"""One phase per original node, the same in every candidate (hash of zone key and node index)."""
return np.array([int(hashlib.md5(f'{key}-{i}'.encode()).hexdigest()[:8], 16) / 2 ** 32 * 2 * np.pi for i in ids])
def place_fixed(par, D, depth, th):
pos = np.zeros((len(par), 2))
for k in np.argsort(depth, kind='stable'):
if par[k] >= 0: pos[k] = pos[par[k]] + D[k] * np.array([np.cos(th[k]), np.sin(th[k])])
return pos
def zone_for(cand, key, Z):
par, D, m, body, cls = pre[key]
ids = np.arange(len(par))
if cand[4] is not None:
lam, cusp, spacing = cand[4]
par, D, m, body, cls, st = RP.replace_zone(par, D, m, body, cls, Z, lam, cusp, spacing)
ids = st['idx']
zn = ZoneLaw(cand[1], cand[2], par, D, m, body, Z)
fam, depth = family(par)
pos = place_fixed(par, D, depth, phases(key, ids))
return zn, par, D, m, body, cls, pos
def reference(zn, Z):
if zn.kind in ('band', 'tapered'):
own, c, e, a, b = g['_arr'](zn.T)
return float((c * WL.band_potential(e, a, b, np.full_like(c, Z))).sum())
return float(np.where(zn.c > 0, zn.law['P'](zn.c, zn.e, np.full_like(zn.c, Z), zn.s), 0).sum())
def heights(zn, pos, X, Z, K):
out = np.empty(len(X))
for i in range(0, len(X), 20000):
out[i:i + 20000] = zn.potential(pos, X[i:i + 20000])
return (out - reference(zn, Z)) / K
def forces(zn, pos, X):
out = np.empty((len(X), 2))
for i in range(0, len(X), 20000):
out[i:i + 20000] = zn.field(pos, X[i:i + 20000])
return out
def q(v, ps=(10, 50, 90)):
v = np.sort(np.asarray(v, float))
return [round(float(v[min(len(v) - 1, int(p / 100 * len(v)))]), 3) for p in ps] if len(v) else []
def smoothstep(a, b, x):
t = np.clip((x - a) / (b - a), 0, 1)
return t * t * (3 - 2 * t)
# ---- the minimap port: Assets/Shaders/IsolinesUI.shader frag (lines 151-207) with Minimap Gravity Lines.mat values ----
MAT = dict(Color=np.array([0, .4, .6, 1]), AngleColor=np.array([1.4980392, 0, 1.2705883, .5019608]),
DangerColor=np.array([2.670157, .22367807, 0, 1]), LineWidth=.15, LineFade=5., AngleWidth=.05, AngleFade=1.15,
DangerSteepness=1.5, Scale=10.) # Assets/Materials/UI/Minimap Gravity Lines.mat, m_Floats and m_Colors
def minimap(h, depth_range):
"""h: drawn height grid (GetHeight, <= 0 in wells), rows = +y. Returns rgba per the shader's col."""
height = h # shader 161: height = -tex, tex = -GetHeight
me = height
e = np.roll(height, -1, axis=1); n = np.roll(height, -1, axis=0)
plan = np.stack([e - me, n - me], -1) # calcGrad, shader 144-149 (forward, one texel)
slope = np.linalg.norm(plan, axis=-1) + 1e-12 # shader 171
start, dr = 0.0, depth_range # ZoneRenderer.cs 625-626: _StartDepth 0, _DepthRange = max planet depth
blend = smoothstep(start, start + dr / 32, -height) # shader 165
danger = smoothstep(0, MAT['DangerSteepness'], (slope / MAT['Scale']) ** 2) # shader 173
linecol = (MAT['Color'][None, None] * (1 - danger[..., None]) + MAT['DangerColor'][None, None] * danger[..., None]) * blend[..., None]
col = np.zeros(h.shape + (4,))
spacing = dr / 20 # shader 178
for ih in range(1, 22): # shader 179-182
iso = np.abs(height + start + ih * spacing)
col += (1 - smoothstep(MAT['LineWidth'], MAT['LineWidth'] * MAT['LineFade'], iso / slope))[..., None] * linecol
angle = np.arctan2(plan[..., 1], plan[..., 0]) / np.pi + 1 # shader 184
angcol = (blend * slope / MAT['Scale'])[..., None] * (MAT['AngleColor'][None, None] * (1 - danger[..., None]) + MAT['DangerColor'][None, None] * danger[..., None])
for ia in np.arange(0.5, 13, 1.0): # shader 195-201
iso = np.abs(angle - ia / 6.0)
l = 1 - smoothstep(MAT['AngleWidth'], MAT['AngleWidth'] * MAT['AngleFade'], iso)
l = l * smoothstep(-start - dr, -start, height)
col += l[..., None] * angcol
return col, danger
def write_png(path, rgb):
hgt, wid, _ = rgb.shape
raw = b''.join(b'\x00' + rgb[y].tobytes() for y in range(hgt - 1, -1, -1)) # rows = +y, PNG top row = max y
def chunk(t, d): return struct.pack('>I', len(d)) + t + d + struct.pack('>I', zlib.crc32(t + d) & 0xffffffff)
with open(path, 'wb') as f:
f.write(b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', wid, hgt, 8, 2, 0, 0, 0))
+ chunk(b'IDAT', zlib.compress(raw, 9)) + chunk(b'IEND', b''))
def composite(col):
bg = np.array([.02, .03, .05]) # stand-in for the Minimap Background material
a = np.clip(col[..., 3:4], 0, 1)
rgb = np.clip(col[..., :3], 0, 1) * a + bg * (1 - a)
return (rgb * 255).astype(np.uint8)
# ---- grid analysis ----
def grid_for(Z):
nx = int(math.ceil(2.1 * Z / TEXEL))
xs = (np.arange(nx) - (nx - 1) / 2) * TEXEL
X = np.stack(np.meshgrid(xs, xs), -1).reshape(-1, 2) # rows = +y
return xs, X, nx
def labels_by_descent(h):
ny, nx = h.shape
idx = np.arange(ny * nx).reshape(ny, nx)
best = idx.copy(); bh = h.copy()
pad = np.pad(h, 1, constant_values=np.inf); pidx = np.pad(idx, 1, constant_values=-1)
for dy in (-1, 0, 1):
for dx in (-1, 0, 1):
if dy == 0 and dx == 0: continue
nh = pad[1 + dy:1 + dy + ny, 1 + dx:1 + dx + nx]; ni = pidx[1 + dy:1 + dy + ny, 1 + dx:1 + dx + nx]
better = nh < bh
bh = np.where(better, nh, bh); best = np.where(better, ni, best)
lab = best.ravel()
for _ in range(64):
nxt = lab[lab]
if np.array_equal(nxt, lab): break
lab = nxt
return lab.reshape(ny, nx)
def spills(h, lab):
"""For every basin: (spill height, spill-into basin, spill cell pair) over 4-neighbour boundaries."""
best = {}
for ax in (0, 1):
a = lab; b = np.roll(lab, -1, axis=ax); ha = h; hb = np.roll(h, -1, axis=ax)
sl = (slice(None, -1), slice(None)) if ax == 0 else (slice(None), slice(None, -1))
a, b, ha, hb = a[sl], b[sl], ha[sl], hb[sl]
mask = a != b
pas = np.maximum(ha, hb)[mask]; la = a[mask]; lb = b[mask]
ys, xs = np.nonzero(mask)
order = np.argsort(pas)
for o in order:
for (u, v) in ((la[o], lb[o]), (lb[o], la[o])):
if u not in best:
y, x = ys[o], xs[o]
best[u] = (pas[o], v, (y, x, ax))
return best
def components(mask):
"""4-connected components by min-label propagation with pointer jumping (labels are cell indices)."""
ny, nx = mask.shape
big = ny * nx
lab = np.where(mask, np.arange(big).reshape(ny, nx), big).ravel()
flat_mask = mask.ravel()
while True:
old = lab.copy()
L = lab.reshape(ny, nx)
for ax in (0, 1):
for sh in (1, -1):
r = np.roll(L, sh, axis=ax)
L = np.where(mask, np.minimum(L, r), big)
lab = L.ravel()
ext = np.append(lab, big)
for _ in range(20):
lab = np.where(flat_mask, np.minimum(lab, ext[lab]), big); ext = np.append(lab, big)
if np.array_equal(lab, old): break
return lab.reshape(ny, nx)
def analyse(cand, key, z):
Z = float(z[0]['zoneRadius'])
zn, par, D, m, body, cls, pos = zone_for(cand, key, Z)
xs, X, nx = grid_for(Z)
h = heights(zn, pos, X, Z, cand[3]).reshape(nx, nx)
F = forces(zn, pos, X)
disc = (np.linalg.norm(X, axis=1) <= Z).reshape(nx, nx)
bodies = np.where(body)[0]
cell = lambda p: (int(round(p[1] / TEXEL + (nx - 1) / 2)), int(round(p[0] / TEXEL + (nx - 1) / 2)))
dr = max(1.0, max(-heights(zn, pos, pos[bodies], Z, cand[3]).min(), 0.0))
col, danger = minimap(h, dr)
steep = danger > 0.5
dead = (np.linalg.norm(F, axis=1) < FLAT).reshape(nx, nx)
free = disc & ~steep
comp = components(free)
sizes = collections.Counter(comp[free].tolist())
big = [s for s in sizes.values() if s >= 0.005 * disc.sum()]
lab = labels_by_descent(h)
sp = spills(h, lab)
out = dict(dead=dead[disc].mean(), steep=steep[disc].mean(), lanes=len(big) <= 1, blob=0, nb=0, nested=0, nn=0, curv=[], dr=dr)
bl = {j: lab[cell(pos[j])] if 0 <= cell(pos[j])[0] < nx and 0 <= cell(pos[j])[1] < nx else None for j in bodies}
share = collections.Counter(v for v in bl.values() if v is not None)
for j in bodies:
if par[j] < 0 or bl[j] is None: continue
out['nb'] += 1
b = bl[j]
prom = (sp[b][0] - h.ravel()[b]) if b in sp else 0.0
if share[b] > 1 or prom < dr / 20: out['blob'] += 1
p = par[j]
if body[p] and bl.get(p) is not None and b in sp:
out['nn'] += 1; out['nested'] += int(sp[b][1] == bl[p])
if b in sp and share[b] == 1:
y, x, ax = sp[b][2]
pt = np.array([xs[x] + (TEXEL / 2 if ax == 1 else 0), xs[y] + (TEXEL / 2 if ax == 0 else 0)])
e = 1.0
Fx1, Fx0 = zn.field(pos, (pt + [e, 0])[None])[0], zn.field(pos, (pt - [e, 0])[None])[0]
Fy1, Fy0 = zn.field(pos, (pt + [0, e])[None])[0], zn.field(pos, (pt - [0, e])[None])[0]
H = -np.array([(Fx1 - Fx0) / (2 * e), (Fy1 - Fy0) / (2 * e)]) # Hessian of phi = -dF/dx
ev = np.linalg.eigvalsh((H + H.T) / 2)
out['curv'].append(float(np.min(np.abs(ev))))
return out, h, col, zn, pos, body, par, cls
def cusp_pairs(cand, key, z):
Z = float(z[0]['zoneRadius'])
zn, par, D, m, body, cls, pos = zone_for(cand, key, Z)
res = []
t = np.concatenate([np.geomspace(1e-4, 0.05, 150), np.linspace(0.05, 0.98, 250)[1:]])
for k in range(len(par)):
p = par[k]
if p < 0 or not body[p] or not body[k]: continue
X = pos[k][None] + t[:, None] * (pos[p] - pos[k])[None]
X = np.concatenate([pos[k][None], X])
phi = zn.potential(pos, X)
ck = strength(m[k])
i0 = int(np.argmin(phi[:60])) # the child's own minimum, near the child
rest = phi[i0:]
interior = [i for i in range(1, len(rest) - 1) if rest[i] >= rest[i - 1] and rest[i] > rest[i + 1]]
if interior:
s_ = interior[0]
res.append((cls[k], True, (rest[s_] - rest[:s_ + 1].min()) / ck))
else:
res.append((cls[k], False, 0.0))
return res
if __name__ == '__main__':
keys = sorted(zones.keys())
# the four render zones, picked from today's generator (same zones in every candidate)
def stats_of(key):
par, D, m, body, cls = pre[key]; fam, depth = family(par)
bodyless = [i for i in range(len(par)) if not body[i] and cls[i] != 'Belt']
return dict(Z=float(zones[key][0]['zoneRadius']), moons=int(((depth >= 2) & body).sum()),
rosette=any(par[i] < 0 for i in bodyless), binary=any(par[i] >= 0 for i in bodyless), n=len(par))
S = {k: stats_of(k) for k in keys}
pick = {
'small': next(k for k in keys if S[k]['Z'] == 1000 and S[k]['n'] >= 10 and not S[k]['rosette']),
'binary': next(k for k in keys if S[k]['binary'] and S[k]['Z'] == 2125),
'rosette': next(k for k in keys if S[k]['rosette'] and abs(S[k]['Z'] - 4182) < 1),
'moons': max((k for k in keys if abs(S[k]['Z'] - 6846) < 1 and not S[k]['rosette']), key=lambda k: S[k]['moons']),
}
print('render zones:', {n: (k, S[k]) for n, k in pick.items()})
sample = [k for i, k in enumerate(keys) if i % STRIDE == 0] # every STRIDE-th zone (60 at 25), every Z in proportion
for ci, cand in enumerate(CANDIDATES):
if ONLY is not None and ci not in ONLY: continue
cusp = collections.defaultdict(list); vanished = collections.Counter(); total = collections.Counter()
for key in keys:
for c, ok, pr in cusp_pairs(cand, key, zones[key]):
total[c] += 1
if ok: cusp[c].append(pr)
else: vanished[c] += 1
agg = collections.defaultdict(list)
for key in sample:
o = analyse(cand, key, zones[key])[0]
for kk in ('dead', 'steep', 'lanes'): agg[kk].append(o[kk])
agg['blob'].append((o['blob'], o['nb'])); agg['nested'].append((o['nested'], o['nn'])); agg['curv'] += o['curv']
blob = sum(a for a, b in agg['blob']) / max(1, sum(b for a, b in agg['blob']))
nest = sum(a for a, b in agg['nested']) / max(1, sum(b for a, b in agg['nested']))
print(f'== {cand[0]}')
for c in ('Sun', 'GasGiant', 'Planet', 'Planetoid'):
print(f' cusp {c:9s}: vanished {vanished[c]}/{total[c]}; prominence/c p10/50 {q(cusp[c], (10, 50))}')
print(f' dead ground (|F| < {FLAT}) share of disc p50/p90 {q(agg["dead"], (50, 90))}; steep share p50/p90 {q(agg["steep"], (50, 90))}; '
f'lanes open in {np.mean(agg["lanes"]):.2f} of zones')
print(f' blob share {blob:.3f}; nested share {nest:.3f}; saddle min |eigenvalue| p50 {q(agg["curv"], (50,))} 1/s^2 (n {len(agg["curv"])})')
for name, key in pick.items():
o, h, col, zn, pos, body, par, cls = analyse(cand, key, zones[key])
write_png(os.path.join(OUT, f'C{ci}-{name}.png'), composite(col))
print(f' render {name} {key}: steep {o["steep"]:.3f}, dead {o["dead"]:.3f}, lanes {o["lanes"]}, blob {o["blob"]}/{o["nb"]}, '
f'nested {o["nested"]}/{o["nn"]}, bodies {int(body.sum())}, depth range {o["dr"]:.1f}')
sys.stdout.flush()

View file

@ -0,0 +1,106 @@
# 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

View file

@ -7,7 +7,8 @@
# S(t) = t^k sum_{j<k} C(k-1+j, j) (1-t)^j (the order-k smoothstep: S - 0 and 1 - S vanish to order k at the ends)
# potential: 0 for s >= b^2; on the band 1/2 [R ln((t+tau)/(1+tau)) - (I(1) - I(t))], tau = (a^2+eps^2)/(b^2-a^2),
# where (1 - S(t)) = (t + tau) Q(t) + R and I is the antiderivative of Q; inside, 1/2 ln((s+eps^2)/(a^2+eps^2)) plus
# the band's value at t = 0. a = b = inf is the global term 1/2 ln(r^2 + eps^2).
# the band's value at t = 0. a = b = inf is the global term 1/2 ln(r^2 + eps^2). For tau > 1 (softening comparable to
# the band) the band is summed as a series in (1 - t)/(1 + tau) instead (band_potential), which stays exact in float32.
# The potential is a function of r^2 (C-infinity at the centre and wherever B is constant) and C^k on the circles
# r = a and r = b (the k-th derivative of the pull jumps there).
#
@ -86,9 +87,22 @@ def band_potential(eps, a, b, r):
q, R = _band_coeffs(tau)
def band(t):
return 0.5 * (R * np.log((t + tau) / (1 + tau)) - (_qint(q, np.ones_like(t)) - _qint(q, t)))
def band_series(t):
# tau > 1: the closed form cancels catastrophically (float32 error 6e-3 at tau 3, 13 at tau 10). With y = 1 - t,
# 1 - S(1 - y) = S(y) (the smoothstep is symmetric) and 1/(t + tau) = sum_k y^k / (1 + tau)^(k+1), ratio <= 1/2:
# band = -1/2 sum_k (1+tau)^-(k+1) sum_j S_j Y^(j+k+1) / (j+k+1), Y = 1 - t; 40 terms reach 1e-12 at tau = 1.
Y = 1 - t; acc = np.zeros_like(t); w = 1 / (1 + tau)
for k in range(40):
inner = np.zeros_like(t)
for j in range(4, len(SC)):
inner = inner + SC[j] * Y ** (j + k + 1) / (j + k + 1)
acc = acc + w ** (k + 1) * inner
return -0.5 * acc
def band_any(t):
return np.where(tau <= 1, band(t), band_series(t))
t = np.clip((s - A2) / span, 0.0, 1.0)
inside = 0.5 * np.log((s + eps * eps) / (A2 + eps * eps)) + band(np.zeros_like(t))
val = np.where(s <= A2, inside, np.where(s >= B2, 0.0, band(t)))
inside = 0.5 * np.log((s + eps * eps) / (A2 + eps * eps)) + band_any(np.zeros_like(t))
val = np.where(s <= A2, inside, np.where(s >= B2, 0.0, band_any(t)))
return np.where(inf, 0.5 * np.log(s + eps * eps), val)
# ---- the first draft: tapered window from the centre (see the header) ----