Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests_all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions emod_api/campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
5 changes: 2 additions & 3 deletions emod_api/demographics/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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": []}
Expand Down
26 changes: 13 additions & 13 deletions tests/test_campaign_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 ---

Expand Down
8 changes: 6 additions & 2 deletions tests/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down