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
Fixed
A stray, unmatched closing code fence at the end of
docs/tutorials/microsim.qmdopened a never-closed code block that swallowed Quarto’s hidden navigation markup, so the rendered microsimulation tutorial showed that markup as raw text at the bottom of the page (#76).The site-wide social preview image resolved to a 404 on every tutorial page:
docs/_quarto.ymlused a document-relativeimagepath, which Quarto resolves against each page’s own directory rather than the project root, so pages undertutorials/picked up an extratutorials/segment. Switched to a project-relative path (#74).
Added
95% uncertainty intervals in the incremental cost-effectiveness ratio (ICER) table. When
icer_tablereceives a probabilisticOutcomeswith more than one iteration, each ofcost,effect,inc_cost,inc_effect, andicergains_lo/_hiinterval columns; the level is set byinterval=(default 0.95) andinterval=Noneomits them. Dominance and the frontier are settled once on the mean costs and effects, and the incremental measures, which are differences between interventions, are formed within each parameter draw in a paired way before being summarized by percentiles. Incremental cost and effect are now shown for every intervention against its comparator, the cheapest frontier intervention still above it in cost, so a dominated intervention carries the negative incremental effect or excess cost that marks it; the ICER stays a frontier quantity, blank for dominated interventions.heormodel.reportgainsformat_icer_table, which renders that table for reading, with each estimate rounded and written aspoint (low, high)(#81).Search-engine metadata across the documentation website: a site-wide meta description, Open Graph and Twitter card tags with a preview image, a
robots.txtthat points to the generated sitemap,WebSiteandSoftwareSourceCodestructured data, a keyword-rich home-page title with a lead paragraph describing the package, andkeywordson the main tutorials covering the cost-effectiveness and value-of-information terms readers search for. Anllms.txtfile gives answer engines and language models a curated map of the site, androbots.txtnames the major answer-engine and language-model crawlers as welcome (#72).Three calibration tutorials and example scripts that, with the surrogate tutorial, form a sequence on one shared three-state Markov model and one observed survey, so the posteriors are comparable across methods:
examples/calibrate_abc.py(approximate Bayesian computation on the model),examples/calibrate_sbi.py(neural posterior estimation on the model), andexamples/calibrate_microsim.py(a stochastic microsimulation whose replicate noise a Gaussian process surrogate carries into a posterior that is wider than the deterministic one and narrows as the simulated population grows) (#66).
Changed
plot_ce_planenow draws each intervention’s iterations as nested highest-density regions, the 50%, 80% and 95% regions of the cloud, rather than a scatter of points, which reads the spread more clearly once there are many iterations. An intervention with too few iterations falls back to a scatter, andkind="scatter"restores a point per iteration (#73).The documentation website renders its plots in the Poppins font at a higher figure resolution. The font is installed by the documentation workflow and styles the built site only, so it is not added to the installed package (#73).
The documentation website now uses a top navigation bar with a contextual sidebar per section, the tutorials are grouped into an ordered learning path (getting started, model engines, sensitivity and value of information, calibration, replications) with prerequisites first, and a new tutorials landing page (
docs/tutorials/index.qmd) shows a card grid per group. The home page keeps the runnable quickstart with a title banner and a card grid, and adocs/theme.scsslayer adds one accent color and typography polish over the flatly and darkly themes. The engine concept page moved out of the website todevdocs/engines.md(#70).The surrogate-accelerated calibration tutorial now runs both approximate Bayesian computation and neural posterior estimation against the same Gaussian process surrogate and matches them to a direct calibration, a like-for-like comparison that replaces pairing one method with the surrogate and the other with the model. The calibration target is a binomial survey of 1,000 people rather than an added measurement error, so the sampling error in the data and the noise in the model are represented separately (#66).
0.7.4 - 2026-07-18
Added
- Surrogate-accelerated calibration tutorial and
examples/surrogate_calibration.py: train a Gaussian process surrogate of a three-state Markov model on a 60-point Latin hypercube design, then calibrate two transition probabilities with neural posterior estimation from thesbipackage using the surrogate as the simulator. The posterior matches anabc_calibratereference run to within about 0.001 on every parameter mean and standard deviation, at more than a hundred times fewer model runs (60 versus several thousand). Adds asurrogateoptional dependency extra forsbi(#61).
0.7.3 - 2026-07-12
Changed
- Releases publish to PyPI from
.github/workflows/release.ymldirectly: the separatetag-release.ymlworkflow is folded intorelease.yml, so the publish job’s PyPI trusted-publisher identity is alwaysrelease.ymlregardless of which job triggered it, fixing theinvalid-publisherfailurev0.7.2’s publish hit. The tag and GitHub release are also now attributed to the repository owner instead ofgithub-actions[bot](#54).
0.7.2 - 2026-07-12
Added
- An “Open in Colab” badge on every tutorial with executable code, backed by a runnable notebook under
docs/_notebooks/thatdocs/build_colab_notebooks.pygenerates from the tutorial. Each notebook opens with a setup cell that installsheormodelfrom PyPI; the discrete-event simulation replication also clones the repository for the model code and mortality table it reads (#45). - An “Open in Colab” badge on the Quickstart section of the homepage, with a sentence pointing readers to it as the way to run any tutorial in Google Colab without installing anything locally (#49).
MarkovModel.trace(params, intervention)returns the cohort occupancy trace (acyclecolumn and one occupancy column per state), the public parallel toODEModel.trajectory, for inspecting and validating a cohort model (#43).
Fixed
- The Colab badge on every tutorial page no longer shows a stray “Open In Colab” caption underneath it. Pandoc turned the markdown image link into a
<figure>with the alt text as caption when it was the sole content of a paragraph; the badge now renders as raw HTML instead (#49).
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).