1
"""
Description: The goal is to model a population of people who are infected with a virus.
"""

# TODO: Make a curve for each case so we can see in real time how much the virus is spreading.

# -------------------- IMPORTS --------------------


from random import randint, random
from matplotlib import pyplot as plt, animation as anim
import math


# --------------------  GLOBAL VARIABLES --------------------


number_of_dots = 100  # number of dots to generate
shape = "o"  # tip: use '.' instead if you put a value < 3 in minimal_distance

HEIGHT_WIDTH = 100  # Window height and width (yes, window shape must be a square)
BORDER_MIN = 1  # minimum distance from the border
BORDER_MAX = HEIGHT_WIDTH - 1  # maximum distance from the border

minimal_distance = 3  # Minimal distance at initialization and for contamination
time = 0  # Time to initialize
time_step = 0.1  # Time step for the simulation

transmission_rate = 0.7  # Chance of a dot to be infected
time_to_cure = 40  # Time to cure a dot
time_before_being_contagious_again = 40  # Time before being contagious again
virus_mortality = 0.0005  # Chance of a dot to die per tick


# -------------------- CLASSES & METHODS --------------------


class Dot:
    def __init__(self, x: int, y: int):
        """Constructor for the Dot class

        Args:
            x (int): abscissa of the dot
            y (int): ordinate of the dot
        """
        self.x = x
        self.y = y
        self.velx = (random() - 0.5) / 5
        self.vely = (random() - 0.5) / 5
        self.is_infected = False
        self.infected_at = -1
        self.has_been_infected = False
        self.cured_at = -1

    def init_checker(x: int, y: int, already_used_coords: list):
        """Checks if the dot is in a distance of a minimal_distance from another dot

        Args:
            x (int): absissa of the dot
            y (int): ordinate of the dot
            already_used_coords (list): list of already occupated coordinates (by initialized dots)

        Returns:
            boolean: Whether the Dot should be initialized or not
        """
        for coord in already_used_coords:
            if Dot.get_distance(coord[0], x, coord[1], y) < minimal_distance:
                return False
        return True

    def get_distance(x1: float, y1: float, x2: float, y2: float):
        """Gets the distance between two Dot objects

        Args:
            x1 (float): abscissa of the first dot
            y1 (float): ordinate of the first dot
            x2 (float): abscissa of the second dot
            y2 (float): ordinate of the second dot

        Returns:
            float: distance between the two dots
        """
        return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)

    def initalize_multiple_dots():
        """Generates a list of Dots

        Returns:
            list: initialized dots
        """
        dots = []
        already_used_coords = []

        while len(dots) != number_of_dots:
            randx = randint(BORDER_MIN, BORDER_MAX)
            randy = randint(BORDER_MIN, BORDER_MAX)
            # So the dots keep distances between each other
            if Dot.init_checker(randx, randy, already_used_coords):
                dot = Dot(randx, randy)
                already_used_coords.append((randx, randy))
            else:
                continue
            dots.append(dot)
        print("There are", len(dots), "dots in the area.")
        return dots

    def move(self):
        """Moves the dot and makes sure they don't go out of the area or touch each other. They've 4% chance to change direction."""
        global dots, dead_dots

        if random() < 0.96:
            self.x = self.x + self.velx
            self.y = self.y + self.vely

        else:
            self.x = self.x + self.velx
            self.y = self.y + self.vely
            # Change 2 to lower value to make the dots go faster
            self.velx = (random() - 0.5) / (2 / (time_step + 1))
            # Change 2 to lower value to make the dots go faster
            self.vely = (random() - 0.5) / (2 / (time_step + 1))

        if self.x >= BORDER_MAX:
            self.x = BORDER_MAX
            self.velx = -1 * self.velx

        if self.x <= BORDER_MIN:
            self.x = BORDER_MIN
            self.velx = -1 * self.velx

        if self.y >= BORDER_MAX:
            self.y = BORDER_MAX
            self.vely = -1 * self.vely

        if self.y <= BORDER_MIN:
            self.y = BORDER_MIN
            self.vely = -1 * self.vely

        if (
            random() < transmission_rate
            and not self.has_been_infected
            and not self.is_infected
        ):
            for dot in dots:
                if (
                    dot.is_infected
                    and Dot.get_distance(self.x, self.y, dot.x, dot.y)
                    < minimal_distance
                ):
                    self.is_infected = True
                    self.infected_at = time
                    break
        
        if self.is_infected and random() < virus_mortality:
            dead_dots.append(self)
            dots.remove(self)

    def move_all(dots: list):
        """Moves a given list of dots. Make sur that infected dots goes in the correct axes

        Args:
            dots (list): List of Dot objects
        """
        global healthy_dots, infected_dots, cured_dots, time
        for dot in dots:
            dot.move()
            if (
                dot.is_infected
                and dot.infected_at != -1
                and dot.infected_at + time_to_cure < time
            ):
                dot.is_infected = False
                dot.has_been_infected = True
                dot.cured_at = time

            if (
                dot.has_been_infected
                and dot.cured_at != -1
                and dot.cured_at + time_before_being_contagious_again < time
            ):
                dot.has_been_infected = False
                dot.infected_at = -1
                dot.cured_at = -1

        healthy_dots.set_data(
            [
                dot.x
                for dot in dots
                if not dot.is_infected and not dot.has_been_infected
            ],
            [
                dot.y
                for dot in dots
                if not dot.is_infected and not dot.has_been_infected
            ],
        )

        infected_dots.set_data(
            [dot.x for dot in dots if dot.is_infected],
            [dot.y for dot in dots if dot.is_infected],
        )

        cured_dots.set_data(
            [dot.x for dot in dots if dot.has_been_infected],
            [dot.y for dot in dots if dot.has_been_infected],
        )

        time += time_step
        plt.title(
            f"Healthy: {len([dot for dot in dots if not dot.is_infected and not dot.has_been_infected])}"
            + f"   |   Infected: {len([dot for dot in dots if dot.is_infected])}"
            + f"   |   Cured: {len([dot for dot in dots if dot.has_been_infected])}"
            + f"   |   Dead: {len([dot for dot in dead_dots])}",
            color="black",
        )

        
        def updateAxes():
            """Updates axes of the second window"""


# -------------------- MAIN FUNCTION --------------------


def main():
    global dots, dead_dots, axes

    dots = Dot.initalize_multiple_dots()
    random_infected = randint(0, len(dots) - 1)
    dots[random_infected].is_infected = True
    dots[random_infected].infected_at = time

    figureDots = plt.figure(facecolor="white")
    axes = plt.axes(xlim=(0, HEIGHT_WIDTH), ylim=(0, HEIGHT_WIDTH))

    global healthy_dots
    healthy_dots = axes.plot(
        [dot.x for dot in dots if not dot.is_infected],
        [dot.y for dot in dots if not dot.is_infected],
        f"g{shape}",
    )[0]

    global infected_dots
    infected_dots = axes.plot(
        [dot.x for dot in dots if dot.is_infected],
        [dot.y for dot in dots if dot.is_infected],
        f"r{shape}",
    )[0]

    global cured_dots
    cured_dots = axes.plot(
        [dot.x for dot in dots if dot.has_been_infected],
        [dot.y for dot in dots if dot.has_been_infected],
        f"b{shape}",
    )[0]
    
    global dead_dots
    dead_dots = []

    # We need to keep this in an unsed variable, otherwise the function won't work
    a = anim.FuncAnimation(figureDots, lambda z: Dot.move_all(dots), frames=60, interval=5)
    plt.axis('off')
    plt.show()


# -------------------- MAIN CALL --------------------


if __name__ == "__main__":
    main()

I'm trying to model a population with a virus and I would like to add another window where we can see in real time a graph of each type of the population (infected, healthy, dead, cured).

0

1 Answer 1

0

Correct me if I misunderstood your question but I think you're looking for subfigures: https://matplotlib.org/devdocs/gallery/subplots_axes_and_figures/subfigures.html

You can create subfigures like so:

import matplotlib.pyplot as plt

fig = plt.figure()
ax1 = fig.add_subplot(211)
ax2 = fig.add_subplot(212)

There are multiple ways of adding subfigures, this is how I usually do it, the linked example above has another method.

The subfigures are added in the format [row column subfig_number] where subfig_number goes from the top left subfigure row-wise from left to right to the bottom right figure.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.