JSON model format¶
Overview of document structure¶
In addition to creating models programmatically using the Python API, models can be described in a JSON (JavaScript Object Notation) document 1.
The overall structure of the model is given below. A description of the contents of each of the first level items is given in this document. The most important are the nodes and edges sections.
{
"metadata": {},
"timestepper": {},
"solver": {},
"nodes": {},
"edges": {},
"parameters": {}
}
Some examples of JSON models can be found in the tests/models folder.
Metadata¶
The metadata section includes information about the model as key-value pairs. It is expected as a minimum to include a "title"
and "description"
and may additionally include keys such as "author"
.
{"metadata": {
"title": "Example",
"description": "An example for the documentation",
"author": "John Smith"
}}
Timestepper¶
The timestepper defines the period a model is run for and the timestep used. It corresponds directly to the pywr.core.Timestepper
instance on the model. It has three properties: the start date, end date and timestep.
The example below describes a model that will run from 1st January 2016 to 31st December 2016 using a 7 day timestep.
{"timestepper": {
"start": "2016-01-01",
"end": "2016-12-31",
"timestep": 7
}}
Solver¶
The solver section contains items to be passed to the solver. The only required item is the name of the solver to use. Other items will be specific to the solver.
{"solver": {
"name": "glpk"
}}
Nodes¶
The nodes section describes the nodes in the model. As a minimum a node must have a name
and a type
. There are two fundamental types of node in Pywr (pywr.core.Node
and pywr.core.Storage
) which have different properties.
Where a parameter can be described as a simple scalar value it is sufficient to pass the value directly (e.g. "cost": 10.0
). See also the parameters section for details on defining non-scalar parameters.
Non-storage nodes¶
The Node
type and it’s subtypes have a max_flow
and cost
property, both of which have default values.
{"nodes": [
{
"name": "groundwater",
"type": "input",
"max_flow": 23.0,
"cost": 10.0
}
]}
In addition to the basic input
, output
and link
types, subtypes can be created by specifying the appropriate name. Some subtypes will provide additional properties; often these correspond directly to the keyword arguments of the class. For example, a river gauge which has a soft MRF constraint is demonstrated below. The "mrf"
property is the minimum residual flow required, the "mrf_cost"
is the cost applied to that minimum flow, and the "cost"
property is the cost associated with the residual flow.
{"nodes": [
{
"name": "Teddington GS",
"type": "rivergauge",
"mrf": 200.0,
"cost": 0.0,
"mrf_cost": -1000.0
}
]}
Storage nodes¶
The Storage
type and it’s subtypes have a max_volume
, min_volume
and initial_volume
, as well as num_inputs
and num_outputs
. The maximum and initial volumes must be specified, whereas the others have default values.
{"nodes": [
{
"name": "Big Wet Lake",
"type": "storage",
"max_volume": 1000,
"initial_volume": 700,
"min_volume": 0,
"num_inputs": 1,
"num_outputs": 1,
"cost": -10.0
}
]}
When defining a storage node with multiple inputs or outputs connections need to be made using the slot notation (discussed in the edges section).
Edges¶
The edges section describes the connections between nodes. As a minimum an edge is defined as a two-item list containing the names of the nodes to connect (given in the order corresponding to the direction of flow), e.g.:
{"edges": [
["supply", "intermediate"],
["intermediate", "demand"]
]}
Additionally the to and from slots can be specified. For example the code below connects reservoirA slot 2 to reservoirB slot 3.
{"edges": [
["reservoirA", "reservoirB", 2, 3]
]}
Parameters¶
Sometimes it is convenient to define a Parameter
used in the model in the "parameters"
section instead of inside a node, for instance if the parameter is needed by more than one node.
{
"nodes": [
{
"name": "groundwater",
"type": "input",
"max_flow": "gw_flow"
}
],
"parameters": [
{
"name": "gw_flow",
"type": "constant",
"value": 23.0
}
]
}
Parameters can be more complicated than simple scalar values. For instance, a time varying parameter can be defined using a monthly or daily profile which repeats each year.
{"parameters": [
{
"name": "mrf_profile",
"type": "monthlyprofile",
"values": [10, 10, 10, 10, 50, 50, 50, 50, 20, 20, 10, 10]
}
]}
Instead of defining the data inline using the "values"
property, external data can be referenced as below. The URL should be relative to the JSON document not the current working directory.
{"parameters": [
{
"name": "catchment_inflow",
"type": "dataframe",
"url": "data/catchmod_outputs_v2.csv",
"column": "Flow",
"index_col": "Date",
"parse_dates": true
}
]}
Loading a JSON document¶
A Pywr JSON document can be loaded into a Model instance by using the Model.load class-method:
from pywr.model import Model
my_model = model.load('/path/to/my_model.json')
my_model.run()
Once a model is loaded if a reference to an actual node is required, using .get …
node = my_model.nodes.get("River Thames")
if node:
print(f"max_flow: {node.max_flow}")
else:
print("Not found")
… or try-except is preferable to avoid searching twice.
try:
node = model.nodes["River Thames"]
except KeyError:
print("Not found")
else:
print(f"max_flow: {node.max_flow}")
It is also possible to test for node and component membership using their names:
assert "River Thames" in model.nodes
assert "Demand" in model.parameters
Debugging and syntax errors¶
The JSON format is not sensitive to white space but is otherwise quite strict. When the json module fails to parse a document an exception will be raised. The exception includes a (somewhat cryptic) description of the problem and usefully includes a line number (see example below).
>>> model = Model.loads(data)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/snorf/Desktop/pywr/pywr/core.py", line 316, in loads
data = json.loads(data)
File "/Users/snorf/miniconda3/envs/pywr/lib/python3.4/json/__init__.py", line 318, in loads
return _default_decoder.decode(s)
File "/Users/snorf/miniconda3/envs/pywr/lib/python3.4/json/decoder.py", line 343, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "/Users/snorf/miniconda3/envs/pywr/lib/python3.4/json/decoder.py", line 359, in raw_decode
obj, end = self.scan_once(s, idx)
ValueError: Expecting property name enclosed in double quotes: line 17 column 9 (char 372)
Common mistakes when writing JSON documents “by hand” include:
Trailing commas at the end of a list (
["like", "this",]
)Strings not enclosed in quotes (
name
instead of"name"
)