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
|
import unittest
import numpy as np
from tests.helpers import globe_world, line_world, make_history, make_race
from worldhistory.hazards import make_hazard
from worldhistory.regions import Regions
from worldhistory.state import new_state
def hist(**hz):
return make_history(regions={"north": {"kind": "box", "lat": [30, 90], "lon": [-180, 180]}}, hazard=[hz])
class HazardTest(unittest.TestCase):
def setUp(self):
self.w = globe_world(1)
self.st = new_state(1, self.w.n, np.zeros((1, 6)), seed=0)
self.st.P[:] = 100.0
def test_static_in_window_and_region(self):
h = hist(kind="static", region="north", mortality=0.1, years=[10, 30])
hz = make_hazard(h.hazards[0])
reg = Regions(self.w, h.regions)
north = reg("north")
hz.step(self.st, self.w, [make_race()], reg, 0, np.random.default_rng(0))
self.assertTrue((self.st.P == 100).all())
hz.step(self.st, self.w, [make_race()], reg, 10, np.random.default_rng(0))
np.testing.assert_allclose(self.st.P[0, north], 90.0)
np.testing.assert_allclose(self.st.P[0, ~north], 100.0)
def test_static_push_drives_people_off_land(self):
# megafauna: on land in the region, a share flees each step (not killed); drift is told to avoid those cells
w = line_world(4, ocean=[False, False, True, True])
h = make_history(regions={"coast": {"kind": "box", "lat": [-1, 1], "lon": [-1, 99]}},
hazard=[{"kind": "static", "name": "beasts", "region": "coast", "mortality": 0.0,
"years": [0, 100], "push": 0.2}])
st = new_state(1, 4, np.zeros((1, 6)), seed=0)
st.P[:] = 100.0
hz = make_hazard(h.hazards[0])
hz.step(st, w, [make_race()], Regions(w, h.regions), 0, np.random.default_rng(0))
np.testing.assert_allclose(st.P[0], 100.0)
np.testing.assert_allclose(st.push[0], [20, 20, 0, 0])
np.testing.assert_allclose(hz.danger, [1, 1, 0, 0])
hz.step(st, w, [make_race()], Regions(w, h.regions), 200, np.random.default_rng(0))
np.testing.assert_allclose(hz.danger, 0) # outside its years: no danger
def test_roaming_local_stay_home_and_raid(self):
h = hist(kind="roaming", start=20, home_region="north", home_near="a", local=10, wanderers=0,
radius_km=600, raid_chance=0.5, growth=0.0, infight=0.0, raid_death=0.0)
hz = make_hazard(h.hazards[0])
reg = Regions(self.w, h.regions)
rng = np.random.default_rng(3)
hz.step(self.st, self.w, [make_race()], reg, 10, rng)
self.assertTrue((self.st.P == 100).all()) # not started yet
stats = [hz.step(self.st, self.w, [make_race()], reg, t, rng) for t in range(20, 200, 10)]
self.assertTrue(reg("north")[hz.home].all())
self.assertEqual(len(hz.home), 10)
self.assertGreater(sum(s["raids"] for s in stats), 0)
self.assertTrue((self.st.P[0, reg("north")] < 100).any())
np.testing.assert_array_equal(self.st.P[0, self.w.lat < 20], 100.0) # north (lat ≥ 30) + 600 km only
def test_roaming_wanderer_leaves_home_region(self):
h = hist(kind="roaming", start=0, home_region="north", local=0, wanderers=1, wander_stop=1.0,
wander_step_km=1500.0, growth=0.0, infight=0.0)
hz = make_hazard(h.hazards[0])
reg = Regions(self.w, h.regions)
rng = np.random.default_rng(0)
for t in range(0, 400, 10):
hz.step(self.st, self.w, [make_race()], reg, t, rng)
self.assertTrue((self.st.P[0, ~reg("north")] < 100).any())
def test_roaming_growth_and_deaths(self):
h = hist(kind="roaming", start=0, home_region="north", local=100, wanderers=0, growth=0.1,
clutch=[2, 4], infight=0.05, raid_chance=0.0)
hz = make_hazard(h.hazards[0])
reg = Regions(self.w, h.regions)
rng = np.random.default_rng(0)
births = deaths = 0
for t in range(0, 500, 10):
s = hz.step(self.st, self.w, [make_race()], reg, t, rng)
births += s["births"]
deaths += s["deaths"]
self.assertGreater(births, 0)
self.assertGreater(deaths, 0)
self.assertEqual(len(hz.home), 100 + births - deaths)
def test_roaming_growth_levels_off_at_cap(self):
# growth 0.1/step uncapped would give 20·1.1^100 ≈ 275k units; logistic cap holds it near 60
h = hist(kind="roaming", start=0, home_region="north", local=20, wanderers=0, growth=0.1,
clutch=[2, 4], infight=0.0, raid_chance=0.0, cap=60)
hz = make_hazard(h.hazards[0])
reg = Regions(self.w, h.regions)
rng = np.random.default_rng(0)
for t in range(0, 1000, 10):
hz.step(self.st, self.w, [make_race()], reg, t, rng)
self.assertGreater(len(hz.home), 40)
self.assertLessEqual(len(hz.home), 60 + 4)
def test_roaming_fallback_home(self):
self.st.P[:] = 0.0
h = hist(kind="roaming", start=0, home_region="north", home_near="a", local=5, wanderers=0)
hz = make_hazard(h.hazards[0])
reg = Regions(self.w, h.regions)
hz.step(self.st, self.w, [make_race()], reg, 0, np.random.default_rng(0))
self.assertTrue(reg("north")[hz.home].all())
|