1515
1616from collections import defaultdict
1717from pathlib import Path
18- from typing import TYPE_CHECKING , List , Optional , Sequence , Union
18+ from typing import TYPE_CHECKING , List , Optional , Sequence , Set
1919
2020import numpy as np
2121import pandas as pd
2626from tlo .util import BitsetHandler
2727
2828if TYPE_CHECKING :
29+ from typing import Union
30+
2931 from tlo .population import IndividualProperties
3032
3133logger = logging .getLogger (__name__ )
3234logger .setLevel (logging .INFO )
3335
36+
3437# ---------------------------------------------------------------------------------------------------------
3538# MODULE DEFINITIONS
3639# ---------------------------------------------------------------------------------------------------------
@@ -218,6 +221,7 @@ def __init__(self, name=None, spurious_symptoms=None):
218221
219222 self .recognised_module_names = None
220223 self .spurious_symptom_resolve_event = None
224+ self .symptom_tracker = defaultdict (set )
221225
222226 def get_column_name_for_symptom (self , symptom_name ):
223227 """get the column name that corresponds to the symptom_name"""
@@ -395,6 +399,10 @@ def change_symptom(self, person_id, symptom_string, add_or_remove, disease_modul
395399 self .bsh .set (person_id , disease_module .name , columns = sy_columns )
396400 self ._persons_with_newly_onset_symptoms = self ._persons_with_newly_onset_symptoms .union (person_id )
397401
402+ # Update symptom tracker
403+ for pid in person_id :
404+ self .symptom_tracker [pid ] |= set (symptom_string )
405+
398406 # If a duration is given, schedule the auto-resolve event to turn off these symptoms after specified time.
399407 if duration_in_days is not None :
400408 auto_resolve_event = SymptomManager_AutoResolveEvent (self ,
@@ -417,10 +425,17 @@ def change_symptom(self, person_id, symptom_string, add_or_remove, disease_modul
417425 # Do the remove:
418426 self .bsh .unset (person_id , disease_module .name , columns = sy_columns )
419427
428+ # Update symptom tracker. Remove if no other module is causing this symptom.
429+ for pid in person_id :
430+ for sym in symptom_string :
431+ symptom_col = self .get_column_name_for_symptom (sym )
432+ if self .bsh .is_empty (pid , columns = symptom_col ):
433+ self .symptom_tracker [pid ].discard (sym )
434+
420435 def who_has (self , list_of_symptoms ):
421436 """
422437 This is a helper function to look up who has a particular symptom or set of symptoms.
423- It returns a list of indicies for person that have all of the symptoms specified
438+ It returns a list of indices for person that has all of the symptoms specified
424439
425440 :param: list_of_symptoms : string or list of strings for the symptoms of interest
426441 :return: list of person_ids for those with all of the symptoms in list_of_symptoms who are alive
@@ -462,7 +477,7 @@ def who_not_have(self, symptom_string: str) -> pd.Index:
462477 & self .bsh .is_empty (
463478 slice (None ), columns = self .get_column_name_for_symptom (symptom_string )
464479 )
465- ]
480+ ]
466481
467482 def has_what (
468483 self ,
@@ -492,6 +507,10 @@ def has_what(
492507 else True
493508 ), "Disease Module Name is not recognised"
494509
510+ # Faster to get current symptoms using tracker when no disease is specified
511+ if disease_module is None and person_id is not None :
512+ return list (self ._get_current_symptoms_from_tracker (person_id ))
513+
495514 if individual_details is not None :
496515 # We are working in an IndividualDetails context, avoid lookups to the
497516 # population DataFrame as we have this context stored already.
@@ -503,10 +522,10 @@ def has_what(
503522 symptom
504523 for symptom in self .symptom_names
505524 if individual_details [
506- self .bsh ._get_columns (self .get_column_name_for_symptom (symptom ))
507- ]
508- & int_repr
509- != 0
525+ self .bsh ._get_columns (self .get_column_name_for_symptom (symptom ))
526+ ]
527+ & int_repr
528+ != 0
510529 ]
511530 else :
512531 return [
@@ -582,6 +601,17 @@ def clear_symptoms(self, person_id: Union[int, Sequence[int]], disease_module: M
582601 sy_columns = [self .get_column_name_for_symptom (sym ) for sym in self .symptom_names ]
583602 self .bsh .unset (person_id , disease_module .name , columns = sy_columns )
584603
604+ # Update bookkeeping
605+ for pid in person_id :
606+ for sym in self .symptom_names :
607+ symptom_col = self .get_column_name_for_symptom (sym )
608+ if self .bsh .is_empty (pid , columns = symptom_col ):
609+ self .symptom_tracker [pid ].discard (sym )
610+
611+ # Remove the person's entry from the tracker is the symptom set is empty
612+ if pid in self .symptom_tracker and not self .symptom_tracker [pid ]:
613+ del self .symptom_tracker [pid ]
614+
585615 def caused_by (self , disease_module : Module ):
586616 """Find the persons experiencing symptoms due to a particular module.
587617 Returns a dict of the form {<<person_id>>, <<list_of_symptoms>>}."""
@@ -600,6 +630,17 @@ def get_persons_with_newly_onset_symptoms(self):
600630 def reset_persons_with_newly_onset_symptoms (self ):
601631 self ._persons_with_newly_onset_symptoms .clear ()
602632
633+ def _get_current_symptoms_from_tracker (self , person_id : int ) -> Set [str ]:
634+ """Get the current symptoms for a person. Works with bookkeeping dictionary"""
635+ return self .symptom_tracker .get (person_id , set ())
636+
637+ def clear_symptoms_for_deceased_person (self , person_id : int ):
638+ """Clears symptoms by deleting the dead person's ID in the tracker"""
639+ # Remove person from tracker entirely
640+ if person_id in self .symptom_tracker :
641+ del self .symptom_tracker [person_id ]
642+
643+
603644# ---------------------------------------------------------------------------------------------------------
604645# EVENTS
605646# ---------------------------------------------------------------------------------------------------------
@@ -696,22 +737,21 @@ def apply(self, population):
696737 do_not_have_symptom = self .module .who_not_have (symptom_string = symp )
697738
698739 for group in ['children' , 'adults' ]:
699-
700740 p = self .generic_symptoms ['prob_per_day' ][group ][symp ]
701741 dur = self .generic_symptoms ['duration_in_days' ][group ][symp ]
702742 persons_eligible_to_get_symptom = group_indices [group ][
703743 group_indices [group ].isin (do_not_have_symptom )
704744 ]
705745 persons_to_onset_with_this_symptom = persons_eligible_to_get_symptom [
706746 self .rand (len (persons_eligible_to_get_symptom )) < p
707- ]
747+ ]
708748
709749 # Do onset
710750 self .sim .modules ['SymptomManager' ].change_symptom (
711751 symptom_string = symp ,
712752 add_or_remove = '+' ,
713753 person_id = persons_to_onset_with_this_symptom ,
714- duration_in_days = None , # <- resolution for these is handled by the SpuriousSymptomsResolve Event
754+ duration_in_days = None , # <- resolution for these is handled by the SpuriousSymptomsResolve Event
715755 disease_module = self .module ,
716756 )
717757
0 commit comments