
Welcome! We’ve developed pystorms with the objective of making the research of stormwater control more accessible. This open-source python package provides a curated collection of diverse stormwater networks and rain events, coupled with a streamlined programming interface and a stormwater simulator to provide a standalone package for developing and evaluating stormwater control algorithms. Please refer to the manuscript for more details on the motivation behind this effort.
pystorms is available on pypi and can be directly installed using pip. It is supported on OSX, Linux, and Windows. pystorms requires Python 3.9 or newer, and installs pyswmm(≥ 1.0.1), swmmio(≥ 0.6.11), numpy(≥ 1.18.4), pandas(≥ 1.0), and PyYAML(≥ 5.3) with it. Please raise an issue on the repository or reach out if you encounter any installation issues.
pip install pystorms
In pystorms , control of stormwater networks is abstracted as scenarios, named after letters from Greek alphabet.
import pystorms def controller(state): """ Your control algorithm that generates actions based on state information. """ return actions env = pystorms.scenarios.theta() done = False while not done: state = env.state() actions = controller(state) done = env.step(actions)
Scenario theta is a synthetic example for prototyping control algorithms. Six other scenarios inspired from real-world stormwater are included to provide realistic examples for evaluating stormwater control algorithms. Please refer to the documentation for more details.
Every scenario now takes two keywords. version selects the network: "1" is the scenario as published, and "2" is a harder variant of the same network for theta, alpha, gamma, delta and epsilon.
level decides how trustworthy the instrumentation is: "1" reports the true state, "2" adds sensor noise, drift and calibration bias and lets valves stick, and "3" intensifies all of that and adds sensors that drop out.
The two are independent, so a controller can be tested on a fixed network while only the measurement quality changes.
env = pystorms.scenarios.theta(version="2", level="3") done = False while not done: state = env.state() # noisy, drifting, biased readings done = env.step(controller(state)) # stuck valves ignore the command
The faults are drawn from numpy's random state when the scenario is built, so seed it first for a repeatable comparison. The versions and levels page documents the changes to each scenario and what each level does, and the changelog lists everything else that changed.
Version 2 tightens the objective and modifies the network so that a controller tuned for the original scenario has less room to look good. Version 1 is untouched and stays the default.
| Scenario | What version 2 changes |
|---|---|
| theta | Flow threshold halved to 0.25 m³/s. The first basin is half as deep. The event ends a day and a half earlier. |
| alpha | The five weirs join the five orifices in the action space, for ten controllable assets. Every orifice is widened to the interceptor diameter. |
| gamma | Flow threshold lowered from 4.0 to 3.0. Basins 5 and 9 are removed from the state space, the action space and the performance targets. |
| delta | Outflow threshold lowered from 12.0 to 0.5. The event runs three days longer with 30 percent more rainfall. The downstream conduit restrictions and the uncontrollable subcatchment flooding are removed, and the routing step is fixed at five seconds so every run has the same number of steps. |
| epsilon | TSS loading threshold lowered to 70 percent of the original. The event runs to the middle of February with 10 percent more rainfall. |
| beta, zeta | No second version. Asking for one raises ValueError. |
The version 2 network is built when the scenario is constructed, by rewriting the shipped SWMM input file. The copy goes to a per user cache directory (PYSTORMS_CACHE, or ~/.cache/pystorms/networks), never into the installed package.
Every version 2 scenario ships with two rule based controllers to measure against, plus the uncontrolled response. Their parameters were tuned by Bayesian optimization on level 1 and then evaluated as-is on levels 2 and 3, so the tables below show how much of a controller's benefit survives noisy sensors and sticking valves. The implementations, the tuned parameters, the optimization scripts and the plotting code live in the baseline_controllers directory of the repository.
| Scenario | Controller | Idea |
|---|---|---|
| theta, gamma, epsilon | Constant flow | Each outlet targets a fixed release rate, chosen by the optimizer. Valve positions follow from the orifice equation and the measured depth. |
| theta, gamma, epsilon | Equal filling | Basins release in proportion to how full they are relative to the network average, so no single basin floods while others sit empty. |
| alpha | Structural | Orifices fully open, weirs fixed at optimized crest settings. No feedback. |
| alpha | Constant flow, equal filling | As above, over the ten controllable orifices and weirs. |
| delta | Static plus rule | Fixed positions for the four weirs, plus a depth at which the infiltration basin valve opens. |
| delta | Proportional outflow | Adds proportional feedback on the network outflow to the static positions, releasing more when the outflow is far below its threshold and the basins are within their depth bounds. |
Performance on the version 2 scenarios, lower is better. Percentages are relative to the uncontrolled response at the same level.
| theta | Level | Uncontrolled | Constant flow | Equal filling | |
|---|---|---|---|---|---|
| 1 | 6.92e+03 | 5.45e+03 (−21.2%) | 5.42e+03 (−21.6%) | ||
| 2 | 6.92e+03 | 5.46e+03 (−21.1%) | 5.47e+03 (−20.9%) | ||
| 3 | 6.92e+03 | 6.36e+03 (−8.1%) | 6.36e+03 (−8.1%) | ||
| alpha | Level | Uncontrolled | Structural | Constant flow | Equal filling |
| 1 | 7.58e+03 | 4.19e+03 (−44.7%) | 3.74e+03 (−50.7%) | 3.81e+03 (−49.7%) | |
| 2 | 7.58e+03 | 4.19e+03 (−44.7%) | 3.73e+03 (−50.8%) | 3.81e+03 (−49.8%) | |
| 3 | 7.58e+03 | 4.19e+03 (−44.7%) | 3.85e+03 (−49.2%) | 3.89e+03 (−48.6%) | |
| gamma | Level | Uncontrolled | Constant flow | Equal filling | |
| 1 | 2.68e+05 | 1.01e+05 (−62.2%) | 1.18e+05 (−56.0%) | ||
| 2 | 2.68e+05 | 9.69e+04 (−63.8%) | 1.16e+05 (−56.8%) | ||
| 3 | 2.68e+05 | 8.23e+05 (+207.5%) | 1.36e+05 (−49.0%) | ||
| delta | Level | Uncontrolled | Static plus rule | Proportional outflow | |
| 1 | 9.94e+04 | 2.08e+04 (−79.1%) | 1.84e+04 (−81.5%) | ||
| 2 | 9.94e+04 | 8.34e+04 (−16.1%) | 5.58e+04 (−43.9%) | ||
| 3 | 9.94e+04 | 1.33e+05 (+34.3%) | 1.45e+05 (+45.7%) | ||
| epsilon | Level | Uncontrolled | Constant flow | Equal filling | |
| 1 | 7.35e+04 | 6.90e+04 (−6.0%) | 6.85e+04 (−6.8%) | ||
| 2 | 7.35e+04 | 6.98e+04 (−5.0%) | 6.94e+04 (−5.6%) | ||
| 3 | 7.35e+04 | 6.13e+04 (−16.5%) | 6.15e+04 (−16.3%) |
Preparing the version 2 scenarios turned up a number of problems in the shipped scenarios and in the code that runs them. This table tracks what was wrong, what 2.0 does about it, and whether it changes the numbers a 1.0 script produced. The changelog has the full list, including fixes outside the scenarios.
| Scenario | Issue in 1.0 | Fix in 2.0 | Effect on results |
|---|---|---|---|
| delta | Seven subcatchments in the network file carried an initial moisture deficit of 4.0. SWMM caps it at 1.0 and rejects the file (ERROR 235), so the scenario could not be built on any current engine. | Set to 0.4, reading as a misplaced decimal against the 0.187 and 0.177 on the neighbouring rows. | delta runs again. Version 2 already applied the same correction at runtime, so its numbers are unchanged. |
| epsilon | The pollutant state and the TSS loading objective read the concentration in node 001 rather than in conduit 001, which share an ID. | Link pollutants are read from the link. | epsilon performance shifts by about a tenth of a percent. |
| all | Actions passed as a dict set each valve to its position in the dict (0, 1, 2, ...) instead of the given value. | Dict values are applied to the named assets. | Only affects callers that used dicts; array and list actions were always correct. |
| all, level 3 | A dropped out sensor was zeroed before noise, drift and bias were added, so it reported noise around zero rather than the flat zero the documentation described. | The zero is applied last. | Level 3 readings during dropouts. The true state and the performance measure were never affected. |
| all | An integer or unsupported level built the scenario and then crashed inside state(); an unsupported version silently ran version 1. | Both keywords are validated when the scenario is built. Integers are accepted; anything undefined raises ValueError. | None. |
| beta, zeta | Accepted version="2" and ran version 1 without saying so. | Raise ValueError, since no second version exists. | None. |
| gamma, version 2 | Basins 5 and 9 were removed from the configuration lists while iterating over them, which skipped every following entry. It happened to give the right result only because the skipped entries never matched. | Rewritten as a filter. | None. |
| all | The rewritten version 2 networks, and the report and output files SWMM writes on every run, landed inside the installed package, which fails on a read-only install. | Written to a per user cache directory instead. | None. |
| all | A scenario that was stopped early could not be closed, and under pyswmm 2 the next scenario then failed with MultiSimulationError. | env.terminate() and context manager support on every scenario. | None. |
If you want to be more involved, please consider the following:
pystorms is developed by the open-storm group, a consortium of universities and industry partners developing open-source solutions for smarter stormwater systems.