diff options
Diffstat (limited to 'mapgen/environment.py')
| -rw-r--r-- | mapgen/environment.py | 158 |
1 files changed, 158 insertions, 0 deletions
diff --git a/mapgen/environment.py b/mapgen/environment.py new file mode 100644 index 0000000..fbd3c8e --- /dev/null +++ b/mapgen/environment.py @@ -0,0 +1,158 @@ +"""Stage `environment`: Holdridge life zone + seasonality tag + lithology + landform + ground.""" +from __future__ import annotations + +import numpy as np + +from .config import params +from .crust import CRATON, LIP, POST_OROGEN, PRE_OROGEN, RIFT, SCAR, VOLCANO +from .graph import nbr_max, nbr_min, steepest_receivers +from . import minerals as MN +from .noise import fbm + +REGIONS = ["polar", "subpolar", "boreal", "cool temperate", "warm temperate", "subtropical", "tropical"] +ZONES = { + "polar": ["desert"], + "subpolar": ["dry tundra", "moist tundra", "wet tundra", "rain tundra"], + "boreal": ["desert", "dry scrub", "moist forest", "wet forest", "rain forest"], + "cool temperate": ["desert", "desert scrub", "steppe", "moist forest", "wet forest", "rain forest"], + "warm temperate": ["desert", "desert scrub", "thorn steppe", "dry forest", "moist forest", "wet forest", + "rain forest"], + "subtropical": ["desert", "desert scrub", "thorn woodland", "dry forest", "moist forest", "wet forest", + "rain forest"], + "tropical": ["desert", "desert scrub", "thorn woodland", "very dry forest", "dry forest", "moist forest", + "wet forest", "rain forest"], +} +THRESH = { # annual precipitation (mm) upper bounds between successive zones + "polar": [], "subpolar": [125, 250, 500], "boreal": [125, 250, 500, 1000], + "cool temperate": [125, 250, 500, 1000, 2000], "warm temperate": [125, 250, 500, 1000, 2000, 4000], + "subtropical": [125, 250, 500, 1000, 2000, 4000], "tropical": [125, 250, 500, 1000, 2000, 4000, 8000], +} +HOLDRIDGE_NAMES = [f"{r} {z}" for r in REGIONS for z in ZONES[r]] + +SEAS_F, SEAS_S, SEAS_W, SEAS_M = 0, 1, 2, 3 +SEASONALITY_NAMES = ["even rainfall", "dry summer", "dry winter / summer monsoon", "tropical monsoon"] + +LI_OCEAN, LI_GRANITE, LI_LIMESTONE, LI_BASALT, LI_ANDESITE, LI_SANDSTONE, LI_METAMORPHIC = range(7) +LITHOLOGY_NAMES = ["oceanic basalt", "granite/gneiss", "limestone", "basalt", "andesite", "sandstone/shale", + "metamorphic"] + +(LF_OCEAN, LF_PLAIN, LF_HILLS, LF_MOUNTAINS, LF_PLATEAU, LF_RIFT, LF_ESCARPMENT, LF_ARC, LF_MASSIF, + LF_BASALT_PLATEAU, LF_DUNES, LF_BADLANDS) = range(12) +LANDFORM_NAMES = ["ocean", "plain", "hills", "mountains", "plateau", "rift valley", "escarpment", + "volcanic arc", "volcanic massif", "basalt plateau", "dunes", "badlands"] + +(GR_NONE, GR_WETLAND, GR_BOG, GR_FLOODPLAIN, GR_DELTA, GR_MANGROVE, GR_SALT_FLAT, GR_PERMAFROST, + GR_KARST) = range(9) +GROUND_NAMES = ["—", "wetland", "bog", "floodplain", "delta", "mangrove", "salt flat", "permafrost", "karst"] + +LEGENDS = {"deposits": MN.LEGEND, "holdridge": HOLDRIDGE_NAMES, "seasonality": SEASONALITY_NAMES, "landform": LANDFORM_NAMES, + "lithology": LITHOLOGY_NAMES, "ground": GROUND_NAMES} + +DEFAULTS = {"relief_km": 100.0, "coastal_km": 60.0, "dry_ratio_s": 3.0, "dry_ratio_w": 4.0, "monsoon_min_mm": 1500.0, "flat_m_per_km": 1.0, + "wet_min_mm": 700.0, "karst_min_mm": 800.0, "small_river_km3_yr": 5.0, "life": True} + + +def holdridge(bio, p, tmin): + region = np.select([bio < 1.5, bio < 3, bio < 6, bio < 12, (bio < 24) & (tmin < 0), bio < 24], + [0, 1, 2, 3, 4, 5], default=6).astype(np.int8) + zone = np.zeros(len(bio), np.int16) + offset = 0 + for ri, r in enumerate(REGIONS): + sel = region == ri + zone[sel] = offset + np.searchsorted(THRESH[r], p[sel], side="right") + offset += len(ZONES[r]) + return zone, region + + +def seasonality(p_jun, p_dec, lat, tmin, p_ann, P): + summer = np.where(lat >= 0, p_jun, p_dec) + winter = np.where(lat >= 0, p_dec, p_jun) + tag = np.full(len(lat), SEAS_F, np.int8) + tag[summer < winter / P["dry_ratio_s"]] = SEAS_S + w = winter < summer / P["dry_ratio_w"] + tag[w] = SEAS_W + tag[w & (tmin >= 18.0) & (p_ann >= P["monsoon_min_mm"])] = SEAS_M + return tag + + +def lithology(g, z, continental, age, d_over, seed): + nz = fbm(g.xyz, seed + 51, 4, 5.0) + lit = np.full(g.n, LI_GRANITE, np.int8) + lit[continental & (z < 600) & (nz > 0.1)] = LI_SANDSTONE + lit[continental & (z < 300) & (nz < -0.1)] = LI_LIMESTONE + lit[np.isin(age, [POST_OROGEN, PRE_OROGEN]) & (z > 2000)] = LI_METAMORPHIC + lit[~continental] = LI_OCEAN + lit[(d_over > 100) & (d_over < 320)] = LI_ANDESITE + lit[np.isin(age, [LIP, VOLCANO, SCAR])] = LI_BASALT + return lit + + +def landform(g, z, age, lit, d_over, p_ann, ocean=None, relief_km=100.0): + hi, lo = np.asarray(z, dtype=np.float64), np.asarray(z, dtype=np.float64) + for _ in range(max(1, int(round(relief_km / g.spacing_km)))): # window ≈ relief_km at any resolution + hi, lo = nbr_max(g, hi), nbr_min(g, lo) + relief = hi - lo + lf = np.full(g.n, LF_PLAIN, np.int8) + lf[relief >= 300] = LF_HILLS + lf[(z > 1000) & (relief < 500)] = LF_PLATEAU + lf[(relief >= 1500) | (z > 2500)] = LF_MOUNTAINS + lf[(p_ann < 250) & (relief < 300) & (lit == LI_SANDSTONE)] = LF_DUNES + lf[(p_ann >= 250) & (p_ann < 500) & (relief >= 200) & (relief < 800) & (lit == LI_SANDSTONE)] = LF_BADLANDS + lf[age == RIFT] = LF_RIFT + lf[age == LIP] = LF_BASALT_PLATEAU + ocean = z <= 0 if ocean is None else ocean + lf[(d_over > 100) & (d_over < 320) & ~ocean] = LF_ARC + lf[age == VOLCANO] = LF_MASSIF + lf[age == SCAR] = LF_ESCARPMENT + lf[ocean] = LF_OCEAN + return lf, relief + + +def ground(g, z, lit, t_mean, t_min, p_ann, dist_ocean, river, strahler, discharge, lake, salt_flat, P, ocean=None): + land = ~(z <= 0 if ocean is None else ocean) + _, slope, _ = steepest_receivers(g, z) + flat = slope < P["flat_m_per_km"] + coastal = land & (dist_ocean < P["coastal_km"]) + wet = land & flat & ~lake & (p_ann > P["wet_min_mm"]) & (discharge > P["small_river_km3_yr"]) + gr = np.zeros(g.n, np.int8) + gr[land & (t_mean < -2.0)] = GR_PERMAFROST + gr[land & (lit == LI_LIMESTONE) & (p_ann > P["karst_min_mm"])] = GR_KARST + gr[wet & (t_mean < 5.0)] = GR_BOG + gr[wet & (t_mean >= 5.0)] = GR_WETLAND + gr[river & (strahler >= 3) & flat] = GR_FLOODPLAIN + gr[coastal & flat & (t_min >= 20.0) & (p_ann > 1000.0)] = GR_MANGROVE + gr[river & (strahler >= 4) & coastal] = GR_DELTA + gr[salt_flat] = GR_SALT_FLAT + if not P["life"]: + gr[np.isin(gr, [GR_WETLAND, GR_BOG, GR_MANGROVE])] = GR_NONE + return gr + + +def run(ctx) -> dict: + g = ctx.grid + P = params(ctx.cfg, "environment", DEFAULTS) + (z, cont, age, d_over, t_mean, t_min, p_ann, p_jun, p_dec, bio, dist_ocean, river, strahler, q, lake, + salt) = ctx.need("elevation_eroded_m", "continental", "age_class", "d_over_km", "T_mean", "T_min", "P_ann", + "P_jun", "P_dec", "biotemp", "dist_ocean_km", "river", "strahler", "discharge_km3_yr", + "lake", "salt_flat") + z = z.astype(np.float64) + bed = z + if "lake_level_m" in ctx.data: # the ground's shape: lakes at their water, not their beds + lev = np.asarray(ctx.data["lake_level_m"], dtype=np.float64) + z = np.where(np.isfinite(lev), np.maximum(z, lev), z) + ocean = np.asarray(ctx.data["ocean"]) if "ocean" in ctx.data else z <= 0 + zone, region = holdridge(bio, p_ann, t_min) + lit = lithology(g, z, cont, age, d_over, ctx.seed) + lf, relief = landform(g, z, age, lit, d_over, p_ann, ocean, P["relief_km"]) + gr = ground(g, z, lit, t_mean, t_min, p_ann, dist_ocean, river, strahler, q, lake, salt, P, ocean) + coal = (lit == LI_SANDSTONE) & (p_ann > 600) & ~ocean & bool(P["life"]) + get = lambda k, v: np.asarray(ctx.data[k]) if k in ctx.data else np.full(g.n, v) + deposits, main = MN.place(g, {"land": ~ocean & ~lake, "z": bed, "lit": lit, "age": age, "lf": lf, "relief": relief, + "t_mean": t_mean, "p_ann": p_ann, "d_coll": get("d_coll_km", np.inf), + "salt": salt, "endo": get("endorheic", False), "dist_ocean": dist_ocean, + "coal": coal}, ctx.seed) + iron = np.uint32(MN.BIT["bog_iron"] | MN.BIT["ironstone"] | MN.BIT["iron_high"]) + return {"deposits": deposits, "deposit_main": main,"holdridge": zone, "hold_region": region, + "seasonality": seasonality(p_jun, p_dec, g.lat, t_min, p_ann, P), + "landform": lf, "relief_m": relief, "lithology": lit, "ground": gr, + "coal_potential": coal, "iron_potential": (deposits & iron) > 0} |
