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

Multiple-demand

So far, a Pymetropolis configuration has described a single, homogeneous demand: one set of trip-generation rules, one set of mode preferences, one set of departure-time preferences, applied to every agent in the simulation.

In practice, you often need to simulate several demand sources at once, each with its own logic. A common example is combining a synthetic population of persons (generated from a survey-based pipeline such as Eqasim) with a freight fleet described by an origin-destination matrix: persons and trucks have completely different trip-generation processes, different available modes, and different preferences, yet they compete for the same road capacity and must be simulated together for congestion to be modeled correctly.

Pymetropolis supports this by letting a single simulation combine several populations. Each population has its own, fully independent demand-side configuration — reusing the exact same TOML keys you already know from a single-demand setup — while sharing everything that is not demand-specific: the road network, the simulation period, the METROPOLIS2 executables, and so on.

The main population

Every Pymetropolis configuration already defines one population implicitly: whatever demand-related sections ([synthetic_population], [node_od_matrix], [mode_choice], [modes], [departure_time], etc.) you put directly in the main configuration file. This is called the main population.

Everything you already know about configuring the demand side of a simulation applies, unchanged, to the main population. Multiple-demand support is purely additive: you keep writing your main configuration file exactly as before, and simply add one or more extra populations alongside it.

Adding extra populations

An extra population is declared with the extra_populations parameter, which lists the paths (relative to the main configuration file) to one TOML file per extra population.

# config.toml
extra_populations = ["persons.toml", "trucks.toml"]

Each of these files is a standalone configuration file for that population’s demand side. It uses the exact same keys as a single-demand configuration — [synthetic_population], [node_od_matrix], [mode_choice], [modes], [departure_time], [departure_time_choice], etc. — with no renaming or extra nesting required. The only addition is a mandatory population_name key at the top of the file, giving that population a unique name.

# trucks.toml
population_name = "trucks"

[node_od_matrix]
each = 200

[mode_choice]
modes = ["car_driver"]

[modes.car_driver]
alpha = 25

Pymetropolis runs the entire demand-side pipeline independently for every population, using the exact same Steps as a single-demand configuration. Every intermediate MetroFile produced along the way is stored under a subdirectory of main_directory named after the population — for example demand/trucks/ for the trucks population above, or demand/population/ for the main population — so populations never collide with one another even though they go through identical Steps.

Each population’s copy of a demand Step also needs a name of its own, distinct from the other populations’. For the main population, this name is simply the Step’s class name (e.g. NodeODMatrixStep); for an extra population, it is the population name and the class name joined with a double underscore, e.g. trucks__NodeODMatrixStep.

Population names

Population names must be:

  • Unique across the main population and every extra population.
  • Free of the - (dash) character (it is used internally to keep agent and trip identifiers unique once populations are combined — see below).
  • Different from "population", which is reserved for the main population.

Disabling the main population

Sometimes you do not want the main configuration file to define a population of its own — for example, if all of your demand comes from extra populations and the main file should only hold shared settings. Set main_population to false to disable it:

# config.toml
main_population = false
extra_populations = ["persons.toml", "trucks.toml"]

With main_population = false, any [synthetic_population], [node_od_matrix], [mode_choice], etc. sections in the main file are simply ignored, and only the listed extra populations are simulated.

Shared parameters

Most configuration keys are population-specific: if a key is not defined in a given population’s own file, it is left undefined for that population, even if the main configuration file happens to define the same key (because it applies to the main population). This keeps populations fully isolated from one another by default.

A small number of keys are shared instead (e.g., crs, random_seed). For these, if a population’s own file does not set the key, its value is inherited from the main configuration file. This lets you define the projected CRS or the random seed once, in the main file, and have it apply to every population, while still allowing an individual population to override it if needed.

Everything related to the road network, the simulation itself ([simulation], [grid_network], [road_network], etc.) and the METROPOLIS2 executables ([metropolis_core]) is defined only once, in the main configuration file: it is not population-specific and extra-population files should not repeat it.

Example

Combining a synthetic population of persons (the main population) with a truck fleet described by an origin-destination matrix (an extra population) could look like this:

Tip

For a complete, runnable config, see examples/extra/bottleneck-multi-demand/ in the Pymetropolis repository: the Bottleneck case study split into two populations with different desired arrival times.

# config.toml
main_directory = "my-simulation/"
random_seed = 123454321

extra_populations = ["trucks.toml"]

[road_network]
default_speed_limit = 90

[synthetic_population]
eqasim_output = "eqasim-output/"

[mode_choice]
modes = ["car_driver", "public_transit", "walking"]

[simulation]
period = [06:00:00, 10:00:00]
recording_interval = 60
learning_factor = 0.1
nb_iterations = 200

[metropolis_core]
exec_path = "execs/metropolis_cli"
routing_exec_path = "execs/routing_cli"
# trucks.toml
population_name = "trucks"

[node_od_matrix]
each = 200

[mode_choice]
modes = ["car_driver"]

[modes.car_driver]
alpha = 25

Here, trucks.toml does not repeat random_seed, [road_network], [simulation], or [metropolis_core]: these are read from the main file, either because they are shared parameters (random_seed) or because they are not population-specific at all (everything else). The truck population has its own, much simpler mode choice (car only) and a completely different trip-generation process (a fixed-size origin-destination matrix rather than a synthetic population), independent of the persons’ demand.

How populations are combined

Merged simulation input

Metropolis-Core itself has no notion of “populations”: it simulates a single, homogeneous set of agents on a shared road network. Pymetropolis therefore merges every population’s agents, trips, and alternatives into the single run/input/agents.parquet file (and similarly for trips and alternatives) before running the simulation, via WriteMetroAgentsStep, WriteMetroTripsStep, and WriteMetroAlternativesStep.

Agent and trip identifiers are only guaranteed unique within their own population, so Pymetropolis prefixes them with the population name when merging (e.g. trucks-102, population-102 for a trip of the main population). This is why population names cannot contain the - character: it is used as the separator between the population name and the original identifier.

Note

This identifier prefixing is only visible if you inspect the merged run/input/*.parquet files directly. It does not affect the population-specific files.

Results

Result and post-processing steps run once per population as well, and read each population’s own rows back out of the merged simulation output. Population-specific result files are stored under a subdirectory named after the population, e.g. results/trucks/trip_results.parquet and results/population/trip_results.parquet for the example above. Aggregate results that are not population-specific (e.g. convergence graphs) remain shared, in results/ directly.