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>
133 lines
5.3 KiB
Python
133 lines
5.3 KiB
Python
"""
|
||
Luna (GJ0d-1) terrain builder.
|
||
|
||
Data source:
|
||
- Elevation: LOLA (Lunar Orbiter Laser Altimeter) DEM
|
||
Available at various resolutions from USGS Astrogeology.
|
||
We use the 4ppd (1440×720) or 16ppd version.
|
||
|
||
Luna properties:
|
||
- Min elevation: ~-9100 m (South Pole-Aitken basin)
|
||
- Max elevation: ~10786 m (near Engel'gardt crater rim)
|
||
- No atmosphere, no water
|
||
- body_type: "moon" → uses lunar biome palette (classes 31/32/33)
|
||
"""
|
||
|
||
import numpy as np
|
||
|
||
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_raw_binary,
|
||
resample_to_grid, normalize_01,
|
||
compute_hillshade, assemble_terrain,
|
||
temperature_grid_analytical,
|
||
)
|
||
from tooling.core import console
|
||
|
||
# LOLA GDR — available as PDS IMG files
|
||
# 4ppd (1440 × 720) — compact version
|
||
LOLA_4PPD_URL = "https://pds-geosciences.wustl.edu/lro/lro-l-lola-3-rdr-v1/lrolol_1xxx/data/lola_gdr/cylindrical/img/ldem_4.img"
|
||
LOLA_4PPD_FILE = "lola_gdr_4ppd.img"
|
||
LOLA_4PPD_W = 1440
|
||
LOLA_4PPD_H = 720
|
||
|
||
# 16ppd (5760 × 2880) — higher quality
|
||
LOLA_16PPD_URL = "https://pds-geosciences.wustl.edu/lro/lro-l-lola-3-rdr-v1/lrolol_1xxx/data/lola_gdr/cylindrical/img/ldem_16.img"
|
||
LOLA_16PPD_FILE = "lola_gdr_16ppd.img"
|
||
LOLA_16PPD_W = 5760
|
||
LOLA_16PPD_H = 2880
|
||
|
||
# Luna physical constants
|
||
LUNA_MIN_ELEV_M = -9100.0
|
||
LUNA_MAX_ELEV_M = 10786.0
|
||
LUNA_EQUATORIAL_TEMP_K = 220.0 # mean dayside ~220K
|
||
LUNA_POLAR_TEMP_K = 100.0 # permanently shadowed craters ~40K, average ~100K
|
||
|
||
|
||
def _load_lola(use_16ppd: bool = False) -> np.ndarray:
|
||
"""Load LOLA DEM, return elevation in metres."""
|
||
if use_16ppd:
|
||
url, filename, w, h = LOLA_16PPD_URL, LOLA_16PPD_FILE, LOLA_16PPD_W, LOLA_16PPD_H
|
||
else:
|
||
url, filename, w, h = LOLA_4PPD_URL, LOLA_4PPD_FILE, LOLA_4PPD_W, LOLA_4PPD_H
|
||
|
||
path = ensure_cached(url, filename)
|
||
console.event(f" loading LOLA: {path} ({w}x{h})")
|
||
|
||
# LOLA GDR: little-endian int16 (LSB_INTEGER per PDS label)
|
||
# with a scaling factor of 0.5 metres.
|
||
try:
|
||
arr = load_raw_binary(str(path), w, h, dtype="<i2", offset=0)
|
||
# LOLA int16 values are in units of 0.5m (scale factor 0.5)
|
||
arr = arr * 0.5
|
||
except ValueError:
|
||
# If int16 doesn't work, try float32
|
||
arr = load_raw_binary(str(path), w, h, dtype="<f4", offset=0)
|
||
|
||
# Handle nodata
|
||
arr[arr > 20000] = 0.0
|
||
arr[arr < -20000] = 0.0
|
||
|
||
console.event(f" LOLA range: [{arr.min():.0f}, {arr.max():.0f}] m")
|
||
return arr
|
||
|
||
|
||
def build_terrain(body_def: dict) -> dict:
|
||
"""Build Luna terrain dict from LOLA data."""
|
||
from tooling.domains.atlas.planet.planet_simulation import compute_biome
|
||
|
||
console.event(" Luna: loading LOLA data...")
|
||
|
||
# ── 1. Elevation ────────────────────────────────────────────────────
|
||
lola_raw = _load_lola(use_16ppd=False)
|
||
|
||
# LOLA cylindrical: col 0 = 0° longitude — shift to 180°W
|
||
from tooling.domains.atlas.planet.sol_data.shared import greenwich_to_dateline
|
||
lola_shifted = greenwich_to_dateline(lola_raw)
|
||
|
||
elevation_m = resample_to_grid(lola_shifted, GRID_H, GRID_W, order=1)
|
||
elevation = normalize_01(elevation_m, LUNA_MIN_ELEV_M, LUNA_MAX_ELEV_M)
|
||
|
||
# No liquid — sea level at 0
|
||
sea_level = 0.0
|
||
surface_water = np.zeros((GRID_H, GRID_W), dtype=bool)
|
||
|
||
console.event(" elevation normalised")
|
||
|
||
# ── 2. Temperature ──────────────────────────────────────────────────
|
||
temperature_K = temperature_grid_analytical(
|
||
base_T_K=LUNA_EQUATORIAL_TEMP_K,
|
||
elevation=elevation,
|
||
lapse_rate_K_per_unit=10.0,
|
||
lat_gradient_K=120.0, # huge contrast equator to poles
|
||
)
|
||
# Clamp minimum
|
||
temperature_K = np.maximum(temperature_K, 40.0)
|
||
|
||
console.event(f" temperature: [{temperature_K.min():.0f}, {temperature_K.max():.0f}] K")
|
||
|
||
# ── 3. Moisture ─────────────────────────────────────────────────────
|
||
moisture = np.zeros((GRID_H, GRID_W), dtype=np.float32)
|
||
|
||
# ── 4. Biome ────────────────────────────────────────────────────────
|
||
# body_type: "moon" + atmosphere: "none" → lunar palette (31/32/33)
|
||
biome = compute_biome(body_def, elevation, sea_level, surface_water,
|
||
temperature_K, moisture)
|
||
console.event(f" biomes: {len(np.unique(biome))} classes")
|
||
|
||
# ── 5. Hillshade ────────────────────────────────────────────────────
|
||
hillshade = compute_hillshade(elevation)
|
||
|
||
# ── 6. Assemble ─────────────────────────────────────────────────────
|
||
return assemble_terrain(
|
||
elevation=elevation,
|
||
temperature_K=temperature_K,
|
||
moisture=moisture,
|
||
biome=biome,
|
||
surface_water=surface_water,
|
||
hillshade=hillshade,
|
||
rivers=[],
|
||
sea_level=sea_level,
|
||
)
|