44from enum import Enum
55from typing import TYPE_CHECKING
66
7- from tlo import DateOffset , logging
7+ from tlo import DateOffset
88
99if TYPE_CHECKING :
1010 from tlo import Simulation
1111
12- import pandas as pd
13-
1412from 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
3014class 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-
4326class 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
34179class RegularEvent (Event ):
34280 """An event that automatically reschedules itself at a fixed frequency."""
0 commit comments