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 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..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() @@ -90,9 +91,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 --- 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)