Global Health Policy Simulation model
| Home | Quick Start | User Guide | Schemas | Models | Architecture | Data Model | Developer Guide | Technical docs | API |
Last updated: August 2026
This guide is the person-centric view of Health-GPS. If you open one modelling page and want the overall gist of the framework, start here.
Health-GPS builds a virtual population of Person entities and updates their characteristics each simulated year. The pages below walk through every main attribute on that person: where it lives in memory, which module sets it, which inputs drive it, and what can change later.
I wrote it for modellers and engineers who need a clear map without reading the whole codebase first. Code pointers are included so you can jump into the source when you need detail.
| Topic | Document |
|---|---|
| Module pipeline (short) | Models overview |
| Module and ModelName I/O | Simulation models reference |
| FINCH income, PA, predictors | FINCH guide |
| Height by income stratum | Height CSV quintile plan |
| Weight by income stratum | Weight quintile plan |
| Income categories / strata | Dynamic income categories, Income quintile plan |
| Person IDs across scenarios | Same person ID plan |
| Config layout | User Guide, Schemas |
| Architecture | Software Architecture |
| Technical index | technical/README.md |
Every simulated individual is a Person (src/HealthGPS/person.h). Think of it as the full state vector for one life course inside one scenario run.
The Mermaid diagram below is the overview used on the documentation home page as well. Each box names an attribute group and the core assignment equation. Section 2 writes those equations out in full, matching the C++ implementation.
%%{init: {"flowchart": {"nodeSpacing": 36, "rankSpacing": 48, "padding": 16}}}%%
flowchart TB
subgraph ROW1 [" "]
direction LR
subgraph DEMO ["1. Demographics"]
direction LR
AGE["Age<br/>n from population shares<br/>yearly: age := age + 1"]
GEN["Gender<br/>age-sex table / SRB<br/>male = 1, female = 0"]
REG["Region / ethnicity<br/>CDF from prevalence"]
SEC["Sector<br/>Bernoulli rural prevalence"]
end
subgraph SOCIO ["2. Socio-economic"]
direction LR
SES["SES<br/>ses ~ Normal(mu, sigma)<br/>newborns only on update"]
INC["Continuous income<br/>I = Z + eps, then clamp<br/>equal-rank categories"]
CAT["Categorical income<br/>softmax logits to category"]
end
end
PERSON(["PERSON<br/>virtual individual state<br/>src/HealthGPS/person.h"])
subgraph ROW2 [" "]
direction LR
subgraph RF ["3. Behaviour and risk factors"]
direction LR
PA["Physical activity<br/>simple: mu * exp(eps - 0.5 sigma^2)<br/>or continuous: clamp(Z + eps)"]
FOOD["Foods / nutrients<br/>Stage 1: logistic P(zero)<br/>Stage 2: mu * BoxCox^-1(Z, lambda)"]
end
subgraph BODY ["4. Body, disease, and status"]
direction LR
WHB["Weight / Height / BMI<br/>W = W_exp * q(EI/PA)<br/>BMI = W / h_m^2"]
DIS["Diseases and death<br/>P = rate * RR / mean RR<br/>P_death = 1 - survival"]
end
end
DEMO --> PERSON
SOCIO --> PERSON
PERSON --> RF
PERSON --> BODY
How Health-GPS models a person. Top row: demographics and socio-economic status side by side. Centre: Person. Bottom row: behaviour/risk factors and body/disease/status side by side. Section 2 writes the equations in full.
| Group | Fields on Person |
Typical owner |
|---|---|---|
| Identity | id() |
Population |
| Core demographics | age, gender, region, ethnicity |
Demographic module |
| Urban / rural | sector |
StaticLinear (when configured) |
| SES noise | ses |
SES module |
| Income | income_continuous, income, income_adjustment_stratum |
StaticLinear + project_requirements |
| Physical activity | physical_activity (+ often risk_factors["PhysicalActivity"]) |
StaticLinear or hierarchical RF |
| Risk factors | risk_factors map |
Static then dynamic RF models |
| Anthropometrics | Height, Weight, BMI inside risk_factors |
Kevin Hall (FINCH) or HLM/EBHLM (France) |
| Diseases | diseases map |
Disease module |
| Status | alive / emigrated / event times | Demographic + migration |
Many nutrients and behaviours exist only inside risk_factors. Dedicated members (physical_activity, income_continuous) exist when the model needs them outside that map as well.
These are the equations Health-GPS actually evaluates when assigning person characteristics. Notation matches the code (evaluate_linear_model, inverse_box_cox, update_age_and_death_events, Kevin Hall helpers, and the default disease model).
Used for continuous income, continuous PA, Box-Cox risk factors, and logistic Stage 1 scores:
Z = intercept + sum_k (beta_k * x_k) + sum_m (beta_m * log(x_m))
evaluate_linear_model skips metadata coefficients such as stddev, min, max, and lambda. Predictors x_k come from person fields and risk_factors (age, gender encodings, region/ethnicity dummies, income, nutrients, and so on). See the FINCH guide for predictor name mapping.
initial cohort: id(i) = i + 1 for slot index i = 0 .. N-1
new entrants: id = next_person_id++ (newborns and immigrants)
IDs are never reused after death or emigration.
n_age,sex = round( population_share(year, age, sex) * virtual_population_size )
Yearly for survivors:
age := age + 1
Birth sex uses the life-table sex ratio at birth (SRB). Encoding used elsewhere:
gender_value(male) = 1
gender_value(female) = 0
With prevalence tables enabled:
U ~ Uniform(0, 1)
region = first category whose cumulative prevalence(age, gender) >= U
U ~ Uniform(0, 1)
ethnicity = first category whose cumulative prevalence(age_group, gender, region) >= U
Age group is Under18 if age < 18, else Over18. Newborns require an exact age_0 region row.
U ~ Uniform(0, 1)
sector = rural if U < rural_prevalence(age_group, gender)
= urban otherwise
At age 18, if currently rural:
p_urban = 1 - rural_prevalence(Over18, gender) / rural_prevalence(Under18, gender)
U ~ Uniform(0, 1); become urban if U < p_urban
ses ~ Normal(mu, sigma)
from modelling.ses_model (function_name = "normal", parameters [mu, sigma]). Redrawn only for newborns on yearly update.
I0 = Z_income
eps ~ Normal(0, sigma_income) if stddev coefficient present
I = clamp(I0 + eps, min, max) if min/max coefficients present
Store income_continuous = I and risk_factors["income"] = I.
Equal-rank reporting categories with C categories (project_requirements.income.categories):
sort active incomes I_(1) <= ... <= I_(n)
threshold_j = I_( round((n-1) * j/C) ) for j = 1 .. C-1
category(I) = strata[j] if I <= threshold_j, else highest stratum
Adjustment strata (0 .. N-1) use the same equal-rank idea with N = adjustment_income_stratum_count.
logit_c = intercept_c + Sum beta_{c,k} * x_k
p_c = exp(logit_c) / Sum_j exp(logit_j)
category ~ Categorical(p)
Simple type:
mu = expected PhysicalActivity(gender, age)
eps ~ Normal(0, sigma)
PA = clamp( mu * exp(eps - 1/2 sigma^2) )
Continuous type:
PA = clamp( Z_PA + eps ), eps ~ Normal(0, sigma)
Both write physical_activity and risk_factors["PhysicalActivity"].
Correlated residuals first (r from Cholesky of the residual correlation), then optional two-stage path.
Stage 1 (if a logistic model exists for factor f):
p_zero = logistic(Z_logistic)
U ~ Uniform(0, 1)
if U < p_zero: risk_factors[f] = 0
Stage 2 (non-zero path, or Box-Cox-only factors):
Z = Z_linear + r * sigma
if |lambda| ~= 0: BoxCox^{-1}(Z, lambda) = exp(Z)
else: BoxCox^{-1}(Z, lambda) = (lambda Z + 1)^(1/lambda) (0 if base <= 0)
risk_factors[f] = clamp( mu_f * BoxCox^{-1}(Z, lambda) )
mu_f is the expected factors-mean value for that person (gender, age, and optional income stratum).
(EI/PA)_expected = EnergyIntake_expected / PhysicalActivity_expected
(EI/PA)_actual = EnergyIntake / PhysicalActivity
q = quantile( (EI/PA)_actual / (EI/PA)_expected ; weight quantile curve )
Weight = Weight_expected * q
Height (slope and sigma may depend on income adjustment stratum):
eps_H = Height_residual ~ Normal(0, sigma) drawn once
Height = Height_expected * (Weight^slope / mean(Weight^slope)) * (exp(eps_H) / exp(1/2 sigma^2))
h_m = Height / 100
BMI = Weight / h_m^2
Prevalence initialisation and yearly incidence share the same shape:
RR = product over risk-factor RRs * product over comorbid-disease RRs
P = rate(age, sex) * RR / mean_RR(age, sex)
U ~ Uniform(0, 1); disease becomes active if U < P
rate is prevalence at init and incidence on yearly update. Intervention runs may apply PIF:
P := P * (1 - PIF(age, sex, years_since_start))
Remission (if modelled):
U ~ Uniform(0, 1); status := free if U < remission(age, sex)
Residual (non-modelled) mortality is calibrated from life-table death rates and mean excess-mortality products. For an active person each year:
survival = (1 - residual_death(age, sex))
for each active disease d:
survival = survival * (1 - excess_mortality_d)
P_death = 1 - survival
U ~ Uniform(0, 1); die if age >= max_age or U < P_death
Survivors then take age := age + 1.
Attributes are not all drawn once. Health-GPS uses a fixed module order at initialisation and again each year.
flowchart TB
subgraph init [Initialisation once per run]
P0[Population creates slots and IDs]
D0[Demographics: age, gender, region, ethnicity]
S0[SES draw]
R0[Static RF model then dynamic generate]
X0[Diseases: prevalence start]
A0[Analysis snapshot]
P0 --> D0 --> S0 --> R0 --> X0 --> A0
end
subgraph year [Each simulated year]
D1[Demographics: death, age++, births]
M1[Net immigration]
S1[SES for newborns only]
R1[Static then dynamic RF update]
X1[Disease remission and incidence]
A1[Analysis publish]
D1 --> M1 --> S1 --> R1 --> X1 --> A1
end
init --> year
| Timing | What typically happens |
|---|---|
| Population construction | Lifetime-unique id for each slot |
| Demographic init | Age and gender from population tables; optional region and ethnicity |
| SES init | Continuous ses for everyone |
| Static RF generate | Sector, income, PA, foods/nutrients (model-dependent) |
| Dynamic RF generate | Height, weight, BMI, energy state (Kevin Hall) or hierarchical updates (EBHLM) |
| Disease init | Starting prevalence |
| Yearly demographic | Deaths, survivors age by one year, births at age 0 |
| Yearly RF update | Newborns re-initialised; children/adults follow dynamic rules |
| Yearly disease | Remission then new incidence |
| Migration | Emigration flags or immigrant clones with new IDs |
For the module-level I/O view, see Simulation models reference. For the short pipeline summary, see Models overview.
| Field | Private id_; read with id(). Sentinel Person::unassigned_id (0) until Population assigns an ID. |
| Initial cohort | Slot index i gets ID i + 1. |
| Newborns and immigrants | Monotonic next_person_id_++. Slots may be reused; IDs are never reused. |
| Inputs | None. Same seed and size give the same initial ID sequence across baseline and intervention. |
| Later changes | Never. Death and emigration do not free the ID for reuse. |
flowchart LR
CTOR[Population constructor] --> INIT["IDs 1..N"]
BIRTH[add_newborn_babies] --> NEXT[allocate_next_person_id]
ADD[add immigrant / entity] --> NEXT
NEXT --> UNIQUE[Lifetime-unique ID]
Detail: Same person ID plan. Optional CSV export: Individual ID tracking plan.
| Field | unsigned int age |
| Initial set | DemographicModule::initialise_population fills age by sex from datastore population shares for the start year, scaled to the virtual population size. |
| Inputs | Backend population tables; inputs.settings (country_code, size_fraction, age_range). |
| Each year | Survivors: age += 1. Newborns enter at age 0. People at or above age_range upper bound die before ageing further. |
| Field | core::Gender gender |
| Initial set | Male and female counts from the same age-sex distribution (with rounding correction on the last bins). |
| Births | Sex ratio at birth from the life table / birth process; newborns created via add_newborn_babies(..., gender, ...). |
| Encoding | gender_to_value: male = 1, female = 0. FINCH linear models may also use gender2 (see FINCH guide). |
| Later changes | Fixed for life. |
flowchart TD
POP[Datastore population by year, age, sex] --> SHARE[Age-sex shares]
SHARE --> SIZE[Scale to virtual population size]
SIZE --> ASSIGN[Set person.age and person.gender]
BIRTHS[Yearly births from birth rates] --> BABY[New person age 0 + sampled sex]
SURV[Survivors after death process] --> INC[age plus 1]
Code: src/HealthGPS/demographic.cpp (initialise_population, update_age_and_death_events).
These are optional demographics extras. France-style HLM packs often leave them unused. FINCH-style packs usually enable region and ethnicity through project_requirements.demographics.
| Field | std::string region (default "unknown") |
| When | Init for everyone; yearly only for newborns (age == 0). |
| How | initialise_region: CDF sample from age- and gender-specific prevalence. Exact age_0 required for newborns; other ages may use closest available age key. |
| Inputs | Region prevalence CSV registered from the static model file (RegionFile path). Gate: project_requirements.demographics.region. |
| Field | std::string ethnicity |
| When | After region, same timing (init + newborns). |
| How | initialise_ethnicity: prevalence by age group (Under18 / Over18), gender, and current region. |
| Inputs | Ethnicity CSV (EthnicityFile). Gate: project_requirements.demographics.ethnicity. |
| Remap | CSV codes "1".."4" become "ethnicity1".."ethnicity4"; other names kept as-is. |
| Field | core::Sector sector |
| When | StaticLinear init; yearly update mainly for rural people turning 18. |
| How | initialise_sector uses Under18/Over18 rural prevalence by gender. At age 18, rural people may move to urban with probability derived from the prevalence ratio. |
| Inputs | RuralPrevalence in the static model JSON. |
flowchart TD
PR[project_requirements.demographics] --> GATE{region / ethnicity enabled?}
GATE -->|yes| REGCSV[Region prevalence CSV]
GATE -->|yes| ETHCSV[Ethnicity prevalence CSV]
REGCSV --> REG[person.region]
REG --> ETH[person.ethnicity depends on region]
RURAL[RuralPrevalence in static JSON] --> SEC[person.sector]
SEC --> AGE18[Age 18 rural to urban transition]
Code: demographic.cpp for region/ethnicity; static_linear_model.cpp for sector.
SES noise and income are different. Mixing them up is a common source of confusion when reading France vs FINCH configs.
| Field | double ses |
| How | SESNoiseModule: draw N(mean, sd) from modelling.ses_model (function_name must be "normal"). |
| When | Everyone at init; newborns only on yearly update. Adults keep their draw. |
| Role | Continuous predictor for hierarchical models (especially HLM France). |
| Field | Meaning |
|---|---|
income_continuous |
Continuous income value |
risk_factors["income"] |
Usually the same continuous value in FINCH |
income (core::Income) |
Reporting category (Low / LowerMid / UpperMid / High, or 3-category layout) |
income_adjustment_stratum |
Rank bucket 0..N-1 for optional quintile factors-mean tables |
Typical order inside StaticLinear generate:
income_adjustment_stratum).income categories for outputs (project_requirements.income.categories).initialise_categorical_income draws a category from logits / softmax and stores the category numeric value in risk_factors["income"]. No continuous income pipeline.
flowchart TD
SESMOD[modelling.ses_model] --> SES[person.ses]
INCMODE{Income model type}
INCMODE -->|continuous FINCH| CONT[income_continuous + RF income]
INCMODE -->|categorical| CAT[person.income enum]
CONT --> ADJ[Optional factors-mean]
ADJ --> STRAT[Optional adjustment strata 0..N-1]
STRAT --> FINAL[Final income categories 3 or 4 or 5]
CAT --> OUT[Outputs / predictors]
FINAL --> OUT
Deep dives: FINCH guide, Dynamic income categories plan, Income quintile factor means plan.
| Fields | physical_activity and usually risk_factors["PhysicalActivity"] (kept in sync on the StaticLinear path). |
| Gate | project_requirements.physical_activity.enabled |
| Simple type | Expected mean by age/sex * lognormal-style noise. |
| Continuous type | Linear model + noise + clamp to configured bounds. |
| When | Init for the population; yearly re-init for newborns. Adults are not randomly re-drawn each year on this path. |
| Kevin Hall | Reads PA when forming energy / PA ratios for weight; it does not own the PA assignment. |
| HLM France | PA is often just another hierarchical risk factor (PA) inside HLM/EBHLM, not the dedicated StaticLinear PA models. |
flowchart LR
PR[project_requirements.physical_activity] --> EN{enabled?}
EN -->|no| SKIP[Leave unset / unused]
EN -->|yes| TYPE{type}
TYPE -->|simple| SIMP[Mean by age sex + noise]
TYPE -->|continuous| CONT[Linear model + noise + clamp]
SIMP --> STORE[physical_activity and RF PhysicalActivity]
CONT --> STORE
STORE --> KH[Kevin Hall may read PA]
On FINCH / Kevin Hall packs these are explicit anthropometrics. On HLM France, BMI (and related factors) usually come from the hierarchical model instead.
risk_factors["Weight"], then optionally mean-adjust the population.Children under 19 typically re-initialise weight on update. Adults use the Kevin Hall energy-balance run.
After weight:
resolve_height_params_for_person).update_height combines expected height, weight slope term, and residual.BMI = Weight / (Height in metres)^2, recomputed after anthropometric updates.
flowchart TD
FOOD[Foods / nutrients from static model] --> EI[Energy intake]
PA[Physical activity] --> RATIO[EI/PA actual vs expected]
EI --> RATIO
RATIO --> WQ[Weight quantile curve by stratum]
WQ --> W[Weight]
W --> H[Height from slope, std, residual]
W --> BMI[BMI]
H --> BMI
Detail and console tables: Height CSV quintile plan, Weight quintile plan.
Everything else in Person.risk_factors (foods, nutrients, residuals, policy/trend copies, Kevin Hall internal state, and so on) is owned by the registered static and dynamic models.
{name}_residual.flowchart TB
CFG[modelling.risk_factor_models] --> ST[Static model generate]
CFG --> DY[Dynamic model generate / update]
ST --> MAP[person.risk_factors]
DY --> MAP
MAP --> DIS[Disease relative risks]
MAP --> OUT[Analysis aggregates and optional ID tracking]
Predictor naming, Box-Cox, and policy equations for FINCH: FINCH guide. Per-model I/O: Simulation models reference.
| Field | diseases map of Disease{status, start_time, time_since_onset} |
| Status | free or active |
| Initial | Prevalence draw using risk-factor relative risks (and mean RR normalisation). |
| Yearly | Remission (if applicable), then incidence. Age 0 clears disease history before infant logic runs. |
| Cancer models | Track time_since_onset and duration-based excess mortality. |
| Inputs | Datastore disease rates and RR tables; diseases selected in running; optional PIF on intervention incidence. |
| Feedback | Active disease excess mortality feeds the demographic death calculation. |
flowchart TD
INIT[initialise_disease_status from prevalence x RR] --> YEAR[Each year]
YEAR --> REM[Remission draws for active cases]
REM --> INC[Incidence draws for free cases]
INC --> EX[Excess mortality tables]
EX --> DEATH[Demographic death hazard]
Code: disease.cpp, default_disease_model.cpp, default_cancer_model.cpp.
| Field / accessor | Meaning |
|---|---|
is_alive() |
False after die(time) |
has_emigrated() |
True after emigrate(time) |
time_of_death() / time_of_migration() |
Event times |
is_active() |
Alive and not emigrated |
Death. update_age_and_death_events combines residual (non-modelled) mortality with excess mortality from active diseases. People at max age die. Survivors then age by one year.
Emigration. When net migration for an age-sex cell is negative, active people in that cell are marked emigrated.
Immigration. When net migration is positive, the engine clones a similar active person (partial_clone_entity), assigns a new lifetime ID, and adds them. The clone copies age, gender, region, ethnicity, ses, sector, income category, risk factors, and diseases. Continuous-income extras and some PA/stratum bookkeeping may be incomplete until later module updates.
flowchart TD
LIFE[Active person] --> HAZ{Death hazard this year?}
HAZ -->|yes| DEAD[die time]
HAZ -->|no| AGE[age plus 1]
LIFE --> NET{Net migration for age-sex}
NET -->|outflow| EMI[emigrate time]
NET -->|inflow| CLONE[Clone similar person + new ID]
Same Person type. Different models fill different fields.
| Attribute | HLM France typical path | FINCH typical path |
|---|---|---|
| ID, age, gender, death, migration | Shared demographic / population logic | Same |
| Region / ethnicity / sector | Usually unused | Enabled via demographics + StaticLinear |
ses |
Important HLM predictor | Drawn, but income is the main socio-economic driver |
| Income continuous / category / stratum | Usually unused | StaticLinear + project_requirements |
| Physical activity | Hierarchical RF (PA) |
Dedicated PA models + member field |
| Foods / nutrients | HLM then EBHLM | StaticLinear (+ policies / trends) |
| Height / Weight / BMI physiology | BMI as hierarchical RF | Kevin Hall anthropometrics |
| Diseases | Shared disease module | Shared disease module |
flowchart LR
subgraph france [HLM France stack]
HLM[static hlm] --> EBHLM[dynamic ebhlm]
end
subgraph finch [FINCH stack]
SL[static staticlinear] --> KH[dynamic kevinhall]
end
france --> PERSON[Person state]
finch --> PERSON
Example packs: HLM_France, KevinHall_FINCH.
| Concern | Primary code | Primary config / data |
|---|---|---|
| Person fields | src/HealthGPS/person.h |
n/a |
| IDs | population.cpp |
running seeds / population size |
| Age, gender, births, deaths | demographic.cpp |
inputs.settings, datastore population / births / deaths |
| Region / ethnicity | demographic.cpp |
project_requirements.demographics, region/ethnicity CSVs |
| SES | ses_noise_module.cpp |
modelling.ses_model |
| Sector, income, PA, foods | static_linear_model.cpp |
static model JSON + CSVs, project_requirements |
| Height, weight, BMI | kevin_hall_model.cpp |
dynamic Kevin Hall JSON + CSVs |
| Hierarchical RF (France) | static_hierarchical_linear_model.cpp, dynamic_hierarchical_linear_model.cpp |
HLM / EBHLM JSON |
| Diseases | disease.cpp, default disease/cancer models |
datastore disease tables, running disease list |
| Migration clone | simulation.cpp |
population targets vs simulated counts |
| Factors-mean adjustment | risk_factor_adjustable_model.cpp |
modelling.baseline_adjustments |
Author: Mahima Ghosh