Command line
The jupedsim-network command runs a scenario written as a JSON file
and writes the results to CSV and JSON files. It needs no Python code.
It calls the Python API, so a scenario gives the same results from the
file as from Python, byte for byte.
Install
pip install jupedsim-networkThis installs the jupedsim-network command. python -m jupedsim_network
runs the same command. The only dependency is still NumPy.
For coloured --help text, install the cli extra, which adds
rich-argparse:
pip install "jupedsim-network[cli]"Without it, --help prints plain text with the same groups. Every
command works either way.
For the terminal UI, which
edits, checks and runs a scenario file in forms, install the tui
extra:
pip install "jupedsim-network[tui]"First run
Write the scenario of Getting started
to scenario.json:
jupedsim-network initWrote scenario.json (the getting-started scenario).
Next steps:
1. Check it: jupedsim-network check scenario.json
2. Run it: jupedsim-network run scenario.json
3. Edit nodes, connections and populations for your building, then check and run again.
Check it:
jupedsim-network check scenario.jsonScenario scenario.json: 3 nodes, 2 links, 1 population.
Nodes:
0 office room 200 m²
1 flight stair 10.8 m²
2 street safe
Links:
0 office->flight door 0.78 persons/s
1 flight->street stair 0.91 persons/s
Populations:
0 office -> street
Trial run, seed 1: evacuation time 323.0 s; 120 of 120 agents safe.
The scenario is valid.
Check: the last line reads The scenario is valid. and the exit
status is 0 (echo $? prints 0).
check builds the network and the simulation, lists the nodes with
their area, the links with their capacity and the safe node that each
population’s route leads to, and makes one trial run. It writes no
files.
Run it:
jupedsim-network run scenario.jsonScenario scenario.json: 3 nodes, 2 links, 1 population.
Run, seed 1: evacuation time 323.0 s; 120 of 120 agents safe.
Monte Carlo, 500 runs, seed 1: q 0.5 383.25 s, q 0.95 550.05 s; 0 incomplete.
Wrote scenario_results/: summary.json, agents.csv, link_flow.csv, occupancy.csv, runs.csv
The 500 runs take 15–20 s on a laptop (Apple M3 Pro).
Check: the Monte Carlo line ends with 0 incomplete, and
scenario_results/ holds five files.
These are the numbers of Getting started: the door passes 0.78 persons/s and the stair link 0.91 persons/s, the run with seed 1 ends at 323.0 s, and over 500 runs the median is 383.25 s and the 95th percentile 550.05 s. Getting started explains them.
Try one change. Open scenario.json, set the width of the first
connection from 0.9 to 1.2, and run check again. The office->flight
link gets a larger capacity, but the evacuation time of the trial run
stays the same: the pre-movement times set it
(Try one change).
The four commands
| Command | What it does |
|---|---|
init [PATH] [--example NAME] [--force] | Writes a packaged scenario to PATH (default scenario.json). NAME is getting-started (default) or office-wing, the worked example. The file is copied unchanged. An existing file is kept unless you pass --force. |
check SCENARIO.json | Builds the scenario, lists it and makes one trial run with the seed of run.seed. Writes nothing. |
run SCENARIO.json [options] | Makes one run with run.seed, then a Monte Carlo set if the file has a monte_carlo section or --runs is given, and writes the result files. |
tui [SCENARIO.json] [--theme NAME] | Opens the terminal UI; needs the tui extra. |
Options of run. Each one overrides the matching key of the file.
| Option | Overrides |
|---|---|
-o DIR, --output DIR | Result directory; default <scenario stem>_results/ next to the scenario file |
--seed N | run.seed |
--runs N | monte_carlo.runs |
--mc-seed N | monte_carlo.seed |
--quantile Q | monte_carlo.quantiles; repeat it for several quantiles |
--no-monte-carlo | Runs the single realisation only |
run writes summary.json, agents.csv, link_flow.csv,
occupancy.csv and, with Monte Carlo, runs.csv to DIR, replacing
files of the same name; without Monte Carlo it removes an earlier
runs.csv. Other files stay. An error writes nothing and leaves earlier
results in DIR as they were.
check makes one run with one seed. A scenario that passes can still
fail with another seed, for example when a sampled speed is 0 or a
sampled count overfills its start node. run --runs 1000 can
therefore fail after check passed. “Valid” means that the file is
well formed and the API accepts it; it says nothing about whether the
model is valid for your building
(Limitations).The scenario file
This is the file that init writes. It describes the same building as
the Python code of
Getting started,
call for call:
{
"format_version": 1,
"description": "Getting started: an office of 200 m² with 120 occupants empties through a 0.9 m door into a stair flight, which leads to the street. Pre-movement times are log-normal with a mean of 120 s and a standard deviation of 60 s.",
"nodes": [
{
"name": "office",
"kind": "room",
"area": 200.0
},
{
"name": "flight",
"kind": "stair",
"area": 10.8,
"riser": 0.18,
"tread": 0.28
},
{
"name": "street",
"kind": "safe"
}
],
"connections": [
{
"source": "office",
"target": "flight",
"width": 0.9,
"length": 25.0,
"bidirectional": false
},
{
"source": "flight",
"target": "street",
"width": 1.2,
"kind": "stair",
"length": 9.0,
"bidirectional": false
}
],
"populations": [
{
"node": "office",
"count": 120,
"speed": 1.2,
"pre_movement": {
"distribution": "LogNormal",
"mean": 120.0,
"std": 60.0
}
}
],
"simulation": {
"dt": 0.5
},
"run": {
"seed": 1
},
"monte_carlo": {
"runs": 500,
"seed": 1,
"quantiles": [
0.5,
0.95
]
}
}Each section goes to one part of the API:
| Key | Type | Goes to | Arguments on |
|---|---|---|---|
format_version | integer, required | Must be 1 | |
software_version | string | Version of jupedsim-network that wrote the file; written by the export, ignored when reading | |
hydraulic_parameters | object | HydraulicParameters fields that differ from the defaults; absent means all defaults | Networks |
description | string | Free text, not passed on | |
nodes | array, required | add_room, add_stair or add_safe, chosen by kind ("room", "stair", "safe") | Networks |
connections | array, required | Network.connect | Networks |
populations | array, required, not empty | Population | Populations |
simulation | object | Keyword arguments of NetworkSimulation | Running |
run | object | seed of run() | Running |
monte_carlo | object | runs and seed of run_many(); quantiles for quantile() | Running, Results |
The arguments, their defaults and their errors are documented once, on the pages in the last column. The rules below are those of the file.
Keys are the argument names of the API
The loader reads the allowed keys from the signatures of the API, so
the file has no names of its own, and every value is passed on
unchanged. Two names differ from what you might guess: a population
starts in node, and a stair takes riser and tread in m.
Three names are not shared between the file and the API:
recordofrun()is not in the file:runalways records (What the command line adds).progressofrun_many()is not in the file.quantilesis in the file only. It is not an argument ofrun_many(); it feedsMonteCarloResult.quantile().
A key you leave out takes the API default. The loader adds no defaults
of its own. An unknown key is an error that lists the valid keys, so a
typo cannot fall back to a default without notice. Here typo.json is
a copy of scenario.json with max_density misspelt as max_densty:
jupedsim-network check typo.jsonjupedsim-network check: error: typo.json: simulation: unknown key 'max_densty'; valid keys: counterflow, dt, max_density, routing_policy, split_ties, supply_reduction, t_max
Watch for these
- Connections are two-way unless you say otherwise. As in Python,
bidirectionaldefaults totrue: a connection without"bidirectional": falsealso creates the reverse link, and the two directions share one door undercounterflow. The packaged scenarios setfalseon every connection (One-way and two-way links). - Order matters. The API is called in array order. Node and link indices, the agent order and the rule that a route tie goes to the first link created all follow from it. Swapping two tied connections or two populations can change the result (Routes).
- Two keys are called
length. On a node,length×widthonly sets the area. On a connection,lengthis the distance walked in the source node, along the incline on a stair (Length).
Distributions
count, speed, pre_movement and start_distance take a plain
number or a distribution object such as
{"distribution": "LogNormal", "mean": 120.0, "std": 60.0}.
distributionis one ofFixed,Uniform,Normal,LogNormal,TriangularandWeibull.- The other keys are the arguments of that class, plus
lowerandupper(Distributions). area_factortakes a plain number only."lower": nullis not the same as leavinglowerout. Left out, the distribution keeps its default truncation at 0.nullremoves the lower bound.loweris the only key wherenulland a missing key differ.
What the loader checks
- The structure: required and unknown keys, JSON types, duplicate keys,
UTF-8 text, at most 64 levels of nesting, and finite numbers (
NaN,Infinityand1e999are rejected). monte_carlo.runsis at least 1, and every quantile lies in [0, 1].- Each distribution object is sampled once, with its own random generator, so the check does not change the results.
Every other check is done by the API, with the API’s message. The message starts with the JSON path of the entry (Errors).
Warnings, such as a stair outside the SFPE range or route ties with
split_ties: false, are printed to stderr as warning: <path>: … and
stored under warnings in summary.json. They never change the exit
status, so read every one.
What the command line adds
Three behaviours belong to the command line and are not in the API:
- Quantiles. If neither the file nor
--quantilegives quantiles,runreports 0.5 and 0.95. - Drawn seeds. With
"seed": null, or no seed,checkandrundraw a seed, print it asseed <N> (drawn)and store it insummary.jsonwith"seed_drawn": true. In Python,seed=Noneuses a fresh seed that is never recorded. - Time series.
runalways records occupancy and link flow. This does not touch the random numbers, so the results are the same.
"runs": 0 in the file is an error, even when --runs is given.
Input checks
The file and Python reject the same inputs with the same message, at
the JSON path of the entry; the list is on
Limitations
(issue #53).
For example, riser and tread in mm (180 and 280), a negative
plain pre_movement, start_distance, speed or count,
area_factor 0 or negative, a negative boundary_layer, a room with
area together with length or width, and a dt below 1e-6 s are
errors. A Fixed
value outside its bounds and a distribution with lower above upper
are rejected when the file is loaded. max_density at or below
1.88 m⁻² turns supply reduction off with a warning
(Limitations).
Result files
run writes these files to the result directory:
| File | Content |
|---|---|
summary.json | Inputs, seeds, evacuation time, quantiles, links and warnings (below) |
agents.csv | One row per agent, in population order: sampled pre-movement time and exit time |
link_flow.csv | Per step, the agents that passed each link |
occupancy.csv | Per step, the agents in each node |
runs.csv | Monte Carlo only: one row per run, with its evacuation time and number of agents |
All files are UTF-8 with \n line endings. Times are in s from the
start of the run, which is also the origin of pre_movement. A missing
value is an empty field in a CSV file and null in JSON.
summary.json repeats every unit in its units block.
The CSV headers of the getting-started run:
agent,pre_movement_sampled,exit_timetime,0:office->flight,1:flight->streettime,office,flightrun,evacuation_time,agentsagents.csv.exit_timeis empty for an agent still inside att_max. The file has no population column, because a run does not record the population of an agent andcountcan be sampled; agents are in the order of the populations.link_flow.csvcounts agents (heads) per step, in the step(time − dt, time]. It is not a flow rate: divide bydtfor persons/s. Row 0 is time 0 and all zeros. Columns are headedindex:name, because a custom name or two links between the same nodes can repeat a link name. Thelinkslist insummary.jsonmaps each index to its source and target.occupancy.csvcounts agents (heads) in each node attime, not the area-weighted load thatmax_densitylimits (Limitations). Safe nodes are left out.runs.csv.evacuation_timeis empty for an incomplete run.agentsvaries whencountis a distribution.
Times in the files
- The
timecolumn starts at 0, before the first step. Its last value can exceedt_maxby less thandt. The values are those of the API’stimes, which the step loop rounds to 9 decimals. exit_timeis the end of the step in which the agent reached safety, an upper bound to withindt. It equals thetimevalue of that step inlink_flow.csvandoccupancy.csv, so the files join exactly. The value is the API’sexit_times, written unchanged so that the files match Python.- Monte Carlo quantiles in
summary.jsonare interpolated between runs and are not step times. pre_movement_sampledis the sampled time. The agent starts walking at the first step that starts at or after it.- The files keep every value at full precision. The terminal rounds
times to 0.01 s and prints the shortest form, so a value in the
terminal can differ in the last digits from the one in the files.
Capacities are rounded too:
0.78in the terminal is0.7800000000000001insummary.json. An unfinished quantile prints as> t_max. An unfinished run printsnot finished by t_max; <k> of <n> agents safe.
Fields of summary.json
format_version(1) andjupedsim_network, the package version.scenario:pathas given on the command line, andsha256of the file’s bytes.units: the unit and time base of every column.dtandt_max.run:seed,seed_drawn,evacuation_time(nullif an agent was still inside att_max),agentsandevacuated.monte_carlo, ornullwithout Monte Carlo:runs,seed,seed_drawn,incomplete, andquantilesas a list ofqandvalue.valueisnullwhere the quantile is only known to exceedt_max(infin Python).links: index, name, source, target, kind and capacity in persons/s of every link, reverse links included.warnings.
Seeds and reproducibility
run.seed and monte_carlo.seed are separate seeds. They give the same
results as run(seed) and run_many(runs, seed) in Python, byte for
byte. --seed overrides the first and --mc-seed the second.
To replay a run with a drawn seed, pass the printed value in place of
N, for example --seed 1234:
jupedsim-network run scenario.json --seed N
--mc-seed replays a whole Monte Carlo set. The command line cannot
replay one run of a set. Use Python for that: row i of runs.csv is
the run with seed numpy.random.SeedSequence(seed).spawn(runs)[i]
(Replay a critical run).
A result is fixed by four things: the scenario file (its sha256 is in
summary.json), the seeds, the jupedsim-network version (also in
summary.json) and the major version of NumPy. Keep summary.json
with the results.
- A file means “these inputs under this version’s defaults”.
format_versionversions the layout of the file, not the model defaults. A key left out takes the default of the installed version, which can change between versions. This holds forhydraulic_parameterstoo: a coefficient not listed there takes its default. An exported file records the version that wrote it insoftware_version; the loader does not use it. - NumPy 1 and NumPy 2 differ slightly.
summary.json,link_flow.csv,occupancy.csvandruns.csvare the same.agents.csvdiffers in the last digits of a few sampled pre-movement times.
Exit status
| Code | Meaning |
|---|---|
| 0 | run: every run finished. check: the scenario is valid and the trial run finished. init: the file was written. |
| 2 | run: the single run or at least one Monte Carlo run did not finish by t_max. All result files are written. check: the scenario is valid, but the trial run did not finish by t_max. |
| 1 | An error, including a wrong option. An error message and no traceback. Nothing is written, and earlier results in DIR stay as they were. |
| 130 | Interrupted with Ctrl-C. The message jupedsim-network: interrupted and no traceback. Nothing is written, except when the interrupt falls while the finished files are moved into DIR; then DIR can hold a mix of new and earlier files. |
Code 2 is not an error: the results are written, and the quantiles that
depend on unfinished runs read > t_max
(t_max and incomplete runs).
A shell script with set -e that expects unfinished runs must accept 2:
jupedsim-network run scenario.json || [ $? -eq 2 ]A wrong option also gives 1, unlike the usual 2 of Python’s
argparse, so that 2 always means an unfinished run.
From Python
Five names of jupedsim_network.scenario move a scenario between
Python and the file. They are importable from jupedsim_network. To
write the Getting started
simulation to a file and run it from there:
from jupedsim_network import Scenario, load_scenario, save_scenario
scenario = Scenario.from_simulation(
simulation,
seed=1,
monte_carlo={"runs": 500, "seed": 1, "quantiles": [0.5, 0.95]},
)
save_scenario(scenario, "office.json")
loaded = load_scenario("office.json")
result = loaded.simulation.run(seed=loaded.run_seed)result is the same as simulation.run(seed=1), byte for byte.
jupedsim-network run office.json gives the same numbers as the
packaged file.
Scenario.from_simulation(simulation, *, seed=None, monte_carlo=None, description=None)writes the arguments that the network, the populations and the simulation were built with, in the order they were created.- An argument equal to its API default is left out.
- Derived values, such as capacities, are never written.
seed=Noneleaves out therunsection, andmonte_carlotakes a dict as above.- What the format cannot express raises
ScenarioErrorwith its path: aDistributionsubclass of your own or a node name that is not a string. Nothing is dropped without notice.
save_scenario(scenario, path)writes UTF-8 JSON indented by two spaces, in key order, and adds no defaults.scenariois aScenario, a scenario dict, which is checked and built first, or aNetworkSimulation, which is written without arunormonte_carlosection. A file written bysave_scenariois written again byte for byte bysave_scenario(load_scenario(path), path2).load_scenario(path)reads, checks and builds a scenario and returns aScenario. It raisesScenarioErrorfor an invalid scenario andOSErrorfor a file it cannot read.scenario_from_dict(data, source=None)checks and builds a scenario held in memory. The terminal UI checks every edit with it.datais whatjson.loadreturns: dicts, lists, strings, finite numbers, booleans andNone. A tuple is rejected.- The checks and messages are those of
load_scenario, except the checks of the JSON text itself (UTF-8, duplicate keys). - It returns a
Scenariowhosedatais a copy ofdata. sourceis stored asScenario.source.- It raises
ScenarioError.
Scenariois a frozen dataclass withdata, the document in file order, as a plain dict: changing it does not changesimulation, andsave_scenariowrites it without checking it again (issue #47). To edit a scenario, change a dict of your own and pass it toscenario_from_dictorsave_scenario, which checks a dict first;simulation, theNetworkSimulationbuilt at load;warnings, a tuple of messages that start with their path;source, the path of the file, orNone;- the properties
run_seedandmonte_carlo; build(), which builds a fresh simulation and returns it with its warnings;to_dict().
ScenarioErroris aValueErrorwithpath(such as"connections[1].width") andmessage.str()gives"<path>: <message>".
An exported file is shorter than the packaged one, apart from the
software_version line. Exported from the
Getting started simulation, it has no "speed": 1.2, an empty
"simulation": {} (0.5 s is the default dt) and the stair keys in
the order of the signature. Both files build the same simulation, but
the exported one lists only what differs from the defaults of the
installed version.
Errors
A scenario error ends the command with one line on stderr: the file, the JSON path of the entry and the message.
jupedsim-network check: error: typo.json: simulation: unknown key 'max_densty'; valid keys: counterflow, dt, max_density, routing_policy, split_ties, supply_reduction, t_max
jupedsim-network check: error: speed0.json: populations[0]: Agent speeds must be positive.
jupedsim-network check: error: ln0.json: populations[0].pre_movement: cannot sample LogNormal(lower=0.0, upper=None, mean=0, std=60.0): float division by zero
jupedsim-network check: error: policy.json: simulation.routing_policy: routing_policy must be one of 'shortest_distance', 'shortest_free_flow_time'; got 'fastest'.
The first and the last are found while the file is loaded. The second
comes from the trial run, so check prints the scenario listing first.
A wrong option prints the usage: line before the error.
- After the path comes the API’s own message. The tables on Networks and Running list them with their causes.
- Populations are counted from 0:
populations[0]on the command line ispopulation 0in Python. Initial population exceeds max_density in [...]concerns all populations together and has the pathrun.- A file that is not UTF-8 gives
not UTF-8 text: byte 0xNN at position P; save the file as UTF-8. Save it again as UTF-8.
Next steps
- Terminal UI: edit, check and run the same file in forms.
- Networks and
Populations: the keys of
nodes,connectionsandpopulations, with their defaults. - Running: choosing
dt,max_densityandt_max. - Limitations: read before you use a result.