Health-GPS

Logo

Global Health Policy Simulation model

View the Project on GitHub imperialCHEPI/healthgps

Same person ID across baseline and intervention

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

Plan summary

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.

Work items

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).

Goal

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.

Why this is safe

What data is transferred

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.

Design: ID assignment rules

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

Implementation plan

1. Person (person.h, person.cpp)

2. Population (population.h, population.cpp)

3. Tests (Population.Test.cpp)

4. No changes to

5. Commenting convention (MAHIMA)

File change summary

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.

Order of implementation

  1. Person: add constructors and set_id, with comments.
  2. Population: constructor, then add_newborn_babies, then add, with comments.
  3. Run existing tests; fix any that assume previous ID behaviour (only Population tests might need a new case).
  4. Add the new Population test for index-based ID.

Update: Lifetime-unique ID strategy (no ID reuse after death/emigration)

Why this update is needed

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.

Updated objective

Updated design (minimal changes)

  1. Keep initial population IDs deterministic and aligned across scenarios:
    • Initial slot i still gets ID i + 1.
  2. Add a Population-owned monotonic counter:
    • next_person_id_ in Population private state.
    • Initialise to initial_size + 1 after population construction.
  3. For all post-initial entrants (newborns and add() entities):
    • Assign ID = next_person_id_++ regardless of recycled or appended slot.
  4. Continue slot recycling for memory efficiency:
    • Reuse memory slots, not person IDs.

Why this preserves performance

Baseline/intervention compatibility after update

Delta to implementation steps

1) Population only (primary behavior change)

2) Person

3) Tests update

Validation checklist (updated)

  1. Run Population.Test and ensure lifetime-unique assertions pass.
  2. Run targeted simulation/analysis tests covering individual tracking output.
  3. Smoke-check individual tracking CSV: same ID should not switch to a different person profile due to slot recycling.
  4. Confirm no changes to scenario sync behavior between baseline/intervention.
  5. Debug builds: Population::allocate_next_person_id() asserts each new ID equals next_person_id_ before increment (MAHIMA; no hash set; zero release cost).

Summary of why this is better than current implemented version

Additional clarifications from review Q&A

1) What if a person is alive in intervention but dead in baseline?

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.

2) Is scenario data transfer one-way only?

Yes. Synchronisation is baseline -> intervention only. Intervention receives baseline-generated aggregate synchronisation tables; intervention does not send these back to baseline.

3) What data is transferred across scenarios?

Only aggregate tables, never person-level records:

No person object, no person ID, no region/ethnicity per-person payload is transferred.

Before vs updated behavior flowcharts

Before (implemented slot-based ID reuse)

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]

Updated (lifetime-unique IDs with slot reuse only)

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]

Baseline/intervention sync and divergence model

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]

Runtime and memory impact at large scale (14 million people)

Assumptions for this estimate

Complexity impact

Memory delta for 14M population

Runtime effect estimate (relative)

Relative impact plot (normalized)

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