Advanced#

Multi-processing#

pystorms environments can be seamlessly adopted for multiprocessing. SWMM allows a single open simulation per process, so running scenarios in separate processes is the way to evaluate several controllers at once.

Note

Every process running the same scenario writes the same .rpt and .out files, and every process building the same version 2 scenario rewrites the same derived network file, in the cache directory. Give each worker its own directory by setting PYSTORMS_CACHE before it imports pystorms, for example to a tempfile.mkdtemp() path.

import pystorms
import numpy as np
from multiprocessing import Pool

worker#

This function takes a controller as an argument, enabling us to evaluate multiple control strategies simultaniously.

def worker(config):
    env = pystorms.scenarios.gamma()
    done = False
    # different controllers
    controller = config["controller"]
    while not done:
        actions = controller(env.state())
        done = env.step(actions)
    return env.performance()

swarm#

This function maps the worker function onto multiple processors and return the performance

def generate_swarm(config, worker, processors, jobs):
    """
    Generate workers based on the environment and controller
    """
    if type(config) == list:
        swarm_inputs = config
    else:
        swarm_inputs = [config for i in range(0, jobs)]

    with Pool(processors) as p:
        data = p.map(worker, swarm_inputs)
    return data

Example:#

# Define two generic controllers
def control_1(state):
    return np.ones(11)

def control_2(state):
    return np.zeros(11)

# Create the config file
config = [{"controller": control_1}, {"controller": control_2}]
generate_swarm(config, worker, 2, 2)
[15736.942557976538, 282982.5873304134]

Lets time it to check that the function is running on mutiple processors. If sucessful, simulation time should be half.

Serial and Parallel#

%%timeit
worker(config[0])
worker(config[1])
15.1 s ± 434 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit
generate_swarm(config, worker, 2, 2)
10.8 s ± 42.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Thats 3 seconds more than what i expected. This might be due the initialization cost. This should go down as the number of simulations increase.

%%timeit
worker(config[0])
worker(config[1])
worker(config[0])
worker(config[1])
worker(config[0])
worker(config[1])
44.1 s ± 79.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit
generate_swarm(config, worker, 3, 6)
10.8 s ± 68.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit
generate_swarm(config, worker, 6, 6)
11.1 s ± 451 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Thats consistent!

pyswmm functionality#

pystorms is built on pyswmm. It uses pyswmm as its back-end for interacting with EPA-SWMM’s computational engine. Hence, all the functionality in pyswmm is inherently available in pystorms. pystorms by defaults supports a subset of the states though its API. These subsets of states (i.e. depth, flows, inflows) were chosen to represent frequently used parameters for making control decisions. Refer to the documentation on states for more details on the supported parameters. pystorms architecture is designed to enable users easy access to all the existing pyswmm functionality.

import pystorms
import pyswmm.toolkitapi as tkai

This example demonstrates how pyswmm functionality can be invoked from pystorms.

env = pystorms.scenarios.theta()

All function calls being used in the environment for populating state vector are listed in the env.env.methods dictionary.

env.env.methods
{'depthN': <bound method environment._getNodeDepth of <pystorms.environment.environment object at 0x11929d690>>,
 'depthL': <bound method environment._getLinkDepth of <pystorms.environment.environment object at 0x11929d690>>,
 'volumeN': <bound method environment._getNodeVolume of <pystorms.environment.environment object at 0x11929d690>>,
 'volumeL': <bound method environment._getLinkVolume of <pystorms.environment.environment object at 0x11929d690>>,
 'flow': <bound method environment._getLinkFlow of <pystorms.environment.environment object at 0x11929d690>>,
 'flooding': <bound method environment._getNodeFlooding of <pystorms.environment.environment object at 0x11929d690>>,
 'inflow': <bound method environment._getNodeInflow of <pystorms.environment.environment object at 0x11929d690>>,
 'pollutantN': <bound method environment._getNodePollutant of <pystorms.environment.environment object at 0x11929d690>>,
 'pollutantL': <bound method environment._getLinkPollutant of <pystorms.environment.environment object at 0x11929d690>>,
 'simulation_time': <bound method environment._getCurrentSimulationDateTime of <pystorms.environment.environment object at 0x11929d690>>}

Let us say, we want to use the hydraulic head in a node as a state, which is not one of the built in queries. All we have to do is add a function call reading it to the dict.

def _getNodeHead(NodeID):
    return env.env.sim._model.getNodeResult(NodeID, tkai.NodeResults.newHead.value)

env refers to the scenario being initialized. For example, if the scenario was initialized as sce the first class in the return statement would be sce. env in <scenario class>.env refers to the environment class used to communicate with pyswmm/EPA-SWMM. sim._model refers to the EPA-SWMM simulation initialized by invoking the scenario class. getNodeResult is the functional call that queries the head from EPA-SWMM.

env.env.methods["headN"] = _getNodeHead

Lets add the head to the state vector

env.config["states"]
[('P1', 'depthN'), ('P2', 'depthN')]
env.config["states"].append(('P1', 'headN'))
env.config["states"].append(('P2', 'headN'))

NOTE: Each state is a tuple of the element ID and the name of the query. Refer to environment.py for more details on how the state vector is populated.

env.config["states"]
[('P1', 'depthN'), ('P2', 'depthN'), ('P1', 'headN'), ('P2', 'headN')]

Now when env.state() is called, it returns both the depth and the head in the nodes

env.state()
array([0., 0., 0., 0.])

Refer to pyswmm documentation for details on the all supported parameters.

Example#

import pystorms
import pandas as pd
import pyswmm.toolkitapi as tkai
import matplotlib.pyplot as plt


# Create the function call for reading the head
def getNodeHead(NodeID):
    return env.env.sim._model.getNodeResult(NodeID, tkai.NodeResults.newHead.value)


# Initalize scenario
env = pystorms.scenarios.theta()

# Update the methods dict
env.env.methods["headN"] = getNodeHead
# Update state vector
env.env.config["states"].append(("P1", "headN"))
env.env.config["states"].append(("P2", "headN"))

done = False
data = []
while not done:
    state = env.state()
    done = env.step([1, 1])
    data.append(state)

data = pd.DataFrame(data, columns=["depthP1", "depthP2", "headP1", "headP2"])
data.plot()
plt.show()

Defining States#

import pystorms
import numpy as np
import matplotlib.pyplot as plt

In scenario gamma, states are defined as the depths in the basins of the network. If needed custom states can be defined by the users, overwriting or appending to the default states provided in the library. Consider the following example,

env = pystorms.scenarios.gamma()

States defined in gamma scenario can be queried as follows,

env.config["states"]
[('1', 'depthN'),
 ('2', 'depthN'),
 ('3', 'depthN'),
 ('4', 'depthN'),
 ('5', 'depthN'),
 ('6', 'depthN'),
 ('7', 'depthN'),
 ('8', 'depthN'),
 ('9', 'depthN'),
 ('10', 'depthN'),
 ('11', 'depthN')]

Appending the state list with the new state choice would update the state vector returned by the env.state() function.

For example, consider the case when we want to add outflow from the 4th basin to the state.

env.config["states"].append(("O4", "flow"))
env.config["states"]
[('1', 'depthN'),
 ('2', 'depthN'),
 ('3', 'depthN'),
 ('4', 'depthN'),
 ('5', 'depthN'),
 ('6', 'depthN'),
 ('7', 'depthN'),
 ('8', 'depthN'),
 ('9', 'depthN'),
 ('10', 'depthN'),
 ('11', 'depthN'),
 ('O4', 'flow')]

When we run the simulation, env.state() function should return a vector of length 12.

done = False
while not done:
    state = env.state()
    done = env.step(np.ones(11))
len(state)
12

Users are not limited to the flows in the network. They are allowed to access any value computed by the swmm network.

Supported state queries:
  • depthN : Depth in nodes

  • depthL : Depth in links

  • volumeN : Volume in nodes

  • volumeL : Volume in links

  • flow : Flow in links/orifices/weirs

  • flooding : Flooding in nodes

  • inflow : Inflow into nodes

  • pollutantN : Pollutant concentration in nodes; the state tuple carries the pollutant name as a third entry

  • pollutantL : Pollutant concentration in links; likewise

At levels 2 and 3 the noise, drift and bias are applied to every entry of the state vector, custom ones included. See Versions and levels.