From 6396093b82a5247aab00912334b1cead6724d6db Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Tue, 12 Dec 2023 17:26:59 +0000 Subject: [PATCH 1/7] Long lines fixed in arcs.py --- wsimod/arcs/arcs.py | 154 +++++++++++++++++++++++--------------------- 1 file changed, 80 insertions(+), 74 deletions(-) diff --git a/wsimod/arcs/arcs.py b/wsimod/arcs/arcs.py index 63563c6e..f7559c4e 100644 --- a/wsimod/arcs/arcs.py +++ b/wsimod/arcs/arcs.py @@ -10,7 +10,8 @@ from wsimod.core import constants from wsimod.core.core import DecayObj, WSIObj -# from wsimod.nodes import nodes #Complains about circular imports.. I don't think it should do.. +# from wsimod.nodes import nodes #Complains about circular imports. +# I don't think it should do.. class Arc(WSIObj): @@ -104,10 +105,11 @@ def send_push_request(self, vqip, tag="default", force=False): Args: vqip (dict): A dict VQIP of water to push - tag (str, optional): optional message to direct the out_port's query_handler which - function to call. Defaults to 'default'. + tag (str, optional): optional message to direct the out_port's query_ + handler which function to call. Defaults to 'default'. force (bool, optional): Argument used to cause function to ignore tank - capacity of out_port, possibly resulting in pooling. Should not be used unless + capacity of out_port, possibly resulting in pooling. Should not be used + unless out_port is a tank object. Defaults to False. Returns: @@ -151,8 +153,10 @@ def send_pull_request(self, vqip, tag="default"): another node (out_port). Args: - vqip (dict): A dict VQIP of water to pull (by default, only 'volume' key is used) - tag (str, optional): optional message to direct the out_port's query_handler which + vqip (dict): A dict VQIP of water to pull (by default, only 'volume' key is + used) + tag (str, optional): optional message to direct the out_port's query_handler + which function to call. Defaults to 'default'. Returns: @@ -189,10 +193,10 @@ def send_push_check(self, vqip=None, tag="default"): another node (out_port). Args: - vqip (dict): A dict VQIP of water to push that can be specified. Defaults to None, - which returns maximum capacity to push. - tag (str, optional): optional message to direct the out_port's query_handler which - function to call. Defaults to 'default'. + vqip (dict): A dict VQIP of water to push that can be specified. Defaults to + None, which returns maximum capacity to push. + tag (str, optional): optional message to direct the out_port's + query_handler which function to call. Defaults to 'default'. Returns: (dict): A VQIP amount of water that could be pushed @@ -206,10 +210,10 @@ def send_pull_check(self, vqip=None, tag="default"): Args: vqip (dict): A dict VQIP of water to pull that can be specified (by default, - only the 'volume' key is used). Defaults to None, which returns all available - water to pull. - tag (str, optional): optional message to direct the out_port's query_handler which - function to call. Defaults to 'default'. + only the 'volume' key is used). Defaults to None, which returns all + available water to pull. + tag (str, optional): optional message to direct the out_port's + query_handler which function to call. Defaults to 'default'. Returns: (dict): A VQIP amount of water that could be pulled @@ -226,8 +230,8 @@ def get_excess(self, direction, vqip=None, tag="default"): vqip (dict, optional): A VQIP amount to push/pull that can be specified. Defaults to None, which returns all available water to pull or maximum capacity to push (depending on 'direction'). - tag (str, optional): optional message to direct the out_port's query_handler which - function to call. Defaults to 'default'. + tag (str, optional): optional message to direct the out_port's query_handler + which function to call. Defaults to 'default'. Returns: (dict): A VQIP amount of water that could be pulled/pushed @@ -243,14 +247,13 @@ def get_excess(self, direction, vqip=None, tag="default"): node_excess = self.in_port.pull_check(vqip, tag) excess = min(pipe_excess, node_excess["volume"]) - # TODO - sensible to min(vqip, excess) here? (though it should be applied by node) + # TODO sensible to min(vqip, excess) here? (though it should be applied by node) return self.v_change_vqip(node_excess, excess) def end_timestep(self): """End timestep in an arc, resetting flow/vqip in/out (which determine) - the capacity for that timestep. - """ + the capacity for that timestep.""" self.vqip_in = self.empty_vqip() self.vqip_out = self.empty_vqip() self.flow_in = 0 @@ -317,11 +320,12 @@ def send_pull_request(self, vqip, tag="default", time=0): not been extensively tested. Args: - vqip (_type_): A dict VQIP of water to pull (by default, only 'volume' key is used) - tag (str, optional): optional message to direct the out_port's query_handler which - function to call. Defaults to 'default'. - time (int, optional): Travel time for request to spend in the arc (in addition to the - arc's 'number_of_timesteps' parameter). Defaults to 0. + vqip (_type_): A dict VQIP of water to pull (by default, only 'volume' key + is used) + tag (str, optional): optional message to direct the out_port's query_handler + which function to call. Defaults to 'default'. + time (int, optional): Travel time for request to spend in the arc (in + addition to the arc's 'number_of_timesteps' parameter). Defaults to 0. Returns: (dict): A VQIP amount of water that was successfully pulled. @@ -358,12 +362,12 @@ def send_push_request(self, vqip_, tag="default", force=False, time=0): Args: vqip_ (dict): A dict VQIP of water to push. - tag (str, optional): optional message to direct the out_port's query_handler which - function to call. Defaults to 'default'. - force (bool, optional): Ignore the capacity of the arc (note does not currently, - pass the force argument to the out_port). Defaults to False. - time (int, optional): Travel time for request to spend in the arc (in addition to the - arc's 'number_of_timesteps' parameter). Defaults to 0. + tag (str, optional): optional message to direct the out_port's query_handler + which function to call. Defaults to 'default'. + force (bool, optional): Ignore the capacity of the arc (note does not + currently, pass the force argument to the out_port). Defaults to False. + time (int, optional): Travel time for request to spend in the arc (in + addition to the arc's 'number_of_timesteps' parameter). Defaults to 0. Returns: (dict): A VQIP amount of water that was not successfully pushed @@ -410,8 +414,8 @@ def enter_arc(self, request, direction, tag): request (dict): A dict with a VQIP under the 'vqip' key and the travel time under the 'time' key. direction (str): Direction of flow, can be 'push' or 'pull - tag (str, optional): optional message to direct the out_port's query_handler which - function to call. Defaults to 'default'. + tag (str, optional): optional message to direct the out_port's + query_handler which function to call. Defaults to 'default'. Returns: (dict): The request dict with additional information entered for the queue. @@ -433,8 +437,8 @@ def enter_queue(self, request, direction=None, tag="default"): request (dict): A dict with a VQIP under the 'vqip' key and the travel time under the 'time' key. direction (str): Direction of flow, can be 'push' or 'pull - tag (str, optional): optional message to direct the out_port's query_handler which - function to call. Defaults to 'default'. + tag (str, optional): optional message to direct the out_port's + query_handler which function to call. Defaults to 'default'. """ # Update inflows and format request @@ -458,16 +462,17 @@ def update_queue(self, direction=None, backflow_enabled=True): Args: - direction (str, optional): Direction of flow, can be 'push' or 'pull. Defaults to None. - backflow_enabled (bool, optional): Enable backflow, described above, if not enabled - then the request will remain in the queue until all water has been received. - Defaults to True. + direction (str, optional): Direction of flow, can be 'push' or 'pull. + Defaults to None. + backflow_enabled (bool, optional): Enable backflow, described above, if not + enabled then the request will remain in the queue until all water has + been received. Defaults to True. Returns: - total_backflow (dict): In the case of a push direction, any backflow will be returned - as a VQIP amount - total_removed (dict): In the case of a pull direction, any pulled water will be returned - as a VQIP amount + total_backflow (dict): In the case of a push direction, any backflow will be + returned as a VQIP amount + total_removed (dict): In the case of a pull direction, any pulled water will + be returned as a VQIP amount """ done_requests = [] @@ -500,7 +505,8 @@ def update_queue(self, direction=None, backflow_enabled=True): vqip_ = self.v_change_vqip(vqip, removed) total_removed = self.sum_vqip(total_removed, vqip_) - # Assume that any water that cannot arrive at destination this timestep is backflow + # Assume that any water that cannot arrive at destination this + # timestep is backflow rejected = self.v_change_vqip(vqip, vqip["volume"] - removed) if backflow_enabled | ( @@ -609,13 +615,13 @@ def update_queue(self, direction=None, backflow_enabled=True): Args: direction (str): Direction of flow, can be 'push' only. Defaults to 'push' - backflow_enabled (bool, optional): Enable backflow, described above, if not enabled - then the request will remain in the queue until all water has been received. - Defaults to True. + backflow_enabled (bool, optional): Enable backflow, described above, if not + enabled then the request will remain in the queue until all water has + been received. Defaults to True. Returns: - backflow (dict): In the case of a push direction, any backflow will be returned - as a VQIP amount + backflow (dict): In the case of a push direction, any backflow will be + returned as a VQIP amount """ # TODO - can this work for pulls?? @@ -676,9 +682,9 @@ def __init__(self, decays={}, **kwargs): """A QueueArc that applies decays from a DecayObj. Args: - decays (dict, optional): A dict of dicts containing a key for each pollutant that decays - and within that, a key for each parameter (a constant and exponent). - Defaults to {}. + decays (dict, optional): A dict of dicts containing a key for each pollutant + that decays and within that, a key for each parameter (a constant and + exponent). Defaults to {}. """ self.decays = decays @@ -696,8 +702,8 @@ def enter_queue(self, request, direction=None, tag="default"): request (dict): A dict with a VQIP under the 'vqip' key and the travel time under the 'time' key. direction (str): Direction of flow, can be 'push' or 'pull - tag (str, optional): optional message to direct the out_port's query_handler which - function to call. Defaults to 'default'. + tag (str, optional): optional message to direct the out_port's + query_handler which function to call. Defaults to 'default'. """ # Update inflows and format @@ -742,9 +748,9 @@ def __init__(self, decays={}, **kwargs): """An AltQueueArc that applies decays from a DecayObj. Args: - decays (dict, optional): A dict of dicts containing a key for each pollutant that decays - and within that, a key for each parameter (a constant and exponent). - Defaults to {}. + decays (dict, optional): A dict of dicts containing a key for each pollutant + that decays and within that, a key for each parameter (a constant and + exponent). Defaults to {}. """ self.decays = {} @@ -834,11 +840,11 @@ def send_push_deny(self, vqip, tag="default", force=False): Args: vqip (dict): A dict VQIP of water to push - tag (str, optional): optional message to direct the out_port's query_handler which - function to call. Defaults to 'default'. + tag (str, optional): optional message to direct the out_port's + query_handler which function to call. Defaults to 'default'. force (bool, optional): Argument used to cause function to ignore tank - capacity of out_port, possibly resulting in pooling. Should not be used unless - out_port is a tank object. Defaults to False. + capacity of out_port, possibly resulting in pooling. Should not be used + unless out_port is a tank object. Defaults to False. Returns: (dict): A VQIP amount of water that was not successfully pushed @@ -850,10 +856,10 @@ def send_push_check_deny(self, vqip=None, tag="default"): """Function used to deny any push checks. Args: - vqip (dict): A dict VQIP of water to push that can be specified. Defaults to None, - which returns maximum capacity to push. - tag (str, optional): optional message to direct the out_port's query_handler which - function to call. Defaults to 'default'. + vqip (dict): A dict VQIP of water to push that can be specified. Defaults to + None, which returns maximum capacity to push. + tag (str, optional): optional message to direct the out_port's + query_handler which function to call. Defaults to 'default'. Returns: (dict): An empty VQIP amount of water indicating no water can be pushed @@ -881,11 +887,11 @@ def send_pull_deny(self, vqip, tag="default", force=False): Args: vqip (dict): A dict VQIP of water to pull - tag (str, optional): optional message to direct the out_port's query_handler which - function to call. Defaults to 'default'. + tag (str, optional): optional message to direct the out_port's + query_handler which function to call. Defaults to 'default'. force (bool, optional): Argument used to cause function to ignore tank - capacity of out_port, possibly resulting in pooling. Should not be used unless - out_port is a tank object. Defaults to False. + capacity of out_port, possibly resulting in pooling. Should not be used + unless out_port is a tank object. Defaults to False. Returns: (dict): A VQIP amount of water that was successfully pulled @@ -897,10 +903,10 @@ def send_pull_check_deny(self, vqip=None, tag="default"): """Function used to deny any pull checks. Args: - vqip (dict): A dict VQIP of water to pull that can be specified. Defaults to None, - which returns maximum capacity to pull. - tag (str, optional): optional message to direct the out_port's query_handler which - function to call. Defaults to 'default'. + vqip (dict): A dict VQIP of water to pull that can be specified. Defaults to + None, which returns maximum capacity to pull. + tag (str, optional): optional message to direct the out_port's + query_handler which function to call. Defaults to 'default'. Returns: (dict): An empty VQIP amount of water indicating no water can be pulled @@ -910,12 +916,12 @@ def send_pull_check_deny(self, vqip=None, tag="default"): class SewerArc(Arc): - """ """ + """""" pass class WeirArc(SewerArc): - """ """ + """""" pass From e5b233b33d0807776657665ffa851351c8adb7d5 Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Tue, 12 Dec 2023 17:37:41 +0000 Subject: [PATCH 2/7] Add and run docformatter --- pyproject.toml | 12 +- requirements-dev.txt | 12 +- requirements.txt | 4 +- wsimod/arcs/arcs.py | 139 +++++++++-------------- wsimod/core/constants.py | 1 - wsimod/core/core.py | 83 +++++--------- wsimod/demo/create_oxford.py | 2 - wsimod/nodes/catchment.py | 14 +-- wsimod/nodes/demand.py | 40 +++---- wsimod/nodes/distribution.py | 22 ++-- wsimod/nodes/land.py | 207 +++++++++++++--------------------- wsimod/nodes/nodes.py | 139 +++++++---------------- wsimod/nodes/nutrient_pool.py | 61 ++++------ wsimod/nodes/sewer.py | 33 ++---- wsimod/nodes/storage.py | 60 +++------- wsimod/nodes/waste.py | 3 - wsimod/nodes/wtw.py | 60 ++++------ wsimod/orchestration/model.py | 55 +++------ 18 files changed, 330 insertions(+), 617 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ddb42540..e4c940cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dev = [ "ruff", "pip-tools", "pytest-cov", + "docformatter" ] demos = [ @@ -62,8 +63,13 @@ addopts = "-v -p no:warnings --cov=wsimod --cov-report=html" select = ["E", "F", "I"] # pycodestyle, Pyflakes, isort. Ignoring pydocstyle (D), for now exclude = ["docs", "tests"] # Let's ignore tests and docs folders, for now -[tool.ruff.per-file-ignores] -"wsimod/*" = ["E501"] # 176 lines (all comments) are too long (>88). Ignoring, for now +#[tool.ruff.per-file-ignores] +#"wsimod/*" = ["E501"] # 176 lines (all comments) are too long (>88). Ignoring, for now [tool.ruff.pydocstyle] -convention = "google" \ No newline at end of file +convention = "google" + +[tool.docformatter] +recursive = true +black = true +in-place = true \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 333b4f37..5370e498 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,22 +10,20 @@ build==1.0.3 # via pip-tools cfgv==3.4.0 # via pre-commit +charset-normalizer==3.3.2 + # via docformatter click==8.1.7 # via # black # pip-tools -colorama==0.4.6 - # via - # build - # click - # pytest - # tqdm coverage[toml]==7.3.2 # via pytest-cov dill==0.3.7 # via wsimod (pyproject.toml) distlib==0.3.7 # via virtualenv +docformatter==1.7.5 + # via wsimod (pyproject.toml) filelock==3.13.1 # via virtualenv identify==2.5.31 @@ -81,6 +79,8 @@ tqdm==4.66.1 # via wsimod (pyproject.toml) tzdata==2023.3 # via pandas +untokenize==0.1.1 + # via docformatter virtualenv==20.24.6 # via pre-commit wheel==0.41.3 diff --git a/requirements.txt b/requirements.txt index 31a11e07..5d873544 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,10 +2,8 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --output-file=requirements.txt +# pip-compile # -colorama==0.4.6 - # via tqdm dill==0.3.7 # via wsimod (pyproject.toml) numpy==1.26.2 diff --git a/wsimod/arcs/arcs.py b/wsimod/arcs/arcs.py index f7559c4e..5defb38f 100644 --- a/wsimod/arcs/arcs.py +++ b/wsimod/arcs/arcs.py @@ -4,7 +4,6 @@ @author: Barney Converted to totals on Thur Apr 21 2022 - """ from wsimod.core import constants @@ -26,9 +25,9 @@ def __init__( out_port=None, **kwargs, ): - """Arc objects are the way for information to be passed between nodes in - WSIMOD. They have an in_port (where a message comes from) and an out_port - (where a message goes to). + """Arc objects are the way for information to be passed between nodes in WSIMOD. + They have an in_port (where a message comes from) and an out_port (where a + message goes to). Returns: name (str): Name of arc. Defaults to ''. @@ -38,7 +37,6 @@ def __init__( when flexibility exists in_port: A WSIMOD node object where the arc starts out_port: A WSIMOD node object where the arc ends - """ # Default essential parameters self.name = name @@ -94,14 +92,13 @@ def arc_mass_balance(self): Examples: arc_in, arc_out, arc_ds = my_arc.arc_mass_balance() - """ in_, ds_, out_ = self.mass_balance() return in_, ds_, out_ def send_push_request(self, vqip, tag="default", force=False): - """Function used to transmit a push request from one node (in_port) to - another node (out_port). + """Function used to transmit a push request from one node (in_port) to another + node (out_port). Args: vqip (dict): A dict VQIP of water to push @@ -114,7 +111,6 @@ def send_push_request(self, vqip, tag="default", force=False): Returns: (dict): A VQIP amount of water that was not successfully pushed - """ vqip = self.copy_vqip(vqip) @@ -149,8 +145,8 @@ def send_push_request(self, vqip, tag="default", force=False): return reply def send_pull_request(self, vqip, tag="default"): - """Function used to transmit a pull request from one node (in_port) to - another node (out_port). + """Function used to transmit a pull request from one node (in_port) to another + node (out_port). Args: vqip (dict): A dict VQIP of water to pull (by default, only 'volume' key is @@ -161,7 +157,6 @@ def send_pull_request(self, vqip, tag="default"): Returns: (dict): A VQIP amount of water that was successfully pulled - """ volume = vqip["volume"] # Apply pipe capacity @@ -189,8 +184,8 @@ def send_pull_request(self, vqip, tag="default"): return vqip def send_push_check(self, vqip=None, tag="default"): - """Function used to transmit a push check from one node (in_port) to - another node (out_port). + """Function used to transmit a push check from one node (in_port) to another + node (out_port). Args: vqip (dict): A dict VQIP of water to push that can be specified. Defaults to @@ -200,13 +195,12 @@ def send_push_check(self, vqip=None, tag="default"): Returns: (dict): A VQIP amount of water that could be pushed - """ return self.get_excess(direction="push", vqip=vqip, tag=tag) def send_pull_check(self, vqip=None, tag="default"): - """Function used to transmit a pull check from one node (in_port) to - another node (out_port). + """Function used to transmit a pull check from one node (in_port) to another + node (out_port). Args: vqip (dict): A dict VQIP of water to pull that can be specified (by default, @@ -217,13 +211,12 @@ def send_pull_check(self, vqip=None, tag="default"): Returns: (dict): A VQIP amount of water that could be pulled - """ return self.get_excess(direction="pull", vqip=vqip, tag=tag) def get_excess(self, direction, vqip=None, tag="default"): - """Calculate how much could be pull/pulled along the arc by combining both - arc capacity and out_port check information. + """Calculate how much could be pull/pulled along the arc by combining both arc + capacity and out_port check information. Args: direction (str): should be 'pull' or 'push' @@ -235,7 +228,6 @@ def get_excess(self, direction, vqip=None, tag="default"): Returns: (dict): A VQIP amount of water that could be pulled/pushed - """ # Pipe capacity pipe_excess = self.capacity - self.flow_in @@ -252,8 +244,8 @@ def get_excess(self, direction, vqip=None, tag="default"): return self.v_change_vqip(node_excess, excess) def end_timestep(self): - """End timestep in an arc, resetting flow/vqip in/out (which determine) - the capacity for that timestep.""" + """End timestep in an arc, resetting flow/vqip in/out (which determine) the + capacity for that timestep.""" self.vqip_in = self.empty_vqip() self.vqip_out = self.empty_vqip() self.flow_in = 0 @@ -268,10 +260,10 @@ class QueueArc(Arc): """""" def __init__(self, number_of_timesteps=0, **kwargs): - """A queue arc that stores each push or pull individually in the queue. - Enables implementation of travel time. A fixed number of timesteps can be - specified as a parameter, and additional number of timesteps can be - specified when the requests are made. + """A queue arc that stores each push or pull individually in the queue. Enables + implementation of travel time. A fixed number of timesteps can be specified as a + parameter, and additional number of timesteps can be specified when the requests + are made. The queue is a list of requests, where their travel time is decremented by 1 each timestep. Any requests with a travel time of 0 will be sent @@ -280,7 +272,6 @@ def __init__(self, number_of_timesteps=0, **kwargs): Args: number_of_timesteps (int, optional): Fixed number of timesteps that it takes to traverse the arc. Defaults to 0. - """ self.number_of_timesteps = number_of_timesteps self.queue = [] @@ -296,7 +287,6 @@ def queue_arc_ds(self): Returns: (dict): A VQIP amount of change - """ self.queue_storage = self.queue_arc_sum() return self.extract_vqip(self.queue_storage, self.queue_storage_) @@ -306,7 +296,6 @@ def queue_arc_sum(self): Returns: (dict): A VQIP amount of water/pollutants in the arc - """ queue_storage = self.empty_vqip() for request in self.queue: @@ -314,10 +303,10 @@ def queue_arc_sum(self): return queue_storage def send_pull_request(self, vqip, tag="default", time=0): - """Function used to transmit a pull request from one node (in_port) to - another node (out_port). Any pulled water is immediately removed from the - out_port and then takes the travel time to be received. This function has - not been extensively tested. + """Function used to transmit a pull request from one node (in_port) to another + node (out_port). Any pulled water is immediately removed from the out_port and + then takes the travel time to be received. This function has not been + extensively tested. Args: vqip (_type_): A dict VQIP of water to pull (by default, only 'volume' key @@ -329,7 +318,6 @@ def send_pull_request(self, vqip, tag="default", time=0): Returns: (dict): A VQIP amount of water that was successfully pulled. - """ volume = vqip["volume"] # Apply pipe capacity @@ -357,8 +345,8 @@ def send_pull_request(self, vqip, tag="default", time=0): return reply def send_push_request(self, vqip_, tag="default", force=False, time=0): - """Function used to transmit a push request from one node (in_port) to - another node (out_port). + """Function used to transmit a push request from one node (in_port) to another + node (out_port). Args: vqip_ (dict): A dict VQIP of water to push. @@ -366,12 +354,11 @@ def send_push_request(self, vqip_, tag="default", force=False, time=0): which function to call. Defaults to 'default'. force (bool, optional): Ignore the capacity of the arc (note does not currently, pass the force argument to the out_port). Defaults to False. - time (int, optional): Travel time for request to spend in the arc (in + time (int, optional): Travel time for request to spend in the arc (in addition to the arc's 'number_of_timesteps' parameter). Defaults to 0. Returns: (dict): A VQIP amount of water that was not successfully pushed - """ vqip = self.copy_vqip(vqip_) @@ -419,7 +406,6 @@ def enter_arc(self, request, direction, tag): Returns: (dict): The request dict with additional information entered for the queue. - """ request["average_flow"] = request["vqip"]["volume"] / (request["time"] + 1) request["direction"] = direction @@ -439,7 +425,6 @@ def enter_queue(self, request, direction=None, tag="default"): direction (str): Direction of flow, can be 'push' or 'pull tag (str, optional): optional message to direct the out_port's query_handler which function to call. Defaults to 'default'. - """ # Update inflows and format request request = self.enter_arc(request, direction, tag) @@ -448,8 +433,7 @@ def enter_queue(self, request, direction=None, tag="default"): self.queue.append(request) def update_queue(self, direction=None, backflow_enabled=True): - """Iterate over all requests in the queue, removing them if they have no - volume. + """Iterate over all requests in the queue, removing them if they have no volume. If a request is a push and has 0 travel time remaining then the push will be triggered at the out_port, if the out_port responds that @@ -465,7 +449,7 @@ def update_queue(self, direction=None, backflow_enabled=True): direction (str, optional): Direction of flow, can be 'push' or 'pull. Defaults to None. backflow_enabled (bool, optional): Enable backflow, described above, if not - enabled then the request will remain in the queue until all water has + enabled then the request will remain in the queue until all water has been received. Defaults to True. Returns: @@ -473,7 +457,6 @@ def update_queue(self, direction=None, backflow_enabled=True): returned as a VQIP amount total_removed (dict): In the case of a pull direction, any pulled water will be returned as a VQIP amount - """ done_requests = [] @@ -532,11 +515,10 @@ def update_queue(self, direction=None, backflow_enabled=True): print("No direction") def end_timestep(self): - """End timestep in an arc, resetting flow/vqip in/out (which determine) - the capacity for that timestep. + """End timestep in an arc, resetting flow/vqip in/out (which determine) the + capacity for that timestep. Update times of requests in the queue. - """ self.vqip_in = self.empty_vqip() self.vqip_out = self.empty_vqip() @@ -561,11 +543,10 @@ class AltQueueArc(QueueArc): """""" def __init__(self, **kwargs): - """A simpler queue arc that has a queue that is a dict where each key is - the travel time. + """A simpler queue arc that has a queue that is a dict where each key is the + travel time. Cannot be used if arc capacity is dynamic. Cannot be used for pulls. - """ self.queue_arc_sum = self.alt_queue_arc_sum @@ -578,7 +559,6 @@ def alt_queue_arc_sum(self): Returns: (dict): A VQIP amount of water/pollutants in the arc - """ queue_storage = self.empty_vqip() for request in self.queue.values(): @@ -594,7 +574,6 @@ def enter_queue(self, request, direction="push", tag="default"): direction (str): Direction of flow, can be 'push' only. Defaults to 'push' tag (str, optional): Optional message for out_port's query handler, can be 'default' only. Defaults to 'default'. - """ # Update inflows and format request request = self.enter_arc(request, direction, tag) @@ -610,8 +589,8 @@ def enter_queue(self, request, direction="push", tag="default"): def update_queue(self, direction=None, backflow_enabled=True): """Trigger the push of water in the 0th key for the queue, if the out_port - responds that it cannot receive the push, then this water will be returned - as backflow (if enabled). + responds that it cannot receive the push, then this water will be returned as + backflow (if enabled). Args: direction (str): Direction of flow, can be 'push' only. Defaults to 'push' @@ -622,7 +601,6 @@ def update_queue(self, direction=None, backflow_enabled=True): Returns: backflow (dict): In the case of a push direction, any backflow will be returned as a VQIP amount - """ # TODO - can this work for pulls?? @@ -647,11 +625,10 @@ def update_queue(self, direction=None, backflow_enabled=True): return backflow def end_timestep(self): - """End timestep in an arc, resetting flow/vqip in/out (which determine) - the capacity for that timestep. + """End timestep in an arc, resetting flow/vqip in/out (which determine) the + capacity for that timestep. Update timings in the queue. - """ self.vqip_in = self.empty_vqip() self.vqip_out = self.empty_vqip() @@ -685,7 +662,6 @@ def __init__(self, decays={}, **kwargs): decays (dict, optional): A dict of dicts containing a key for each pollutant that decays and within that, a key for each parameter (a constant and exponent). Defaults to {}. - """ self.decays = decays @@ -695,8 +671,8 @@ def __init__(self, decays={}, **kwargs): self.mass_balance_out.append(lambda: self.total_decayed) def enter_queue(self, request, direction=None, tag="default"): - """Add a request into the arc's queue list. Apply the make_decay function - (i.e., the decay that occur's this timestep). + """Add a request into the arc's queue list. Apply the make_decay function (i.e., + the decay that occur's this timestep). Args: request (dict): A dict with a VQIP under the 'vqip' key and the travel @@ -704,7 +680,6 @@ def enter_queue(self, request, direction=None, tag="default"): direction (str): Direction of flow, can be 'push' or 'pull tag (str, optional): optional message to direct the out_port's query_handler which function to call. Defaults to 'default'. - """ # Update inflows and format request = self.enter_arc(request, direction, tag) @@ -720,12 +695,11 @@ def enter_queue(self, request, direction=None, tag="default"): self.queue.append(request) def end_timestep(self): - """End timestep in an arc, resetting flow/vqip in/out (which determine) - the capacity for that timestep. - - Update times of requests in the queue. Apply the make_decay function - (i.e., the decay that occurs in the following timestep). + """End timestep in an arc, resetting flow/vqip in/out (which determine) the + capacity for that timestep. + Update times of requests in the queue. Apply the make_decay function (i.e., the + decay that occurs in the following timestep). """ self.vqip_in = self.empty_vqip() self.vqip_out = self.empty_vqip() @@ -751,7 +725,6 @@ def __init__(self, decays={}, **kwargs): decays (dict, optional): A dict of dicts containing a key for each pollutant that decays and within that, a key for each parameter (a constant and exponent). Defaults to {}. - """ self.decays = {} @@ -764,8 +737,8 @@ def __init__(self, decays={}, **kwargs): self.mass_balance_out.append(lambda: self.total_decayed) def enter_queue(self, request, direction=None, tag="default"): - """Add a request into the arc's queue. Apply the make_decay function - (i.e., the decay that occur's this timestep). + """Add a request into the arc's queue. Apply the make_decay function (i.e., the + decay that occur's this timestep). Args: request (dict): A dict with a VQIP under the 'vqip' key and the travel @@ -773,7 +746,6 @@ def enter_queue(self, request, direction=None, tag="default"): direction (str): Direction of flow, can be 'push' only. Defaults to 'push' tag (str, optional): Optional message for out_port's query handler, can be 'default' only. Defaults to 'default'. - """ # TODO- has no tags @@ -793,12 +765,11 @@ def enter_queue(self, request, direction=None, tag="default"): self.max_travel = max(self.max_travel, request["time"]) def _end_timestep(self): - """End timestep in an arc, resetting flow/vqip in/out (which determine) - the capacity for that timestep. - - Update timings in the queue. Apply the make_decay function (i.e., the - decay that occurs in the following timestep). + """End timestep in an arc, resetting flow/vqip in/out (which determine) the + capacity for that timestep. + Update timings in the queue. Apply the make_decay function (i.e., the decay that + occurs in the following timestep). """ self.vqip_in = self.empty_vqip() self.vqip_out = self.empty_vqip() @@ -827,9 +798,8 @@ class PullArc(Arc): def __init__(self, **kwargs): """Subclass of Arc where pushes return no availability to push. - This creates an Arc where only pull requests/checks can be sent, similar - to a river abstraction. - + This creates an Arc where only pull requests/checks can be sent, similar to a + river abstraction. """ super().__init__(**kwargs) self.send_push_request = self.send_push_deny @@ -848,7 +818,6 @@ def send_push_deny(self, vqip, tag="default", force=False): Returns: (dict): A VQIP amount of water that was not successfully pushed - """ return vqip @@ -863,7 +832,6 @@ def send_push_check_deny(self, vqip=None, tag="default"): Returns: (dict): An empty VQIP amount of water indicating no water can be pushed - """ return self.empty_vqip() @@ -874,9 +842,8 @@ class PushArc(Arc): def __init__(self, **kwargs): """Subclass of Arc where pushes return no availability to pull. - This creates an Arc where only push requests/checks can be sent, similar - to a CSO. - + This creates an Arc where only push requests/checks can be sent, similar to a + CSO. """ super().__init__(**kwargs) self.send_pull_request = self.send_pull_deny @@ -895,7 +862,6 @@ def send_pull_deny(self, vqip, tag="default", force=False): Returns: (dict): A VQIP amount of water that was successfully pulled - """ return self.empty_vqip() @@ -910,7 +876,6 @@ def send_pull_check_deny(self, vqip=None, tag="default"): Returns: (dict): An empty VQIP amount of water indicating no water can be pulled - """ return self.empty_vqip() diff --git a/wsimod/core/constants.py b/wsimod/core/constants.py index 2fc40cd7..4055f212 100644 --- a/wsimod/core/constants.py +++ b/wsimod/core/constants.py @@ -2,7 +2,6 @@ """Created on Fri Dec 6 15:17:07 2019. @author: bdobson - """ from wsimod.core import constants diff --git a/wsimod/core/core.py b/wsimod/core/core.py index f129a566..46c6eda9 100644 --- a/wsimod/core/core.py +++ b/wsimod/core/core.py @@ -4,7 +4,6 @@ @author: Barney Converted to totals on Thur Apr 21 2022 - """ from math import log10 @@ -15,18 +14,17 @@ class WSIObj: """""" def __init__(self): - """WSIObj is the base object of everything in WSIMOD. It is used to - perform VQIP operations and mass balance checking behaviour. + """WSIObj is the base object of everything in WSIMOD. It is used to perform VQIP + operations and mass balance checking behaviour. - RSE has suggested that it would make more sense to leave VQIP operations - as regular functions in a module or associated them with a VQIP class. + RSE has suggested that it would make more sense to leave VQIP operations as + regular functions in a module or associated them with a VQIP class. - Predefining empty_vqip_predefined in a class object is sensible though - because it is the foundation of many operations, and copying a dict is - many times quicker than copying a class. + Predefining empty_vqip_predefined in a class object is sensible though because + it is the foundation of many operations, and copying a dict is many times + quicker than copying a class. For now I will leave WSIObj as the base object, but this may change. - """ # Predefine empty concentrations because copying is quicker than defining self.empty_vqip_predefined = dict.fromkeys(constants.POLLUTANTS + ["volume"], 0) @@ -41,7 +39,6 @@ def empty_vqip(self): Examples: >>> obj = WSIObj() >>> obj.empty_vqip() - """ return self.empty_vqip_predefined.copy() @@ -53,14 +50,12 @@ def copy_vqip(self, t): Returns: (dict): A copy of t - """ return t.copy() def blend_vqip(self, c1, c2): - """Blends together two VQIPs that are assumed to have pollutant entries - set as pollution concentrations, blending occurs with proportionate - mixing. + """Blends together two VQIPs that are assumed to have pollutant entries set as + pollution concentrations, blending occurs with proportionate mixing. NOTE: VQIPs in WSIMOD in general store pollution as a total rather than a concentration. So you should only blend if you are doing it intentionally and @@ -72,7 +67,6 @@ def blend_vqip(self, c1, c2): Returns: c (dict): A new VQIP where c1 and c2 have been proportionately blended - """ # Blend two vqips given as concentrations c = self.empty_vqip() @@ -87,9 +81,9 @@ def blend_vqip(self, c1, c2): return c def sum_vqip(self, t1, t2): - """Combines two VQIPs where pollutant entries are assumed to be given as - mass. Volume and additive pollutants are summed while non additive - pollutants are proportionately blended. + """Combines two VQIPs where pollutant entries are assumed to be given as mass. + Volume and additive pollutants are summed while non additive pollutants are + proportionately blended. Args: t1 (dict): A VQIP where pollutant entries are mass totals @@ -105,7 +99,6 @@ def sum_vqip(self, t1, t2): >>> t = sum_vqip(t1, t2) >>> print(t) {'phosphate' : 0.5, 'volume' : 110, 'temperature' : 10.45} - """ # Sum two vqips given as totals t = self.copy_vqip(t1) @@ -123,15 +116,13 @@ def sum_vqip(self, t1, t2): return t def concentration_to_total(self, c): - """Convert a VQIP that has pollutant entries as concentrations into mass - totals. + """Convert a VQIP that has pollutant entries as concentrations into mass totals. Args: c (dict): A VQIP where pollutant entries are concentrations Returns: c (dict): A VQIP where pollutant entries are mass totals - """ c = self.copy_vqip(c) for pollutant in constants.ADDITIVE_POLLUTANTS: @@ -148,7 +139,6 @@ def total_to_concentration(self, t): Returns: c (dict): A VQIP where pollutant entries are concentrations - """ c = self.copy_vqip(t) for pollutant in constants.ADDITIVE_POLLUTANTS: @@ -176,7 +166,6 @@ def extract_vqip(self, t1, t2): >>> t = extract_vqip(t1, t2) >>> print(t) {'phosphate' : 0, 'volume' : 90, 'temperature' : 10} - """ # TODO should probably be called 'subtract_vqip' # TODO need to analyse uses of this to see if it is sensible to do something for non additive @@ -202,7 +191,6 @@ def extract_vqip_c(self, c1, c2): Returns: c (dict): A copy of c1 where each additive pollutant and volume has had c2 proportionately extracted from it - """ c = self.copy_vqip(c1) @@ -226,7 +214,6 @@ def v_distill_vqip(self, t, v): Returns: t (dict): Updated VQIP - """ # Distill v from t t = self.copy_vqip(t) @@ -247,7 +234,6 @@ def v_distill_vqip_c(self, c, v): Returns: c (dict): Updated VQIP - """ # Distill v from c c = self.copy_vqip(c) @@ -260,8 +246,8 @@ def v_distill_vqip_c(self, c, v): return c_ def v_change_vqip(self, t, v): - """Change the volume of a VQIP, where pollutants are mass totals, and - update pollutant values in proportion to the change in volume. + """Change the volume of a VQIP, where pollutants are mass totals, and update + pollutant values in proportion to the change in volume. Args: t (dict): A VQIP where pollutant entries are mass totals to get @@ -280,7 +266,6 @@ def v_change_vqip(self, t, v): >>> print(to_extract) {'volume': 10, 'phosphate': 0.025} - """ t = self.copy_vqip(t) if t["volume"] > 0: @@ -307,7 +292,6 @@ def v_change_vqip_c(self, c, v): Returns: c (dict): A new VQIP with volume udpated - """ # Change volume of vqip c = self.copy_vqip(c) @@ -315,8 +299,8 @@ def v_change_vqip_c(self, c, v): return c def ds_vqip(self, t, t_): - """Get difference between each additive pollutant and volume for VQIPs - where pollutants are given as mass totals. + """Get difference between each additive pollutant and volume for VQIPs where + pollutants are given as mass totals. Args: t (dict): A VQIP where pollutant entries are mass totals to subtract @@ -333,7 +317,6 @@ def ds_vqip(self, t, t_): >>> t = ds_vqip(t1, t2) >>> print(t) {'phosphate' : 0.05, 'volume' : 10, 'temperature' : 0} - """ ds = self.empty_vqip() for pol in constants.ADDITIVE_POLLUTANTS + ["volume"]: @@ -341,9 +324,8 @@ def ds_vqip(self, t, t_): return ds def ds_vqip_c(self, c, c_): - """Get difference between each additive pollutant and volume for VQIPs - where pollutants are given as concentrations but difference is given as - mass totals. + """Get difference between each additive pollutant and volume for VQIPs where + pollutants are given as concentrations but difference is given as mass totals. NOTE: VQIPs in WSIMOD in general store pollution as a total rather than a concentration. So you should only work with concentrations if you are doing it intentionally and know what you're doing. @@ -356,7 +338,6 @@ def ds_vqip_c(self, c, c_): Returns: ds (dict): Difference between c and c_ in mass totals - """ ds = self.empty_vqip() ds["volume"] = c["volume"] - c_["volume"] @@ -366,8 +347,8 @@ def ds_vqip_c(self, c, c_): return ds def compare_vqip(self, t1, t2): - """Compare two VQIPs and check if the difference between each key is less - ' than constants.FLOAT_ACCURACY. + """Compare two VQIPs and check if the difference between each key is less ' than + constants.FLOAT_ACCURACY. Args: t1 (dict): A VQIP @@ -375,7 +356,6 @@ def compare_vqip(self, t1, t2): Returns: bool: True if the difference is less for each key, False otherwise - """ reply = True for v in t1.keys(): @@ -384,8 +364,8 @@ def compare_vqip(self, t1, t2): return reply def mass_balance(self): - """Call all mass balance functions and compare to see if discrepancy - (i.e., if in_ != (out_ + ds_) for volume or for any additive pollutant). + """Call all mass balance functions and compare to see if discrepancy (i.e., if + in_ != (out_ + ds_) for volume or for any additive pollutant). Comparison is performed in the magnitude of the largest value of in_, ds_ or out_. And so judgement should be exercised as to whether a mass balance has @@ -398,7 +378,6 @@ def mass_balance(self): Raises: Message if mass balance does not close to constants.FLOAT_ACCURACY - """ # Iterate over mass_balance_in functions, summing values in in_ in_ = self.empty_vqip() @@ -473,7 +452,6 @@ def __init__(self, decays): Raises: Message if no access to temperature data - """ # Store decays self.decays = decays @@ -490,15 +468,14 @@ def __init__(self, decays): self.total_decayed = self.empty_vqip() def make_decay(self, vqip): - """Make decay, reading tempature and updating pollutant amounts. A wrapper - for generic_temperature_decay. + """Make decay, reading tempature and updating pollutant amounts. A wrapper for + generic_temperature_decay. Args: vqip (dict): A VQIP to decay where pollutants are given as mass totals Returns: vqip_ (dict): A VQIP with pollutant amounts updated - """ # Read temperature data temperature = self.data_input_object.data_input_dict[ @@ -511,8 +488,8 @@ def make_decay(self, vqip): return vqip_ def generic_temperature_decay(self, t, d, temperature): - """Performs temperature sensitive pollutant decay calculations for a VQIP - where pollutants are given as mass totals. + """Performs temperature sensitive pollutant decay calculations for a VQIP where + pollutants are given as mass totals. Args: t (dict): A VQIP to decay where pollutants are given as mass totals @@ -523,7 +500,6 @@ def generic_temperature_decay(self, t, d, temperature): t (dict): A VQIP with updated pollutant values diff (dict): A VQIP storing the change in pollutant values (decreases stored as positive numbers) - """ t = self.copy_vqip(t) diff = self.empty_vqip() @@ -542,8 +518,8 @@ def generic_temperature_decay(self, t, d, temperature): return t, diff def generic_temperature_decay_c(self, c, d, temperature): - """Performs temperature sensitive pollutant decay calculations for a VQIP - where pollutants are given as concentrations. + """Performs temperature sensitive pollutant decay calculations for a VQIP where + pollutants are given as concentrations. Args: c (dict): A VQIP to decay where pollutants are given as concentrations. @@ -555,7 +531,6 @@ def generic_temperature_decay_c(self, c, d, temperature): concentrations) diff (dict): A VQIP storing the change in pollutant values (decreases stored as positive numbers). Pollutants as mass totals. - """ c = self.copy_vqip(c) diff = self.empty_vqip() diff --git a/wsimod/demo/create_oxford.py b/wsimod/demo/create_oxford.py index 80fc0dbb..7dbb9724 100644 --- a/wsimod/demo/create_oxford.py +++ b/wsimod/demo/create_oxford.py @@ -2,7 +2,6 @@ """Created on Tue Nov 1 09:03:17 2022. @author: Barney - """ import os import re @@ -502,7 +501,6 @@ def create_timeseries(amount, dates, variable): Returns: (DataFrame): the formatted dataframe - """ df = pd.DataFrame(columns=["date", "variable", "value"]) df["date"] = dates diff --git a/wsimod/nodes/catchment.py b/wsimod/nodes/catchment.py index fd2c4c54..640d2383 100644 --- a/wsimod/nodes/catchment.py +++ b/wsimod/nodes/catchment.py @@ -4,7 +4,6 @@ @author: bdobson Converted to totals on 2022-05-03 - """ from wsimod.core import constants from wsimod.nodes.nodes import Node @@ -18,8 +17,8 @@ def __init__( name, data_input_dict={}, ): - """Node that reads input data to create VQIPs that are pushed downstream - and tracks abstractions made from the node, adjusting pushes accordingly. + """Node that reads input data to create VQIPs that are pushed downstream and + tracks abstractions made from the node, adjusting pushes accordingly. Args: name (str): Node name @@ -41,7 +40,6 @@ def __init__( - Values for each variable defined in `constants.POLLUTANTS` also stored in `data_input_dict` at the model timestep. _Units_: kg/m3/timestep (additive pollutants) - """ # Update args super().__init__(name) @@ -59,12 +57,11 @@ def __init__( self.end_timestep = self.end_timestep_ def get_flow(self): - """Read volume data, read pollutant data, convert additibve pollutants - from kg/m3 to kg. + """Read volume data, read pollutant data, convert additibve pollutants from + kg/m3 to kg. Returns: vqip (dict): Return read data as a VQIP - """ # TODO (if used) - note that if flow is < float accuracy then it won't # get pushed, and the pollutants will 'disappear', causing a mass balance error @@ -93,7 +90,6 @@ def get_avail(self): Returns: avail (dict): A VQIP of water available for abstraction - """ # Get available vqip avail = self.get_flow() @@ -113,7 +109,6 @@ def pull_check_abstraction(self, vqip=None): Returns: avail (dict): A VQIP of water available for abstraction - """ # Respond to abstraction check request avail = self.get_avail() @@ -131,7 +126,6 @@ def pull_set_abstraction(self, vqip): Returns: avail (dict): A VQIP of water abstracted - """ # Respond to abstraction set request avail = self.get_avail() diff --git a/wsimod/nodes/demand.py b/wsimod/nodes/demand.py index 4f623bdc..bab6b79d 100644 --- a/wsimod/nodes/demand.py +++ b/wsimod/nodes/demand.py @@ -4,7 +4,6 @@ @author: bdobson Converted to totals BD 2022-05-03 - """ from wsimod.core import constants from wsimod.nodes.nodes import Node @@ -36,7 +35,6 @@ def __init__( Functions intended to call in orchestration: create_demand - """ # TODO should temperature be defined in pollutant dict? # TODO a lot of this should be moved to ResidentialDemand @@ -66,12 +64,11 @@ def __init__( self.mass_balance_out.append(lambda: self.total_received) def create_demand(self): - """Function to call get_demand, which should return a dict with keys that - match the keys in directions. - - A dict that determines how to push_distributed the generated - wastewater/garden irrigation. Water is drawn from attached nodes. + """Function to call get_demand, which should return a dict with keys that match + the keys in directions. + A dict that determines how to push_distributed the generated wastewater/garden + irrigation. Water is drawn from attached nodes. """ demand = self.get_demand() total_requested = 0 @@ -113,7 +110,6 @@ def get_demand(self): Returns: (dict): A VQIP that will contain constant demand - """ # TODO read/gen demand pol = self.v_change_vqip(self.empty_vqip(), self.constant_demand) @@ -137,7 +133,6 @@ def get_demand(self): Returns: (dict): A dict of VQIPs, where the keys match with directions in Demand/create_demand - """ return {"house": self.get_demand()} @@ -156,8 +151,7 @@ def __init__( constant_weighting=0.2, **kwargs, ): - """Subclass of demand with functions to handle internal and external water - use. + """Subclass of demand with functions to handle internal and external water use. Args: population (float, optional): population of node. Defaults to 1. @@ -192,7 +186,6 @@ def __init__( _Units_: m3/timestep - `data_input_dict` should contain air temperature at model timestep. _Units_: C - """ self.gardening_efficiency = gardening_efficiency self.population = population @@ -211,7 +204,6 @@ def get_demand(self): Returns: (dict): A dict of VQIPs, where the keys match with directions in Demand/create_demand - """ water_output = {} @@ -221,16 +213,15 @@ def get_demand(self): return water_output def get_garden_demand(self): - """Calculate garden water demand in the current timestep by get_connected - to all attached land nodes. This check should return garden water demand. - Applies irrigation coefficient. Can function when a single population node - is connected to multiple land nodes, however, the capacity and preferences - of arcs should be updated to reflect what is possible based on area. + """Calculate garden water demand in the current timestep by get_connected to all + attached land nodes. This check should return garden water demand. Applies + irrigation coefficient. Can function when a single population node is connected + to multiple land nodes, however, the capacity and preferences of arcs should be + updated to reflect what is possible based on area. Returns: vqip (dict): A VQIP of garden water use (including pollutants) to be pushed to land - """ # Get garden water demand excess = self.get_connected( @@ -245,15 +236,14 @@ def get_garden_demand(self): return vqip def apply_gardening_pollutants(self, excess): - """Holder function to apply pollutants (i.e., presumably fertiliser) to - the garden. + """Holder function to apply pollutants (i.e., presumably fertiliser) to the + garden. Args: excess (float): A volume of water applied to a garden Returns: (dict): A VQIP of water that includes pollutants to be sent to land - """ # TODO Fertilisers are currently applied in the land node... which is preferable? vqip = self.empty_vqip() @@ -268,7 +258,6 @@ def excess_to_garden_demand(self, excess): Returns: (float): Amount of water actually applied to garden - """ # TODO Anything more than this needed? # (yes - population presence if eventually included!) @@ -276,12 +265,11 @@ def excess_to_garden_demand(self, excess): return excess * self.gardening_efficiency def get_house_demand(self): - """Per capita calculations for household wastewater generation. Applies - weighted temperature calculation. + """Per capita calculations for household wastewater generation. Applies weighted + temperature calculation. Returns: (dict): A VQIP containg foul water - """ # TODO water that is consumed but not sent onwards as foul # Total water required diff --git a/wsimod/nodes/distribution.py b/wsimod/nodes/distribution.py index 25dd22ec..63a7cc0b 100644 --- a/wsimod/nodes/distribution.py +++ b/wsimod/nodes/distribution.py @@ -2,7 +2,6 @@ """Created on Sun Aug 14 16:27:14 2022. @author: bdobson - """ from wsimod.core import constants @@ -10,10 +9,10 @@ def decorate_leakage_set(self, f): - """Decorator to extend the functionality of `f` by introducing leakage. This - is achieved by adjusting the volume of the request (vqip) to include - anticipated leakage, calling the original function `f`, and then distributing - the leaked amount to groundwater. + """Decorator to extend the functionality of `f` by introducing leakage. This is + achieved by adjusting the volume of the request (vqip) to include anticipated + leakage, calling the original function `f`, and then distributing the leaked amount + to groundwater. Args: self (instance of Distribution class): The Distribution object to be @@ -24,7 +23,6 @@ def decorate_leakage_set(self, f): Returns: pull_set (function): The decorated function which includes the original functionality of `f` and additional leakage operations. - """ def pull_set(vqip, **kwargs): @@ -62,9 +60,9 @@ def pull_set(vqip, **kwargs): def decorate_leakage_check(self, f): - """Decorator to extend the functionality of `f` by introducing leakage. This - is achieved by adjusting the volume of the request (vqip) to include - anticipated leakage and then calling the original function `f`. + """Decorator to extend the functionality of `f` by introducing leakage. This is + achieved by adjusting the volume of the request (vqip) to include anticipated + leakage and then calling the original function `f`. Args: self (instance of Distribution class): The Distribution object to be @@ -75,7 +73,6 @@ def decorate_leakage_check(self, f): Returns: pull_check (function): The decorated function which includes the original functionality of `f` and additional leakage operations. - """ def pull_check(vqip, **kwargs): @@ -153,7 +150,6 @@ def __init__(self, **kwargs): Input data and parameter requirements: - None - """ super().__init__(**kwargs) # Update handlers @@ -168,15 +164,13 @@ def __init__(self, **kwargs): self.mass_balance_in.append(lambda: self.supplied) def pull_set_unlimited(self, vqip): - """Respond that VQIP was fulfilled and update state variables for mass - balance. + """Respond that VQIP was fulfilled and update state variables for mass balance. Args: vqip (dict): A VQIP amount to request Returns: vqip (dict): A VQIP amount that was supplied - """ # TODO maybe need some pollutant concentrations? vqip = self.v_change_vqip(self.empty_vqip(), vqip["volume"]) diff --git a/wsimod/nodes/land.py b/wsimod/nodes/land.py index ad9693c2..8d99b811 100644 --- a/wsimod/nodes/land.py +++ b/wsimod/nodes/land.py @@ -2,7 +2,6 @@ """Created on Fri May 20 08:58:58 2022. @author: Barney - """ import sys from bisect import bisect_left @@ -165,15 +164,13 @@ def apply_irrigation(self): """Iterate over any irrigation functions (needs further testing.. maybe). - """ for f in self.irrigation_functions: f() def run(self): - """Call the run function in all surfaces, update surface/subsurface/ - percolation tanks, discharge to rivers/groundwater. - """ + """Call the run function in all surfaces, update surface/subsurface/ percolation + tanks, discharge to rivers/groundwater.""" # Run all surfaces for surface in self.surfaces: surface.run() @@ -218,15 +215,14 @@ def run(self): self.subsurface_runoff.push_storage(reply_subsurface, force=True) def push_set_sewer(self, vqip): - """Receive water from a sewer and send it to the first ImperviousSurface - in surfaces. + """Receive water from a sewer and send it to the first ImperviousSurface in + surfaces. Args: vqip (dict): A VQIP amount to be sent to the impervious surface Returns: vqip (dict): A VQIP amount of water that was not received - """ # TODO currently just push to the first impervious surface... not sure if people will be having multiple impervious surfaces. If people would be only having one then it would make sense to store as a parameter... this is probably fine for now for surface in self.surfaces: @@ -247,15 +243,14 @@ def end_timestep(self): tanks.end_timestep() def get_surface(self, surface_): - """Return a surface from the list of surfaces by the 'surface' entry in - the surface. I.e., the name of the surface. + """Return a surface from the list of surfaces by the 'surface' entry in the + surface. I.e., the name of the surface. Args: surface_ (str): Name of the surface Returns: surface (Surface): The first surface that matches the name - """ for surface in self.surfaces: if surface.surface == surface_: @@ -285,15 +280,14 @@ def __init__( pollutant_load={}, **kwargs, ): - """A subclass of DecayTank. Each Surface is anticipated to represent a - different land cover type of a Land node. Besides functioning as a Tank, - Surfaces have three lists of functions (inflows, processes and outflows) - where behaviour can be added by appending new functions. We anticipate - that customised surfaces should be a subclass of Surface or its subclasses - and add functions to these lists. These lists are executed (inflows first, - then processes, then outflows) in the run function, which is called by the - run function in Land. The lists must return any model inflows or outflows - as a VQIP for mass balance checking. + """A subclass of DecayTank. Each Surface is anticipated to represent a different + land cover type of a Land node. Besides functioning as a Tank, Surfaces have + three lists of functions (inflows, processes and outflows) where behaviour can + be added by appending new functions. We anticipate that customised surfaces + should be a subclass of Surface or its subclasses and add functions to these + lists. These lists are executed (inflows first, then processes, then outflows) + in the run function, which is called by the run function in Land. The lists must + return any model inflows or outflows as a VQIP for mass balance checking. If a user wishes the DecayTank portion to be active, then can provide 'decays', which are passed upwards (see wsimod/core/core.py/DecayObj for @@ -331,7 +325,6 @@ def __init__( nitrates. `nhx-dry` describes nitrogen as ammonia. `srp/noy/ nhx-wet` can also be used to specify wet deposition. _Units_: kg/m2/timestep (data is read at a monthly timestep) - """ # Assign parameters self.depth = depth @@ -380,15 +373,13 @@ def run(self): ) def get_data_input(self, var): - """Read data input from parent Land node (i.e., for - precipitation/et0/temp). + """Read data input from parent Land node (i.e., for precipitation/et0/temp). Args: var (str): Name of variable Returns: Data read - """ return self.parent.get_data_input(var) @@ -400,14 +391,13 @@ def get_data_input_surface(self, var): Returns: Data read - """ return self.data_input_dict[(var, self.parent.monthyear)] def dry_deposition_to_tank(self, vqip): - """Generic function for allocating dry pollution deposition to the - surface. Simply sends the pollution into the tank (some subclasses - overwrite this behaviour). + """Generic function for allocating dry pollution deposition to the surface. + Simply sends the pollution into the tank (some subclasses overwrite this + behaviour). Args: vqip (dict): A VQIP amount of dry deposition to send to tank @@ -415,16 +405,15 @@ def dry_deposition_to_tank(self, vqip): Returns: vqip (dict): A VQIP amount of dry deposition that entered the tank (used for mass balance checking) - """ # Default behaviour is to just enter the tank _ = self.push_storage(vqip, force=True) return vqip def wet_deposition_to_tank(self, vqip): - """Generic function for allocating wet pollution deposition to the - surface. Simply sends the pollution into the tank (some subclasses - overwrite this behaviour). + """Generic function for allocating wet pollution deposition to the surface. + Simply sends the pollution into the tank (some subclasses overwrite this + behaviour). Args: vqip (dict): A VQIP amount of wet deposition to send to tank @@ -432,20 +421,18 @@ def wet_deposition_to_tank(self, vqip): Returns: vqip (dict): A VQIP amount of wet deposition that entered the tank (used for mass balance checking) - """ # Default behaviour is to just enter the tank _ = self.push_storage(vqip, force=True) return vqip def simple_deposition(self): - """Inflow function to cause simple pollution deposition to occur, updating - the surface tank. + """Inflow function to cause simple pollution deposition to occur, updating the + surface tank. Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ pollution = self.empty_vqip() @@ -463,13 +450,12 @@ def simple_deposition(self): return (pollution, self.empty_vqip()) def atmospheric_deposition(self): - """Inflow function to cause dry atmospheric deposition to occur, updating - the surface tank. + """Inflow function to cause dry atmospheric deposition to occur, updating the + surface tank. Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ # TODO double check units in preprocessing - is weight of N or weight of NHX/noy? @@ -491,13 +477,12 @@ def atmospheric_deposition(self): return (in_, self.empty_vqip()) def precipitation_deposition(self): - """Inflow function to cause wet precipitation deposition to occur, - updating the surface tank. + """Inflow function to cause wet precipitation deposition to occur, updating the + surface tank. Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ # TODO double check units - is weight of N or weight of NHX/noy? @@ -523,12 +508,12 @@ class ImperviousSurface(Surface): """""" def __init__(self, pore_depth=0, et0_to_e=1, **kwargs): - """A surface to represent impervious surfaces that drain to storm sewers. - Runoff is generated by the surface tank overflowing, if a user wants all - precipitation to immediately go to runoff then they should reduce - 'pore_depth', however generally this is not what happens and a small (a - few mm) depth should be assigned to the tank. Also includes urban - pollution deposition, though this will only be mobilised if runoff occurs. + """A surface to represent impervious surfaces that drain to storm sewers. Runoff + is generated by the surface tank overflowing, if a user wants all precipitation + to immediately go to runoff then they should reduce 'pore_depth', however + generally this is not what happens and a small (a few mm) depth should be + assigned to the tank. Also includes urban pollution deposition, though this will + only be mobilised if runoff occurs. Note that the tank does not have a runoff coefficient because it doesn't make sense from an integrated perspective. If a user wants to mimic runoff @@ -542,7 +527,6 @@ def __init__(self, pore_depth=0, et0_to_e=1, **kwargs): et0_to_e (float, optional): Multiplier applied to the parent's data timeseries of et0 to determine how much evaporation takes place on the ImperviousSurface. Defaults to 1. - """ # Assign parameters self.et0_to_e = et0_to_e # Total evaporation @@ -560,8 +544,7 @@ def __init__(self, pore_depth=0, et0_to_e=1, **kwargs): self.outflows.append(self.push_to_sewers) def precipitation_evaporation(self): - """Inflow function that is a simple rainfall-evaporation model, updating - the. + """Inflow function that is a simple rainfall-evaporation model, updating the. surface tank. All precipitation that is not evaporated is forced into the tank (even though some of that will later be pushed to sewers) - this enables runoff @@ -570,7 +553,6 @@ def precipitation_evaporation(self): Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ # Read data in length units precipitation_depth = self.get_data_input("precipitation") @@ -616,13 +598,12 @@ def precipitation_evaporation(self): return (self.precipitation, self.evaporation) def push_to_sewers(self): - """Outflow function that distributes ponded water (i.e., surface runoff) - to the parent node's attached sewers. + """Outflow function that distributes ponded water (i.e., surface runoff) to the + parent node's attached sewers. Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ # Get runoff surface_runoff = self.pull_ponded() @@ -655,8 +636,7 @@ def __init__( ihacres_p=10, **kwargs, ): - """A generic pervious surface that represents hydrology with the IHACRES - model. + """A generic pervious surface that represents hydrology with the IHACRES model. Args: depth (float, optional): Soil tank (i.e., root) depth. Defaults to 0.75. @@ -710,7 +690,6 @@ def __init__( _Units_: - - ihacres_p. _Units_: - - """ # Assign parameters (converting field capacity and wilting point to depth) self.field_capacity = field_capacity @@ -763,7 +742,6 @@ def get_cmd(self): Returns: (float): current moisture deficit - """ return self.get_excess()["volume"] / self.area @@ -772,7 +750,6 @@ def get_smc(self): Returns: (float): soil moisture content - """ # Depth of soil moisture return self.storage["volume"] / self.area @@ -788,14 +765,13 @@ def get_climate(self): return precipitation_depth, evaporation_depth def ihacres(self): - """Inflow function that runs the IHACRES model equations, updates tanks, - and store flows in state variables (which are later sent to the parent - land node in the route function). + """Inflow function that runs the IHACRES model equations, updates tanks, and + store flows in state variables (which are later sent to the parent land node in + the route function). Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ # Read data (leave in depth units since that is what IHACRES equations are in) precipitation_depth, evaporation_depth = self.get_climate() @@ -918,13 +894,12 @@ def ihacres(self): return (in_, out_) def route(self): - """An outflow function that sends percolation, subsurface runoff and - surface runoff to their respective tanks in the parent land node. + """An outflow function that sends percolation, subsurface runoff and surface + runoff to their respective tanks in the parent land node. Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ self.parent.surface_runoff.push_storage(self.infiltration_excess, force=True) self.parent.subsurface_runoff.push_storage(self.subsurface_flow, force=True) @@ -941,7 +916,6 @@ def calculate_soil_temperature(self): Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ auto = self.storage["temperature"] * self.soil_temp_w_prev air = self.get_data_input("temperature") * self.soil_temp_w_air @@ -1200,18 +1174,17 @@ def __init__( ] def pull_storage(self, vqip): - """Pull water from the surface, updating the surface storage VQIP. - Nutrient pool pollutants (nitrate/nitrite/ammonia/phosphate/org- - phosphorus/ org-nitrogen) are removed in proportion to their amounts in - the dissolved nutrient pools, if they are simulated. Other pollutants are - removed in proportion to their amount in the surface tank. + """Pull water from the surface, updating the surface storage VQIP. Nutrient pool + pollutants (nitrate/nitrite/ammonia/phosphate/org- phosphorus/ org-nitrogen) are + removed in proportion to their amounts in the dissolved nutrient pools, if they + are simulated. Other pollutants are removed in proportion to their amount in the + surface tank. Args: vqip (dict): VQIP amount to be pulled, (only 'volume' key is needed) Returns: reply (dict): A VQIP amount successfully pulled from the tank - """ if self.storage["volume"] == 0: return self.empty_vqip() @@ -1263,8 +1236,7 @@ def pull_storage(self, vqip): return reply def quick_interp(self, x, xp, yp): - """A simple version of np.interp to intepolate crop information on the - fly. + """A simple version of np.interp to intepolate crop information on the fly. Args: x (int): Current time (i.e., day of year) @@ -1273,7 +1245,6 @@ def quick_interp(self, x, xp, yp): Returns: y (float): Interpolated value for current time - """ x_ind = bisect_left(xp, x) x_left = xp[x_ind - 1] @@ -1286,13 +1257,12 @@ def quick_interp(self, x, xp, yp): def calc_crop_cover(self): """Process function that calculates how much crop cover there is, assigns - whether crops are sown/harvested, and calculates et0_coefficient based on - growth stage of crops. + whether crops are sown/harvested, and calculates et0_coefficient based on growth + stage of crops. Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ # Get current day of year doy = self.parent.t.dayofyear @@ -1377,14 +1347,12 @@ def adjust_vqip_to_liquid(self, vqip, deposition, in_): return vqip def effective_precipitation_flushing(self): - """Remove the nutrients brought out by effective precipitation, which is - surface runoff, subsurface runoff, and percolation, from the nutrients - pool. + """Remove the nutrients brought out by effective precipitation, which is surface + runoff, subsurface runoff, and percolation, from the nutrients pool. Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ # inorganic out = self.nutrient_pool.get_empty_nutrient() @@ -1428,7 +1396,6 @@ def fertiliser(self): Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ # TODO tidy up fertiliser/manure/residue/deposition once preprocessing is sorted @@ -1461,7 +1428,6 @@ def manure(self): Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ # Scale for surface nhx = self.get_data_input_surface("nhx-manure") * self.area @@ -1494,7 +1460,6 @@ def residue(self): Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ nhx = self.get_data_input_surface("nhx-residue") * self.area noy = self.get_data_input_surface("noy-residue") * self.area @@ -1521,13 +1486,12 @@ def residue(self): return (vqip, self.empty_vqip()) def soil_pool_transformation(self): - """A process function that run transformation functions in the nutrient - pool and updates the pollutant concentrations in the surface tank. + """A process function that run transformation functions in the nutrient pool and + updates the pollutant concentrations in the surface tank. Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ # Initialise mass balance tracking variables in_ = self.empty_vqip() @@ -1591,13 +1555,12 @@ def soil_pool_transformation(self): return (in_, out_) def calc_temperature_dependence_factor(self): - """Process function that calculates the temperature dependence factor for - the nutrient pool (which impacts soil pool transformations). + """Process function that calculates the temperature dependence factor for the + nutrient pool (which impacts soil pool transformations). Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ # Parameters/equations from HYPE documentation if self.storage["temperature"] > 5: @@ -1612,13 +1575,12 @@ def calc_temperature_dependence_factor(self): return (self.empty_vqip(), self.empty_vqip()) def calc_soil_moisture_dependence_factor(self): - """Process function that calculates the soil moisture dependence factor - for the nutrient pool (which impacts soil pool transformations). + """Process function that calculates the soil moisture dependence factor for the + nutrient pool (which impacts soil pool transformations). Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ # Parameters/equations from HYPE documentation current_soil_moisture = self.get_smc() @@ -1638,13 +1600,12 @@ def calc_soil_moisture_dependence_factor(self): return (self.empty_vqip(), self.empty_vqip()) def calc_crop_uptake(self): - """Process function that calculates how much nutrient crops uptake and - updates nutrient pool and surface tank. + """Process function that calculates how much nutrient crops uptake and updates + nutrient pool and surface tank. Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ # Parameters/equations from HYPE documentation @@ -1709,13 +1670,12 @@ def calc_crop_uptake(self): return (self.empty_vqip(), self.empty_vqip()) def erosion(self): - """Outflow function that erodes adsorbed/humus phosphorus and sediment and - sends onwards to percolation/surface runoff/subsurface runoff. + """Outflow function that erodes adsorbed/humus phosphorus and sediment and sends + onwards to percolation/surface runoff/subsurface runoff. Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ # Parameters/equations from HYPE documentation (which explains why my documentation is a bit ambiguous - because theirs is too) @@ -1873,13 +1833,12 @@ def erosion(self): return (in_, self.empty_vqip()) def denitrification(self): - """Outflow function that performs denitirication processes, updating - nutrient pool and soil tank. + """Outflow function that performs denitirication processes, updating nutrient + pool and soil tank. Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ # Parameters/equations from HYPE documentation # TODO could more of this be moved to NutrientPool @@ -1938,7 +1897,6 @@ def adsorption(self): Returns: (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. - """ # Parameters/equations from HYPE documentation # TODO could this be moved to the nutrient pool? @@ -2047,8 +2005,7 @@ def adsorption(self): return (in_, out_) def dry_deposition_to_tank(self, vqip): - """Allocate dry deposition to surface tank, updating nutrient pool - accordingly. + """Allocate dry deposition to surface tank, updating nutrient pool accordingly. Args: vqip (dict): A VQIP amount of dry deposition to send to tank @@ -2056,7 +2013,6 @@ def dry_deposition_to_tank(self, vqip): Returns: vqip (dict): A VQIP amount of dry deposition that entered the tank (used for mass balance checking) - """ # Convert to nutrients deposition = self.nutrient_pool.get_empty_nutrient() @@ -2072,8 +2028,7 @@ def dry_deposition_to_tank(self, vqip): return vqip def wet_deposition_to_tank(self, vqip): - """Allocate wet deposition to surface tank, updating nutrient pool - accordingly. + """Allocate wet deposition to surface tank, updating nutrient pool accordingly. Args: vqip (dict): A VQIP amount of dry deposition to send to tank @@ -2081,7 +2036,6 @@ def wet_deposition_to_tank(self, vqip): Returns: vqip (dict): A VQIP amount of dry deposition that entered the tank (used for mass balance checking) - """ # Convert to nutrients deposition = self.nutrient_pool.get_empty_nutrient() @@ -2101,15 +2055,14 @@ class IrrigationSurface(GrowingSurface): """""" def __init__(self, irrigation_coefficient=0.1, **kwargs): - """A subclass of GrowingSurface that can calculate water demand for the - crops that is not met by precipitation and use the parent node to acquire - water. When the surface is created by the parent node, the irrigate - function below is assigned. + """A subclass of GrowingSurface that can calculate water demand for the crops + that is not met by precipitation and use the parent node to acquire water. When + the surface is created by the parent node, the irrigate function below is + assigned. Args: irrigation_coefficient (float, optional): proportion area irrigated * proportion of demand met. Defaults to 0.1. - """ # Assign param self.irrigation_coefficient = irrigation_coefficient # proportion area irrigated * proportion of demand met @@ -2118,8 +2071,7 @@ def __init__(self, irrigation_coefficient=0.1, **kwargs): def irrigate(self): """Calculate water demand for crops and call parent node to acquire water, - updating surface tank and nutrient pools. - """ + updating surface tank and nutrient pools.""" if self.days_after_sow: # Irrigation is just difference between evaporation and precipitation amount irrigation_demand = ( @@ -2159,17 +2111,16 @@ class GardenSurface(GrowingSurface): # TODO - probably a simplier version of this is useful, building just on pervioussurface def __init__(self, **kwargs): - """A specific surface for gardens that treats the garden as a grass crop, - but that can calculate/receive irrigation through functions that are - assigned by the parent land node's handlers, which in turn are expected to - be triggered by a query from an attached Demand node. - """ + """A specific surface for gardens that treats the garden as a grass crop, but + that can calculate/receive irrigation through functions that are assigned by the + parent land node's handlers, which in turn are expected to be triggered by a + query from an attached Demand node.""" super().__init__(**kwargs) def calculate_irrigation_demand(self, ignore_vqip=None): - """A check function (assigned by parent to push check from demand nodes) - that calculations irrigation demand (i.e., difference between evaporation - and preciptiation). + """A check function (assigned by parent to push check from demand nodes) that + calculations irrigation demand (i.e., difference between evaporation and + preciptiation). Args: ignore_vqip (any, optional): Conventional push checks send an optional @@ -2179,7 +2130,6 @@ def calculate_irrigation_demand(self, ignore_vqip=None): Returns: reply (dict): A VQIP amount of irrigation demand (note only 'volume' key is used) - """ # Calculate irrigation demand irrigation_demand = max( @@ -2206,7 +2156,6 @@ def receive_irrigation_demand(self, vqip): Returns: (dict): A VQIP amount of irrigation that was not received (should always be empty) - """ # update tank return self.push_storage(vqip, force=True) diff --git a/wsimod/nodes/nodes.py b/wsimod/nodes/nodes.py index 175ec58f..8aea2ee2 100644 --- a/wsimod/nodes/nodes.py +++ b/wsimod/nodes/nodes.py @@ -4,7 +4,6 @@ @author: Barney Converted to totals on Thur Apr 21 2022 - """ from wsimod.arcs.arcs import AltQueueArc, DecayArcAlt from wsimod.core import constants @@ -16,8 +15,8 @@ class Node(WSIObj): """""" def __init__(self, name, data_input_dict=None): - """Base class for CWSD nodes. Constructs all the necessary attributes for - the node object. + """Base class for CWSD nodes. Constructs all the necessary attributes for the + node object. Args: name (str): Name of node @@ -34,7 +33,6 @@ def __init__(self, name, data_input_dict=None): Input data and parameter requirements: - All nodes require a `name` - """ # Get node types @@ -94,7 +92,6 @@ def total_in(self): Examples: >>> node_inflow = my_node.total_in() - """ in_ = self.empty_vqip() for arc in self.in_arcs.values(): @@ -110,7 +107,6 @@ def total_out(self): Examples: >>> node_outflow = my_node.total_out() - """ out_ = self.empty_vqip() for arc in self.out_arcs.values(): @@ -128,14 +124,12 @@ def node_mass_balance(self): Examples: >>> node_in, node_out, node_ds = my_node.node_mass_balance() - """ in_, ds_, out_ = self.mass_balance() return in_, ds_, out_ def pull_set(self, vqip, tag="default"): - """Receives pull set requests from arcs and passes request to query - handler. + """Receives pull set requests from arcs and passes request to query handler. Args: vqip (dict): the VQIP pull request (by default, only the 'volume' key is @@ -148,13 +142,11 @@ def pull_set(self, vqip, tag="default"): Examples: >>> water_received = my_node.pull_set({'volume' : 10}) - """ return self.query_handler(self.pull_set_handler, vqip, tag) def push_set(self, vqip, tag="default"): - """Receives push set requests from arcs and passes request to query - handler. + """Receives push set requests from arcs and passes request to query handler. Args: vqip (_type_): the VQIP push request @@ -166,13 +158,11 @@ def push_set(self, vqip, tag="default"): Examples: water_not_pushed = my_node.push_set(wastewater_vqip) - """ return self.query_handler(self.push_set_handler, vqip, tag) def pull_check(self, vqip=None, tag="default"): - """Receives pull check requests from arcs and passes request to query - handler. + """Receives pull check requests from arcs and passes request to query handler. Args: vqip (dict, optional): the VQIP pull check (by default, only the @@ -187,13 +177,11 @@ def pull_check(self, vqip=None, tag="default"): Examples: >>> water_available = my_node.pull_check({'volume' : 10}) >>> total_water_available = my_node.pull_check() - """ return self.query_handler(self.pull_check_handler, vqip, tag) def push_check(self, vqip=None, tag="default"): - """Receives push check requests from arcs and passes request to query - handler. + """Receives push check requests from arcs and passes request to query handler. Args: vqip (dict, optional): the VQIP push check. Defaults to None, which @@ -207,7 +195,6 @@ def push_check(self, vqip=None, tag="default"): Examples: >>> total_available_push_capacity = my_node.push_check() >>> available_push_capacity = my_node.push_check(wastewater_vqip) - """ return self.query_handler(self.push_check_handler, vqip, tag) @@ -232,7 +219,6 @@ def get_direction_arcs(self, direction, of_type=None): >>> arcs_to_push_to = my_node.get_direction_arcs('push') >>> arcs_to_pull_from = my_node.get_direction_arcs('pull') >>> arcs_from_reservoirs = my_node.get_direction_arcs('pull', of_type = 'Reservoir') - """ if of_type is None: # Return all arcs @@ -292,7 +278,6 @@ def get_connected(self, direction="pull", of_type=None, tag="default"): >>> avail_sewer_push_to_sewers = my_node.get_direction_arcs('push', of_type = 'Sewer', tag = 'Sewer') - """ # Initialise connected dict connected = {"avail": 0, "priority": 0, "allocation": {}, "capacity": {}} @@ -314,8 +299,8 @@ def get_connected(self, direction="pull", of_type=None, tag="default"): return connected def query_handler(self, handler, ip, tag): - """Sends all push/pull requests/checks using the handler (i.e., ensures - the correct function is used that lines up with 'tag'). + """Sends all push/pull requests/checks using the handler (i.e., ensures the + correct function is used that lines up with 'tag'). Args: handler (dict): contains all push/pull requests for various tags @@ -328,7 +313,6 @@ def query_handler(self, handler, ip, tag): Raises: Message if no functions are defined for tag and if request/check function fails - """ try: return handler[tag](ip) @@ -341,9 +325,9 @@ def query_handler(self, handler, ip, tag): return handler[tag](ip) def pull_distributed(self, vqip, of_type=None, tag="default"): - """Send pull requests to all (or specified by type) nodes connecting to - self. Iterate until request is met or maximum iterations are hit. - Streamlines if only one in_arc exists. + """Send pull requests to all (or specified by type) nodes connecting to self. + Iterate until request is met or maximum iterations are hit. Streamlines if only + one in_arc exists. Args: vqip (dict): Total amount to pull (by default, only the @@ -355,7 +339,6 @@ def pull_distributed(self, vqip, of_type=None, tag="default"): Returns: pulled (dict): VQIP of combined pulled water - """ if len(self.in_arcs) == 1: # If only one in_arc, just pull from that @@ -406,9 +389,9 @@ def pull_distributed(self, vqip, of_type=None, tag="default"): return pulled def push_distributed(self, vqip, of_type=None, tag="default"): - """Send push requests to all (or specified by type) nodes connecting to - self. Iterate until request is met or maximum iterations are hit. - Streamlines if only one in_arc exists. + """Send push requests to all (or specified by type) nodes connecting to self. + Iterate until request is met or maximum iterations are hit. Streamlines if only + one in_arc exists. Args: vqip (dict): Total amount to push @@ -419,7 +402,6 @@ def push_distributed(self, vqip, of_type=None, tag="default"): Returns: not_pushed_ (dict): VQIP of water that cannot be pushed - """ if len(self.out_arcs) == 1: # If only one out_arc, just send the water down that @@ -493,7 +475,6 @@ def check_basic(self, direction, vqip=None, of_type=None, tag="default"): Returns: avail (dict): VQIP responses summed over all requests - """ f, arcs = self.get_direction_arcs(direction, of_type) @@ -509,8 +490,8 @@ def check_basic(self, direction, vqip=None, of_type=None, tag="default"): def pull_check_basic(self, vqip=None, of_type=None, tag="default"): """Default node check behaviour that treats a node like a junction. Water - available to pull is just the water available to pull from upstream - connected nodes. + available to pull is just the water available to pull from upstream connected + nodes. Args: vqip (dict, optional): VQIP from handler of amount to pull check @@ -523,14 +504,13 @@ def pull_check_basic(self, vqip=None, of_type=None, tag="default"): Returns: (dict): VQIP check response of upstream nodes - """ return self.check_basic("pull", vqip, of_type, tag) def push_check_basic(self, vqip=None, of_type=None, tag="default"): """Default node check behaviour that treats a node like a junction. Water - available to push is just the water available to push to downstream - connected nodes. + available to push is just the water available to push to downstream connected + nodes. Args: vqip (dict, optional): VQIP from handler of amount to push check. @@ -542,7 +522,6 @@ def push_check_basic(self, vqip=None, of_type=None, tag="default"): Returns: (dict): VQIP check response of downstream nodes - """ return self.check_basic("push", vqip, of_type, tag) @@ -558,7 +537,6 @@ def pull_set_deny(self, vqip): Raises: Message when called, since it would usually occur if a model is improperly connected - """ print("Attempted pull set from deny") return self.empty_vqip() @@ -575,7 +553,6 @@ def pull_check_deny(self, vqip=None): Raises: Message when called, since it would usually occur if a model is improperly connected - """ print("Attempted pull check from deny") return self.empty_vqip() @@ -592,7 +569,6 @@ def push_set_deny(self, vqip): Raises: Message when called, since it would usually occur if a model is improperly connected - """ print("Attempted push set to deny") return vqip @@ -609,7 +585,6 @@ def push_check_deny(self, vqip=None): Raises: Message when called, since it would usually occur if a model is improperly connected - """ print("Attempted push check to deny") return self.empty_vqip() @@ -622,7 +597,6 @@ def push_check_accept(self, vqip=None): Returns: (dict): VQIP or an unbounded capacity, indicating all water can be received - """ if not vqip: vqip = self.empty_vqip() @@ -630,15 +604,14 @@ def push_check_accept(self, vqip=None): return vqip def get_data_input(self, var): - """Read data from data_input_dict. Keys are tuples with the first entry as - the variable to read and second entry the time. + """Read data from data_input_dict. Keys are tuples with the first entry as the + variable to read and second entry the time. Args: var (str): Name of variable Returns: Data read - """ return self.data_input_dict[(var, self.t)] @@ -646,14 +619,11 @@ def end_timestep(self): """Empty function intended to be called at the end of every timestep. Subclasses will overwrite this functions. - """ pass def reinit(self): - """Empty function to be written if reinitialisation capability is - added. - """ + """Empty function to be written if reinitialisation capability is added.""" pass @@ -778,7 +748,6 @@ def __init__(self, capacity=0, area=1, datum=10, initial_storage=0): as volume dict: Tank will be initialised with this VQIP Defaults to 0 (i.e., no volume, no pollutants). - """ # Set parameters self.capacity = capacity @@ -813,7 +782,6 @@ def ds(self): Returns: (dict): Change in storage - """ return self.ds_vqip(self.storage, self.storage_) @@ -833,7 +801,6 @@ def pull_ponded(self): {'volume' : 1, 'phosphate' : 0.02} >>> print(my_tank.storage) {'volume' : 9, 'phosphate' : 0.18} - """ # Get amount ponded = max(self.storage["volume"] - self.capacity, 0) @@ -860,7 +827,6 @@ def get_avail(self, vqip=None): {'volume' : 10, 'phosphate' : 0.2} >>> print(my_tank.get_avail({'volume' : 1})) {'volume' : 1, 'phosphate' : 0.02} - """ reply = self.copy_vqip(self.storage) if vqip is None: @@ -888,7 +854,6 @@ def get_excess(self, vqip=None): {'volume' : 4, 'phosphate' : 0.16} >>> print(my_tank.get_excess({'volume' : 2})) {'volume' : 2, 'phosphate' : 0.08} - """ vol = max(self.capacity - self.storage["volume"], 0) if vqip is not None: @@ -899,8 +864,8 @@ def get_excess(self, vqip=None): return self.v_change_vqip(self.storage, vol) def push_storage(self, vqip, force=False): - """Push water into tank, updating the storage VQIP. Force argument can be - used to ignore tank capacity. + """Push water into tank, updating the storage VQIP. Force argument can be used + to ignore tank capacity. Args: vqip (dict): VQIP amount to be pushed @@ -925,7 +890,6 @@ def push_storage(self, vqip, force=False): {'phosphate': 0, 'volume': 0} >>> print(my_tank.storage) {'volume': 15.0, 'phosphate': 0.7} - """ if force: # Directly add request to storage @@ -946,9 +910,8 @@ def push_storage(self, vqip, force=False): return reply def pull_storage(self, vqip): - """Pull water from tank, updating the storage VQIP. Pollutants are removed - from tank in proportion to 'volume' in vqip (pollutant values in vqip are - ignored). + """Pull water from tank, updating the storage VQIP. Pollutants are removed from + tank in proportion to 'volume' in vqip (pollutant values in vqip are ignored). Args: vqip (dict): VQIP amount to be pulled, (only 'volume' key is needed) @@ -963,7 +926,6 @@ def pull_storage(self, vqip): {'volume': 5.0, 'phosphate': 0.2} >>> print(my_tank.storage) {'volume': 0, 'phosphate': 0} - """ # Pull from Tank by volume (taking pollutants in proportion) if self.storage["volume"] == 0: @@ -981,8 +943,8 @@ def pull_storage(self, vqip): return reply def pull_pollutants(self, vqip): - """Pull water from tank, updating the storage VQIP. Pollutants are removed - from tank in according to their values in vqip. + """Pull water from tank, updating the storage VQIP. Pollutants are removed from + tank in according to their values in vqip. Args: vqip (dict): VQIP amount to be pulled @@ -997,7 +959,6 @@ def pull_pollutants(self, vqip): {'volume': 2.0, 'phosphate': 0.15} >>> print(my_tank.storage) {'volume': 3, 'phosphate': 0.05} - """ # Adjust based on available mass for pol in constants.ADDITIVE_POLLUTANTS + ["volume"]: @@ -1008,8 +969,8 @@ def pull_pollutants(self, vqip): return vqip def get_head(self, datum=None, non_head_storage=0): - """Area volume calculation for head calcuations. Datum and storage that - does not contribute to head can be specified. + """Area volume calculation for head calcuations. Datum and storage that does not + contribute to head can be specified. Args: datum (float, optional): Value to add to pressure head in tank. @@ -1029,7 +990,6 @@ def get_head(self, datum=None, non_head_storage=0): 12 >>> print(my_tank.get_head(non_head_storage = 1, datum = 0)) 2 - """ # If datum not provided use object datum if datum is None: @@ -1052,7 +1012,6 @@ def evaporate(self, evap): Returns: evap (float): Volumetric amount of evaporation successfully removed - """ avail = self.get_avail()["volume"] @@ -1090,8 +1049,7 @@ def push_total_c(self, vqip): def end_timestep(self): """Function to be called by parent object, tracks previously timestep's - storage. - """ + storage.""" self.storage_ = self.copy_vqip(self.storage) def reinit(self): @@ -1104,15 +1062,14 @@ class ResidenceTank(Tank): """""" def __init__(self, residence_time=2, **kwargs): - """A tank that has a residence time property that limits storage pulled - from the 'pull_outflow' function. + """A tank that has a residence time property that limits storage pulled from the + 'pull_outflow' function. Args: residence_time (float, optional): Residence time, in theory given in timesteps, in practice it just means that storage / residence time can be pulled each time pull_outflow is called. Defaults to 2. - """ self.residence_time = residence_time super().__init__(**kwargs) @@ -1123,7 +1080,6 @@ def pull_outflow(self): Returns: outflow (dict): A VQIP with volume of pulled volume and pollutants proportionate to the tank's pollutants - """ # Calculate outflow outflow = self.storage["volume"] / self.residence_time @@ -1146,7 +1102,6 @@ def __init__(self, decays={}, parent=None, **kwargs): decays (dict): A dict of dicts containing a key for each pollutant that decays and, within that, a key for each parameter (a constant and exponent) parent (object): An object that can be used to read temperature data from - """ # Store parameters self.parent = parent @@ -1173,7 +1128,6 @@ def decay_ds(self): Returns: ds (dict): A VQIP of change in storage and total decayed - """ ds = self.ds_vqip(self.storage, self.storage_) ds = self.sum_vqip(ds, self.total_decayed) @@ -1185,14 +1139,13 @@ class QueueTank(Tank): def __init__(self, number_of_timesteps=0, **kwargs): """A tank with an internal queue arc, whose queue must be completed before - storage is available for use. The storage that has completed the queue is - under the 'active_storage' property. + storage is available for use. The storage that has completed the queue is under + the 'active_storage' property. Args: number_of_timesteps (int, optional): Built in delay for the internal queue - it is always added to the queue time, although delay can be provided with pushes only. Defaults to 0. - """ # Set parameters self.number_of_timesteps = number_of_timesteps @@ -1215,7 +1168,6 @@ def get_avail(self): Returns: (dict): VQIP of active_storage - """ return self.copy_vqip(self.active_storage) @@ -1231,7 +1183,6 @@ def push_storage(self, vqip, time=0, force=False): Returns: reply (dict): A VQIP of water that could not be received by the tank - """ if force: # Directly add request to storage, skipping queue @@ -1249,17 +1200,15 @@ def push_storage(self, vqip, time=0, force=False): return reply def pull_storage(self, vqip): - """Pull storage from the QueueTank, only water in active_storage is - available. Returning water pulled and updating tank states. Pollutants are - removed from tank in proportion to 'volume' in vqip (pollutant values in - vqip are ignored). + """Pull storage from the QueueTank, only water in active_storage is available. + Returning water pulled and updating tank states. Pollutants are removed from + tank in proportion to 'volume' in vqip (pollutant values in vqip are ignored). Args: vqip (dict): VQIP amount to pull, only 'volume' property is used Returns: reply (dict): VQIP amount that was pulled - """ # Adjust based on available volume reply = min(vqip["volume"], self.active_storage["volume"]) @@ -1276,16 +1225,14 @@ def pull_storage(self, vqip): return reply def pull_storage_exact(self, vqip): - """Pull storage from the QueueTank, only water in active_storage is - available. Pollutants are removed from tank in according to their values - in vqip. + """Pull storage from the QueueTank, only water in active_storage is available. + Pollutants are removed from tank in according to their values in vqip. Args: vqip (dict): A VQIP amount to pull Returns: reply (dict): A VQIP amount successfully pulled - """ # Adjust based on available reply = self.copy_vqip(vqip) @@ -1343,9 +1290,7 @@ def push_set(self, vqip, tag="default"): return self.empty_vqip() def _end_timestep(self): - """Wrapper for end_timestep that also ends the timestep in the - internal_arc. - """ + """Wrapper for end_timestep that also ends the timestep in the internal_arc.""" self.internal_arc.end_timestep() self.internal_arc.update_queue() self.storage_ = self.copy_vqip(self.storage) @@ -1372,7 +1317,6 @@ def __init__(self, decays={}, parent=None, number_of_timesteps=1, **kwargs): number_of_timesteps (int, optional): Built in delay for the internal queue - it is always added to the queue time, although delay can be provided with pushes only. Defaults to 0. - """ # Initialise QueueTank super().__init__(number_of_timesteps=number_of_timesteps, **kwargs) @@ -1389,8 +1333,7 @@ def __init__(self, decays={}, parent=None, number_of_timesteps=1, **kwargs): def _end_timestep(self): """End timestep wrapper that removes decayed pollutants and calls internal - arc. - """ + arc.""" # TODO Should the active storage decay if decays are given (probably.. though that sounds like a nightmare)? self.storage = self.extract_vqip(self.storage, self.internal_arc.total_decayed) self.storage_ = self.copy_vqip(self.storage) diff --git a/wsimod/nodes/nutrient_pool.py b/wsimod/nodes/nutrient_pool.py index 2085ed29..195b3436 100644 --- a/wsimod/nodes/nutrient_pool.py +++ b/wsimod/nodes/nutrient_pool.py @@ -2,7 +2,6 @@ """Created on Thu May 19 16:42:20 2022. @author: barna - """ from wsimod.core import constants @@ -21,11 +20,10 @@ def __init__( fraction_manure_to_dissolved_inorganic={"N": 0.5, "P": 0.1}, fraction_residue_to_fast={"N": 0.1, "P": 0.1}, ): - """A class to track nutrient pools in a soil tank, intended to be - initialised and called by GrowingSurfaces (see - wsimod/nodes/land.py/GrowingSurface) and their subclasses. Contains five - pools, which have a storage that tracks the mass of nutrients. Equations - and parameters are based on HYPE. + """A class to track nutrient pools in a soil tank, intended to be initialised + and called by GrowingSurfaces (see wsimod/nodes/land.py/GrowingSurface) and + their subclasses. Contains five pools, which have a storage that tracks the mass + of nutrients. Equations and parameters are based on HYPE. Args: fraction_dry_n_to_dissolved_inorganic (float, optional): fraction of dry nitrogen deposition going into the soil dissolved inorganic nitrogen pool, with the rest added to the fast pool. Defaults to 0.9. @@ -74,7 +72,6 @@ def __init__( _Units_: -, all should in [0-1] - degrhpar, dishpar, minfpar, disfpar, immobdpar. _Units_: -, all should in [0-1] - """ # TODO I don't think anyone will change most of these params... they could maybe just be set here self.init_empty() @@ -130,8 +127,8 @@ def init_store(self): self.storage = self.get_empty_nutrient() def allocate_inorganic_irrigation(self, irrigation): - """Assign inorganic irrigation, which is assumed to contain dissolved - inorganic nutrients and thus updates that pool. + """Assign inorganic irrigation, which is assumed to contain dissolved inorganic + nutrients and thus updates that pool. Args: irrigation (dict): A dict that contains the amount of nutrients entering @@ -141,15 +138,14 @@ def allocate_inorganic_irrigation(self, irrigation): irrigation (dict): irrigation above, because no transformations take place (i.e., dissolved inorganic is what is received and goes straight into that pool) - """ # Update pool self.dissolved_inorganic_pool.receive(irrigation) return irrigation def allocate_organic_irrigation(self, irrigation): - """Assign organic irrigation, which is assumed to contain dissolved - organic nutrients and thus updates that pool. + """Assign organic irrigation, which is assumed to contain dissolved organic + nutrients and thus updates that pool. Args: irrigation (dict): A dict that contains the amount of nutrients entering @@ -159,16 +155,14 @@ def allocate_organic_irrigation(self, irrigation): irrigation (dict): irrigation above, because no transformations take place (i.e., dissolved organic is what is received and goes straight into that pool) - """ # Update pool self.dissolved_organic_pool.receive(irrigation) return irrigation def allocate_dry_deposition(self, deposition): - """Assign dry deposition, which is assumed to go to both dissolved - inorganic pool and fast pool (nitrogen) and the adsorbed pool - (phosphorus). + """Assign dry deposition, which is assumed to go to both dissolved inorganic + pool and fast pool (nitrogen) and the adsorbed pool (phosphorus). Args: deposition (dict): A dict that contains the amount of nutrients entering @@ -178,7 +172,6 @@ def allocate_dry_deposition(self, deposition): (dict): A dict describing the amount of nutrients that enter the nutrient pool in a dissolved form (and thus need to be tracked by the soil water tank) - """ # Update pools self.fast_pool.storage["N"] += deposition["N"] * self.fraction_dry_n_to_fast @@ -203,15 +196,14 @@ def allocate_wet_deposition(self, deposition): deposition (dict): deposition above, because no transformations take place (i.e., dissolved inorganic is what is received and goes straight into that pool) - """ # Update pool self.dissolved_inorganic_pool.receive(deposition) return deposition def allocate_manure(self, manure): - """Assign manure, which is assumed to go to both dissolved inorganic pool - and fast pool. + """Assign manure, which is assumed to go to both dissolved inorganic pool and + fast pool. Args: manure (dict): A dict that contains the amount of nutrients entering @@ -221,7 +213,6 @@ def allocate_manure(self, manure): (dict): A dict describing the amount of nutrients that enter the nutrient pool in a dissolved form (and thus need to be tracked by the soil water tank) - """ # Assign a proportion of nutrients to the dissolved inorganic pool self.dissolved_inorganic_pool.receive( @@ -236,8 +227,7 @@ def allocate_manure(self, manure): ) def allocate_residue(self, residue): - """Assign residue, which is assumed to go to both humus pool and fast - pool. + """Assign residue, which is assumed to go to both humus pool and fast pool. Args: residue (dict): A dict that contains the amount of nutrients entering @@ -247,7 +237,6 @@ def allocate_residue(self, residue): (dict): A dict describing the amount of nutrients that enter the nutrient pool in a dissolved form (and thus need to be tracked by the soil water tank) - i.e., none because fast and humus pool are both solid - """ # Assign a proportion of nutrients to the humus pool self.humus_pool.receive( @@ -260,8 +249,8 @@ def allocate_residue(self, residue): return self.empty_nutrient() def allocate_fertiliser(self, fertiliser): - """Assign fertiliser, which is assumed to contain dissolved inorganic - nutrients and thus updates that pool. + """Assign fertiliser, which is assumed to contain dissolved inorganic nutrients + and thus updates that pool. Args: fertiliser (dict): A dict that contains the amount of nutrients entering @@ -271,7 +260,6 @@ def allocate_fertiliser(self, fertiliser): fertiliser (dict): fertiliser above, because no transformations take place (i.e., dissolved inorganic is what is received and goes straight into that pool) - """ self.dissolved_inorganic_pool.receive(fertiliser) return fertiliser @@ -286,7 +274,6 @@ def extract_dissolved(self, proportion): (dict): A dict of dicts, where the top level distinguishes between organic and inorganic nutrients, and the bottom level describes how much nutrients (i.e., N and P) have been extracted from those pools - """ # Extract from dissolved inorganic pool reply_di = self.dissolved_inorganic_pool.extract( @@ -311,13 +298,12 @@ def get_erodable_P(self): Returns: (float): total phosphorus - """ return self.adsorbed_inorganic_pool.storage["P"] + self.humus_pool.storage["P"] def erode_P(self, amount_P): - """Update humus and adsorbed inorganic pools to erode some amount. Removed - in proportion to amount in both pools. + """Update humus and adsorbed inorganic pools to erode some amount. Removed in + proportion to amount in both pools. Args: amount_P (float): Amount of phosphorus to be eroded @@ -325,7 +311,6 @@ def erode_P(self, amount_P): Returns: (float): Amount of phosphorus eroded from the humus pool (float): Amount of phosphorus eroded from the adsorbed inorganic pool - """ # Calculate proportion of adsorbed to be eroded fraction_adsorbed = self.adsorbed_inorganic_pool.storage["P"] / ( @@ -346,15 +331,14 @@ def erode_P(self, amount_P): return reply_humus["P"], reply_adsorbed["P"] def soil_pool_transformation(self): - """Function to be called by a GrowingSurface that performs and tracks - changes resulting from soil transformation processes. + """Function to be called by a GrowingSurface that performs and tracks changes + resulting from soil transformation processes. Returns: (float): increase in dissolved inorganic nutrients resulting from transformations (negative value indicates a decrease) (float): increase in dissolved organic nutrients resulting from transformations (negative value indicates a decrease) - """ # For mass balance purposes, assume fast is inorganic and humus is organic @@ -413,7 +397,6 @@ def temp_soil_process(self, parameter, extract_pool, receive_pool): Returns: to_extract (dict): A dict containing the amount extracted of each nutrient (for mass balance) - """ # Initialise nutrients to_extract = self.get_empty_nutrient() @@ -435,7 +418,6 @@ def get_empty_nutrient(self): Returns: (dict): A dict containing 0 for each nutrient - """ return self.empty_nutrient.copy() @@ -448,7 +430,6 @@ def multiply_nutrients(self, nutrient, factor): Returns: (dict): Multiplied nutrients - """ return {x: nutrient[x] * factor[x] for x in constants.NUTRIENTS} @@ -457,7 +438,6 @@ def receive(self, nutrients): Args: nutrients (dict): Amount of nutrients to update store by - """ # Increase storage for nutrient, amount in nutrients.items(): @@ -472,7 +452,6 @@ def sum_nutrients(self, n1, n2): Returns: (dict): Summed nutrients - """ reply = self.get_empty_nutrient() for nutrient in constants.NUTRIENTS: @@ -488,7 +467,6 @@ def subtract_nutrients(self, n1, n2): Returns: (dict): subtracted nutrients - """ reply = self.get_empty_nutrient() for nutrient in constants.NUTRIENTS: @@ -503,7 +481,6 @@ def extract(self, nutrients): Returns: (dict): amount of nutrients successfully removed - """ reply = self.get_empty_nutrient() for nutrient, amount in nutrients.items(): diff --git a/wsimod/nodes/sewer.py b/wsimod/nodes/sewer.py index 6d2a78c5..2351218f 100644 --- a/wsimod/nodes/sewer.py +++ b/wsimod/nodes/sewer.py @@ -3,7 +3,6 @@ @author: bdobson Converted to totals on 2022-05-03 - """ from wsimod.core import constants from wsimod.nodes.nodes import Node, QueueTank @@ -22,9 +21,9 @@ def __init__( chamber_floor=10, data_input_dict={}, ): - """Sewer node that has a QueueTank and storage capacity. Think carefully - about parameterising this tank, because of course the amount of water that - can flow through a sewer in a timestep is different in reality than in a. + """Sewer node that has a QueueTank and storage capacity. Think carefully about + parameterising this tank, because of course the amount of water that can flow + through a sewer in a timestep is different in reality than in a. steady state (e.g., a sewer that can handle a peak of 6m3/s in practice could not handle 6 * 86400 m3 of water in a day because that water does not flow @@ -76,7 +75,6 @@ def __init__( - `capacity`, `chamber_area`, `chamber_datum` describe the dimensions of the `Tank` that controls flow. _Units_: cubic metres, squared metres, metres - """ # Set parameters self.capacity = capacity @@ -122,7 +120,6 @@ def push_check_sewer(self, vqip=None): Returns: excess (dict): Sewer tank excess - """ # Get excess excess = self.sewer_tank.get_excess() @@ -133,33 +130,30 @@ def push_check_sewer(self, vqip=None): return excess def push_set_sewer(self, vqip): - """Generic push request setting that implements basic queue travel time - (it does NOT implement timearea travel time). Updates the sewer tank - storage. Assumes that the inflow arc has accurately calculated capacity - with push_check_sewer, thus the water is forced. + """Generic push request setting that implements basic queue travel time (it does + NOT implement timearea travel time). Updates the sewer tank storage. Assumes + that the inflow arc has accurately calculated capacity with push_check_sewer, + thus the water is forced. Args: vqip (dict): A VQIP amount of water to push Returns: (dict): A VQIP amount of water that was not received - """ # Sewer to sewer push, update queued tank return self.sewer_tank.push_storage(vqip, time=self.pipe_time) def push_set_land(self, vqip): - """Push request that applies pipe_timearea (see __init__ for description). - As with push_set_sewer, push is also forced. Used to receive flow from - land or demand that is assumed to occur widely across some kind of sewer - catchment. + """Push request that applies pipe_timearea (see __init__ for description). As + with push_set_sewer, push is also forced. Used to receive flow from land or + demand that is assumed to occur widely across some kind of sewer catchment. Args: vqip (dict): A VQIP amount to be pushed Returns: (dict): A VQIP amount that was not received - """ # Land/demand to sewer push, update queued tank @@ -176,10 +170,8 @@ def push_set_land(self, vqip): def make_discharge(self): """Function to trigger downstream sewer flow. - Updates sewer tank travel time, pushes to WWTW, then sewer, then CSO. May - flood land if, after these attempts, the sewer tank storage is above - capacity. - + Updates sewer tank travel time, pushes to WWTW, then sewer, then CSO. May flood + land if, after these attempts, the sewer tank storage is above capacity. """ self.sewer_tank.internal_arc.update_queue(direction="push") # TODO... do I need to do anything with this backflow... does it ever happen? @@ -241,7 +233,6 @@ def __init__( """Alternate legacy sewer class... I dont think this is needed any more. - """ # TODO above diff --git a/wsimod/nodes/storage.py b/wsimod/nodes/storage.py index 68024301..b3e856d7 100644 --- a/wsimod/nodes/storage.py +++ b/wsimod/nodes/storage.py @@ -3,7 +3,6 @@ @author: bdobson Converted to totals on 2022-05-03 - """ from math import exp @@ -36,7 +35,6 @@ def __init__( Functions intended to call in orchestration: distribute (optional, depends on subclass) - """ # Set parameters self.initial_storage = initial_storage @@ -83,7 +81,6 @@ def push_set_storage(self, vqip): Returns: reply (dict): A VQIP amount that was not successfully pushed - """ # Update tank reply = self.tank.push_storage(vqip) @@ -91,9 +88,7 @@ def push_set_storage(self, vqip): return reply def distribute(self): - """Optional function that discharges all tank storage with - push_distributed. - """ + """Optional function that discharges all tank storage with push_distributed.""" # Distribute any active storage storage = self.tank.pull_storage(self.tank.get_avail()) retained = self.push_distributed(storage) @@ -102,9 +97,8 @@ def distribute(self): print("Storage unable to push") def get_percent(self): - """Function that returns the volume in the storage tank expressed as a - percent of capacity. - """ + """Function that returns the volume in the storage tank expressed as a percent + of capacity.""" return self.tank.storage["volume"] / self.tank.capacity def end_timestep(self): @@ -172,7 +166,6 @@ def __init__( parameters (a constant and a temperature sensitivity exponent) as values. _Units_: - - """ self.residence_time = residence_time self.infiltration_threshold = infiltration_threshold @@ -191,9 +184,7 @@ def distribute(self): print("Storage unable to push") def infiltrate(self): - """Calculate amount of water available for infiltration and send to - sewers. - """ + """Calculate amount of water available for infiltration and send to sewers.""" # Calculate infiltration avail = self.tank.get_avail()["volume"] avail = max(avail - self.tank.capacity * self.infiltration_threshold, 0) @@ -214,9 +205,9 @@ class QueueGroundwater(Storage): # TODO - no infiltration as yet def __init__(self, timearea={0: 1}, data_input_dict={}, **kwargs): - """Alternate formulation of Groundwater that uses a timearea property to - enable more nonlinear time behaviour of baseflow routing. Uses the - QueueTank or DecayQueueTank (see nodes.py/Tank subclassses). + """Alternate formulation of Groundwater that uses a timearea property to enable + more nonlinear time behaviour of baseflow routing. Uses the QueueTank or + DecayQueueTank (see nodes.py/Tank subclassses). NOTE: abstraction behaviour from this kind of node need careful checking @@ -250,7 +241,6 @@ def __init__(self, timearea={0: 1}, data_input_dict={}, **kwargs): parameters (a constant and a temperature sensitivity exponent) as values. _Units_: - - """ self.timearea = timearea # TODO not used @@ -282,15 +272,14 @@ def __init__(self, timearea={0: 1}, data_input_dict={}, **kwargs): def push_set_timearea(self, vqip): """Push setting that enables timearea behaviour, (see __init__ for - description).Used to receive flow that is assumed to occur widely across - some kind of catchment. + description).Used to receive flow that is assumed to occur widely across some + kind of catchment. Args: vqip (dict): A VQIP that has been pushed Returns: reply (dict): A VQIP amount that was not successfuly receivesd - """ reply = self.empty_vqip() # Iterate over timearea diagram @@ -330,7 +319,6 @@ def pull_check_active(self, vqip=None): Returns: (dict): A VQIP amount that is available to pull - """ if vqip is None: return self.tank.active_storage @@ -340,16 +328,15 @@ def pull_check_active(self, vqip=None): def pull_set_active(self, vqip): # TODO - this is quite weird behaviour, and inconsistent with pull_check_active - """Pull proportionately from both the active storage and the queue. - Adjudging groundwater abstractions to not be particularly sensitive to the - within catchment travel time. + """Pull proportionately from both the active storage and the queue. Adjudging + groundwater abstractions to not be particularly sensitive to the within + catchment travel time. Args: vqip (dict): A VQIP amount to be pulled (only volume key is used) Returns: pulled (dict): A VQIP amount that was successfully pulled - """ # Calculate actual pull total_storage = self.tank.storage["volume"] @@ -459,7 +446,6 @@ def __init__( _Units_: - - minimum required flow _Units_: m3/day - """ # Set parameters self.depth = depth # [m] @@ -576,7 +562,6 @@ def pull_check_river(self, vqip=None): Returns: avail (dict): A VQIP amount that can be pulled - """ # Get storage avail = self.tank.get_avail() @@ -605,7 +590,6 @@ def pull_set_river(self, vqip): Returns: (dict): A VQIP amount that was pulled - """ # Calculate available pull avail = self.pull_check_river(vqip) @@ -629,7 +613,6 @@ def push_set_river(self, vqip): Returns: (dict): A VQIP amount that was not successfully received - """ _ = self.tank.push_storage(vqip, force=True) return self.empty_vqip() @@ -643,7 +626,6 @@ def get_din_pool(self): Returns: (float): total din - """ return sum( [self.tank.storage[x] for x in self.din_components] @@ -655,7 +637,6 @@ def biochemical_processes(self): Returns: in_ (dict): A VQIP amount that represents total gain in pollutant amounts out_ (dict): A VQIP amount that represents total loss in pollutant amounts - """ # TODO make more modular self.update_depth() @@ -818,12 +799,11 @@ def biochemical_processes(self): return in_, out_ def get_riverrc(self): - """Get river outflow coefficient (i.e., how much water leaves the tank in - this timestep). + """Get river outflow coefficient (i.e., how much water leaves the tank in this + timestep). Returns: riverrc (float): outflow coeffficient - """ # Calculate travel time total_time = self.length / self.velocity @@ -902,7 +882,6 @@ def __init__(self, **kwargs): parameters (a constant and a temperature sensitivity exponent) as values. _Units_: - - """ super().__init__(**kwargs) @@ -919,8 +898,8 @@ class RiverReservoir(Reservoir): """""" def __init__(self, environmental_flow=0, **kwargs): - """A reservoir with a natural river inflow, includes an environmental - downstream flow to satisfy. + """A reservoir with a natural river inflow, includes an environmental downstream + flow to satisfy. Args: environmental_flow (float, optional): Downstream environmental flow. @@ -955,7 +934,6 @@ def __init__(self, environmental_flow=0, **kwargs): parameters (a constant and a temperature sensitivity exponent) as values. _Units_: - - """ # Parameters self.environmental_flow = environmental_flow @@ -978,7 +956,6 @@ def push_set_river_reservoir(self, vqip): Returns: reply (dict): A VQIP amount that was not successfully received - """ # Apply normal reservoir storage # We do this under the assumption that spill is mixed in with the reservoir @@ -999,8 +976,8 @@ def push_set_river_reservoir(self, vqip): return reply def push_check_river_reservoir(self, vqip=None): - """A push check to receive water, assumes spill may occur and checks - downstream capacity. + """A push check to receive water, assumes spill may occur and checks downstream + capacity. Args: vqip (dict, optional): A VQIP that can be used to limit the volume in @@ -1008,7 +985,6 @@ def push_check_river_reservoir(self, vqip=None): Returns: excess (dict): A VQIP amount of water that cannot be received - """ # Check downstream capacity (i.e., that would be spilled) downstream_availability = self.get_connected( diff --git a/wsimod/nodes/waste.py b/wsimod/nodes/waste.py index 85e7c0d7..5f381d63 100644 --- a/wsimod/nodes/waste.py +++ b/wsimod/nodes/waste.py @@ -2,7 +2,6 @@ """Created on Mon Nov 15 14:20:36 2021. @author: bdobson - """ from wsimod.nodes.nodes import Node @@ -25,7 +24,6 @@ def __init__(self, name): Input data and parameter requirements: - None - """ # Update args super().__init__(name) @@ -47,6 +45,5 @@ def push_set_accept(self, vqip): Returns: (dict): An empty VQIP, indicating all water was received - """ return self.empty_vqip() diff --git a/wsimod/nodes/wtw.py b/wsimod/nodes/wtw.py index 19da7a32..31601b1d 100644 --- a/wsimod/nodes/wtw.py +++ b/wsimod/nodes/wtw.py @@ -3,7 +3,6 @@ @author: bdobson Converted to totals on 2022-05-03 - """ from wsimod.core import constants from wsimod.nodes.nodes import Node, Tank @@ -20,12 +19,11 @@ def __init__( liquor_multiplier={}, percent_solids=0.0002, ): - """Generic treatment processes that apply temperature a sensitive - transform of pollutants into liquor and solids (behaviour depends on - subclass). Push requests are stored in the current_input state variable, - but treatment must be triggered with treat_current_input. This treated - water is stored in the discharge_holder state variable, which will be sent - different depending on FWTW/WWTW. + """Generic treatment processes that apply temperature a sensitive transform of + pollutants into liquor and solids (behaviour depends on subclass). Push requests + are stored in the current_input state variable, but treatment must be triggered + with treat_current_input. This treated water is stored in the discharge_holder + state variable, which will be sent different depending on FWTW/WWTW. Args: name (str): Node name @@ -62,7 +60,6 @@ def __init__( _Units_: - - `liquor_multiplier` and `percent_solids` describe the proportion of throughput that goes to liquor/solids. - """ # Set/Default parameters self.treatment_throughput_capacity = treatment_throughput_capacity @@ -103,14 +100,12 @@ def get_excess_throughput(self): Returns: (float): Amount of volume that can still be treated this timestep - """ return max(self.treatment_throughput_capacity - self.current_input["volume"], 0) def treat_current_input(self): """Run treatment processes this timestep, including temperature sensitive - transforms, liquoring, solids. - """ + transforms, liquoring, solids.""" # Treat current input influent = self.copy_vqip(self.current_input) @@ -175,9 +170,9 @@ def __init__( stormwater_storage_elevation=10, **kwargs, ): - """A wastewater treatment works wrapper for WTW. Contains a temporary - stormwater storage tank. Liquor is combined with current_effluent and re- - treated while solids leave the model. + """A wastewater treatment works wrapper for WTW. Contains a temporary stormwater + storage tank. Liquor is combined with current_effluent and re- treated while + solids leave the model. Args: stormwater_storage_capacity (float, optional): Capacity of stormwater tank. Defaults to 10. @@ -200,7 +195,6 @@ def __init__( - See `wtw.py/WTW` for treatment. - Stormwater tank `capacity`, `area`, and `datum`. _Units_: cubic metres, squared metres, metres - """ # Set parameters self.stormwater_storage_capacity = stormwater_storage_capacity @@ -275,7 +269,6 @@ def push_check_sewer(self, vqip=None): Returns: (dict): excess - """ # Get excess excess_throughput = self.get_excess_throughput() @@ -291,15 +284,13 @@ def push_check_sewer(self, vqip=None): return self.v_change_vqip(vqip, vol) def push_set_sewer(self, vqip): - """Receive water, first try to update current_input, and then stormwater - tank. + """Receive water, first try to update current_input, and then stormwater tank. Args: vqip (dict): A VQIP amount to be treated and then stored Returns: (dict): A VQIP amount of water that was not treated - """ # Receive water from sewers vqip = self.copy_vqip(vqip) @@ -328,16 +319,15 @@ def push_set_sewer(self, vqip): return vqip def pull_set_reuse(self, vqip): - """Enables WWTW to receive pulls of the treated water (i.e., for - wastewater reuse or satisfaction of environmental flows). Intended to be - called in between calculate_discharge and make_discharge. + """Enables WWTW to receive pulls of the treated water (i.e., for wastewater + reuse or satisfaction of environmental flows). Intended to be called in between + calculate_discharge and make_discharge. Args: vqip (dict): A VQIP amount to be pulled (only 'volume' key is used) Returns: reply (dict): Amount of water that has been pulled - """ # Satisfy request with treated (volume) reply_vol = min(vqip["volume"], self.treated["volume"]) @@ -350,11 +340,10 @@ def pull_set_reuse(self, vqip): return reply def pull_check_reuse(self, vqip=None): - """Pull check available water. Simply returns the previous timestep's - treated throughput. This is of course inaccurate (which may lead to - slightly longer calulcations), but it is much more flexible. This hasn't - been recently tested so it might be that returning treated would be fine - (and more accurate!). + """Pull check available water. Simply returns the previous timestep's treated + throughput. This is of course inaccurate (which may lead to slightly longer + calulcations), but it is much more flexible. This hasn't been recently tested so + it might be that returning treated would be fine (and more accurate!). Args: vqip (dict, optional): A VQIP that can be used to limit the volume in @@ -363,7 +352,6 @@ def pull_check_reuse(self, vqip=None): Returns: (dict): A VQIP amount of water available. Currently just the previous timestep's treated throughput - """ # Respond to request of water for reuse/MRF return self.copy_vqip(self.treated) @@ -389,10 +377,10 @@ def __init__( data_input_dict={}, **kwargs, ): - """A freshwater treatment works wrapper for WTW. Contains service - reservoirs that treated water is released to and pulled from. Cannot allow - deficit (thus any deficit is satisfied by water entering the model 'via - other means'). Liquor and solids are sent to sewers. + """A freshwater treatment works wrapper for WTW. Contains service reservoirs + that treated water is released to and pulled from. Cannot allow deficit (thus + any deficit is satisfied by water entering the model 'via other means'). Liquor + and solids are sent to sewers. Args: service_reservoir_storage_capacity (float, optional): Capacity of service @@ -423,7 +411,6 @@ def __init__( - See `wtw.py/WTW` for treatment. - Service reservoir tank `capacity`, `area`, and `datum`. _Units_: cubic metres, squared metres, metres - """ # Default parameters self.service_reservoir_storage_capacity = service_reservoir_storage_capacity @@ -467,8 +454,7 @@ def __init__( def treat_water(self): """Pulls water, aiming to fill service reservoirs, calls WTW - treat_current_input, avoids deficit, sends liquor and solids to sewers. - """ + treat_current_input, avoids deficit, sends liquor and solids to sewers.""" # Calculate how much water is needed target_throughput = self.service_reservoir_tank.get_excess() target_throughput = min( @@ -521,7 +507,6 @@ def pull_check_fwtw(self, vqip=None): Returns: (dict): A VQIP of availability in service reservoirs - """ return self.service_reservoir_tank.get_avail(vqip) @@ -533,7 +518,6 @@ def pull_set_fwtw(self, vqip): Returns: pulled (dict): A VQIP amount that was successfully pulled - """ # Pull pulled = self.service_reservoir_tank.pull_storage(vqip) diff --git a/wsimod/orchestration/model.py b/wsimod/orchestration/model.py index e01e3b68..d79c6523 100644 --- a/wsimod/orchestration/model.py +++ b/wsimod/orchestration/model.py @@ -2,7 +2,6 @@ """Created on Mon Jul 4 16:01:48 2022. @author: bdobson - """ import csv import gzip @@ -31,13 +30,11 @@ class to_datetime: # TODO document and make better def __init__(self, date_string): - """Simple datetime wrapper that has key properties used in WSIMOD - components. + """Simple datetime wrapper that has key properties used in WSIMOD components. Args: date_string (str): A string containing the date, expected in format %Y-%m-%d or %Y-%m. - """ self._date = self._parse_date(date_string) @@ -137,7 +134,6 @@ def __init__(self): Returns: Model: An empty model object - """ super().__init__() self.arcs = {} @@ -165,9 +161,7 @@ def all_subclasses(cls): self.nodes_type = {x: {} for x in self.nodes_type} def get_init_args(self, cls): - """Get the arguments of the __init__ method for a class and its - superclasses. - """ + """Get the arguments of the __init__ method for a class and its superclasses.""" init_args = [] for c in cls.__mro__: # Get the arguments of the __init__ method @@ -218,14 +212,13 @@ def load(self, address, config_name="config.yml", overrides={}): self.dates = [to_datetime(x) for x in data["dates"]] def save(self, address, config_name="config.yml", compress=False): - """Save the model object to a yaml file and input data to csv.gz format in - the directory specified. + """Save the model object to a yaml file and input data to csv.gz format in the + directory specified. Args: address (str): Path to a directory config_name (str, optional): Name of yaml model file. Defaults to 'model.yml' - """ if not os.path.exists(address): os.mkdir(address) @@ -396,7 +389,6 @@ def load_pickle(self, fid): >>> # of the previous run >>> new_model = Model() >>> new_model = new_model.load_pickle('model_at_end_of_run.pkl') - """ file = open(fid, "rb") return pickle.load(file) @@ -409,20 +401,17 @@ def save_pickle(self, fid): Returns: message (str): Exit message of pickle dump - """ file = open(fid, "wb") pickle.dump(self, file) return file.close() def add_nodes(self, nodelist): - """Add nodes to the model object from a list of dicts, where each dict - contains all of the parameters for a node. Intended to be called before - add_arcs. + """Add nodes to the model object from a list of dicts, where each dict contains + all of the parameters for a node. Intended to be called before add_arcs. Args: nodelist (list): List of dicts, where a dict is a node - """ def all_subclasses(cls): @@ -457,13 +446,11 @@ def all_subclasses(cls): self.nodelist = [x for x in self.nodes.values()] def add_instantiated_nodes(self, nodelist): - """Add nodes to the model object from a list of objects, where each object - is an already instantiated node object. Intended to be called before - add_arcs. + """Add nodes to the model object from a list of objects, where each object is an + already instantiated node object. Intended to be called before add_arcs. Args: nodelist (list): list of objects that are nodes - """ self.nodelist = nodelist self.nodes = {x.name: x for x in nodelist} @@ -471,12 +458,11 @@ def add_instantiated_nodes(self, nodelist): self.nodes_type[x.__class__.__name__][x.name] = x def add_arcs(self, arclist): - """Add nodes to the model object from a list of dicts, where each dict - contains all of the parameters for an arc. + """Add nodes to the model object from a list of dicts, where each dict contains + all of the parameters for an arc. Args: arclist (list): list of dicts, where a dict is an arc - """ river_arcs = {} for arc in arclist: @@ -513,12 +499,11 @@ def add_arcs(self, arclist): self.river_discharge_order.append(node[0]) def add_instantiated_arcs(self, arclist): - """Add arcs to the model object from a list of objects, where each object - is an already instantiated arc object. + """Add arcs to the model object from a list of objects, where each object is an + already instantiated arc object. Args: arclist (list): list of objects that are arcs. - """ self.arclist = arclist self.arcs = {x.name: x for x in arclist} @@ -549,8 +534,8 @@ def add_instantiated_arcs(self, arclist): self.river_discharge_order.append(node[0]) def assign_upstream(self, arcs, upstreamness): - """Recursive function to trace upstream up arcs to determine which are the - most upstream. + """Recursive function to trace upstream up arcs to determine which are the most + upstream. Args: arcs (list): list of dicts where dicts are arcs @@ -560,7 +545,6 @@ def assign_upstream(self, arcs, upstreamness): Returns: upstreamness (dict): final version of upstreamness - """ upstreamness_ = upstreamness.copy() in_nodes = [ @@ -580,8 +564,7 @@ def assign_upstream(self, arcs, upstreamness): def debug_node_mb(self): """Simple function that iterates over nodes calling their mass balance - function. - """ + function.""" for node in self.nodelist: _ = node.node_mass_balance() @@ -590,7 +573,6 @@ def default_settings(self): Returns: (dict): default settings - """ return { "arcs": {"flows": True, "pollutants": True}, @@ -606,7 +588,6 @@ def change_runoff_coefficient(self, relative_change, nodes=None): node is multiplied by (grass area is changed in compensation) nodes (list, optional): list of land nodes to change the parameters of. Defaults to None, which applies the change to all land nodes. - """ # Multiplies impervious area by relative change and adjusts grassland accordingly if nodes is None: @@ -686,7 +667,6 @@ def run( 'function' : @ (x, model) sum([y['storage'] < (model.nodes['my_reservoir'].tank.capacity / 2) for y in x]) }] _, _, results, _ = my_model.run(record_all = False, objectives = objectives) - """ if record_arcs is None: record_arcs = [] @@ -990,9 +970,8 @@ def enablePrint(stdout): return flows, tanks, objective_results, surfaces def reinit(self): - """Reinitialise by ending all node/arc timesteps and calling reinit - function in all nodes (generally zero-ing their storage values). - """ + """Reinitialise by ending all node/arc timesteps and calling reinit function in + all nodes (generally zero-ing their storage values).""" for node in self.nodes.values(): node.end_timestep() for prop in dir(node): From 1421d7783f640f4dea5cbd029a3b4c6b74718948 Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Tue, 12 Dec 2023 17:39:46 +0000 Subject: [PATCH 3/7] Fix long lines in wtw.py --- wsimod/nodes/wtw.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/wsimod/nodes/wtw.py b/wsimod/nodes/wtw.py index 31601b1d..24b578e1 100644 --- a/wsimod/nodes/wtw.py +++ b/wsimod/nodes/wtw.py @@ -112,7 +112,8 @@ def treat_current_input(self): # Calculate effluent, liquor and solids discharge_holder = self.empty_vqip() - # Assume non-additive pollutants are unchanged in discharge and are proportionately mixed in liquor + # Assume non-additive pollutants are unchanged in discharge and are + # proportionately mixed in liquor for key in constants.NON_ADDITIVE_POLLUTANTS: discharge_holder[key] = influent[key] self.liquor[key] = ( @@ -123,8 +124,10 @@ def treat_current_input(self): + influent["volume"] * self.liquor_multiplier["volume"] ) - # TODO this should probably just be for process_parameters.keys() to avoid having to declare non changing parameters - # TODO should the liquoring be temperature sensitive too? As it is the solids will take the brunt of the temperature variability which maybe isn't sensible + # TODO this should probably just be for process_parameters.keys() to avoid + # having to declare non changing parameters + # TODO should the liquoring be temperature sensitive too? As it is the solids + # will take the brunt of the temperature variability which maybe isn't sensible for key in constants.ADDITIVE_POLLUTANTS + ["volume"]: if key != "volume": # Temperature sensitive transform @@ -175,9 +178,12 @@ def __init__( solids leave the model. Args: - stormwater_storage_capacity (float, optional): Capacity of stormwater tank. Defaults to 10. - stormwater_storage_area (float, optional): Area of stormwater tank. Defaults to 1. - stormwater_storage_elevation (float, optional): Datum of stormwater tank. Defaults to 10. + stormwater_storage_capacity (float, optional): Capacity of stormwater tank. + Defaults to 10. + stormwater_storage_area (float, optional): Area of stormwater tank. + Defaults to 1. + stormwater_storage_elevation (float, optional): Datum of stormwater tank. + Defaults to 10. Functions intended to call in orchestration: calculate_discharge @@ -238,7 +244,8 @@ def calculate_discharge(self): # Run WWTW model # Try to clear stormwater - # TODO (probably more tidy to use push_set_sewer? though maybe less computationally efficient) + # TODO (probably more tidy to use push_set_sewer? though maybe less + # computationally efficient) excess = self.get_excess_throughput() if (self.stormwater_tank.get_avail()["volume"] > constants.FLOAT_ACCURACY) & ( excess > constants.FLOAT_ACCURACY @@ -402,7 +409,8 @@ def __init__( - See `wtw.py/WTW` for treatment. - Stores treated water in a service reservoir tank, with a single tank per `FWTW` node. - - Aims to satisfy a throughput that would top up the service reservoirs until full. + - Aims to satisfy a throughput that would top up the service reservoirs + until full. - Currently, will not allow a deficit, thus introducing water from 'other measures' if pulls cannot fulfil demand. Behaviour under a deficit should be determined and validated before introducing. @@ -444,8 +452,10 @@ def __init__( datum=self.service_reservoir_storage_elevation, initial_storage=self.service_reservoir_initial_storage, ) - # self.service_reservoir_tank.storage['volume'] = self.service_reservoir_inital_storage - # self.service_reservoir_tank.storage_['volume'] = self.service_reservoir_inital_storage + # self.service_reservoir_tank.storage['volume'] = + # self.service_reservoir_inital_storage + # self.service_reservoir_tank.storage_['volume'] = + # self.service_reservoir_inital_storage # Mass balance self.mass_balance_in.append(lambda: self.total_deficit) From 9f35466574a1d239fec030a7d12243c753eaa3b7 Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Tue, 12 Dec 2023 17:46:09 +0000 Subject: [PATCH 4/7] Fix long lines nodes.py --- wsimod/nodes/nodes.py | 56 ++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/wsimod/nodes/nodes.py b/wsimod/nodes/nodes.py index 8aea2ee2..7bea027e 100644 --- a/wsimod/nodes/nodes.py +++ b/wsimod/nodes/nodes.py @@ -218,7 +218,8 @@ def get_direction_arcs(self, direction, of_type=None): Examples: >>> arcs_to_push_to = my_node.get_direction_arcs('push') >>> arcs_to_pull_from = my_node.get_direction_arcs('pull') - >>> arcs_from_reservoirs = my_node.get_direction_arcs('pull', of_type = 'Reservoir') + >>> arcs_from_reservoirs = my_node.get_direction_arcs('pull', of_type = + 'Reservoir') """ if of_type is None: # Return all arcs @@ -426,7 +427,8 @@ def push_distributed(self, vqip, of_type=None, tag="default"): connected = self.get_connected(direction="push", of_type=of_type, tag=tag) iter_ = 0 if not_pushed > connected["avail"]: - # If more water than can be pushed, ignore preference and allocate all available based on capacity + # If more water than can be pushed, ignore preference and allocate all + # available based on capacity connected["priority"] = connected["avail"] connected["allocation"] = connected["capacity"] @@ -631,22 +633,26 @@ def reinit(self): This is an attempt to generalise the behaviour of pull/push_distributed It doesn't yet work... - def general_distribute(self, vqip, of_type = None, tag = 'default', direction = None): + def general_distribute(self, vqip, of_type = None, tag = 'default', direction = + None): if direction == 'push': arcs = self.out_arcs arcs_type = self.out_arcs_type tracker = self.copy_vqip(vqip) - requests = {x.name : lambda y : x.send_push_request(y, tag) for x in arcs.values()} + requests = {x.name : lambda y : x.send_push_request(y, tag) for x in arcs. + values()} elif direction == 'pull': arcs = self.in_arcs arcs_type = self.in_arcs_type tracker = self.empty_vqip() - requests = {x.name : lambda y : x.send_pull_request(y, tag) for x in arcs.values()} + requests = {x.name : lambda y : x.send_pull_request(y, tag) for x in arcs. + values()} else: print('No direction') if len(arcs) == 1: - if (of_type == None) | any([x in of_type for x, y in arcs_type.items() if len(y) > 0]): + if (of_type == None) | any([x in of_type for x, y in arcs_type.items() if + len(y) > 0]): arc = next(iter(arcs.keys())) return requests[arc](vqip) else: @@ -665,7 +671,8 @@ def general_distribute(self, vqip, of_type = None, tag = 'default', direction = (connected['avail'] > constants.FLOAT_ACCURACY)) & (iter_ < constants.MAXITER)): - amount = min(connected['avail'], target['volume']) #Deficit or amount still to push + amount = min(connected['avail'], target['volume']) #Deficit or amount + still to push replies = self.empty_vqip() for key, allocation in connected['allocation'].items(): @@ -794,7 +801,8 @@ def pull_ponded(self): Examples: >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] - >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 10, 'phosphate' : 0.2}) + >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 10, + 'phosphate' : 0.2}) >>> print(my_tank.storage) {'volume' : 10, 'phosphate' : 0.2} >>> print(my_tank.pull_ponded()) @@ -820,7 +828,8 @@ def get_avail(self, vqip=None): Examples: >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] - >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 10, 'phosphate' : 0.2}) + >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 10, + 'phosphate' : 0.2}) >>> print(my_tank.storage) {'volume' : 10, 'phosphate' : 0.2} >>> print(my_tank.get_avail()) @@ -849,7 +858,8 @@ def get_excess(self, vqip=None): Examples: >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] - >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, 'phosphate' : 0.2}) + >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, + 'phosphate' : 0.2}) >>> print(my_tank.get_excess()) {'volume' : 4, 'phosphate' : 0.16} >>> print(my_tank.get_excess({'volume' : 2})) @@ -860,7 +870,8 @@ def get_excess(self, vqip=None): vol = min(vqip["volume"], vol) # Adjust storage pollutants to match volume in vqip - # TODO the v_change_vqip in the reply here is a weird default (if a VQIP is not provided) + # TODO the v_change_vqip in the reply here is a weird default (if a VQIP is not + # provided) return self.v_change_vqip(self.storage, vol) def push_storage(self, vqip, force=False): @@ -879,7 +890,8 @@ def push_storage(self, vqip, force=False): >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] >>> constants.POLLUTANTS = ['phosphate'] >>> constants.NON_ADDITIVE_POLLUTANTS = [] - >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, 'phosphate' : 0.2}) + >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, + 'phosphate' : 0.2}) >>> my_push = {'volume' : 10, 'phosphate' : 0.5} >>> reply = my_tank.push_storage(my_push) >>> print(reply) @@ -921,7 +933,8 @@ def pull_storage(self, vqip): Examples: >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] - >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, 'phosphate' : 0.2}) + >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, + 'phosphate' : 0.2}) >>> print(my_tank.pull_storage({'volume' : 6})) {'volume': 5.0, 'phosphate': 0.2} >>> print(my_tank.storage) @@ -954,7 +967,8 @@ def pull_pollutants(self, vqip): Examples: >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] - >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, 'phosphate' : 0.2}) + >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, + 'phosphate' : 0.2}) >>> print(my_tank.pull_pollutants({'volume' : 2, 'phosphate' : 0.15})) {'volume': 2.0, 'phosphate': 0.15} >>> print(my_tank.storage) @@ -1042,7 +1056,8 @@ def push_total_c(self, vqip): Returns: """ - # Push vqip to storage where pollutants are given as a concentration rather than storage + # Push vqip to storage where pollutants are given as a concentration rather + # than storage vqip = self.concentration_to_total(self.vqip) self.storage = self.sum_vqip(self.storage, vqip) return self.empty_vqip() @@ -1099,8 +1114,9 @@ def __init__(self, decays={}, parent=None, **kwargs): beginning of the timestep. Args: - decays (dict): A dict of dicts containing a key for each pollutant that decays - and, within that, a key for each parameter (a constant and exponent) + decays (dict): A dict of dicts containing a key for each pollutant that + decays and, within that, a key for each parameter (a constant and + exponent) parent (object): An object that can be used to read temperature data from """ # Store parameters @@ -1161,7 +1177,8 @@ def __init__(self, number_of_timesteps=0, **kwargs): self.internal_arc = AltQueueArc( in_port=self, out_port=self, number_of_timesteps=self.number_of_timesteps ) - # TODO should mass balance call internal arc (is this arc called in arc mass balance?) + # TODO should mass balance call internal arc (is this arc called in arc mass + # balance?) def get_avail(self): """Return the active_storage of the tank. @@ -1334,7 +1351,8 @@ def __init__(self, decays={}, parent=None, number_of_timesteps=1, **kwargs): def _end_timestep(self): """End timestep wrapper that removes decayed pollutants and calls internal arc.""" - # TODO Should the active storage decay if decays are given (probably.. though that sounds like a nightmare)? + # TODO Should the active storage decay if decays are given (probably.. though + # that sounds like a nightmare)? self.storage = self.extract_vqip(self.storage, self.internal_arc.total_decayed) self.storage_ = self.copy_vqip(self.storage) self.internal_arc.end_timestep() From c6cd604b642e79ead513fed225dbced496f5bb10 Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Wed, 10 Jan 2024 08:53:37 +0000 Subject: [PATCH 5/7] Fix line length issues --- wsimod/core/constants.py | 6 +- wsimod/core/core.py | 78 +++++----- wsimod/nodes/demand.py | 55 +++---- wsimod/nodes/land.py | 276 +++++++++++++++++++--------------- wsimod/nodes/nutrient_pool.py | 111 ++++++++------ wsimod/nodes/sewer.py | 3 +- wsimod/nodes/storage.py | 213 ++++++++++++-------------- wsimod/orchestration/model.py | 9 +- wsimod/validation.py | 3 +- 9 files changed, 404 insertions(+), 350 deletions(-) diff --git a/wsimod/core/constants.py b/wsimod/core/constants.py index 4055f212..d529bcc2 100644 --- a/wsimod/core/constants.py +++ b/wsimod/core/constants.py @@ -8,7 +8,8 @@ M3_S_TO_ML_D = 86.4 MM_KM2_TO_ML = 1e-3 * 1e6 * 1e3 * 1e-6 # mm->m, km2->m2, m3->l, l->Ml MM_M2_TO_ML = 1e-3 * 1e3 * 1e-6 # mm->m, m3->l, l->Ml -MM_M2_TO_SIM_VOLUME = MM_M2_TO_ML # SIM volume is by default ML, but can be changed by changing MM_TO_SIM_VOLUME +MM_M2_TO_SIM_VOLUME = MM_M2_TO_ML # SIM volume is by default ML, but can be changed by +# changing MM_TO_SIM_VOLUME MM_M2_TO_M3 = 1e-3 # mm->m ML_TO_M3 = 1000 PCT_TO_PROP = 1 / 100 @@ -116,7 +117,8 @@ def set_default_pollutants(): "do", "temperature", "ph", - ] # e.g. pollutants whose concentration should not increase if volume is distilled out + ] # e.g. pollutants whose concentration should not increase if volume is distilled + # out constants.ADDITIVE_POLLUTANTS = [ "org-phosphorus", "phosphate", diff --git a/wsimod/core/core.py b/wsimod/core/core.py index 46c6eda9..d73b606d 100644 --- a/wsimod/core/core.py +++ b/wsimod/core/core.py @@ -62,8 +62,8 @@ def blend_vqip(self, c1, c2): know what you're doing. This won't do anything on VQIPs with 0 volume. Args: - c1 (dict): A VQIP where pollutant entries are concentrations - c2 (dict): A VQIP where pollutant entries are concentrations + c1 (dict): A VQIP where pollutant entries are concentrations c2 (dict): A + VQIP where pollutant entries are concentrations Returns: c (dict): A new VQIP where c1 and c2 have been proportionately blended @@ -86,8 +86,8 @@ def sum_vqip(self, t1, t2): proportionately blended. Args: - t1 (dict): A VQIP where pollutant entries are mass totals - t2 (dict): A VQIP where pollutant entries are mass totals + t1 (dict): A VQIP where pollutant entries are mass totals t2 (dict): A VQIP + where pollutant entries are mass totals Returns: t (dict): A VQIP that is the sum of t1 and t2 (except for non-additive @@ -142,7 +142,8 @@ def total_to_concentration(self, t): """ c = self.copy_vqip(t) for pollutant in constants.ADDITIVE_POLLUTANTS: - # Divide concentration by volume to get concentration for additive pollutants + # Divide concentration by volume to get concentration for additive + # pollutants c[pollutant] /= c["volume"] return c @@ -167,8 +168,8 @@ def extract_vqip(self, t1, t2): >>> print(t) {'phosphate' : 0, 'volume' : 90, 'temperature' : 10} """ - # TODO should probably be called 'subtract_vqip' - # TODO need to analyse uses of this to see if it is sensible to do something for non additive + # TODO should probably be called 'subtract_vqip' TODO need to analyse uses of + # this to see if it is sensible to do something for non additive t = self.copy_vqip(t1) # Directly subtract t2 from t1 for vol and additive pollutants for pol in constants.ADDITIVE_POLLUTANTS + ["volume"]: @@ -181,7 +182,8 @@ def extract_vqip_c(self, c1, c2): concentrations. Operation performed for volume and additive pollutants. NOTE: VQIPs in WSIMOD in general store pollution as a total rather than a - concentration. So you should only work with concentrations if you are doing it intentionally and know what you're doing. + concentration. So you should only work with concentrations if you are doing it + intentionally and know what you're doing. Args: c1 (dict): A VQIP where pollutant entries are concentrations to subtract @@ -225,7 +227,8 @@ def v_distill_vqip_c(self, c, v): concentrations. NOTE: VQIPs in WSIMOD in general store pollution as a total rather than a - concentration. So you should only work with concentrations if you are doing it intentionally and know what you're doing. + concentration. So you should only work with concentrations if you are doing it + intentionally and know what you're doing. Args: c (dict): A VQIP where pollutant entries are concentrations to remove @@ -284,11 +287,12 @@ def v_change_vqip_c(self, c, v): """Change the volume of a VQIP, where pollutants are concentrations. NOTE: VQIPs in WSIMOD in general store pollution as a total rather than a - concentration. So you should only work with concentrations if you are doing it intentionally and know what you're doing. + concentration. So you should only work with concentrations if you are doing it + intentionally and know what you're doing. Args: - c (dict): A VQIP where pollutant entries are concentrations - v (float): Volume to change c's volume to + c (dict): A VQIP where pollutant entries are concentrations v (float): + Volume to change c's volume to Returns: c (dict): A new VQIP with volume udpated @@ -328,7 +332,8 @@ def ds_vqip_c(self, c, c_): pollutants are given as concentrations but difference is given as mass totals. NOTE: VQIPs in WSIMOD in general store pollution as a total rather than a - concentration. So you should only work with concentrations if you are doing it intentionally and know what you're doing. + concentration. So you should only work with concentrations if you are doing it + intentionally and know what you're doing. Args: c (dict): A VQIP where pollutant entries are concentrations to subtract @@ -351,8 +356,7 @@ def compare_vqip(self, t1, t2): constants.FLOAT_ACCURACY. Args: - t1 (dict): A VQIP - t2 (dict): A VQIP + t1 (dict): A VQIP t2 (dict): A VQIP Returns: bool: True if the difference is less for each key, False otherwise @@ -372,9 +376,9 @@ def mass_balance(self): actually occurred Returns: - in_ (dict): A VQIP of the total from mass_balance_in functions - ds_ (dict): A VQIP of the total from mass_balance_ds functions - out_ (dict): A VQIP of the total from mass_balance_out functions + in_ (dict): A VQIP of the total from mass_balance_in functions ds_ (dict): A + VQIP of the total from mass_balance_ds functions out_ (dict): A VQIP of the + total from mass_balance_out functions Raises: Message if mass balance does not close to constants.FLOAT_ACCURACY @@ -413,8 +417,8 @@ def mass_balance(self): out_10 = out_[v] if abs(in_10 - ds_10 - out_10) > constants.FLOAT_ACCURACY: - # Print mass balance error - # Print actual difference rather than magnitude comparison to enable user judgement + # Print mass balance error Print actual difference rather than magnitude + # comparison to enable user judgement print( "mass balance error for {0} of {1} in {2}".format( @@ -428,25 +432,25 @@ def mass_balance(self): class DecayObj(WSIObj): """""" - # TODO - internet says this is a bad idea (diamond will occur when a Node - - # a type of WSIObj inherits a DecayObj - also a type of WSIObj). The reason - # diamonds are problems is because there can be conflicts in functions. But - # I don't want anyone to overwrite WSIObj functions so I don't see an issue? + # TODO - internet says this is a bad idea (diamond will occur when a Node - a type + # of WSIObj inherits a DecayObj - also a type of WSIObj). The reason diamonds are + # problems is because there can be conflicts in functions. But I don't want anyone + # to overwrite WSIObj functions so I don't see an issue? def __init__(self, decays): """A WSIObj that has decay functions built in. Args: - decays (dict): A dict of dicts containing a key for each pollutant that decays + decays (dict): A dict of dicts containing a key for each pollutant that + decays and, within that, a key for each parameter (a constant and exponent) Examples: The 'constant' parameter represents what proportion of an amount will decrease each time make_decay is called. Lower value will reduce decay. - Bounded between 0 and 1. - The 'exponent' parameter represents how temperature sensitive the decay - is. The higher the value, the more pollution occurs at higher values. - Values expected to vary between 1 (no temperature sensitivity) and 1.1 - (high temperature sensitivity). + Bounded between 0 and 1. The 'exponent' parameter represents how temperature + sensitive the decay is. The higher the value, the more pollution occurs at + higher values. Values expected to vary between 1 (no temperature + sensitivity) and 1.1 (high temperature sensitivity). >>> decays = {'phosphate' : {'constant' : 0.001, 'exponent' : 1.005}} @@ -492,13 +496,12 @@ def generic_temperature_decay(self, t, d, temperature): pollutants are given as mass totals. Args: - t (dict): A VQIP to decay where pollutants are given as mass totals - d (dict): decays in a DecayObj - temperature (float): temperature + t (dict): A VQIP to decay where pollutants are given as mass totals d + (dict): decays in a DecayObj temperature (float): temperature Returns: - t (dict): A VQIP with updated pollutant values - diff (dict): A VQIP storing the change in pollutant values (decreases + t (dict): A VQIP with updated pollutant values diff (dict): A VQIP storing + the change in pollutant values (decreases stored as positive numbers) """ t = self.copy_vqip(t) @@ -522,9 +525,8 @@ def generic_temperature_decay_c(self, c, d, temperature): pollutants are given as concentrations. Args: - c (dict): A VQIP to decay where pollutants are given as concentrations. - d (dict): decays in a DecayObj - temperature (float): temperature + c (dict): A VQIP to decay where pollutants are given as concentrations. d + (dict): decays in a DecayObj temperature (float): temperature Returns: t (dict): A VQIP with updated pollutant values (pollutants as diff --git a/wsimod/nodes/demand.py b/wsimod/nodes/demand.py index bab6b79d..131ac80d 100644 --- a/wsimod/nodes/demand.py +++ b/wsimod/nodes/demand.py @@ -23,22 +23,22 @@ def __init__( ResidentialDemand is in use. Args: - name (str): node name - constant_demand (float, optional): A constant portion of demand if no subclass + name (str): node name constant_demand (float, optional): A constant portion + of demand if no subclass is used. Defaults to 0. - pollutant_load (dict, optional): Pollutant mass per timestep of constant_demand. + pollutant_load (dict, optional): Pollutant mass per timestep of + constant_demand. Defaults to {}. data_input_dict (dict, optional): Dictionary of data inputs relevant for the node (temperature). Keys are tuples where first value is the name of - the variable to read from the dict and the second value is the - time. Defaults to {} + the variable to read from the dict and the second value is the time. + Defaults to {} Functions intended to call in orchestration: create_demand """ - # TODO should temperature be defined in pollutant dict? - # TODO a lot of this should be moved to ResidentialDemand - # Assign parameters + # TODO should temperature be defined in pollutant dict? TODO a lot of this + # should be moved to ResidentialDemand Assign parameters self.constant_demand = constant_demand self.pollutant_load = pollutant_load # Update args @@ -54,10 +54,8 @@ def __init__( self.total_backup = self.empty_vqip() # ew self.total_received = self.empty_vqip() - # Mass balance - # Because we assume demand is always satisfied - # received water 'disappears' for mass balance - # and consumed water 'appears' (this makes) + # Mass balance Because we assume demand is always satisfied received water + # 'disappears' for mass balance and consumed water 'appears' (this makes) # introduction of pollutants easy self.mass_balance_in.append(lambda: self.total_demand) self.mass_balance_out.append(lambda: self.total_backup) @@ -154,20 +152,20 @@ def __init__( """Subclass of demand with functions to handle internal and external water use. Args: - population (float, optional): population of node. Defaults to 1. - per_capita (float, optional): Volume per person per timestep of water + population (float, optional): population of node. Defaults to 1. per_capita + (float, optional): Volume per person per timestep of water used. Defaults to 0.12. pollutant_load (dict, optional): Mass per person per timestep of different pollutants generated. Defaults to {}. gardening_efficiency (float, optional): Value between 0 and 1 that translates irrigation demand from GardenSurface into water requested - from the distribution network. Should account for percent of garden - that is irrigated and the efficacy of people in meeting their garden - water demand. Defaults to 0.6*0.7. + from the distribution network. Should account for percent of garden that + is irrigated and the efficacy of people in meeting their garden water + demand. Defaults to 0.6*0.7. data_input_dict (dict, optional): Dictionary of data inputs relevant for the node (temperature). Keys are tuples where first value is the name of - the variable to read from the dict and the second value is the - time. Defaults to {} + the variable to read from the dict and the second value is the time. + Defaults to {} constant_temp (float, optional): A constant temperature associated with generated water. Defaults to 30 constant_weighting (float, optional): Proportion of temperature that is @@ -175,8 +173,10 @@ def __init__( Key assumptions: - Per capita calculations to generate demand based on population. - - Pollutant concentration of generated demand uses a fixed mass per person per timestep. - - Temperature of generated wastewater is based partially on air temperature and partially on a constant. + - Pollutant concentration of generated demand uses a fixed mass per person + per timestep. + - Temperature of generated wastewater is based partially on air temperature + and partially on a constant. - Can interact with `land.py/GardenSurface` to simulate garden water use. Input data and parameter requirements: @@ -245,7 +245,8 @@ def apply_gardening_pollutants(self, excess): Returns: (dict): A VQIP of water that includes pollutants to be sent to land """ - # TODO Fertilisers are currently applied in the land node... which is preferable? + # TODO Fertilisers are currently applied in the land node... which is + # preferable? vqip = self.empty_vqip() vqip["volume"] = excess return vqip @@ -259,8 +260,8 @@ def excess_to_garden_demand(self, excess): Returns: (float): Amount of water actually applied to garden """ - # TODO Anything more than this needed? - # (yes - population presence if eventually included!) + # TODO Anything more than this needed? (yes - population presence if eventually + # included!) return excess * self.gardening_efficiency @@ -271,15 +272,15 @@ def get_house_demand(self): Returns: (dict): A VQIP containg foul water """ - # TODO water that is consumed but not sent onwards as foul - # Total water required + # TODO water that is consumed but not sent onwards as foul Total water required consumption = self.population * self.per_capita # Apply pollutants foul = self.copy_vqip(self.pollutant_load) # Scale to population for pol in constants.ADDITIVE_POLLUTANTS: foul[pol] *= self.population - # Update volume and temperature (which is weighted based on air temperature and constant_temp) + # Update volume and temperature (which is weighted based on air temperature and + # constant_temp) foul["volume"] = consumption foul["temperature"] = ( self.get_data_input("temperature") * (1 - self.constant_weighting) diff --git a/wsimod/nodes/land.py b/wsimod/nodes/land.py index 8d99b811..f34fb750 100644 --- a/wsimod/nodes/land.py +++ b/wsimod/nodes/land.py @@ -37,32 +37,31 @@ def __init__( surfaces) Args: - name (str): node name. - subsurface_residence_time (float, optional): Residence time for + name (str): node name. subsurface_residence_time (float, optional): + Residence time for subsurface flow (see nodes.py/ResidenceTank). Defaults to 5. percolation_residence_time (int, optional): Residence time for percolation flow (see nodes.py/ResidenceTank). Defaults to 50. surface_residence_time (int, optional): Residence time for surface flow (see nodes.py/ResidenceTank). Defaults to 1. surfaces (list, optional): list of dicts where each dict describes the - parameters of each surface in the Land node. Each dict also contains - an entry under 'type_' which describes which subclass of surface to - use. Defaults to []. + parameters of each surface in the Land node. Each dict also contains an + entry under 'type_' which describes which subclass of surface to use. + Defaults to []. data_input_dict (dict, optional): Dictionary of data inputs relevant for the node (generally, et0, precipitation and temperature). Keys are tuples where first value is the name of the variable to read from the dict and the second value is the time. Defaults to {}. Functions intended to call in orchestration: - run - apply_irrigation (if used) + run apply_irrigation (if used) Key assumptions: - Percolation, surface runoff, and subsurface runoff, can be described with a residence-time method. - Flows to percolation, surface runoff, and subsurface runoff are - generated by different hydrological response units (subclasses - of `land.py/Surface`), but aggregated for a given land node. + generated by different hydrological response units (subclasses of + `land.py/Surface`), but aggregated for a given land node. - Flows to percolation are distributed to `storage.py/Groundwater` nodes while surface/subsurface runoff to `nodes.py/Node` or `storage.py/River` nodes. @@ -73,8 +72,7 @@ def __init__( Input data and parameter requirements: - Precipitation and evapotranspiration are in the `data_input_dict` - at the model timestep. - _Units_: metres/timestep + at the model timestep. _Units_: metres/timestep - Temperature in the `data_input_dict` at the model timestep. _Units_: C - Residence time of surface, subsurface and percolation flows. @@ -88,7 +86,8 @@ def __init__( super().__init__(name, data_input_dict=data_input_dict) - # This could be a deny but then you would have to know in advance whether a demand node has any gardening or not + # This could be a deny but then you would have to know in advance whether a + # demand node has any gardening or not self.push_check_handler[("Demand", "Garden")] = lambda x: self.empty_vqip() self.push_set_handler[("Demand", "Garden")] = lambda x: self.empty_vqip() @@ -131,9 +130,9 @@ def __init__( self.push_check_handler["default"] = self.push_check_deny self.push_set_handler["Sewer"] = self.push_set_sewer - # Create subsurface runoff, surface runoff and percolation tanks - # Can also do as timearea if this seems dodge (that is how it is done in IHACRES) - # TODO should these be decayresidencetanks? + # Create subsurface runoff, surface runoff and percolation tanks Can also do as + # timearea if this seems dodge (that is how it is done in IHACRES) TODO should + # these be decayresidencetanks? self.subsurface_runoff = ResidenceTank( residence_time=self.subsurface_residence_time, capacity=constants.UNBOUNDED_CAPACITY, @@ -224,7 +223,10 @@ def push_set_sewer(self, vqip): Returns: vqip (dict): A VQIP amount of water that was not received """ - # TODO currently just push to the first impervious surface... not sure if people will be having multiple impervious surfaces. If people would be only having one then it would make sense to store as a parameter... this is probably fine for now + # TODO currently just push to the first impervious surface... not sure if people + # will be having multiple impervious surfaces. If people would be only having + # one then it would make sense to store as a parameter... this is probably fine + # for now for surface in self.surfaces: if isinstance(surface, ImperviousSurface): vqip = self.surface.push_storage(vqip, force=True) @@ -289,55 +291,57 @@ def __init__( in the run function, which is called by the run function in Land. The lists must return any model inflows or outflows as a VQIP for mass balance checking. - If a user wishes the DecayTank portion to be active, then can provide - 'decays', which are passed upwards (see wsimod/core/core.py/DecayObj for - documentation) + If a user wishes the DecayTank portion to be active, then can provide 'decays', + which are passed upwards (see wsimod/core/core.py/DecayObj for documentation) Args: surface (str, optional): String description of the surface type. Doesn't serve a modelling purpose, just used for user reference. Defaults to ''. - area (float, optional): Area of surface. Defaults to 1. - depth (float, optional): Depth of tank (this has different physical + area (float, optional): Area of surface. Defaults to 1. depth (float, + optional): Depth of tank (this has different physical implications for different subclasses). Defaults to 1. data_input_dict (dict, optional): Dictionary of data inputs relevant for - the surface (generally, deposition). Keys are tuples where first - value is the name of the variable to read from the dict and the - second value is the time. Note that this input should be specific to - the surface, and is not intended to be the same data input as for the - land node. Also note that with each surface having its own timeseries - of data inputs, this can take up a lot of memory, thus the default - behavior is to have this as monthly data where the time variable is a - monthyear. Defaults to {}. + the surface (generally, deposition). Keys are tuples where first value + is the name of the variable to read from the dict and the second value + is the time. Note that this input should be specific to the surface, and + is not intended to be the same data input as for the land node. Also + note that with each surface having its own timeseries of data inputs, + this can take up a lot of memory, thus the default behavior is to have + this as monthly data where the time variable is a monthyear. Defaults to + {}. pollutant_load (dict, optional): A dict of different pollutant amounts that are deposited on the surface (units are mass per area per timestep). Defaults to {}. Key assumptions: - - Generic `Surface` that reads data and can apply simple forms of pollution deposition. + - Generic `Surface` that reads data and can apply simple forms of pollution + deposition. - Formulated as a `Tank` object. - Ammonia->Nitrite->Nitrate decay takes place if parameters describing this - process are provided in `decays` (see `core.py/DecayObj` for transformation - details). + process are provided in `decays` (see `core.py/DecayObj` for + transformation details). Input data and parameter requirements: - `data_input_dict` can contain a variety of pollutant deposition data. - `srp-dry` describes phosphate. `noy-dry` describes nitrogen as - nitrates. `nhx-dry` describes nitrogen as ammonia. `srp/noy/ - nhx-wet` can also be used to specify wet deposition. - _Units_: kg/m2/timestep (data is read at a monthly timestep) + `srp-dry` describes phosphate. `noy-dry` describes nitrogen as nitrates. + `nhx-dry` describes nitrogen as ammonia. `srp/noy/ nhx-wet` can also be + used to specify wet deposition. _Units_: kg/m2/timestep (data is read at + a monthly timestep) """ # Assign parameters self.depth = depth self.data_input_dict = data_input_dict self.surface = surface self.pollutant_load = pollutant_load - # TODO this is a decaytank but growing surfaces don't have decay parameters... is it a problem.. we don't even take decays as an explicit argument and insert them in kwargs.. + # TODO this is a decaytank but growing surfaces don't have decay parameters... + # is it a problem.. we don't even take decays as an explicit argument and insert + # them in kwargs.. capacity = area * depth # Parameters super().__init__(capacity=capacity, area=area, **kwargs) - # Populate function lists - # TODO.. not sure why I have deposition but no precipitation here + # Populate function lists TODO.. not sure why I have deposition but no + # precipitation here if "nhx-dry" in set(x[0] for x in data_input_dict.keys()): self.inflows = [self.atmospheric_deposition, self.precipitation_deposition] else: @@ -350,11 +354,12 @@ def __init__( def run(self): """Call run function (called from Land node).""" if "nitrite" in constants.POLLUTANTS: - # Assume that if nitrite is modelled then nitrification is also modelled - # You will need ammonia->nitrite->nitrate decay to accurate simulate ammonia + # Assume that if nitrite is modelled then nitrification is also modelled You + # will need ammonia->nitrite->nitrate decay to accurate simulate ammonia # Thus, these decays are simulated here - # NOTE decay in a decaytank happens at start of timestep (confusingly) in the end_timestep function + # NOTE decay in a decaytank happens at start of timestep (confusingly) in + # the end_timestep function self.storage["nitrate"] += self.total_decayed["nitrite"] self.parent.running_inflow_mb["nitrate"] += self.total_decayed["nitrite"] @@ -457,7 +462,8 @@ def atmospheric_deposition(self): (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. """ - # TODO double check units in preprocessing - is weight of N or weight of NHX/noy? + # TODO double check units in preprocessing - is weight of N or weight of + # NHX/noy? # Read data and scale nhx = self.get_data_input_surface("nhx-dry") * self.area @@ -581,8 +587,8 @@ def precipitation_evaporation(self): net_precipitation *= self.area net_precipitation = self.v_change_vqip(self.empty_vqip(), net_precipitation) - # Assign a temperature value - # TODO how hot is rain? No idea... just going to use surface air temperature + # Assign a temperature value TODO how hot is rain? No idea... just going to + # use surface air temperature net_precipitation["temperature"] = self.get_data_input("temperature") # Update tank @@ -608,13 +614,15 @@ def push_to_sewers(self): # Get runoff surface_runoff = self.pull_ponded() - # Distribute - # TODO in cwsd_partition this is done with timearea + # Distribute TODO in cwsd_partition this is done with timearea reply = self.parent.push_distributed(surface_runoff, of_type=["Sewer"]) - # Update tank (forcing, because if the water can't go to the sewer, where else can it go) + # Update tank (forcing, because if the water can't go to the sewer, where else + # can it go) _ = self.push_storage(reply, force=True) - # TODO... possibly this could flow to attached river or land nodes.. or other surfaces? I expect this doesn't matter for large scale models.. but may need to be revisited for detailed sewer models + # TODO... possibly this could flow to attached river or land nodes.. or other + # surfaces? I expect this doesn't matter for large scale models.. but may need + # to be revisited for detailed sewer models # Return empty mass balance because outflows are handled by parent return (self.empty_vqip(), self.empty_vqip()) @@ -647,9 +655,9 @@ def __init__( (i.e., when water content in the soil tank is above this value - flows of any kind can be generated). Defaults to 0.3. wilting_point (float, optional): The wilting point IHACRES parameter (i.e., - when water content content in the soil tank is above this value - - plants can uptake water and evaporation from the soil tank can occur). - Defaults to 0.12. + when water content content in the soil tank is above this value - plants + can uptake water and evaporation from the soil tank can occur). Defaults + to 0.12. infiltration_capacity (float, optional): Depth of water per day that can enter the soil tank. Non infiltrated water will pond and travel as surface runoff from the parent Land node. Defaults to 0.5. @@ -710,7 +718,8 @@ def __init__( self.soil_temp_w_deep = 0.1 # deep soil temperature weighting self.soil_temp_deep = 10 # deep soil temperature - # IHACRES is a deficit not a tank, so doesn't really have a capacity in this way... and if it did.. I don't know if it would be the root depth + # IHACRES is a deficit not a tank, so doesn't really have a capacity in this + # way... and if it did.. I don't know if it would be the root depth super().__init__(depth=depth * total_porosity, **kwargs) # Calculate subsurface coefficient @@ -732,8 +741,9 @@ def __init__( self.calculate_soil_temperature ) # Calculate soil temp + dependence factor - # self.processes.append(self.decay) #apply generic decay (currently handled by decaytank at end of timestep) - # TODO decaytank uses air temperature not soil temperature... probably need to just give it the decay function + # self.processes.append(self.decay) #apply generic decay (currently handled by + # decaytank at end of timestep) TODO decaytank uses air temperature not soil + # temperature... probably need to just give it the decay function self.outflows.append(self.route) @@ -784,7 +794,8 @@ def ihacres(self): # Get current moisture deficit current_moisture_deficit_depth = self.get_cmd() - # IHACRES equations (we do (depth - wilting_point_m or field capacity) to convert from a deficit to storage tank) + # IHACRES equations (we do (depth - wilting_point_m or field capacity) to + # convert from a deficit to storage tank) evaporation = evaporation_depth * min( 1, exp( @@ -805,10 +816,12 @@ def ihacres(self): ) ) - # Can't evaporate more than available moisture (presumably the IHACRES equation prevents this ever being needed) + # Can't evaporate more than available moisture (presumably the IHACRES equation + # prevents this ever being needed) evaporation = min(evaporation, precipitation_depth + self.get_smc()) - # Scale to volumes and apply proportions to work out percolation/surface runoff/subsurface runoff + # Scale to volumes and apply proportions to work out percolation/surface + # runoff/subsurface runoff surface = outflow * self.surface_coefficient * self.area percolation = ( outflow @@ -871,7 +884,8 @@ def ihacres(self): ) ) - # TODO saturation excess (think it should just be 'pull_ponded' presumably in net effective precipitation? ) + # TODO saturation excess (think it should just be 'pull_ponded' presumably in + # net effective precipitation? ) # Convert to VQIPs infiltration_excess = self.v_change_vqip(self.empty_vqip(), infiltration_excess) @@ -946,8 +960,8 @@ def __init__( """Extensive surface subclass that implements the CatchWat equations (Liu, Dobson & Mijic (2022) Science of the total environment), which in turn are primarily based on FAO document: - https://www.fao.org/3/x0490e/x0490e0ehtm#soil%20water%20availability. - This surface is a pervious surface that also has things that grow on it. This + https://www.fao.org/3/x0490e/x0490e0ehtm#soil%20water%20availability. This + surface is a pervious surface that also has things that grow on it. This behaviour includes soil nutrient pools, crop planting/harvest calendars, erosion, crop behaviour. @@ -956,18 +970,19 @@ def __init__( nitrogen in different states and performs transformations that occur in the phosphorus/nitrogen cycle. It is assumed that the phosphate/nitrate/nitrite/ ammonia amounts in this Surface tank should track the dissolved inorganic pool - in the nutrient pool. Meanwhile, the org-phosphorus/org-nitrogen amounts in - this tank should track the dissolved organic pool in the nutrient pool. The - total amount of pollutants that enter this tank may not be the same as the - total amount that leave, because pollutants are transformed between inorganic/ - organic and between wet/dry states - these transformations are accounted for - in mass balance. + in the nutrient pool. Meanwhile, the org-phosphorus/org-nitrogen amounts in this + tank should track the dissolved organic pool in the nutrient pool. The total + amount of pollutants that enter this tank may not be the same as the total + amount that leave, because pollutants are transformed between inorganic/ organic + and between wet/dry states - these transformations are accounted for in mass + balance. For users to quickly enable/disable these nutrient processes, which are computationally intensive (in current case studies they account for about half - of the total runtime), they are only active if 'nitrate' is one of the - modelled pollutants. Note that the code will not check if nitrite/phosphate/ - org-phosphorus/org-nitrogen/ammonia are also included, but they should be if nitrate is included and otherwise the code will crash with a key error. + of the total runtime), they are only active if 'nitrate' is one of the modelled + pollutants. Note that the code will not check if nitrite/phosphate/ + org-phosphorus/org-nitrogen/ammonia are also included, but they should be if + nitrate is included and otherwise the code will crash with a key error. Args: rooting_depth (float, optional): Depth of the soil tank (i.e., how deep do @@ -980,37 +995,44 @@ def __init__( This list shows changing crop factor at different times of year in relation to crop_factor_stage_dates. See wsimod/preprocessing/ england_data_formatting.py/format_surfaces for further details on - formulating these - since the interpolation used to find crop_factors - in between the given values in the list is a bit involved. Defaults to + formulating these - since the interpolation used to find crop_factors in + between the given values in the list is a bit involved. Defaults to [1,1]. crop_factor_stage_dates (list, optional): Dates associated with crop_factor_stages. Defaults to [0, 365]. sowing_day (int, optional): day of year that crops are sown. Defaults to 1. harvest_day (int, optional): day of year that crops are harvest. Defaults to 365. - initial_soil_storage (dict or float, optional): Initial mass of solid pollutants + initial_soil_storage (dict or float, optional): Initial mass of solid + pollutants in the soil nutrient pools (fast and adsorbed inorganic pools) Key assumptions: - - In the soil water module, crop stages and crop coefficients control the evapotranspiration. - - Fertiliser and manure application are the major source of soil nutrients, which are added - into soil nutrient pools, including dissovled inorganic, dissolved organic, fast and humus - for both nitrogen and phosphorus. - - Nutrient transformation processes in soil are simulated, including fluxes between the soil - nutrient pools, denitrification for nitrogen, adsorption/desorption for phosphorus. These - processes are affected by temperature and soil moisture. - - Crop uptake of nutrients are simulated based on crop stages, which is different for spring-sown + - In the soil water module, crop stages and crop coefficients control the + evapotranspiration. + - Fertiliser and manure application are the major source of soil nutrients, + which are added + into soil nutrient pools, including dissovled inorganic, dissolved + organic, fast and humus for both nitrogen and phosphorus. + - Nutrient transformation processes in soil are simulated, including fluxes + between the soil + nutrient pools, denitrification for nitrogen, adsorption/desorption for + phosphorus. These processes are affected by temperature and soil + moisture. + - Crop uptake of nutrients are simulated based on crop stages, which is + different for spring-sown and autumn-sown crops. - - Soil erosion from the growing surface is simulated as one of the major sources of suspended solids - in rivers, which is mainly affected by rainfall energy and crop/ground cover. Phosphorus - will also be eroded along with the soil particles, in both adsorbed inorganic and - humus form. + - Soil erosion from the growing surface is simulated as one of the major + sources of suspended solids + in rivers, which is mainly affected by rainfall energy and crop/ground + cover. Phosphorus will also be eroded along with the soil particles, in + both adsorbed inorganic and humus form. Input data and parameter requirements: - `data_input_dict` can contain a variety of pollutant deposition data. - `srp-fertiliser` describes phosphate. `noy-fertiliser` describes nitrogen as - nitrates. `nhx-fertiliser` describes nitrogen as ammonia. `srp/noy/ - nhx-manure` can also be used to specify manure application. + `srp-fertiliser` describes phosphate. `noy-fertiliser` describes + nitrogen as nitrates. `nhx-fertiliser` describes nitrogen as ammonia. + `srp/noy/ nhx-manure` can also be used to specify manure application. _Units_: kg/m2/timestep (data is read at a monthly timestep) - Rooting depth. _Units_: m @@ -1036,7 +1058,8 @@ def __init__( # Crop parameters self.crop_cover_max = 0.9 # [-] 0~1 self.ground_cover_max = 0.3 # [-] - # TODO... really I should just have this as an annual profile parameter and do away with interpolation etc. + # TODO... really I should just have this as an annual profile parameter and do + # away with interpolation etc. self.crop_factor_stages = crop_factor_stages self.crop_factor_stage_dates = crop_factor_stage_dates self.sowing_day = sowing_day @@ -1046,7 +1069,8 @@ def __init__( self.satact = 0.6 # [-] for calculating soil_moisture_dependence_factor self.thetaupp = 0.12 # [-] for calculating soil_moisture_dependence_factor self.thetalow = 0.08 # [-] for calculating soil_moisture_dependence_factor - self.thetapow = 1 # [-] for calculating soil_moisture_dependence_factorself.satact = 0.6 # [-] for calculating soil_moisture_dependence_factor + self.thetapow = 1 # [-] for calculating soil_moisture_dependence_factorself. + # satact = 0.6 # [-] for calculating soil_moisture_dependence_factor # Crop uptake parameters self.uptake1 = ( @@ -1068,12 +1092,15 @@ def __init__( self.srfilt = ( 0.7 # [-] ratio of eroded sediment left in surface runoff after filtration ) - self.macrofilt = 0.01 # [-] ratio of eroded sediment left in subsurface flow after filtration + self.macrofilt = 0.01 # [-] ratio of eroded sediment left in subsurface flow + # after filtration # Denitrification parameters self.limpar = 0.7 # [-] above which denitrification begins - self.exppar = 2.5 # [-] exponential parameter for soil_moisture_dependence_factor_exp calculation - self.hsatINs = 1 # [mg/l] for calculation of half-saturation concentration dependence factor + self.exppar = 2.5 # [-] exponential parameter for + # soil_moisture_dependence_factor_exp calculation + self.hsatINs = 1 # [mg/l] for calculation of half-saturation concentration + # dependence factor self.denpar = 0.015 # [-] denitrification rate coefficient # Adsorption parameters @@ -1239,9 +1266,8 @@ def quick_interp(self, x, xp, yp): """A simple version of np.interp to intepolate crop information on the fly. Args: - x (int): Current time (i.e., day of year) - xp (list): Predefined times (i.e., list of days of year) - yp (list): Predefined values associated with xp + x (int): Current time (i.e., day of year) xp (list): Predefined times (i.e., + list of days of year) yp (list): Predefined values associated with xp Returns: y (float): Interpolated value for current time @@ -1292,7 +1318,8 @@ def calc_crop_cover(self): doy, self.crop_factor_stage_dates, self.crop_factor_stages ) if self.days_after_sow: - # Move outside of this if, if you want nonzero crop/ground cover outside of season + # Move outside of this if, if you want nonzero crop/ground cover outside of + # season self.crop_cover = self.quick_interp( doy, self.harvest_sow_calendar, self.crop_cover_stages ) @@ -1317,11 +1344,10 @@ def calc_crop_cover(self): def adjust_vqip_to_liquid(self, vqip, deposition, in_): """Function to interoperate between surface tank and nutrient pool. Most depositions are given in terms of ammonia/nitrate/phosphate - they are then - aggregated to total N or P to enter the nutrient pools. Depending on the - source of deposition these may transform (e.g., some go to dissolved and some - to solids) upon entering the nutrient pool. To reflect these transformations - in the soil tank, the amounts entering the soil tank are adjusted - proportionately. + aggregated to total N or P to enter the nutrient pools. Depending on the source + of deposition these may transform (e.g., some go to dissolved and some to + solids) upon entering the nutrient pool. To reflect these transformations in the + soil tank, the amounts entering the soil tank are adjusted proportionately. Args: vqip (dict): A VQIP amount of pollutants originally intended to enter the @@ -1497,8 +1523,8 @@ def soil_pool_transformation(self): in_ = self.empty_vqip() out_ = self.empty_vqip() - # Get proportion of nitrogen that is nitrate in the soil tank - # NOTE ignores nitrite - couldn't find enough information on it + # Get proportion of nitrogen that is nitrate in the soil tank NOTE ignores + # nitrite - couldn't find enough information on it nitrate_proportion = self.storage["nitrate"] / ( self.storage["nitrate"] + self.storage["ammonia"] ) @@ -1509,8 +1535,8 @@ def soil_pool_transformation(self): increase_in_dissolved_organic, ) = self.nutrient_pool.soil_pool_transformation() - # Update tank and mass balance - # TODO .. there is definitely a neater way to write this + # Update tank and mass balance TODO .. there is definitely a neater way to write + # this if increase_in_dissolved_inorganic["N"] > 0: # Increase in inorganic nitrogen, rescale back to nitrate and ammonia in_["nitrate"] = increase_in_dissolved_inorganic["N"] * nitrate_proportion @@ -1677,7 +1703,8 @@ def erosion(self): (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. """ - # Parameters/equations from HYPE documentation (which explains why my documentation is a bit ambiguous - because theirs is too) + # Parameters/equations from HYPE documentation (which explains why my + # documentation is a bit ambiguous - because theirs is too) # Convert precipitation to MM since all the equations assume that precipitation_depth = self.get_data_input("precipitation") * constants.M_TO_MM @@ -1712,8 +1739,8 @@ def erosion(self): else: mobilised_flow = 0 - # Sum flows (not sure why surface runoff isn't included) - # TODO I'm pretty sure it should be included here + # Sum flows (not sure why surface runoff isn't included) TODO I'm pretty sure it + # should be included here total_flows = ( self.infiltration_excess["volume"] + self.subsurface_flow["volume"] @@ -1730,7 +1757,8 @@ def erosion(self): ) # [kg/km2] # TODO not sure what conversion this HYPE 1000 is referring to - # soil erosion with adsorbed inorganic phosphorus and humus phosphorus (erodedP as P in eroded sediments and effect of enrichment) + # soil erosion with adsorbed inorganic phosphorus and humus phosphorus (erodedP + # as P in eroded sediments and effect of enrichment) if erodingflow > 4: enrichment = 1.5 elif erodingflow > 0: @@ -1806,7 +1834,8 @@ def erosion(self): percolation_erodedP * org_removed / eff_erodedP ) - # TODO Leon reckons this is conceptually dodgy.. but i'm not sure where else adsorbed inorganic phosphorus should go + # TODO Leon reckons this is conceptually dodgy.. but i'm not sure where else + # adsorbed inorganic phosphorus should go self.infiltration_excess["phosphate"] += ( surface_erodedP * inorg_removed / eff_erodedP ) @@ -1817,7 +1846,9 @@ def erosion(self): percolation_erodedP * inorg_removed / eff_erodedP ) - # Entering the model (no need to uptake surface tank because both adsorbed inorganic pool and humus pool are solids and so no tracked in the soil water tank) + # Entering the model (no need to uptake surface tank because both adsorbed + # inorganic pool and humus pool are solids and so no tracked in the soil + # water tank) in_["phosphate"] = inorg_removed in_["org-phosphorus"] = org_removed else: @@ -1840,9 +1871,8 @@ def denitrification(self): (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. """ - # Parameters/equations from HYPE documentation - # TODO could more of this be moved to NutrientPool - # Calculate soil moisture dependence of denitrification + # Parameters/equations from HYPE documentation TODO could more of this be moved + # to NutrientPool Calculate soil moisture dependence of denitrification soil_moisture_content = self.get_smc() if soil_moisture_content > self.field_capacity_m: denitrifying_soil_moisture_dependence = 1 @@ -1879,7 +1909,8 @@ def denitrification(self): denitrified_request ) - # Leon reckons this should leave the model (though I think technically some small amount goes to nitrite) + # Leon reckons this should leave the model (though I think technically some + # small amount goes to nitrite) out_ = self.empty_vqip() out_["nitrate"] = denitrified_N["N"] @@ -1898,8 +1929,8 @@ def adsorption(self): (tuple): A tuple containing a VQIP amount for model inputs and outputs for mass balance checking. """ - # Parameters/equations from HYPE documentation - # TODO could this be moved to the nutrient pool? + # Parameters/equations from HYPE documentation TODO could this be moved to the + # nutrient pool? # Initialise mass balance checking in_ = self.empty_vqip() @@ -1966,7 +1997,8 @@ def adsorption(self): if abs(ad_P_equi_conc - conc_sol) > 1e-6: request = self.nutrient_pool.get_empty_nutrient() - # TODO not sure about this if statement, surely it would be triggered every time + # TODO not sure about this if statement, surely it would be triggered every + # time adsdes = (ad_P_equi_conc - conc_sol) * ( 1 - exp(-self.kadsdes) ) # kinetic adsorption/desorption @@ -2065,7 +2097,8 @@ def __init__(self, irrigation_coefficient=0.1, **kwargs): proportion of demand met. Defaults to 0.1. """ # Assign param - self.irrigation_coefficient = irrigation_coefficient # proportion area irrigated * proportion of demand met + self.irrigation_coefficient = irrigation_coefficient # proportion area + # irrigated * proportion of demand met super().__init__(**kwargs) @@ -2109,7 +2142,8 @@ def irrigate(self): class GardenSurface(GrowingSurface): """""" - # TODO - probably a simplier version of this is useful, building just on pervioussurface + # TODO - probably a simplier version of this is useful, building just on + # pervioussurface def __init__(self, **kwargs): """A specific surface for gardens that treats the garden as a grass crop, but that can calculate/receive irrigation through functions that are assigned by the diff --git a/wsimod/nodes/nutrient_pool.py b/wsimod/nodes/nutrient_pool.py index 195b3436..54590db3 100644 --- a/wsimod/nodes/nutrient_pool.py +++ b/wsimod/nodes/nutrient_pool.py @@ -26,54 +26,74 @@ def __init__( of nutrients. Equations and parameters are based on HYPE. Args: - fraction_dry_n_to_dissolved_inorganic (float, optional): fraction of dry nitrogen deposition going into the soil dissolved inorganic nitrogen pool, with the rest added to the fast pool. Defaults to 0.9. - degrhpar (dict, optional): reference humus degradation rate (fraction of humus pool to fast pool). Defaults to {'N' : 7 * 1e-5, 'P' : 7 * 1e-6}. - dishpar (dict, optional): reference humus dissolution rate (fraction of humus pool to dissolved organic pool). Defaults to {'N' : 7 * 1e-5, 'P' : 7 * 1e-6}. - minfpar (dict, optional): reference fast pool mineralisation rate (fraction of fast pool to dissolved inorganic pool). Defaults to {'N' : 0.00013, 'P' : 0.000003}. - disfpar (dict, optional): reference fast pool dissolution rate (fraction of fast pool to dissolved organic pool). Defaults to {'N' : 0.000003, 'P' : 0.0000001}. - immobdpar (dict, optional): reference immobilisation rate (fraction of dissolved inorganic pool to fast pool). Defaults to {'N' : 0.0056, 'P' : 0.2866}. - fraction_manure_to_dissolved_inorganic (dict, optional): fraction of nutrients from applied manure to dissolved inorganic pool, with the rest added to the fast pool. Defaults to {'N' : 0.5, 'P' : 0.1}. - fraction_residue_to_fast (dict, optional): fraction of nutrients from residue to fast pool, with the rest added to the humus pool. Defaults to {'N' : 0.1, 'P' : 0.1}. + fraction_dry_n_to_dissolved_inorganic (float, optional): fraction of dry + nitrogen deposition going into the soil dissolved inorganic nitrogen pool, + with the rest added to the fast pool. Defaults to 0.9. degrhpar (dict, + optional): reference humus degradation rate (fraction of humus pool to fast + pool). Defaults to {'N' : 7 * 1e-5, 'P' : 7 * 1e-6}. dishpar (dict, + optional): reference humus dissolution rate (fraction of humus pool to + dissolved organic pool). Defaults to {'N' : 7 * 1e-5, 'P' : 7 * 1e-6}. + minfpar (dict, optional): reference fast pool mineralisation rate (fraction + of fast pool to dissolved inorganic pool). Defaults to {'N' : 0.00013, 'P' : + 0.000003}. disfpar (dict, optional): reference fast pool dissolution rate + (fraction of fast pool to dissolved organic pool). Defaults to {'N' : + 0.000003, 'P' : 0.0000001}. immobdpar (dict, optional): reference + immobilisation rate (fraction of dissolved inorganic pool to fast pool). + Defaults to {'N' : 0.0056, 'P' : 0.2866}. + fraction_manure_to_dissolved_inorganic (dict, optional): fraction of + nutrients from applied manure to dissolved inorganic pool, with the rest + added to the fast pool. Defaults to {'N' : 0.5, 'P' : 0.1}. + fraction_residue_to_fast (dict, optional): fraction of nutrients from + residue to fast pool, with the rest added to the humus pool. Defaults to + {'N' : 0.1, 'P' : 0.1}. Key assumptions: - Four nutrient pools are conceptualised for both nitrogen and phosphorus in soil, which includes humus pool, fast pool, dissolved inorganic pool, - and dissolved organic pool. Humus and fast pool represent immobile pool of - organic nutrients in the soil with slow and fast turnover, respectively. - Dissolved inorganic and organic pool represent nutrients in dissolved phase - in soil water (for phosphorus, dissolved organic pool might contain particulate - phase). Given that phoshphorus can be adsorbed and attached to soil particles, - an adsorbed inorganic pool is created specifically for phosphorus. + and dissolved organic pool. Humus and fast pool represent immobile pool + of organic nutrients in the soil with slow and fast turnover, + respectively. Dissolved inorganic and organic pool represent nutrients + in dissolved phase in soil water (for phosphorus, dissolved organic pool + might contain particulate phase). Given that phoshphorus can be adsorbed + and attached to soil particles, an adsorbed inorganic pool is created + specifically for phosphorus. - The major sources of nutrients to soil are conceptualised as - atmospheric deposition: - dry deposition: - - for nitrogen, inorganic fraction of dry deposition is added to the dissovled + - for nitrogen, inorganic fraction of dry deposition is added to + the dissovled inorganic pool, while the rest is added to the fast pool; - for phosphorus, all is added to adsorbed inorganic pool. - wet deposition: all is added to the dissolved inorganic pool. - fertilisers: all added to the dissolved inorganic pool. - - manure: the inorganic fraction is added to the dissovled inorganic pool, with + - manure: the inorganic fraction is added to the dissovled inorganic + pool, with the rest added to the fast pool. - - residue: the part with fast turnover is added to the fast pool, with the rest + - residue: the part with fast turnover is added to the fast pool, with + the rest added to the humus pool. - - Nutrient fluxes between these pools are simulated to represent the biochemical processes - that can transform the nutrients between different forms. These processes include - - degradation of humus pool to fast pool - - dissolution of humus pool to dissovled organic pool - - mineralisation of fast pool to dissolved inorganic pool - - dissolution of fast pool to dissolved organic pool - - immobilisation of dissolved inroganic pool to fast pool - The rate of these processes are affected by the soil temperature and moisture conditions. - - When soil erosion happens, a portion of both the adsorbed inorganic pool and humus pool + - Nutrient fluxes between these pools are simulated to represent the + biochemical processes + that can transform the nutrients between different forms. These + processes include - degradation of humus pool to fast pool - dissolution + of humus pool to dissovled organic pool - mineralisation of fast pool to + dissolved inorganic pool - dissolution of fast pool to dissolved organic + pool - immobilisation of dissolved inroganic pool to fast pool The rate + of these processes are affected by the soil temperature and moisture + conditions. + - When soil erosion happens, a portion of both the adsorbed inorganic pool + and humus pool for phosphorus will be eroded as well. Input data and parameter requirements: - - fraction_dry_n_to_dissolved_inorganic, fraction_manure_to_dissolved_inorganic, fraction_residue_to_fast. + - fraction_dry_n_to_dissolved_inorganic, + fraction_manure_to_dissolved_inorganic, fraction_residue_to_fast. _Units_: -, all should in [0-1] - degrhpar, dishpar, minfpar, disfpar, immobdpar. _Units_: -, all should in [0-1] """ - # TODO I don't think anyone will change most of these params... they could maybe just be set here + # TODO I don't think anyone will change most of these params... they could maybe + # just be set here self.init_empty() # Assign parameters @@ -153,8 +173,8 @@ def allocate_organic_irrigation(self, irrigation): Returns: irrigation (dict): irrigation above, because no transformations take place - (i.e., dissolved organic is what is received and goes straight into - that pool) + (i.e., dissolved organic is what is received and goes straight into that + pool) """ # Update pool self.dissolved_organic_pool.receive(irrigation) @@ -309,8 +329,8 @@ def erode_P(self, amount_P): amount_P (float): Amount of phosphorus to be eroded Returns: - (float): Amount of phosphorus eroded from the humus pool - (float): Amount of phosphorus eroded from the adsorbed inorganic pool + (float): Amount of phosphorus eroded from the humus pool (float): Amount of + phosphorus eroded from the adsorbed inorganic pool """ # Calculate proportion of adsorbed to be eroded fraction_adsorbed = self.adsorbed_inorganic_pool.storage["P"] / ( @@ -348,7 +368,8 @@ def soil_pool_transformation(self): # Turnover of humus amount = self.temp_soil_process(self.degrhpar, self.humus_pool, self.fast_pool) - # This is solid inorganic to solid organic... no tracking needed since solid nutrients aren't tracked in mass balance of the surface soil water tank! + # This is solid inorganic to solid organic... no tracking needed since solid + # nutrients aren't tracked in mass balance of the surface soil water tank! # Dissolution of humus amount = self.temp_soil_process( @@ -380,7 +401,8 @@ def soil_pool_transformation(self): ) increase_in_dissolved_inorganic = self.subtract_nutrients( increase_in_dissolved_inorganic, amount - ) # TODO will a negative value affect the consequent processes in growing surface? + ) # TODO will a negative value affect the consequent processes in growing + # surface? return increase_in_dissolved_inorganic, increase_in_dissolved_organic @@ -389,13 +411,15 @@ def temp_soil_process(self, parameter, extract_pool, receive_pool): remove nutrients from the extract pool and update the receive pool. Args: - parameter (dict): A dict containing a parameter for each nutrient for the given process + parameter (dict): A dict containing a parameter for each nutrient for the + given process (units in per timestep) - extract_pool (NutrientStore): The pool to extract from - receive_pool (NutrientStore): The pool to receive extracted nutrients + extract_pool (NutrientStore): The pool to extract from receive_pool + (NutrientStore): The pool to receive extracted nutrients Returns: - to_extract (dict): A dict containing the amount extracted of each nutrient (for mass + to_extract (dict): A dict containing the amount extracted of each nutrient + (for mass balance) """ # Initialise nutrients @@ -425,8 +449,8 @@ def multiply_nutrients(self, nutrient, factor): """Multiply nutrients by factors. Args: - nutrient (dict): Dict of nutrients to multiply - factor (dict): Dict of factors to multiply for each nutrient + nutrient (dict): Dict of nutrients to multiply factor (dict): Dict of + factors to multiply for each nutrient Returns: (dict): Multiplied nutrients @@ -447,8 +471,7 @@ def sum_nutrients(self, n1, n2): """Sum two nutrients. Args: - n1 (dict): Dict of nutrients - n2 (dict): Dict of nutrients + n1 (dict): Dict of nutrients n2 (dict): Dict of nutrients Returns: (dict): Summed nutrients @@ -462,8 +485,8 @@ def subtract_nutrients(self, n1, n2): """Subtract two nutrients. Args: - n1 (dict): Dict of nutrients to subtract from - n2 (dict): Dict of nutrients to subtract + n1 (dict): Dict of nutrients to subtract from n2 (dict): Dict of nutrients + to subtract Returns: (dict): subtracted nutrients diff --git a/wsimod/nodes/sewer.py b/wsimod/nodes/sewer.py index 2351218f..af566658 100644 --- a/wsimod/nodes/sewer.py +++ b/wsimod/nodes/sewer.py @@ -69,7 +69,8 @@ def __init__( Input data and parameter requirements: - `pipe_timearea` is a dictionary containing the timearea diagram. _Units_: duration of flow (in timesteps) and proportion of flow - - `pipe_time` describes the travel time of water received from upstream `Sewer` + - `pipe_time` describes the travel time of water received from upstream + `Sewer` objects. _Units_: number of timesteps - `capacity`, `chamber_area`, `chamber_datum` describe the dimensions of the diff --git a/wsimod/nodes/storage.py b/wsimod/nodes/storage.py index b3e856d7..eab68665 100644 --- a/wsimod/nodes/storage.py +++ b/wsimod/nodes/storage.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- """Created on Mon Nov 15 14:20:36 2021. -@author: bdobson -Converted to totals on 2022-05-03 +@author: bdobson Converted to totals on 2022-05-03 """ from math import exp @@ -26,12 +25,12 @@ def __init__( """A Node wrapper for a Tank or DecayTank. Args: - name (str): node name - capacity (float, optional): Tank capacity (see nodes.py/Tank). Defaults to 0. - area (float, optional): Tank area (see nodes.py/Tank). Defaults to 0. - datum (float, optional): Tank datum (see nodes.py/Tank). Defaults to 0. - decays (dict, optional): Tank decays if needed, (see nodes.py/DecayTank). Defaults to None. - initial_storage (float or dict, optional): Initial storage (see nodes.py/Tank). Defaults to 0. + name (str): node name capacity (float, optional): Tank capacity (see + nodes.py/Tank). Defaults to 0. area (float, optional): Tank area (see + nodes.py/Tank). Defaults to 0. datum (float, optional): Tank datum (see + nodes.py/Tank). Defaults to 0. decays (dict, optional): Tank decays if + needed, (see nodes.py/DecayTank). Defaults to None. initial_storage (float + or dict, optional): Initial storage (see nodes.py/Tank). Defaults to 0. Functions intended to call in orchestration: distribute (optional, depends on subclass) @@ -150,9 +149,9 @@ def __init__( `waste.py/Waste` nodes. - Infiltration to `sewer.py/Sewer` nodes occurs when the storage in the tank is greater than a specified threshold, at a rate - proportional to the sqrt of volume above the threshold. (Note, - this behaviour is __not validated__ and a high uncertainty process - in general) + proportional to the sqrt of volume above the threshold. (Note, this + behaviour is __not validated__ and a high uncertainty process in + general) - If `decays` are provided to model water quality transformations, see `core.py/DecayObj`. @@ -160,12 +159,10 @@ def __init__( - Groundwater tank `capacity`, `area`, and `datum`. _Units_: cubic metres, squared metres, metres - Infiltration behaviour determined by an `infiltration_threshold` - and `infiltration_pct`. - _Units_: proportion of capacity + and `infiltration_pct`. _Units_: proportion of capacity - Optional dictionary of decays with pollutants as keys and decay - parameters (a constant and a temperature sensitivity exponent) - as values. - _Units_: - + parameters (a constant and a temperature sensitivity exponent) as + values. _Units_: - """ self.residence_time = residence_time self.infiltration_threshold = infiltration_threshold @@ -213,10 +210,10 @@ def __init__(self, timearea={0: 1}, data_input_dict={}, **kwargs): Args: timearea (dict, optional): Time area diagram that enables flows to - take a range of different durations to 'traverse' the tank. The keys - of the dict are the number of timesteps while the values are the - proportion of flow. E.g., {0 : 0.7, 1 : 0.3} means 70% of flow takes - 0 timesteps and 30% takes 1 timesteps. Defaults to {0 : 1}. + take a range of different durations to 'traverse' the tank. The keys of + the dict are the number of timesteps while the values are the proportion + of flow. E.g., {0 : 0.7, 1 : 0.3} means 70% of flow takes 0 timesteps + and 30% takes 1 timesteps. Defaults to {0 : 1}. data_input_dict (dict, optional): Dictionary of data inputs relevant for the node (though I don't think it is used). Defaults to {}. @@ -238,9 +235,8 @@ def __init__(self, timearea={0: 1}, data_input_dict={}, **kwargs): - `timearea` is a dictionary containing the timearea diagram. _Units_: duration of flow (in timesteps) and proportion of flow - Optional dictionary of decays with pollutants as keys and decay - parameters (a constant and a temperature sensitivity exponent) - as values. - _Units_: - + parameters (a constant and a temperature sensitivity exponent) as + values. _Units_: - """ self.timearea = timearea # TODO not used @@ -282,8 +278,8 @@ def push_set_timearea(self, vqip): reply (dict): A VQIP amount that was not successfuly receivesd """ reply = self.empty_vqip() - # Iterate over timearea diagram - # TODO timearea diagram behaviour be generalised across nodes + # Iterate over timearea diagram TODO timearea diagram behaviour be generalised + # across nodes for time, normalised in self.timearea.items(): vqip_ = self.v_change_vqip(vqip, vqip["volume"] * normalised) reply_ = self.tank.push_storage(vqip_, time=time) @@ -374,17 +370,19 @@ def pull_set_active(self, vqip): # Recalculate storage self.tank.storage = self.extract_vqip(self.tank.storage, pulled) return pulled - # elif isinstance(self.tank.internal_arc.queue, list): - # for req in self.tank.internal_arc.queue: - # t_pulled = req['vqtip']['volume'] * total_pull / total_storage - # req['vqtip'] = self.v_change_vqip(req['vqtip'], req['vqtip']['volume'] - t_pulled) - # pulled += t_pulled - # a_pulled = self.tank.active_storage['volume'] * total_pull / total_storage - # self.tank.active_storage = self.v_change_vqip(self.tank.active_storage, self.tank.active_storage['volume'] - a_pulled) - # pulled += a_pulled + # elif isinstance(self.tank.internal_arc.queue, list): for req in + # self.tank.internal_arc.queue: t_pulled = req['vqtip']['volume'] * + # total_pull / total_storage req['vqtip'] = + # self.v_change_vqip(req['vqtip'], req['vqtip']['volume'] - + # t_pulled) pulled += t_pulled a_pulled = + # self.tank.active_storage['volume'] * total_pull / total_storage + # self.tank.active_storage = + # self.v_change_vqip(self.tank.active_storage, + # self.tank.active_storage['volume'] - a_pulled) pulled += a_pulled # #Recalculate storage - doing this differently causes numerical errors - # new_v = sum([x['vqtip']['volume'] for x in self.tank.internal_arc.queue])+ self.tank.active_storage['volume'] + # new_v = sum([x['vqtip']['volume'] for x in + # self.tank.internal_arc.queue])+ self.tank.active_storage['volume'] # self.tank.storage = self.v_change_vqip(self.tank.storage, new_v) # return self.v_change_vqip(self.tank.storage, pulled) @@ -407,11 +405,12 @@ def __init__( """Node that contains extensive in-river biochemical processes. Args: - depth (float, optional): River tank depth. Defaults to 2. - length (float, optional): River tank length. Defaults to 200. - width (float, optional): River tank width. Defaults to 20. - velocity (float, optional): River velocity (if someone wants to calculate - this on the fly that would also work). Defaults to 0.2*constants.M_S_TO_M_DT. + depth (float, optional): River tank depth. Defaults to 2. length (float, + optional): River tank length. Defaults to 200. width (float, optional): + River tank width. Defaults to 20. velocity (float, optional): River velocity + (if someone wants to calculate + this on the fly that would also work). Defaults to + 0.2*constants.M_S_TO_M_DT. damp (float, optional): Flow delay and attentuation parameter. Defaults to 0.1. mrf (float, optional): Minimum required flow in river (volume per timestep), @@ -422,20 +421,22 @@ def __init__( Key assumptions: - River is conceptualised as a water tank that receives flows from various - sources (e.g., runoffs from urban and rural land, baseflow from groundwater), - interacts with water infrastructure (e.g., abstraction for irrigation and - domestic supply, sewage and treated effluent discharge), and discharges - flows downstream. It has length and width as shape parameters, average - velocity to indicate flow speed and capacity to indicate the maximum storage limit. - - Flows from different sources into rivers will fully mix. River tank is assumed to - have delay and attenuation effects when generate outflows. These effects are - simulated based on the average velocity. - - In-river biochemical processes are simulated as sources/sinks of nutrients - in the river tank, including - - denitrification (for nitrogen) - - phytoplankton absorption/release (for nitrogen and phosphorus) - - macrophyte uptake (for nitrogen and phosphorus) - These processes are affected by river temperature. + sources (e.g., runoffs from urban and rural land, baseflow from + groundwater), interacts with water infrastructure (e.g., abstraction for + irrigation and domestic supply, sewage and treated effluent discharge), + and discharges flows downstream. It has length and width as shape + parameters, average velocity to indicate flow speed and capacity to + indicate the maximum storage limit. + - Flows from different sources into rivers will fully mix. River tank is + assumed to + have delay and attenuation effects when generate outflows. These effects + are simulated based on the average velocity. + - In-river biochemical processes are simulated as sources/sinks of + nutrients + in the river tank, including - denitrification (for nitrogen) - + phytoplankton absorption/release (for nitrogen and phosphorus) - + macrophyte uptake (for nitrogen and phosphorus) These processes are + affected by river temperature. Input data and parameter requirements: - depth, length, width @@ -467,13 +468,14 @@ def __init__( super().__init__(**kwargs) - # TODO check units - # TODO Will a user want to change any of these? - # Wide variety of river parameters (from HYPE) + # TODO check units TODO Will a user want to change any of these? Wide variety of + # river parameters (from HYPE) self.uptake_PNratio = 1 / 7.2 # [-] P:N during crop uptake self.bulk_density = 1300 # [kg/m3] soil density - self.denpar_w = 0.0015 # 0.001, # [kg/m2/day] reference denitrification rate in water course - self.T_wdays = 5 # [days] weighting constant for river temperature calculation (similar to moving average period) + self.denpar_w = 0.0015 # 0.001, # [kg/m2/day] reference denitrification rate + # in water course + self.T_wdays = 5 # [days] weighting constant for river temperature calculation + # (similar to moving average period) self.halfsatINwater = ( 1.5 * constants.MG_L_TO_KG_M3 ) # [kg/m3] half saturation parameter for denitrification in river @@ -506,19 +508,16 @@ def __init__( self.lagged_total_phosphorus = [] self.din_components = ["ammonia", "nitrate"] - # TODO need a cleaner way to do this depending on whether e.g., nitrite is included + # TODO need a cleaner way to do this depending on whether e.g., nitrite is + # included # Initialise paramters self.current_depth = 0 # [m] - # self.river_temperature = 0 # [degree C] - # self.river_denitrification = 0 # [kg/day] - # self.macrophyte_uptake_N = 0 # [kg/day] - # self.macrophyte_uptake_P = 0 # [kg/day] - # self.sediment_particulate_phosphorus_pool = 60000 # [kg] - # self.sediment_pool = 1000000 # [kg] - # self.benthos_source_sink = 0 # [kg/day] - # self.t_res = 0 # [day] - # self.outflow = self.empty_vqip() + # self.river_temperature = 0 # [degree C] self.river_denitrification = 0 # + # [kg/day] self.macrophyte_uptake_N = 0 # [kg/day] self.macrophyte_uptake_P = 0 + # # [kg/day] self.sediment_particulate_phosphorus_pool = 60000 # [kg] + # self.sediment_pool = 1000000 # [kg] self.benthos_source_sink = 0 # [kg/day] + # self.t_res = 0 # [day] self.outflow = self.empty_vqip() # Update end_teimstep self.end_timestep = self.end_timestep_ @@ -540,18 +539,16 @@ def __init__( self.mass_balance_in.append(lambda: self.bio_in) self.mass_balance_out.append(lambda: self.bio_out) - # TODO something like this might be needed if you want sewers backing up from river height... would need to incorporate expected river outflow - # def get_dt_excess(self, vqip = None): - # reply = self.empty_vqip() - # reply['volume'] = self.tank.get_excess()['volume'] + self.tank.get_avail()['volume'] * self.get_riverrc() - # if vqip is not None: - # reply['volume'] = min(vqip['volume'], reply['volume']) - # return reply - - # def push_set_river(self, vqip): - # vqip_ = vqip.copy() - # vqip_ = self.v_change_vqip(vqip_, min(vqip_['volume'], self.get_dt_excess()['volume'])) - # _ = self.tank.push_storage(vqip_, force=True) + # TODO something like this might be needed if you want sewers backing up from river + # height... would need to incorporate expected river outflow def get_dt_excess(self, + # vqip = None): reply = self.empty_vqip() reply['volume'] = + # self.tank.get_excess()['volume'] + self.tank.get_avail()['volume'] * + # self.get_riverrc() if vqip is not None: reply['volume'] = min(vqip['volume'], + # reply['volume']) return reply + + # def push_set_river(self, vqip): vqip_ = vqip.copy() vqip_ = + # self.v_change_vqip(vqip_, min(vqip_['volume'], + # self.get_dt_excess()['volume'])) _ = self.tank.push_storage(vqip_, force=True) # return self.extract_vqip(vqip, vqip_) def pull_check_river(self, vqip=None): @@ -629,7 +626,8 @@ def get_din_pool(self): """ return sum( [self.tank.storage[x] for x in self.din_components] - ) # TODO + self.tank.storage['nitrite'] but nitrite might not be modelled... need some ways to address this + ) # TODO + self.tank.storage['nitrite'] but nitrite might not be modelled... + # need some ways to address this def biochemical_processes(self): """Runs all biochemical processes and updates pollutant amounts. @@ -698,8 +696,7 @@ def biochemical_processes(self): din = self.get_din_pool() - # Calculate moving averages - # TODO generalise + # Calculate moving averages TODO generalise temp_10_day = sum(self.lagged_temperatures[-10:]) / 10 temp_20_day = sum(self.lagged_temperatures[-20:]) / 20 total_phos_365_day = sum(self.lagged_total_phosphorus) / self.max_phosphorus_lag @@ -715,8 +712,7 @@ def biochemical_processes(self): else: totalphosfcn = 0 - # Mineralisation/production - # TODO this feels like it could be much tidier + # Mineralisation/production TODO this feels like it could be much tidier minprodN = ( self.prodNpar * totalphosfcn * tempfcn * self.area * self.current_depth ) # [kg N/day] @@ -772,8 +768,7 @@ def biochemical_processes(self): din = self.get_din_pool() - # macrophyte uptake - # temperature dependence factor + # macrophyte uptake temperature dependence factor tempfcn1 = (max(0, self.tank.storage["temperature"]) / 20) ** 0.3 tempfcn2 = (self.tank.storage["temperature"] - temp_20_day) / 5 tempfcn = max(0, tempfcn1 * tempfcn2) @@ -793,9 +788,7 @@ def biochemical_processes(self): out_["phosphate"] += macrophyte_uptake_P self.tank.storage["phosphate"] -= macrophyte_uptake_P - # TODO - # source/sink for benthos sediment P - # suspension/resuspension + # TODO source/sink for benthos sediment P suspension/resuspension return in_, out_ def get_riverrc(self): @@ -818,8 +811,7 @@ def get_riverrc(self): def calculate_discharge(self): """""" if "nitrate" in constants.POLLUTANTS: - # TODO clumsy - # Run biochemical processes + # TODO clumsy Run biochemical processes in_, out_ = self.biochemical_processes() # Mass balance self.bio_in = in_ @@ -827,8 +819,7 @@ def calculate_discharge(self): def distribute(self): """Run biochemical processes, track mass balance, and distribute water.""" - # self.calculate_discharge() - # Get outflow + # self.calculate_discharge() Get outflow outflow = self.tank.pull_storage( {"volume": self.tank.storage["volume"] * self.get_riverrc()} ) @@ -847,8 +838,7 @@ def pull_check_fp(self, vqip=None): Returns: """ - # TODO Pull checking for riparian buffer, needs updating - # update river depth + # TODO Pull checking for riparian buffer, needs updating update river depth self.update_depth() return self.current_depth, self.area, self.width, self.river_tank.storage @@ -879,9 +869,8 @@ def __init__(self, **kwargs): - Tank `capacity`, `area`, and `datum`. _Units_: cubic metres, squared metres, metres - Optional dictionary of decays with pollutants as keys and decay - parameters (a constant and a temperature sensitivity exponent) - as values. - _Units_: - + parameters (a constant and a temperature sensitivity exponent) as + values. _Units_: - """ super().__init__(**kwargs) @@ -917,8 +906,8 @@ def __init__(self, environmental_flow=0, **kwargs): inflowing arcs. - Reservoir aims to satisfy a static `environmental_flow`. - If tank capacity is exceeded, reservoir spills downstream - towards `nodes.py/Node`, `storage.py/River` or `waste.py/Waste` - nodes. Spill counts towards `environmental_flow`. + towards `nodes.py/Node`, `storage.py/River` or `waste.py/Waste` nodes. + Spill counts towards `environmental_flow`. - Evaporation/precipitation onto surface area currently ignored. - Currently, if a reservoir satisfies a pull from a downstream node, it does __not__ count towards `environmental_flow`. @@ -931,9 +920,8 @@ def __init__(self, environmental_flow=0, **kwargs): - `environmental_flow` _Units_: cubic metres/timestep - Optional dictionary of decays with pollutants as keys and decay - parameters (a constant and a temperature sensitivity exponent) - as values. - _Units_: - + parameters (a constant and a temperature sensitivity exponent) as + values. _Units_: - """ # Parameters self.environmental_flow = environmental_flow @@ -957,13 +945,12 @@ def push_set_river_reservoir(self, vqip): Returns: reply (dict): A VQIP amount that was not successfully received """ - # Apply normal reservoir storage - # We do this under the assumption that spill is mixed in with the reservoir - # If the reservoir can't spill everything you'll get some weird numbers in - # reply, but if your reservoir can't spill as much as you like then you - # should probably be pushing the right amount through push_check_river_reservoir - # Some cunning could probably avoid this by checking vqip, but this is a - # serious edge case + # Apply normal reservoir storage We do this under the assumption that spill is + # mixed in with the reservoir If the reservoir can't spill everything you'll get + # some weird numbers in reply, but if your reservoir can't spill as much as you + # like then you should probably be pushing the right amount through + # push_check_river_reservoir Some cunning could probably avoid this by checking + # vqip, but this is a serious edge case _ = self.tank.push_storage(vqip, force=True) spill = self.tank.pull_ponded() @@ -1003,8 +990,8 @@ def push_check_river_reservoir(self, vqip=None): def satisfy_environmental(self): """Satisfy environmental flow downstream.""" - # Calculate how much environmental flow is yet to satisfy - # #(some may have been already if pull-and-take abstractions have taken place) + # Calculate how much environmental flow is yet to satisfy #(some may have been + # already if pull-and-take abstractions have taken place) to_satisfy = max( self.environmental_flow - self.total_environmental_satisfied, 0 ) diff --git a/wsimod/orchestration/model.py b/wsimod/orchestration/model.py index d79c6523..6aca8ffa 100644 --- a/wsimod/orchestration/model.py +++ b/wsimod/orchestration/model.py @@ -589,7 +589,8 @@ def change_runoff_coefficient(self, relative_change, nodes=None): nodes (list, optional): list of land nodes to change the parameters of. Defaults to None, which applies the change to all land nodes. """ - # Multiplies impervious area by relative change and adjusts grassland accordingly + # Multiplies impervious area by relative change and adjusts grassland + # accordingly if nodes is None: nodes = self.nodes_type["Land"].values() @@ -664,7 +665,8 @@ def run( }, {'element_type' : 'tanks', 'name' : 'my_reservoir', - 'function' : @ (x, model) sum([y['storage'] < (model.nodes['my_reservoir'].tank.capacity / 2) for y in x]) + 'function' : @ (x, model) sum([y['storage'] < (model.nodes + ['my_reservoir'].tank.capacity / 2) for y in x]) }] _, _, results, _ = my_model.run(record_all = False, objectives = objectives) """ @@ -811,7 +813,8 @@ def enablePrint(stdout): largest = max(sys_in[v], sys_in[v], sys_in[v]) if largest > constants.FLOAT_ACCURACY: - # Convert perform comparison in a magnitude to match the largest value + # Convert perform comparison in a magnitude to match the largest + # value magnitude = 10 ** int(log10(largest)) in_10 = sys_in[v] / magnitude out_10 = sys_in[v] / magnitude diff --git a/wsimod/validation.py b/wsimod/validation.py index c42fb6f6..2ca45c5b 100644 --- a/wsimod/validation.py +++ b/wsimod/validation.py @@ -170,7 +170,8 @@ def read_data( The keys to control this proces are: filename: Filename of the data to load - filter (optional): List of filters for the dataframe, each a dictionary in the form: + filter (optional): List of filters for the dataframe, each a dictionary in the + form: where: column to filer is: value of that column scaling (optional): List of variable scaling, each a dictionary of the form: From 07906ac9d0cdb7a38a2fca8b7258f32221c0e828 Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Wed, 10 Jan 2024 08:57:04 +0000 Subject: [PATCH 6/7] Fix remaining issues --- wsimod/arcs/arcs.py | 2 +- wsimod/nodes/nodes.py | 28 ++++++++++++++-------------- wsimod/nodes/wtw.py | 14 +++++++------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/wsimod/arcs/arcs.py b/wsimod/arcs/arcs.py index 5defb38f..173e3d3d 100644 --- a/wsimod/arcs/arcs.py +++ b/wsimod/arcs/arcs.py @@ -488,7 +488,7 @@ def update_queue(self, direction=None, backflow_enabled=True): vqip_ = self.v_change_vqip(vqip, removed) total_removed = self.sum_vqip(total_removed, vqip_) - # Assume that any water that cannot arrive at destination this + # Assume that any water that cannot arrive at destination this # timestep is backflow rejected = self.v_change_vqip(vqip, vqip["volume"] - removed) diff --git a/wsimod/nodes/nodes.py b/wsimod/nodes/nodes.py index 7bea027e..b8ba231a 100644 --- a/wsimod/nodes/nodes.py +++ b/wsimod/nodes/nodes.py @@ -218,7 +218,7 @@ def get_direction_arcs(self, direction, of_type=None): Examples: >>> arcs_to_push_to = my_node.get_direction_arcs('push') >>> arcs_to_pull_from = my_node.get_direction_arcs('pull') - >>> arcs_from_reservoirs = my_node.get_direction_arcs('pull', of_type = + >>> arcs_from_reservoirs = my_node.get_direction_arcs('pull', of_type = 'Reservoir') """ if of_type is None: @@ -427,7 +427,7 @@ def push_distributed(self, vqip, of_type=None, tag="default"): connected = self.get_connected(direction="push", of_type=of_type, tag=tag) iter_ = 0 if not_pushed > connected["avail"]: - # If more water than can be pushed, ignore preference and allocate all + # If more water than can be pushed, ignore preference and allocate all # available based on capacity connected["priority"] = connected["avail"] connected["allocation"] = connected["capacity"] @@ -801,7 +801,7 @@ def pull_ponded(self): Examples: >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] - >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 10, + >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 10, 'phosphate' : 0.2}) >>> print(my_tank.storage) {'volume' : 10, 'phosphate' : 0.2} @@ -828,7 +828,7 @@ def get_avail(self, vqip=None): Examples: >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] - >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 10, + >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 10, 'phosphate' : 0.2}) >>> print(my_tank.storage) {'volume' : 10, 'phosphate' : 0.2} @@ -858,7 +858,7 @@ def get_excess(self, vqip=None): Examples: >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] - >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, + >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, 'phosphate' : 0.2}) >>> print(my_tank.get_excess()) {'volume' : 4, 'phosphate' : 0.16} @@ -870,7 +870,7 @@ def get_excess(self, vqip=None): vol = min(vqip["volume"], vol) # Adjust storage pollutants to match volume in vqip - # TODO the v_change_vqip in the reply here is a weird default (if a VQIP is not + # TODO the v_change_vqip in the reply here is a weird default (if a VQIP is not # provided) return self.v_change_vqip(self.storage, vol) @@ -890,7 +890,7 @@ def push_storage(self, vqip, force=False): >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] >>> constants.POLLUTANTS = ['phosphate'] >>> constants.NON_ADDITIVE_POLLUTANTS = [] - >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, + >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, 'phosphate' : 0.2}) >>> my_push = {'volume' : 10, 'phosphate' : 0.5} >>> reply = my_tank.push_storage(my_push) @@ -933,7 +933,7 @@ def pull_storage(self, vqip): Examples: >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] - >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, + >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, 'phosphate' : 0.2}) >>> print(my_tank.pull_storage({'volume' : 6})) {'volume': 5.0, 'phosphate': 0.2} @@ -967,7 +967,7 @@ def pull_pollutants(self, vqip): Examples: >>> constants.ADDITIVE_POLLUTANTS = ['phosphate'] - >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, + >>> my_tank = Tank(capacity = 9, initial_storage = {'volume' : 5, 'phosphate' : 0.2}) >>> print(my_tank.pull_pollutants({'volume' : 2, 'phosphate' : 0.15})) {'volume': 2.0, 'phosphate': 0.15} @@ -1056,7 +1056,7 @@ def push_total_c(self, vqip): Returns: """ - # Push vqip to storage where pollutants are given as a concentration rather + # Push vqip to storage where pollutants are given as a concentration rather # than storage vqip = self.concentration_to_total(self.vqip) self.storage = self.sum_vqip(self.storage, vqip) @@ -1114,8 +1114,8 @@ def __init__(self, decays={}, parent=None, **kwargs): beginning of the timestep. Args: - decays (dict): A dict of dicts containing a key for each pollutant that - decays and, within that, a key for each parameter (a constant and + decays (dict): A dict of dicts containing a key for each pollutant that + decays and, within that, a key for each parameter (a constant and exponent) parent (object): An object that can be used to read temperature data from """ @@ -1177,7 +1177,7 @@ def __init__(self, number_of_timesteps=0, **kwargs): self.internal_arc = AltQueueArc( in_port=self, out_port=self, number_of_timesteps=self.number_of_timesteps ) - # TODO should mass balance call internal arc (is this arc called in arc mass + # TODO should mass balance call internal arc (is this arc called in arc mass # balance?) def get_avail(self): @@ -1351,7 +1351,7 @@ def __init__(self, decays={}, parent=None, number_of_timesteps=1, **kwargs): def _end_timestep(self): """End timestep wrapper that removes decayed pollutants and calls internal arc.""" - # TODO Should the active storage decay if decays are given (probably.. though + # TODO Should the active storage decay if decays are given (probably.. though # that sounds like a nightmare)? self.storage = self.extract_vqip(self.storage, self.internal_arc.total_decayed) self.storage_ = self.copy_vqip(self.storage) diff --git a/wsimod/nodes/wtw.py b/wsimod/nodes/wtw.py index 24b578e1..7cec54a7 100644 --- a/wsimod/nodes/wtw.py +++ b/wsimod/nodes/wtw.py @@ -112,7 +112,7 @@ def treat_current_input(self): # Calculate effluent, liquor and solids discharge_holder = self.empty_vqip() - # Assume non-additive pollutants are unchanged in discharge and are + # Assume non-additive pollutants are unchanged in discharge and are # proportionately mixed in liquor for key in constants.NON_ADDITIVE_POLLUTANTS: discharge_holder[key] = influent[key] @@ -124,9 +124,9 @@ def treat_current_input(self): + influent["volume"] * self.liquor_multiplier["volume"] ) - # TODO this should probably just be for process_parameters.keys() to avoid + # TODO this should probably just be for process_parameters.keys() to avoid # having to declare non changing parameters - # TODO should the liquoring be temperature sensitive too? As it is the solids + # TODO should the liquoring be temperature sensitive too? As it is the solids # will take the brunt of the temperature variability which maybe isn't sensible for key in constants.ADDITIVE_POLLUTANTS + ["volume"]: if key != "volume": @@ -244,7 +244,7 @@ def calculate_discharge(self): # Run WWTW model # Try to clear stormwater - # TODO (probably more tidy to use push_set_sewer? though maybe less + # TODO (probably more tidy to use push_set_sewer? though maybe less # computationally efficient) excess = self.get_excess_throughput() if (self.stormwater_tank.get_avail()["volume"] > constants.FLOAT_ACCURACY) & ( @@ -409,7 +409,7 @@ def __init__( - See `wtw.py/WTW` for treatment. - Stores treated water in a service reservoir tank, with a single tank per `FWTW` node. - - Aims to satisfy a throughput that would top up the service reservoirs + - Aims to satisfy a throughput that would top up the service reservoirs until full. - Currently, will not allow a deficit, thus introducing water from 'other measures' if pulls cannot fulfil demand. Behaviour under a @@ -452,9 +452,9 @@ def __init__( datum=self.service_reservoir_storage_elevation, initial_storage=self.service_reservoir_initial_storage, ) - # self.service_reservoir_tank.storage['volume'] = + # self.service_reservoir_tank.storage['volume'] = # self.service_reservoir_inital_storage - # self.service_reservoir_tank.storage_['volume'] = + # self.service_reservoir_tank.storage_['volume'] = # self.service_reservoir_inital_storage # Mass balance From 624b89d0966fa083b07112a7f23a59e2fd4e1848 Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Wed, 10 Jan 2024 09:02:55 +0000 Subject: [PATCH 7/7] Remove unnecessary dependencies --- pyproject.toml | 13 ++----------- requirements-dev.txt | 6 ------ 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e4c940cd..a3882d8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,8 +33,7 @@ dev = [ "black", "ruff", "pip-tools", - "pytest-cov", - "docformatter" + "pytest-cov" ] demos = [ @@ -63,13 +62,5 @@ addopts = "-v -p no:warnings --cov=wsimod --cov-report=html" select = ["E", "F", "I"] # pycodestyle, Pyflakes, isort. Ignoring pydocstyle (D), for now exclude = ["docs", "tests"] # Let's ignore tests and docs folders, for now -#[tool.ruff.per-file-ignores] -#"wsimod/*" = ["E501"] # 176 lines (all comments) are too long (>88). Ignoring, for now - [tool.ruff.pydocstyle] -convention = "google" - -[tool.docformatter] -recursive = true -black = true -in-place = true \ No newline at end of file +convention = "google" \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 5370e498..912ffccd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,8 +10,6 @@ build==1.0.3 # via pip-tools cfgv==3.4.0 # via pre-commit -charset-normalizer==3.3.2 - # via docformatter click==8.1.7 # via # black @@ -22,8 +20,6 @@ dill==0.3.7 # via wsimod (pyproject.toml) distlib==0.3.7 # via virtualenv -docformatter==1.7.5 - # via wsimod (pyproject.toml) filelock==3.13.1 # via virtualenv identify==2.5.31 @@ -79,8 +75,6 @@ tqdm==4.66.1 # via wsimod (pyproject.toml) tzdata==2023.3 # via pandas -untokenize==0.1.1 - # via docformatter virtualenv==20.24.6 # via pre-commit wheel==0.41.3