Skip to content

Commit ae931ba

Browse files
committed
cpp tutorials (2,10,11) + comment on constant flows in t2
1 parent 3a4ae02 commit ae931ba

6 files changed

Lines changed: 484 additions & 4 deletions

File tree

cpp-tutorials/CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ target_link_libraries(tutorial1 PRIVATE memilio ode_secir Boost::filesystem)
5757
add_executable(exercise1 exercises/exercise1.cpp)
5858
target_link_libraries(exercise1 PRIVATE memilio ode_secir Boost::filesystem)
5959

60+
add_executable(tutorial2 tutorial2.cpp)
61+
target_link_libraries(tutorial2 PRIVATE memilio ode_secir Boost::filesystem)
62+
6063
add_executable(tutorial3 tutorial3.cpp)
6164
target_link_libraries(tutorial3 PRIVATE memilio ode_secir Boost::filesystem)
6265

@@ -66,6 +69,12 @@ target_link_libraries(tutorial5 PRIVATE memilio ode_secir Boost::filesystem)
6669
add_executable(tutorial7 tutorial7.cpp)
6770
target_link_libraries(tutorial7 PRIVATE memilio ode_secir Boost::filesystem)
6871

72+
add_executable(tutorial10 tutorial10.cpp)
73+
target_link_libraries(tutorial10 PRIVATE memilio ode_secir Boost::filesystem)
74+
75+
add_executable(tutorial11 tutorial11.cpp)
76+
target_link_libraries(tutorial11 PRIVATE memilio ode_secir Boost::filesystem)
77+
6978
add_executable(tutorial12_ensemble_sims tutorial12_ensemble_sims.cpp)
7079
target_link_libraries(tutorial12_ensemble_sims PRIVATE memilio ode_secir)
7180

cpp-tutorials/tutorial10.cpp

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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+
// Home contacts: baseline 4/day, minimum 1/day (irreducible household contacts)
77+
contacts[(size_t)Location::Home] = mio::ContactMatrix<ScalarType>(Eigen::MatrixX<ScalarType>::Constant(1, 1, 4.0),
78+
Eigen::MatrixX<ScalarType>::Constant(1, 1, 1.0));
79+
// School contacts: baseline 3/day
80+
contacts[(size_t)Location::School] =
81+
mio::ContactMatrix<ScalarType>(Eigen::MatrixX<ScalarType>::Constant(1, 1, 3.0));
82+
// Work contacts: baseline 2/day
83+
contacts[(size_t)Location::Work] = mio::ContactMatrix<ScalarType>(Eigen::MatrixX<ScalarType>::Constant(1, 1, 2.0));
84+
// Other contacts: baseline 1/day
85+
contacts[(size_t)Location::Other] = mio::ContactMatrix<ScalarType>(Eigen::MatrixX<ScalarType>::Constant(1, 1, 1.0));
86+
87+
m.parameters.get<mio::osecir::ContactPatterns<ScalarType>>() = contacts;
88+
89+
// Initial populations: 0.5% Exposed, 0.5% InfectedNoSymptoms, rest Susceptible
90+
m.populations[{mio::AgeGroup(0), mio::osecir::InfectionState::Exposed}] = 0.005 * total_population;
91+
m.populations[{mio::AgeGroup(0), mio::osecir::InfectionState::InfectedNoSymptoms}] = 0.005 * total_population;
92+
m.populations.set_difference_from_total({mio::AgeGroup(0), mio::osecir::InfectionState::Susceptible},
93+
total_population);
94+
return m;
95+
}
96+
97+
int main()
98+
{
99+
// *** Baseline Simulation Without NPIs ***
100+
// We first create and simulate the model without any interventions to obtain a baseline.
101+
auto model_no_npi = create_model();
102+
auto result_no_npi = mio::osecir::simulate<ScalarType>(t0, tmax, dt, model_no_npi);
103+
104+
// *** Adding Location-specific NPIs ***
105+
// We now model a lockdown scenario in which three NPIs are activated simultaneously on day 20:
106+
//
107+
// Intervention | Location | Damping D | Effect
108+
// School closure | School | 1.0 | All school contacts eliminated
109+
// Home-office mandate | Work | 0.5 | Work contacts reduced by 50%
110+
// Public transport restriction| Other | 0.8 | Other contacts reduced by 80%
111+
//
112+
// The key difference from Tutorial 3 is that each Damping is applied to a specific location matrix by
113+
// indexing cont_freq_mat[location_index] before calling add_damping. In Tutorial 3, add_damping was called
114+
// on the whole group, reducing all contact matrices simultaneously. Home contacts are left unrestricted
115+
// here, but they cannot fall below the minimum of 1 contact/day regardless.
116+
117+
ScalarType t_npi_start = 20.0; // day at which interventions start
118+
119+
auto model_with_npi = create_model();
120+
auto& cm = model_with_npi.parameters.get<mio::osecir::ContactPatterns<ScalarType>>().get_cont_freq_mat();
121+
122+
// School closure: damping of 1.0 -> effective school contacts reach the minimum (0)
123+
cm[(size_t)Location::School].add_damping(1.0, mio::SimulationTime<ScalarType>(t_npi_start));
124+
// Home-office mandate: damping of 0.5 -> work contacts halved
125+
cm[(size_t)Location::Work].add_damping(0.5, mio::SimulationTime<ScalarType>(t_npi_start));
126+
// Restrictions in public transport: damping of 0.8 -> other contacts reduced by 80%
127+
cm[(size_t)Location::Other].add_damping(0.8, mio::SimulationTime<ScalarType>(t_npi_start));
128+
129+
// We can verify the effective total contact rates before and after the NPIs by evaluating the contact
130+
// matrix group at a specific point in time.
131+
auto contacts_before = cm.get_matrix_at(mio::SimulationTime<ScalarType>(t_npi_start - 1));
132+
auto contacts_after = cm.get_matrix_at(mio::SimulationTime<ScalarType>(t_npi_start + 1));
133+
std::cout << "Total contacts before NPIs (day " << (int)(t_npi_start - 1) << "): " << contacts_before(0, 0)
134+
<< " / day\n";
135+
std::cout << "Total contacts after NPIs (day " << (int)(t_npi_start + 1) << "): " << contacts_after(0, 0)
136+
<< " / day\n";
137+
138+
// We simulate the NPI scenario from t0 to tmax.
139+
auto result_with_npi = mio::osecir::simulate<ScalarType>(t0, tmax, dt, model_with_npi);
140+
141+
// *** Print results ***
142+
// Interpolate both results to full integer days and print the InfectedSymptoms column for comparison.
143+
auto interp_no_npi = mio::interpolate_simulation_result(result_no_npi);
144+
auto interp_with_npi = mio::interpolate_simulation_result(result_with_npi);
145+
146+
std::cout << "\n--- Without NPIs ---\n";
147+
interp_no_npi.print_table({"S", "E", "C", "C_confirmed", "I", "I_confirmed", "H", "U", "R", "D"}, 12, 4);
148+
std::cout << "\n--- With location-specific NPIs ---\n";
149+
interp_with_npi.print_table({"S", "E", "C", "C_confirmed", "I", "I_confirmed", "H", "U", "R", "D"}, 12, 4);
150+
151+
// Optional: export to CSV for plotting in Python (see tutorial10.py for the corresponding visualization).
152+
// interp_no_npi.export_csv("result_no_npi.csv",
153+
// {"S", "E", "C", "C_confirmed", "I", "I_confirmed", "H", "U", "R", "D"});
154+
// interp_with_npi.export_csv("result_with_npi.csv",
155+
// {"S", "E", "C", "C_confirmed", "I", "I_confirmed", "H", "U", "R", "D"});
156+
157+
// *** Summary ***
158+
// In this tutorial, we have introduced location-specific contact patterns via ContactMatrixGroup and
159+
// applied targeted NPIs to individual location matrices. Key takeaways:
160+
// - A ContactMatrixGroup collects one ContactMatrix per location (Home, School, Work, Other).
161+
// - Each ContactMatrix has a baseline (normal contacts) and a minimum (irreducible lower bound).
162+
// - Location-specific NPIs are applied with cont_freq_mat[location_index].add_damping(...).
163+
// In contrast, Tutorial 3 used add_damping on the whole group (all matrices simultaneously).
164+
// - The effective contact rate per location is C_eff = C_baseline - D*(C_baseline - C_minimum),
165+
// and the ODE model receives the sum over all locations.
166+
167+
return 0;
168+
}

0 commit comments

Comments
 (0)