|
| 1 | +import marimo |
| 2 | + |
| 3 | +__generated_with = "0.20.4" |
| 4 | +app = marimo.App(width="medium") |
| 5 | + |
| 6 | + |
| 7 | +@app.cell |
| 8 | +def _(): |
| 9 | + import marimo as mo |
| 10 | + import matplotlib.pyplot as plt |
| 11 | + import pandas as pd |
| 12 | + import numpy as np |
| 13 | + |
| 14 | + return mo, np, plt |
| 15 | + |
| 16 | + |
| 17 | +@app.cell(hide_code=True) |
| 18 | +def _(mo): |
| 19 | + mo.md(r""" |
| 20 | + # Simulating an ODE-based model with a Damping from Python |
| 21 | + ## Introduction |
| 22 | +
|
| 23 | + In the previous tutorial, we created, initialized and simulated MEmilio's ODE-based SECIR-type model with one (age) group. In this tutorial, we will show how to incorporate non-pharmaceutical interventions (NPIs) through the use of `Dampings` in the ODE-based SECIR-type model. |
| 24 | +
|
| 25 | + The example requires to have the memilio-simulation package installed which can be accessed under https://github.com/SciCompMod/memilio/tree/main/pycode/memilio-simulation. |
| 26 | + """) |
| 27 | + return |
| 28 | + |
| 29 | + |
| 30 | +@app.cell(hide_code=True) |
| 31 | +def _(mo): |
| 32 | + mo.md(r""" |
| 33 | + ## Model Setup |
| 34 | +
|
| 35 | + We first import the needed functions from the memilio-simulation package: |
| 36 | + """) |
| 37 | + return |
| 38 | + |
| 39 | + |
| 40 | +@app.cell |
| 41 | +def _(): |
| 42 | + import memilio.simulation.osecir as osecir |
| 43 | + from memilio.simulation import AgeGroup, Damping, LogLevel, set_log_level |
| 44 | + set_log_level(LogLevel.Off) |
| 45 | + return AgeGroup, Damping, osecir |
| 46 | + |
| 47 | + |
| 48 | +@app.cell |
| 49 | +def _(mo): |
| 50 | + mo.md(r""" |
| 51 | + Next, we create and initialize a SECIR-type model with one age group. For a detailed description on that, see Tutorial 1. |
| 52 | + """) |
| 53 | + return |
| 54 | + |
| 55 | + |
| 56 | +@app.cell |
| 57 | +def _(AgeGroup, np, osecir): |
| 58 | + # Initialize total population, simulation start time, simulation time frame and initial step size |
| 59 | + total_population = 100000 |
| 60 | + t0 = 0 |
| 61 | + tmax = 100 |
| 62 | + dt = 0.1 |
| 63 | + contact_frequency = 10 |
| 64 | + |
| 65 | + # Create model with one age group |
| 66 | + model = osecir.Model(1) |
| 67 | + |
| 68 | + # Set infection state stay times (in days) |
| 69 | + group = AgeGroup(0) |
| 70 | + model.parameters.TimeExposed[group] = 3.2 |
| 71 | + model.parameters.TimeInfectedNoSymptoms[group] = 2. |
| 72 | + model.parameters.TimeInfectedSymptoms[group] = 6. |
| 73 | + model.parameters.TimeInfectedSevere[group] = 12. |
| 74 | + model.parameters.TimeInfectedCritical[group] = 8. |
| 75 | + |
| 76 | + # Set infection state transition probabilities |
| 77 | + model.parameters.RelativeTransmissionNoSymptoms[group] = 0.67 |
| 78 | + model.parameters.TransmissionProbabilityOnContact[group] = 0.1 |
| 79 | + model.parameters.RecoveredPerInfectedNoSymptoms[group] = 0.2 |
| 80 | + model.parameters.RiskOfInfectionFromSymptomatic[group] = 0.25 |
| 81 | + model.parameters.SeverePerInfectedSymptoms[group] = 0.2 |
| 82 | + model.parameters.CriticalPerSevere[group] = 0.25 |
| 83 | + model.parameters.DeathsPerCritical[group] = 0.3 |
| 84 | + |
| 85 | + # Set contact frequency |
| 86 | + model.parameters.ContactPatterns.cont_freq_mat[0].baseline = np.ones((1, 1)) * 5 |
| 87 | + return dt, group, model, t0, tmax, total_population |
| 88 | + |
| 89 | + |
| 90 | +@app.cell |
| 91 | +def _(mo): |
| 92 | + mo.md(r""" |
| 93 | + ### Exercise |
| 94 | + Please set the contact frequency to 10. |
| 95 | + """) |
| 96 | + return |
| 97 | + |
| 98 | + |
| 99 | +@app.cell |
| 100 | +def _(): |
| 101 | + # Insert code here |
| 102 | + return |
| 103 | + |
| 104 | + |
| 105 | +@app.cell |
| 106 | +def _(mo): |
| 107 | + mo.md(r""" |
| 108 | + After the model initialization, we add a contact reduction (`Damping`) that represents an NPI like e.g. mask wearing or social distancing. Dampings are a factor applied to the contact frequency and can be added to the model at fixed simulation time points before simulating. They have a *Level* and a *Type*. A damping with a given level and type replaces the previously active one with the same level and type, while all currently active dampings of one level and different types are summed up. If two dampings have different levels (independent of the type) they are combined multiplicatively. In the following we apply a `Damping` of 0.9 after 10 days and another damping of 0.6 after 20 days which means that the contacts are reduced by 10% and 40%, respectively. In general, it is also possible to increase the contact rate by using negative Damping values. To always retain a minimum level of contacts, a minimum contact frequency can be set that is never deceeded. In our example we set this minimum contact rate to 0. |
| 109 | + """) |
| 110 | + return |
| 111 | + |
| 112 | + |
| 113 | +@app.cell |
| 114 | +def _(Damping, model, np): |
| 115 | + # Set minimum contact frequency |
| 116 | + model.parameters.ContactPatterns.cont_freq_mat[0].minimum = np.zeros((1, 1)) |
| 117 | + |
| 118 | + # Add contact reduction by 10% after 10 days |
| 119 | + model.parameters.ContactPatterns.cont_freq_mat.add_damping(Damping(coeffs=np.ones((1, 1)) * 0.9, t=10.0, level=0, type=0)) |
| 120 | + # Add contact reduction by 40% after 20 days |
| 121 | + model.parameters.ContactPatterns.cont_freq_mat.add_damping(Damping(coeffs=np.ones((1, 1)) * 0.6, t=20.0, level=0, type=0)) |
| 122 | + return |
| 123 | + |
| 124 | + |
| 125 | +@app.cell |
| 126 | +def _(mo): |
| 127 | + mo.md(r""" |
| 128 | + ### Exercise |
| 129 | + Please create a damping that replaces the 10% reduction after 10 days by a 20% reduction. Additionally, add a damping after 40 days that increases the contact rate by 50%. |
| 130 | + """) |
| 131 | + return |
| 132 | + |
| 133 | + |
| 134 | +@app.cell |
| 135 | +def _(): |
| 136 | + # Insert code here |
| 137 | + return |
| 138 | + |
| 139 | + |
| 140 | +@app.cell |
| 141 | +def _(mo): |
| 142 | + mo.md(r""" |
| 143 | + Again, we start with 0.5% of the population initially in `Exposed` and 0.5% initially in `InfectedNoSymptoms` while the remaining 99% is `Susceptible`. |
| 144 | + """) |
| 145 | + return |
| 146 | + |
| 147 | + |
| 148 | +@app.cell |
| 149 | +def _(group, model, osecir, total_population): |
| 150 | + # 1% of the population is initially infected, 0.5% Exposed and 0.5% in the pre- or asymptomatic state |
| 151 | + model.populations[group, osecir.InfectionState.Exposed] = 0.005 * total_population |
| 152 | + model.populations[group, osecir.InfectionState.InfectedNoSymptoms] = 0.005 * total_population |
| 153 | + # The rest of the population is Susceptible |
| 154 | + model.populations.set_difference_from_total( |
| 155 | + (group, osecir.InfectionState.Susceptible), total_population) |
| 156 | + return |
| 157 | + |
| 158 | + |
| 159 | +@app.cell |
| 160 | +def _(mo): |
| 161 | + mo.md(r""" |
| 162 | + ## Model simulation |
| 163 | +
|
| 164 | + We simulate the model from `t0` to `tmax` with initial step size `dt` and subsequently print the time series result: |
| 165 | + """) |
| 166 | + return |
| 167 | + |
| 168 | + |
| 169 | +@app.cell |
| 170 | +def _(dt, model, osecir, t0, tmax): |
| 171 | + # Simulate model from t0 to tmax with initial step size dt |
| 172 | + result = osecir.simulate(t0, tmax, dt, model) |
| 173 | + result.print_table() |
| 174 | + return (result,) |
| 175 | + |
| 176 | + |
| 177 | +@app.cell |
| 178 | +def _(mo): |
| 179 | + mo.md(r""" |
| 180 | + Subsequently, we interpolate the simulated time series to full days using the linear interpolation function: |
| 181 | + """) |
| 182 | + return |
| 183 | + |
| 184 | + |
| 185 | +@app.cell |
| 186 | +def _(osecir, result): |
| 187 | + # Interpolate result to full days |
| 188 | + interpolated_result = osecir.interpolate_simulation_result(result) |
| 189 | + interpolated_result.print_table() |
| 190 | + return |
| 191 | + |
| 192 | + |
| 193 | +@app.cell |
| 194 | +def _(mo): |
| 195 | + mo.md(r""" |
| 196 | + ## Visualization of model output |
| 197 | +
|
| 198 | + We plot the trajectories of all infected compartments i.e. `Exposed`, `InfectedNoSymptoms`, `InfectedSymptoms`, `InfectedSevere` and `InfectedCritical`. |
| 199 | + """) |
| 200 | + return |
| 201 | + |
| 202 | + |
| 203 | +@app.cell |
| 204 | +def _(osecir, plt, result): |
| 205 | + # Convert time series to array |
| 206 | + result_array = result.as_ndarray() |
| 207 | + |
| 208 | + # Plot the number of infected with symptoms over time |
| 209 | + fig, ax = plt.subplots() |
| 210 | + time = result_array[0, :] |
| 211 | + ax.plot(time, result_array[1 + int(osecir.InfectionState.Exposed), :], label='Exposed') |
| 212 | + ax.plot(time, result_array[1 + int(osecir.InfectionState.InfectedNoSymptoms), :], label='Infected No Symptoms') |
| 213 | + ax.plot(time, result_array[1 + int(osecir.InfectionState.InfectedSymptoms), :], label='Infected Symptoms') |
| 214 | + ax.plot(time, result_array[1 + int(osecir.InfectionState.InfectedSevere), :], label='Infected Severe') |
| 215 | + ax.plot(time, result_array[1 + int(osecir.InfectionState.InfectedCritical), :], label='Infected Critical') |
| 216 | + ax.set_xlabel('Time [days]') |
| 217 | + ax.set_ylabel('Individuals [#]') |
| 218 | + ax.legend() |
| 219 | + plt.show() |
| 220 | + return |
| 221 | + |
| 222 | + |
| 223 | +if __name__ == "__main__": |
| 224 | + app.run() |
0 commit comments