"""Stage `crust`: continental vs oceanic crust, age classes, oceanic age."""
from __future__ import annotations
import numpy as np
from .config import params
from . import plateaus as PL
from .graph import distance_to, smooth_km
from .noise import fbm, name_seed, ridged
from .plates import CONV, DIV
from .sphere import azimuth_deg, gc_dist_km, latlon_to_xyz
OCEANIC, CRATON, PRE_OROGEN, POST_OROGEN, RIFT, LIP, SCAR, VOLCANO = range(8)
AGE_NAMES = ["oceanic", "craton (3g era)", "pre-Lightening orogen", "post-Lightening orogen",
"rift", "Lightening basalt province", "collapse scar", "overshoot volcano"]
DEFAULTS = {"continental_threshold": 0.25, "shelf_km": 450.0, "edge_noise": 0.5, "edge_noise_freq": 4.0, "edge_noise_gain": 0.65, "orogen_width_km": 700.0, "rift_width_km": 200.0,
"pre_orogen_threshold": 0.72, "half_spreading_km_myr": 30.0, "max_ocean_age_myr": 200.0,
"scar_inner": 0.4, "scar_outer": 1.6, "scar_halfwidth_deg": 25.0}
def center_dist(g, center):
return gc_dist_km(g.xyz, latlon_to_xyz(*center), g.radius_km)
def lip_profile(g, lip, seed):
r = lip["radius_km"] * (1.0 + 0.25 * fbm(g.xyz, name_seed(seed, lip["name"]), 3, 6.0))
x = center_dist(g, lip["center"]) / r
return np.clip((1.0 - x) / 0.25, 0.0, 1.0)
def _scar_azimuths(v, seed):
if "scar_azimuths" in v:
return list(v["scar_azimuths"])
rng = np.random.default_rng(name_seed(seed, v["name"]))
return list(rng.uniform(0, 360, size=2))
def patch_field(g, patch: dict, seed: int):
"""[[land_patch]]: land hint (strength) over a disc whose radius varies ± 25 % · edge_noise (fractal edge)."""
d = center_dist(g, patch["center"])
r = float(patch["radius_km"])
out = np.zeros(g.n)
near = d < 1.3 * r
if near.any():
w = np.clip(2.0 * fbm(g.xyz[near], name_seed(seed, patch["name"]), 6, 12.0), -1.0, 1.0)
out[near] = patch.get("strength", 1.0) * (d[near] < r * (1.0 + 0.25 * patch.get("edge_noise", 0.0) * w))
return out
def run(ctx) -> dict:
g = ctx.grid
P = params(ctx.cfg, "crust", DEFAULTS)
land, land_hint, btype = ctx.need("sk_land", "m_land_hint", "bnd_type")
rough = (fbm(g.xyz, ctx.seed + 23, 7, P["edge_noise_freq"], gain=P["edge_noise_gain"])
if P["edge_noise"] > 0 else None)
def continental_of(raw):
field = smooth_km(g, raw, P["shelf_km"])
if rough is not None: # fractal margins: bays, peninsulas, offshore continental fragments
f = np.clip(field, 0.0, 1.0)
near = 4.0 * f * (1.0 - f) # strongest at the margin; none deep inland / far offshore
field = field + P["edge_noise"] * near * rough / max(float(rough.std()), 1e-12)
else:
field = np.maximum(field, (raw > 0.5).astype(float))
cont = field > P["continental_threshold"]
for m in ctx.tect.get("microcontinent", []):
cont |= center_dist(g, m["center"]) < m["radius_km"]
return cont
raw = np.clip(land + 0.5 * land_hint, 0.0, 1.0)
continental_base = continental_of(raw) # without land patches: its sea level is the world's
patches = sum((patch_field(g, p, ctx.seed) for p in ctx.tect.get("land_patch", [])), np.zeros(g.n))
continental = continental_of(np.clip(raw + patches, 0.0, 1.0)) if patches.any() else continental_base.copy()
plateau_id = PL.cell_ids(g.xyz, ctx.tect.get("plateau", []), ctx.seed, g.radius_km)
continental |= plateau_id >= 0 # sunken plateaus: continental crust that never rose
d_conv = distance_to(g, btype == CONV)
d_div = distance_to(g, btype == DIV)
age = np.full(g.n, CRATON, np.int8)
age[continental & (ridged(g.xyz, ctx.seed + 21, 4, 4.0) > P["pre_orogen_threshold"])] = PRE_OROGEN
age[continental & (d_div < P["rift_width_km"])] = RIFT
age[continental & (d_conv < P["orogen_width_km"])] = POST_OROGEN
age[~continental] = OCEANIC
for lip in ctx.tect.get("lip", []):
age[lip_profile(g, lip, ctx.seed) > 0] = LIP
for v in ctx.tect.get("volcano", []):
d = center_dist(g, v["center"])
r = v["radius_km"]
age[d < r] = VOLCANO
az = azimuth_deg(latlon_to_xyz(*v["center"]), g.xyz)
for a in _scar_azimuths(v, ctx.seed):
diff = np.abs((az - a + 180.0) % 360.0 - 180.0)
age[(diff < P["scar_halfwidth_deg"]) & (d > P["scar_inner"] * r) & (d < P["scar_outer"] * r)] = SCAR
oceanic_age = np.minimum(d_div / P["half_spreading_km_myr"], P["max_ocean_age_myr"])
ocean_age = np.where(continental & (plateau_id < 0), 0.0, oceanic_age) # plateaus: the floor around them
return {"continental": continental, "age_class": age, "ocean_age_myr": ocean_age.astype(np.float32),
"d_conv_km": d_conv, "d_div_km": d_div, "continental_base": continental_base, "plateau_id": plateau_id,
"land_patch": patches.astype(np.float32)}