How do I sweep over scenario shapes that axes / apply can’t express?

How do I sweep over scenario shapes that axes / apply can’t express?#

run_sweep(base, axes=..., apply=...) is the right tool when every trial is the same scenario with one or two knobs turned. Sometimes trials differ in ways that don’t fit:

  • the journey (which stages to visit) changes per trial,

  • you want to add or remove elements per trial,

  • the geometry itself depends on a trial parameter.

For those, build each trial scenario explicitly with Scenario.copy() + direct edits, then hand them to run_sweep_from_factory.

import logging
from datetime import datetime

logging.getLogger("jupedsim_scenarios").setLevel(logging.WARNING)
print(f"Executed on {datetime.now().strftime('%d.%m.%Y, %H:%M')}")

from jupedsim_scenarios import load_scenario, run_sweep_from_factory

base = load_scenario("../assets/bottleneck.zip")
Executed on 21.07.2026, 09:46

Factory: one fresh Scenario per trial#

The factory receives the trial-parameters dict and returns a Scenario. Start from base.copy() so each trial is isolated from the others, then change whatever you need.

def make_trial(params):
    scenario = base.copy()
    scenario.set_agent_count(0, params["agents"])
    scenario.set_agent_params(0, desired_speed=params["speed"])
    scenario.max_simulation_time = params["horizon"]
    scenario.source_path = params["label"]
    return scenario


trials = [
    {"label": "calm",        "agents": 20, "speed": 1.0, "horizon": 60},
    {"label": "normal",      "agents": 30, "speed": 1.3, "horizon": 60},
    {"label": "rushed",      "agents": 30, "speed": 1.8, "horizon": 45},
    {"label": "overcrowded", "agents": 50, "speed": 1.3, "horizon": 60},
]

sweep = run_sweep_from_factory(
    make_trial,
    trials=trials,
    seeds=[100, 101, 102],
    workers=2,
)

df = sweep.to_dataframe()[["label", "agents", "speed", "seed", "evacuation_time", "success"]]
df.head(8)
INFO - Using fallback logic: No journeys defined
INFO - Processing with parameters: {'number': 20, 'radius': 0.2, 'v0': 1.0, 'flow_start_time': 0, 'flow_end_time': 10, 'percentage': None, 'distribution_mode': 'by_number', 'use_flow_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'v0_distribution': 'constant', 'desired_speed': 1.0}
INFO - Using default parameters: v0=1.0, radius=0.2, n_agents=20
INFO - Distribution jps-distributions_0: {'number': 20, 'radius': 0.2, 'v0': 1.0, 'distribution_mode': 'by_number', 'percentage': None, 'use_flow_spawning': False, 'flow_start_time': 0, 'flow_end_time': 10, 'strict_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'radius_std': None, 'v0_distribution': 'constant', 'v0_std': None}
INFO - Added 20 agents using fallback logic (immediate), prepared 0 flow sources
INFO - Using fallback logic: No journeys defined
INFO - Processing with parameters: {'number': 20, 'radius': 0.2, 'v0': 1.0, 'flow_start_time': 0, 'flow_end_time': 10, 'percentage': None, 'distribution_mode': 'by_number', 'use_flow_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'v0_distribution': 'constant', 'desired_speed': 1.0}
INFO - Using default parameters: v0=1.0, radius=0.2, n_agents=20
INFO - Distribution jps-distributions_0: {'number': 20, 'radius': 0.2, 'v0': 1.0, 'distribution_mode': 'by_number', 'percentage': None, 'use_flow_spawning': False, 'flow_start_time': 0, 'flow_end_time': 10, 'strict_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'radius_std': None, 'v0_distribution': 'constant', 'v0_std': None}
INFO - Added 20 agents using fallback logic (immediate), prepared 0 flow sources
INFO - Using fallback logic: No journeys defined
INFO - Processing with parameters: {'number': 20, 'radius': 0.2, 'v0': 1.0, 'flow_start_time': 0, 'flow_end_time': 10, 'percentage': None, 'distribution_mode': 'by_number', 'use_flow_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'v0_distribution': 'constant', 'desired_speed': 1.0}
INFO - Using default parameters: v0=1.0, radius=0.2, n_agents=20
INFO - Distribution jps-distributions_0: {'number': 20, 'radius': 0.2, 'v0': 1.0, 'distribution_mode': 'by_number', 'percentage': None, 'use_flow_spawning': False, 'flow_start_time': 0, 'flow_end_time': 10, 'strict_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'radius_std': None, 'v0_distribution': 'constant', 'v0_std': None}
INFO - Added 20 agents using fallback logic (immediate), prepared 0 flow sources
INFO - Using fallback logic: No journeys defined
INFO - Processing with parameters: {'number': 30, 'radius': 0.2, 'v0': 1.3, 'flow_start_time': 0, 'flow_end_time': 10, 'percentage': None, 'distribution_mode': 'by_number', 'use_flow_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'v0_distribution': 'constant', 'desired_speed': 1.3}
INFO - Using default parameters: v0=1.3, radius=0.2, n_agents=30
INFO - Distribution jps-distributions_0: {'number': 30, 'radius': 0.2, 'v0': 1.3, 'distribution_mode': 'by_number', 'percentage': None, 'use_flow_spawning': False, 'flow_start_time': 0, 'flow_end_time': 10, 'strict_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'radius_std': None, 'v0_distribution': 'constant', 'v0_std': None}
INFO - Added 30 agents using fallback logic (immediate), prepared 0 flow sources
INFO - Using fallback logic: No journeys defined
INFO - Processing with parameters: {'number': 30, 'radius': 0.2, 'v0': 1.3, 'flow_start_time': 0, 'flow_end_time': 10, 'percentage': None, 'distribution_mode': 'by_number', 'use_flow_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'v0_distribution': 'constant', 'desired_speed': 1.3}
INFO - Using default parameters: v0=1.3, radius=0.2, n_agents=30
INFO - Distribution jps-distributions_0: {'number': 30, 'radius': 0.2, 'v0': 1.3, 'distribution_mode': 'by_number', 'percentage': None, 'use_flow_spawning': False, 'flow_start_time': 0, 'flow_end_time': 10, 'strict_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'radius_std': None, 'v0_distribution': 'constant', 'v0_std': None}
INFO - Added 30 agents using fallback logic (immediate), prepared 0 flow sources
INFO - Using fallback logic: No journeys defined
INFO - Processing with parameters: {'number': 30, 'radius': 0.2, 'v0': 1.3, 'flow_start_time': 0, 'flow_end_time': 10, 'percentage': None, 'distribution_mode': 'by_number', 'use_flow_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'v0_distribution': 'constant', 'desired_speed': 1.3}
INFO - Using default parameters: v0=1.3, radius=0.2, n_agents=30
INFO - Distribution jps-distributions_0: {'number': 30, 'radius': 0.2, 'v0': 1.3, 'distribution_mode': 'by_number', 'percentage': None, 'use_flow_spawning': False, 'flow_start_time': 0, 'flow_end_time': 10, 'strict_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'radius_std': None, 'v0_distribution': 'constant', 'v0_std': None}
INFO - Added 30 agents using fallback logic (immediate), prepared 0 flow sources
INFO - Using fallback logic: No journeys defined
INFO - Processing with parameters: {'number': 30, 'radius': 0.2, 'v0': 1.8, 'flow_start_time': 0, 'flow_end_time': 10, 'percentage': None, 'distribution_mode': 'by_number', 'use_flow_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'v0_distribution': 'constant', 'desired_speed': 1.8}
INFO - Using default parameters: v0=1.8, radius=0.2, n_agents=30
INFO - Distribution jps-distributions_0: {'number': 30, 'radius': 0.2, 'v0': 1.8, 'distribution_mode': 'by_number', 'percentage': None, 'use_flow_spawning': False, 'flow_start_time': 0, 'flow_end_time': 10, 'strict_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'radius_std': None, 'v0_distribution': 'constant', 'v0_std': None}
INFO - Added 30 agents using fallback logic (immediate), prepared 0 flow sources
INFO - Using fallback logic: No journeys defined
INFO - Processing with parameters: {'number': 30, 'radius': 0.2, 'v0': 1.8, 'flow_start_time': 0, 'flow_end_time': 10, 'percentage': None, 'distribution_mode': 'by_number', 'use_flow_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'v0_distribution': 'constant', 'desired_speed': 1.8}
INFO - Using default parameters: v0=1.8, radius=0.2, n_agents=30
INFO - Distribution jps-distributions_0: {'number': 30, 'radius': 0.2, 'v0': 1.8, 'distribution_mode': 'by_number', 'percentage': None, 'use_flow_spawning': False, 'flow_start_time': 0, 'flow_end_time': 10, 'strict_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'radius_std': None, 'v0_distribution': 'constant', 'v0_std': None}
INFO - Added 30 agents using fallback logic (immediate), prepared 0 flow sources
INFO - Using fallback logic: No journeys defined
INFO - Processing with parameters: {'number': 30, 'radius': 0.2, 'v0': 1.8, 'flow_start_time': 0, 'flow_end_time': 10, 'percentage': None, 'distribution_mode': 'by_number', 'use_flow_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'v0_distribution': 'constant', 'desired_speed': 1.8}
INFO - Using default parameters: v0=1.8, radius=0.2, n_agents=30
INFO - Distribution jps-distributions_0: {'number': 30, 'radius': 0.2, 'v0': 1.8, 'distribution_mode': 'by_number', 'percentage': None, 'use_flow_spawning': False, 'flow_start_time': 0, 'flow_end_time': 10, 'strict_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'radius_std': None, 'v0_distribution': 'constant', 'v0_std': None}
INFO - Added 30 agents using fallback logic (immediate), prepared 0 flow sources
INFO - Using fallback logic: No journeys defined
INFO - Processing with parameters: {'number': 50, 'radius': 0.2, 'v0': 1.3, 'flow_start_time': 0, 'flow_end_time': 10, 'percentage': None, 'distribution_mode': 'by_number', 'use_flow_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'v0_distribution': 'constant', 'desired_speed': 1.3}
INFO - Using default parameters: v0=1.3, radius=0.2, n_agents=50
INFO - Distribution jps-distributions_0: {'number': 50, 'radius': 0.2, 'v0': 1.3, 'distribution_mode': 'by_number', 'percentage': None, 'use_flow_spawning': False, 'flow_start_time': 0, 'flow_end_time': 10, 'strict_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'radius_std': None, 'v0_distribution': 'constant', 'v0_std': None}
INFO - Added 50 agents using fallback logic (immediate), prepared 0 flow sources
INFO - Using fallback logic: No journeys defined
INFO - Processing with parameters: {'number': 50, 'radius': 0.2, 'v0': 1.3, 'flow_start_time': 0, 'flow_end_time': 10, 'percentage': None, 'distribution_mode': 'by_number', 'use_flow_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'v0_distribution': 'constant', 'desired_speed': 1.3}
INFO - Using default parameters: v0=1.3, radius=0.2, n_agents=50
INFO - Distribution jps-distributions_0: {'number': 50, 'radius': 0.2, 'v0': 1.3, 'distribution_mode': 'by_number', 'percentage': None, 'use_flow_spawning': False, 'flow_start_time': 0, 'flow_end_time': 10, 'strict_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'radius_std': None, 'v0_distribution': 'constant', 'v0_std': None}
INFO - Added 50 agents using fallback logic (immediate), prepared 0 flow sources
INFO - Using fallback logic: No journeys defined
INFO - Processing with parameters: {'number': 50, 'radius': 0.2, 'v0': 1.3, 'flow_start_time': 0, 'flow_end_time': 10, 'percentage': None, 'distribution_mode': 'by_number', 'use_flow_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'v0_distribution': 'constant', 'desired_speed': 1.3}
INFO - Using default parameters: v0=1.3, radius=0.2, n_agents=50
INFO - Distribution jps-distributions_0: {'number': 50, 'radius': 0.2, 'v0': 1.3, 'distribution_mode': 'by_number', 'percentage': None, 'use_flow_spawning': False, 'flow_start_time': 0, 'flow_end_time': 10, 'strict_spawning': False, 'use_premovement': False, 'premovement_distribution': 'gamma', 'premovement_param_a': None, 'premovement_param_b': None, 'premovement_seed': None, 'radius_distribution': 'constant', 'radius_std': None, 'v0_distribution': 'constant', 'v0_std': None}
INFO - Added 50 agents using fallback logic (immediate), prepared 0 flow sources
label agents speed seed evacuation_time success
0 calm 20 1.0 100 32.00 True
1 calm 20 1.0 101 28.61 True
2 calm 20 1.0 102 28.47 True
3 normal 30 1.3 100 33.01 True
4 normal 30 1.3 101 36.61 True
5 normal 30 1.3 102 35.76 True
6 rushed 30 1.8 100 28.82 True
7 rushed 30 1.8 101 30.49 True

Aggregate per trial label#

df.groupby("label").agg(
    mean_evac=("evacuation_time", "mean"),
    std_evac=("evacuation_time", "std"),
    success_rate=("success", "mean"),
)
mean_evac std_evac success_rate
label
calm 29.693333 1.998858 1.0
normal 35.126667 1.881710 1.0
overcrowded 54.240000 1.745766 1.0
rushed 30.286667 1.376311 1.0

Returning extras from the factory#

Return a (scenario, extras) tuple to attach arbitrary per-trial data — useful for analysis that needs metadata the sweep itself doesn’t track (e.g. the polygon you used, a description, a hash).

def make_trial_with_extras(params):
    scenario = base.copy()
    scenario.set_agent_count(0, params["agents"])
    notes = f"{params['agents']} agents, default speed"
    return scenario, {"notes": notes, "recipe_version": 2}


sweep_x = run_sweep_from_factory(
    make_trial_with_extras,
    trials=[{"agents": 20}, {"agents": 40}],
    seeds=[200],
    workers=1,
)

for trial in sweep_x:
    print(trial.axis_values, "->", trial.extras)
sweep_x.cleanup()
{'agents': 20} -> {'notes': '20 agents, default speed', 'recipe_version': 2}
{'agents': 40} -> {'notes': '40 agents, default speed', 'recipe_version': 2}
2

When to pick which sweep#

Use run_sweep

Use run_sweep_from_factory

One base scenario, knobs turned per trial.

Each trial constructs a different scenario shape.

Cartesian product of axes is what you want.

Trials are an explicit list of dicts, not a grid.

Mutations are 1–2 setter calls.

Mutations need conditionals or add_*/remove_*.

You want the dataframe columns named after axes.

You want richer per-trial metadata via extras.

sweep.cleanup()
12