Skip to content

Commit 047b407

Browse files
authored
Add Virus-Antibody model (#253)
Adds a Virus-Antibody model that simulates how the immune reaction declines as a confrontation between antibody agents and virus agents. The global idea is to model how the immune system can struggle against new virus but is able to adapt over time and beat a same virus if it comes back. The results are quite interesting as the simulation can go both ways (virus win or antibodies win) with a little tweak in the base parameters (check the readme for some examples).
1 parent 1ea707c commit 047b407

File tree

11 files changed

+555
-0
lines changed

11 files changed

+555
-0
lines changed

examples/virus_antibody/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Virus-Antibody Model
2+
3+
This model is a simulation of immune reaction declined as a confrontation between antibody agents and virus agents. The global idea is to model how the immune system can struggle against new virus but is able to adapt over time and beat a same virus if it comes back. The results are quite interesting as the simulation can go both ways (virus win or antibodies win) with a little tweak in the base parameters.
4+
5+
6+
**It showcases :**
7+
- **Usage of memory in agents** : divided into a short term memory using a deque to easily add and remove memories in case of a new virus encounter, and a long term memory (here a simple list)
8+
- **Agent knowledge sharing** : the antibodies are able to share short term memory)
9+
- **Usage of weak referencing** to avoid coding errors (antibodies can store viruses in a `self.target` attribute)
10+
- Emergence of completely **different outcomes** with only small changes in parameters
11+
12+
13+
For example, with a given set of fixed parameters :
14+
| Virus mutation rate = 0.15 (antibodies win) | Virus mutation rate = 0.2 (viruses win) |
15+
|--------------------------------------------------|--------------------------------------------------|
16+
| ![](images/antibodies_win.png) | ![](images/viruses_win.png) |
17+
18+
19+
20+
21+
## How It Works
22+
23+
1. **Initialization**: The model initializes a population of viruses and antibodies in a continuous 2D space.
24+
2. **Agent Behavior**:
25+
- Antibodies move randomly until they detect a virus within their sight range (becomes purple), than pursue the virus.
26+
- Antibodies pass on all the virus DNA in their short term memory to the nearest antibodies (cf. example)
27+
- Viruses move randomly and can duplicate or mutate.
28+
3. **Engagement (antibody vs virus)**: When an antibody encounters a virus:
29+
- If the antibody has the virus's DNA in its memory, it destroys the virus.
30+
- Otherwise, the virus may defeat the antibody, causing it to lose health or become inactive temporarily.
31+
4. **Duplication**: Antibodies and viruses can duplicate according to their duplication rate.
32+
33+
34+
> Example for memory transmission : Let's look at two antibodies A1 and A2
35+
> `A1.st_memory() = [ABC]` and `A1.lt_memory() = [ABC]`
36+
> `A2.st_memory() = [DEF]` and `A2.lt() = [DEF]`
37+
>
38+
> After A1 encounters A2,
39+
> `A1.st_memory() = [DEF]` and `A1.lt() = [ABC, DEF]`
40+
> `A2.st_memory() = [ABC]` and `A1.lt() = [DEF, ABC]`
41+
>
42+
> A1 and A2 'switched' short term memory but both have the two viruses DNA in their long term memory
43+
44+
For further details, here is the full architecture of this model :
45+
46+
<div align="center">
47+
<img src="images/virus_antibody_architecture.png" width="550"/>
48+
</div>
49+
50+
## Usage
51+
52+
After cloning the repo and installing mesa on pip, run the application with :
53+
```bash
54+
solara run app.py
55+
```
56+
57+
## A couple more of interesting cases
58+
59+
| An interesting tendency inversion | high duplication + high mutation = both grow (more viruses) | high duplication + low mutation = both grow (more antibodies) |
60+
|---|---|---|
61+
| <img src="images/pattern.png" width="550"/> | <img src="images/grow_virus_wins.png" width="450"/> | <img src="images/grow_antibody_wins.png" width="450"/> |

examples/virus_antibody/agents.py

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
"""
2+
Mesa implementation of Virus/Antibody model: Agents module.
3+
"""
4+
5+
import copy
6+
import weakref
7+
from collections import deque
8+
9+
import numpy as np
10+
from mesa.experimental.continuous_space import ContinuousSpaceAgent
11+
12+
13+
class CellularAgent(ContinuousSpaceAgent):
14+
def _random_move(self, speed=1):
15+
"""Random walk in a 2D space."""
16+
perturb = np.array(
17+
[
18+
self.random.uniform(-0.5, 0.5),
19+
self.random.uniform(-0.5, 0.5),
20+
]
21+
)
22+
self.direction = self.direction + perturb
23+
norm = np.linalg.norm(self.direction)
24+
if norm > 0:
25+
self.direction /= norm
26+
self.position += self.direction * speed
27+
28+
29+
class AntibodyAgent(CellularAgent):
30+
"""An Antibody agent. They move randomly until they see a virus, go fight it.
31+
If they lose, stay KO for a bit, lose health and back to random moving.
32+
"""
33+
34+
speed = 1.5
35+
sight_range = 10
36+
ko_timeout = 15
37+
memory_capacity = 3
38+
health = 2
39+
40+
def __init__(
41+
self,
42+
model,
43+
space,
44+
duplication_rate,
45+
initial_position=(0, 0),
46+
direction=(1, 1),
47+
):
48+
super().__init__(model=model, space=space)
49+
50+
# Movement & characteristics
51+
self.position = initial_position
52+
self.direction = np.array(direction, dtype=float)
53+
self.duplication_rate = duplication_rate
54+
55+
# Memory
56+
self.st_memory: deque = deque(maxlen=self.memory_capacity)
57+
self.lt_memory: list = []
58+
59+
# Target & KO state
60+
self.target = None # will hold a weakref.ref or None
61+
self.ko_steps_left = 0
62+
63+
def step(self):
64+
nearby_agents, _ = self.space.get_agents_in_radius(
65+
self.position, self.sight_range
66+
)
67+
nearby_viruses = [a for a in nearby_agents if isinstance(a, VirusAgent)]
68+
nearby_antibodies = [
69+
a
70+
for a in nearby_agents
71+
if isinstance(a, AntibodyAgent) and a.unique_id != self.unique_id
72+
]
73+
74+
# Acquire a virus target if we don't already have one
75+
if self.target is None and nearby_viruses:
76+
closest = nearby_viruses[0]
77+
self.target = weakref.ref(closest)
78+
79+
# Communicate and maybe duplicate
80+
self.communicate(nearby_antibodies)
81+
if self.random.random() < self.duplication_rate:
82+
self.duplicate()
83+
84+
# Then move
85+
self.move()
86+
87+
def communicate(self, nearby_antibodies) -> bool:
88+
for other in nearby_antibodies:
89+
to_share = [
90+
dna for dna in self.st_memory if dna and dna not in other.lt_memory
91+
]
92+
if to_share:
93+
other.st_memory.extend(to_share)
94+
other.lt_memory.extend(to_share)
95+
return True
96+
97+
def duplicate(self):
98+
clone = AntibodyAgent(
99+
self.model,
100+
self.space,
101+
duplication_rate=self.duplication_rate,
102+
initial_position=self.position,
103+
direction=self.direction,
104+
)
105+
# Copy over memory
106+
clone.st_memory = deque(maxlen=self.memory_capacity)
107+
clone.st_memory.extend([item for item in self.st_memory if item])
108+
clone.lt_memory = [item for item in self.lt_memory if item]
109+
clone.target = None
110+
clone.ko_steps_left = 0
111+
112+
def move(self):
113+
# Dereference weakref if needed
114+
target = (
115+
self.target()
116+
if isinstance(self.target, weakref.ReferenceType)
117+
else self.target
118+
)
119+
120+
new_pos = None
121+
122+
# KO state: target refers back to self
123+
if target is self:
124+
self.ko_steps_left -= 1
125+
if self.ko_steps_left <= 0:
126+
self.target = None
127+
128+
# Random walk if no target
129+
elif target is None:
130+
self._random_move()
131+
132+
# Chase a valid virus target
133+
else:
134+
if getattr(target, "space", None) is not None:
135+
vec = np.array(target.position) - np.array(self.position)
136+
dist = np.linalg.norm(vec)
137+
if dist > self.speed:
138+
self.direction = vec / dist
139+
new_pos = self.position + self.direction * self.speed
140+
else:
141+
self.engage_virus(target)
142+
else:
143+
self.target = None
144+
145+
if new_pos is not None:
146+
self.position = new_pos
147+
148+
def engage_virus(self, virus) -> str:
149+
dna = copy.deepcopy(virus.dna)
150+
if dna in self.st_memory or dna in self.lt_memory:
151+
virus.remove()
152+
self.target = None
153+
154+
else:
155+
# KO (or death)
156+
self.health -= 1
157+
if self.health <= 0:
158+
self.remove()
159+
160+
self.st_memory.append(dna)
161+
self.lt_memory.append(dna)
162+
self.ko_steps_left = self.ko_timeout
163+
# mark KO state by weak-ref back to self
164+
self.target = weakref.ref(self)
165+
return "ko"
166+
167+
168+
class VirusAgent(CellularAgent):
169+
"""A virus agent: random movement, mutation, duplication, passive to antibodies."""
170+
171+
speed = 1
172+
173+
def __init__(
174+
self,
175+
model,
176+
space,
177+
mutation_rate,
178+
duplication_rate,
179+
position=(0, 0),
180+
dna=None,
181+
):
182+
super().__init__(model=model, space=space)
183+
184+
self.position = position
185+
self.mutation_rate = mutation_rate
186+
self.duplication_rate = duplication_rate
187+
self.direction = np.array((1, 1), dtype=float)
188+
self.dna = dna if dna is not None else self.generate_dna()
189+
190+
def step(self):
191+
if self.random.random() < self.duplication_rate:
192+
self.duplicate()
193+
self._random_move()
194+
195+
def duplicate(self):
196+
VirusAgent(
197+
self.model,
198+
self.space,
199+
mutation_rate=self.mutation_rate,
200+
duplication_rate=self.duplication_rate,
201+
position=self.position,
202+
dna=self.generate_dna(self.dna),
203+
)
204+
205+
def generate_dna(self, dna=None):
206+
if dna is None:
207+
return [self.random.randint(0, 9) for _ in range(3)]
208+
idx = self.random.randint(0, 2)
209+
chance = self.random.random()
210+
if chance < self.mutation_rate / 2:
211+
dna[idx] = (dna[idx] + 1) % 10
212+
elif chance < self.mutation_rate:
213+
dna[idx] = (dna[idx] - 1) % 10
214+
return dna

0 commit comments

Comments
 (0)