From 9da5c75c57e12ea79f119e0a955f793134c91ce5 Mon Sep 17 00:00:00 2001 From: Sam Bessey Date: Tue, 22 Feb 2022 10:40:57 -0500 Subject: [PATCH 01/10] feat: add agebin computed attribute This allows agents to be assortatively mixed by age. --- tests/agent_test.py | 6 +++ tests/partnering_test.py | 85 +++++++++++++++++++++++++++++++++++++ titan/agent.py | 6 +++ titan/params/assort_mix.yml | 2 +- titan/params/classes.yml | 22 ++++++++++ 5 files changed, 120 insertions(+), 1 deletion(-) diff --git a/tests/agent_test.py b/tests/agent_test.py index 9f75198c..45c5b122 100644 --- a/tests/agent_test.py +++ b/tests/agent_test.py @@ -257,3 +257,9 @@ def test_clear_set(make_agent): assert s.members == set() assert a not in s assert s.num_members() == 0 + +@pytest.mark.unit +def test_agebin(make_agent): + a = make_agent() + a.age = 30 + assert a.agebin == 1 diff --git a/tests/partnering_test.py b/tests/partnering_test.py index 35190481..1374de7a 100644 --- a/tests/partnering_test.py +++ b/tests/partnering_test.py @@ -583,6 +583,91 @@ def test_get_assort_hiv_prep(make_population, make_agent, params): assert partner == p2 +@pytest.mark.unit +def test_get_assort_agebin_same(make_population, make_agent, params): + pop = make_population() + a = make_agent(age=30) + p1 = make_agent(age=30) + p2 = make_agent(age=66) + for bond in params.classes.bond_types: + a.target_partners[bond] = 1 + p1.target_partners[bond] = 1 + p2.target_partners[bond] = 1 + a.partners[bond] = set() + p1.partners[bond] = set() + p2.partners[bond] = set() + pop.add_agent(a) + pop.add_agent(p1) + pop.add_agent(p2) + + params.features.assort_mix = True + + test_rule = ObjMap( + { + "attribute": "agebin", + "partner_attribute": "__agent__", + "bond_types": ["Sex"], + "agent_value": "__any__", + "partner_values": {"__other__": 0.1, "__same__": 0.9}, + } + ) + params.assort_mix["test_rule"] = test_rule + + partner = select_partner( + a, + pop.all_agents.members, + pop.sex_partners, + pop.pwid_agents, + params, + FakeRandom(0.5), + "Sex", + ) + + assert partner == p1 + + +@pytest.mark.unit +def test_get_assort_agebin_other(make_population, make_agent, params): + pop = make_population() + a = make_agent(age=30) + p1 = make_agent(age=30) + p2 = make_agent(age=16) + for bond in params.classes.bond_types: + a.target_partners[bond] = 1 + p1.target_partners[bond] = 1 + p2.target_partners[bond] = 1 + a.partners[bond] = set() + p1.partners[bond] = set() + p2.partners[bond] = set() + pop.add_agent(a) + pop.add_agent(p1) + pop.add_agent(p2) + + params.features.assort_mix = True + + test_rule = ObjMap( + { + "attribute": "agebin", + "partner_attribute": "__agent__", + "bond_types": ["Sex"], + "agent_value": "__any__", + "partner_values": {"__other__": 0.9, "__same__": 0.1}, + } + ) + params.assort_mix["test_rule"] = test_rule + + partner = select_partner( + a, + pop.all_agents.members, + pop.sex_partners, + pop.pwid_agents, + params, + FakeRandom(0.5), + "Sex", + ) + assert partner == p2 + + @pytest.mark.unit def test_get_assort_bond_type(make_population, make_agent, params): pop = make_population() diff --git a/titan/agent.py b/titan/agent.py index 9b0ac045..ac19773e 100644 --- a/titan/agent.py +++ b/titan/agent.py @@ -109,6 +109,12 @@ def __ne__(self, other) -> bool: def __hash__(self) -> int: return self.id + @property + def agebin(self): + for key, val in self.location.params.classes.age_bins.items(): + if self.age >= val.min_age and self.age < val.max_age: + return key + def iter_partners(self) -> Iterator["Agent"]: """ Get an iterator over an agent's partners diff --git a/titan/params/assort_mix.yml b/titan/params/assort_mix.yml index 4601f9e5..0c5b0734 100644 --- a/titan/params/assort_mix.yml +++ b/titan/params/assort_mix.yml @@ -15,7 +15,7 @@ assort_mix: class: bond_types default: [] agent_value: - description: "Value of agent's attribute which triggers using this rule. Can use the value `__any__` to then allow assoring on partner_values of `__same__` and `__other__`." + description: "Value of agent's attribute which triggers using this rule. Can use the value `__any__` to then allow assorting on partner_values of `__same__` and `__other__`." type: none partner_values: description: "Which partners to select if assortative mixing happens. Expects a sub-key of an attribute value with a value of the probability. `__other__` can be used as an attribute value as a fallthrough option. If the `partner_attribute` is `location`, it is also possible to use the key `__neighbor__` here to indicate a probability of assorting with agents from neighboring locations (vs. `__same__` or `__other__`). All of the probabilities should add to one." diff --git a/titan/params/classes.yml b/titan/params/classes.yml index f55563a8..7c3fb255 100644 --- a/titan/params/classes.yml +++ b/titan/params/classes.yml @@ -185,3 +185,25 @@ classes: default: replace: enter_type: replace + age_bins: + type: definition + description: bin of ages, mainly used for assortative mixing + fields: + min_age: + description: lower bound of this "bin" + type: int + min: 0 + max_age: + description: upper bound of this "bin" + type: int + min: 0 + default: + 0: + min_age: 16 + max_age: 25 + 1: + min_age: 25 + max_age: 45 + 2: + min_age: 45 + max_age: 65 From 300aabc8822fa4394aa4f8fa194de8b32df48f7f Mon Sep 17 00:00:00 2001 From: Sam Bessey Date: Tue, 22 Feb 2022 10:42:34 -0500 Subject: [PATCH 02/10] update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ad4c61ea..d4cd32e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "titan-model" -version = "3.0.0" +version = "3.1.0" description = "TITAN Agent Based Model" license = "GPL-3.0-only" authors = ["Sam Bessey ", "Mary McGrath "] From ac187f9fc75a14ea330001b872802a633adef281 Mon Sep 17 00:00:00 2001 From: Sam Bessey Date: Tue, 22 Feb 2022 11:55:25 -0500 Subject: [PATCH 03/10] style: run black --- tests/agent_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/agent_test.py b/tests/agent_test.py index 45c5b122..44fc684c 100644 --- a/tests/agent_test.py +++ b/tests/agent_test.py @@ -258,6 +258,7 @@ def test_clear_set(make_agent): assert a not in s assert s.num_members() == 0 + @pytest.mark.unit def test_agebin(make_agent): a = make_agent() From 3bc33d46c9d48388d13b9a5c7aa6e68c0ac372fa Mon Sep 17 00:00:00 2001 From: Sam Bessey Date: Wed, 23 Feb 2022 15:41:49 -0500 Subject: [PATCH 04/10] style: make age_bin consistent --- tests/agent_test.py | 4 ++-- tests/partnering_test.py | 8 ++++---- titan/agent.py | 6 ++++-- titan/params/classes.yml | 6 +++--- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/agent_test.py b/tests/agent_test.py index 44fc684c..47368444 100644 --- a/tests/agent_test.py +++ b/tests/agent_test.py @@ -260,7 +260,7 @@ def test_clear_set(make_agent): @pytest.mark.unit -def test_agebin(make_agent): +def test_age_bin(make_agent): a = make_agent() a.age = 30 - assert a.agebin == 1 + assert a.age_bin == 1 diff --git a/tests/partnering_test.py b/tests/partnering_test.py index 1374de7a..41bf2798 100644 --- a/tests/partnering_test.py +++ b/tests/partnering_test.py @@ -584,7 +584,7 @@ def test_get_assort_hiv_prep(make_population, make_agent, params): @pytest.mark.unit -def test_get_assort_agebin_same(make_population, make_agent, params): +def test_get_assort_age_bin_same(make_population, make_agent, params): pop = make_population() a = make_agent(age=30) p1 = make_agent(age=30) @@ -604,7 +604,7 @@ def test_get_assort_agebin_same(make_population, make_agent, params): test_rule = ObjMap( { - "attribute": "agebin", + "attribute": "age_bin", "partner_attribute": "__agent__", "bond_types": ["Sex"], "agent_value": "__any__", @@ -627,7 +627,7 @@ def test_get_assort_agebin_same(make_population, make_agent, params): @pytest.mark.unit -def test_get_assort_agebin_other(make_population, make_agent, params): +def test_get_assort_age_bin_other(make_population, make_agent, params): pop = make_population() a = make_agent(age=30) p1 = make_agent(age=30) @@ -647,7 +647,7 @@ def test_get_assort_agebin_other(make_population, make_agent, params): test_rule = ObjMap( { - "attribute": "agebin", + "attribute": "age_bin", "partner_attribute": "__agent__", "bond_types": ["Sex"], "agent_value": "__any__", diff --git a/titan/agent.py b/titan/agent.py index ac19773e..e0719caa 100644 --- a/titan/agent.py +++ b/titan/agent.py @@ -110,10 +110,12 @@ def __hash__(self) -> int: return self.id @property - def agebin(self): + def age_bin(self): for key, val in self.location.params.classes.age_bins.items(): - if self.age >= val.min_age and self.age < val.max_age: + if self.age >= val.min_age and self.age <= val.max_age: return key + # If not in an established bin, return "next" bin + return key + 1 def iter_partners(self) -> Iterator["Agent"]: """ diff --git a/titan/params/classes.yml b/titan/params/classes.yml index 7c3fb255..a215a1cb 100644 --- a/titan/params/classes.yml +++ b/titan/params/classes.yml @@ -187,7 +187,7 @@ classes: enter_type: replace age_bins: type: definition - description: bin of ages, mainly used for assortative mixing + description: bin of ages into categories for use in assortative mixing by age fields: min_age: description: lower bound of this "bin" @@ -200,10 +200,10 @@ classes: default: 0: min_age: 16 - max_age: 25 + max_age: 24 1: min_age: 25 - max_age: 45 + max_age: 44 2: min_age: 45 max_age: 65 From c4bbead84f44ca794c1d4985344100c7be5a1b6e Mon Sep 17 00:00:00 2001 From: Sam Bessey Date: Wed, 23 Feb 2022 15:42:43 -0500 Subject: [PATCH 05/10] test: add age bin class to test params --- tests/params/basic.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/params/basic.yml b/tests/params/basic.yml index 537e2cd1..d69c32fa 100644 --- a/tests/params/basic.yml +++ b/tests/params/basic.yml @@ -391,3 +391,13 @@ classes: new_ag: enter_type: new_agent prob: 1.0 + age_bins: + 0: + min_age: 16 + max_age: 24 + 1: + min_age: 25 + max_age: 44 + 2: + min_age: 45 + max_age: 65 From fd876490d11a59e54b09724d0919d5dbabcac69d Mon Sep 17 00:00:00 2001 From: Sam Bessey Date: Mon, 28 Feb 2022 10:03:10 -0500 Subject: [PATCH 06/10] test: add test for age_bin erroring --- tests/agent_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/agent_test.py b/tests/agent_test.py index 47368444..7846b97c 100644 --- a/tests/agent_test.py +++ b/tests/agent_test.py @@ -264,3 +264,10 @@ def test_age_bin(make_agent): a = make_agent() a.age = 30 assert a.age_bin == 1 + +@pytest.mark.unit +def test_age_bin_error(make_agent): + a = make_agent() + a.age = -1 + with pytest.raises(ValueError): + a.age_bin From 5d6caa7c4a194b68aa7ea3da53b7814722a5e587 Mon Sep 17 00:00:00 2001 From: Sam Bessey Date: Mon, 28 Feb 2022 10:03:29 -0500 Subject: [PATCH 07/10] fix: revert to min and max for age bins --- tests/params/basic.yml | 5 ++++- titan/agent.py | 4 ++-- titan/params/classes.yml | 12 ++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/params/basic.yml b/tests/params/basic.yml index d69c32fa..5748d1f1 100644 --- a/tests/params/basic.yml +++ b/tests/params/basic.yml @@ -392,6 +392,9 @@ classes: enter_type: new_agent prob: 1.0 age_bins: + min: + min_age: 0 + max_age: 15 0: min_age: 16 max_age: 24 @@ -400,4 +403,4 @@ classes: max_age: 44 2: min_age: 45 - max_age: 65 + max_age: 999 diff --git a/titan/agent.py b/titan/agent.py index e0719caa..3f394f78 100644 --- a/titan/agent.py +++ b/titan/agent.py @@ -114,8 +114,8 @@ def age_bin(self): for key, val in self.location.params.classes.age_bins.items(): if self.age >= val.min_age and self.age <= val.max_age: return key - # If not in an established bin, return "next" bin - return key + 1 + raise ValueError(f"Agent age {self.age} must be in age_bins") + def iter_partners(self) -> Iterator["Agent"]: """ diff --git a/titan/params/classes.yml b/titan/params/classes.yml index a215a1cb..48deb770 100644 --- a/titan/params/classes.yml +++ b/titan/params/classes.yml @@ -187,14 +187,14 @@ classes: enter_type: replace age_bins: type: definition - description: bin of ages into categories for use in assortative mixing by age + description: bin of ages into categories for use in assortative mixing by age. Will error if agent age is not in a bin. fields: min_age: - description: lower bound of this "bin" + description: lower bound of this "bin". Inclusive type: int min: 0 max_age: - description: upper bound of this "bin" + description: upper bound of this "bin". Inclusive type: int min: 0 default: @@ -203,7 +203,7 @@ classes: max_age: 24 1: min_age: 25 - max_age: 44 + max_age: 54 2: - min_age: 45 - max_age: 65 + min_age: 55 + max_age: 9999 From 7b19ee58815a591e210f5aed5397d5321de7a326 Mon Sep 17 00:00:00 2001 From: Sam Bessey Date: Mon, 28 Feb 2022 10:32:50 -0500 Subject: [PATCH 08/10] style: black --- tests/agent_test.py | 1 + titan/agent.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/agent_test.py b/tests/agent_test.py index 7846b97c..c1245da0 100644 --- a/tests/agent_test.py +++ b/tests/agent_test.py @@ -265,6 +265,7 @@ def test_age_bin(make_agent): a.age = 30 assert a.age_bin == 1 + @pytest.mark.unit def test_age_bin_error(make_agent): a = make_agent() diff --git a/titan/agent.py b/titan/agent.py index 3f394f78..f5bd7172 100644 --- a/titan/agent.py +++ b/titan/agent.py @@ -116,7 +116,6 @@ def age_bin(self): return key raise ValueError(f"Agent age {self.age} must be in age_bins") - def iter_partners(self) -> Iterator["Agent"]: """ Get an iterator over an agent's partners From a0df8b14a39a21cc0676cb966b3da15859635a41 Mon Sep 17 00:00:00 2001 From: Sam Bessey Date: Mon, 28 Feb 2022 10:55:21 -0500 Subject: [PATCH 09/10] docs: update descriptions for new params; clarify age_bin value error --- titan/agent.py | 4 +++- titan/params/classes.yml | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/titan/agent.py b/titan/agent.py index f5bd7172..7b8db7e4 100644 --- a/titan/agent.py +++ b/titan/agent.py @@ -114,7 +114,9 @@ def age_bin(self): for key, val in self.location.params.classes.age_bins.items(): if self.age >= val.min_age and self.age <= val.max_age: return key - raise ValueError(f"Agent age {self.age} must be in age_bins") + raise ValueError( + f"Agent age ({self.age}) must be within an age bin [params.classes.age_bins]" + ) def iter_partners(self) -> Iterator["Agent"]: """ diff --git a/titan/params/classes.yml b/titan/params/classes.yml index 48deb770..1243c161 100644 --- a/titan/params/classes.yml +++ b/titan/params/classes.yml @@ -187,19 +187,19 @@ classes: enter_type: replace age_bins: type: definition - description: bin of ages into categories for use in assortative mixing by age. Will error if agent age is not in a bin. + description: Bin of ages into categories for use in assortative mixing by age. Will error if the agent's age is not in a bin. fields: min_age: - description: lower bound of this "bin". Inclusive + description: Lower bound (inclusive) of this "bin". type: int min: 0 max_age: - description: upper bound of this "bin". Inclusive + description: Upper bound (inclusive) of this "bin". type: int min: 0 default: 0: - min_age: 16 + min_age: 0 max_age: 24 1: min_age: 25 From 524cc786aada8cd0c78f480fc9fba5a1cd1d5032 Mon Sep 17 00:00:00 2001 From: Sam Bessey Date: Mon, 28 Feb 2022 10:58:21 -0500 Subject: [PATCH 10/10] test: change age bin names to string in test --- tests/agent_test.py | 2 +- tests/params/basic.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/agent_test.py b/tests/agent_test.py index c1245da0..a87906d1 100644 --- a/tests/agent_test.py +++ b/tests/agent_test.py @@ -263,7 +263,7 @@ def test_clear_set(make_agent): def test_age_bin(make_agent): a = make_agent() a.age = 30 - assert a.age_bin == 1 + assert a.age_bin == "mid_adult" @pytest.mark.unit diff --git a/tests/params/basic.yml b/tests/params/basic.yml index 5748d1f1..60443957 100644 --- a/tests/params/basic.yml +++ b/tests/params/basic.yml @@ -392,15 +392,15 @@ classes: enter_type: new_agent prob: 1.0 age_bins: - min: + fallback_young: min_age: 0 max_age: 15 - 0: + young_adult: min_age: 16 max_age: 24 - 1: + mid_adult: min_age: 25 max_age: 44 - 2: + fallback_old: min_age: 45 max_age: 999