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
|
import unittest
import numpy as np
from tests.helpers import line_world, make_race
from worldhistory.adaptation import adapt
from worldhistory.config import CONDITIONS
from worldhistory.habitat import environment, fitness, native_optima
from worldhistory.state import new_state
D = CONDITIONS.index("depth")
TOL = {"depth": {"optimum": 0, "width": 50, "lo": 0, "hi": 600, "rate": 2.0}}
def world_and_state(race, pops):
w = line_world(4, ocean=[False, True, True, True], elevation_m=[10, -200, -600, -1000])
st = new_state(1, 4, native_optima([race]), seed=0)
st.P[0] = pops
return w, st
class AdaptationTest(unittest.TestCase):
def test_moves_at_most_rate_times_years(self):
r = make_race(tolerance=TOL)
w, st = world_and_state(r, [0, 10, 0, 0])
adapt(st, environment(w), [r], np.ones((1, 4)), 10)
self.assertAlmostEqual(float(st.O[0, D, 1]), 20.0) # 2 m/yr × 10 yr toward the deep end (400)
self.assertEqual(float(st.O[0, D, 0]), 0.0) # empty cells unchanged
def test_multiplier_speeds_adaptation(self):
r = make_race(tolerance=TOL)
w, st = world_and_state(r, [0, 10, 0, 0])
adapt(st, environment(w), [r], np.ones((1, 4)), 10, mult=np.full((1, 4), 2.0))
self.assertAlmostEqual(float(st.O[0, D, 1]), 40.0) # pressed: 2 × (2 m/yr × 10 yr)
def test_turnover_slows_poor_habitat(self):
r = make_race(tolerance=TOL)
w, st = world_and_state(r, [0, 10, 0, 0])
adapt(st, environment(w), [r], np.zeros((1, 4)), 10)
self.assertAlmostEqual(float(st.O[0, D, 1]), 6.0) # × 0.3
def test_soft_limit_brakes_but_does_not_stop(self):
r = make_race(tolerance=TOL) # hi 600, width 50
w, st = world_and_state(r, [0, 0, 0, 10])
st.O[0, D, 3] = 650.0 # 1 width past hi
adapt(st, environment(w), [r], np.ones((1, 4)), 10)
self.assertAlmostEqual(float(st.O[0, D, 3]), 650.0 + 20.0 * np.exp(-0.5), places=4)
for _ in range(200):
adapt(st, environment(w), [r], np.ones((1, 4)), 10)
self.assertGreater(float(st.O[0, D, 3]), 650.0)
self.assertLess(float(st.O[0, D, 3]), 800.0) # far slower than the free rate (2 m/yr)
def test_inward_moves_are_not_braked(self):
r = make_race(tolerance=TOL)
w = line_world(3) # all land: target depth 0
st = new_state(1, 3, native_optima([r]), seed=0)
st.P[0] = 10
st.O[0, D] = 700.0 # past hi, moving back in
adapt(st, environment(w), [r], np.ones((1, 3)), 10)
np.testing.assert_allclose(st.O[0, D], 680.0)
def test_rain_does_not_drift_at_sea(self):
r = make_race(habitat={"realm": "both", "terms": [{"p": "sea", "w": 1}]},
tolerance={"rain": {"optimum": 3.0, "width": 0.3, "lo": 1.5, "hi": 3.5, "rate": 0.01}})
w = line_world(3, ocean=True, elevation_m=-100)
st = new_state(1, 3, native_optima([r]), seed=0)
st.P[0] = 10
adapt(st, environment(w), [r], np.ones((1, 3)), 10)
np.testing.assert_allclose(st.O[0, CONDITIONS.index("rain")], 3.0)
def test_inland_returns_to_native(self):
r = make_race(tolerance=TOL)
w = line_world(3) # all land: depth span [0, 0]
st = new_state(1, 3, native_optima([r]), seed=0)
st.P[0] = 10
st.O[0, D] = 100.0
adapt(st, environment(w), [r], np.ones((1, 3)), 10)
np.testing.assert_allclose(st.O[0, D], 80.0)
def test_adapting_costs_the_old(self):
r = make_race(tolerance=TOL)
w, st = world_and_state(r, [0, 0, 0, 10])
env = environment(w)
O_before = st.O[0].copy()
for _ in range(60):
adapt(st, env, [r], np.ones((1, 4)), 10)
at_surface_before = fitness(env, O_before, r)[0]
at_surface_after = fitness(env, np.repeat(st.O[0][:, 3:4], 4, 1), r)[0]
self.assertAlmostEqual(float(at_surface_before), 1.0)
self.assertLess(float(at_surface_after), 0.1) # deep-adapted people cannot live at the surface
class ProspectiveTest(unittest.TestCase):
def test_empty_cells_take_neighbour_optimum(self):
from worldhistory.adaptation import prospective
r = make_race(tolerance=TOL)
w, st = world_and_state(r, [0, 30, 0, 10])
st.O[0, D] = [0.0, 100.0, 0.0, 400.0]
prospective(st, w)
# cell 0: only neighbour 1 occupied -> 100; cell 2: (30*100 + 10*400) / 40 = 175; occupied cells unchanged
np.testing.assert_allclose(st.O[0, D], [100.0, 100.0, 175.0, 400.0])
def test_isolated_empty_cells_keep_their_value(self):
from worldhistory.adaptation import prospective
r = make_race(tolerance=TOL)
w, st = world_and_state(r, [0, 0, 0, 0])
st.O[0, D] = [5.0, 6.0, 7.0, 8.0]
prospective(st, w)
np.testing.assert_allclose(st.O[0, D], [5.0, 6.0, 7.0, 8.0])
def test_fractional_groups_keep_their_optimum(self):
# all groups count (spec §1.3): a thin frontier (P < 1) is not "empty"
from worldhistory.adaptation import prospective
r = make_race(tolerance=TOL)
w, st = world_and_state(r, [100, 0.5, 0, 0])
st.O[0, D] = [0.0, 900.0, 0.0, 0.0]
prospective(st, w)
self.assertEqual(float(st.O[0, D, 1]), 900.0)
self.assertEqual(float(st.O[0, D, 2]), 900.0) # the empty cell beyond takes the frontier's optimum
|