Skip to content

Adding medical workers and hospital interactions#120

Open
debog wants to merge 77 commits into
developmentfrom
dg/medical_workers
Open

Adding medical workers and hospital interactions#120
debog wants to merge 77 commits into
developmentfrom
dg/medical_workers

Conversation

@debog

@debog debog commented Apr 17, 2025

Copy link
Copy Markdown
Collaborator

Added medical workers and a capacity-limited hospital model to HospitalModel:

  • The entire medical-workers / hospital model is gated behind a single switch. With it off (the default), medical workers are ordinary workers, there is no hospital capacity limit or in-hospital transmission, and the code reproduces the development branch exactly — for both census and UrbanPop initialization:
agent.model_medical_workers (default: false)
  • Assign a certain percentage of workers in each community as medical workers (doctors/RNs). Used for census initialization (UrbanPop carries NAICS industry codes natively). Specified as:
agent.med_workers_proportion (default: 0.13)
  • Add interaction between medical workers and between medical workers and patients. The interaction transmissivities (doctor-to-doctor, doctor-to-patient, patient-to-doctor, patient-to-patient) are respectively specified by (PPE-reduced default values in parentheses):
disease.xmit_hosp_d2d (0.1*xmit_work)
disease.xmit_hosp_d2p (0.1*xmit_work)
disease.xmit_hosp_p2d (0.1*xmit_work)
disease.xmit_hosp_p2p (0.3*xmit_work)
  • Making survival/death probability of hospitalized agents dependent on a hospital score (treatment quality) computed from the hospital load (number of patients divided by capacity). The capacity is the staffed-bed supply scaled by the currently-available medical workforce,
capacity   = bed_supply * (available_workers / full_strength_workers)
bed_supply = (staffed_beds_per_1000 / 1000) * community_population

so capacity falls as medical workers fall ill, withdraw, or die. The staffed-bed density (at full workforce strength) is specified by:

hospital_model.staffed_beds_per_1000 (default: 2.4)

The treatment-quality decay under overload is controlled by:

hospital_model.score_minimum (default: 0.1)
hospital_model.halfscore_load (default: 5)
  • The hospital diagnostics (capacity, load, number of patients, available and full-strength medical workers, bed supply) can be written as plot files by specifying:
hospital_model.write_pltfiles (default: false)
  • Optionally placing hospitals from real public data (HHS facility capacity) instead of the uniform per-capita bed density. A data file is built by utilities/build_hospital_data.py (county or census-tract level, tied to a year), and used via:
hospital_model.use_HHS_data (default: false)
hospital_model.hospital_data_file  (path to data/HospitalData/<region>_hospitals_<year>.dat)

County-level data scales each community's bed supply to its county's real supply; tract-level data places hospitals at the tracts that have them and routes patients and staff to the nearest hospital (reusing the home<->work agent movement). County and tract example files for CA, the SF Bay Area, NM, MA, and the whole US are included under data/HospitalData/.

Note: All the new input parameters are optional. The model is off by default (agent.model_medical_workers = false), in which case the code behaves exactly the same as the development branch for both census and UrbanPop initialization.

debog added 30 commits March 27, 2025 16:22
…Us and CPUs, as well as various number of MPI ranks
…edical workers NAICS from worker interaction model
…s for each community based on number of available medical workers
…multiple components; added the option to write this out as plotfiles
…s to screen; writing hospital data with the same plotting interval as plt files
…ing of overcrowded hospitals and underserved patients
debog and others added 30 commits June 2, 2026 16:56
Brings PRs #144-#152 into the medical-workers branch: Weather module (#149),
UrbanPop IO optimization (#144), multi-disease hospital-stats fix (#146),
disease-period CDF sampling / EpiCast comparison (#152), worker-assignment
fix for phantom transcontinental commutes (#147), and random/air-travel
fixes (#151). (#145 chkdiff fix was already present.)

Resolved one conflict in src/HospitalModel.H (treatAgents): combined
development's corrected multi-disease hospital-stat bookkeeping
(exit_*/died_today/rm_from arrays, immune_length_loc on recovery) with the
medical-workers treatment-quality / capacity-limited mortality, indexing
the baseline severity by status_d.

Builds clean (make -j2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reformat the merged treatAgents() p_death expression in HospitalModel.H
to satisfy the repo clang-format (v19) check. Whitespace only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hospital patient capacity was computed from all active medical workers
(n_mw_arr component 1), so acute_medworkers_proportion had no effect on
capacity, load, or mortality -- it only fed the n_t_mw>0 gate and a
diagnostic. Capacity must reflect the medical workers treating the modeled
disease: use n_a_mw (= acute_medworkers_proportion * active).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a master switch agent.model_medical_workers (default false). When off, medical workers are ordinary workers, there is no hospital capacity limit or in-hospital transmission, and hospitalized agents die at the baseline rate, so the run reproduces development exactly for both census and UrbanPop initialization. Gate interactDay, treatAgents, WorkCandidate, and the census medical-worker assignment on the flag; remove an over-strict naics-equality assert in BinaryInteractionWork.

Redesign hospital capacity (Option 2): capacity = bed_supply x (available/baseline), bed_supply = (staffed_beds_per_1000/1000) x residential population. This replaces the degenerate num_patients_per_doctor x acute_medworkers_proportion pair with a single staffed_beds_per_1000 knob (default 2.4, AHA). Bed supply is recorded once at init from residential population (initHospitalCapacityModel/initBedSupply); the workforce enters only through the availability fraction. Rename acute -> frontline. Set realistic, model-off-inert defaults (med_workers_proportion 0.13, staffed_beds_per_1000 2.4, nonzero xmit_hosp_*). Update how_to_run.rst.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add hospital_model.use_HIFLD_HHS_data (default false): set each community's staffed-bed supply from real per-county or per-tract hospital data instead of the uniform staffed_beds_per_1000 density. County-level data apportions a county's beds to its communities by population; tract-level data places beds at the tracts that have hospitals and routes each community's patients to the nearest hospital tract, reusing the home/work agent-movement machinery (hosp_i/hosp_j set from a per-community assignment built at init from the global demographic data and the domain raster).

Add utilities/build_hospital_data.py to build the data files from public sources (HHS facility capacity, HIFLD hospitals), tied to a year, for a state, metro area (--counties), or the whole US. Commit county-level examples for CA, BayArea, NM, MA, US. Document in docs/source/usage/data.rst, data/HospitalData/README.md, and how_to_run.rst.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…o hospitals

Route the medical workforce to hospitals in tract mode (analog of the patient routing): retarget each medical worker's work_i/work_j to its assigned hospital community, so staff, beds, and patients all aggregate at the real hospital cells. Capacity becomes beds x catchment workforce availability; the per-cell-workforce mismatch and the n_baseline==0 silent-disable are gone, and non-hospital cells are inert. Justified because the worker-flow data only resolves home/work to census tracts.

HIFLD Open is being deprecated, so source hospital data from HHS instead: the HHS facility dataset carries geocoded_hospital_address points, providing both bed counts and locations for county and tract placement (no HIFLD needed). HHS is now the default --source; HIFLD remains a legacy local-CSV option. Rename input hospital_model.use_HIFLD_HHS_data -> use_HHS_data.

Commit real tract-level data for CA, BayArea, NM, MA, US (HHS geocoded points joined to Census tracts, week 2021-12-26), validated end-to-end. Update docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e hospital data

HHS has no pre-COVID (Jan 2020) data -- the facility series starts ~April 2020, comprehensive from ~late July 2020 (ICU beds only from then). Use 2020-09-27, the earliest clean low-COVID lull with full reporting, so the staffed-bed counts represent baseline (full-strength) capacity rather than surge-affected staffing. Replace the 2021-12-26 (Omicron peak) county and tract example files for CA, BayArea, NM, MA, US with the 2020-09-27 versions; update docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…CUDA Release crash)

The medical-workers init/reroute device lambdas shared a translation unit with generateCellData. On the matrix build (CUDA + RDC + -maxrregcount=255 + fast-math + -O3) the added device code perturbed the Release register allocation of generateCellData's kernel, faulting in writePlotFile -- only on matrix, only in Release (Debug was clean), and only after these gated-off additions (35cdfd3 was clean). Move readHospital* + initHospitalCapacityModel + the reroute (now rerouteHospitalizedToHospital) into AgentContainerHospital.cpp so generateCellData's compilation is no longer affected. No functional change; builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reflow the lines flagged by the clang-format CI (v19): collapse short
blocks/declarations that fit within the 130-column limit, align trailing
comments, and drop a stray blank line. No functional change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bed-supply initialization device kernels lived in the HospitalModel
class template (initBedSupply, setBedSupply), so they were instantiated in
every translation unit that includes the template. Following the earlier
fix in 3dda948 ("creating hospital data MultiFab in AgentContainer instead
of HospitalModel to avoid weird CUDA error"), write the per-community bed
supply directly into AgentContainer's m_hosp_data MultiFab (which the
hospital model already aliases) from the concrete AgentContainerHospital.cpp
translation unit instead.

This removes initBedSupply/setBedSupply from the template (replaced by a
host bedsPerThousand() accessor) and drops the temporary bed_supply
MultiFabs and the setBedSupply copy kernel. No functional change: the same
component (HospMod::bed_supply) gets the same values; the per-capita,
county-apportioned, and tract-placed paths are unchanged. Builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Encapsulate the hospital bed-supply/patient-routing feature in a new HospitalData
class held by AgentContainer through a std::unique_ptr (with an out-of-line dtor),
replacing the by-value iMultiFab m_hosp_assignment and bool m_tract_routing members.

Those by-value members changed AgentContainer's layout, which re-instantiated
ParticleToMesh<AgentContainer> and tipped the marginal -O3 codegen of its inner
FabArray::PC_local_gpu copy kernel into an "invalid device function" miscompile in
generateCellData (Release only). Keeping the iMultiFab and all device/STL code in
HospitalData.cpp restores AgentContainer's translation unit to the working shape.

No functional change; AgentContainerHospital.cpp is superseded by HospitalData.cpp.

Co-authored-by: Cursor Agent <cursor-agent@cursor.sh>
Write medical_workers.dat (one file per disease, only when the medical-workers
model is on) with daily medical-worker vs other-worker counts: total,
susceptible, and newly infected for each group. This provides the in-hospital
transmission (xmit_hosp_*) calibration target -- the HCW infection hazard
relative to other workers.

AgentContainer::getMedicalWorkerCounts is a ParticleReduce kept in
HospitalData.cpp (not AgentContainer.cpp) so the new device kernel stays out of
the AgentContainer translation unit. main.cpp writes the file alongside
output.dat; output.dat and the regtest baselines are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…inimum)

The capacity-limited treatment-quality multiplier q that scales hospitalized
mortality (p_death = 1 - q*(1 - p_death_baseline)) was accumulated by compounding
the daily community score over the whole stay (q = product of daily scores). Over
a multi-day stay this drives q toward zero under sustained overload, so even a
moderate ~3x load produced an unrealistic ~5-6x mortality multiplier.

Add hospital_model.treatment_score_type (AMREX_ENUM):
  minimum     q = min of the daily scores (the worst day of the stay)  [default]
  cumulative  q = product of the daily scores (the previous behavior)
The minimum model makes the overall mortality multiplier linear in the baseline,
so it calibrates cleanly to the ~2x-at-peak-strain target from caseload-surge
studies. Inert when the medical-worker model is off (regression tests unchanged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
For load > 1 the score used (e^{L-1})/(e^{L_half}+e^{L-1}), which at L=1 equals
1/(e^{L_half}+1) != 0 -- so the score stepped down from the L<=1 branch (about
0.969 at floor 0.1, L_half 3.34) instead of leaving 1 continuously. Use
(e^{L-1}-1)/((e^{L_half}-1)+(e^{L-1}-1)): the numerator vanishes at L=1 so the
score joins the within-capacity branch continuously, while the half-score load
and the floor are preserved exactly. Recalibrating M(2.5)=2x shifts the
calibrated halfscore_load from 3.34 to 3.13 (decks updated separately).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
readHospitalDataLevel now returns county only when the data-file header marks
level=county; otherwise it defaults to tract-level placement (real hospital
locations + patient/workforce routing). The existing county files carry an
explicit level=county header, so they are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
rerouteHospitalized indexed the per-cell hospital-assignment MultiFab at each
agent's home cell using the current tile's array. Once an admitted agent is moved
to its hospital cell, its home cell lies outside that tile, so the lookup read out
of bounds and produced a garbage hospital cell -- tripping the patient-cell
assertion in computeHospitalScores a few weeks into a run. Route only just-admitted
agents (hosp still equals home, set by assignHospital, so the agent is at its home
cell and the lookup is in-tile); already-routed agents keep their fixed hospital
cell. Found by a local 4-rank Bay Area validation run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
treatAgents deposited the per-community death and hospitalization-exit statistics at
each agent's home cell using the current tile's array. With tract-level routing a
hospitalized agent sits in its hospital cell, so the home cell is outside the tile
and the deposit was an out-of-bounds write -- dropping deaths (mesh vs agent death
counts diverged and tripped the main.cpp consistency assertion) and unbalancing the
hospitalization counts. Capture each agent's in-tile cell at the start of treatAgents
and deposit there; the per-disease totals (summed over cells) are unchanged, so the
output and the consistency checks are correct. Also move the deceased back to their
home community so plt-based mortality is attributed by residence, matching the
no-routing baseline. Validated by a local 4-rank Bay Area run completing past the
admission/death onset that previously crashed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BayArea.dat is 2000-vintage (1405 tracts; San Francisco has 176, not the 197 of
2010), but the tract bed file was built on 2020 TIGER tracts. 20 of 60 hospitals
fell in post-2000 tract splits the demographic file does not have, so a third of
the beds (7,240 of 10,876 at 39 of 58 tracts) never placed. Rejoin the HHS
facility points to the 2000 Bay Area tracts: all 10,876 staffed beds at 58
hospital tracts now place. Bundle the 2000 tract shapefile and fix the build
command + add a vintage note in the README so the file stays reproducible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rker-death diagnostic

- Patient transfer: a just-admitted patient whose nearest hospital is over
  capacity is routed to the lowest-load hospital in the same county (none in a
  one-hospital county), from the one-day-lagged per-hospital load. On by default
  under tract routing (hospital_model.patient_transfer). Optional per-day transfer
  log (hospital_model.transfer_output_file): day, from/to FIPS+tract, n_patients.
- Hospital workgroups: split each hospital's medical workforce into groups of
  ~workgroup_size instead of one group per hospital, bounding worker-to-worker
  in-hospital transmission to realistic team sizes (med/surg units ~30 beds).
  hospital_model.workgroup_size overrides agent.workgroup_size.
- Diagnostics: medical_workers.dat gains cumulative MW_dead/OW_dead columns
  (appended; the first six columns are unchanged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n private methods

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirrors the workplace exclusion. Without it, tract routing pools a hospital's whole medical workforce (same work cell, work_nborhood=0) into one neighborhood group and over-infects them (~94% even with in-hospital channels off).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… fallback path, matching the tract-data path
Add a per-disease DiseaseStats::hospital_acquired field that counts
in-hospital cross-disease infections: a patient admitted for one disease
who acquires another while hospitalized. Hospitalized agents are cut off
from community/household/work/school contact, so any new infection of a
patient is necessarily hospital-acquired. The count is deposited at the
hospital cell and written to the plotfiles per disease; the disease_stats
MultiFab gains one component (5 -> 6).

Also fix a pre-existing out-of-bounds write in infectAgents. new_cases was
deposited at the agent's home cell, but a hospitalized agent sits in its
hospital cell, which with tract-level routing can be in a different box. A
hospitalized agent acquiring a second disease therefore wrote outside the
tile's FAB and corrupted the disease-stats mesh, surfacing as a spurious
death-count assertion failure. This was latent until now: it requires a
multi-disease run with in-hospital patient transmission, where an
already-infected hospitalized agent becomes newly infected with another
disease. Deposit at the agent's current (in-box) cell instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…missions

The hospital occupancy counts (ward, ICU, ventilator) are per-agent nested
levels (ward superset ICU superset ventilator), deposited once at admission and
removed as the patient steps down. The removal combined per-disease exit flags:
for a patient hospitalized for two diseases at once (from community co-infection),
it removed the patient from the ward as soon as one disease's stay ended, even
though the other still kept the patient hospitalized. This tripped the
discharge-consistency assertion (a not-yet-discharged patient flagged for ward
removal) and, more generally, double-decremented the occupancy counts. The bug
was latent until now: it requires a multi-disease run with the hospital model and
a patient admitted for two diseases simultaneously.

Replace the per-disease exit flags with the patient's nested hospital level (the
most intensive level across its diseases), captured before and after the
per-disease treatment update. Each occupancy count is decremented exactly when
the patient drops below that level, so a patient hospitalized for two diseases
leaves a level only when neither disease keeps it there, and is discharged only
when no disease keeps it hospitalized. For a single disease this reproduces the
previous behavior bit-for-bit (verified: identical output over a 60-day Bay Area
COVID-19 run with the hospital model) and adds no random-number draws.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
p_death = min(1, M*p0) with M = 1 + (M_max-1)(1-q)/(1-s_min), capped at 1 and identical across baseline-risk groups, replacing 1 - q(1-p0) which drove low-risk patients toward certain death at extreme overload. New input hospital_model.mortality_multiplier_max (default 3.0). The score floor s_min now only normalizes the score field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant