Repository navigation
Expand file tree
/
Copy pathtutorial03_02.py
More file actions
225 lines (170 loc) · 8.75 KB
/
Copy pathtutorial03_02.py
File metadata and controls
225 lines (170 loc) · 8.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
import marimo
__generated_with = "0.24.0"
app = marimo.App(width="medium")
@app.cell
def _():
import marimo as mo
import matplotlib.pyplot as plt
import numpy as np
return mo, np, plt
@app.cell(hide_code=True)
def _(mo):
mo.md(r"""
# Combining Multiple Interventions
## Introduction
In the previous tutorial, we applied non-pharmaceutical interventions by reducing the contacts in a population via a multiplicative factor. For more advanced simulations and studies of interventions, we can model several interventions in place at the same time. In this tutorial we want to show how we combine multiple interventions. Every `Damping` has a **Level** and a **Type**:
- Dampings with the same **Level** but different **Type** are **summed**. This fits measures that each affect a different, non-overlapping part of the population, e.g., a school closure and an office closure, since neither affects the contacts the other one does.
- Dampings on different **Levels** are combined multiplicatively, i.e., two dampings `a` and `b` on different levels combine to `1 - (1-a)(1-b)`. This fits measures of a fundamentally different kind acting on the *same* population, e.g., mask-wearing (which reduces the risk of whatever contacts happen) and homeschooling (which reduces how many contacts happen in the first place). Since mask-wearing's effect only applies to the contacts homeschooling leaves behind, the two effects compound rather than simply add.
- A new damping with the same **Level and Type** as an existing one **replaces** it from its start time onward instead of adding to it, e.g., strengthening or relaxing a single measure over time.
""")
return
@app.cell(hide_code=True)
def _(mo):
mo.md(r"""
## Model Setup
We use the same SECIR-type model with one age group as for the previous tutorial. For a detailed description on that, see Tutorial 1.
""")
return
@app.cell
def _():
import memilio.simulation.osecir as osecir
from memilio.simulation import AgeGroup, Damping, LogLevel, set_log_level
set_log_level(LogLevel.Off)
return AgeGroup, Damping, osecir
@app.cell
def _(AgeGroup, np, osecir):
# Initialize total population, simulation start time, simulation time frame and initial step size
total_population = 100000
t0 = 0
tmax = 100
dt = 0.1
contact_frequency = 10
# Create model with one age group
model = osecir.Model(1)
# Set infection state stay times (in days)
group = AgeGroup(0)
model.parameters.TimeExposed[group] = 3.2
model.parameters.TimeInfectedNoSymptoms[group] = 2.
model.parameters.TimeInfectedSymptoms[group] = 6.
model.parameters.TimeInfectedSevere[group] = 12.
model.parameters.TimeInfectedCritical[group] = 8.
# Set infection state transition probabilities
model.parameters.RelativeTransmissionNoSymptoms[group] = 0.67
model.parameters.TransmissionProbabilityOnContact[group] = 0.1
model.parameters.RecoveredPerInfectedNoSymptoms[group] = 0.2
model.parameters.RiskOfInfectionFromSymptomatic[group] = 0.25
model.parameters.SeverePerInfectedSymptoms[group] = 0.2
model.parameters.CriticalPerSevere[group] = 0.25
model.parameters.DeathsPerCritical[group] = 0.3
# Set contact frequency
model.parameters.ContactPatterns.cont_freq_mat[0].baseline = np.ones((1, 1)) * contact_frequency
# 1% of the population is initially infected, 0.5% Exposed and 0.5% in the pre- or asymptomatic state
model.populations[group, osecir.InfectionState.Exposed] = 0.005 * total_population
model.populations[group, osecir.InfectionState.InfectedNoSymptoms] = 0.005 * total_population
# The rest of the population is Susceptible
model.populations.set_difference_from_total(
(group, osecir.InfectionState.Susceptible), total_population)
return dt, model, t0, tmax
@app.cell(hide_code=True)
def _(mo):
mo.md(r"""
## Same Level, Different Types: Additive
Suppose two measures start on day 10, each affecting a different, non-overlapping part of the population: a partial school closure reduces contacts by 20%, and an office closure reduces contacts by a further 40%. We give them the same `level=0` but different `type`, and MEmilio sums their values: `0.2 + 0.4 = 0.6`.
""")
return
@app.cell
def _(Damping, model, np):
model.parameters.ContactPatterns.cont_freq_mat.add_damping(
Damping(coeffs=np.ones((1, 1)) * 0.2, t=10.0, level=0, type=0)) # partial school closure
model.parameters.ContactPatterns.cont_freq_mat.add_damping(
Damping(coeffs=np.ones((1, 1)) * 0.4, t=10.0, level=0, type=1)) # office closure
return
@app.cell(hide_code=True)
def _(mo):
mo.md(r"""
## A Different Level: Multiplicative
On day 25, a mask mandate takes effect, reducing the risk of whatever contacts still happen by 30%, regardless of how many contacts the school/office closures already removed. Since it's a fundamentally different kind of measure acting on the same population, we place it on a new `level=1`. It combines multiplicatively with the level-0 total: `1 - (1 - 0.6)(1 - 0.3) = 0.72`
""")
return
@app.cell
def _(Damping, model, np):
model.parameters.ContactPatterns.cont_freq_mat.add_damping(
Damping(coeffs=np.ones((1, 1)) * 0.3, t=25.0, level=1, type=0)) # mask mandate
return
@app.cell(hide_code=True)
def _(mo):
mo.md(r"""
## Same Level and Type: Replaces
At day 40, the partial school closure is escalated to full homeschooling, now reducing contacts by 40% instead of 20%. Because this damping shares its `level=0, type=0` with the day-10 school closure, it **replaces** it rather than adding to it. From day 40 onward, level 0 is the sum of this new value and the still-active office-closure damping: `0.4 + 0.4 = 0.8`, which then combines multiplicatively with the mask mandate on level 1: `1 - (1 - 0.8)(1 - 0.3) = 0.86`.
""")
return
@app.cell
def _(Damping, model, np):
model.parameters.ContactPatterns.cont_freq_mat.add_damping(
Damping(coeffs=np.ones((1, 1)) * 0.4, t=40.0, level=0, type=0)) # full homeschooling
return
@app.cell(hide_code=True)
def _(mo):
mo.md(r"""
## Verifying the Combined Effect
We sample the effective contact rate at a few time points and compare against the values
computed by hand above.
""")
return
@app.cell
def _(model):
baseline = model.parameters.ContactPatterns.cont_freq_mat[0].baseline[0, 0]
sample_times = [0, 15, 20, 30, 39, 45]
for t_sample in sample_times:
effective = model.parameters.ContactPatterns.cont_freq_mat.get_matrix_at(t_sample)[0, 0]
reduction = 1 - effective / baseline
print(f"t = {t_sample:>3}: effective contacts = {effective:.2f}, "
f"combined reduction = {reduction:.2f}")
return
@app.cell(hide_code=True)
def _(mo):
mo.md(r"""
Expected values:
- `t = 0`: no damping active, reduction = 0.
- `t = 15`: only the day-10 school/office closures are active, reduction = 0.6 (`0.2 + 0.4`).
- `t = 20`: unchanged, still 0.6.
- `t = 30`: the mask mandate is active too, reduction = 0.72 (`1 - (1-0.6)(1-0.3)`).
- `t = 39`: unchanged, still 0.72.
- `t = 45`: full homeschooling has replaced the partial school closure, reduction = 0.86
(`1 - (1 - (0.4+0.4))(1-0.3)`).
## Model Simulation
""")
return
@app.cell
def _(dt, model, osecir, t0, tmax):
result = osecir.simulate(t0, tmax, dt, model)
print(result.print_table(return_string=True))
return (result,)
@app.cell(hide_code=True)
def _(mo):
mo.md(r"""
## Visualization of Model Output
""")
return
@app.cell
def _(osecir, plt, result):
result_array = result.as_ndarray()
fig, ax = plt.subplots()
time = result_array[0, :]
ax.plot(time, result_array[1 + int(osecir.InfectionState.Exposed), :], label='Exposed')
ax.plot(time, result_array[1 + int(osecir.InfectionState.InfectedNoSymptoms), :], label='Infected No Symptoms')
ax.plot(time, result_array[1 + int(osecir.InfectionState.InfectedSymptoms), :], label='Infected Symptoms')
ax.plot(time, result_array[1 + int(osecir.InfectionState.InfectedSevere), :], label='Infected Severe')
ax.plot(time, result_array[1 + int(osecir.InfectionState.InfectedCritical), :], label='Infected Critical')
for t_damping, label in [(10, 'school + office closure'), (25, 'mask mandate'),
(40, 'full homeschooling')]:
ax.axvline(t_damping, color='gray', linestyle='--', linewidth=0.8)
ax.text(t_damping, 1.01, label, rotation=45, ha='left', va='bottom',
fontsize=8, color='gray', transform=ax.get_xaxis_transform())
ax.set_xlabel('Time [days]')
ax.set_ylabel('Individuals [#]')
ax.legend()
plt.show()
return
if __name__ == "__main__":
app.run()