worldgen

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

master

raw · 10560 bytes

import unittest

import numpy as np

from mapgen import crust, elevation as EL, plates
from tests.helpers import make_ctx


def scenario(tect, land_fn, land_fraction, res=3, gravity=None, lock=None):
    ctx = make_ctx(res, tect=tect, cfg={"build": {"land_fraction": land_fraction}})
    g = ctx.grid
    z0 = np.zeros(g.n)
    ctx.data.update({"sk_land": land_fn(g).astype(float), "m_land_hint": z0, "sk_mountains": z0,
                     "m_mountain_hint": z0, "m_gravity_zones": z0 if gravity is None else gravity(g),
                     "m_lock": z0 if lock is None else lock(g)})
    ctx.data.update(plates.run(ctx))
    ctx.data.update(crust.run(ctx))
    return ctx


SUBDUCT = {"plate": [
    {"id": "c", "seed": [0.0, -30.0], "kind": "continental", "motion": [90.0, 3.0]},
    {"id": "o", "seed": [0.0, 10.0], "kind": "oceanic", "motion": [270.0, 5.0]}]}
COLLIDE = {"plate": [
    {"id": "a", "seed": [0.0, -30.0], "kind": "continental", "motion": [90.0, 4.0]},
    {"id": "b", "seed": [0.0, 30.0], "kind": "continental", "motion": [270.0, 4.0]}]}


class ElevationTest(unittest.TestCase):
    def test_sea_level_hits_target(self):
        rng = np.random.default_rng(1)
        z = rng.normal(size=5000) * 1000
        area = rng.uniform(1, 2, size=5000)
        zs = EL.solve_sea_level(z, area, 0.3)
        self.assertAlmostEqual(area[zs > 0].sum() / area.sum(), 0.3, delta=0.005)

    def test_subduction_trench_and_coastal_range(self):
        ctx = scenario(SUBDUCT, lambda g: (np.abs(g.lat) < 40) & (g.lon > -70) & (g.lon < 0), 0.12)
        out = EL.run(ctx)
        z, g = out["elevation_m"], ctx.grid
        band = np.abs(g.lat) < 30
        ocean_side = band & (ctx.data["plate"] == 1) & (out["d_sub_km"] < 300)
        cont_side = band & (ctx.data["plate"] == 0) & (out["d_over_km"] < 600)
        self.assertLess(z[ocean_side].min(), -7000)
        self.assertGreater(z[cont_side].max(), 2500)

    def test_collision_builds_high_range(self):
        ctx = scenario(COLLIDE, lambda g: (np.abs(g.lat) < 30) & (np.abs(g.lon) < 60), 0.15)
        out = EL.run(ctx)
        near = out["d_coll_km"] < 300
        self.assertGreater(out["elevation_m"][near].max(), 6000)
        self.assertLessEqual(out["elevation_m"].max(), 12000)

    def test_low_gravity_zone_raises_relief(self):
        land = lambda g: (np.abs(g.lat) < 30) & (np.abs(g.lon) < 60)
        zone = lambda g: np.where((np.abs(g.lat) < 20) & (g.lon > 10) & (g.lon < 50), -1.0, 0.0)
        base = EL.run(scenario(COLLIDE, land, 0.15))["elevation_m"]
        ctx = scenario(COLLIDE, land, 0.15, gravity=zone)
        low = EL.run(ctx)["elevation_m"]
        inside = zone(ctx.grid) < 0
        self.assertGreater(low[inside & (base > 0)].mean(), base[inside & (base > 0)].mean() * 1.5)

    def test_lock_forces_coast(self):
        land = lambda g: (np.abs(g.lat) < 30) & (np.abs(g.lon) < 60)
        lock = lambda g: np.ones(g.n)
        ctx = scenario(COLLIDE, land, 0.15, lock=lock)
        z = EL.run(ctx)["elevation_m"]
        self.assertTrue(np.all((z > 0) == (ctx.data["sk_land"] > 0.5)))


class SeaLevelRegressionTest(unittest.TestCase):
    LAND = staticmethod(lambda g: (g.lat > -25) & (g.lat < 45) & (np.abs(g.lon) < 62))   # ≈ 0.195 of the sphere

    def test_continents_not_lifted(self):
        ctx = scenario(COLLIDE, self.LAND, 0.19)
        out = EL.run(ctx)
        z, g = out["elevation_m"], ctx.grid
        self.assertLess(np.median(z[self.LAND(g) & (out["d_coll_km"] > 1500)]), 1500)
        self.assertLess(np.median(z[~ctx.data["continental"]]), -3000)

    def test_target_above_continental_area_errors(self):
        from mapgen.pipeline import StageError
        with self.assertRaisesRegex(StageError, "land_fraction"):
            EL.run(scenario(COLLIDE, self.LAND, 0.30))


class LockThresholdTest(unittest.TestCase):
    def test_near_neutral_lock_does_nothing(self):
        land = lambda g: (np.abs(g.lat) < 30) & (np.abs(g.lon) < 60)
        base = EL.run(scenario(COLLIDE, land, 0.15))["elevation_m"]
        tiny = EL.run(scenario(COLLIDE, land, 0.15, lock=lambda g: np.full(g.n, 1 / 255)))["elevation_m"]
        np.testing.assert_array_equal(base, tiny)


class MarginProfileTest(unittest.TestCase):
    @staticmethod
    def _profile(extra):
        from mapgen.graph import distance_to, ocean_mask
        land = lambda g: (np.abs(g.lat) < 30) & (np.abs(g.lon) < 60)
        g = make_ctx(4).grid
        frac = g.area_km2[land(g)].sum() / g.area_km2.sum()
        ctx = make_ctx(4, tect=COLLIDE, cfg={"build": {"land_fraction": round(frac - 0.003, 4)},
                                             "crust": {"edge_noise": 0.0}, "elevation": extra})
        z0 = np.zeros(g.n)
        ctx.data.update({"sk_land": land(g).astype(float), "m_land_hint": z0, "sk_mountains": z0,
                         "m_mountain_hint": z0, "m_gravity_zones": z0, "m_lock": z0})
        ctx.data.update(plates.run(ctx))
        ctx.data.update(crust.run(ctx))
        z = EL.run(ctx)["elevation_m"].astype(float)
        sea = ocean_mask(g, z, 5.0e6)
        d_coast_land = distance_to(g, sea)
        d_coast_sea = distance_to(g, ~sea)
        far = (~sea) & (d_coast_land > 1200) & (np.abs(g.lon) > 20)       # interior, away from the collision belt
        coastal = (~sea) & (d_coast_land < 250) & (np.abs(g.lon) > 20)
        shelf = sea & (d_coast_sea < 200)
        return np.median(z[coastal]), np.median(z[far]), np.median(z[shelf])

    def test_coastal_lowlands_and_shelves(self):
        step = self._profile({"coast_noise_m": 0.0, "margin_km": 1.0, "slope_km": 1.0})
        coastal, interior, shelf = self._profile({})
        self.assertLess(coastal, interior - 300)              # coastal plains sit well below the interior
        self.assertGreater(shelf, -800)                        # a shallow shelf fringes the coast
        self.assertGreaterEqual(step[0], step[1] - 300)        # the old step margin had no coastal lowlands


class ConnectedSeaLevelTest(unittest.TestCase):
    def test_interior_pits_do_not_count_as_sea(self):
        from mapgen.graph import ocean_mask
        from tests.helpers import small_grid
        g = small_grid(3)
        land = (np.abs(g.lat) < 40) & (np.abs(g.lon) < 70)
        z = np.where(land, 300.0 + 40.0 * (70.0 - np.abs(g.lon)), -4000.0)     # rises inland (continuous)
        pits = land & ((g.lat % 10) < 3) & ((g.lon % 10) < 3) & (np.abs(g.lon) < 55)
        z[pits] = -500.0                                                    # many deep interior pits
        target = 0.8 * g.area_km2[land].sum() / g.area_km2.sum()
        zs = EL.solve_sea_level_connected(g, z, target, 5.0e6)
        sea = ocean_mask(g, zs, 5.0e6)
        self.assertAlmostEqual(g.area_km2[~sea].sum() / g.area_km2.sum(), target, delta=0.01)


class RevisionElevationTest(unittest.TestCase):
    LAND = staticmethod(lambda g: np.abs(g.lon + 30) < 25)

    def test_without_patches_the_relief_is_solved_once(self):
        from unittest import mock
        ctx = scenario(SUBDUCT, self.LAND, 0.1)
        with mock.patch.object(EL, "_relief", wraps=EL._relief) as rel:
            EL.run(ctx)
        self.assertEqual(rel.call_count, 1)

    def test_a_land_patch_keeps_the_sea_level_of_the_world_without_it(self):
        from mapgen.crust import center_dist
        za = EL.run(scenario(SUBDUCT, self.LAND, 0.1))["elevation_m"]
        tect = {**SUBDUCT, "land_patch": [{"name": "fill", "center": [0.0, 120.0], "radius_km": 1500.0}]}
        b = scenario(tect, self.LAND, 0.1)
        zb = EL.run(b)["elevation_m"]
        g = b.grid
        far = (za > 0) & (center_dist(g, [0.0, 120.0]) > 5000.0)
        self.assertGreater(far.sum(), 20)
        np.testing.assert_allclose(zb[far], za[far], atol=1e-3)     # the other coasts don't move
        i = g.cell_index(0.0, 120.0)
        self.assertGreater(zb[i] - za[i], 1000.0, "the patch is continental ground now")

    def test_a_land_patch_is_land_though_the_sea_level_drowns_bare_crust(self):
        from mapgen.crust import center_dist
        tect = {**SUBDUCT, "land_patch": [{"name": "fill", "center": [0.0, 120.0], "radius_km": 1500.0}]}
        b = scenario(tect, self.LAND, 0.05)
        z = EL.run(b)["elevation_m"]
        inner = center_dist(b.grid, [0.0, 120.0]) < 1000.0
        self.assertGreater(float(np.mean(z[inner] > 0)), 0.9)

    def test_plateaus_get_their_surface_after_the_sea_level_solve(self):
        from mapgen import plateaus as PL
        from mapgen.crust import center_dist
        plats = [{"name": "p", "center": [0.0, 120.0], "area_km2": 3.0e6, "top_m": [1500.0, 3000.0]},
                 {"name": "q", "center": [40.0, 150.0], "area_km2": 1.0e6, "top_m": [1000.0, 1500.0],
                  "islands": True}]
        za = EL.run(scenario(SUBDUCT, self.LAND, 0.1))["elevation_m"]
        b = scenario({**SUBDUCT, "plateau": plats}, self.LAND, 0.1)
        zb = EL.run(b)["elevation_m"]
        g, ids = b.grid, b.data["plateau_id"]
        away = (center_dist(g, [0.0, 120.0]) > 2500.0) & (center_dist(g, [40.0, 150.0]) > 1500.0)
        np.testing.assert_allclose(zb[away], za[away], atol=1e-3)
        _, short = PL.semi_axes(plats[0])
        core = np.flatnonzero(ids == 0)
        core = core[(1.0 - PL.rho(g.xyz[core], plats[0], b.seed, g.radius_km)) * short > PL.MARGIN_KM]
        self.assertGreater(len(core), 5)
        self.assertTrue(1500.0 - PL.RELIEF_M <= float(np.median(-zb[core])) <= 3000.0 + PL.RELIEF_M)
        self.assertLessEqual(zb[ids == 0].max(), PL.HIDDEN_MAX_M)
        near_q = center_dist(g, [40.0, 150.0]) < 1500.0
        self.assertGreater(zb[near_q].max(), 0.0, "the island plateau breaks the surface")

    def test_land_added_marks_what_the_patch_makes_land(self):
        from mapgen.crust import center_dist
        from mapgen.graph import ocean_mask
        a = EL.run(scenario(SUBDUCT, self.LAND, 0.05))
        tect = {**SUBDUCT, "land_patch": [{"name": "fill", "center": [0.0, 120.0], "radius_km": 1500.0}]}
        b = scenario(tect, self.LAND, 0.05)
        ob = EL.run(b)
        g = b.grid
        land_a = ~ocean_mask(g, a["elevation_m"])
        land_b = ~ocean_mask(g, ob["elevation_m"])
        added = ob["land_added"]
        self.assertGreater(float(added[center_dist(g, [0.0, 120.0]) < 1000.0].mean()), 0.9)
        self.assertFalse((added & land_a).any(), "land without the patch is not added land")
        self.assertFalse((added & ~land_b).any(), "added land is land")
        self.assertFalse(a["land_added"].any(), "no patch, no plateau: nothing added")