""" 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 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, ) # 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) print(f" loading Io mosaic: {path}") albedo = load_image_as_elevation(str(path), invert=False) except Exception as e: print(f" WARNING: Io mosaic unavailable ({e}), generating synthetic") 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.""" import sys sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from planet_simulation import compute_biome print(" 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, )