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>
144 lines
5.9 KiB
Python
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,
|
|
)
|