Reservoir Operating Rules
This example shows an example of a simple network with a reservoir and how it impacts the downstream flows.
First let us generate a random network
net.random_tree(8, seed=1232434)
nodes.reservoir = false
# define two reservoirs
nodes[7,1].reservoir = true
# a function to generate flows
func gen_flow(seed, ind){
random(seed+ind) * int(random_bool(0.3, seed+ind)) * (1 + int(random_bool(0.1, seed+ind)))
}
N = 40
# define flows at the leaves
nodes.noleaves = true;
leaves.noleaves = false;
leaves$flows = [gen_flow(INDEX, i) for i in 1:N]
nodes(noleaves)$local_flows = [{gen_flow(INDEX,i)/2} for i in 1:N]
nodes<inp>(noleaves)$flows = $local_flows + (inputs$flows -> @sum)
nodes.mean_flow = sr_mean("flows")
nodes.max_flow = $flows ->+ func(a, LAST=0) {max([a, LAST], 0)}
# rudimentary visualization of the network with flow series
nodes(reservoir).visual = {nodeshape="box", nodecolor="black"}
net.cairo.plot("flows", "./output/ro-reservoir-table.svg", normalize=true)
Results:
nm<inv> {sr_min("flows")}
Results:
{
"3" = 0.0,
"7" = 0.0,
"2" = 0.0,
"4" = 0.0,
"1" = 0.0,
"0" = 0.0,
"5" = 0.08524753881832114,
"6" = 0.08524753881832114
}
Now let us have a simple rule, that reservoirs operate by providing flows at a constant if it is less than mean flow at that location
nodes[7].low_flow_limit = 0.05
nodes[1].low_flow_limit = 0.1
leaves$c_flows = $flows
nodes<inp>(noleaves)$c_flows = if (reservoir) {
($local_flows, inputs$c_flows) -> func(l, i) {
x = l + sum(i);
# we can use node variables because it is a node context
max([x], node.low_flow_limit)
}
} else {
$local_flows + (inputs$c_flows -> @sum)
}
net.cairo.plot("c_flows", "./output/ro-reservoir-table-2.svg", normalize=true)
Results:
If we look at the minimum flows, we can see how it has maintained the provided minimum flows from the reservoirs onwards.
nm<inv> {[sr_min("flows"), sr_min("c_flows")]}
Results:
{
"3" = [0.0, 0.0],
"7" = [0.0, 0.05],
"2" = [0.0, 0.0],
"4" = [0.0, 0.05],
"1" = [0.0, 0.1],
"0" = [0.0, 0.0],
"5" = [0.08524753881832114, 0.1244055736719738],
"6" = [0.08524753881832114, 0.13524753881832113]
}
Of course we did not put any mass balance into the equation. We can’t simply provide the extra flow without taking it from somewhere, so we should make a storage value and save it.
Let’s provide both reservoirs with storage values of 5 unit. And see how it changes when we provide the minimum flow by augmenting the flow using storage.
nodes(reservoir).storage = 5.0
leaves$c_flows = $flows
nodes<inp>(noleaves)$c_flows = if (reservoir) {
($local_flows, inputs$c_flows) -> func(l, i) {
x = l + sum(i);
if (x < node.low_flow_limit) {
aug = node.low_flow_limit - x
node.storage = node.storage - aug
node.low_flow_limit
} else {x}
}
} else {
$local_flows + (inputs$c_flows -> @sum)
}
nm<inv>.storage
Results:
{
"3" = <None>,
"7" = 4.090680944257121,
"2" = <None>,
"4" = <None>,
"1" = 4.902766460431184,
"0" = <None>,
"5" = <None>,
"6" = <None>
}
The storage at the end shows how much of the water was used up to provide the minimum flows.
Now, let’s try to maintain the storage at a certain level by taking excess values from the flows when it has more than the minimum.
nodes(reservoir).storage = 5.0
leaves$c_flows = $flows
nodes<inp>(noleaves)$c_flows = if (reservoir) {
($local_flows, inputs$c_flows) -> func(l, i) {
x = l + sum(i);
if (x < node.low_flow_limit) {
aug = node.low_flow_limit - x
node.storage = node.storage - aug
node.low_flow_limit
} else {
if (node.storage >= 5.0) { x } else {
excess = x - node.low_flow_limit
take = excess * 0.8
node.storage = storage + take
x - take
}
}
}
} else {
$local_flows + (inputs$c_flows -> @sum)
}
nm<inv>.storage
Results:
{
"3" = <None>,
"7" = 4.87491229927272,
"2" = <None>,
"4" = <None>,
"1" = 5.142603270575256,
"0" = <None>,
"5" = <None>,
"6" = <None>
}
While we have more water in storage here compared to the previous, we still did not recover the storage loss, this is because our minimum flow was arbritary
net.cairo.plot("c_flows", "./output/ro-reservoirs-final.svg", normalize=true, config={delta_y=40.0})
Results:
Let’s look at the mean, minimum and maximum flows at the nodes before and after the reservoir operation rules.
nm<inv> {[sr_mean('flows'), sr_mean('c_flows')]}
Results:
{
"3" = [0.05201108811624298, 0.05201108811624298],
"7" = [0.07940064308632276, 0.08252783560450483],
"2" = [0.054779109940159644, 0.054779109940159644],
"4" = [0.16521959389081808, 0.16834678640900014],
"1" = [0.19622891913238585, 0.19579102988618646],
"0" = [0.06038987573525525, 0.06038987573525525],
"5" = [0.2868137327352687, 0.2863758434890693],
"6" = [0.31700867060289634, 0.31657078135669703]
}
nm<inv> {[sr_min('flows'), sr_min('c_flows')]}
Results:
{
"3" = [0.0, 0.0],
"7" = [0.0, 0.05],
"2" = [0.0, 0.0],
"4" = [0.0, 0.05],
"1" = [0.0, 0.1],
"0" = [0.0, 0.0],
"5" = [0.08524753881832114, 0.1],
"6" = [0.08524753881832114, 0.13524753881832113]
}
nm<inv> {[sr_max('flows'), sr_max('c_flows')]}
Results:
{
"3" = [0.26922029023541016, 0.26922029023541016],
"7" = [0.26922029023541016, 0.26922029023541016],
"2" = [0.26922029023541016, 0.26922029023541016],
"4" = [0.4076741577386759, 0.4076741577386759],
"1" = [0.4076741577386759, 0.4076741577386759],
"0" = [0.29202287154046747, 0.29202287154046747],
"5" = [0.676894447974086, 0.676894447974086],
"6" = [0.676894447974086, 0.676894447974086]
}
Conclusion
In this chapter we generated a sample network, assigned two nodes as reservoirs and defined rules to provide minimum flows as well as algorithms to recover storage volume.
Bonus HEC-DSS
For bonus, let’s save the data into HEC-DSS format.
nodes do dss.save_series($flows, "./output/ro-reservoir-test.dss", r"/{NAME}/Flow/1910Jan01//1Day/F/")
nodes do dss.save_series($flows, "./output/ro-reservoir-test.dss", r"/{NAME}/ModFlow/1910Jan01//1Day/F/")
Results:
You can download the dss and test it here