Files
settled-reach/tooling/domains/atlas/planet/sol_data/ice_moons.py
T
jpmschweitzerandClaude Opus 5.5 668772075c refactor(tooling): T-1288 — planet-gen becomes reach atlas planet
The 30-file tree moves under atlas as its third rung (D-243), ten verbs
fronting it. Each verb restates its module's options so `--help` describes
something; tooling/test_planet_router.py hands every declared option to the
module's own argparse and fails on drift, and now runs in make test-tooling.

The 2026-09-02 half of this move had converted the top-level imports and the
repo roots. Finishing it found what the half-move left:

- Lazy in-function imports, and all of sol_data/, still named siblings bare.
  They resolved only through sys.path.insert hacks, so under reach the first
  globe render in generate, batch or sol-import would have raised
  ModuleNotFoundError. Qualified; the hacks are gone.
- 247 print() calls and a stdout progress writer that fired once per 8 KB
  block. Report verbs (audit, quality) write through console.out, progress
  through console.event, and download progress is throttled to 10% steps
  so a job log is not tens of thousands of lines.
- Every error exit raises ReachError with a fix.

Two checks that could not fail:

- batch --verify-determinism printed a warning and exited 0 on a mismatch.
- import-provinces exited 0 with errors > 0.

Both now raise. The 271-body bake is only safe to re-run because the first
one holds.

sol-import --body is action="append" in the module but the router took one
value, so --body GJ0d --body GJ0e kept one. Now repeatable, and _flags repeats
list options.

test_conformance walked one level, so a nested group was reported as a verb
missing @command and its ten verbs were never checked. It recurses now;
proven by stripping @command from `planet quality` and watching it fail.

Stray PNGs from the 2026-09-03 runaway router-test run are parked in
.cache/t1288-stray-pngs/, not committed. Their reliefmaps differ from HEAD
while the heightmap regenerated byte-identical — filed as T-1291.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 16:08:02 +02:00

142 lines
6.0 KiB
Python

"""
Ice moon terrain builder — Europa, Ganymede, Callisto, Enceladus.
These bodies lack high-quality global DEMs. We use available mosaics
(albedo/reflectance) to derive synthetic elevation:
- Bright = ice ridges/highlands (high)
- Dark = mare/chaos terrain/craters (low)
Each moon gets specific temperature and appearance tuning.
"""
import numpy as np
from scipy.ndimage import gaussian_filter
from tooling.domains.atlas.planet.sol_data.download import ensure_cached
from tooling.domains.atlas.planet.sol_data.shared import (
GRID_W, GRID_H,
load_image_as_elevation, resample_to_grid, normalize_01,
compute_hillshade, assemble_terrain,
temperature_grid_analytical,
)
from tooling.core import console
# ─── Per-moon configuration ─────────────────────────────────────────────────
MOON_CONFIG = {
"GJ0f-2": { # Europa
"name": "Europa",
"mosaic_url": "https://astrogeology.usgs.gov/cache/images/3c79b3867c0dc5ec2ea33e485a079e58_europa_voyager_galileo_ssi_global_mosaic_500m.jpg",
"mosaic_file": "europa_galileo_mosaic.jpg",
"base_temp_K": 102.0,
"lat_gradient_K": 10.0,
"sigma": 2.0, # smooth albedo → elevation
"invert_albedo": False, # bright = ridges (high)
},
"GJ0f-3": { # Ganymede
"name": "Ganymede",
"mosaic_url": "https://astrogeology.usgs.gov/cache/images/f60b3c06c92f59834f2d4cf9b46cb8f7_ganymede_voyager_galileo_global_mosaic_1km.jpg",
"mosaic_file": "ganymede_galileo_mosaic.jpg",
"base_temp_K": 110.0,
"lat_gradient_K": 15.0,
"sigma": 3.0,
"invert_albedo": False,
},
"GJ0f-4": { # Callisto
"name": "Callisto",
"mosaic_url": "https://astrogeology.usgs.gov/cache/images/26b4e80eeb35d46c53d56cded56deeef_callisto_voyager_galileo_global_mosaic_1km.jpg",
"mosaic_file": "callisto_galileo_mosaic.jpg",
"base_temp_K": 115.0,
"lat_gradient_K": 12.0,
"sigma": 4.0,
"invert_albedo": False,
},
"GJ0g-2": { # Enceladus
"name": "Enceladus",
"mosaic_url": "https://astrogeology.usgs.gov/cache/images/1e9fede316c8c47fdc0b96f4c09e4915_enceladus_cassini_iss_global_mosaic_100m.jpg",
"mosaic_file": "enceladus_cassini_mosaic.jpg",
"base_temp_K": 75.0,
"lat_gradient_K": 8.0,
"sigma": 2.0,
"invert_albedo": False,
},
}
def _load_mosaic_as_elevation(config: dict) -> np.ndarray:
"""Load a global mosaic and convert to synthetic elevation."""
try:
path = ensure_cached(config["mosaic_url"], config["mosaic_file"])
console.event(f" loading {config['name']} mosaic: {path}")
albedo = load_image_as_elevation(str(path),
invert=config.get("invert_albedo", False))
albedo = resample_to_grid(albedo, GRID_H, GRID_W, order=1)
except Exception as e:
console.event(f"{config['name']} mosaic unavailable ({e}), synthetic", level="warn")
albedo = _synthetic_ice_terrain(config["name"])
# Smooth albedo to create plausible topography
sigma = config.get("sigma", 3.0)
elevation = gaussian_filter(albedo, sigma=sigma)
return normalize_01(elevation)
def _synthetic_ice_terrain(name: str) -> np.ndarray:
"""Generate synthetic ice moon terrain if mosaic unavailable."""
seed = hash(name) & 0xFFFFFFFF
rng = np.random.default_rng(seed)
base = rng.random((GRID_H, GRID_W)).astype(np.float32)
base = gaussian_filter(base, sigma=6.0)
# Add craters
for _ in range(20):
cy, cx = rng.integers(0, GRID_H), rng.integers(0, GRID_W)
r = rng.integers(5, 20)
y, x = np.ogrid[-cy:GRID_H-cy, -cx:GRID_W-cx]
mask = x*x + y*y <= r*r
base[mask] *= 0.5
return normalize_01(base)
def build_terrain(body_def: dict) -> dict:
"""Build ice moon terrain dict from mosaic data."""
from tooling.domains.atlas.planet.planet_simulation import compute_biome
body_id = body_def["id"]
config = MOON_CONFIG.get(body_id)
if config is None:
raise ValueError(f"No ice moon config for {body_id}")
console.event(f" {config['name']}: loading data...")
# ── 1. Elevation ────────────────────────────────────────────────────
elevation = _load_mosaic_as_elevation(config)
sea_level = 0.0
surface_water = np.zeros((GRID_H, GRID_W), dtype=bool)
# ── 2. Temperature ──────────────────────────────────────────────────
temperature_K = temperature_grid_analytical(
base_T_K=config["base_temp_K"],
elevation=elevation,
lapse_rate_K_per_unit=5.0,
lat_gradient_K=config["lat_gradient_K"],
)
temperature_K = np.maximum(temperature_K, 40.0)
# ── 3. Moisture ─────────────────────────────────────────────────────
moisture = np.zeros((GRID_H, GRID_W), dtype=np.float32)
# ── 4. Biome ────────────────────────────────────────────────────────
biome = compute_biome(body_def, elevation, sea_level, surface_water,
temperature_K, moisture)
# ── 5. Hillshade ────────────────────────────────────────────────────
hillshade = compute_hillshade(elevation)
return assemble_terrain(
elevation=elevation, temperature_K=temperature_K,
moisture=moisture, biome=biome,
surface_water=surface_water, hillshade=hillshade,
rivers=[], sea_level=sea_level,
)