diff options
| author | godosa <godosa@godosa.eu> | 2026-10-06 23:52:03 +0200 |
|---|---|---|
| committer | godosa <godosa@godosa.eu> | 2026-10-06 23:52:03 +0200 |
| commit | 346b1c5195bffc71ceaa9262453e3c189656400b (patch) | |
| tree | 01ac0d31e2724cd6abcc689a5a228e2cbea2f6cf /tests/test_elevation.py | |
| download | worldgen-346b1c5195bffc71ceaa9262453e3c189656400b.tar.gz worldgen-346b1c5195bffc71ceaa9262453e3c189656400b.zip | |
worldgen: initial public history
Diffstat (limited to 'tests/test_elevation.py')
| -rw-r--r-- | tests/test_elevation.py | 207 |
1 files changed, 207 insertions, 0 deletions
diff --git a/tests/test_elevation.py b/tests/test_elevation.py new file mode 100644 index 0000000..3a96460 --- /dev/null +++ b/tests/test_elevation.py @@ -0,0 +1,207 @@ +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") |
