Changelog
All notable changes to heormodel are documented here. Format follows Keep a Changelog, versions follow Semantic Versioning.
Each entry links to the pull request that introduced it. Add a line under [Unreleased] in the same PR that makes the change; see RELEASING.md for how [Unreleased] turns into a release.
Unreleased
0.7.1 - 2026-07-11
Added
.zenodo.jsonandCITATION.cffso every GitHub release archives to Zenodo with a DOI once the one-time Zenodo integration is enabled; see RELEASING.md (#39).
0.7.0 - 2026-07-11
Changed
- Renamed the package’s arm vocabulary to PICOTS terms (Population, Intervention, Comparator, Outcomes, Timing, Setting):
Strategyis nowInterventioneverywhere (theinterventions=constructor argument, theinterventionoutcome index level,Outcomes.interventions, and every model-function callback’s second argument), andStrategy.overridesis nowIntervention.decision_levers.Interventiongained anis_comparatorflag: at most one intervention in a sequence may set it, andheormodel.models._interventions.comparator_ofreads it back. Every engine resolves the flag at construction and carries the comparator name on theOutcomesit returns (Outcomes.comparator);heormodel.cea.ce_planeandheormodel.report.plot_ce_planefall back to it, then to the first intervention, when their owncomparatorargument is omitted. No deprecated aliases: every call site must switch to the new names.
Added
Ordinary differential equation engine (#33):
heormodel.models.ODEModelintegrates a user-supplied system of ordinary differential equations over population compartments and returns the standardOutcomesstructure, for transmission models where a force of infection couples the compartments non-linearly.ODESpeccarries one intervention’s right-hand side, initial compartment sizes, per-compartment cost and effect rates, and an optional flow-event channel for one-time costs attached to a rate of movement (a dose administered, an infection treated). The engine augments the system with two accumulator equations and integrates the discounted reward flows withscipy.integrate.solve_ivp, reusingheormodel.models._accrualfor discounting; it is deterministic, so it satisfiesModelEnginewith no random streams.ODEModel.trajectoryreturns compartment occupancy over time for inspection and plotting.examples/seir_vaccination.pyand a website tutorial run a susceptible-exposed-infectious-recovered vaccination cost-effectiveness analysis end to end: the program nearly eliminates the modelled epidemic and buys quality-adjusted life-years at an incremental cost-effectiveness ratio around 3,200, and the expected value of perfect information at a 50,000 threshold is driven almost entirely by the basic reproduction number. A stochastic compartmental counterpart is planned indevdocs/roadmap/16-stochastic-compartmental-engine.md.Continuous-time Sick-Sicker replication (roadmap item 13):
examples/mdm_des/(a folder of building blocks composed byrun.py) and a website tutorial reproduce the published figures of the discrete-event simulation tutorial of Lopez-Mendez, Goldhaber-Fiebert, and Alarid-Escudero (Medical Decision Making 2026;46(5):533-548) on the continuous clock ofMicrosimModel, with the base case, epidemiological outcomes, and a 1,000-set probabilistic analysis with acceptability, expected loss, and EVPI curves. It matches the companion code on the two points that move the numbers: each one-time transition amount (onset cost, onset disutility, cost of dying) accrues over the sojourn that ends in it (reconstructed from the event history), and six parameters the companion draws but never reads are held at their base case. Three framework additions carry it:heormodel.models.LifeTablesamples time to death from piecewise-constant age-specific mortality rates by exact cumulative-hazard inversion under a hazard ratio;MicrosimModelreturns the event history viaevaluate(trace="events")on either clock, whichheormodel.models.state_occupancyturns into state occupancy over time; andheormodel.cea.expected_losswithheormodel.report.plot_expected_lossadds expected loss curves whose minimum equals EVPI. A test cross-validates the replication’s all-exponential variant, including the sojourn-accrued transition amounts, against the continuous-time Markov chain closed form (#27).MicrosimModelcallbacks renamed for clarity:hazardsis nowevent_times,transitionis nowtransition_probabilities, andpayoffsis nowstate_costs_and_utilities, each named for what the caller supplies (#27).Value-of-information tutorial:
examples/voi_tutorial.pyand a website tutorial run EVPI, EVPPI, and EVSI end to end on the Gaussian linear decision model that anchors the regression VoI literature (Strong, Oakley & Brennan, 2014; Strong, Oakley, Brennan & Breeze, 2015), framed as a two-intervention cost-effectiveness decision at 30,000 per QALY. Its incremental net benefit is Normal, so EVPI, per-parameter EVPPI, and the EVSI of a proposed effect study have closed forms via the unit normal loss integral. Every estimate lands within about one percent of its closed form at 100,000 iterations, and a test asserts the EVPI, the EVPPI ranking, and the EVSI against those closed forms as a second published reference point (#20).Markov vs microsimulation cross-validation:
examples/markov_vs_microsim.pyand a website tutorial build one Sick-Sicker-style model twice, as aMarkovModelcohort trace and aMicrosimModelindividual simulation from the same rates. The homogeneous microsimulation mean converges to the cohort trace within a fraction of a percent at 40,000 individuals, cross-validating the two engines. A mean-1 frailty on the progression and mortality hazards then raises the microsimulation QALYs about 8% above the cohort on unchanged mean rates, the risk heterogeneity a cohort averages away, with duration-dependent mortality as a short second example. A test asserts both the convergence and the divergence (#18).Parameter inputs from data in
heormodel.params:single_drawwraps one named set of point values as a one-row draw matrix (iteration 0) for a base-case run, andParameterSet.at_meansis the same call on a distribution set’s analytic means.read_drawsvalidates a CSV path or DataFrame as a draw matrix, honouring an explicititerationcolumn and rejecting non-numeric columns.resample_posteriorresamples a weighted parameter table into an unweighted draw matrix by drawing whole rows with replacement in proportion to the weights, so joint correlation survives. Each result flows throughrun_psaunchanged.examples/parameter_inputs.pyand the parameter-inputs tutorial run all three end to end (#13).Deterministic sensitivity analysis:
heormodel.dsabuilds scenario designs that run throughrun_psaunchanged.one_waysweeps a single parameter,one_at_a_timesweeps each parameter in turn (the tornado design), andgridtakes the full factorial of several parameters (the heatmap design). Each returns a(design, descriptor)pair: the design is a draw matrix of scenarios, the descriptor a tidy table naming what each scenario varied.heormodel.report.tornado_datanow reads a one-way or one-at-a-time DSA result as well as a PSA, andheatmap_datareshapes a two-parameter grid into a matrix.examples/dsa.pyand a website tutorial run all three forms on the Sick-Sicker model (#14).
Changed
- Documentation narrative order: the tutorials now climb from the analysis layer to the most detailed engine. The sequence is bring your own outputs, the Markov cohort model, the microsimulation engine, Markov vs microsimulation models, discrete-event simulation, the full pipeline, and the calibration workflow. The Markov cohort tutorial moves from Replications into Tutorials; the time-dependent cohort and microsimulation replications stay under Replications as validation exhibits. Each tutorial’s forward link,
get-started.qmd, and the README follow the same order (#19). run_psaruns in parallel over all cores by default (n_jobs=-1). Passsequential=Truefor an in-process run (the readable off switch for debugging and reproducibility checks), orn_jobsfor an explicit worker count; a run with one iteration or one available core falls back to sequential. The numbers are identical whichever way the run is split, because each iteration is seeded by its index. Aprogressargument shows a completed-count and time-remaining readout onstderras experiments finish, driven by the mean throughput of finished work; it is on whenstderris a terminal and quiet otherwise, so CI logs and docs builds stay silent unlessprogress=Trueis explicit (#15).
Changed
The
Interventionvalue object, and one canonical spelling for interventions (#28).interventions=now accepts a sequence of names orIntervention(name, decision_levers={})objects everywhere; a bare string is shorthand for a decision-lever-free intervention. The name-to-decision-levers mapping form is removed, so there is exactly one canonical spelling plus the string shorthand. The microsimulation examples and tutorials now branch on the intervention name for the treatment arm instead of encoding it as an{"on_treatment": 1.0}pseudo-parameter, which kept a float flag out of theParameterSetwhereevppi_rankingand the deterministic sensitivity builders could not see it. Decision levers stay for genuine numeric scenario knobs such as a discrete-event server count.TimelineandPopulationvalue objects are deferred until a new engine lands (noted indevdocs/architecture.md).The runner owns execution, a clean break with no aliases (#28). Seeding and the event side channel move out of the engines and into
run_psa:- Stochastic engines (
MicrosimModel,DESModel) no longer takeseed_managerat construction, andevaluate(draws)no longer takes atraceflag.evaluate(draws)returnsOutcomesonly, matching theModelEngineprotocol, so the# type: ignorethe discrete-event example needed is gone. run_psagainsseedandcollectand returns aRunResultwithoutcomes,events, andindividualsfields (unset fields areNone). Read the panel withrun_psa(model, draws, seed=...).outcomes.RunResultis deliberately not iterable, so it cannot be mistaken for the old(outcomes, trace)tuple.collect="events"gathers the state-change or resource history andcollect="individuals"the per-individual accruals, working identically for every engine and in parallel.run_psabuilds the per-iteration streams fromseedand hands them to a stochastic engine through the widerStochasticEngineprotocol (evaluate_streamed(draws, streams=..., collect=...) -> EngineResult); deterministic engines keep the plainModelEngineshape and ignore seeding. Because streams stay keyed by iteration index, outcomes and the collected log are invariant tobatch_sizeandn_jobs.SeedManagerstays public (it is the recordcapture_runstores) but is no longer a constructor argument. Stream derivation reproduces the previous numbers exactly, so the replication gallery is unchanged.
- Stochastic engines (
One vocabulary across the engines, a clean break with no aliases (#28). Every engine now spells a shared concept the same way:
MarkovModel:model_fnis nowtransitions_and_rewards,startis nowinitial_state, andhalf_cycle_correctionis nowcycle_correction(its"half-cycle"option is now"half_cycle").MicrosimModelno longer takes aclockargument. Build the two kernels throughMicrosimModel.discrete(...)andMicrosimModel.continuous(...); each constructor carries only its kernel’s parameters, so a parameter that belongs to the other clock is aTypeErrorat the call site. The discrete clock takesn_cycles(washorizon) and per-cyclestate_rewards(wasstate_costs_and_utilities); the continuous clock takeshorizonand per-yearstate_reward_rates.half_cycle_correction=Trueis nowcycle_correction="half_cycle", sharing the string withMarkovModel.DESModel:entitiesis nowpopulationandn_entitiesis nown_individuals, matchingMicrosimModel.- Every engine accepts
interventionsas either a sequence of names or a name-to-decision-levers mapping, and every user-supplied model function receives the intervention name. Microsimulation callbacks gain it as their second argument:transition_probabilities(params, intervention, state, attrs, rng),state_rewards(params, intervention, state, attrs), andevent_times(params, intervention, state, attrs, rng). - A discrete-event
processreads the run’s horizon back astoolkit.horizoninstead of duplicating it as a module constant. The replication examples reproduce their published numbers unchanged, since nothing numeric changed.
Fixed
heormodel.__version__reported0.0.0on every install because the package read its version fromversion("heval"), the pre-rename distribution name. It now readsversion("heormodel"). The stalehevalname is also gone from every module docstring, cross-reference, and error message insrc/, so the optional-dependency hints now point atheormodel[des]andheormodel[calibration](#28).
0.6.0 - 2026-07-05
Changed
- Resonant engine names and clearer parameters (breaking, no aliases). The engines are now
MarkovModel(wasMarkovCohortEngine),MicrosimModel(foldsDiscreteTimeMicrosimEngineandContinuousTimeMicrosimEngineinto one class with aclockargument,"discrete"by default,"continuous"for the competing-hazards path), andDESModel(wasDESEngine). The Markov structure callback ismodel_fnin place ofbuild. Every engine takes onediscount_rate(annual, default0.03, applied to costs and effects) in place ofdiscount_costanddiscount_effect;cycle_lengthscales the annual clock. The pre-0.6 names are removed outright rather than deprecated (#11).
0.5.0 - 2026-07-05
Added
- Cohort state-transition engine:
MarkovCohortEnginesweeps a cohort trace across PSA iterations and emitsOutcomes. Transitions may be one matrix or a per-cycle array (age-varying rates); rewards accrue per state and, optionally, per transition (a one-time cost of dying or disutility of onset). Supports Simpson’s 1/3, half-cycle, or no within-cycle correction, reusingheormodel.models._accrualfor discounting.CohortSpeccarries one intervention’s matrices, andgen_wccbuilds the correction weights (#9). duration_groupsonDiscreteTimeMicrosimEngine: a per-individual counter of consecutive cycles spent in a set of states, so a sojourn that progresses (Sick to Sicker) keeps counting wheretime_in_statewould reset (#9).- Three replications of published Sick-Sicker cost-effectiveness tutorials, each matching the source’s deterministic results, with runnable scripts and website tutorials: cohort state-transition (
examples/mdm_cohort.py), time-dependent cohort with age-varying mortality (examples/mdm_cohort_timedep.py), and microsimulation (examples/mdm_microsim.py). A replication gallery page collects them with citations (#9).
0.4.0 - 2026-07-04
Added
- Discrete-event simulation engine:
DESEnginewraps SimPy. The environment, process functions, and resources stay the user’s own code; the engine adds a per-entity toolkit for discounted cost and utility accrual, per-iteration seeding from aSeedManagerso results do not depend onn_jobs, and an optional event log. It reusesheormodel.models._accrualand uses common random numbers across interventions by default, staying coherent with the microsimulation engines (#8). heormodel.models.queue_waits: derive per-request waiting times from aDESEnginetrace, so queueing reports come from the event log rather than engine internals (#8).simpyas an optional dependency behind thedesextra (uv pip install 'heormodel[des]') (#8).- Discrete-event example (
examples/des.py) and website tutorial, validated against an M/M/1 queue and the exponential cohort solution (#8).
0.3.0 - 2026-07-04
Added
- Microsimulation engines:
DiscreteTimeMicrosimEngineadvances an individual-level population on a cycle grid with history-dependent transitions and heterogeneity, andContinuousTimeMicrosimEngineraces competing time-to-event samplers between events. Both emit the standardOutcomesschema, seed each iteration from aSeedManagerso results do not depend onn_jobs, and use common random numbers across interventions by default (#7). heormodel.models._accrual: shared cost and utility accrual, discounting, and aggregation toOutcomes, used by both engines and reserved for the discrete-event engine (#7).SeedManager.child_sequencereturns a per-key seed sequence, so iteration-indexed streams stay identical however a run is chunked across workers (#7).- Microsimulation example (
examples/microsim.py) and website tutorial, validated against the closed-form cohort solution it mirrors (#7).
0.2.0 - 2026-07-04
Added
- Documentation website built with Quarto and quartodoc, published to GitHub Pages from CI on every merge to
main: executed tutorials, concept pages, a generated API reference, the roadmap, and this changelog (#3). heormodel.params.mix_drawscombines draw matrices from different sources (a calibrated posterior and literature draws) into one PSA matrix, resampling whole rows so joint correlation survives and sources stay independent (#5).capture_runrecords adraw_sourcesmap, so the run report shows where each parameter’s draws came from (#5).- Calibration workflow example (
examples/calibration_workflow.py) and website tutorial: calibrate a natural-history model’s rates, mix them with literature parameters, and run CEA and VoI on the result (#5).
Changed
- Docstring cross-references use plain backticks instead of Sphinx roles, so they render cleanly on the website and in
help()(#3). - Distribution spec strings format floats at 6 significant digits, so provenance records and run reports stay readable (#3).
RunRecord.model_card()renamed toto_markdown(), titled “Run report”: “model card” is ML documentation jargon, not a HEOR term (#6).
0.1.0 - 2026-07-04
Initial release: parameter sampling (heormodel.params), the Outcomes schema and ModelEngine protocol (heormodel.models), the PSA run loop and bring-your-own-outputs ingestion (heormodel.run), cost-effectiveness analysis (heormodel.cea), value-of-information analysis (heormodel.voi), optional ABC calibration (heormodel.calibrate), and reporting plots (heormodel.report).