aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/tests/test_demography.py
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 00:01:14 +0200
committergodosa <godosa@godosa.eu>2026-10-07 00:01:14 +0200
commited1dea2639b1191421de3986483aedcc14067a12 (patch)
tree0118c6e119a84f1a9433ed0a304c8be1c9098463 /tests/test_demography.py
downloadworldhistory-ed1dea2639b1191421de3986483aedcc14067a12.tar.gz
worldhistory-ed1dea2639b1191421de3986483aedcc14067a12.zip
worldhistory: initial public history
Diffstat (limited to 'tests/test_demography.py')
-rw-r--r--tests/test_demography.py123
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