Getting Started

Loading a PyPES Model

For most users, the first step to using PyPES will be to pip install the Python package:

pip install pype-schema

This installation should come with two sample facility configurations: one for water resource recovery (wrrf_sample.json) and one for desalination (desal_sample.json). To load the water resource recovery facility (WRRF) example, run the following code from the pype_schema/data folder:

from pype_schema.parse_json import JSONParser

parser = JSONParser("wrrf_sample.json")
network = parser.initialize_network()

Model Structure

The visualize module can be used to view the loaded PyPES representation with either of the pyvis or networkx packages.

from pype_schema.visualize import draw_graph

pyvis = True
draw_graph(network, pyvis)

Running the above code should produce the following HTML visualization:

Example directed graph representation of a city's water and energy infrastructure

PyPES models can have nodes nested within nodes. For example, the WWTP node nested inside the top-level network can be visualized as follows:

node_id = "WWTP"
draw_graph(network.get_node(node_id), pyvis)
Example directed graph representation of a city's wastewater treatment plant (WWTP)

Now, let’s discuss some of the components of the graph.

Connection

Conceptually, connections are objects in a WRRF that act as a vector to transport contents from a source to a destination.

In practice, there will be an abstract Python class Connection with two subclasses: Pipe and Wire. Representing Connections contains tables with required attributes (e.g., source, destination) and optional attributes of those classes (e.g., diameter, contents). All Connection objects have a dictionary of tags with the tag IDs as keys and Tag or VirtualTag objects as values.

Node

Conceptually, nodes are objects that represent a wide variety of entities, from clarifiers, aerators, and filters for treatment to batteries, pumps, and cogenerators for energy modeling.

In practice, there will be an abstract Python class Node with many subclasses. Representing Nodes contains tables with all the potential node classes (e..g, Tank, Filtration, Battery, etc.), r equired attributes of those classes (e.g., id, contents), and optional attributes of those classes (e.g., volume, contents). All Node objects have a dictionary of tags with the tag IDs as keys and Tag or VirtualTag objects as values.

Tag

Conceptually, tags are sensors that collect WRRF data, such as flow rate or temperature. They are not a part of the graph like nodes or connections, but are associated with nodes and connections along with other attributes.

Each Tag object contains attributes related to data being collected, such as the string ID, type of measurement, and units (see Representing Tags for more details).

Modifying the Model

The graphical representation of the facility can be modified programmatically. This can be useful for modeling upgrades to existing infrastructure by seamlessly comparing two configurations side-by-side.

For example, to add a 10,000 gallon storage tank at 1,000 meters elevation to the water distribution network:

from pype_schema.node import Tank
from pype_schema.utils import parse_quantity, ContentsType

volume = parse_quantity(10000, "gal")
elevation = parse_quantity(1000, "m")

# create the battery node
tank = Tank(
    "StorageTank",
    ContentsType.DrinkingWater,
    ContentsType.DrinkingWater,
    elevation,
    volume
)

# add the node to the facility
wds = network.get_node("WaterDistribution")
wds.add_node(tank)

Now that there is a node inside the water distribution network, the connection can be modified to have an entry_point. The entry_point and exit_point attributes allow a user to specify the subnode to which a connection is starting or ending at when the connection goes between levels in the graph. For example, in this case we want to specify that the drinking water treatment plant effluent goes not only to the water distribution network, but to a specific storage tank within the distribution network:

from pype_schema.connection import Connection

wds_conn = network.select_objs(
    source_id="DrinkingWaterFacility",
    dest_id="WaterDistribution",
    obj_type=Connection
)[0]
wds_conn.entry_point = tank

The next section, Querying the Model, explains querying using select_objs in further detail.

Rather than adding components to the model one-by-one in Python, a user can edit the JSON file directly and then re-load the model (see JSON Representation)

Querying the Model

PyPES offers built-in search capabilities that allow users to find nodes, connections, and tags matching desired characteristics.

Modeling applications can be generalized through the use of these queries. For example, to calculate the natural gas purchases at a facility without knowing how many boilers or cogenerators (if any) exist, a user could query for all connections with ContentsType are NaturalGas entering the WWTP node:

ng_conns = network.select_objs(
    dest_id="WWTP",
    contents_type=ContentsType.NaturalGas,
    obj_type=Connection,
    recurse=True
)

Its often more convenient to get all the tags directly. Then, if the data is in CSV format the tags correspond to column names that can be operated on:

from pype_schema.tag import Tag
import pandas as pd

df = pd.read_csv("sample_data.csv")

ng_tags = network.select_objs(
    dest_id="WWTP",
    contents_type=ContentsType.NaturalGas,
    obj_type=Tag,
    recurse=True
)

ng_tag_ids = [tag.id for tag in ng_tags]
ng_import_timeseries = df[ng_tag_ids].sum(axis=1)

Then, ng_import_timeseries could be used for whatever application the user desires. For example, to plot the natural gas imports over time:

import matplotlib.pyplot as plt

for tag in ng_tags:
    plt.plot(df[tag.id], label=tag.id)

plt.plot(ng_import_timeseries, label="TotalGasPurchases")
plt.xlabel("Hour")
plt.ylabel("Natural Gas Imports (m$^3$ / day)")
plt.legend(loc='upper right', bbox_to_anchor=(1.45, 1.0))
plt.savefig("ng-imports.png", bbox_inches="tight")
Timeseries of natural gas imports to the cogenerator and boiler combined to calculate total imports using PyPES

Unit IDs are used to specify identical parallel processes. For example, a cogenerator may have two engines. Therefore, dest_unit_id was specified as “total” because there may be unit-level tags, and summing both unit-level and total tags would lead to overcounting.

There are a number of optional arguments to select_objs, most of which default to None. The function is fully documented in node.Node.select_objs.