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/*.parquetfiles 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.