: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 a node (.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 a node on-the-fly. If you are creating a new type of node that is a generic physical object, then you should probably create a new subclass of existing classes. However, if you are aiming to alter the behaviour of a physical object 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 implementing a minimum required flow at the abstraction location.
Create baseline¶
We will first create and simulate the Oxford model to formulate baseline results.
Start by importing packages.
import os
import pandas as pd
from matplotlib import pyplot as plt
from wsimod.core import constants
from wsimod.demo.create_oxford import create_oxford_model
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(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]
3%|██████ | 41/1456 [00:00<00:03, 400.80it/s]
6%|████████████▏ | 82/1456 [00:00<00:03, 405.51it/s]
8%|██████████████████▏ | 123/1456 [00:00<00:03, 404.60it/s]
11%|████████████████████████▎ | 164/1456 [00:00<00:03, 402.07it/s]
14%|██████████████████████████████▍ | 205/1456 [00:00<00:03, 404.24it/s]
17%|████████████████████████████████████▍ | 246/1456 [00:00<00:02, 404.44it/s]
20%|██████████████████████████████████████████▌ | 287/1456 [00:00<00:02, 402.31it/s]
23%|████████████████████████████████████████████████▋ | 328/1456 [00:00<00:02, 389.91it/s]
25%|██████████████████████████████████████████████████████▌ | 368/1456 [00:00<00:02, 391.79it/s]
28%|████████████████████████████████████████████████████████████▌ | 408/1456 [00:01<00:02, 353.00it/s]
31%|██████████████████████████████████████████████████████████████████▌ | 449/1456 [00:01<00:02, 366.61it/s]
34%|████████████████████████████████████████████████████████████████████████▋ | 490/1456 [00:01<00:02, 376.93it/s]
36%|██████████████████████████████████████████████████████████████████████████████▊ | 531/1456 [00:01<00:02, 384.33it/s]
39%|████████████████████████████████████████████████████████████████████████████████████▊ | 572/1456 [00:01<00:02, 390.03it/s]
42%|██████████████████████████████████████████████████████████████████████████████████████████▉ | 613/1456 [00:01<00:02, 394.34it/s]
45%|████████████████████████████████████████████████████████████████████████████████████████████████▊ | 653/1456 [00:01<00:02, 392.09it/s]
48%|██████████████████████████████████████████████████████████████████████████████████████████████████████▊ | 693/1456 [00:01<00:01, 392.26it/s]
50%|████████████████████████████████████████████████████████████████████████████████████████████████████████████▋ | 733/1456 [00:01<00:01, 387.47it/s]
53%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████▊ | 774/1456 [00:01<00:01, 393.35it/s]
56%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▉ | 815/1456 [00:02<00:01, 397.71it/s]
59%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▉ | 856/1456 [00:02<00:01, 400.23it/s]
62%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████ | 897/1456 [00:02<00:01, 402.30it/s]
64%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ | 938/1456 [00:02<00:01, 402.85it/s]
67%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ | 979/1456 [00:02<00:01, 398.50it/s]
70%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌ | 1020/1456 [00:02<00:01, 399.08it/s]
73%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌ | 1060/1456 [00:02<00:00, 397.73it/s]
76%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌ | 1101/1456 [00:02<00:00, 399.08it/s]
78%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▋ | 1142/1456 [00:02<00:00, 401.80it/s]
81%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▋ | 1183/1456 [00:03<00:00, 401.69it/s]
84%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▋ | 1224/1456 [00:03<00:00, 396.38it/s]
87%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▊ | 1265/1456 [00:03<00:00, 397.60it/s]
90%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▋ | 1305/1456 [00:03<00:00, 389.39it/s]
92%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌ | 1345/1456 [00:03<00:00, 392.37it/s]
95%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▋ | 1386/1456 [00:03<00:00, 396.46it/s]
98%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌ | 1426/1456 [00:03<00:00, 385.66it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1456/1456 [00:03<00:00, 392.64it/s]
When we plot results, we see no variability in abstractions and reservoir levels, maybe this is fine, but the river flow is getting drawn down quite low (down to 0.75m3/s, which is less than one quarter of the Q95 flow).
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 node¶
To protect the river from abstractions during low flows, we can customise the abstraction node to implement a MRF.
Our new node will want to behave similar to the old node, but with different behaviour when another node is pulling water from it. Thus we will define new functions to accommodate this.
The first function we will define is a 'pull check' - that is, how should the node respond when another node queries how much water could be pulled from it.
The only new line in comparison to that defined in the default node is where we 'Apply MRF'. We introduce two new variables, one for the minimum required flow (mrf), and one for the mrf already satisfied.
def pull_check_mrf(node, vqip=None):
"""
Args:
node:
vqip:
Returns:
"""
# Respond to a pull check to the node
# Work out how much water is available upstream
reply = node.pull_check_basic()
# Apply MRF
reply["volume"] = max(
reply["volume"] - (node.mrf - node.mrf_satisfied_this_timestep), 0
)
# If the pulled amount has been specified, pick the minimum of the available and that
if vqip is not None:
reply["volume"] = min(reply["volume"], vqip["volume"])
# Return avaiable
return reply
We must also define what will happen during a 'pull set' - that is, how should the node respond when another node requests water to pull from it.
The default function for a node to do this is pull_distributed
, which pulls
water from upstream nodes. We still call pull_distributed, however we
increase the amount we request via it by the amount of MRF yet to satisfy.
Any water which satsifies the MRF goes towards mrf_satisfied_this_timestep
,
then the rest is available in the 'reply', for use in the pull request.
Finally, we route the water that was used to satisfy the mrf downstream so
that it cannot be used again this timestep.
def pull_set_mrf(node, vqip):
"""
Args:
node:
vqip:
Returns:
"""
# Respond to a pull request to the node
# Copy the request for boring reasons
request = node.copy_vqip(vqip)
# Increase the request by the amount of MRF not yet satisfied
request["volume"] += node.mrf - node.mrf_satisfied_this_timestep
# Pull the new updated request from upstream nodes
reply = node.pull_distributed(request)
# First satisfy the MRF
reply_to_mrf = min((node.mrf - node.mrf_satisfied_this_timestep), reply["volume"])
node.mrf_satisfied_this_timestep += reply_to_mrf
reply_to_mrf = node.v_change_vqip(reply, reply_to_mrf)
# Update reply (less the amount for MRF)
reply = node.extract_vqip(reply, reply_to_mrf)
# Then route that water downstream so it is not available
mrf_route_reply = node.push_distributed(reply_to_mrf, of_type=["Node"])
if mrf_route_reply["volume"] > constants.FLOAT_ACCURACY:
print("warning MRF not able to push")
# Return pulled amount
return reply
We should also adjust the behaviour of a 'push set' - that is, how should the node respond when another node pushes water to it. We will need to do this because we want water that this node sends downstream on interactions that are not to do with pulls to still update the minimum required flow.
The default behaviour for a node to do a push set is push_distributed
, which
pushes water to downstream nodes. We do this as normal, but then update the
mrf_satisfied_this_timestep
def push_set_mrf(node, vqip):
"""
Args:
node:
vqip:
Returns:
"""
# Respond to a push request to the node
# Push water downstream
reply = node.push_distributed(vqip)
total_pushed_downstream = vqip["volume"] - reply["volume"]
# Allow this water to contribute to mrf
node.mrf_satisfied_this_timestep = min(
node.mrf, node.mrf_satisfied_this_timestep + total_pushed_downstream
)
# Return the amount not pushed
return reply
We also create a function to reset the mrf_satisfied_this_timestep
at the end
of each timestep
def end_timestep(node):
"""
Args:
node:
"""
# Update MRF satisfied
node.mrf_satisfied_this_timestep = 0
Finally we write a wrapper to assign the predefined functions to the node, and to set the mrf parameter.
def convert_to_mrf(node, mrf=5):
"""
Args:
node:
mrf:
"""
# Initialise MRF variables
node.mrf = mrf
node.mrf_satisfied_this_timestep = 0
# Change pull functions to the ones defined above
node.pull_set_handler["default"] = lambda x: pull_set_mrf(node, x)
node.pull_check_handler["default"] = lambda x: pull_check_mrf(node, x)
# Change end timestep function to one defined above
node.end_timestep = lambda: end_timestep(node)
Now we can create a new model
customised_model = create_oxford_model(data_folder)
.. and convert the abstraction node to apply an MRF
new_mrf = 3 * constants.M3_S_TO_M3_DT
convert_to_mrf(customised_model.nodes["farmoor_abstraction"], mrf=new_mrf)
Inspect results¶
Let us rerun and view the results to see if it has worked.
flows_mrf, tanks_mrf, _, _ = customised_model.run()
flows_mrf = pd.DataFrame(flows_mrf)
tanks_mrf = pd.DataFrame(tanks_mrf)
0%| | 0/1456 [00:00<?, ?it/s]
3%|█████▊ | 39/1456 [00:00<00:03, 386.39it/s]
5%|███████████▋ | 78/1456 [00:00<00:03, 385.92it/s]
8%|█████████████████▌ | 118/1456 [00:00<00:03, 392.04it/s]
11%|███████████████████████▍ | 158/1456 [00:00<00:03, 390.34it/s]
14%|█████████████████████████████▎ | 198/1456 [00:00<00:03, 392.92it/s]
16%|███████████████████████████████████▌ | 240/1456 [00:00<00:03, 400.58it/s]
19%|█████████████████████████████████████████▋ | 281/1456 [00:00<00:02, 398.89it/s]
22%|███████████████████████████████████████████████▌ | 321/1456 [00:00<00:02, 394.43it/s]
25%|█████████████████████████████████████████████████████▌ | 361/1456 [00:00<00:02, 394.55it/s]
28%|███████████████████████████████████████████████████████████▍ | 401/1456 [00:01<00:02, 392.59it/s]
30%|█████████████████████████████████████████████████████████████████▍ | 441/1456 [00:01<00:02, 392.52it/s]
33%|███████████████████████████████████████████████████████████████████████▎ | 481/1456 [00:01<00:02, 394.34it/s]
36%|█████████████████████████████████████████████████████████████████████████████▌ | 523/1456 [00:01<00:02, 400.23it/s]
39%|███████████████████████████████████████████████████████████████████████████████████▋ | 564/1456 [00:01<00:02, 399.55it/s]
42%|█████████████████████████████████████████████████████████████████████████████████████████▊ | 605/1456 [00:01<00:02, 399.36it/s]
44%|███████████████████████████████████████████████████████████████████████████████████████████████▋ | 645/1456 [00:01<00:02, 395.37it/s]
47%|█████████████████████████████████████████████████████████████████████████████████████████████████████▌ | 685/1456 [00:01<00:01, 391.31it/s]
50%|███████████████████████████████████████████████████████████████████████████████████████████████████████████▌ | 725/1456 [00:01<00:02, 337.19it/s]
53%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████▍ | 765/1456 [00:01<00:01, 351.87it/s]
55%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▍ | 805/1456 [00:02<00:01, 364.44it/s]
58%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌ | 846/1456 [00:02<00:01, 375.23it/s]
61%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▋ | 888/1456 [00:02<00:01, 386.83it/s]
64%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▉ | 930/1456 [00:02<00:01, 394.41it/s]
67%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ | 972/1456 [00:02<00:01, 400.31it/s]
70%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▋ | 1014/1456 [00:02<00:01, 403.56it/s]
72%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▊ | 1055/1456 [00:02<00:01, 400.54it/s]
75%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▊ | 1096/1456 [00:02<00:00, 398.57it/s]
78%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▋ | 1136/1456 [00:02<00:00, 387.40it/s]
81%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌ | 1175/1456 [00:03<00:00, 385.92it/s]
83%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▎ | 1214/1456 [00:03<00:00, 384.21it/s]
86%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████ | 1253/1456 [00:03<00:00, 380.39it/s]
89%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▊ | 1292/1456 [00:03<00:00, 383.16it/s]
91%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▋ | 1332/1456 [00:03<00:00, 386.47it/s]
94%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌ | 1372/1456 [00:03<00:00, 389.06it/s]
97%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌ | 1412/1456 [00:03<00:00, 389.66it/s]
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▍| 1452/1456 [00:03<00:00, 392.27it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1456/1456 [00:03<00:00, 388.28it/s]
We can see that, at multiple low flow points in the timeseries, the customised river flow downstream of abstractions is higher than the baseline. We also see that this results in highly variable abstractions, which are non- existant when the river flow is below the MRF, and much higher following low flows. Finally we see significant impacts on the reservoir storage, which is now dynamic and much more realistic looking.
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_mrf.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")
plot_mrf = pd.DataFrame(
index=[customised_model.dates[0], customised_model.dates[-1]],
columns=["MRF"],
data=[new_mrf, new_mrf],
)
plot_mrf.plot(ax=axs[0], color="k", ls="--")
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_mrf.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_mrf.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()
What next¶
Creating a new node is one way to achieve customisable behaviour to fit your use case, but it is not the only way to do this. Sometimes it may be easier to customise an arc.