worldgen

git clone https://git.godosa.eu/worldgen

master

raw · 8625 bytes

"""Mineral deposits: where each ore, fuel and industrial mineral can be mined, by
geological rules, placed at an Earth-like share of the land. Everything a society needs to reach steam, rifled guns
and aluminium airships is somewhere; the good stuff (high-grade iron ore and the alloy metals) lies mostly deep in
mountains. Each deposit is a bit of `deposits` (uint32); `deposit_main` names the rarest one in a cell (a map layer).

A deposit's cells: the land cells (not sea, not under lakes) where its rule allows it, scored by the rule × a district
noise of its own (deposits cluster into mining districts). Half the `share` is taken province by province (≈ 1500 km
cells, each its share of its own land, best-scored first): every region has its local bog iron, coal pits and small
mines where its rocks allow. The rest goes to the best-scored land world-wide (the great districts)."""
from __future__ import annotations

import numpy as np

from scipy.spatial import cKDTree

from .noise import fbm, name_seed

# name, label, share of land (fraction). Shares are rough Earth-like extents of workable districts, not grades.
DEPOSITS = [
    ("bog_iron", "bog / laterite iron (low grade)", 0.030),
    ("ironstone", "ironstone (sedimentary iron)", 0.025),
    ("iron_high", "high-grade iron (magnetite, banded)", 0.006),
    ("coal", "coal (incl. coking)", 0.050),
    ("copper", "copper", 0.012),
    ("molybdenum", "molybdenum", 0.002),
    ("tin", "tin", 0.003),
    ("tungsten", "tungsten", 0.002),
    ("lead_zinc", "lead & zinc", 0.010),
    ("silver", "silver", 0.005),
    ("gold", "gold", 0.008),
    ("manganese", "manganese", 0.004),
    ("chromium", "chromium", 0.0015),
    ("nickel", "nickel", 0.003),
    ("cobalt", "cobalt", 0.001),
    ("vanadium", "vanadium & titanium", 0.002),
    ("platinum", "platinum metals", 0.0005),
    ("mercury", "mercury (cinnabar)", 0.0012),
    ("sulfur", "sulfur", 0.004),
    ("saltpetre", "saltpetre (nitrates)", 0.004),
    ("bauxite", "bauxite (aluminium)", 0.010),
    ("fluorite", "fluorite / cryolite (flux)", 0.0025),
    ("rock_salt", "rock salt & evaporites", 0.018),
    ("potash", "potash", 0.004),
    ("phosphate", "phosphate", 0.005),
    ("oil_gas", "oil & gas", 0.025),
    ("helium", "helium (with gas)", 0.0025),
    ("diamond", "diamonds (kimberlite)", 0.0004),
    ("magnesite", "magnesite / dolomite (magnesium)", 0.003),
    ("kaolin", "kaolin / fire clay", 0.010),
    ("glass_sand", "glass sand (quartz)", 0.015),
    ("graphite", "graphite", 0.002),
]
NAMES = [d[0] for d in DEPOSITS]
BIT = {n: 1 << i for i, n in enumerate(NAMES)}
LEGEND = ["—"] + [d[1] for d in DEPOSITS]
# the good stuff: mostly in deep mountains
DEEP = ("iron_high", "tungsten", "molybdenum", "chromium", "cobalt", "vanadium", "platinum", "manganese", "tin")


PROVINCE_KM = 1500.0   # spacing of the provinces that each take their local share
LOCAL = 0.5            # part of a deposit's share taken province by province
LOCAL_MIN = 0.25       # ... only on ground at least this good relative to the world's marginal district


def provinces(g, seed):
    """Province index of each cell: the nearest of random centres ≈ PROVINCE_KM apart."""
    n = max(1, int(round(4 * np.pi * g.radius_km ** 2 / PROVINCE_KM ** 2)))
    c = np.random.default_rng(name_seed(seed, "deposit-provinces")).normal(size=(n, 3))
    c /= np.linalg.norm(c, axis=1)[:, None]
    return cKDTree(c).query(g.xyz / np.linalg.norm(g.xyz, axis=1)[:, None])[1]


def _rank_select(score, share, land, prov):
    """`share` × land cells where score > 0: LOCAL of it the best of each province (its share of its own land, on
    ground ≥ LOCAL_MIN × the world's k-th best score), the rest the best world-wide."""
    ok = np.flatnonzero((score > 0) & land)
    k = min(len(ok), int(round(share * land.sum())))
    out = np.zeros(len(score), bool)
    if k == 0:
        return out
    cut = LOCAL_MIN * np.partition(score[ok], len(ok) - k)[len(ok) - k]     # the k-th best world-wide
    ok_l = ok[score[ok] >= cut]
    quota = np.floor(LOCAL * share * np.bincount(prov[land], minlength=prov.max() + 1) + 0.5).astype(np.int64)
    o = ok_l[np.lexsort((-score[ok_l], prov[ok_l]))]           # by province, best first
    p = prov[o]
    first = np.r_[0, np.flatnonzero(p[1:] != p[:-1]) + 1]
    rank = np.arange(len(o)) - np.repeat(first, np.diff(np.r_[first, len(o)]))
    local = o[rank < quota[p]]
    out[local[np.argsort(-score[local], kind="stable")[:k]]] = True
    rest = ok[~out[ok]]
    m = k - int(out.sum())
    if m > 0:
        out[rest[np.argpartition(-score[rest], m - 1)[:m]]] = True
    return out


def rules(f):
    """Each deposit's suitability (≥ 0, 0 = impossible) from the geology/climate fields in f."""
    from .crust import CRATON, LIP, POST_OROGEN, PRE_OROGEN, RIFT, VOLCANO
    from .environment import (LF_ARC, LF_MASSIF, LF_MOUNTAINS, LI_ANDESITE, LI_BASALT, LI_GRANITE, LI_LIMESTONE,
                              LI_METAMORPHIC, LI_SANDSTONE, LF_DUNES, LF_PLAIN)
    z, lit, age, lf, rel = f["z"], f["lit"], f["age"], f["lf"], f["relief"]
    t, p = f["t_mean"], f["p_ann"]
    b = lambda m: np.asarray(m, dtype=np.float64)
    deep = np.clip((z - 800.0) / 2000.0, 0, 1) * np.clip(rel / 1500.0, 0.3, 1.0)   # deep-mountain mining ground
    deep = np.maximum(deep, 0.6 * b(np.isin(lf, [LF_MOUNTAINS, LF_MASSIF])) * np.clip(z / 2500.0, 0, 1))
    oro = b(np.isin(age, [PRE_OROGEN, POST_OROGEN]))
    craton = b(age == CRATON)
    rift = b(age == RIFT)
    arc = b((lit == LI_ANDESITE) | (lf == LF_ARC))
    volc = b(age == VOLCANO) + arc
    gran = b(lit == LI_GRANITE)
    meta = b(lit == LI_METAMORPHIC)
    lime = b(lit == LI_LIMESTONE)
    sand = b(lit == LI_SANDSTONE)
    basalt = b((lit == LI_BASALT) | (age == LIP))
    sed = lime + sand
    low = b(z < 800)
    hot, wet = b(t > 20), b(p > 1200)
    arid = b(p < 250) * b(t > 10)
    suture = b(f["d_coll"] < 400) * (oro + meta)
    return {
        "bog_iron": b(p > 700) * low * (b(lf == LF_PLAIN) + 0.5) + hot * wet * low,
        "ironstone": sed * b(z < 1200),
        "iron_high": deep * (1.0 + craton + meta + oro),
        "coal": b(f["coal"]),
        "copper": arc * (1 + deep) + 0.4 * sand * rift + 0.3 * basalt,
        "molybdenum": arc * deep,
        "tin": (gran + meta) * oro * (0.3 + deep),
        "tungsten": (gran + meta) * (oro + 0.3) * deep,
        "lead_zinc": lime + 0.3 * sand * (1 + rift),
        "silver": arc + 0.3 * lime + 0.3 * deep * oro,
        "gold": (oro + craton * 0.7 + meta) * (0.3 + deep) + 0.3 * arc,
        "manganese": (sed + craton) * (0.2 + deep),
        "chromium": suture * deep + 0.3 * craton * basalt * deep,
        "nickel": basalt * hot * wet + (craton + suture) * deep,
        "cobalt": (basalt * hot * wet + craton * deep + arc * 0.3) * deep,
        "vanadium": (basalt + craton) * deep,
        "platinum": craton * (basalt + meta + 0.3) * deep,
        "mercury": volc * (0.5 + 0.5 * b(f["t_mean"] > -30)),
        "sulfur": volc + 0.5 * b(f["salt"]) + 0.3 * sed * arid,
        "saltpetre": arid * b(lf != LF_MOUNTAINS) + 0.3 * lime * b(p > 800),
        "bauxite": hot * wet * (gran + basalt + 0.3) * b(rel < 800) + 0.4 * lime * b(t > 14) * b(p > 600),
        "fluorite": (gran + lime * 0.4) * (rift + oro + 0.2),
        "rock_salt": b(f["salt"]) + 0.6 * b(f["endo"]) + 0.3 * sed * b(p < 500),
        "potash": b(f["salt"]) + 0.3 * b(f["endo"]) + 0.2 * sed * b(p < 400),
        "phosphate": sed * low * b(f["dist_ocean"] < 400) + 0.3 * lime,
        "oil_gas": sed * low * (1 + b(f["d_coll"] < 1200) + rift),
        "helium": sed * low * (craton + gran),
        "diamond": craton * (1 + deep),
        "magnesite": (lime + meta * 0.5) * (0.3 + deep) + 0.3 * b(f["salt"]),
        "kaolin": (gran + sand * 0.4) * b(p > 900) * b(t > 8),
        "glass_sand": sand * (1 + b(lf == LF_DUNES) + b(f["dist_ocean"] < 150)),
        "graphite": meta * (0.3 + deep),
    }


def place(g, f, seed):
    """(deposits uint32 bitmask, deposit_main uint8 legend index) over the grid."""
    land = np.asarray(f["land"], bool)
    R = rules(f)
    prov = provinces(g, seed)
    bits = np.zeros(g.n, np.uint32)
    main = np.zeros(g.n, np.uint8)
    best = np.full(g.n, np.inf)
    for i, (name, _, share) in enumerate(DEPOSITS):
        nz = 0.5 + 0.5 * fbm(g.xyz, name_seed(seed, "deposit:" + name), 3, 6.0)       # mining districts
        sel = _rank_select(R[name] * (0.25 + nz) ** 2, share, land, prov)
        bits[sel] |= np.uint32(1 << i)
        rarer = sel & (share < best)
        main[rarer], best[rarer] = i + 1, share
    return bits, main