From 96911ffc4ba8cb35112f6e8a6eedeb3a3f6bc3ba Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 18 Oct 2026 07:06:47 +0200 Subject: [PATCH] aetheria-gravity map: belt shapes (body fact B60, model sub-table, rationale, cut sequence) Co-Authored-By: Claude Opus 5.5 --- docs/aetheria-gravity-map.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/aetheria-gravity-map.md b/docs/aetheria-gravity-map.md index caf7cc957..a90c843e5 100644 --- a/docs/aetheria-gravity-map.md +++ b/docs/aetheria-gravity-map.md @@ -97,6 +97,7 @@ All read at c6519383 in the detached worktree `C:\aeth-wt\ag2-docs` on 2026-10-1 - **B57. Belt rocks and the floater layer** (imagination-ag4-floater, 2026-10-17, uncommitted xunit probe over `ChildrenWithinReachTests.Zones`, all 384 zones of seeds 1-6 at 1d4aa626). 240 zones hold a belt, 450 belts, 42693 rocks; rocks per belt p10/50/90/max 31/75/186/499, at most 6 belts per zone. |field height| under a rock p10/50/90/99 41/165/323/434 (max 1027); field spread across one belt p50 41, p99 111. Rocks are drawn at field + AsteroidVerticalOffset (-10 in Settings.asset; the asteroid shader adds the Surface Displacement texel, which is -GetHeight), but the chunk reticle (ActionGameManager.ChunkWorldPosition) sits at absolute y -10, so it misses the drawn rock by the field depth. Rock radius pose.w p10/50/90/max 3.03/3.73/5.41/6.00; the LowPoly Small meshes measure 1.06-1.46 from their centre (FBX vertices; Unity file-scale reading unverified), so a rock centred on the floater layer (0, ships +3) reaches the ship plane at the median. - **B58. A ring is checked before its members settle** (imagination-ag5-ring, 2026-10-17: scratchpad `imagination-ag5-ring/`, jobs job2 and job3 via ygg-verify at f948992d and 26c44020, `ZoneGenerator.Place` instrumented by `instr.patch`, the candidate fix `fix.patch`, constructed fixture `ProbeRing.cs`; `ChildrenWithinReachTests.Zones`, all 384 zones of seeds 1-6). Source: `Settle`'s ring branch judges `ringDistance >= 1.5 x Roche(heavy.Mass)` (ZoneGenerator.cs:672) right after the circumbinary children fold into heavy, but a member's own children fold into it only when the queue reaches that member (ZoneGenerator.cs:687-694), after the check, so its mass is not final when it is read. Probe at f948992d: of 63 standing production rings, one (7 members, radius 98.302) ends 0.07% inside its heaviest member's margin (98.367), inside the test's 1e-3 tolerance; the constructed rings (two members, the lighter given a child of half the heavy's mass, radius 1.02 x margin) all stand, with the lighter member grown past the heavy (1000 -> 1400 for 900 + 500). At 26c44020 (place names): 4 production rings violate; the failing seed-1 ring (9 members, radius 126.029) passed its check at heavy mass 14591.9 (margin 120.68), then heavy's own children folded in to 19148.25 (margin 129.16); in all 4 the heaviest at the end is the member that was checked, so the seed-1 shape is the heavy growing, not a member overtaking it. Settling the members inside their node's Settle and checking `ring.Max(Mass)` folds exactly the violating rings (master: 1 flip in 384 zones, a ring folding into one body, so topology moves there; constructed: all 3 fold, root mass 2400 for mA 1000), and the full suite passes with it: f948992d 2215/0, 26c44020 2224/0, so no seeded test repins on either. Other order inferences, by source read: a body child's cusp bound (`AtLeast`, ZoneGenerator.cs:607-614) reads `WellMass(child)` before the child's own folds, and circumbinary children read `WellMass(heavy)` before heavy settles: the same shape, growing as mass^0.05 (the test's own `CuspTolerance` comment), absorbed by its 1e-2 tolerance. The Roche lower bound is re-read on every fold iteration (`lower()`), so it is final; belt slots read that final `lo` and siblings placed in the same pass; the circumbinary distance, chord caps and subzone isolation (`rootReach` from `WellTree.SubzoneReach`, centres only) are mass-free. The GPU parity fixture is hand-built (GravityFieldParityVerify.cs:152-190) and never calls the generator. - **B59. A bodyless ring member is left off its ring** (imagination-ag5-zero-dist scratchpad, 2026-10-18; xunit probe `ImagProbe` with `ProbeLog` instrumentation of `ZoneGenerator.Settle`, Yggdrasil jobs j3/j4 at 390a5958, every zone of seeds 1-30, 1920 zones). Shape: a planet expanded into a binary (`ExpandRosette(2)`) has a member expanded again in a later satellite pass, so the member is `Empty` with its own ring. Settle's ring is `c.Ring && WellMass(c) > 0`, and WellMass of an Empty node is 0, so a bodyless member is not on its node's ring. 85 nodes in 1920 zones have a bodyless ring member: 81 have one (the other member is a body), and 4 have two. With one, `ring` is the body alone and the bodyless member goes to `PlaceChildren` as a circumbinary child. With two, `ring.Count == 0` returns, both members keep distance 0, and the queue settles each one with reach `Reach x 0`: their inner rings get distance 0 too and fold, which leaves two bodies stacked at their node's centre. These are exactly the finding's zones: seed 10 zone 608, 13/792, 21/1285 and 30/1856, each with 2 bodies at distance 0 under a bodyless parent. *Candidate* (a bodyless node is one body of its family's mass, `OneBody`, for ring membership, the heavy member and the margin, as `AtLeast` already reads it; and a recursive Settle in place of the queue and the settled set). The order change alone gives a dump of all 1920 zones identical to head. The candidate has 0 zero-distance zones, changes 0 of the 384 zones of seeds 1-6, and changes exactly the 4 zones above in 1-30 (orbits 18 to 16, 32 to 30, 32 to 30 and 13 to 11; one body fewer each, because the outer ring of two folded binaries cannot keep its margin and becomes one body). The 81 one-bodyless rings fold under both generators: all had the same dump. Under the candidate, no node in seeds 1-30 has a bodyless ring member together with a non-ring child, and none has massive children but no massive ring member. Under the constants, a ring's circumbinary children always fold: the lower bound `(1 + RingReach) Beta ringDistance` equals the window's top for roots and non-roots alike (B51). ChildrenWithinReachTests takes 5 s at seeds 1-9 and 6 s at 1-30 (test time; 25 s and 64 s wall with build). At 1-30 the head generator fails RingsKeep at seed 10 ("ring of radius 0"), and the candidate passes 18 of 18. The full suite under the candidate at seeds 1-30 fails 2 of 2327. ANarrowAuthoredBeltKeepsItsWidth hit a load timeout (51 s) and passes alone. StationPlacementTests.AStationNeverSitsOnABinarysBodylessNode reads "0 bodyless nodes and 188 stations": its forced fixture (every satellite a binary, 4 passes) found its bodyless nodes only in the defect's shapes. Once every binary is judged, they all fold. Under base the same fixture holds 1202 bodyless nodes. Under the candidate, forcing rosettes (RosetteProbability 1) gives 338 bodyless nodes but only 2 stations, while production settings with parentless bodyless nodes counted (root rosettes) give 41 bodyless nodes and 191 stations, and the test passes (job j11 at b9e16872). +- **B60. Belt shapes, measured** (imagination-ag6-belt-shapes scratchpad, 2026-10-18: xunit probe `ImagBelt` over every zone of seeds 1-12, 768 zones, at 70c7a497, Yggdrasil job j4; prior art `docs/research/2026-10-17-belt-shapes-prior-art.md`). Source read at 70c7a497: a belt is one `AsteroidBeltData` body on an `OrbitData` of its own, and each rock persists (Distance, Phase, Size, RotationSpeed) about that orbit's parent (ZoneData.cs:96-106, 201-215). Zone derives each rock's period from the rock's own distance, `PeriodAround(parent, rock.Distance)` (Zone.cs:236-242), and gives the well tree the belt's farthest rock as its rail's extent (Zone.cs:216-230). `BeltTargets` bands rocks by distance and keys them by turn (BeltTargets.cs:15-21), so it serves any (Distance, Phase) draw. Nothing in Zone, BeltTargets, DrawAhead or ZoneRenderer reads an annulus as such: the shape lives only in the generator's rock draw (ZoneGenerator.cs:149-160) and Place's slot (636-651). Measured: 921 belts in 485 of the 768 zones (414 of them under a root); rocks per belt p50 73 (p10 33, p90 183); width / D p50 0.19. Hosts, the 7301 massive bodies with a parent: period p50 111 s (p10 52, p90 249); d ln T / d ln r at the host p50 0.92 (p10 0.83, p90 0.98). So a co-orbital rock 1% off its host's distance drifts p50 0.29 turns an hour against it (p90 0.56). On own-distance rails, a Trojan swarm (about 0.07 turn long) with any radial width shears into an annulus within the first hour of zone time. Host reach / D is 0.5 for every non-root body (band law). So L4 and L5, a chord D from the host, lie outside the host's own well. The nearest sibling edge sits at least 0.25 D from any host (p50 0.30), so a swarm of radial half-width under 0.25 D never meets a sibling. 548 of the 7301 hosts already carry a station's bodyless Lagrange orbit (CreateLagrangeOrbit, ZoneGenerator.cs:289-299). Station candidacy refuses a host with any sibling within RingWindow of its distance (252-256), so a swarm on the host's rail would bar the host. Lanes: take the 2545 childless light bodies (mass < BeltMassCeiling) with no belt sibling, and a lane of half-width E = max(support, RocheMargin x Roche) round each (E / D p50 0.19). The lane leaves room on both sides inside the neighbours' spacing and the window for 468 of them, and on one side only for 1257. 820 have room on neither side. The outermost child sits at the window, so its outer side is empty by construction. Edges: a non-root family's outermost extent sits at p50 0.325 of its reach (max 0.333, the window at reach / 3). The fade band from Gamma x reach to the reach is 0.5 of the reach wide in every family, because support never exceeds Gamma x reach. A belt extent fed to the tree beyond the window pushes support past Gamma x reach and narrows that band; at an extent of reach / Beta the band is gone (WellTree.cs:224-229). Sole roots fill out to the 0.75 R window. PopulatedRadius / Pack.Radius is p50 0.88 (min 0.41, max 0.90), and any rock beyond it moves PopulatedRadius (Zone.cs:736-744). ## Model page @@ -120,6 +121,21 @@ One row per kind this campaign touches. "Today" is c6519383; "target" is the sha Cells not filled, plainly: whether a per-body multiplier on c or the core is ever wanted (none exists; the brush multipliers go in grid-lowering); what record names an Ink pillar; whether the zone needs a non-gravitational edge once the bowl is gone; real generated loadout masses (B8 is a bound, not a measurement). + +### Belt shapes (2026-10-18, imagination-ag6-belt-shapes, read at 70c7a497; B60; ruling belt-shapes-direction) + +| kind | identity | lifecycle | authority (one owner per decision; forbidden writers) | +|---|---|---|---| +| **Belt** (`AsteroidBeltData`, a field of rocks) | Body record key minted by the cache; its name is the key's first 8 characters. One `OrbitData` of its own, whose Parent is the body the rocks orbit. Injective. A belt's shape is not a name or an enum that readers branch on. It is the generator's draw of the belt's rocks, plus at most two rules Zone reads: the period rule and the extent rule (rows below). | Created once by ZoneGenerator.GenerateZone and persisted in the zone pack; rocks never leave their draw. A changed generator changes new zones, and seeded tests repin. Saved packs keep their rocks, and a rule persisted later reads its default (today's annulus) on an old pack: a trailing MessagePack key is compatible drift (B13). Field kind is chosen at generation or first load (FieldKinds.Ensure). | ZoneGenerator owns which belts exist, each belt's host, slot and lane, and the rock draw: Place sets the bounds, GenerateZone draws the rocks. Zone owns the rocks' periods and the extent it gives WellTree. Forbidden writers: the renderer, BeltTargets, DrawAhead and mining, which read poses only. No reader computes its own period, and nothing outside Zone's two rules branches on shape. | +| **Rock** (`Asteroid`) | (belt key, index) is the `ChunkId`. The index order is fixed at generation and never re-sorted, because wear (`ChunkWear`) is keyed by it. | Persisted (Distance, Phase, Size, RotationSpeed) about the belt's parent; wear and respawn live on Zone. | The generator draws a rock. Zone owns its wear and its pose (`AsteroidBelt.Pose`). | +| **Period rule** | A property of the belt, not of the rock. Today each rock reads its own distance (`PeriodAround(parent, rock.Distance)`). In a co-orbital shape (Trojans, question `trojan-identity`), every rock rides its belt's rail and shares the rail's period, so the swarm keeps its shape. On own-distance rails it shears within an hour (B60). | Derived at zone build, never persisted. If Trojans ride their rail, the rule is one persisted flag on AsteroidBeltData. | Zone.PeriodAround alone; the rule says only which distance it reads. Forbidden: a period formula in Pose, in BeltTargets or in the generator. | +| **Extent rule** | What a belt's rail adds to its parent's L_F. Today it is the farthest rock (B53). A riding belt adds only its rail's Distance, because its rocks' periods never read their own distance. An edge (Oort-like) belt must add nothing beyond the window, or the family's fade band narrows (B60). | Derived at zone build. | Zone, the only WellNode builder in play, plus the generator's clearance tree. Both call one helper, so the rule is spelled once. | +| **Belt host** | A generator-time relation only: the body a lane or a Trojan swarm is drawn round (a GeneratorPlanet). It is not persisted. A Trojan belt's rail copies the host's (Parent, Distance, Phase), and that copy is all Zone needs. | Lives for one GenerateEntities call. | ZoneGenerator. | +| **Lagrange station orbit** | A bodyless OrbitData on a host's rail, 1/6 turn ahead of or behind it (CreateLagrangeOrbit). | Created at generation and persisted. | ZoneGenerator's station placement. Station candidacy must not count a host's own Trojan belt as a ring neighbour, or Trojans bar stations (B60). Whether a station may sit inside its host's swarm is part of question `trojan-identity`. | +| **Belt generation settings** | `ZoneGenerationSettings` in Settings.asset (an LFS object): BeltProbability 0.25, BeltMassCeiling 500, AsteroidBeltWidth (a ceiling that never binds, B56), AsteroidCount = sqrt(mass x D) + 11. Each new shape adds its own probability and spread fields there. | Authored. | Settings.asset. Forbidden: a tunable shape's numbers as constants in code. The spacing constants stay GravityField and ZoneGenerator constants, as ruled. | + +Cells not filled, plainly. Each waits on a question admitted 2026-10-18: which shape comes first (`belt-shape-order`); what a Trojan swarm is and whether stations share it (`trojan-identity`); which bodies get lanes and how wide (`lane-host-and-width`); which wells get edge material (`edge-material-wells`); and whether edge material lights the zone (`edge-material-light`). The edge material's place in the potential is not a question: it stays out of the extent, because the law breaks otherwise (B60). + ## Rationale - Why the drawn grid is a lowering, not an owner: `grid-shows-how-you-move` makes the slope a promise about motion. Six brush kinds on the gravity layers have no sim term or a different envelope (B3), so today the grid shows motion that does not happen (sector noise, wormholes, rim fades) and hides motion that does (the bowl in small zones is drawn at the same depth everywhere while felt only under the gate). @@ -143,6 +159,11 @@ Cells not filled, plainly: whether a per-body multiplier on c or the core is eve - Why Settle recurses instead of draining a queue (B59): a node's Settle reads only itself, its parent's chord cap and its own subtree, so the visit order is free, and the order change alone leaves all 1920 zones of seeds 1-30 unchanged. Recursion puts each child's Settle right after its parent places it, so every node settles once by construction. The settled set and the queue's guard then have nothing left to guard; Soul's surviving mutant on that guard (follow-up `queue-settle-guard-pin`) disappears along with it. Why a bodyless member belongs to its node's ring: under `binaries-orbited-from-afar`, nothing orbits a binary except its members (round the node) and children far enough away to see one body. A member that is itself a binary is still a member, and from afar it pulls as one body of its family's mass, the reading `AtLeast` already used. One helper now carries that reading everywhere a mass is read. - Why the ring's members settle inside their node's Settle rather than the check reading a predicted mass (B58): a member's final mass is whatever its own placement folds, which only its Settle computes; predicting it would be that placement spelled a second time. Settling the members first reads the one owner, and a member's Settle depends only on the Distance and chord cap its node has just written, so nothing else in the breadth-first order moves. - A constraint for pseudospace, not solved here: a push with curl (B11) has no height, so under `grid-shows-how-you-move` it either never moves ships, or is drawn by another channel, or is restricted to its gradient part. +- Why a belt's shape is its draw plus two rules, not a Shape enum its readers branch on: every reader (Pose, BeltTargets, DrawAhead, mining, the renderer, the minimap) already serves any (Distance, Phase) set (B60). A shape enum read beyond Zone would make each of those readers a second owner of what a shape means. +- Why Trojans ride their rail rather than their own distance: at d ln T / d ln r of about 0.92, any radial width shears the swarm apart within the first hour (B60). Real Trojans are held by libration about L4 and L5 (prior art, section 2.1), which rails cannot produce. The libration-averaged motion is the host's period, so riding the rail is the rail model of a tadpole orbit. The period still comes from the parent's field, read at the guiding centre's distance (`field-owns-orbits`). Libration itself could be added later as a phase wobble. +- Why a Trojan swarm is not the host's child: L4 and L5 lie a chord D from the host, outside its reach of 0.5 D (B60). Rocks orbiting the host would have the wrong motion and sit in the wrong well. +- Why a lane is as wide as the Roche margin rather than a Hill radius: this law's reach does not depend on mass (it is 0.5 D), and Hill's m^(1/3) is not a curve of this law (prior art, section 1.3). The Roche margin is the law's own measure of how close things come to a body, and a body's own children already keep it. +- Why edge material stays out of the extent: otherwise support would pass Gamma x reach and the family's fade band would narrow toward nothing (B60, WellTree.cs:224-229), so Oort-like rocks would change gravity. Their periods read the family's fading pull, which is what the field gives there. ## Cut sequence @@ -156,3 +177,7 @@ Mapped and admitted (2026-10-14, base 11b2d7ad): `aetheria-gravity:cut_spec:cut- - **rosette-equilibria** (follow-up `rosette-generation-r3`, rulings `rosettes-artificial-derived-from-fields`, `rosettes-placed-in-equilibrium`, `rosettes-rare-and-huge`, `binaries-orbited-from-afar`): pairs and rosettes placed as relative equilibria under the band law, members on their own radii (a pair at r_i proportional to c_partner; twisted crowns for alternating rosettes), one spin for every member, so no ring shears (B51); WellTree's ring group accepts members on two radii; members always suns, rare and huge. Waits on children-within-reach (`cut-children-within-reach.r3`), which places rings as one radius and merges root rings that cannot fit a multi-root zone's reach (B55); rare, huge rosettes need a sole-root zone or a large area. - **grid-noise-presentation**: the Sector Waves noise (after grid-lowering on layer 10, drawn only into the nebula patch height) and fine-grid detail move to non-slope presentation (grid-gravity-only-ruled); it also owns the Main Menu's own Surface Displacement brushes (B29, follow-up `menu-grid-brushes`). Waits on grid-lowering. - **top-speed-field**: top speed becomes a hull field; no drag below it, drag on the excess above it; the VelocityLimit clamp (Behaviors/VelocityLimit.cs) and always-on hull drag (Ship.cs:127-129) go; AI pilots brake actively; open: drag shape above the cap, consumables as modifiers, whether loot and wreckage come to rest (aetheria-gravity:ruling:no-drag-below-top-speed, target invariant orbits-do-not-decay). Waits on potential-owner and orbits-from-field. +- **belt-shapes** (follow-up `belt-shapes`, ruling `belt-shapes-direction`; B60; questions `belt-shape-order`, `trojan-identity`, `lane-host-and-width`, `edge-material-wells`, `edge-material-light`, all admitted 2026-10-18). One cut per shape. Each waits on its own question, and the order is `belt-shape-order`'s answer. No spec is admitted until those answers land. + - **trojan-swarms**: a co-orbital belt on a copy of the host's rail, with rocks about L4 and L5 that ride the rail's period. It adds one persisted flag on AsteroidBeltData and changes Zone's period and extent rules; station candidacy stops counting a host's own swarm as a neighbour. Waits on `trojan-identity`. + - **laned-belts**: a light childless body keeps its body, and its belt spans its neighbours' slots less a lane of the body's Roche margin. This cut folds in `belt-after-belt-edge-pin`, which sits in the same Place loop. Waits on `lane-host-and-width`. + - **edge-material**: sparse Oort-like rocks in a family's fade band, or beyond a root's window, kept out of the extent and, per the light question, out of the populated radius. Waits on `edge-material-wells` and `edge-material-light`.