Environment Configuration — Terrain Stacks
Real-terrain v2 composes one simulation-ready terrain from multiple
real data products — a coarse gap-free DEM base, finer partial-coverage DEM
insets, sunlit and permanently-shadowed-region (PSR) colour mosaics, masks,
and quality layers — instead of the one-DEM/one-ortho pairing of the v1
manifest sites. This page is the operator-facing guide to that stack model:
the per-body source catalog, arbitrary lat/lon region requests
(RealTerrain(body, lat_deg, lon_deg, size_m) and
srb asset download terrain --lat --lon --size-m), the deshade knob, the
consent gate, and how a real terrain’s companion assets (sky / lighting /
rocks) are arbitrated against the ephemeris sun when both
want to own the scene.
For the fusion engine itself (blending, provenance, budgets) see Real Terrain Assets; for authoring catalog layers and v2 manifest sites see Adding Terrain Sites.
Read this first — the shipped catalogs now carry real, curated data, but coverage is still narrow. The v2 request path is implemented and now proven end to end against real network products, not just a synthetic fixture:
srb/terrain/catalog/moon.yamlcarries 19 real, sha256-pinned layers (2 LOLA polar DEM bases, 4 NAC DTMs, 1 Haworth SfS DEM, 4 SfS A3CLR DEM/ortho, 1 ShadowCam DTM + 1 cmosaic, 1 NAC ROI, 1 WAC_EMP colour mosaic, 1 LOLA LDAM 1064 nm colour map, 1 PGDA LPSR mask, 2NAC_POLE_SOUTH_CMreference tiles);mars.yamlcarries 2 (a HiRISE Jezero DTM + ortho);earth.yamlcarries 2 (one Copernicus GLO-30 polar tile, one 3DEP 1 m non-polar tile). Note thataccess: cog_remoteremains a catalog compatibility label. Production no longer gives its URL directly to GDAL: the v2 loader downloads the complete source through SRB’s redirect-validating, size-bounded provider, verifies its pinned SHA-256, then asks rasterio to read a local window. Consequently every contributing production layer recordsbytes_verified: true. This closes an allowlisted-origin redirect escape at the cost of first-bake download size and latency. Four real region bakes — Moon polar, Mars Jezero, Earth polar, Earth non-polar — complete end to end witharea_fraction=1.0on every DEM base layer. That claim is scoped to DEM bases deliberately: the per-product mask DN decoding shipped later on this branch moved the Moon-polar colour base off0.0, but not to1.0either, so no single figure covers both kinds of base. See Catalog status per body below anddocs/research/real_terrain_v2/for the full §6.11 validation study run against this data. Coverage is still narrow, and a request outside every curated footprint still resolves to an empty plan: in an environment that degrades to the procedural terrain with a one-time warning naming the reason; on the CLI it is a clear error (exit 1). LDAM is now curated asimbrium_ldam_50s_1000m: its detached PDS3.LBL/.IMGpair was fetched over HTTPS, independently hash/size verified, footprint/radiometry reviewed, and used in two deterministic real 8 km colour bakes. WAC_EMP 643 nm remains priority 3 and therefore precedes priority-4 LDAM in their overlap; LDAM is 1064 nm active-laser albedo, so a spectral seam can remain and no gain is invented. The contribution bake used an isolated LDEM+LDAM catalog because no shipped Moon DEM covers the 50S–60S annulus where LDAM would otherwise be the sole colour source. See the catalog header and durable P2-05 evidence. TheNAC_POLE_SOUTH_CM_065/CM_135mosaics were blocked on the same list for their per-tile size until 2026-08-02, when the one band-1 tile covering Connecting Ridge was downloaded and pinned per epoch (lroc_nac_pole_south_cm065_p892s2250_1mand itscm135sibling, 8,276,814,528 B each,allow_large_download: trueplusreference_only: true, so they are never resolved as bake input at all — only by explicitlayer_id); the remaining tiles stay unpinned, an operator decision per site. (A third family, SfS A3CLR, was blocked for the same allowlist reason until 2026-08-02, when the allowlist was deliberately widened and its Connecting Ridge / Haworth DEM + orthomosaic layers were pinned for real from the Zenodo archive.)SRB_TERRAIN_CATALOG_DIRstill lets you swap in a hand-built or fixture catalog for testing.
The catalog model
A per-body source catalog (srb/terrain/catalog/<body>.yaml, loaded by
srb.terrain.catalog.loader) is the reviewed, pinned record of every real
data product v2 may touch: URL(s), sha256, footprint, native GSD, license,
priority_class, and the curated half of the large-download consent gate.
Given a region, the resolver (srb.terrain.catalog.resolve.resolve_stacks)
intersects the catalog’s footprints with the requested patch and emits a
CompositionPlan — a prioritized DEM stack plus sunlit-colour, PSR-colour,
mask, and quality stacks — which the fusion engine bakes into one terrain.
Preview a plan without moving a byte:
srb asset stack --body moon --lat -89.46 --lon 137.3 --size 2000
srb asset stack --body moon --lat -89.46 --lon 137.3 --size 2000 --json
srb asset stack also prints whether deshading would apply
(deshade: on (request) / off) — note the printed status reflects only
the --deshade flag; a manifest site’s own deshade: true can still force
it on for a site bake (see Deshade
below).
There are two ways to consume the catalog:
- A declared-stack manifest site — a
srb/terrain/manifest.yamlentry (manifestversion: 2) that names which catalog layers to fuse (dem_stack/color_stack). Bakes through the sameRealTerrain(body=..., site=...)/srb asset download terrain <site>calls a v1 site uses. - An arbitrary region request — no manifest entry at all; the resolver finds coverage by footprint intersection. The rest of this page is mostly about this form.
Catalog status per body
| Body | Layers | Curated families |
|---|---|---|
| moon | 19 | pgda_ldem_83s_10mpp / pgda_ldem_80s_20mpp (LOLA polar DEM bases); lroc_nac_dtm_shackrdge02 / nobile03 / malapert02 / malapert03 (NAC DTMs); usgs_astro_haworth_sfs_dem_1m (Haworth SfS DEM); zenodo_sfs_a3clr06_connridge_dem_5m / zenodo_sfs_a3clr06_connridge_omos_5m / zenodo_sfs_a3clr02_haworth_dem_5m / zenodo_sfs_a3clr02_haworth_omos_5m (SfS A3CLR DEM + orthomosaic, Connecting Ridge and Haworth); lroc_shadowcam_dtm_faustini_6m + lroc_shadowcam_cmosaic_faustini01_20m (ShadowCam DTM + cmosaic); lroc_nac_roi_haworth_lo1_20m (NAC ROI); lroc_wac_emp_643nm_south_100m (WAC_EMP 643 nm colour); imbrium_ldam_50s_1000m (LOLA LDAM 1064 nm normal albedo, nominal 1000 m at the pole, cap:50S); pgda_lpsr_85s_60m (PGDA LPSR mask); lroc_nac_pole_south_cm065_p892s2250_1m + lroc_nac_pole_south_cm135_p892s2250_1m (NAC_POLE_SOUTH_CM band-1 tile P892S2250, one per subsolar-longitude epoch — reference_only: true, so resolvable only by explicit layer_id, never as bake input) |
| mars | 2 | usgs_astro_hirise_jezero_dtm_1m + usgs_astro_hirise_jezero_ortho_1m (HiRISE Jezero DTM + ortho) |
| earth | 2 | copernicus_glo30_dryvalleys_s78e161 (Copernicus GLO-30 polar); usgs_3dep_nm_southeast_x45y364_1m (3DEP 1 m non-polar) |
This is a curated subset, not exhaustive global coverage — a request whose
footprint isn’t covered by any of the layers above still falls back to the
v1 single-source path (or, for a region request, an empty plan). The moon
starter-set spec targeted ~15 layers. LDAM is now pinned as
imbrium_ldam_50s_1000m: a detached PDS3 .LBL/.IMG bundle from the
HTTPS-only imbrium provider, with independent file hashes and sizes. Its
cap:50S footprint is the inscribed extent, and its nominal 1000 m/pixel
resolution is true at the pole. WAC_EMP’s priority 3 keeps its 643 nm product
ahead of priority-4 LDAM where both cover; consumers must tolerate the possible
643 nm/1064 nm spectral seam.
NAC_POLE_SOUTH_CM_065/CM_135 left that list on 2026-08-02: the one
band-1 tile covering Connecting Ridge (P892S2250) is now pinned per
epoch as a reference_only: true comparison reference; the remaining
tiles stay unpinned, an operator decision per site — the 2026-08-02
download re-measured the host at 8.2–8.4 MB/s (~17 min for the 8.28 GB
band-1 tile), not the ~1.3 MB/s the original curation saw, so per-tile size
(8.3–31.2 GB) rather than throughput is what makes each one a decision. See
Adding Terrain Sites → Adding a v2 source layer
for the authoring workflow to curate more layers.
Providers
Every catalog layer’s provider is a closed, exact-hostname allowlist —
srb/terrain/sources/*.py. Phase 5b adds three Mars/Earth providers on the
same public-bucket pattern as the existing ones, and imbrium (2026-08-02)
is listed alongside them:
| Provider | Body | Hosts | Auth |
|---|---|---|---|
uahirise | mars | hirise.lpl.arizona.edu, www.uahirise.org, uahirise.org | none |
copernicus_s3 | earth | copernicus-dem-30m.s3.amazonaws.com, copernicus-dem-90m.s3.amazonaws.com | none |
usgs_3dep | earth | prd-tnm.s3.amazonaws.com | none |
imbrium | moon | imbrium.mit.edu | none |
A fourth, imbrium (imbrium.mit.edu, the LOLA team’s MIT archive, moon,
no auth), was registered on 2026-08-02. It is the only provider that
accepts https only: the archive is linked from PGDA over plain
http://, but the host was probed and serves valid TLS, so the plaintext
scheme is refused rather than transcribed — and since the fetch path
re-checks _accepts_url on every redirect hop, that also turns an
https → http downgrade redirect into a hard error. The curated LDAM layer
uses this provider (see the LDAM note above).
All three Phase 5b providers are plain HTTPS GETs against public,
unauthenticated hosts/S3
buckets — there is no credential plumbing anywhere in the fetch path
(no API keys, no signed URLs, no AWS SigV4). This is a real limitation, not
just an unimplemented convenience: a product that requires authenticated
access (a private bucket, an API-key-gated endpoint) cannot be curated into
the catalog today regardless of license, until credentialed access is added.
Existing Mars USGS products (CTX, HRSC-MOLA) need no new provider — the
existing usgs_astro hosts already cover them.
Region requests
from srb.assets.scenery import RealTerrain
terrain = RealTerrain(body="moon", lat_deg=-89.66, lon_deg=0.0, size_m=2000.0)
lat_deg / lon_deg / size_m are all-three-or-none — setting any one
without the others is a validation error naming the missing field(s) — and
mutually exclusive with both site= and dem_path= (and with the
patch-override fields patch_size_m/patch_center_xy_m: a region’s own
size_m and resolved center define the bake footprint). lod maps through;
size_m may be at most 8000 m (MAX_REGION_SIZE_M — tile larger areas as
multiple requests).
Because these are plain numeric fields (not a nested config), the intent
is that they be CLI-addressable on any env whose scenery is a RealTerrain
via Hydra’s dotted-override syntax:
srb agent teleop -e _ground \
env.scenery.lat_deg=-89.66 env.scenery.lon_deg=0.0 env.scenery.size_m=2000
This form does not currently work — confirmed, not just untested. Investigated while running the first real moon v2 bake (SPICE/Terrain remaining-work Task 6): Hydra’s
ConfigStorestruct forenv.sceneryis built from_ground’s declared default (AssetVariant.PROCEDURAL), and Hydra’s struct mode then rejects any override key that isn’t already present in that struct —ConfigAttributeError: Key 'lat_deg' is not in struct. Prefixing+to add a “new key” gets past struct validation but then fails differently (AssetResolverresolves to the baseSceneryclass rather thanRealTerrain, so the requiredbodyfield is missing). This is a real Hydra/AssetResolverlimitation on this override surface, not a naming typo — no fix has landed for it. The working ways to construct a region request today are the pure Python constructor (RealTerrain(body=..., lat_deg=..., lon_deg=..., size_m=...)) and the dedicatedsrb asset download terrain --lat --lon --size-m/srb asset stack --lat --lon --sizeCLI subcommands below — both parse and run.
Region identity: the slug
A region request’s cache identity is a deterministic, machine-independent slug used everywhere a manifest site’s name would be:
region_{body}_{lat_deg:+08.4f}_{lon_deg:+09.4f}_{int(round(size_m))}m
# e.g. region_moon_-89.6600_+000.0000_2000m
The request’s fields themselves are normalized before the slug (or anything else) reads them, so two requests within the same quantum are genuinely the same region — same fields, same cache entry, same bake:
lon_degis wrapped to[-180, 180)(solon=180andlon=-180are one region), and signed zero collapses to0.0;lat_degandlon_degare rounded to1e-4degrees (≈3 m on the Moon) — requests differing only inside that quantum share one cached region;size_mis rounded to whole metres.
The slug’s field widths are pinned by golden-string tests — changing them would silently orphan every existing cache entry.
Bodies and canonical CRSs
Three bodies are supported end-to-end (Body = Literal["moon", "mars", "earth"]): request path, catalog schema, providers, materials/physics/
lighting/augment presets, and companion/scenery wiring. Each body has a
canonical polar pair plus a canonical geographic CRS, resolved by
srb.terrain.ingest.planetary_crs.canonical_crs_for and matched against raw
DEM headers by normalize_crs (projection-parameter matching first,
authority code only as a fast path — see that module’s docstring):
| Body | South-polar | North-polar | Geographic |
|---|---|---|---|
| moon | IAU_2015:30135 | IAU_2015:30130 | IAU_2015:30100 |
| mars | IAU_2015:49935 | IAU_2015:49930 | IAU_2015:49900 |
| earth | EPSG:3031 | EPSG:3413 | EPSG:4326 |
Mars’s polar/geographic entries use the IAU_2015 sphere radius
3396190.0 m (not the 3389500 m best-fit ellipsoid figure sometimes quoted
elsewhere). Earth’s polar entries use the WGS84 semi-major axis
6378137.0 m; EPSG:3413’s non-zero central meridian (lon_0 = -45) means
it is reachable only via the authority fast path, never by projection-
parameter matching (a documented asymmetry with EPSG:3031 — see the code
comment at planetary_crs.py’s _CANONICAL_PARAM_SIGNATURES).
Parameter matching is scale-aware as of 2026-08-02: a polar-stereographic
grid’s lat_ts/k_0 are normalised to one effective scale at the origin and
compared, so an equivalent WKT spelling of EPSG:3031 (standard parallel
-71) still matches it, while an unscaled south-polar WGS84 grid — a
genuinely different grid, ~2.7% off in ground scale — no longer does. Such a
grid now falls through to matched_by="body_default" (EPSG:4326) and keeps
its own CRS, instead of being silently treated as EPSG:3031 and skipping the
reprojection that would have corrected it. No shipped catalog layer is
affected — all of them are unscaled polar or geographic — but a user-supplied
local DEM can be.
Body-aware, non-polar-capable resolution
Every request resolves in a body-aware projected CRS (Phase 5b, D12 lift): a
polar request (|lat_deg| >= 60°) uses the body’s canonical polar-
stereographic CRS from the table above, and any other request uses a
per-request local stereographic CRS centered on its own lat/lon — so
comparing catalog footprints (also pinned in projected metres) against the
request never mixes CRSes. There is no longer a structural non-polar gate:
a request with no catalog footprint actually covering it still returns the
resolver’s empty plan with a warning, which means procedural fallback in
an env and a clear error on the CLI, exactly like the empty-catalog case.
The local CRS (local_stereographic_crs(body, lat_deg, lon_deg)) is a proj
string of the form +proj=stere +lat_0=<lat> +lon_0=<lon> +R=<body radius> +units=m +no_defs (Earth uses +ellps=WGS84 in place of +R=), built fresh
per request from its own already-quantized (1e-4°) center — it is exact at
the center point (the center always projects to (0, 0)) and its
distortion over a patch radius up to MAX_REGION_SIZE_M (8 km, the region
size cap) is negligible for terrain-scale use; nothing this projection feeds
(mesh, footprint containment, location-variety offsets) needs sub-8-km-scale
angular accuracy. Because its lat_0/lon_0 are the request’s own center
rather than a zero origin, it never matches one of the table’s canonical
parameter signatures — normalize_crs on it always reports
matched_by="body_default", and the bake’s georef block keeps this proj
string verbatim rather than trying to canonicalize it to an authority id.
This helper raises at the poles themselves (|lat_deg| >= 90 - 1e-9) — polar
requests must go through the canonical-CRS path above instead, since an
unscaled oblique-stereographic pole center would silently collide with (and
for Earth, differ in scale from) the canonical polar entry.
Footprints: the bbox: form
A catalog layer’s footprint field (see
Adding Terrain Sites)
now accepts a third shorthand alongside cap:<lat><N|S> and WKT
POLYGON(...)/MULTIPOLYGON(...) in projected metres:
bbox:<lonmin>,<latmin>,<lonmax>,<latmax>
— a plain geographic-degrees footprint, body-agnostic and independent of whatever projected CRS the region happens to resolve into. This is the form non-polar (and most Mars/Earth) layers are expected to use, since authoring a WKT polygon in a per-region local stereographic CRS that doesn’t exist until request time is impractical. Rules, enforced at schema load and at footprint-containment time:
lonmin/lonmaxaccept the full[-360, 360]pre-normalization range — the dominant convention for planetary product metadata (LOLA/HRSC/CTX 0–360° east longitude) works unmodified — and are normalized to[-180, 180)once, at parse time, before any containment check runs.lonmin > lonmaxafter normalization is a deliberate antimeridian wrap (the box spans ±180°), not an error — containment islon >= lonmin or lon <= lonmaxin that case, mirroring the region-slug’s own lon handling.latmin <= latmaxis enforced (unlike lon, an inverted latitude band has no valid meaning):latmin > latmaxraises apydantic.ValidationErrornaming the footprint string, at catalog-load time, before any byte moves.- Containment against a request is evaluated by projecting each sample point
back to lat/lon (
xy_to_latlonin the region’s own resolved CRS, whatever that is — canonical polar or local stereographic) and testing the degree box directly, sobbox:footprints work identically in every body-aware CRS from the section above.
cap: and WKT-in-projected-metres are unchanged; WKT stays usable only when
the catalog author and the region resolver agree on a shared canonical
projected CRS (effectively: polar layers only, since a non-polar region’s
CRS is generated fresh per request and can’t be known ahead of time when the
catalog entry is authored).
What happens when a region can’t resolve
An empty DEM plan — any request whose footprint no curated layer covers
(most requests today, since coverage is narrow — see
Catalog status per body), or a request against a
body/region with no catalog coverage at all — raises RegionUnresolvedError
inside the bake path. The two consumers treat it differently, on purpose:
- Environments (
RealTerrainasenv.scenery): the existing terrain degrade ladder catches it and falls back to the procedural terrain, with a one-time warning naming the region slug and the reason. Training keeps running; the terrain just isn’t real. - CLI (
srb asset download terrain --lat ...): prints the same reason and exits 1 — a download command has no procedural fallback by design: a download that cannot resolve should say so, not silently bake noise. When the body’s loaded catalog really has zero layers, the error is followed by a note naming the catalog file and the dormancy state.
CLI
# Arbitrary region (mutually exclusive with a positional site target,
# --all, and the site-only flags --patch/--patch-size/--center/--no-ortho):
srb asset download terrain --lat -89.66 --lon 0.0 --size-m 2000 \
[--body moon] [--lod medium] [--deshade] [--force]
# Preview what a region would resolve to (moves no bytes):
srb asset stack --body moon --lat -89.66 --lon 0.0 --size 2000 [--deshade]
# Assess without baking (doctor/info know region-derived cache entries):
srb asset doctor terrain [--deshade]
The region download routes through the exact same bake_or_load path the
envs use, so consent, caching, and provenance all apply unchanged. A
remediation command printed by doctor/availability (download_command)
reproduces the request faithfully — including --deshade when the
assessment was deshade-keyed, since a deshaded bake is a different cache
entry (below).
Per-env location variety
Region requests (only region requests — a curated site or a local
dem_path have a fixed footprint) can fan out into K distinct patches,
one baked USD per patch, distributed across environments by the Isaac Lab
clone planner instead of every env sharing the same terrain:
from srb.assets.scenery import RealTerrain
# K=8 patches: index 0 is the requested center verbatim; the other 7 are
# deterministic seeded offsets within a 3000 m disk around it.
terrain = RealTerrain(
body="moon",
lat_deg=-89.66,
lon_deg=0.0,
size_m=2000.0,
num_locations=8,
location_spread_m=3000.0,
)
# Explicit centers instead of derived offsets (all share size_m/lod/deshade):
terrain = RealTerrain(
body="moon",
size_m=2000.0,
locations=[(-89.66, 0.0), (-89.40, 15.0), (-89.80, -20.0)],
)
Pre-bake the same set from the CLI (serial, same bake_or_load path, same
consent/caching/provenance rules as a single-location download):
srb asset download terrain --lat -89.66 --lon 0.0 --size-m 2000 \
--num-locations 8 --location-spread-m 3000
Each line printed is <slug>: <path>, one per baked location, in derivation
order. --num-locations/--location-spread-m are rejected (exit 2) off the
--lat/--lon/--size-m region form, exactly like --patch-size/--center;
--num-locations below 1 is also exit 2. A RegionUnresolvedError on any
location prints that location’s slug and stops the run — locations after the
failing one are never attempted, and any already-baked predecessors are kept
(partial completion, not all-or-nothing).
Semantics
| Aspect | Behavior |
|---|---|
| Primary patch | Index 0 is always the requested (lat_deg, lon_deg) center verbatim (or locations[0] for the explicit-list form). Companions, the SimForge augment layer, and any other single-path reader use RealTerrain.primary_baked_usd_path, which anchors to this patch. |
| Derivation | Requests 1..K-1 come from numpy.random.default_rng([905, seed]) (a fixed salt decoupling this stream from every other consumer of seed), sampled uniformly in a disk of radius location_spread_m (r = location_spread_m · √u) in the request’s own resolved projected CRS (canonical polar-stereographic near the poles, local stereographic elsewhere — see Bodies and canonical CRSs). location_spread_m defaults to size_m when omitted. |
| Prefix-stability | The RNG stream does not depend on K, so raising num_locations (e.g. 4 → 8) keeps the first 4 derived slugs — and their cache entries — unchanged; it only appends new ones. |
| Slug uniqueness | After the usual 1e-4 deg / whole-metre normalization, the K requests must resolve to K distinct slugs. A collision (spread too small relative to the quantum, or duplicate entries in an explicit locations list) raises RegionInvalidError naming the duplicate slugs and suggesting a larger location_spread_m or de-duplicating locations — never a silent dedupe or re-jitter. |
| Any latitude | Variety works at any latitude since Phase 5b: the offset math runs in the request’s own projected CRS (canonical polar-stereographic near the poles, local oblique stereographic elsewhere — see body-aware resolution above), so neither derived nor explicit centers are restricted to the polar band anymore. |
locations vs num_locations | Mutually exclusive: locations fixes K to its own length and requires size_m (it has no single center to pair with lat_deg/lon_deg); passing both locations and num_locations != 1, or locations together with a lat_deg/lon_deg center, is a validation error. |
| Companions | A region request has no curated companions regardless of K (unchanged from the single-location case) — they attach only for a curated site. |
K==1 (the default) is byte-identical to the pre-Phase-5a single-patch
behavior: same UsdFileCfg spawner, same cache entries, same slugs. K>1
instead spawns a genuine MultiAssetSpawnerCfg (func=spawn_multi_asset_compat)
at the env-scoped prim path, which Isaac Lab’s clone planner distributes one
variant per environment.
Deshade (illumination-corrected colour)
Real sunlit colour mosaics have the sun baked into them: topographic shading
from the capture-time illumination fights the renderer’s own lighting.
deshade=True corrects for this at fusion time — for each sunlit colour
layer, the engine models a hillshade of the fused DEM at that layer’s
catalogued capture illumination (sun_azimuth_deg/sun_elevation_deg on
the catalog layer) and divides it out, mean-preserving (the layer’s overall
brightness does not shift) and NaN-safe (nodata holes pass through
untouched). Rules:
- PSR colour layers are never deshaded — a permanently shadowed region has no capture-time sun to divide out.
- A sunlit layer without catalogued sun angles is left un-deshaded, with
a warning naming it that lands in the bake’s
meta.jsonfusionwarnings— never a silent skip. - If no fused DEM is available to model hillshade from, colour is left un-deshaded with a warning.
Three knob surfaces, one precedence rule:
| Surface | Form |
|---|---|
| Site manifest | deshade: true on the site entry (srb/terrain/manifest.yaml) |
| Python | RealTerrain(..., deshade=True) |
| CLI | --deshade on srb asset download terrain / srb asset stack / srb asset doctor |
Precedence is manifest or request: a request-level deshade=True can
only turn deshading on; it can never disable a manifest-declared
deshade: true. The effective flag is folded into the v2 cache key
(any knob that changes baked pixels must be), so deshaded and non-deshaded
bakes of the same region/site coexist as distinct cache entries; the default
False keeps every pre-existing cache key byte-identical.
v1 sites cannot deshade. A v1 (single-source sources.dem) site has no
fusion engine in its bake path, so deshade=True there is not silently
inert — it raises (DeshadeUnsupportedError), and the CLI checks the same
manifest fact before a single byte is fetched rather than after the
whole DEM download.
Consent
Unchanged from Phase 2: a v2 catalog layer whose estimated fetch exceeds
1 GiB needs both the layer’s curated allow_large_download: true and
the operator’s SRB_TERRAIN_LARGE_OK=1 environment variable at fetch time
(check_layer_consent, an AND — env-var only, never an interactive prompt).
Region requests add no second consent mechanism; the same gate governs every
byte the region path moves. See
allow_large_download + SRB_TERRAIN_LARGE_OK
for the full rules and why srb asset stack’s printed “consent required”
line is only a descriptive preview.
Companions and the ephemeris sun
A curated real-terrain site can carry companion products — a sky dome,
a lighting rig (possibly ephemeris-baked; see
Terrain lighting), and rock sets. As of
Phase 4 (OI-1), the env path attaches them automatically:
BaseEnvCfg._add_scenery calls attach_companions() whenever the resolved
scenery is a RealTerrain with attach_companions_enabled=True (the
default; set it False to opt a task out and keep the pre-OI-1 scene).
Curated companion product ledger
Companion lifecycle is product-specific. Source presence and an Isaac-free pipeline contract do not establish publication or rendered-scene readiness.
| Product | Family and owner | Current disposition | Publication/runtime acceptance |
|---|---|---|---|
apollo17_pan | Sky dome; srb/terrain/hdri | Retain with condition. The curated manifest consumes it, but its catalog URL and hash are placeholders, so it is not publishable. | Supply a licensed stable URL and real hash; verify fetch, texture generation, and dome.usd; then capture an Isaac curated-site run attaching skydome without fallback. |
jezero_late_morning | Lighting rig; srb/terrain/lights | Retain. This is a procedural static preset consumed by Jezero, not a downloaded O3 product. Runtime acceptance remains deferred. | Verify a lights.usd bake and an Isaac Jezero run proving lighting attachment and arbitration without fallback. |
lunar_basalt_set | Rock set; srb/terrain/rocks | Retain with condition. Apollo sites consume it, but both catalog inputs use placeholder URLs and hashes, so it is not publishable. | Supply stable URLs and real hashes; verify downloads plus identity.json and rock.usd generations; then capture an Isaac run attaching rocks_00 and rocks_01. |
Region requests remain outside this ledger because they have no curated
manifest entry. SimForge/augment rocks are also excluded: they are synthetic
augmentation inputs, not these curated companion products.
Both a companion lighting product and an enabled env.ephemeris want to own
“what lights this scene”, so attachment is arbitrated
(srb.terrain.companion_scene.arbitrate_companion_scene) under a fixed
precedence. Every suppression warns once, naming both conflicting knobs:
| # | Condition | Outcome |
|---|---|---|
| 1 | env.ephemeris.enabled and drive_sunlight | The ephemeris sun owns lighting: the companion lighting product is suppressed (warn-once). If drive_skydome is also set, the companion sky is suppressed too. Companion rocks still attach. |
| 2 | Else, the companion set contains lighting | The companion owns the scene sun: scene.sunlight is set to None and the four static sunlight randomizers (randomize_sunlight_*) are skipped (warn-once when any were configured) — scene.lighting and scene.sunlight are mutually exclusive. A manifest lighting: "ephemeris" site under an env without env.ephemeris.enabled gets its baked ephemeris preset this way. |
| 3 | The companion set contains sky | The companion sky replaces the domain-default skydome and randomize_skydome_orientation is nulled (deterministic curated sky) — unless the env explicitly configured a non-default skydome string (e.g. env.skydome=high_res, which wins over the companion sky) or explicitly disabled the skydome (env.skydome=null/false — an operator disable also wins; the companion must not silently re-add a dome). |
Additional rules:
- Region terrains have no curated companions (there is no manifest entry
to declare them), so a region request attaches nothing — silently: absence
is not a conflict, nothing warns.
env.ephemerisis the way to get a real sun on a region patch; a manifestlighting: "ephemeris"is unreachable for region requests until a manifest entry exists. - All-or-nothing degrade. If building/attaching a site’s companions fails — placeholder catalog entries until the O3 data track lands real products, or an offline run — the whole companion set for that site is dropped with a one-time warning and the env builds with the pre-OI-1 scene. Per-product degrade (keep the sky when only the lighting product is broken) is a post-O3 candidate, not implemented.
Rendered-output change, deferred by data. The wiring above changes what a curated-site env renders compared to pre-OI-1 — a site’s curated sky/lighting now actually reaches the running scene. Today that change is latent: the shipped sites’ companion catalog entries are placeholder products (O3-blocked), so attachment degrades with the warn-once and envs render exactly as before. The moment real companion products land, curated-site envs will light and sky differently than they did pre-OI-1. Opt out per task with
attach_companions_enabled=False, or override the individual companions (sky=None,lighting=None) on the scenery.Observed against the first real moon v2 bake (2026-08). Real-Terrain-v2 Task 5 curated
srb/terrain/catalog/moon.yamlwith 12 real, sha256-pinned DEM/colour/mask source layers, and a real region bake (region_moon_-89.6600_+000.0000_2000m) was run against it end to end: the DEM (windowed COG read) and the sole covering colour product (lroc_wac_emp_643nm_south_100m, a single-band 643 nm reflectance mosaic — 1,394,200,920 bytes, sha256-verified) and mask (pgda_lpsr_85s_60m, 656,183 bytes) all fetched correctly over the real network. This confirms the paragraph above’s premise does not apply to region requests at all, regardless of data: per “Additional rules” above, a region request has no manifest entry and therefore attaches zero companions unconditionally — real colour landing in the per-body source catalog (moon.yaml/mars.yaml/earth.yaml) can never flip that, by construction. The companion track this note is actually about is the separate presentation catalogs —srb/terrain/hdri/catalog.yaml,srb/terrain/lights/catalog.yaml,srb/terrain/rocks/catalog.yaml— which back curated-sitesky=/rocks=(lighting=presets that aren’t"ephemeris"are procedural, not downloaded, so they are unaffected). Those still carry only placeholderfile:///tmp/...entries with dummy sha256 hashes; Task 5’s moon.yaml curation is a different data track and does not touch them. So this note stays open for curated sites, now for a more specific, verified reason: it needs its own O3-equivalent curation pass over the hdri/lights/rocks catalogs, not (only) the per-body source catalogs.Update (SPICE/Terrain remaining-work, Task 15, 2026-08): the bake above now completes end to end. At Task 6 time, real single-band colour data exposed a pre-existing ingest defect: a
(H, W, 1)array —srb.terrain.ingest.dem.load_dem_window_multiband’s documented shape for a genuinely 1-band “colour” product — was rejected bysrb.terrain.fusion.color._to_hwc3_float64, which only accepts(H, W)or(H, W, 3). Task 15 fixed this (D-C) at theapi.pyingest call site — replicating a genuinely single-band colour product to 3 channels before it reachesfusion/, which stays untouched (git diff -- srb/terrain/fusion/empty throughout) — alongside two sibling defects in the same windowed-read path found by the same investigation: D-A (no CRS reprojection between a source’s native transform and the destination grid) and D-B (non-square geographic pixels rejected outright). The same region (region_moon_-89.6600_+000.0000_2000m) now bakes end to end,area_fraction=1.0on every base layer, and three more real bakes (Mars Jezero, Earth polar, Earth non-polar) complete the same way. Region requests still attach zero companions regardless (unchanged structural fact from “Additional rules” above), so this still does not exercise companion-arbitration code or its degrade path — that observation stays open until the hdri/lights/ rocks catalogs get their own curation pass, independent of whether the per-body source catalogs or the fusion ingest path work.
Illumination sidecar (SPICE Phase 5)
A baked patch can carry a precomputed illumination sidecar
(illumination.npz, schema srb_illum/2) alongside its terrain.usd — a
separate, out-of-band artifact produced by srb ephemeris import, not a bake
input (it is not part of the CacheKey). RealTerrain.illumination_product_path exposes it (derived from the primary/index-0
patch only — see the per-env location variety limits above for what that
means under num_locations > 1), and srb/core/mdp/illumination.py’s
masked-spawn term and illumination_fraction_at reward helper consume it.
See Illumination Products for
the full schema, importer, and consumer reference.
Augmentation database (srb_augdb/1, Phase 5c)
A baked patch can carry a deterministic augmentation database —
versioned synthetic crater/rock placements derived from the patch’s own
fused truth, alongside terrain.usd. It is generated at bake time by
srb.terrain.augmentation.database.generate_augmentation_db and persisted
as a compressed sidecar (augmentation.npz) via save_augmentation_db.
Schema (srb_augdb/1, AUGMENT_DB_VERSION = 3). Two float32 arrays.
x_m/y_m are patch-local mesh-frame meters (x = grid east, y = grid
north, origin at the patch center — the same XY frame as mesh_frame {"center_xy": True}):
| Array | Shape | Columns |
|---|---|---|
craters | (Nc, 4) | x_m, y_m, radius_m, depth_m |
rocks | (Nr, 6) | x_m, y_m, z_m, scale, yaw_rad, asset_idx |
z_m (SPICE/Terrain remaining-work Task 10, db_version bumped 1→2) is
sampled bilinearly from the bake’s own DEM at generation time, 0.0 when no
DEM was available. z_m is on the mesh’s Z datum, not the DEM’s raw
datum (re-review finding C1, db_version bumped 2→3): it is the bilinear
DEM sample MINUS the same centre-pixel origin heightmap_to_mesh(..., z_origin="center") subtracts from the mesh — i.e. the same mesh_frame {"z_origin": "center"} the baked USD itself uses — so a rock’s (x_m, y_m, z_m) lands directly on the baked mesh surface. db_version == 2 sidecars
predate this fix and carry z_m on the DEM’s raw (often kilometres-off-zero
for real planetary DEMs) datum instead; there is no in-memory upgrade for
that case, only a rebake. load_augmentation_db_file auto-upgrades a
pre-existing v1 sidecar in memory (inserts a zero z_m column at index 2,
records meta["upgraded_from"] = 1) so old cache entries keep loading
without a rebake. Zero-density kinds are (0, 4)/(0, 6) arrays, never
None. A
meta JSON block records schema, db_version, seed, slug, body,
preset, per-kind counts (requested vs. kept, see the shortfall rule
below), provenance: "synthetic", and generated_from (patch_size_m,
gsd_m, max_slope_deg, and dem_stats — None when no DEM array was
available to seed the slope-exclusion proxy).
Generated from fused truth, not from nothing. bake_or_load passes the
in-memory fused DEM it already holds at bake time (dem.heights,
dem.gsd_m) into the generator; positions are otherwise uniform over the
patch, then rejection-resampled away from cells whose local slope-magnitude
proxy (np.gradient of the DEM) exceeds max_slope_deg (25° by default).
Densities and size ranges come from the body’s AugmentPreset
(rock_density_per_m2, rock_size_range_m, crater_density_per_m2,
crater_radius_range_m — the crater fields exist in every preset today, but
every shipped preset’s crater_density_per_m2 is 0.0, so shipped
databases carry rocks only, in (0, 4) crater arrays).
Determinism/version contract. The generator’s RNG stream is
np.random.default_rng([761, AUGMENT_DB_VERSION, seed, *sha256(slug)-ints])
— no hash() (PEP 456 randomizes it per process), no dict-iteration-order
dependence. The same (seed, db_version, slug) triple — plus the same
preset and DEM input — reproduces byte-identical craters/rocks arrays on
any machine, in any process, regardless of hash-randomization or unrelated
np.random global-state use elsewhere in the process. Changing the
stream (the salt, the RNG construction, the placement rules, the schema
layout) requires bumping AUGMENT_DB_VERSION — the version is folded
into the seed itself, so a bump silently re-derives every existing database
rather than colliding with it.
Shortfall on steep terrain. Slope-exclusion never “loops until
satisfied” (that would make the stream length, and thus determinism, depend
on the acceptance rate): the generator draws a fixed multiple
(_OVERSAMPLE = 4) of candidate positions up front and keeps the first N
that pass the slope test, in draw order. If fewer than N candidates survive
(e.g. a patch that is mostly steep), the database simply carries fewer
placements than the density implies — recorded honestly in
meta["counts"] (*_requested vs. *_kept), never silently retried to
make up the difference.
Bake wiring. Generated for both v1 and v2 bakes alike, but only when
the body’s effective augment preset has nonzero rock or crater density —
moon and mars presets do (rocks), earth’s EARTH_DEFAULT is all-zero, so
earth bakes carry no augmentation.npz and no meta.json block at all.
Generation runs inside _bake, writes into the tmp bake directory before
the same atomic promote loop that lands provenance.tif, and is registered
as meta.json["augmentation_db"] = {"file": "augmentation.npz", "schema": "srb_augdb/1", "db_version": 3, "counts": {...}}. A cache hit never
regenerates. Generation failure (including the DEM/gsd sanity guard
tripping on a malformed input) degrades with a logged warning — it never
fails the whole bake, since the sidecar is a bonus artifact, not a
requirement for a usable terrain. The sidecar is not part of
CacheKey — like the illumination sidecar, it is derived from an
already-baked patch and cannot influence the cache key that produced it.
Read API. srb.terrain.augmentation.database.load_augmentation_db(baked_dir)
returns an AugmentationDb | None — None when the meta block or the
sidecar file is absent; a file that is PRESENT but fails schema/shape/
finiteness validation raises ValueError (a corrupt sidecar is an error,
not a silent absence). RealTerrain.augmentation_db_path mirrors
illumination_product_path’s “sidecar next to the primary baked USD”
convention, including the same per-env location-variety limit: only the
PRIMARY (index-0) patch’s database is exposed when num_locations > 1.
Honesty note (D1, updated by Task 11). Phase 5c shipped generation, persistence, the read API, and a cross-process determinism proof. Bake-time crater stamping is now implemented, opt-in (
srb.terrain.augmentation.stamping.stamp_craters): setTerrainSpec.augment_stamp = True(also exposed asRealTerrain.augment_stamp) and the generated database’s craters are cut into the DEM itself — a parabolic bowl to the rim radiusR, then a raised-cosine rim collar decaying back to grade at1.4 R, rim height0.06 * depth(a documented profile, not a physical simulation) — inside_bake, right after the nodata-repair step and beforeheightmap_to_meshruns, so the deformation is baked into the mesh, materials, and collision alike, not a cosmetic overlay; rockz_mis re-sampled on the stamped surface so placements still sit on the ground.augment_stamp = Falseis the default and is inert everywhere: every shipped preset keepscrater_density_per_m2 = 0.0, so stamping only does anything once paired with a preset that sets it (lunar_cratered/mars_cratered— dataclass copies of the body defaults withcrater_density_per_m2 = 0.002andcrater_radius_range_m = (1.0, 8.0), or a future custom one). Selecting that preset (re-review finding I2) isRealTerrain.augment_preset = "lunar_cratered"(or"mars_cratered"), threaded verbatim intoTerrainSpec.augment_preset—_bakeresolvesspec.augment_preset or default_augment_preset_for_body(spec.body)for BOTH sidecar generation and stamping, so an explicit override actually reaches the bake instead of_bakesilently re-resolving the (alwayscrater_density_per_m2 = 0.0) body default underneath anaugment_stamp = Truethat then has nothing to stamp.augment_presetmust name a preset whose ownbodymatches the spec’sbody; an unknown name or a wrong-body one is rejected loudly, atRealTerrain(...)construction time, not lazily at bake time (re-review finding I4 — corrects an earlier version of this note that claimed the opposite):bake_or_loadcomputes aCacheKeyunconditionally before_bakeever runs, andCacheKey.from_specresolves the effective preset throughcompanion_scene.resolve_effective_augment_preset— the single helper shared by the cache key,_bake’s generation block, andcompanions()’s SimForge/database augment layer — which raisesKeyErrorfor an unknown name andValueErrorfor a known preset whosebodydisagrees, so all three consumers of this one field fail the same way, at the same time, naming the offending value and the presets available for that body. Unlike the sidecar itself, this knob does reachCacheKey:hash12()appendsaugment_stamp={db_version}:{density:g}:{radius_min:g}:{radius_max:g}(the resolved — override-or-body-default — preset’s own crater knobs) wheneveraugment_stampisTrueor an explicitaugment_presetwas set (the latter changes GENERATED craters/rocks even when stamping is off) — the same append-only “inert default” precedentdeshade=1/synthetic=1established, so every existing golden hash and every default (no override) bake’s cache key are unaffected. A stamping failure degrades the same way generation does — a logged warning, DEM left unstamped, never a broken bake. Rock placements are now wired into the scene, explicitly, viaaugment="database"(Task 12 — see below); this closes the augmentation clause’s “identical scatter across machines” half for rocks. Crater placements are consumed by stamping (the DEM deformation above) but are not separately spawned as scene prims — a crater has no standalone mesh to place, only a DEM cut.
Explicit rock scatter — augment="database" (Task 12)
RealTerrain.augment gains a third mode alongside "none" (default) and
"simforge" (legacy, unchanged): augment="database" places rocks itself,
at the exact transforms recorded in the bake-time augmentation database
sidecar, instead of delegating to SimForge’s own internal scatter.
- Why a third mode.
augment="simforge"hands SimForge a rock count and a seed viaSimforgeAssetCfg/spawn_simforge_assets— SimForge decides where each rock goes internally, with no per-instance transform API SRB can read back.augment="database"instead reads the(Nr, 6)rocksarray already generated bysrb.terrain.augmentation.database([x_m, y_m, z_m, scale, yaw_rad, asset_idx], patch-local mesh frame) and spawns one static prim per row at that row’s own position/scale/yaw — the same positions every time, on every machine, because the sidecar itself is already proven deterministic (see above). - Variant baking.
srb.terrain.augmentation.database_scatterbakes a small, fixed catalog of_ROCK_ASSET_SLOTS = 8rock variant USDs once (via SimForge’s own generator machinery — the sameasset.generator_type(...).generate_subprocess(...)call SimForge’s spawner uses internally, just without the scatter half), cached by SimForge’s own subprocess cache. Each database row picks a variant byasset_idx % len(variants), so_ROCK_ASSET_SLOTSneed not exactly equal the row count. - Placement.
rock_pose(row)maps a row to(pos, quat_xyzw)—pos = (x_m, y_m, z_m)unchanged, and a yaw-only rotation about z:(0, 0, sin(yaw/2), cos(yaw/2))in xyzw (w-last) order, matching Isaac Lab’sAssetBaseCfg.InitialStateCfg.rotconvention in SRB’s fork (identity(0, 0, 0, 1)) — verified against the fork’sisaaclab/assets/asset_base_cfg.pyandisaaclab/utils/math.pybefore writing a single orientation value, since this fork is xyzw, not mainline IsaacLab’s wxyz. - Cap semantics.
RealTerrain.augment_max_rocks: int = 256caps the number of rocks placed.select_rowstakes the first N rows in the database’s own draw order — never a random subsample — so which rocks get placed under a cap is itself deterministic and stable across repeated runs with the same sidecar. - Degrade, never crash. A missing sidecar (no
augmentation.npz/meta.jsonblock) or one with zero rocks logs a warning naming the baked directory and yields no rock companions at all — the terrain still spawns. - Earth has no rock generator.
simforge_ext._build_rock_generatornow raisesValueErrorfor any body other than"moon"/"mars"(a defense-in-depth guard, not a promise of earth rock data): every shipped earth preset shipsrock_density_per_m2 = 0.0, so the zero-density short-circuit inbuild_real_terrain_augment_cfg— and the empty-rocks degrade path above foraugment="database"— means this raise is never reached by a default-configured bake. "simforge"is unchanged. The legacy mode’s placement, seeding, and behavior are untouched by this task;augment="database"is purely additive, and the default (augment="none") is unaffected either way.
Synthetic super-resolution layer slot (Phase 5c)
The catalog schema’s radiometry enum includes a "synthetic" class,
reserved for a future super-resolution or model-generated DEM/color
product. The resolver treats it as a lowest-priority, opt-in slot:
- Skipped by default.
radiometry == "synthetic"layers never become candidates at all in_build_sorted_candidatesunless the request explicitly admits them — a catalog with one synthetic DEM layer and one real DEM layer resolves to the real one only, no warning needed (there is nothing wrong to warn about). - Strictly last when admitted. The resolver’s total sort key is now
(is_synthetic, priority_class, native_gsd_m, layer_id)— a synthetic layer sorts after every non-synthetic candidate regardless of its ownpriority_class/native_gsd_m. Even a syntheticpriority_class=0, native_gsd_m=0.5layer loses to a realpriority_class=9layer. This key is byte-equivalent to the pre-Phase-5c 3-tuple whenever no candidate is synthetic (which is every shipped catalog today), so default resolution is unaffected. - Declared-stack sites admit synthetic layers implicitly. A v2 site’s
manifest-declared
dem_stack/color_stackentries are matched bylayer_id, not filtered byradiometry— naming a synthetic layer in a manifest is explicit authorial intent, so it is never skipped the way an unrequested footprint-search candidate would be. The opt-in gate only matters for footprint-searched (resolve_stacks) and declared-site mask/quality/PSR-color candidates.
Opt-in surface. RegionRequest.allow_synthetic: bool = False and
TerrainSpec.allow_synthetic: bool = False (threaded through exactly like
deshade). The flag is deliberately not part of the cache-site slug
(a region’s identity string), but it is pixel-changing once a catalog
ships a synthetic layer, so CacheKey.hash12() appends "synthetic=1" to
the hashed material only when the flag is True — the same append-only
“inert default” precedent deshade=1 established. allow_synthetic=False
keeps hash12() byte-identical to every existing golden hash; the default
plan for every existing catalog is unaffected either way, since none ships
a synthetic layer.
A region-form TerrainSpec.allow_synthetic=True whose region sub-spec
disagrees (region.allow_synthetic=False) raises at spec-construction time
— region terrain specs read the effective flag from
RegionRequest.allow_synthetic, so a spec-level flag that the region does
not also carry would silently do nothing, which the boundary check refuses
to allow.
Scope. The slot is schema + resolver plumbing only (D7/D8). No shipped catalog carries a synthetic layer; no super-resolution model is integrated. This is unrelated to the augmentation database’s rock scatter (now wired explicitly via
augment="database", Task 12, above) — no synthetic-radiometry product feeds rock/crater placement today, and nothing here changes with that landing.
Current limits
- Catalog coverage is real but narrow. The shipped catalogs carry 19
(moon) / 2 (mars) / 2 (earth) real, sha256-pinned layers (see
Catalog status per body) — a region request
outside every curated footprint still degrades/errs as described above.
access: cog_remoteremains a compatibility label, but production first downloads and SHA-256-verifies the complete source through SRB’s bounded, redirect-validating provider. Rasterio windows that local payload. Direct GDAL/vsicurlnetwork access is refused until its transport can enforce a per-hop destination-host policy. First use therefore needs full-source space and explicit large-download consent where applicable; later use reuses the content-addressed source and patch caches. Each contributing layer recordsbytes_verified: trueinmeta.jsonfusion provenance. LDAM is pinned and production-baked, but remains a coarse 1064 nm product behind the 643 nm WAC_EMP base in their overlap; a spectral seam is possible. No shipped Moon DEM covers the 50S–60S annulus where LDAM is the sole colour source, so its isolated-catalog contribution proof does not create new public end-to-end terrain coverage there. TheNAC_POLE_SOUTH_CM_065/CM_135mosaics are no longer blocked — the one band-1 tile covering Connecting Ridge is pinned per epoch as areference_only: truecomparison reference (never bake input); the remaining tiles stay unpinned, a multi-GB-download operator decision per site. SfS A3CLR is no longer among them — it was pinned on 2026-08-02 for the two regions containing sites this repo already bakes (Connecting Ridge, Haworth); the other eleven A3CLR regions remain uncurated, not blocked. - Non-polar coverage exists but is thin. Non-polar resolution itself is no longer gated (Phase 5b, D12 lift — any latitude projects and resolves against the catalog), and the earth non-polar bake (3DEP, local stereographic) proves the path against real data; but only that one non-polar layer is curated today.
- Real Mars/Earth product curation is delivered, not exhaustive. The Phase 5 exit criterion’s real-data half is now demonstrated in-repo: one real Mars HiRISE bake (Jezero) and two real Earth bakes (Copernicus polar, 3DEP non-polar) complete end to end (SPICE/Terrain remaining-work Tasks 7/8/15). This is one curated site per body, not broad coverage — most Mars/Earth locations still have no catalog layer.
- A colour read carries no validity mask. A cross-CRS windowed read of a
source that declares no nodata and is not floating-point (integer imagery)
now marks out-of-footprint pixels with a warp alpha band, and the
single-band DEM reader turns that into
NaN— this subsystem’s nodata currency. The colour reader (read_planned_window_multiband, which returns(H, W, C)in the source’s native integer dtype) has nowhere to put it:Demhas no mask field, andNaNwould force thefloat32upcast the integer path exists to avoid. So a colour read returns whatever GDAL wrote in those pixels (0). Closing this needs a mask channel across the ingest↔fusion interface; it is deliberately not faked in-band. Seesrb/terrain/ingest/window.py::_vrt_fill_nodata_for. - No credentialed source access. The three Phase 5b providers
(
uahirise,copernicus_s3,usgs_3dep, above) only reach public, unauthenticated hosts/buckets — a product behind an API key or a private bucket cannot be curated today. - No UTM (or other non-zero-origin) canonical CRS entries. The
authority/parameter-matching scheme in
planetary_crs.pyassumes a canonical entry’sx_0/y_0/lon_0are all zero (see the module docstring); a UTM zone’s non-zero central meridian is structurally unrepresentable as a canonical entry the wayEPSG:3413already is not (reachable by authority id only). The per-region local stereographic CRS covers the practical need instead. - A hot region cache hit still pays the resolve cost — the v2 cache key needs the resolved plan’s stack hashes, so a catalog load + footprint resolve runs before the cache can even be checked, on every call.
srb asset stackregion previews and region downloads share the resolver but not a cache — the preview is recomputed each run (it is cheap and moves no bytes).- Companion degrade is all-or-nothing per site (above).
- The augmentation database is read-only by default — craters are cut
into the DEM only when the opt-in
TerrainSpec.augment_stampknob is set (lunar_cratered/mars_crateredpresets exist for exactly this; see the honesty note above), and it is still not consumed by SimForge’s scatter path; outside of stamping, the only proven consumption is the read API and the cross-process determinism test. No catalog ships a synthetic layer — the super-resolution slot’s opt-in gate, demotion, and cache-key append are exercised against a fixture catalog only, like the rest of the v2 resolver machinery. - Per-env location variety (
num_locations/locations) assigns variants to environments uniform-randomly with replacement — Isaac Lab’s defaultcloner_strategies.random, not round-robin — viatorch.randintwith no explicit generator, so the assignment is per-run nondeterministic unless the caller seeds torch’s global RNG; some environments can end up sharing the same patch and others may not draw a given patch at all. - Memory/VRAM scales with K. All K patch prototypes stay resident on the
stage (under
/World/template) regardless ofnum_envs— a K=8 request costs roughly 8x the terrain-asset memory of K=1, even in a 2-env run. - K competes for the clone-planner’s fixed combination budget. A
region’s
MultiAssetSpawnerCfgis an unshrinkable group inmulti_asset_compat.convert_scene_multi_variant_spawners— it multiplies the scene’sfixed_productrather than being capped itself — so a larger K leaves less of the fixedMAX_CLONE_PLAN_COMBINATIONS = 65536budget for other multi-variant spawners in the same scene, shrinking SimForge spawners’num_assetsbudgets (rocks, procedural scatter, etc.) to keep the total combination count under the cap. - K bakes run serially and eagerly at
RealTerrainconstruction time — the CLI andRealTerrain.model_post_initboth bake location 0, then 1, … K-1 in a single synchronous loop before the cfg is usable; there is no lazy or parallel bake path. Each location still pays the “cache hit still pays a resolve” cost noted above, K times over.