Map: flight control allocates thrust (body facts, authority map, rationale); locomotion-cut.md superseded for Cuts 2-3

Imagination pass, aetheria-release, session self-2026-10-09-morning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-09 13:42:38 +02:00
commit 439d488ac8
2 changed files with 159 additions and 0 deletions

View file

@ -6913,3 +6913,158 @@ the .NET runtime installed, which is the install the ruling removes).
| Layout buffer | `scene.aetheria_layout` | filled by Load or New Ship, edited by Rasterise, Flip Nose, Resize and hardpoint ops, undoable | the author; persisted only by Save |
| Grid placement | `aetheria.grid_origin` = [x, y, width, height] on the collection | written by Rasterise only, undoable | Rasterise |
| Pending ship | `aetheria.pending` = [name, reference] on the collection | set by New Ship, cleared by the Save that creates the file | New Ship, then Save |
## Flight control allocates thrust
Pass: Imagination, session `self-2026-10-09-morning`. Body: `GameCult/Aetheria` origin/master `80d2c7f3`,
detached worktree, removed at the end. Ruling: `flight-control-allocates-thrust` (operator words in its
`operator_quote`). Prior art: `F:\Projects\aetheria-thrust-allocation-prior-art-2026-10-09.md` (Eyes; cited
as PA). This section supersedes the body facts, consumer audit and Cuts 2-3 of `docs/locomotion-cut.md`
(2026-09-22), which predate the aether drive's deletion (`2cfdc728`) and the LookDirection-to-Turn change.
That file's Cut 4 (AI heading planner) and Cut 5 (combat facing) are carried as follow-ups, not as specs.
### Body facts
- **TA1. The plant.** 2D translation plus kinematic yaw. `Thruster` is the only propulsor
(`Ship.cs:138-139`). Per unit throttle a thruster adds `-push * Thrust / Mass` to velocity along its mount
and turns `Direction` by `Torque * Thrust * TorqueMultiplier / Mass` rad/s (`Thruster.cs:127-130`).
`Torque` is the sine of the moment arm (`Thruster.cs:65-70`). `Axis` saturates to [0,1]
(`Thruster.cs:46-50`). Authored `TorqueFloor` 0.5, `TorqueMultiplier` 0.1 (`Settings.asset`, read by
the probe through `AuthoredSettings`).
- **TA2. The mixer and its satellites** (`Ship.cs` at `80d2c7f3`): bucket sets `:33-40`, built in
`Activate` `:97-116` from mount rotation and from `Torque` against `TorqueFloor`; aggregates `:51-60`;
`TurnTime` `:62-67` (no caller); `ItemDestroyed` pruning `:121`, `:124-134`; `RecalculateThrust` and six
`Recalculate*` `:150-227`; the mixer `:236-259` (strafe trim with an unguarded division by the trim
count at `:237`, `:246`). Readers outside `Ship.cs`: the aggregates only in
`EvasionTermTests.cs:708-709` and `:997-1056`; `TurnTime` none; `IAnalogBehavior`
(`Behaviors.cs:96-99`) only `Thruster.cs:40` and a comment at `Entity.cs:2343`. `Thruster.Manoeuvre`
(`Thruster.cs:98-114`) is the envelope's per-thruster term and the only other `TorqueFloor` reader in
source (`:109`); tests read the floor at `EvasionTermTests.cs:331-332,383-384` and
`SteeringTests.cs:304-305`.
- **TA3. The Duel ship, probed** (scratch test in `RunStartTests`, Yggdrasil job, CultMath `614fd445`,
2026-10-09). Longinus, mass 4235, four thrusters. Per unit throttle (starboard m/s^2, forward m/s^2,
clockwise deg/s):
| Thruster | Cell, mount | Thrust | Column |
|---|---|---|---|
| Large Drive | (1,0) Reversed | 228441 | (0, 53.94, +49.24) |
| Large Drive | (3,0) Reversed | 259158 | (0, 61.19, -55.86) |
| Talaria | (2,14) CounterClockwise | 123144 | (+29.08, 0, +166.26) |
| Talaria | (3,14) Clockwise | 92312 | (-21.80, 0, -124.63) |
The two drives share a design and a lot quality (0.300) and still differ by 13% in live thrust, so
held forward thrust is a 6.6 deg/s turn. The two Talarias have equal torque-to-sideways ratios, so
together they cannot make a pure torque: any turn on this hull pushes it sideways.
- **TA4. Allocation probe** (same job; `player.Update` stepped alone at 1/60 s; a scratch hook replaced
the mixer with a `BoundedLeastSquares` solve; rows normalised per axis by the box extreme, ridge 1e-3;
"prio" is a yaw row weight of 1000; heading in degrees, clockwise positive; axes are the four throttles
in the table's order):
| Case | mixer | WLS w=1 | WLS w=3 | prio |
|---|---|---|---|---|
| forward, intact, 2 s heading | -12.09 | -6.41 | -1.38 | **0.017** |
| forward, strong drive off, 2 s | 97.41 | 59.06 | 14.24 | **-0.014** (axes .92 0 0 .36) |
| forward, weak drive off, 2 s | -109.65 | -56.22 | -11.47 | **0.026** (axes 0 .90 .29 0) |
| forward, nose Talaria (2,14) off, 2 s | -12.09 | -8.83 | -2.97 | **0.012** |
| strafe right, 2 s | 0 (no strafe at all) | 181.39 | 39.29 | **0.023** (axes 0 .85 .28 0) |
| forward + Turn 1, mean deg/s | 159.9 | 94.2 | 189.0 | **214.9** (axes 1 0 1 0) |
| Turn 1 in place, mean deg/s | 166.1 (axes 0 0 1 0) | 97.8 (.57 0 .42 0) | 188.9 (1 0 .84 0) | **214.9** (1 0 1 0) |
| Turn -1 in place | -124.6 | -81.8 | -158.8 | **-179.6** |
| Turn 1, Talaria (2,14) off | 0 (stuck) | 12.4 | 36.8 | **48.8** (drive only) |
| intent zero | 0 | 0 | 0 | 0 |
Box extremes: clockwise 215.5 deg/s, counter-clockwise 180.5 deg/s, forward 115.1 m/s^2, starboard
29.1, port 21.8. A yaw-first solve holds heading to hundredths of a degree whatever is lost; a weighted
solve at the old map's default (w = 3) leaves a drift the ruling forbids. Solve cost: 14.2 us mean
(including per-call allocation, n = 4), at most 11 active-set iterations over 2000 random intents.
- **TA5. The Execute gate drops small balancing throttles.** `Thruster.Execute` and `PowerRequest` act only
above throttle .01 (`Thruster.cs:87,124`). Probe: weighted solve, full forward, 10 s: heading -7.49 deg
with the gate, -4.56 without; forward 0.3, 10 s: -0.213 deg with, -0.028 without. A balancing throttle
below .01 on a 166 deg/s Talaria leaves up to 1.7 deg/s unbalanced.
- **TA6. A power-gated column deadlocks.** `PowerRequest` is proportional to the throttle
(`Thruster.cs:87`) and `PowerBus.Step` grants after `Ship.Update` (`Entity.cs:1463`), so a column zeroed
for `PowerSupply <= 1e-4` gets throttle 0, requests nothing, and is never granted. The column's thrust is
therefore `EvaluateNominalPower(Thrust)` (full grant; heat, durability, quality and modifiers live,
`Entity.cs:1856`) and its liveness is `Item.Active` only. A browned-out thruster still under-delivers
after the solve (follow-up `allocator-brownout-columns`).
- **TA7. The Duel arena moves the player by itself.** With forward thrust the player's velocity swings
between about +28 and -28 m/s along the nose every ~60 ticks with no `VelocityLimit` clamp (limit 100,
drag 0.1), while intent zero leaves it at rest. Translation measured in the Duel is not thrust; tests that
measure translation use `RestoredHullsTests.BuildThrustedShip`. Cause not traced (the zone's field at
the spawn is the first suspect).
- **TA8. CultMath 0.3.0 re-rolls generated content.** Full suite at `80d2c7f3` with only
`CultMathRevision` moved to `614fd445` (tag `cultmath-unity-v0.3.0`): 1057 pass, 5 fail.
`TutorialGalaxy_unchanged` (faction homes differ), three `RestoredHullsTests` that pick zones by name
(`RestoredHullsTests.cs:624,740,807`: "EAC-7089", "EAC-2733" no longer exist) and
`ACoastingAgileShipIsEasierToHitThanTheSameShipJinking` (hits 95 jinking against 78 coasting; its target
is `stage.Generated("Djinni")`, `EvasionTermTests.cs:520`, so the target's fit follows the galaxy). The
cause is 0.3.0's breaking `snoise` kernel (its CHANGELOG), read in C# by `Settings.cs:72-73` (galaxy
cloud density), `GlobalData.cs:30` and `Environment.cs:132,144`. Shaders use the GPU Noise plugin, not
CultMath (`Background.shader:39`), so nothing on the GPU moves. Every other DemoTerminus test passes.
Between the tags CultMath also adds `phacelle` (MPL-2.0 files), `cellular`, `smin_grad`, the `*_grad`
noises and `BoundedLeastSquares`; no existing signature changes.
- **TA9. The pin guard.** `Directory.Build.targets:21` conditions `VerifyCultLibRevision` on a
backslash-spelled project identity; `docs/mining-cut-refresh.md:155-157` infers it never fires on Linux.
Yggdrasil's `~/eureka-verify/pins` holds `cultlib-45c2f40` and `cultlib-6d5e209` only;
`~/eureka-verify/repos/CultLib.git` contains `614fd445`.
### Authority map
- **Owner:** `ThrustAllocator` (new, one per `Ship`, stepped in `Ship.Update`) turns intent into every
thruster's throttle.
- **Inputs:** intent (`MovementDirection`, or full forward while `Entity.ThrottleLocked`; `Turn`), and one
column per thruster: its body push and turn per unit throttle at full grant, zero when the item is not
`Active`.
- **Outputs:** `Thruster.Axis` for the tick, read only by `Execute`, `PowerRequest` and presentation.
- **Derived:** the box extremes (what intent is a fraction of) are derived from the columns each tick; the
envelope becomes the same extremes (cut `envelope-from-columns`). The warm start is command state, never
truth.
- **Demoted or dead:** the bucket sets, aggregates, strafe trim, `TurnTime`, `RemoveThruster` pruning and
`IAnalogBehavior` are deleted. `TorqueFloor` is no longer an owner of anything; it dies with
`Thruster.Manoeuvre`. A thruster's capability is no longer classified by mount or by a floor; it is its
column.
- **Forbidden writers:** any `Axis` write outside `Ship.Update`; any rule that classifies a thruster as
"for" rotation or translation; any second solve or fallback mixer; any liveness set kept across ticks.
- **Shared paths:** player helm, agents, the throttle lock and tests all reach the throttles only through
intent.
### Model page rows (changed)
| Kind | Named by | Over time | Decides |
|---|---|---|---|
| Thruster column (runtime) | the `Thruster` behaviour | rebuilt every tick from live stats and `Item.Active` | `ThrustAllocator` reads; item state stays `EquippedItem`'s and `PowerBus`'s |
| Allocator warm start (runtime) | the ship's `ThrustAllocator` | last tick's throttles; reset when the thruster count changes | `ThrustAllocator`; never saved |
| `GameplaySettings.TorqueFloor` (authored) | field name | deleted by `envelope-from-columns`; the `Settings.asset` line is left for Unity to drop (it lands in `AuthoredSettings.Unplaced`) | nobody after the cut |
### Rationale
- **Yaw before translation for a hold.** The ruling forbids yaw from asymmetry, and TA4 shows a weighted
solve cannot deliver that at any weight that also lets a turn trade: holding is exact only when the yaw
row dominates. Prior art agrees for spacecraft-like plants: MechJeb weights torque 200 times translation,
and the Space Engineers allocation proposal serves rotation authority first (PA A3). Multirotors do the
opposite (PX4 and ArduPilot sacrifice yaw, PA A2) because yaw is not their stability axis. Translation
first was rejected: it reproduces the mixer's drift. What gives under saturation is translation, so a
damaged ship crabs rather than spins; the operator expected exactly that ("crab walking just became
optimal", `docs/locomotion-cut.md:25-29` at `80d2c7f3`).
- **What a full turn spends is the operator's call** (question `turn-authority`), because the operator's
2026-09-22 words on rotation demand (`docs/locomotion-cut.md:82-87` at `80d2c7f3`) and TA4's numbers pull different
ways.
- **One solve with a large yaw weight, not two sequential solves.** The large-weight form approximates
sequential least squares (PA A1, Schofield) with one call, deterministic and allocation-free; a yaw
weight of 1000 against a ridge of 1e-4 keeps the conditioning near 1e5, inside the range the solver
documents as exact (`BoundedLeastSquares.cs` remarks).
- **The ridge** makes the optimum unique, so the warm start never changes the answer and two equal
thrusters share a demand. It costs 0.01% of full thrust.
- **No actuator interface.** One kind of actuator exists. The allocator takes columns and returns
throttles, so a vectored row (follow-up `vectored-thruster-rows`) is more columns: a row tilting within
+-theta becomes an axial variable in [0,1] and a lateral variable in [-sin theta, sin theta], and its slew
rate narrows the lateral variable's bounds each tick. Bounds stay boxes, which is what the solver takes;
the row maps its two solved variables back to a throttle and an angle.
- **The pin bump is its own cut.** The solver exists only from 0.3.0, and 0.3.0 re-rolls generated content
(TA8). Backporting the solver to a 0.2.5 was rejected: CultLib releases from main, and a branch release
would carry the old noise kernel's seam bug forward. `no-save-compatibility-before-players` covers the
re-rolled galaxy.
- **Order.** `cultmath-0-3-pin`, then `thrust-allocator-core`; `throttle-lock` lands on the mixer as
specified, then `thrust-allocator` deletes the mixer, keeps throttle-lock's one intent line as the
allocator's input and rewrites throttle-lock's two mixer-shaped tests in allocator terms. One owner of
`Ship.Update`'s thruster block at every step.

View file

@ -2,6 +2,10 @@
Date: 2026-09-22
Superseded on 2026-10-09 for its body facts, consumer audit and Cuts 2-3 by `docs/aetheria-release-map.md`,
section "Flight control allocates thrust": the aether drive was deleted and `Ship.Turn` replaced `LookDirection`
as the facing command after this was written. Cuts 4 and 5 remain design input only.
Status: target and cut map, from an Imagination pass revised on 2026-09-22. Cut 1 (the Longinus and Djinni hulls,
five thruster designs, and tutorial entrance station rule) landed on `codex/locomotion-cut1` and was merged into
`codex/fire-control-12` at `e160bbe5` on 2026-09-26. Cuts 2 to 5 remain target work. The operator's Q1, Q2,