Skip to content

Per pathogen health progression - #106

Merged
Julian-Patzner merged 12 commits into
mainfrom
per_pathogen_health_progression
Sep 9, 2026
Merged

Per pathogen health progression#106
Julian-Patzner merged 12 commits into
mainfrom
per_pathogen_health_progression

Conversation

@Julian-Patzner

@Julian-Patzner Julian-Patzner commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

GEMS 1.2 made the simulation multipathogen but left host care and mortality global: one DefaultHealthProgression held a single SevereHealthProfile and a single CriticalHealthProfile for the whole run, so every pathogen shared one hospitalization and death rate (by default). This release moves those profiles onto the progression categories, where each pathogen carries its own, and leaves HealthProgression responsible only for combining a host's concurrent infections into one care timeline.

A 1.2 config's [HealthProgression] section is still read, with a deprecation warning, but two Critical timing keys were renamed and have to be updated by hand; see Critical Timing. Simulations built from Julia may need further changes; see Breaking Changes.


Per-Pathogen Health Profiles

Each progression category carries its own care and mortality. In Julia it takes a profile as health, e.g. Critical(...; health = CriticalHealthProfile(hospital_probability = 0.95)). In a config file it takes a health sub-table, e.g. [Pathogens.Covid19.progressions.Critical.health], holding the keys that used to sit under [HealthProgression.parameters.critical]. Either form also accepts the parameters flat among the disease timings, but not both at once.

Simulation collects the profiles into a HealthProfileIndex keyed by (pathogen_id, progression_id), available as health_profiles(sim). Built once at construction, it is what replaced select_health_profile: a HealthProgression is handed the index and looks up whatever an infection needs, instead of knowing which profile belongs to which pathogen. Because the key is the progression and not the tier, two categories that peak at the same tier can carry different profiles.

Embedding previously worked only for single-pathogen runs, and was rejected alongside an explicit health_progression or a [HealthProgression] section. Those restrictions are gone.


HealthProgression

HealthProgression no longer routes infections to profiles: select_health_profile is gone, DefaultHealthProgression is an empty struct, and calculate_health_progression! takes the HealthProfileIndex as a new argument. LegacyHealthProgression goes with it: it wrapped a private copy of the same routing for the legacy categories, which the index now covers. GEMS._health_profile(index, infection) looks up an infection's profile, or nothing when its category carries no health, and calculate_health_profile draws the standard schedule from it, so a custom policy can reuse the default draw instead of reimplementing it.

The [HealthProgression] config section now names only the combination policy and its parameters block is optional, so the section is needed only for a custom policy. DefaultConf.toml was migrated accordingly, its two sub-tables moving onto the Covid19 Severe and Critical progressions.


Critical Timing

CriticalHealthProfile now anchors the ward at severeness_onset and the ICU at critical_onset, putting it on parity with SevereHealthProfile. Two keys change name accordingly, in both Julia and config files:

1.2 1.3
critical_onset_to_hospital_admission severeness_onset_to_hospital_admission
hospital_admission_to_icu_admission critical_onset_to_icu_admission

There is no fallback for the old names; they reach the constructor as unknown keywords and error.

Renaming alone does not reproduce 1.2 timings. Ward admissions now fall earlier, measured from severeness onset instead of critical onset. The ICU is scheduled from critical onset instead of from the ward admission, and if it lands first, the ward admission is moved to it. LegacyCritical is unaffected.


Deprecations

  • severe/critical sub-tables under [HealthProgression.parameters] are still read as tier-level defaults, filling in only the categories that embed no health of their own, with a deprecation warning.
  • The care keyword on Severe and Critical is deprecated in favour of health.

Breaking Changes

The renamed Critical timing keys above affect config files as well as Julia. The rest affect simulations built from Julia only.

  • Two CriticalHealthProfile keywords were renamed and its ward and ICU admissions re-anchored; see Critical Timing.
  • DefaultHealthProgression(severe = ..., critical = ...) no longer takes those keywords. Embed the profiles on the progressions instead.
  • A custom HealthProgression must implement calculate_health_progression! for its own type, with index::HealthProfileIndex added after new_infection.
  • compute_health! takes the HealthProfileIndex after hp.
  • The care field on Severe and Critical is now called health.
  • select_health_profile and LegacyHealthProgression are removed.

Julian-Patzner and others added 4 commits September 4, 2026 14:58
…and mortality rates

harvest every pathogen's embedded profiles into a (pathogen_id, progression_id)
table instead of collapsing them into one DefaultHealthProgression; this drops the
single-pathogen restriction, so two pathogens with identical severity stratification
can differ in mortality by one parameter
every concrete HealthProgression was a router implementing only select_health_profile,
which is why an explicit policy and embedded care conflicted. routing moves to a
(pathogen_id, progression_id) index passed to calculate_health_progression!;
PerPathogenHealthProgression, LegacyHealthProgression and select_health_profile are
gone, DefaultHealthProgression is fieldless, and its tier profiles move to StandardOfCare
@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@HLZaunstoeck HLZaunstoeck left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue #1 – StandardOfCare
--> StandardOfCare as described is not a standard if not applied to all categories equally
----> also does only work for pre-set categories has to be adapted when using new categories
--> StandardOfCare does not avoid multiple identical blocks when it still needs subsections for severe and critical
----> StandardOfCare as baseline across all progressions?
--> feels a bit like the old system with a new name
Requested Change:
--> StandardOfCare applies to all categories unless flagged otherwise agnostic of categories used
--> StandardOfCare can use subsets of categories to avoid multiple lines

Question #1 – _demand and _set_demand! assumes no additional care levels
--> is this intended?

Question #2 – order of application
--> Why is the newest the standard not the oldest? If provided with legacy and new version, new version is used (with warning).

Variable Naming
“CriticalHealthProfile” implies a connection to critical progression but can be assigned to other progressions
--> maybe rename to “ICUHealthProfile” or similar to avoid confusion
--> same is true for SevereHealthProfile

@Julian-Patzner

Julian-Patzner commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

Issue #1 – StandardOfCare
--> StandardOfCare as described is not a standard if not applied to all categories equally
----> also does only work for pre-set categories has to be adapted when using new categories
--> StandardOfCare does not avoid multiple identical blocks when it still needs subsections for severe and critical
----> StandardOfCare as baseline across all progressions?
--> feels a bit like the old system with a new name
Requested Change:
--> StandardOfCare applies to all categories unless flagged otherwise agnostic of categories used
--> StandardOfCare can use subsets of categories to avoid multiple lines

Some of these requests are hard to implement: a HealthProfile is anchored to a tier's onset field (SevereHealthProfile to severeness_onset, CriticalHealthProfile to critical_onset) and their parameters are disjoint, so one profile cannot apply to all categories. I removed StandardOfCare from the public API instead. Per-pathogen health now goes into a health block on each progression, and StandardOfCare is purely for backwards compatibility (to be removed when we update to 2.0)

Question #1 – _demand and _set_demand! assumes no additional care levels
--> is this intended?

Added ArgumentError throws to both. Additional care levels are not trivial and require extra fields on the individual, for example. We may implement this in the future.

Question #2 – order of application
--> Why is the newest the standard not the oldest? If provided with legacy and new version, new version is used (with warning).

The newest wins because otherwise adding the new form would do nothing until you delete the old one. determine_health_progression should be easier to read now.

Variable Naming “CriticalHealthProfile” implies a connection to critical progression but can be assigned to other progressions
--> maybe rename to “ICUHealthProfile” or similar to avoid confusion --> same is true for SevereHealthProfile

Left as is. You are correct that they can be assigned to other progressions, but SevereHealthProfile is always anchored at severeness_onset and CriticalHealthProfile to critical_onset. ICUHealthProfile wouldn't be entirely accurate, because it ignores deaths and ward admissions.

@Julian-Patzner
Julian-Patzner merged commit 4e2ae71 into main Sep 9, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants