diff options
| author | godosa <godosa@godosa.eu> | 2026-10-07 00:01:14 +0200 |
|---|---|---|
| committer | godosa <godosa@godosa.eu> | 2026-10-07 00:01:14 +0200 |
| commit | ed1dea2639b1191421de3986483aedcc14067a12 (patch) | |
| tree | 0118c6e119a84f1a9433ed0a304c8be1c9098463 /tests/test_demography.py | |
| download | worldhistory-ed1dea2639b1191421de3986483aedcc14067a12.tar.gz worldhistory-ed1dea2639b1191421de3986483aedcc14067a12.zip | |
worldhistory: initial public history
Diffstat (limited to 'tests/test_demography.py')
| -rw-r--r-- | tests/test_demography.py | 123 |
1 files changed, 123 insertions, 0 deletions
diff --git a/tests/test_demography.py b/tests/test_demography.py new file mode 100644 index 0000000..26a5f04 --- /dev/null +++ b/tests/test_demography.py @@ -0,0 +1,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 |
