Skip to content

Commit ebe0ebc

Browse files
committed
Use broadcasting in HSI events too
1 parent 16f5e67 commit ebe0ebc

File tree

5 files changed

+209
-583
lines changed

5 files changed

+209
-583
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:f68e30f87dbe757b98cea2658c8f0c40cab629c4b6825a012ce90e12a27bc612
3+
size 102

src/tlo/events.py

Lines changed: 7 additions & 269 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,12 @@
44
from enum import Enum
55
from typing import TYPE_CHECKING
66

7-
from tlo import DateOffset, logging
7+
from tlo import DateOffset
88

99
if TYPE_CHECKING:
1010
from tlo import Simulation
1111

12-
import pandas as pd
13-
1412
from tlo.notify import notifier
15-
from tlo.util import convert_chain_links_into_EAV
16-
17-
import copy
18-
19-
logger = logging.getLogger(__name__)
20-
logger.setLevel(logging.INFO)
21-
22-
logger_chain = logging.getLogger('tlo.simulation')
23-
logger_chain.setLevel(logging.INFO)
24-
25-
logger_summary = logging.getLogger(f"{__name__}.summary")
26-
logger_summary.setLevel(logging.INFO)
27-
28-
debug_chains = True
2913

3014
class Priority(Enum):
3115
"""Enumeration for the Priority, which is used in sorting the events in the simulation queue."""
@@ -39,7 +23,6 @@ def __lt__(self, other):
3923
return self.value < other.value
4024
return NotImplemented
4125

42-
4326
class Event:
4427
"""Base event class, from which all others inherit.
4528
@@ -78,265 +61,20 @@ def apply(self, target):
7861
"""
7962
raise NotImplementedError
8063

81-
def mni_values_differ(self, v1, v2):
82-
83-
if isinstance(v1, list) and isinstance(v2, list):
84-
return v1 != v2 # simple element-wise comparison
85-
86-
if pd.isna(v1) and pd.isna(v2):
87-
return False # treat both NaT/NaN as equal
88-
return v1 != v2
89-
90-
def compare_entire_mni_dicts(self,entire_mni_before, entire_mni_after):
91-
diffs = {}
92-
93-
all_individuals = set(entire_mni_before.keys()) | set(entire_mni_after.keys())
94-
95-
for person in all_individuals:
96-
if person not in entire_mni_before: # but is afterward
97-
for key in entire_mni_after[person]:
98-
if self.mni_values_differ(entire_mni_after[person][key],self.sim.modules['PregnancySupervisor'].default_all_mni_values[key]):
99-
if person not in diffs:
100-
diffs[person] = {}
101-
diffs[person][key] = entire_mni_after[person][key]
102-
103-
elif person not in entire_mni_after: # but is beforehand
104-
for key in entire_mni_before[person]:
105-
if self.mni_values_differ(entire_mni_before[person][key],self.sim.modules['PregnancySupervisor'].default_all_mni_values[key]):
106-
if person not in diffs:
107-
diffs[person] = {}
108-
diffs[person][key] = self.sim.modules['PregnancySupervisor'].default_all_mni_values[key]
109-
110-
else: # person is in both
111-
# Compare properties
112-
for key in entire_mni_before[person]:
113-
if self.mni_values_differ(entire_mni_before[person][key],entire_mni_after[person][key]):
114-
if person not in diffs:
115-
diffs[person] = {}
116-
diffs[person][key] = entire_mni_after[person][key]
117-
118-
return diffs
119-
120-
def compare_population_dataframe_and_mni(self,df_before, df_after, entire_mni_before, entire_mni_after):
121-
""" This function compares the population dataframe and mni dictionary before/after a population-wide event has occurred.
122-
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. """
123-
124-
# Create a mask of where values are different
125-
diff_mask = (df_before != df_after) & ~(df_before.isna() & df_after.isna())
126-
if 'PregnancySupervisor' in self.sim.modules:
127-
diff_mni = self.compare_entire_mni_dicts(entire_mni_before, entire_mni_after)
128-
else:
129-
diff_mni = []
130-
131-
# Create an empty list to store changes for each of the individuals
132-
chain_links = {}
133-
len_of_diff = len(diff_mask)
134-
135-
# Loop through each row of the mask
136-
persons_changed = []
137-
138-
for idx, row in diff_mask.iterrows():
139-
changed_cols = row.index[row].tolist()
140-
141-
if changed_cols: # Proceed only if there are changes in the row
142-
persons_changed.append(idx)
143-
# Create a dictionary for this person
144-
# First add event info
145-
link_info = {
146-
'EventName': type(self).__name__,
147-
}
148-
149-
# Store the new values from df_after for the changed columns
150-
for col in changed_cols:
151-
link_info[col] = df_after.at[idx, col]
152-
153-
if idx in diff_mni:
154-
# This person has also undergone changes in the mni dictionary, so add these here
155-
for key in diff_mni[idx]:
156-
link_info[col] = diff_mni[idx][key]
157-
158-
# Append the event and changes to the individual key
159-
chain_links[idx] = link_info
160-
161-
if 'PregnancySupervisor' in self.sim.modules:
162-
# For individuals which only underwent changes in mni dictionary, save changes here
163-
if len(diff_mni)>0:
164-
for key in diff_mni:
165-
if key not in persons_changed:
166-
# If individual hadn't been previously added due to changes in pop df, add it here
167-
link_info = {
168-
'EventName': type(self).__name__,
169-
}
170-
171-
for key_prop in diff_mni[key]:
172-
link_info[key_prop] = diff_mni[key][key_prop]
173-
174-
chain_links[key] = link_info
175-
176-
return chain_links
177-
178-
179-
def store_chains_to_do_before_event(self) -> tuple[bool, pd.Series, pd.DataFrame, dict, dict, bool]:
180-
""" 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. """
181-
182-
# Initialise these variables
183-
print_chains = False
184-
df_before = []
185-
row_before = pd.Series()
186-
mni_instances_before = False
187-
mni_row_before = {}
188-
entire_mni_before = {}
189-
190-
# Only print event if it belongs to modules of interest and if it is not in the list of events to ignore
191-
if all(sub not in str(self) for sub in self.sim.generate_event_chains_ignore_events):
192-
193-
# Will eventually use this once I can actually GET THE NAME OF THE SELF
194-
#if not set(self.sim.generate_event_chains_ignore_events).intersection(str(self)):
195-
196-
print_chains = True
197-
198-
# Target is single individual
199-
if self.target != self.sim.population:
200-
201-
# Save row for comparison after event has occurred
202-
row_before = self.sim.population.props.loc[abs(self.target)].copy().fillna(-99999)
203-
204-
# Check if individual is already in mni dictionary, if so copy her original status
205-
if 'PregnancySupervisor' in self.sim.modules:
206-
mni = self.sim.modules['PregnancySupervisor'].mother_and_newborn_info
207-
if self.target in mni:
208-
mni_instances_before = True
209-
mni_row_before = mni[self.target].copy()
210-
else:
211-
mni_row_before = None
212-
213-
else:
214-
215-
# This will be a population-wide event. In order to find individuals for which this led to
216-
# a meaningful change, make a copy of the while pop dataframe/mni before the event has occurred.
217-
df_before = self.sim.population.props.copy()
218-
if 'PregnancySupervisor' in self.sim.modules:
219-
entire_mni_before = copy.deepcopy(self.sim.modules['PregnancySupervisor'].mother_and_newborn_info)
220-
else:
221-
entire_mni_before = None
222-
223-
return print_chains, row_before, df_before, mni_row_before, entire_mni_before, mni_instances_before
224-
225-
def store_chains_to_do_after_event(self, row_before, df_before, mni_row_before, entire_mni_before, mni_instances_before) -> dict:
226-
""" 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. """
227-
228-
chain_links = {}
229-
230-
# Target is single individual
231-
if self.target != self.sim.population:
232-
233-
# Copy full new status for individual
234-
row_after = self.sim.population.props.loc[abs(self.target)].fillna(-99999)
235-
236-
# Check if individual is in mni after the event
237-
mni_instances_after = False
238-
if 'PregnancySupervisor' in self.sim.modules:
239-
mni = self.sim.modules['PregnancySupervisor'].mother_and_newborn_info
240-
if self.target in mni:
241-
mni_instances_after = True
242-
else:
243-
mni_instances_after = None
244-
245-
# Create and store event for this individual, regardless of whether any property change occurred
246-
link_info = {
247-
'EventName' : type(self).__name__,
248-
}
249-
250-
# Store (if any) property changes as a result of the event for this individual
251-
for key in row_before.index:
252-
if row_before[key] != row_after[key]: # Note: used fillna previously, so this is safe
253-
link_info[key] = row_after[key]
254-
255-
if 'PregnancySupervisor' in self.sim.modules:
256-
# Now check and store changes in the mni dictionary, accounting for following cases:
257-
# Individual is in mni dictionary before and after
258-
if mni_instances_before and mni_instances_after:
259-
for key in mni_row_before:
260-
if self.mni_values_differ(mni_row_before[key], mni[self.target][key]):
261-
link_info[key] = mni[self.target][key]
262-
# Individual is only in mni dictionary before event
263-
elif mni_instances_before and not mni_instances_after:
264-
default = self.sim.modules['PregnancySupervisor'].default_all_mni_values
265-
for key in mni_row_before:
266-
if self.mni_values_differ(mni_row_before[key], default[key]):
267-
link_info[key] = default[key]
268-
# Individual is only in mni dictionary after event
269-
elif mni_instances_after and not mni_instances_before:
270-
default = self.sim.modules['PregnancySupervisor'].default_all_mni_values
271-
for key in default:
272-
if self.mni_values_differ(default[key], mni[self.target][key]):
273-
link_info[key] = mni[self.target][key]
274-
# Else, no need to do anything
275-
276-
# Add individual to the chain links
277-
chain_links[self.target] = link_info
278-
279-
else:
280-
# Target is entire population. Identify individuals for which properties have changed
281-
# and store their changes.
282-
283-
# Population frame after event
284-
df_after = self.sim.population.props
285-
if 'PregnancySupervisor' in self.sim.modules:
286-
entire_mni_after = copy.deepcopy(self.sim.modules['PregnancySupervisor'].mother_and_newborn_info)
287-
else:
288-
entire_mni_after = None
289-
290-
# Create and store the event and dictionary of changes for affected individuals
291-
chain_links = self.compare_population_dataframe_and_mni(df_before, df_after, entire_mni_before, entire_mni_after)
292-
293-
return chain_links
294-
29564

29665
def run(self):
29766
"""Make the event happen."""
29867

299-
# Collect relevant information before event takes place
300-
# If statement outside or inside dispatch notification?
301-
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-
306-
print_chains, row_before, df_before, mni_row_before, entire_mni_before, mni_instances_before = self.store_chains_to_do_before_event()
68+
69+
# Dispatch notification that event is about to run
70+
notifier.dispatch("event.about_to_run", data={"target": self.target, "link_info" : {"EventName": type(self).__name__}})
30771

30872
self.apply(self.target)
30973
self.post_apply_hook()
31074

311-
# Collect event info + meaningful property changes of individuals. Combined, these will constitute a 'link'
312-
# in the individual's event chain.
313-
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-
319-
chain_links = self.store_chains_to_do_after_event(row_before, df_before, mni_row_before, entire_mni_before, mni_instances_before)
320-
321-
if chain_links:
322-
# Convert chain_links into EAV
323-
ednav = convert_chain_links_into_EAV(chain_links)
324-
325-
logger_chain.info(key='event_chains',
326-
data= ednav.to_dict(),
327-
description='Links forming chains of events for simulated individuals')
328-
"""
329-
# Create empty logger for entire pop
330-
pop_dict = {i: '' for i in range(FACTOR_POP_DICT)} # Always include all possible individuals
331-
pop_dict.update(chain_links)
332-
333-
# Log chain_links here
334-
if len(chain_links)>0:
335-
336-
logger_chain.info(key='event_chains',
337-
data= pop_dict,
338-
description='Links forming chains of events for simulated individuals')
339-
"""
75+
# Dispatch notification that event has just ran
76+
notifier.dispatch("event.has_just_ran", data={"target": self.target, "link_info" : {"EventName": type(self).__name__}})
77+
34078

34179
class RegularEvent(Event):
34280
"""An event that automatically reschedules itself at a fixed frequency."""

0 commit comments

Comments
 (0)