fix(assets): mountain rock could never fire — kelvin compared to 0.35 (T-1295)

compute_biome receives absolute kelvin (the Whittaker table is kelvin; the
0-1 normalisation happens afterwards in simulate(), for the renderer only),
but two terrain rules compared it to 0.35. That was the case from the
generator's first commit (f84275edf):

- mountain rock, `temperature < 0.35`: never true, so the class never
  appeared through this rule on any body;
- sulfuric scrub, `temperature > 0.35`: always true.

0.35 can only mean a fraction of the world's own range, the normalisation
compute_moisture already uses, so both rules now compare against that. A
world with no temperature range reads 0 (the cold end).

tooling/test_planet_biome_rules.py pins the rule both ways on a synthetic
body: cold high ground turns to rock (>90%, since anomaly scatter repaints
~3%), and warm high ground and low ground do not. Mutation-proven: restoring
the kelvin comparison fails it at a rock share of exactly 0.000. Wired into
make test-tooling.

Sulfuric scrub remains unreachable for a different reason: it converts rock
only at elev_norm 0.25-0.65, and rock exists only above 0.65. It is left as
found, since making it reachable is a design call and belongs to the balancing
pass, so it is not pinned.

Found while shaping the D-258 biome bake. The committed reliefmaps and globes
were rendered with the bug, and the coming bake reflects the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 14:03:37 +02:00
co-authored by Claude Opus 5.5
parent 112c099f51
commit 3cfec9025d
5 changed files with 381 additions and 2 deletions
@@ -716,7 +716,15 @@ def compute_biome(body_def, elevation, sea_level, surface_water,
has_ice_deposition = atmo not in ("none",) and hydro_here not in ("none", "subsurface")
if has_ice_deposition:
biome[land & (elev_norm > 0.85)] = 17
biome[land & (elev_norm > 0.65) & (temperature < 0.35)] = 18
# `temperature` is absolute kelvin here, but the two rules below were
# written against a 0-1 scale and compared kelvin to 0.35 from the first
# commit: mountain rock could never fire, and the sulfuric test was always
# true (T-1295). 0.35 only means something as a fraction of this world's own
# range — the same normalisation simulate() applies for the renderer — so
# that is what they now compare against. A world with no range reads 0.
t_span = float(temperature.max() - temperature.min())
t_norm = (temperature - temperature.min()) / t_span if t_span > 0 else np.zeros_like(temperature)
biome[land & (elev_norm > 0.65) & (t_norm < 0.35)] = 18
# ── Modifier stack ─────────────────────────────────────────────────────
env = body_def.get("environment", {})
@@ -751,7 +759,7 @@ def compute_biome(body_def, elevation, sea_level, surface_water,
# Sulfuric substrate: scrub on volcanic mid-elevations
if substrate == "sulfuric":
scrub = land & (elev_norm > 0.25) & (elev_norm < 0.65) & (temperature > 0.35)
scrub = land & (elev_norm > 0.25) & (elev_norm < 0.65) & (t_norm > 0.35)
biome[scrub & (biome == 18)] = EXOTIC_CLASSES["sulfuric_scrub"]
# ── Anomaly scatter ─────────────────────────────────────────────────
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""
Pins the mountain-rock rule in `compute_biome`, which compared kelvin
against 0.35 (T-1295).
`compute_biome` receives absolute kelvin, but the rule was written against a
0-1 scale, so from the generator's first commit mountain rock could never
fire. Nothing noticed, because no test asked the rule to fire, or to hold
back. These do both, on a synthetic body whose answer is known.
The sulfuric-scrub rule had the same unit bug and is fixed alongside it, but
it is not pinned here: it converts rock only at elev_norm 0.25-0.65, while
rock exists only above 0.65, so it cannot fire at all. Making it reachable is
a design call, not a units fix.
Run directly or via `make test-tooling`:
.venv/bin/python tooling/test_planet_biome_rules.py
"""
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import numpy as np # noqa: E402
from tooling.domains.atlas.planet.planet_simulation import ( # noqa: E402
GRID_H,
GRID_W,
compute_biome,
)
MOUNTAIN_ROCK = 18
SEA_LEVEL = 0.2
def _body(substrate: str = "silicate") -> dict:
return {
"seed": 7,
"planet_class": "temperate",
"body_type": "planet",
"physical": {"atmosphere": "standard"},
# No hydrosphere: keeps high ground out of the snow rule, so rock is
# the only rule that can claim it.
"environment": {"hydrosphere": "none", "substrate": substrate},
}
def _world(temp_k: np.ndarray, elev: np.ndarray) -> np.ndarray:
water = np.zeros((GRID_H, GRID_W), dtype=bool)
moisture = np.full((GRID_H, GRID_W), 0.5, dtype=np.float32)
return compute_biome(_body(), elev, SEA_LEVEL, water, temp_k, moisture)
class MountainRockTests(unittest.TestCase):
def setUp(self):
# West half high ground, east half low. Temperature runs 275-305 K
# west to east, the temperate band, so the high west is the cold third.
self.elev = np.full((GRID_H, GRID_W), 0.3, dtype=np.float32)
self.elev[:, : GRID_W // 2] = 0.95 # elev_norm ~0.94, above the 0.65 gate
ramp = np.linspace(275.0, 305.0, GRID_W, dtype=np.float32)
self.temp = np.tile(ramp, (GRID_H, 1))
def test_cold_high_ground_is_rock(self):
biome = _world(self.temp, self.elev)
cold_high = biome[:, : int(GRID_W * 0.30)]
share = float((cold_high == MOUNTAIN_ROCK).mean())
# The anomaly scatter repaints ~3% of land, so "all of it" is too
# strong; the dead rule scored exactly 0 here.
self.assertGreater(share, 0.9, f"cold high ground rock share {share:.3f}")
def test_warm_or_low_ground_is_not_rock(self):
biome = _world(self.temp, self.elev)
warm_high = biome[:, int(GRID_W * 0.40) : GRID_W // 2]
low = biome[:, GRID_W // 2 :]
self.assertEqual(int((warm_high == MOUNTAIN_ROCK).sum()), 0)
self.assertEqual(int((low == MOUNTAIN_ROCK).sum()), 0)
if __name__ == "__main__":
unittest.main()