|
| 1 | +#include "ode_secir/model.h" |
| 2 | +#include "memilio/compartments/simulation.h" |
| 3 | +#include "memilio/data/analyze_result.h" |
| 4 | + |
| 5 | +#include <iostream> |
| 6 | + |
| 7 | +// *** Introduction *** |
| 8 | +// In Tutorial 3, we applied contact reductions (Dampings) uniformly to a single contact matrix. In reality, |
| 9 | +// contacts between individuals occur in different locations: at home, at school, at work, and in other places |
| 10 | +// such as transport. Different non-pharmaceutical interventions (NPIs) target different settings, for instance |
| 11 | +// a school closure reduces school contacts, while a home-office mandate mainly reduces work contacts. |
| 12 | +// |
| 13 | +// Large-scale studies, such as the POLYMOD project, have measured social contact patterns across different |
| 14 | +// locations (like home, school, work, and others) in various countries. For instance, see |
| 15 | +// Mossong J, Hens N, Jit M, Beutels P, Auranen K, et al. (2008) Social Contacts and Mixing Patterns Relevant to the Spread of Infectious Diseases. |
| 16 | +// PLoS Med 5(3): e74. https://doi.org/10.1371/journal.pmed.0050074 |
| 17 | +// Prem K, Cook AR, Jit M (2017) Projecting social contact matrices in 152 countries using contact surveys and demographic data. |
| 18 | +// PLoS Comput Biol 13(9): e1005697. https://doi.org/10.1371/journal.pcbi.1005697 |
| 19 | +// |
| 20 | +// In this tutorial, we extend the approach from Tutorial 3 by splitting the contact matrix into |
| 21 | +// location-specific contact matrices using ContactMatrixGroup. This allows us to apply NPIs to individual |
| 22 | +// locations, allowing a more realistic and detailed representation of intervention effects. |
| 23 | + |
| 24 | +// Location indices matching the four contact contexts. |
| 25 | +enum class Location : size_t |
| 26 | +{ |
| 27 | + Home = 0, |
| 28 | + School = 1, |
| 29 | + Work = 2, |
| 30 | + Other = 3 |
| 31 | +}; |
| 32 | + |
| 33 | +// *** Location-specific Contact Matrices *** |
| 34 | +// Instead of a single contact matrix, we now use a ContactMatrixGroup, containing one ContactMatrix per |
| 35 | +// location. Each ContactMatrix has two components: |
| 36 | +// Baseline: the typical number of daily contacts at the location under regular conditions. |
| 37 | +// Minimum: the minimal contact rate even under the strictest restrictions, as some contacts, especially |
| 38 | +// at home, cannot be fully avoided. |
| 39 | +// |
| 40 | +// The effective contact rate at simulation time t for a location l is: |
| 41 | +// C_l(t) = C_l_baseline - D_l(t) * (C_l_baseline - C_l_minimum) |
| 42 | +// where D_l(t) <= 1 is the damping coefficient. For D > 0 contacts are reduced; for D = 1 they reach the |
| 43 | +// minimum; and for D < 0 contacts are increased above the baseline. The upper bound D <= 1 is enforced by |
| 44 | +// MEmilio's C++ core. The ODE model uses the sum of all location-specific matrices as the total contact rate. |
| 45 | +// |
| 46 | +// The total baseline contact rate (sum over all locations) equals 4+3+2+1 = 10 contacts/day, matching Tutorial 3. |
| 47 | + |
| 48 | +ScalarType total_population = 100000; |
| 49 | +ScalarType t0 = 0; |
| 50 | +ScalarType tmax = 100; |
| 51 | +ScalarType dt = 0.1; |
| 52 | + |
| 53 | +// Create an OSECIR model with location-specific contact matrices. |
| 54 | +mio::osecir::Model<ScalarType> create_model() |
| 55 | +{ |
| 56 | + mio::osecir::Model<ScalarType> m(1); // one age group |
| 57 | + |
| 58 | + // Set infection state stay times (in days) |
| 59 | + m.parameters.get<mio::osecir::TimeExposed<ScalarType>>() = 3.2; |
| 60 | + m.parameters.get<mio::osecir::TimeInfectedNoSymptoms<ScalarType>>() = 2.; |
| 61 | + m.parameters.get<mio::osecir::TimeInfectedSymptoms<ScalarType>>() = 6.; |
| 62 | + m.parameters.get<mio::osecir::TimeInfectedSevere<ScalarType>>() = 12.; |
| 63 | + m.parameters.get<mio::osecir::TimeInfectedCritical<ScalarType>>() = 8.; |
| 64 | + |
| 65 | + // Set infection state transition probabilities |
| 66 | + m.parameters.get<mio::osecir::RelativeTransmissionNoSymptoms<ScalarType>>() = 0.67; |
| 67 | + m.parameters.get<mio::osecir::TransmissionProbabilityOnContact<ScalarType>>() = 0.1; |
| 68 | + m.parameters.get<mio::osecir::RecoveredPerInfectedNoSymptoms<ScalarType>>() = 0.2; |
| 69 | + m.parameters.get<mio::osecir::RiskOfInfectionFromSymptomatic<ScalarType>>() = 0.25; |
| 70 | + m.parameters.get<mio::osecir::SeverePerInfectedSymptoms<ScalarType>>() = 0.2; |
| 71 | + m.parameters.get<mio::osecir::CriticalPerSevere<ScalarType>>() = 0.25; |
| 72 | + m.parameters.get<mio::osecir::DeathsPerCritical<ScalarType>>() = 0.3; |
| 73 | + |
| 74 | + // Create ContactMatrixGroup: 4 location matrices, each of size 1x1 (one age group). |
| 75 | + mio::ContactMatrixGroup<ScalarType> contacts(4, 1); |
| 76 | + // EXERCISE: Set up the ContactMatrix for each location with the correct baseline and (where applicable) |
| 77 | + // minimum values. The baselines should sum to 10 contacts/day, matching Tutorial 3. |
| 78 | + // Home: baseline = 4, minimum = 1 (irreducible household contacts) |
| 79 | + // School: baseline = 3 |
| 80 | + // Work: baseline = 2 |
| 81 | + // Other: baseline = 1 |
| 82 | + // Hint: use mio::ContactMatrix<ScalarType>(Eigen::MatrixX<ScalarType>::Constant(1, 1, <value>)) |
| 83 | + // or the two-argument form for baseline + minimum. |
| 84 | + // contacts[(size_t)Location::Home] = ??? |
| 85 | + // contacts[(size_t)Location::School] = ??? |
| 86 | + // contacts[(size_t)Location::Work] = ??? |
| 87 | + // contacts[(size_t)Location::Other] = ??? |
| 88 | + |
| 89 | + m.parameters.get<mio::osecir::ContactPatterns<ScalarType>>() = contacts; |
| 90 | + |
| 91 | + // Initial populations: 0.5% Exposed, 0.5% InfectedNoSymptoms, rest Susceptible |
| 92 | + m.populations[{mio::AgeGroup(0), mio::osecir::InfectionState::Exposed}] = 0.005 * total_population; |
| 93 | + m.populations[{mio::AgeGroup(0), mio::osecir::InfectionState::InfectedNoSymptoms}] = 0.005 * total_population; |
| 94 | + m.populations.set_difference_from_total({mio::AgeGroup(0), mio::osecir::InfectionState::Susceptible}, |
| 95 | + total_population); |
| 96 | + return m; |
| 97 | +} |
| 98 | + |
| 99 | +int main() |
| 100 | +{ |
| 101 | + // *** Baseline Simulation Without NPIs *** |
| 102 | + // We first create and simulate the model without any interventions to obtain a baseline. |
| 103 | + auto model_no_npi = create_model(); |
| 104 | + auto result_no_npi = mio::osecir::simulate<ScalarType>(t0, tmax, dt, model_no_npi); |
| 105 | + |
| 106 | + // *** Adding Location-specific NPIs *** |
| 107 | + // We now model a lockdown scenario in which three NPIs are activated simultaneously on day 20: |
| 108 | + // |
| 109 | + // Intervention | Location | Damping D | Effect |
| 110 | + // School closure | School | 1.0 | All school contacts eliminated |
| 111 | + // Home-office mandate | Work | 0.5 | Work contacts reduced by 50% |
| 112 | + // Public transport restriction| Other | 0.8 | Other contacts reduced by 80% |
| 113 | + // |
| 114 | + // The key difference from Tutorial 3 is that each Damping is applied to a specific location matrix by |
| 115 | + // indexing cont_freq_mat[location_index] before calling add_damping. In Tutorial 3, add_damping was called |
| 116 | + // on the whole group, reducing all contact matrices simultaneously. Home contacts are left unrestricted |
| 117 | + // here, but they cannot fall below the minimum of 1 contact/day regardless. |
| 118 | + |
| 119 | + ScalarType t_npi_start = 20.0; // day at which interventions start |
| 120 | + |
| 121 | + auto model_with_npi = create_model(); |
| 122 | + auto& cm = model_with_npi.parameters.get<mio::osecir::ContactPatterns<ScalarType>>().get_cont_freq_mat(); |
| 123 | + |
| 124 | + // EXERCISE: Add the three location-specific dampings that take effect at t_npi_start: |
| 125 | + // School closure: D = 1.0 (all school contacts eliminated) |
| 126 | + // Home-office mandate: D = 0.5 (work contacts halved) |
| 127 | + // Public transport restriction: D = 0.8 (other contacts reduced by 80%) |
| 128 | + // Hint: cm[(size_t)Location::School].add_damping(<coeff>, mio::SimulationTime<ScalarType>(t_npi_start)); |
| 129 | + // ??? |
| 130 | + |
| 131 | + // We can verify the effective total contact rates before and after the NPIs by evaluating the contact |
| 132 | + // matrix group at a specific point in time. |
| 133 | + auto contacts_before = cm.get_matrix_at(mio::SimulationTime<ScalarType>(t_npi_start - 1)); |
| 134 | + auto contacts_after = cm.get_matrix_at(mio::SimulationTime<ScalarType>(t_npi_start + 1)); |
| 135 | + std::cout << "Total contacts before NPIs (day " << (int)(t_npi_start - 1) << "): " << contacts_before(0, 0) |
| 136 | + << " / day\n"; |
| 137 | + std::cout << "Total contacts after NPIs (day " << (int)(t_npi_start + 1) << "): " << contacts_after(0, 0) |
| 138 | + << " / day\n"; |
| 139 | + |
| 140 | + // We simulate the NPI scenario from t0 to tmax. |
| 141 | + auto result_with_npi = mio::osecir::simulate<ScalarType>(t0, tmax, dt, model_with_npi); |
| 142 | + |
| 143 | + // *** Print results *** |
| 144 | + // Interpolate both results to full integer days and print the InfectedSymptoms column for comparison. |
| 145 | + auto interp_no_npi = mio::interpolate_simulation_result(result_no_npi); |
| 146 | + auto interp_with_npi = mio::interpolate_simulation_result(result_with_npi); |
| 147 | + |
| 148 | + std::cout << "\n--- Without NPIs ---\n"; |
| 149 | + interp_no_npi.print_table({"S", "E", "C", "C_confirmed", "I", "I_confirmed", "H", "U", "R", "D"}, 12, 4); |
| 150 | + std::cout << "\n--- With location-specific NPIs ---\n"; |
| 151 | + interp_with_npi.print_table({"S", "E", "C", "C_confirmed", "I", "I_confirmed", "H", "U", "R", "D"}, 12, 4); |
| 152 | + |
| 153 | + // Optional: export to CSV for plotting in Python (see tutorial10.py for the corresponding visualization). |
| 154 | + // interp_no_npi.export_csv("result_no_npi.csv", |
| 155 | + // {"S", "E", "C", "C_confirmed", "I", "I_confirmed", "H", "U", "R", "D"}); |
| 156 | + // interp_with_npi.export_csv("result_with_npi.csv", |
| 157 | + // {"S", "E", "C", "C_confirmed", "I", "I_confirmed", "H", "U", "R", "D"}); |
| 158 | + |
| 159 | + // *** Summary *** |
| 160 | + // In this tutorial, we have introduced location-specific contact patterns via ContactMatrixGroup and |
| 161 | + // applied targeted NPIs to individual location matrices. Key takeaways: |
| 162 | + // - A ContactMatrixGroup collects one ContactMatrix per location (Home, School, Work, Other). |
| 163 | + // - Each ContactMatrix has a baseline (normal contacts) and a minimum (irreducible lower bound). |
| 164 | + // - Location-specific NPIs are applied with cont_freq_mat[location_index].add_damping(...). |
| 165 | + // In contrast, Tutorial 3 used add_damping on the whole group (all matrices simultaneously). |
| 166 | + // - The effective contact rate per location is C_eff = C_baseline - D*(C_baseline - C_minimum), |
| 167 | + // and the ODE model receives the sum over all locations. |
| 168 | + |
| 169 | + return 0; |
| 170 | +} |
0 commit comments