raw · 14781 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 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 | import json import unittest import numpy as np from scipy.spatial import cKDTree import rivers as RV from tests.test_serve import built_world def net_and_arrays(): root = built_world() d = dict(np.load(root / "out" / "r2" / "cells.npz")) meta = json.loads((root / "out" / "r2" / "cells_meta.json").read_text()) return RV.RiverNet(d, meta["legends"], meta["radius_km"]), d class RiverNetTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.net, cls.a = net_and_arrays() cls.r = np.where(cls.a["river"].astype(bool))[0] def test_levels_never_above_ground_or_below_the_cap(self): z, lev = self.a["z_surface_m"][self.r], self.net.level[self.r] self.assertTrue(np.all(np.isfinite(lev))) self.assertTrue(np.all(lev <= z + 1e-6)) self.assertTrue(np.all(z - lev <= self.net.cap[self.r] + 1e-6)) def test_levels_never_rise_downstream(self): recv, riv = self.a["recv"], self.a["river"].astype(bool) down = recv[self.r] on = riv[down] self.assertTrue(np.all(self.net.level[down[on]] <= self.net.level[self.r[on]] + 1e-6)) def test_levels_meet_their_mouth(self): recv, ocean, lake = self.a["recv"], self.a["ocean"].astype(bool), self.a["lake"].astype(bool) sea = self.r[ocean[recv[self.r]]] self.assertTrue(len(sea) > 0) k = np.isin(self.net.seg_a, sea) np.testing.assert_allclose(self.net.level_b[k], 0.0) lk = self.r[lake[recv[self.r]]] k = np.isin(self.net.seg_a, lk) np.testing.assert_allclose(self.net.level_b[k], self.a["z_filled_m"][recv[self.net.seg_a[k]]]) def test_drops_below_lakes_are_cataracts_not_cliffs(self): recv, riv, lake = self.a["recv"], self.a["river"].astype(bool), self.a["lake"].astype(bool) src = np.where(lake & riv[recv] & (recv != np.arange(len(recv))))[0] for i in src: j = recv[i] dist = np.linalg.norm(self.a["g_xyz"][i] - self.a["g_xyz"][j]) * self.net.R drop = self.a["z_filled_m"][i] - self.net.level[j] self.assertLessEqual(drop, RV.KNICK_M_KM * dist + 1e-6 + max(0.0, self.a["z_filled_m"][i] - self.a["z_surface_m"][j])) def test_geometry_per_segment(self): n = len(self.net.seg_a) # river cells + lake outlets self.assertGreaterEqual(n, len(self.r)) for k in ("a_xyz", "b_xyz", "level_a", "level_b", "half_w", "floor", "wall", "meander", "reach", "seg_len"): self.assertEqual(len(getattr(self.net, k)), n, k) self.assertTrue(np.all(self.net.half_w >= 0.03) and np.all(self.net.wall > 0)) class StyleTest(unittest.TestCase): def test_hard_dry_rock_makes_steeper_narrower_valleys(self): g = RV.valley_style("plateau", "—", "granite/gneiss", 400.0) s = RV.valley_style("plateau", "—", "sandstone/shale", 1800.0) self.assertGreater(g[1], 3 * s[1]) # walls self.assertLess(g[0], s[0]) # floor self.assertGreater(g[1], 2500) # the granite gorge: sheer def test_floodplains_are_wide_and_meander(self): f = RV.valley_style("plain", "floodplain", "sandstone/shale", 900.0) m = RV.valley_style("mountains", "—", "granite/gneiss", 900.0) self.assertGreater(f[0], 10 * m[0]) self.assertGreater(f[2], 0) self.assertEqual(m[2], 0) def test_channel_width(self): self.assertAlmostEqual(RV.half_width_km(2.0), 0.004 * np.sqrt(2.0 * 31.7), places=9) class ValleyTest(unittest.TestCase): def test_profile(self): d = np.array([0.0, 0.05, 0.2, 1.0, 2.0]) v = RV.valley_surface(d, 100.0, 0.1, 0.3, 2000.0) self.assertEqual(v[0], 100.0) # channel: the water level self.assertEqual(v[1], 100.0) self.assertTrue(101.0 <= v[2] <= 103.0) # floor self.assertAlmostEqual(v[4] - v[3], 2000.0, places=6) # wall: 2,000 m per km def test_points_are_a_global_lattice(self): net, _ = net_and_arrays() s = int(np.argmax(net.seg_len)) c = net.a_xyz[s] p1, _ = net.segment_points(s, c, 50.0, 5.0) p2, _ = net.segment_points(s, c, 400.0, 5.0) self.assertGreater(len(p2), len(p1)) a = {tuple(np.round(v, 12)) for v in p1} b = {tuple(np.round(v, 12)) for v in p2} self.assertTrue(a <= b, "a wider query returns a superset of the same points") def test_antimeridian_segment(self): net, _ = net_and_arrays() a = np.array([np.cos(np.radians(10)) * np.cos(np.radians(179.9)), np.cos(np.radians(10)) * np.sin(np.radians(179.9)), np.sin(np.radians(10))]) b = np.array([np.cos(np.radians(10)) * np.cos(np.radians(-179.9)), np.cos(np.radians(10)) * np.sin(np.radians(-179.9)), np.sin(np.radians(10))]) pts = RV.densify(a, b, 0.0, 1.0, 1.0 / net.R) # 1 km steps (a chord in planet radii) self.assertTrue(np.all(np.abs(np.linalg.norm(pts, axis=1) - 1) < 1e-12)) self.assertLess(np.max(np.linalg.norm(np.diff(pts, axis=0), axis=1)) * net.R, 1.01) class RoughWallTest(unittest.TestCase): def test_walls_keep_the_terrain_roughness_but_the_channel_stays_flat(self): d = np.array([0.0, 0.3, 1.0, 2.0]) smooth = RV.valley_surface(d, 100.0, 0.1, 0.1, 500.0) rough = RV.valley_surface(d, 100.0, 0.1, 0.1, 500.0, rough=np.array([80.0, 80.0, 80.0, -40.0])) self.assertEqual(rough[0], smooth[0]) # the water level self.assertAlmostEqual(rough[2] - smooth[2], 80.0) # a wall well away from the floor: full roughness self.assertAlmostEqual(rough[3] - smooth[3], -40.0) self.assertTrue(0 < rough[1] - smooth[1] < 80.0) # fading in above the floor LEG = {"landform": ["ocean", "plain", "hills"], "ground": ["—", "floodplain"], "lithology": ["granite/gneiss", "sandstone/shale"], "age_class": ["oceanic", "craton (3g era)"]} def synthetic(cells): """A tiny world. cells: (lat, lon, z, discharge, landform, ground, receiver index, kind: river|ocean|lake|land).""" ll = np.radians([(c[0], c[1]) for c in cells]) a = {"g_xyz": np.stack([np.cos(ll[:, 0]) * np.cos(ll[:, 1]), np.cos(ll[:, 0]) * np.sin(ll[:, 1]), np.sin(ll[:, 0])], 1), "z_surface_m": np.array([c[2] for c in cells], float), "z_filled_m": np.array([c[2] for c in cells], float), "discharge_km3_yr": np.array([c[3] for c in cells], float), "landform": np.array([LEG["landform"].index(c[4]) for c in cells]), "ground": np.array([LEG["ground"].index(c[5]) for c in cells]), "recv": np.array([c[6] for c in cells]), "river": np.array([c[7] == "river" for c in cells]), "ocean": np.array([c[7] == "ocean" for c in cells]), "lake": np.array([c[7] == "lake" for c in cells]), "lithology": np.ones(len(cells), int), "age_class": np.ones(len(cells), int), "P_ann": np.full(len(cells), 900.0)} return RV.RiverNet(a, LEG, 12742.0) def xyz(lat, lon): la, lo = np.radians(lat), np.radians(lon) return np.array([[np.cos(la) * np.cos(lo), np.cos(la) * np.sin(lo), np.sin(la)]]) class PerValleyTest(unittest.TestCase): # a big floodplain river along the equator (east to the sea) and a small hill stream 22 km north joining it net = synthetic([(0, 0.0, 100, 5000, "plain", "floodplain", 1, "river"), (0, 0.3, 90, 5000, "plain", "floodplain", 2, "river"), (0, 0.6, 80, 5000, "plain", "floodplain", 3, "river"), (0, 0.9, 70, 5000, "plain", "floodplain", 4, "river"), (0, 1.2, -50, 0, "ocean", "—", 4, "ocean"), (0.1, 0.3, 400, 2, "hills", "—", 6, "river"), (0.1, 0.6, 380, 2, "hills", "—", 2, "river")]) def test_a_big_valley_is_not_crowded_out_by_a_nearby_stream(self): q = xyz(0.05, 0.45) # 11 km from both centrelines V, ch, dr = self.net.valleys(q, 0.5) big = float(np.interp(0.45, [0.3, 0.6], [self.net.level[1], self.net.level[2]])) self.assertLessEqual(V[0], big + 3.0 + 1e-6, "on the big river's floodplain floor") def test_the_answer_does_not_depend_on_the_other_query_points(self): q = xyz(0.05, 0.45) many = np.vstack([q, xyz(3.0, 20.0), xyz(-0.2, 0.1), xyz(0.08, 0.5)]) a, b = self.net.valleys(q, 0.5)[0], self.net.valleys(many, 0.5)[0] self.assertEqual(a[0], b[0]) def test_floors_stay_inside_the_reach(self): self.assertTrue(np.all(self.net.floor <= RV.FLOOR_MAX_KM + 1e-9)) self.assertTrue(np.all(self.net.reach >= self.net.half_w + self.net.floor)) def test_rough_walls_never_dip_below_the_water(self): v = RV.valley_surface(np.array([0.6, 1.0]), 100.0, 0.1, 0.3, 500.0, rough=np.array([-500.0, -500.0])) self.assertTrue(np.all(v >= 101.0)) class BankTest(unittest.TestCase): def test_a_river_doubling_back_below_itself_keeps_its_banks_above_its_water(self): # a canyon river runs east, then back west 5.5 km north and 400 m lower: its lower floor must not # undercut the upper reach (dry pits below the water beside it) net = synthetic([(0, 0.0, 900, 300, "hills", "—", 1, "river"), (0, 0.3, 700, 300, "hills", "—", 2, "river"), (0.05, 0.0, 300, 300, "hills", "—", 3, "river"), (0.05, -0.3, -50, 0, "ocean", "—", 3, "ocean")]) la, lo = np.meshgrid(np.linspace(-0.03, 0.08, 40), np.linspace(-0.05, 0.35, 80), indexing="ij") q = np.vstack([xyz(a, b) for a, b in zip(la.ravel(), lo.ravel())]) V, ch, _ = net.valleys(q, 0.1) best, lev, bank = np.full(len(q), np.inf), np.zeros(len(q)), np.zeros(len(q), bool) c = q.mean(0) / np.linalg.norm(q.mean(0)) for s in range(len(net.seg_a)): # the nearest reach, its water, its floor zone p, t = net.segment_points(s, c, 60.0, 0.05) d, k = cKDTree(p).query(q) d *= net.R b = d < best best[b], bank[b] = d[b], (d <= net.half_w[s] + net.floor[s])[b] lev[b] = (net.level_a[s] + (net.level_b[s] - net.level_a[s]) * t[k])[b] dry = ~ch & bank self.assertGreater(dry.sum(), 100) self.assertEqual(int((V[dry] < lev[dry]).sum()), 0, "floor below the water of the reach beside it") class LakeOutletTest(unittest.TestCase): def test_a_lake_outlet_joins_its_river_starting_at_the_lake_surface(self): net = synthetic([(0, 0.0, 1100, 0, "hills", "—", 1, "lake"), (0, 0.3, 1100, 300, "plain", "—", 2, "river"), (0, 0.6, 600, 300, "plain", "—", 3, "river"), (0, 0.9, -50, 0, "ocean", "—", 3, "ocean")]) k = np.where(net.seg_a == 0)[0] self.assertEqual(len(k), 1, "the lake outlet has a segment") self.assertEqual(net.level_a[k[0]], 1100.0) self.assertEqual(net.seg_b[k[0]], 1) class GroundTest(unittest.TestCase): def test_given_levels_reach_their_shoulders_and_sub_pixel_streams_are_not_drawn(self): cells = [(0, 0.0, 800, 5000, "plateau", "—", 1, "river"), (0, 0.3, 700, 5000, "plateau", "—", 2, "river"), (0, 0.6, -50, 0, "ocean", "—", 2, "ocean")] levels = np.array([400.0, 300.0, 0.0]) ground = np.array([800.0, 700.0, 0.0]) LEG2 = dict(LEG, landform=LEG["landform"] + ["plateau"]) ll = np.radians([(c[0], c[1]) for c in cells]) arr = {"g_xyz": np.stack([np.cos(ll[:, 0]) * np.cos(ll[:, 1]), np.cos(ll[:, 0]) * np.sin(ll[:, 1]), np.sin(ll[:, 0])], 1), "z_surface_m": ground.copy(), "z_filled_m": ground.copy(), "discharge_km3_yr": np.array([5000.0, 5000.0, 0.0]), "landform": np.array([3, 3, 0]), "ground": np.zeros(3, int), "recv": np.array([1, 2, 2]), "river": np.array([True, True, False]), "ocean": np.array([False, False, True]), "lake": np.zeros(3, bool), "lithology": np.zeros(3, int), "age_class": np.ones(3, int), "P_ann": np.full(3, 300.0)} net = RV.RiverNet(arr, LEG2, 12742.0, levels=levels, ground=ground) k = 0 self.assertGreaterEqual(net.reach[k], net.half_w[k] + net.floor[k] + (ground[0] - levels[0]) / net.wall[k] - 1e-9) small = synthetic([(0, 0.0, 100, 0.2, "plain", "—", 1, "river"), (0, 0.3, 90, 0.2, "plain", "—", 2, "river"), (0, 0.6, -50, 0, "ocean", "—", 2, "ocean")]) q = xyz(0.0, 0.15) self.assertFalse(small.channel(q, 5.0)[1][0], "a 0.2 km³/yr stream is not drawn at 5 km per pixel") self.assertTrue(small.channel(q, 0.01)[1][0], "but is at street level") class ChunkTest(unittest.TestCase): def test_a_coarse_tile_splits_into_compact_chunks(self): net = synthetic([(0, 0.0, 100, 5, "plain", "—", 1, "river"), (0, 0.3, 90, 5, "plain", "—", 2, "river"), (0, 0.6, -50, 0, "ocean", "—", 2, "ocean")]) la, lo = np.meshgrid(np.linspace(-52.0, -46.4, 256), np.linspace(-45.0, -33.75, 256), indexing="ij") # a z5 tile a, o = np.radians(la.ravel()), np.radians(lo.ravel()) chunks = net._chunks(np.stack([np.cos(a) * np.cos(o), np.cos(a) * np.sin(o), np.sin(a)], 1)) self.assertLess(len(chunks), 300, "not thousands of one-row strips") self.assertEqual(sorted(np.concatenate([c[0] for c in chunks]).tolist()), list(range(256 * 256))) class RiverNetStateTest(unittest.TestCase): @classmethod def setUpClass(cls): import refine as RF from tests.test_serve import built_world w = RF.WorldCells(built_world() / "out" / "r2") cls.a = {k: np.asarray(w.a[k]) for k in RF.NET_KEYS} cls.legends, cls.R = w.meta["legends"], float(w.meta["radius_km"]) def test_state_round_trip_answers_the_same(self): import rivers as RV net = RV.RiverNet(self.a, self.legends, self.R) arrays, meta = net.state() self.assertEqual(set(arrays), set(RV.STATE)) back = RV.RiverNet.from_state(arrays, meta) for k in RV.STATE: np.testing.assert_array_equal(getattr(back, k), getattr(net, k)) self.assertEqual(back.max_extent, net.max_extent) p = np.asarray(self.a["g_xyz"][:200], dtype=np.float64) np.testing.assert_array_equal(back.valleys(p, 5.0)[1], net.valleys(p, 5.0)[1]) np.testing.assert_array_equal(back.channel(p, 5.0)[1], net.channel(p, 5.0)[1]) def test_tree_is_built_on_first_use(self): import rivers as RV net = RV.RiverNet.from_state(*RV.RiverNet(self.a, self.legends, self.R).state()) self.assertIsNone(net._tree) self.assertIsNotNone(net.tree) self.assertIs(net.tree, net.tree) |