aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/mapgen/environment.py
blob: fbd3c8e67426de7e12666e2bc45a6a128076a40e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
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}