Skip to content

WSIMOD models

  1. Introduction

  2. What data do you need

  3. WSIMOD model

    3.1. Nodes

    3.2. Arcs

    3.3. Model properties

  4. Input data

  5. Limitations

Introduction

In other tutorials, we generally create nodes and arcs in a dictionary format to best explain how to understand what is going on in WSIMOD. However, this is not a streamlined way to setup or use a model in practice. Instead, we provide a structured format that a model can be loaded from and the ability to save models to this format. In this tutorial we will describe the format, and give examples.

What data do you need?

WSIMOD combines a variety of different models into different types of node/component. Our goal is a highly flexible approach to representing a wide variety of water systems. The result is that your data requirements will be highly specific to your project, the systems that you want to include, and the questions you want to ask of your model. To understand what model components you should include, and what data requirements these have, we recommend familiarising yourself with the WSIMOD approach through completing the tutorials and viewing the API. Ultimately though, all nodes and arcs can be resolved in terms of dictionaries that determine their parameters/input data, which makes them ideal to be structured using the PyYAML data langauge.

WSIMOD model

As demonstrated in the tutorials, a model object is a helpful way to contain your nodes/arcs and orchestrate your simulation. From the model object, you can save or load data by providing a data directory. Critically, these functions interact with a config.yml file that contains all of the information to describe your model. Below, we will explain this file using examples from quickstart demo, highlighting the key features of this config file. You can create this yourself by running the demo and appending the following command:

my_model.save(<directory_address_on_your_filesystem>)

The top level entries of the config.yml file created in the provided directorty are:

nodes
arcs
pollutants
additive_pollutants
non_additive_pollutants
float_accuracy
dates

, which cover all of the properties needed to describe a model object.

To load a model, we to initialise a model object and call the load function:

my_model = Model()
my_model.load(<directory_address_on_your_filesystem>)

Nodes

The nodes entry of config.yml will contain the information required to initialise all of the components in your model. Below are a sewer node and default node.

nodes:
  my_sewer:
    chamber_area: 1
    name: my_sewer
    pipe_time: 0
    chamber_floor: 10
    capacity: 0.04
    pipe_timearea:
      0: 1
    type_: Sewer
    node_type_override: Sewer
  my_river:
    name: my_river
    type_: Node
    node_type_override: Node

By inspecting the Sewer API, we can see that this entry contains the parameters required to initiliase a Sewer object, however the capacity and name fields have been updated to the values that they are set as in the quickstart demo.

We see two additional fields of type_ and node_type_override. If only type_ is provided, then this will specify the object that is created which should match an object in WSIMOD. It also specifies how other nodes view it, for example, a Sewer node receives water differently from Land nodes than it does from Demand nodes. However, there are a variety of subclasses of Demand node, so to ensure the Sewer object treats all Demand subclasses the same, we overwrite the __class__.__name__ property in each subclass so that the model treats (e.g.,) ResidentialDemand as a Demand object. In cases such as these, to ensure WSIMOD creates a ResidentialDemand object that is treated like a Demand object, we specify the type_ as Demand but the node_type_override as ResidentialDemand.

Arcs

The arcs entry of config.yml contains the initialisation fields of the Arc object. If we inspect the quickstart demo, we will see that the storm_outflow arc was not initiliased with any values for the preference or capacity parameter. They are saved by the Model.save() function because, upon initialisation, if they are not provided, the arc receives a default unbounded capacity and a neutral preference of 1.

arcs:
  storm_outflow:
    capacity: 1000000000000000.0
    name: storm_outflow
    preference: 1
    type_: Arc
    in_port: my_sewer
    out_port: my_river

Model properties

The pollutants entries are used to tell the model which pollutants should be simulated, which are additive (i.e., mass based), and which are non-additive (e.g., temperature).

The float_accuracy entry provides a number used in mass balance checking. Common sense is suggested in interpreting mass balance errors.

The dates entry is written if the model object has a dates property and is a list of dates for which the model will run for if the Model.run() function is called. It is assumed that the dates are compatible with the dates provided in the input data.

Input data

While the config.yml file contains information to parameterise and initialise WSIMOD objects, timeseries input data is stored separately to create a more manageable model directory. As established through the tutorials, any node timeseries input data must be provided as a dictionary where the keys are tuples containing the variable and time, and stored in the data_input_dict property. For example, using the Catchment node, which is primarily a data reader:

# Imports
from wsimod.core import constants
from wsimod.nodes.catchment import Catchment
from wsimod.orchestration.model import to_datetime

# Model only temperature and phosphate
constants.set_simple_pollutants()

# Create input data dictionary
date = to_datetime('2000-01-01')
forcing_data = {('flow', date) : 2,
                    ('phosphate', date) : 0.2,
                    ('temperature', date) : 10}

# Create Catchment object
my_catch = Catchment(name = 'my_catchment', data_input_dict = forcing_data)

# Assign date
my_catch.t = date

# Get flows
print(my_catch.get_flow())
{'volume': 2, 'phosphate': 0.4, 'temperature': 10}

We note that dates can take any hashable format, however some components require the date to have properties such as dayofyear, and so recommend using datetime like objects, we provide a simple datetime wrapper in model.to_datetime.

When we call Model.save() it will convert each node's data_input_dict into a separate .csv (or .csv.gz if the compress option is specified), and create an entry called filename for the node in the config.yml file.

For example,

import os
from wsimod.orchestration.model import Model

my_model = Model()
my_model.add_instantiated_nodes([my_catch])
my_model.save(os.getcwd())

Which will create a config.yml file containing the nodes entry of:

nodes:
  my_catchment:
    name: my_catchment
    type_: Catchment
    node_type_override: Catchment
    filename: my_catchment-inputs.csv

and a file in the current directory named my_catchment-inputs.csv containing:

node,variable,time,value
my_catchment,flow,2000-01-01,2
my_catchment,phosphate,2000-01-01,0.2
my_catchment,temperature,2000-01-01,10

Limitations

It is important that there are key limitations to model saving/loading. To avoid overly complicated config.yml files that are easy for a user to edit, we opted to only save the properties of a node/arc required to initialise that object. This means that state variables, user-added properties, or method overriding are not preserved when the model is saved. Thus, when the model is loaded, its objects are initialised as if new. However, if a user wishes to achieve this then the Model/save_pickle and Model/load_pickle functions can help to achieve it, though these create binary files that are not user readable.