Network comparison demo¶
Note - this script can also be opened in interactive Python if you wanted to
play around. On the GitHub it is in docs/notebooks
. To run this on your
local machine, you will need to install the optional dependencies for doc:
pip install swmmanywhere[doc]
Introduction¶
This script demonstrates how to use SWMManywhere when you have a real network. It shows how to tell SWMManywhere where the necessary data is, and how, when this is provided, a suite of metrics are calculated to compare the real network with the synthesised network.
Since this is a notebook, we will define config
as a dictionary rather than a yaml file, but the same principles apply.
Initial setup¶
Here we will run one of the Bellinge sub-networks which is provided in the test data. We will keep everything in a temporary directory.
# Imports
from __future__ import annotations
import tempfile
from pathlib import Path
from swmmanywhere.defs import copy_test_data
from swmmanywhere.logging import set_verbose
from swmmanywhere.swmmanywhere import swmmanywhere
from swmmanywhere.utilities import plot_map
# Create temporary directory
temp_dir = tempfile.TemporaryDirectory(dir=".")
base_dir = Path(temp_dir.name)
# Make a folder for real data
real_dir = base_dir / "real"
real_dir.mkdir(exist_ok=True)
# Copy test data into the real data folder
copy_test_data(real_dir)
/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm
Create config file¶
Below, we update our config file to use new coordinates, and provide the real data. As set out in the schema, a variety of data entries can be provided to describe the real network, all CRS of shapefiles must be in the UTM CRS for the project:
graph(essential) - a graph file in JSON format created fromnx.node_link_datasubcatchments(essential) - a GeoJSON file with subcatchment outlines, with headings forid, andimpervious_area(only needed if calculating the metric,bias_flood_depth).inp- an inp file to run the model with, matching the appropriate graph and subcatchments.results- a results file to compare the model with.
At least one of results or inp must be provided. If inp is provided then the
model will be run and the results compared to the real data, otherwise results will
be loaded directly.
Note that the need to separately provide a graph and subcatchments will be
removed following fixing of this.
# Define config
config = {
"base_dir": base_dir,
"project": "bellinge_small",
"bbox": [10.309, 55.333, 10.317, 55.339],
"run_settings": {"duration": 3600},
"real": {
"graph": real_dir / "bellinge_small_graph.json",
"inp": real_dir / "bellinge_small.inp",
"subcatchments": real_dir / "bellinge_small_subcatchments.geojson",
},
"parameter_overrides": {
"topology_derivation": {
"allowable_networks": ["drive"],
"omit_edges": ["bridge"],
}
},
}
Run SWMManywhere¶
We make the swmmanywhere call as normal, but can observe that there is an additional
model run (scroll towards the bottom) where the real model inp is being run and the
metrics are shown to have been calculated in the log.
## Run SWMManywhere
set_verbose(True)
outputs = swmmanywhere(config)
2026/05/08 09:21:06 | Creating project structure.
2026/05/08 09:21:06 | Project structure created at tmp50pcnb8m
2026/05/08 09:21:06 | Project name: bellinge_small
2026/05/08 09:21:06 | Bounding box: [10.309, 55.333, 10.317, 55.339],
number: 1
2026/05/08 09:21:06 | Model number: 1
2026/05/08 09:21:06 | Loading and setting parameters.
2026/05/08 09:21:06 | Setting topology_derivation allowable_networks to ['drive']
2026/05/08 09:21:06 | Setting topology_derivation omit_edges to ['bridge']
2026/05/08 09:21:06 | Allowable networks have been changed, removing old street graph.
2026/05/08 09:21:06 | Running downloads.
2026/05/08 09:21:06 | downloading elevation to tmp50pcnb8m/bellinge_small/bbox_1/download/elevation.tif
2026/05/08 09:21:09 | downloading buildings to tmp50pcnb8m/bellinge_small/bbox_1/download/building.geoparquet
2026/05/08 09:21:47 | downloading network to tmp50pcnb8m/bellinge_small/bbox_1/download/street.json
2026/05/08 09:21:49 | downloading river network to tmp50pcnb8m/bellinge_small/bbox_1/download/river.json
2026/05/08 09:21:50 | No water network found within the bounding box.
2026/05/08 09:21:50 | Iterating graphs.
2026/05/08 09:21:50 | Iterating graph functions.
2026/05/08 09:21:50 | graphfcn: assign_id completed.
2026/05/08 09:21:50 | graphfcn: fix_geometries completed.
2026/05/08 09:21:50 | graphfcn: remove_non_pipe_allowable_links completed.
2026/05/08 09:21:50 | graphfcn: calculate_streetcover completed.
2026/05/08 09:21:50 | graphfcn: remove_parallel_edges completed.
2026/05/08 09:21:50 | graphfcn: to_undirected completed.
2026/05/08 09:21:50 | graphfcn: split_long_edges completed.
2026/05/08 09:21:50 | graphfcn: merge_street_nodes completed.
2026/05/08 09:21:50 | graphfcn: assign_id completed.
2026/05/08 09:21:51 | graphfcn: clip_to_catchments completed.
2026/05/08 09:21:52 | graphfcn: calculate_contributing_area completed.
2026/05/08 09:21:52 | graphfcn: set_elevation completed.
2026/05/08 09:21:52 | graphfcn: double_directed completed.
2026/05/08 09:21:52 | graphfcn: fix_geometries completed.
2026/05/08 09:21:52 | graphfcn: set_surface_slope completed.
2026/05/08 09:21:52 | graphfcn: set_chahinian_slope completed.
2026/05/08 09:21:52 | graphfcn: set_chahinian_angle completed.
2026/05/08 09:21:52 | graphfcn: calculate_weights completed.
2026/05/08 09:21:53 | No outfalls found for subgraph containing
203, using this node as outfall.
2026/05/08 09:21:53 | No outfalls found for subgraph containing
92, using this node as outfall.
2026/05/08 09:21:53 | No outfalls found for subgraph containing
108, using this node as outfall.
2026/05/08 09:21:53 | No outfalls found for subgraph containing
127, using this node as outfall.
2026/05/08 09:21:53 | No outfalls found for subgraph containing
178, using this node as outfall.
2026/05/08 09:21:53 | No outfalls found for subgraph containing
193, using this node as outfall.
2026/05/08 09:21:53 | No outfalls found for subgraph containing
230, using this node as outfall.
2026/05/08 09:21:53 | graphfcn: identify_outfalls completed.
2026/05/08 09:21:53 | Total graph weight 60.63269095005905.
2026/05/08 09:21:53 | graphfcn: derive_topology completed.
0%| | 0/147 [00:00<?, ?it/s]
20%|āā | 30/147 [00:00<00:00, 298.79it/s]
48%|āāāāā | 70/147 [00:00<00:00, 353.47it/s]
72%|āāāāāāāā | 106/147 [00:00<00:00, 320.11it/s]
95%|āāāāāāāāāā| 139/147 [00:00<00:00, 286.05it/s]
100%|āāāāāāāāāā| 147/147 [00:00<00:00, 289.25it/s]
2026/05/08 09:21:53 | graphfcn: pipe_by_pipe completed.
2026/05/08 09:21:53 | graphfcn: fix_geometries completed.
2026/05/08 09:21:53 | graphfcn: assign_id completed.
2026/05/08 09:21:53 | Saving final graph and writing inp file.
2026/05/08 09:21:54 | Running the synthetic model.
2026/05/08 09:21:54 | tmp50pcnb8m/bellinge_small/bbox_1/model_1/model_1.inp initialised in pyswmm
2026/05/08 09:21:54 | Starting simulation for: tmp50pcnb8m/bellinge_small/bbox_1/model_1/model_1.inp
0%| | 0/3600 [00:00<?, ?it/s]
45%|āāāāā | 1626.0/3600 [00:00<00:00, 16232.54it/s]
90%|āāāāāāāāā | 3251.0/3600 [00:00<00:00, 13567.39it/s]
2026/05/08 09:21:54 | Model run complete.
3603.0it [00:00, 13316.27it/s]
2026/05/08 09:21:54 | Writing synthetic results.
2026/05/08 09:21:54 | Running the real model.
2026/05/08 09:21:54 | tmp50pcnb8m/real/bellinge_small.inp initialised in pyswmm
2026/05/08 09:21:54 | Starting simulation for: tmp50pcnb8m/real/bellinge_small.inp
0%| | 0/3600 [00:00<?, ?it/s]
2026/05/08 09:21:54 | Model run complete.
3603.0it [00:00, 148147.45it/s]
2026/05/08 09:21:54 | Iterating metrics.
2026/05/08 09:21:54 | outfall_nse_flow completed
2026/05/08 09:21:54 | outfall_kge_flow completed
2026/05/08 09:21:54 | outfall_relerror_flow completed
2026/05/08 09:21:54 | outfall_relerror_length completed
2026/05/08 09:21:54 | outfall_relerror_npipes completed
2026/05/08 09:21:54 | outfall_relerror_nmanholes completed
2026/05/08 09:21:54 | outfall_relerror_diameter completed
2026/05/08 09:21:54 | outfall_nse_flooding completed
2026/05/08 09:21:54 | outfall_kge_flooding completed
2026/05/08 09:21:54 | outfall_relerror_flooding completed
2026/05/08 09:21:54 | grid_nse_flooding completed
2026/05/08 09:21:54 | grid_kge_flooding completed
2026/05/08 09:21:54 | grid_relerror_flooding completed
2026/05/08 09:21:54 | subcatchment_nse_flooding completed
2026/05/08 09:21:54 | subcatchment_kge_flooding completed
2026/05/08 09:21:54 | subcatchment_relerror_flooding completed
2026/05/08 09:21:55 | bias_flood_depth completed
2026/05/08 09:21:55 | kstest_edge_betweenness completed
2026/05/08 09:21:55 | kstest_betweenness completed
2026/05/08 09:21:55 | outfall_kstest_diameters completed
2026/05/08 09:21:55 | nc_deltacon0 completed
2026/05/08 09:21:55 | nc_laplacian_dist completed
2026/05/08 09:21:55 | nc_vertex_edge_distance completed
2026/05/08 09:21:55 | Metrics complete
/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/swmmanywhere/metric_utilities.py:1077: RuntimeWarning: divide by zero encountered in scalar divide return (syn_tot - real_tot) / real_tot
Plot results¶
We can plot the real network data and simulation (click on links for flow and nodes for flooding).
## View real data
plot_map(real_dir)
... and we can plot the synthesised network
## View output
model_file = outputs[0]
plot_map(model_file.parent)
.. but of course we can see that the two networks do not perfectly line up. So we
can't exactly plot our timeseries side-by-side. To quantify this properly we need to
draw on SWMManywhere's ability to compare two networks that don't line up, which is
done using the metrics output, automatically calculated when real data is
provided.
# View metrics
metrics = outputs[1]
print(metrics)
{'outfall_nse_flow': np.float64(0.2611657798207503), 'outfall_kge_flow': np.float64(0.09650544557281104), 'outfall_relerror_flow': np.float64(-0.5138751594565549), 'outfall_relerror_length': np.float64(0.06157090216508091), 'outfall_relerror_npipes': np.float64(0.5), 'outfall_relerror_nmanholes': np.float64(0.4666666666666667), 'outfall_relerror_diameter': np.float64(-0.13043478260869573), 'outfall_nse_flooding': inf, 'outfall_kge_flooding': inf, 'outfall_relerror_flooding': inf, 'grid_nse_flooding': nan, 'grid_kge_flooding': nan, 'grid_relerror_flooding': nan, 'subcatchment_nse_flooding': nan, 'subcatchment_kge_flooding': nan, 'subcatchment_relerror_flooding': nan, 'bias_flood_depth': np.float64(inf), 'kstest_edge_betweenness': np.float64(0.90625), 'kstest_betweenness': np.float64(0.5591836734693878), 'outfall_kstest_diameters': np.float64(0.6190476190476191), 'nc_deltacon0': np.float64(0.0009428600561345575), 'nc_laplacian_dist': np.float64(3.7416573867739413), 'nc_vertex_edge_distance': 0.940766550522648}
For more information on using metrics see our metrics guide. To understand how to make use of such information, see our paper for example.