""" This module defines the Individual abstract class, and its
HeallthyIndividual and InfectedIndividual subclasses and all of their
properties and methods.
"""
from infection.decorators.debugging_decorator import debugging_decorator
from quads import BoundingBox
from abc import ABC, abstractmethod
import numpy as np
from random import uniform
import logging
logging.basicConfig(level=10, format="%(threadName)s:%(message)s")
MAX_COOLDOWN = 30
MAX_TIME_INFECTED = 2000
[docs]class Individual(ABC):
""" This is the definition of the Individual abstract class. It inherits
from the ABC class to make it an abstract class.
"""
@property
@abstractmethod
def status(self):
""" String property used to track the infection status of the
individual.
"""
pass
@property
@abstractmethod
def simulation(self):
""" To store an instance of the simulation class."""
pass
@property
@abstractmethod
def recovered(self):
""" Boolean property used to track when the individual has recovered
from an infection.
"""
pass
@property
@abstractmethod
def time_infected(self):
""" Integer property to track how long the individual has been
infected.
"""
pass
[docs] @abstractmethod
def infection(self):
""" Abstract method that will control the infection status of the
individual and decide if it will get infected or if it will
recover.
"""
pass
[docs] @abstractmethod
def recover(self):
""" Abstract method that will set the properties when the individual
recovers from infection
"""
pass
[docs]class HealthyIndividual(Individual):
""" This is the definition of the HealthyIndividual class. It inherits
from the Individual abstract class.
Args:
simulation (Simulation): Instance of the Simulation class.
infection_probability (Float): A value from 0 to 1 that
determines how likely is an individual to get infected.
Attributes:
simulation: To store the instance of the simulation class.
infection_probability: To store the infection_probability.
recovered: Boolean property used to track when the individual has
recovered from an infection. False by default. True when it has
recovered from an infection. An individual that has recovered
from an infection can no longer get infected.
time_infected: Integer property that tracks during how many cycles
the individual has been infected.
max_time_infected: Integer property that defines how many cycles need
to pass until the individual recovers.
Set to MAX_TIME_INFECTED.
cooldown: Integer property that tracks how many cycles have passed
after the last infection evaluation to avoid evaluating against
the same individual multiple times after crossing paths.
max_cooldown: Integer property that defines how many cycles need
to pass until the infection evaluations restart.
Set to MAX_COOLDOWN.
status: String property that tracks the infection status of the
individual. "healthy" by default.
"""
def __init__(self, simulation, infection_probability, **kwargs):
super(Individual, self).__init__(**kwargs)
self._simulation = simulation
self._infection_probability = infection_probability
self._recovered = False
self._time_infected = 0
self._max_time_infected = MAX_TIME_INFECTED
self._cooldown = 0
self._max_cooldown = MAX_COOLDOWN
self._status = "healthy"
@property
def status(self):
return self._status
@status.setter
def status(self, status):
self._status = status
@property
def time_infected(self):
return self._time_infected
@time_infected.setter
def time_infected(self, time_infected):
self._time_infected = time_infected
@property
def max_time_infected(self):
return self._max_time_infected
@property
def cooldown(self):
return self._cooldown
@cooldown.setter
def cooldown(self, cooldown):
self._cooldown = cooldown
@property
def max_cooldown(self):
return self._max_cooldown
@property
def recovered(self):
return self._recovered
@recovered.setter
def recovered(self, recovered):
self._recovered = recovered
@property
def infection_probability(self):
return self._infection_probability
@infection_probability.setter
def infection_probability(self, infection_probability):
self._infection_probability = infection_probability
@property
def simulation(self):
return self._simulation
@simulation.setter
def simulation(self, simulation):
self._simulation = simulation
[docs] @debugging_decorator
def count_infected_neighbors(self, circular_button, quad_tree):
""" Method that searches the quad_tree to find the number of infected
individuals within the provided radius and returns the count in
the infected_neighbor_count variable.
Args:
circular_button (CircularButton): Instance of the button
containing the individual.
quad_tree (QuadTree): A quadtree structure that contains the
positions of all the individuals in the simulation for fast
neighbor search.
"""
infected_neighbor_count = 0
infection_radius = circular_button.simulation.individual_size
""" Get all Points in the quadtree within the individual's radius,
including the individual itself.
"""
others = quad_tree.within_bb(
BoundingBox(circular_button.x - infection_radius,
circular_button.y - infection_radius,
circular_button.x + infection_radius,
circular_button.y + infection_radius))
if len(others) > 1:
infected_others = list(filter(lambda x: x.data == "infected",
others))
""" BoundingBox is a square around the individual's position so
we still need to filter out some individuals that may be
outside the infection_radius and the individual itself.
"""
for infected_other in infected_others:
distance = circular_button.distance((infected_other.x,
infected_other.y))
if distance > 0 and distance <= infection_radius:
infected_neighbor_count += 1
return infected_neighbor_count
[docs] @debugging_decorator
def evaluate_infection(self, circular_button, quad_tree):
""" Method that calls the count_infected_neighbors method and if
there's one or more infected neighbors, a formula is used to
randomly calculate if the individual should get infected based on
the infection_probability and the number of infected neighbors.
Args:
circular_button (CircularButton): Instance of the button
containing the individual.
quad_tree (QuadTree): A quadtree structure that contains the
positions of all the individuals in the simulation for fast
neighbor search.
"""
infected_neighbor_count = self.count_infected_neighbors(
circular_button, quad_tree)
if infected_neighbor_count > 0:
infected = sum(np.random.choice(
[0, 1],
size=infected_neighbor_count,
p=[1 - self.infection_probability,
self.infection_probability]))
return infected, infected_neighbor_count
return 0, infected_neighbor_count
[docs] @debugging_decorator
def sick(self, circular_button):
""" Method to set the individual's properties when it gets sick.
Args:
circular_button (CircularButton): Instance of the button
containing the individual.
"""
self.status = "infected"
self.simulation.safe_sum_infected(1)
self.simulation.safe_sum_healthy(-1)
circular_button.color = self.simulation.infected_color
circular_button.speed = uniform(0.3, 0.8)
logging.info("Infected!")
[docs] @debugging_decorator
def recover(self, circular_button):
""" Method to set the individual's properties when it recovers.
Args:
circular_button (CircularButton): Instance of the button
containing the individual.
"""
self.recovered = True
self.status = "healthy"
self.simulation.safe_sum_healthy(1)
self.simulation.safe_sum_infected(-1)
circular_button.color = self.simulation.recovered_color
circular_button.speed = uniform(0.5, 0.9)
logging.info("Recovered!")
[docs] @debugging_decorator
def infection(self, circular_button, quad_tree):
""" Method that controls if the individual will get infected by
being around one or more infected individuals in the provided
quad_tree, or if the individual is now recovered because
self.max_time_infected cycles have passed after infection.
Args:
circular_button (CircularButton): The instance of the button
containing the individual to change its properties.
quad_tree (QuadTree): A quadtree structure that contains the
position of all the individuals in the canvas for fast
neighbor search.
"""
if self.recovered:
pass
elif self.cooldown > 0:
self.cooldown -= 1
elif self.status == "infected":
self.time_infected += 1
if self.time_infected == self.max_time_infected:
self.recover(circular_button)
else:
infected, infected_neighbour_count = self.evaluate_infection(
circular_button, quad_tree)
if infected_neighbour_count > 0:
if infected > 0:
self.sick(circular_button)
else:
self.cooldown = self.max_cooldown
logging.info(f"Contact with {infected_neighbour_count} \
infected neighbors but no infection.")
[docs]class InfectedIndividual(Individual):
""" This is the definition of the InfectedIndividual class. It inherits
from the Individual abstract class.
Args:
simulation (Simulation): Instance of the Simulation class.
Attributes:
simulation: To store the instance of the simulation class.
recovered: Boolean property used to track when the individual has
recovered from an infection. False by default. True when it has
recovered from an infection. An individual that has recovered
from an infection can no longer get infected.
time_infected: Integer property that tracks during how many cycles
the individual has been infected.
max_time_infected: Integer property that defines how many cycles need
to pass until the individual recovers.
Set to MAX_TIME_INFECTED.
status: String property that tracks the infection status of the
individual. "infected" by default.
"""
def __init__(self, simulation, **kwargs):
super(Individual, self).__init__(**kwargs)
self._simulation = simulation
self._recovered = False
self._time_infected = 0
self._max_time_infected = MAX_TIME_INFECTED
self._status = "infected"
@property
def status(self):
return self._status
@status.setter
def status(self, status):
self._status = status
@property
def time_infected(self):
return self._time_infected
@time_infected.setter
def time_infected(self, time_infected):
self._time_infected = time_infected
@property
def max_time_infected(self):
return self._max_time_infected
@property
def recovered(self):
return self._recovered
@recovered.setter
def recovered(self, recovered):
self._recovered = recovered
@property
def simulation(self):
return self._simulation
@simulation.setter
def simulation(self, simulation):
self._simulation = simulation
[docs] @debugging_decorator
def recover(self, circular_button):
""" Method to set the individual's properties when it recovers.
Args:
circular_button (CircularButton): Instance of the button
containing the individual.
"""
self.recovered = True
self.status = "healthy"
self.simulation.safe_sum_healthy(1)
self.simulation.safe_sum_infected(-1)
circular_button.color = self.simulation.recovered_color
circular_button.speed = uniform(0.5, 0.9)
logging.info("Recovered!")
[docs] @debugging_decorator
def infection(self, circular_button, quad_tree):
""" Method that controls if the individual is now recovered because
self.max_time_infected cycles have passed after infection.
Args:
circular_button (CircularButton): The instance of the button
containing the individual to change its properties.
quad_tree (QuadTree): A quadtree structure that contains the
position of all the individuals in the canvas for fast
neighbor search. Ignored in the InfectedIndividual class.
"""
if self.recovered:
pass
self.time_infected += 1
if self.time_infected == self.max_time_infected:
self.recover(circular_button)