Del 4: Mutationer

Gode nyheder! Din model er blevet godt modtaget af regeringen, og de begynder snart at tage den i brug, for at vurdere, hvilke tiltag de skal sætte i værks. Pludselig bliver du dog ringet op af en forsker fra Statens Serum Institut, der fortæller dig, at din model er mangelfuld! De siger, at modellen mangler detaljer om, hvordan sygdommen kan mutere sig selv hen ad vejen. Forskeren giver dig en liste over ting, der skal tilføjes, og du skynder dig at gå i gang.

Virus-klasse

Fordi, at virussens opførsel bliver mere avanceret, er det nu nødvendigt at give den sin egen klasse, ligesom med Person klassen. Tilføj følgende klasse, oven over Person klassen:

class Virus():
    def __init__(self, mutation):
        self.infection_level = 600
        self.mutation = mutation

    def mutate(self):
        return Virus(self.mutation)

infection_level skal have samme funktionalitet som før. Vi kommer til at beskrive mutation senere.

Erstat nu denne kode i Person.setup:

if randint(1,50) == 1:
    self.infect(model)

med denne:

self.virus = None
if randint(1,50) == 1:
    self.infect(model, Virus(5))

I stedet for at agenten bare “simulerer” en virus ved at bruge sin category og infection_level, bærer den nu rundt på et virus-objekt, der holder styr på dette.

Dette betyder så også, at vi skal ændre alle de steder, der har noget at gøre med agentens infektion, til at bruge denne klasse i stedet. Ændr Person.infect til denne:

def infect(self, model):
    model.Susceptible -= 1
    model.Infectious += 1
    self.color = (200,0,0)
    self.category = 1
    self.virus = virus

og Person.turn_immune til denne:

def turn_immune(self, model):
    model.Infectious -= 1
    model.Recovered += 1
    self.color = (0,0,200)
    self.category = 2
    self.virus = None

Ændr til sidst dette stykke i Person.step:

if self.category == 1:
    for agent in self.agents_nearby(model.infection_distance):
        if agent.category == 0:
            agent.infect(model)
    self.infection_level -= 1
    if self.infection_level == 0:
        self.turn_immune(model)

til dette:

if self.category == 1:
    for agent in self.agents_nearby(model.infection_distance):
        if agent.category == 0:
            agent.infect(model, self.virus.mutate())
    self.virus.infection_level -= 1
    if self.virus.infection_level == 0:
        self.turn_immune(model)

Her inficerer vi altså den anden agent med et nyt virus-objekt lavet med Virus.mutate, fremfor “bare” at sætte dens infection_level.

Prøv at køre modellen, og se, om alt kører som det burde. Der burde der ikke være nogen forskel fra sidst.

Mutationsstadier

Hovedideen med at lave Virus-klassen er, at vi kan gemme information om dens mutationsstadie i den, fremfor at gemme den i agenten, der bærer den.

Vi vil nu ændre en smule i modellens opsætning. I stedet for, at der kun findes én variant af sygdommen, gør vi nu sådan, at sygdommen kan findes i flere varianter, og at man, hvis man har været smittet, kun bliver immun over for den variant, man har været smittet med.

Vi starter med at give agenten en liste over immuniteter. Tilføj denne linje til Person.setup inden, at agenten bliver tilfældigt inficeret:

self.immunities = []

Denne liste skal så indeholde alle de mutations-ID’er for de virusser, den har været smittet med. I den sammenhæng skal vi også checke, at agenten ikke bliver smittet med en immun virus, når den inficeres. I Person.infect, sæt alt koden ind i følgende if-sætning:

if not virus.mutation in self.immunities:

Så køres resten af koden ikke, hvis agenten allerede har været smittet med denne variation af virus.

Vi vil gerne have mulighed for at se med et øjekast, hvilken slags mutation, en agent er inficeret med. Ændr derfor denne linje i Person.infect:

self.color(200,0,0)

til denne:

self.color = (200,150-30*virus.mutation,150-30*virus.mutation)

Jo højere Virus.mutation er, jo mere rød farves agenten.

Samtidig ændrer vi nu lidt på Person.turn_immune, da agenterne i stedet bliver gradvist immune, fremfor at blive komplet immune efter første gang med sygdommen.

Erstat Person.turn_immune med nedenstående:

def turn_immune(self, model):
    model.Infectious -= 1
    model.Susceptible += 1
    self.color = (200-30*len(self.immunities),200,200-30*len(self.immunities))
    self.category = 0
    self.immunities.append(self.virus.mutation)
    self.virus = None

Der er nogle ændringer i forhold til den nuværende:

  • I stedet for at sætte agentens kategori til 2, sætter vi den tilbage til 0, da agenten egentlig ikke bliver immun, men går tilbage til at være modtagelig. Af samme årsag lægger vi 1 til model.Susceptible i stedet for model.Recovered.
  • Agentens farve bliver nu mere grøn, jo mere resistent den er (altså jo flere sygdomme den har haft).
  • Vi tilføjer virussens “mutation-ID” til agentens liste over immuniteter. Den kan altså ikke smittes med denne mutation fremover.

Ændr i samme omgang også denne linje i Person.setup:

self.color = (0,200,0)

til denne:

self.color = (200,200,200)

Vi gør også sådan, at hvis en virus har muteret nok gange, kan den ikke længere smitte. Opdater if-sætningen i smittetrinet i Person.step, sådan at der i stedet for:

if agent.category == 0:
    agent.infect(model, self.virus.mutate())

står:

if agent.category == 0 and self.virus.mutation > 0:
    agent.infect(model, self.virus.mutate())

Til sidst gør vi sådan, at der er en 25% chance for, at virussen muterer, når den spredes til en anden agent. Erstat Virus.mutate med:

def mutate(self):
    if randint(1,4) < 4:
        return Virus(self.mutation)
    else:
        return Virus(self.mutation-1)

Prøv at køre modellen nu, og observer grafen. Kan du se, hvordan de forskellige “bølger” af mutationer optræder?

../_images/epidemic-4.2.png

Mutationseffekter

Lige nu har de forskellige mutationer ikke nogen egentlig forskel, ud over deres farve. Vi laver nu om på det, sådan at deres sygdomsperiode og infektionsradius ændres, når de muterer.

Vi gør dette ved at ændre på den måde, Virus-objektet oprettes på. Erstat Virus.__init__ med følgende:

def __init__(self, mutation, duration, radius):
    self.mutation = mutation
    self.duration = duration
    self.radius = radius
    self.infection_level = self.duration

Dette gør, at vi kan specificere varigheden og rækkevidden for et virus-objekt, når vi laver det.

Ændr på samme måde Virus.mutate til følgende:

def mutate(self):
    if randint(1,4) < 4:
        return Virus(self.mutation,
                     self.duration,
                     self.radius)
    else:
        return Virus(self.mutation-1,
                     self.duration + randint(-100,100),
                     self.radius + randint(-5,5))

Her gør vi sådan, at virussens varighed og rækkevidde justeres en smule, når den muterer.

Når vi opretter en ny Virus, bliver vi så nødt til også at give en oprindelig værdi for varighed og rækkevidde. Ændr denne linje i Person.setup:

self.infect(model, Virus(5))

til denne:

self.infect(model, Virus(5, 600, model.infection_distance))

Til sidst, ændr denne linje i Person.step:

for agent in self.agents_nearby(model.infection_distance):

til denne:

for agent in self.agents_nearby(self.virus.distance):

Prøv at køre modellen og se, om du ser en mærkbar forskel.

Samlet kode

Her er den samlede kode du gerne skulle have nu:

from agents import Model, Agent, run
from random import randint


class Virus:
    def __init__(self, infection_level, mutation):
        self.infection_level = infection_level
        self.mutation = mutation


class Person(Agent):
    def setup(self, model):
        model.Susceptible += 1
        self.category = 0
        self.color = (200, 200, 200)

        self.immunities = []
        self.virus = None

        if randint(1, 50) == 1:
            self.infect(model, Virus(600, 5))

        if model.enable_groups:
            self.group = randint(1, 5)
            self.group_indicator = model.add_ellipse(
                self.x - 10, self.y - 10, 20, 20, (0, 0, 0)
            )
            if self.group == 1:
                self.group_indicator.color = (200, 200, 0)
            elif self.group == 2:
                self.group_indicator.color = (0, 200, 200)
            elif self.group == 3:
                self.group_indicator.color = (200, 0, 200)
            elif self.group == 4:
                self.group_indicator.color = (100, 100, 100)
            elif self.group == 5:
                self.group_indicator.color = (250, 150, 0)

    def step(self, model):
        if model.enable_groups:
            self.group_indicator.x = self.x - 10
            self.group_indicator.y = self.y - 10
        new_direction = 0
        nearby_agents = 0
        for agent in self.agents_nearby(model.social_distance):
            if model.enable_groups and agent.group != self.group:
                new_direction += self.direction_to(agent.x, agent.y)
                nearby_agents += 1
        if nearby_agents > 0:
            self.direction = (new_direction / nearby_agents) + 180
        else:
            self.direction += randint(-10, 10)
        self.forward()
        if self.category == 1:
            for agent in self.agents_nearby(model.infection_distance):
                if agent.category == 0 and self.virus.mutation > 0:
                    agent.infect(
                        model, Virus(600, self.virus.mutation - randint(0, 1))
                    )
            self.virus.infection_level -= 1
            if self.virus.infection_level == 0:
                self.turn_immune(model)

    def infect(self, model, virus):
        if virus.mutation not in self.immunities:
            model.Susceptible -= 1
            model.Infectious += 1
            self.color = (
                200,
                150 - 30 * virus.mutation,
                150 - 30 * virus.mutation,
            )
            self.category = 1
            self.virus = virus

    def turn_immune(self, model):
        model.Infectious -= 1
        model.Susceptible += 1
        self.color = (
            200 - 30 * len(self.immunities),
            200,
            200 - 30 * len(self.immunities),
        )
        self.category = 0
        self.immunities.append(self.virus.mutation)
        self.virus = None


def model_setup(model):
    model.reset()
    model.Susceptible = 0
    model.Infectious = 0
    model.Recovered = 0
    for person in range(100):
        model.add_agent(Person())


def model_step(model):
    for person in model.agents:
        person.step(model)
    model.update_plots()


epidemic_model = Model("Epidemimodel", 100, 100)

epidemic_model.add_button("Setup", model_setup)
epidemic_model.add_button("Go", model_step, toggle=True)
epidemic_model.line_chart(["Susceptible", "Infectious"], [(0, 200, 0), (200, 0, 0)])
epidemic_model.bar_chart(["Susceptible", "Infectious"], (200, 200, 200))
epidemic_model.add_checkbox("enable_groups")
epidemic_model.add_controller_row()
epidemic_model.add_slider("social_distance", 50, 0, 80)
epidemic_model.add_controller_row()
epidemic_model.add_slider("infection_distance", 15, 0, 40)

run(epidemic_model)
[1]https://en.wikipedia.org/wiki/Compartmental_models_in_epidemiology#The_SIR_model