Skip to content

Calibration - #1107

Open
dhensle wants to merge 102 commits into
ActivitySim:mainfrom
RSGInc:calibration
Open

dhensle wants to merge 102 commits into
ActivitySim:mainfrom
RSGInc:calibration

Conversation

@dhensle

@dhensle dhensle commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

This pull request introduces the new automated calibration workflow for ActivitySim, modularizing calibration logic into a dedicated package and integrating it into the main simulation runner. The changes include the addition of a comprehensive calibration package, updates to the CLI to support calibration runs and output preservation, and enhancements to model settings validation to include calibration configuration.

Calibration package introduction and integration:

  • Added a new activitysim.core.calibration package, splitting calibration logic into focused modules such as orchestrator.py, settings.py, component.py, and more, with a clear public API. [1] [2] [3]
  • Updated activitysim/cli/run.py to import and utilize the calibration package, including logic to determine when to preserve outputs and to run the calibration workflow before standard or multiprocess simulation. [1] [2] [3]

Settings and validation improvements:

  • Enhanced model settings validation in settings_checker.py to explicitly validate the optional calibration.yaml configuration file using the same error aggregation as other settings. [1] [2]

Random number generator and reproducibility:

  • In initialize_households, ensured that both households and persons dataframes are registered as random number generator channels to support reproducible calibration and simulation.

Documentation:

  • Added a detailed README.md for the calibration package, describing its architecture, configuration, iteration/recovery semantics, and integration with the main workflow.

americalexander and others added 8 commits September 21, 2026 12:37
At the component iteration limit, calibration already ran an extra
simulation with the newly persisted coefficients. However, that call
bypassed expression evaluation and reporting, leaving convergence status,
histories, and reports describing the preceding simulation. Final model
outputs could match a target while calibration still reported failure.

Move the existing final simulation into an evaluation-only loop pass.
Rebuild the expression context, evaluate targets and convergence, and
refresh histories and enabled generic and bespoke reports. Skip coefficient
adjustments, bound clipping, and coefficient persistence on this pass so
reported results describe the coefficients actually simulated.

Give the final evaluation its own history index at
submodel_max_iterations + 1, preserving the preceding coefficient update
and recording zero final coefficient change. Retain the existing count of
calibration update iterations and the early-convergence behavior.

Add regression coverage for final-pass convergence and nonconvergence,
early convergence, refreshed reports and CSV histories, and evaluation
without coefficient adjustment or bound clipping. Document the final-pass
behavior and history numbering.

Validation: 39 calibration unit tests and 6 integration tests passed,
including single/multiprocess equivalence, restart, continuation, and
rewind scenarios. Black and git diff --check passed.
The calibration CSV reader selected only required columns, silently
removing the optional default_increment column. Although the coefficient
updater supported this setting, it always received rows without it and
fell back to 2.0. A configured increment of 0.25 therefore produced a 2.0
adjustment when a log-ratio or odds-ratio calculation needed the fallback.

Retain default_increment during CSV loading and validate it as a finite,
nonnegative numeric magnitude. Use 2.0 for omitted columns and blank cells,
and preserve explicit zero values so users can disable fallback adjustments
for individual rows. Leave the separate fallback damping behavior unchanged.

Document these semantics and add CSV-to-update regression coverage for
both adjustment methods and directions, custom and zero increments,
missing and blank values, and invalid inputs.

Validation: all 51 calibration unit tests passed. Black and staged
whitespace checks passed.
The updater preserved held or converged values before applying bounds,
which could still change them. For example, a held coefficient of 5 with
bounds [-1, 1] was silently clipped to 1. Clipping a converged coefficient
also changed the value used by the simulation that established convergence,
leaving the reported result inconsistent with the persisted coefficient.

Calculate adjustments and apply bounds only to coefficients eligible for
an update: neither held nor converged, and outside an evaluation-only pass.
Continue evaluating target differences and reporting bound flags for fixed
values; hold_fast does not imply convergence.

Retain the existing warning policy for initial values outside bounds and
document that held or converged values remain unchanged. Users requiring a
hard constraint must correct starting values or bounds before calibration.
Bounds continue to clip eligible coefficient updates normally.

Add regression coverage for held and converged coefficients above and below
bounds, unchanged coefficient records, target convergence status, initial
warnings, and clipping of eligible updates in both directions.

Validation: all 59 calibration unit tests passed. Black and staged
whitespace checks passed.
… rows

Templated models previously accepted every coefficient listed anywhere in
COEFFICIENT_TEMPLATE, including entries whose generic names never appeared
in the utility specification. This allowed calibration to adjust unused
coefficients without affecting the model's utilities.

Extract tokens from utility columns first, then map only referenced template
rows to their segment-specific coefficient names. Preserve direct-token
validation for models without templates and the template reader's handling
of blank cells. Labels, descriptions, expressions, and comments do not count
as coefficient references in utility columns.

Add regression coverage for referenced and unused template rows, multiple
segments, blank-cell inheritance, supplemental specifications, and direct
coefficient references. Document that this is a static reference check,
not a guarantee that a segment occurs in the simulation sample.

Validation: 61 calibration unit tests passed. The stricter check detects
exactly the six disconnected zero-auto SHARED2 coefficients in the original
SANDAG calibration tables; corrected SANDAG configurations pass five tests.
Both prototype MTC calibration configurations also validate. Formatting and
staged whitespace checks passed. No full regional calibration was run.
Calibration constants were appended to four expected parameter tables,
while Larch includes them in alphabetical order. Positional CSV comparisons
therefore paired different coefficients and failed despite passing likelihood
checks. Reorder those expected rows without changing any values or tolerances,
and sort unique-name parameter tables before regression comparison. Preserve
the existing grouped order for concatenated segment tables with repeated names.

Add focused checks that row reordering passes while missing, extra, renamed,
duplicate, and materially changed parameters fail. Check that the four affected
baselines retain unique, sorted names.

Add a focused Windows estimation workflow using the same locked dependencies
as Core Testing. Rebuild estimation data from a fresh cache and upload complete
CSV comparisons and JUnit results so numerical discrepancies can be inspected
without relying on truncated console output.

Local validation: seven parameter-comparison tests passed; all expected CSV
row contents are unchanged. Windows estimation validation follows in Actions.
Integrate estimation-test-fix while excluding the dedicated
.github/workflows/estimation-tests.yml validation workflow from the
calibration branch.

Sort uniquely named parameter results and reorder the four affected
regression baselines without changing expected values or tolerances.
Preserve grouped comparisons with repeated parameter names and add checks
that meaningful parameter changes still fail regression validation.

The source fix passed all 28 existing estimation mode tests and seven new
regression checks on Windows in driftlesslabs/activitysim Actions run
35681053967. Only the standalone workflow is excluded from this merge;
the existing Core Testing estimation job remains unchanged.
Fix calibration diagnostics, coefficient handling, and estimation regressions
@janzill

janzill commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

I've reviewed the PR and I think overall the workflow is easy enough to follow and run and consistent with ActivitySim's choice model specifications. It's also quite flexible and well documented, with a good example.

The only thing I would like to understand a bit better is the implementation of multi-processing and associated helpers. The calibration version seems to provide similar functionality to the core implementation, so I wonder if we could extend the core instead. I think there are a couple of issues that opus5.5 found that are a consequence of having two versions here, see 2, 3, 4 below. Is there a technical reason that makes them hard to combine?

Opus5.5 review results:

Findings

1. Declare or defer the Matplotlib runtime dependency

Location: cli/run.py:17,
calibration/reporting.py:10–20.

run.py imports calibration at module scope. Its package initializer imports
component, which imports expressions, which imports reporting.
reporting immediately imports matplotlib and matplotlib.pyplot. However,
pyproject.toml lists Matplotlib only in the development dependency group.
The lock file's Sharrow 2.16.2 dependency list also omits Matplotlib;
traversing the locked runtime dependency graph confirms it is absent from the
entire runtime dependency closure.

A normal installation without development dependencies can therefore fail while
importing the CLI, before it can parse arguments or check whether calibration is
enabled. activitysim -h, create, and ordinary runs are affected because the
CLI initializer imports run.

2. Recover all sliced and explicitly coalesced tables from subprocesses

Location: calibration/multiprocess.py:277–314.

_restore_from_subprocess_pipelines reads every table from subprocess 0, then
concatenates only tables explicitly named in slice.tables. Normal ActivitySim
multiprocessing also discovers dependent tables through their indexes and foreign
keys, and honors slice.coalesce. For example, the prototype MTC household step
lists households and persons; its tours are also partitioned across workers.

When recovery falls back to subprocess checkpoints because the requested
model-level checkpoint is absent from the parent pipeline, tours from every
worker except worker 0 are discarded. The function installs the incomplete
snapshot and returns success. Other dependent tables, such as trips and joint
tour participants, have the same exposure.

3. Execute pending models between the resume point and the next calibrated component

Location: calibration/orchestrator.py:542–562.

The loop initializes last_calibrated_component = None. Components at or before
resume_after are skipped without updating it. Intermediate models run only when
that variable is non-null. Consequently, resuming after a calibrated component,
or an intermediate model following one, does not run the remaining ordinary
models before the next calibrated component.

For initialize → model_a → between → model_b → output, with A and B calibrated,
resume_after: model_a should run between, then B. Instead it starts B.
B then attempts to restore the checkpoint for between, which may not exist;
checkpoint fallback can leave it with missing or incorrect inputs.

4. Include park-and-ride buffers in calibration's shared-resource allocation

Location: calibration/multiprocess.py:514–570.

_initialize_mp_shared_resources allocates skim and shadow-pricing buffers but
omits allocate_park_and_ride_lot_choice_buffers. The normal multiprocess runner
calls that allocator
(mp_tasks.py:1740–1747).
Calibration supplies its preallocated dictionary to that runner, so the runner
skips its entire normal allocation block.

A multiprocess calibration run that reaches park-and-ride capacity iteration
(ITERATE_WITH_TOUR_MODE_CHOICE) therefore lacks shared_pnr_choice and its
associated buffers. ParkAndRideCapacity directly indexes those keys whenever
data_buffers is present, producing KeyError('shared_pnr_choice').

5. Persist newly created coefficients before the first model simulation

Location: calibration/component.py:119–123,
first simulation at 159–166,
first persistence at 193–194.

_ensure_coefficients_exist adds missing calibration coefficients to the local
DataFrame and logs that they were added with value zero. The component then
runs before that DataFrame is written to the coefficient CSV. Ordinary model
steps read their coefficients from disk, where the new names are still absent.

The first simulation therefore fails while evaluating its utilities, even though
the framework has accepted and logged the addition. The user guide explicitly
promises automatic creation of missing coefficients at zero
(calibration.rst:712–714).

6. Normalize numeric hold flags before converting them to strings

Location: calibration/component.py:262–268.

The parser accepts 1 as a true hold_fast value and defaults blank cells to
false. If the same CSV column contains a numeric 1 and a blank, pandas infers
floating-point values. The true cell becomes 1.0; converting it to a string
produces "1.0", which the accepted true-value list omits. It is silently
classified as false and the coefficient is updated.

This branch has not been deployed

No deployments
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.

4 participants