class Epidemic: """A class for modelling and simulating epidemics.""" def __init__(self, epidemic_params): """The constructor of the class. Full params' list: - nr_individuals: number of individuals in the population - initial_infects: number of infects at the start of the simulation - initial_immunes: number of immunes at the start of the simulation - infect_prob: probability of getting infected after a contact with an infect - contact_rate: rate of the contact between individuals - recover_rate: rate of recover from infection - immune_after_recovery: if true, individuals becomes immunes after recovery - immunization_vanish_rate: if not zero, immunization is temporary with given rate - death_rate: rate of death caused by epidemic - newborn_can_be_infect: if true, newborn can be infect - newborn_can_be_immune: if true, newborn can be immune - newborn_prob: probability that a new individual born after a contact - natural_death_prob: probability of death not caused by epidemic - run_time: time duration of the simulation - debug: show more info about the running simulation - process_debug: show more info about SimPy processes during the simulation - progress: show a progress indicator during the simulation - stats: show some stats at the end of the simulation - plot: show a plot representing the evolution of the population at the end of the simulation """ # setting up the epidemic's parameters with metaprogramming for param in epidemic_params: self.__dict__[param] = epidemic_params.get(param) # setting the uninitialized parameters to their default values self.check_and_set_default_value(['initial_immunes', 'recover_rate', 'death_rate', 'immunization_vanish_rate', 'newborn_prob', 'natural_death_prob'], ['immune_after_recovery', 'newborn_can_be_immune', 'newborn_can_be_infect', 'debug', 'process_debug', 'progress', 'stats', 'plot']) # setting the random number generator using the python standard one self.rng = random.Random() # checking some features of the model from parameters passed to the constructor self.model_has_immunization = self.model_has_immunization() self.model_immunization_is_permanent = self.model_immunization_is_permanent() self.model_has_recovering = self.model_has_recovering() self.model_has_death = self.model_has_death() self.model_has_vital_dynamics = self.model_has_vital_dynamics() self.model_newborns_always_susceptibles = self.model_newborns_always_susceptibles() self.model_has_new_susceptibles = self.model_has_new_susceptibles() self.model_has_new_infects = self.model_has_new_infects() # initialize the population counters self.total_infects = self.initial_infects self.total_immunes = self.initial_immunes self.total_susceptibles = self.nr_individuals - self.initial_infects - self.initial_immunes self.total_newborns = 0 self.total_natural_deaths = 0 self.total_deaths = 0 # setting up the monitors for watching interesting variables self.m_suscettibili = Monitor(name="Suscettibili", ylab="suscettibili") self.m_suscettibili.append([0, self.total_susceptibles]) self.m_infetti = Monitor(name="Infetti", ylab='infetti') self.m_infetti.append([0, self.initial_infects]) if self.model_has_immunization: self.m_immuni = Monitor(name="Immuni", ylab='immuni') self.m_immuni.append([0, self.initial_immunes]) # setting up the array of all the individuals partecipating to the simulation self.all_individuals = [] # initialize the simulation environment (time, events, ...) initialize() for i in range(self.nr_individuals): # add individuals to the simulation with the specified health_status if i >= (self.nr_individuals - self.initial_infects): ind = self.Individual(self, ind_id=i, health_status='infect') elif i >= (self.nr_individuals - self.initial_infects - self.initial_immunes): ind = self.Individual(self, ind_id=i, health_status='immune') else: ind = self.Individual(self, ind_id=i) # activate it with function live() activate(ind, ind.live(), at=0.0) self.start_time = time.time() if self.process_debug: self.show_processes_status() # start the simulation simulate(until=self.run_time) self.stop_time = time.time() if self.process_debug: self.show_processes_status() # show final stats if required by params if self.stats: self.show_stats() # show plot if required by params if self.plot: self.show_plot() def model_has_recovering(self): """Checks from parameters if the epidemiological model has recovering.""" if self.recover_rate: return True else: return False def model_has_immunization(self): """Checks from parameters if the epidemiological model has immunization.""" if (self.model_has_recovering and self.immune_after_recovery) or (self.newborn_prob and self.newborn_can_be_immune): return True else: return False def model_immunization_is_permanent(self): """Checks from parameters if the epidemiological model has permanent immunization.""" if self.model_has_immunization and self.immunization_vanish_rate == 0: return True else: return False def model_has_death(self): """Checks from parameters if the epidemiological model has death.""" if self.death_rate: return True else: return False def model_has_vital_dynamics(self): """Checks from parameters if the epidemiological model has vital dynamics (births and natural deaths).""" if (self.newborn_prob or self.natural_death_prob): return True else: return False def model_has_new_susceptibles(self): """Checks if the number of the susceptibles can increase during the simulation.""" if self.model_has_recovering or \ (self.model_has_immunization and not self.model_immunization_is_permanent) or \ self.model_has_vital_dynamics: return True else: return False def model_has_new_infects(self): """Checks if the number of the infects can increase during the simulation, but not for susceptibles getting infected.""" if self.model_has_vital_dynamics and not self.model_newborns_always_susceptibles: return True else: return False def model_newborns_always_susceptibles(self): """Checks if newborns are always susceptibles in the epidemiological model.""" if self.model_has_vital_dynamics and not (self.newborn_can_be_immune and self.newborn_can_be_infect): return True else: return False def check_and_set_default_value(self, num_params, bool_params): """Checks if some optional parameters are set and, if not, set them to their default values. The default values of the optional parameters are: - 0 for every numerical parameters; - True for parameters "progress", "plot", "stats" - False for every other boolean parameters Input: two dictionaries of optional parameters of the constructor of the class Output: none (but the optional parameters are setted with their default values) """ for param in num_params: if hasattr(self, param) == False: self.__dict__[param] = 0 for param in bool_params: if hasattr(self, param) == False: if param in ['progress', 'plot', 'stats']: self.__dict__[param] = True else: self.__dict__[param] = False def show_processes_status(self): """Returns the current internal status of the simulation processes.""" active_counter = 0 passive_counter = 0 terminated_counter = 0 interrupted_counter = 0 for ind in self.all_individuals: if ind.active(): active_counter += 1 if ind.passive(): passive_counter += 1 if ind.terminated(): terminated_counter += 1 if ind.interrupted(): interrupted_counter += 1 print "Processes' status: %3i active, %3i passive, %3i terminated, %3i interrupted" % (active_counter, passive_counter, terminated_counter, interrupted_counter) def wait(self, rate): '''Returns the time interval before the next event.''' return self.rng.expovariate(rate) def next_event(self, events_rates): """Returns the next event, choosen from the given ones, and the time interval before it. Input: a dictionary of possible events and their respective rates Output: one of the possible events passed as input, and the time interval before it. """ rnd = self.rng.random() total_rates = 0 for event in events_rates: total_rates += events_rates.get(event) cumulated_rates = 0 for event in events_rates: cumulated_rates += events_rates.get(event) if rnd <= cumulated_rates/total_rates: returned_event = event break returned_event_interval = self.wait(total_rates) return returned_event, returned_event_interval def observe_vars(self): """Watches and records the values of the monitored variables.""" self.m_infetti.observe(self.total_infects) self.m_suscettibili.observe(self.total_susceptibles) if self.model_has_immunization: self.m_immuni.observe(self.total_immunes) def check_termination_conds(self): """Checks particular situations in which the simulation can be stopped before the time runs out.""" if self.total_infects == 0: if self.debug: print "[%f] STOP: infection ended, no more infects!"% (now()) print "\nSimulation ended prematurely: infection ended, no more infects." return True if not self.model_has_new_susceptibles: if self.total_susceptibles == 0 and self.total_immunes == 0: if self.debug: print "[%f] STOP: infection extended to the whole population!"% (now()) print "\nSimulation ended prematurely: infection extended to the whole population." return True if self.model_immunization_is_permanent and not self.model_has_vital_dynamics: if self.total_susceptibles == 0 and self.total_immunes == 0: if self.debug: print "[%f] STOP: infection ended, permanent immunizzation extended to the whole population!"% (now()) print "\nSimulation ended prematurely: infection ended, permanent immunizzation extended to the whole population." return True return False def check_current_nr_individuals(self): """Checks the correct number of individuals during the simulation.""" assert self.nr_individuals + self.total_newborns - self.total_natural_deaths - self.total_deaths == self.total_susceptibles + self.total_infects + self.total_immunes, "error: assert failed on the current number of individuals." def stop_simulation(self): """Stops the simulation.""" stopSimulation() def show_plot(self): """Plots the number of infected, susceptibles and, eventually, immunes against time using gnuplot-py.""" try: import Gnuplot except ImportError: print "warning: the gnuplot-py module cannot be found. The simulation will run anyway, but you could not plot the graph." pass g = Gnuplot.Gnuplot() if os.getenv('DISPLAY') == None: g('set term png') g('set output "test.png"') else: g('persist=1') g.xlabel('t') g.ylabel('number of individuals') x_infetti = self.m_infetti.tseries() y_infetti = self.m_infetti.yseries() x_suscettibili = self.m_suscettibili.tseries() y_suscettibili = self.m_suscettibili.yseries() g_infetti = Gnuplot.Data(x_infetti, y_infetti, inline=True, title='Infects', with='steps 1')