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 nodesdepthL: Depth in linksvolumeN: Volume in nodesvolumeL: Volume in linksflow: Flow in links/orifices/weirsflooding: Flooding in nodesinflow: Inflow into nodespollutantN: Pollutant concentration in nodes; the state tuple carries the pollutant name as a third entrypollutantL: 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.