:warning: Warning: this code does not represent best practice for customisation. Instead we recommend use of decorators to overwrite behaviour. Users can see examples of decorators in the customise_interactions and customise_riverreservoir guides. This guide may still be useful for WSIMOD code examples, and it will still work in many cases.
There is currently a GitHub (issue)[https://github.com/barneydobson/wsi/issues/11] to revise this guide.
Customise an arc (.py)¶
Note - this script can also be opened in interactive Python if you wanted to play around. On the GitHub it is in docs/demo/scripts
Introduction¶
In this tutorial we will demonstrate how to customise an arc on-the-fly. If you are creating a new type of arc that is a generic physical object (e.g., a weir), then you should probably create a new subclass of existing classes. However, if you are aiming to alter the behaviour of an arc that already has a class to fit some super specific behaviour, then it may be more suitable to customise on-the-fly. In addition, customising on-the-fly is very similar to creating a subclass, so the skills demonstrated here are likely to be transferrable.
We will use the Oxford demo as our base model, and build off it by assigning a varying capacity to the river abstraction depending on reservoir storage
Create baseline¶
We will first create and simulate the Oxford model to formulate baseline results. We use a version of the model with the minimum required flow from the node customisation demo.
Start by importing packages.
import os
import pandas as pd
from matplotlib import pyplot as plt
from wsimod.demo.create_oxford import create_oxford_model_mrf
The model can be automatically created using the data_folder
# Select the root path for the data folder. Use the appropriate value for your case.
data_folder = os.path.join(os.path.abspath(""), "docs", "demo", "data")
baseline_model = create_oxford_model_mrf(data_folder)
Simulate baseline flows
baseline_flows, baseline_tanks, _, _ = baseline_model.run()
baseline_flows = pd.DataFrame(baseline_flows)
baseline_tanks = pd.DataFrame(baseline_tanks)
0%| | 0/1456 [00:00<?, ?it/s]
2%|▏ | 33/1456 [00:00<00:04, 320.40it/s]
5%|▍ | 66/1456 [00:00<00:04, 322.03it/s]
7%|▋ | 99/1456 [00:00<00:04, 321.87it/s]
9%|▉ | 132/1456 [00:00<00:04, 267.40it/s]
11%|█▏ | 164/1456 [00:00<00:04, 283.69it/s]
14%|█▎ | 197/1456 [00:00<00:04, 295.70it/s]
16%|█▌ | 231/1456 [00:00<00:03, 307.80it/s]
18%|█▊ | 264/1456 [00:00<00:03, 311.90it/s]
20%|██ | 296/1456 [00:00<00:03, 313.25it/s]
23%|██▎ | 328/1456 [00:01<00:03, 314.48it/s]
25%|██▍ | 361/1456 [00:01<00:03, 316.13it/s]
27%|██▋ | 393/1456 [00:01<00:03, 315.03it/s]
29%|██▉ | 426/1456 [00:01<00:03, 317.04it/s]
32%|███▏ | 459/1456 [00:01<00:03, 318.49it/s]
34%|███▍ | 492/1456 [00:01<00:03, 320.49it/s]
36%|███▌ | 526/1456 [00:01<00:02, 324.81it/s]
38%|███▊ | 559/1456 [00:01<00:02, 324.13it/s]
41%|████ | 592/1456 [00:01<00:02, 324.81it/s]
43%|████▎ | 625/1456 [00:01<00:02, 323.23it/s]
45%|████▌ | 658/1456 [00:02<00:02, 322.73it/s]
47%|████▋ | 691/1456 [00:02<00:02, 319.68it/s]
50%|████▉ | 724/1456 [00:02<00:02, 320.22it/s]
52%|█████▏ | 757/1456 [00:02<00:02, 320.95it/s]
54%|█████▍ | 790/1456 [00:02<00:02, 321.54it/s]
57%|█████▋ | 823/1456 [00:02<00:01, 321.23it/s]
59%|█████▉ | 857/1456 [00:02<00:01, 324.50it/s]
61%|██████ | 891/1456 [00:02<00:01, 327.94it/s]
64%|██████▎ | 925/1456 [00:02<00:01, 330.17it/s]
66%|██████▌ | 959/1456 [00:03<00:01, 330.34it/s]
68%|██████▊ | 993/1456 [00:03<00:01, 332.13it/s]
71%|███████ | 1027/1456 [00:03<00:01, 330.76it/s]
73%|███████▎ | 1061/1456 [00:03<00:01, 326.55it/s]
75%|███████▌ | 1094/1456 [00:03<00:01, 313.10it/s]
77%|███████▋ | 1126/1456 [00:03<00:01, 288.78it/s]
79%|███████▉ | 1157/1456 [00:03<00:01, 294.14it/s]
82%|████████▏ | 1188/1456 [00:03<00:00, 296.16it/s]
84%|████████▎ | 1219/1456 [00:03<00:00, 298.26it/s]
86%|████████▌ | 1251/1456 [00:03<00:00, 302.87it/s]
88%|████████▊ | 1283/1456 [00:04<00:00, 306.86it/s]
90%|█████████ | 1315/1456 [00:04<00:00, 310.02it/s]
93%|█████████▎| 1347/1456 [00:04<00:00, 312.17it/s]
95%|█████████▍| 1379/1456 [00:04<00:00, 313.71it/s]
97%|█████████▋| 1411/1456 [00:04<00:00, 314.54it/s]
99%|█████████▉| 1443/1456 [00:04<00:00, 315.08it/s]
100%|██████████| 1456/1456 [00:04<00:00, 314.28it/s]
When we plot the results, we see some alarmingly low reservoir levels - I am sure that Thames Water would not be happy about this
f, axs = plt.subplots(3, 1, figsize=(6, 6))
baseline_flows.groupby("arc").get_group("farmoor_to_mixer").set_index("time")[
["flow"]
].plot(ax=axs[0], title="River flow downstream of abstractions")
axs[0].set_yscale("symlog")
axs[0].set_ylim([10e3, 10e7])
baseline_flows.groupby("arc").get_group("abstraction_to_farmoor").set_index("time")[
["flow"]
].plot(ax=axs[1], title="Abstraction")
axs[1].legend()
baseline_tanks.groupby("node").get_group("farmoor").set_index("time")[["storage"]].plot(
ax=axs[2], title="Reservoir storage"
)
axs[2].legend()
f.tight_layout()
Customise arc¶
The easiest way to customise an arc is by changing its get_excess
function. This is the function that determines how much capacity there is in
the arc when called.
In reality it is common for operational constraints to be seasonal, so we will define monthly amounts that, when reservoir levels are below them, the abstraction capacity can be increased. These amounts have the month as the key and the percentage that the reservoir is full as values.
levels = {
1: 0.7,
2: 0.8,
3: 0.9,
4: 0.95,
5: 0.95,
6: 0.95,
7: 0.95,
8: 0.9,
9: 0.8,
10: 0.7,
11: 0.7,
12: 0.7,
}
We also need to determine what the new abstraction capacity will be, we will choose a simple multiplier on the existing capacity
capacity_multiplier = 1.25
We can now redefine the get_excess
function. We can start with a trimmed down
version of the original get_excess
function:
def get_excess(self, direction, vqip = None, tag = 'default'):
pipe_excess = self.capacity - self.flow_in
node_excess = self.in_port.pull_check(vqip, tag)
excess = min(pipe_excess, node_excess['volume'])
return self.v_change_vqip(node_excess, excess)
This shows only the bits of the function that are relevant for pulls. In order to implement the new variable capacity, we will need to:
-identify the month
-identify the reservoir volume expressed as a percent
-adjust the capacity to reflect possibility of increased abstractions
def get_excess_new(arc, direction, vqip=None, tag="default"):
"""
Args:
arc:
direction:
vqip:
tag:
Returns:
"""
# All nodes have access to the 't' parameter, which is a datetime object
# that can return the month
month = arc.out_port.t.month
# Get percent full
pct = arc.out_port.get_percent()
# Adjust capacity
if arc.levels[month] > pct:
capacity = arc.capacity * arc.capacity_multiplier
else:
capacity = arc.capacity
# Proceed with get_excess function as normal
pipe_excess = capacity - arc.flow_in
node_excess = arc.in_port.pull_check(vqip, tag)
excess = min(pipe_excess, node_excess["volume"])
return arc.v_change_vqip(node_excess, excess)
Finally we need a wrapper to assign the new function and the associated parameters
def apply_variable_capacity(arc, levels, multiplier):
"""
Args:
arc:
levels:
multiplier:
"""
# Assign parameters
arc.levels = levels
arc.capacity_multiplier = multiplier
# Change get_excess function to new one above
arc.get_excess = lambda **x: get_excess_new(arc, **x)
Now we can create a new model
customised_model = create_oxford_model_mrf(data_folder)
.. and assign the new variable capacity
apply_variable_capacity(
customised_model.arcs["abstraction_to_farmoor"], levels, capacity_multiplier
)
Inspect results¶
Let us rerun and view the results to see if it has worked.
flows_var_cap, tanks_var_cap, _, _ = customised_model.run()
flows_var_cap = pd.DataFrame(flows_var_cap)
tanks_var_cap = pd.DataFrame(tanks_var_cap)
0%| | 0/1456 [00:00<?, ?it/s]
2%|▏ | 28/1456 [00:00<00:05, 270.72it/s]
4%|▍ | 56/1456 [00:00<00:05, 269.50it/s]
6%|▌ | 83/1456 [00:00<00:05, 269.43it/s]
8%|▊ | 110/1456 [00:00<00:05, 267.98it/s]
9%|▉ | 137/1456 [00:00<00:05, 219.86it/s]
11%|█▏ | 165/1456 [00:00<00:05, 236.11it/s]
13%|█▎ | 193/1456 [00:00<00:05, 247.03it/s]
15%|█▌ | 224/1456 [00:00<00:04, 264.70it/s]
18%|█▊ | 255/1456 [00:00<00:04, 276.19it/s]
20%|█▉ | 284/1456 [00:01<00:04, 278.86it/s]
21%|██▏ | 313/1456 [00:01<00:04, 280.67it/s]
23%|██▎ | 342/1456 [00:01<00:04, 276.68it/s]
25%|██▌ | 370/1456 [00:01<00:03, 273.91it/s]
27%|██▋ | 398/1456 [00:01<00:03, 269.64it/s]
29%|██▉ | 426/1456 [00:01<00:03, 269.94it/s]
31%|███ | 454/1456 [00:01<00:03, 265.20it/s]
33%|███▎ | 481/1456 [00:01<00:03, 266.36it/s]
35%|███▌ | 513/1456 [00:01<00:03, 280.09it/s]
37%|███▋ | 544/1456 [00:02<00:03, 287.44it/s]
39%|███▉ | 575/1456 [00:02<00:03, 291.23it/s]
42%|████▏ | 605/1456 [00:02<00:02, 290.86it/s]
44%|████▎ | 635/1456 [00:02<00:02, 287.46it/s]
46%|████▌ | 664/1456 [00:02<00:02, 280.40it/s]
48%|████▊ | 693/1456 [00:02<00:02, 276.88it/s]
50%|████▉ | 721/1456 [00:02<00:02, 273.93it/s]
51%|█████▏ | 749/1456 [00:02<00:02, 270.39it/s]
53%|█████▎ | 777/1456 [00:02<00:02, 270.19it/s]
55%|█████▌ | 805/1456 [00:02<00:02, 270.26it/s]
57%|█████▋ | 833/1456 [00:03<00:02, 266.55it/s]
59%|█████▉ | 860/1456 [00:03<00:02, 266.36it/s]
61%|██████ | 891/1456 [00:03<00:02, 277.75it/s]
63%|██████▎ | 921/1456 [00:03<00:01, 283.00it/s]
65%|██████▌ | 953/1456 [00:03<00:01, 293.17it/s]
68%|██████▊ | 985/1456 [00:03<00:01, 299.44it/s]
70%|██████▉ | 1017/1456 [00:03<00:01, 303.48it/s]
72%|███████▏ | 1048/1456 [00:03<00:01, 298.16it/s]
74%|███████▍ | 1078/1456 [00:03<00:01, 295.27it/s]
76%|███████▌ | 1108/1456 [00:04<00:01, 291.51it/s]
78%|███████▊ | 1138/1456 [00:04<00:01, 291.14it/s]
80%|████████ | 1168/1456 [00:04<00:00, 289.38it/s]
82%|████████▏ | 1197/1456 [00:04<00:00, 286.59it/s]
84%|████████▍ | 1226/1456 [00:04<00:00, 280.04it/s]
86%|████████▌ | 1255/1456 [00:04<00:00, 275.91it/s]
88%|████████▊ | 1283/1456 [00:04<00:00, 273.25it/s]
90%|█████████ | 1311/1456 [00:04<00:00, 269.69it/s]
92%|█████████▏| 1338/1456 [00:04<00:00, 269.12it/s]
94%|█████████▍| 1365/1456 [00:04<00:00, 268.43it/s]
96%|█████████▌| 1392/1456 [00:05<00:00, 268.08it/s]
97%|█████████▋| 1419/1456 [00:05<00:00, 267.85it/s]
99%|█████████▉| 1446/1456 [00:05<00:00, 267.63it/s]
100%|██████████| 1456/1456 [00:05<00:00, 275.03it/s]
We can see that the reservoir storage is able to recharge much more quickly from the lowest point because of the increased abstractions. We also see negligible change in river flow because the increased abstractions are mainly occurring when the flow is quite high. This is still unlikely to be a preferred option in practice because the lowest reservoir storage is unchanged. This happens because the increased capacity available is not helpful when the flows are so low that the minimum required flow is active. Better luck next time Thames Water!
f, axs = plt.subplots(3, 1, figsize=(6, 6))
plot_flows1 = pd.concat(
[
baseline_flows.groupby("arc")
.get_group("farmoor_to_mixer")
.set_index("time")
.flow.rename("Baseline"),
flows_var_cap.groupby("arc")
.get_group("farmoor_to_mixer")
.set_index("time")
.flow.rename("Customised"),
],
axis=1,
)
plot_flows1.plot(ax=axs[0], title="River flow downstream of abstractions")
axs[0].set_yscale("symlog")
axs[0].set_ylim([10e3, 10e7])
plot_flows2 = pd.concat(
[
baseline_flows.groupby("arc")
.get_group("abstraction_to_farmoor")
.set_index("time")
.flow.rename("Baseline"),
flows_var_cap.groupby("arc")
.get_group("abstraction_to_farmoor")
.set_index("time")
.flow.rename("Customised"),
],
axis=1,
)
plot_flows2.plot(ax=axs[1], title="Abstraction")
axs[1].legend()
plot_tanks = pd.concat(
[
baseline_tanks.groupby("node")
.get_group("farmoor")
.set_index("time")
.storage.rename("Baseline"),
tanks_var_cap.groupby("node")
.get_group("farmoor")
.set_index("time")
.storage.rename("Customised"),
],
axis=1,
)
plot_tanks.plot(ax=axs[2], title="Reservoir storage")
axs[2].legend()
f.tight_layout()