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>
116 lines
4.8 KiB
Python
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,
|
|
)
|