Merge eureka/aetheria-fauna-target-r3 (docs only)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
b285f45c9f
86 changed files with 1850 additions and 30 deletions
|
|
@ -267,7 +267,7 @@ Probed at `1746e8bd`.
|
|||
`aetheria-fauna:question:fauna-placement`, `:fauna-flow-coupling`, `:flow-moves-what`, `:flow-slope-terms`. The
|
||||
options and recommendations are in the mind; the research for the first is the placement prior-art file.
|
||||
|
||||
### Cut sequence (r2, one line each; each is mapped when it is about to run)
|
||||
### Cut sequence r2 (superseded by the r3 sequence below)
|
||||
|
||||
1. `fauna-flow-field` (cut spec admitted): the flow's owner moves into `ServerShared` (`FlowField`, `Zone.Flow`), the
|
||||
shader lowers it from zone time, and a Unity parity check pins HLSL to C#.
|
||||
|
|
@ -303,3 +303,173 @@ options and recommendations are in the mind; the research for the first is the p
|
|||
24. `flow-slope-gradient-parity`: the brush-rendered heightfield and `Zone.GetHeight` agree, so the slope term can
|
||||
be switched on (`flow-slope-terms`).
|
||||
25. `orbit-sync-helper`: the generic flying aid that holds a ship on the local orbital flow. Waits on an operator yes.
|
||||
|
||||
## r3: placement is a closed form evaluated at run time
|
||||
|
||||
Written by Imagination (agent imagination-aeth-fauna-r3, session self-2026-10-14-ag) against `origin/master`
|
||||
`b836af48`, with the flow owner read at `eureka/aetheria-fauna-flow-field` `5f5a1e4d`. The operator ruled
|
||||
`fauna-placement-ruled` against `physarum-at-generation`: placement is "deterministic based on the gravity slope and
|
||||
flow that makes nice patterns", because "the level is constantly changing". The target is revised to r3 (Placement
|
||||
law, invariants `fauna-placement-closed-form`, `-story`, `-nested`, `-no-pop`). The B20 probe and the r2 rationale for
|
||||
a generation pass stand as evidence of what the slime look is; the generation pass itself is dropped. Prior art for
|
||||
the closed form: `docs/research/2026-10-14-fauna-placement-closed-form.md` (`CF:<line>`).
|
||||
|
||||
### Body facts (r3)
|
||||
|
||||
| # | Question | Result |
|
||||
|---|---|---|
|
||||
| B24 | Does the gravity field move? | Yes, in closed form of zone time. Orbit positions are `OrbitData.Evaluate(frac(_time / Period + Phase)) * Distance` plus the parent's position, cached per tick (`Zone.cs:476-497`). `GetHeight` subtracts each gas giant's `RadialWaves(..., _time * speed)` (`Zone.cs:645-655`), and `Sun` derives from `GasGiant` (`Zone.cs:771`), so the sun's well ripples too; `GasGiantData` defaults give wave speed x8 (`ZoneData.cs:199`). The slope at a point is a function of `_time`, and nothing in `GetHeight` can be evaluated at another time without a time parameter. |
|
||||
| B25 | Which pointwise closed form makes the slime look? (scratch probe, not committed) | A pure-Python transliteration of `FlowField.Global`/`Slope`/`Scroll` at `5f5a1e4d` and `Zone.GetHeight`/`GetNormal`/`PowerPulse`/`RadialWaves` at `b836af48`, on the B20 zone (sun, four planets, a gas giant; radius 2000; live flow scale 512, amplitude 15, scroll speed .025), 256² grid at t = 100 s, float64 (C# is float32). Density images were written per candidate, member-dot images under fixed per-candidate thresholds (jittered 16 u lattice, about 49k candidates), and correlated with the B20 Physarum maps (64x32 blocks). Results follow in the next rows. Scripts, images and how to rerun: `docs/research/2026-10-14-fauna-habitat-probe/` (README there). These rows' member counts normalise density by its zone maximum, which the law may not do (CF:56); B25g gives the counts under the spec's law. The first `members_c6_band_soft_windrows_swirl.png` was a mislabelled render and is not kept. |
|
||||
| B25a | Slope band alone (`c0`: the operator's `Deposition(slope)`, zero past slope .8) | Rings on every planet's shoulder, a bright bridge where two shoulders meet; correlation with Physarum A .517 (B .483), the highest of all candidates. 1,435 members. Changes 3.3 % of members in 1 s and 20 % in 8 s from the wells' ripples alone (B24). |
|
||||
| B25b | Band times flow convergence (`c1`, gain 3; `c6`, gain 1.5, slope terms on: swirl 6, slide 1.5) | Langmuir-windrow analogue (CF:17-18). The band keeps its rings, cut by the flow's convergence into a crisp triangular texture: `Tri3D` is piecewise linear (`tri`), so its derivatives are piecewise constant and the convergence field is a tiling of hard-edged triangles and diagonal streaks. `c6` correlation .464 (B .449); `c1` .428. Members gather in clumps on the shoulders with thin lines between wells. Under continuous evaluation 20-22 % of members change in 1 s and 89-94 % in 8 s: the flow scrolls a full first-octave phase in 40 s (1 / .025). Convergence std over the zone is .147 /s live and .151 /s with the slope terms on. |
|
||||
| B25c | Band times flow-along-contour (`c5`, narrow; `c7`, wide) | Members where the flow runs along the well's contour (neither climbing nor falling). Fine scalloped arcs, more organic than `c6`, but sparse (94 members narrow, 395 wide) and unstable: 42-134 % of members change in 1 s. `c7` correlation .480. |
|
||||
| B25d | Okubo-Weiss strain (`c2`) and gravity saddles (`c3`, `c4`) | `c2` (band times sigmoid of W, CF:11) correlation .507, texture like `c1`. `c3` (negative Hessian determinant of the height: outer shoulders and saddles between wells) makes clean rings with dark gaps and no links, .377. `c4` (`c3` times convergence) .316. |
|
||||
| B25e | Does a presence fade on the density stop pops? (`docs/research/2026-10-14-fauna-habitat-probe/pop_probe.py`) | No. `c6` with a fade band of .1 around each threshold, 600 shoulder candidates, 180 steps of 1/60 s from t = 100: the density moves by up to .267 in one step, so presence moves by up to .35 in one step (4 steps over .25; .56 with a band of .05). The same with the slope terms off, so the jumps are the global `Tri3D` term's: its convergence changes almost discontinuously in time at some points. |
|
||||
| B25f | Does reading the flow term at the member's own cycle start stop them? (`docs/research/2026-10-14-fauna-habitat-probe/pop_cycle.py`) | Yes. Band at now, convergence at the member's cycle start (period 8 s, hashed phase), same fade and candidates: presence moves by at most .0003 per step within a cycle, 0 steps over .25; the flow term changes only at the 221 cycle turns, where `fauna-flow-coupling-ruled` already fades the member. The slope band moves smoothly (B24's ripples are continuous). `FlowField.At` takes a scroll, and `FlowField.Scroll(settings, t)` is pure in t, so the flow at a past zone time needs no new owner. |
|
||||
| B25g | The options under the spec's law (`habitat_options.py`, `pop_maps.py`) | Absolute density, fixed constants of `cut-fauna-habitat-placement.r1`, candidate thresholds .1 + .9h, fade .1, slope terms on, 49,083 candidates at 16 u. Hosted at t = 100 and changed under continuous evaluation in 1 s / 8 s: band-only 4,388, 2.0 % / 12.2 %; band-times-convergence 2,235, 10.3 % / 47.1 %; band-times-strain 2,721, 7.9 % / 31.3 %; band-times-contour-flow 2,307, 15.8 % / 49.2 %. No-pop, convergence law, 5,607 shoulder candidates, 60 steps of 1/60 s, largest presence step with cycle turns excluded and members moving > .1 in a step: presence-fade .33, 2 members; cycle-sampled-flow .0010, 0; cycle-sampled-all .0000, 0. Images: `option_density_<option>.png`, `option_members_<option>.png`, `option_nopop_<option>.png`. |
|
||||
| B25h | Is the triangular lattice in the convergence the law's or the probe's? (`lattice_probe.py`) | The law's. Along a 2,048 u transect at 0.5 u spacing, all 40 of the 40 largest convergence jumps lie within 1 u of a kink line predicted from `tri`'s arguments in `FlowField.Tri3D` (both octaves; 53 lines cross the transect). The derivative step does not make it: convergence with step 6 and step .5 correlates .964. Rounding the inputs to float32 moves the flow by at most 5.6e-9 u/s (arithmetic stays float64, so this bounds input rounding only). The shader never renders the flow's derivative; it advects nebula noise and stardust by the flow's value, which is continuous, so the lattice is not visible in game today. A density driven by convergence would carry it into member placement: crisp in a density render, a soft patchiness at the member density of `option_members_band-times-convergence.png`. |
|
||||
| B26 | Is Sol a zone the code can tell apart? | No. No `ServerShared` code names Sol (grep of `Sol\b` outside `Solar`/`Solid`/`Solve`); a zone's seed is `GalaxyZone.Name.StableHash()` (`Zone.cs:144`, `CombatSeed`). `fauna-none-in-sol` needs the per-zone environment of `sol-flatspace-zone`. |
|
||||
| B27 | Is there placement code to cut? | No. Nothing under `Assets/Scripts/ServerShared` or `tests/Aetheria.Shared.Tests` names habitat or fauna at `b836af48`. The generation pass of r2 was never built. |
|
||||
|
||||
### Decisions made here, not forks
|
||||
|
||||
- **Candidates are a hashed jittered lattice with fixed per-candidate thresholds**, not a blue-noise set. It is the
|
||||
simplest deterministic, position-stable construction (CF:42 [M]); it is HZD's shape (CF:33 [V]), and nesting
|
||||
follows from the fixed thresholds (CF:40). Blue noise (Wei 2008, recursive Wang tiles, CF:42-43 [S]) buys evenness
|
||||
the jitter already roughly gives; it can replace the lattice later behind the same `Candidate` function.
|
||||
- **The hash comes from `SimulationDice.Mix`** on the zone seed, a new `HabitatStream` and the cell, so placement
|
||||
uses the one dice owner (B9) and no `UnityEngine.Random`.
|
||||
- **The convergence is normalised by a fixed scale in the settings** (.15 /s, B25b), never by a zone-wide statistic,
|
||||
which would make the law path-integrated (CF:56).
|
||||
- **Settings are a record, bound per class later.** `HabitatSettings` lands with `Habitat`; the krill cut binds a
|
||||
record to the krill field kind. Per-class tuning by eye follows the `flow-slope-terms` pattern.
|
||||
- **Sol** waits for `sol-flatspace-zone` (B26), which makes Sol host no fauna kind.
|
||||
|
||||
### Questions (admitted in the mind, raised in `cut-fauna-habitat-placement.r1`)
|
||||
|
||||
`aetheria-fauna:question:habitat-density-driver` (which flow quantity shapes the band; the images above are its
|
||||
evidence) and `aetheria-fauna:question:habitat-no-pop` (how a member is carried through a change of hosting).
|
||||
|
||||
### Cut sequence (r3, one line each; each is mapped when it is about to run)
|
||||
|
||||
1. `fauna-flow-field` (spec admitted; third Soul pass): the flow's owner in `ServerShared` (`FlowField`, `Zone.Flow`).
|
||||
2. `fauna-habitat-placement` (spec admitted, waits on 1 and the two questions): `Habitat`, `HabitatSettings`,
|
||||
`Zone.HabitatDensity`/`HabitatPresence`, fixed-threshold candidates, tests and the cost measured.
|
||||
3. `fauna-krill-swarms`: fauna fields derived per zone (not generated), krill members at hosted candidates, advected
|
||||
cycles (`fauna-flow-coupling-ruled`), the index provider and `ChunkVisible` through presence. Waits on 1, 2.
|
||||
4. `fauna-krill-catalog`: AetherDb authors the Krill field kind and its `HabitatSettings`.
|
||||
5. `fauna-field-presentation`: fauna kinds pick their own look and fade by presence; `SectorRenderer` counts belts only.
|
||||
6. `fauna-minnow-schools`: minnows, fast milling, a habitat on the krill density.
|
||||
7. `fauna-anchors`: field frames that move across the flow under class laws (`fauna-motion-model-ruled`).
|
||||
8. `fauna-sharks-and-whales`: sharks by krill density (ideal free distribution, CF:49-50), whales round gas giants.
|
||||
9. `fauna-disturbance`: sparse reaction records; schools scatter, sharks veer; reactions are tells.
|
||||
10. `fauna-attacks-strike`: sharks strike and minnow schools scour, through the ordinary damage path.
|
||||
11. `fauna-attacks-signal`: whale song as noise in the shared signal space (with aetheria-ew), drifter discharge.
|
||||
12. `fauna-harvest-goods`: catalog goods per kind and yields through the economy's yield table; fields refill.
|
||||
13. `fauna-harvest-gear`: resonance fishing, nets and trawls as gear with makers.
|
||||
14. `fauna-void-spiders`: nets across trojan points, snaring and tethering; silk.
|
||||
15. `fauna-gas-giant-drifters`: a cloud-deck class bound to gas giants.
|
||||
16. `fauna-edge-giants`: larger classes far from Sol; the hosted class mix scales with distance from Sol.
|
||||
17. `fauna-springs`: wormhole mouths raise habitat density (a pointwise term of distance to a mouth).
|
||||
18. `fauna-genre-uses`: cultivators, necrotech and mecha uses, each with its owner's campaign.
|
||||
19. `sol-flatspace-zone`: a per-zone environment override; Sol is cloudless, flowless and hosts no fauna kind.
|
||||
20. `fauna-returns`: what an observer is shown (`fauna-distance-ruled`, `fauna-disguise-path-ruled`).
|
||||
21. `fauna-in-ew-blend`: fauna background kinds with motion-law envelopes in aetheria-ew's judge.
|
||||
22. `fauna-krill-obscure`: dense krill thin sight lines through `Zone.Obscuration`. Waits on an operator yes.
|
||||
23. `flow-moves-matter`: floating items, mines and vented clouds drift with the flow (`flow-moves-what-ruled`).
|
||||
24. `flow-slope-gradient-parity`: the brush-rendered heightfield and `Zone.GetHeight` agree (`flow-slope-terms`).
|
||||
25. `orbit-sync-helper`: the generic flying aid that holds a ship on the local orbital flow. Waits on an operator yes.
|
||||
|
||||
## r4: placement is a trophic DAG
|
||||
|
||||
Written by Imagination (agent imagination-aeth-trophic-dag, session self-2026-10-14-ag) against `origin/master`
|
||||
`7633408f` (the flow owner `FlowField`/`Zone.Flow` is on master) and CultLib `main` `6c902fd4`. The operator ruled
|
||||
`habitat-trophic-dag`: none of the r3 density drivers gives "the filament feeling"; placement is "a few layers of
|
||||
ecological abstraction, all deterministic, to author the various species with", whose trophic levels "form a DAG
|
||||
which we can evaluate in either shader passes or in C#". `voronoi-primitive` adds the weighted Voronoi she likes.
|
||||
Self relayed a correction on 2026-10-14, her words: "Parity between shader and C# is CultMath's whole raison
|
||||
d'etre". The target is revised to r4 ("Placement law"). The r3 rows B24-B27 and the habitat probe stand; B25's
|
||||
candidates are superseded as designs. Prior art: `docs/research/2026-10-14-trophic-dag-filaments-prior-art.md`
|
||||
(`TD:<line>`, tags [V] read, [S] search summary, [M] memory or derivation). The species the graph authors come from
|
||||
the lore branch `eureka/aetheria-fauna-lore` `74c7e212`, note `Open Water` (`OW`).
|
||||
|
||||
### Body facts (r4)
|
||||
|
||||
| # | Question | Result |
|
||||
|---|---|---|
|
||||
| B28 | What does CultMath already give a node library? | Integer PCG hashes `pcg`, `pcg3d`, `pcg4d` (Jarzynski and Olano), "integer-only, so dxc and C# agree bit for bit" (`packages/cultmath/src/CultMath/math.cs:393-426`, `shaders/CultMath.hlsl:396-426`). `cellular(float3)`: Worley F1 and F2 - F1 with analytic gradients, jitter from `pcg3d` of the integer cell, 3D, unweighted (`math.cs:849-975`, `CultMath.hlsl:440-503`). `smin_grad` (`math.cs:831-847`, `CultMath.hlsl:428-437`). `distance_to_segment` in C# only (`math.cs:319`; no HLSL twin). No weighted cellular, no diagram over explicit weighted sites, no site-graph gate. |
|
||||
| B29 | How does CultMath own parity? | Two legs, one owner each (`packages/cultmath/docs/design.md:202-206`): HLSL against C# by the mirror test, which compiles the text of `CultMath.hlsl` as C# and compares float32 bit patterns (`design.md:118-124`), plus a clean `dxc -T lib_6_3` compile (`design.md:114-116`); GLSL against C# by golden fixtures run on a device. The mirror proves the composition in the text, not that a GPU agrees (`design.md:119-121`); device HLSL runs are consumers' today (Asura `gpu-snoise-parity`; Aetheria `FlowFieldParityVerify`). CultMath ships a Unity compute smoke (`packages/cultmath/scripts/unity-smoke/`). Aetheria pins `cultmath-unity-v0.4.1` (`Packages/manifest.json`). |
|
||||
| B30 | Does Aetheria already keep a math twin outside CultMath? | Yes. `FlowField` uses CultMath's `math`, but its `Tri`, `Tri3` and `Tri3D` are Aetheria functions (`FlowField.cs:66-72`) whose HLSL twin is `Volumetric.cginc`, pinned by Aetheria's own `FlowFieldParityVerify` (`RelativeTolerance` 1.5e-4). That is a second parity owner beside CultMath. |
|
||||
| B31 | What is parked in CultMath that a DAG would use? | The tape: steps 2-5 (expression graph lowered to bytecode with C# evaluators, pruning, an HLSL interpreter, meshing) are parked until Asura is stable (`docs/cultmath-tape-target.md:3-8`, ruling `tape-target-unparks-when-asura-stable`). "Truth and lowerings" (`:84-92`): the field is a typed CultCache document, and the CPU evaluator and the HLSL interpreter are lowerings of it. Step 1 (intervals) and affine forms are on main. |
|
||||
| B32 | Do the candidates make filaments? (`docs/research/2026-10-14-trophic-dag-probe/`, README there; wells orbit, slope terms on, t = 0) | `skein` draws strands between neighbouring wells, bent by the flow (`density_skein.png`); `lattice` draws a fine cellular web, dense on the shoulders, dim elsewhere, with no long links (`density_lattice.png`); `skein-web` draws both: strands from well to well with a web hanging off them (`density_skein-web.png`, `layer_L2-krill.png`). Correlation with the B20 Physarum map at 64x32 blocks (this measures the envelope, not filaments: a block is 62 u, wider than a strand): band .512, lattice .359, skein-web .296, krill .277, skein .225, slack .171. Hosted krill at 16 u: band 2,075, skein-web 1,475, skein 639, lattice 419. |
|
||||
| B33 | Does the site graph pop as wells orbit? | The hard Gabriel graph over the six bodies changes its edge set 29 times in 1,400 s (one gas-giant orbit), each change a strand appearing or vanishing whole (TD:33 [M], now measured). The soft gate (empty-disc margin through smoothstep(-.15, .15)) moves by at most .00075 per 1/60 s. |
|
||||
| B34 | Do the candidates pop under the krill carriage? (`run1.log`, `win.log`, `cyc.log`; fade .1, 60 steps of 1/60 s from t = 100, flow read at the member's cycle start, cycle turns excluded) | Largest presence step: band .0035, slack .013, skein .054, lattice .124 (8 steps over .1), skein-web .493 (8 steps). The lattice steps are not discontinuities: a traced member's web value rises .03 per step for 5 steps, a thin weighted border sweeping a fixed candidate. Widening the search window to 7x7 changes nothing; lowering the weight boost to .6 removes the lattice steps (.047); reading the lattice drift at the member's cycle start, as the flow is read, removes them at full boost: lattice .011, skein-web .066, 0 steps over .1 (`cyc.log`). |
|
||||
| B35 | How fast do hosted sets turn over? | Members changed in 1 s, all terms / flow frozen at t's cycle (wells only): band .032 / .032; skein .244 / .106; lattice .594 / .449 (.262 with the drift at cycle start); skein-web .305 / .186 (.216 with the drift at cycle start); slack .089 / .089. The strands ride their wells at about 6 u/s across 16 u candidates, so a tenth of strand members change each second through the fade: motion, not popping (B34). |
|
||||
| B36 | What does a stencil predator cost? (Python, one evaluation) | krill .26 ms, minnows 3.19 ms (9 krill), sharks 29.3 ms (81 krill): each ring-stencil level multiplies the cost by 9. Hosted: minnows 274 of 5,432 candidates at 48 u; sharks 19 of 755 at 128 u; lanterns 36 of 3,063 at 64 u. |
|
||||
| B37 | What does a diagram over the bodies draw, per metric? (`docs/research/2026-10-14-body-voronoi-probe/`, README there; sites = the six bodies, weight = each well's Gaussian width `sigma = r / sqrt(128)`, edge = first-order border distance through a 40 u Gaussian, warped as the strands) | `power` (`d^2 - sigma^2`): long, nearly straight edges spanning the zone between the bodies, a triangle of filaments (`edges_power.png`). `add` (`d - sigma`): long hyperbolic arcs, two near-parallel bars north of the sun (`edges_add.png`). `mult` (`d / sigma`): rings round the small bodies only; the sun's cell covers most of the zone (sigma ratio up to 4.7:1; the ring round a 274-mass planet is about 264 u) (`edges_mult.png`). `pot-newton` (equal `m/d`, i.e. multiplicative with weight = mass, CultLib `tape-register-model.md` 5a): rings of about 12 u, the sun swallows the zone. `pot-well` (equal depth on the zone's real well profile): rings like `mult`, plus dotted arcs where each well's compact support ends (r/2). `flow-aniso` (`mult`, across-flow distance 2.5x): edges shattered into short fragments, because the metric turns with the flow on the flow's 512 u scale. `lp05`: axis-aligned bars across the zone; `lpinf`: square cells round the small bodies. Correlation with the B20 Physarum map: mult-strands .388, power-strands .371, mult .335, flow-aniso .322, power .313, skein-web .296, add .263. |
|
||||
| B38 | Do they pop as the bodies orbit? (16 u krill, fade .1, 60 steps of 1/60 s from t = 100, flow warp and web drift at the member's cycle start, bodies now; `run2.log`, `run3.log`) | Worst presence step / members changed in 1 s / hosted: skein-web .014 / .205 / 1,501; power .029 / .312 / 3,817; power-strands .029 / .269 / 4,965; flow-aniso .037 / .300 / 4,090; pot-newton .048 / .299 / 689; add .090 / .338 / 4,077; mult .091 / .279 / 3,270; mult-strands .091 / .238 / 4,462; pot-well, lp05, lpinf 1.0 (1, 72 and 237 steps over .1). `mult`'s .091 is the web, not the edge: without the cell boost .023, without the alignment .055 (`diag2.log`); the cell weights are read from the moving edges now. |
|
||||
| B39 | Why did B34's reading pop more? (`run1.log`, `diag_edges.log`, `trace.py`) | `trophic_probe.py` reads only the flow's scroll at the member's cycle start; the slope terms (swirl 6, slide 1.5) read the wells now. The slope terms use the normalised slope direction, which turns quickly on flat ground as the wells move, and the 20 s warp is a lever on it: at a candidate on slope .019 the warped point moved 1.4 u per 1/60 s. With the whole warp at cycle start, the edges alone drop from .152 to .023 (mult) and .126 to .029 (power), and the ruled skein-web from .066 to .014. B34's skein-web number was this reading's. |
|
||||
| B40 | Where does the first-order border distance break? | Where the metric's gradient is singular or jumps: `lp05` (singular on the axes through each site), `lpinf` (jumps on the diagonals), `pot-well` (unbounded at the support rim): presence steps of 1.0. Also where the second-nearest site changes, the normaliser `|grad(F2 - F1)|` jumps: invisible inside the 40 u edge, visible in a 4x halo (power's web with no cell boost: .197-.301, `diag2.log`). A raw `F2 - F1` profile has no such jump but its width varies with the weights. |
|
||||
| B41 | Does the 5x5 lattice window hold the body-pulled weights? (`diag2.log`, 3,385 points) | Cell weights span .5-2.6 (5.2:1), and the alignment stretches across-edge distance up to 2x more. 5x5 against 7x7: mult differs at 1 point by .28; power and add at none. CultLib `lattice-site-weight.md` (`21281a39`) finds the window must grow with the weight ratio (5x5 off by .017 at about 12:1); the alignment is a query-point anisotropy, which does change winners, unlike a query-point scalar weight. |
|
||||
| B42 | What does one evaluation visit? | Every variant is pointwise over explicit sites; none needs a grid. Large layer: the 6 bodies (the two winners' gradient is analytic). Web: 25 cells, each reading the large layer at its centre (25 x 6), so 181 sites per evaluation; with strands 193 (6 soft-Gabriel edges, strand and halo); skein-web 187. Python: .36-.74 ms per evaluation. CultMath: `mult` and `power` are gap (2) as mapped; `add`, Minkowski and `flow-aniso` extend gap (2) with a metric parameter, `flow-aniso` also with a direction operand; `pot-well` is the zone's well profile as a per-site score, an Aetheria function, not a CultMath metric. The web's per-cell read of the large layer is the cell-centre weight placement of `cultmath-tapes:question:lattice-site-weight-source`. |
|
||||
|
||||
### Decisions made here, not forks
|
||||
|
||||
- **The layers are the lore's web.** `OW` gives energy budgets (starlight, heat-light, gravity waves, mind-light),
|
||||
a current with fronts, shear and slack, and the Skein: "a slime-mould web that grows toward food along the gravity
|
||||
slope, linking well to well". The filament layer is therefore the Skein, a species of its own, and the layers
|
||||
above read it. The DAG is donor-controlled: each level is a function of the levels below and never regulates
|
||||
them (TD:56 [M]); a predator that thins its prey closes a cycle, which needs iteration (TD:56), so it is out.
|
||||
- **Strands link wells; the wells' Voronoi borders are slack.** In the adhesion model, filaments are borders between
|
||||
void cells and the mass sits at the vertices (TD:38 [V]). Here the mass is the wells, so filaments between wells
|
||||
are the dual: edges of a site graph, the graphs the Physarum agents approximate (TD:9, prior doc). The wells'
|
||||
weighted Voronoi borders are where pulls cancel, which `OW` gives to the ambushers (lanterns, void spiders).
|
||||
With multiplicative weights the border round a small well is an Apollonius circle (TD:20 [M];
|
||||
`density_slack.png` shows the circles).
|
||||
- **The site graph's gate is soft.** B33: a hard Gabriel graph pops a whole strand 29 times per gas-giant orbit;
|
||||
the soft gate is continuous. The graph is computed per tick from the orbit positions `Zone` already caches per
|
||||
tick (B24), over the zone's bodies only; wormhole mouths and belts join as sites in their own cuts.
|
||||
- **Hunting radius is a stencil, or a widened kernel.** A predator reads its prey's density as the mean over a
|
||||
fixed ring of taps: pointwise, exact by definition, the same in C# and HLSL. It costs 9x per level (B36), so a
|
||||
stencil reads at most two levels down. Where the prey is a sum of Gaussian kernels (the Skein's strands) a
|
||||
predator reads the same kernels widened instead, the exact blur of a Gaussian profile (TD:78 [M]). Grid-only
|
||||
operators (blur of arbitrary fields, distance transforms, eikonal paths, jump flooding: TD:19, TD:78) are
|
||||
presentation-only, never sim-visible.
|
||||
- **Fast drift is read at the member's cycle start.** `habitat-no-pop-ruled` reads the krill's flow at its cycle
|
||||
start. B34 extends it, measured: the flow warp and the lattice web's drift are read at the member's cycle start;
|
||||
the slope band and the site graph, which move slowly and continuously, are read now and carried by the fade.
|
||||
- **Nodes are CultMath functions; Aetheria composes.** Per Self's relay of the operator. Each primitive the DAG
|
||||
needs is a CultMath function with an HLSL twin in `CultMath.hlsl`, pinned by CultMath's mirror test and dxc
|
||||
compile (B29). Aetheria's `Habitat` is a composition of those calls and owns no math twin. Gaps to fill in
|
||||
CultLib, not helpers in Aetheria: (1) a 2D weighted lattice cellular with the smooth-min blend (her node) on
|
||||
`pcg3d` cells and zone time; (2) a weighted diagram over an explicit span of sites (F1, F2 and first-order border
|
||||
distance, multiplicative and power weights); (3) the HLSL twin of `distance_to_segment`; (4) the soft Gabriel
|
||||
gate over a site span. Gap (5): CultMath has no parity path for a consumer's composition, only for its own text
|
||||
(B29), so a presenter that draws a composed layer (Skein glow, glim haze) waits on a CultMath-owned composition
|
||||
check rather than a second `FlowFieldParityVerify`. B30's `Tri3D` twin is the existing instance of the same split.
|
||||
- **The search window follows the weight range.** A multiplicatively weighted lattice node needs its window to
|
||||
cover the weight ratio (TD:20 [M]); the probe's 1.6x boost needed no more than 5x5 (B34). The CultMath node states
|
||||
the weight range it supports instead of trusting the caller.
|
||||
|
||||
### Questions (admitted in the mind)
|
||||
|
||||
`aetheria-fauna:question:skein-filament-construction` (which construction is the Skein; images per option in
|
||||
`docs/research/2026-10-14-trophic-dag-probe/`) and `aetheria-fauna:question:trophic-graph-tape` (hand-written CultMath
|
||||
compositions now and the tape later, or unpark the tape for this).
|
||||
`aetheria-fauna:question:body-diagram-metric` (which metric draws the large-scale Skein from the bodies, after the
|
||||
operator's sketch; images per option in `docs/research/2026-10-14-body-voronoi-probe/`, facts B37-B42).
|
||||
|
||||
### Cut sequence (r4, one line each; each is mapped when it is about to run; r3 lines 4-25 stand after line 7)
|
||||
|
||||
1. `fauna-flow-field`: on master (`FlowField`, `Zone.Flow`, `FlowFieldParityVerify`).
|
||||
2. `cultmath-trophic-nodes` (CultLib): gaps (1)-(4) above with HLSL twins, mirror tests and stated weight ranges;
|
||||
a CultMath Unity tag. Waits on both questions.
|
||||
3. `fauna-trophic-habitat` (Aetheria): the CultMath pin bump; `Habitat` composes environment, Skein, glim and krill
|
||||
in C# at candidates; per-species settings records; the r3 candidates and fade; tests for the r4 invariants; the
|
||||
per-candidate cost measured. Replaces r3 line 2. Waits on 2.
|
||||
4. `fauna-krill-swarms`: as r3 line 3, on 3's krill hosting.
|
||||
5. `cultmath-composition-parity` (CultLib): gap (5), a CultMath-owned check that a consumer's C# composition and its
|
||||
HLSL twin agree on a device; `FlowField`'s `Tri3D` moves under it. Waits on Self's scoping.
|
||||
6. `fauna-trophic-presenter`: the Skein and glim drawn in the nebula from the same composition. Waits on 3 and 5.
|
||||
7. `fauna-minnow-schools`, then predators by stencil and ambushers on slack (r3 lines 6-8), re-mapped as species of
|
||||
the DAG per `fauna-bestiary-open` and `OW`.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,40 @@
|
|||
# Aetheria spaceborne fauna: target
|
||||
|
||||
Campaign `aetheria-fauna`, target content r2 (r1 was admitted from the r0 draft at `f9705d32`; Self admits r2).
|
||||
Repo `GameCult/Aetheria`, working branch `master`, cut branches `eureka/aetheria-fauna-<cut>`. Written by
|
||||
Imagination (agents imagination-aeth-fauna and imagination-aeth-fauna-r2, session self-2026-10-13-ag2); r2 is
|
||||
against `origin/master` `1746e8bd`. Body facts and probes are in `docs/aetheria-fauna-map.md` (cited below as
|
||||
`B<n>`); prior art is `docs/research/2026-10-13-fauna-prior-art.md` (`PA:<line>`; `[M]` items are from memory and
|
||||
unverified) and `docs/research/2026-10-13-fauna-placement-prior-art.md` (placement). This is the target and the
|
||||
model page. Open forks are questions in the mind under `aetheria-fauna`.
|
||||
Campaign `aetheria-fauna`, target content r4 (r1 was admitted from the r0 draft at `f9705d32`, r2 by Self; r3 and
|
||||
r4 are admitted by Imagination). Repo `GameCult/Aetheria`, working branch `master`, cut branches
|
||||
`eureka/aetheria-fauna-<cut>`; the DAG's node primitives land in `GameCult/CultLib` (CultMath). Written by
|
||||
Imagination (agents imagination-aeth-fauna, imagination-aeth-fauna-r2, imagination-aeth-fauna-r3 and
|
||||
imagination-aeth-trophic-dag, sessions self-2026-10-13-ag2 and self-2026-10-14-ag); r4 is against `origin/master`
|
||||
`7633408f` and CultLib `6c902fd4`. Trophic DAG prior art: `docs/research/2026-10-14-trophic-dag-filaments-prior-art.md`
|
||||
(`TD:<line>`); its probe: `docs/research/2026-10-14-trophic-dag-probe/`. Body facts and probes are in `docs/aetheria-fauna-map.md` (cited below as `B<n>`); prior art is
|
||||
`docs/research/2026-10-13-fauna-prior-art.md` (`PA:<line>`; `[M]` items are from memory and unverified),
|
||||
`docs/research/2026-10-13-fauna-placement-prior-art.md` (placement by simulation) and
|
||||
`docs/research/2026-10-14-fauna-placement-closed-form.md` (`CF:<line>`; its tags: `[V]` read, `[S]` search summary
|
||||
only, `[M]` memory or derivation, unverified). This is the target and the model page. Open forks are questions in
|
||||
the mind under `aetheria-fauna`.
|
||||
|
||||
r4 applies `habitat-trophic-dag` (2026-10-14), after the r3 density drivers missed the look:
|
||||
|
||||
> I'm not getting the filament feeling. I think we need a few layers of ecological abstraction, all deterministic, to
|
||||
> author the various species with. The trophic levels should form a DAG which we can evaluate in either shader
|
||||
> passes or in C#
|
||||
|
||||
with `voronoi-primitive` (a weighted Voronoi node, her reference shader a sketch of the look) and her correction,
|
||||
relayed by Self: "Parity between shader and C# is CultMath's whole raison d'etre". Placement becomes a layered,
|
||||
donor-controlled trophic DAG whose filament layer is the lore's Skein, whose nodes are CultMath functions with HLSL
|
||||
twins, and which Aetheria composes ("Placement law" below; map section r4). The krill carriage of
|
||||
`habitat-no-pop-ruled` stands and is extended to every fast term (map B34).
|
||||
|
||||
r3 applies `fauna-placement-ruled` (2026-10-13), against the recommendation to run a Physarum pass at generation:
|
||||
|
||||
> Nothing we generate ahead of time would suffice, because the level is constantly changing. I'd imagine we can come
|
||||
> up with something deterministic based on the gravity slope and flow that makes nice patterns.
|
||||
|
||||
Placement is therefore a pure function evaluated when it is asked, never a generated and saved product. The
|
||||
invariant `fauna-placement-seeded` ("the saved zone pack is the authority") is replaced by the placement invariants
|
||||
below. r3 also carries the rulings admitted after r2: drift by advected cycles (`fauna-flow-coupling-ruled`), loose
|
||||
matter drifts with the flow and ships do not (`flow-moves-what-ruled`), the slope terms are turned on
|
||||
(`flow-slope-terms-ruled`), and presenters take their phase from zone time (`presenter-phase-from-zone-time`).
|
||||
|
||||
r2 applies the operator's rulings of 2026-10-13: fauna attack ships in various ways (`fauna-interaction-ruled`),
|
||||
are harvestable and prominent in Elysium (`fauna-harvest-ruled`), are placed to tell an ecological story shaped by
|
||||
|
|
@ -59,8 +87,8 @@ gather shows the shape of the wells and the currents, and where they are missing
|
|||
the standard a disguised ship must fly to (`disguise-held-by-flying`).
|
||||
3. **Each class has one motion law, legible and learnable.** An observer who watches a return for long enough can
|
||||
test it against the class's law. The laws are written to be told apart:
|
||||
- *krill* clouds ride the local orbital flow, as belt rocks do: their velocity is the flow at their distance. This
|
||||
is the drifting-insect tell (PA:9);
|
||||
- *krill* drift with the global flow: each member advects along the flow at its seed point for one cycle, fades
|
||||
and re-seeds (`fauna-flow-coupling-ruled`). This is the drifting-insect tell (PA:9);
|
||||
- *minnow* schools ride the flow and mill fast inside their school, so their jitter is quick;
|
||||
- *sharks* cut across the flow, patrol and sprint, and accelerate harder than anything else in the class list;
|
||||
- *whales* are slow, huge and hot, and swing in long arcs.
|
||||
|
|
@ -71,8 +99,8 @@ gather shows the shape of the wells and the currents, and where they are missing
|
|||
100,000 stepped bodies that read the zone force take 16 ms. Neither form scales to fauna counts.
|
||||
5. **One detection rule.** Fauna are seen through `Sensor.Gain` by way of `Entity.PassiveInfo`, as chunks are
|
||||
(`Entity.ChunkInfo`, B5). There is no second sensor model and no fauna-specific threshold.
|
||||
6. **Deterministic.** A fauna's state is a function of the zone's seed, its saved field data, zone time and sim
|
||||
events on fixed steps (`SimClock`, B9). It uses no `UnityEngine.Random`, no wall clock and no frame time. Any dice
|
||||
6. **Deterministic.** A fauna's state is a function of the zone's seed, its world laws, its bodies' orbits, zone
|
||||
time and sim events on fixed steps (`SimClock`, B9). Nothing about where fauna are is saved. It uses no `UnityEngine.Random`, no wall clock and no frame time. Any dice
|
||||
come from `SimulationDice` on a stream of their own.
|
||||
7. **The zone owns fauna; presenters read them.** No Unity callback writes fauna state. The renderer draws them as it
|
||||
draws belts, from a pose evaluated at zone time (`ZoneRenderer.cs:543-569`, B7).
|
||||
|
|
@ -81,19 +109,65 @@ gather shows the shape of the wells and the currents, and where they are missing
|
|||
(`Volumetric.cginc` `flow()`, B15) is evaluated by one `ServerShared` function of position and zone time from the
|
||||
zone's flow settings. Fauna drift with it. The shader lowers the same function from the same settings and time,
|
||||
and a parity check pins the two. No presenter accumulates its own flow time (B16).
|
||||
10. **Placement tells an ecological story and is decided once.** The generator places fauna from the zone's gravity
|
||||
and flow, deterministically from the zone seed, and saves what it placed with the zone pack. Fauna gather on
|
||||
the shoulders of wells and along links between them, not in the cores and not in flat space (placement probe,
|
||||
`docs/research/2026-10-13-fauna-placement-prior-art.md`). The method is the question `fauna-placement`.
|
||||
10. **Placement tells an ecological story, and the level writes it as it moves.** Where fauna are is a closed form
|
||||
of the gravity slope and the flow, evaluated at zone time whenever a query, presenter or sensor asks
|
||||
(`fauna-placement-ruled`). Wells orbit, gas giants and suns ripple their wells (B24), and the flow scrolls, so the
|
||||
pattern moves with the level: a placement generated ahead would go stale. Life follows the Skein, the
|
||||
slime-mould web strung from well to well, and the energy the wells give off; predators follow their prey, and
|
||||
ambushers wait in the slack between wells. Nothing gathers in the cores, and open flat space away from the Skein
|
||||
is nearly empty. The law is in "Placement law" below.
|
||||
11. **No fauna in Sol.** Sol is a single flatspace zone with no fauna inside it; the strangeness begins at its edge.
|
||||
12. **Harm and harvest go through existing owners.** A fauna attack is damage through the path a weapon's hit
|
||||
takes; a harvest yields items through the economy's yield path; neither gets a fauna-only damage or loot rule.
|
||||
|
||||
## Placement law
|
||||
|
||||
Where fauna of a species are is decided by a **trophic DAG**: layers of fields, each a deterministic function of the
|
||||
layers below it and of world facts, evaluated at a point at zone time when a query, presenter or sensor asks
|
||||
(`habitat-trophic-dag`). Pointwise fields of slope and flow alone draw bands and blobs (B25); filaments need an
|
||||
input that is not local, and the DAG takes it from the zone's bodies, which every point can read (map r4).
|
||||
|
||||
| Layer | Fields (the first web, from the lore's Open Water; the bestiary stays open) | Reads |
|
||||
|---|---|---|
|
||||
| 0 Environment | Slope and the operator's `Deposition(slope)` band, the steep-core mask (`Zone.GetNormal`); the flow (`Zone.Flow`); the zone's bodies: positions from the per-tick orbit cache, gravity radius, kind; energy kernels round them: starlight inside a sun's light radius, heat-light on warm well floors. Later sources join here: gravity-wave rings (whales), mind-light round crews and stations (moths), wormhole mouths (upwelling), wrecks (succession). | World facts only |
|
||||
| 1 Structure | **The Skein**: strands between neighbouring bodies (a site graph with a soft gate, each edge a Gaussian strand, bent by a Zel'dovich warp of the flow, TD:39 [V]) and a weighted lattice Voronoi web hanging off them (`voronoi-primitive`). **Slack**: the borders of the bodies' weighted Voronoi, where pulls cancel. Which construction is the Skein is the question `skein-filament-construction`. | Layer 0 |
|
||||
| 2 Producers | Glim: energy times the Skein's feeding. | 0, 1 |
|
||||
| 3 Grazers | Krill: the Skein times a saturating response to glim (Holling II, TD:55 [S]). Minnows: krill over a hunting ring. | 0-2 |
|
||||
| 4 Predators and ambushers | Sharks: minnows over a wider ring, on the band. Lanterns and void spiders: slack times krill over a ring. | 0-3 |
|
||||
|
||||
1. **Operators.** A sim-visible layer uses only: pointwise arithmetic, remap curves, smoothstep, min, max and smooth
|
||||
min, saturating responses; CultMath nodes (integer hash, weighted lattice cellular, weighted diagram over
|
||||
explicit sites, segment kernel, soft site-graph gate); a **stencil** (the mean of a lower layer at the point and a
|
||||
fixed ring of taps, at most two levels deep, 9x the cost per level, B36); and **kernel widening** where the prey
|
||||
is a sum of Gaussian kernels (the exact blur, TD:78 [M]). Grid operators (blur of arbitrary fields, distance
|
||||
transforms, eikonal paths, jump flooding, TD:19, TD:78), iteration and stored fields are presentation-only.
|
||||
2. **Donor control.** A layer never reads itself or a layer above it: predators follow prey and never thin it in
|
||||
the law (TD:56 [M]). Thinning is a sim event (harvest, attack wear), not a placement term.
|
||||
3. **Candidates with fixed thresholds** (r3, kept). Each species has a lattice of candidate seed points, one per
|
||||
cell, jittered by a hash of the zone seed and the cell, each with its own fixed threshold from the same hash; a
|
||||
candidate hosts a member when its species' density there is above its threshold (Horizon Zero Dawn's shape,
|
||||
CF:33 [V]; nesting pinned as an invariant, CF:40 [M]). The member's identity is the cell, a `ChunkId` with no
|
||||
stored state. Candidate spacing is per species (16 u krill, 48 u minnows, 128 u sharks in the probe).
|
||||
4. **No pop.** Terms that move fast are read at the member's own cycle start (`habitat-no-pop-ruled` for the flow,
|
||||
extended to the flow warp and the lattice web's drift, measured in B34); terms that move slowly and continuously
|
||||
(the slope band, the soft site graph, B33) are read now and carried by the fade. A member never appears or
|
||||
vanishes at full presence.
|
||||
5. **Nodes in CultMath, compositions in Aetheria.** Every node primitive is a CultMath function with an HLSL twin,
|
||||
pinned by CultMath's mirror test (map B29); what CultMath lacks is a gap filled in CultLib (map r4, gaps 1-5),
|
||||
never a helper in Aetheria. Each species' composition is C# in `ServerShared` (`Habitat`) calling those
|
||||
functions, with its parameters in a per-species settings record; the sim evaluates only C#. A presenter that
|
||||
draws a composed layer (the Skein's glow, glim haze) uses the composition's HLSL twin under a CultMath-owned
|
||||
composition check. Whether compositions later become data on CultMath's parked tape is the question
|
||||
`trophic-graph-tape`.
|
||||
|
||||
The law costs nothing per tick: it runs per candidate when asked, as `Zone.ChunkPose` does for rocks; the site graph
|
||||
is per zone per tick over a handful of bodies.
|
||||
|
||||
## Scale classes
|
||||
|
||||
| Class | Shape in the sim | Light | Motion law (tell) | Prop role |
|
||||
|---|---|---|---|---|
|
||||
| Krill | A cloud of thousands of members that share one orbital slot; members sway within a bounded radius | Faint reflection per member; the cloud is bright in aggregate | Velocity equals the local orbital flow; slow bounded sway | Hide in a cloud; later, a dense cloud thins sight lines as a vapour cloud does (`Zone.Obscuration`) |
|
||||
| Krill | Thousands of members at hosted candidates on the well shoulders; each advects along the flow for one cycle, fades and re-seeds | Faint reflection per member; a patch is bright in aggregate | Drifts with the global flow (advected cycles) | Hide in a cloud; later, a dense cloud thins sight lines as a vapour cloud does (`Zone.Obscuration`) |
|
||||
| Minnow | Schools of hundreds that ride the flow | Faint reflection, a bright school | Flow plus fast milling inside the school | Clutter of small returns; a ship moving through a school is noticed (later: scatter) |
|
||||
| Shark | One to three members, each about the size of a ship | Reflection and moderate thermal | Crosses the flow; patrols with sprints | A ship-sized return a hunter must think about; the best disguise target for a fighter |
|
||||
| Whale | One member, many times a ship's size | Strong thermal; heard song (later) | Slow long arcs | A bright anchor; a ship near it is hard to separate (later: shadowing) |
|
||||
|
|
@ -115,14 +189,15 @@ The rows follow the rulings `fauna-in-sim-ruled`, `fauna-identity-ruled` and `fa
|
|||
| Kind | Identity (what names it) | Lifecycle | Authority (who decides; forbidden writers) |
|
||||
|---|---|---|---|
|
||||
| **Field kind** (existing, extended) | `FieldKindData` catalog record (`ZoneData.cs:122`), by key. A fauna kind is a field kind with a non-zero member sway and zero belt `GenerationWeight`, so a rock belt never draws it (`FieldKinds.Assign`, "Zero never picks it"). | Authored in AetherDb, append-only MessagePack keys. | The catalog. Forbidden: runtime writes. |
|
||||
| **Swarm** (new use of an existing kind) | A field: an `AsteroidBeltData` body in `ZonePack.Planets`, named by its record key, as a belt is. Its members have orbital elements (distance and phase) clustered into one arc, and its kind is a fauna kind. | Created by `ZoneGenerator.GenerateZone` and saved with the zone pack, like a belt. Members are never saved except for their wear. | `ZoneGenerator` creates it; `Zone` holds it (`AsteroidBelts`). Forbidden: presenters, entities. |
|
||||
| **Member** (a chunk of a fauna field) | `ChunkId(field, index)` (`Zone.cs:825`; the type was named `Field` so other field kinds could join, per its own comment). | Exists unless broken (`Zone.ChunkExists`). Wear and respawn are lazy comparisons against zone time (`Zone.Wear`), as for rocks. Its pose is a pure function of zone time: the orbital slot plus the kind's bounded sway. | `AsteroidBelt.Pose` alone computes the pose; `Zone.Wear` is the only writer of damage. Forbidden: any stored pose, any per-tick member loop. |
|
||||
| **Fauna field** (one per fauna kind a zone hosts) | The fauna kind's catalog key within the zone: a zone hosts every fauna kind its world laws admit, so the field is derived, not generated. | Derived when the zone is built, from its world laws and the catalog; never saved. Only its members' wear is saved (`ZonePack.ChunkWear`). | `Zone`. Forbidden: `ZoneGenerator` writing fauna into the pack; presenters; entities. |
|
||||
| **Member** (a chunk of a fauna field) | `ChunkId(field, index)` (`Zone.cs:825`; the type was named `Field` so other field kinds could join, per its own comment); the index is a lattice cell. | Present while its candidate is hosted (placement law) and it is not broken (`Zone.ChunkExists`). Wear and respawn are lazy comparisons against zone time (`Zone.Wear`), as for rocks. Its pose is a pure function of zone time: its seed point advected along the flow for its cycle. | The habitat law decides hosting; the class law alone computes the pose; `Zone.Wear` is the only writer of damage. Forbidden: any stored pose or hosting bit, any per-tick member loop. |
|
||||
| **Fauna provider** (existing, widened) | `BeltTargets`, one per field (`Zone.cs:175`). | Built with the zone, never saved. | Its region bounds must hold for every member. Sway widens the slack by the kind's amplitude. Forbidden: a second index or a fauna scan. |
|
||||
| **Fauna detection** (derived) | `(observer, member)`, never stored. | Derived per query: `ChunkInfo` → `PassiveInfo` → `Sensor.Gain`. | `Entity`. Forbidden: any fauna-specific threshold or gain. |
|
||||
| **Swarm anchor** (later, sharks and whales) | Per field: a law that moves the field's frame across the flow. | Pure function of zone time plus sparse event records (fork `fauna-motion-model`). | `Zone`. Forbidden: per-member stepping. |
|
||||
| **Disturbance** (later) | `(member, time, push)`, sparse, as `ChunkWear` is. | Written when a hull passes through or a shot lands, then decays lazily against zone time. | `Zone`. It is read only by `Pose`. |
|
||||
| **Flow field** (new owner) | One law: `FlowField`, evaluated per zone by `Zone.Flow(position)` from the zone's world-law settings (`PlanetSettings.Flow`). | Settings authored in `Settings.asset`; time is zone time. Never stored per position. | `Zone`. Derived: every shader flow uniform (`_FlowScroll` and the rest). Forbidden: `VolumeSampling._flowScroll`, `ZoneEnvironment.Flow`, any frame-time accumulator. |
|
||||
| **Habitat placement** (generation intermediate) | None: it exists only inside `ZoneGenerator.GenerateZone`. | Computed once per zone from the seed, gravity and flow; consumed to place fields; discarded. | `ZoneGenerator`. Its products, fauna fields, are saved as belts are. Forbidden: recomputing on load, saving the intermediate map. |
|
||||
| **Habitat** (new owner, a law: the trophic DAG) | `Habitat` in `ServerShared`: one composition per species over CultMath nodes, a species' settings record, a candidate's hashed seed point and threshold. | Never stored: evaluated at zone time per candidate when asked. | `Habitat` decides density and hosting; `Zone` supplies world facts (`GetNormal`, `Flow`, body positions from the per-tick orbit cache); CultMath owns every node and its HLSL twin. Forbidden: a generation pass, a saved or cached habitat map, a per-tick sweep, a math twin in Aetheria, a grid operator in a sim-visible layer. |
|
||||
| **Skein site graph** (derived) | Per zone: the soft gate of each pair of bodies (map r4). | Recomputed per tick from orbit positions; never saved. | `Habitat`, through a CultMath gate function. Forbidden: a hard (0/1) gate; a cached graph across ticks. |
|
||||
| **Attack** (later, per class) | `(member or swarm, ship, time)`, an event. | Raised by a class law when its trigger holds; applied through the ordinary damage path. | `Zone`. Forbidden: a fauna-only damage rule. |
|
||||
| **Harvest yield** (later) | Item kinds in the catalog, per fauna kind. | A member broken or netted by harvesting gear yields items through the economy's yield table; the field refills by respawn. | The catalog and the yield table. Forbidden: fauna-only loot code. |
|
||||
| **Fauna background kind** (aetheria-ew) | Entries of aetheria-ew's `BackgroundKind` for fauna classes (that target's row says "more by ruling"). | Static data. | `GameplaySettings`, per aetheria-ew. Its envelope gains a motion term per class (fork `fauna-disguise-path`). |
|
||||
|
|
@ -137,8 +212,8 @@ The rows follow the rulings `fauna-in-sim-ruled`, `fauna-identity-ruled` and `fa
|
|||
`Examined` counter and by `Zone.Update` touching no member, not by timing.
|
||||
- `fauna-one-gain`: a fauna member is visible exactly when `ChunkVisible` says so.
|
||||
- `fauna-bounds-hold`: every member of a swaying or drifting field lies inside the regions its provider offers.
|
||||
- `fauna-tell-holds`: each class's members move by that class's law; a krill cloud moves with the orbital flow and
|
||||
the global flow, within its sway bound. Each class gets its own tell test.
|
||||
- `fauna-tell-holds`: each class's members move by that class's law; a krill member advects with the global flow
|
||||
from its seed point for its cycle. Each class gets its own tell test.
|
||||
- `fauna-never-lie`: no claim, suppression or belief is written for a fauna.
|
||||
- `flow-one-owner`: the global flow has one owner, `FlowField` in `ServerShared`, evaluated at zone time from the
|
||||
zone's flow settings. Every shader flow uniform is derived from it; no presenter keeps its own flow clock.
|
||||
|
|
@ -146,22 +221,44 @@ The rows follow the rulings `fauna-in-sim-ruled`, `fauna-identity-ruled` and `fa
|
|||
the same position, time, settings and gradient, within a stated tolerance; a parity check that evaluates both
|
||||
pins it and fails when either side changes alone.
|
||||
- `flow-step-free`: the flow at a position and zone time does not depend on step size or frame rate.
|
||||
- `fauna-none-in-sol`: Sol's zone holds no fauna field; generation never places one there.
|
||||
- `fauna-placement-seeded`: a zone's fauna placement is a function of its seed and its generated bodies on the
|
||||
generating runtime; the saved zone pack is the authority after that, and loading never recomputes placement.
|
||||
- `fauna-none-in-sol`: Sol's zone hosts no fauna kind, so no candidate there is ever hosted.
|
||||
- `fauna-placement-closed-form`: whether a candidate hosts a member at zone time t is a pure function of the zone
|
||||
seed, the species' settings, the world facts at t (slope, flow at the member's cycle start, the bodies' positions)
|
||||
and the lower layers evaluated at bounded points. Nothing about placement is generated ahead, saved, cached across
|
||||
ticks or stepped.
|
||||
- `fauna-placement-story`: density is zero in well cores; along a Skein strand between two bodies it exceeds the
|
||||
density at a point of equal slope off the strand; ambushers' density peaks on the slack borders; a predator's
|
||||
density rises with its prey's within its hunting ring.
|
||||
- `fauna-placement-nested`: each candidate's threshold is fixed, so raising the density at a candidate never
|
||||
un-hosts it and lowering it never hosts it.
|
||||
- `fauna-placement-no-pop`: a member never appears or vanishes at full presence; every change of hosting is carried
|
||||
through a fade. Within a member's cycle its presence moves by at most a stated bound per 1/60 s step as the bodies
|
||||
orbit (the probe's worst was .066, map B34).
|
||||
- `trophic-dag-acyclic`: each layer reads only world facts and the layers below it; no layer reads itself or a layer
|
||||
above it.
|
||||
- `trophic-dag-pointwise`: every sim-visible layer is evaluated at a point from world facts, the per-tick site
|
||||
graph and a bounded number of evaluations of lower layers (stencils at most two levels deep); no grid, no
|
||||
iteration, no stored field.
|
||||
- `trophic-nodes-in-cultmath`: every node primitive a layer calls is a CultMath function with an HLSL twin pinned by
|
||||
CultMath's parity checks; Aetheria holds compositions only.
|
||||
- `skein-gate-continuous`: the Skein's site-graph gates are continuous in the bodies' positions, so no strand
|
||||
appears or vanishes as the bodies orbit.
|
||||
|
||||
## Not in scope
|
||||
|
||||
- Ambient presentation-only life (the slime trails) beyond leaving it untouched; its rules may be reused at
|
||||
generation, but the presenter stays presentation.
|
||||
- Ambient presentation-only life (the slime trails) beyond leaving it untouched; the habitat law reuses its
|
||||
`Deposition(slope)` curve, but the presenter stays presentation.
|
||||
- Population dynamics: no layer is stepped or regulated by the layer above it; harvest and attack wear thin
|
||||
members as sim events through `Zone.Wear`, not as placement terms.
|
||||
- Phantom positions (aetheria-ew `ew-where-lies-ruled`).
|
||||
- Ships, floating items, mines and clouds drifting with the flow, until `flow-moves-what` is ruled.
|
||||
- Ships drifting with the flow (`flow-moves-what-ruled`: ships do not; floating items, mines and vented clouds do,
|
||||
in a later cut).
|
||||
- Per-zone flow and nebula environments; Sol's cloudless bubble is a later cut, and until it lands every zone
|
||||
shares one flow.
|
||||
|
||||
Attacks and harvest are in scope as later cuts, designed per class (`fauna-interaction-ruled`,
|
||||
`fauna-harvest-ruled`); the first cuts build the ground they stand on: the flow, placement and the krill field.
|
||||
`fauna-harvest-ruled`); the first cuts build the ground they stand on: the flow, the habitat law and the krill field.
|
||||
|
||||
## Cut sequence
|
||||
|
||||
The map has the first cut drafted as a spec section and every later cut as one line.
|
||||
The map's r4 cut sequence (with the r3 lines it keeps) has one line per cut; each is mapped when it is about to run.
|
||||
|
|
|
|||
54
docs/research/2026-10-14-body-voronoi-probe/README.md
Normal file
54
docs/research/2026-10-14-body-voronoi-probe/README.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Body-Voronoi probe (2026-10-14)
|
||||
|
||||
Serves the operator's sketch after `aetheria-fauna:ruling:skein-filament-construction-ruled`: "I was imagining the
|
||||
voronoi cells to be the gravity objects themselves, aren't there a bunch of fun ways to play with the distance
|
||||
metrics to get filaments that way too?" Facts are map rows B37-B42 in `docs/aetheria-fauna-map.md`. Prior art:
|
||||
CultLib `docs/research/tape-register-model.md` section 5 (`dfc720e7`) and `docs/research/lattice-site-weight.md`
|
||||
(`21281a39`).
|
||||
|
||||
`body_voronoi_probe.py` imports `trophic_probe.py` (`../2026-10-14-trophic-dag-probe/`: orbiting wells, slope terms
|
||||
on, the flow warp, the lattice web, the members and pop probes) and replaces only the large-scale filament layer.
|
||||
Same zone, same frame (t = 0), same 16 u krill candidates and thresholds, same fade .1.
|
||||
|
||||
**The large layer.** A diagram over the zone's six bodies (sites = bodies, read now), drawn as an edge profile: the
|
||||
distance to the border to first order, `(F2 - F1) / |grad(F2 - F1)|`, through a Gaussian of half-width 40 u. Each
|
||||
body's weight is its well's Gaussian width `sigma = r / sqrt(128)` (the zone's `pp(d/r)^16` term is about
|
||||
`exp(-d^2 / 2 sigma^2)`): sun 482 u, planets 180, 180, 146, 103, gas giant 305. Bent by the same flow warp as the
|
||||
strands (20 s).
|
||||
|
||||
| Variant | Metric `d_i` |
|
||||
|---|---|
|
||||
| `pot-well` | Equal well depth on the zone's real profile: `-ln(depth_i * pp(d/r_i)^16)` |
|
||||
| `pot-newton` | Equal point-mass potential `m/d`: multiplicative with weight = mass (section 5a's identity) |
|
||||
| `mult` | `d / sigma_i`: Apollonius circles, her shader's trick |
|
||||
| `add` | `d - sigma_i`: hyperbola arcs |
|
||||
| `power` | `d^2 - sigma_i^2`: straight edges, the adhesion model's tiling |
|
||||
| `flow-aniso` | `mult` with the across-flow component stretched 2.5x; the flow direction is the warp's own |
|
||||
| `lp05`, `lpinf` | `mult` in the Minkowski p = .5 and p = inf (Chebyshev) norms, axis-aligned |
|
||||
| `skein-web` | Control: the ruled Skein (`trophic_probe.py`), strands plus web |
|
||||
| `mult-strands`, `power-strands` | The diagram's edges and the strands together, with the web |
|
||||
|
||||
**The web.** `trophic_probe.lattice_web` with two changes: each lattice cell's hash weight is raised by up to 1.6x by
|
||||
the large-edge profile (3x wider) at the cell centre, `(cx + .5) * LAT` (cell-centre weight in
|
||||
`lattice-site-weight.md`'s terms; `trophic_probe.py`'s well proximity is also read at the cell centre, not at the
|
||||
jittered site), and the cell metric is stretched up to 2x across the large edge's normal at the query point, so cells
|
||||
shrink and align along the edges. The web hangs within 4 edge widths of a large edge, or on the band.
|
||||
|
||||
Images (zone 4,000 u across, red = bodies): `density_<variant>.png` (krill habitat density, t = 0),
|
||||
`members_<variant>.png` (hosted krill candidates), `edges_<variant>.png` (the large layer alone, no web; also for the
|
||||
pruned variants).
|
||||
|
||||
Logs:
|
||||
- `run1.log`: all variants as `trophic_probe.py` reads the flow (scroll at the member's cycle start, the slope terms
|
||||
with the wells now). `run2.log` and `run3.log` (`power-strands`): the whole warp at cycle start, slope terms
|
||||
included (`WARP_AT_CYCLE=1`). The member images here are run 2 and 3's.
|
||||
- `diag_edges.log`: the large layer alone, run-1 reading. `trace.py`: the worst step traced to the warped point
|
||||
moving 1.4 u per 1/60 s.
|
||||
- `diag2.log` (run-2 reading): web ablation (boost, align) and the lattice window, 5x5 against 7x7.
|
||||
|
||||
To rerun: copy this folder, `../2026-10-14-trophic-dag-probe/trophic_probe.py` and
|
||||
`../2026-10-14-fauna-habitat-probe/habitat_probe.py` into one scratch directory, then
|
||||
`py -3 -u body_voronoi_probe.py 256` (about 15 minutes), the same with `WARP_AT_CYCLE=1` (writes into `cyc/`),
|
||||
`py -3 -u diag.py` with `DIAG=edges`, and `WARP_AT_CYCLE=1 py -3 -u diag2.py`. `habitat_probe.py` reads the B20 map
|
||||
from `C:\aeth-wt\fauna-target-r3\docs\research\2026-10-13-fauna-placement-probe\output.txt`. Python 3 only, no
|
||||
packages. Hashes are the probe's own integer hash, not CultMath `pcg3d`; the images are qualitative evidence.
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
"""Body-Voronoi probe (Imagination, aetheria-fauna r4). Pure Python 3, no packages.
|
||||
|
||||
The operator's sketch after skein-web was ruled: the Voronoi cells are the gravity bodies themselves, and the
|
||||
distance metric is the place to play. The large-scale filament layer here is a diagram over the zone's bodies
|
||||
(sites = bodies, orbiting), drawn as an F2 - F1 edge profile: distance to the border to first order,
|
||||
(F2 - F1) / |grad(F2 - F1)| through a Gaussian of width EW_U. The lattice web of trophic_probe.py hangs off it, its
|
||||
per-site weight boosted by the large edge's profile at each lattice site (cells shrink near the edges) and its
|
||||
metric stretched across the edge normal at the query point (cells align along the edges).
|
||||
|
||||
Reuses trophic_probe.py (orbiting wells, slope terms on, the flow warp, the lattice web, members and pop probes) and
|
||||
habitat_probe.py (Zone.GetHeight/GetNormal and FlowField transliterations). Hashes are the probe's own.
|
||||
|
||||
py -3 -u body_voronoi_probe.py [grid=256] [only=comma,list]
|
||||
"""
|
||||
import sys, os, math, time
|
||||
ARGS = sys.argv[1:]
|
||||
sys.argv = [sys.argv[0], "256"]
|
||||
import trophic_probe as T
|
||||
H = T.H
|
||||
|
||||
OUT = os.path.dirname(os.path.abspath(__file__))
|
||||
G = int(ARGS[0]) if ARGS else 256
|
||||
ONLY = ARGS[1].split(",") if len(ARGS) > 1 else None
|
||||
T.WEB_AT_CYCLE = True # ruled with skein-web: flow and web drift read at the member's cycle start
|
||||
EW_U = 40.0 # large edge half-width (u)
|
||||
HALO = 4.0 # web hangs within this many edge widths of a large edge (or on the band)
|
||||
ANISO = 2.5 # flow-stretched metric: distance across the flow counts this many times more
|
||||
ALIGN = 1.0 # lattice cells: across-edge component stretched by up to 1 + ALIGN near a large edge
|
||||
VISITS = [0] # sites visited per evaluation (the CultMath cost measure)
|
||||
|
||||
# sigma: each well's Gaussian width. Zone's term pp(d/r)^16 = (1 - 4 d^2/r^2)^16 ~ exp(-64 d^2/r^2) = exp(-d^2 / 2 s^2)
|
||||
SIG = [w["r"] / math.sqrt(128.0) for w in H.WELLS]
|
||||
MASS = [b[1] for b in H.BODIES]
|
||||
|
||||
# ---------------- per-site metrics: d_i(q, flow direction) ----------------
|
||||
def m_mult(i, dx, dz, fd): VISITS[0] += 1; return math.hypot(dx, dz) / SIG[i]
|
||||
def m_add(i, dx, dz, fd): VISITS[0] += 1; return math.hypot(dx, dz) - SIG[i]
|
||||
def m_power(i, dx, dz, fd): VISITS[0] += 1; return dx * dx + dz * dz - SIG[i] ** 2
|
||||
def m_newton(i, dx, dz, fd): VISITS[0] += 1; return math.hypot(dx, dz) / MASS[i] # potential m/d equal: w = mass
|
||||
def m_well(i, dx, dz, fd):
|
||||
"""Equal well depth on the zone's real profile: depth_i * pp(d/r_i)^16 equal (its log, so smaller is deeper)."""
|
||||
VISITS[0] += 1; w = H.WELLS[i]; u = 4 * (dx * dx + dz * dz) / w["r"] ** 2
|
||||
return 1e9 if u >= 1 else -(math.log(w["depth"]) + w["exp"] * math.log(1 - u))
|
||||
def m_aniso(i, dx, dz, fd):
|
||||
VISITS[0] += 1; a = dx * fd[0] + dz * fd[1]; c = -dx * fd[1] + dz * fd[0]
|
||||
return math.sqrt(a * a + (ANISO * c) ** 2) / SIG[i]
|
||||
def m_lp(p):
|
||||
def f(i, dx, dz, fd):
|
||||
VISITS[0] += 1
|
||||
if p == math.inf: return max(abs(dx), abs(dz)) / SIG[i]
|
||||
return (abs(dx) ** p + abs(dz) ** p) ** (1 / p) / SIG[i]
|
||||
return f
|
||||
|
||||
def body_border(q, metric, fd=(1.0, 0.0)):
|
||||
"""Distance (u) from q to the nearest border of the body diagram under `metric`, first order, and the border's
|
||||
unit normal (grad of F2 - F1). Visits every body once; the gradient re-evaluates only the two winners."""
|
||||
ds = sorted((metric(i, q[0] - w["pos"][0], q[1] - w["pos"][1], fd), i) for i, w in enumerate(H.WELLS))
|
||||
(d1, i1), (d2, i2) = ds[0], ds[1]
|
||||
if d2 >= 1e8: return 1e9, (1.0, 0.0)
|
||||
def g(x, z):
|
||||
a, b = H.WELLS[i1]["pos"], H.WELLS[i2]["pos"]
|
||||
return metric(i2, x - b[0], z - b[1], fd) - metric(i1, x - a[0], z - a[1], fd)
|
||||
e = 1.0; v = VISITS[0]
|
||||
gx = (g(q[0] + e, q[1]) - g(q[0] - e, q[1])) / (2 * e); gz = (g(q[0], q[1] + e) - g(q[0], q[1] - e)) / (2 * e)
|
||||
VISITS[0] = v # the gradient is analytic in C#; the probe's finite difference is not counted
|
||||
gl = math.hypot(gx, gz) or 1e-9
|
||||
return (d2 - d1) / gl, (gx / gl, gz / gl)
|
||||
|
||||
def edge_profile(dist, widen=1.0): return math.exp(-(dist / (EW_U * widen)) ** 2)
|
||||
|
||||
# ---------------- the lattice web, weights pulled toward the large edges ----------------
|
||||
SITE_W = {} # per tick: lattice site -> large-edge profile at its centre (cleared by set_time)
|
||||
_set_time = T.set_time
|
||||
def set_time(t): SITE_W.clear(); _set_time(t)
|
||||
T.set_time = set_time
|
||||
|
||||
def lattice_web_edges(q, t, metric, near, nrm):
|
||||
"""trophic_probe.lattice_web with the well-proximity boost replaced by the large-edge profile at each lattice
|
||||
site, and the cell metric stretched by (1 + ALIGN * near) across the large edge's normal at the query point."""
|
||||
if T.CUR_T0[0] is not None: t = T.CUR_T0[0]
|
||||
LAT = T.LAT; px, pz = q[0] / LAT, q[1] / LAT; nx, nz = math.floor(px), math.floor(pz); fx, fz = px - nx, pz - nz
|
||||
st = 1.0 + ALIGN * near; m = 8.0; f1 = 8.0; f2 = 8.0; KW = T.KW
|
||||
for j in range(-T.WIN, T.WIN + 1):
|
||||
for i in range(-T.WIN, T.WIN + 1):
|
||||
cx, cz = nx + i, nz + j; VISITS[0] += 1
|
||||
ox, oz = H.hash01(cx, cz, 11), H.hash01(cx, cz, 12); wt = ox * .5 + .5
|
||||
key = (cx, cz, metric)
|
||||
if key not in SITE_W:
|
||||
SITE_W[key] = edge_profile(body_border(((cx + .5) * LAT, (cz + .5) * LAT), metric)[0], 3.0)
|
||||
else: VISITS[0] += len(H.WELLS) # count the per-site evaluation even when the probe memoises it
|
||||
wt *= 1.0 + T.BOOST * SITE_W[key]
|
||||
ox = .5 + .5 * math.sin(T.OMEGA * t + 6.2831 * ox); oz = .5 + .5 * math.sin(T.OMEGA * t + 6.2831 * oz)
|
||||
vx, vz = i - fx + ox, j - fz + oz
|
||||
a = vx * nrm[0] + vz * nrm[1]; c = -vx * nrm[1] + vz * nrm[0]
|
||||
d = math.hypot(st * a, c) * wt
|
||||
if d < f1: f1, f2 = d, f1
|
||||
elif d < f2: f2 = d
|
||||
h = T.smooth(0.0, 1.0, .5 + .5 * (m - d) / KW)
|
||||
m = m + (d - m) * h - h * (1 - h) * KW / (1 + 3 * KW)
|
||||
gap0 = .25 * KW / (1 + 3 * KW)
|
||||
soft = max(0.0, min(1.0, (f1 - m) / gap0))
|
||||
return max(soft ** 2, math.exp(-((f2 - f1) / T.EW) ** 2))
|
||||
|
||||
WARP_AT_CYCLE = os.environ.get("WARP_AT_CYCLE") == "1"
|
||||
_warp = T.warp
|
||||
if WARP_AT_CYCLE: OUT = os.path.join(OUT, "cyc"); os.makedirs(OUT, exist_ok=True)
|
||||
def warp(x, z, t, sc, tau=None):
|
||||
"""WARP_AT_CYCLE=1: the whole flow warp at the member's cycle start, slope terms included (the wells at t0).
|
||||
trophic_probe reads only the scroll at cycle start; its slope terms read the wells now (B34's runs)."""
|
||||
t0 = T.CUR_T0[0]
|
||||
if not WARP_AT_CYCLE or t0 is None: return _warp(x, z, t, sc, tau)
|
||||
keep = [w["pos"] for w in H.WELLS]
|
||||
for (kind, m, d, ph), w in zip(H.BODIES, H.WELLS):
|
||||
a = ph + (2 * math.pi * t0 / d if d > 0 else 0.0); w["pos"] = (math.cos(a) * d, math.sin(a) * d)
|
||||
try: return _warp(x, z, t0, sc, tau)
|
||||
finally:
|
||||
for w, p in zip(H.WELLS, keep): w["pos"] = p
|
||||
T.warp = warp
|
||||
|
||||
def flow_dir_of(x, z, q):
|
||||
"""The flow direction the warp used: u = (x - q) / TAU, so it is read when the warp is."""
|
||||
ux, uz = x - q[0], z - q[1]; l = math.hypot(ux, uz) or 1e-9; return ux / l, uz / l
|
||||
|
||||
def flow_dir(x, z, t, sc):
|
||||
ux, uz = T.flow_at(x, z, t, sc); l = math.hypot(ux, uz) or 1e-9; return ux / l, uz / l
|
||||
|
||||
def body_web(metric, strands=False):
|
||||
def D(x, z, t, sc):
|
||||
s, b, c = T.env(x, z, t); q = T.warp(x, z, t, sc)
|
||||
fd = flow_dir_of(x, z, q) if metric is m_aniso else (1.0, 0.0)
|
||||
dist, nrm = body_border(q, metric, fd); e = edge_profile(dist); halo = edge_profile(dist, HALO)
|
||||
big = e; near = halo
|
||||
if strands:
|
||||
st = T.strands(q); sh = T.strands(q, widen=4.0); VISITS[0] += 2 * len(T.pairs(False))
|
||||
big = max(e, st); near = max(halo, sh)
|
||||
web = lattice_web_edges(T.warp(x, z, t, sc, T.WEB_TAU), t, metric, halo, nrm) * max(near, b)
|
||||
return max(big, .8 * web) * (1 - c)
|
||||
return D
|
||||
|
||||
def skein_web_counted(x, z, t, sc):
|
||||
VISITS[0] += 2 * len(T.pairs(False)) + 25 * (1 + len(H.WELLS)); return T.D_skein_web(x, z, t, sc)
|
||||
|
||||
VARIANTS = {
|
||||
"pot-well": body_web(m_well), "pot-newton": body_web(m_newton), "mult": body_web(m_mult),
|
||||
"add": body_web(m_add), "power": body_web(m_power), "flow-aniso": body_web(m_aniso),
|
||||
"lp05": body_web(m_lp(.5)), "lpinf": body_web(m_lp(math.inf)),
|
||||
"skein-web": skein_web_counted, "mult-strands": body_web(m_mult, strands=True),
|
||||
"power-strands": body_web(m_power, strands=True),
|
||||
}
|
||||
|
||||
def run(name, fn, cands, t=0.0):
|
||||
t1 = time.time(); vals = T.grid(fn, t, G); T.save(vals, os.path.join(OUT, "density_" + name), G)
|
||||
cr = T.phys_corr(vals, G)
|
||||
a = T.members(fn, cands, t); b = T.members(fn, cands, t + 1); n = max(1, len(a))
|
||||
W2 = 600; px = [(10, 14, 30)] * (W2 * W2)
|
||||
for c in cands:
|
||||
if (c[0], c[1]) in a:
|
||||
i, j = int((c[2] / (2 * H.R) + .5) * W2), int((c[3] / (2 * H.R) + .5) * W2)
|
||||
if 0 <= i < W2 and 0 <= j < W2: px[j * W2 + i] = (235, 230, 170)
|
||||
H.mark_bodies(px, W2); H.write_png(os.path.join(OUT, "members_" + name + ".png"), W2, W2, px)
|
||||
worst, big, nn = T.pop_probe(fn, cands, 100.0, 1500)
|
||||
T.set_time(0.0); sc = H.scroll(0.0); VISITS[0] = 0; SITE_W.clear(); k = 40
|
||||
c0 = time.perf_counter()
|
||||
for m in range(k): SITE_W.clear(); fn(300.0 + 7 * m, 200.0 - 5 * m, 0.0, sc)
|
||||
ms = (time.perf_counter() - c0) / k * 1e3
|
||||
print(f"{name}: corr physarumA {cr:.3f}; members {len(a)}; changed in 1 s {len(a ^ b) / n:.3f}; "
|
||||
f"presence step max {worst:.4f} ({big} steps > .1 of {nn}); sites visited per evaluation {VISITS[0] / k:.0f}; "
|
||||
f"{ms:.2f} ms/eval (Python); {time.time() - t1:.0f}s", flush=True)
|
||||
|
||||
def main():
|
||||
print(f"warp at cycle start {WARP_AT_CYCLE}; grid {G}; sigma {[round(s) for s in SIG]}; edge width {EW_U} u; web at cycle start {T.WEB_AT_CYCLE}", flush=True)
|
||||
cands = T.cand_list(16.0)
|
||||
for name, fn in VARIANTS.items():
|
||||
if ONLY and name not in ONLY: continue
|
||||
run(name, fn, cands)
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
BIN
docs/research/2026-10-14-body-voronoi-probe/density_add.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/density_add.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/density_flow-aniso.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/density_flow-aniso.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/density_mult-strands.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/density_mult-strands.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/density_mult.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/density_mult.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/density_power-strands.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/density_power-strands.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/density_power.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/density_power.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/density_skein-web.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/density_skein-web.png
(Stored with Git LFS)
Normal file
Binary file not shown.
28
docs/research/2026-10-14-body-voronoi-probe/diag.py
Normal file
28
docs/research/2026-10-14-body-voronoi-probe/diag.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
"""Diagnostics: where the presence steps over .1 come from, and the large layer alone (edges_<name>.png)."""
|
||||
import sys; sys.argv = ['x', '256']
|
||||
import body_voronoi_probe as B
|
||||
T, H = B.T, B.H
|
||||
mode = sys.argv[1:] or []
|
||||
cands = T.cand_list(16.0)
|
||||
def edges_only(metric):
|
||||
def D(x, z, t, sc):
|
||||
s, b, c = T.env(x, z, t); q = T.warp(x, z, t, sc)
|
||||
fd = B.flow_dir_of(x, z, q) if metric is B.m_aniso else (1.0, 0.0)
|
||||
return B.edge_profile(B.body_border(q, metric, fd)[0]) * (1 - c)
|
||||
return D
|
||||
MET = {"pot-well": B.m_well, "pot-newton": B.m_newton, "mult": B.m_mult, "add": B.m_add, "power": B.m_power,
|
||||
"flow-aniso": B.m_aniso, "lp05": B.m_lp(.5), "lpinf": B.m_lp(float('inf'))}
|
||||
import os
|
||||
which = os.environ.get("DIAG", "edges")
|
||||
if which == "edges":
|
||||
for n, m in MET.items():
|
||||
f = edges_only(m); vals = T.grid(f, 0.0, 256); T.save(vals, os.path.join(B.OUT, "edges_" + n), 256)
|
||||
w, big, nn = T.pop_probe(f, cands, 100.0, 1500)
|
||||
print(f"edges-only {n}: presence step max {w:.4f} ({big} steps > .1 of {nn})", flush=True)
|
||||
else:
|
||||
for boost, align in ((0.0, 1.0), (1.6, 0.0), (0.0, 0.0)):
|
||||
T.BOOST, B.ALIGN = boost, align
|
||||
for n in ("mult", "power"):
|
||||
w, big, nn = T.pop_probe(B.VARIANTS[n], cands, 100.0, 1500)
|
||||
print(f"BOOST {boost} ALIGN {align} {n}: presence step max {w:.4f} ({big} steps > .1 of {nn})", flush=True)
|
||||
print("diag done", flush=True)
|
||||
11
docs/research/2026-10-14-body-voronoi-probe/diag2.log
Normal file
11
docs/research/2026-10-14-body-voronoi-probe/diag2.log
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
BOOST 0.0 ALIGN 1.0 mult: presence step max 0.0234 (0 steps > .1 of 1500)
|
||||
BOOST 0.0 ALIGN 1.0 power: presence step max 0.3012 (2 steps > .1 of 1500)
|
||||
BOOST 1.6 ALIGN 0.0 mult: presence step max 0.0545 (0 steps > .1 of 1500)
|
||||
BOOST 1.6 ALIGN 0.0 power: presence step max 0.0287 (0 steps > .1 of 1500)
|
||||
BOOST 0.0 ALIGN 0.0 mult: presence step max 0.0234 (0 steps > .1 of 1500)
|
||||
BOOST 0.0 ALIGN 0.0 power: presence step max 0.1970 (1 steps > .1 of 1500)
|
||||
window mult: WIN 2 vs 3 max |diff| 0.2794, points differing > .01: 1 of 3385
|
||||
window power: WIN 2 vs 3 max |diff| 0.0000, points differing > .01: 0 of 3385
|
||||
window add: WIN 2 vs 3 max |diff| 0.0000, points differing > .01: 0 of 3385
|
||||
site boost profile range 0.000..1.000: weight range .5*(1+1.6*min) .. 1.0*(1+1.6*max) = 0.50..2.60, ratio 5.2:1; with ALIGN the across-edge distance is stretched up to 2.0x more
|
||||
diag2 done
|
||||
26
docs/research/2026-10-14-body-voronoi-probe/diag2.py
Normal file
26
docs/research/2026-10-14-body-voronoi-probe/diag2.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"""Run with WARP_AT_CYCLE=1. (a) Which part of the web pops: BOOST/ALIGN ablation on mult and power.
|
||||
(b) Lattice search window against the body-pulled weight range: WIN 2 vs WIN 3 on the density at 4,000 points."""
|
||||
import os, sys, math; sys.argv = ['x', '256']
|
||||
import body_voronoi_probe as B
|
||||
T, H = B.T, B.H
|
||||
cands = T.cand_list(16.0)
|
||||
for boost, align in ((0.0, 1.0), (1.6, 0.0), (0.0, 0.0)):
|
||||
T.BOOST, B.ALIGN = boost, align
|
||||
for n in ("mult", "power"):
|
||||
w, big, nn = T.pop_probe(B.VARIANTS[n], cands, 100.0, 1500)
|
||||
print(f"BOOST {boost} ALIGN {align} {n}: presence step max {w:.4f} ({big} steps > .1 of {nn})", flush=True)
|
||||
T.BOOST, B.ALIGN = 1.6, 1.0
|
||||
T.set_time(0.0); sc = H.scroll(0.0)
|
||||
pts = [(((k * 0.6180339887) % 1 - .5) * 2 * H.R * .95, ((k * 0.7548776662) % 1 - .5) * 2 * H.R * .95) for k in range(4000)]
|
||||
pts = [p for p in pts if p[0] ** 2 + p[1] ** 2 < H.R ** 2]
|
||||
wmin, wmax = 9, 0
|
||||
for n in ("mult", "power", "add"):
|
||||
f = B.VARIANTS[n]; T.WIN = 2; a = [f(x, z, 0.0, sc) for x, z in pts]
|
||||
T.WIN = 3; b = [f(x, z, 0.0, sc) for x, z in pts]; T.WIN = 2
|
||||
d = [abs(u - v) for u, v in zip(a, b)]
|
||||
print(f"window {n}: WIN 2 vs 3 max |diff| {max(d):.4f}, points differing > .01: {sum(x > .01 for x in d)} of {len(d)}", flush=True)
|
||||
ws = [v for v in B.SITE_W.values()]
|
||||
print(f"site boost profile range {min(ws):.3f}..{max(ws):.3f}: weight range .5*(1+1.6*min) .. 1.0*(1+1.6*max) = "
|
||||
f"{.5 * (1 + 1.6 * min(ws)):.2f}..{1 + 1.6 * max(ws):.2f}, ratio {(1 + 1.6 * max(ws)) / (.5 * (1 + 1.6 * min(ws))):.1f}:1; "
|
||||
f"with ALIGN the across-edge distance is stretched up to {1 + B.ALIGN:.1f}x more", flush=True)
|
||||
print("diag2 done", flush=True)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
edges-only pot-well: presence step max 1.0000 (2 steps > .1 of 1500)
|
||||
edges-only pot-newton: presence step max 0.1869 (6 steps > .1 of 1500)
|
||||
edges-only mult: presence step max 0.1522 (6 steps > .1 of 1500)
|
||||
edges-only add: presence step max 0.1903 (8 steps > .1 of 1500)
|
||||
edges-only power: presence step max 0.1260 (9 steps > .1 of 1500)
|
||||
edges-only flow-aniso: presence step max 0.1645 (30 steps > .1 of 1500)
|
||||
edges-only lp05: presence step max 1.0000 (100 steps > .1 of 1500)
|
||||
edges-only lpinf: presence step max 0.5637 (34 steps > .1 of 1500)
|
||||
BIN
docs/research/2026-10-14-body-voronoi-probe/edges_add.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/edges_add.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/edges_flow-aniso.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/edges_flow-aniso.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/edges_lp05.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/edges_lp05.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/edges_lpinf.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/edges_lpinf.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/edges_mult.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/edges_mult.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/edges_pot-newton.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/edges_pot-newton.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/edges_pot-well.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/edges_pot-well.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/edges_power.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/edges_power.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/members_add.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/members_add.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/members_flow-aniso.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/members_flow-aniso.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/members_mult-strands.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/members_mult-strands.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/members_mult.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/members_mult.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/members_power-strands.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/members_power-strands.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/members_power.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/members_power.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-body-voronoi-probe/members_skein-web.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-body-voronoi-probe/members_skein-web.png
(Stored with Git LFS)
Normal file
Binary file not shown.
11
docs/research/2026-10-14-body-voronoi-probe/run1.log
Normal file
11
docs/research/2026-10-14-body-voronoi-probe/run1.log
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
grid 256; sigma [482, 180, 180, 305, 146, 103]; edge width 40.0 u; web at cycle start True
|
||||
pot-well: corr physarumA 0.279; members 3566; changed in 1 s 0.327; presence step max 1.0000 (1 steps > .1 of 1500); sites visited per evaluation 181; 0.76 ms/eval (Python); 85s
|
||||
pot-newton: corr physarumA 0.338; members 683; changed in 1 s 0.318; presence step max 0.1869 (8 steps > .1 of 1500); sites visited per evaluation 181; 0.65 ms/eval (Python); 90s
|
||||
mult: corr physarumA 0.335; members 3280; changed in 1 s 0.296; presence step max 0.1522 (8 steps > .1 of 1500); sites visited per evaluation 181; 0.52 ms/eval (Python); 86s
|
||||
add: corr physarumA 0.263; members 4088; changed in 1 s 0.347; presence step max 0.1144 (7 steps > .1 of 1500); sites visited per evaluation 181; 0.54 ms/eval (Python); 88s
|
||||
power: corr physarumA 0.313; members 3866; changed in 1 s 0.306; presence step max 0.1260 (9 steps > .1 of 1500); sites visited per evaluation 181; 0.65 ms/eval (Python); 88s
|
||||
flow-aniso: corr physarumA 0.322; members 4076; changed in 1 s 0.311; presence step max 0.1645 (22 steps > .1 of 1500); sites visited per evaluation 181; 0.62 ms/eval (Python); 99s
|
||||
lp05: corr physarumA 0.274; members 6682; changed in 1 s 0.307; presence step max 1.0000 (115 steps > .1 of 1500); sites visited per evaluation 181; 0.87 ms/eval (Python); 88s
|
||||
lpinf: corr physarumA 0.307; members 3756; changed in 1 s 0.282; presence step max 1.0000 (237 steps > .1 of 1500); sites visited per evaluation 181; 0.66 ms/eval (Python); 87s
|
||||
skein-web: corr physarumA 0.296; members 1475; changed in 1 s 0.216; presence step max 0.0656 (0 steps > .1 of 1500); sites visited per evaluation 187; 0.41 ms/eval (Python); 110s
|
||||
mult-strands: corr physarumA 0.388; members 4443; changed in 1 s 0.248; presence step max 0.1522 (8 steps > .1 of 1500); sites visited per evaluation 193; 0.73 ms/eval (Python); 99s
|
||||
11
docs/research/2026-10-14-body-voronoi-probe/run2.log
Normal file
11
docs/research/2026-10-14-body-voronoi-probe/run2.log
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
warp at cycle start True; grid 256; sigma [482, 180, 180, 305, 146, 103]; edge width 40.0 u; web at cycle start True
|
||||
pot-well: corr physarumA 0.279; members 3588; changed in 1 s 0.326; presence step max 1.0000 (1 steps > .1 of 1500); sites visited per evaluation 181; 0.65 ms/eval (Python); 86s
|
||||
pot-newton: corr physarumA 0.338; members 689; changed in 1 s 0.299; presence step max 0.0483 (0 steps > .1 of 1500); sites visited per evaluation 181; 0.60 ms/eval (Python); 84s
|
||||
mult: corr physarumA 0.335; members 3270; changed in 1 s 0.279; presence step max 0.0910 (0 steps > .1 of 1500); sites visited per evaluation 181; 0.51 ms/eval (Python); 85s
|
||||
add: corr physarumA 0.263; members 4077; changed in 1 s 0.338; presence step max 0.0899 (0 steps > .1 of 1500); sites visited per evaluation 181; 0.50 ms/eval (Python); 84s
|
||||
power: corr physarumA 0.313; members 3817; changed in 1 s 0.312; presence step max 0.0287 (0 steps > .1 of 1500); sites visited per evaluation 181; 0.74 ms/eval (Python); 85s
|
||||
flow-aniso: corr physarumA 0.322; members 4090; changed in 1 s 0.300; presence step max 0.0373 (0 steps > .1 of 1500); sites visited per evaluation 181; 0.66 ms/eval (Python); 88s
|
||||
lp05: corr physarumA 0.274; members 6662; changed in 1 s 0.306; presence step max 1.0000 (72 steps > .1 of 1500); sites visited per evaluation 181; 0.74 ms/eval (Python); 87s
|
||||
lpinf: corr physarumA 0.307; members 3761; changed in 1 s 0.275; presence step max 1.0000 (237 steps > .1 of 1500); sites visited per evaluation 181; 0.61 ms/eval (Python); 85s
|
||||
skein-web: corr physarumA 0.296; members 1501; changed in 1 s 0.205; presence step max 0.0138 (0 steps > .1 of 1500); sites visited per evaluation 187; 0.36 ms/eval (Python); 106s
|
||||
mult-strands: corr physarumA 0.388; members 4462; changed in 1 s 0.238; presence step max 0.0910 (0 steps > .1 of 1500); sites visited per evaluation 193; 0.62 ms/eval (Python); 89s
|
||||
2
docs/research/2026-10-14-body-voronoi-probe/run3.log
Normal file
2
docs/research/2026-10-14-body-voronoi-probe/run3.log
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
warp at cycle start True; grid 256; sigma [482, 180, 180, 305, 146, 103]; edge width 40.0 u; web at cycle start True
|
||||
power-strands: corr physarumA 0.371; members 4965; changed in 1 s 0.269; presence step max 0.0287 (0 steps > .1 of 1500); sites visited per evaluation 193; 0.60 ms/eval (Python); 88s
|
||||
26
docs/research/2026-10-14-body-voronoi-probe/trace.py
Normal file
26
docs/research/2026-10-14-body-voronoi-probe/trace.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"""Trace the worst presence step of the edges-only layer: where it is and what moves."""
|
||||
import sys, os, math; sys.argv = ['x', '256']
|
||||
import body_voronoi_probe as B
|
||||
T, H = B.T, B.H
|
||||
cands = T.cand_list(16.0)
|
||||
name = os.environ.get("M", "mult"); met = {"mult": B.m_mult, "power": B.m_power, "add": B.m_add}[name]
|
||||
def f(x, z, t, sc):
|
||||
s, b, c = T.env(x, z, t); q = T.warp(x, z, t, sc); return B.edge_profile(B.body_border(q, met)[0]) * (1 - c), q
|
||||
T.set_time(100.0)
|
||||
near = [c for c in cands if abs(T.density_at(lambda x, z, t, sc: f(x, z, t, sc)[0], c, 100.0)[0] - c[4]) < .3][:1500]
|
||||
best = (0, None)
|
||||
prev = {}
|
||||
for k in range(61):
|
||||
tt = 100 + k / 60; T.set_time(tt)
|
||||
for c in near:
|
||||
sc, t0 = T.cyc_scroll(tt, c[5]); d, q = f(c[2], c[3], tt, sc); p = T.presence(d, c[4])
|
||||
if c in prev and prev[c][1] == t0:
|
||||
st = abs(p - prev[c][0])
|
||||
if st > best[0]: best = (st, c, k, d, prev[c][2], q, prev[c][3], t0)
|
||||
prev[c] = (p, t0, d, q)
|
||||
st, c, k, d, dprev, q, qprev, t0 = best
|
||||
print(f"{name}: step {st:.3f} at cand ({c[2]:.0f},{c[3]:.0f}) thr {c[4]:.2f} frame {k}: density {dprev:.3f} -> {d:.3f}; warped q {qprev} -> {q}; t0 {t0}")
|
||||
for kk in range(k - 3, k + 3):
|
||||
tt = 100 + kk / 60; T.set_time(tt); sc, t0 = T.cyc_scroll(tt, c[5]); s, b, cc = T.env(c[2], c[3], tt)
|
||||
qq = T.warp(c[2], c[3], tt, sc); dist, n = B.body_border(qq, met)
|
||||
print(f" frame {kk}: t0 {t0:.3f} slope {s:.4f} core {cc:.3f} border dist {dist:.2f} warped q ({qq[0]:.2f},{qq[1]:.2f})")
|
||||
6
docs/research/2026-10-14-fauna-habitat-probe/README.md
Normal file
6
docs/research/2026-10-14-fauna-habitat-probe/README.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Fauna habitat probe (2026-10-14)
|
||||
|
||||
Serves ruling `aetheria-fauna:ruling:fauna-placement-ruled` and the questions `aetheria-fauna:question:habitat-density-driver` and `:habitat-no-pop`, raised in `cut-fauna-habitat-placement.r1`. Facts are map rows B25-B25h in `docs/aetheria-fauna-map.md`.
|
||||
`habitat_probe.py` is a pure-Python, float64 transliteration of `FlowField` (branch `eureka/aetheria-fauna-flow-field` at `5f5a1e4d`) and `Zone.GetHeight`/`GetNormal` (`b836af48`) on the B20 zone. It is not the C# law, which is float32; the images are qualitative evidence, and the numbers are the transliteration's.
|
||||
Images: `option_density_*` and `option_members_*` (one per density-driver option, spec r1 constants, slope terms on), `option_nopop_*` (one per no-pop option; red = a member that popped), and the earlier `density_c*` and `members_c0` renders (normalised per image).
|
||||
To rerun, copy this folder to a scratch directory (the scripts write PNGs next to themselves), then `py -3 habitat_options.py`, `py -3 pop_maps.py`, `py -3 lattice_probe.py`, `py -3 pop_probe.py` (env FADE), `py -3 pop_cycle.py`, or `py -3 habitat_probe.py 256 live|swirl <candidates> <member candidates>`. Python 3 only, no packages; each takes 1-5 minutes.
|
||||
BIN
docs/research/2026-10-14-fauna-habitat-probe/density_c0_slope_band_live.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/density_c0_slope_band_live.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-fauna-habitat-probe/density_c1_windrows_live.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/density_c1_windrows_live.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-fauna-habitat-probe/density_c2_strain_live.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/density_c2_strain_live.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-fauna-habitat-probe/density_c5_contour_currents_swirl.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/density_c5_contour_currents_swirl.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-fauna-habitat-probe/density_c6_band_soft_windrows_swirl.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/density_c6_band_soft_windrows_swirl.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-fauna-habitat-probe/density_c7_band_wide_contour_swirl.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/density_c7_band_wide_contour_swirl.png
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -0,0 +1,69 @@
|
|||
"""Density and member images per option of aetheria-fauna:question:habitat-density-driver, under the spec's law
|
||||
(cut-fauna-habitat-placement.r1): absolute density in [0, 1], fixed constants (no zone-wide normalisation),
|
||||
candidate threshold .1 + .9 h, presence = smoothstep(thr - .1, thr + .1, density), hosted = presence > 0.
|
||||
Slope terms on (swirl 6, slide 1.5: flow-slope-terms-ruled turns them on). Uses habitat_probe.py's transliteration.
|
||||
"""
|
||||
import math, os, sys, zlib, struct
|
||||
sys.argv = sys.argv[:1]
|
||||
import habitat_probe as H
|
||||
|
||||
H.FLOW.update(swirl=6.0, slope=1.5)
|
||||
OUT = H.OUT; R = H.R; T = 100.0
|
||||
|
||||
|
||||
def band(s): return H.deposition(s) / 2
|
||||
|
||||
|
||||
OPTIONS = { # option label -> density of the locals (s, div, W, det, a)
|
||||
"band-only": lambda s, div, W, det, a: band(s),
|
||||
"band-times-convergence": lambda s, div, W, det, a: band(s) * H.sig(1.5 * -div / .15),
|
||||
"band-times-strain": lambda s, div, W, det, a: band(s) * H.sig(3 * W / .086),
|
||||
"band-times-contour-flow": lambda s, div, W, det, a: band(s) * (.25 + .75 * math.exp(-(a / .45) ** 2)),
|
||||
}
|
||||
|
||||
|
||||
def read_png(path):
|
||||
d = open(path, "rb").read(); i = 8; idat = b""; w = h = 0
|
||||
while i < len(d):
|
||||
n = struct.unpack(">I", d[i:i + 4])[0]; t = d[i + 4:i + 8]; c = d[i + 8:i + 8 + n]
|
||||
if t == b"IHDR": w, h = struct.unpack(">II", c[:8])
|
||||
if t == b"IDAT": idat += c
|
||||
i += 12 + n
|
||||
return zlib.decompress(idat)
|
||||
|
||||
|
||||
old1 = os.path.join(OUT, "members_c1_windrows.png"); old6 = os.path.join(OUT, "members_c6_band_soft_windrows_swirl.png")
|
||||
if os.path.exists(old1) and os.path.exists(old6):
|
||||
print(f"old members_c1 vs old members_c6_swirl: bytes identical = {read_png(old1) == read_png(old6)}")
|
||||
|
||||
# densities on a 256 grid
|
||||
G = 256; grid = []
|
||||
sc = H.scroll(T)
|
||||
for j in range(G):
|
||||
for i in range(G):
|
||||
x = ((i + .5) / G - .5) * 2 * R; z = ((j + .5) / G - .5) * 2 * R
|
||||
grid.append(None if x * x + z * z > R * R else H.locals_at(x, z, T, sc))
|
||||
for name, f in OPTIONS.items():
|
||||
px = [(0, 0, 0) if v is None else H.ramp(f(*v)) for v in grid] # absolute [0, 1] scale, same for all options
|
||||
H.mark_bodies(px, G); p = os.path.join(OUT, f"option_density_{name}.png"); H.write_png(p, G, G, px); print("png", p)
|
||||
|
||||
# members: 16 u jittered lattice, evaluated at T, T+1, T+8
|
||||
C = 16.0; n = int(2 * R / C); cands = []
|
||||
for j in range(n):
|
||||
for i in range(n):
|
||||
x = (i + H.hash01(i, j, 1)) * C - R; z = (j + H.hash01(i, j, 2)) * C - R
|
||||
if x * x + z * z <= R * R: cands.append((i, j, x, z, .1 + .9 * H.hash01(i, j, 3)))
|
||||
loc = {}
|
||||
for tt in (T, T + 1.0, T + 8.0):
|
||||
s2 = H.scroll(tt); loc[tt] = [H.locals_at(x, z, tt, s2) for (_, _, x, z, _) in cands]
|
||||
for name, f in OPTIONS.items():
|
||||
pres = {tt: [H.smoothstep(c[4] - .1, c[4] + .1, f(*v)) for c, v in zip(cands, loc[tt])] for tt in loc}
|
||||
hosted = {tt: {k for k, p in enumerate(pres[tt]) if p > 0} for tt in pres}
|
||||
a = hosted[T]
|
||||
print(f"{name}: {len(a)} hosted of {len(cands)} candidates; changed in 1 s {len(a ^ hosted[T + 1]) / max(1, len(a)):.3f}, in 8 s {len(a ^ hosted[T + 8]) / max(1, len(a)):.3f} (continuous evaluation)")
|
||||
W2 = 600; px = [(10, 14, 30)] * (W2 * W2)
|
||||
for k in a:
|
||||
_, _, x, z, _ = cands[k]; b = pres[T][k]
|
||||
pi_, pj = int((x / (2 * R) + .5) * W2), int((z / (2 * R) + .5) * W2)
|
||||
if 0 <= pi_ < W2 and 0 <= pj < W2: px[pj * W2 + pi_] = tuple(int(30 + (c - 30) * b) for c in (235, 230, 170))
|
||||
H.mark_bodies(px, W2); p = os.path.join(OUT, f"option_members_{name}.png"); H.write_png(p, W2, W2, px); print("png", p)
|
||||
211
docs/research/2026-10-14-fauna-habitat-probe/habitat_probe.py
Normal file
211
docs/research/2026-10-14-fauna-habitat-probe/habitat_probe.py
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
"""Habitat closed-form probe (Imagination, aetheria-fauna r3). Pure Python, no deps.
|
||||
|
||||
Transliterates, term for term:
|
||||
- FlowField.Global / Slope / Scroll (branch eureka/aetheria-fauna-flow-field @ 5f5a1e4d, FlowField.cs)
|
||||
- Zone.GetHeight / GetNormal / PowerPulse / RadialWaves (b836af48, Zone.cs:631-720)
|
||||
- the earlier probe's zone: same bodies, same PlanetSettings values (2026-10-13 probe)
|
||||
Evaluates candidate pointwise habitat densities over the zone, writes a PNG per candidate, member-dot PNGs
|
||||
under the fixed-threshold (HZD dither) placement, and turnover numbers. Float64 here; C# is float32.
|
||||
"""
|
||||
import math, struct, zlib, sys, os, time
|
||||
|
||||
OUT = os.path.dirname(os.path.abspath(__file__))
|
||||
R = 2000.0
|
||||
G = int(sys.argv[1]) if len(sys.argv) > 1 else 200
|
||||
GOLD = 1.61803398875
|
||||
|
||||
# --- PlanetSettings (earlier probe = Settings.asset values) ---
|
||||
def curve(m, e, mul, c=0.0): return mul * m ** e + c
|
||||
BODIES = [("Sun", 14152, 0, 0), ("Planet", 274, 610, .4), ("Planet", 274, 1050, 2.6),
|
||||
("GasGiant", 2260, 1400, 4.4), ("Planet", 120, 1700, 4.6), ("Planet", 29, 450, 3.9)]
|
||||
WELLS = []
|
||||
for kind, m, d, ph in BODIES:
|
||||
pos = (math.cos(ph) * d, math.sin(ph) * d)
|
||||
w = dict(pos=pos, r=curve(m, .25, 500), depth=curve(m, .175, 30), exp=16.0, gas=kind in ("Sun", "GasGiant"))
|
||||
if w["gas"]: # Sun : GasGiant; GasGiantData defaults: radius x1, depth x1, speed x8
|
||||
w.update(wr=curve(m, .25, 500), wd=curve(m, .3, .5), wf=curve(m, .45, .2), ws=.025 * 8)
|
||||
WELLS.append(w)
|
||||
|
||||
def pp(x, e):
|
||||
x *= 2; x = max(-1.0, min(1.0, x)); return ((x + 1) * (1 - x)) ** e
|
||||
|
||||
def height(x, z, t):
|
||||
h = -pp(math.hypot(x, z) / (R * 2), 2) * 64
|
||||
for w in WELLS:
|
||||
dx, dz = x - w["pos"][0], z - w["pos"][1]
|
||||
d2 = dx * dx + dz * dz
|
||||
if d2 < w["r"] ** 2: h -= pp(math.sqrt(d2) / w["r"], w["exp"]) * w["depth"]
|
||||
if w["gas"] and d2 < w["wr"] ** 2:
|
||||
u = math.sqrt(d2) / w["wr"]
|
||||
h -= pp(u, 8) * math.cos((u * 2) ** 1.25 * w["wf"] + t * w["ws"]) * w["wd"]
|
||||
return h
|
||||
|
||||
def normal(x, z, t, step=.1):
|
||||
a = height(x - step, z, t) - height(x + step, z, t)
|
||||
b = height(x, z - step, t) - height(x, z + step, t)
|
||||
n = math.sqrt(a * a + b * b + (2 * step) ** 2)
|
||||
return a / n, 2 * step / n, b / n # .xzy: (x, y=up, z)
|
||||
|
||||
# --- FlowField (5f5a1e4d) ---
|
||||
FLOW = dict(scale=512.0, amp=15.0, speed=.025, slope=0.0, swirl=0.0)
|
||||
def frac(v): return v - math.floor(v)
|
||||
def tri(v): return abs(frac(v) - .5)
|
||||
def tri3(p): return (tri(p[2] + tri(p[1])), tri(p[2] + tri(p[0])), tri(p[1] + tri(p[0])))
|
||||
def nrm(v):
|
||||
l = math.sqrt(v[0] ** 2 + v[1] ** 2 + v[2] ** 2); return (v[0] / l, v[1] / l, v[2] / l)
|
||||
def scroll(t): s = FLOW["speed"] * t; return frac(s), frac(s * GOLD)
|
||||
def flow_global(x, y, z, sc):
|
||||
f = (x / FLOW["scale"], y / FLOW["scale"], z / FLOW["scale"])
|
||||
a = nrm(tri3((f[0], f[1] - sc[0], f[2])))
|
||||
b = nrm(tri3((f[0] * GOLD, f[1] * GOLD - sc[1], f[2] * GOLD)))
|
||||
c = (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0])
|
||||
A = FLOW["amp"]; return c[0] * A, c[1] * A * .5, c[2] * A
|
||||
def flow_xz(x, z, t, sc):
|
||||
g = flow_global(x, 0.0, z, sc); ux, uz = g[0], g[2]
|
||||
if FLOW["slope"] or FLOW["swirl"]:
|
||||
n = normal(x, z, t); dx, dz = n[0], n[2]; l = math.hypot(dx, dz)
|
||||
if l > 0:
|
||||
dx, dz = dx / l, dz / l
|
||||
ux += dz * FLOW["swirl"] + dx * FLOW["slope"]; uz += -dx * FLOW["swirl"] + dz * FLOW["slope"]
|
||||
return ux, uz
|
||||
|
||||
# --- local quantities at a point (all pointwise) ---
|
||||
E = 6.0 # finite-difference step for flow and Hessian derivatives (flow scale 512)
|
||||
def smoothstep(a, b, v): v = max(0.0, min(1.0, (v - a) / (b - a))); return v * v * (3 - 2 * v)
|
||||
def deposition(s): # the operator's Slime Renderer.prefab Deposition curve
|
||||
return 0.0 if s > .8 else 2.0 * smoothstep(0.0, .6, s) ** .8
|
||||
def sig(v): return 1.0 / (1.0 + math.exp(-max(-40.0, min(40.0, v))))
|
||||
|
||||
def locals_at(x, z, t, sc):
|
||||
n = normal(x, z, t); s = math.acos(max(-1.0, min(1.0, n[1]))) / (math.pi / 2)
|
||||
uxp, uzp = flow_xz(x + E, z, t, sc); uxm, uzm = flow_xz(x - E, z, t, sc)
|
||||
uxq, uzq = flow_xz(x, z + E, t, sc); uxr, uzr = flow_xz(x, z - E, t, sc)
|
||||
dux_dx, duz_dx = (uxp - uxm) / (2 * E), (uzp - uzm) / (2 * E)
|
||||
dux_dz, duz_dz = (uxq - uxr) / (2 * E), (uzq - uzr) / (2 * E)
|
||||
div = dux_dx + duz_dz
|
||||
sn, ss, om = dux_dx - duz_dz, duz_dx + dux_dz, duz_dx - dux_dz
|
||||
W = sn * sn + ss * ss - om * om
|
||||
h0 = height(x, z, t)
|
||||
hxx = (height(x + E, z, t) - 2 * h0 + height(x - E, z, t)) / E ** 2
|
||||
hzz = (height(x, z + E, t) - 2 * h0 + height(x, z - E, t)) / E ** 2
|
||||
hxz = (height(x + E, z + E, t) - height(x + E, z - E, t) - height(x - E, z + E, t) + height(x - E, z - E, t)) / (4 * E * E)
|
||||
det = hxx * hzz - hxz * hxz
|
||||
ux, uz = flow_xz(x, z, t, sc); dx, dz = n[0], n[2]; dl = math.hypot(dx, dz); ul = math.hypot(ux, uz)
|
||||
a = 0.0 if dl == 0 or ul == 0 else (ux * dx + uz * dz) / (dl * ul)
|
||||
return s, div, W, det, a
|
||||
|
||||
# scale constants (set from the grid's own spread on the first pass)
|
||||
K = dict(div=1.0, W=1.0, det=1.0)
|
||||
CANDS = {
|
||||
"c0_slope_band": lambda s, div, W, det, a: deposition(s) / 2,
|
||||
"c1_windrows": lambda s, div, W, det, a: deposition(s) / 2 * sig(3 * -div / K["div"]),
|
||||
"c2_strain": lambda s, div, W, det, a: deposition(s) / 2 * sig(3 * W / K["W"]),
|
||||
"c3_saddle_links": lambda s, div, W, det, a: (0.0 if s > .8 else 1.0) * sig(3 * -det / K["det"]) * smoothstep(0.02, .25, s),
|
||||
"c4_links_windrows": lambda s, div, W, det, a: (0.0 if s > .8 else 1.0) * sig(3 * -det / K["det"]) * smoothstep(0.02, .25, s) * sig(3 * -div / K["div"]),
|
||||
# krill hold where the current runs along the well's contour: flow neither climbs nor falls (a ~ 0)
|
||||
"c6_band_soft_windrows": lambda s, div, W, det, a: deposition(s) / 2 * sig(1.5 * -div / K["div"]),
|
||||
"c7_band_wide_contour": lambda s, div, W, det, a: deposition(s) / 2 * (.25 + .75 * math.exp(-(a / .45) ** 2)),
|
||||
"c5_contour_currents": lambda s, div, W, det, a: deposition(s) / 2 * math.exp(-(a / .2) ** 2),
|
||||
}
|
||||
|
||||
def grid_pass(t):
|
||||
sc = scroll(t); rows = []
|
||||
for j in range(G):
|
||||
row = []
|
||||
for i in range(G):
|
||||
x = ((i + .5) / G - .5) * 2 * R; z = ((j + .5) / G - .5) * 2 * R
|
||||
row.append(None if x * x + z * z > R * R else locals_at(x, z, t, sc))
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
def std(vals):
|
||||
m = sum(vals) / len(vals); return math.sqrt(sum((v - m) ** 2 for v in vals) / len(vals)) or 1.0
|
||||
|
||||
# --- PNG ---
|
||||
def ramp(v): # dark navy -> teal -> pale gold
|
||||
v = max(0.0, min(1.0, v)); stops = [(0, (10, 14, 30)), (.45, (20, 120, 130)), (.8, (200, 210, 120)), (1, (255, 245, 210))]
|
||||
for (a, ca), (b, cb) in zip(stops, stops[1:]):
|
||||
if v <= b:
|
||||
f = (v - a) / (b - a); return tuple(int(ca[k] + (cb[k] - ca[k]) * f) for k in range(3))
|
||||
def write_png(path, w, h, px):
|
||||
raw = b"".join(b"\x00" + bytes(c for p in px[y * w:(y + 1) * w] for c in p) for y in range(h - 1, -1, -1))
|
||||
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", w, h, 8, 2, 0, 0, 0))
|
||||
+ chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b""))
|
||||
|
||||
def mark_bodies(px, w):
|
||||
for wl in WELLS:
|
||||
i = int((wl["pos"][0] / (2 * R) + .5) * w); j = int((wl["pos"][1] / (2 * R) + .5) * w)
|
||||
for di in range(-2, 3):
|
||||
for dj in range(-2, 3):
|
||||
if 0 <= i + di < w and 0 <= j + dj < w: px[(j + dj) * w + i + di] = (255, 60, 60)
|
||||
|
||||
# --- physarum A from the 2026-10-13 probe (ASCII 64x32) for a correlation against the slime look ---
|
||||
def physarum_ascii(name):
|
||||
p = r"C:\aeth-wt\fauna-target-r3\docs\research\2026-10-13-fauna-placement-probe\output.txt"
|
||||
lines = open(p, encoding="utf-8").read().splitlines(); rampc = " .:-=+*#%@"
|
||||
k = next(i for i, l in enumerate(lines) if l.startswith("@@MAP " + name))
|
||||
rows = [l for l in lines[k + 1:k + 40] if l.startswith("|")][:32]
|
||||
return [[rampc.index(c) for c in r[1:65]] for r in reversed(rows)] # printed top=y high
|
||||
|
||||
def corr(a, b):
|
||||
ma, mb = sum(a) / len(a), sum(b) / len(b)
|
||||
sa = math.sqrt(sum((v - ma) ** 2 for v in a)); sb = math.sqrt(sum((v - mb) ** 2 for v in b))
|
||||
return sum((x - ma) * (y - mb) for x, y in zip(a, b)) / (sa * sb or 1)
|
||||
|
||||
def hash01(i, j, salt):
|
||||
s = (i * 73856093 ^ j * 19349663 ^ salt * 83492791) & 0xffffffff
|
||||
s ^= 2747636419; s = (s * 2654435769) & 0xffffffff; s ^= s >> 16; s = (s * 2654435769) & 0xffffffff; s ^= s >> 16
|
||||
s = (s * 2654435769) & 0xffffffff; return s / 4294967295.0
|
||||
|
||||
ONLY = sys.argv[3].split(",") if len(sys.argv) > 3 else list(CANDS)
|
||||
MEMB = sys.argv[4].split(",") if len(sys.argv) > 4 and sys.argv[4] else []
|
||||
def main():
|
||||
tag = sys.argv[2] if len(sys.argv) > 2 else "live"
|
||||
if tag == "swirl": FLOW.update(swirl=6.0, slope=1.5)
|
||||
t0 = time.time(); T = 100.0
|
||||
L = grid_pass(T)
|
||||
flat = [v for r in L for v in r if v is not None]
|
||||
K["div"] = std([v[1] for v in flat]); K["W"] = std([v[2] for v in flat]); K["det"] = std([v[3] for v in flat])
|
||||
print(f"[{tag}] grid {G}x{G} at t={T}: {time.time() - t0:.1f}s; std div={K['div']:.4g} W={K['W']:.4g} det={K['det']:.4g}")
|
||||
phys = {n: [c for r in physarum_ascii(n) for c in r] for n in ("physarum A", "physarum B")}
|
||||
for name, f in CANDS.items():
|
||||
if name not in ONLY: continue
|
||||
vals = [[None if v is None else f(*v) for v in r] for r in L]
|
||||
mx = max(v for r in vals for v in r if v is not None) or 1
|
||||
px = [(0, 0, 0) if v is None else ramp(v / mx) for r in vals for v in r]
|
||||
mark_bodies(px, G); path = os.path.join(OUT, f"density_{name}_{tag}.png"); write_png(path, G, G, px)
|
||||
# 64x32 block means for correlation with the physarum maps
|
||||
bw, bh = G // 64, G // 32; blocks = []
|
||||
for by in range(32):
|
||||
for bx in range(64):
|
||||
cell = [vals[y][x] or 0 for y in range(by * bh, (by + 1) * bh) for x in range(bx * bw, (bx + 1) * bw)]
|
||||
blocks.append(sum(cell) / len(cell))
|
||||
cov = sum(1 for v in flat if f(*v) > .5 * mx) / len(flat)
|
||||
print(f" {name}: png {path}\n corr physarumA={corr(blocks, phys['physarum A']):.3f} physarumB={corr(blocks, phys['physarum B']):.3f} area>half-max={cov:.3f}")
|
||||
# members under fixed per-candidate thresholds (HZD dither shape): jittered lattice, cell C, hashed threshold
|
||||
C = 16.0; n = int(2 * R / C); P = 8.0
|
||||
for name in MEMB:
|
||||
f = CANDS[name]; mx = None; members = {}; t1 = time.time()
|
||||
for tt in (T, T + 1.0, T + P):
|
||||
sc = scroll(tt); m = set(); dens = {}
|
||||
for j in range(n):
|
||||
for i in range(n):
|
||||
x = (i + hash01(i, j, 1)) * C - R; z = (j + hash01(i, j, 2)) * C - R
|
||||
if x * x + z * z > R * R: continue
|
||||
d = f(*locals_at(x, z, tt, sc)); dens[(i, j)] = d
|
||||
if mx is None: mx = max(dens.values()) or 1
|
||||
for k, d in dens.items():
|
||||
if d / mx > hash01(k[0], k[1], 3) ** .5 * .9 + .1: m.add(k) # threshold in (.1, 1)
|
||||
members[tt] = m
|
||||
a, b, c = members[T], members[T + 1.0], members[T + P]
|
||||
print(f" members {name}: {len(a)} of ~{int(math.pi * (R / C) ** 2)} candidates; changed in 1 s {len(a ^ b) / max(1, len(a)):.3f}, in {P:.0f} s {len(a ^ c) / max(1, len(a)):.3f} ({time.time() - t1:.0f}s)")
|
||||
W2 = 600; px = [(10, 14, 30)] * (W2 * W2)
|
||||
for (i, j) in a:
|
||||
x = (i + hash01(i, j, 1)) * C - R; z = (j + hash01(i, j, 2)) * C - R
|
||||
pi_, pj = int((x / (2 * R) + .5) * W2), int((z / (2 * R) + .5) * W2)
|
||||
if 0 <= pi_ < W2 and 0 <= pj < W2: px[pj * W2 + pi_] = (235, 230, 170)
|
||||
mark_bodies(px, W2); path = os.path.join(OUT, f"members_{name}_{tag}.png"); write_png(path, W2, W2, px); print(f" png {path}")
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
"""Is density_c6's triangular lattice the FlowField law's own structure or a probe artifact?
|
||||
Transect along x at z = 300 (y = 0, t = 100, slope terms off: the global term alone), 0.5 u spacing over 2048 u.
|
||||
1. Kinks of the law: tri(v) = |frac(v) - .5| is piecewise linear, so Tri3D's derivatives jump where an argument of
|
||||
a tri crosses a multiple of 1/2. Predict those x from the law's own arguments (both octaves) and check where the
|
||||
largest convergence jumps sit.
|
||||
2. Finite-difference step: convergence with step 6 (probe and spec) vs step .5: correlation.
|
||||
3. float32: the flow evaluated with every input and the tri arguments rounded to float32 vs float64: max difference.
|
||||
"""
|
||||
import math, struct, sys
|
||||
sys.argv = sys.argv[:1]
|
||||
import habitat_probe as H
|
||||
|
||||
H.FLOW.update(swirl=0.0, slope=0.0)
|
||||
T = 100.0; sc = H.scroll(T); Z = 300.0; S = H.FLOW["scale"]; GOLD = H.GOLD
|
||||
|
||||
|
||||
def conv(x, z, e):
|
||||
a = H.flow_xz(x + e, z, T, sc); b = H.flow_xz(x - e, z, T, sc)
|
||||
c = H.flow_xz(x, z + e, T, sc); d = H.flow_xz(x, z - e, T, sc)
|
||||
return -((a[0] - b[0]) / (2 * e) + (c[1] - d[1]) / (2 * e))
|
||||
|
||||
|
||||
xs = [-1024 + .5 * k for k in range(4097)]
|
||||
c6 = [conv(x, Z, 6.0) for x in xs]; c05 = [conv(x, Z, .5) for x in xs]
|
||||
m6, m5 = sum(c6) / len(c6), sum(c05) / len(c05)
|
||||
r = sum((a - m6) * (b - m5) for a, b in zip(c6, c05)) / math.sqrt(sum((a - m6) ** 2 for a in c6) * sum((b - m5) ** 2 for b in c05))
|
||||
print(f"FD step 6 vs .5 along the transect: correlation {r:.3f}")
|
||||
|
||||
# predicted kink x: arguments of tri along the transect (y field = -scroll, z fixed), both octaves
|
||||
def kinks():
|
||||
out = []
|
||||
for oc, sh in ((1.0, sc[0]), (GOLD, sc[1])):
|
||||
py, pz = -sh, Z / S * oc
|
||||
ty, tz = H.tri(py), H.tri(pz)
|
||||
# tri(p.x) kinks: p.x = k/2; tri(p.z + tri(p.x)) and tri(p.y + tri(p.x)) kink where tri(p.x) + const = k/2
|
||||
for k in range(-40, 41):
|
||||
out.append(k / 2 * S / oc)
|
||||
for base in (pz, py):
|
||||
for k in range(-40, 41):
|
||||
tv = k / 2 - base # tri(p.x) must equal tv in [0, .5]
|
||||
if 0 <= tv <= .5:
|
||||
for m in range(-20, 21):
|
||||
for px in (m + .5 - tv, m + .5 + tv):
|
||||
out.append(px * S / oc)
|
||||
return sorted(set(round(v, 3) for v in out if -1030 < v < 1030))
|
||||
|
||||
|
||||
K = kinks()
|
||||
jumps = sorted(((abs(c05[k + 1] - c05[k]), xs[k]) for k in range(len(xs) - 1)), reverse=True)[:40]
|
||||
near = sum(1 for _, x in jumps if min(abs(x - k) for k in K) <= 1.0)
|
||||
print(f"law kink lines crossing the 2048 u transect: {len(K)}; of the 40 largest convergence jumps (step .5), {near} lie within 1 u of a predicted kink")
|
||||
spacing = sorted(K); gaps = [b - a for a, b in zip(spacing, spacing[1:]) if b - a > .5]
|
||||
print(f"median spacing of kink lines along x: {sorted(gaps)[len(gaps) // 2]:.1f} u (flow scale {S:.0f} u)")
|
||||
|
||||
|
||||
def f32(v): return struct.unpack("f", struct.pack("f", v))[0]
|
||||
|
||||
|
||||
md = 0.0
|
||||
for x in xs[::8]:
|
||||
a = H.flow_global(x, 0.0, Z, sc)
|
||||
b = H.flow_global(f32(x), 0.0, f32(Z), (f32(sc[0]), f32(sc[1])))
|
||||
md = max(md, abs(a[0] - b[0]), abs(a[2] - b[2]))
|
||||
print(f"flow with float32-rounded inputs vs float64: max |difference| {md:.2e} u/s (amplitude 15)")
|
||||
BIN
docs/research/2026-10-14-fauna-habitat-probe/members_c0_slope_band.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/members_c0_slope_band.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_density_band-only.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_density_band-only.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_density_band-times-contour-flow.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_density_band-times-contour-flow.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_density_band-times-convergence.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_density_band-times-convergence.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_density_band-times-strain.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_density_band-times-strain.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_members_band-only.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_members_band-only.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_members_band-times-contour-flow.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_members_band-times-contour-flow.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_members_band-times-convergence.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_members_band-times-convergence.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_members_band-times-strain.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_members_band-times-strain.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_nopop_cycle-sampled-all.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_nopop_cycle-sampled-all.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_nopop_cycle-sampled-flow.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_nopop_cycle-sampled-flow.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_nopop_presence-fade.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-fauna-habitat-probe/option_nopop_presence-fade.png
(Stored with Git LFS)
Normal file
Binary file not shown.
32
docs/research/2026-10-14-fauna-habitat-probe/pop_cycle.py
Normal file
32
docs/research/2026-10-14-fauna-habitat-probe/pop_cycle.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# cycle-sampled-flow: band at now, convergence at the member's own cycle start (period 8 s, hashed phase).
|
||||
import math, sys, os
|
||||
sys.argv = sys.argv[:1]
|
||||
import habitat_probe as H
|
||||
H.FLOW.update(swirl=6.0, slope=1.5)
|
||||
P, F, C, R = 8.0, .1, 16.0, H.R
|
||||
cands = []
|
||||
for j in range(250):
|
||||
for i in range(250):
|
||||
x = (i + H.hash01(i, j, 1)) * C - R; z = (j + H.hash01(i, j, 2)) * C - R
|
||||
if x * x + z * z < R * R: cands.append((x, z, .1 + .9 * H.hash01(i, j, 3), H.hash01(i, j, 4)))
|
||||
sc0 = H.scroll(100.0)
|
||||
cands = [c for c in cands if H.deposition(H.locals_at(c[0], c[1], 100.0, sc0)[0]) / 2 > .05][:600]
|
||||
def start(t, ph): return math.floor(t / P + ph) * P - ph * P
|
||||
cache = {}
|
||||
def pres(c, t):
|
||||
x, z, th, ph = c; t0 = start(t, ph)
|
||||
key = (x, z, t0)
|
||||
if key not in cache: cache[key] = H.locals_at(x, z, t0, H.scroll(t0))[1]
|
||||
s = H.locals_at(x, z, t, H.scroll(t))[0]
|
||||
d = H.deposition(s) / 2 * H.sig(1.5 * -cache[key] / .15)
|
||||
return H.smoothstep(th - F, th + F, d), t0
|
||||
prev = None; maxd = 0.0; jumps = 0; boundary = 0
|
||||
for k in range(181):
|
||||
t = 100.0 + k / 60.0
|
||||
cur = [pres(c, t) for c in cands]
|
||||
if prev:
|
||||
for (a, s0), (b, s1) in zip(prev, cur):
|
||||
if s0 != s1: boundary += 1; continue # the cycle turned: the member is faded here by the flow-coupling ruling
|
||||
maxd = max(maxd, abs(a - b)); jumps += abs(a - b) > .25
|
||||
prev = cur
|
||||
print(f"cycle-sampled-flow: {len(cands)} candidates, 180 steps: max |d presence|/step within a cycle {maxd:.4f}, steps>0.25: {jumps}, cycle turns {boundary}")
|
||||
60
docs/research/2026-10-14-fauna-habitat-probe/pop_maps.py
Normal file
60
docs/research/2026-10-14-fauna-habitat-probe/pop_maps.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"""One image per option of aetheria-fauna:question:habitat-no-pop. Law: band-times-convergence (spec r1 constants),
|
||||
slope terms on, 16 u lattice thinned to every second cell on the shoulders (band > .05 at t = 100), fade .1,
|
||||
cycle period 8 s with a hashed phase. Steps of 1/60 s for 1 s. A dot is a hosted member at t = 100, brightness its
|
||||
presence; red marks a member whose presence moved by more than .1 in one step without its cycle turning
|
||||
(a pop); a cycle turn is excluded because the advected cycle fades the member there (fauna-flow-coupling-ruled).
|
||||
"""
|
||||
import math, os, sys
|
||||
sys.argv = sys.argv[:1]
|
||||
import habitat_probe as H
|
||||
|
||||
H.FLOW.update(swirl=6.0, slope=1.5)
|
||||
R, C, P, F, T = H.R, 16.0, 8.0, .1, 100.0
|
||||
n = int(2 * R / C); cands = []
|
||||
sc0 = H.scroll(T)
|
||||
for j in range(0, n, 2):
|
||||
for i in range(0, n, 2):
|
||||
x = (i + H.hash01(i, j, 1)) * C - R; z = (j + H.hash01(i, j, 2)) * C - R
|
||||
if x * x + z * z <= R * R and H.deposition(H.locals_at(x, z, T, sc0)[0]) / 2 > .05:
|
||||
cands.append((x, z, .1 + .9 * H.hash01(i, j, 3), H.hash01(i, j, 4)))
|
||||
|
||||
|
||||
def dens(s, div): return H.deposition(s) / 2 * H.sig(1.5 * -div / .15)
|
||||
def start(t, ph): return math.floor(t / P + ph) * P - ph * P
|
||||
|
||||
|
||||
cache = {}
|
||||
def at(x, z, t):
|
||||
k = (x, z, t)
|
||||
if k not in cache: cache[k] = H.locals_at(x, z, t, H.scroll(t))
|
||||
return cache[k]
|
||||
|
||||
|
||||
def presence(mode, c, t):
|
||||
x, z, th, ph = c; t0 = start(t, ph)
|
||||
if mode == "presence-fade": s, div = at(x, z, t)[0], at(x, z, t)[1]
|
||||
elif mode == "cycle-sampled-flow": s, div = at(x, z, t)[0], at(x, z, t0)[1]
|
||||
else: s, div = at(x, z, t0)[0], at(x, z, t0)[1] # cycle-sampled-all
|
||||
return H.smoothstep(th - F, th + F, dens(s, div)), t0
|
||||
|
||||
|
||||
for mode in ("presence-fade", "cycle-sampled-flow", "cycle-sampled-all"):
|
||||
prev = [presence(mode, c, T) for c in cands]; first = [p for p, _ in prev]; popped = set(); maxd = 0.0
|
||||
for k in range(1, 61):
|
||||
cur = [presence(mode, c, T + k / 60.0) for c in cands]
|
||||
for idx, ((a, s0), (b, s1)) in enumerate(zip(prev, cur)):
|
||||
if mode != "presence-fade" and s0 != s1: continue
|
||||
maxd = max(maxd, abs(a - b))
|
||||
if abs(a - b) > .1: popped.add(idx)
|
||||
prev = cur
|
||||
hosted = [i for i, p in enumerate(first) if p > 0]
|
||||
W2 = 600; px = [(10, 14, 30)] * (W2 * W2)
|
||||
for idx in set(hosted) | popped:
|
||||
x, z = cands[idx][0], cands[idx][1]; b = first[idx]
|
||||
col = (255, 50, 50) if idx in popped else tuple(int(30 + (c - 30) * b) for c in (235, 230, 170))
|
||||
pi_, pj = int((x / (2 * R) + .5) * W2), int((z / (2 * R) + .5) * W2)
|
||||
for di in (0, 1):
|
||||
for dj in (0, 1):
|
||||
if 0 <= pi_ + di < W2 and 0 <= pj + dj < W2: px[(pj + dj) * W2 + pi_ + di] = col
|
||||
H.mark_bodies(px, W2); p = os.path.join(H.OUT, f"option_nopop_{mode}.png"); H.write_png(p, W2, W2, px)
|
||||
print(f"{mode}: {len(cands)} shoulder candidates, {len(hosted)} hosted at t=100; 60 steps of 1/60 s: max |d presence| per step (cycle turns excluded) {maxd:.4f}, members popping (> .1 in a step) {len(popped)}; png {p}")
|
||||
28
docs/research/2026-10-14-fauna-habitat-probe/pop_probe.py
Normal file
28
docs/research/2026-10-14-fauna-habitat-probe/pop_probe.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# presence-fade under the c6 law (band x sigmoid(1.5 * conv / .15)), slope terms on, 1/60 steps over 3 s.
|
||||
import math, sys
|
||||
_argv = sys.argv; sys.argv = sys.argv[:1]
|
||||
import habitat_probe as H
|
||||
H.FLOW.update(swirl=float(__import__("os").environ.get("SWIRL", "6")), slope=float(__import__("os").environ.get("SLIDE", "1.5")))
|
||||
def smooth(a, b, v): return H.smoothstep(a, b, v)
|
||||
def density(x, z, t, sc):
|
||||
s, div, W, det, a = H.locals_at(x, z, t, sc)
|
||||
return H.deposition(s) / 2 * H.sig(1.5 * -div / .15)
|
||||
C, R = 16.0, H.R; cands = []
|
||||
for j in range(0, 250, 1):
|
||||
for i in range(0, 250, 1):
|
||||
x = (i + H.hash01(i, j, 1)) * C - R; z = (j + H.hash01(i, j, 2)) * C - R
|
||||
if x * x + z * z < R * R: cands.append((x, z, .1 + .9 * H.hash01(i, j, 3)))
|
||||
# keep candidates on the shoulders (band > .05 at t0) so the sample is the population that can host
|
||||
sc = H.scroll(100.0)
|
||||
cands = [c for c in cands if H.deposition(H.locals_at(c[0], c[1], 100.0, sc)[0]) / 2 > .05][:600]
|
||||
F = float(__import__("os").environ.get("FADE", ".05"))
|
||||
prev = None; maxd = 0.0; maxdd = 0.0; jumps = 0; steps = 180
|
||||
for k in range(steps + 1):
|
||||
t = 100.0 + k / 60.0; sc = H.scroll(t)
|
||||
d = [density(x, z, t, sc) for x, z, _ in cands]
|
||||
p = [smooth(th - F, th + F, v) for v, (_, _, th) in zip(d, cands)]
|
||||
if prev:
|
||||
for a, b, da, db in zip(prev[0], p, prev[1], d):
|
||||
maxd = max(maxd, abs(a - b)); maxdd = max(maxdd, abs(da - db)); jumps += abs(a - b) > .25
|
||||
prev = (p, d)
|
||||
print(f"fade {F}: {len(cands)} shoulder candidates, {steps} steps of 1/60 s: max |d presence|/step {maxd:.3f}, max |d density|/step {maxdd:.4f}, steps>0.25: {jumps}")
|
||||
57
docs/research/2026-10-14-fauna-placement-closed-form.md
Normal file
57
docs/research/2026-10-14-fauna-placement-closed-form.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Closed-form, runtime-evaluated fauna placement: prior-art facts
|
||||
|
||||
Serves ruling `aetheria-fauna:ruling:fauna-placement-ruled` (placement is a deterministic closed form of gravity slope and flow), ahead of fauna target r3. Complements `2026-10-13-fauna-placement-prior-art.md`.
|
||||
Agent eyes-fauna-placement-closed-form, session self-2026-10-14-ag, campaign aetheria-fauna, 2026-10-14. No recommendations.
|
||||
|
||||
Tags: [V] verified by reading the source text this pass (full text or fetched page). [S] a search-result summary or snippet, page not read in full. [M] from memory or derived by me, unverified. "PW" = pointwise (needs only the field and its spatial derivatives at the point, at one instant). "PI" = path-integrated (needs trajectories, history, or a spatial integral).
|
||||
Already covered by the earlier docs (not repeated): Physarum/MCPM, EcoBrush, D8, Maxent basics, float determinism, boids/LOD, radar/DSL clutter.
|
||||
|
||||
## 1. Patchiness from flow: what local quantity predicts aggregation
|
||||
|
||||
1.1 Okubo-Weiss. W = s_n^2 + s_s^2 - omega^2 (normal strain, shear strain, relative vorticity), for horizontally non-divergent 2D flow. W>0 deformation (saddle), W<0 rotation (vortex), W=0 needs quadratic terms. Eigenvalues of the velocity-gradient matrix are (gamma +- sqrt(W))/2 with gamma the divergence; complex eigenvalues give spirals that converge if gamma<0. Sign convention varies between sources (py-eddy-tracker docs contradict their own code). PW: needs only the 2x2 velocity gradient at the point. It classifies local flow topology; it does not itself give a density. [V] https://en.wikipedia.org/wiki/Okubo%E2%80%93Weiss_parameter (page is flagged there for expert attention). Originals: Okubo 1970 Deep-Sea Res. 17(3):445-454; Weiss 1991 Physica D 48:273-294 [V as citations on that page]. 3D analogues cited there: Q criterion (Hunt, Wray, Moin 1988), Chong-Perry-Cantwell 1990, Jeong-Hussain 1995 (lambda2) [V as citations].
|
||||
1.2 Okubo-Weiss to plankton link: no source found that ties W to plankton accumulation directly; W is used to split vortex versus strain regions in an island-wake biology paper (arXiv 0802.3532) and a nutrient-flux study (Sci. Rep. 2024 SI) [S]. Known caveats of W as a vortex detector (Eulerian, frame-dependent, misclassifies in time-dependent flows; Basdevant and Philipovitch 1994) [M].
|
||||
1.3 FTLE: FTLE(x) = log(lambda_max(C))/(2T), C = Cauchy-Green tensor of the flow map over [t, t+T]. Needs advecting particles or a perturbation over T. PI. T is a free parameter. [V] https://lagrangian.readthedocs.io/en/latest/contents.html . Backward-time FTLE ridges approximate attracting LCS; tracer-gradient alignment with backward finite-time Lyapunov vectors is shown for freely decaying 2D turbulence and observed in realistic ocean flows, not presented as general [V, same page].
|
||||
1.4 FTLE failure modes: ridges capture hyperbolic LCS only under additional conditions; ridges connect high stretching with high shear, so false positives; no reliable detection of elliptic structures. A twelve-method benchmark found false positives and negatives even in mathematically justified methods [V] Hadjighasem et al., "A critical comparison of Lagrangian methods for coherent structure detection", Chaos 27, 053104 (2017), https://arxiv.org/abs/1704.05716 (Sec. VI strengths/weaknesses list; also the FTLE paragraph in Sec. II). Haller 2015, Annu. Rev. Fluid Mech. 47:137-162 (review, not read this pass) [S].
|
||||
1.5 FTLE/FSLE and plankton, field evidence: d'Ovidio et al. 2004 GRL (Mediterranean mixing structures, FSLE); Lehahn, d'Ovidio, Levy, Heifetz 2007 (stirring of the NE Atlantic spring bloom); a Palmer Deep canyon study reporting phytoplankton patches associated with FTLE ridges, strongest in stratified conditions [S, summaries only]. d'Ovidio et al. 2010 PNAS "Fluid dynamical niches of phytoplankton types" [M]. All need a velocity time series and advection: PI.
|
||||
1.6 Infinitesimal-time limit (my derivation, [M]): as T goes to 0 the FTLE tends to the largest eigenvalue of the symmetric strain tensor S = (A + A^T)/2 at the point (A = velocity gradient), an instantaneous PW quantity that carries none of the history that builds ridges. The instantaneous growth rate of a passive-scalar gradient is d(grad c)/dt = -A^T grad c, PW given the scalar's gradient.
|
||||
1.7 Langmuir circulation: counter-rotating wind-aligned cells from the interaction of Stokes drift and vertical vorticity (Craik-Leibovich 1976 equations); surface convergence lines make windrows of buoyant material, divergence zones between them stay clear; spacing from under 2 m to over 1 km, streaks up to 15 km downwind [S] https://en.wikipedia.org/wiki/Langmuir_circulation plus sea-ice and LES papers via search. The operative local quantity is surface convergence -div_h(u); the cell spacing is set by mixed-layer physics, not a local gradient. If the roll field is given analytically, -div_h(u) is PW [M].
|
||||
1.8 Buoyant-tracer accumulation versus convergence (LES): tracer accumulates in regions of horizontal convergence (downwelling at the surface); less buoyant tracer is submerged more easily and resurfaces in upwelling zones, giving a more uniform distribution; the most buoyant tracer stays near the strongest convergence and reached concentrations up to 50 times those of the least buoyant in the same flow; not every downwelling region fills, convective vortices trap particles in a subset of nodes; clustering is quantified with the Gini coefficient and set by the friction-to-convective velocity ratio and the buoyancy [V] Dingwall, Chor, Taylor, J. Fluid Mech. 954, A27 (2023), doi 10.1017/jfm.2022.969 (text read: abstract, Sec. 1, discussion of figs 3-4). The stated control is particle slip velocity versus peak vertical velocity (about 0.02 m/s there). A simulation result, not a closed form.
|
||||
1.9 Flotsam on fronts: D'Asaro et al. 2018 PNAS 115, "Ocean convergence and the dispersion of flotsam", cited by that LES paper for accumulation in narrow 10-100 m density fronts [V as citation]; the paper itself not read.
|
||||
|
||||
## 2. Divergence as density proxy: when closed forms exist
|
||||
|
||||
2.1 Continuity: d(rho)/dt + div(rho u) = 0. Along a fluid trajectory D rho/Dt = -rho div u, so rho(t) = rho0 exp(-integral of div u dt) = rho0 / det(Jacobian of the flow map). This is PI: it needs the trajectory or the time-integrated divergence along it; no pointwise form [M, standard]. Balkovsky-Falkovich-Fouxon treat the same object as the "volume compression factor along the Lagrangian trajectory" in a compressible flow (arXiv chao-dyn/9912027, Phys. Rev. Lett. 86:2790, 2001, Sec. 2, around Eq. 2.11) [V as to that phrase; equation not worked through].
|
||||
2.2 Steady Eulerian density: d/dt=0 gives div(rho u) = 0. In 1D, or along a channel of fixed cross-section, rho = C/u(x) pointwise (flux conservation), C fixed by total mass. In 2D/3D with div u != 0 there is no pointwise solution in general: integrate along streamlines or solve the PDE; with sinks, the steady state concentrates on the sinks [M, standard continuity reasoning; not sourced this pass].
|
||||
2.3 -div u is the instantaneous compression rate, d(ln rho)/dt = -div u at the moving particle: a PW rate, not a density [M, follows from 2.1].
|
||||
2.4 Inertial particles in an incompressible flow: to first order in the Stokes time tau, v = u - tau Du/Dt is compressible, div v = -tau tr(A^2) with A = grad u. PW given A (and Du/Dt). Heavy particles leave high-vorticity regions and collect in low-vorticity/high-strain regions; light particles the opposite. Read: in compressible fields preferential concentration occurs where field divergence is negative; in incompressible fields where strain is high and skewness negative; Eq. 25 defines |S|^2 and |omega|^2 and notes that the average of d_a u_b d_b u_a = |S|^2 - |omega|^2/2 vanishes for incompressible fields [V] "Preferential concentration vs. clustering in inertial particle transport by random velocity fields", arXiv 0811.2927, Secs. II, IV (Eqs. 21-27), V. The explicit div v = -tau tr(A^2) is the standard form given in a search summary of Balkovsky et al.; prefactors unchecked [S/M]. Maxey 1987, J. Fluid Mech. 174:441-465, relates the particle-flow compressibility to local straining of the carrier (read through abstract and a secondary account only; the "preferential sweeping" naming is Wang and Maxey 1993) [S]. Validity: weak inertia; the clustering is intermittent with heavy-tailed fluctuations [V] BFF abstract.
|
||||
2.5 Clustering of inertial particles can also occur in incompressible flow ("clumps and voids") [V] arXiv 0811.2927 Sec. IV.
|
||||
2.6 Failure modes of the divergence proxy [M]: -div u is a rate, not a density; in a steady flow with sinks the exponential integrates to a singularity at the sink; in an incompressible flow (div u = 0) a uniform passive-tracer number density stays uniform, so no pointwise convergence signal exists and patchiness comes only from the tracer's own initial inhomogeneity or from inertia/buoyancy/swimming.
|
||||
|
||||
## 3. Gravity-slope (topographic) habitat suitability and game density functions
|
||||
|
||||
3.1 Seamount/topography aggregation mechanisms in the literature: enhanced prey flux past topography and interception of vertical migrators (topographic blockage); higher abundance at crests tied to enhanced current amplitudes and convergence zones (Hawaii study) [S] https://deepsea.uac.pt/doi-2901 and linked results. Slope is one covariate among depth, rugosity, aspect, BPI, VRM in Maxent studies; depth was usually the best predictor and slope the weakest in a Western Pacific snapper study [S] agris.fao.org record 671224ca7f591113e2a47f06. Suitability in those is a fitted function; no source found that derives a niche from gradient magnitude alone, and none for shelf-break or upwelling covariates in seamount models [S].
|
||||
3.2 Horizon Zero Dawn (Guerrilla, GDC 2017, J. van Muijden): placement is "data driven, deterministic, locally stable"; per-layer DENSITYMAP step evaluates artist logic over mipmapped world-data maps (heights, Erosion_Flow/Wear/Deposition, Water_Flow, Water_Vorticity, Terrain_Cavity, painted Placement_* maps); GENERATE discretises the density map by dithering against a precomputed pattern ("even spread thresholds, maximize 2D distance"), one sample per thread; layered dithering with a two-sided threshold test avoids inter-layer collision readback; about 250 microseconds average GPU load, 500+ asset types, 100,000+ objects; regenerated at run time around the player [V] https://www.guerrilla-games.com/media/News/Files/GDC2017_VanMuijden_GPUBasedProceduralPlacementInHorizonZeroDawn.pdf (slides read, 238 MB PDF). The density is a continuous scalar combined from baked maps; the spatial pattern comes from the fixed threshold pattern. Inputs are baked 2D maps, not analytic pointwise functions.
|
||||
3.3 Unreal PCG: "Normal To Density" turns the angle between a point's normal and a reference vector into density with Result = Result^(1/Strength) and an Offset; "Density Filter" keeps points whose density is in [LowerBound, UpperBound] (0-1) with optional remap; "Density Noise" modulates density by bounded noise (Density = OriginalDensity op DensityNoise); Surface Sampler can keep zero-density points; seed controls repeatability [S] https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/PCG/UPCGNormalToDensitySettings , .../UPCGDensityFilterSettings , .../UPCGSpatialNoiseSettings .
|
||||
3.4 Houdini: Scatter SOP "Density Attribute" varies point density smoothly across a primitive from a point or vertex attribute; Relax Iterations gives even spacing (turn off for random look); LOP Scatter Instances has an Up Axis mask with Max Angle [S] https://www.sidefx.com/docs/houdini/nodes/lop/scatterinstances.html and forum posts https://www.sidefx.com/forum/post/178860/ .
|
||||
3.5 Sources of the "nice patterns": in HZD, painted/generated maps plus the fixed blue-noise-like threshold pattern [V]; in Unreal/Houdini, a density attribute plus relaxation or density noise [S]. No Man's Sky (GDC 2017 talks) not opened this pass [M, unverified]. No source found for a gradient/curvature-to-pattern recipe.
|
||||
|
||||
## 4. Determinism and stability under motion and field change
|
||||
|
||||
4.1 Fixed per-point threshold with field-dependent density: HZD's dither-based discretisation compares the density value to a fixed pattern value; each candidate's threshold is constant, so the set at lower density is a subset of the set at higher density (monotone nesting) and a point appears or disappears only when the density at it crosses its own threshold [V for the dither design; the nesting consequence is my reading, M]. "Locally stable" is in the slide's own goal list [V].
|
||||
4.2 Independently generated sets per density level are not nested and change abruptly between levels; one fix is a hierarchy where denser levels grow out from sparser ones [S] 3D-TSV, arXiv 2112.09202 (as found in search; page not read).
|
||||
4.3 Blue-noise candidates: Wei, "Parallel Poisson disk sampling", ACM TOG 27(3), SIGGRAPH 2008: grid cells, concurrent sampling of non-conflicting cells, adaptive sampling from an importance field, constant passes for a target count [S]. Kopf, Cohen, Deussen, Lischinski, "Recursive Wang Tiles for Real-Time Blue Noise", ACM TOG 25(3):509-518 (2006) (authorship per search; not Lagae-Dutre): tile-based blue noise with spatially varying density; stability by position-indexed tiles is [M]. Ahmed, Niese, Huang, Deussen, "An adaptive point sampler on a regular lattice", ACM TOG 36(4) (2017): one sample per lattice tile, word-based tile IDs, bit-reversal traversal of sub-tiles for varying density [S]. Ulichney void-and-cluster (1993) and Mitchell best-candidate (1991) are the classic ordered-threshold constructions [M]. Hash-per-cell jittered grids are the simplest deterministic, position-stable placement but are not blue noise [M].
|
||||
4.4 Spatiotemporal blue noise masks (Wolfe et al., EGSR 2022): values well distributed over time, blue spectra in space and time, built by reformulating the void-and-cluster energy function [V] https://www.ea.com/seed/news/egsr-2022-blue-noise (page read; paper PDF not readable by the fetcher). Aimed at TAA and Monte Carlo, not point placement.
|
||||
4.5 Render-side anti-popping: dithered LOD cross-fade with the opacity-mask dither threshold ramped over about 0.25-0.5 s (Cesium for Unreal) [S] https://cesium.com/blog/2022/10/20/smoother-lod-transitions-in-cesium-for-unreal/ ; hysteresis offset added to LOD distance to avoid flicker at a boundary (Blender BGE commit, default 10%) [S] https://demo.gitea.com/kannonier8/blender/src/commit/e7d051043dc82c2991eb491e0630d9b1065934c3/release . Side effect: a half-faded LOD still casts shadows/occlusion [S].
|
||||
4.6 Time-varying field [M, reasoning; no source found on temporal coherence of density-threshold placement]: with continuous d(x,t) and fixed thresholds, appearance is continuous at the rate of change of d; a fast field (the earlier probe measured flow speed comparable to rail speed, 6.06 vs 6.28 u/s) still moves the set at that rate. Hysteresis needs separate enter/exit thresholds.
|
||||
|
||||
## 5. Predator-prey arrangement as a closed form
|
||||
|
||||
5.1 Ideal free distribution (Fretwell and Lucas 1970, Acta Biotheoretica 19:16-36 [M]): payoff equal and maximal in all occupied patches. With input rates r_i, the fraction in patch i is p_i = r_i / (r_1 + ... + r_n) (Parker's input-matching rule). PW given the resource field plus a global sum for normalisation [S] https://fix.prf.jcu.cz/~krivan/clanky/KrivanCressmanSchneider-TPB-2008.pdf (Krivan, Cressman, Schneider, Theor. Popul. Biol. 73 (2008); PDF fetched, formula seen only in search snippets).
|
||||
5.2 Predator distribution proportional to prey: x_i = P R_i / sum R_j; with interference exponent m in (0,1), predator density scales as prey density to the power 1/m; the proportional case is globally asymptotically stable under replicator dynamics; input matching is often imperfect, with poor patches over-used because of interference [S] Tsybulin and Zelenchuk 2024, Mathematics 12(2):275 (https://ideas.repec.org/a/gam/jmathe/v12y2024i2p275-d1319317.html ), https://dmorris.lakeheadu.ca/29.pdf .
|
||||
5.3 Two-trophic-level IFD (both species move): equilibria depend on the functional response; Holling type II destabilises the spatial distribution; some equilibria put all predators in one patch; the theory is developed for two-patch systems [V as to existence of these statements in the Krivan et al. 2008 PDF text, Sec. 3.2 and Fig. 5, details partial]. No continuous-space closed form for both species found [S].
|
||||
5.4 Tritrophic IFD test (lady beetles, aphids, beans): predators aggregated in the highest-quality patch as predicted; prey distribution near uniform [S] https://czaw.org/?p=1905 .
|
||||
5.5 If the prey density is itself a pointwise field (Secs. 1-3), the IFD predator field composes pointwise, with the global sum if totals are held fixed [M, composition].
|
||||
|
||||
## Pointwise versus path-integrated
|
||||
PW: Okubo-Weiss W (and Q, lambda2); instantaneous divergence -div u; largest strain eigenvalue (T->0 FTLE); inertial-particle compressibility -tau tr(A^2); slope/curvature of the gravity field; density = f(local quantities) with a fixed per-point threshold; ideal free distribution given a prior field (plus a global sum when totals are fixed).
|
||||
PI: FTLE and FSLE; density from exp(-integral of div u dt) along a trajectory; steady-state Eulerian density in 2D/3D compressible flow; LCS ridge extraction; Langmuir windrow position unless the roll field is analytic (then PW).
|
||||
101
docs/research/2026-10-14-trophic-dag-filaments-prior-art.md
Normal file
101
docs/research/2026-10-14-trophic-dag-filaments-prior-art.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Trophic DAG and filaments: prior art (2026-10-14)
|
||||
|
||||
Serves ruling `aetheria-fauna:ruling:habitat-trophic-dag` (deterministic layered ecology, a trophic DAG evaluable in shader passes or C#, with the filament feeling of the Physarum maps). Facts only; follows `2026-10-14-fauna-placement-closed-form.md`.
|
||||
|
||||
|
||||
Agent eyes-trophic-dag, session self-2026-10-14-ag, campaign aetheria-fauna, 2026-10-14. Serves ruling `aetheria-fauna:ruling:habitat-trophic-dag`. Facts only, no recommendations.
|
||||
Tags: [V] read this pass (page or PDF text fetched and read); [S] search-result summary only; [M] memory or my own derivation, unverified.
|
||||
Not repeated (covered in Aetheria `docs/research/2026-10-13-fauna-placement-prior-art.md` and `2026-10-14-fauna-placement-closed-form.md`, read at HEAD of `C:\aeth-wt\fauna-target-r3`): Jones 2010/2011 Physarum agents, Tero 2010 existence, MCPM existence, EcoBrush, D8, Maxent, Okubo-Weiss/FTLE/divergence, ideal free distribution formula p_i = r_i/sum r, HZD placement (density maps, dither GENERATE), Unreal Normal-To-Density/Density Filter, blue-noise/stability notes, float determinism in general. Those docs state: the Jones 2011 result that the agent network approximates Steiner trees and relative neighbourhood graphs of its food sources, and a probe where the agent pass drew filaments on the shoulders of wells (correlation 0.28 with a pointwise suitability map).
|
||||
"PW" = pointwise (value at x from sources and fields near x, no grid, no iteration over the plane).
|
||||
|
||||
## 1. Filament constructions
|
||||
|
||||
### 1.1 Cellular / Voronoi edges
|
||||
1.1.1 [V] F2-F1 is exactly 0 on cell borders but is not a distance: it grows and shrinks with the spacing of the two feature points either side of an edge, so a threshold gives uneven line width (some borders too thin, others swallow the cell). Quilez: "sort of works, but not completely". https://iquilezles.org/articles/voronoilines/
|
||||
1.1.2 [V] Quilez exact distance-to-border is two passes over neighbour cells: pass 1 searches 3x3 around the shading point for the closest feature point; pass 2 searches 5x5 centred on that point's cell, taking the minimum of the signed distance to the perpendicular bisector, `dot(0.5*(mr+r), normalize(r-mr))`. Cost 9+25 = 34 cell evaluations, constant per pixel, PW. Stated limits: wrong distances near corners where three borders meet; isolines inside cells inaccurate. Same page.
|
||||
1.1.3 [S] Blender's Voronoi node code credits Quilez's distance-to-edge routine (MIT) and uses a reduced scan window (-1..1 rather than -2..2) as a performance tradeoff. https://developer.blender.org/rB3848507511a7546ab396c80f069b48de3332860f
|
||||
1.1.4 [S] Worley's basis function is continuous except on Voronoi edges and at seeds (Wikipedia Worley noise).
|
||||
1.1.5 [S] Heuristic cosmic-web graphics recipe found in search (no primary source): domain-warp coordinates with fBm, then Worley F1/F2 3x3, `smoothstep(width,0,F2-F1)` for filaments, hashed cell sites as halos. Search note: such Voronoi-based models "do not follow a hierarchical multiscale distribution and contain no curved structures".
|
||||
1.1.6 [V] Jump Flooding Algorithm (Rong and Tan 2006): log2(N) passes for an NxN grid (step N/2 down to 1), each pass 9 samples per pixel; approximate with few small errors; variants JFA+1, JFA+2, 1+JFA lower errors; builds Voronoi, centroidal Voronoi, power diagrams and distance transforms; not covered on the page: weighted seeds. https://en.wikipedia.org/wiki/Jump_flooding_algorithm . Original: https://www.comp.nus.edu.sg/~tants/jfa/i3d06.pdf [S].
|
||||
1.1.7 [M] Weighted Voronoi: the power diagram (cell by |x-p_i|^2 - w_i) has straight bisector edges and is evaluated pointwise by the same neighbour search only if the search window is widened with the weight range; additively weighted (|x-p_i| - w_i) has hyperbolic edges. Neither derivation checked against a source this pass.
|
||||
1.1.8 [M] Stability under moving seeds: F1 and F2 are continuous in x and in seed position; F2-F1 and the exact border distance are continuous in position, but the border (zero set) re-routes through topological flips (an edge between two cells disappears, another appears, when four seeds become cocircular), so a thin filament pops at a flip while the field stays continuous.
|
||||
|
||||
### 1.2 Distance-field ridges, medial axis
|
||||
1.2.1 [M] The medial axis of a shape is the ridge/skeleton of its distance transform; for the point set of wells the distance field's ridges are the Voronoi edges (so 1.1 is this case). Ridge detection from a distance field needs second derivatives or a grid; no source read this pass.
|
||||
1.2.2 [V] Distance-transform costs on GPU: JFA log2(N) passes (1.1.6). Eikonal solvers: fast sweeping needs 2^n directional sweeps in Gauss-Seidel order, ordering limits parallelism (a 2025 paper restructures to row/column parallel); one 2025 paper observes convergence in 2-3 iterations independent of grid size [S single observation]; fast iterative method (Huang 2021) needs no ordering, active list plus remedy list, "runs faster in most cases", iteration scaling not stated in abstract [V abstract]. https://arxiv.org/abs/2106.15869 ; search summary https://arxiv.org/pdf/2603.00035 [S]. Not PW: grid-wide.
|
||||
1.2.3 [S] Heat method (Crane, Weischedel, Wardetzky, ACM TOG 2013): two linear solves (heat diffusion for a short time, then a Poisson solve on the normalised gradient field); prefactorable; per-vertex approximation of geodesic distance to a source set; converges under refinement. https://arxiv.org/pdf/1204.6216 . Grid-wide, not PW.
|
||||
1.2.4 [M] Geodesic (least-cost) paths on a cost surface = ridge/valley of the sum of two eikonal fields (d_A + d_B minimal along the least-cost path between A and B): a way to draw a path between two sources from two distance fields; one pair of eikonal solves per source pair. Derivation not checked.
|
||||
|
||||
### 1.3 Source graphs drawn as distance-to-segment fields
|
||||
1.3.1 [V] Relative neighbourhood graph (Toussaint 1980): edge pq iff no third point is closer to both than they are to each other; EMST is a subgraph; RNG is a subgraph of the Delaunay triangulation; O(n log n) (Supowit 1983), linear from the Delaunay triangulation; the Urquhart graph is a fast approximation. Wikipedia does not discuss locality. https://en.wikipedia.org/wiki/Relative_neighborhood_graph
|
||||
1.3.2 [V] Gabriel graph: edge pq iff the closed disc with diameter pq is empty; contains EMST, RNG and nearest-neighbour graph; subgraph of Delaunay; linear from Delaunay; not a spanner; per-edge test is a local geometric check, page says nothing on distributed computation. https://en.wikipedia.org/wiki/Gabriel_graph . Both are beta-skeletons [V].
|
||||
1.3.3 [M] Pointwise evaluation of "distance to nearest graph edge": for n sources the per-edge test is O(n) per candidate pair, so a PW evaluation of the graph is O(n^2)-O(n^3) per point unless sources are bucketed; with sources few per zone (tens) the graph can be computed once per zone change and the field evaluated as min over edges of point-segment distance, O(E) per point. The graph itself is the cache. Not sourced.
|
||||
1.3.4 [M] Stability: an edge enters or leaves the Gabriel/RNG when a third source crosses the lens or disc, a discrete event; the min-segment-distance field jumps by a segment's width at that instant (pop). EMST edges change when two candidate edge lengths cross. Continuous relaxations (beta-skeleton with continuous beta, soft edge weights) are not sourced.
|
||||
1.3.5 [S] Tero-style Steiner/minimum-length networks: the Physarum model produced approximations to Steiner minimum trees for up to 16 terminals with low precision; no proven iteration bound for Steiner approximation; improved heuristics claim few iterations experimentally. https://arxiv.org/pdf/1712.02910 ; https://arxiv.org/pdf/2110.08233 ; https://www.inderscience.com/offer.php?id=120753 .
|
||||
|
||||
### 1.4 Cosmic web: adhesion model, Zel'dovich, geometric construction
|
||||
1.4.1 [V] Adhesion model = inviscid Burgers dynamics, accepted as a good description of large-scale filamentary structure. Velocity potential psi(x,t) = max_q[psi0(q) - |x-q|^2/(2t)]: at an Eulerian point x, lower an upward paraboloid of curvature radius t on the initial potential psi0(q); the first-contact abscissa is the Lagrangian coordinate q(x,t); simultaneous contact at several points marks a shock at x. Equivalent to the Legendre transform H(x,t) = sup_q[x.q - |q|^2/2 + t psi0(q)] and the convex hull of the Lagrangian potential |q|^2/2 - t psi0(q). https://arxiv.org/pdf/1009.1974 (PDF text read); https://ar5iv.labs.arxiv.org/html/0912.3603 (read through char 100k)
|
||||
1.4.2 [V] In 2D the Eulerian pattern is a Voronoi-like tessellation dual to the Lagrangian convex-hull triangulation: voids = polygons (one per hull vertex, matter-free); walls/filaments = segments separating two polygons (little mass); nodes = vertices where three meet (almost all the mass, proportional to the Lagrangian triangle area). Not a true Voronoi diagram (a planar cut of a higher-dimensional one). Same sources. Density here is a Jacobian of the geometric model; it does not satisfy the continuity equation inside shocks (agrees outside) [V].
|
||||
1.4.3 [V] Pre-shock regime: the Zel'dovich linear map x = q + t u0(q); the paper warns that setting viscosity to 0 in the Zel'dovich form is not the same as the inviscid limit (multi-flow regions). https://ar5iv.labs.arxiv.org/html/0912.3603
|
||||
1.4.4 [V] Cost: 1D Burgers solved in O(N) via the convex envelope then Legendre transform (earlier O(N ln N)); the 2D convex hull is O(N_tot ln N_tot) in the cited text, N_tot = N^2 grid points. Needs the global hull at time t: not PW in general. https://arxiv.org/pdf/1009.1974
|
||||
1.4.5 [M] The argmax in 1.4.1 for a potential made of discrete weighted sites (psi0 = piecewise from sites with weights w_i) is the power diagram of the sites; evaluating the argmax at a point is O(n) over sites: PW with n sites, and the shock set is where two terms tie, which is the same construction as 1.1 with a time-dependent weight ratio. Derivation, not read in a source. The Lagrangian-to-Eulerian "t" is a free parameter of how far the cells have contracted.
|
||||
1.4.6 [S] Filament finders for real cosmological data are not constructions but extractors: DisPerSE (discrete Morse theory on a Delaunay density estimate, persistence-filtered; filaments connect onto halos by topology), Bisous (marked point process of cylinders, visit map), T-ReX (tree-based ridge extractor, compared to Bisous/DisPerSE/Nexus), MST/FoF historically. https://arxiv.org/pdf/1009.4015 ; https://arxiv.org/pdf/1603.08957 ; https://ar5iv.labs.arxiv.org/html/1912.00732 . No source found on rendering synthetic filaments between given halos by closed form other than 1.4.1-1.4.2 and 1.1.5.
|
||||
1.4.7 [S] MCPM/Polyphorm (Elek et al. arXiv 2204.01256): probabilistic generalisation of Jones's agent model, 3D, run on galaxy positions to produce a density map of the web; abstract gives no parameter, iteration or cost figures; page read was abstract only. https://arxiv.org/abs/2204.01256
|
||||
|
||||
### 1.5 Physarum transport-network model (equations, closed form?)
|
||||
1.5.1 [V] Model (Tero-Kobayashi-Nakagaki 2007 as restated by Bonifaci, Mehlhorn, Varma): electrical network, edge resistance R_e = L_e / D_e, unit current s0 -> s1, Kirchhoff conservation at nodes (linear system for potentials), diameter dynamics dD_e/dt = |Q_e| - D_e; equilibrium |Q_e| = D_e. https://arxiv.org/pdf/1106.0423 (PDF text read).
|
||||
1.5.2 [V] Result: for any graph and initial diameters the network converges to the shortest s0-s1 path (D=1 on the path, 0 elsewhere); convergence of the transportation-problem generalisation is weaker; exponential rate proved only after flow directions stabilise; the paper lists the initial-stage behaviour as open. Same PDF, sections 1, 6, 7 and the open-questions text.
|
||||
1.5.3 [S] Steady states of the adaptive Hagen-Poiseuille class are non-unique and depend on initial conductivities; Tero 2010 uses dD/dt = f(|Q|) - r D with f a sigmoid/power law (this pass could not open the 2010 Science text: statement from search summary and memory). Continuum form dD/dt = (|grad p| D)^mu - delta D, mu=1 steady state |grad p| = delta or D=0 [S: arXiv 2511.08531]. https://arxiv.org/pdf/2305.12244 ; https://arxiv.org/pdf/2511.08531 ; https://arxiv.org/pdf/2309.10577 .
|
||||
1.5.4 [V] No closed form: each iteration needs a global linear solve (potentials); an explicit-iteration count to a Steiner-like network is not bounded in any source found. Pointwise: no. Fixed point depends on iteration history.
|
||||
1.5.5 [M] Few-iteration cheap relatives that are PW: 1.1.2 and 1.3.3 (their outputs are the graphs Jones 2011 says the agent model approximates).
|
||||
|
||||
## 2. Layered ecology as a node graph
|
||||
|
||||
### 2.1 Ecology models with a DAG-shaped structure
|
||||
2.1.1 [S] NPZ model: nutrient N, phytoplankton P, zooplankton Z; dN/dt = supply - uptake + recycling; dP/dt = uptake - grazing - mortality; dZ/dt = assimilation of grazing - mortality; grazing Holling II g = gmax P/(K+P) or Holling III (Oschlies and Garcon); spatial form dC_i/dt + div(v C_i) = F_i(N,P,Z) + D_i lap C_i. Zooplankton loss exponent 1 vs 2 changes stability. https://arxiv.org/pdf/nlin/0205009 ; https://arxiv.org/pdf/0802.3532 . No single canonical citable form found in this pass.
|
||||
2.1.2 [M] Steady-state food chain with donor control: each level's density is a monotone function of the level below (e.g. Holling-saturating) times conversion, producing a feed-forward chain N -> P -> Z -> higher predators (DAG); two-way coupling (predator regulates prey) is a cycle, solved only by iteration or equilibrium algebra. From general ecology; no source read.
|
||||
2.1.3 [S] Nonlocal predator-prey (convolution in prey competition, top-hat or heat kernels) can generate Turing-type stationary patterns not present in the local model and bistability; no Gaussian-kernel analysis found; infinite-support kernel existence of stationary patterns reported open. https://arxiv.org/pdf/2209.09761 ; https://arxiv.org/pdf/2208.03533 ; https://arxiv.org/pdf/1904.05224 ; repository.rudn.ru/records/article/record/71933 .
|
||||
2.1.4 [prior doc] IFD composes pointwise with a global sum (closed-form doc 5.1, 5.5). Trophic IFD for two moving species: no continuous closed form (5.3).
|
||||
|
||||
### 2.2 Graph-authored placement in games/tools
|
||||
2.2.1 [prior doc, V] HZD: per-layer density-map step over mipmapped world maps then dithered discretisation; layers are authored in a graph editor; "data driven, deterministic, locally stable". Search: GDC Vault title and Guerrilla page confirm graph editor, slides listing "Density Based Placement". https://www.guerrilla-games.com/read/gpu-based-procedural-placement-in-horizon-zero-dawn
|
||||
2.2.2 [S] Unreal PCG: nodes act on point data and attribute sets; Density Remap D' = (Omax-Omin)(D-Imin)/(Imax-Imin)+Omin; Distance to Density (superseded by the more efficient native Distance node); Curve Remap Density; Attribute Filter; Point Filter and Point Filter Range (can filter by sampling spatial data); Self Pruning removes overlaps by extents/density with a randomised tie-break; Density Noise; Normal to Density; Surface Sampler. https://dev.epicgames.com/documentation/en-us/unreal-engine/procedural-content-generation-framework-node-reference-in-unreal-engine
|
||||
2.2.3 [S] Houdini Scatter SOP density attribute plus Relax Iterations; LOP Scatter Instances Up Axis mask (closed-form doc 3.4).
|
||||
2.2.4 [S] Gaea: masks are heightmaps, Combine node blend modes (Add, Subtract, Max...), Mask node Blur and Iterations, Accumulator for global masks (Snow, Water, Debris, Trees, Depth, FreshWater), Data Maps (rainfall, sediment); Gaea processes the whole surface then applies the mask. World Machine: "Vegetation Scatter" macro takes a texture plus weightmap; Select Wetness device and erosion flow map used for plant placement; users report blur as the dominant cost in big builds (65k of 85k seconds in one report). https://docs.quadspinner.com/Guide/Graph/Graph.html ; https://forum.world-machine.com/t/very-slow-nodes-any-workaround-faster-alternate-way/3530 ; https://forum.world-machine.com/t/trees-in-world-machine/4726 . No wildlife placement nodes found in either.
|
||||
2.2.5 [S] Spore: ecological niche = amount of life a planet's ecosystem can support; placement ordered by trophic level (plants before herbivores before carnivores; a carnivore alone with a single herbivore species overhunts it) at generation. Eco (Strange Loop): persistent server simulation of plants and animals with food-chain dependence; marketing-level descriptions only. https://spore.fandom.com/wiki/Ecological_niche ; https://www.pcgamer.com/uk/eco-is-an-open-world-survival-game-that-will-let-you-destroy-the-planet . No technical documentation found for either.
|
||||
2.2.6 [M] Operator vocabulary in these graphs (from 2.2.1-2.2.4): pointwise arithmetic and remap curves, min/max/add/multiply blends, blur or convolution (feeding "nearby" influence), distance to a feature, threshold or range filter, noise modulation, dither or Poisson relaxation to turn density into discrete placements, global-count normalisation. Distance, blur and pruning are the nonlocal operators.
|
||||
|
||||
## 3. One graph, two evaluators
|
||||
|
||||
3.1 [V] Houdini: VEX cannot run on the GPU; the GPU path is a separately written OpenCL kernel (explicit kernel, bindings, no foreach, thin built-in library, fixed-or-node-dependent precision `fpreal`); data stays on GPU only inside a Compiled Block, every other SOP pulls it back; OpenCL also runs on CPU, so "one algorithm" is the OpenCL one. The page does not say VEX and OpenCL results agree. https://www.sidefx.com/docs/houdini/vex/ocl.html
|
||||
3.2 [S] Houdini 20.5 added a GPU Volume Blur SOP; forum: heightfield AO OpenCL branch vs VEX branch built side-by-side, the VEX one "very slow"; 144x-192x speedups are single anecdotes.
|
||||
3.3 [V] Unreal PCG: GPU nodes form Compute Graphs; Custom HLSL runs on GPU only (no CPU execution); GPU output not claimed identical to CPU (weighted mesh selection "similar to the CPU implementation", over-allocation heuristic can drop instances); the doc recommends seeding from position (`ComputeSeedFromPosition()`); virtual-texture sampling needs priming or can be inaccurate; GPU-spawned instances exist only on the GPU and are not saved; CPU/GPU transfers have a cost, Skip Readback to CPU available; feature Beta/Experimental. https://dev.epicgames.com/documentation/en-us/unreal-engine/using-pcg-with-gpu-processing-in-unreal-engine
|
||||
3.4 [S] Substance Designer: Pixel Processor runs a function per pixel with no knowledge of neighbours; Value Processor computes a single value; the CPU (SSE2) and GPU engines can give different outputs (noise/grain differences) and the vendors advise using the same engine everywhere; CPU engine lacks 16-bit and greyscale 8-bit; no documented bit-exact guarantee. https://experienceleague.adobe.com/en/docs/substance-3d-designer/using/substance-graphs/nodes-reference-for-substance-graphs/atomic-nodes/pixel-processor ; https://help.maxon.net/c4d/2026/en-us/Content/html/PREFSSUBSTANCE-PREFS_SUBSTANCE_MAIN_GROUP.html .
|
||||
3.5 [S] Shader-graph-to-code: Unity Shader Graph maps each node's inputs to variables and calls prebuilt per-node functions (many wrapping HLSL intrinsics); ShaderGen generates HLSL/GLSL/Metal from one C# source. https://arxiv.org/pdf/2312.17587 ; https://github.com/feliwir/ShaderGen . No source found for generating C# and HLSL from one graph with parity testing.
|
||||
3.6 [V, local] CultLib research `docs/research/cycles-fma-contraction.md` (git `ae6ef5a8`) records: HLSL `precise` keeps mul and add separate, `mad` may fuse; SPIR-V allows rearrangement and different rounding unless `NoContraction`; GLSL `precise` prohibits arbitrary fma; Vulkan OpFDiv 2.5 ULP, OpFMod inherits `x - y*floor(x/y)`; CUDA `--use_fast_math`; Metal fast math vs safe; `Math.FusedMultiplyAdd` is one rounding, the .NET JIT keeps `a*b+c` as two roundings (dotnet/runtime #10137 closed with explicit-API stance), FMA software fallback misrounding bug (#98704) fixed; DXC/Unity default contraction not confirmed. `F:\Projects\CultLib` (read via git show).
|
||||
3.7 [V] Hash portability: PCG hash recommended as the default GPU hash (Jarzynski and Olano, JCGT 9(3):21-38, 2020; 32-bit RXS-M-XS; pcg2d etc.); these pages do not state CPU/GPU bit-equality or float-conversion caveats. Integer multiply/add/shift/xor are exactly specified operations [M]. https://www.jcgt.org/published/0009/03/02/paper.pdf ; https://reedbeta.com/blog/hash-functions-for-gpu-rendering
|
||||
3.8 [S] GPU bilinear texture filtering is fixed-function with undocumented, vendor-varying precision; an RGBA8 texture filtered stays 8-bit; fractional weights are quantised (older hardware reasoning ~6-8 bits); to get exact interpolation load four texels (or Gather) and blend in float32 in the shader; verify empirically. https://gamedev.net/blogs/entry/1696749-tip-of-the-day-bilinear-filtering-accuracy ; https://reedbeta.com/blog/texture-gathers-and-coordinate-precision ; https://community.khronos.org/t/texture-filtering-precision/41917
|
||||
3.9 [M] Node types by CPU pointwise cost (derived, not sourced):
|
||||
- cheap PW in C#, exact tie to shader via same expression: arithmetic, min/max/clamp, remap curves, smoothstep, thresholds, integer hash noise, analytic fields (gravity height, flow), Worley/Voronoi with fixed neighbour window (1.1.2), distance to a small set of points/segments/circles, ideal-free-style ratios if the global sum is a cached constant.
|
||||
- need a cache but not a grid: graph of sources (Gabriel/RNG/MST) computed per zone change; global normalisers (sum of resource), per-source constants.
|
||||
- force a grid or bounded neighbourhood sampling: blur/convolution of a non-analytic field (cost K^2 samples per point per node; separable halves it), distance transforms, eikonal/geodesic, flow accumulation, ridge detection by derivatives of non-analytic fields, Poisson-disk/dither with neighbour tests, anything that reads another node's value at offsets.
|
||||
- analytic closed-form convolution exists only when the input is a sum of Gaussians/radial kernels (blur of a point-source sum is again a point-source sum) [M].
|
||||
3.10 [prior doc] HZD solved GPU-side density-to-instance via fixed dither threshold per candidate (one sample per thread), evaluated from baked maps; no CPU evaluator described.
|
||||
|
||||
## Closing table: filament constructions
|
||||
|
||||
| Construction | PW? | Cost per evaluation | Stability as sources move | Reference |
|
||||
|---|---|---|---|---|
|
||||
| Worley F2-F1 | PW | 9 cells (3x3) | field continuous; width uneven; borders re-route at flips | 1.1.1 [V] |
|
||||
| Voronoi exact border distance (Quilez) | PW | 34 cells, constant | continuous distance; border pops at cell flips; corner errors | 1.1.2 [V] |
|
||||
| Power / additively weighted diagram | PW with widened window [M] | window grows with weight spread | as Voronoi; weights vary edge position continuously [M] | 1.1.6-1.1.7 |
|
||||
| Voronoi/power on JFA grid | grid, log2(N)+0..2 passes | 9 samples x passes per cell | approximate, small errors; same flips | 1.1.6 [V] |
|
||||
| Distance-field ridge / medial axis | grid (derivatives) [M] | grid pass plus ridge test | ridge branches pop at topological changes [M] | 1.2.1 [M] |
|
||||
| Eikonal least-cost / geodesic | grid, many sweeps (2^n sweeps; 2-3 iterations observed in one paper) | per source (pair) | continuous in cost field, path can switch homotopy (pop) [M] | 1.2.2 [V/S] |
|
||||
| Heat method | grid, 2 linear solves | prefactored solve | smooth (short-time heat) | 1.2.3 [S] |
|
||||
| MST / RNG / Gabriel over sources, distance-to-segment | PW only after graph is cached per zone [M] | O(E) per point; graph O(n log n) from Delaunay | edges appear/disappear discretely (pop) [M] | 1.3 [V definitions] |
|
||||
| Adhesion/Burgers power-diagram argmax | PW over n sites [M]; global hull O(N^2 ln N^2) for fields [V] | O(n) per point | tessellation continuous in t and weights, flips at node events [M] | 1.4 [V] |
|
||||
| Zel'dovich map | PW (x = q + t u0(q)) [V] | O(1) | valid pre-shock only; folds after [V] | 1.4.3 [V] |
|
||||
| Physarum current-reinforcement | no (global linear solve per iteration) [V] | iterations x solve; no bound found | history dependent, non-unique steady states [S] | 1.5 [V/S] |
|
||||
| Agent Physarum (Jones/MCPM) | no (stochastic, grid trail map) | seconds at 40k agents (prior doc probe) | chaotic; save the result (prior doc) | prior docs |
|
||||
| DisPerSE / Bisous / T-ReX | no (extractors on data) | n/a | n/a | 1.4.6 [S] |
|
||||
40
docs/research/2026-10-14-trophic-dag-probe/README.md
Normal file
40
docs/research/2026-10-14-trophic-dag-probe/README.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Trophic DAG probe (2026-10-14)
|
||||
|
||||
Serves rulings `aetheria-fauna:ruling:habitat-trophic-dag` and `:voronoi-primitive`, and the questions
|
||||
`aetheria-fauna:question:skein-filament-construction` and `:trophic-graph-tape`. Facts are map rows B28-B36 in
|
||||
`docs/aetheria-fauna-map.md`; prior art is `docs/research/2026-10-14-trophic-dag-filaments-prior-art.md` (`TD:<line>`).
|
||||
|
||||
`trophic_probe.py` imports `habitat_probe.py` (the float64 transliteration of `Zone.GetHeight`/`GetNormal` and
|
||||
`FlowField`, in `../2026-10-14-fauna-habitat-probe/`) and adds what that probe lacked: the wells orbit, as
|
||||
`Zone.GetOrbitPosition` moves them (period = distance in seconds, `Settings.asset` `OrbitPeriod` exponent 1,
|
||||
multiplier 1; about 6.3 u/s for every planet). The flow's slope terms are on (swirl 6, slide 1.5, as B25g). Its hash
|
||||
is the probe's own integer hash, not CultMath `pcg3d`; images are qualitative evidence for a C# law, not the law.
|
||||
|
||||
Filament candidates (krill habitat density, each times the steep-core mask):
|
||||
|
||||
| Name | Construction |
|
||||
|---|---|
|
||||
| `band` | The operator's `Deposition(slope)` alone (r3's baseline, B25a). Control, not an option. |
|
||||
| `skein` | Strands: a Gabriel graph over the zone's wells (TD:31 [V]) with a soft gate (the empty-disc margin through a smoothstep), each edge a Gaussian strand of width 28 u + 3 % of its length, bent by a Zel'dovich warp of the flow, `x - 20 s * u(x)` (TD:39 [V]). |
|
||||
| `skein-hard` | `skein` with the hard 0/1 Gabriel gate, for the pop count. |
|
||||
| `lattice` | The operator's reference node: a lattice smooth Voronoi (260 u cells) with her hashed per-site multiplicative weight, raised by up to 1.6x near wells so the cells shrink on the shoulders; output is the border (her smooth-min gap, and F2 - F1 at width .1 cell), warped by the flow for 6 s, times the band widened (`band^.75`). Sites drift on zone time (.05 rad/s). |
|
||||
| `skein-web` | `skein` plus `lattice` hanging off the strands: the web is gated by a strand halo (4x width) or the band. |
|
||||
| `slack` | A multiplicatively weighted Voronoi of the wells (weight = gravity radius), distance to the cell border to first order, `(F2 - F1) / |grad(F2 - F1)|`: the saddles where pulls cancel (Apollonius circles round small wells, TD:20 [M]). |
|
||||
|
||||
Trophic layers over `skein-web` (`layer_*.png`): `L1-glim` (starlight inside the sun's light radius plus heat-light on
|
||||
warm well floors, fed along the Skein), `L2-krill` (Skein times a Holling-II saturation of glim, TD:55 [S]),
|
||||
`L2-minnows` (Holling of krill averaged over the point and 8 taps on a 48 u ring), `L3-sharks` (minnows over a
|
||||
160 u ring, times the band), `L3-lanterns` (slack times krill over a 120 u ring).
|
||||
|
||||
Images: `density_<candidate>.png` (zone 4000 u across, t = 0, absolute density, red = bodies), `members_<candidate>.png`
|
||||
(hosted krill candidates at 16 u, fixed thresholds .1 + .9h), `layer_<layer>.png`, `members_L2-minnows.png` (48 u),
|
||||
`members_L3-sharks.png` (128 u), `members_L3-lanterns.png` (64 u).
|
||||
|
||||
Logs: `run1.log` (the full run, 1,211 s), `win.log` (search window and weight range against presence steps),
|
||||
`cyc.log` (the lattice drift read at the member's cycle start).
|
||||
|
||||
To rerun: copy this folder and `../2026-10-14-fauna-habitat-probe/habitat_probe.py` into one scratch directory
|
||||
(the scripts write PNGs next to themselves), then `py -3 -u trophic_probe.py 256` (about 20 minutes),
|
||||
`py -3 -u winprobe.py`, `py -3 -u cycprobe.py`. `habitat_probe.py` reads the B20 Physarum map from
|
||||
`C:\aeth-wt\fauna-target-r3\docs\research\2026-10-13-fauna-placement-probe\output.txt`; edit the path if the
|
||||
worktree moved. Python 3 only, no packages.
|
||||
2
docs/research/2026-10-14-trophic-dag-probe/cyc.log
Normal file
2
docs/research/2026-10-14-trophic-dag-probe/cyc.log
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
web at cycle start, lattice: presence step max 0.0113, 0 steps > .1 of 1500; members 435, changed in 1 s 0.262
|
||||
web at cycle start, skein-web: presence step max 0.0656, 0 steps > .1 of 1500; members 1475, changed in 1 s 0.216
|
||||
7
docs/research/2026-10-14-trophic-dag-probe/cycprobe.py
Normal file
7
docs/research/2026-10-14-trophic-dag-probe/cycprobe.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import sys; sys.argv=['x','64']
|
||||
import trophic_probe as T
|
||||
cands=T.cand_list(16.0); T.WEB_AT_CYCLE=True
|
||||
for name in ("lattice","skein-web"):
|
||||
w,b,n=T.pop_probe(T.CANDS[name],cands,100.0,1500)
|
||||
a=T.members(T.CANDS[name],cands,0.0); b1=T.members(T.CANDS[name],cands,1.0)
|
||||
print(f"web at cycle start, {name}: presence step max {w:.4f}, {b} steps > .1 of {n}; members {len(a)}, changed in 1 s {len(a^b1)/max(1,len(a)):.3f}", flush=True)
|
||||
BIN
docs/research/2026-10-14-trophic-dag-probe/density_band.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/density_band.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/density_lattice.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/density_lattice.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/density_skein-web.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/density_skein-web.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/density_skein.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/density_skein.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/density_slack.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/density_slack.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/layer_L1-glim.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/layer_L1-glim.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/layer_L2-krill.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/layer_L2-krill.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/layer_L2-minnows.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/layer_L2-minnows.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/layer_L3-lanterns.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/layer_L3-lanterns.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/layer_L3-sharks.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/layer_L3-sharks.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/members_L2-minnows.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/members_L2-minnows.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/members_L3-lanterns.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/members_L3-lanterns.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/members_L3-sharks.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/members_L3-sharks.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/members_band.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/members_band.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/members_lattice.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/members_lattice.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/members_skein-web.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/members_skein-web.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/members_skein.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/members_skein.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/research/2026-10-14-trophic-dag-probe/members_slack.png
(Stored with Git LFS)
Normal file
BIN
docs/research/2026-10-14-trophic-dag-probe/members_slack.png
(Stored with Git LFS)
Normal file
Binary file not shown.
27
docs/research/2026-10-14-trophic-dag-probe/run1.log
Normal file
27
docs/research/2026-10-14-trophic-dag-probe/run1.log
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
grid 256, wells orbit (period = distance s), slope terms on, TAU 20.0 s, lattice 260.0 u
|
||||
gabriel over 1400 s: hard edge-set changes 29; soft gate largest step per 1/60 s 0.00075
|
||||
49109 krill candidates at 16 u
|
||||
band wells-only (flow frozen at t's cycle): changed in 1 s 0.0323, 60 s 0.891
|
||||
band: corr physarumA 0.512; members 2075; changed in 1 s 0.032, 8 s 0.283, 60 s 0.891; presence step max 0.0035 (0 steps > .1, 2500 near-threshold candidates); 23s
|
||||
skein wells-only (flow frozen at t's cycle): changed in 1 s 0.1064, 60 s 1.498
|
||||
skein: corr physarumA 0.225; members 639; changed in 1 s 0.244, 8 s 1.102, 60 s 1.549; presence step max 0.0536 (0 steps > .1, 2500 near-threshold candidates); 58s
|
||||
skein-hard wells-only (flow frozen at t's cycle): changed in 1 s 0.1089, 60 s 1.495
|
||||
skein-hard: corr physarumA 0.225; members 643; changed in 1 s 0.244, 8 s 1.098, 60 s 1.546; presence step max 0.0536 (0 steps > .1, 2500 near-threshold candidates); 55s
|
||||
lattice wells-only (flow frozen at t's cycle): changed in 1 s 0.4487, 60 s 1.871
|
||||
lattice: corr physarumA 0.359; members 419; changed in 1 s 0.594, 8 s 1.680, 60 s 1.874; presence step max 0.1236 (8 steps > .1, 2500 near-threshold candidates); 179s
|
||||
skein-web wells-only (flow frozen at t's cycle): changed in 1 s 0.1864, 60 s 1.504
|
||||
skein-web: corr physarumA 0.296; members 1475; changed in 1 s 0.305, 8 s 1.144, 60 s 1.534; presence step max 0.4933 (8 steps > .1, 2500 near-threshold candidates); 217s
|
||||
slack wells-only (flow frozen at t's cycle): changed in 1 s 0.0893, 60 s 1.756
|
||||
slack: corr physarumA 0.171; members 3942; changed in 1 s 0.089, 8 s 0.672, 60 s 1.756; presence step max 0.0132 (0 steps > .1, 2500 near-threshold candidates); 26s
|
||||
L1-glim: grid 256, corr physarumA 0.485; 18s
|
||||
L2-krill: grid 256, corr physarumA 0.277; 19s
|
||||
L2-minnows: grid 128, corr physarumA 0.360; 42s
|
||||
L3-sharks: grid 128, corr physarumA 0.403; 376s
|
||||
L3-lanterns: grid 128, corr physarumA 0.148; 42s
|
||||
members L2-minnows: 274 of 5432 at 48 u; changed in 1 s 0.124, 8 s 0.478; 52s
|
||||
members L3-sharks: 19 of 755 at 128 u; changed in 1 s 0.105, 8 s 0.526; 65s
|
||||
members L3-lanterns: 36 of 3063 at 64 u; changed in 1 s 0.139, 8 s 0.861; 31s
|
||||
cost krill: 0.26 ms per evaluation (Python)
|
||||
cost minnows: 3.19 ms per evaluation (Python)
|
||||
cost sharks: 29.26 ms per evaluation (Python)
|
||||
total 1211s
|
||||
307
docs/research/2026-10-14-trophic-dag-probe/trophic_probe.py
Normal file
307
docs/research/2026-10-14-trophic-dag-probe/trophic_probe.py
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
"""Trophic DAG probe (Imagination, aetheria-fauna r4). Pure Python 3, no packages.
|
||||
|
||||
Evaluates candidate filament nodes and a layered trophic DAG on the B20 zone (habitat_probe.py's transliteration
|
||||
of Zone.GetHeight/GetNormal and FlowField, float64), with the wells ORBITING as Zone.GetOrbitPosition moves them
|
||||
(period = distance seconds, Settings.asset OrbitPeriod exponent 1, multiplier 1). Writes per-layer density images,
|
||||
a members image per candidate, and prints per candidate: correlation with the B20 Physarum maps, hosted members,
|
||||
turnover in 1 s / 8 s / 60 s and the largest presence step per 1/60 s (fade band .1). Hashes are the probe's own
|
||||
integer hash, not CultMath pcg; images are qualitative evidence for the C# law, not the law.
|
||||
|
||||
py -3 trophic_probe.py [grid=256] [only=comma,list]
|
||||
"""
|
||||
import sys, os, math, time
|
||||
ARGS = sys.argv[1:]
|
||||
sys.argv = [sys.argv[0], "256"]
|
||||
import habitat_probe as H
|
||||
|
||||
OUT = os.path.dirname(os.path.abspath(__file__))
|
||||
G = int(ARGS[0]) if ARGS else 256
|
||||
ONLY = ARGS[1].split(",") if len(ARGS) > 1 else None
|
||||
R = H.R
|
||||
H.FLOW.update(swirl=6.0, slope=1.5) # flow-slope-terms-ruled: slope terms on (B25g's values)
|
||||
TAU = 20.0 # Zel'dovich warp time (s): strands bent by the flow, x -> x - TAU u(x)
|
||||
WEB_TAU = 6.0 # the lattice web is warped less: its cells are smaller than a strand's run
|
||||
CYCLE = 8.0 # krill cycle period (s), FlowSettings.Period
|
||||
FADE = .1
|
||||
|
||||
def smooth(a, b, v):
|
||||
if a == b: return 1.0 if v >= b else 0.0
|
||||
v = (v - a) / (b - a); v = 0.0 if v < 0 else 1.0 if v > 1 else v; return v * v * (3 - 2 * v)
|
||||
|
||||
# ---------------- L0: environment (wells move) ----------------
|
||||
def set_time(t):
|
||||
for (kind, m, d, ph), w in zip(H.BODIES, H.WELLS):
|
||||
a = ph + (2 * math.pi * t / d if d > 0 else 0.0)
|
||||
w["pos"] = (math.cos(a) * d, math.sin(a) * d)
|
||||
pairs_cache.clear()
|
||||
|
||||
def slope(x, z, t):
|
||||
n = H.normal(x, z, t); return math.acos(max(-1.0, min(1.0, n[1]))) / (math.pi / 2)
|
||||
|
||||
def band(s): return H.deposition(s) / 2.0 # the operator's Deposition(slope), [0, 1]; zero past slope .8
|
||||
def core(s): return smooth(.7, .8, s) # steep core mask
|
||||
def envelope(b): return b ** .75 # the band, widened: where the web may live
|
||||
|
||||
def flow_at(x, z, t, sc): return H.flow_xz(x, z, t, sc)
|
||||
|
||||
# ---------------- L0.5 filament nodes ----------------
|
||||
pairs_cache = {}
|
||||
def pairs(hard):
|
||||
"""Per-zone site graph over the wells: Gabriel graph, gate soft (margin smoothstep) or hard (0/1).
|
||||
Recomputed per tick from orbit positions (the zone already caches those per tick)."""
|
||||
if hard in pairs_cache: return pairs_cache[hard]
|
||||
W = H.WELLS; out = []
|
||||
for i in range(len(W)):
|
||||
for j in range(i + 1, len(W)):
|
||||
a, b = W[i]["pos"], W[j]["pos"]; L = math.dist(a, b); m = ((a[0] + b[0]) / 2, (a[1] + b[1]) / 2); r = L / 2
|
||||
margin = min([(math.dist(m, W[k]["pos"]) - r) / r for k in range(len(W)) if k not in (i, j)] or [9.0])
|
||||
gate = (1.0 if margin > 0 else 0.0) if hard else smooth(-.15, .15, margin)
|
||||
if gate > 0: out.append((a, b, L, gate, 28.0 + .03 * L))
|
||||
pairs_cache[hard] = out; return out
|
||||
|
||||
def seg_dist(p, a, b):
|
||||
ax, az = b[0] - a[0], b[1] - a[1]; L2 = ax * ax + az * az
|
||||
u = 0.0 if L2 == 0 else max(0.0, min(1.0, ((p[0] - a[0]) * ax + (p[1] - a[1]) * az) / L2))
|
||||
return math.hypot(p[0] - a[0] - u * ax, p[1] - a[1] - u * az)
|
||||
|
||||
def strands(q, hard=False, widen=1.0):
|
||||
best = 0.0
|
||||
for a, b, L, gate, sig in pairs(hard):
|
||||
d = seg_dist(q, a, b) / (sig * widen); best = max(best, gate * math.exp(-d * d))
|
||||
return best
|
||||
|
||||
def warp(x, z, t, sc, tau=None):
|
||||
ux, uz = flow_at(x, z, t, sc); tau = TAU if tau is None else tau; return (x - tau * ux, z - tau * uz)
|
||||
|
||||
def well_slack(x, z):
|
||||
"""Multiplicatively weighted Voronoi of the wells (weight = gravity radius): distance to the cell border,
|
||||
first order (F2 - F1) / |grad(F2 - F1)|. Borders are Apollonius circles round the small wells."""
|
||||
ds = []
|
||||
for w in H.WELLS:
|
||||
dx, dz = x - w["pos"][0], z - w["pos"][1]; r = math.hypot(dx, dz) or 1e-6
|
||||
ds.append((r / w["r"], dx / (r * w["r"]), dz / (r * w["r"])))
|
||||
ds.sort()
|
||||
(d1, gx1, gz1), (d2, gx2, gz2) = ds[0], ds[1]
|
||||
gl = math.hypot(gx2 - gx1, gz2 - gz1) or 1e-9
|
||||
return (d2 - d1) / gl
|
||||
|
||||
LAT = 260.0 # lattice Voronoi cell (u)
|
||||
OMEGA = .05 # site drift rate (rad/s), zone time
|
||||
KW = .35 # smooth-min width (cell units), her w
|
||||
WEB_AT_CYCLE = False # read the lattice web's drift at the member's cycle start (as the flow)
|
||||
WIN = 2 # search window half-size (cells); her reference uses 2
|
||||
BOOST = 1.6 # site weight raised near wells by up to this factor
|
||||
EW = .10 # border line half-width (cell units)
|
||||
def lattice_web(q, t):
|
||||
"""The operator's reference node: lattice smooth Voronoi, per-site multiplicative weight on distance, smooth-min
|
||||
blend. Weight = her hashed .5-1, raised near wells so cells shrink there (denser web on the shoulders).
|
||||
Output: 1 on cell borders, falling into the cells, from F1 minus the smooth minimum."""
|
||||
if CUR_T0[0] is not None: t = CUR_T0[0]
|
||||
px, pz = q[0] / LAT, q[1] / LAT; nx, nz = math.floor(px), math.floor(pz); fx, fz = px - nx, pz - nz
|
||||
m = 8.0; f1 = 8.0; f2 = 8.0
|
||||
for j in range(-WIN, WIN + 1):
|
||||
for i in range(-WIN, WIN + 1):
|
||||
cx, cz = nx + i, nz + j
|
||||
ox, oz = H.hash01(cx, cz, 11), H.hash01(cx, cz, 12)
|
||||
wt = ox * .5 + .5
|
||||
sx, sz = (cx + .5) * LAT, (cz + .5) * LAT
|
||||
prox = max(math.exp(-(math.dist((sx, sz), w["pos"]) / (2.2 * .103 * w["r"])) ** 2) for w in H.WELLS)
|
||||
wt *= 1.0 + BOOST * prox
|
||||
ox = .5 + .5 * math.sin(OMEGA * t + 6.2831 * ox); oz = .5 + .5 * math.sin(OMEGA * t + 6.2831 * oz)
|
||||
d = math.hypot(i - fx + ox, j - fz + oz) * wt
|
||||
if d < f1: f1, f2 = d, f1
|
||||
elif d < f2: f2 = d
|
||||
h = smooth(0.0, 1.0, .5 + .5 * (m - d) / KW)
|
||||
m = m + (d - m) * h - h * (1 - h) * KW / (1 + 3 * KW)
|
||||
gap0 = .25 * KW / (1 + 3 * KW)
|
||||
soft = max(0.0, min(1.0, (f1 - m) / gap0)) # her smooth-min gap: 1 on borders, soft falloff
|
||||
return max(soft ** 2, math.exp(-((f2 - f1) / EW) ** 2)) # F2 - F1 border line of width EW (cell units)
|
||||
|
||||
# ---------------- candidate krill habitat densities (filament candidates) ----------------
|
||||
def env(x, z, t):
|
||||
s = slope(x, z, t); return s, band(s), core(s)
|
||||
|
||||
def D_band(x, z, t, sc):
|
||||
s, b, c = env(x, z, t); return b
|
||||
|
||||
def D_skein(x, z, t, sc, hard=False):
|
||||
s, b, c = env(x, z, t); q = warp(x, z, t, sc)
|
||||
return strands(q, hard) * (1 - c) * (.55 + .45 * b)
|
||||
|
||||
def D_skein_hard(x, z, t, sc): return D_skein(x, z, t, sc, True)
|
||||
|
||||
def D_lattice(x, z, t, sc):
|
||||
s, b, c = env(x, z, t); q = warp(x, z, t, sc, WEB_TAU)
|
||||
return lattice_web(q, t) * (1 - c) * envelope(b)
|
||||
|
||||
def skein_web(x, z, t, sc):
|
||||
"""Strands between wells plus the lattice web hanging off them (web gated by a wide strand halo and the band)."""
|
||||
s, b, c = env(x, z, t); q = warp(x, z, t, sc)
|
||||
st = strands(q); halo = strands(q, widen=4.0)
|
||||
web = lattice_web(warp(x, z, t, sc, WEB_TAU), t) * max(halo, b)
|
||||
return max(st, .8 * web) * (1 - c), s, b, c
|
||||
|
||||
def D_skein_web(x, z, t, sc): return skein_web(x, z, t, sc)[0]
|
||||
|
||||
def D_slack(x, z, t, sc):
|
||||
s, b, c = env(x, z, t); d = well_slack(x, z); return math.exp(-(d / 70.0) ** 2) * (1 - c)
|
||||
|
||||
# ---------------- L1-L3: the trophic DAG over skein_web ----------------
|
||||
def glim(x, z, t, sc):
|
||||
"""Producers: starlight (sun's light radius) plus heat-light on warm well floors, fed along the skein."""
|
||||
sun = H.WELLS[0]; light = math.exp(-(math.dist((x, z), sun["pos"]) / (300 * 14152 ** .25 * .5)) ** 2)
|
||||
heat = sum(math.exp(-(math.dist((x, z), w["pos"]) / (.16 * w["r"])) ** 2) for w in H.WELLS[1:])
|
||||
F, s, b, c = skein_web(x, z, t, sc)
|
||||
return min(1.0, (.35 * light + .9 * heat) * (.3 + .7 * F) + .35 * F), F, b, c
|
||||
|
||||
def holling(p, k=.25): return p * (1 + k) / (p + k) # Holling II saturation, 0 -> 0, 1 -> 1
|
||||
|
||||
def krill(x, z, t, sc):
|
||||
g, F, b, c = glim(x, z, t, sc); return F * holling(g) * (1 - c)
|
||||
|
||||
RING = [(math.cos(2 * math.pi * k / 8), math.sin(2 * math.pi * k / 8)) for k in range(8)]
|
||||
def stencil(f, x, z, t, sc, r):
|
||||
"""Prey density over a hunting radius: the mean of the prey field at the point and 8 taps on a ring of radius r
|
||||
(pointwise: 9 evaluations of the layer below, no grid)."""
|
||||
return (f(x, z, t, sc) + sum(f(x + r * cx, z + r * cz, t, sc) for cx, cz in RING)) / 9.0
|
||||
|
||||
def minnows(x, z, t, sc):
|
||||
s, b, c = env(x, z, t); return min(1.0, 1.6 * holling(stencil(krill, x, z, t, sc, 48.0)) * (.4 + .6 * b))
|
||||
|
||||
def sharks(x, z, t, sc):
|
||||
s, b, c = env(x, z, t); return min(1.0, 2.0 * stencil(minnows, x, z, t, sc, 160.0) * (.3 + .7 * b))
|
||||
|
||||
def lanterns(x, z, t, sc):
|
||||
return D_slack(x, z, t, sc) * min(1.0, 2.5 * stencil(krill, x, z, t, sc, 120.0))
|
||||
|
||||
CANDS = {"band": D_band, "skein": D_skein, "skein-hard": D_skein_hard, "lattice": D_lattice,
|
||||
"skein-web": D_skein_web, "slack": D_slack}
|
||||
LAYERS = {"L1-glim": lambda x, z, t, sc: glim(x, z, t, sc)[0], "L2-krill": krill, "L2-minnows": minnows,
|
||||
"L3-sharks": sharks, "L3-lanterns": lanterns}
|
||||
|
||||
# ---------------- rendering and measurement ----------------
|
||||
def grid(f, t, g):
|
||||
set_time(t); sc = H.scroll(t); vals = []
|
||||
for j in range(g):
|
||||
row = []
|
||||
for i in range(g):
|
||||
x = ((i + .5) / g - .5) * 2 * R; z = ((j + .5) / g - .5) * 2 * R
|
||||
row.append(None if x * x + z * z > R * R else f(x, z, t, sc))
|
||||
vals.append(row)
|
||||
return vals
|
||||
|
||||
def save(vals, name, g):
|
||||
px = [(0, 0, 0) if v is None else H.ramp(v) for r in vals for v in r]
|
||||
H.mark_bodies(px, g); path = os.path.join(OUT, name + ".png"); H.write_png(path, g, g, px); return path
|
||||
|
||||
def phys_corr(vals, g):
|
||||
phys = [c for r in H.physarum_ascii("physarum A") for c in r]
|
||||
bw, bh = g // 64, g // 32; blocks = []
|
||||
for by in range(32):
|
||||
for bx in range(64):
|
||||
cell = [vals[y][x] or 0 for y in range(by * bh, (by + 1) * bh) for x in range(bx * bw, (bx + 1) * bw)]
|
||||
blocks.append(sum(cell) / len(cell))
|
||||
return H.corr(blocks, phys)
|
||||
|
||||
def cand_list(C):
|
||||
n = int(R / C); out = []
|
||||
for j in range(-n, n):
|
||||
for i in range(-n, n):
|
||||
x = (i + H.hash01(i, j, 1)) * C; z = (j + H.hash01(i, j, 2)) * C
|
||||
if x * x + z * z <= R * R: out.append((i, j, x, z, .1 + .9 * H.hash01(i, j, 3), H.hash01(i, j, 4)))
|
||||
return out
|
||||
|
||||
def cyc_scroll(t, ph):
|
||||
"""habitat-no-pop-ruled: the flow term is read at the member's own cycle start."""
|
||||
t0 = t - ((t / CYCLE + ph) % 1.0) * CYCLE; return H.scroll(t0), t0
|
||||
|
||||
CUR_T0 = [None]
|
||||
def density_at(f, c, t):
|
||||
sc, t0 = cyc_scroll(t, c[5]); CUR_T0[0] = t0 if WEB_AT_CYCLE else None
|
||||
try: return f(c[2], c[3], t, sc), t0
|
||||
finally: CUR_T0[0] = None
|
||||
|
||||
def members(f, cands, t, tref=None):
|
||||
set_time(t)
|
||||
if tref is None: return {(c[0], c[1]) for c in cands if density_at(f, c, t)[0] > c[4]}
|
||||
return {(c[0], c[1]) for c in cands if f(c[2], c[3], t, cyc_scroll(tref, c[5])[0]) > c[4]} # flow frozen at tref's cycle
|
||||
|
||||
def presence(d, th): return smooth(th - FADE, th + FADE, d)
|
||||
|
||||
def pop_probe(f, cands, t, nmax=2500):
|
||||
set_time(t)
|
||||
near = [c for c in cands if abs(density_at(f, c, t)[0] - c[4]) < .3][:nmax]
|
||||
prev = {}; worst = 0.0; big = 0
|
||||
for k in range(61):
|
||||
tt = t + k / 60.0; set_time(tt)
|
||||
for c in near:
|
||||
d, t0 = density_at(f, c, tt); p = presence(d, c[4]); key = (c[0], c[1])
|
||||
if key in prev and prev[key][1] == t0: # cycle turns excluded (carried by the cycle fade)
|
||||
step = abs(p - prev[key][0]); worst = max(worst, step); big += step > .1
|
||||
prev[key] = (p, t0)
|
||||
return worst, big, len(near)
|
||||
|
||||
def gabriel_flips(T=1400.0, dt=1.0 / 60):
|
||||
"""Hard Gabriel edge-set changes, and the soft gate's largest step per 1/60 s, as the wells orbit."""
|
||||
flips = 0; worst = 0.0; prev_h = None; prev_s = None; t = 0.0
|
||||
while t <= T:
|
||||
set_time(t); W = H.WELLS; hs, ss = set(), {}
|
||||
for i in range(len(W)):
|
||||
for j in range(i + 1, len(W)):
|
||||
a, b = W[i]["pos"], W[j]["pos"]; r = math.dist(a, b) / 2; m = ((a[0] + b[0]) / 2, (a[1] + b[1]) / 2)
|
||||
mg = min((math.dist(m, W[k]["pos"]) - r) / r for k in range(len(W)) if k not in (i, j))
|
||||
if mg > 0: hs.add((i, j))
|
||||
ss[(i, j)] = smooth(-.15, .15, mg)
|
||||
if prev_h is not None:
|
||||
flips += len(hs ^ prev_h); worst = max(worst, max(abs(ss[k] - prev_s[k]) for k in ss))
|
||||
prev_h, prev_s = hs, ss; t += dt
|
||||
return flips, worst
|
||||
|
||||
def main():
|
||||
t0 = time.time(); T = 0.0
|
||||
print(f"grid {G}, wells orbit (period = distance s), slope terms on, TAU {TAU} s, lattice {LAT} u")
|
||||
f, w = gabriel_flips()
|
||||
print(f"gabriel over 1400 s: hard edge-set changes {f}; soft gate largest step per 1/60 s {w:.5f}")
|
||||
cands = cand_list(16.0); print(f"{len(cands)} krill candidates at 16 u")
|
||||
for name, fn in CANDS.items():
|
||||
if ONLY and name not in ONLY: continue
|
||||
t1 = time.time(); vals = grid(fn, T, G); p = save(vals, "density_" + name, G)
|
||||
cr = phys_corr(vals, G)
|
||||
a = members(fn, cands, T); b = members(fn, cands, T + 1); c8 = members(fn, cands, T + 8); c60 = members(fn, cands, T + 60)
|
||||
n = max(1, len(a))
|
||||
W2 = 600; px = [(10, 14, 30)] * (W2 * W2)
|
||||
for c in cands:
|
||||
if (c[0], c[1]) in a:
|
||||
pi_, pj = int((c[2] / (2 * R) + .5) * W2), int((c[3] / (2 * R) + .5) * W2)
|
||||
if 0 <= pi_ < W2 and 0 <= pj < W2: px[pj * W2 + pi_] = (235, 230, 170)
|
||||
H.mark_bodies(px, W2); H.write_png(os.path.join(OUT, "members_" + name + ".png"), W2, W2, px)
|
||||
a0 = members(fn, cands, T, T); w1 = members(fn, cands, T + 1, T); w60 = members(fn, cands, T + 60, T)
|
||||
print(f" {name} wells-only (flow frozen at t's cycle): changed in 1 s {len(a0 ^ w1) / max(1, len(a0)):.4f}, 60 s {len(a0 ^ w60) / max(1, len(a0)):.3f}")
|
||||
wst, big, nn = pop_probe(fn, cands, 100.0)
|
||||
print(f"{name}: corr physarumA {cr:.3f}; members {len(a)}; changed in 1 s {len(a ^ b) / n:.3f}, 8 s {len(a ^ c8) / n:.3f}, "
|
||||
f"60 s {len(a ^ c60) / n:.3f}; presence step max {wst:.4f} ({big} steps > .1, {nn} near-threshold candidates); {time.time() - t1:.0f}s")
|
||||
if ONLY is None or "layers" in ONLY:
|
||||
for name, fn in LAYERS.items():
|
||||
g = G if name.startswith(("L1", "L2-krill")) else G // 2
|
||||
t1 = time.time(); vals = grid(fn, T, g); save(vals, "layer_" + name, g)
|
||||
print(f"{name}: grid {g}, corr physarumA {phys_corr(vals, g):.3f}; {time.time() - t1:.0f}s")
|
||||
# members per layer at its own candidate spacing
|
||||
for name, fn, C in (("L2-minnows", minnows, 48.0), ("L3-sharks", sharks, 128.0), ("L3-lanterns", lanterns, 64.0)):
|
||||
cs = cand_list(C); t1 = time.time(); a = members(fn, cs, T); b = members(fn, cs, T + 1); c8 = members(fn, cs, T + 8)
|
||||
n = max(1, len(a)); W2 = 600; px = [(10, 14, 30)] * (W2 * W2)
|
||||
for c in cs:
|
||||
if (c[0], c[1]) in a:
|
||||
pi_, pj = int((c[2] / (2 * R) + .5) * W2), int((c[3] / (2 * R) + .5) * W2)
|
||||
for di in range(-1, 2):
|
||||
for dj in range(-1, 2):
|
||||
if 0 <= pi_ + di < W2 and 0 <= pj + dj < W2: px[(pj + dj) * W2 + pi_ + di] = (235, 230, 170)
|
||||
H.mark_bodies(px, W2); H.write_png(os.path.join(OUT, "members_" + name + ".png"), W2, W2, px)
|
||||
print(f"members {name}: {len(a)} of {len(cs)} at {C:.0f} u; changed in 1 s {len(a ^ b) / n:.3f}, 8 s {len(a ^ c8) / n:.3f}; {time.time() - t1:.0f}s")
|
||||
# cost per evaluation (Python, relative): one krill vs one minnow vs one shark evaluation
|
||||
set_time(T); sc = H.scroll(T)
|
||||
for name, fn in (("krill", krill), ("minnows", minnows), ("sharks", sharks)):
|
||||
t1 = time.perf_counter(); [fn(300.0 + k, 200.0, T, sc) for k in range(20)]
|
||||
print(f"cost {name}: {(time.perf_counter() - t1) / 20 * 1e3:.2f} ms per evaluation (Python)")
|
||||
print(f"total {time.time() - t0:.0f}s")
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
6
docs/research/2026-10-14-trophic-dag-probe/win.log
Normal file
6
docs/research/2026-10-14-trophic-dag-probe/win.log
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
WIN 2 BOOST 1.6 lattice: presence step max 0.1236, 8 steps > .1 of 1500 candidates
|
||||
WIN 2 BOOST 1.6 skein-web: presence step max 0.4933, 8 steps > .1 of 1500 candidates
|
||||
WIN 3 BOOST 1.6 lattice: presence step max 0.1236, 8 steps > .1 of 1500 candidates
|
||||
WIN 3 BOOST 1.6 skein-web: presence step max 0.4933, 8 steps > .1 of 1500 candidates
|
||||
WIN 2 BOOST 0.6 lattice: presence step max 0.0471, 0 steps > .1 of 1500 candidates
|
||||
WIN 2 BOOST 0.6 skein-web: presence step max 0.2361, 11 steps > .1 of 1500 candidates
|
||||
8
docs/research/2026-10-14-trophic-dag-probe/winprobe.py
Normal file
8
docs/research/2026-10-14-trophic-dag-probe/winprobe.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import sys; sys.argv=['x','64']
|
||||
import trophic_probe as T
|
||||
cands=T.cand_list(16.0)
|
||||
for win,boost in ((2,1.6),(3,1.6),(2,0.6)):
|
||||
T.WIN, T.BOOST = win, boost
|
||||
for name in ("lattice","skein-web"):
|
||||
w,b,n=T.pop_probe(T.CANDS[name],cands,100.0,1500)
|
||||
print(f"WIN {win} BOOST {boost} {name}: presence step max {w:.4f}, {b} steps > .1 of {n} candidates", flush=True)
|
||||
Loading…
Reference in a new issue