-
Notifications
You must be signed in to change notification settings - Fork 77
Pollutant Module: get and set pollutants during simulation
To address the limitations of SWMM's water quality module, SWMM's C source code was modified to allow users to access and modify pollutant concentrations during any routing time step of a simulation through external tools like PySWMM.
The current pollutant process is quite complex. When swmm_run()
is called, it initializes three main processes:
-
it parses the relevant pollutant and treatment information from the SWMM input file [Section 2.1];
-
it initializes all the relevant pollutant information [Section 2.2]; and
-
it runs pollutant related functions during the simulation [Section 2.3]. Each of these three processes are explained below. We also direct the reader to a detailed flow chart here.
The following functions are called to parse the input file to get the treatment equation.
-
swmm_run()
callsswmm_open()
-
swmm_open()
callsproject_readInput()
-
project_readInput()
callsinput_readData()
-
input_readData()
callsparseLine()
which parses the treatment equation
The following functions are called to initialize water quality states.
-
swmm_run()
callsswmm_start()
-
swmm_start()
callsproj_init()
androuting_open()
-
proj_init()
initializes the internal states of all objects-
Calls
link_initState()
which initializes link's water quality statesLink[j].oldQual[p] = 0.0
andLink[j].newQual[p] = 0.0
-
Calls
node_initState()
which initializes node's water quality statesNode[j].oldQual[p] = 0.0
andNode[j].newQual[p] = 0.0
-
-
routing_open()
callstreatmnt_open()
andqualrout_init()
-
treatmnt_open()
allocates memory for commuting pollutant removals by treatment -
qualrout_init()
initializes water quality concentrations in all nodes (Node[i].oldQual[p] = c; Node[i].newQual[p] = c
) and links (Link[i].oldQual[p] = c; Link[i].newQual[p] = c
)
-
The following functions are called during a simulation to handlε pollutants and pollutant treatment.
-
swmm_run()
callsswmm_step()
-
swmm_step()
callsexecRouting()
-
execRouting()
callsrouting_exec()
-
routing_exec()
calls the following functions:-
link_setOldQualState()
which replaces link's old water quality state values with new ones using the following code:Link[j].oldQual[p]=Link[j].newQual[p]; Link[j].newQual[p] = 0.0
-
node_setOldQualState()
which replaces nodes's old water quality state values with new ones using the following code:Node[j].oldQual[p]=Node[j].newQual[p]; Node[j].newQual[p] = 0.0
-
qualrout_execute()
routes water quality constituents through the network over current time step by calling several functions.
-
-
qualrout_execute()
calls the following functions:-
findLinkQual()
which finds new quality in a link at the end of the current time step. Link quality is that of the upstream node when the link is not a conduit or is a dummy link. Otherwise, it calls several mass balance functions to calculate the link concentration which is set toLink[i]/newQual[p] = c2
-
findNodeQual()
which finds new quality in a node with no storage volume. If there is no flow into the node, then concentration is equal to mass inflow divided by node flow. Otherwise, the concentration is zero. -
findStorageQual()
calls several mass balance functions to calculate the concentration in a storage node which is set toNode[j]/newQual[p] = c2
-
treatmnt_treat()
which updates the pollutant concentrations at a node after treatment.
-
-
findLinkQual()
andfindNodeQual()
calls the following mass balance functions:-
massbal_addSeepageLoss()
which computes the mass balance accounting for seepage loss -
getReactedQual()
which applies 1st order reaction to a pollutant over a given timestep -
getMixedQual()
which finds pollutant concentration within a completely mixed reaction -
massbal_addToFinalStorage()
which adds mass remaining on dry surface to routing totals
-
-
treatmnt_treat()
does the following:-
Removal is zero if there is no treatment equation in the input file
-
Removal is zero if the inflow is zero
-
Otherwise, it evaluates the treatment expression to find R[p] by calling
getRemoval(p)
-
getRemoval(p)
which finds new quality in a node with no storage volume
-
-
It then updates the nodal concentrations and mass balances by calling
massbal_addReactedMass()
and setsNode[j].newQual[p] = cOut
-
massbal_addReactedMass()
which adds mass reacted during the current time step to routing totals
-
-
SWMM has some pollutant modeling limitations that must be addressed. The water quality module is limited by the range of treatment measures that can be modeled, specifically, limited nutrient treatment capabilities inside storage nodes (e.g., basins, wetlands). SWMM cannot simulate pollutant treatment inside links (e.g., conduits, channels) or pollutant generation processes (e.g., resuspension, erosion) inside any stormwater asset. Pollutant treatment cannot be turned on or off based on site conditions or other parameters, requiring treatment to run for the entire simulation. All of these constraints limit a user's ability to model complex pollutant transformations, necessitating a more generalizable and scalable approach.
Our idea is to extend the water quality modeling capabilities of SWMM by providing an API that enables users to inject and modify the pollutant in SWMM. We believe that this approach might be sustainable in the long run rather than augmenting the pollutant treatment models provided in SWMM. Through this API, users can read the inflow pollutant concentration into a node or link, model the pollutant transformation in a high-level programming language like Python, and set the computed values back into the SWMM. This approach relies upon the existing quality routing in SWMM's engine for transporting the pollutants through the stormwater network.
Modeling pollutant transformations in Python enables us to leverage the scientific computing stack to develop complex pollutant models. We plan on releasing a Python package (StormReactor) that builds on the newly added getters and setters to provide an easy to use toolkit for modeling pollutant transformations. StormReactor will provide implementations of CSTR, erosion, and other established pollutant transformation models for users to build on. We also plan on submitting a pull request to PySWMM with the pollutant getters and setters as well. StormReactor would then have PySWMM as a dependency.
In terms of specific use cases, we have updated the pollutant implementation in SWMM such that users can introduce pollutants in links to model flow driven erosion in channels. In nodes, as alluded above, users can model pollutant removal as a combination of reactors; this would vital in pollutant based control of stormwater systems. More details on the use case are provided in Section 4.0.
In terms of implementation details, we introduced extPollutFlag
in nodes and links. This flag is pollutant and object (i.e., link or node) specific. It enables users to control which pollutant they want to model externally; the flag controls when SWMM skips its pollutant removal in favor of external pollutant values. Externally set pollutant values are accounted for in SWMM's mass balance functionality. In this implementation, all the pollutants can be considered as internal, which can be modified externally. More details on the code modifications are provided in Section 3.1.
To address the limitations of SWMM's water quality module, we modified SWMM's C source code. An overview of the changes include:
-
Added new pollutant-based variables to
objects.h
to enable external pollutant handling [Section 3.1.1] -
Allocate memory for the new pollutant-based variables in
project.c
-
Added new getters to
toolkitenums.h
,toolkit.h
, andtoolkit.c
to access pollutant-based variables [Section 3.1.2] -
Added new setters to
toolkit.h
andtoolkit.c
to set pollutant concentrations [Section 3.1.3] -
Added
extPollutFlag
as a global variable inglobal.h
and initialize it by settingextPollutFlag = 0
inswmm5.c
[Section 3.1.1] -
Modified the functions
qualrout_execute()
,findLinkQual()
,findStorageQual()
inqualrout.c
for external pollutant handling [Section 3.1.4] -
Modified the function
treatmnt_treat()
intreatmnt.c
to enable external pollutant handling [Section 3.1.5]
The following summarizes the variables and their type and description added in the SWMM source code.
-
extQual
(int): External Quality State: Container for storing pollutant value that user wants to set in the node or link. -
extPollutFlag
(int): External Pollutant Flag: Flag that controls when the value in extQual is set in a node/link. If it is 0, pollutant is computed by native SWMM. When it is 1, pollutant in the node/link is set to the value in extQual. -
hrt
(double): Hydraulic Retention Time: HRT (i.e., water age) in a node is computed by SWMM. This exposes the computed HRT to users. -
inQual
(double): Inflow Quality State: Container for storing inflow quality state used in pollutant mass balance calculations. -
reactorQual
(double): Concentration in the Mixed Reactor: Container for storing reactor concentration used in pollutant mass balance calculations.
NOTE: hrt
, inQual
, and reactorQual
variables were created to expose SWMM's internal states. The other two variables were created to switch between SWMM's pollutant routing and external pollutant handling.
NOTE: The pollutant-based variables added in SWMM's source code to enable external pollutant handling. All variables are defined in object.h
except extPollutFlag
, which is defined in global.h
.
Three getters were added to toolkit.h
, and toolkit.c
in SWMM's source code. Getters enable users to access pollutant-based variables which are defined in toolkitenums.h
in SWMM's source code. They can be obtained by calling the following functions:
-
swmm_getNodePollut(int index, SMNodePollut type, double **pollutArray, int *length)
-
index
refers to the index of the node in the SWMM input file -
type
refers to theSMNodePollut
property type code -
**pollutArray
and*length
are not input parameters, they are the function output, which is an array of the pollutant data
-
-
swmm_getNodeResult(int index, SMNodeResult type, double *result)
-
index
refers to the index of the node in the SWMM input file -
type
refers to theSMNodeResult
property type code -
*result
is not an input parameter, it is the function output
-
-
swmm_getLinkPollut(int index, SMLinkPollut type, double **pollutArray, int *length)
-
index
refers to the index of the node in the SWMM input file -
type
refers to theSMLinkPollut
property type code -
**pollutArray
and*length
are not input parameters, they are the function output, which is an array of the pollutant data
-
The following summarizes the getter functionality, specifically result property and description:
-
SMNODEQUAL
: Get the current concentration in a node -
SMNODECIN
: Get the inflow concentration in a node -
SMNODEREACTORC
: Get the reactor concentration in a node -
SMHRT
: the hydraulic residence time (hours) in a node -
SMLINKQUAL
: Get the current concentration in a link -
SMTOTALLOAD
: Get the total quality mass loading in a link -
SMLINKREACTORC
: Get the reactor concentration in a link
NOTE: The pollutant-based variables that can be accessed by using the getters described above. These pollutant-based variables were added to toolkitenums.h
in SWMM's source code.
Two pollutant concentration setters, swmm_setNodePollut
and swmm_setLinkPollut
, were added to toolkit.h
and toolkit.c
in SWMM's source code which enable a user to set the pollutant concentration in a node or link at any routing time step, respectively.
-
swmm_setNodePollut(int index, int pollutant_index, double pollutant_value)
-
index
refers to the index of the node in the SWMM input file -
pollutantindex
refers to the index of desired pollutant in the SWMM input file -
pollutantvalue
refers to the value of the pollutant concentration you want to set in SWMM
-
-
swmm_setLinkPollut(int index, int type, int pollutant_index, double pollutant_value)
-
index
refers to the index of the link in the SWMM input file -
type
needs to be set toSMLINKQUAL
which sets the link's qual and allows accounting for loss and mixing calculation -
pollutantindex
refers to the index of desired pollutant in the SWMM input file -
pollutantvalue
refers to the value of the pollutant concentration you want to set in SWMM
-
When a setter is called, SWMM does the following:
-
changes the global variable
extPollutFlag
from 0 to 1.extPollutFlag = 0
by default, meaning SWMM uses its traditional pollutant functionality. WhenextPollutFlag = 1
, SWMM follows the external pollutant handling functionality as outlined in Sections 3.1.4 and 3.1.5. -
enacts the following depending on if it is a node or link setter:
-
node: sets
Node[index].extQual[pollutantindex] = pollutantvalue
andNode[index].extPollutFlag[pollutantindex] = 1
, turning on theextPollutFlag
for the specific pollutant in the specific node -
link: sets
Link[index].extQual[pollutantindex] = pollutantvalue
andLink[index].extPollutFlag[pollutantindex] = 1
, turning on theextPollutFlag
for the specific pollutant in the specific link
-
NOTE: The setters added to toolkit.h
and toolkit.c
in SWMM's source code. The setters enables a user to change the pollutant concentration in a link or node at any time step.
First, we modified findStorageQual()
by adding Node[j].reactorQual[p] = c2
so that users can access the node reactor
concentration using the appropriate getter [Section 3.1.2].
Then, we modified qualrout_execute()
. In qualrout_execute()
we check the value of the extPollutFlag
. When extPollutFlag
is set to 1, SWMM does the following for nodes:
-
runs
treatmnt_setInflow
intreatmnt.c
to compute the inflow concentrations to a node [Section 3.1.5] -
runs
treatmnt_treat()
intreatmnt.c
to update the pollutant concentrations at a node after treatment [Section 3.1.5] -
at the end of the time step, sets
extPollutFlag=0
so that if user does not want to have external control during the next time step, SWMM can reverts back to its traditional pollutant functionality
Finally, we modified findLinkQual()
. Here is where the link concentration is set when the setter is used. We had to use this function because treatmnt.c
is only for updating node concentrations. In findLinkQual()
, we added lossExtQual
, a new double variable which is the loss value for external water quality. When extPollutFlag
is set to 1, SWMM does the following in findLinkQual()
:
-
calculates
lossExtQual
-
inputs
lossExtQual
to the functionmassbaladdReactedMass()
-
sets
Link[i].newQual[p] = Link[i].extQual[p]
-
resets the flag
Link[i].extPollutFlag[p] = 0
so that if user does not want to have external control during the next time step, SWMM can reverts back to its traditional pollutant functionality
First off, treatmnt.c
is only for nodes since SWMM only allows pollutant treatment in nodes. When Node[index].extPollutFlag[pollutantindex] = 1
, SWMM follows the external pollutant handling functionality added to treatmnt.c
. First,
SWMM runs treatmnt_setInflow()
which computes and saves array of inflow concentrations to a node. Then, SWMM runs treatmnt_treat()
, doing the following:
-
Sets
R[p] = 0.0
, meaning it will compute removal using a treatment equation provided in the input file for that pollutant in that node -
Sets
cOut = Node[j].extQual[p]
-
Calculates mass loss (as it normally would)
-
Resets the flag to default SWMM treatment
if (Node[j].extPollutFlag[p] == 1) Node[j].extPollutFlag[p] = 0
-
Adds mass balance totals and revises nodal concentration (as it normally would)
It should be noted that we do not set newQual
from an external function call directly, because we would have to re-implement mass
balance. By updating cOut
, we can build on SWMM's existing mass balance functionality. Furthermore, if we do not have the container
variable extQual
and have to set these pollutants at each step directly, we would have to interrupt the treatmnt_treat()
function in qualrout.c
, make an external function call to Python, read the variable, set it, and repeat this for every node/link and pollutant. By setting the pollutants in a container before the SWMM step is called, we can limit the number of external function calls we have to make, thus reducing the computational overhead.
Pollutants injected using this API are accounted for in SWMM's mass-balance. When a pollutant is injected or removed in a node or link, it is accounted for in SWMM's mass balance using the same sub-routines used by SWMM for accounting pollutant interactions.
Mass Balance in links for external pollutants is handled in qualrout.c and findlinkqual function
Mass Balance in nodes for external pollutants is handled in treatmnt.c and treatmnt_treat function
Users can access and modify pollutant concentrations in any node or link during any routing time step. Users can use new getters [Section 3.1.2] to access pollutant-based variables to model pollutant transformations and then set the new new pollutant concentration using new setters externally (e.g., Python) [Section 3.1.3].
Users can either:
-
build their model directly in a Python script using the appropriate getters and setters; or
-
add their new method to StormReactor's code base.
Example code snippet shows how a Python reactor model can be implemented for a node. The code uses getters to obtain both water quality and quantity variables which are then used to compute the new nitrate concentration. Then the setter is used to set the new node concentration in SWMM.
# Import required modules
from pyswmm import Simulation, Nodes, Links
from scipy.integrate import ode
import numpy as np
import matplotlib.pyplot as plt
#----------------------------------------------------------------------#
# CSTR Equation
def CSTR_tank(t, C, Qin, Cin, Qout, V, k):
dCdt = (Qin*Cin - Qout*C)/V - k*C
return dCdt
#----------------------------------------------------------------------#
# Setup simulation
with Simulation("./NO.inp") as sim:
# Get asset information
Wetland = Nodes(sim)["93-49759"]
# Setup dt calculation
start_time = sim.start_time
last_timestep = start_time
# Setup CSTR solver
solver = ode(CSTR_tank)
solver.set_integrator("dopri5")
# Step through the simulation
for index,step in enumerate(sim):
# Calculate dt
current_step = sim.current_time
dt = (current_step - last_timestep).total_seconds()
last_timestep = current_step
# Get wetland parameters
Wt_if = Wetland.total_inflow
Wt_d = Wetland.depth
Wt_v = Wetland.volume
Wt_of = Wetland.total_outflow
k_ni = 0.000087 # rate/5 sec
# Get parameters to calculate NO
Cin_NO = sim._model.getNodeCin("93-49759",0)
Wetland_Cin.append(Cin_NO)
#Solve Nitrate ODE
if index == 0:
solver.set_initial_value(0.0, 0.0)
solver.set_f_params(Wt_if,Cin_NO,Wt_of,Wt_v,k_ni)
else:
solver1.set_initial_value(solver1.y, solver1.t)
solver1.set_f_params(Wt_if,Cin_NO,Wt_of,Wt_v,k_ni)
# Set new concentration
sim._model.setNodePollutant("93-49759", 0, solver1.y[0])
sim._model.swmm_end()
Example code snippet shows how a Python erosion model can be implemented for a link. The code uses getters to obtain both water quality and quantity variables which are then used to compute the new sediment concentration. Then the setter is used to set the new link concentration in SWMM. Details on adding a new pollutant method to StormReactor's code base can be found here.
# Import required modules
from pyswmm import Simulation, Nodes, Links
from scipy.integrate import ode
import numpy as np
import matplotlib.pyplot as plt
#----------------------------------------------------------------------#
"""
Engelund-Hansen Erosion(1967)
Engelund and Hansen (1967) developed a procedure for predicting stage-
discharge relationships and sediment transport in alluvial streams.
w = channel width (SI: m, US: ft)
So = bottom slope (SI: m/m, US: ft/ft)
Ss = specific gravity of sediment (for soil usually between 2.65-2.80)
d50 = mean sediment particle diameter (SI/US: mm)
d = depth (SI: m, US: ft)
qt = sediment discharge per unit width (SI: kg/m-s, US: lb/ft-s)
Qt = sediment discharge (SI: kg/s, US: lb/s)
"""
#----------------------------------------------------------------------#
# Setup simulation
with Simulation("./TSS.inp") as sim:
# Get asset information
Channel = Links(sim)["95-69044"]
# Setup dt calculation
start_time = sim.start_time
last_timestep = start_time
# Step through the simulation
for index,step in enumerate(sim):
# Calculate dt
current_step = sim.current_time
dt = (current_step - last_timestep).total_seconds()
last_timestep = current_step
# Get channel parameters
Cin = sim._model.getLinkC2("95-69044", 0)
Q = sim._model.getLinkResult("95-69044", 0)
d = sim._model.getLinkResult("95-69044", 1)
v = sim._model.getConduitVelocity("95-69044")
w = 10.0
So = 0.001
Ss = 2.68
d50 = 0.7
# Erosion calculations
g = 9.81 # m/s^2
ρw = 1000 # kg/m^3
mm_m = 0.001 # m/mm
kg_mg = 1000000 # mg/kg
L_m3 = 0.001 # m3/L
if v != 0.0:
qt = 0.1*(1/((2*g*So*d)/v**2))*((d*So/((Ss-1)*d50))\
*(1/mm_m))**(5/2)*Ss*ρw*((Ss-1)*g*(d50*mm_m)**3)**(1/2) # kg/m-s
Qt = w*qt # kg/s
else:
Qt = 0.0
if Q != 0.0:
Cnew = (Qt/Q)*L_m3*kg_mg # mg/L
Cnew = max(Cin, Cin+Cnew)
# Set new concentration
sim._model.setLinkPollutant("95-69044", "SM_LINKQUAL", 0, Cnew)
sim._model.swmm_end()