aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/tests/test_hydrology.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_hydrology.py')
-rw-r--r--tests/test_hydrology.py133
1 files changed, 133 insertions, 0 deletions
diff --git a/tests/test_hydrology.py b/tests/test_hydrology.py
new file mode 100644
index 0000000..3e9314a
--- /dev/null
+++ b/tests/test_hydrology.py
@@ -0,0 +1,133 @@
+import unittest
+
+import numpy as np
+
+from mapgen import graph as G
+from mapgen import hydrology as HY
+from tests.helpers import make_ctx
+
+
+def basin_ctx(p_basin, pet_basin):
+ ctx = make_ctx(3)
+ g = ctx.grid
+ ocean = g.lat < -20
+ z = np.where(ocean, -3000.0, 200.0 + 30.0 * (g.lat + 20)) # land rises northward
+ c = g.xyz[g.cell_index(40.0, 0.0)]
+ d = g.radius_km * np.arccos(np.clip(g.xyz @ c, -1, 1))
+ z = np.where(d < 800, z - 1500 * (1 - d / 800), z) # closed basin
+ ctx.data.update({"elevation_eroded_m": z.astype(np.float32), "P_ann": np.full(g.n, p_basin),
+ "PET": np.full(g.n, pet_basin)})
+ return ctx, d
+
+
+class HydrologyTest(unittest.TestCase):
+ def test_strahler(self):
+ recv = np.array([2, 2, 6, 5, 5, 6, 6])
+ levels = G.receiver_levels(recv)
+ o = HY.strahler(recv, levels, np.ones(7, bool))
+ np.testing.assert_array_equal(o, [1, 1, 2, 1, 1, 2, 3])
+
+ def test_arid_basin_is_endorheic_with_salt(self):
+ ctx, d = basin_ctx(100.0, 1500.0)
+ out = HY.run(ctx)
+ inside = d < 300
+ self.assertTrue(out["endorheic"][inside].all())
+ self.assertTrue(out["salt_flat"][inside].any() or out["lake"][inside].any())
+
+ def test_humid_basin_overflows(self):
+ ctx, d = basin_ctx(2500.0, 400.0)
+ out = HY.run(ctx)
+ inside = d < 300
+ self.assertTrue(out["lake"][inside].all())
+ self.assertFalse(out["endorheic"][inside].any())
+
+ def test_mass_balance_and_rivers(self):
+ ctx, _ = basin_ctx(2500.0, 400.0)
+ out = HY.run(ctx)
+ g = ctx.grid
+ roots = out["recv"] == np.arange(g.n)
+ total = np.sum(out["runoff_mm"] * g.area_km2) * 1e-6
+ self.assertAlmostEqual(out["discharge_km3_yr"][roots].sum(), total, delta=total * 1e-6)
+ self.assertTrue(out["river"].any())
+ self.assertTrue(np.all(out["strahler"][out["river"]] >= 1))
+ land = ctx.data["elevation_eroded_m"] > 0
+ nonendo = land & ~out["endorheic"] & ~roots
+ self.assertTrue(np.all(out["z_filled_m"][out["recv"][nonendo]] < out["z_filled_m"][nonendo]))
+
+
+class InlandDepressionTest(unittest.TestCase):
+ def test_humid_depression_below_sea_level_becomes_lake(self):
+ ctx, d = basin_ctx(2500.0, 400.0)
+ g = ctx.grid
+ z = ctx.data["elevation_eroded_m"].astype(np.float64)
+ z[d < 300] = -40.0
+ ctx.data["elevation_eroded_m"] = z.astype(np.float32)
+ ctx.data["ocean"] = G.ocean_mask(g, z, 1.0e6)
+ out = HY.run(ctx)
+ self.assertTrue(out["lake"][d < 300].all())
+
+
+def two_pit_ctx(p, pet):
+ ctx = make_ctx(3)
+ g = ctx.grid
+ ocean = g.lat < -20
+ z = np.where(ocean, -3000.0, 200.0 + 30.0 * (g.lat + 20))
+ for lon in (-3.5, 3.5):
+ c = g.xyz[g.cell_index(40.0, lon)]
+ d = g.radius_km * np.arccos(np.clip(g.xyz @ c, -1, 1))
+ z = np.where(d < 900, z - (1500 + 200 * (lon > 0)) * (1 - d / 900), z) # two pits, one depression
+ ctx.data.update({"elevation_eroded_m": z.astype(np.float32), "P_ann": np.full(g.n, p), "PET": np.full(g.n, pet)})
+ return ctx, z
+
+
+class TerminalTest(unittest.TestCase):
+ def test_every_land_root_is_lake_or_salt(self):
+ ctx, z = two_pit_ctx(700.0, 1400.0)
+ out = HY.run(ctx)
+ roots = (out["recv"] == np.arange(len(z))) & (z > 0) | (out["recv"] == np.arange(len(z))) & out["endorheic"]
+ self.assertTrue(out["endorheic"].any())
+ self.assertTrue(np.all(out["lake"][roots] | out["salt_flat"][roots]))
+ self.assertEqual(int((roots & out["endorheic"]).sum()), 1) # one terminal for the depression
+
+ def test_mass_balance_with_lake_evaporation(self):
+ ctx, z = two_pit_ctx(1000.0, 1200.0)
+ out = HY.run(ctx)
+ g = ctx.grid
+ roots = out["recv"] == np.arange(g.n)
+ total = np.sum(out["runoff_mm"] * g.area_km2) * 1e-6
+ self.assertGreater(out["lake_loss_km3_yr"].sum(), 0)
+ self.assertAlmostEqual(out["discharge_km3_yr"][roots].sum() + out["lake_loss_km3_yr"].sum(), total,
+ delta=total * 1e-6)
+
+
+class LakeBedTest(unittest.TestCase):
+ def test_lakes_carved_below_level_and_rerun_is_stable(self):
+ ctx, d = basin_ctx(2500.0, 400.0)
+ z0 = np.asarray(ctx.data["elevation_eroded_m"], dtype=np.float64)
+ out = HY.run(ctx)
+ lake, lev, z = out["lake"], out["lake_level_m"], out["elevation_eroded_m"].astype(np.float64)
+ self.assertTrue(lake.any())
+ self.assertTrue(np.all(np.isnan(lev[~lake])))
+ self.assertTrue(np.all(z[lake] <= lev[lake] - HY.DEFAULTS["lake_min_m"] + 1e-3))
+ np.testing.assert_array_equal(z[~lake], z0[~lake].astype(np.float32)) # only lake beds change
+ ctx.data.update({"elevation_eroded_m": out["elevation_eroded_m"]}) # again (eras rerun hydrology)
+ again = HY.run(ctx)
+ np.testing.assert_array_equal(again["lake"], lake)
+ np.testing.assert_allclose(again["lake_level_m"][lake], lev[lake])
+ np.testing.assert_array_equal(again["elevation_eroded_m"], out["elevation_eroded_m"])
+
+ def test_bigger_lakes_deeper(self):
+ from tests.helpers import make_ctx as mk
+ g = mk(3).grid
+ P = {**HY.DEFAULTS, "lake_depth_exp": 0.3}
+ z = np.zeros(g.n)
+ lake = np.zeros(g.n, bool)
+ small, big = g.cell_index(10.0, 0.0), np.flatnonzero(g.xyz @ g.xyz[g.cell_index(-30.0, 90.0)] > 0.97)
+ lake[small] = lake[big] = True
+ lid = np.where(lake, 0, -1)
+ lid[big] = 1
+ bed = HY.lake_beds(g, z, np.where(lake, 0.0, np.nan), lake, lid, np.zeros(g.n, bool), np.zeros(g.n, bool),
+ np.full(g.n, 1000.0), 1, P)
+ self.assertLess(bed[big].min(), -P["lake_min_m"])
+ self.assertLessEqual(bed[small], -P["lake_min_m"])
+ self.assertTrue(np.all(bed[~lake] == 0))