Skip to content

A tutorial on the basic architecture of an interactive pyswmm web app.

Notifications You must be signed in to change notification settings

royar-hash/interactive-swmm-tutorial

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

74 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation


Logo

building an interactive pyswmm web app


Explore the docs »

View Demo · Report Bug · Request Feature

Table of Contents
  1. About The Project
  2. Step One: Preparing Your Geospatial Data
  3. Step Two: Set up Your App
  4. Step Three: Passing Data from the UI to Python
  5. Step Four: Passing Data from Python to the UI
  6. Step Five: Run Your App
  7. Ideas
  8. License
  9. Contact
  10. Acknowledgments

About The Project

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.
App

(back to top)

Built With

(back to top)

Step One: Preparing Your Geospatial Data

  1. 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.

  1. 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.

  1. 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.
links.geojson
{
"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 ] ] } }
]
}
  1. 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.

  2. 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.

recipe.json
{
  "version": 1,
  "layers": {
    "links": {
      "source": "mapbox://tileset-source/YOUR_MAPBOX_USERNAME/links-source",
      "minzoom": 12,
      "maxzoom": 16
    }
  }
}

Step Two: Set Up Your App

  1. Create a directory and virtual environment for your app.
$ mkdir your-app-directory
$ cd your-app-directory
$ python3 -m venv venv
  1. 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.

  1. 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

Step Three: Passing Data from the UI to Python

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.

Start Button

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.
templates/index.html
<form id="start" method="POST" action="#">
  <input id="start_button" class="button" type="submit" value="start sim">
</form>  
static/script.js
$('form#start').submit(function(event) {
    socket.emit('start_button', {data: 'pressed'});
    return false;
});
app.py
@socketio.on('start_button')
def handle_message(data):
  with Simulation('./model/model.inp') as sim:
    for step in sim:
      # do stuff

Controllable Asset

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.

templates/index.html
<input type="range" min="0" max="100" value="0" class="slider" id="gate_1_slider">
static/script.js
$('input#gate_1_slider').on('input', function(event) {
    socket.emit('gate_1_change', {
        who:$(this).attr('id'),  
        data: $(this).val()
        });
        return false;
    });
app.py
@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.

Step Four: Passing Data from Python to theUI

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.

Updating Text

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.

templates/index.html
<div class='tracker' id="outfall_1_tracker"></div>
app.py
emit('outfall_1_update',{'data':' '+str(round(outfall_1_flow_emit,2)})
static/script.js
    socket.on('outfall_1_update', function(msg) {
        $('#outfall_1_tracker').text($('<div/>').text(msg.data).html());
    })

Updating a Chart

We do this with Chart.js.

templates/index.html
<canvas id='chart'></canvas>
app.py
emit('storage_update',storage_depth)
static/script.js
socket.on('storage_update', function(depth) {
    chart.data.datasets[0].data.push(depth)
    chart.update()
});

Updating the Map

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>
app.py
emit('flow_update', flow_json)
static/script.js
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.

Step Five: Run Your App

(venv) $ python3 app.py

Note

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.

(back to top)

Ideas

This architecture is flexible and adaptable - you could use it to do all kinds of things.

  • Dynamic visualizations

(back to top)

License

Distributed under the MIT License. See LICENSE.txt for more information.

(back to top)

Contact

Ariel Roy - royar@umich.edu
text

(back to top)

Acknowledgments

(back to top)

About

A tutorial on the basic architecture of an interactive pyswmm web app.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published