Files
settled-reach/tooling/planet-gen/sol_data/ice_moons.py
T
jpmschweitzerandClaude Opus 4.6 18bdb1ed3d feat(assets): add Sol system handcrafted terrain pipeline
Custom pipeline for GJ-0 (Sol) that imports real NASA/USGS
planetary data instead of procedural generation. Produces the
same output format (heightmap.png, globe.png, markers.json).

Real data bodies:
- Earth: ETOPO2022 elevation + WorldClim climate + 14 rivers
- Mars: MOLA DEM + ferric biome classes + terraformed water
- Luna: LOLA DEM + lunar biome palette

Procedural fallback for Mercury, Venus, Phobos, Deimos.
Synthetic elevation from albedo for Io, Europa, Ganymede,
Callisto, Titan, Enceladus. Gas giants use existing renderer.

New biome classes 34-36 (ferric_dust/highland/lowland) for
Mars iron oxide surface. Earth features: 50 cities (smart
scatter by continent), 15 named rivers, oceans, mountains.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 22:22:18 +02:00

144 lines
5.9 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 pathlib import Path
from scipy.ndimage import gaussian_filter
from sol_data.download import ensure_cached
from sol_data.shared import (
GRID_W, GRID_H,
load_image_as_elevation, resample_to_grid, normalize_01,
compute_hillshade, assemble_terrain,
temperature_grid_analytical,
)
# ─── 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"])
print(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:
print(f" WARNING: {config['name']} mosaic unavailable ({e}), synthetic")
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."""
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from 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}")
print(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,
)