Table of Contents
This tutorial will walk you through the basic architecture of building an interactive SWMM web app using Flask-SocketIO and Mapbox GL JS.
Normally, running a SWMM simulation is pretty boring. You set a control strategy, hit start, and then you pretty much can't see or do anything until the simulation is done running.
This flexible, adaptable web app architecture combines the modeling power of SWMM with the interactivity and real-time visualization of actual stormwater systems! Think of it as a game-like simulation, kind of like Rollercoaster Tycoon, except I didn't write it in assembly and for sewers. Sewer Tycoon™.
You can change the status of controllable assets as the simulation is running and immediately visualize the effects of your actions. Unlike running a typical SWMM simulation, you aren't locked into a control strategy from the start and you can see exactly what's going on inside the model.
This project is a product of Professor Branko Kerkez's Digital Water Lab at the University of Michigan.
Here's what the example app looks like. Click here to see and interact with the finished product.
- Pick your SWMM model! For this tutorial, we're going to use this simple model with two orifices, a storage node, two conduits, and two outfalls. Here's what our sample model looks like in PCSWMM. If you want to download the model yourself and take a look, it can be found in this repository at
model/model.inp
.
- Decide what assets from your SWMM model you want to be able to dynamically visualize (the things you want to change color or size as the simulation is running). In this case, we want to be able to see the flow in the four links. The link names and IDs are shown below.
- Export the assets you picked as GeoJSON files. In PCSWMM, you can do this via the 'Export' button. If you want, you can modify the file with the Python package geopandas or another tool - since the GeoJSON from SWMM will probably have lots of information you don't need, this can be helpful to simplify things. Each asset you want to visualize MUST have a unique numeric id. Here's what the GeoJSON looks like for the links in our example model.
{
"type": "FeatureCollection",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "type": "Feature", "properties": { "id": "1", "Name": "CDT-15" }, "geometry": { "type": "LineString", "coordinates": [ [ -127.541829345687319, 35.183023046142772 ], [ -127.541448648467139, 35.182925678088793 ] ] } },
{ "type": "Feature", "properties": { "id": "2", "Name": "EXISTING300" }, "geometry": { "type": "LineString", "coordinates": [ [ -127.541838783547476, 35.182976748811171 ], [ -127.541476299031828, 35.182882774764849 ] ] } },
{ "type": "Feature", "properties": { "id": "3", "Name": "ORI-11" }, "geometry": { "type": "LineString", "coordinates": [ [ -127.541896415203198, 35.18301601810515 ], [ -127.541842694367915, 35.182978920293252 ], [ -127.541838783547476, 35.182976748811171 ] ] } },
{ "type": "Feature", "properties": { "id": "4", "Name": "ORI-13" }, "geometry": { "type": "LineString", "coordinates": [ [ -127.541896415203198, 35.18301601810515 ], [ -127.541829345687319, 35.183023046142772 ] ] } }
]
}
-
If you don't have a free Mapbox account, now is the time to make one! Click here to create your account. Once you've made your account, click on 'Create a token' on your dashboard and make a secret token with tilesets:write, tilesets:read, and tilesets:list permissions. Make sure to save your secret token somewhere you won't lose it! You'll need it in the next step.
-
Take your GeoJSON file or files and upload to Mapbox as tilesets using the Mapbox Tilesets CLI. You have to use the method linked; just uploading stuff in Mapbox Studio won't work for our purposes. Here's an example recipe used to upload the example GeoJSON.
{
"version": 1,
"layers": {
"links": {
"source": "mapbox://tileset-source/YOUR_MAPBOX_USERNAME/links-source",
"minzoom": 12,
"maxzoom": 16
}
}
}
- Create a directory and virtual environment for your app.
$ mkdir your-app-directory
$ cd your-app-directory
$ python3 -m venv venv
- Activate your virtual environment and install the following dependencies with the package installer.
$ source venv/bin/activate
(venv) ~/your-app-directory $
pip install pyswmm
pip install flask
pip install flask-socketio
pip install eventlet
pip install pandas
The list of packages above is just a start. You may need to install other packages depending on what you want to do with your app.
- Within your project directory, create the file structure below. There are four main files you'll need to create: app.py, style.css, script.js, and index.html. The app.py should be in the main directory, the style and script files in the 'static' folder, and the index file in the 'templates' folder.
├── your-app-directory
│ ├── model
│ │ ├── swmm-model.inp
│ ├── static
│ │ ├── script.js
│ │ ├── style.css
│ ├── templates
│ │ ├── index.html
│ ├── app.py
For our example app, we have a start button and two sliders on the user interface: one for each orifice in the SWMM model. Our goal is to achieve the following:
- When the start button is pressed, the PySWMM simulation starts to run
- When the user changes the slider, the new value of the slider is set as the target setting of the corresponding controllable asset. For example, if the user moves the gate 1 slider all the way down to 0, we want to set the target setting of ORI-11 in the SWMM model to 0 as well.
The basic idea for the start button is as follows:
- Create a button in HTML.
- Write a socket.emit() function that is triggered when the button is pressed.
- Write a corresponding socket.on() function in Python that contains your PySWMM loop. Here's what the code snippets for the start button look like.
<form id="start" method="POST" action="#">
<input id="start_button" class="button" type="submit" value="start sim">
</form>
$('form#start').submit(function(event) {
socket.emit('start_button', {data: 'pressed'});
return false;
});
@socketio.on('start_button')
def handle_message(data):
with Simulation('./model/model.inp') as sim:
for step in sim:
# do stuff
The basic idea for a controllable asset is this:
- Write a socket.emit() function that throws out the data that you want on the backend. For example, for the sliders, you want a function that throws out the new value every time the user changes the position of the slider.
- Write a corresponding @socket.on() function in Python that captures and does something with the data you send with your socket.emit() function. For the sliders, this means processing that data and then updating a json file with the latest information. Then, that json file is read at every step of the PySWMM loop and the data used to update the target position of the controllable asset.
Note that you could adapt this to pass anything from the user interface back to Python; the same principle will work if you have an off/on switch on the UI that you want to turn a pump on or off in PySWMM, for example.
Here's what the code snippets for a controllable asset look like.
<input type="range" min="0" max="100" value="0" class="slider" id="gate_1_slider">
$('input#gate_1_slider').on('input', function(event) {
socket.emit('gate_1_change', {
who:$(this).attr('id'),
data: $(this).val()
});
return false;
});
@socketio.on('gate_1_change')
def handle_slider(data):
json_object = json.dumps(data)
with open('gate_1.json', 'w') as outfile:
outfile.write(json_object)
Here's a visual of what the basic architecture looks like for passing data from the user interface to Python.
Obviously, just passing data from the user interface to Python makes for a pretty boring web app. We want to be able to update the UI as the simulation is running so that the user can see the impact of their actions.
This is the easiest one to do, so we'll start here. Use this method when you want to update some text on the UI: maybe the current flow of a link, the percentage of the simulation that's complete, or the current status of a slider bar.
<div class='tracker' id="outfall_1_tracker"></div>
emit('outfall_1_update',{'data':' '+str(round(outfall_1_flow_emit,2)})
socket.on('outfall_1_update', function(msg) {
$('#outfall_1_tracker').text($('<div/>').text(msg.data).html());
})
We do this with Chart.js.
<canvas id='chart'></canvas>
emit('storage_update',storage_depth)
socket.on('storage_update', function(depth) {
chart.data.datasets[0].data.push(depth)
chart.update()
});
This is where Mapbox gets involved. Note that Mapbox's setFeatureState supports more than just color! See here for more info. There are lots of options here to use this architecture in a totally different way.
<div id="map"></div>
emit('flow_update', flow_json)
socket.on('flow_update', function(flow_json) {
const flow_obj = JSON.parse(flow_json)
for (const flow_item of flow_obj) {
map.setFeatureState({
source: "links",
sourceLayer: "links",
id: flow_item.geoid},
{conduit_flow:flow_item.flow}
);
};
});
Here's a visual of what the basic architecture looks like going from Python to the UI.
(venv) $ python3 app.py
This is not an exhaustive tutorial and is only meant to illustrate the general principals involved in creating an interactive SWMM web app. For more details, dig into the example files provided in this repository.
This architecture is flexible and adaptable - you could use it to do all kinds of things.
- Dynamic visualizations
Distributed under the MIT License. See LICENSE.txt
for more information.
Ariel Roy - royar@umich.edu