Tutorial 3: Customize with build functions
This tutorial introduces custom build functions — the mechanism for modifying the baseline
country model. Instead of passing the Zambia model's functions directly to EMODTask, you
write your own that call the baseline first and then apply additional changes.
File: tutorials/tutorial_3_build_functions.py
What are build functions?
Build functions are the functions that EMODTask calls to construct the configuration files
for each simulation. There are four:
| Function | Purpose |
|---|---|
build_config(config) |
Sets simulation-wide configuration parameters |
build_campaign(campaign) |
Defines when and how interventions are distributed |
build_demographics() |
Sets up population, mortality, fertility, and relationship parameters |
build_reports(reporters) |
Configures which output reports to generate |
Each build function is called once per simulation. If your experiment has a sweep of 10 simulations, each build function runs 10 times.
Modifying configuration
This build_config calls the Zambia baseline first and then modifies two parameters —
doubling the initial population and reducing the simulation duration by 10 years:
def build_config(config):
zambia = cm.ZambiaForTraining
config = zambia.build_config(config)
config.parameters.x_Base_Population = config.parameters.x_Base_Population * 2.0
config.parameters.Simulation_Duration = config.parameters.Simulation_Duration - (10 * 365)
return config
The larger population makes the simulation more statistically robust, while the shorter duration partially offsets the added runtime. Simulations may take 5–7 minutes.
Adding an intervention to the campaign
This build_campaign calls the Zambia baseline to build the standard care cascade and then
adds an annual mass distribution of a long-acting PrEP (LA-PrEP) intervention starting in 2025,
with coverage increasing each year. Using a Python for loop avoids duplicating the same
event configuration 15 times in JSON:
def build_campaign(campaign):
zambia = cm.ZambiaForTraining
zambia.build_campaign(campaign)
laprep = ControlledVaccine(campaign,
waning_config=MapPiecewise(
days=[0, 180, 210, 240, 270, 300, 330],
effects=[0.8, 0.8, 0.7, 0.5, 0.3, 0.1, 0.0]),
common_intervention_parameters=CIP(intervention_name="LA-PrEP"))
ip_restrictions = PropertyRestrictions(
individual_property_restrictions=[["Accessibility: Yes", "Risk: HIGH"],
["Accessibility: Yes", "Risk: MEDIUM"]])
laprep_coverages = [0.1, 0.3, 0.5, 0.5, 0.5, 0.7, 0.7, 0.7, 0.8, 0.8, 0.8, 0.8, 0.9, 0.9, 0.9]
start_year = 2025
for coverage in laprep_coverages:
start_day = (start_year - 1960.5) * 365
add_intervention_scheduled(campaign, start_day=start_day,
target_demographics_config=TDC(demographic_coverage=coverage),
property_restrictions=ip_restrictions,
intervention_list=[laprep])
start_year += 1
return campaign
The ip_restrictions target only people who have access to healthcare (Accessibility: Yes)
and are at high or medium risk. This property must exist in the demographics — which is what
build_demographics sets up.
Modifying demographics
This build_demographics calls the Zambia baseline and then raises the proportion of the
population with access to healthcare to 90%:
def build_demographics():
zambia = cm.ZambiaForTraining
demographics = zambia.build_demographics()
demographics.AddIndividualPropertyAndHINT(
Property="Accessibility",
Values=["Yes", "No"],
InitialDistribution=[0.9, 0.1],
overwrite_existing=True)
return demographics
Using custom build functions
The custom functions are passed to EMODTask.from_defaults() in place of the country
model's built-in ones:
task = emod_task.EMODTask.from_defaults(
eradication_path=manifest.eradication_path,
schema_path=manifest.schema_file,
config_builder=build_config,
campaign_builder=build_campaign,
demographics_builder=build_demographics,
report_builder=add_reports)
The rest of the script — sweeping Run_Number, running the experiment, downloading results,
and plotting — is the same as Tutorial 2. Results are saved to tutorial_3_results/.
plot_inset_chart produces a grid of all channels from the InsetChart.json of each run,
with one line per realization, giving a quick overview of the simulation over time:
plot_population_by_age shows the population over time for each run:
plot_prevalence_for_dir shows the fraction of the population infected with HIV over time
for each run:
plot_onART_by_age shows the fraction of infected people on ART over time for each run:



