Files
settled-reach/tooling/domains/atlas/planet/sol_data/io_moon.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

116 lines
4.8 KiB
Python

"""
Io (GJ0f-1) terrain builder.
Io is the most volcanically active body in the solar system due to
tidal heating from Jupiter. Surface is covered in sulfur and volcanic
deposits. No published global DEM exists at useful resolution — we use
the Galileo/Voyager global mosaic (albedo) to derive synthetic elevation.
Data source:
- Surface: USGS Io Galileo/Voyager global mosaic
- Elevation: synthetic from albedo (dark = caldera/lava, bright = sulfur)
Properties:
- Surface temp: ~130K background, 400-1800K at volcanic hotspots
- planet_class: "volcanic", atmosphere: "none"
"""
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
# Io global mosaic (Galileo SSI + Voyager) — JPEG from USGS
# If direct download isn't available, fall back to procedural
IO_MOSAIC_URL = "https://astrogeology.usgs.gov/cache/images/bf08a5b6fa0c2ed73117dc1b6c516fa8_io_galileo_voyager_global_mosaic_1km.jpg"
IO_MOSAIC_FILE = "io_galileo_mosaic.jpg"
IO_BACKGROUND_TEMP_K = 130.0
IO_HOTSPOT_TEMP_K = 600.0
def _load_io_mosaic() -> np.ndarray:
"""Load Io global mosaic and convert to synthetic elevation."""
try:
path = ensure_cached(IO_MOSAIC_URL, IO_MOSAIC_FILE)
console.event(f" loading Io mosaic: {path}")
albedo = load_image_as_elevation(str(path), invert=False)
except Exception as e:
console.event(f"Io mosaic unavailable ({e}), generating synthetic", level="warn")
return _synthetic_io_terrain()
# Resample to grid
albedo = resample_to_grid(albedo, GRID_H, GRID_W, order=1)
# Convert albedo to elevation:
# Dark regions (low albedo) = calderas/lava flows = low elevation
# Bright regions (high albedo) = sulfur deposits = high elevation
# Smooth to create plausible topography
elevation = gaussian_filter(albedo, sigma=3.0)
elevation = normalize_01(elevation)
return elevation
def _synthetic_io_terrain() -> np.ndarray:
"""Generate synthetic Io-like terrain if mosaic unavailable."""
rng = np.random.default_rng(42)
base = rng.random((GRID_H, GRID_W)).astype(np.float32)
base = gaussian_filter(base, sigma=8.0)
# Add volcanic calderas (circular depressions)
for _ in range(30):
cy, cx = rng.integers(0, GRID_H), rng.integers(0, GRID_W)
r = rng.integers(3, 15)
y, x = np.ogrid[-cy:GRID_H-cy, -cx:GRID_W-cx]
mask = x*x + y*y <= r*r
base[mask] *= 0.3
return normalize_01(base)
def build_terrain(body_def: dict) -> dict:
"""Build Io terrain dict."""
from tooling.domains.atlas.planet.planet_simulation import compute_biome
console.event(" Io: loading data...")
# ── 1. Elevation ────────────────────────────────────────────────────
elevation = _load_io_mosaic()
sea_level = 0.0
surface_water = np.zeros((GRID_H, GRID_W), dtype=bool)
# ── 2. Temperature ──────────────────────────────────────────────────
# Background ~130K, volcanic hotspots much hotter
temperature_K = temperature_grid_analytical(
base_T_K=IO_BACKGROUND_TEMP_K,
elevation=elevation,
lapse_rate_K_per_unit=-200.0, # low elevation = hot (lava)
lat_gradient_K=10.0,
)
# Volcanic hotspots: low-elevation areas are hot
hotspot_mask = elevation < 0.25
temperature_K[hotspot_mask] += 300.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,
)