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
|
import unittest
import numpy as np
from tests.helpers import line_world, make_race
from worldhistory.demography import (allee, capacity, crowding, grow, harvest_shocks, overlap_matrix,
stochastic_round, veins)
class DemographyTest(unittest.TestCase):
def test_capacity_formula(self):
w = line_world(2)
r = make_race(demography={"density": 2.0})
K = capacity(w, r, np.array([1.0, 0.5]), np.ones(2), np.ones(2), np.array([11.0, 11.0]), np.ones(2),
np.ones(2))
# density * area * q * (1 + gain q²): 2*10000*1*12, 2*10000*0.5*(1+11*0.25)
np.testing.assert_allclose(K, [240000, 37500])
def test_logistic_reaches_K(self):
P = np.array([[10.0]])
K = np.array([[1000.0]])
r = [make_race(demography={"growth": 0.5})]
for _ in range(200):
grow(P, K, crowding(P, K, np.zeros((1, 1)), r, 2.0), np.ones((1, 1)), r, np.ones((1, 1)))
self.assertAlmostEqual(float(P[0, 0]), 1000.0, places=3)
def test_growth_rate_low_gear(self):
P = np.array([[10.0, 10.0]])
K = np.array([[1e9, 1e9]])
r = [make_race(demography={"growth": 0.1})]
grow(P, K, crowding(P, K, np.zeros((1, 1)), r, 2.0), np.array([[1.0, 0.0]]), r, np.ones((1, 2)))
np.testing.assert_allclose(P[0], [11.0, 10.3], rtol=1e-6) # r*(0.3+0.7q)
def test_grow_returns_births(self):
r = [make_race()]
P = np.array([[100.0, 100.0]])
b = grow(P, np.array([[1e6, 50.0]]), np.array([[0.0, 2.0]]), np.ones((1, 2)), r, np.ones((1, 2)))
# gross births (generational turnover), not net growth: a crowded, shrinking group still bears children
np.testing.assert_allclose(b[0], [0.1 * 100, 0.1 * 100])
def test_allee(self):
self.assertLess(allee(np.array(10.0), 75.0), 0)
self.assertGreater(allee(np.array(100.0), 75.0), 0)
self.assertAlmostEqual(float(allee(np.array(1.0), 1000.0)), -0.3)
P = np.array([[10.0]])
r = [make_race(demography={"growth": 0.1, "allee": 75.0})]
grow(P, np.array([[1e6]]), np.zeros((1, 1)), np.ones((1, 1)), r, np.ones((1, 1)))
self.assertLess(float(P[0, 0]), 10.0)
def test_no_capacity_halves(self):
P = np.array([[10.0]])
grow(P, np.array([[0.0]]), np.zeros((1, 1)), np.ones((1, 1)), [make_race()], np.ones((1, 1)))
self.assertAlmostEqual(float(P[0, 0]), 5.0)
def test_competition(self):
P = np.array([[100.0], [100.0]])
K = np.array([[1000.0], [1000.0]])
rs = [make_race("a"), make_race("b")]
c0 = crowding(P, K, np.zeros((2, 2)), rs, 1.0)
c1 = crowding(P, K, np.array([[0, 1.0], [1.0, 0]]), rs, 1.0)
np.testing.assert_allclose(c0[:, 0], [0.1, 0.1])
np.testing.assert_allclose(c1[:, 0], [0.2, 0.2])
def test_tolerated_share_sharpens(self):
P = np.array([[100.0], [100.0]])
K = np.array([[1000.0], [1000.0]])
rs = [make_race("a", temperament={"tolerated_share": 0.1}), make_race("b", temperament={"tolerated_share": 0.9})]
c = crowding(P, K, np.array([[0, 1.0], [1.0, 0]]), rs, 2.0)
np.testing.assert_allclose(c[:, 0], [0.3, 0.2]) # a: others' share 0.5 > 0.1 → ×2
def test_overlap_matrix(self):
S = np.array([[1.0, 0, 0], [1.0, 0, 0], [0, 0, 1.0]])
calm = {"xenophobia": 0.0, "pace": 0.5, "structure": 0.5, "warlike": 0.0}
rs = [make_race("a", temperament=calm), make_race("b", temperament=calm), make_race("c", temperament=calm)]
a = overlap_matrix(S, rs)
self.assertAlmostEqual(a[0, 1], 0.5) # sim 1 × friction 0.5
self.assertAlmostEqual(a[0, 2], 0.0) # disjoint habitats
rs[0].temperament = {**calm, "xenophobia": 1.0}
a = overlap_matrix(S, rs)
self.assertAlmostEqual(a[0, 1], 1.0) # a minds b more than b minds a
self.assertAlmostEqual(a[1, 0], 0.5)
rs[1].overlap = {"a": 0.05}
self.assertAlmostEqual(overlap_matrix(S, rs)[1, 0], 0.05)
def test_shocks_mean_one_and_scale(self):
w = line_world(20000)
e = np.random.default_rng(1).normal(size=w.n)
m = harvest_shocks(w, np.ones(w.n), np.ones(w.n), e)
self.assertAlmostEqual(float(m.mean()), 1.0, places=2)
np.testing.assert_array_equal(harvest_shocks(w, np.zeros(w.n), np.ones(w.n), e), 1.0)
def test_veins_need_viable_community(self):
np.testing.assert_allclose(veins(np.array([50.0, 100.0]), np.array([1000.0, 1000.0]), 0.01, 75.0),
[50.0, 109.0])
def test_stochastic_round_expectation(self):
P = np.full((1, 100000), 2.3)
stochastic_round(P, np.random.default_rng(0))
self.assertTrue(set(np.unique(P)) <= {2.0, 3.0})
self.assertAlmostEqual(float(P.mean()), 2.3, places=2)
Q = np.array([[7.5, 0.0]])
stochastic_round(Q, np.random.default_rng(0))
np.testing.assert_array_equal(Q, [[7.5, 0.0]])
class RangeTest(unittest.TestCase):
def test_wide_range_crowding(self):
# a clan drawing on a wide range: crowding = people over capacity summed across the neighbourhood
from tests.helpers import line_world
w = line_world(3)
P, K = np.array([[300.0, 0.0, 0.0]]), np.array([[100.0, 100.0, 100.0]])
narrow = crowding(P, K, np.zeros((1, 1)), [make_race()], 2.0)
wide = crowding(P, K, np.zeros((1, 1)), [make_race(demography={"range": 1})], 2.0, world=w)
self.assertAlmostEqual(float(narrow[0, 0]), 3.0)
self.assertAlmostEqual(float(wide[0, 0]), 1.5) # 300 over cells 0+1 (200)
def test_world_range_crowding(self):
# range -1: crowding is the whole people's numbers against its whole capacity (few, widely scattered clans)
from tests.helpers import line_world
w = line_world(3)
P, K = np.array([[300.0, 0.0, 0.0]]), np.array([[100.0, 100.0, 0.0]])
c = crowding(P, K, np.zeros((1, 1)), [make_race(demography={"range": -1})], 2.0, world=w)
np.testing.assert_allclose(c[0], [1.5, 1.5, 0.0]) # 300 / 200 where it can live
|