Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
eb809d9
initial calibration commit
dhensle Jun 9, 2026
0876597
do not validate settings if file not found and not mandatory
dhensle Jun 9, 2026
6571db1
calibration settings, bespoke reports changes
americalexander Jul 2, 2026
6ccf84d
Fix global iterations
americalexander Jul 3, 2026
f2ea947
Loading from 2+ global iters
americalexander Jul 3, 2026
72758e6
Bugfix in plotting coefficient momentum
americalexander Jul 6, 2026
b065459
Add coef_delta to calibration records
americalexander Jul 6, 2026
3c135c7
Update graphs, handle zero modeled vals
americalexander Jul 6, 2026
b1db935
Batch output graphs
americalexander Jul 6, 2026
0b1f723
Minor plotting updates
americalexander Jul 6, 2026
96527ff
expose context to bespoke functions
dhensle Jul 7, 2026
d347f9c
output updates
dhensle Jul 7, 2026
9b02608
add component settings
americalexander Jul 7, 2026
4475471
Add calibration configs
americalexander Jul 7, 2026
1bf010c
passing context dict
dhensle Jul 7, 2026
dc731d0
updated specs for calibration
dhensle Jul 7, 2026
034545c
Add tour mode choice calibration coefficients
americalexander Jul 7, 2026
91bdb05
Merge branch 'calibration' of https://github.com/RSGInc/ActivitySim i…
americalexander Jul 7, 2026
fbf24d8
Update workplace location survey file directory
americalexander Jul 7, 2026
33ee993
auto ownership & tour mc relative paths
americalexander Jul 7, 2026
ff71c98
Allow no weights in tour mc summary
americalexander Jul 7, 2026
217b3e7
Remove duplicated checkpoint directories
americalexander Jul 13, 2026
aadd529
Minor bugfixes
americalexander Jul 14, 2026
67fd29f
Separate Claude attempt at MP calibration
americalexander Jul 14, 2026
6de9e48
MultiprocessStep creation fix
americalexander Jul 14, 2026
de894c2
Alleged checkpoint fix
americalexander Jul 14, 2026
3c78700
Deduplicate checkpoints
americalexander Jul 14, 2026
9856ee6
Prevent resume_after setting
americalexander Jul 14, 2026
fd7950a
More resume_after fixes
americalexander Jul 15, 2026
97a29d0
update existing table status
americalexander Jul 16, 2026
dbdff18
Checkpoint rename
americalexander Jul 16, 2026
d2dd7ae
Store last checkpoint
americalexander Jul 16, 2026
510b5d4
Blacken! It works!
americalexander Jul 16, 2026
0ee33d3
Set calibration coefficients to zero
americalexander Jul 16, 2026
fcfcb3c
Add override CI test coefficients for calibration
americalexander Jul 16, 2026
9d9f006
Add estimation mode regression test coeffs
americalexander Jul 16, 2026
afebfe3
More coefficient adds
americalexander Jul 16, 2026
4cda8e6
Add calibration documentation RST
americalexander Jul 17, 2026
15df92c
move calib documentation into users guide
dhensle Jul 21, 2026
4e38277
all converged fix and dead restart_after
dhensle Jul 21, 2026
391d9a1
blacken
dhensle Jul 21, 2026
eb4e6ba
Fix resume_after bug
americalexander Jul 21, 2026
de119ed
partial MP resume_after fix
americalexander Jul 22, 2026
f0e9ccb
More checkpointing work
americalexander Jul 29, 2026
c9afece
Add tour mode choice handler
americalexander Jul 29, 2026
ba8a142
Clean checkpointing
americalexander Jul 30, 2026
c7f3c77
More checkpointing fixes
americalexander Jul 30, 2026
c9aca45
Restore from subprocess checkpoints
americalexander Jul 30, 2026
a19638b
resume_after bugfix
americalexander Jul 31, 2026
2fd3db7
add subproc reuse
americalexander Aug 4, 2026
cc344c7
Modify breadcrumb logic
americalexander Aug 5, 2026
fcc6567
Add RNG channel reloading
americalexander Aug 11, 2026
a854b67
Make survey file optional
americalexander Aug 11, 2026
1b4eb8b
Add single-process channel restoration
americalexander Aug 11, 2026
7b66d4f
Update rng_channels
americalexander Aug 11, 2026
c0c937d
Modify single-process run approach
americalexander Aug 11, 2026
cf2c743
handle index_to_channel
americalexander Aug 11, 2026
ec6e269
Modify configured mode to use same path
americalexander Aug 11, 2026
c287efb
Remove memory sidecar process from calib
americalexander Aug 11, 2026
52ee5cd
Add reloading of tables in reregister_rng
americalexander Aug 11, 2026
af641ed
Maybe a different approach to checkpoint restoration
americalexander Aug 11, 2026
b38b329
Restore correct checkpoint
americalexander Aug 11, 2026
b452056
Fix add_table bug
americalexander Aug 12, 2026
d169bd4
Different approach to checkpoint/channel restoration
americalexander Aug 12, 2026
2eb889d
Add table invalidation
americalexander Aug 12, 2026
67b87a4
Additional precursor guard
americalexander Aug 12, 2026
8a19f6c
Blacken
americalexander Aug 12, 2026
21a0d78
Fix calibrate_component precursor calls
americalexander Aug 12, 2026
0437b70
Add temporary diagnostic tracing
americalexander Aug 12, 2026
2f58828
More diagnostic logging
americalexander Aug 12, 2026
6563525
Change to default tables_to_invalidate
americalexander Aug 12, 2026
210454e
Change when table invalidation occurs
americalexander Aug 12, 2026
e8f6a33
Fix SP checkpointing
americalexander Aug 12, 2026
fb923bb
fixing resume after and rng calib invariance
dhensle Aug 14, 2026
92bbc84
fixing pipeline and rng restarting issues
dhensle Aug 14, 2026
af1e21b
fixing more pipeline restart issues
dhensle Aug 17, 2026
022b4fb
appropriately handling crashing in middle of iteration
dhensle Aug 19, 2026
d79517e
refactor into separate files
dhensle Aug 19, 2026
ce7844f
if everything is converged, will continue to run downstream models
dhensle Aug 19, 2026
e9dcffb
blacken
dhensle Aug 19, 2026
adf17f9
remove coefficient rollback functionality
dhensle Aug 19, 2026
66e1037
better handling of internal memory handling between iterations
dhensle Aug 20, 2026
1f55863
run modes and resume after tests
dhensle Aug 20, 2026
97fbf16
nailing down resume_after and iteration counting
dhensle Aug 25, 2026
910a0e0
blacken
dhensle Aug 25, 2026
2ef9828
removing unused calibration.yaml settings
dhensle Aug 25, 2026
cd8f8b5
clarifying resume_after readme
dhensle Aug 25, 2026
896fc06
aligning tests and language around restart functionality
dhensle Aug 26, 2026
f414f23
setting checking cleanup
dhensle Aug 26, 2026
e2ee5b5
Merge remote-tracking branch 'upstream/main' into calibration
dhensle Aug 26, 2026
6c59ee2
addressing RNG and resume review comments
dhensle Aug 26, 2026
0b953b2
clearer logging at max iterations
dhensle Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions activitysim/abm/models/initialize.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,12 +154,19 @@ def initialize_households(
households = state.get_dataframe("households")
assert not households._is_view
chunk_sizer.log_df(trace_label, "households", households)
del households
chunk_sizer.log_df(trace_label, "households", None)

persons = state.get_dataframe("persons")
assert not persons._is_view
chunk_sizer.log_df(trace_label, "persons", persons)

rng = state.get_rn_generator()
if "households" not in rng.channels:
rng.add_channel("households", households)
if "persons" not in rng.channels:
rng.add_channel("persons", persons)

del households
chunk_sizer.log_df(trace_label, "households", None)
del persons
chunk_sizer.log_df(trace_label, "persons", None)

Expand Down
28 changes: 24 additions & 4 deletions activitysim/abm/models/settings_checker.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,24 +4,26 @@
from pydantic import BaseModel as PydanticBase
from typing import Type, Optional

from activitysim.core import config
from activitysim.core.calibration.settings import (
CALIBRATION_SETTINGS_FILE_NAME,
CalibrationConfig,
)
from activitysim.core.configuration.base import PydanticReadable

# import core settings
from activitysim.core.configuration.logit import (
LogitNestSpec,
TourLocationComponentSettings,
TourModeComponentSettings,
TemplatedLogitComponentSettings,
)
from activitysim.core import config
from activitysim.core.configuration.network import NetworkSettings
from activitysim.core.exceptions import ModelConfigurationError
from activitysim.core.workflow import State
from activitysim.core.simulate import (
eval_coefficients,
eval_nest_coefficients,
read_model_coefficient_template,
)
from activitysim.core.exceptions import ModelConfigurationError

# import model settings
from activitysim.abm.models.accessibility import AccessibilitySettings
Expand DownExpand Up@@ -637,6 +639,24 @@ def check_model_settings(
# Collect all errors
all_errors = []

# calibration.yaml is optional and is not itself a model step, so validate
# it explicitly using the same Pydantic/error-aggregation path as model
# settings files.
try:
CalibrationConfig.read_settings_file(
state.filesystem,
CALIBRATION_SETTINGS_FILE_NAME,
mandatory=False,
)
except Exception as error:
all_errors.append(
SettingsCheckerError(
"calibration",
error,
CALIBRATION_SETTINGS_FILE_NAME,
)
)

# additional logging set up
formatter = logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
Expand Down
60 changes: 57 additions & 3 deletions activitysim/cli/run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@

import numpy as np

from activitysim.core import chunk, config, mem, timing, tracing, workflow
from activitysim.core import calibration, chunk, config, mem, timing, tracing, workflow
from activitysim.core.configuration import FileSystem, Settings
from activitysim.core.run_id import RunId

Expand DownExpand Up@@ -336,8 +336,13 @@ def run(args):
resume_after = state.settings.resume_after

# cleanup if not resuming
if not resume_after:
preserve_calibration_outputs = calibration.calibration_run_should_preserve_outputs(
state
)
if not resume_after and not preserve_calibration_outputs:
cleanup_output_files(state)
elif preserve_calibration_outputs:
logger.info("preserving output files based on calibration restart preflight")
elif state.settings.cleanup_trace_files_on_resume:
tracing.delete_trace_files(state)

Expand DownExpand Up@@ -418,7 +423,56 @@ def run(args):
check_model_settings(state, extension_settings=extension_checker_settings)

try:
if state.settings.multiprocess:
if calibration.calibration_enabled(state):
logger.info("evaluate calibration workflow")

calibration_result = calibration.run_calibration_loop(
state=state,
models=state.settings.models,
)

if calibration_result.model_system_ran:
logger.info(
"calibration workflow complete converged=%s "
"completed_global_iterations=%s configured_global_iterations=%s",
calibration_result.converged,
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)
else:
if (
calibration_result.completed_global_iterations
>= calibration_result.configured_global_iterations
):
logger.info(
"calibration workflow skipped: no model system steps were run "
"because completed_global_iterations=%s has reached the "
"configured run.global_iterations=%s limit. Increase "
"run.global_iterations to request additional iterations, or "
"remove output/calibration/calibration_progress.json to start a fresh run.",
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)
else:
logger.info(
"calibration workflow skipped: no model system steps were run "
"because the prior calibration run is already complete "
"(completed_global_iterations=%s, run.global_iterations=%s). "
"Change run.global_iterations to a new value above the completed "
"count to request additional iterations, or remove "
"output/calibration/calibration_progress.json to start a fresh run.",
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)

if state.settings.cleanup_pipeline_after_run:
state.checkpoint.cleanup()
else:
state.checkpoint.close_store()

mem.log_global_hwm()

elif state.settings.multiprocess:
logger.info("run multiprocess simulation")

from activitysim.core import mp_tasks
Expand Down
122 changes: 122 additions & 0 deletions activitysim/core/calibration/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
# ActivitySim calibration package

This package implements ActivitySim's automated coefficient calibration workflow.
The modules are divided by responsibility so orchestration, model execution, and
calibration math can evolve independently.

- `__init__.py` defines the public API and compatibility access to former private
attributes from `activitysim.core.calibration`.
- `settings.py` defines calibration configuration models, result types, and
configuration loading.
- `orchestrator.py` coordinates global iterations and model-component sequencing.
- `component.py` runs component iterations, validates calibration specifications,
and calculates coefficient updates.
- `expressions.py` builds expression contexts, evaluates target/model expressions,
and loads optional helper modules.
- `coefficients.py` locates, reads, and writes model coefficient files.
- `reporting.py` writes iteration histories, summaries, plots, and final snapshots.
- `recovery.py` durably tracks calibration progress. Coefficient files are the
authoritative current state and are never rolled back when a run resumes.
- `execution.py` restores pipeline state and dispatches model execution.
- `multiprocess.py` contains subprocess orchestration, shared-resource setup, and
multiprocess pipeline restoration.

Component settings may specify `model_settings_file` when a workflow step does
not follow the conventional `<component_name>.yaml` naming pattern. Components
that share model settings may point to the same coefficient file.

## Global iterations, recovery attempts, and `resume_after`

`calibration.yaml` `run.global_iterations` is the desired total number of
completed logical calibration iterations, not the number to execute on each
ActivitySim invocation. The run-control contract is:

1. An unchanged setting on a completed run is a no-op, detected before normal
output cleanup so the pipeline and final outputs are preserved.
2. If a completed run's setting is changed to a value greater than the number
actually completed, calibration continues until the new total. This includes
lowering a previous maximum after early convergence, such as changing 5 to 3
after convergence completed iteration 2.
3. A changed setting less than or equal to the number already completed is a
no-op; completed coefficient updates are never undone implicitly.
4. Top-level `settings.yaml` `resume_after` has normal ActivitySim semantics for
the first global iteration entered by the current invocation. Later global
iterations ignore it and execute every calibrated component.
5. A global iteration counts only if it has at least one durable calibrated
component result, either from the current attempt or an earlier attempt of
that same logical iteration.
6. `global_iterations` cannot be lowered below an interrupted iteration because
its coefficient files may already contain updates from that iteration. The
run stops with instructions to resume the iteration or deliberately reset
progress and coefficients.
7. Startup logs report the detected completed count, requested target, selected
action, starting iteration and attempt, and `resume_after` value.

Calibration records the state needed to apply this contract in
`output/calibration/calibration_progress.json`.

Recovery attempts are distinct from global iterations. The first execution of a
global iteration is attempt 1. Restarting an interrupted global iteration creates
attempt 2, then attempt 3 if another restart is needed. A recovery attempt does
not consume an additional global iteration.

Calibration uses only the top-level `settings.yaml` `resume_after` setting. On
the first global iteration executed by an ActivitySim invocation, including a
new attempt of an interrupted iteration, it behaves like `resume_after` in a
non-calibration run:

- when set to a model name, that model is treated as complete, its checkpoint is
restored, and execution begins with the following model; and
- when unset or `null`, execution starts at the beginning of the top-level
`models` list. Calibration does not automatically continue after the last
model completed by the preceding attempt.

A named value must occur in the top-level `models` list and must identify a
model-level checkpoint. Calibration does not define a special `initialize`
value. A value such as `initialize_landuse` has ordinary model-name semantics:
that model is skipped and execution begins with the next model. The `_` shorthand
for the last checkpoint is not accepted in calibration mode.

`resume_after` affects only the first global iteration entered by the current
invocation. Calibrated models at or before a named resume point are skipped in
that iteration. Any later global iterations in the same invocation ignore
`resume_after`, restart immediately before the first calibrated model, and run
the normal complete calibration sequence.

For example, suppose global iteration 3 attempt 1 was interrupted after
calibrated `model_a` completed:

- `resume_after: model_a` starts attempt 2 after model A, preserving its
attempt-1 result; and
- `resume_after: null` starts attempt 2 at the beginning of the complete model
list, so model A runs again.

Coefficient files are the authoritative current state and are never rolled back
during recovery. Updates written before the interruption, along with subsequent
manual edits, become the starting coefficients for the new attempt. Rewinding
across a completed calibrated model therefore applies another calibration update
to its current coefficients. Calibration logs a warning when `resume_after`
causes this kind of rewind.

Iteration histories are append-only across attempts. Standard record, summary,
and generic-report rows include `global_iter`, `attempt`, and `component_iter`,
so a rerun does not replace the coefficient transition written by an earlier
attempt. The coefficient trajectory plots show the complete sequence, including
the initial value and every subsequent update. X-axis labels use the compact
form `G<global>-A<attempt>-C<component>`, for example `G3-A2-C1`.

After each calibrated component completes, the progress file records its attempt,
component-iteration count, and convergence result. A restart that skips that
component retains this state. This allows a run that crashed in downstream,
non-calibrated models to finish the same terminal global iteration without losing
the calibrated components' convergence decision.

Calibration is marked complete only after the terminal global iteration has run
all remaining models in the top-level `models` list and the final coefficient
snapshot has been written. A crash after the last calibrated model but before the
last production model therefore leaves the global iteration in progress.

Dependencies should flow from `orchestrator.py` into the focused modules. Lower
level modules should not import the orchestrator. Multiprocess code imports back
into `execution.py` only inside the single-component runner to avoid an import-time
cycle.
71 changes: 71 additions & 0 deletions activitysim/core/calibration/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
# ActivitySim
# See full license in LICENSE.txt.
"""Automated calibration support for ActivitySim model runs.

The public API is intentionally small. Private attributes from the former
single-file module remain available through ``__getattr__`` for compatibility
while callers migrate to the package modules.
"""

from . import (
coefficients,
component,
execution,
expressions,
multiprocess,
recovery,
reporting,
settings,
)
from .orchestrator import calibration_run_should_preserve_outputs, run_calibration_loop
from .settings import (
CalibrationComponentResult,
CalibrationComponentSettings,
CalibrationConfig,
CalibrationReportsSettings,
CalibrationRunResult,
CalibrationRunSettings,
calibration_enabled,
read_calibration_settings,
)

__all__ = [
"CalibrationComponentResult",
"CalibrationComponentSettings",
"CalibrationConfig",
"CalibrationReportsSettings",
"CalibrationRunResult",
"CalibrationRunSettings",
"calibration_run_should_preserve_outputs",
"calibration_enabled",
"read_calibration_settings",
"run_calibration_loop",
]

_COMPATIBILITY_MODULES = (
settings,
component,
expressions,
coefficients,
reporting,
recovery,
execution,
multiprocess,
)


def __getattr__(name):
for module in _COMPATIBILITY_MODULES:
if hasattr(module, name):
return getattr(module, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def __dir__():
compatibility_names = {
name
for module in _COMPATIBILITY_MODULES
for name in vars(module)
if not name.startswith("__")
}
return sorted(set(globals()) | compatibility_names)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
eb809d9
initial calibration commit
dhensle Jun 9, 2026
0876597
do not validate settings if file not found and not mandatory
dhensle Jun 9, 2026
6571db1
calibration settings, bespoke reports changes
americalexander Jul 2, 2026
6ccf84d
Fix global iterations
americalexander Jul 3, 2026
f2ea947
Loading from 2+ global iters
americalexander Jul 3, 2026
72758e6
Bugfix in plotting coefficient momentum
americalexander Jul 6, 2026
b065459
Add coef_delta to calibration records
americalexander Jul 6, 2026
3c135c7
Update graphs, handle zero modeled vals
americalexander Jul 6, 2026
b1db935
Batch output graphs
americalexander Jul 6, 2026
0b1f723
Minor plotting updates
americalexander Jul 6, 2026
96527ff
expose context to bespoke functions
dhensle Jul 7, 2026
d347f9c
output updates
dhensle Jul 7, 2026
9b02608
add component settings
americalexander Jul 7, 2026
4475471
Add calibration configs
americalexander Jul 7, 2026
1bf010c
passing context dict
dhensle Jul 7, 2026
dc731d0
updated specs for calibration
dhensle Jul 7, 2026
034545c
Add tour mode choice calibration coefficients
americalexander Jul 7, 2026
91bdb05
Merge branch 'calibration' of https://github.com/RSGInc/ActivitySim i…
americalexander Jul 7, 2026
fbf24d8
Update workplace location survey file directory
americalexander Jul 7, 2026
33ee993
auto ownership & tour mc relative paths
americalexander Jul 7, 2026
ff71c98
Allow no weights in tour mc summary
americalexander Jul 7, 2026
217b3e7
Remove duplicated checkpoint directories
americalexander Jul 13, 2026
aadd529
Minor bugfixes
americalexander Jul 14, 2026
67fd29f
Separate Claude attempt at MP calibration
americalexander Jul 14, 2026
6de9e48
MultiprocessStep creation fix
americalexander Jul 14, 2026
de894c2
Alleged checkpoint fix
americalexander Jul 14, 2026
3c78700
Deduplicate checkpoints
americalexander Jul 14, 2026
9856ee6
Prevent resume_after setting
americalexander Jul 14, 2026
fd7950a
More resume_after fixes
americalexander Jul 15, 2026
97a29d0
update existing table status
americalexander Jul 16, 2026
dbdff18
Checkpoint rename
americalexander Jul 16, 2026
d2dd7ae
Store last checkpoint
americalexander Jul 16, 2026
510b5d4
Blacken! It works!
americalexander Jul 16, 2026
0ee33d3
Set calibration coefficients to zero
americalexander Jul 16, 2026
fcfcb3c
Add override CI test coefficients for calibration
americalexander Jul 16, 2026
9d9f006
Add estimation mode regression test coeffs
americalexander Jul 16, 2026
afebfe3
More coefficient adds
americalexander Jul 16, 2026
4cda8e6
Add calibration documentation RST
americalexander Jul 17, 2026
15df92c
move calib documentation into users guide
dhensle Jul 21, 2026
4e38277
all converged fix and dead restart_after
dhensle Jul 21, 2026
391d9a1
blacken
dhensle Jul 21, 2026
eb4e6ba
Fix resume_after bug
americalexander Jul 21, 2026
de119ed
partial MP resume_after fix
americalexander Jul 22, 2026
f0e9ccb
More checkpointing work
americalexander Jul 29, 2026
c9afece
Add tour mode choice handler
americalexander Jul 29, 2026
ba8a142
Clean checkpointing
americalexander Jul 30, 2026
c7f3c77
More checkpointing fixes
americalexander Jul 30, 2026
c9aca45
Restore from subprocess checkpoints
americalexander Jul 30, 2026
a19638b
resume_after bugfix
americalexander Jul 31, 2026
2fd3db7
add subproc reuse
americalexander Aug 4, 2026
cc344c7
Modify breadcrumb logic
americalexander Aug 5, 2026
fcc6567
Add RNG channel reloading
americalexander Aug 11, 2026
a854b67
Make survey file optional
americalexander Aug 11, 2026
1b4eb8b
Add single-process channel restoration
americalexander Aug 11, 2026
7b66d4f
Update rng_channels
americalexander Aug 11, 2026
c0c937d
Modify single-process run approach
americalexander Aug 11, 2026
cf2c743
handle index_to_channel
americalexander Aug 11, 2026
ec6e269
Modify configured mode to use same path
americalexander Aug 11, 2026
c287efb
Remove memory sidecar process from calib
americalexander Aug 11, 2026
52ee5cd
Add reloading of tables in reregister_rng
americalexander Aug 11, 2026
af641ed
Maybe a different approach to checkpoint restoration
americalexander Aug 11, 2026
b38b329
Restore correct checkpoint
americalexander Aug 11, 2026
b452056
Fix add_table bug
americalexander Aug 12, 2026
d169bd4
Different approach to checkpoint/channel restoration
americalexander Aug 12, 2026
2eb889d
Add table invalidation
americalexander Aug 12, 2026
67b87a4
Additional precursor guard
americalexander Aug 12, 2026
8a19f6c
Blacken
americalexander Aug 12, 2026
21a0d78
Fix calibrate_component precursor calls
americalexander Aug 12, 2026
0437b70
Add temporary diagnostic tracing
americalexander Aug 12, 2026
2f58828
More diagnostic logging
americalexander Aug 12, 2026
6563525
Change to default tables_to_invalidate
americalexander Aug 12, 2026
210454e
Change when table invalidation occurs
americalexander Aug 12, 2026
e8f6a33
Fix SP checkpointing
americalexander Aug 12, 2026
fb923bb
fixing resume after and rng calib invariance
dhensle Aug 14, 2026
92bbc84
fixing pipeline and rng restarting issues
dhensle Aug 14, 2026
af1e21b
fixing more pipeline restart issues
dhensle Aug 17, 2026
022b4fb
appropriately handling crashing in middle of iteration
dhensle Aug 19, 2026
d79517e
refactor into separate files
dhensle Aug 19, 2026
ce7844f
if everything is converged, will continue to run downstream models
dhensle Aug 19, 2026
e9dcffb
blacken
dhensle Aug 19, 2026
adf17f9
remove coefficient rollback functionality
dhensle Aug 19, 2026
66e1037
better handling of internal memory handling between iterations
dhensle Aug 20, 2026
1f55863
run modes and resume after tests
dhensle Aug 20, 2026
97fbf16
nailing down resume_after and iteration counting
dhensle Aug 25, 2026
910a0e0
blacken
dhensle Aug 25, 2026
2ef9828
removing unused calibration.yaml settings
dhensle Aug 25, 2026
cd8f8b5
clarifying resume_after readme
dhensle Aug 25, 2026
896fc06
aligning tests and language around restart functionality
dhensle Aug 26, 2026
f414f23
setting checking cleanup
dhensle Aug 26, 2026
e2ee5b5
Merge remote-tracking branch 'upstream/main' into calibration
dhensle Aug 26, 2026
6c59ee2
addressing RNG and resume review comments
dhensle Aug 26, 2026
0b953b2
clearer logging at max iterations
dhensle Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions activitysim/abm/models/initialize.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,12 +154,19 @@ def initialize_households(
households = state.get_dataframe("households")
assert not households._is_view
chunk_sizer.log_df(trace_label, "households", households)
del households
chunk_sizer.log_df(trace_label, "households", None)

persons = state.get_dataframe("persons")
assert not persons._is_view
chunk_sizer.log_df(trace_label, "persons", persons)

rng = state.get_rn_generator()
if "households" not in rng.channels:
rng.add_channel("households", households)
if "persons" not in rng.channels:
rng.add_channel("persons", persons)

del households
chunk_sizer.log_df(trace_label, "households", None)
del persons
chunk_sizer.log_df(trace_label, "persons", None)

Expand Down
28 changes: 24 additions & 4 deletions activitysim/abm/models/settings_checker.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,24 +4,26 @@
from pydantic import BaseModel as PydanticBase
from typing import Type, Optional

from activitysim.core import config
from activitysim.core.calibration.settings import (
CALIBRATION_SETTINGS_FILE_NAME,
CalibrationConfig,
)
from activitysim.core.configuration.base import PydanticReadable

# import core settings
from activitysim.core.configuration.logit import (
LogitNestSpec,
TourLocationComponentSettings,
TourModeComponentSettings,
TemplatedLogitComponentSettings,
)
from activitysim.core import config
from activitysim.core.configuration.network import NetworkSettings
from activitysim.core.exceptions import ModelConfigurationError
from activitysim.core.workflow import State
from activitysim.core.simulate import (
eval_coefficients,
eval_nest_coefficients,
read_model_coefficient_template,
)
from activitysim.core.exceptions import ModelConfigurationError

# import model settings
from activitysim.abm.models.accessibility import AccessibilitySettings
Expand DownExpand Up@@ -637,6 +639,24 @@ def check_model_settings(
# Collect all errors
all_errors = []

# calibration.yaml is optional and is not itself a model step, so validate
# it explicitly using the same Pydantic/error-aggregation path as model
# settings files.
try:
CalibrationConfig.read_settings_file(
state.filesystem,
CALIBRATION_SETTINGS_FILE_NAME,
mandatory=False,
)
except Exception as error:
all_errors.append(
SettingsCheckerError(
"calibration",
error,
CALIBRATION_SETTINGS_FILE_NAME,
)
)

# additional logging set up
formatter = logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
Expand Down
60 changes: 57 additions & 3 deletions activitysim/cli/run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@

import numpy as np

from activitysim.core import chunk, config, mem, timing, tracing, workflow
from activitysim.core import calibration, chunk, config, mem, timing, tracing, workflow
from activitysim.core.configuration import FileSystem, Settings
from activitysim.core.run_id import RunId

Expand DownExpand Up@@ -336,8 +336,13 @@ def run(args):
resume_after = state.settings.resume_after

# cleanup if not resuming
if not resume_after:
preserve_calibration_outputs = calibration.calibration_run_should_preserve_outputs(
state
)
if not resume_after and not preserve_calibration_outputs:
cleanup_output_files(state)
elif preserve_calibration_outputs:
logger.info("preserving output files based on calibration restart preflight")
elif state.settings.cleanup_trace_files_on_resume:
tracing.delete_trace_files(state)

Expand DownExpand Up@@ -418,7 +423,56 @@ def run(args):
check_model_settings(state, extension_settings=extension_checker_settings)

try:
if state.settings.multiprocess:
if calibration.calibration_enabled(state):
logger.info("evaluate calibration workflow")

calibration_result = calibration.run_calibration_loop(
state=state,
models=state.settings.models,
)

if calibration_result.model_system_ran:
logger.info(
"calibration workflow complete converged=%s "
"completed_global_iterations=%s configured_global_iterations=%s",
calibration_result.converged,
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)
else:
if (
calibration_result.completed_global_iterations
>= calibration_result.configured_global_iterations
):
logger.info(
"calibration workflow skipped: no model system steps were run "
"because completed_global_iterations=%s has reached the "
"configured run.global_iterations=%s limit. Increase "
"run.global_iterations to request additional iterations, or "
"remove output/calibration/calibration_progress.json to start a fresh run.",
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)
else:
logger.info(
"calibration workflow skipped: no model system steps were run "
"because the prior calibration run is already complete "
"(completed_global_iterations=%s, run.global_iterations=%s). "
"Change run.global_iterations to a new value above the completed "
"count to request additional iterations, or remove "
"output/calibration/calibration_progress.json to start a fresh run.",
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)

if state.settings.cleanup_pipeline_after_run:
state.checkpoint.cleanup()
else:
state.checkpoint.close_store()

mem.log_global_hwm()

elif state.settings.multiprocess:
logger.info("run multiprocess simulation")

from activitysim.core import mp_tasks
Expand Down
122 changes: 122 additions & 0 deletions activitysim/core/calibration/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
# ActivitySim calibration package

This package implements ActivitySim's automated coefficient calibration workflow.
The modules are divided by responsibility so orchestration, model execution, and
calibration math can evolve independently.

- `__init__.py` defines the public API and compatibility access to former private
attributes from `activitysim.core.calibration`.
- `settings.py` defines calibration configuration models, result types, and
configuration loading.
- `orchestrator.py` coordinates global iterations and model-component sequencing.
- `component.py` runs component iterations, validates calibration specifications,
and calculates coefficient updates.
- `expressions.py` builds expression contexts, evaluates target/model expressions,
and loads optional helper modules.
- `coefficients.py` locates, reads, and writes model coefficient files.
- `reporting.py` writes iteration histories, summaries, plots, and final snapshots.
- `recovery.py` durably tracks calibration progress. Coefficient files are the
authoritative current state and are never rolled back when a run resumes.
- `execution.py` restores pipeline state and dispatches model execution.
- `multiprocess.py` contains subprocess orchestration, shared-resource setup, and
multiprocess pipeline restoration.

Component settings may specify `model_settings_file` when a workflow step does
not follow the conventional `<component_name>.yaml` naming pattern. Components
that share model settings may point to the same coefficient file.

## Global iterations, recovery attempts, and `resume_after`

`calibration.yaml` `run.global_iterations` is the desired total number of
completed logical calibration iterations, not the number to execute on each
ActivitySim invocation. The run-control contract is:

1. An unchanged setting on a completed run is a no-op, detected before normal
output cleanup so the pipeline and final outputs are preserved.
2. If a completed run's setting is changed to a value greater than the number
actually completed, calibration continues until the new total. This includes
lowering a previous maximum after early convergence, such as changing 5 to 3
after convergence completed iteration 2.
3. A changed setting less than or equal to the number already completed is a
no-op; completed coefficient updates are never undone implicitly.
4. Top-level `settings.yaml` `resume_after` has normal ActivitySim semantics for
the first global iteration entered by the current invocation. Later global
iterations ignore it and execute every calibrated component.
5. A global iteration counts only if it has at least one durable calibrated
component result, either from the current attempt or an earlier attempt of
that same logical iteration.
6. `global_iterations` cannot be lowered below an interrupted iteration because
its coefficient files may already contain updates from that iteration. The
run stops with instructions to resume the iteration or deliberately reset
progress and coefficients.
7. Startup logs report the detected completed count, requested target, selected
action, starting iteration and attempt, and `resume_after` value.

Calibration records the state needed to apply this contract in
`output/calibration/calibration_progress.json`.

Recovery attempts are distinct from global iterations. The first execution of a
global iteration is attempt 1. Restarting an interrupted global iteration creates
attempt 2, then attempt 3 if another restart is needed. A recovery attempt does
not consume an additional global iteration.

Calibration uses only the top-level `settings.yaml` `resume_after` setting. On
the first global iteration executed by an ActivitySim invocation, including a
new attempt of an interrupted iteration, it behaves like `resume_after` in a
non-calibration run:

- when set to a model name, that model is treated as complete, its checkpoint is
restored, and execution begins with the following model; and
- when unset or `null`, execution starts at the beginning of the top-level
`models` list. Calibration does not automatically continue after the last
model completed by the preceding attempt.

A named value must occur in the top-level `models` list and must identify a
model-level checkpoint. Calibration does not define a special `initialize`
value. A value such as `initialize_landuse` has ordinary model-name semantics:
that model is skipped and execution begins with the next model. The `_` shorthand
for the last checkpoint is not accepted in calibration mode.

`resume_after` affects only the first global iteration entered by the current
invocation. Calibrated models at or before a named resume point are skipped in
that iteration. Any later global iterations in the same invocation ignore
`resume_after`, restart immediately before the first calibrated model, and run
the normal complete calibration sequence.

For example, suppose global iteration 3 attempt 1 was interrupted after
calibrated `model_a` completed:

- `resume_after: model_a` starts attempt 2 after model A, preserving its
attempt-1 result; and
- `resume_after: null` starts attempt 2 at the beginning of the complete model
list, so model A runs again.

Coefficient files are the authoritative current state and are never rolled back
during recovery. Updates written before the interruption, along with subsequent
manual edits, become the starting coefficients for the new attempt. Rewinding
across a completed calibrated model therefore applies another calibration update
to its current coefficients. Calibration logs a warning when `resume_after`
causes this kind of rewind.

Iteration histories are append-only across attempts. Standard record, summary,
and generic-report rows include `global_iter`, `attempt`, and `component_iter`,
so a rerun does not replace the coefficient transition written by an earlier
attempt. The coefficient trajectory plots show the complete sequence, including
the initial value and every subsequent update. X-axis labels use the compact
form `G<global>-A<attempt>-C<component>`, for example `G3-A2-C1`.

After each calibrated component completes, the progress file records its attempt,
component-iteration count, and convergence result. A restart that skips that
component retains this state. This allows a run that crashed in downstream,
non-calibrated models to finish the same terminal global iteration without losing
the calibrated components' convergence decision.

Calibration is marked complete only after the terminal global iteration has run
all remaining models in the top-level `models` list and the final coefficient
snapshot has been written. A crash after the last calibrated model but before the
last production model therefore leaves the global iteration in progress.

Dependencies should flow from `orchestrator.py` into the focused modules. Lower
level modules should not import the orchestrator. Multiprocess code imports back
into `execution.py` only inside the single-component runner to avoid an import-time
cycle.
71 changes: 71 additions & 0 deletions activitysim/core/calibration/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
# ActivitySim
# See full license in LICENSE.txt.
"""Automated calibration support for ActivitySim model runs.

The public API is intentionally small. Private attributes from the former
single-file module remain available through ``__getattr__`` for compatibility
while callers migrate to the package modules.
"""

from . import (
coefficients,
component,
execution,
expressions,
multiprocess,
recovery,
reporting,
settings,
)
from .orchestrator import calibration_run_should_preserve_outputs, run_calibration_loop
from .settings import (
CalibrationComponentResult,
CalibrationComponentSettings,
CalibrationConfig,
CalibrationReportsSettings,
CalibrationRunResult,
CalibrationRunSettings,
calibration_enabled,
read_calibration_settings,
)

__all__ = [
"CalibrationComponentResult",
"CalibrationComponentSettings",
"CalibrationConfig",
"CalibrationReportsSettings",
"CalibrationRunResult",
"CalibrationRunSettings",
"calibration_run_should_preserve_outputs",
"calibration_enabled",
"read_calibration_settings",
"run_calibration_loop",
]

_COMPATIBILITY_MODULES = (
settings,
component,
expressions,
coefficients,
reporting,
recovery,
execution,
multiprocess,
)


def __getattr__(name):
for module in _COMPATIBILITY_MODULES:
if hasattr(module, name):
return getattr(module, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def __dir__():
compatibility_names = {
name
for module in _COMPATIBILITY_MODULES
for name in vars(module)
if not name.startswith("__")
}
return sorted(set(globals()) | compatibility_names)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
eb809d9
initial calibration commit
dhensle Jun 9, 2026
0876597
do not validate settings if file not found and not mandatory
dhensle Jun 9, 2026
6571db1
calibration settings, bespoke reports changes
americalexander Jul 2, 2026
6ccf84d
Fix global iterations
americalexander Jul 3, 2026
f2ea947
Loading from 2+ global iters
americalexander Jul 3, 2026
72758e6
Bugfix in plotting coefficient momentum
americalexander Jul 6, 2026
b065459
Add coef_delta to calibration records
americalexander Jul 6, 2026
3c135c7
Update graphs, handle zero modeled vals
americalexander Jul 6, 2026
b1db935
Batch output graphs
americalexander Jul 6, 2026
0b1f723
Minor plotting updates
americalexander Jul 6, 2026
96527ff
expose context to bespoke functions
dhensle Jul 7, 2026
d347f9c
output updates
dhensle Jul 7, 2026
9b02608
add component settings
americalexander Jul 7, 2026
4475471
Add calibration configs
americalexander Jul 7, 2026
1bf010c
passing context dict
dhensle Jul 7, 2026
dc731d0
updated specs for calibration
dhensle Jul 7, 2026
034545c
Add tour mode choice calibration coefficients
americalexander Jul 7, 2026
91bdb05
Merge branch 'calibration' of https://github.com/RSGInc/ActivitySim i…
americalexander Jul 7, 2026
fbf24d8
Update workplace location survey file directory
americalexander Jul 7, 2026
33ee993
auto ownership & tour mc relative paths
americalexander Jul 7, 2026
ff71c98
Allow no weights in tour mc summary
americalexander Jul 7, 2026
217b3e7
Remove duplicated checkpoint directories
americalexander Jul 13, 2026
aadd529
Minor bugfixes
americalexander Jul 14, 2026
67fd29f
Separate Claude attempt at MP calibration
americalexander Jul 14, 2026
6de9e48
MultiprocessStep creation fix
americalexander Jul 14, 2026
de894c2
Alleged checkpoint fix
americalexander Jul 14, 2026
3c78700
Deduplicate checkpoints
americalexander Jul 14, 2026
9856ee6
Prevent resume_after setting
americalexander Jul 14, 2026
fd7950a
More resume_after fixes
americalexander Jul 15, 2026
97a29d0
update existing table status
americalexander Jul 16, 2026
dbdff18
Checkpoint rename
americalexander Jul 16, 2026
d2dd7ae
Store last checkpoint
americalexander Jul 16, 2026
510b5d4
Blacken! It works!
americalexander Jul 16, 2026
0ee33d3
Set calibration coefficients to zero
americalexander Jul 16, 2026
fcfcb3c
Add override CI test coefficients for calibration
americalexander Jul 16, 2026
9d9f006
Add estimation mode regression test coeffs
americalexander Jul 16, 2026
afebfe3
More coefficient adds
americalexander Jul 16, 2026
4cda8e6
Add calibration documentation RST
americalexander Jul 17, 2026
15df92c
move calib documentation into users guide
dhensle Jul 21, 2026
4e38277
all converged fix and dead restart_after
dhensle Jul 21, 2026
391d9a1
blacken
dhensle Jul 21, 2026
eb4e6ba
Fix resume_after bug
americalexander Jul 21, 2026
de119ed
partial MP resume_after fix
americalexander Jul 22, 2026
f0e9ccb
More checkpointing work
americalexander Jul 29, 2026
c9afece
Add tour mode choice handler
americalexander Jul 29, 2026
ba8a142
Clean checkpointing
americalexander Jul 30, 2026
c7f3c77
More checkpointing fixes
americalexander Jul 30, 2026
c9aca45
Restore from subprocess checkpoints
americalexander Jul 30, 2026
a19638b
resume_after bugfix
americalexander Jul 31, 2026
2fd3db7
add subproc reuse
americalexander Aug 4, 2026
cc344c7
Modify breadcrumb logic
americalexander Aug 5, 2026
fcc6567
Add RNG channel reloading
americalexander Aug 11, 2026
a854b67
Make survey file optional
americalexander Aug 11, 2026
1b4eb8b
Add single-process channel restoration
americalexander Aug 11, 2026
7b66d4f
Update rng_channels
americalexander Aug 11, 2026
c0c937d
Modify single-process run approach
americalexander Aug 11, 2026
cf2c743
handle index_to_channel
americalexander Aug 11, 2026
ec6e269
Modify configured mode to use same path
americalexander Aug 11, 2026
c287efb
Remove memory sidecar process from calib
americalexander Aug 11, 2026
52ee5cd
Add reloading of tables in reregister_rng
americalexander Aug 11, 2026
af641ed
Maybe a different approach to checkpoint restoration
americalexander Aug 11, 2026
b38b329
Restore correct checkpoint
americalexander Aug 11, 2026
b452056
Fix add_table bug
americalexander Aug 12, 2026
d169bd4
Different approach to checkpoint/channel restoration
americalexander Aug 12, 2026
2eb889d
Add table invalidation
americalexander Aug 12, 2026
67b87a4
Additional precursor guard
americalexander Aug 12, 2026
8a19f6c
Blacken
americalexander Aug 12, 2026
21a0d78
Fix calibrate_component precursor calls
americalexander Aug 12, 2026
0437b70
Add temporary diagnostic tracing
americalexander Aug 12, 2026
2f58828
More diagnostic logging
americalexander Aug 12, 2026
6563525
Change to default tables_to_invalidate
americalexander Aug 12, 2026
210454e
Change when table invalidation occurs
americalexander Aug 12, 2026
e8f6a33
Fix SP checkpointing
americalexander Aug 12, 2026
fb923bb
fixing resume after and rng calib invariance
dhensle Aug 14, 2026
92bbc84
fixing pipeline and rng restarting issues
dhensle Aug 14, 2026
af1e21b
fixing more pipeline restart issues
dhensle Aug 17, 2026
022b4fb
appropriately handling crashing in middle of iteration
dhensle Aug 19, 2026
d79517e
refactor into separate files
dhensle Aug 19, 2026
ce7844f
if everything is converged, will continue to run downstream models
dhensle Aug 19, 2026
e9dcffb
blacken
dhensle Aug 19, 2026
adf17f9
remove coefficient rollback functionality
dhensle Aug 19, 2026
66e1037
better handling of internal memory handling between iterations
dhensle Aug 20, 2026
1f55863
run modes and resume after tests
dhensle Aug 20, 2026
97fbf16
nailing down resume_after and iteration counting
dhensle Aug 25, 2026
910a0e0
blacken
dhensle Aug 25, 2026
2ef9828
removing unused calibration.yaml settings
dhensle Aug 25, 2026
cd8f8b5
clarifying resume_after readme
dhensle Aug 25, 2026
896fc06
aligning tests and language around restart functionality
dhensle Aug 26, 2026
f414f23
setting checking cleanup
dhensle Aug 26, 2026
e2ee5b5
Merge remote-tracking branch 'upstream/main' into calibration
dhensle Aug 26, 2026
6c59ee2
addressing RNG and resume review comments
dhensle Aug 26, 2026
0b953b2
clearer logging at max iterations
dhensle Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions activitysim/abm/models/initialize.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,12 +154,19 @@ def initialize_households(
households = state.get_dataframe("households")
assert not households._is_view
chunk_sizer.log_df(trace_label, "households", households)
del households
chunk_sizer.log_df(trace_label, "households", None)

persons = state.get_dataframe("persons")
assert not persons._is_view
chunk_sizer.log_df(trace_label, "persons", persons)

rng = state.get_rn_generator()
if "households" not in rng.channels:
rng.add_channel("households", households)
if "persons" not in rng.channels:
rng.add_channel("persons", persons)

del households
chunk_sizer.log_df(trace_label, "households", None)
del persons
chunk_sizer.log_df(trace_label, "persons", None)

Expand Down
28 changes: 24 additions & 4 deletions activitysim/abm/models/settings_checker.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,24 +4,26 @@
from pydantic import BaseModel as PydanticBase
from typing import Type, Optional

from activitysim.core import config
from activitysim.core.calibration.settings import (
CALIBRATION_SETTINGS_FILE_NAME,
CalibrationConfig,
)
from activitysim.core.configuration.base import PydanticReadable

# import core settings
from activitysim.core.configuration.logit import (
LogitNestSpec,
TourLocationComponentSettings,
TourModeComponentSettings,
TemplatedLogitComponentSettings,
)
from activitysim.core import config
from activitysim.core.configuration.network import NetworkSettings
from activitysim.core.exceptions import ModelConfigurationError
from activitysim.core.workflow import State
from activitysim.core.simulate import (
eval_coefficients,
eval_nest_coefficients,
read_model_coefficient_template,
)
from activitysim.core.exceptions import ModelConfigurationError

# import model settings
from activitysim.abm.models.accessibility import AccessibilitySettings
Expand DownExpand Up@@ -637,6 +639,24 @@ def check_model_settings(
# Collect all errors
all_errors = []

# calibration.yaml is optional and is not itself a model step, so validate
# it explicitly using the same Pydantic/error-aggregation path as model
# settings files.
try:
CalibrationConfig.read_settings_file(
state.filesystem,
CALIBRATION_SETTINGS_FILE_NAME,
mandatory=False,
)
except Exception as error:
all_errors.append(
SettingsCheckerError(
"calibration",
error,
CALIBRATION_SETTINGS_FILE_NAME,
)
)

# additional logging set up
formatter = logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
Expand Down
60 changes: 57 additions & 3 deletions activitysim/cli/run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@

import numpy as np

from activitysim.core import chunk, config, mem, timing, tracing, workflow
from activitysim.core import calibration, chunk, config, mem, timing, tracing, workflow
from activitysim.core.configuration import FileSystem, Settings
from activitysim.core.run_id import RunId

Expand DownExpand Up@@ -336,8 +336,13 @@ def run(args):
resume_after = state.settings.resume_after

# cleanup if not resuming
if not resume_after:
preserve_calibration_outputs = calibration.calibration_run_should_preserve_outputs(
state
)
if not resume_after and not preserve_calibration_outputs:
cleanup_output_files(state)
elif preserve_calibration_outputs:
logger.info("preserving output files based on calibration restart preflight")
elif state.settings.cleanup_trace_files_on_resume:
tracing.delete_trace_files(state)

Expand DownExpand Up@@ -418,7 +423,56 @@ def run(args):
check_model_settings(state, extension_settings=extension_checker_settings)

try:
if state.settings.multiprocess:
if calibration.calibration_enabled(state):
logger.info("evaluate calibration workflow")

calibration_result = calibration.run_calibration_loop(
state=state,
models=state.settings.models,
)

if calibration_result.model_system_ran:
logger.info(
"calibration workflow complete converged=%s "
"completed_global_iterations=%s configured_global_iterations=%s",
calibration_result.converged,
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)
else:
if (
calibration_result.completed_global_iterations
>= calibration_result.configured_global_iterations
):
logger.info(
"calibration workflow skipped: no model system steps were run "
"because completed_global_iterations=%s has reached the "
"configured run.global_iterations=%s limit. Increase "
"run.global_iterations to request additional iterations, or "
"remove output/calibration/calibration_progress.json to start a fresh run.",
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)
else:
logger.info(
"calibration workflow skipped: no model system steps were run "
"because the prior calibration run is already complete "
"(completed_global_iterations=%s, run.global_iterations=%s). "
"Change run.global_iterations to a new value above the completed "
"count to request additional iterations, or remove "
"output/calibration/calibration_progress.json to start a fresh run.",
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)

if state.settings.cleanup_pipeline_after_run:
state.checkpoint.cleanup()
else:
state.checkpoint.close_store()

mem.log_global_hwm()

elif state.settings.multiprocess:
logger.info("run multiprocess simulation")

from activitysim.core import mp_tasks
Expand Down
122 changes: 122 additions & 0 deletions activitysim/core/calibration/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
# ActivitySim calibration package

This package implements ActivitySim's automated coefficient calibration workflow.
The modules are divided by responsibility so orchestration, model execution, and
calibration math can evolve independently.

- `__init__.py` defines the public API and compatibility access to former private
attributes from `activitysim.core.calibration`.
- `settings.py` defines calibration configuration models, result types, and
configuration loading.
- `orchestrator.py` coordinates global iterations and model-component sequencing.
- `component.py` runs component iterations, validates calibration specifications,
and calculates coefficient updates.
- `expressions.py` builds expression contexts, evaluates target/model expressions,
and loads optional helper modules.
- `coefficients.py` locates, reads, and writes model coefficient files.
- `reporting.py` writes iteration histories, summaries, plots, and final snapshots.
- `recovery.py` durably tracks calibration progress. Coefficient files are the
authoritative current state and are never rolled back when a run resumes.
- `execution.py` restores pipeline state and dispatches model execution.
- `multiprocess.py` contains subprocess orchestration, shared-resource setup, and
multiprocess pipeline restoration.

Component settings may specify `model_settings_file` when a workflow step does
not follow the conventional `<component_name>.yaml` naming pattern. Components
that share model settings may point to the same coefficient file.

## Global iterations, recovery attempts, and `resume_after`

`calibration.yaml` `run.global_iterations` is the desired total number of
completed logical calibration iterations, not the number to execute on each
ActivitySim invocation. The run-control contract is:

1. An unchanged setting on a completed run is a no-op, detected before normal
output cleanup so the pipeline and final outputs are preserved.
2. If a completed run's setting is changed to a value greater than the number
actually completed, calibration continues until the new total. This includes
lowering a previous maximum after early convergence, such as changing 5 to 3
after convergence completed iteration 2.
3. A changed setting less than or equal to the number already completed is a
no-op; completed coefficient updates are never undone implicitly.
4. Top-level `settings.yaml` `resume_after` has normal ActivitySim semantics for
the first global iteration entered by the current invocation. Later global
iterations ignore it and execute every calibrated component.
5. A global iteration counts only if it has at least one durable calibrated
component result, either from the current attempt or an earlier attempt of
that same logical iteration.
6. `global_iterations` cannot be lowered below an interrupted iteration because
its coefficient files may already contain updates from that iteration. The
run stops with instructions to resume the iteration or deliberately reset
progress and coefficients.
7. Startup logs report the detected completed count, requested target, selected
action, starting iteration and attempt, and `resume_after` value.

Calibration records the state needed to apply this contract in
`output/calibration/calibration_progress.json`.

Recovery attempts are distinct from global iterations. The first execution of a
global iteration is attempt 1. Restarting an interrupted global iteration creates
attempt 2, then attempt 3 if another restart is needed. A recovery attempt does
not consume an additional global iteration.

Calibration uses only the top-level `settings.yaml` `resume_after` setting. On
the first global iteration executed by an ActivitySim invocation, including a
new attempt of an interrupted iteration, it behaves like `resume_after` in a
non-calibration run:

- when set to a model name, that model is treated as complete, its checkpoint is
restored, and execution begins with the following model; and
- when unset or `null`, execution starts at the beginning of the top-level
`models` list. Calibration does not automatically continue after the last
model completed by the preceding attempt.

A named value must occur in the top-level `models` list and must identify a
model-level checkpoint. Calibration does not define a special `initialize`
value. A value such as `initialize_landuse` has ordinary model-name semantics:
that model is skipped and execution begins with the next model. The `_` shorthand
for the last checkpoint is not accepted in calibration mode.

`resume_after` affects only the first global iteration entered by the current
invocation. Calibrated models at or before a named resume point are skipped in
that iteration. Any later global iterations in the same invocation ignore
`resume_after`, restart immediately before the first calibrated model, and run
the normal complete calibration sequence.

For example, suppose global iteration 3 attempt 1 was interrupted after
calibrated `model_a` completed:

- `resume_after: model_a` starts attempt 2 after model A, preserving its
attempt-1 result; and
- `resume_after: null` starts attempt 2 at the beginning of the complete model
list, so model A runs again.

Coefficient files are the authoritative current state and are never rolled back
during recovery. Updates written before the interruption, along with subsequent
manual edits, become the starting coefficients for the new attempt. Rewinding
across a completed calibrated model therefore applies another calibration update
to its current coefficients. Calibration logs a warning when `resume_after`
causes this kind of rewind.

Iteration histories are append-only across attempts. Standard record, summary,
and generic-report rows include `global_iter`, `attempt`, and `component_iter`,
so a rerun does not replace the coefficient transition written by an earlier
attempt. The coefficient trajectory plots show the complete sequence, including
the initial value and every subsequent update. X-axis labels use the compact
form `G<global>-A<attempt>-C<component>`, for example `G3-A2-C1`.

After each calibrated component completes, the progress file records its attempt,
component-iteration count, and convergence result. A restart that skips that
component retains this state. This allows a run that crashed in downstream,
non-calibrated models to finish the same terminal global iteration without losing
the calibrated components' convergence decision.

Calibration is marked complete only after the terminal global iteration has run
all remaining models in the top-level `models` list and the final coefficient
snapshot has been written. A crash after the last calibrated model but before the
last production model therefore leaves the global iteration in progress.

Dependencies should flow from `orchestrator.py` into the focused modules. Lower
level modules should not import the orchestrator. Multiprocess code imports back
into `execution.py` only inside the single-component runner to avoid an import-time
cycle.
71 changes: 71 additions & 0 deletions activitysim/core/calibration/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
# ActivitySim
# See full license in LICENSE.txt.
"""Automated calibration support for ActivitySim model runs.

The public API is intentionally small. Private attributes from the former
single-file module remain available through ``__getattr__`` for compatibility
while callers migrate to the package modules.
"""

from . import (
coefficients,
component,
execution,
expressions,
multiprocess,
recovery,
reporting,
settings,
)
from .orchestrator import calibration_run_should_preserve_outputs, run_calibration_loop
from .settings import (
CalibrationComponentResult,
CalibrationComponentSettings,
CalibrationConfig,
CalibrationReportsSettings,
CalibrationRunResult,
CalibrationRunSettings,
calibration_enabled,
read_calibration_settings,
)

__all__ = [
"CalibrationComponentResult",
"CalibrationComponentSettings",
"CalibrationConfig",
"CalibrationReportsSettings",
"CalibrationRunResult",
"CalibrationRunSettings",
"calibration_run_should_preserve_outputs",
"calibration_enabled",
"read_calibration_settings",
"run_calibration_loop",
]

_COMPATIBILITY_MODULES = (
settings,
component,
expressions,
coefficients,
reporting,
recovery,
execution,
multiprocess,
)


def __getattr__(name):
for module in _COMPATIBILITY_MODULES:
if hasattr(module, name):
return getattr(module, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def __dir__():
compatibility_names = {
name
for module in _COMPATIBILITY_MODULES
for name in vars(module)
if not name.startswith("__")
}
return sorted(set(globals()) | compatibility_names)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
eb809d9
initial calibration commit
dhensle Jun 9, 2026
0876597
do not validate settings if file not found and not mandatory
dhensle Jun 9, 2026
6571db1
calibration settings, bespoke reports changes
americalexander Jul 2, 2026
6ccf84d
Fix global iterations
americalexander Jul 3, 2026
f2ea947
Loading from 2+ global iters
americalexander Jul 3, 2026
72758e6
Bugfix in plotting coefficient momentum
americalexander Jul 6, 2026
b065459
Add coef_delta to calibration records
americalexander Jul 6, 2026
3c135c7
Update graphs, handle zero modeled vals
americalexander Jul 6, 2026
b1db935
Batch output graphs
americalexander Jul 6, 2026
0b1f723
Minor plotting updates
americalexander Jul 6, 2026
96527ff
expose context to bespoke functions
dhensle Jul 7, 2026
d347f9c
output updates
dhensle Jul 7, 2026
9b02608
add component settings
americalexander Jul 7, 2026
4475471
Add calibration configs
americalexander Jul 7, 2026
1bf010c
passing context dict
dhensle Jul 7, 2026
dc731d0
updated specs for calibration
dhensle Jul 7, 2026
034545c
Add tour mode choice calibration coefficients
americalexander Jul 7, 2026
91bdb05
Merge branch 'calibration' of https://github.com/RSGInc/ActivitySim i…
americalexander Jul 7, 2026
fbf24d8
Update workplace location survey file directory
americalexander Jul 7, 2026
33ee993
auto ownership & tour mc relative paths
americalexander Jul 7, 2026
ff71c98
Allow no weights in tour mc summary
americalexander Jul 7, 2026
217b3e7
Remove duplicated checkpoint directories
americalexander Jul 13, 2026
aadd529
Minor bugfixes
americalexander Jul 14, 2026
67fd29f
Separate Claude attempt at MP calibration
americalexander Jul 14, 2026
6de9e48
MultiprocessStep creation fix
americalexander Jul 14, 2026
de894c2
Alleged checkpoint fix
americalexander Jul 14, 2026
3c78700
Deduplicate checkpoints
americalexander Jul 14, 2026
9856ee6
Prevent resume_after setting
americalexander Jul 14, 2026
fd7950a
More resume_after fixes
americalexander Jul 15, 2026
97a29d0
update existing table status
americalexander Jul 16, 2026
dbdff18
Checkpoint rename
americalexander Jul 16, 2026
d2dd7ae
Store last checkpoint
americalexander Jul 16, 2026
510b5d4
Blacken! It works!
americalexander Jul 16, 2026
0ee33d3
Set calibration coefficients to zero
americalexander Jul 16, 2026
fcfcb3c
Add override CI test coefficients for calibration
americalexander Jul 16, 2026
9d9f006
Add estimation mode regression test coeffs
americalexander Jul 16, 2026
afebfe3
More coefficient adds
americalexander Jul 16, 2026
4cda8e6
Add calibration documentation RST
americalexander Jul 17, 2026
15df92c
move calib documentation into users guide
dhensle Jul 21, 2026
4e38277
all converged fix and dead restart_after
dhensle Jul 21, 2026
391d9a1
blacken
dhensle Jul 21, 2026
eb4e6ba
Fix resume_after bug
americalexander Jul 21, 2026
de119ed
partial MP resume_after fix
americalexander Jul 22, 2026
f0e9ccb
More checkpointing work
americalexander Jul 29, 2026
c9afece
Add tour mode choice handler
americalexander Jul 29, 2026
ba8a142
Clean checkpointing
americalexander Jul 30, 2026
c7f3c77
More checkpointing fixes
americalexander Jul 30, 2026
c9aca45
Restore from subprocess checkpoints
americalexander Jul 30, 2026
a19638b
resume_after bugfix
americalexander Jul 31, 2026
2fd3db7
add subproc reuse
americalexander Aug 4, 2026
cc344c7
Modify breadcrumb logic
americalexander Aug 5, 2026
fcc6567
Add RNG channel reloading
americalexander Aug 11, 2026
a854b67
Make survey file optional
americalexander Aug 11, 2026
1b4eb8b
Add single-process channel restoration
americalexander Aug 11, 2026
7b66d4f
Update rng_channels
americalexander Aug 11, 2026
c0c937d
Modify single-process run approach
americalexander Aug 11, 2026
cf2c743
handle index_to_channel
americalexander Aug 11, 2026
ec6e269
Modify configured mode to use same path
americalexander Aug 11, 2026
c287efb
Remove memory sidecar process from calib
americalexander Aug 11, 2026
52ee5cd
Add reloading of tables in reregister_rng
americalexander Aug 11, 2026
af641ed
Maybe a different approach to checkpoint restoration
americalexander Aug 11, 2026
b38b329
Restore correct checkpoint
americalexander Aug 11, 2026
b452056
Fix add_table bug
americalexander Aug 12, 2026
d169bd4
Different approach to checkpoint/channel restoration
americalexander Aug 12, 2026
2eb889d
Add table invalidation
americalexander Aug 12, 2026
67b87a4
Additional precursor guard
americalexander Aug 12, 2026
8a19f6c
Blacken
americalexander Aug 12, 2026
21a0d78
Fix calibrate_component precursor calls
americalexander Aug 12, 2026
0437b70
Add temporary diagnostic tracing
americalexander Aug 12, 2026
2f58828
More diagnostic logging
americalexander Aug 12, 2026
6563525
Change to default tables_to_invalidate
americalexander Aug 12, 2026
210454e
Change when table invalidation occurs
americalexander Aug 12, 2026
e8f6a33
Fix SP checkpointing
americalexander Aug 12, 2026
fb923bb
fixing resume after and rng calib invariance
dhensle Aug 14, 2026
92bbc84
fixing pipeline and rng restarting issues
dhensle Aug 14, 2026
af1e21b
fixing more pipeline restart issues
dhensle Aug 17, 2026
022b4fb
appropriately handling crashing in middle of iteration
dhensle Aug 19, 2026
d79517e
refactor into separate files
dhensle Aug 19, 2026
ce7844f
if everything is converged, will continue to run downstream models
dhensle Aug 19, 2026
e9dcffb
blacken
dhensle Aug 19, 2026
adf17f9
remove coefficient rollback functionality
dhensle Aug 19, 2026
66e1037
better handling of internal memory handling between iterations
dhensle Aug 20, 2026
1f55863
run modes and resume after tests
dhensle Aug 20, 2026
97fbf16
nailing down resume_after and iteration counting
dhensle Aug 25, 2026
910a0e0
blacken
dhensle Aug 25, 2026
2ef9828
removing unused calibration.yaml settings
dhensle Aug 25, 2026
cd8f8b5
clarifying resume_after readme
dhensle Aug 25, 2026
896fc06
aligning tests and language around restart functionality
dhensle Aug 26, 2026
f414f23
setting checking cleanup
dhensle Aug 26, 2026
e2ee5b5
Merge remote-tracking branch 'upstream/main' into calibration
dhensle Aug 26, 2026
6c59ee2
addressing RNG and resume review comments
dhensle Aug 26, 2026
0b953b2
clearer logging at max iterations
dhensle Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions activitysim/abm/models/initialize.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,12 +154,19 @@ def initialize_households(
households = state.get_dataframe("households")
assert not households._is_view
chunk_sizer.log_df(trace_label, "households", households)
del households
chunk_sizer.log_df(trace_label, "households", None)

persons = state.get_dataframe("persons")
assert not persons._is_view
chunk_sizer.log_df(trace_label, "persons", persons)

rng = state.get_rn_generator()
if "households" not in rng.channels:
rng.add_channel("households", households)
if "persons" not in rng.channels:
rng.add_channel("persons", persons)

del households
chunk_sizer.log_df(trace_label, "households", None)
del persons
chunk_sizer.log_df(trace_label, "persons", None)

Expand Down
28 changes: 24 additions & 4 deletions activitysim/abm/models/settings_checker.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,24 +4,26 @@
from pydantic import BaseModel as PydanticBase
from typing import Type, Optional

from activitysim.core import config
from activitysim.core.calibration.settings import (
CALIBRATION_SETTINGS_FILE_NAME,
CalibrationConfig,
)
from activitysim.core.configuration.base import PydanticReadable

# import core settings
from activitysim.core.configuration.logit import (
LogitNestSpec,
TourLocationComponentSettings,
TourModeComponentSettings,
TemplatedLogitComponentSettings,
)
from activitysim.core import config
from activitysim.core.configuration.network import NetworkSettings
from activitysim.core.exceptions import ModelConfigurationError
from activitysim.core.workflow import State
from activitysim.core.simulate import (
eval_coefficients,
eval_nest_coefficients,
read_model_coefficient_template,
)
from activitysim.core.exceptions import ModelConfigurationError

# import model settings
from activitysim.abm.models.accessibility import AccessibilitySettings
Expand DownExpand Up@@ -637,6 +639,24 @@ def check_model_settings(
# Collect all errors
all_errors = []

# calibration.yaml is optional and is not itself a model step, so validate
# it explicitly using the same Pydantic/error-aggregation path as model
# settings files.
try:
CalibrationConfig.read_settings_file(
state.filesystem,
CALIBRATION_SETTINGS_FILE_NAME,
mandatory=False,
)
except Exception as error:
all_errors.append(
SettingsCheckerError(
"calibration",
error,
CALIBRATION_SETTINGS_FILE_NAME,
)
)

# additional logging set up
formatter = logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
Expand Down
60 changes: 57 additions & 3 deletions activitysim/cli/run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@

import numpy as np

from activitysim.core import chunk, config, mem, timing, tracing, workflow
from activitysim.core import calibration, chunk, config, mem, timing, tracing, workflow
from activitysim.core.configuration import FileSystem, Settings
from activitysim.core.run_id import RunId

Expand DownExpand Up@@ -336,8 +336,13 @@ def run(args):
resume_after = state.settings.resume_after

# cleanup if not resuming
if not resume_after:
preserve_calibration_outputs = calibration.calibration_run_should_preserve_outputs(
state
)
if not resume_after and not preserve_calibration_outputs:
cleanup_output_files(state)
elif preserve_calibration_outputs:
logger.info("preserving output files based on calibration restart preflight")
elif state.settings.cleanup_trace_files_on_resume:
tracing.delete_trace_files(state)

Expand DownExpand Up@@ -418,7 +423,56 @@ def run(args):
check_model_settings(state, extension_settings=extension_checker_settings)

try:
if state.settings.multiprocess:
if calibration.calibration_enabled(state):
logger.info("evaluate calibration workflow")

calibration_result = calibration.run_calibration_loop(
state=state,
models=state.settings.models,
)

if calibration_result.model_system_ran:
logger.info(
"calibration workflow complete converged=%s "
"completed_global_iterations=%s configured_global_iterations=%s",
calibration_result.converged,
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)
else:
if (
calibration_result.completed_global_iterations
>= calibration_result.configured_global_iterations
):
logger.info(
"calibration workflow skipped: no model system steps were run "
"because completed_global_iterations=%s has reached the "
"configured run.global_iterations=%s limit. Increase "
"run.global_iterations to request additional iterations, or "
"remove output/calibration/calibration_progress.json to start a fresh run.",
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)
else:
logger.info(
"calibration workflow skipped: no model system steps were run "
"because the prior calibration run is already complete "
"(completed_global_iterations=%s, run.global_iterations=%s). "
"Change run.global_iterations to a new value above the completed "
"count to request additional iterations, or remove "
"output/calibration/calibration_progress.json to start a fresh run.",
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)

if state.settings.cleanup_pipeline_after_run:
state.checkpoint.cleanup()
else:
state.checkpoint.close_store()

mem.log_global_hwm()

elif state.settings.multiprocess:
logger.info("run multiprocess simulation")

from activitysim.core import mp_tasks
Expand Down
122 changes: 122 additions & 0 deletions activitysim/core/calibration/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
# ActivitySim calibration package

This package implements ActivitySim's automated coefficient calibration workflow.
The modules are divided by responsibility so orchestration, model execution, and
calibration math can evolve independently.

- `__init__.py` defines the public API and compatibility access to former private
attributes from `activitysim.core.calibration`.
- `settings.py` defines calibration configuration models, result types, and
configuration loading.
- `orchestrator.py` coordinates global iterations and model-component sequencing.
- `component.py` runs component iterations, validates calibration specifications,
and calculates coefficient updates.
- `expressions.py` builds expression contexts, evaluates target/model expressions,
and loads optional helper modules.
- `coefficients.py` locates, reads, and writes model coefficient files.
- `reporting.py` writes iteration histories, summaries, plots, and final snapshots.
- `recovery.py` durably tracks calibration progress. Coefficient files are the
authoritative current state and are never rolled back when a run resumes.
- `execution.py` restores pipeline state and dispatches model execution.
- `multiprocess.py` contains subprocess orchestration, shared-resource setup, and
multiprocess pipeline restoration.

Component settings may specify `model_settings_file` when a workflow step does
not follow the conventional `<component_name>.yaml` naming pattern. Components
that share model settings may point to the same coefficient file.

## Global iterations, recovery attempts, and `resume_after`

`calibration.yaml` `run.global_iterations` is the desired total number of
completed logical calibration iterations, not the number to execute on each
ActivitySim invocation. The run-control contract is:

1. An unchanged setting on a completed run is a no-op, detected before normal
output cleanup so the pipeline and final outputs are preserved.
2. If a completed run's setting is changed to a value greater than the number
actually completed, calibration continues until the new total. This includes
lowering a previous maximum after early convergence, such as changing 5 to 3
after convergence completed iteration 2.
3. A changed setting less than or equal to the number already completed is a
no-op; completed coefficient updates are never undone implicitly.
4. Top-level `settings.yaml` `resume_after` has normal ActivitySim semantics for
the first global iteration entered by the current invocation. Later global
iterations ignore it and execute every calibrated component.
5. A global iteration counts only if it has at least one durable calibrated
component result, either from the current attempt or an earlier attempt of
that same logical iteration.
6. `global_iterations` cannot be lowered below an interrupted iteration because
its coefficient files may already contain updates from that iteration. The
run stops with instructions to resume the iteration or deliberately reset
progress and coefficients.
7. Startup logs report the detected completed count, requested target, selected
action, starting iteration and attempt, and `resume_after` value.

Calibration records the state needed to apply this contract in
`output/calibration/calibration_progress.json`.

Recovery attempts are distinct from global iterations. The first execution of a
global iteration is attempt 1. Restarting an interrupted global iteration creates
attempt 2, then attempt 3 if another restart is needed. A recovery attempt does
not consume an additional global iteration.

Calibration uses only the top-level `settings.yaml` `resume_after` setting. On
the first global iteration executed by an ActivitySim invocation, including a
new attempt of an interrupted iteration, it behaves like `resume_after` in a
non-calibration run:

- when set to a model name, that model is treated as complete, its checkpoint is
restored, and execution begins with the following model; and
- when unset or `null`, execution starts at the beginning of the top-level
`models` list. Calibration does not automatically continue after the last
model completed by the preceding attempt.

A named value must occur in the top-level `models` list and must identify a
model-level checkpoint. Calibration does not define a special `initialize`
value. A value such as `initialize_landuse` has ordinary model-name semantics:
that model is skipped and execution begins with the next model. The `_` shorthand
for the last checkpoint is not accepted in calibration mode.

`resume_after` affects only the first global iteration entered by the current
invocation. Calibrated models at or before a named resume point are skipped in
that iteration. Any later global iterations in the same invocation ignore
`resume_after`, restart immediately before the first calibrated model, and run
the normal complete calibration sequence.

For example, suppose global iteration 3 attempt 1 was interrupted after
calibrated `model_a` completed:

- `resume_after: model_a` starts attempt 2 after model A, preserving its
attempt-1 result; and
- `resume_after: null` starts attempt 2 at the beginning of the complete model
list, so model A runs again.

Coefficient files are the authoritative current state and are never rolled back
during recovery. Updates written before the interruption, along with subsequent
manual edits, become the starting coefficients for the new attempt. Rewinding
across a completed calibrated model therefore applies another calibration update
to its current coefficients. Calibration logs a warning when `resume_after`
causes this kind of rewind.

Iteration histories are append-only across attempts. Standard record, summary,
and generic-report rows include `global_iter`, `attempt`, and `component_iter`,
so a rerun does not replace the coefficient transition written by an earlier
attempt. The coefficient trajectory plots show the complete sequence, including
the initial value and every subsequent update. X-axis labels use the compact
form `G<global>-A<attempt>-C<component>`, for example `G3-A2-C1`.

After each calibrated component completes, the progress file records its attempt,
component-iteration count, and convergence result. A restart that skips that
component retains this state. This allows a run that crashed in downstream,
non-calibrated models to finish the same terminal global iteration without losing
the calibrated components' convergence decision.

Calibration is marked complete only after the terminal global iteration has run
all remaining models in the top-level `models` list and the final coefficient
snapshot has been written. A crash after the last calibrated model but before the
last production model therefore leaves the global iteration in progress.

Dependencies should flow from `orchestrator.py` into the focused modules. Lower
level modules should not import the orchestrator. Multiprocess code imports back
into `execution.py` only inside the single-component runner to avoid an import-time
cycle.
71 changes: 71 additions & 0 deletions activitysim/core/calibration/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
# ActivitySim
# See full license in LICENSE.txt.
"""Automated calibration support for ActivitySim model runs.

The public API is intentionally small. Private attributes from the former
single-file module remain available through ``__getattr__`` for compatibility
while callers migrate to the package modules.
"""

from . import (
coefficients,
component,
execution,
expressions,
multiprocess,
recovery,
reporting,
settings,
)
from .orchestrator import calibration_run_should_preserve_outputs, run_calibration_loop
from .settings import (
CalibrationComponentResult,
CalibrationComponentSettings,
CalibrationConfig,
CalibrationReportsSettings,
CalibrationRunResult,
CalibrationRunSettings,
calibration_enabled,
read_calibration_settings,
)

__all__ = [
"CalibrationComponentResult",
"CalibrationComponentSettings",
"CalibrationConfig",
"CalibrationReportsSettings",
"CalibrationRunResult",
"CalibrationRunSettings",
"calibration_run_should_preserve_outputs",
"calibration_enabled",
"read_calibration_settings",
"run_calibration_loop",
]

_COMPATIBILITY_MODULES = (
settings,
component,
expressions,
coefficients,
reporting,
recovery,
execution,
multiprocess,
)


def __getattr__(name):
for module in _COMPATIBILITY_MODULES:
if hasattr(module, name):
return getattr(module, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def __dir__():
compatibility_names = {
name
for module in _COMPATIBILITY_MODULES
for name in vars(module)
if not name.startswith("__")
}
return sorted(set(globals()) | compatibility_names)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
eb809d9
initial calibration commit
dhensle Jun 9, 2026
0876597
do not validate settings if file not found and not mandatory
dhensle Jun 9, 2026
6571db1
calibration settings, bespoke reports changes
americalexander Jul 2, 2026
6ccf84d
Fix global iterations
americalexander Jul 3, 2026
f2ea947
Loading from 2+ global iters
americalexander Jul 3, 2026
72758e6
Bugfix in plotting coefficient momentum
americalexander Jul 6, 2026
b065459
Add coef_delta to calibration records
americalexander Jul 6, 2026
3c135c7
Update graphs, handle zero modeled vals
americalexander Jul 6, 2026
b1db935
Batch output graphs
americalexander Jul 6, 2026
0b1f723
Minor plotting updates
americalexander Jul 6, 2026
96527ff
expose context to bespoke functions
dhensle Jul 7, 2026
d347f9c
output updates
dhensle Jul 7, 2026
9b02608
add component settings
americalexander Jul 7, 2026
4475471
Add calibration configs
americalexander Jul 7, 2026
1bf010c
passing context dict
dhensle Jul 7, 2026
dc731d0
updated specs for calibration
dhensle Jul 7, 2026
034545c
Add tour mode choice calibration coefficients
americalexander Jul 7, 2026
91bdb05
Merge branch 'calibration' of https://github.com/RSGInc/ActivitySim i…
americalexander Jul 7, 2026
fbf24d8
Update workplace location survey file directory
americalexander Jul 7, 2026
33ee993
auto ownership & tour mc relative paths
americalexander Jul 7, 2026
ff71c98
Allow no weights in tour mc summary
americalexander Jul 7, 2026
217b3e7
Remove duplicated checkpoint directories
americalexander Jul 13, 2026
aadd529
Minor bugfixes
americalexander Jul 14, 2026
67fd29f
Separate Claude attempt at MP calibration
americalexander Jul 14, 2026
6de9e48
MultiprocessStep creation fix
americalexander Jul 14, 2026
de894c2
Alleged checkpoint fix
americalexander Jul 14, 2026
3c78700
Deduplicate checkpoints
americalexander Jul 14, 2026
9856ee6
Prevent resume_after setting
americalexander Jul 14, 2026
fd7950a
More resume_after fixes
americalexander Jul 15, 2026
97a29d0
update existing table status
americalexander Jul 16, 2026
dbdff18
Checkpoint rename
americalexander Jul 16, 2026
d2dd7ae
Store last checkpoint
americalexander Jul 16, 2026
510b5d4
Blacken! It works!
americalexander Jul 16, 2026
0ee33d3
Set calibration coefficients to zero
americalexander Jul 16, 2026
fcfcb3c
Add override CI test coefficients for calibration
americalexander Jul 16, 2026
9d9f006
Add estimation mode regression test coeffs
americalexander Jul 16, 2026
afebfe3
More coefficient adds
americalexander Jul 16, 2026
4cda8e6
Add calibration documentation RST
americalexander Jul 17, 2026
15df92c
move calib documentation into users guide
dhensle Jul 21, 2026
4e38277
all converged fix and dead restart_after
dhensle Jul 21, 2026
391d9a1
blacken
dhensle Jul 21, 2026
eb4e6ba
Fix resume_after bug
americalexander Jul 21, 2026
de119ed
partial MP resume_after fix
americalexander Jul 22, 2026
f0e9ccb
More checkpointing work
americalexander Jul 29, 2026
c9afece
Add tour mode choice handler
americalexander Jul 29, 2026
ba8a142
Clean checkpointing
americalexander Jul 30, 2026
c7f3c77
More checkpointing fixes
americalexander Jul 30, 2026
c9aca45
Restore from subprocess checkpoints
americalexander Jul 30, 2026
a19638b
resume_after bugfix
americalexander Jul 31, 2026
2fd3db7
add subproc reuse
americalexander Aug 4, 2026
cc344c7
Modify breadcrumb logic
americalexander Aug 5, 2026
fcc6567
Add RNG channel reloading
americalexander Aug 11, 2026
a854b67
Make survey file optional
americalexander Aug 11, 2026
1b4eb8b
Add single-process channel restoration
americalexander Aug 11, 2026
7b66d4f
Update rng_channels
americalexander Aug 11, 2026
c0c937d
Modify single-process run approach
americalexander Aug 11, 2026
cf2c743
handle index_to_channel
americalexander Aug 11, 2026
ec6e269
Modify configured mode to use same path
americalexander Aug 11, 2026
c287efb
Remove memory sidecar process from calib
americalexander Aug 11, 2026
52ee5cd
Add reloading of tables in reregister_rng
americalexander Aug 11, 2026
af641ed
Maybe a different approach to checkpoint restoration
americalexander Aug 11, 2026
b38b329
Restore correct checkpoint
americalexander Aug 11, 2026
b452056
Fix add_table bug
americalexander Aug 12, 2026
d169bd4
Different approach to checkpoint/channel restoration
americalexander Aug 12, 2026
2eb889d
Add table invalidation
americalexander Aug 12, 2026
67b87a4
Additional precursor guard
americalexander Aug 12, 2026
8a19f6c
Blacken
americalexander Aug 12, 2026
21a0d78
Fix calibrate_component precursor calls
americalexander Aug 12, 2026
0437b70
Add temporary diagnostic tracing
americalexander Aug 12, 2026
2f58828
More diagnostic logging
americalexander Aug 12, 2026
6563525
Change to default tables_to_invalidate
americalexander Aug 12, 2026
210454e
Change when table invalidation occurs
americalexander Aug 12, 2026
e8f6a33
Fix SP checkpointing
americalexander Aug 12, 2026
fb923bb
fixing resume after and rng calib invariance
dhensle Aug 14, 2026
92bbc84
fixing pipeline and rng restarting issues
dhensle Aug 14, 2026
af1e21b
fixing more pipeline restart issues
dhensle Aug 17, 2026
022b4fb
appropriately handling crashing in middle of iteration
dhensle Aug 19, 2026
d79517e
refactor into separate files
dhensle Aug 19, 2026
ce7844f
if everything is converged, will continue to run downstream models
dhensle Aug 19, 2026
e9dcffb
blacken
dhensle Aug 19, 2026
adf17f9
remove coefficient rollback functionality
dhensle Aug 19, 2026
66e1037
better handling of internal memory handling between iterations
dhensle Aug 20, 2026
1f55863
run modes and resume after tests
dhensle Aug 20, 2026
97fbf16
nailing down resume_after and iteration counting
dhensle Aug 25, 2026
910a0e0
blacken
dhensle Aug 25, 2026
2ef9828
removing unused calibration.yaml settings
dhensle Aug 25, 2026
cd8f8b5
clarifying resume_after readme
dhensle Aug 25, 2026
896fc06
aligning tests and language around restart functionality
dhensle Aug 26, 2026
f414f23
setting checking cleanup
dhensle Aug 26, 2026
e2ee5b5
Merge remote-tracking branch 'upstream/main' into calibration
dhensle Aug 26, 2026
6c59ee2
addressing RNG and resume review comments
dhensle Aug 26, 2026
0b953b2
clearer logging at max iterations
dhensle Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions activitysim/abm/models/initialize.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,12 +154,19 @@ def initialize_households(
households = state.get_dataframe("households")
assert not households._is_view
chunk_sizer.log_df(trace_label, "households", households)
del households
chunk_sizer.log_df(trace_label, "households", None)

persons = state.get_dataframe("persons")
assert not persons._is_view
chunk_sizer.log_df(trace_label, "persons", persons)

rng = state.get_rn_generator()
if "households" not in rng.channels:
rng.add_channel("households", households)
if "persons" not in rng.channels:
rng.add_channel("persons", persons)

del households
chunk_sizer.log_df(trace_label, "households", None)
del persons
chunk_sizer.log_df(trace_label, "persons", None)

Expand Down
28 changes: 24 additions & 4 deletions activitysim/abm/models/settings_checker.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,24 +4,26 @@
from pydantic import BaseModel as PydanticBase
from typing import Type, Optional

from activitysim.core import config
from activitysim.core.calibration.settings import (
CALIBRATION_SETTINGS_FILE_NAME,
CalibrationConfig,
)
from activitysim.core.configuration.base import PydanticReadable

# import core settings
from activitysim.core.configuration.logit import (
LogitNestSpec,
TourLocationComponentSettings,
TourModeComponentSettings,
TemplatedLogitComponentSettings,
)
from activitysim.core import config
from activitysim.core.configuration.network import NetworkSettings
from activitysim.core.exceptions import ModelConfigurationError
from activitysim.core.workflow import State
from activitysim.core.simulate import (
eval_coefficients,
eval_nest_coefficients,
read_model_coefficient_template,
)
from activitysim.core.exceptions import ModelConfigurationError

# import model settings
from activitysim.abm.models.accessibility import AccessibilitySettings
Expand DownExpand Up@@ -637,6 +639,24 @@ def check_model_settings(
# Collect all errors
all_errors = []

# calibration.yaml is optional and is not itself a model step, so validate
# it explicitly using the same Pydantic/error-aggregation path as model
# settings files.
try:
CalibrationConfig.read_settings_file(
state.filesystem,
CALIBRATION_SETTINGS_FILE_NAME,
mandatory=False,
)
except Exception as error:
all_errors.append(
SettingsCheckerError(
"calibration",
error,
CALIBRATION_SETTINGS_FILE_NAME,
)
)

# additional logging set up
formatter = logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
Expand Down
60 changes: 57 additions & 3 deletions activitysim/cli/run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@

import numpy as np

from activitysim.core import chunk, config, mem, timing, tracing, workflow
from activitysim.core import calibration, chunk, config, mem, timing, tracing, workflow
from activitysim.core.configuration import FileSystem, Settings
from activitysim.core.run_id import RunId

Expand DownExpand Up@@ -336,8 +336,13 @@ def run(args):
resume_after = state.settings.resume_after

# cleanup if not resuming
if not resume_after:
preserve_calibration_outputs = calibration.calibration_run_should_preserve_outputs(
state
)
if not resume_after and not preserve_calibration_outputs:
cleanup_output_files(state)
elif preserve_calibration_outputs:
logger.info("preserving output files based on calibration restart preflight")
elif state.settings.cleanup_trace_files_on_resume:
tracing.delete_trace_files(state)

Expand DownExpand Up@@ -418,7 +423,56 @@ def run(args):
check_model_settings(state, extension_settings=extension_checker_settings)

try:
if state.settings.multiprocess:
if calibration.calibration_enabled(state):
logger.info("evaluate calibration workflow")

calibration_result = calibration.run_calibration_loop(
state=state,
models=state.settings.models,
)

if calibration_result.model_system_ran:
logger.info(
"calibration workflow complete converged=%s "
"completed_global_iterations=%s configured_global_iterations=%s",
calibration_result.converged,
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)
else:
if (
calibration_result.completed_global_iterations
>= calibration_result.configured_global_iterations
):
logger.info(
"calibration workflow skipped: no model system steps were run "
"because completed_global_iterations=%s has reached the "
"configured run.global_iterations=%s limit. Increase "
"run.global_iterations to request additional iterations, or "
"remove output/calibration/calibration_progress.json to start a fresh run.",
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)
else:
logger.info(
"calibration workflow skipped: no model system steps were run "
"because the prior calibration run is already complete "
"(completed_global_iterations=%s, run.global_iterations=%s). "
"Change run.global_iterations to a new value above the completed "
"count to request additional iterations, or remove "
"output/calibration/calibration_progress.json to start a fresh run.",
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)

if state.settings.cleanup_pipeline_after_run:
state.checkpoint.cleanup()
else:
state.checkpoint.close_store()

mem.log_global_hwm()

elif state.settings.multiprocess:
logger.info("run multiprocess simulation")

from activitysim.core import mp_tasks
Expand Down
122 changes: 122 additions & 0 deletions activitysim/core/calibration/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
# ActivitySim calibration package

This package implements ActivitySim's automated coefficient calibration workflow.
The modules are divided by responsibility so orchestration, model execution, and
calibration math can evolve independently.

- `__init__.py` defines the public API and compatibility access to former private
attributes from `activitysim.core.calibration`.
- `settings.py` defines calibration configuration models, result types, and
configuration loading.
- `orchestrator.py` coordinates global iterations and model-component sequencing.
- `component.py` runs component iterations, validates calibration specifications,
and calculates coefficient updates.
- `expressions.py` builds expression contexts, evaluates target/model expressions,
and loads optional helper modules.
- `coefficients.py` locates, reads, and writes model coefficient files.
- `reporting.py` writes iteration histories, summaries, plots, and final snapshots.
- `recovery.py` durably tracks calibration progress. Coefficient files are the
authoritative current state and are never rolled back when a run resumes.
- `execution.py` restores pipeline state and dispatches model execution.
- `multiprocess.py` contains subprocess orchestration, shared-resource setup, and
multiprocess pipeline restoration.

Component settings may specify `model_settings_file` when a workflow step does
not follow the conventional `<component_name>.yaml` naming pattern. Components
that share model settings may point to the same coefficient file.

## Global iterations, recovery attempts, and `resume_after`

`calibration.yaml` `run.global_iterations` is the desired total number of
completed logical calibration iterations, not the number to execute on each
ActivitySim invocation. The run-control contract is:

1. An unchanged setting on a completed run is a no-op, detected before normal
output cleanup so the pipeline and final outputs are preserved.
2. If a completed run's setting is changed to a value greater than the number
actually completed, calibration continues until the new total. This includes
lowering a previous maximum after early convergence, such as changing 5 to 3
after convergence completed iteration 2.
3. A changed setting less than or equal to the number already completed is a
no-op; completed coefficient updates are never undone implicitly.
4. Top-level `settings.yaml` `resume_after` has normal ActivitySim semantics for
the first global iteration entered by the current invocation. Later global
iterations ignore it and execute every calibrated component.
5. A global iteration counts only if it has at least one durable calibrated
component result, either from the current attempt or an earlier attempt of
that same logical iteration.
6. `global_iterations` cannot be lowered below an interrupted iteration because
its coefficient files may already contain updates from that iteration. The
run stops with instructions to resume the iteration or deliberately reset
progress and coefficients.
7. Startup logs report the detected completed count, requested target, selected
action, starting iteration and attempt, and `resume_after` value.

Calibration records the state needed to apply this contract in
`output/calibration/calibration_progress.json`.

Recovery attempts are distinct from global iterations. The first execution of a
global iteration is attempt 1. Restarting an interrupted global iteration creates
attempt 2, then attempt 3 if another restart is needed. A recovery attempt does
not consume an additional global iteration.

Calibration uses only the top-level `settings.yaml` `resume_after` setting. On
the first global iteration executed by an ActivitySim invocation, including a
new attempt of an interrupted iteration, it behaves like `resume_after` in a
non-calibration run:

- when set to a model name, that model is treated as complete, its checkpoint is
restored, and execution begins with the following model; and
- when unset or `null`, execution starts at the beginning of the top-level
`models` list. Calibration does not automatically continue after the last
model completed by the preceding attempt.

A named value must occur in the top-level `models` list and must identify a
model-level checkpoint. Calibration does not define a special `initialize`
value. A value such as `initialize_landuse` has ordinary model-name semantics:
that model is skipped and execution begins with the next model. The `_` shorthand
for the last checkpoint is not accepted in calibration mode.

`resume_after` affects only the first global iteration entered by the current
invocation. Calibrated models at or before a named resume point are skipped in
that iteration. Any later global iterations in the same invocation ignore
`resume_after`, restart immediately before the first calibrated model, and run
the normal complete calibration sequence.

For example, suppose global iteration 3 attempt 1 was interrupted after
calibrated `model_a` completed:

- `resume_after: model_a` starts attempt 2 after model A, preserving its
attempt-1 result; and
- `resume_after: null` starts attempt 2 at the beginning of the complete model
list, so model A runs again.

Coefficient files are the authoritative current state and are never rolled back
during recovery. Updates written before the interruption, along with subsequent
manual edits, become the starting coefficients for the new attempt. Rewinding
across a completed calibrated model therefore applies another calibration update
to its current coefficients. Calibration logs a warning when `resume_after`
causes this kind of rewind.

Iteration histories are append-only across attempts. Standard record, summary,
and generic-report rows include `global_iter`, `attempt`, and `component_iter`,
so a rerun does not replace the coefficient transition written by an earlier
attempt. The coefficient trajectory plots show the complete sequence, including
the initial value and every subsequent update. X-axis labels use the compact
form `G<global>-A<attempt>-C<component>`, for example `G3-A2-C1`.

After each calibrated component completes, the progress file records its attempt,
component-iteration count, and convergence result. A restart that skips that
component retains this state. This allows a run that crashed in downstream,
non-calibrated models to finish the same terminal global iteration without losing
the calibrated components' convergence decision.

Calibration is marked complete only after the terminal global iteration has run
all remaining models in the top-level `models` list and the final coefficient
snapshot has been written. A crash after the last calibrated model but before the
last production model therefore leaves the global iteration in progress.

Dependencies should flow from `orchestrator.py` into the focused modules. Lower
level modules should not import the orchestrator. Multiprocess code imports back
into `execution.py` only inside the single-component runner to avoid an import-time
cycle.
71 changes: 71 additions & 0 deletions activitysim/core/calibration/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
# ActivitySim
# See full license in LICENSE.txt.
"""Automated calibration support for ActivitySim model runs.

The public API is intentionally small. Private attributes from the former
single-file module remain available through ``__getattr__`` for compatibility
while callers migrate to the package modules.
"""

from . import (
coefficients,
component,
execution,
expressions,
multiprocess,
recovery,
reporting,
settings,
)
from .orchestrator import calibration_run_should_preserve_outputs, run_calibration_loop
from .settings import (
CalibrationComponentResult,
CalibrationComponentSettings,
CalibrationConfig,
CalibrationReportsSettings,
CalibrationRunResult,
CalibrationRunSettings,
calibration_enabled,
read_calibration_settings,
)

__all__ = [
"CalibrationComponentResult",
"CalibrationComponentSettings",
"CalibrationConfig",
"CalibrationReportsSettings",
"CalibrationRunResult",
"CalibrationRunSettings",
"calibration_run_should_preserve_outputs",
"calibration_enabled",
"read_calibration_settings",
"run_calibration_loop",
]

_COMPATIBILITY_MODULES = (
settings,
component,
expressions,
coefficients,
reporting,
recovery,
execution,
multiprocess,
)


def __getattr__(name):
for module in _COMPATIBILITY_MODULES:
if hasattr(module, name):
return getattr(module, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def __dir__():
compatibility_names = {
name
for module in _COMPATIBILITY_MODULES
for name in vars(module)
if not name.startswith("__")
}
return sorted(set(globals()) | compatibility_names)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
eb809d9
initial calibration commit
dhensle Jun 9, 2026
0876597
do not validate settings if file not found and not mandatory
dhensle Jun 9, 2026
6571db1
calibration settings, bespoke reports changes
americalexander Jul 2, 2026
6ccf84d
Fix global iterations
americalexander Jul 3, 2026
f2ea947
Loading from 2+ global iters
americalexander Jul 3, 2026
72758e6
Bugfix in plotting coefficient momentum
americalexander Jul 6, 2026
b065459
Add coef_delta to calibration records
americalexander Jul 6, 2026
3c135c7
Update graphs, handle zero modeled vals
americalexander Jul 6, 2026
b1db935
Batch output graphs
americalexander Jul 6, 2026
0b1f723
Minor plotting updates
americalexander Jul 6, 2026
96527ff
expose context to bespoke functions
dhensle Jul 7, 2026
d347f9c
output updates
dhensle Jul 7, 2026
9b02608
add component settings
americalexander Jul 7, 2026
4475471
Add calibration configs
americalexander Jul 7, 2026
1bf010c
passing context dict
dhensle Jul 7, 2026
dc731d0
updated specs for calibration
dhensle Jul 7, 2026
034545c
Add tour mode choice calibration coefficients
americalexander Jul 7, 2026
91bdb05
Merge branch 'calibration' of https://github.com/RSGInc/ActivitySim i…
americalexander Jul 7, 2026
fbf24d8
Update workplace location survey file directory
americalexander Jul 7, 2026
33ee993
auto ownership & tour mc relative paths
americalexander Jul 7, 2026
ff71c98
Allow no weights in tour mc summary
americalexander Jul 7, 2026
217b3e7
Remove duplicated checkpoint directories
americalexander Jul 13, 2026
aadd529
Minor bugfixes
americalexander Jul 14, 2026
67fd29f
Separate Claude attempt at MP calibration
americalexander Jul 14, 2026
6de9e48
MultiprocessStep creation fix
americalexander Jul 14, 2026
de894c2
Alleged checkpoint fix
americalexander Jul 14, 2026
3c78700
Deduplicate checkpoints
americalexander Jul 14, 2026
9856ee6
Prevent resume_after setting
americalexander Jul 14, 2026
fd7950a
More resume_after fixes
americalexander Jul 15, 2026
97a29d0
update existing table status
americalexander Jul 16, 2026
dbdff18
Checkpoint rename
americalexander Jul 16, 2026
d2dd7ae
Store last checkpoint
americalexander Jul 16, 2026
510b5d4
Blacken! It works!
americalexander Jul 16, 2026
0ee33d3
Set calibration coefficients to zero
americalexander Jul 16, 2026
fcfcb3c
Add override CI test coefficients for calibration
americalexander Jul 16, 2026
9d9f006
Add estimation mode regression test coeffs
americalexander Jul 16, 2026
afebfe3
More coefficient adds
americalexander Jul 16, 2026
4cda8e6
Add calibration documentation RST
americalexander Jul 17, 2026
15df92c
move calib documentation into users guide
dhensle Jul 21, 2026
4e38277
all converged fix and dead restart_after
dhensle Jul 21, 2026
391d9a1
blacken
dhensle Jul 21, 2026
eb4e6ba
Fix resume_after bug
americalexander Jul 21, 2026
de119ed
partial MP resume_after fix
americalexander Jul 22, 2026
f0e9ccb
More checkpointing work
americalexander Jul 29, 2026
c9afece
Add tour mode choice handler
americalexander Jul 29, 2026
ba8a142
Clean checkpointing
americalexander Jul 30, 2026
c7f3c77
More checkpointing fixes
americalexander Jul 30, 2026
c9aca45
Restore from subprocess checkpoints
americalexander Jul 30, 2026
a19638b
resume_after bugfix
americalexander Jul 31, 2026
2fd3db7
add subproc reuse
americalexander Aug 4, 2026
cc344c7
Modify breadcrumb logic
americalexander Aug 5, 2026
fcc6567
Add RNG channel reloading
americalexander Aug 11, 2026
a854b67
Make survey file optional
americalexander Aug 11, 2026
1b4eb8b
Add single-process channel restoration
americalexander Aug 11, 2026
7b66d4f
Update rng_channels
americalexander Aug 11, 2026
c0c937d
Modify single-process run approach
americalexander Aug 11, 2026
cf2c743
handle index_to_channel
americalexander Aug 11, 2026
ec6e269
Modify configured mode to use same path
americalexander Aug 11, 2026
c287efb
Remove memory sidecar process from calib
americalexander Aug 11, 2026
52ee5cd
Add reloading of tables in reregister_rng
americalexander Aug 11, 2026
af641ed
Maybe a different approach to checkpoint restoration
americalexander Aug 11, 2026
b38b329
Restore correct checkpoint
americalexander Aug 11, 2026
b452056
Fix add_table bug
americalexander Aug 12, 2026
d169bd4
Different approach to checkpoint/channel restoration
americalexander Aug 12, 2026
2eb889d
Add table invalidation
americalexander Aug 12, 2026
67b87a4
Additional precursor guard
americalexander Aug 12, 2026
8a19f6c
Blacken
americalexander Aug 12, 2026
21a0d78
Fix calibrate_component precursor calls
americalexander Aug 12, 2026
0437b70
Add temporary diagnostic tracing
americalexander Aug 12, 2026
2f58828
More diagnostic logging
americalexander Aug 12, 2026
6563525
Change to default tables_to_invalidate
americalexander Aug 12, 2026
210454e
Change when table invalidation occurs
americalexander Aug 12, 2026
e8f6a33
Fix SP checkpointing
americalexander Aug 12, 2026
fb923bb
fixing resume after and rng calib invariance
dhensle Aug 14, 2026
92bbc84
fixing pipeline and rng restarting issues
dhensle Aug 14, 2026
af1e21b
fixing more pipeline restart issues
dhensle Aug 17, 2026
022b4fb
appropriately handling crashing in middle of iteration
dhensle Aug 19, 2026
d79517e
refactor into separate files
dhensle Aug 19, 2026
ce7844f
if everything is converged, will continue to run downstream models
dhensle Aug 19, 2026
e9dcffb
blacken
dhensle Aug 19, 2026
adf17f9
remove coefficient rollback functionality
dhensle Aug 19, 2026
66e1037
better handling of internal memory handling between iterations
dhensle Aug 20, 2026
1f55863
run modes and resume after tests
dhensle Aug 20, 2026
97fbf16
nailing down resume_after and iteration counting
dhensle Aug 25, 2026
910a0e0
blacken
dhensle Aug 25, 2026
2ef9828
removing unused calibration.yaml settings
dhensle Aug 25, 2026
cd8f8b5
clarifying resume_after readme
dhensle Aug 25, 2026
896fc06
aligning tests and language around restart functionality
dhensle Aug 26, 2026
f414f23
setting checking cleanup
dhensle Aug 26, 2026
e2ee5b5
Merge remote-tracking branch 'upstream/main' into calibration
dhensle Aug 26, 2026
6c59ee2
addressing RNG and resume review comments
dhensle Aug 26, 2026
0b953b2
clearer logging at max iterations
dhensle Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions activitysim/abm/models/initialize.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,12 +154,19 @@ def initialize_households(
households = state.get_dataframe("households")
assert not households._is_view
chunk_sizer.log_df(trace_label, "households", households)
del households
chunk_sizer.log_df(trace_label, "households", None)

persons = state.get_dataframe("persons")
assert not persons._is_view
chunk_sizer.log_df(trace_label, "persons", persons)

rng = state.get_rn_generator()
if "households" not in rng.channels:
rng.add_channel("households", households)
if "persons" not in rng.channels:
rng.add_channel("persons", persons)

del households
chunk_sizer.log_df(trace_label, "households", None)
del persons
chunk_sizer.log_df(trace_label, "persons", None)

Expand Down
28 changes: 24 additions & 4 deletions activitysim/abm/models/settings_checker.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,24 +4,26 @@
from pydantic import BaseModel as PydanticBase
from typing import Type, Optional

from activitysim.core import config
from activitysim.core.calibration.settings import (
CALIBRATION_SETTINGS_FILE_NAME,
CalibrationConfig,
)
from activitysim.core.configuration.base import PydanticReadable

# import core settings
from activitysim.core.configuration.logit import (
LogitNestSpec,
TourLocationComponentSettings,
TourModeComponentSettings,
TemplatedLogitComponentSettings,
)
from activitysim.core import config
from activitysim.core.configuration.network import NetworkSettings
from activitysim.core.exceptions import ModelConfigurationError
from activitysim.core.workflow import State
from activitysim.core.simulate import (
eval_coefficients,
eval_nest_coefficients,
read_model_coefficient_template,
)
from activitysim.core.exceptions import ModelConfigurationError

# import model settings
from activitysim.abm.models.accessibility import AccessibilitySettings
Expand DownExpand Up@@ -637,6 +639,24 @@ def check_model_settings(
# Collect all errors
all_errors = []

# calibration.yaml is optional and is not itself a model step, so validate
# it explicitly using the same Pydantic/error-aggregation path as model
# settings files.
try:
CalibrationConfig.read_settings_file(
state.filesystem,
CALIBRATION_SETTINGS_FILE_NAME,
mandatory=False,
)
except Exception as error:
all_errors.append(
SettingsCheckerError(
"calibration",
error,
CALIBRATION_SETTINGS_FILE_NAME,
)
)

# additional logging set up
formatter = logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
Expand Down
60 changes: 57 additions & 3 deletions activitysim/cli/run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@

import numpy as np

from activitysim.core import chunk, config, mem, timing, tracing, workflow
from activitysim.core import calibration, chunk, config, mem, timing, tracing, workflow
from activitysim.core.configuration import FileSystem, Settings
from activitysim.core.run_id import RunId

Expand DownExpand Up@@ -336,8 +336,13 @@ def run(args):
resume_after = state.settings.resume_after

# cleanup if not resuming
if not resume_after:
preserve_calibration_outputs = calibration.calibration_run_should_preserve_outputs(
state
)
if not resume_after and not preserve_calibration_outputs:
cleanup_output_files(state)
elif preserve_calibration_outputs:
logger.info("preserving output files based on calibration restart preflight")
elif state.settings.cleanup_trace_files_on_resume:
tracing.delete_trace_files(state)

Expand DownExpand Up@@ -418,7 +423,56 @@ def run(args):
check_model_settings(state, extension_settings=extension_checker_settings)

try:
if state.settings.multiprocess:
if calibration.calibration_enabled(state):
logger.info("evaluate calibration workflow")

calibration_result = calibration.run_calibration_loop(
state=state,
models=state.settings.models,
)

if calibration_result.model_system_ran:
logger.info(
"calibration workflow complete converged=%s "
"completed_global_iterations=%s configured_global_iterations=%s",
calibration_result.converged,
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)
else:
if (
calibration_result.completed_global_iterations
>= calibration_result.configured_global_iterations
):
logger.info(
"calibration workflow skipped: no model system steps were run "
"because completed_global_iterations=%s has reached the "
"configured run.global_iterations=%s limit. Increase "
"run.global_iterations to request additional iterations, or "
"remove output/calibration/calibration_progress.json to start a fresh run.",
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)
else:
logger.info(
"calibration workflow skipped: no model system steps were run "
"because the prior calibration run is already complete "
"(completed_global_iterations=%s, run.global_iterations=%s). "
"Change run.global_iterations to a new value above the completed "
"count to request additional iterations, or remove "
"output/calibration/calibration_progress.json to start a fresh run.",
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)

if state.settings.cleanup_pipeline_after_run:
state.checkpoint.cleanup()
else:
state.checkpoint.close_store()

mem.log_global_hwm()

elif state.settings.multiprocess:
logger.info("run multiprocess simulation")

from activitysim.core import mp_tasks
Expand Down
122 changes: 122 additions & 0 deletions activitysim/core/calibration/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
# ActivitySim calibration package

This package implements ActivitySim's automated coefficient calibration workflow.
The modules are divided by responsibility so orchestration, model execution, and
calibration math can evolve independently.

- `__init__.py` defines the public API and compatibility access to former private
attributes from `activitysim.core.calibration`.
- `settings.py` defines calibration configuration models, result types, and
configuration loading.
- `orchestrator.py` coordinates global iterations and model-component sequencing.
- `component.py` runs component iterations, validates calibration specifications,
and calculates coefficient updates.
- `expressions.py` builds expression contexts, evaluates target/model expressions,
and loads optional helper modules.
- `coefficients.py` locates, reads, and writes model coefficient files.
- `reporting.py` writes iteration histories, summaries, plots, and final snapshots.
- `recovery.py` durably tracks calibration progress. Coefficient files are the
authoritative current state and are never rolled back when a run resumes.
- `execution.py` restores pipeline state and dispatches model execution.
- `multiprocess.py` contains subprocess orchestration, shared-resource setup, and
multiprocess pipeline restoration.

Component settings may specify `model_settings_file` when a workflow step does
not follow the conventional `<component_name>.yaml` naming pattern. Components
that share model settings may point to the same coefficient file.

## Global iterations, recovery attempts, and `resume_after`

`calibration.yaml` `run.global_iterations` is the desired total number of
completed logical calibration iterations, not the number to execute on each
ActivitySim invocation. The run-control contract is:

1. An unchanged setting on a completed run is a no-op, detected before normal
output cleanup so the pipeline and final outputs are preserved.
2. If a completed run's setting is changed to a value greater than the number
actually completed, calibration continues until the new total. This includes
lowering a previous maximum after early convergence, such as changing 5 to 3
after convergence completed iteration 2.
3. A changed setting less than or equal to the number already completed is a
no-op; completed coefficient updates are never undone implicitly.
4. Top-level `settings.yaml` `resume_after` has normal ActivitySim semantics for
the first global iteration entered by the current invocation. Later global
iterations ignore it and execute every calibrated component.
5. A global iteration counts only if it has at least one durable calibrated
component result, either from the current attempt or an earlier attempt of
that same logical iteration.
6. `global_iterations` cannot be lowered below an interrupted iteration because
its coefficient files may already contain updates from that iteration. The
run stops with instructions to resume the iteration or deliberately reset
progress and coefficients.
7. Startup logs report the detected completed count, requested target, selected
action, starting iteration and attempt, and `resume_after` value.

Calibration records the state needed to apply this contract in
`output/calibration/calibration_progress.json`.

Recovery attempts are distinct from global iterations. The first execution of a
global iteration is attempt 1. Restarting an interrupted global iteration creates
attempt 2, then attempt 3 if another restart is needed. A recovery attempt does
not consume an additional global iteration.

Calibration uses only the top-level `settings.yaml` `resume_after` setting. On
the first global iteration executed by an ActivitySim invocation, including a
new attempt of an interrupted iteration, it behaves like `resume_after` in a
non-calibration run:

- when set to a model name, that model is treated as complete, its checkpoint is
restored, and execution begins with the following model; and
- when unset or `null`, execution starts at the beginning of the top-level
`models` list. Calibration does not automatically continue after the last
model completed by the preceding attempt.

A named value must occur in the top-level `models` list and must identify a
model-level checkpoint. Calibration does not define a special `initialize`
value. A value such as `initialize_landuse` has ordinary model-name semantics:
that model is skipped and execution begins with the next model. The `_` shorthand
for the last checkpoint is not accepted in calibration mode.

`resume_after` affects only the first global iteration entered by the current
invocation. Calibrated models at or before a named resume point are skipped in
that iteration. Any later global iterations in the same invocation ignore
`resume_after`, restart immediately before the first calibrated model, and run
the normal complete calibration sequence.

For example, suppose global iteration 3 attempt 1 was interrupted after
calibrated `model_a` completed:

- `resume_after: model_a` starts attempt 2 after model A, preserving its
attempt-1 result; and
- `resume_after: null` starts attempt 2 at the beginning of the complete model
list, so model A runs again.

Coefficient files are the authoritative current state and are never rolled back
during recovery. Updates written before the interruption, along with subsequent
manual edits, become the starting coefficients for the new attempt. Rewinding
across a completed calibrated model therefore applies another calibration update
to its current coefficients. Calibration logs a warning when `resume_after`
causes this kind of rewind.

Iteration histories are append-only across attempts. Standard record, summary,
and generic-report rows include `global_iter`, `attempt`, and `component_iter`,
so a rerun does not replace the coefficient transition written by an earlier
attempt. The coefficient trajectory plots show the complete sequence, including
the initial value and every subsequent update. X-axis labels use the compact
form `G<global>-A<attempt>-C<component>`, for example `G3-A2-C1`.

After each calibrated component completes, the progress file records its attempt,
component-iteration count, and convergence result. A restart that skips that
component retains this state. This allows a run that crashed in downstream,
non-calibrated models to finish the same terminal global iteration without losing
the calibrated components' convergence decision.

Calibration is marked complete only after the terminal global iteration has run
all remaining models in the top-level `models` list and the final coefficient
snapshot has been written. A crash after the last calibrated model but before the
last production model therefore leaves the global iteration in progress.

Dependencies should flow from `orchestrator.py` into the focused modules. Lower
level modules should not import the orchestrator. Multiprocess code imports back
into `execution.py` only inside the single-component runner to avoid an import-time
cycle.
71 changes: 71 additions & 0 deletions activitysim/core/calibration/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
# ActivitySim
# See full license in LICENSE.txt.
"""Automated calibration support for ActivitySim model runs.

The public API is intentionally small. Private attributes from the former
single-file module remain available through ``__getattr__`` for compatibility
while callers migrate to the package modules.
"""

from . import (
coefficients,
component,
execution,
expressions,
multiprocess,
recovery,
reporting,
settings,
)
from .orchestrator import calibration_run_should_preserve_outputs, run_calibration_loop
from .settings import (
CalibrationComponentResult,
CalibrationComponentSettings,
CalibrationConfig,
CalibrationReportsSettings,
CalibrationRunResult,
CalibrationRunSettings,
calibration_enabled,
read_calibration_settings,
)

__all__ = [
"CalibrationComponentResult",
"CalibrationComponentSettings",
"CalibrationConfig",
"CalibrationReportsSettings",
"CalibrationRunResult",
"CalibrationRunSettings",
"calibration_run_should_preserve_outputs",
"calibration_enabled",
"read_calibration_settings",
"run_calibration_loop",
]

_COMPATIBILITY_MODULES = (
settings,
component,
expressions,
coefficients,
reporting,
recovery,
execution,
multiprocess,
)


def __getattr__(name):
for module in _COMPATIBILITY_MODULES:
if hasattr(module, name):
return getattr(module, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def __dir__():
compatibility_names = {
name
for module in _COMPATIBILITY_MODULES
for name in vars(module)
if not name.startswith("__")
}
return sorted(set(globals()) | compatibility_names)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
eb809d9
initial calibration commit
dhensle Jun 9, 2026
0876597
do not validate settings if file not found and not mandatory
dhensle Jun 9, 2026
6571db1
calibration settings, bespoke reports changes
americalexander Jul 2, 2026
6ccf84d
Fix global iterations
americalexander Jul 3, 2026
f2ea947
Loading from 2+ global iters
americalexander Jul 3, 2026
72758e6
Bugfix in plotting coefficient momentum
americalexander Jul 6, 2026
b065459
Add coef_delta to calibration records
americalexander Jul 6, 2026
3c135c7
Update graphs, handle zero modeled vals
americalexander Jul 6, 2026
b1db935
Batch output graphs
americalexander Jul 6, 2026
0b1f723
Minor plotting updates
americalexander Jul 6, 2026
96527ff
expose context to bespoke functions
dhensle Jul 7, 2026
d347f9c
output updates
dhensle Jul 7, 2026
9b02608
add component settings
americalexander Jul 7, 2026
4475471
Add calibration configs
americalexander Jul 7, 2026
1bf010c
passing context dict
dhensle Jul 7, 2026
dc731d0
updated specs for calibration
dhensle Jul 7, 2026
034545c
Add tour mode choice calibration coefficients
americalexander Jul 7, 2026
91bdb05
Merge branch 'calibration' of https://github.com/RSGInc/ActivitySim i…
americalexander Jul 7, 2026
fbf24d8
Update workplace location survey file directory
americalexander Jul 7, 2026
33ee993
auto ownership & tour mc relative paths
americalexander Jul 7, 2026
ff71c98
Allow no weights in tour mc summary
americalexander Jul 7, 2026
217b3e7
Remove duplicated checkpoint directories
americalexander Jul 13, 2026
aadd529
Minor bugfixes
americalexander Jul 14, 2026
67fd29f
Separate Claude attempt at MP calibration
americalexander Jul 14, 2026
6de9e48
MultiprocessStep creation fix
americalexander Jul 14, 2026
de894c2
Alleged checkpoint fix
americalexander Jul 14, 2026
3c78700
Deduplicate checkpoints
americalexander Jul 14, 2026
9856ee6
Prevent resume_after setting
americalexander Jul 14, 2026
fd7950a
More resume_after fixes
americalexander Jul 15, 2026
97a29d0
update existing table status
americalexander Jul 16, 2026
dbdff18
Checkpoint rename
americalexander Jul 16, 2026
d2dd7ae
Store last checkpoint
americalexander Jul 16, 2026
510b5d4
Blacken! It works!
americalexander Jul 16, 2026
0ee33d3
Set calibration coefficients to zero
americalexander Jul 16, 2026
fcfcb3c
Add override CI test coefficients for calibration
americalexander Jul 16, 2026
9d9f006
Add estimation mode regression test coeffs
americalexander Jul 16, 2026
afebfe3
More coefficient adds
americalexander Jul 16, 2026
4cda8e6
Add calibration documentation RST
americalexander Jul 17, 2026
15df92c
move calib documentation into users guide
dhensle Jul 21, 2026
4e38277
all converged fix and dead restart_after
dhensle Jul 21, 2026
391d9a1
blacken
dhensle Jul 21, 2026
eb4e6ba
Fix resume_after bug
americalexander Jul 21, 2026
de119ed
partial MP resume_after fix
americalexander Jul 22, 2026
f0e9ccb
More checkpointing work
americalexander Jul 29, 2026
c9afece
Add tour mode choice handler
americalexander Jul 29, 2026
ba8a142
Clean checkpointing
americalexander Jul 30, 2026
c7f3c77
More checkpointing fixes
americalexander Jul 30, 2026
c9aca45
Restore from subprocess checkpoints
americalexander Jul 30, 2026
a19638b
resume_after bugfix
americalexander Jul 31, 2026
2fd3db7
add subproc reuse
americalexander Aug 4, 2026
cc344c7
Modify breadcrumb logic
americalexander Aug 5, 2026
fcc6567
Add RNG channel reloading
americalexander Aug 11, 2026
a854b67
Make survey file optional
americalexander Aug 11, 2026
1b4eb8b
Add single-process channel restoration
americalexander Aug 11, 2026
7b66d4f
Update rng_channels
americalexander Aug 11, 2026
c0c937d
Modify single-process run approach
americalexander Aug 11, 2026
cf2c743
handle index_to_channel
americalexander Aug 11, 2026
ec6e269
Modify configured mode to use same path
americalexander Aug 11, 2026
c287efb
Remove memory sidecar process from calib
americalexander Aug 11, 2026
52ee5cd
Add reloading of tables in reregister_rng
americalexander Aug 11, 2026
af641ed
Maybe a different approach to checkpoint restoration
americalexander Aug 11, 2026
b38b329
Restore correct checkpoint
americalexander Aug 11, 2026
b452056
Fix add_table bug
americalexander Aug 12, 2026
d169bd4
Different approach to checkpoint/channel restoration
americalexander Aug 12, 2026
2eb889d
Add table invalidation
americalexander Aug 12, 2026
67b87a4
Additional precursor guard
americalexander Aug 12, 2026
8a19f6c
Blacken
americalexander Aug 12, 2026
21a0d78
Fix calibrate_component precursor calls
americalexander Aug 12, 2026
0437b70
Add temporary diagnostic tracing
americalexander Aug 12, 2026
2f58828
More diagnostic logging
americalexander Aug 12, 2026
6563525
Change to default tables_to_invalidate
americalexander Aug 12, 2026
210454e
Change when table invalidation occurs
americalexander Aug 12, 2026
e8f6a33
Fix SP checkpointing
americalexander Aug 12, 2026
fb923bb
fixing resume after and rng calib invariance
dhensle Aug 14, 2026
92bbc84
fixing pipeline and rng restarting issues
dhensle Aug 14, 2026
af1e21b
fixing more pipeline restart issues
dhensle Aug 17, 2026
022b4fb
appropriately handling crashing in middle of iteration
dhensle Aug 19, 2026
d79517e
refactor into separate files
dhensle Aug 19, 2026
ce7844f
if everything is converged, will continue to run downstream models
dhensle Aug 19, 2026
e9dcffb
blacken
dhensle Aug 19, 2026
adf17f9
remove coefficient rollback functionality
dhensle Aug 19, 2026
66e1037
better handling of internal memory handling between iterations
dhensle Aug 20, 2026
1f55863
run modes and resume after tests
dhensle Aug 20, 2026
97fbf16
nailing down resume_after and iteration counting
dhensle Aug 25, 2026
910a0e0
blacken
dhensle Aug 25, 2026
2ef9828
removing unused calibration.yaml settings
dhensle Aug 25, 2026
cd8f8b5
clarifying resume_after readme
dhensle Aug 25, 2026
896fc06
aligning tests and language around restart functionality
dhensle Aug 26, 2026
f414f23
setting checking cleanup
dhensle Aug 26, 2026
e2ee5b5
Merge remote-tracking branch 'upstream/main' into calibration
dhensle Aug 26, 2026
6c59ee2
addressing RNG and resume review comments
dhensle Aug 26, 2026
0b953b2
clearer logging at max iterations
dhensle Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions activitysim/abm/models/initialize.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,12 +154,19 @@ def initialize_households(
households = state.get_dataframe("households")
assert not households._is_view
chunk_sizer.log_df(trace_label, "households", households)
del households
chunk_sizer.log_df(trace_label, "households", None)

persons = state.get_dataframe("persons")
assert not persons._is_view
chunk_sizer.log_df(trace_label, "persons", persons)

rng = state.get_rn_generator()
if "households" not in rng.channels:
rng.add_channel("households", households)
if "persons" not in rng.channels:
rng.add_channel("persons", persons)

del households
chunk_sizer.log_df(trace_label, "households", None)
del persons
chunk_sizer.log_df(trace_label, "persons", None)

Expand Down
28 changes: 24 additions & 4 deletions activitysim/abm/models/settings_checker.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,24 +4,26 @@
from pydantic import BaseModel as PydanticBase
from typing import Type, Optional

from activitysim.core import config
from activitysim.core.calibration.settings import (
CALIBRATION_SETTINGS_FILE_NAME,
CalibrationConfig,
)
from activitysim.core.configuration.base import PydanticReadable

# import core settings
from activitysim.core.configuration.logit import (
LogitNestSpec,
TourLocationComponentSettings,
TourModeComponentSettings,
TemplatedLogitComponentSettings,
)
from activitysim.core import config
from activitysim.core.configuration.network import NetworkSettings
from activitysim.core.exceptions import ModelConfigurationError
from activitysim.core.workflow import State
from activitysim.core.simulate import (
eval_coefficients,
eval_nest_coefficients,
read_model_coefficient_template,
)
from activitysim.core.exceptions import ModelConfigurationError

# import model settings
from activitysim.abm.models.accessibility import AccessibilitySettings
Expand DownExpand Up@@ -637,6 +639,24 @@ def check_model_settings(
# Collect all errors
all_errors = []

# calibration.yaml is optional and is not itself a model step, so validate
# it explicitly using the same Pydantic/error-aggregation path as model
# settings files.
try:
CalibrationConfig.read_settings_file(
state.filesystem,
CALIBRATION_SETTINGS_FILE_NAME,
mandatory=False,
)
except Exception as error:
all_errors.append(
SettingsCheckerError(
"calibration",
error,
CALIBRATION_SETTINGS_FILE_NAME,
)
)

# additional logging set up
formatter = logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
Expand Down
60 changes: 57 additions & 3 deletions activitysim/cli/run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@

import numpy as np

from activitysim.core import chunk, config, mem, timing, tracing, workflow
from activitysim.core import calibration, chunk, config, mem, timing, tracing, workflow
from activitysim.core.configuration import FileSystem, Settings
from activitysim.core.run_id import RunId

Expand DownExpand Up@@ -336,8 +336,13 @@ def run(args):
resume_after = state.settings.resume_after

# cleanup if not resuming
if not resume_after:
preserve_calibration_outputs = calibration.calibration_run_should_preserve_outputs(
state
)
if not resume_after and not preserve_calibration_outputs:
cleanup_output_files(state)
elif preserve_calibration_outputs:
logger.info("preserving output files based on calibration restart preflight")
elif state.settings.cleanup_trace_files_on_resume:
tracing.delete_trace_files(state)

Expand DownExpand Up@@ -418,7 +423,56 @@ def run(args):
check_model_settings(state, extension_settings=extension_checker_settings)

try:
if state.settings.multiprocess:
if calibration.calibration_enabled(state):
logger.info("evaluate calibration workflow")

calibration_result = calibration.run_calibration_loop(
state=state,
models=state.settings.models,
)

if calibration_result.model_system_ran:
logger.info(
"calibration workflow complete converged=%s "
"completed_global_iterations=%s configured_global_iterations=%s",
calibration_result.converged,
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)
else:
if (
calibration_result.completed_global_iterations
>= calibration_result.configured_global_iterations
):
logger.info(
"calibration workflow skipped: no model system steps were run "
"because completed_global_iterations=%s has reached the "
"configured run.global_iterations=%s limit. Increase "
"run.global_iterations to request additional iterations, or "
"remove output/calibration/calibration_progress.json to start a fresh run.",
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)
else:
logger.info(
"calibration workflow skipped: no model system steps were run "
"because the prior calibration run is already complete "
"(completed_global_iterations=%s, run.global_iterations=%s). "
"Change run.global_iterations to a new value above the completed "
"count to request additional iterations, or remove "
"output/calibration/calibration_progress.json to start a fresh run.",
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)

if state.settings.cleanup_pipeline_after_run:
state.checkpoint.cleanup()
else:
state.checkpoint.close_store()

mem.log_global_hwm()

elif state.settings.multiprocess:
logger.info("run multiprocess simulation")

from activitysim.core import mp_tasks
Expand Down
122 changes: 122 additions & 0 deletions activitysim/core/calibration/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
# ActivitySim calibration package

This package implements ActivitySim's automated coefficient calibration workflow.
The modules are divided by responsibility so orchestration, model execution, and
calibration math can evolve independently.

- `__init__.py` defines the public API and compatibility access to former private
attributes from `activitysim.core.calibration`.
- `settings.py` defines calibration configuration models, result types, and
configuration loading.
- `orchestrator.py` coordinates global iterations and model-component sequencing.
- `component.py` runs component iterations, validates calibration specifications,
and calculates coefficient updates.
- `expressions.py` builds expression contexts, evaluates target/model expressions,
and loads optional helper modules.
- `coefficients.py` locates, reads, and writes model coefficient files.
- `reporting.py` writes iteration histories, summaries, plots, and final snapshots.
- `recovery.py` durably tracks calibration progress. Coefficient files are the
authoritative current state and are never rolled back when a run resumes.
- `execution.py` restores pipeline state and dispatches model execution.
- `multiprocess.py` contains subprocess orchestration, shared-resource setup, and
multiprocess pipeline restoration.

Component settings may specify `model_settings_file` when a workflow step does
not follow the conventional `<component_name>.yaml` naming pattern. Components
that share model settings may point to the same coefficient file.

## Global iterations, recovery attempts, and `resume_after`

`calibration.yaml` `run.global_iterations` is the desired total number of
completed logical calibration iterations, not the number to execute on each
ActivitySim invocation. The run-control contract is:

1. An unchanged setting on a completed run is a no-op, detected before normal
output cleanup so the pipeline and final outputs are preserved.
2. If a completed run's setting is changed to a value greater than the number
actually completed, calibration continues until the new total. This includes
lowering a previous maximum after early convergence, such as changing 5 to 3
after convergence completed iteration 2.
3. A changed setting less than or equal to the number already completed is a
no-op; completed coefficient updates are never undone implicitly.
4. Top-level `settings.yaml` `resume_after` has normal ActivitySim semantics for
the first global iteration entered by the current invocation. Later global
iterations ignore it and execute every calibrated component.
5. A global iteration counts only if it has at least one durable calibrated
component result, either from the current attempt or an earlier attempt of
that same logical iteration.
6. `global_iterations` cannot be lowered below an interrupted iteration because
its coefficient files may already contain updates from that iteration. The
run stops with instructions to resume the iteration or deliberately reset
progress and coefficients.
7. Startup logs report the detected completed count, requested target, selected
action, starting iteration and attempt, and `resume_after` value.

Calibration records the state needed to apply this contract in
`output/calibration/calibration_progress.json`.

Recovery attempts are distinct from global iterations. The first execution of a
global iteration is attempt 1. Restarting an interrupted global iteration creates
attempt 2, then attempt 3 if another restart is needed. A recovery attempt does
not consume an additional global iteration.

Calibration uses only the top-level `settings.yaml` `resume_after` setting. On
the first global iteration executed by an ActivitySim invocation, including a
new attempt of an interrupted iteration, it behaves like `resume_after` in a
non-calibration run:

- when set to a model name, that model is treated as complete, its checkpoint is
restored, and execution begins with the following model; and
- when unset or `null`, execution starts at the beginning of the top-level
`models` list. Calibration does not automatically continue after the last
model completed by the preceding attempt.

A named value must occur in the top-level `models` list and must identify a
model-level checkpoint. Calibration does not define a special `initialize`
value. A value such as `initialize_landuse` has ordinary model-name semantics:
that model is skipped and execution begins with the next model. The `_` shorthand
for the last checkpoint is not accepted in calibration mode.

`resume_after` affects only the first global iteration entered by the current
invocation. Calibrated models at or before a named resume point are skipped in
that iteration. Any later global iterations in the same invocation ignore
`resume_after`, restart immediately before the first calibrated model, and run
the normal complete calibration sequence.

For example, suppose global iteration 3 attempt 1 was interrupted after
calibrated `model_a` completed:

- `resume_after: model_a` starts attempt 2 after model A, preserving its
attempt-1 result; and
- `resume_after: null` starts attempt 2 at the beginning of the complete model
list, so model A runs again.

Coefficient files are the authoritative current state and are never rolled back
during recovery. Updates written before the interruption, along with subsequent
manual edits, become the starting coefficients for the new attempt. Rewinding
across a completed calibrated model therefore applies another calibration update
to its current coefficients. Calibration logs a warning when `resume_after`
causes this kind of rewind.

Iteration histories are append-only across attempts. Standard record, summary,
and generic-report rows include `global_iter`, `attempt`, and `component_iter`,
so a rerun does not replace the coefficient transition written by an earlier
attempt. The coefficient trajectory plots show the complete sequence, including
the initial value and every subsequent update. X-axis labels use the compact
form `G<global>-A<attempt>-C<component>`, for example `G3-A2-C1`.

After each calibrated component completes, the progress file records its attempt,
component-iteration count, and convergence result. A restart that skips that
component retains this state. This allows a run that crashed in downstream,
non-calibrated models to finish the same terminal global iteration without losing
the calibrated components' convergence decision.

Calibration is marked complete only after the terminal global iteration has run
all remaining models in the top-level `models` list and the final coefficient
snapshot has been written. A crash after the last calibrated model but before the
last production model therefore leaves the global iteration in progress.

Dependencies should flow from `orchestrator.py` into the focused modules. Lower
level modules should not import the orchestrator. Multiprocess code imports back
into `execution.py` only inside the single-component runner to avoid an import-time
cycle.
71 changes: 71 additions & 0 deletions activitysim/core/calibration/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
# ActivitySim
# See full license in LICENSE.txt.
"""Automated calibration support for ActivitySim model runs.

The public API is intentionally small. Private attributes from the former
single-file module remain available through ``__getattr__`` for compatibility
while callers migrate to the package modules.
"""

from . import (
coefficients,
component,
execution,
expressions,
multiprocess,
recovery,
reporting,
settings,
)
from .orchestrator import calibration_run_should_preserve_outputs, run_calibration_loop
from .settings import (
CalibrationComponentResult,
CalibrationComponentSettings,
CalibrationConfig,
CalibrationReportsSettings,
CalibrationRunResult,
CalibrationRunSettings,
calibration_enabled,
read_calibration_settings,
)

__all__ = [
"CalibrationComponentResult",
"CalibrationComponentSettings",
"CalibrationConfig",
"CalibrationReportsSettings",
"CalibrationRunResult",
"CalibrationRunSettings",
"calibration_run_should_preserve_outputs",
"calibration_enabled",
"read_calibration_settings",
"run_calibration_loop",
]

_COMPATIBILITY_MODULES = (
settings,
component,
expressions,
coefficients,
reporting,
recovery,
execution,
multiprocess,
)


def __getattr__(name):
for module in _COMPATIBILITY_MODULES:
if hasattr(module, name):
return getattr(module, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def __dir__():
compatibility_names = {
name
for module in _COMPATIBILITY_MODULES
for name in vars(module)
if not name.startswith("__")
}
return sorted(set(globals()) | compatibility_names)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
eb809d9
initial calibration commit
dhensle Jun 9, 2026
0876597
do not validate settings if file not found and not mandatory
dhensle Jun 9, 2026
6571db1
calibration settings, bespoke reports changes
americalexander Jul 2, 2026
6ccf84d
Fix global iterations
americalexander Jul 3, 2026
f2ea947
Loading from 2+ global iters
americalexander Jul 3, 2026
72758e6
Bugfix in plotting coefficient momentum
americalexander Jul 6, 2026
b065459
Add coef_delta to calibration records
americalexander Jul 6, 2026
3c135c7
Update graphs, handle zero modeled vals
americalexander Jul 6, 2026
b1db935
Batch output graphs
americalexander Jul 6, 2026
0b1f723
Minor plotting updates
americalexander Jul 6, 2026
96527ff
expose context to bespoke functions
dhensle Jul 7, 2026
d347f9c
output updates
dhensle Jul 7, 2026
9b02608
add component settings
americalexander Jul 7, 2026
4475471
Add calibration configs
americalexander Jul 7, 2026
1bf010c
passing context dict
dhensle Jul 7, 2026
dc731d0
updated specs for calibration
dhensle Jul 7, 2026
034545c
Add tour mode choice calibration coefficients
americalexander Jul 7, 2026
91bdb05
Merge branch 'calibration' of https://github.com/RSGInc/ActivitySim i…
americalexander Jul 7, 2026
fbf24d8
Update workplace location survey file directory
americalexander Jul 7, 2026
33ee993
auto ownership & tour mc relative paths
americalexander Jul 7, 2026
ff71c98
Allow no weights in tour mc summary
americalexander Jul 7, 2026
217b3e7
Remove duplicated checkpoint directories
americalexander Jul 13, 2026
aadd529
Minor bugfixes
americalexander Jul 14, 2026
67fd29f
Separate Claude attempt at MP calibration
americalexander Jul 14, 2026
6de9e48
MultiprocessStep creation fix
americalexander Jul 14, 2026
de894c2
Alleged checkpoint fix
americalexander Jul 14, 2026
3c78700
Deduplicate checkpoints
americalexander Jul 14, 2026
9856ee6
Prevent resume_after setting
americalexander Jul 14, 2026
fd7950a
More resume_after fixes
americalexander Jul 15, 2026
97a29d0
update existing table status
americalexander Jul 16, 2026
dbdff18
Checkpoint rename
americalexander Jul 16, 2026
d2dd7ae
Store last checkpoint
americalexander Jul 16, 2026
510b5d4
Blacken! It works!
americalexander Jul 16, 2026
0ee33d3
Set calibration coefficients to zero
americalexander Jul 16, 2026
fcfcb3c
Add override CI test coefficients for calibration
americalexander Jul 16, 2026
9d9f006
Add estimation mode regression test coeffs
americalexander Jul 16, 2026
afebfe3
More coefficient adds
americalexander Jul 16, 2026
4cda8e6
Add calibration documentation RST
americalexander Jul 17, 2026
15df92c
move calib documentation into users guide
dhensle Jul 21, 2026
4e38277
all converged fix and dead restart_after
dhensle Jul 21, 2026
391d9a1
blacken
dhensle Jul 21, 2026
eb4e6ba
Fix resume_after bug
americalexander Jul 21, 2026
de119ed
partial MP resume_after fix
americalexander Jul 22, 2026
f0e9ccb
More checkpointing work
americalexander Jul 29, 2026
c9afece
Add tour mode choice handler
americalexander Jul 29, 2026
ba8a142
Clean checkpointing
americalexander Jul 30, 2026
c7f3c77
More checkpointing fixes
americalexander Jul 30, 2026
c9aca45
Restore from subprocess checkpoints
americalexander Jul 30, 2026
a19638b
resume_after bugfix
americalexander Jul 31, 2026
2fd3db7
add subproc reuse
americalexander Aug 4, 2026
cc344c7
Modify breadcrumb logic
americalexander Aug 5, 2026
fcc6567
Add RNG channel reloading
americalexander Aug 11, 2026
a854b67
Make survey file optional
americalexander Aug 11, 2026
1b4eb8b
Add single-process channel restoration
americalexander Aug 11, 2026
7b66d4f
Update rng_channels
americalexander Aug 11, 2026
c0c937d
Modify single-process run approach
americalexander Aug 11, 2026
cf2c743
handle index_to_channel
americalexander Aug 11, 2026
ec6e269
Modify configured mode to use same path
americalexander Aug 11, 2026
c287efb
Remove memory sidecar process from calib
americalexander Aug 11, 2026
52ee5cd
Add reloading of tables in reregister_rng
americalexander Aug 11, 2026
af641ed
Maybe a different approach to checkpoint restoration
americalexander Aug 11, 2026
b38b329
Restore correct checkpoint
americalexander Aug 11, 2026
b452056
Fix add_table bug
americalexander Aug 12, 2026
d169bd4
Different approach to checkpoint/channel restoration
americalexander Aug 12, 2026
2eb889d
Add table invalidation
americalexander Aug 12, 2026
67b87a4
Additional precursor guard
americalexander Aug 12, 2026
8a19f6c
Blacken
americalexander Aug 12, 2026
21a0d78
Fix calibrate_component precursor calls
americalexander Aug 12, 2026
0437b70
Add temporary diagnostic tracing
americalexander Aug 12, 2026
2f58828
More diagnostic logging
americalexander Aug 12, 2026
6563525
Change to default tables_to_invalidate
americalexander Aug 12, 2026
210454e
Change when table invalidation occurs
americalexander Aug 12, 2026
e8f6a33
Fix SP checkpointing
americalexander Aug 12, 2026
fb923bb
fixing resume after and rng calib invariance
dhensle Aug 14, 2026
92bbc84
fixing pipeline and rng restarting issues
dhensle Aug 14, 2026
af1e21b
fixing more pipeline restart issues
dhensle Aug 17, 2026
022b4fb
appropriately handling crashing in middle of iteration
dhensle Aug 19, 2026
d79517e
refactor into separate files
dhensle Aug 19, 2026
ce7844f
if everything is converged, will continue to run downstream models
dhensle Aug 19, 2026
e9dcffb
blacken
dhensle Aug 19, 2026
adf17f9
remove coefficient rollback functionality
dhensle Aug 19, 2026
66e1037
better handling of internal memory handling between iterations
dhensle Aug 20, 2026
1f55863
run modes and resume after tests
dhensle Aug 20, 2026
97fbf16
nailing down resume_after and iteration counting
dhensle Aug 25, 2026
910a0e0
blacken
dhensle Aug 25, 2026
2ef9828
removing unused calibration.yaml settings
dhensle Aug 25, 2026
cd8f8b5
clarifying resume_after readme
dhensle Aug 25, 2026
896fc06
aligning tests and language around restart functionality
dhensle Aug 26, 2026
f414f23
setting checking cleanup
dhensle Aug 26, 2026
e2ee5b5
Merge remote-tracking branch 'upstream/main' into calibration
dhensle Aug 26, 2026
6c59ee2
addressing RNG and resume review comments
dhensle Aug 26, 2026
0b953b2
clearer logging at max iterations
dhensle Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions activitysim/abm/models/initialize.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -154,12 +154,19 @@ def initialize_households(
households = state.get_dataframe("households")
assert not households._is_view
chunk_sizer.log_df(trace_label, "households", households)
del households
chunk_sizer.log_df(trace_label, "households", None)

persons = state.get_dataframe("persons")
assert not persons._is_view
chunk_sizer.log_df(trace_label, "persons", persons)

rng = state.get_rn_generator()
if "households" not in rng.channels:
rng.add_channel("households", households)
if "persons" not in rng.channels:
rng.add_channel("persons", persons)

del households
chunk_sizer.log_df(trace_label, "households", None)
del persons
chunk_sizer.log_df(trace_label, "persons", None)

Expand Down
28 changes: 24 additions & 4 deletions activitysim/abm/models/settings_checker.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,24 +4,26 @@
from pydantic import BaseModel as PydanticBase
from typing import Type, Optional

from activitysim.core import config
from activitysim.core.calibration.settings import (
CALIBRATION_SETTINGS_FILE_NAME,
CalibrationConfig,
)
from activitysim.core.configuration.base import PydanticReadable

# import core settings
from activitysim.core.configuration.logit import (
LogitNestSpec,
TourLocationComponentSettings,
TourModeComponentSettings,
TemplatedLogitComponentSettings,
)
from activitysim.core import config
from activitysim.core.configuration.network import NetworkSettings
from activitysim.core.exceptions import ModelConfigurationError
from activitysim.core.workflow import State
from activitysim.core.simulate import (
eval_coefficients,
eval_nest_coefficients,
read_model_coefficient_template,
)
from activitysim.core.exceptions import ModelConfigurationError

# import model settings
from activitysim.abm.models.accessibility import AccessibilitySettings
Expand DownExpand Up@@ -637,6 +639,24 @@ def check_model_settings(
# Collect all errors
all_errors = []

# calibration.yaml is optional and is not itself a model step, so validate
# it explicitly using the same Pydantic/error-aggregation path as model
# settings files.
try:
CalibrationConfig.read_settings_file(
state.filesystem,
CALIBRATION_SETTINGS_FILE_NAME,
mandatory=False,
)
except Exception as error:
all_errors.append(
SettingsCheckerError(
"calibration",
error,
CALIBRATION_SETTINGS_FILE_NAME,
)
)

# additional logging set up
formatter = logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
Expand Down
60 changes: 57 additions & 3 deletions activitysim/cli/run.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@

import numpy as np

from activitysim.core import chunk, config, mem, timing, tracing, workflow
from activitysim.core import calibration, chunk, config, mem, timing, tracing, workflow
from activitysim.core.configuration import FileSystem, Settings
from activitysim.core.run_id import RunId

Expand DownExpand Up@@ -336,8 +336,13 @@ def run(args):
resume_after = state.settings.resume_after

# cleanup if not resuming
if not resume_after:
preserve_calibration_outputs = calibration.calibration_run_should_preserve_outputs(
state
)
if not resume_after and not preserve_calibration_outputs:
cleanup_output_files(state)
elif preserve_calibration_outputs:
logger.info("preserving output files based on calibration restart preflight")
elif state.settings.cleanup_trace_files_on_resume:
tracing.delete_trace_files(state)

Expand DownExpand Up@@ -418,7 +423,56 @@ def run(args):
check_model_settings(state, extension_settings=extension_checker_settings)

try:
if state.settings.multiprocess:
if calibration.calibration_enabled(state):
logger.info("evaluate calibration workflow")

calibration_result = calibration.run_calibration_loop(
state=state,
models=state.settings.models,
)

if calibration_result.model_system_ran:
logger.info(
"calibration workflow complete converged=%s "
"completed_global_iterations=%s configured_global_iterations=%s",
calibration_result.converged,
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)
else:
if (
calibration_result.completed_global_iterations
>= calibration_result.configured_global_iterations
):
logger.info(
"calibration workflow skipped: no model system steps were run "
"because completed_global_iterations=%s has reached the "
"configured run.global_iterations=%s limit. Increase "
"run.global_iterations to request additional iterations, or "
"remove output/calibration/calibration_progress.json to start a fresh run.",
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)
else:
logger.info(
"calibration workflow skipped: no model system steps were run "
"because the prior calibration run is already complete "
"(completed_global_iterations=%s, run.global_iterations=%s). "
"Change run.global_iterations to a new value above the completed "
"count to request additional iterations, or remove "
"output/calibration/calibration_progress.json to start a fresh run.",
calibration_result.completed_global_iterations,
calibration_result.configured_global_iterations,
)

if state.settings.cleanup_pipeline_after_run:
state.checkpoint.cleanup()
else:
state.checkpoint.close_store()

mem.log_global_hwm()

elif state.settings.multiprocess:
logger.info("run multiprocess simulation")

from activitysim.core import mp_tasks
Expand Down
122 changes: 122 additions & 0 deletions activitysim/core/calibration/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
# ActivitySim calibration package

This package implements ActivitySim's automated coefficient calibration workflow.
The modules are divided by responsibility so orchestration, model execution, and
calibration math can evolve independently.

- `__init__.py` defines the public API and compatibility access to former private
attributes from `activitysim.core.calibration`.
- `settings.py` defines calibration configuration models, result types, and
configuration loading.
- `orchestrator.py` coordinates global iterations and model-component sequencing.
- `component.py` runs component iterations, validates calibration specifications,
and calculates coefficient updates.
- `expressions.py` builds expression contexts, evaluates target/model expressions,
and loads optional helper modules.
- `coefficients.py` locates, reads, and writes model coefficient files.
- `reporting.py` writes iteration histories, summaries, plots, and final snapshots.
- `recovery.py` durably tracks calibration progress. Coefficient files are the
authoritative current state and are never rolled back when a run resumes.
- `execution.py` restores pipeline state and dispatches model execution.
- `multiprocess.py` contains subprocess orchestration, shared-resource setup, and
multiprocess pipeline restoration.

Component settings may specify `model_settings_file` when a workflow step does
not follow the conventional `<component_name>.yaml` naming pattern. Components
that share model settings may point to the same coefficient file.

## Global iterations, recovery attempts, and `resume_after`

`calibration.yaml` `run.global_iterations` is the desired total number of
completed logical calibration iterations, not the number to execute on each
ActivitySim invocation. The run-control contract is:

1. An unchanged setting on a completed run is a no-op, detected before normal
output cleanup so the pipeline and final outputs are preserved.
2. If a completed run's setting is changed to a value greater than the number
actually completed, calibration continues until the new total. This includes
lowering a previous maximum after early convergence, such as changing 5 to 3
after convergence completed iteration 2.
3. A changed setting less than or equal to the number already completed is a
no-op; completed coefficient updates are never undone implicitly.
4. Top-level `settings.yaml` `resume_after` has normal ActivitySim semantics for
the first global iteration entered by the current invocation. Later global
iterations ignore it and execute every calibrated component.
5. A global iteration counts only if it has at least one durable calibrated
component result, either from the current attempt or an earlier attempt of
that same logical iteration.
6. `global_iterations` cannot be lowered below an interrupted iteration because
its coefficient files may already contain updates from that iteration. The
run stops with instructions to resume the iteration or deliberately reset
progress and coefficients.
7. Startup logs report the detected completed count, requested target, selected
action, starting iteration and attempt, and `resume_after` value.

Calibration records the state needed to apply this contract in
`output/calibration/calibration_progress.json`.

Recovery attempts are distinct from global iterations. The first execution of a
global iteration is attempt 1. Restarting an interrupted global iteration creates
attempt 2, then attempt 3 if another restart is needed. A recovery attempt does
not consume an additional global iteration.

Calibration uses only the top-level `settings.yaml` `resume_after` setting. On
the first global iteration executed by an ActivitySim invocation, including a
new attempt of an interrupted iteration, it behaves like `resume_after` in a
non-calibration run:

- when set to a model name, that model is treated as complete, its checkpoint is
restored, and execution begins with the following model; and
- when unset or `null`, execution starts at the beginning of the top-level
`models` list. Calibration does not automatically continue after the last
model completed by the preceding attempt.

A named value must occur in the top-level `models` list and must identify a
model-level checkpoint. Calibration does not define a special `initialize`
value. A value such as `initialize_landuse` has ordinary model-name semantics:
that model is skipped and execution begins with the next model. The `_` shorthand
for the last checkpoint is not accepted in calibration mode.

`resume_after` affects only the first global iteration entered by the current
invocation. Calibrated models at or before a named resume point are skipped in
that iteration. Any later global iterations in the same invocation ignore
`resume_after`, restart immediately before the first calibrated model, and run
the normal complete calibration sequence.

For example, suppose global iteration 3 attempt 1 was interrupted after
calibrated `model_a` completed:

- `resume_after: model_a` starts attempt 2 after model A, preserving its
attempt-1 result; and
- `resume_after: null` starts attempt 2 at the beginning of the complete model
list, so model A runs again.

Coefficient files are the authoritative current state and are never rolled back
during recovery. Updates written before the interruption, along with subsequent
manual edits, become the starting coefficients for the new attempt. Rewinding
across a completed calibrated model therefore applies another calibration update
to its current coefficients. Calibration logs a warning when `resume_after`
causes this kind of rewind.

Iteration histories are append-only across attempts. Standard record, summary,
and generic-report rows include `global_iter`, `attempt`, and `component_iter`,
so a rerun does not replace the coefficient transition written by an earlier
attempt. The coefficient trajectory plots show the complete sequence, including
the initial value and every subsequent update. X-axis labels use the compact
form `G<global>-A<attempt>-C<component>`, for example `G3-A2-C1`.

After each calibrated component completes, the progress file records its attempt,
component-iteration count, and convergence result. A restart that skips that
component retains this state. This allows a run that crashed in downstream,
non-calibrated models to finish the same terminal global iteration without losing
the calibrated components' convergence decision.

Calibration is marked complete only after the terminal global iteration has run
all remaining models in the top-level `models` list and the final coefficient
snapshot has been written. A crash after the last calibrated model but before the
last production model therefore leaves the global iteration in progress.

Dependencies should flow from `orchestrator.py` into the focused modules. Lower
level modules should not import the orchestrator. Multiprocess code imports back
into `execution.py` only inside the single-component runner to avoid an import-time
cycle.
71 changes: 71 additions & 0 deletions activitysim/core/calibration/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
# ActivitySim
# See full license in LICENSE.txt.
"""Automated calibration support for ActivitySim model runs.

The public API is intentionally small. Private attributes from the former
single-file module remain available through ``__getattr__`` for compatibility
while callers migrate to the package modules.
"""

from . import (
coefficients,
component,
execution,
expressions,
multiprocess,
recovery,
reporting,
settings,
)
from .orchestrator import calibration_run_should_preserve_outputs, run_calibration_loop
from .settings import (
CalibrationComponentResult,
CalibrationComponentSettings,
CalibrationConfig,
CalibrationReportsSettings,
CalibrationRunResult,
CalibrationRunSettings,
calibration_enabled,
read_calibration_settings,
)

__all__ = [
"CalibrationComponentResult",
"CalibrationComponentSettings",
"CalibrationConfig",
"CalibrationReportsSettings",
"CalibrationRunResult",
"CalibrationRunSettings",
"calibration_run_should_preserve_outputs",
"calibration_enabled",
"read_calibration_settings",
"run_calibration_loop",
]

_COMPATIBILITY_MODULES = (
settings,
component,
expressions,
coefficients,
reporting,
recovery,
execution,
multiprocess,
)


def __getattr__(name):
for module in _COMPATIBILITY_MODULES:
if hasattr(module, name):
return getattr(module, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def __dir__():
compatibility_names = {
name
for module in _COMPATIBILITY_MODULES
for name in vars(module)
if not name.startswith("__")
}
return sorted(set(globals()) | compatibility_names)
Loading
Loading