Skip to content

API Reference - Nodes

This section of the documentation provides a reference for the API of the nodes.nodes module.

Created on Wed Apr 7 08:43:32 2021.

@author: Barney

Converted to totals on Thur Apr 21 2022

DecayQueueTank

Bases: QueueTank

Source code in wsimod/nodes/nodes.py
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
class DecayQueueTank(QueueTank):
    """"""

    def __init__(self, decays={}, parent=None, number_of_timesteps=1, **kwargs):
        """Adds a DecayAltArc in QueueTank to enable decay to occur within the
        internal_arc queue.

        Args:
            decays (dict): A dict of dicts containing a key for each pollutant 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
            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)
        # Replace internal_arc with a DecayArcAlt
        self.internal_arc = DecayArcAlt(
            in_port=self,
            out_port=self,
            number_of_timesteps=number_of_timesteps,
            parent=parent,
            decays=decays,
        )

        self.end_timestep = self._end_timestep

    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)?
        self.storage = self.extract_vqip(self.storage, self.internal_arc.total_decayed)
        self.storage_ = self.copy_vqip(self.storage)
        self.internal_arc.end_timestep()

__init__(decays={}, parent=None, number_of_timesteps=1, **kwargs)

Adds a DecayAltArc in QueueTank to enable decay to occur within the internal_arc queue.

Parameters:

Name Type Description Default
decays dict

A dict of dicts containing a key for each pollutant 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

None
number_of_timesteps int

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.

1
Source code in wsimod/nodes/nodes.py
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
def __init__(self, decays={}, parent=None, number_of_timesteps=1, **kwargs):
    """Adds a DecayAltArc in QueueTank to enable decay to occur within the
    internal_arc queue.

    Args:
        decays (dict): A dict of dicts containing a key for each pollutant 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
        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)
    # Replace internal_arc with a DecayArcAlt
    self.internal_arc = DecayArcAlt(
        in_port=self,
        out_port=self,
        number_of_timesteps=number_of_timesteps,
        parent=parent,
        decays=decays,
    )

    self.end_timestep = self._end_timestep

DecayTank

Bases: Tank, DecayObj

Source code in wsimod/nodes/nodes.py
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
class DecayTank(Tank, DecayObj):
    """"""

    def __init__(self, decays={}, parent=None, **kwargs):
        """A tank that has DecayObj functions. Decay occurs in end_timestep, after
        updating state variables. In this sense, decay is occurring at the very
        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)
            parent (object): An object that can be used to read temperature data from
        """
        # Store parameters
        self.parent = parent

        # Initialise Tank
        Tank.__init__(self, **kwargs)

        # Initialise decay object
        DecayObj.__init__(self, decays)

        # Update timestep and ds functions
        self.end_timestep = self.end_timestep_decay
        self.ds = self.decay_ds

    def end_timestep_decay(self):
        """Update state variables and call make_decay."""
        self.total_decayed = self.empty_vqip()
        self.storage_ = self.copy_vqip(self.storage)

        self.storage = self.make_decay(self.storage)

    def decay_ds(self):
        """Track storage and amount decayed.

        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)
        return ds

__init__(decays={}, parent=None, **kwargs)

A tank that has DecayObj functions. Decay occurs in end_timestep, after updating state variables. In this sense, decay is occurring at the very beginning of the timestep.

Parameters:

Name Type Description Default
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

None
Source code in wsimod/nodes/nodes.py
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
def __init__(self, decays={}, parent=None, **kwargs):
    """A tank that has DecayObj functions. Decay occurs in end_timestep, after
    updating state variables. In this sense, decay is occurring at the very
    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)
        parent (object): An object that can be used to read temperature data from
    """
    # Store parameters
    self.parent = parent

    # Initialise Tank
    Tank.__init__(self, **kwargs)

    # Initialise decay object
    DecayObj.__init__(self, decays)

    # Update timestep and ds functions
    self.end_timestep = self.end_timestep_decay
    self.ds = self.decay_ds

decay_ds()

Track storage and amount decayed.

Returns:

Name Type Description
ds dict

A VQIP of change in storage and total decayed

Source code in wsimod/nodes/nodes.py
1142
1143
1144
1145
1146
1147
1148
1149
1150
def decay_ds(self):
    """Track storage and amount decayed.

    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)
    return ds

end_timestep_decay()

Update state variables and call make_decay.

Source code in wsimod/nodes/nodes.py
1135
1136
1137
1138
1139
1140
def end_timestep_decay(self):
    """Update state variables and call make_decay."""
    self.total_decayed = self.empty_vqip()
    self.storage_ = self.copy_vqip(self.storage)

    self.storage = self.make_decay(self.storage)

Node

Bases: WSIObj

Source code in wsimod/nodes/nodes.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
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.

        Args:
            name (str): Name of node
            data_input_dict (dict, optional): Dictionary of data inputs relevant for
                the node. 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 None.

        Examples:
            >>> my_node = nodes.Node(name = 'london_river_junction')

        Key assumptions:
            - No physical processes represented, can be used as a junction.

        Input data and parameter requirements:
            - All nodes require a `name`
        """

        # Get node types
        def all_subclasses(cls):
            """

            Args:
                cls:

            Returns:

            """
            return set(cls.__subclasses__()).union(
                [s for c in cls.__subclasses__() for s in all_subclasses(c)]
            )

        node_types = [x.__name__ for x in all_subclasses(nodes.Node)] + ["Node"]

        # Default essential parameters
        # Dictionary of arcs
        self.in_arcs = {}
        self.out_arcs = {}
        self.in_arcs_type = {x: {} for x in node_types}
        self.out_arcs_type = {x: {} for x in node_types}

        # Set parameters
        self.name = name
        self.t = None
        self.data_input_dict = data_input_dict

        # Initiailise default handlers
        self.pull_set_handler = {"default": self.pull_distributed}
        self.push_set_handler = {
            "default": lambda x: self.push_distributed(
                x, of_type=["Node", "River", "Waste", "Reservoir"]
            )
        }
        self.pull_check_handler = {"default": self.pull_check_basic}
        self.push_check_handler = {
            "default": lambda x: self.push_check_basic(
                x, of_type=["Node", "River", "Waste", "Reservoir"]
            )
        }

        super().__init__()

        # Mass balance checking
        self.mass_balance_in = [self.total_in]
        self.mass_balance_out = [self.total_out]
        self.mass_balance_ds = [lambda: self.empty_vqip()]

    def total_in(self):
        """Sum flow and pollutant amounts entering a node via in_arcs.

        Returns:
            in_ (dict): Summed VQIP of in_arcs

        Examples:
            >>> node_inflow = my_node.total_in()
        """
        in_ = self.empty_vqip()
        for arc in self.in_arcs.values():
            in_ = self.sum_vqip(in_, arc.vqip_out)

        return in_

    def total_out(self):
        """Sum flow and pollutant amounts leaving a node via out_arcs.

        Returns:
            out_ (dict): Summed VQIP of out_arcs

        Examples:
            >>> node_outflow = my_node.total_out()
        """
        out_ = self.empty_vqip()
        for arc in self.out_arcs.values():
            out_ = self.sum_vqip(out_, arc.vqip_in)

        return out_

    def node_mass_balance(self):
        """Wrapper for core.py/WSIObj/mass_balance. Tracks change in mass balance.

        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

        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.

        Args:
            vqip (dict): the VQIP pull request (by default, only the 'volume' key is
                needed).
            tag (str, optional): optional message to direct query_handler which pull
                function to call. Defaults to 'default'.

        Returns:
            (dict): VQIP received from query_handler

        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.

        Args:
            vqip (_type_): the VQIP push request
            tag (str, optional): optional message to direct query_handler which pull
                function to call. Defaults to 'default'.

        Returns:
            (dict): VQIP not received from query_handler

        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.

        Args:
            vqip (dict, optional): the VQIP pull check (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 query_handler which pull
                function to call. Defaults to 'default'.

        Returns:
            (dict): VQIP available from query_handler

        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.

        Args:
            vqip (dict, optional): the VQIP push check. Defaults to None, which
                returns all available capacity to push
            tag (str, optional): optional message to direct query_handler which pull
                function to call. Defaults to 'default'

        Returns:
            (dict): VQIP available to push from query_handler

        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)

    def get_direction_arcs(self, direction, of_type=None):
        """Identify arcs to/from all attached nodes in a given direction.

        Args:
            direction (str): can be either 'pull' or 'push' to send checks to
                receiving or contributing nodes
            of_type (str or list) : optional, can be specified to send checks only
                to nodes of a given type (must be a subclass in nodes.py)

        Returns:
            f (str): Either 'send_pull_check' or 'send_push_check' depending on
                direction
            arcs (list): List of arc objects

        Raises:
            Message if no direction is specified

        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')
        """
        if of_type is None:
            # Return all arcs
            if direction == "pull":
                arcs = list(self.in_arcs.values())
                f = "send_pull_check"
            elif direction == "push":
                arcs = list(self.out_arcs.values())
                f = "send_push_check"
            else:
                print("No direction")

        else:
            if isinstance(of_type, str):
                of_type = [of_type]

            # Assign arcs/function based on parameters
            arcs = []
            if direction == "pull":
                for type_ in of_type:
                    arcs += list(self.in_arcs_type[type_].values())
                f = "send_pull_check"
            elif direction == "push":
                for type_ in of_type:
                    arcs += list(self.out_arcs_type[type_].values())
                f = "send_push_check"
            else:
                print("No direction")

        return f, arcs

    def get_connected(self, direction="pull", of_type=None, tag="default"):
        """Send push/pull checks to all attached arcs in a given direction.

        Args:
            direction (str, optional): The type of check to send to all attached
                nodes. Can be 'push' or 'pull'. The default is 'pull'.
            of_type (str or list) : optional, can be specified to send checks only
                to nodes of a given type (must be a subclass in nodes.py)
            tag (str, optional): optional message to direct query_handler which pull
                function to call. Defaults to 'default'.

        Returns:
            connected (dict) :
                Dictionary containing keys:
                'avail': (float) - total available volume for push/pull
                'priority': (float) - total (availability * preference)
                                    of attached arcs
                'allocation': (dict) - contains all attached arcs in specified
                                direction and respective (availability * preference)

        Examples:
            >>> vqip_available_to_pull = my_node.get_direction_arcs()
            >>> vqip_available_to_push = my_node.get_direction_arcs('push')
            >>> avail_reservoir_vqip = my_node.get_direction_arcs('pull',
                                                          of_type = 'Reservoir')
            >>> 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": {}}

        # Get arcs
        f, arcs = self.get_direction_arcs(direction, of_type)

        # Iterate over arcs, updating connected dict
        for arc in arcs:
            avail = getattr(arc, f)(tag=tag)["volume"]
            if avail < constants.FLOAT_ACCURACY:
                avail = 0  # Improves convergence
            connected["avail"] += avail
            preference = arc.preference
            connected["priority"] += avail * preference
            connected["allocation"][arc.name] = avail * preference
            connected["capacity"][arc.name] = avail

        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').

        Args:
            handler (dict): contains all push/pull requests for various tags
            ip (vqip): the vqip request
            tag (str): describes what type of push/pull request should be called

        Returns:
            (dict): the VQIP reply from push/pull request

        Raises:
            Message if no functions are defined for tag and if request/check
            function fails
        """
        try:
            return handler[tag](ip)
        except Exception:
            if tag not in handler.keys():
                print("No functions defined for " + tag)
                return handler[tag](ip)
            else:
                print("Some other error")
                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.

        Args:
            vqip (dict): Total amount to pull (by default, only the
                'volume' key is used)
            of_type (str or list) : optional, can be specified to send checks only
                to nodes of a given type (must be a subclass in nodes.py)
            tag (str, optional): optional message to direct query_handler which pull
                function to call. Defaults to 'default'.

        Returns:
            pulled (dict): VQIP of combined pulled water
        """
        if len(self.in_arcs) == 1:
            # If only one in_arc, just pull from that
            if of_type is None:
                pulled = next(iter(self.in_arcs.values())).send_pull_request(
                    vqip, tag=tag
                )
            elif any(
                [x in of_type for x, y in self.in_arcs_type.items() if len(y) > 0]
            ):
                pulled = next(iter(self.in_arcs.values())).send_pull_request(
                    vqip, tag=tag
                )
            else:
                # No viable out arcs
                pulled = self.empty_vqip()
        else:
            # Pull in proportion from connected by priority

            # Initialise pulled, deficit, connected, iter_
            pulled = self.empty_vqip()
            deficit = vqip["volume"]
            connected = self.get_connected(direction="pull", of_type=of_type, tag=tag)
            iter_ = 0

            # Iterate over sending nodes until deficit met
            while (
                (deficit > constants.FLOAT_ACCURACY)
                & (connected["avail"] > constants.FLOAT_ACCURACY)
            ) & (iter_ < constants.MAXITER):
                # Pull from connected
                for key, allocation in connected["allocation"].items():
                    received = self.in_arcs[key].send_pull_request(
                        {"volume": deficit * allocation / connected["priority"]},
                        tag=tag,
                    )
                    pulled = self.sum_vqip(pulled, received)

                # Update deficit, connected and iter_
                deficit = vqip["volume"] - pulled["volume"]
                connected = self.get_connected(
                    direction="pull", of_type=of_type, tag=tag
                )
                iter_ += 1

            if iter_ == constants.MAXITER:
                print("Maxiter reached in {0} at {1}".format(self.name, self.t))
        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.

        Args:
            vqip (dict): Total amount to push
            of_type (str or list) : optional, can be specified to send checks only
                to nodes of a given type (must be a subclass in nodes.py)
            tag (str, optional): optional message to direct query_handler which pull
                function to call. Defaults to '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
            if of_type is None:
                not_pushed_ = next(iter(self.out_arcs.values())).send_push_request(
                    vqip, tag=tag
                )
            elif any(
                [x in of_type for x, y in self.out_arcs_type.items() if len(y) > 0]
            ):
                not_pushed_ = next(iter(self.out_arcs.values())).send_push_request(
                    vqip, tag=tag
                )
            else:
                # No viable out arcs
                not_pushed_ = vqip
        else:
            # Push in proportion to connected by priority
            # Initialise pushed, deficit, connected, iter_
            not_pushed = vqip["volume"]
            not_pushed_ = self.copy_vqip(vqip)
            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
                connected["priority"] = connected["avail"]
                connected["allocation"] = connected["capacity"]

            # Iterate over receiving nodes until sent
            while (
                (not_pushed > constants.FLOAT_ACCURACY)
                & (connected["avail"] > constants.FLOAT_ACCURACY)
                & (iter_ < constants.MAXITER)
            ):
                # Push to connected
                amount_to_push = min(connected["avail"], not_pushed)

                for key, allocation in connected["allocation"].items():
                    to_send = amount_to_push * allocation / connected["priority"]
                    to_send = self.v_change_vqip(not_pushed_, to_send)
                    reply = self.out_arcs[key].send_push_request(to_send, tag=tag)

                    sent = self.extract_vqip(to_send, reply)
                    not_pushed_ = self.extract_vqip(not_pushed_, sent)

                not_pushed = not_pushed_["volume"]
                connected = self.get_connected(
                    direction="push", of_type=of_type, tag=tag
                )
                iter_ += 1

            if iter_ == constants.MAXITER:
                print("Maxiter reached in {0} at {1}".format(self.name, self.t))

        return not_pushed_

    def check_basic(self, direction, vqip=None, of_type=None, tag="default"):
        """Generic function that conveys a pull or push check onwards to connected
        nodes. It is the default behaviour that treats a node like a junction.

        Args:
            direction (str): can be either 'pull' or 'push' to send checks to
                receiving or contributing nodes
            vqip (dict, optional): The VQIP to check. Defaults to None (if pulling
                this will return available water to pull, if pushing then available
                capacity to push).
            of_type (str or list) : optional, can be specified to send checks only
                to nodes of a given type (must be a subclass in nodes.py)
            tag (str, optional): optional message to direct query_handler which pull
                function to call. Defaults to 'default'.

        Returns:
            avail (dict): VQIP responses summed over all requests
        """
        f, arcs = self.get_direction_arcs(direction, of_type)

        # Iterate over arcs, updating total
        avail = self.empty_vqip()
        for arc in arcs:
            avail = self.sum_vqip(avail, getattr(arc, f)(tag=tag))

        if vqip is not None:
            avail = self.v_change_vqip(avail, min(avail["volume"], vqip["volume"]))

        return avail

    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.

        Args:
            vqip (dict, optional): VQIP from handler of amount to pull check
                (by default, only the 'volume' key is used). Defaults to None (which
                returns all availalbe water to pull).
            of_type (str or list) : optional, can be specified to send checks only
                to nodes of a given type (must be a subclass in nodes.py)
            tag (str, optional): optional message to direct query_handler which pull
                function to call. Defaults to '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.

        Args:
            vqip (dict, optional): VQIP from handler of amount to push check.
                Defaults to None (which returns all available capacity to push).
            of_type (str or list) : optional, can be specified to send checks only
                to nodes of a given type (must be a subclass in nodes.py)
            tag (str, optional): optional message to direct query_handler which pull
                function to call. Defaults to 'default'.

        Returns:
            (dict): VQIP check response of downstream nodes
        """
        return self.check_basic("push", vqip, of_type, tag)

    def pull_set_deny(self, vqip):
        """Responds that no water is available to pull from a request.

        Args:
            vqip (dict): A VQIP amount of water requested (ignored)

        Returns:
            (dict): An empty VQIP indicated no water was pulled

        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()

    def pull_check_deny(self, vqip=None):
        """Responds that no water is available to pull from a check.

        Args:
            vqip (dict): A VQIP amount of water requested (ignored)

        Returns:
            (dict): An empty VQIP indicated no water was pulled

        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()

    def push_set_deny(self, vqip):
        """Responds that no water is available to push in a request.

        Args:
            vqip (dict): A VQIP amount of water to push

        Returns:
            vqip (dict): Returns the request indicating no water was pushed

        Raises:
            Message when called, since it would usually occur if a model is
            improperly connected
        """
        print("Attempted push set to deny")
        return vqip

    def push_check_deny(self, vqip=None):
        """Responds that no water is available to push in a check.

        Args:
            vqip (dict): A VQIP amount of water to push check (ignored)

        Returns:
            (dict): An empty VQIP indicated no capacity for pushes exists

        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()

    def push_check_accept(self, vqip=None):
        """Push check function that accepts all water.

        Args:
            vqip (dict, optional): A VQIP that has been pushed (ignored)

        Returns:
            (dict): VQIP or an unbounded capacity, indicating all water can be received
        """
        if not vqip:
            vqip = self.empty_vqip()
            vqip["volume"] = constants.UNBOUNDED_CAPACITY
        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.

        Args:
            var (str): Name of variable

        Returns:
            Data read
        """
        return self.data_input_dict[(var, self.t)]

    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."""
        pass

__init__(name, data_input_dict=None)

Base class for CWSD nodes. Constructs all the necessary attributes for the node object.

Parameters:

Name Type Description Default
name str

Name of node

required
data_input_dict dict

Dictionary of data inputs relevant for the node. 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 None.

None

Examples:

>>> my_node = nodes.Node(name = 'london_river_junction')
Key assumptions
  • No physical processes represented, can be used as a junction.
Input data and parameter requirements
  • All nodes require a name
Source code in wsimod/nodes/nodes.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def __init__(self, name, data_input_dict=None):
    """Base class for CWSD nodes. Constructs all the necessary attributes for the
    node object.

    Args:
        name (str): Name of node
        data_input_dict (dict, optional): Dictionary of data inputs relevant for
            the node. 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 None.

    Examples:
        >>> my_node = nodes.Node(name = 'london_river_junction')

    Key assumptions:
        - No physical processes represented, can be used as a junction.

    Input data and parameter requirements:
        - All nodes require a `name`
    """

    # Get node types
    def all_subclasses(cls):
        """

        Args:
            cls:

        Returns:

        """
        return set(cls.__subclasses__()).union(
            [s for c in cls.__subclasses__() for s in all_subclasses(c)]
        )

    node_types = [x.__name__ for x in all_subclasses(nodes.Node)] + ["Node"]

    # Default essential parameters
    # Dictionary of arcs
    self.in_arcs = {}
    self.out_arcs = {}
    self.in_arcs_type = {x: {} for x in node_types}
    self.out_arcs_type = {x: {} for x in node_types}

    # Set parameters
    self.name = name
    self.t = None
    self.data_input_dict = data_input_dict

    # Initiailise default handlers
    self.pull_set_handler = {"default": self.pull_distributed}
    self.push_set_handler = {
        "default": lambda x: self.push_distributed(
            x, of_type=["Node", "River", "Waste", "Reservoir"]
        )
    }
    self.pull_check_handler = {"default": self.pull_check_basic}
    self.push_check_handler = {
        "default": lambda x: self.push_check_basic(
            x, of_type=["Node", "River", "Waste", "Reservoir"]
        )
    }

    super().__init__()

    # Mass balance checking
    self.mass_balance_in = [self.total_in]
    self.mass_balance_out = [self.total_out]
    self.mass_balance_ds = [lambda: self.empty_vqip()]

check_basic(direction, vqip=None, of_type=None, tag='default')

Generic function that conveys a pull or push check onwards to connected nodes. It is the default behaviour that treats a node like a junction.

Parameters:

Name Type Description Default
direction str

can be either 'pull' or 'push' to send checks to receiving or contributing nodes

required
vqip dict

The VQIP to check. Defaults to None (if pulling this will return available water to pull, if pushing then available capacity to push).

None
of_type str or list)

optional, can be specified to send checks only to nodes of a given type (must be a subclass in nodes.py)

None
tag str

optional message to direct query_handler which pull function to call. Defaults to 'default'.

'default'

Returns:

Name Type Description
avail dict

VQIP responses summed over all requests

Source code in wsimod/nodes/nodes.py
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
def check_basic(self, direction, vqip=None, of_type=None, tag="default"):
    """Generic function that conveys a pull or push check onwards to connected
    nodes. It is the default behaviour that treats a node like a junction.

    Args:
        direction (str): can be either 'pull' or 'push' to send checks to
            receiving or contributing nodes
        vqip (dict, optional): The VQIP to check. Defaults to None (if pulling
            this will return available water to pull, if pushing then available
            capacity to push).
        of_type (str or list) : optional, can be specified to send checks only
            to nodes of a given type (must be a subclass in nodes.py)
        tag (str, optional): optional message to direct query_handler which pull
            function to call. Defaults to 'default'.

    Returns:
        avail (dict): VQIP responses summed over all requests
    """
    f, arcs = self.get_direction_arcs(direction, of_type)

    # Iterate over arcs, updating total
    avail = self.empty_vqip()
    for arc in arcs:
        avail = self.sum_vqip(avail, getattr(arc, f)(tag=tag))

    if vqip is not None:
        avail = self.v_change_vqip(avail, min(avail["volume"], vqip["volume"]))

    return avail

end_timestep()

Empty function intended to be called at the end of every timestep.

Subclasses will overwrite this functions.

Source code in wsimod/nodes/nodes.py
620
621
622
623
624
625
def end_timestep(self):
    """Empty function intended to be called at the end of every timestep.

    Subclasses will overwrite this functions.
    """
    pass

get_connected(direction='pull', of_type=None, tag='default')

Send push/pull checks to all attached arcs in a given direction.

Parameters:

Name Type Description Default
direction str

The type of check to send to all attached nodes. Can be 'push' or 'pull'. The default is 'pull'.

'pull'
of_type str or list)

optional, can be specified to send checks only to nodes of a given type (must be a subclass in nodes.py)

None
tag str

optional message to direct query_handler which pull function to call. Defaults to 'default'.

'default'

Returns:

Type Description

connected (dict) : Dictionary containing keys: 'avail': (float) - total available volume for push/pull 'priority': (float) - total (availability * preference) of attached arcs 'allocation': (dict) - contains all attached arcs in specified direction and respective (availability * preference)

Examples:

>>> vqip_available_to_pull = my_node.get_direction_arcs()
>>> vqip_available_to_push = my_node.get_direction_arcs('push')
>>> avail_reservoir_vqip = my_node.get_direction_arcs('pull',
                                              of_type = 'Reservoir')
>>> avail_sewer_push_to_sewers = my_node.get_direction_arcs('push',
                                                    of_type = 'Sewer',
                                                    tag = 'Sewer')
Source code in wsimod/nodes/nodes.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
def get_connected(self, direction="pull", of_type=None, tag="default"):
    """Send push/pull checks to all attached arcs in a given direction.

    Args:
        direction (str, optional): The type of check to send to all attached
            nodes. Can be 'push' or 'pull'. The default is 'pull'.
        of_type (str or list) : optional, can be specified to send checks only
            to nodes of a given type (must be a subclass in nodes.py)
        tag (str, optional): optional message to direct query_handler which pull
            function to call. Defaults to 'default'.

    Returns:
        connected (dict) :
            Dictionary containing keys:
            'avail': (float) - total available volume for push/pull
            'priority': (float) - total (availability * preference)
                                of attached arcs
            'allocation': (dict) - contains all attached arcs in specified
                            direction and respective (availability * preference)

    Examples:
        >>> vqip_available_to_pull = my_node.get_direction_arcs()
        >>> vqip_available_to_push = my_node.get_direction_arcs('push')
        >>> avail_reservoir_vqip = my_node.get_direction_arcs('pull',
                                                      of_type = 'Reservoir')
        >>> 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": {}}

    # Get arcs
    f, arcs = self.get_direction_arcs(direction, of_type)

    # Iterate over arcs, updating connected dict
    for arc in arcs:
        avail = getattr(arc, f)(tag=tag)["volume"]
        if avail < constants.FLOAT_ACCURACY:
            avail = 0  # Improves convergence
        connected["avail"] += avail
        preference = arc.preference
        connected["priority"] += avail * preference
        connected["allocation"][arc.name] = avail * preference
        connected["capacity"][arc.name] = avail

    return connected

get_data_input(var)

Read data from data_input_dict. Keys are tuples with the first entry as the variable to read and second entry the time.

Parameters:

Name Type Description Default
var str

Name of variable

required

Returns:

Type Description

Data read

Source code in wsimod/nodes/nodes.py
608
609
610
611
612
613
614
615
616
617
618
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.

    Args:
        var (str): Name of variable

    Returns:
        Data read
    """
    return self.data_input_dict[(var, self.t)]

get_direction_arcs(direction, of_type=None)

Identify arcs to/from all attached nodes in a given direction.

Parameters:

Name Type Description Default
direction str

can be either 'pull' or 'push' to send checks to receiving or contributing nodes

required
of_type str or list)

optional, can be specified to send checks only to nodes of a given type (must be a subclass in nodes.py)

None

Returns:

Name Type Description
f str

Either 'send_pull_check' or 'send_push_check' depending on direction

arcs list

List of arc objects

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')
Source code in wsimod/nodes/nodes.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
def get_direction_arcs(self, direction, of_type=None):
    """Identify arcs to/from all attached nodes in a given direction.

    Args:
        direction (str): can be either 'pull' or 'push' to send checks to
            receiving or contributing nodes
        of_type (str or list) : optional, can be specified to send checks only
            to nodes of a given type (must be a subclass in nodes.py)

    Returns:
        f (str): Either 'send_pull_check' or 'send_push_check' depending on
            direction
        arcs (list): List of arc objects

    Raises:
        Message if no direction is specified

    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')
    """
    if of_type is None:
        # Return all arcs
        if direction == "pull":
            arcs = list(self.in_arcs.values())
            f = "send_pull_check"
        elif direction == "push":
            arcs = list(self.out_arcs.values())
            f = "send_push_check"
        else:
            print("No direction")

    else:
        if isinstance(of_type, str):
            of_type = [of_type]

        # Assign arcs/function based on parameters
        arcs = []
        if direction == "pull":
            for type_ in of_type:
                arcs += list(self.in_arcs_type[type_].values())
            f = "send_pull_check"
        elif direction == "push":
            for type_ in of_type:
                arcs += list(self.out_arcs_type[type_].values())
            f = "send_push_check"
        else:
            print("No direction")

    return f, arcs

node_mass_balance()

Wrapper for core.py/WSIObj/mass_balance. Tracks change in mass balance.

Returns:

Name Type Description
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

Examples:

>>> node_in, node_out, node_ds = my_node.node_mass_balance()
Source code in wsimod/nodes/nodes.py
117
118
119
120
121
122
123
124
125
126
127
128
129
def node_mass_balance(self):
    """Wrapper for core.py/WSIObj/mass_balance. Tracks change in mass balance.

    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

    Examples:
        >>> node_in, node_out, node_ds = my_node.node_mass_balance()
    """
    in_, ds_, out_ = self.mass_balance()
    return in_, ds_, out_

pull_check(vqip=None, tag='default')

Receives pull check requests from arcs and passes request to query handler.

Parameters:

Name Type Description Default
vqip dict

the VQIP pull check (by default, only the 'volume' key is used). Defaults to None, which returns all available water to pull.

None
tag str

optional message to direct query_handler which pull function to call. Defaults to 'default'.

'default'

Returns:

Type Description
dict

VQIP available from query_handler

Examples:

>>> water_available = my_node.pull_check({'volume' : 10})
>>> total_water_available = my_node.pull_check()
Source code in wsimod/nodes/nodes.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def pull_check(self, vqip=None, tag="default"):
    """Receives pull check requests from arcs and passes request to query handler.

    Args:
        vqip (dict, optional): the VQIP pull check (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 query_handler which pull
            function to call. Defaults to 'default'.

    Returns:
        (dict): VQIP available from query_handler

    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)

pull_check_basic(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.

Parameters:

Name Type Description Default
vqip dict

VQIP from handler of amount to pull check (by default, only the 'volume' key is used). Defaults to None (which returns all availalbe water to pull).

None
of_type str or list)

optional, can be specified to send checks only to nodes of a given type (must be a subclass in nodes.py)

None
tag str

optional message to direct query_handler which pull function to call. Defaults to 'default'.

'default'

Returns:

Type Description
dict

VQIP check response of upstream nodes

Source code in wsimod/nodes/nodes.py
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
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.

    Args:
        vqip (dict, optional): VQIP from handler of amount to pull check
            (by default, only the 'volume' key is used). Defaults to None (which
            returns all availalbe water to pull).
        of_type (str or list) : optional, can be specified to send checks only
            to nodes of a given type (must be a subclass in nodes.py)
        tag (str, optional): optional message to direct query_handler which pull
            function to call. Defaults to 'default'.

    Returns:
        (dict): VQIP check response of upstream nodes
    """
    return self.check_basic("pull", vqip, of_type, tag)

pull_check_deny(vqip=None)

Responds that no water is available to pull from a check.

Parameters:

Name Type Description Default
vqip dict

A VQIP amount of water requested (ignored)

None

Returns:

Type Description
dict

An empty VQIP indicated no water was pulled

Source code in wsimod/nodes/nodes.py
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
def pull_check_deny(self, vqip=None):
    """Responds that no water is available to pull from a check.

    Args:
        vqip (dict): A VQIP amount of water requested (ignored)

    Returns:
        (dict): An empty VQIP indicated no water was pulled

    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()

pull_distributed(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.

Parameters:

Name Type Description Default
vqip dict

Total amount to pull (by default, only the 'volume' key is used)

required
of_type str or list)

optional, can be specified to send checks only to nodes of a given type (must be a subclass in nodes.py)

None
tag str

optional message to direct query_handler which pull function to call. Defaults to 'default'.

'default'

Returns:

Name Type Description
pulled dict

VQIP of combined pulled water

Source code in wsimod/nodes/nodes.py
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
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.

    Args:
        vqip (dict): Total amount to pull (by default, only the
            'volume' key is used)
        of_type (str or list) : optional, can be specified to send checks only
            to nodes of a given type (must be a subclass in nodes.py)
        tag (str, optional): optional message to direct query_handler which pull
            function to call. Defaults to 'default'.

    Returns:
        pulled (dict): VQIP of combined pulled water
    """
    if len(self.in_arcs) == 1:
        # If only one in_arc, just pull from that
        if of_type is None:
            pulled = next(iter(self.in_arcs.values())).send_pull_request(
                vqip, tag=tag
            )
        elif any(
            [x in of_type for x, y in self.in_arcs_type.items() if len(y) > 0]
        ):
            pulled = next(iter(self.in_arcs.values())).send_pull_request(
                vqip, tag=tag
            )
        else:
            # No viable out arcs
            pulled = self.empty_vqip()
    else:
        # Pull in proportion from connected by priority

        # Initialise pulled, deficit, connected, iter_
        pulled = self.empty_vqip()
        deficit = vqip["volume"]
        connected = self.get_connected(direction="pull", of_type=of_type, tag=tag)
        iter_ = 0

        # Iterate over sending nodes until deficit met
        while (
            (deficit > constants.FLOAT_ACCURACY)
            & (connected["avail"] > constants.FLOAT_ACCURACY)
        ) & (iter_ < constants.MAXITER):
            # Pull from connected
            for key, allocation in connected["allocation"].items():
                received = self.in_arcs[key].send_pull_request(
                    {"volume": deficit * allocation / connected["priority"]},
                    tag=tag,
                )
                pulled = self.sum_vqip(pulled, received)

            # Update deficit, connected and iter_
            deficit = vqip["volume"] - pulled["volume"]
            connected = self.get_connected(
                direction="pull", of_type=of_type, tag=tag
            )
            iter_ += 1

        if iter_ == constants.MAXITER:
            print("Maxiter reached in {0} at {1}".format(self.name, self.t))
    return pulled

pull_set(vqip, tag='default')

Receives pull set requests from arcs and passes request to query handler.

Parameters:

Name Type Description Default
vqip dict

the VQIP pull request (by default, only the 'volume' key is needed).

required
tag str

optional message to direct query_handler which pull function to call. Defaults to 'default'.

'default'

Returns:

Type Description
dict

VQIP received from query_handler

Examples:

>>> water_received = my_node.pull_set({'volume' : 10})
Source code in wsimod/nodes/nodes.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def pull_set(self, vqip, tag="default"):
    """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
            needed).
        tag (str, optional): optional message to direct query_handler which pull
            function to call. Defaults to 'default'.

    Returns:
        (dict): VQIP received from query_handler

    Examples:
        >>> water_received = my_node.pull_set({'volume' : 10})
    """
    return self.query_handler(self.pull_set_handler, vqip, tag)

pull_set_deny(vqip)

Responds that no water is available to pull from a request.

Parameters:

Name Type Description Default
vqip dict

A VQIP amount of water requested (ignored)

required

Returns:

Type Description
dict

An empty VQIP indicated no water was pulled

Source code in wsimod/nodes/nodes.py
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
def pull_set_deny(self, vqip):
    """Responds that no water is available to pull from a request.

    Args:
        vqip (dict): A VQIP amount of water requested (ignored)

    Returns:
        (dict): An empty VQIP indicated no water was pulled

    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()

push_check(vqip=None, tag='default')

Receives push check requests from arcs and passes request to query handler.

Parameters:

Name Type Description Default
vqip dict

the VQIP push check. Defaults to None, which returns all available capacity to push

None
tag str

optional message to direct query_handler which pull function to call. Defaults to 'default'

'default'

Returns:

Type Description
dict

VQIP available to push from query_handler

Examples:

>>> total_available_push_capacity = my_node.push_check()
>>> available_push_capacity = my_node.push_check(wastewater_vqip)
Source code in wsimod/nodes/nodes.py
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
def push_check(self, vqip=None, tag="default"):
    """Receives push check requests from arcs and passes request to query handler.

    Args:
        vqip (dict, optional): the VQIP push check. Defaults to None, which
            returns all available capacity to push
        tag (str, optional): optional message to direct query_handler which pull
            function to call. Defaults to 'default'

    Returns:
        (dict): VQIP available to push from query_handler

    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)

push_check_accept(vqip=None)

Push check function that accepts all water.

Parameters:

Name Type Description Default
vqip dict

A VQIP that has been pushed (ignored)

None

Returns:

Type Description
dict

VQIP or an unbounded capacity, indicating all water can be received

Source code in wsimod/nodes/nodes.py
594
595
596
597
598
599
600
601
602
603
604
605
606
def push_check_accept(self, vqip=None):
    """Push check function that accepts all water.

    Args:
        vqip (dict, optional): A VQIP that has been pushed (ignored)

    Returns:
        (dict): VQIP or an unbounded capacity, indicating all water can be received
    """
    if not vqip:
        vqip = self.empty_vqip()
        vqip["volume"] = constants.UNBOUNDED_CAPACITY
    return vqip

push_check_basic(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.

Parameters:

Name Type Description Default
vqip dict

VQIP from handler of amount to push check. Defaults to None (which returns all available capacity to push).

None
of_type str or list)

optional, can be specified to send checks only to nodes of a given type (must be a subclass in nodes.py)

None
tag str

optional message to direct query_handler which pull function to call. Defaults to 'default'.

'default'

Returns:

Type Description
dict

VQIP check response of downstream nodes

Source code in wsimod/nodes/nodes.py
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
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.

    Args:
        vqip (dict, optional): VQIP from handler of amount to push check.
            Defaults to None (which returns all available capacity to push).
        of_type (str or list) : optional, can be specified to send checks only
            to nodes of a given type (must be a subclass in nodes.py)
        tag (str, optional): optional message to direct query_handler which pull
            function to call. Defaults to 'default'.

    Returns:
        (dict): VQIP check response of downstream nodes
    """
    return self.check_basic("push", vqip, of_type, tag)

push_check_deny(vqip=None)

Responds that no water is available to push in a check.

Parameters:

Name Type Description Default
vqip dict

A VQIP amount of water to push check (ignored)

None

Returns:

Type Description
dict

An empty VQIP indicated no capacity for pushes exists

Source code in wsimod/nodes/nodes.py
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
def push_check_deny(self, vqip=None):
    """Responds that no water is available to push in a check.

    Args:
        vqip (dict): A VQIP amount of water to push check (ignored)

    Returns:
        (dict): An empty VQIP indicated no capacity for pushes exists

    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()

push_distributed(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.

Parameters:

Name Type Description Default
vqip dict

Total amount to push

required
of_type str or list)

optional, can be specified to send checks only to nodes of a given type (must be a subclass in nodes.py)

None
tag str

optional message to direct query_handler which pull function to call. Defaults to 'default'.

'default'

Returns:

Name Type Description
not_pushed_ dict

VQIP of water that cannot be pushed

Source code in wsimod/nodes/nodes.py
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
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.

    Args:
        vqip (dict): Total amount to push
        of_type (str or list) : optional, can be specified to send checks only
            to nodes of a given type (must be a subclass in nodes.py)
        tag (str, optional): optional message to direct query_handler which pull
            function to call. Defaults to '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
        if of_type is None:
            not_pushed_ = next(iter(self.out_arcs.values())).send_push_request(
                vqip, tag=tag
            )
        elif any(
            [x in of_type for x, y in self.out_arcs_type.items() if len(y) > 0]
        ):
            not_pushed_ = next(iter(self.out_arcs.values())).send_push_request(
                vqip, tag=tag
            )
        else:
            # No viable out arcs
            not_pushed_ = vqip
    else:
        # Push in proportion to connected by priority
        # Initialise pushed, deficit, connected, iter_
        not_pushed = vqip["volume"]
        not_pushed_ = self.copy_vqip(vqip)
        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
            connected["priority"] = connected["avail"]
            connected["allocation"] = connected["capacity"]

        # Iterate over receiving nodes until sent
        while (
            (not_pushed > constants.FLOAT_ACCURACY)
            & (connected["avail"] > constants.FLOAT_ACCURACY)
            & (iter_ < constants.MAXITER)
        ):
            # Push to connected
            amount_to_push = min(connected["avail"], not_pushed)

            for key, allocation in connected["allocation"].items():
                to_send = amount_to_push * allocation / connected["priority"]
                to_send = self.v_change_vqip(not_pushed_, to_send)
                reply = self.out_arcs[key].send_push_request(to_send, tag=tag)

                sent = self.extract_vqip(to_send, reply)
                not_pushed_ = self.extract_vqip(not_pushed_, sent)

            not_pushed = not_pushed_["volume"]
            connected = self.get_connected(
                direction="push", of_type=of_type, tag=tag
            )
            iter_ += 1

        if iter_ == constants.MAXITER:
            print("Maxiter reached in {0} at {1}".format(self.name, self.t))

    return not_pushed_

push_set(vqip, tag='default')

Receives push set requests from arcs and passes request to query handler.

Parameters:

Name Type Description Default
vqip _type_

the VQIP push request

required
tag str

optional message to direct query_handler which pull function to call. Defaults to 'default'.

'default'

Returns:

Type Description
dict

VQIP not received from query_handler

Examples:

water_not_pushed = my_node.push_set(wastewater_vqip)

Source code in wsimod/nodes/nodes.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def push_set(self, vqip, tag="default"):
    """Receives push set requests from arcs and passes request to query handler.

    Args:
        vqip (_type_): the VQIP push request
        tag (str, optional): optional message to direct query_handler which pull
            function to call. Defaults to 'default'.

    Returns:
        (dict): VQIP not received from query_handler

    Examples:
        water_not_pushed = my_node.push_set(wastewater_vqip)
    """
    return self.query_handler(self.push_set_handler, vqip, tag)

push_set_deny(vqip)

Responds that no water is available to push in a request.

Parameters:

Name Type Description Default
vqip dict

A VQIP amount of water to push

required

Returns:

Name Type Description
vqip dict

Returns the request indicating no water was pushed

Source code in wsimod/nodes/nodes.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
def push_set_deny(self, vqip):
    """Responds that no water is available to push in a request.

    Args:
        vqip (dict): A VQIP amount of water to push

    Returns:
        vqip (dict): Returns the request indicating no water was pushed

    Raises:
        Message when called, since it would usually occur if a model is
        improperly connected
    """
    print("Attempted push set to deny")
    return vqip

query_handler(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').

Parameters:

Name Type Description Default
handler dict

contains all push/pull requests for various tags

required
ip vqip

the vqip request

required
tag str

describes what type of push/pull request should be called

required

Returns:

Type Description
dict

the VQIP reply from push/pull request

Source code in wsimod/nodes/nodes.py
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
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').

    Args:
        handler (dict): contains all push/pull requests for various tags
        ip (vqip): the vqip request
        tag (str): describes what type of push/pull request should be called

    Returns:
        (dict): the VQIP reply from push/pull request

    Raises:
        Message if no functions are defined for tag and if request/check
        function fails
    """
    try:
        return handler[tag](ip)
    except Exception:
        if tag not in handler.keys():
            print("No functions defined for " + tag)
            return handler[tag](ip)
        else:
            print("Some other error")
            return handler[tag](ip)

reinit()

Empty function to be written if reinitialisation capability is added.

Source code in wsimod/nodes/nodes.py
627
628
629
def reinit(self):
    """Empty function to be written if reinitialisation capability is added."""
    pass

total_in()

Sum flow and pollutant amounts entering a node via in_arcs.

Returns:

Name Type Description
in_ dict

Summed VQIP of in_arcs

Examples:

>>> node_inflow = my_node.total_in()
Source code in wsimod/nodes/nodes.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def total_in(self):
    """Sum flow and pollutant amounts entering a node via in_arcs.

    Returns:
        in_ (dict): Summed VQIP of in_arcs

    Examples:
        >>> node_inflow = my_node.total_in()
    """
    in_ = self.empty_vqip()
    for arc in self.in_arcs.values():
        in_ = self.sum_vqip(in_, arc.vqip_out)

    return in_

total_out()

Sum flow and pollutant amounts leaving a node via out_arcs.

Returns:

Name Type Description
out_ dict

Summed VQIP of out_arcs

Examples:

>>> node_outflow = my_node.total_out()
Source code in wsimod/nodes/nodes.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def total_out(self):
    """Sum flow and pollutant amounts leaving a node via out_arcs.

    Returns:
        out_ (dict): Summed VQIP of out_arcs

    Examples:
        >>> node_outflow = my_node.total_out()
    """
    out_ = self.empty_vqip()
    for arc in self.out_arcs.values():
        out_ = self.sum_vqip(out_, arc.vqip_in)

    return out_

QueueTank

Bases: Tank

Source code in wsimod/nodes/nodes.py
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
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.

        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

        super().__init__(**kwargs)
        self.end_timestep = self._end_timestep
        self.active_storage = self.copy_vqip(self.storage)

        # TODO enable queue to be initialised not empty
        self.out_arcs = {}
        self.in_arcs = {}
        # Create internal queue arc
        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?)

    def get_avail(self):
        """Return the active_storage of the tank.

        Returns:
            (dict): VQIP of active_storage
        """
        return self.copy_vqip(self.active_storage)

    def push_storage(self, vqip, time=0, force=False):
        """Push storage into QueueTank, applying travel time, unless forced.

        Args:
            vqip (dict): A VQIP of the amount to push
            time (int, optional): Number of timesteps to spend in queue, in addition
                to number_of_timesteps property of internal_arc. Defaults to 0.
            force (bool, optional): Force property that will ignore tank capacity
                and ignore travel time. Defaults to 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
            self.storage = self.sum_vqip(self.storage, vqip)
            self.active_storage = self.sum_vqip(self.active_storage, vqip)
            return self.empty_vqip()

        # Push to QueueTank
        reply = self.internal_arc.send_push_request(vqip, force=force, time=time)
        # Update storage
        # TODO storage won't be accurately tracking temperature..
        self.storage = self.sum_vqip(
            self.storage, self.v_change_vqip(vqip, vqip["volume"] - reply["volume"])
        )
        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).

        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"])

        # Update reply to vqip
        reply = self.v_change_vqip(self.active_storage, reply)

        # Extract from active_storage
        self.active_storage = self.extract_vqip(self.active_storage, reply)

        # Extract from storage
        self.storage = self.extract_vqip(self.storage, reply)

        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.

        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)
        for pol in ["volume"] + constants.ADDITIVE_POLLUTANTS:
            reply[pol] = min(reply[pol], self.active_storage[pol])

        # Pull from QueueTank
        self.active_storage = self.extract_vqip(self.active_storage, reply)

        # Extract from storage
        self.storage = self.extract_vqip(self.storage, reply)
        return reply

    def push_check(self, vqip=None, tag="default"):
        """Wrapper for get_excess but applies comparison to volume in VQIP.
        Needed to enable use of internal_arc, which assumes it is connecting nodes .
        rather than tanks.
        NOTE: this is intended only for use with the internal_arc. Pushing to
        QueueTanks should use 'push_storage'.

        Args:
            vqip (dict, optional): VQIP amount to push. Defaults to None.
            tag (str, optional): Tag, see Node, don't think it should actually be
                used for a QueueTank since there are no handlers. Defaults to
                'default'.

        Returns:
            excess (dict): a VQIP amount of excess capacity
        """
        # TODO does behaviour for volume = None need to be defined?
        excess = self.get_excess()
        if vqip is not None:
            excess["volume"] = min(vqip["volume"], excess["volume"])
        return excess

    def push_set(self, vqip, tag="default"):
        """Behaves differently from normal push setting, it assumes sufficient tank
        capacity and receives VQIPs that have reached the END of the internal_arc.
        NOTE: this is intended only for use with the internal_arc. Pushing to
        QueueTanks should use 'push_storage'.

        Args:
            vqip (dict): VQIP amount to push
            tag (str, optional): Tag, see Node, don't think it should actually be
                used for a QueueTank since there are no handlers. Defaults to
                'default'.

        Returns:
            (dict): Returns empty VQIP, indicating all water received (since it
                assumes capacity was checked before entering the internal arc)
        """
        # Update active_storage (since it has reached the end of the internal_arc)
        self.active_storage = self.sum_vqip(self.active_storage, vqip)

        return self.empty_vqip()

    def _end_timestep(self):
        """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)

    def reinit(self):
        """Zeros storages and arc."""
        self.internal_arc.reinit()
        self.storage = self.empty_vqip()
        self.storage_ = self.empty_vqip()
        self.active_storage = self.empty_vqip()

__init__(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.

Parameters:

Name Type Description Default
number_of_timesteps int

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.

0
Source code in wsimod/nodes/nodes.py
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
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.

    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

    super().__init__(**kwargs)
    self.end_timestep = self._end_timestep
    self.active_storage = self.copy_vqip(self.storage)

    # TODO enable queue to be initialised not empty
    self.out_arcs = {}
    self.in_arcs = {}
    # Create internal queue arc
    self.internal_arc = AltQueueArc(
        in_port=self, out_port=self, number_of_timesteps=self.number_of_timesteps
    )

get_avail()

Return the active_storage of the tank.

Returns:

Type Description
dict

VQIP of active_storage

Source code in wsimod/nodes/nodes.py
1183
1184
1185
1186
1187
1188
1189
def get_avail(self):
    """Return the active_storage of the tank.

    Returns:
        (dict): VQIP of active_storage
    """
    return self.copy_vqip(self.active_storage)

pull_storage(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).

Parameters:

Name Type Description Default
vqip dict

VQIP amount to pull, only 'volume' property is used

required

Returns:

Name Type Description
reply dict

VQIP amount that was pulled

Source code in wsimod/nodes/nodes.py
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
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).

    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"])

    # Update reply to vqip
    reply = self.v_change_vqip(self.active_storage, reply)

    # Extract from active_storage
    self.active_storage = self.extract_vqip(self.active_storage, reply)

    # Extract from storage
    self.storage = self.extract_vqip(self.storage, reply)

    return reply

pull_storage_exact(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.

Parameters:

Name Type Description Default
vqip dict

A VQIP amount to pull

required

Returns:

Name Type Description
reply dict

A VQIP amount successfully pulled

Source code in wsimod/nodes/nodes.py
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
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.

    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)
    for pol in ["volume"] + constants.ADDITIVE_POLLUTANTS:
        reply[pol] = min(reply[pol], self.active_storage[pol])

    # Pull from QueueTank
    self.active_storage = self.extract_vqip(self.active_storage, reply)

    # Extract from storage
    self.storage = self.extract_vqip(self.storage, reply)
    return reply

push_check(vqip=None, tag='default')

Wrapper for get_excess but applies comparison to volume in VQIP. Needed to enable use of internal_arc, which assumes it is connecting nodes . rather than tanks. NOTE: this is intended only for use with the internal_arc. Pushing to QueueTanks should use 'push_storage'.

Parameters:

Name Type Description Default
vqip dict

VQIP amount to push. Defaults to None.

None
tag str

Tag, see Node, don't think it should actually be used for a QueueTank since there are no handlers. Defaults to 'default'.

'default'

Returns:

Name Type Description
excess dict

a VQIP amount of excess capacity

Source code in wsimod/nodes/nodes.py
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
def push_check(self, vqip=None, tag="default"):
    """Wrapper for get_excess but applies comparison to volume in VQIP.
    Needed to enable use of internal_arc, which assumes it is connecting nodes .
    rather than tanks.
    NOTE: this is intended only for use with the internal_arc. Pushing to
    QueueTanks should use 'push_storage'.

    Args:
        vqip (dict, optional): VQIP amount to push. Defaults to None.
        tag (str, optional): Tag, see Node, don't think it should actually be
            used for a QueueTank since there are no handlers. Defaults to
            'default'.

    Returns:
        excess (dict): a VQIP amount of excess capacity
    """
    # TODO does behaviour for volume = None need to be defined?
    excess = self.get_excess()
    if vqip is not None:
        excess["volume"] = min(vqip["volume"], excess["volume"])
    return excess

push_set(vqip, tag='default')

Behaves differently from normal push setting, it assumes sufficient tank capacity and receives VQIPs that have reached the END of the internal_arc. NOTE: this is intended only for use with the internal_arc. Pushing to QueueTanks should use 'push_storage'.

Parameters:

Name Type Description Default
vqip dict

VQIP amount to push

required
tag str

Tag, see Node, don't think it should actually be used for a QueueTank since there are no handlers. Defaults to 'default'.

'default'

Returns:

Type Description
dict

Returns empty VQIP, indicating all water received (since it assumes capacity was checked before entering the internal arc)

Source code in wsimod/nodes/nodes.py
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
def push_set(self, vqip, tag="default"):
    """Behaves differently from normal push setting, it assumes sufficient tank
    capacity and receives VQIPs that have reached the END of the internal_arc.
    NOTE: this is intended only for use with the internal_arc. Pushing to
    QueueTanks should use 'push_storage'.

    Args:
        vqip (dict): VQIP amount to push
        tag (str, optional): Tag, see Node, don't think it should actually be
            used for a QueueTank since there are no handlers. Defaults to
            'default'.

    Returns:
        (dict): Returns empty VQIP, indicating all water received (since it
            assumes capacity was checked before entering the internal arc)
    """
    # Update active_storage (since it has reached the end of the internal_arc)
    self.active_storage = self.sum_vqip(self.active_storage, vqip)

    return self.empty_vqip()

push_storage(vqip, time=0, force=False)

Push storage into QueueTank, applying travel time, unless forced.

Parameters:

Name Type Description Default
vqip dict

A VQIP of the amount to push

required
time int

Number of timesteps to spend in queue, in addition to number_of_timesteps property of internal_arc. Defaults to 0.

0
force bool

Force property that will ignore tank capacity and ignore travel time. Defaults to False.

False

Returns:

Name Type Description
reply dict

A VQIP of water that could not be received by the tank

Source code in wsimod/nodes/nodes.py
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
def push_storage(self, vqip, time=0, force=False):
    """Push storage into QueueTank, applying travel time, unless forced.

    Args:
        vqip (dict): A VQIP of the amount to push
        time (int, optional): Number of timesteps to spend in queue, in addition
            to number_of_timesteps property of internal_arc. Defaults to 0.
        force (bool, optional): Force property that will ignore tank capacity
            and ignore travel time. Defaults to 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
        self.storage = self.sum_vqip(self.storage, vqip)
        self.active_storage = self.sum_vqip(self.active_storage, vqip)
        return self.empty_vqip()

    # Push to QueueTank
    reply = self.internal_arc.send_push_request(vqip, force=force, time=time)
    # Update storage
    # TODO storage won't be accurately tracking temperature..
    self.storage = self.sum_vqip(
        self.storage, self.v_change_vqip(vqip, vqip["volume"] - reply["volume"])
    )
    return reply

reinit()

Zeros storages and arc.

Source code in wsimod/nodes/nodes.py
1315
1316
1317
1318
1319
1320
def reinit(self):
    """Zeros storages and arc."""
    self.internal_arc.reinit()
    self.storage = self.empty_vqip()
    self.storage_ = self.empty_vqip()
    self.active_storage = self.empty_vqip()

ResidenceTank

Bases: Tank

Source code in wsimod/nodes/nodes.py
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
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.

        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)

    def pull_outflow(self):
        """Pull storage by residence time from the tank, updating tank storage.

        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
        # Update pollutant amounts
        outflow = self.v_change_vqip(self.storage, outflow)
        # Remove from tank
        outflow = self.pull_storage(outflow)
        return outflow

__init__(residence_time=2, **kwargs)

A tank that has a residence time property that limits storage pulled from the 'pull_outflow' function.

Parameters:

Name Type Description Default
residence_time float

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.

2
Source code in wsimod/nodes/nodes.py
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
def __init__(self, residence_time=2, **kwargs):
    """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)

pull_outflow()

Pull storage by residence time from the tank, updating tank storage.

Returns:

Name Type Description
outflow dict

A VQIP with volume of pulled volume and pollutants proportionate to the tank's pollutants

Source code in wsimod/nodes/nodes.py
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
def pull_outflow(self):
    """Pull storage by residence time from the tank, updating tank storage.

    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
    # Update pollutant amounts
    outflow = self.v_change_vqip(self.storage, outflow)
    # Remove from tank
    outflow = self.pull_storage(outflow)
    return outflow

Tank

Bases: WSIObj

Source code in wsimod/nodes/nodes.py
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
class Tank(WSIObj):
    """"""

    def __init__(self, capacity=0, area=1, datum=10, initial_storage=0):
        """A standard storage object.

        Args:
            capacity (float, optional): Volumetric tank capacity. Defaults to 0.
            area (float, optional): Area of tank. Defaults to 1.
            datum (float, optional): Datum of tank base (not currently used in any
                functions). Defaults to 10.
            initial_storage (optional): Initial storage for tank.
                float: Tank will be initialised with zero pollutants and the float
                    as volume
                dict: Tank will be initialised with this VQIP
                Defaults to 0 (i.e., no volume, no pollutants).
        """
        # Set parameters
        self.capacity = capacity
        self.area = area
        self.datum = datum
        self.initial_storage = initial_storage

        WSIObj.__init__(self)  # Not sure why I do this rather than super()

        # TODO I don't think the outer if statement is needed
        if "initial_storage" in dir(self):
            if isinstance(self.initial_storage, dict):
                # Assume dict is VQIP describing storage
                self.storage = self.copy_vqip(self.initial_storage)
                self.storage_ = self.copy_vqip(
                    self.initial_storage
                )  # Lagged storage for mass balance
            else:
                # Assume number describes initial stroage
                self.storage = self.v_change_vqip(
                    self.empty_vqip(), self.initial_storage
                )
                self.storage_ = self.v_change_vqip(
                    self.empty_vqip(), self.initial_storage
                )  # Lagged storage for mass balance
        else:
            self.storage = self.empty_vqip()
            self.storage_ = self.empty_vqip()  # Lagged storage for mass balance

    def ds(self):
        """Should be called by parent object to get change in storage.

        Returns:
            (dict): Change in storage
        """
        return self.ds_vqip(self.storage, self.storage_)

    def pull_ponded(self):
        """Pull any volume that is above the tank's capacity.

        Returns:
            ponded (vqip): Amount of ponded water that has been removed from the
                tank

        Examples:
            >>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
            >>> 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())
            {'volume' : 1, 'phosphate' : 0.02}
            >>> print(my_tank.storage)
            {'volume' : 9, 'phosphate' : 0.18}
        """
        # Get amount
        ponded = max(self.storage["volume"] - self.capacity, 0)
        # Pull from tank
        ponded = self.pull_storage({"volume": ponded})
        return ponded

    def get_avail(self, vqip=None):
        """Get minimum of the amount of water in storage and vqip (if provided).

        Args:
            vqip (dict, optional): Maximum water required (only 'volume' is used).
                Defaults to None.

        Returns:
            reply (dict): Water available

        Examples:
            >>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
            >>> 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())
            {'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:
            # Return storage
            return reply
        else:
            # Adjust storage pollutants to match volume in vqip
            reply = self.v_change_vqip(reply, min(reply["volume"], vqip["volume"]))
            return reply

    def get_excess(self, vqip=None):
        """Get difference between current storage and tank capacity.

        Args:
            vqip (dict, optional): Maximum capacity required (only 'volume' is
                used). Defaults to None.

        Returns:
            (dict): Difference available

        Examples:
            >>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
            >>> 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}))
            {'volume' : 2, 'phosphate' : 0.08}
        """
        vol = max(self.capacity - self.storage["volume"], 0)
        if vqip is not 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)
        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.

        Args:
            vqip (dict): VQIP amount to be pushed
            force (bool, optional): Argument used to cause function to ignore tank
                capacity, possibly resulting in pooling. Defaults to False.

        Returns:
            reply (dict): A VQIP of water not successfully pushed to the tank

        Examples:
            >>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
            >>> constants.POLLUTANTS = ['phosphate']
            >>> constants.NON_ADDITIVE_POLLUTANTS = []
            >>> 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)
            {'volume' : 6, 'phosphate' : 0.3}
            >>> print(my_tank.storage)
            {'volume': 9.0, 'phosphate': 0.4}
            >>> print(my_tank.push_storage(reply, force = True))
            {'phosphate': 0, 'volume': 0}
            >>> print(my_tank.storage)
            {'volume': 15.0, 'phosphate': 0.7}
        """
        if force:
            # Directly add request to storage
            self.storage = self.sum_vqip(self.storage, vqip)
            return self.empty_vqip()

        # Check whether request can be met
        excess = self.get_excess()["volume"]

        # Adjust accordingly
        reply = max(vqip["volume"] - excess, 0)
        reply = self.v_change_vqip(vqip, reply)
        entered = self.v_change_vqip(vqip, vqip["volume"] - reply["volume"])

        # Update storage
        self.storage = self.sum_vqip(self.storage, entered)

        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).

        Args:
            vqip (dict): VQIP amount to be pulled, (only 'volume' key is needed)

        Returns:
            reply (dict): A VQIP water successfully pulled from the tank

        Examples:
            >>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
            >>> 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)
            {'volume': 0, 'phosphate': 0}
        """
        # Pull from Tank by volume (taking pollutants in proportion)
        if self.storage["volume"] == 0:
            return self.empty_vqip()

        # Adjust based on available volume
        reply = min(vqip["volume"], self.storage["volume"])

        # Update reply to vqip (in proportion to concentration in storage)
        reply = self.v_change_vqip(self.storage, reply)

        # Extract from storage
        self.storage = self.extract_vqip(self.storage, reply)

        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.

        Args:
            vqip (dict): VQIP amount to be pulled

        Returns:
            vqip (dict): A VQIP water successfully pulled from the tank

        Examples:
            >>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
            >>> 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)
            {'volume': 3, 'phosphate': 0.05}
        """
        # Adjust based on available mass
        for pol in constants.ADDITIVE_POLLUTANTS + ["volume"]:
            vqip[pol] = min(self.storage[pol], vqip[pol])

        # Extract from storage
        self.storage = self.extract_vqip(self.storage, 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.

        Args:
            datum (float, optional): Value to add to pressure head in tank.
                Defaults to None.
            non_head_storage (float, optional): Amount of storage that does
                not contribute to generation of head. The tank must exceed
                this value to generate any pressure head. Defaults to 0.

        Returns:
            head (float): Total head in tank

        Examples:
            >>> my_tank = Tank(datum = 10, initial_storage = 5, capacity = 10, area = 2)
            >>> print(my_tank.get_head())
            12.5
            >>> print(my_tank.get_head(non_head_storage = 1))
            12
            >>> print(my_tank.get_head(non_head_storage = 1, datum = 0))
            2
        """
        # If datum not provided use object datum
        if datum is None:
            datum = self.datum

        # Calculate pressure head generating storage
        head_storage = max(self.storage["volume"] - non_head_storage, 0)

        # Perform head calculation
        head = head_storage / self.area + datum

        return head

    def evaporate(self, evap):
        """Wrapper for v_distill_vqip to apply a volumetric subtraction from tank
        storage. Volume removed from storage and no change in pollutant values.

        Args:
            evap (float): Volume to evaporate

        Returns:
            evap (float): Volumetric amount of evaporation successfully removed
        """
        avail = self.get_avail()["volume"]

        evap = min(evap, avail)
        self.storage = self.v_distill_vqip(self.storage, evap)
        return evap

    ##Old function no longer needed (check it is not used anywhere and remove)
    def push_total(self, vqip):
        """

        Args:
            vqip:

        Returns:

        """
        self.storage = self.sum_vqip(self.storage, vqip)
        return self.empty_vqip()

    ##Old function no longer needed (check it is not used anywhere and remove)
    def push_total_c(self, vqip):
        """

        Args:
            vqip:

        Returns:

        """
        # 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()

    def end_timestep(self):
        """Function to be called by parent object, tracks previously timestep's
        storage."""
        self.storage_ = self.copy_vqip(self.storage)

    def reinit(self):
        """Set storage to an empty VQIP."""
        self.storage = self.empty_vqip()
        self.storage_ = self.empty_vqip()

__init__(capacity=0, area=1, datum=10, initial_storage=0)

A standard storage object.

Parameters:

Name Type Description Default
capacity float

Volumetric tank capacity. Defaults to 0.

0
area float

Area of tank. Defaults to 1.

1
datum float

Datum of tank base (not currently used in any functions). Defaults to 10.

10
initial_storage optional

Initial storage for tank. float: Tank will be initialised with zero pollutants and the float as volume dict: Tank will be initialised with this VQIP Defaults to 0 (i.e., no volume, no pollutants).

0
Source code in wsimod/nodes/nodes.py
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
def __init__(self, capacity=0, area=1, datum=10, initial_storage=0):
    """A standard storage object.

    Args:
        capacity (float, optional): Volumetric tank capacity. Defaults to 0.
        area (float, optional): Area of tank. Defaults to 1.
        datum (float, optional): Datum of tank base (not currently used in any
            functions). Defaults to 10.
        initial_storage (optional): Initial storage for tank.
            float: Tank will be initialised with zero pollutants and the float
                as volume
            dict: Tank will be initialised with this VQIP
            Defaults to 0 (i.e., no volume, no pollutants).
    """
    # Set parameters
    self.capacity = capacity
    self.area = area
    self.datum = datum
    self.initial_storage = initial_storage

    WSIObj.__init__(self)  # Not sure why I do this rather than super()

    # TODO I don't think the outer if statement is needed
    if "initial_storage" in dir(self):
        if isinstance(self.initial_storage, dict):
            # Assume dict is VQIP describing storage
            self.storage = self.copy_vqip(self.initial_storage)
            self.storage_ = self.copy_vqip(
                self.initial_storage
            )  # Lagged storage for mass balance
        else:
            # Assume number describes initial stroage
            self.storage = self.v_change_vqip(
                self.empty_vqip(), self.initial_storage
            )
            self.storage_ = self.v_change_vqip(
                self.empty_vqip(), self.initial_storage
            )  # Lagged storage for mass balance
    else:
        self.storage = self.empty_vqip()
        self.storage_ = self.empty_vqip()  # Lagged storage for mass balance

ds()

Should be called by parent object to get change in storage.

Returns:

Type Description
dict

Change in storage

Source code in wsimod/nodes/nodes.py
787
788
789
790
791
792
793
def ds(self):
    """Should be called by parent object to get change in storage.

    Returns:
        (dict): Change in storage
    """
    return self.ds_vqip(self.storage, self.storage_)

end_timestep()

Function to be called by parent object, tracks previously timestep's storage.

Source code in wsimod/nodes/nodes.py
1065
1066
1067
1068
def end_timestep(self):
    """Function to be called by parent object, tracks previously timestep's
    storage."""
    self.storage_ = self.copy_vqip(self.storage)

evaporate(evap)

Wrapper for v_distill_vqip to apply a volumetric subtraction from tank storage. Volume removed from storage and no change in pollutant values.

Parameters:

Name Type Description Default
evap float

Volume to evaporate

required

Returns:

Name Type Description
evap float

Volumetric amount of evaporation successfully removed

Source code in wsimod/nodes/nodes.py
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
def evaporate(self, evap):
    """Wrapper for v_distill_vqip to apply a volumetric subtraction from tank
    storage. Volume removed from storage and no change in pollutant values.

    Args:
        evap (float): Volume to evaporate

    Returns:
        evap (float): Volumetric amount of evaporation successfully removed
    """
    avail = self.get_avail()["volume"]

    evap = min(evap, avail)
    self.storage = self.v_distill_vqip(self.storage, evap)
    return evap

get_avail(vqip=None)

Get minimum of the amount of water in storage and vqip (if provided).

Parameters:

Name Type Description Default
vqip dict

Maximum water required (only 'volume' is used). Defaults to None.

None

Returns:

Name Type Description
reply dict

Water available

Examples:

>>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
>>> 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())
{'volume' : 10, 'phosphate' : 0.2}
>>> print(my_tank.get_avail({'volume' : 1}))
{'volume' : 1, 'phosphate' : 0.02}
Source code in wsimod/nodes/nodes.py
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
def get_avail(self, vqip=None):
    """Get minimum of the amount of water in storage and vqip (if provided).

    Args:
        vqip (dict, optional): Maximum water required (only 'volume' is used).
            Defaults to None.

    Returns:
        reply (dict): Water available

    Examples:
        >>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
        >>> 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())
        {'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:
        # Return storage
        return reply
    else:
        # Adjust storage pollutants to match volume in vqip
        reply = self.v_change_vqip(reply, min(reply["volume"], vqip["volume"]))
        return reply

get_excess(vqip=None)

Get difference between current storage and tank capacity.

Parameters:

Name Type Description Default
vqip dict

Maximum capacity required (only 'volume' is used). Defaults to None.

None

Returns:

Type Description
dict

Difference available

Examples:

>>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
>>> 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}))
{'volume' : 2, 'phosphate' : 0.08}
Source code in wsimod/nodes/nodes.py
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
def get_excess(self, vqip=None):
    """Get difference between current storage and tank capacity.

    Args:
        vqip (dict, optional): Maximum capacity required (only 'volume' is
            used). Defaults to None.

    Returns:
        (dict): Difference available

    Examples:
        >>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
        >>> 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}))
        {'volume' : 2, 'phosphate' : 0.08}
    """
    vol = max(self.capacity - self.storage["volume"], 0)
    if vqip is not 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)
    return self.v_change_vqip(self.storage, vol)

get_head(datum=None, non_head_storage=0)

Area volume calculation for head calcuations. Datum and storage that does not contribute to head can be specified.

Parameters:

Name Type Description Default
datum float

Value to add to pressure head in tank. Defaults to None.

None
non_head_storage float

Amount of storage that does not contribute to generation of head. The tank must exceed this value to generate any pressure head. Defaults to 0.

0

Returns:

Name Type Description
head float

Total head in tank

Examples:

>>> my_tank = Tank(datum = 10, initial_storage = 5, capacity = 10, area = 2)
>>> print(my_tank.get_head())
12.5
>>> print(my_tank.get_head(non_head_storage = 1))
12
>>> print(my_tank.get_head(non_head_storage = 1, datum = 0))
2
Source code in wsimod/nodes/nodes.py
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
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.

    Args:
        datum (float, optional): Value to add to pressure head in tank.
            Defaults to None.
        non_head_storage (float, optional): Amount of storage that does
            not contribute to generation of head. The tank must exceed
            this value to generate any pressure head. Defaults to 0.

    Returns:
        head (float): Total head in tank

    Examples:
        >>> my_tank = Tank(datum = 10, initial_storage = 5, capacity = 10, area = 2)
        >>> print(my_tank.get_head())
        12.5
        >>> print(my_tank.get_head(non_head_storage = 1))
        12
        >>> print(my_tank.get_head(non_head_storage = 1, datum = 0))
        2
    """
    # If datum not provided use object datum
    if datum is None:
        datum = self.datum

    # Calculate pressure head generating storage
    head_storage = max(self.storage["volume"] - non_head_storage, 0)

    # Perform head calculation
    head = head_storage / self.area + datum

    return head

pull_pollutants(vqip)

Pull water from tank, updating the storage VQIP. Pollutants are removed from tank in according to their values in vqip.

Parameters:

Name Type Description Default
vqip dict

VQIP amount to be pulled

required

Returns:

Name Type Description
vqip dict

A VQIP water successfully pulled from the tank

Examples:

>>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
>>> 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)
{'volume': 3, 'phosphate': 0.05}
Source code in wsimod/nodes/nodes.py
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
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.

    Args:
        vqip (dict): VQIP amount to be pulled

    Returns:
        vqip (dict): A VQIP water successfully pulled from the tank

    Examples:
        >>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
        >>> 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)
        {'volume': 3, 'phosphate': 0.05}
    """
    # Adjust based on available mass
    for pol in constants.ADDITIVE_POLLUTANTS + ["volume"]:
        vqip[pol] = min(self.storage[pol], vqip[pol])

    # Extract from storage
    self.storage = self.extract_vqip(self.storage, vqip)
    return vqip

pull_ponded()

Pull any volume that is above the tank's capacity.

Returns:

Name Type Description
ponded vqip

Amount of ponded water that has been removed from the tank

Examples:

>>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
>>> 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())
{'volume' : 1, 'phosphate' : 0.02}
>>> print(my_tank.storage)
{'volume' : 9, 'phosphate' : 0.18}
Source code in wsimod/nodes/nodes.py
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
def pull_ponded(self):
    """Pull any volume that is above the tank's capacity.

    Returns:
        ponded (vqip): Amount of ponded water that has been removed from the
            tank

    Examples:
        >>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
        >>> 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())
        {'volume' : 1, 'phosphate' : 0.02}
        >>> print(my_tank.storage)
        {'volume' : 9, 'phosphate' : 0.18}
    """
    # Get amount
    ponded = max(self.storage["volume"] - self.capacity, 0)
    # Pull from tank
    ponded = self.pull_storage({"volume": ponded})
    return ponded

pull_storage(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).

Parameters:

Name Type Description Default
vqip dict

VQIP amount to be pulled, (only 'volume' key is needed)

required

Returns:

Name Type Description
reply dict

A VQIP water successfully pulled from the tank

Examples:

>>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
>>> 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)
{'volume': 0, 'phosphate': 0}
Source code in wsimod/nodes/nodes.py
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
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).

    Args:
        vqip (dict): VQIP amount to be pulled, (only 'volume' key is needed)

    Returns:
        reply (dict): A VQIP water successfully pulled from the tank

    Examples:
        >>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
        >>> 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)
        {'volume': 0, 'phosphate': 0}
    """
    # Pull from Tank by volume (taking pollutants in proportion)
    if self.storage["volume"] == 0:
        return self.empty_vqip()

    # Adjust based on available volume
    reply = min(vqip["volume"], self.storage["volume"])

    # Update reply to vqip (in proportion to concentration in storage)
    reply = self.v_change_vqip(self.storage, reply)

    # Extract from storage
    self.storage = self.extract_vqip(self.storage, reply)

    return reply

push_storage(vqip, force=False)

Push water into tank, updating the storage VQIP. Force argument can be used to ignore tank capacity.

Parameters:

Name Type Description Default
vqip dict

VQIP amount to be pushed

required
force bool

Argument used to cause function to ignore tank capacity, possibly resulting in pooling. Defaults to False.

False

Returns:

Name Type Description
reply dict

A VQIP of water not successfully pushed to the tank

Examples:

>>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
>>> constants.POLLUTANTS = ['phosphate']
>>> constants.NON_ADDITIVE_POLLUTANTS = []
>>> 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)
{'volume' : 6, 'phosphate' : 0.3}
>>> print(my_tank.storage)
{'volume': 9.0, 'phosphate': 0.4}
>>> print(my_tank.push_storage(reply, force = True))
{'phosphate': 0, 'volume': 0}
>>> print(my_tank.storage)
{'volume': 15.0, 'phosphate': 0.7}
Source code in wsimod/nodes/nodes.py
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
def push_storage(self, vqip, force=False):
    """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
        force (bool, optional): Argument used to cause function to ignore tank
            capacity, possibly resulting in pooling. Defaults to False.

    Returns:
        reply (dict): A VQIP of water not successfully pushed to the tank

    Examples:
        >>> constants.ADDITIVE_POLLUTANTS = ['phosphate']
        >>> constants.POLLUTANTS = ['phosphate']
        >>> constants.NON_ADDITIVE_POLLUTANTS = []
        >>> 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)
        {'volume' : 6, 'phosphate' : 0.3}
        >>> print(my_tank.storage)
        {'volume': 9.0, 'phosphate': 0.4}
        >>> print(my_tank.push_storage(reply, force = True))
        {'phosphate': 0, 'volume': 0}
        >>> print(my_tank.storage)
        {'volume': 15.0, 'phosphate': 0.7}
    """
    if force:
        # Directly add request to storage
        self.storage = self.sum_vqip(self.storage, vqip)
        return self.empty_vqip()

    # Check whether request can be met
    excess = self.get_excess()["volume"]

    # Adjust accordingly
    reply = max(vqip["volume"] - excess, 0)
    reply = self.v_change_vqip(vqip, reply)
    entered = self.v_change_vqip(vqip, vqip["volume"] - reply["volume"])

    # Update storage
    self.storage = self.sum_vqip(self.storage, entered)

    return reply

push_total(vqip)

Parameters:

Name Type Description Default
vqip
required

Returns:

Source code in wsimod/nodes/nodes.py
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
def push_total(self, vqip):
    """

    Args:
        vqip:

    Returns:

    """
    self.storage = self.sum_vqip(self.storage, vqip)
    return self.empty_vqip()

push_total_c(vqip)

Parameters:

Name Type Description Default
vqip
required

Returns:

Source code in wsimod/nodes/nodes.py
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
def push_total_c(self, vqip):
    """

    Args:
        vqip:

    Returns:

    """
    # 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()

reinit()

Set storage to an empty VQIP.

Source code in wsimod/nodes/nodes.py
1070
1071
1072
1073
def reinit(self):
    """Set storage to an empty VQIP."""
    self.storage = self.empty_vqip()
    self.storage_ = self.empty_vqip()