Skip to content

Commit 16f5e67

Browse files
committed
Use broadcaster to collect events. Need to expand to include HSI events
1 parent 02278b3 commit 16f5e67

File tree

4 files changed

+299
-0
lines changed

4 files changed

+299
-0
lines changed

src/tlo/events.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import pandas as pd
1313

14+
from tlo.notify import notifier
1415
from tlo.util import convert_chain_links_into_EAV
1516

1617
import copy
@@ -296,7 +297,12 @@ def run(self):
296297
"""Make the event happen."""
297298

298299
# Collect relevant information before event takes place
300+
# If statement outside or inside dispatch notification?
299301
if self.sim.generate_event_chains:
302+
303+
# Dispatch notification that event is about to run
304+
notifier.dispatch("event_about_to_run", data={"target": self.target, "EventName": type(self).__name__})
305+
300306
print_chains, row_before, df_before, mni_row_before, entire_mni_before, mni_instances_before = self.store_chains_to_do_before_event()
301307

302308
self.apply(self.target)
@@ -305,6 +311,11 @@ def run(self):
305311
# Collect event info + meaningful property changes of individuals. Combined, these will constitute a 'link'
306312
# in the individual's event chain.
307313
if self.sim.generate_event_chains and print_chains:
314+
315+
print("About to pass")
316+
# Dispatch notification that event is about to run
317+
notifier.dispatch("event_has_just_ran", data={"target": self.target, "EventName": type(self).__name__})
318+
308319
chain_links = self.store_chains_to_do_after_event(row_before, df_before, mni_row_before, entire_mni_before, mni_instances_before)
309320

310321
if chain_links:
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
from tlo.notify import notifier
2+
3+
from pathlib import Path
4+
from typing import Optional
5+
from tlo import Module, logging, population
6+
from tlo.population import Population
7+
import pandas as pd
8+
9+
from tlo.util import convert_chain_links_into_EAV
10+
11+
import copy
12+
13+
logger = logging.getLogger(__name__)
14+
logger.setLevel(logging.INFO)
15+
16+
class CollectEventChains(Module):
17+
18+
def __init__(self, name=None):
19+
super().__init__(name)
20+
21+
# This is how I am passing data from fnc taking place before event to the one after
22+
# It doesn't seem very elegant but not sure how else to go about it
23+
self.print_chains = False
24+
self.df_before = []
25+
self.row_before = pd.Series()
26+
self.mni_instances_before = False
27+
self.mni_row_before = {}
28+
self.entire_mni_before = {}
29+
30+
def initialise_simulation(self, sim):
31+
notifier.add_listener("event_about_to_run", self.on_notification_event_about_to_run)
32+
notifier.add_listener("event_has_just_ran", self.on_notification_event_has_just_ran)
33+
34+
def read_parameters(self, resourcefilepath: Optional[Path] = None):
35+
pass
36+
37+
def initialise_population(self, population):
38+
pass
39+
40+
def on_birth(self, mother, child):
41+
pass
42+
43+
def on_notification_sim_about_to_start(self,data):
44+
pass
45+
46+
def on_notification_event_about_to_run(self, data):
47+
"""Do this when notified that an event is about to run. This function checks whether this event should be logged as part of the event chains, and if so stored required information before the event has occurred. """
48+
print("This is the data I received ", data)
49+
50+
# Initialise these variables
51+
self.print_chains = False
52+
self.df_before = []
53+
self.row_before = pd.Series()
54+
self.mni_instances_before = False
55+
self.mni_row_before = {}
56+
self.entire_mni_before = {}
57+
58+
print("My Modules")
59+
print(self.sim.modules.keys())
60+
# Only print event if it belongs to modules of interest and if it is not in the list of events to ignore
61+
if all(sub not in str(data['EventName']) for sub in self.sim.generate_event_chains_ignore_events):
62+
63+
# Will eventually use this once I can actually GET THE NAME OF THE SELF
64+
#if not set(self.sim.generate_event_chains_ignore_events).intersection(str(self)):
65+
66+
self.print_chains = True
67+
68+
# Target is single individual
69+
if not isinstance(data["target"], Population):
70+
71+
# Save row for comparison after event has occurred
72+
self.row_before = self.sim.population.props.loc[abs(data['target'])].copy().fillna(-99999)
73+
74+
# Check if individual is already in mni dictionary, if so copy her original status
75+
if 'PregnancySupervisor' in self.sim.modules:
76+
mni = self.sim.modules['PregnancySupervisor'].mother_and_newborn_info
77+
if data['target'] in mni:
78+
self.mni_instances_before = True
79+
self.mni_row_before = mni[data['target']].copy()
80+
else:
81+
self.mni_row_before = None
82+
83+
else:
84+
85+
# This will be a population-wide event. In order to find individuals for which this led to
86+
# a meaningful change, make a copy of the while pop dataframe/mni before the event has occurred.
87+
self.df_before = self.sim.population.props.copy()
88+
if 'PregnancySupervisor' in self.sim.modules:
89+
self.entire_mni_before = copy.deepcopy(self.sim.modules['PregnancySupervisor'].mother_and_newborn_info)
90+
else:
91+
self.entire_mni_before = None
92+
93+
return
94+
95+
96+
def on_notification_event_has_just_ran(self, data):
97+
""" If print_chains=True, this function logs the event and identifies and logs the any property changes that have occured to one or multiple individuals as a result of the event taking place. """
98+
print("This is the data I received ", data)
99+
100+
chain_links = {}
101+
102+
# Target is single individual
103+
if not isinstance(data["target"], Population):
104+
105+
# Copy full new status for individual
106+
row_after = self.sim.population.props.loc[abs(data['target'])].fillna(-99999)
107+
108+
# Check if individual is in mni after the event
109+
mni_instances_after = False
110+
if 'PregnancySupervisor' in self.sim.modules:
111+
mni = self.sim.modules['PregnancySupervisor'].mother_and_newborn_info
112+
if data['target'] in mni:
113+
mni_instances_after = True
114+
else:
115+
mni_instances_after = None
116+
117+
# Create and store event for this individual, regardless of whether any property change occurred
118+
link_info = {
119+
'EventName' : data['EventName'],
120+
}
121+
122+
# Store (if any) property changes as a result of the event for this individual
123+
for key in self.row_before.index:
124+
if self.row_before[key] != row_after[key]: # Note: used fillna previously, so this is safe
125+
link_info[key] = row_after[key]
126+
127+
if 'PregnancySupervisor' in self.sim.modules:
128+
# Now check and store changes in the mni dictionary, accounting for following cases:
129+
# Individual is in mni dictionary before and after
130+
if self.mni_instances_before and mni_instances_after:
131+
for key in self.mni_row_before:
132+
if self.mni_values_differ(mni_row_before[key], mni[data['target']][key]):
133+
link_info[key] = mni[data['target']][key]
134+
# Individual is only in mni dictionary before event
135+
elif self.mni_instances_before and not mni_instances_after:
136+
default = self.sim.modules['PregnancySupervisor'].default_all_mni_values
137+
for key in self.mni_row_before:
138+
if self.mni_values_differ(mni_row_before[key], default[key]):
139+
link_info[key] = default[key]
140+
# Individual is only in mni dictionary after event
141+
elif mni_instances_after and not self.mni_instances_before:
142+
default = self.sim.modules['PregnancySupervisor'].default_all_mni_values
143+
for key in default:
144+
if self.mni_values_differ(default[key], mni[data['target']][key]):
145+
link_info[key] = mni[data['target']][key]
146+
# Else, no need to do anything
147+
148+
# Add individual to the chain links
149+
chain_links[data['target']] = link_info
150+
151+
else:
152+
# Target is entire population. Identify individuals for which properties have changed
153+
# and store their changes.
154+
155+
# Population frame after event
156+
df_after = self.sim.population.props
157+
if 'PregnancySupervisor' in self.sim.modules:
158+
entire_mni_after = copy.deepcopy(self.sim.modules['PregnancySupervisor'].mother_and_newborn_info)
159+
else:
160+
entire_mni_after = None
161+
162+
# Create and store the event and dictionary of changes for affected individuals
163+
chain_links = self.compare_population_dataframe_and_mni(self.df_before, df_after, self.entire_mni_before, entire_mni_after)
164+
165+
if chain_links:
166+
# Convert chain_links into EAV
167+
ednav = convert_chain_links_into_EAV(chain_links)
168+
169+
logger.info(key='event_chains',
170+
data= ednav.to_dict(),
171+
description='Links forming chains of events for simulated individuals')
172+
173+
# Reset variables
174+
self.print_chains = False
175+
self.df_before = []
176+
self.row_before = pd.Series()
177+
self.mni_instances_before = False
178+
self.mni_row_before = {}
179+
self.entire_mni_before = {}
180+
181+
return
182+
183+
def mni_values_differ(self, v1, v2):
184+
185+
if isinstance(v1, list) and isinstance(v2, list):
186+
return v1 != v2 # simple element-wise comparison
187+
188+
if pd.isna(v1) and pd.isna(v2):
189+
return False # treat both NaT/NaN as equal
190+
return v1 != v2
191+
192+
def compare_entire_mni_dicts(self,entire_mni_before, entire_mni_after):
193+
diffs = {}
194+
195+
all_individuals = set(entire_mni_before.keys()) | set(entire_mni_after.keys())
196+
197+
for person in all_individuals:
198+
if person not in entire_mni_before: # but is afterward
199+
for key in entire_mni_after[person]:
200+
if self.mni_values_differ(entire_mni_after[person][key],self.sim.modules['PregnancySupervisor'].default_all_mni_values[key]):
201+
if person not in diffs:
202+
diffs[person] = {}
203+
diffs[person][key] = entire_mni_after[person][key]
204+
205+
elif person not in entire_mni_after: # but is beforehand
206+
for key in entire_mni_before[person]:
207+
if self.mni_values_differ(entire_mni_before[person][key],self.sim.modules['PregnancySupervisor'].default_all_mni_values[key]):
208+
if person not in diffs:
209+
diffs[person] = {}
210+
diffs[person][key] = self.sim.modules['PregnancySupervisor'].default_all_mni_values[key]
211+
212+
else: # person is in both
213+
# Compare properties
214+
for key in entire_mni_before[person]:
215+
if self.mni_values_differ(entire_mni_before[person][key],entire_mni_after[person][key]):
216+
if person not in diffs:
217+
diffs[person] = {}
218+
diffs[person][key] = entire_mni_after[person][key]
219+
220+
return diffs
221+
222+
def compare_population_dataframe_and_mni(self,df_before, df_after, entire_mni_before, entire_mni_after):
223+
""" This function compares the population dataframe and mni dictionary before/after a population-wide event has occurred.
224+
It allows us to identify the individuals for which this event led to a significant (i.e. property) change, and to store the properties which have changed as a result of it. """
225+
226+
# Create a mask of where values are different
227+
diff_mask = (df_before != df_after) & ~(df_before.isna() & df_after.isna())
228+
if 'PregnancySupervisor' in self.sim.modules:
229+
diff_mni = self.compare_entire_mni_dicts(entire_mni_before, entire_mni_after)
230+
else:
231+
diff_mni = []
232+
233+
# Create an empty list to store changes for each of the individuals
234+
chain_links = {}
235+
len_of_diff = len(diff_mask)
236+
237+
# Loop through each row of the mask
238+
persons_changed = []
239+
240+
for idx, row in diff_mask.iterrows():
241+
changed_cols = row.index[row].tolist()
242+
243+
if changed_cols: # Proceed only if there are changes in the row
244+
persons_changed.append(idx)
245+
# Create a dictionary for this person
246+
# First add event info
247+
link_info = {
248+
'EventName': type(self).__name__,
249+
}
250+
251+
# Store the new values from df_after for the changed columns
252+
for col in changed_cols:
253+
link_info[col] = df_after.at[idx, col]
254+
255+
if idx in diff_mni:
256+
# This person has also undergone changes in the mni dictionary, so add these here
257+
for key in diff_mni[idx]:
258+
link_info[col] = diff_mni[idx][key]
259+
260+
# Append the event and changes to the individual key
261+
chain_links[idx] = link_info
262+
263+
if 'PregnancySupervisor' in self.sim.modules:
264+
# For individuals which only underwent changes in mni dictionary, save changes here
265+
if len(diff_mni)>0:
266+
for key in diff_mni:
267+
if key not in persons_changed:
268+
# If individual hadn't been previously added due to changes in pop df, add it here
269+
link_info = {
270+
'EventName': type(self).__name__,
271+
}
272+
273+
for key_prop in diff_mni[key]:
274+
link_info[key_prop] = diff_mni[key][key_prop]
275+
276+
chain_links[key] = link_info
277+
278+
return chain_links
279+
280+
281+

src/tlo/methods/fullmodel.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
cardio_metabolic_disorders,
99
care_of_women_during_pregnancy,
1010
cervical_cancer,
11+
collect_event_chains,
1112
contraception,
1213
copd,
1314
demography,
@@ -116,6 +117,7 @@ def fullmodel(
116117
copd.Copd,
117118
depression.Depression,
118119
epilepsy.Epilepsy,
120+
collect_event_chains.CollectEventChains,
119121
]
120122
return [
121123
module_class(

src/tlo/simulation.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
import pandas as pd
1414
import tlo.population
1515
import numpy as np
16+
import tlo.methods.collect_event_chains
17+
18+
from tlo.notify import notifier
19+
from tlo.methods.collect_event_chains import CollectEventChains
1620
from tlo.util import df_to_EAV, convert_chain_links_into_EAV
1721

1822
try:
@@ -148,6 +152,7 @@ def __init__(
148152

149153
# Whether simulation has been initialised
150154
self._initialised = False
155+
151156

152157
def _configure_logging(
153158
self,

0 commit comments

Comments
 (0)