worldmap-viewer

git clone https://git.godosa.eu/worldmap-viewer

master

raw · 14781 bytes

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)