""" 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, )