Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Config inheritance

It is common to explore a family of closely related scenarios that only differ from one another by a handful of parameters — e.g., a baseline simulation and a variant with a new public-transit line. Each scenario is a full Pymetropolis configuration in its own right, but writing every scenario as an independent, complete TOML file duplicates every shared setting and makes it easy for the scenarios to silently drift apart.

The parent_config parameter lets a configuration inherit its values from another configuration, so a scenario file only needs to state how it differs from the one it is derived from.

Tip

For a complete, runnable example, see examples/extra/bottleneck-multi-scenario/ in the Pymetropolis repository: three Bottleneck case study scenarios chained with parent_config, each overriding only a few keys from the previous one.

Declaring a parent config

# scenario2.toml
main_directory = "scenario2/"
parent_config = "scenario1.toml"

[modes.car_driver]
alpha = 20
  • Allowed values: a string representing a valid path.
  • The path is resolved relative to the configuration file that declares it, not to the current working directory.
  • main_directory is not inherited: every configuration, including a derived one, must still declare its own main_directory.

The parent configuration (scenario1.toml above) is an ordinary, standalone configuration file — it can be run on its own, exactly as if parent_config did not exist. A configuration can itself declare a parent_config, forming a chain of any length (scenario3.toml inheriting from scenario2.toml inheriting from scenario1.toml, and so on); a chain cannot be cyclic.

How parameters are resolved

A configuration key is looked up in the derived configuration file first; if it is not defined there, it is looked up in the parent configuration file, then the parent’s own parent, and so on up the chain. In other words, a parameter defined anywhere in the chain applies, and the nearest definition wins.

Given the example above, scenario2.toml only overrides modes.car_driver.alpha — every other parameter ( [road_network], [od_matrix], [departure_time_choice], etc.) keeps whatever parameter scenario1.toml gives it. A third configuration inheriting from scenario2.toml would in turn see modes.car_driver.alpha from scenario2.toml, and everything else still from scenario1.toml.

This inheritance also applies to extra populations, main_population, and custom_steps: a derived configuration only needs to list the populations or custom-step files it adds on top of its parent’s.

Note

Relative paths and secret values are always resolved against the configuration file that actually defines them, whichever configuration of the chain that is — never against the derived configuration’s own directory or secrets file. This way, a path or secret written in a parent configuration always resolves the same way, whether that configuration is run directly or only inherited from.

Avoiding redundant work

Beyond simply saving you from repeating shared settings, parent_config also lets Pymetropolis reuse a parent’s outputs instead of recomputing them: whenever a Step’s configuration and inputs are unaffected by whatever a derived configuration overrides, that Step is not run again — it is read from wherever it was (or will be) computed for the nearest ancestor that already defines it identically.

Continuing the example: since scenario2.toml only changes modes.car_driver.alpha and the linear-schedule penalties, every Step upstream of those parameters — the road network, the synthetic population, the OD matrix — is reused unchanged from scenario1.toml; only the demand and simulation Steps that actually depend on the changed parameters are recomputed. A third scenario changing only departure_time_choice.mu on top of scenario2.toml would in turn reuse everything up to and including the departure-time choice inputs from scenario2.toml (not scenario1.toml, since those Steps’ parameters were changed there), and only recompute the departure-time choice and simulation Steps themselves.

Run with --dry-run to see which Steps are reused, and from which configuration:

pymetropolis --dry-run scenario3.toml
1. GridNetworkStep [up to date] (from: scenario1.toml)
...
5. LinearScheduleStep [up to date] (from: scenario2.toml)
...

A reused Step’s output is not duplicated into the derived configuration’s own main_directory: it is read from, and if it needs to be (re)computed, written into, the ancestor configuration’s own main_directory — even if that ancestor has never actually been run. This means a scenario at the end of a long chain can be run entirely on its own, without running any of its ancestors first: running it computes whichever ancestor Steps are still missing, writing each one under the main_directory of the configuration that owns it, so that a sibling scenario derived from the same ancestor (or the ancestor itself) later finds it already available and never recomputes it either.