@@ -290,6 +290,99 @@ def run_calibration_loop(
290290 state .filesystem .pipeline_file_name = original_pipeline_name
291291
292292
293+ def _get_pipeline_checkpoint_names (
294+ state : workflow .State ,
295+ pipeline_path ,
296+ ) -> list [str ]:
297+ """Read checkpoint names from a pipeline store file without fully restoring."""
298+ from activitysim .core .workflow .checkpoint import HdfStore , ParquetStore
299+
300+ pipeline_path = Path (pipeline_path )
301+ if state .settings .checkpoint_format == "hdf" :
302+ store = HdfStore (pipeline_path , mode = "r" )
303+ else :
304+ store = ParquetStore (pipeline_path , mode = "r" )
305+ try :
306+ return store .list_checkpoint_names ()
307+ finally :
308+ store .close ()
309+
310+
311+ def _resolve_checkpoint_name (
312+ state : workflow .State ,
313+ resume_after : str ,
314+ checkpoint_names : list [str ],
315+ ) -> str :
316+ """Resolve a model-name ``resume_after`` to an existing pipeline checkpoint.
317+
318+ In single-process pipelines model names are used directly as checkpoint
319+ names. In multiprocess pipelines only step-level checkpoint names (e.g.
320+ ``mp_households``) exist in the main pipeline store. This maps a model
321+ name to the enclosing multiprocess step checkpoint so the calibration
322+ module can restore the correct pipeline state.
323+ """
324+ from activitysim .core .exceptions import CheckpointNameNotFoundError
325+
326+ if resume_after in checkpoint_names :
327+ return resume_after
328+
329+ all_models = state .settings .models
330+ mp_steps = state .settings .multiprocess_steps
331+
332+ if not mp_steps or resume_after not in all_models :
333+ raise CheckpointNameNotFoundError (
334+ f"Checkpoint '{ resume_after } ' not found in pipeline and cannot "
335+ f"be resolved. Available checkpoints: { checkpoint_names } "
336+ )
337+
338+ resume_idx = all_models .index (resume_after )
339+ step_boundaries = [all_models .index (s .begin ) for s in mp_steps ]
340+ step_boundaries .append (len (all_models ))
341+
342+ for i , step in enumerate (mp_steps ):
343+ if step_boundaries [i ] <= resume_idx < step_boundaries [i + 1 ]:
344+ if step .name in checkpoint_names :
345+ return step .name
346+ # Also check for calibration-prefixed step names from a prior
347+ # calibration run's multiprocess steps.
348+ for cp in checkpoint_names :
349+ if cp .startswith (f"calibration_{ step .name } " ):
350+ return cp
351+ break
352+
353+ raise CheckpointNameNotFoundError (
354+ f"Cannot resolve '{ resume_after } ' to any available checkpoint. "
355+ f"Available: { checkpoint_names } "
356+ )
357+
358+
359+ def _effective_resume_index (
360+ state : workflow .State ,
361+ checkpoint_name : str ,
362+ ) -> int :
363+ """Return the index of the last model covered by a checkpoint.
364+
365+ For model-level checkpoints this is the model's own index in
366+ ``settings.models``. For step-level checkpoints this is the index of
367+ the last model in the enclosing multiprocess step.
368+ """
369+ all_models = state .settings .models
370+ if checkpoint_name in all_models :
371+ return all_models .index (checkpoint_name )
372+
373+ mp_steps = state .settings .multiprocess_steps or []
374+ step_boundaries = [all_models .index (s .begin ) for s in mp_steps ]
375+ step_boundaries .append (len (all_models ))
376+
377+ for i , step in enumerate (mp_steps ):
378+ if step .name == checkpoint_name or (
379+ checkpoint_name .startswith (f"calibration_{ step .name } " )
380+ ):
381+ return step_boundaries [i + 1 ] - 1
382+
383+ return - 1
384+
385+
293386def _run_precursor_components (
294387 state : workflow .State ,
295388 models : list [str ],
@@ -306,7 +399,11 @@ def _run_precursor_components(
306399 prior_pipeline = state .checkpoint .store .filename
307400 state .checkpoint .close_store ()
308401 state .filesystem .pipeline_file_name = f"pipeline_calibration_iter_{ global_iter } "
309- state .checkpoint .restore_from (prior_pipeline , checkpoint_name = resume_after )
402+ # In multiprocess pipelines the model-level checkpoint may not exist;
403+ # resolve to the enclosing step checkpoint.
404+ checkpoint_names = _get_pipeline_checkpoint_names (state , prior_pipeline )
405+ resolved = _resolve_checkpoint_name (state , resume_after , checkpoint_names )
406+ state .checkpoint .restore_from (prior_pipeline , checkpoint_name = resolved )
310407 else :
311408 _run_in_configured_mode (
312409 state ,
@@ -1394,7 +1491,29 @@ def _run_in_configured_mode(
13941491 if not models :
13951492 return
13961493
1494+ # Remember the original last model — downstream code (e.g.
1495+ # _calibrate_component) references it as a checkpoint name.
1496+ original_last_model = models [- 1 ]
1497+
13971498 if state .settings .multiprocess :
1499+ # In multiprocess mode the checkpoint system uses step-level names,
1500+ # so model-level resume_after cannot be passed through directly.
1501+ # Instead, trim the models list to exclude those already completed.
1502+ if resume_after :
1503+ all_models = state .settings .models
1504+ if resume_after in all_models :
1505+ resume_idx = all_models .index (resume_after )
1506+ models = [m for m in models if all_models .index (m ) > resume_idx ]
1507+
1508+ if not models :
1509+ # All models are covered by the resume point — restore the
1510+ # pipeline state and create a model-level checkpoint so that
1511+ # downstream code can reference it by name.
1512+ if not state .checkpoint .store_is_open ():
1513+ state .checkpoint .restore (resume_after = "_" )
1514+ state .checkpoint .add (original_last_model )
1515+ return
1516+
13981517 _run_multiprocess_with_overrides (
13991518 state ,
14001519 models = models ,
@@ -1405,13 +1524,43 @@ def _run_in_configured_mode(
14051524 # Restore it into the parent process state so tables are accessible
14061525 # for calibration expression evaluation.
14071526 _restore_parent_state_from_pipeline (state )
1408- # Add a checkpoint named after the last model so that model-name
1409- # references (e.g. _prior_step_name, resume_after on global_iter > 1)
1410- # resolve correctly. Without this, only the step-level coalesce name
1411- # exists in the pipeline.
1412- state .checkpoint .add (models [ - 1 ] )
1527+ # Add a checkpoint named after the original last model so that
1528+ # model-name references (e.g. _prior_step_name, resume_after on
1529+ # global_iter > 1) resolve correctly. Without this, only the
1530+ # step-level coalesce name exists in the pipeline.
1531+ state .checkpoint .add (original_last_model )
14131532 return
14141533
1534+ # Single-process mode: resolve resume_after to an existing checkpoint
1535+ # when the pipeline has step-level checkpoints from a prior multiprocess
1536+ # run (model-level checkpoints won't exist in that pipeline).
1537+ if resume_after :
1538+ pipeline_path = state .checkpoint .default_pipeline_file_path ()
1539+ try :
1540+ checkpoint_names = _get_pipeline_checkpoint_names (
1541+ state , pipeline_path
1542+ )
1543+ except Exception :
1544+ checkpoint_names = []
1545+
1546+ if checkpoint_names and resume_after not in checkpoint_names :
1547+ resolved = _resolve_checkpoint_name (
1548+ state , resume_after , checkpoint_names
1549+ )
1550+ # The resolved checkpoint may cover more models than
1551+ # resume_after — trim the models list accordingly.
1552+ end_idx = _effective_resume_index (state , resolved )
1553+ if end_idx >= 0 :
1554+ all_models = state .settings .models
1555+ models = [m for m in models if all_models .index (m ) > end_idx ]
1556+
1557+ if not models :
1558+ state .checkpoint .restore (resolved )
1559+ state .checkpoint .add (original_last_model )
1560+ return
1561+
1562+ resume_after = resolved
1563+
14151564 state .run (
14161565 models = models ,
14171566 resume_after = resume_after ,
0 commit comments