raw · 10560 bytes
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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 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") |