aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/tests/test_demography.py
blob: 26a5f04eeca2c91b875de76f970ab67d982fd3bd (plain)
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