Skip to content

API Reference - WTW

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

Created on Mon Nov 15 14:20:36 2021.

@author: bdobson Converted to totals on 2022-05-03

FWTW

Bases: WTW

Source code in wsimod/nodes/wtw.py
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
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
class FWTW(WTW):
    """"""

    def __init__(
        self,
        service_reservoir_storage_capacity=10,
        service_reservoir_storage_area=1,
        service_reservoir_storage_elevation=10,
        service_reservoir_initial_storage=0,
        data_input_dict={},
        **kwargs,
    ):
        """A freshwater treatment works wrapper for WTW. Contains service reservoirs
        that treated water is released to and pulled from. Cannot allow deficit (thus
        any deficit is satisfied by water entering the model 'via other means'). Liquor
        and solids are sent to sewers.

        Args:
            service_reservoir_storage_capacity (float, optional): Capacity of service
                reservoirs. Defaults to 10.
            service_reservoir_storage_area (float, optional): Area of service
                reservoirs. Defaults to 1.
            service_reservoir_storage_elevation (float, optional): Datum of service
                reservoirs. Defaults to 10.
            service_reservoir_initial_storage (float or dict, optional): initial
                storage of service reservoirs (see nodes.py/Tank for details).
                Defaults to 0.
            data_input_dict (dict, optional): Dictionary of data inputs relevant for
                the node (though I don't think it is used). Defaults to {}.

        Functions intended to call in orchestration:
            treat_water

        Key assumptions:
            - See `wtw.py/WTW` for treatment.
            - Stores treated water in a service reservoir tank, with a single tank
                per `FWTW` node.
            - Aims to satisfy a throughput that would top up the service reservoirs
                until full.
            - Currently, will not allow a deficit, thus introducing water from
                'other measures' if pulls cannot fulfil demand. Behaviour under a
                deficit should be determined and validated before introducing.

        Input data and parameter requirements:
            - See `wtw.py/WTW` for treatment.
            - Service reservoir tank `capacity`, `area`, and `datum`.
                _Units_: cubic metres, squared metres, metres
        """
        # Default parameters
        self.service_reservoir_storage_capacity = service_reservoir_storage_capacity
        self.service_reservoir_storage_area = service_reservoir_storage_area
        self.service_reservoir_storage_elevation = service_reservoir_storage_elevation
        self.service_reservoir_initial_storage = service_reservoir_initial_storage
        # TODO don't think data_input_dict is used
        self.data_input_dict = data_input_dict

        # Update args
        super().__init__(**kwargs)
        self.end_timestep = self.end_timestep_

        # Update handlers
        self.pull_set_handler["default"] = self.pull_set_fwtw
        self.pull_check_handler["default"] = self.pull_check_fwtw

        self.push_set_handler["default"] = self.push_set_deny
        self.push_check_handler["default"] = self.push_check_deny

        # Initialise parameters
        self.total_deficit = self.empty_vqip()
        self.total_pulled = self.empty_vqip()
        self.previous_pulled = self.empty_vqip()
        self.unpushed_sludge = self.empty_vqip()

        # Create tanks
        self.service_reservoir_tank = Tank(
            capacity=self.service_reservoir_storage_capacity,
            area=self.service_reservoir_storage_area,
            datum=self.service_reservoir_storage_elevation,
            initial_storage=self.service_reservoir_initial_storage,
        )
        # self.service_reservoir_tank.storage['volume'] =
        # self.service_reservoir_inital_storage
        # self.service_reservoir_tank.storage_['volume'] =
        # self.service_reservoir_inital_storage

        # Mass balance
        self.mass_balance_in.append(lambda: self.total_deficit)
        self.mass_balance_ds.append(lambda: self.service_reservoir_tank.ds())
        self.mass_balance_out.append(lambda: self.unpushed_sludge)

    def apply_overrides(self, overrides=Dict[str, Any]):
        """Apply overrides to the service reservoir tank and FWTW.

        Enables a user to override any parameter of the service reservoir tank, and
        then calls any overrides in WTW.

        Args:
            overrides (Dict[str, Any]): Dict describing which parameters should
                be overridden (keys) and new values (values). Defaults to {}.
        """
        self.service_reservoir_storage_capacity = overrides.pop(
            "service_reservoir_storage_capacity",
            self.service_reservoir_storage_capacity,
        )
        self.service_reservoir_storage_area = overrides.pop(
            "service_reservoir_storage_area", self.service_reservoir_storage_area
        )
        self.service_reservoir_storage_elevation = overrides.pop(
            "service_reservoir_storage_elevation",
            self.service_reservoir_storage_elevation,
        )

        self.service_reservoir_tank.capacity = self.service_reservoir_storage_capacity
        self.service_reservoir_tank.area = self.service_reservoir_storage_area
        self.service_reservoir_tank.datum = self.service_reservoir_storage_elevation
        super().apply_overrides(overrides)

    def treat_water(self):
        """Pulls water, aiming to fill service reservoirs, calls WTW
        treat_current_input, avoids deficit, sends liquor and solids to sewers."""
        # Calculate how much water is needed
        target_throughput = self.service_reservoir_tank.get_excess()
        target_throughput = min(
            target_throughput["volume"], self.treatment_throughput_capacity
        )

        # Pull water
        throughput = self.pull_distributed({"volume": target_throughput})

        # Calculate deficit (assume is equal to difference between previous treated
        # throughput and current throughput)
        # TODO think about this a bit more
        deficit = max(target_throughput - throughput["volume"], 0)
        # deficit = max(self.previous_pulled['volume'] - throughput['volume'], 0)
        deficit = self.v_change_vqip(self.previous_pulled, deficit)

        # Introduce deficit
        self.current_input = self.sum_vqip(throughput, deficit)

        # Track deficit
        self.total_deficit = self.sum_vqip(self.total_deficit, deficit)

        if self.total_deficit["volume"] > constants.FLOAT_ACCURACY:
            print(
                "Service reservoirs not filled at {0} on {1}".format(self.name, self.t)
            )

        # Run treatment processes
        self.treat_current_input()

        # Discharge liquor and solids to sewers
        push_back = self.sum_vqip(self.liquor, self.solids)
        rejected = self.push_distributed(push_back, of_type="Sewer")
        self.unpushed_sludge = self.sum_vqip(self.unpushed_sludge, rejected)
        if rejected["volume"] > constants.FLOAT_ACCURACY:
            print("nowhere for sludge to go")

        # Send water to service reservoirs
        excess = self.service_reservoir_tank.push_storage(self.treated)
        _ = self.service_reservoir_tank.push_storage(excess, force=True)
        if excess["volume"] > 0:
            print("excess treated water")

    def pull_check_fwtw(self, vqip=None):
        """Pull checks query service reservoirs.

        Args:
            vqip (dict, optional): A VQIP that can be used to limit the volume in
                the return value (only volume key is used). Defaults to None.

        Returns:
            (dict): A VQIP of availability in service reservoirs
        """
        return self.service_reservoir_tank.get_avail(vqip)

    def pull_set_fwtw(self, vqip):
        """Pull treated water from service reservoirs.

        Args:
            vqip (dict): a VQIP amount to pull

        Returns:
            pulled (dict): A VQIP amount that was successfully pulled
        """
        # Pull
        pulled = self.service_reservoir_tank.pull_storage(vqip)
        # Update total_pulled this timestep
        self.total_pulled = self.sum_vqip(self.total_pulled, pulled)
        return pulled

    def end_timestep_(self):
        """Update state variables."""
        self.service_reservoir_tank.end_timestep()
        self.total_deficit = self.empty_vqip()
        self.previous_pulled = self.copy_vqip(self.total_pulled)
        self.total_pulled = self.empty_vqip()
        self.treated = self.empty_vqip()
        self.unpushed_sludge = self.empty_vqip()

    def reinit(self):
        """Call tank reinit."""
        self.service_reservoir_tank.reinit()

__init__(service_reservoir_storage_capacity=10, service_reservoir_storage_area=1, service_reservoir_storage_elevation=10, service_reservoir_initial_storage=0, data_input_dict={}, **kwargs)

A freshwater treatment works wrapper for WTW. Contains service reservoirs that treated water is released to and pulled from. Cannot allow deficit (thus any deficit is satisfied by water entering the model 'via other means'). Liquor and solids are sent to sewers.

Parameters:

Name Type Description Default
service_reservoir_storage_capacity float

Capacity of service reservoirs. Defaults to 10.

10
service_reservoir_storage_area float

Area of service reservoirs. Defaults to 1.

1
service_reservoir_storage_elevation float

Datum of service reservoirs. Defaults to 10.

10
service_reservoir_initial_storage float or dict

initial storage of service reservoirs (see nodes.py/Tank for details). Defaults to 0.

0
data_input_dict dict

Dictionary of data inputs relevant for the node (though I don't think it is used). Defaults to {}.

{}
Functions intended to call in orchestration

treat_water

Key assumptions
  • See wtw.py/WTW for treatment.
  • Stores treated water in a service reservoir tank, with a single tank per FWTW node.
  • Aims to satisfy a throughput that would top up the service reservoirs until full.
  • Currently, will not allow a deficit, thus introducing water from 'other measures' if pulls cannot fulfil demand. Behaviour under a deficit should be determined and validated before introducing.
Input data and parameter requirements
  • See wtw.py/WTW for treatment.
  • Service reservoir tank capacity, area, and datum. Units: cubic metres, squared metres, metres
Source code in wsimod/nodes/wtw.py
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
def __init__(
    self,
    service_reservoir_storage_capacity=10,
    service_reservoir_storage_area=1,
    service_reservoir_storage_elevation=10,
    service_reservoir_initial_storage=0,
    data_input_dict={},
    **kwargs,
):
    """A freshwater treatment works wrapper for WTW. Contains service reservoirs
    that treated water is released to and pulled from. Cannot allow deficit (thus
    any deficit is satisfied by water entering the model 'via other means'). Liquor
    and solids are sent to sewers.

    Args:
        service_reservoir_storage_capacity (float, optional): Capacity of service
            reservoirs. Defaults to 10.
        service_reservoir_storage_area (float, optional): Area of service
            reservoirs. Defaults to 1.
        service_reservoir_storage_elevation (float, optional): Datum of service
            reservoirs. Defaults to 10.
        service_reservoir_initial_storage (float or dict, optional): initial
            storage of service reservoirs (see nodes.py/Tank for details).
            Defaults to 0.
        data_input_dict (dict, optional): Dictionary of data inputs relevant for
            the node (though I don't think it is used). Defaults to {}.

    Functions intended to call in orchestration:
        treat_water

    Key assumptions:
        - See `wtw.py/WTW` for treatment.
        - Stores treated water in a service reservoir tank, with a single tank
            per `FWTW` node.
        - Aims to satisfy a throughput that would top up the service reservoirs
            until full.
        - Currently, will not allow a deficit, thus introducing water from
            'other measures' if pulls cannot fulfil demand. Behaviour under a
            deficit should be determined and validated before introducing.

    Input data and parameter requirements:
        - See `wtw.py/WTW` for treatment.
        - Service reservoir tank `capacity`, `area`, and `datum`.
            _Units_: cubic metres, squared metres, metres
    """
    # Default parameters
    self.service_reservoir_storage_capacity = service_reservoir_storage_capacity
    self.service_reservoir_storage_area = service_reservoir_storage_area
    self.service_reservoir_storage_elevation = service_reservoir_storage_elevation
    self.service_reservoir_initial_storage = service_reservoir_initial_storage
    # TODO don't think data_input_dict is used
    self.data_input_dict = data_input_dict

    # Update args
    super().__init__(**kwargs)
    self.end_timestep = self.end_timestep_

    # Update handlers
    self.pull_set_handler["default"] = self.pull_set_fwtw
    self.pull_check_handler["default"] = self.pull_check_fwtw

    self.push_set_handler["default"] = self.push_set_deny
    self.push_check_handler["default"] = self.push_check_deny

    # Initialise parameters
    self.total_deficit = self.empty_vqip()
    self.total_pulled = self.empty_vqip()
    self.previous_pulled = self.empty_vqip()
    self.unpushed_sludge = self.empty_vqip()

    # Create tanks
    self.service_reservoir_tank = Tank(
        capacity=self.service_reservoir_storage_capacity,
        area=self.service_reservoir_storage_area,
        datum=self.service_reservoir_storage_elevation,
        initial_storage=self.service_reservoir_initial_storage,
    )
    # self.service_reservoir_tank.storage['volume'] =
    # self.service_reservoir_inital_storage
    # self.service_reservoir_tank.storage_['volume'] =
    # self.service_reservoir_inital_storage

    # Mass balance
    self.mass_balance_in.append(lambda: self.total_deficit)
    self.mass_balance_ds.append(lambda: self.service_reservoir_tank.ds())
    self.mass_balance_out.append(lambda: self.unpushed_sludge)

apply_overrides(overrides=Dict[str, Any])

Apply overrides to the service reservoir tank and FWTW.

Enables a user to override any parameter of the service reservoir tank, and then calls any overrides in WTW.

Parameters:

Name Type Description Default
overrides Dict[str, Any]

Dict describing which parameters should be overridden (keys) and new values (values). Defaults to {}.

Dict[str, Any]
Source code in wsimod/nodes/wtw.py
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
def apply_overrides(self, overrides=Dict[str, Any]):
    """Apply overrides to the service reservoir tank and FWTW.

    Enables a user to override any parameter of the service reservoir tank, and
    then calls any overrides in WTW.

    Args:
        overrides (Dict[str, Any]): Dict describing which parameters should
            be overridden (keys) and new values (values). Defaults to {}.
    """
    self.service_reservoir_storage_capacity = overrides.pop(
        "service_reservoir_storage_capacity",
        self.service_reservoir_storage_capacity,
    )
    self.service_reservoir_storage_area = overrides.pop(
        "service_reservoir_storage_area", self.service_reservoir_storage_area
    )
    self.service_reservoir_storage_elevation = overrides.pop(
        "service_reservoir_storage_elevation",
        self.service_reservoir_storage_elevation,
    )

    self.service_reservoir_tank.capacity = self.service_reservoir_storage_capacity
    self.service_reservoir_tank.area = self.service_reservoir_storage_area
    self.service_reservoir_tank.datum = self.service_reservoir_storage_elevation
    super().apply_overrides(overrides)

end_timestep_()

Update state variables.

Source code in wsimod/nodes/wtw.py
646
647
648
649
650
651
652
653
def end_timestep_(self):
    """Update state variables."""
    self.service_reservoir_tank.end_timestep()
    self.total_deficit = self.empty_vqip()
    self.previous_pulled = self.copy_vqip(self.total_pulled)
    self.total_pulled = self.empty_vqip()
    self.treated = self.empty_vqip()
    self.unpushed_sludge = self.empty_vqip()

pull_check_fwtw(vqip=None)

Pull checks query service reservoirs.

Parameters:

Name Type Description Default
vqip dict

A VQIP that can be used to limit the volume in the return value (only volume key is used). Defaults to None.

None

Returns:

Type Description
dict

A VQIP of availability in service reservoirs

Source code in wsimod/nodes/wtw.py
619
620
621
622
623
624
625
626
627
628
629
def pull_check_fwtw(self, vqip=None):
    """Pull checks query service reservoirs.

    Args:
        vqip (dict, optional): A VQIP that can be used to limit the volume in
            the return value (only volume key is used). Defaults to None.

    Returns:
        (dict): A VQIP of availability in service reservoirs
    """
    return self.service_reservoir_tank.get_avail(vqip)

pull_set_fwtw(vqip)

Pull treated water from service reservoirs.

Parameters:

Name Type Description Default
vqip dict

a VQIP amount to pull

required

Returns:

Name Type Description
pulled dict

A VQIP amount that was successfully pulled

Source code in wsimod/nodes/wtw.py
631
632
633
634
635
636
637
638
639
640
641
642
643
644
def pull_set_fwtw(self, vqip):
    """Pull treated water from service reservoirs.

    Args:
        vqip (dict): a VQIP amount to pull

    Returns:
        pulled (dict): A VQIP amount that was successfully pulled
    """
    # Pull
    pulled = self.service_reservoir_tank.pull_storage(vqip)
    # Update total_pulled this timestep
    self.total_pulled = self.sum_vqip(self.total_pulled, pulled)
    return pulled

reinit()

Call tank reinit.

Source code in wsimod/nodes/wtw.py
655
656
657
def reinit(self):
    """Call tank reinit."""
    self.service_reservoir_tank.reinit()

treat_water()

Pulls water, aiming to fill service reservoirs, calls WTW treat_current_input, avoids deficit, sends liquor and solids to sewers.

Source code in wsimod/nodes/wtw.py
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
def treat_water(self):
    """Pulls water, aiming to fill service reservoirs, calls WTW
    treat_current_input, avoids deficit, sends liquor and solids to sewers."""
    # Calculate how much water is needed
    target_throughput = self.service_reservoir_tank.get_excess()
    target_throughput = min(
        target_throughput["volume"], self.treatment_throughput_capacity
    )

    # Pull water
    throughput = self.pull_distributed({"volume": target_throughput})

    # Calculate deficit (assume is equal to difference between previous treated
    # throughput and current throughput)
    # TODO think about this a bit more
    deficit = max(target_throughput - throughput["volume"], 0)
    # deficit = max(self.previous_pulled['volume'] - throughput['volume'], 0)
    deficit = self.v_change_vqip(self.previous_pulled, deficit)

    # Introduce deficit
    self.current_input = self.sum_vqip(throughput, deficit)

    # Track deficit
    self.total_deficit = self.sum_vqip(self.total_deficit, deficit)

    if self.total_deficit["volume"] > constants.FLOAT_ACCURACY:
        print(
            "Service reservoirs not filled at {0} on {1}".format(self.name, self.t)
        )

    # Run treatment processes
    self.treat_current_input()

    # Discharge liquor and solids to sewers
    push_back = self.sum_vqip(self.liquor, self.solids)
    rejected = self.push_distributed(push_back, of_type="Sewer")
    self.unpushed_sludge = self.sum_vqip(self.unpushed_sludge, rejected)
    if rejected["volume"] > constants.FLOAT_ACCURACY:
        print("nowhere for sludge to go")

    # Send water to service reservoirs
    excess = self.service_reservoir_tank.push_storage(self.treated)
    _ = self.service_reservoir_tank.push_storage(excess, force=True)
    if excess["volume"] > 0:
        print("excess treated water")

WTW

Bases: Node

A generic Water Treatment Works (WTW) node.

This class is a generic water treatment works node. It is intended to be subclassed into freshwater and wastewater treatment works (FWTW and WWTW respectively).

Source code in wsimod/nodes/wtw.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
class WTW(Node):
    """A generic Water Treatment Works (WTW) node.

    This class is a generic water treatment works node. It is intended to be
    subclassed into freshwater and wastewater treatment works (FWTW and WWTW
    respectively).
    """

    def __init__(
        self,
        name,
        treatment_throughput_capacity=10,
        process_parameters={},
        liquor_multiplier={},
        percent_solids=0.0002,
    ):
        """Generic treatment processes that apply temperature a sensitive transform of
        pollutants into liquor and solids (behaviour depends on subclass). Push requests
        are stored in the current_input state variable, but treatment must be triggered
        with treat_current_input. This treated water is stored in the discharge_holder
        state variable, which will be sent different depending on FWTW/WWTW.

        Args:
            name (str): Node name
            treatment_throughput_capacity (float, optional): Amount of volume per
                timestep of water that can be treated. Defaults to 10.
            process_parameters (dict, optional): Dict of dicts for each pollutant.
                Top level key describes pollutant. Next level key describes the
                constant portion of the transform and the temperature sensitive
                exponent portion (see core.py/DecayObj for more detailed
                explanation). Defaults to {}.
            liquor_multiplier (dict, optional): Keys for each pollutant that
                describes how much influent becomes liquor. Defaults to {}.
            percent_solids (float, optional): Proportion of volume that becomes solids.
                All pollutants that do not become effluent or liquor become solids.
                Defaults to 0.0002.

        Functions intended to call in orchestration:
            None (use FWTW or WWTW subclass)

        Key assumptions:
            - Throughput can be modelled entirely with a set capacity.
            - Pollutant reduction for the entire treatment process can be modelled
                primarily with a single (temperature sensitive) transformation for
                each pollutant.
            - Liquor and solids are tracked and calculated with proportional
                multiplier parameters.

        Input data and parameter requirements:
            - `treatment_throughput_capacity`
                _Units_: cubic metres/timestep
            - `process_parameters` contains the constant (non temperature
                sensitive) and exponent (temperature sensitive) transformations
                applied to treated water for each pollutant.
                _Units_: -
            - `liquor_multiplier` and `percent_solids` describe the proportion of
                throughput that goes to liquor/solids.
        """
        # Set/Default parameters
        self.treatment_throughput_capacity = treatment_throughput_capacity
        if len(process_parameters) > 0:
            self.process_parameters = process_parameters
        else:
            self.process_parameters = {
                x: {"constant": 0.01, "exponent": 1.001}
                for x in constants.ADDITIVE_POLLUTANTS
            }
        if len(liquor_multiplier) > 0:
            self._liquor_multiplier = liquor_multiplier
        else:
            self._liquor_multiplier = {x: 0.7 for x in constants.ADDITIVE_POLLUTANTS}
            self._liquor_multiplier["volume"] = 0.03

        self._percent_solids = percent_solids

        # Update args
        super().__init__(name)

        self.process_parameters["volume"] = {"constant": self.calculate_volume()}

        # Update handlers
        self.push_set_handler["default"] = self.push_set_deny
        self.push_check_handler["default"] = self.push_check_deny

        # Initialise parameters
        self.current_input = self.empty_vqip()
        self.treated = self.empty_vqip()
        self.liquor = self.empty_vqip()
        self.solids = self.empty_vqip()

    def calculate_volume(self):
        """Calculate the volume proportion of treated water.

        Returns:
            (float): Volume of treated water
        """
        return 1 - self._percent_solids - self._liquor_multiplier["volume"]

    @property
    def percent_solids(self):
        return self._percent_solids

    @percent_solids.setter
    def percent_solids(self, value):
        self._percent_solids = value
        self.process_parameters["volume"]["constant"] = self.calculate_volume()

    @property
    def liquor_multiplier(self):
        return self._liquor_multiplier

    @liquor_multiplier.setter
    def liquor_multiplier(self, value):
        self._liquor_multiplier.update(value)
        self.process_parameters["volume"]["constant"] = self.calculate_volume()

    def apply_overrides(self, overrides=Dict[str, Any]):
        """Override parameters.

        Enables a user to override any of the following parameters:
        percent_solids, treatment_throughput_capacity, process_parameters (the
        entire dict does not need to be redefined, only changed values need to
        be included), liquor_multiplier (as with process_parameters).

        Args:
            overrides (Dict[str, Any]): Dict describing which parameters should
                be overridden (keys) and new values (values). Defaults to {}.
        """
        self.percent_solids = overrides.pop("percent_solids", self._percent_solids)
        self.liquor_multiplier = overrides.pop(
            "liquor_multiplier", self._liquor_multiplier
        )
        process_parameters = overrides.pop("process_parameters", {})
        for key, value in process_parameters.items():
            self.process_parameters[key].update(value)

        self.treatment_throughput_capacity = overrides.pop(
            "treatment_throughput_capacity", self.treatment_throughput_capacity
        )
        super().apply_overrides(overrides)

    def get_excess_throughput(self):
        """How much excess treatment capacity is there.

        Returns:
            (float): Amount of volume that can still be treated this timestep
        """
        return max(self.treatment_throughput_capacity - self.current_input["volume"], 0)

    def treat_current_input(self):
        """Run treatment processes this timestep, including temperature sensitive
        transforms, liquoring, solids."""
        # Treat current input
        influent = self.copy_vqip(self.current_input)

        # Calculate effluent, liquor and solids
        discharge_holder = self.empty_vqip()

        # Assume non-additive pollutants are unchanged in discharge and are
        # proportionately mixed in liquor
        for key in constants.NON_ADDITIVE_POLLUTANTS:
            discharge_holder[key] = influent[key]
            self.liquor[key] = (
                self.liquor[key] * self.liquor["volume"]
                + influent[key] * influent["volume"] * self.liquor_multiplier["volume"]
            ) / (
                self.liquor["volume"]
                + influent["volume"] * self.liquor_multiplier["volume"]
            )

        # TODO this should probably just be for process_parameters.keys() to avoid
        # having to declare non changing parameters
        # TODO should the liquoring be temperature sensitive too? As it is the solids
        # will take the brunt of the temperature variability which maybe isn't sensible
        for key in constants.ADDITIVE_POLLUTANTS + ["volume"]:
            if key != "volume":
                # Temperature sensitive transform
                temp_factor = self.process_parameters[key]["exponent"] ** (
                    constants.DECAY_REFERENCE_TEMPERATURE - influent["temperature"]
                )
            else:
                temp_factor = 1
            # Calculate discharge
            discharge_holder[key] = (
                influent[key] * self.process_parameters[key]["constant"] * temp_factor
            )
            # Calculate liquor
            self.liquor[key] = influent[key] * self.liquor_multiplier[key]

        # Calculate solids volume
        self.solids["volume"] = influent["volume"] * self.percent_solids

        # All remaining pollutants go to solids
        for key in constants.ADDITIVE_POLLUTANTS:
            self.solids[key] = influent[key] - discharge_holder[key] - self.liquor[key]

        # Blend with any existing discharge
        self.treated = self.sum_vqip(self.treated, discharge_holder)

        if self.treated["volume"] > self.current_input["volume"]:
            print("more treated than input")

    def end_timestep(self):
        """"""
        # Reset state variables
        self.current_input = self.empty_vqip()
        self.treated = self.empty_vqip()

__init__(name, treatment_throughput_capacity=10, process_parameters={}, liquor_multiplier={}, percent_solids=0.0002)

Generic treatment processes that apply temperature a sensitive transform of pollutants into liquor and solids (behaviour depends on subclass). Push requests are stored in the current_input state variable, but treatment must be triggered with treat_current_input. This treated water is stored in the discharge_holder state variable, which will be sent different depending on FWTW/WWTW.

Parameters:

Name Type Description Default
name str

Node name

required
treatment_throughput_capacity float

Amount of volume per timestep of water that can be treated. Defaults to 10.

10
process_parameters dict

Dict of dicts for each pollutant. Top level key describes pollutant. Next level key describes the constant portion of the transform and the temperature sensitive exponent portion (see core.py/DecayObj for more detailed explanation). Defaults to {}.

{}
liquor_multiplier dict

Keys for each pollutant that describes how much influent becomes liquor. Defaults to {}.

{}
percent_solids float

Proportion of volume that becomes solids. All pollutants that do not become effluent or liquor become solids. Defaults to 0.0002.

0.0002
Functions intended to call in orchestration

None (use FWTW or WWTW subclass)

Key assumptions
  • Throughput can be modelled entirely with a set capacity.
  • Pollutant reduction for the entire treatment process can be modelled primarily with a single (temperature sensitive) transformation for each pollutant.
  • Liquor and solids are tracked and calculated with proportional multiplier parameters.
Input data and parameter requirements
  • treatment_throughput_capacity Units: cubic metres/timestep
  • process_parameters contains the constant (non temperature sensitive) and exponent (temperature sensitive) transformations applied to treated water for each pollutant. Units: -
  • liquor_multiplier and percent_solids describe the proportion of throughput that goes to liquor/solids.
Source code in wsimod/nodes/wtw.py
 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
def __init__(
    self,
    name,
    treatment_throughput_capacity=10,
    process_parameters={},
    liquor_multiplier={},
    percent_solids=0.0002,
):
    """Generic treatment processes that apply temperature a sensitive transform of
    pollutants into liquor and solids (behaviour depends on subclass). Push requests
    are stored in the current_input state variable, but treatment must be triggered
    with treat_current_input. This treated water is stored in the discharge_holder
    state variable, which will be sent different depending on FWTW/WWTW.

    Args:
        name (str): Node name
        treatment_throughput_capacity (float, optional): Amount of volume per
            timestep of water that can be treated. Defaults to 10.
        process_parameters (dict, optional): Dict of dicts for each pollutant.
            Top level key describes pollutant. Next level key describes the
            constant portion of the transform and the temperature sensitive
            exponent portion (see core.py/DecayObj for more detailed
            explanation). Defaults to {}.
        liquor_multiplier (dict, optional): Keys for each pollutant that
            describes how much influent becomes liquor. Defaults to {}.
        percent_solids (float, optional): Proportion of volume that becomes solids.
            All pollutants that do not become effluent or liquor become solids.
            Defaults to 0.0002.

    Functions intended to call in orchestration:
        None (use FWTW or WWTW subclass)

    Key assumptions:
        - Throughput can be modelled entirely with a set capacity.
        - Pollutant reduction for the entire treatment process can be modelled
            primarily with a single (temperature sensitive) transformation for
            each pollutant.
        - Liquor and solids are tracked and calculated with proportional
            multiplier parameters.

    Input data and parameter requirements:
        - `treatment_throughput_capacity`
            _Units_: cubic metres/timestep
        - `process_parameters` contains the constant (non temperature
            sensitive) and exponent (temperature sensitive) transformations
            applied to treated water for each pollutant.
            _Units_: -
        - `liquor_multiplier` and `percent_solids` describe the proportion of
            throughput that goes to liquor/solids.
    """
    # Set/Default parameters
    self.treatment_throughput_capacity = treatment_throughput_capacity
    if len(process_parameters) > 0:
        self.process_parameters = process_parameters
    else:
        self.process_parameters = {
            x: {"constant": 0.01, "exponent": 1.001}
            for x in constants.ADDITIVE_POLLUTANTS
        }
    if len(liquor_multiplier) > 0:
        self._liquor_multiplier = liquor_multiplier
    else:
        self._liquor_multiplier = {x: 0.7 for x in constants.ADDITIVE_POLLUTANTS}
        self._liquor_multiplier["volume"] = 0.03

    self._percent_solids = percent_solids

    # Update args
    super().__init__(name)

    self.process_parameters["volume"] = {"constant": self.calculate_volume()}

    # Update handlers
    self.push_set_handler["default"] = self.push_set_deny
    self.push_check_handler["default"] = self.push_check_deny

    # Initialise parameters
    self.current_input = self.empty_vqip()
    self.treated = self.empty_vqip()
    self.liquor = self.empty_vqip()
    self.solids = self.empty_vqip()

apply_overrides(overrides=Dict[str, Any])

Override parameters.

Enables a user to override any of the following parameters: percent_solids, treatment_throughput_capacity, process_parameters (the entire dict does not need to be redefined, only changed values need to be included), liquor_multiplier (as with process_parameters).

Parameters:

Name Type Description Default
overrides Dict[str, Any]

Dict describing which parameters should be overridden (keys) and new values (values). Defaults to {}.

Dict[str, Any]
Source code in wsimod/nodes/wtw.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def apply_overrides(self, overrides=Dict[str, Any]):
    """Override parameters.

    Enables a user to override any of the following parameters:
    percent_solids, treatment_throughput_capacity, process_parameters (the
    entire dict does not need to be redefined, only changed values need to
    be included), liquor_multiplier (as with process_parameters).

    Args:
        overrides (Dict[str, Any]): Dict describing which parameters should
            be overridden (keys) and new values (values). Defaults to {}.
    """
    self.percent_solids = overrides.pop("percent_solids", self._percent_solids)
    self.liquor_multiplier = overrides.pop(
        "liquor_multiplier", self._liquor_multiplier
    )
    process_parameters = overrides.pop("process_parameters", {})
    for key, value in process_parameters.items():
        self.process_parameters[key].update(value)

    self.treatment_throughput_capacity = overrides.pop(
        "treatment_throughput_capacity", self.treatment_throughput_capacity
    )
    super().apply_overrides(overrides)

calculate_volume()

Calculate the volume proportion of treated water.

Returns:

Type Description
float

Volume of treated water

Source code in wsimod/nodes/wtw.py
104
105
106
107
108
109
110
def calculate_volume(self):
    """Calculate the volume proportion of treated water.

    Returns:
        (float): Volume of treated water
    """
    return 1 - self._percent_solids - self._liquor_multiplier["volume"]

end_timestep()

Source code in wsimod/nodes/wtw.py
216
217
218
219
220
def end_timestep(self):
    """"""
    # Reset state variables
    self.current_input = self.empty_vqip()
    self.treated = self.empty_vqip()

get_excess_throughput()

How much excess treatment capacity is there.

Returns:

Type Description
float

Amount of volume that can still be treated this timestep

Source code in wsimod/nodes/wtw.py
155
156
157
158
159
160
161
def get_excess_throughput(self):
    """How much excess treatment capacity is there.

    Returns:
        (float): Amount of volume that can still be treated this timestep
    """
    return max(self.treatment_throughput_capacity - self.current_input["volume"], 0)

treat_current_input()

Run treatment processes this timestep, including temperature sensitive transforms, liquoring, solids.

Source code in wsimod/nodes/wtw.py
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
def treat_current_input(self):
    """Run treatment processes this timestep, including temperature sensitive
    transforms, liquoring, solids."""
    # Treat current input
    influent = self.copy_vqip(self.current_input)

    # Calculate effluent, liquor and solids
    discharge_holder = self.empty_vqip()

    # Assume non-additive pollutants are unchanged in discharge and are
    # proportionately mixed in liquor
    for key in constants.NON_ADDITIVE_POLLUTANTS:
        discharge_holder[key] = influent[key]
        self.liquor[key] = (
            self.liquor[key] * self.liquor["volume"]
            + influent[key] * influent["volume"] * self.liquor_multiplier["volume"]
        ) / (
            self.liquor["volume"]
            + influent["volume"] * self.liquor_multiplier["volume"]
        )

    # TODO this should probably just be for process_parameters.keys() to avoid
    # having to declare non changing parameters
    # TODO should the liquoring be temperature sensitive too? As it is the solids
    # will take the brunt of the temperature variability which maybe isn't sensible
    for key in constants.ADDITIVE_POLLUTANTS + ["volume"]:
        if key != "volume":
            # Temperature sensitive transform
            temp_factor = self.process_parameters[key]["exponent"] ** (
                constants.DECAY_REFERENCE_TEMPERATURE - influent["temperature"]
            )
        else:
            temp_factor = 1
        # Calculate discharge
        discharge_holder[key] = (
            influent[key] * self.process_parameters[key]["constant"] * temp_factor
        )
        # Calculate liquor
        self.liquor[key] = influent[key] * self.liquor_multiplier[key]

    # Calculate solids volume
    self.solids["volume"] = influent["volume"] * self.percent_solids

    # All remaining pollutants go to solids
    for key in constants.ADDITIVE_POLLUTANTS:
        self.solids[key] = influent[key] - discharge_holder[key] - self.liquor[key]

    # Blend with any existing discharge
    self.treated = self.sum_vqip(self.treated, discharge_holder)

    if self.treated["volume"] > self.current_input["volume"]:
        print("more treated than input")

WWTW

Bases: WTW

Wastewater Treatment Works (WWTW) node.

Source code in wsimod/nodes/wtw.py
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
class WWTW(WTW):
    """Wastewater Treatment Works (WWTW) node."""

    def __init__(
        self,
        stormwater_storage_capacity=10,
        stormwater_storage_area=1,
        stormwater_storage_elevation=10,
        **kwargs,
    ):
        """A wastewater treatment works wrapper for WTW. Contains a temporary stormwater
        storage tank. Liquor is combined with current_effluent and re- treated while
        solids leave the model.

        Args:
            stormwater_storage_capacity (float, optional): Capacity of stormwater tank.
                Defaults to 10.
            stormwater_storage_area (float, optional): Area of stormwater tank.
                Defaults to 1.
            stormwater_storage_elevation (float, optional): Datum of stormwater tank.
                Defaults to 10.

        Functions intended to call in orchestration:
            calculate_discharge

            make_discharge

        Key assumptions:
            - See `wtw.py/WTW` for treatment.
            - When `treatment_throughput_capacity` is exceeded, water is first sent
                to a stormwater storage tank before denying pushes. Leftover water
                in this tank aims to be treated in subsequent timesteps.
            - Can be pulled from to simulate active wastewater effluent use.

        Input data and parameter requirements:
            - See `wtw.py/WTW` for treatment.
            - Stormwater tank `capacity`, `area`, and `datum`.
                _Units_: cubic metres, squared metres, metres
        """
        # Set parameters
        self.stormwater_storage_capacity = stormwater_storage_capacity
        self.stormwater_storage_area = stormwater_storage_area
        self.stormwater_storage_elevation = stormwater_storage_elevation

        # Update args
        super().__init__(**kwargs)

        self.end_timestep = self.end_timestep_

        # Update handlers
        self.pull_set_handler["default"] = self.pull_set_reuse
        self.pull_check_handler["default"] = self.pull_check_reuse
        self.push_set_handler["Sewer"] = self.push_set_sewer
        self.push_check_handler["Sewer"] = self.push_check_sewer
        self.push_check_handler["default"] = self.push_check_sewer
        self.push_set_handler["default"] = self.push_set_sewer

        # Create tank
        self.stormwater_tank = Tank(
            capacity=self.stormwater_storage_capacity,
            area=self.stormwater_storage_area,
            datum=self.stormwater_storage_elevation,
        )

        # Initialise states
        self.liquor_ = self.empty_vqip()
        self.previous_input = self.empty_vqip()
        self.current_input = self.empty_vqip()  # TODO is this not done in WTW?

        # Mass balance
        self.mass_balance_out.append(lambda: self.solids)  # Assume these go to landfill
        self.mass_balance_ds.append(lambda: self.stormwater_tank.ds())
        self.mass_balance_ds.append(
            lambda: self.ds_vqip(self.liquor, self.liquor_)
        )  # Change in liquor

    def apply_overrides(self, overrides=Dict[str, Any]):
        """Apply overrides to the stormwater tank and WWTW.

        Enables a user to override any parameter of the stormwater tank, and
        then calls any overrides in WTW.

        Args:
            overrides (Dict[str, Any]): Dict describing which parameters should
                be overridden (keys) and new values (values). Defaults to {}.
        """
        self.stormwater_storage_capacity = overrides.pop(
            "stormwater_storage_capacity", self.stormwater_storage_capacity
        )
        self.stormwater_storage_area = overrides.pop(
            "stormwater_storage_area", self.stormwater_storage_area
        )
        self.stormwater_storage_elevation = overrides.pop(
            "stormwater_storage_elevation", self.stormwater_storage_elevation
        )
        self.stormwater_tank.area = self.stormwater_storage_area
        self.stormwater_tank.capacity = self.stormwater_storage_capacity
        self.stormwater_tank.datum = self.stormwater_storage_elevation
        super().apply_overrides(overrides)

    def calculate_discharge(self):
        """Clear stormwater tank if possible, and call treat_current_input."""
        # Run WWTW model

        # Try to clear stormwater
        # TODO (probably more tidy to use push_set_sewer? though maybe less
        # computationally efficient)
        excess = self.get_excess_throughput()
        if (self.stormwater_tank.get_avail()["volume"] > constants.FLOAT_ACCURACY) & (
            excess > constants.FLOAT_ACCURACY
        ):
            to_pull = min(excess, self.stormwater_tank.get_avail()["volume"])
            to_pull = self.v_change_vqip(self.stormwater_tank.storage, to_pull)
            cleared_stormwater = self.stormwater_tank.pull_storage(to_pull)
            self.current_input = self.sum_vqip(self.current_input, cleared_stormwater)

        # Run processes
        self.current_input = self.sum_vqip(self.current_input, self.liquor)
        self.treat_current_input()

    def make_discharge(self):
        """Discharge treated effluent."""
        reply = self.push_distributed(self.treated)
        self.treated = self.empty_vqip()
        if reply["volume"] > constants.FLOAT_ACCURACY:
            _ = self.stormwater_tank.push_storage(reply, force=True)
            print("WWTW couldnt push")

    def push_check_sewer(self, vqip=None):
        """Check throughput and stormwater tank capacity.

        Args:
            vqip (dict, optional): A VQIP that can be used to limit the volume in
                the return value (only volume key is used). Defaults to None.

        Returns:
            (dict): excess
        """
        # Get excess
        excess_throughput = self.get_excess_throughput()
        excess_tank = self.stormwater_tank.get_excess()
        # Combine tank and throughput
        vol = excess_tank["volume"] + excess_throughput
        # Update volume
        if vqip is None:
            vqip = self.empty_vqip()
        else:
            vol = min(vol, vqip["volume"])

        return self.v_change_vqip(vqip, vol)

    def push_set_sewer(self, vqip):
        """Receive water, first try to update current_input, and then stormwater tank.

        Args:
            vqip (dict): A VQIP amount to be treated and then stored

        Returns:
            (dict): A VQIP amount of water that was not treated
        """
        # Receive water from sewers
        vqip = self.copy_vqip(vqip)
        # Check if can directly be treated
        sent_direct = self.get_excess_throughput()

        sent_direct = min(sent_direct, vqip["volume"])

        sent_direct = self.v_change_vqip(vqip, sent_direct)

        self.current_input = self.sum_vqip(self.current_input, sent_direct)

        if sent_direct["volume"] == vqip["volume"]:
            # If all added to input, no problem
            return self.empty_vqip()

        # Next try temporary storage
        vqip = self.v_change_vqip(vqip, vqip["volume"] - sent_direct["volume"])

        vqip = self.stormwater_tank.push_storage(vqip)

        if vqip["volume"] < constants.FLOAT_ACCURACY:
            return self.empty_vqip()
        else:
            # TODO what to do here ???
            return vqip

    def pull_set_reuse(self, vqip):
        """Enables WWTW to receive pulls of the treated water (i.e., for wastewater
        reuse or satisfaction of environmental flows). Intended to be called in between
        calculate_discharge and make_discharge.

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

        Returns:
            reply (dict): Amount of water that has been pulled
        """
        # Satisfy request with treated (volume)
        reply_vol = min(vqip["volume"], self.treated["volume"])
        # Update pollutants
        reply = self.v_change_vqip(self.treated, reply_vol)
        # Update treated
        self.treated = self.v_change_vqip(
            self.treated, self.treated["volume"] - reply_vol
        )
        return reply

    def pull_check_reuse(self, vqip=None):
        """Pull check available water. Simply returns the previous timestep's treated
        throughput. This is of course inaccurate (which may lead to slightly longer
        calulcations), but it is much more flexible. This hasn't been recently tested so
        it might be that returning treated would be fine (and more accurate!).

        Args:
            vqip (dict, optional): A VQIP that can be used to limit the volume in
                the return value (only volume key is used). Defaults to None.

        Returns:
            (dict): A VQIP amount of water available. Currently just the previous
                timestep's treated throughput
        """
        # Respond to request of water for reuse/MRF
        return self.copy_vqip(self.treated)

    def end_timestep_(self):
        """End timestep function to update state variables."""
        self.liquor_ = self.copy_vqip(self.liquor)
        self.previous_input = self.copy_vqip(self.current_input)
        self.current_input = self.empty_vqip()
        self.solids = self.empty_vqip()
        self.stormwater_tank.end_timestep()

__init__(stormwater_storage_capacity=10, stormwater_storage_area=1, stormwater_storage_elevation=10, **kwargs)

A wastewater treatment works wrapper for WTW. Contains a temporary stormwater storage tank. Liquor is combined with current_effluent and re- treated while solids leave the model.

Parameters:

Name Type Description Default
stormwater_storage_capacity float

Capacity of stormwater tank. Defaults to 10.

10
stormwater_storage_area float

Area of stormwater tank. Defaults to 1.

1
stormwater_storage_elevation float

Datum of stormwater tank. Defaults to 10.

10
Functions intended to call in orchestration

calculate_discharge

make_discharge

Key assumptions
  • See wtw.py/WTW for treatment.
  • When treatment_throughput_capacity is exceeded, water is first sent to a stormwater storage tank before denying pushes. Leftover water in this tank aims to be treated in subsequent timesteps.
  • Can be pulled from to simulate active wastewater effluent use.
Input data and parameter requirements
  • See wtw.py/WTW for treatment.
  • Stormwater tank capacity, area, and datum. Units: cubic metres, squared metres, metres
Source code in wsimod/nodes/wtw.py
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
def __init__(
    self,
    stormwater_storage_capacity=10,
    stormwater_storage_area=1,
    stormwater_storage_elevation=10,
    **kwargs,
):
    """A wastewater treatment works wrapper for WTW. Contains a temporary stormwater
    storage tank. Liquor is combined with current_effluent and re- treated while
    solids leave the model.

    Args:
        stormwater_storage_capacity (float, optional): Capacity of stormwater tank.
            Defaults to 10.
        stormwater_storage_area (float, optional): Area of stormwater tank.
            Defaults to 1.
        stormwater_storage_elevation (float, optional): Datum of stormwater tank.
            Defaults to 10.

    Functions intended to call in orchestration:
        calculate_discharge

        make_discharge

    Key assumptions:
        - See `wtw.py/WTW` for treatment.
        - When `treatment_throughput_capacity` is exceeded, water is first sent
            to a stormwater storage tank before denying pushes. Leftover water
            in this tank aims to be treated in subsequent timesteps.
        - Can be pulled from to simulate active wastewater effluent use.

    Input data and parameter requirements:
        - See `wtw.py/WTW` for treatment.
        - Stormwater tank `capacity`, `area`, and `datum`.
            _Units_: cubic metres, squared metres, metres
    """
    # Set parameters
    self.stormwater_storage_capacity = stormwater_storage_capacity
    self.stormwater_storage_area = stormwater_storage_area
    self.stormwater_storage_elevation = stormwater_storage_elevation

    # Update args
    super().__init__(**kwargs)

    self.end_timestep = self.end_timestep_

    # Update handlers
    self.pull_set_handler["default"] = self.pull_set_reuse
    self.pull_check_handler["default"] = self.pull_check_reuse
    self.push_set_handler["Sewer"] = self.push_set_sewer
    self.push_check_handler["Sewer"] = self.push_check_sewer
    self.push_check_handler["default"] = self.push_check_sewer
    self.push_set_handler["default"] = self.push_set_sewer

    # Create tank
    self.stormwater_tank = Tank(
        capacity=self.stormwater_storage_capacity,
        area=self.stormwater_storage_area,
        datum=self.stormwater_storage_elevation,
    )

    # Initialise states
    self.liquor_ = self.empty_vqip()
    self.previous_input = self.empty_vqip()
    self.current_input = self.empty_vqip()  # TODO is this not done in WTW?

    # Mass balance
    self.mass_balance_out.append(lambda: self.solids)  # Assume these go to landfill
    self.mass_balance_ds.append(lambda: self.stormwater_tank.ds())
    self.mass_balance_ds.append(
        lambda: self.ds_vqip(self.liquor, self.liquor_)
    )  # Change in liquor

apply_overrides(overrides=Dict[str, Any])

Apply overrides to the stormwater tank and WWTW.

Enables a user to override any parameter of the stormwater tank, and then calls any overrides in WTW.

Parameters:

Name Type Description Default
overrides Dict[str, Any]

Dict describing which parameters should be overridden (keys) and new values (values). Defaults to {}.

Dict[str, Any]
Source code in wsimod/nodes/wtw.py
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
def apply_overrides(self, overrides=Dict[str, Any]):
    """Apply overrides to the stormwater tank and WWTW.

    Enables a user to override any parameter of the stormwater tank, and
    then calls any overrides in WTW.

    Args:
        overrides (Dict[str, Any]): Dict describing which parameters should
            be overridden (keys) and new values (values). Defaults to {}.
    """
    self.stormwater_storage_capacity = overrides.pop(
        "stormwater_storage_capacity", self.stormwater_storage_capacity
    )
    self.stormwater_storage_area = overrides.pop(
        "stormwater_storage_area", self.stormwater_storage_area
    )
    self.stormwater_storage_elevation = overrides.pop(
        "stormwater_storage_elevation", self.stormwater_storage_elevation
    )
    self.stormwater_tank.area = self.stormwater_storage_area
    self.stormwater_tank.capacity = self.stormwater_storage_capacity
    self.stormwater_tank.datum = self.stormwater_storage_elevation
    super().apply_overrides(overrides)

calculate_discharge()

Clear stormwater tank if possible, and call treat_current_input.

Source code in wsimod/nodes/wtw.py
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def calculate_discharge(self):
    """Clear stormwater tank if possible, and call treat_current_input."""
    # Run WWTW model

    # Try to clear stormwater
    # TODO (probably more tidy to use push_set_sewer? though maybe less
    # computationally efficient)
    excess = self.get_excess_throughput()
    if (self.stormwater_tank.get_avail()["volume"] > constants.FLOAT_ACCURACY) & (
        excess > constants.FLOAT_ACCURACY
    ):
        to_pull = min(excess, self.stormwater_tank.get_avail()["volume"])
        to_pull = self.v_change_vqip(self.stormwater_tank.storage, to_pull)
        cleared_stormwater = self.stormwater_tank.pull_storage(to_pull)
        self.current_input = self.sum_vqip(self.current_input, cleared_stormwater)

    # Run processes
    self.current_input = self.sum_vqip(self.current_input, self.liquor)
    self.treat_current_input()

end_timestep_()

End timestep function to update state variables.

Source code in wsimod/nodes/wtw.py
447
448
449
450
451
452
453
def end_timestep_(self):
    """End timestep function to update state variables."""
    self.liquor_ = self.copy_vqip(self.liquor)
    self.previous_input = self.copy_vqip(self.current_input)
    self.current_input = self.empty_vqip()
    self.solids = self.empty_vqip()
    self.stormwater_tank.end_timestep()

make_discharge()

Discharge treated effluent.

Source code in wsimod/nodes/wtw.py
343
344
345
346
347
348
349
def make_discharge(self):
    """Discharge treated effluent."""
    reply = self.push_distributed(self.treated)
    self.treated = self.empty_vqip()
    if reply["volume"] > constants.FLOAT_ACCURACY:
        _ = self.stormwater_tank.push_storage(reply, force=True)
        print("WWTW couldnt push")

pull_check_reuse(vqip=None)

Pull check available water. Simply returns the previous timestep's treated throughput. This is of course inaccurate (which may lead to slightly longer calulcations), but it is much more flexible. This hasn't been recently tested so it might be that returning treated would be fine (and more accurate!).

Parameters:

Name Type Description Default
vqip dict

A VQIP that can be used to limit the volume in the return value (only volume key is used). Defaults to None.

None

Returns:

Type Description
dict

A VQIP amount of water available. Currently just the previous timestep's treated throughput

Source code in wsimod/nodes/wtw.py
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
def pull_check_reuse(self, vqip=None):
    """Pull check available water. Simply returns the previous timestep's treated
    throughput. This is of course inaccurate (which may lead to slightly longer
    calulcations), but it is much more flexible. This hasn't been recently tested so
    it might be that returning treated would be fine (and more accurate!).

    Args:
        vqip (dict, optional): A VQIP that can be used to limit the volume in
            the return value (only volume key is used). Defaults to None.

    Returns:
        (dict): A VQIP amount of water available. Currently just the previous
            timestep's treated throughput
    """
    # Respond to request of water for reuse/MRF
    return self.copy_vqip(self.treated)

pull_set_reuse(vqip)

Enables WWTW to receive pulls of the treated water (i.e., for wastewater reuse or satisfaction of environmental flows). Intended to be called in between calculate_discharge and make_discharge.

Parameters:

Name Type Description Default
vqip dict

A VQIP amount to be pulled (only 'volume' key is used)

required

Returns:

Name Type Description
reply dict

Amount of water that has been pulled

Source code in wsimod/nodes/wtw.py
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
def pull_set_reuse(self, vqip):
    """Enables WWTW to receive pulls of the treated water (i.e., for wastewater
    reuse or satisfaction of environmental flows). Intended to be called in between
    calculate_discharge and make_discharge.

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

    Returns:
        reply (dict): Amount of water that has been pulled
    """
    # Satisfy request with treated (volume)
    reply_vol = min(vqip["volume"], self.treated["volume"])
    # Update pollutants
    reply = self.v_change_vqip(self.treated, reply_vol)
    # Update treated
    self.treated = self.v_change_vqip(
        self.treated, self.treated["volume"] - reply_vol
    )
    return reply

push_check_sewer(vqip=None)

Check throughput and stormwater tank capacity.

Parameters:

Name Type Description Default
vqip dict

A VQIP that can be used to limit the volume in the return value (only volume key is used). Defaults to None.

None

Returns:

Type Description
dict

excess

Source code in wsimod/nodes/wtw.py
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
def push_check_sewer(self, vqip=None):
    """Check throughput and stormwater tank capacity.

    Args:
        vqip (dict, optional): A VQIP that can be used to limit the volume in
            the return value (only volume key is used). Defaults to None.

    Returns:
        (dict): excess
    """
    # Get excess
    excess_throughput = self.get_excess_throughput()
    excess_tank = self.stormwater_tank.get_excess()
    # Combine tank and throughput
    vol = excess_tank["volume"] + excess_throughput
    # Update volume
    if vqip is None:
        vqip = self.empty_vqip()
    else:
        vol = min(vol, vqip["volume"])

    return self.v_change_vqip(vqip, vol)

push_set_sewer(vqip)

Receive water, first try to update current_input, and then stormwater tank.

Parameters:

Name Type Description Default
vqip dict

A VQIP amount to be treated and then stored

required

Returns:

Type Description
dict

A VQIP amount of water that was not treated

Source code in wsimod/nodes/wtw.py
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
def push_set_sewer(self, vqip):
    """Receive water, first try to update current_input, and then stormwater tank.

    Args:
        vqip (dict): A VQIP amount to be treated and then stored

    Returns:
        (dict): A VQIP amount of water that was not treated
    """
    # Receive water from sewers
    vqip = self.copy_vqip(vqip)
    # Check if can directly be treated
    sent_direct = self.get_excess_throughput()

    sent_direct = min(sent_direct, vqip["volume"])

    sent_direct = self.v_change_vqip(vqip, sent_direct)

    self.current_input = self.sum_vqip(self.current_input, sent_direct)

    if sent_direct["volume"] == vqip["volume"]:
        # If all added to input, no problem
        return self.empty_vqip()

    # Next try temporary storage
    vqip = self.v_change_vqip(vqip, vqip["volume"] - sent_direct["volume"])

    vqip = self.stormwater_tank.push_storage(vqip)

    if vqip["volume"] < constants.FLOAT_ACCURACY:
        return self.empty_vqip()
    else:
        # TODO what to do here ???
        return vqip