Global Health Policy Simulation model
| Home | Quick Start | User Guide | Schemas | Models | Architecture | Data Model | Developer Guide | Technical docs | API |
| Related: Individual ID tracking plan | Technical index | Documentation index |
Title: Same person ID across baseline and intervention
Overview: Assign Person IDs from population index (id = index + 1) so the same logical person has the same ID in both baseline and intervention runs. Changes are limited to Person and Population with MAHIMA comments; no change to scenario logic or event bus.
| Status | Item |
|---|---|
| Done | Confirm and document baseline -> intervention sync contract (aggregate tables only, one-way) in the plan and implementation notes. |
| Done | Add Population monotonic next_person_id_ state initialised to initial_size + 1 after initial population construction. |
| Done | Update Population add and add_newborn_babies so all post-initial entrants get ID from next_person_id_++ on both recycled-slot and append paths. |
| Done | Keep initial ID assignment as i + 1 to preserve baseline/intervention initial cohort comparability. |
| Done | Replace slot-reuse ID assertions with lifetime-unique assertions in Population tests, including recycled-slot replacement cases. |
| Done | Validate tracking output expectations (no demographic identity swapping under one ID due to slot reuse) with targeted run/test checks. |
| Done | Record runtime and memory impact checks for large runs (including 14M-population assumptions and observed deltas). |
Make the same logical person have the same ID in both baseline and intervention by deriving ID from population index (ID = index + 1) instead of a global counter. This enables tracking individuals across scenarios without breaking existing behaviour.
interventions_book_ keyed by entity.id(). Baseline and intervention are separate Simulation/Scenario instances, so they have separate maps. Same ID in both runs does not cause collision.message.id() is ResultEventMessage::id() (event type enum), not Person ID - no change.Only aggregate tables, not person-level records:
NetImmigrationMessage = age x gender net migration table ResidualMortalityMessage = age x gender residual mortality rates No person object, no ID, no region/ethnicity individual records are transferred between scenarios.
| Context | ID assigned |
|---|---|
Initial population slot i |
i + 1 |
Newborn replacing slot i |
i + 1 (slot keeps its ID) |
Newborn added via emplace_back (new slot) |
people_.size() (new index + 1) |
Person added via add() (immigration clone) |
Set to slot index + 1 after placement |
flowchart LR
subgraph baseline [Baseline Population]
B0[Slot 0 ID 1]
B1[Slot 1 ID 2]
Bi[Slot i ID i+1]
end
subgraph intervention [Intervention Population]
I0[Slot 0 ID 1]
I1[Slot 1 ID 2]
Ii[Slot i ID i+1]
end
B0 -.same logical person.-> I0
B1 -.same logical person.-> I1
Bi -.same logical person.-> Ii
Person(std::size_t id) - sets id_ = id (for initial population construction).Person(core::Gender gender, std::size_t id) - sets gender and id_ = id (for newborns).void set_id(std::size_t id) - allows Population to assign ID after placing a person (e.g. immigration clone). Document that it is for internal use by Population.id_ at Person::unassigned_id (0) until Population assigns a lifetime-unique ID via set_id or explicit-ID constructors. Removed global Person::newUID to avoid duplicate-ID space separate from Population::next_person_id_.people_(size) (default-constructed Persons) with a loop that creates each person with ID = index + 1:
people_.reserve(size); then for (size_t i = 0; i < size; ++i) people_.emplace_back(i + 1);Person(std::size_t id) constructor.recycle.at(index): create Person(gender, recycle.at(index) + 1) so the newborn gets that slot’s ID.Person(gender, people_.size() + 1) so the new slot gets ID = new index + 1.people_.at(recycle.at(0)) = std::move(person): call people_.at(recycle.at(0)).set_id(recycle.at(0) + 1).people_.emplace_back(std::move(person)): call people_.back().set_id(people_.size()).set_id ordering.unassigned_id until Population placement.Population(init_size) and then add(Person{...}) or add_newborn_babies(...). After our changes:
population[i].id() == i + 1 for several indices; add newborns (replace and emplace), assert the replaced slot keeps ID = slot_index + 1 and the new tail has ID = size.Person{} (unassigned ID). population().add() assigns the next lifetime-unique ID via set_id.context.population()[index]; does not create Person objects.entity.id() as key within their own map; same ID in baseline and intervention is desired and safe.Population::next_person_id_ (and initial 1..N).// MAHIMA: explaining the change.| File | Changes |
|---|---|
| person.h | Add Person(std::size_t id), Person(core::Gender, std::size_t id), void set_id(std::size_t id); MAHIMA block for index-based ID. |
| person.cpp | Implement new constructors and set_id; MAHIMA comments. |
| population.cpp | Constructor: build vector with Person(i+1); add_newborn_babies: use ID = slot+1 or size+1; add: call set_id after placement; MAHIMA comments. |
| Population.Test.cpp | Add test verifying ID == index + 1 for initial and after add/newborns; MAHIMA comment. |
set_id, with comments.The implemented slot-based rule (ID = slot index + 1) achieved baseline/intervention alignment
for initial people, but it also reuses IDs when dead/emigrated slots are recycled. That allows one
ID to represent multiple different people over time (e.g. changed sex/age/region/ethnicity in
tracking), which breaks lifetime person identity.
i still gets ID i + 1.next_person_id_ in Population private state.initial_size + 1 after population construction.add() entities):
ID = next_person_id_++ regardless of recycled or appended slot.std::size_t per Population object (negligible).1..N).entity.id() as opaque key.std::size_t next_person_id_{1}; private member.Person(i + 1).next_person_id_ = size + 1.add(Person, time):
set_id(next_person_id_++) (not slot index based).add_newborn_babies(number, gender, time):
next_person_id_++.set_id and constructors.Person{} and Person(gender) behavior unchanged.1..init_size).Population.Test and ensure lifetime-unique assertions pass.Population::allocate_next_person_id() asserts each new ID equals next_person_id_
before increment (MAHIMA; no hash set; zero release cost).This is valid and expected. Baseline and intervention are separate simulation populations; a shared starting ID means “same initial person for comparison”, not “forced identical life outcome”. Policy effects can keep someone alive in intervention while baseline has death for the matched starting ID.
Yes. Synchronisation is baseline -> intervention only. Intervention receives baseline-generated aggregate synchronisation tables; intervention does not send these back to baseline.
Only aggregate tables, never person-level records:
NetImmigrationMessage: age x gender net migration table.ResidualMortalityMessage: age x gender residual mortality table.No person object, no person ID, no region/ethnicity per-person payload is transferred.
flowchart TD
startA[PersonInSlot_i_ID_iPlus1] --> deathA[PersonDiesOrEmigrates]
deathA --> recycleA[Slot_iMarkedRecyclable]
recycleA --> newbornA[NewPersonPlacedInSlot_i]
newbornA --> sameIdA[AssignedID_iPlus1_Again]
sameIdA --> mixedA[SameIDMapsToDifferentPeopleOverTime]
flowchart TD
initB[InitialSlot_i_GetsID_iPlus1] --> counterB[next_person_id_InitialisedTo_NPlus1]
counterB --> deathB[PersonDiesOrEmigrates]
deathB --> recycleB[Slot_iReusedForMemoryOnly]
recycleB --> newPersonB[NewPersonPlacedInRecycledOrNewSlot]
newPersonB --> assignB[AssignID_next_person_id_ThenIncrement]
assignB --> uniqueB[IDNeverReused_LifetimeUnique]
flowchart LR
subgraph base [BaselineRun]
baseInit[InitialIDs_1_to_N]
baseSync[CreateAggregateTables]
end
subgraph inter [InterventionRun]
interInit[InitialIDs_1_to_N]
interReceive[ReceiveAggregateTables]
interPolicy[PolicyChangesTrajectory]
end
baseInit -->|"SameInitialIDMapping"| interInit
baseSync -->|"OneWaySync"| interReceive
interPolicy --> outcomeDiverge[SameStartID_CanHaveDifferentDeathYear]
std::size_t next_person_id_ per Population.+ sizeof(std::size_t) (typically 8 bytes) per Population object.xychart-beta
title "Relative Runtime and Memory Impact (14M Population)"
x-axis ["CurrentPlan","UpdatedPlan"]
y-axis "Normalized value" 0 --> 1.1
bar [1.00,1.00]
line [1.00,1.000001]
Notes:
Author: Mahima Ghosh