From 9a58a3d7bd619bc8a0ecaa82142c2c5227126c7e Mon Sep 17 00:00:00 2001 From: Svetlana Titova Date: Tue, 23 Jun 2026 23:01:46 -0700 Subject: [PATCH 1/3] previous release removed IndividualAttributes, but that key is required by EMOD for emod-common/malaria/hiv; renaming get_custom_*_events to validate_* to better reflect their functionality. --- emod_api/campaign.py | 16 ++++++++-------- emod_api/demographics/node.py | 4 +--- tests/test_campaign_module.py | 26 +++++++++++++------------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/emod_api/campaign.py b/emod_api/campaign.py index 86bd6de..a9c5747 100644 --- a/emod_api/campaign.py +++ b/emod_api/campaign.py @@ -217,7 +217,7 @@ def _validate_custom_events(listened_list, broadcast_list, builtin_list, level): return list(broadcast) -def get_custom_coordinator_events(): +def validate_custom_coordinator_events(): """Validate and return deduplicated custom coordinator-level events. Returns: @@ -231,7 +231,7 @@ def get_custom_coordinator_events(): return _validate_custom_events(coordinator_events_listened, coordinator_events_broadcast, coordinator_builtin_events, "coordinator") -def get_custom_node_events(): +def validate_custom_node_events(): """Validate and return deduplicated custom node-level events. Returns: @@ -245,7 +245,7 @@ def get_custom_node_events(): return _validate_custom_events(node_events_listened, node_events_broadcast, node_builtin_events, "node") -def get_custom_individual_events(): +def validate_custom_individual_events(): """Validate and return deduplicated custom individual-level events. Returns: @@ -263,7 +263,7 @@ def get_recv_trigger(trigger, old=use_old_adhoc_handling): """Register an individual-level event as listened to. Tracks which individual events are used throughout the simulation - so that ``get_custom_individual_events`` can validate that every + so that ``validate_custom_individual_events`` can validate that every listened-to event has a corresponding broadcast. Args: @@ -283,7 +283,7 @@ def set_listened_node_event(event: str) -> str: """Register a node-level event as listened to. Tracks which node events are used throughout the simulation so - that ``get_custom_node_events`` can validate that every listened-to + that ``validate_custom_node_events`` can validate that every listened-to event has a corresponding broadcast. Args: @@ -302,7 +302,7 @@ def set_listened_coordinator_event(event: str) -> str: """Register a coordinator-level event as listened to. Tracks which coordinator events are used throughout the simulation - so that ``get_custom_coordinator_events`` can validate that every + so that ``validate_custom_coordinator_events`` can validate that every listened-to event has a corresponding broadcast. Args: @@ -337,7 +337,7 @@ def set_broadcast_node_event(event: str) -> str: """Register a node-level event as broadcast. Tracks which node events are used throughout the simulation so - that ``get_custom_node_events`` can validate that every broadcast + that ``validate_custom_node_events`` can validate that every broadcast event has something listening to it. Args: @@ -356,7 +356,7 @@ def set_broadcast_coordinator_event(event: str) -> str: """Register a coordinator-level event as broadcast. Tracks which coordinator events are used throughout the simulation - so that ``get_custom_coordinator_events`` can validate that every + so that ``validate_custom_coordinator_events`` can validate that every broadcast event has something listening to it. Args: diff --git a/emod_api/demographics/node.py b/emod_api/demographics/node.py index cfcd6e7..8806654 100644 --- a/emod_api/demographics/node.py +++ b/emod_api/demographics/node.py @@ -90,9 +90,7 @@ def to_dict(self) -> dict: "NodeAttributes": self.node_attributes.to_dict()} if self.individual_attributes: - ia_dict = self.individual_attributes.to_dict() - if ia_dict: - d["IndividualAttributes"] = ia_dict + d["IndividualAttributes"] = self.individual_attributes.to_dict() if self.individual_properties: ip_dict = {"IndividualProperties": []} diff --git a/tests/test_campaign_module.py b/tests/test_campaign_module.py index a6e74c1..97b5838 100644 --- a/tests/test_campaign_module.py +++ b/tests/test_campaign_module.py @@ -107,21 +107,21 @@ def test_save(self): data = json.load(f) self.assertDictEqual(data, self.campaign.campaign_dict) - def test_get_custom_individual_events_builtin_excluded(self): + def test_validate_custom_individual_events_builtin_excluded(self): if not self.campaign.individual_builtin_events: self.skipTest("No individual builtin events in schema") builtin_event = self.campaign.individual_builtin_events[0] self.campaign.get_recv_trigger(builtin_event) - result = self.campaign.get_custom_individual_events() + result = self.campaign.validate_custom_individual_events() self.assertNotIn(builtin_event, result) - def test_get_custom_individual_events_broadcast_mirrors_builtin_warns(self): + def test_validate_custom_individual_events_broadcast_mirrors_builtin_warns(self): if not self.campaign.individual_builtin_events: self.skipTest("No individual builtin events in schema") builtin_event = self.campaign.individual_builtin_events[0] self.campaign.get_send_trigger(builtin_event) with self.assertWarns(UserWarning): - self.campaign.get_custom_individual_events() + self.campaign.validate_custom_individual_events() def test_set_schema_populates_node_builtin_events(self): if not self.campaign.node_builtin_events: @@ -244,54 +244,54 @@ def tearDown(self): def test_individual_valid_pair(self): self.campaign.get_recv_trigger("CustomEvt") self.campaign.get_send_trigger("CustomEvt") - result = self.campaign.get_custom_individual_events() + result = self.campaign.validate_custom_individual_events() self.assertIn("CustomEvt", result) def test_individual_listened_not_broadcast_raises(self): self.campaign.get_recv_trigger("OrphanedEvt") with self.assertRaises(ValueError): - self.campaign.get_custom_individual_events() + self.campaign.validate_custom_individual_events() def test_individual_broadcast_not_listened_warns(self): self.campaign.get_send_trigger("UnlistenedEvt") with self.assertWarns(UserWarning): - self.campaign.get_custom_individual_events() + self.campaign.validate_custom_individual_events() # --- node --- def test_node_valid_pair(self): self.campaign.set_listened_node_event("NodeEvt") self.campaign.set_broadcast_node_event("NodeEvt") - result = self.campaign.get_custom_node_events() + result = self.campaign.validate_custom_node_events() self.assertIn("NodeEvt", result) def test_node_listened_not_broadcast_raises(self): self.campaign.set_listened_node_event("OrphanedNodeEvt") with self.assertRaises(ValueError): - self.campaign.get_custom_node_events() + self.campaign.validate_custom_node_events() def test_node_broadcast_not_listened_warns(self): self.campaign.set_broadcast_node_event("UnlistenedNodeEvt") with self.assertWarns(UserWarning): - self.campaign.get_custom_node_events() + self.campaign.validate_custom_node_events() # --- coordinator --- def test_coordinator_valid_pair(self): self.campaign.set_listened_coordinator_event("CoordEvt") self.campaign.set_broadcast_coordinator_event("CoordEvt") - result = self.campaign.get_custom_coordinator_events() + result = self.campaign.validate_custom_coordinator_events() self.assertIn("CoordEvt", result) def test_coordinator_listened_not_broadcast_raises(self): self.campaign.set_listened_coordinator_event("OrphanedCoordEvt") with self.assertRaises(ValueError): - self.campaign.get_custom_coordinator_events() + self.campaign.validate_custom_coordinator_events() def test_coordinator_broadcast_not_listened_warns(self): self.campaign.set_broadcast_coordinator_event("UnlistenedCoordEvt") with self.assertWarns(UserWarning): - self.campaign.get_custom_coordinator_events() + self.campaign.validate_custom_coordinator_events() # --- builtin filtering --- From fecb8117e69de9e43b1f80df5c38bb53d0462e8f Mon Sep 17 00:00:00 2001 From: Svetlana Titova Date: Tue, 23 Jun 2026 23:04:02 -0700 Subject: [PATCH 2/3] too many tests areb eing run --- .github/workflows/tests_all.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests_all.yml b/.github/workflows/tests_all.yml index 14e8625..e6fb08a 100644 --- a/.github/workflows/tests_all.yml +++ b/.github/workflows/tests_all.yml @@ -19,7 +19,7 @@ jobs: max-parallel: 8 matrix: os: [ubuntu-latest, windows-latest] - python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: ['3.10', '3.14'] steps: - name: Checkout repo uses: actions/checkout@v6 From 7ecb2e9ea04d8403f46d6399457e9565ffc2676f Mon Sep 17 00:00:00 2001 From: Svetlana Titova Date: Tue, 23 Jun 2026 23:12:11 -0700 Subject: [PATCH 3/3] fixing for non-Generic EMOD. --- emod_api/demographics/node.py | 1 + tests/test_node.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/emod_api/demographics/node.py b/emod_api/demographics/node.py index 8806654..fd96ef2 100644 --- a/emod_api/demographics/node.py +++ b/emod_api/demographics/node.py @@ -50,6 +50,7 @@ def __init__(self, super().__init__() self.forced_id = forced_id self.meta = meta if meta else {} + # EMOD requires IndividualAttributes in every node, even if empty. self.individual_attributes = individual_attributes if individual_attributes else IndividualAttributes() self.individual_properties = individual_properties if individual_properties else IndividualProperties() diff --git a/tests/test_node.py b/tests/test_node.py index 6c9e44e..d75c9ee 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -93,7 +93,9 @@ def test_set_user_parameter(self): node = Node(lat=0,lon=0,pop=100, individual_attributes=individual_attributes_1) self.assertEqual(node.to_dict()["IndividualAttributes"]["user_defined_2"], 2) node = Node(lat=0,lon=0,pop=100, individual_attributes=individual_attributes_2) - self.assertNotIn("IndividualAttributes", node.to_dict()) + # EMOD requires IndividualAttributes in every node, even if empty + self.assertIn("IndividualAttributes", node.to_dict()) + self.assertNotIn("user_defined_2", node.to_dict()["IndividualAttributes"]) ips = [IndividualProperty(property='cloudy', values=["yes", "no"], initial_distribution=[0.5, 0.5])] individual_properties_1 = IndividualProperties(ips) @@ -118,7 +120,9 @@ def test_extra_node_attributes(self): node_3 = Node(lat=1, lon=2, pop=100, node_attributes=node_attributes, individual_attributes=individual_attributes) self.assertEqual(node_1.to_dict()["NodeAttributes"]["Test_1"], 1) - self.assertTrue("IndividualAttributes" not in node_1.to_dict()) + # EMOD requires IndividualAttributes in every node, even if empty + self.assertIn("IndividualAttributes", node_1.to_dict()) + self.assertNotIn("Test_2", node_1.to_dict()["IndividualAttributes"]) self.assertTrue("Test_1" not in node_2.to_dict()["NodeAttributes"]) self.assertEqual(node_2.to_dict()["IndividualAttributes"]["Test_2"], 2)