diff --git a/schema/BasinForcing.schema.json b/schema/BasinForcing.schema.json
index 956cc7e99..0f0ad5eb2 100644
--- a/schema/BasinForcing.schema.json
+++ b/schema/BasinForcing.schema.json
@@ -9,37 +9,30 @@
},
"time": {
"format": "date-time",
- "description": "time",
"type": "string"
},
"precipitation": {
"format": "double",
- "description": "precipitation",
"type": "number"
},
"infiltration": {
"format": "double",
- "description": "infiltration",
"type": "number"
},
"urban_runoff": {
"format": "double",
- "description": "urban_runoff",
"type": "number"
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"potential_evaporation": {
"format": "double",
- "description": "potential_evaporation",
"type": "number"
},
"drainage": {
"format": "double",
- "description": "drainage",
"type": "number"
}
},
diff --git a/schema/BasinProfile.schema.json b/schema/BasinProfile.schema.json
index 9f771318f..98dc78dd0 100644
--- a/schema/BasinProfile.schema.json
+++ b/schema/BasinProfile.schema.json
@@ -9,17 +9,14 @@
},
"area": {
"format": "double",
- "description": "area",
"type": "number"
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"level": {
"format": "double",
- "description": "level",
"type": "number"
}
},
diff --git a/schema/BasinState.schema.json b/schema/BasinState.schema.json
index a185891c4..906bf5a96 100644
--- a/schema/BasinState.schema.json
+++ b/schema/BasinState.schema.json
@@ -9,12 +9,10 @@
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"level": {
"format": "double",
- "description": "level",
"type": "number"
}
},
diff --git a/schema/BasinStatic.schema.json b/schema/BasinStatic.schema.json
index 77e34e79f..a47d2970b 100644
--- a/schema/BasinStatic.schema.json
+++ b/schema/BasinStatic.schema.json
@@ -9,32 +9,26 @@
},
"precipitation": {
"format": "double",
- "description": "precipitation",
"type": "number"
},
"infiltration": {
"format": "double",
- "description": "infiltration",
"type": "number"
},
"urban_runoff": {
"format": "double",
- "description": "urban_runoff",
"type": "number"
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"potential_evaporation": {
"format": "double",
- "description": "potential_evaporation",
"type": "number"
},
"drainage": {
"format": "double",
- "description": "drainage",
"type": "number"
}
},
diff --git a/schema/Config.schema.json b/schema/Config.schema.json
new file mode 100644
index 000000000..f324fc81b
--- /dev/null
+++ b/schema/Config.schema.json
@@ -0,0 +1,180 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "output": {
+ "default": {
+ "basin": "output/basin.arrow",
+ "flow": "output/flow.arrow",
+ "control": "output/control.arrow",
+ "outstate": null,
+ "compression": "zstd",
+ "compression_level": 6
+ },
+ "$ref": "https://deltares.github.io/Ribasim/schema/Output.schema.json"
+ },
+ "starttime": {
+ "format": "date-time",
+ "type": "string"
+ },
+ "update_timestep": {
+ "format": "double",
+ "default": 86400,
+ "type": "number"
+ },
+ "input_dir": {
+ "format": "default",
+ "default": ".",
+ "type": "string"
+ },
+ "output_dir": {
+ "format": "default",
+ "default": ".",
+ "type": "string"
+ },
+ "level_boundary": {
+ "default": {
+ "static": null,
+ "time": null
+ },
+ "$ref": "https://deltares.github.io/Ribasim/schema/level_boundary.schema.json"
+ },
+ "pump": {
+ "default": {
+ "static": null
+ },
+ "$ref": "https://deltares.github.io/Ribasim/schema/pump.schema.json"
+ },
+ "discrete_control": {
+ "default": {
+ "condition": null,
+ "logic": null
+ },
+ "$ref": "https://deltares.github.io/Ribasim/schema/discrete_control.schema.json"
+ },
+ "solver": {
+ "default": {
+ "algorithm": "QNDF",
+ "saveat": [
+ ],
+ "adaptive": true,
+ "dt": 0,
+ "abstol": 1.0e-6,
+ "reltol": 0.001,
+ "maxiters": 1000000000,
+ "sparse": true,
+ "autodiff": true
+ },
+ "$ref": "https://deltares.github.io/Ribasim/schema/Solver.schema.json"
+ },
+ "flow_boundary": {
+ "default": {
+ "static": null,
+ "time": null
+ },
+ "$ref": "https://deltares.github.io/Ribasim/schema/flow_boundary.schema.json"
+ },
+ "pid_control": {
+ "default": {
+ "static": null,
+ "time": null
+ },
+ "$ref": "https://deltares.github.io/Ribasim/schema/pid_control.schema.json"
+ },
+ "fractional_flow": {
+ "default": {
+ "static": null
+ },
+ "$ref": "https://deltares.github.io/Ribasim/schema/fractional_flow.schema.json"
+ },
+ "relative_dir": {
+ "format": "default",
+ "default": ".",
+ "type": "string"
+ },
+ "endtime": {
+ "format": "date-time",
+ "type": "string"
+ },
+ "manning_resistance": {
+ "default": {
+ "static": null
+ },
+ "$ref": "https://deltares.github.io/Ribasim/schema/manning_resistance.schema.json"
+ },
+ "tabulated_rating_curve": {
+ "default": {
+ "static": null,
+ "time": null
+ },
+ "$ref": "https://deltares.github.io/Ribasim/schema/tabulated_rating_curve.schema.json"
+ },
+ "logging": {
+ "default": {
+ "verbosity": {
+ "level": 0
+ },
+ "timing": false
+ },
+ "$ref": "https://deltares.github.io/Ribasim/schema/Logging.schema.json"
+ },
+ "outlet": {
+ "default": {
+ "static": null
+ },
+ "$ref": "https://deltares.github.io/Ribasim/schema/outlet.schema.json"
+ },
+ "geopackage": {
+ "format": "default",
+ "type": "string"
+ },
+ "terminal": {
+ "default": {
+ "static": null
+ },
+ "$ref": "https://deltares.github.io/Ribasim/schema/terminal.schema.json"
+ },
+ "basin": {
+ "default": {
+ "forcing": null,
+ "profile": null,
+ "state": null,
+ "static": null
+ },
+ "$ref": "https://deltares.github.io/Ribasim/schema/basin.schema.json"
+ },
+ "linear_resistance": {
+ "default": {
+ "static": null
+ },
+ "$ref": "https://deltares.github.io/Ribasim/schema/linear_resistance.schema.json"
+ }
+ },
+ "required": [
+ "starttime",
+ "endtime",
+ "update_timestep",
+ "relative_dir",
+ "input_dir",
+ "output_dir",
+ "geopackage",
+ "output",
+ "solver",
+ "logging",
+ "terminal",
+ "pid_control",
+ "level_boundary",
+ "pump",
+ "tabulated_rating_curve",
+ "flow_boundary",
+ "basin",
+ "manning_resistance",
+ "discrete_control",
+ "outlet",
+ "linear_resistance",
+ "fractional_flow"
+ ],
+ "$id": "https://deltares.github.io/Ribasim/schema/Config.schema.json",
+ "title": "Config",
+ "description": "A Config object based on Ribasim.config.Config",
+ "type": "object"
+}
diff --git a/schema/DiscreteControlCondition.schema.json b/schema/DiscreteControlCondition.schema.json
index 68eda57ca..3ce188fe2 100644
--- a/schema/DiscreteControlCondition.schema.json
+++ b/schema/DiscreteControlCondition.schema.json
@@ -9,29 +9,29 @@
},
"greater_than": {
"format": "double",
- "description": "greater_than",
"type": "number"
},
"listen_feature_id": {
"format": "default",
- "description": "listen_feature_id",
"type": "integer"
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"variable": {
"format": "default",
- "description": "variable",
"type": "string"
},
"look_ahead": {
"format": "default",
- "description": "look_ahead",
- "type": [
- "number"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "number"
+ }
]
}
},
diff --git a/schema/DiscreteControlLogic.schema.json b/schema/DiscreteControlLogic.schema.json
index a0364d83d..76a549b6b 100644
--- a/schema/DiscreteControlLogic.schema.json
+++ b/schema/DiscreteControlLogic.schema.json
@@ -9,17 +9,14 @@
},
"truth_state": {
"format": "default",
- "description": "truth_state",
"type": "string"
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"control_state": {
"format": "default",
- "description": "control_state",
"type": "string"
}
},
diff --git a/schema/Edge.schema.json b/schema/Edge.schema.json
index 95c8557bd..43626db5f 100644
--- a/schema/Edge.schema.json
+++ b/schema/Edge.schema.json
@@ -9,22 +9,18 @@
},
"edge_type": {
"format": "default",
- "description": "edge_type",
"type": "string"
},
"fid": {
"format": "default",
- "description": "fid",
"type": "integer"
},
"to_node_id": {
"format": "default",
- "description": "to_node_id",
"type": "integer"
},
"from_node_id": {
"format": "default",
- "description": "from_node_id",
"type": "integer"
}
},
diff --git a/schema/FlowBoundaryStatic.schema.json b/schema/FlowBoundaryStatic.schema.json
index 2bb39e060..8300ea9fd 100644
--- a/schema/FlowBoundaryStatic.schema.json
+++ b/schema/FlowBoundaryStatic.schema.json
@@ -9,19 +9,21 @@
},
"active": {
"format": "default",
- "description": "active",
- "type": [
- "boolean"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "boolean"
+ }
]
},
"flow_rate": {
"format": "double",
- "description": "flow_rate",
"type": "number"
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
}
},
diff --git a/schema/FlowBoundaryTime.schema.json b/schema/FlowBoundaryTime.schema.json
index bc65c4633..dc64986ce 100644
--- a/schema/FlowBoundaryTime.schema.json
+++ b/schema/FlowBoundaryTime.schema.json
@@ -9,17 +9,14 @@
},
"time": {
"format": "date-time",
- "description": "time",
"type": "string"
},
"flow_rate": {
"format": "double",
- "description": "flow_rate",
"type": "number"
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
}
},
diff --git a/schema/FractionalFlowStatic.schema.json b/schema/FractionalFlowStatic.schema.json
index f98567832..64bed064f 100644
--- a/schema/FractionalFlowStatic.schema.json
+++ b/schema/FractionalFlowStatic.schema.json
@@ -9,19 +9,21 @@
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"fraction": {
"format": "double",
- "description": "fraction",
"type": "number"
},
"control_state": {
"format": "default",
- "description": "control_state",
- "type": [
- "string"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
]
}
},
diff --git a/schema/LevelBoundaryStatic.schema.json b/schema/LevelBoundaryStatic.schema.json
index 82de99862..f635afce0 100644
--- a/schema/LevelBoundaryStatic.schema.json
+++ b/schema/LevelBoundaryStatic.schema.json
@@ -9,19 +9,21 @@
},
"active": {
"format": "default",
- "description": "active",
- "type": [
- "boolean"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "boolean"
+ }
]
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"level": {
"format": "double",
- "description": "level",
"type": "number"
}
},
diff --git a/schema/LevelBoundaryTime.schema.json b/schema/LevelBoundaryTime.schema.json
index fb4ef108f..e50696c32 100644
--- a/schema/LevelBoundaryTime.schema.json
+++ b/schema/LevelBoundaryTime.schema.json
@@ -9,17 +9,14 @@
},
"time": {
"format": "date-time",
- "description": "time",
"type": "string"
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"level": {
"format": "double",
- "description": "level",
"type": "number"
}
},
diff --git a/schema/LinearResistanceStatic.schema.json b/schema/LinearResistanceStatic.schema.json
index a5d6efdb6..5a5146ea9 100644
--- a/schema/LinearResistanceStatic.schema.json
+++ b/schema/LinearResistanceStatic.schema.json
@@ -9,26 +9,32 @@
},
"active": {
"format": "default",
- "description": "active",
- "type": [
- "boolean"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "boolean"
+ }
]
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"resistance": {
"format": "double",
- "description": "resistance",
"type": "number"
},
"control_state": {
"format": "default",
- "description": "control_state",
- "type": [
- "string"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
]
}
},
diff --git a/schema/Logging.schema.json b/schema/Logging.schema.json
new file mode 100644
index 000000000..86fb596a7
--- /dev/null
+++ b/schema/Logging.schema.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "timing": {
+ "format": "default",
+ "default": false,
+ "type": "boolean"
+ },
+ "verbosity": {
+ "format": "default",
+ "default": "info",
+ "type": "string"
+ }
+ },
+ "required": [
+ "verbosity",
+ "timing"
+ ],
+ "$id": "https://deltares.github.io/Ribasim/schema/Logging.schema.json",
+ "title": "Logging",
+ "description": "A Logging object based on Ribasim.config.Logging",
+ "type": "object"
+}
diff --git a/schema/ManningResistanceStatic.schema.json b/schema/ManningResistanceStatic.schema.json
index 5094f68df..22bad56df 100644
--- a/schema/ManningResistanceStatic.schema.json
+++ b/schema/ManningResistanceStatic.schema.json
@@ -3,12 +3,10 @@
"properties": {
"length": {
"format": "double",
- "description": "length",
"type": "number"
},
"manning_n": {
"format": "double",
- "description": "manning_n",
"type": "number"
},
"remarks": {
@@ -19,31 +17,36 @@
},
"active": {
"format": "default",
- "description": "active",
- "type": [
- "boolean"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "boolean"
+ }
]
},
"profile_width": {
"format": "double",
- "description": "profile_width",
"type": "number"
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"profile_slope": {
"format": "double",
- "description": "profile_slope",
"type": "number"
},
"control_state": {
"format": "default",
- "description": "control_state",
- "type": [
- "string"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
]
}
},
diff --git a/schema/Node.schema.json b/schema/Node.schema.json
index b7b93b2fc..dfbf5f509 100644
--- a/schema/Node.schema.json
+++ b/schema/Node.schema.json
@@ -9,12 +9,10 @@
},
"fid": {
"format": "default",
- "description": "fid",
"type": "integer"
},
"type": {
"format": "default",
- "description": "type",
"type": "string"
}
},
diff --git a/schema/OutletStatic.schema.json b/schema/OutletStatic.schema.json
index af03e5539..d8feae988 100644
--- a/schema/OutletStatic.schema.json
+++ b/schema/OutletStatic.schema.json
@@ -3,9 +3,13 @@
"properties": {
"max_flow_rate": {
"format": "default",
- "description": "max_flow_rate",
- "type": [
- "number"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "number"
+ }
]
},
"remarks": {
@@ -16,40 +20,54 @@
},
"active": {
"format": "default",
- "description": "active",
- "type": [
- "boolean"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "boolean"
+ }
]
},
"min_crest_level": {
"format": "default",
- "description": "min_crest_level",
- "type": [
- "number"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "number"
+ }
]
},
"flow_rate": {
"format": "double",
- "description": "flow_rate",
"type": "number"
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"control_state": {
"format": "default",
- "description": "control_state",
- "type": [
- "string"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
]
},
"min_flow_rate": {
"format": "default",
- "description": "min_flow_rate",
- "type": [
- "number"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "number"
+ }
]
}
},
diff --git a/schema/Output.schema.json b/schema/Output.schema.json
new file mode 100644
index 000000000..42fffb7e9
--- /dev/null
+++ b/schema/Output.schema.json
@@ -0,0 +1,53 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "compression": {
+ "format": "default",
+ "default": "zstd",
+ "type": "string"
+ },
+ "basin": {
+ "format": "default",
+ "default": "output/basin.arrow",
+ "type": "string"
+ },
+ "flow": {
+ "format": "default",
+ "default": "output/flow.arrow",
+ "type": "string"
+ },
+ "control": {
+ "format": "default",
+ "default": "output/control.arrow",
+ "type": "string"
+ },
+ "outstate": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ },
+ "compression_level": {
+ "format": "default",
+ "default": 6,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "basin",
+ "flow",
+ "control",
+ "compression",
+ "compression_level"
+ ],
+ "$id": "https://deltares.github.io/Ribasim/schema/Output.schema.json",
+ "title": "Output",
+ "description": "A Output object based on Ribasim.config.Output",
+ "type": "object"
+}
diff --git a/schema/PIDControlStatic.schema.json b/schema/PIDControlStatic.schema.json
index b7725031f..df64576ee 100644
--- a/schema/PIDControlStatic.schema.json
+++ b/schema/PIDControlStatic.schema.json
@@ -3,7 +3,6 @@
"properties": {
"integral": {
"format": "double",
- "description": "integral",
"type": "number"
},
"remarks": {
@@ -14,41 +13,44 @@
},
"listen_node_id": {
"format": "default",
- "description": "listen_node_id",
"type": "integer"
},
"active": {
"format": "default",
- "description": "active",
- "type": [
- "boolean"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "boolean"
+ }
]
},
"proportional": {
"format": "double",
- "description": "proportional",
"type": "number"
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"target": {
"format": "double",
- "description": "target",
"type": "number"
},
"derivative": {
"format": "double",
- "description": "derivative",
"type": "number"
},
"control_state": {
"format": "default",
- "description": "control_state",
- "type": [
- "string"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
]
}
},
diff --git a/schema/PidControlTime.schema.json b/schema/PidControlTime.schema.json
index c86236d97..de34eaef8 100644
--- a/schema/PidControlTime.schema.json
+++ b/schema/PidControlTime.schema.json
@@ -3,7 +3,6 @@
"properties": {
"integral": {
"format": "double",
- "description": "integral",
"type": "number"
},
"remarks": {
@@ -14,39 +13,37 @@
},
"listen_node_id": {
"format": "default",
- "description": "listen_node_id",
"type": "integer"
},
"time": {
"format": "date-time",
- "description": "time",
"type": "string"
},
"proportional": {
"format": "double",
- "description": "proportional",
"type": "number"
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"target": {
"format": "double",
- "description": "target",
"type": "number"
},
"derivative": {
"format": "double",
- "description": "derivative",
"type": "number"
},
"control_state": {
"format": "default",
- "description": "control_state",
- "type": [
- "string"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
]
}
},
diff --git a/schema/PumpStatic.schema.json b/schema/PumpStatic.schema.json
index fe4a95864..afe80e42c 100644
--- a/schema/PumpStatic.schema.json
+++ b/schema/PumpStatic.schema.json
@@ -3,9 +3,13 @@
"properties": {
"max_flow_rate": {
"format": "default",
- "description": "max_flow_rate",
- "type": [
- "number"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "number"
+ }
]
},
"remarks": {
@@ -16,33 +20,43 @@
},
"active": {
"format": "default",
- "description": "active",
- "type": [
- "boolean"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "boolean"
+ }
]
},
"flow_rate": {
"format": "double",
- "description": "flow_rate",
"type": "number"
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"control_state": {
"format": "default",
- "description": "control_state",
- "type": [
- "string"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
]
},
"min_flow_rate": {
"format": "default",
- "description": "min_flow_rate",
- "type": [
- "number"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "number"
+ }
]
}
},
diff --git a/schema/Solver.schema.json b/schema/Solver.schema.json
new file mode 100644
index 000000000..ca6c57bd9
--- /dev/null
+++ b/schema/Solver.schema.json
@@ -0,0 +1,76 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "reltol": {
+ "format": "double",
+ "default": 0.001,
+ "type": "number"
+ },
+ "saveat": {
+ "format": "default",
+ "anyOf": [
+ {
+ "items": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "default": [
+ ]
+ },
+ "maxiters": {
+ "format": "default",
+ "default": 1000000000,
+ "type": "integer"
+ },
+ "autodiff": {
+ "format": "default",
+ "default": true,
+ "type": "boolean"
+ },
+ "adaptive": {
+ "format": "default",
+ "default": true,
+ "type": "boolean"
+ },
+ "algorithm": {
+ "format": "default",
+ "default": "QNDF",
+ "type": "string"
+ },
+ "abstol": {
+ "format": "double",
+ "default": 1.0e-6,
+ "type": "number"
+ },
+ "dt": {
+ "format": "double",
+ "default": 0,
+ "type": "number"
+ },
+ "sparse": {
+ "format": "default",
+ "default": true,
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "algorithm",
+ "saveat",
+ "adaptive",
+ "dt",
+ "abstol",
+ "reltol",
+ "maxiters",
+ "sparse",
+ "autodiff"
+ ],
+ "$id": "https://deltares.github.io/Ribasim/schema/Solver.schema.json",
+ "title": "Solver",
+ "description": "A Solver object based on Ribasim.config.Solver",
+ "type": "object"
+}
diff --git a/schema/TabulatedRatingCurveStatic.schema.json b/schema/TabulatedRatingCurveStatic.schema.json
index b89ea9ccf..cb244575b 100644
--- a/schema/TabulatedRatingCurveStatic.schema.json
+++ b/schema/TabulatedRatingCurveStatic.schema.json
@@ -9,31 +9,36 @@
},
"active": {
"format": "default",
- "description": "active",
- "type": [
- "boolean"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "boolean"
+ }
]
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"discharge": {
"format": "double",
- "description": "discharge",
"type": "number"
},
"level": {
"format": "double",
- "description": "level",
"type": "number"
},
"control_state": {
"format": "default",
- "description": "control_state",
- "type": [
- "string"
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
]
}
},
diff --git a/schema/TabulatedRatingCurveTime.schema.json b/schema/TabulatedRatingCurveTime.schema.json
index f65ae219c..47c2ddeb7 100644
--- a/schema/TabulatedRatingCurveTime.schema.json
+++ b/schema/TabulatedRatingCurveTime.schema.json
@@ -9,22 +9,18 @@
},
"time": {
"format": "date-time",
- "description": "time",
"type": "string"
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
},
"discharge": {
"format": "double",
- "description": "discharge",
"type": "number"
},
"level": {
"format": "double",
- "description": "level",
"type": "number"
}
},
diff --git a/schema/TerminalStatic.schema.json b/schema/TerminalStatic.schema.json
index 4d4091a68..257433ed7 100644
--- a/schema/TerminalStatic.schema.json
+++ b/schema/TerminalStatic.schema.json
@@ -9,7 +9,6 @@
},
"node_id": {
"format": "default",
- "description": "node_id",
"type": "integer"
}
},
diff --git a/schema/basin.schema.json b/schema/basin.schema.json
new file mode 100644
index 000000000..6113db247
--- /dev/null
+++ b/schema/basin.schema.json
@@ -0,0 +1,59 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "profile": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ },
+ "static": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ },
+ "forcing": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ },
+ "state": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ }
+ },
+ "required": [
+ ],
+ "$id": "https://deltares.github.io/Ribasim/schema/basin.schema.json",
+ "title": "basin",
+ "description": "A basin object based on Ribasim.config.basin",
+ "type": "object"
+}
diff --git a/schema/discrete_control.schema.json b/schema/discrete_control.schema.json
new file mode 100644
index 000000000..22a244703
--- /dev/null
+++ b/schema/discrete_control.schema.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "logic": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ },
+ "condition": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ }
+ },
+ "required": [
+ ],
+ "$id": "https://deltares.github.io/Ribasim/schema/discrete_control.schema.json",
+ "title": "discrete_control",
+ "description": "A discrete_control object based on Ribasim.config.discrete_control",
+ "type": "object"
+}
diff --git a/schema/flow_boundary.schema.json b/schema/flow_boundary.schema.json
new file mode 100644
index 000000000..29f922f9a
--- /dev/null
+++ b/schema/flow_boundary.schema.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "time": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ },
+ "static": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ }
+ },
+ "required": [
+ ],
+ "$id": "https://deltares.github.io/Ribasim/schema/flow_boundary.schema.json",
+ "title": "flow_boundary",
+ "description": "A flow_boundary object based on Ribasim.config.flow_boundary",
+ "type": "object"
+}
diff --git a/schema/fractional_flow.schema.json b/schema/fractional_flow.schema.json
new file mode 100644
index 000000000..940b2977d
--- /dev/null
+++ b/schema/fractional_flow.schema.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "static": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ }
+ },
+ "required": [
+ ],
+ "$id": "https://deltares.github.io/Ribasim/schema/fractional_flow.schema.json",
+ "title": "fractional_flow",
+ "description": "A fractional_flow object based on Ribasim.config.fractional_flow",
+ "type": "object"
+}
diff --git a/schema/level_boundary.schema.json b/schema/level_boundary.schema.json
new file mode 100644
index 000000000..f4c2d70c8
--- /dev/null
+++ b/schema/level_boundary.schema.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "time": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ },
+ "static": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ }
+ },
+ "required": [
+ ],
+ "$id": "https://deltares.github.io/Ribasim/schema/level_boundary.schema.json",
+ "title": "level_boundary",
+ "description": "A level_boundary object based on Ribasim.config.level_boundary",
+ "type": "object"
+}
diff --git a/schema/linear_resistance.schema.json b/schema/linear_resistance.schema.json
new file mode 100644
index 000000000..96565c7b3
--- /dev/null
+++ b/schema/linear_resistance.schema.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "static": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ }
+ },
+ "required": [
+ ],
+ "$id": "https://deltares.github.io/Ribasim/schema/linear_resistance.schema.json",
+ "title": "linear_resistance",
+ "description": "A linear_resistance object based on Ribasim.config.linear_resistance",
+ "type": "object"
+}
diff --git a/schema/manning_resistance.schema.json b/schema/manning_resistance.schema.json
new file mode 100644
index 000000000..aef075138
--- /dev/null
+++ b/schema/manning_resistance.schema.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "static": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ }
+ },
+ "required": [
+ ],
+ "$id": "https://deltares.github.io/Ribasim/schema/manning_resistance.schema.json",
+ "title": "manning_resistance",
+ "description": "A manning_resistance object based on Ribasim.config.manning_resistance",
+ "type": "object"
+}
diff --git a/schema/outlet.schema.json b/schema/outlet.schema.json
new file mode 100644
index 000000000..1e6e2efd5
--- /dev/null
+++ b/schema/outlet.schema.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "static": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ }
+ },
+ "required": [
+ ],
+ "$id": "https://deltares.github.io/Ribasim/schema/outlet.schema.json",
+ "title": "outlet",
+ "description": "A outlet object based on Ribasim.config.outlet",
+ "type": "object"
+}
diff --git a/schema/pid_control.schema.json b/schema/pid_control.schema.json
new file mode 100644
index 000000000..5e7eb7024
--- /dev/null
+++ b/schema/pid_control.schema.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "time": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ },
+ "static": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ }
+ },
+ "required": [
+ ],
+ "$id": "https://deltares.github.io/Ribasim/schema/pid_control.schema.json",
+ "title": "pid_control",
+ "description": "A pid_control object based on Ribasim.config.pid_control",
+ "type": "object"
+}
diff --git a/schema/pump.schema.json b/schema/pump.schema.json
new file mode 100644
index 000000000..2679991b2
--- /dev/null
+++ b/schema/pump.schema.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "static": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ }
+ },
+ "required": [
+ ],
+ "$id": "https://deltares.github.io/Ribasim/schema/pump.schema.json",
+ "title": "pump",
+ "description": "A pump object based on Ribasim.config.pump",
+ "type": "object"
+}
diff --git a/schema/tabulated_rating_curve.schema.json b/schema/tabulated_rating_curve.schema.json
new file mode 100644
index 000000000..2d1beff66
--- /dev/null
+++ b/schema/tabulated_rating_curve.schema.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "time": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ },
+ "static": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ }
+ },
+ "required": [
+ ],
+ "$id": "https://deltares.github.io/Ribasim/schema/tabulated_rating_curve.schema.json",
+ "title": "tabulated_rating_curve",
+ "description": "A tabulated_rating_curve object based on Ribasim.config.tabulated_rating_curve",
+ "type": "object"
+}
diff --git a/schema/terminal.schema.json b/schema/terminal.schema.json
new file mode 100644
index 000000000..c6ca4c650
--- /dev/null
+++ b/schema/terminal.schema.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "static": {
+ "format": "default",
+ "anyOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "default": null
+ }
+ },
+ "required": [
+ ],
+ "$id": "https://deltares.github.io/Ribasim/schema/terminal.schema.json",
+ "title": "terminal",
+ "description": "A terminal object based on Ribasim.config.terminal",
+ "type": "object"
+}
diff --git a/search.json b/search.json
index 210d09dae..2f4cb3e2c 100644
--- a/search.json
+++ b/search.json
@@ -599,7 +599,7 @@
"href": "python/examples.html",
"title": "Examples",
"section": "",
- "text": "1 Basic model with static forcing\n\nimport geopandas as gpd\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\n\nimport ribasim\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\n \"node_id\": [1, 1, 3, 3, 6, 6, 9, 9],\n \"area\": [0.01, 1000.0] * 4,\n \"level\": [0.0, 1.0] * 4,\n }\n)\n\n# Convert steady forcing to m/s\n# 2 mm/d precipitation, 1 mm/d evaporation\nseconds_in_day = 24 * 3600\nprecipitation = 0.002 / seconds_in_day\nevaporation = 0.001 / seconds_in_day\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [0],\n \"drainage\": [0.0],\n \"potential_evaporation\": [evaporation],\n \"infiltration\": [0.0],\n \"precipitation\": [precipitation],\n \"urban_runoff\": [0.0],\n }\n)\nstatic = static.iloc[[0, 0, 0, 0]]\nstatic[\"node_id\"] = [1, 3, 6, 9]\n\nbasin = ribasim.Basin(profile=profile, static=static)\n\nSetup linear resistance:\n\nlinear_resistance = ribasim.LinearResistance(\n static=pd.DataFrame(\n data={\"node_id\": [10, 12], \"resistance\": [5e3, (3600.0 * 24) / 100.0]}\n )\n)\n\nSetup Manning resistance:\n\nmanning_resistance = ribasim.ManningResistance(\n static=pd.DataFrame(\n data={\n \"node_id\": [2],\n \"length\": [900.0],\n \"manning_n\": [0.04],\n \"profile_width\": [6.0],\n \"profile_slope\": [3.0],\n }\n )\n)\n\nSet up a rating curve node:\n\n# Discharge: lose 1% of storage volume per day at storage = 1000.0.\nq1000 = 1000.0 * 0.01 / seconds_in_day\n\nrating_curve = ribasim.TabulatedRatingCurve(\n static=pd.DataFrame(\n data={\n \"node_id\": [4, 4],\n \"level\": [0.0, 1.0],\n \"discharge\": [0.0, q1000],\n }\n )\n)\n\nSetup fractional flows:\n\nfractional_flow = ribasim.FractionalFlow(\n static=pd.DataFrame(\n data={\n \"node_id\": [5, 8, 13],\n \"fraction\": [0.3, 0.6, 0.1],\n }\n )\n)\n\nSetup pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": [7],\n \"flow_rate\": [0.5 / 3600],\n }\n )\n)\n\nSetup level boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [11, 17],\n \"level\": [0.5, 1.5],\n }\n )\n)\n\nSetup flow boundary:\n\nflow_boundary = ribasim.FlowBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [15, 16],\n \"flow_rate\": [1e-4, 1e-4],\n }\n )\n)\n\nSetup terminal:\n\nterminal = ribasim.Terminal(\n static=pd.DataFrame(\n data={\n \"node_id\": [14],\n }\n )\n)\n\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: Basin,\n (1.0, 0.0), # 2: ManningResistance\n (2.0, 0.0), # 3: Basin\n (3.0, 0.0), # 4: TabulatedRatingCurve\n (3.0, 1.0), # 5: FractionalFlow\n (3.0, 2.0), # 6: Basin\n (4.0, 1.0), # 7: Pump\n (4.0, 0.0), # 8: FractionalFlow\n (5.0, 0.0), # 9: Basin\n (6.0, 0.0), # 10: LinearResistance\n (2.0, 2.0), # 11: LevelBoundary\n (2.0, 1.0), # 12: LinearResistance\n (3.0, -1.0), # 13: FractionalFlow\n (3.0, -2.0), # 14: Terminal\n (3.0, 3.0), # 15: FlowBoundary\n (0.0, 1.0), # 16: FlowBoundary\n (6.0, 1.0), # 17: LevelBoundary\n ]\n)\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_id, node_type = ribasim.Node.get_node_ids_and_types(\n basin,\n manning_resistance,\n rating_curve,\n pump,\n fractional_flow,\n linear_resistance,\n level_boundary,\n flow_boundary,\n terminal,\n)\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(node_id, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array(\n [1, 2, 3, 4, 4, 5, 6, 8, 7, 9, 11, 12, 4, 13, 15, 16, 10], dtype=np.int64\n)\nto_id = np.array(\n [2, 3, 4, 5, 8, 6, 7, 9, 9, 10, 12, 3, 13, 14, 6, 1, 17], dtype=np.int64\n)\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\n \"from_node_id\": from_id,\n \"to_node_id\": to_id,\n \"edge_type\": len(from_id) * [\"flow\"],\n },\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"basic\",\n node=node,\n edge=edge,\n basin=basin,\n level_boundary=level_boundary,\n flow_boundary=flow_boundary,\n pump=pump,\n linear_resistance=linear_resistance,\n manning_resistance=manning_resistance,\n tabulated_rating_curve=rating_curve,\n fractional_flow=fractional_flow,\n terminal=terminal,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2021-01-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"basic\")\n\n\n\n2 Update the basic model with transient forcing\nThis assumes you have already created the basic model with static forcing.\n\nimport numpy as np\nimport pandas as pd\nimport xarray as xr\n\nimport ribasim\n\n\nmodel = ribasim.Model.from_toml(datadir / \"basic/basic.toml\")\n\n\ntime = pd.date_range(model.starttime, model.endtime)\nday_of_year = time.day_of_year.to_numpy()\nseconds_per_day = 24 * 60 * 60\nevaporation = (\n (-1.0 * np.cos(day_of_year / 365.0 * 2 * np.pi) + 1.0) * 0.0025 / seconds_per_day\n)\nrng = np.random.default_rng(seed=0)\nprecipitation = (\n rng.lognormal(mean=-1.0, sigma=1.7, size=time.size) * 0.001 / seconds_per_day\n)\n\nWe’ll use xarray to easily broadcast the values.\n\ntimeseries = (\n pd.DataFrame(\n data={\n \"node_id\": 1,\n \"time\": time,\n \"drainage\": 0.0,\n \"potential_evaporation\": evaporation,\n \"infiltration\": 0.0,\n \"precipitation\": precipitation,\n \"urban_runoff\": 0.0,\n }\n )\n .set_index(\"time\")\n .to_xarray()\n)\n\nbasin_ids = model.basin.static[\"node_id\"].to_numpy()\nbasin_nodes = xr.DataArray(\n np.ones(len(basin_ids)), coords={\"node_id\": basin_ids}, dims=[\"node_id\"]\n)\nforcing = (timeseries * basin_nodes).to_dataframe().reset_index()\n\n\nstate = pd.DataFrame(\n data={\n \"node_id\": basin_ids,\n \"level\": 1.4,\n \"concentration\": 0.0,\n }\n)\n\n\nmodel.basin.forcing = forcing\nmodel.basin.state = state\n\n\nmodel.modelname = \"basic_transient\"\nmodel.write(datadir / \"basic_transient\")\n\nNow run the model with ribasim basic-transient/basic.toml. After running the model, read back the output:\n\ndf_basin = pd.read_feather(datadir / \"basic_transient/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\ndf_basin_wide[\"level\"].plot()\n\n<Axes: xlabel='time'>\n\n\n\n\n\n\ndf_flow = pd.read_feather(datadir / \"basic_transient/output/flow.arrow\")\ndf_flow[\"edge\"] = list(zip(df_flow.from_node_id, df_flow.to_node_id))\ndf_flow[\"flow_m3d\"] = df_flow.flow * 86400\nax = df_flow.pivot_table(index=\"time\", columns=\"edge\", values=\"flow_m3d\").plot()\nax.legend(bbox_to_anchor=(1.3, 1), title=\"Edge\")\n\n<matplotlib.legend.Legend at 0x7f35e2e9a550>\n\n\n\n\n\n\ntype(df_flow)\n\npandas.core.frame.DataFrame\n\n\n\n\n3 Model with discrete control\nThe model constructed below consists of a single basin which slowly drains trough a TabulatedRatingCurve, but is held within a range around a target level (setpoint) by two connected pumps. These two pumps behave like a reversible pump. When pumping can be done in only one direction, and the other direction is only possible under gravity, use an Outlet for that direction.\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: Basin\n (1.0, 0.5), # 2: Pump\n (1.0, -0.5), # 3: Pump\n (2.0, 0.0), # 4: LevelBoundary\n (-1.0, 0.0), # 5: TabulatedRatingCurve\n (-2.0, 0.0), # 6: Terminal\n (1.0, 0.0), # 7: DiscreteControl\n ]\n)\n\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_type = [\n \"Basin\",\n \"Pump\",\n \"Pump\",\n \"LevelBoundary\",\n \"TabulatedRatingCurve\",\n \"Terminal\",\n \"DiscreteControl\",\n]\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(np.arange(len(xy)) + 1, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array([1, 3, 4, 2, 1, 5, 7, 7], dtype=np.int64)\nto_id = np.array([3, 4, 2, 1, 5, 6, 2, 3], dtype=np.int64)\n\nedge_type = 6 * [\"flow\"] + 2 * [\"control\"]\n\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\"from_node_id\": from_id, \"to_node_id\": to_id, \"edge_type\": edge_type},\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\n \"node_id\": [1, 1],\n \"area\": [1000.0, 1000.0],\n \"level\": [0.0, 1.0],\n }\n)\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [1],\n \"drainage\": [0.0],\n \"potential_evaporation\": [0.0],\n \"infiltration\": [0.0],\n \"precipitation\": [0.0],\n \"urban_runoff\": [0.0],\n }\n)\n\nstate = pd.DataFrame(data={\"node_id\": [1], \"level\": [20.0]})\n\nbasin = ribasim.Basin(profile=profile, static=static, state=state)\n\nSetup the discrete control:\n\ncondition = pd.DataFrame(\n data={\n \"node_id\": 3 * [7],\n \"listen_feature_id\": 3 * [1],\n \"variable\": 3 * [\"level\"],\n \"greater_than\": [5.0, 10.0, 15.0], # min, setpoint, max\n }\n)\n\nlogic = pd.DataFrame(\n data={\n \"node_id\": 5 * [7],\n \"truth_state\": [\"FFF\", \"U**\", \"T*F\", \"**D\", \"TTT\"],\n \"control_state\": [\"in\", \"in\", \"none\", \"out\", \"out\"],\n }\n)\n\ndiscrete_control = ribasim.DiscreteControl(condition=condition, logic=logic)\n\nThe above control logic can be summarized as follows: - If the level gets above the maximum, activate the control state “out” until the setpoint is reached; - If the level gets below the minimum, active the control state “in” until the setpoint is reached; - Otherwise activate the control state “none”.\nSetup the pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": 3 * [2] + 3 * [3],\n \"control_state\": 2 * [\"none\", \"in\", \"out\"],\n \"flow_rate\": [0.0, 2e-3, 0.0, 0.0, 0.0, 2e-3],\n }\n )\n)\n\nThe pump data defines the following:\n\n\n\nControl state\nPump #2 flow rate (m/s)\nPump #3 flow rate (m/s)\n\n\n\n\n“none”\n0.0\n0.0\n\n\n“in”\n2e-3\n0.0\n\n\n“out”\n0.0\n2e-3\n\n\n\nSetup the level boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(data={\"node_id\": [4], \"level\": [10.0]})\n)\n\nSetup the rating curve:\n\nrating_curve = ribasim.TabulatedRatingCurve(\n static=pd.DataFrame(\n data={\"node_id\": 2 * [5], \"level\": [2.0, 15.0], \"discharge\": [0.0, 1e-3]}\n )\n)\n\nSetup the terminal:\n\nterminal = ribasim.Terminal(static=pd.DataFrame(data={\"node_id\": [6]}))\n\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"level_setpoint_with_minmax\",\n node=node,\n edge=edge,\n basin=basin,\n pump=pump,\n level_boundary=level_boundary,\n tabulated_rating_curve=rating_curve,\n terminal=terminal,\n discrete_control=discrete_control,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2021-01-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nListen edges are plotted with a dashed line since they are not present in the “Edge / static” schema but only in the “Control / condition” schema.\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"level_setpoint_with_minmax\")\n\nNow run the model with level_setpoint_with_minmax/level_setpoint_with_minmax.toml. After running the model, read back the output:\n\nfrom matplotlib.dates import date2num\n\ndf_basin = pd.read_feather(datadir / \"level_setpoint_with_minmax/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\n\nax = df_basin_wide[\"level\"].plot()\n\ngreater_than = model.discrete_control.condition.greater_than\n\nax.hlines(\n greater_than,\n df_basin.time[0],\n df_basin.time.max(),\n lw=1,\n ls=\"--\",\n color=\"k\",\n)\n\ndf_control = pd.read_feather(\n datadir / \"level_setpoint_with_minmax/output/control.arrow\"\n)\n\ny_min, y_max = ax.get_ybound()\nax.fill_between(df_control.time[:2], 2 * [y_min], 2 * [y_max], alpha=0.2, color=\"C0\")\nax.fill_between(df_control.time[2:4], 2 * [y_min], 2 * [y_max], alpha=0.2, color=\"C0\")\n\nax.set_xticks(\n date2num(df_control.time).tolist(),\n df_control.control_state.tolist(),\n rotation=50,\n)\n\nax.set_yticks(greater_than, [\"min\", \"setpoint\", \"max\"])\nax.set_ylabel(\"level\")\nplt.show()\n\n\n\n\nThe highlighted regions show where a pump is active.\nLet’s print an overview of what happened with control:\n\nmodel.print_discrete_control_record(\n datadir / \"level_setpoint_with_minmax/output/control.arrow\"\n)\n\n0. At 2020-01-01 00:00:00 the control node with ID 7 reached truth state TTT:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level > 10.0\n For node ID 1 (Basin): level > 15.0\n\n This yielded control state \"out\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.002\n\n1. At 2020-02-09 01:17:29.324000 the control node with ID 7 reached truth state TFF:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level < 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"none\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.0\n\n2. At 2020-07-05 13:24:51.165000 the control node with ID 7 reached truth state FFF:\n For node ID 1 (Basin): level < 5.0\n For node ID 1 (Basin): level < 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"in\":\n For node ID 2 (Pump): flow_rate = 0.002\n For node ID 3 (Pump): flow_rate = 0.0\n\n3. At 2020-08-11 11:49:59.015000 the control node with ID 7 reached truth state TTF:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level > 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"none\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.0\n\n\n\nNote that crossing direction specific truth states (containing “U”, “D”) are not present in this overview even though they are part of the control logic. This is because in the control logic for this model these truth states are only used to sustain control states, while the overview only shows changes in control states.\n\n\n4 Model with PID control\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: FlowBoundary\n (1.0, 0.0), # 2: Basin\n (2.0, 0.5), # 3: Pump\n (3.0, 0.0), # 4: LevelBoundary\n (1.5, 1.0), # 5: PidControl\n (2.0, -0.5), # 6: outlet\n (1.5, -1.0), # 7: PidControl\n ]\n)\n\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_type = [\n \"FlowBoundary\",\n \"Basin\",\n \"Pump\",\n \"LevelBoundary\",\n \"PidControl\",\n \"Outlet\",\n \"PidControl\",\n]\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(np.arange(len(xy)) + 1, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array([1, 2, 3, 4, 6, 5, 7], dtype=np.int64)\nto_id = np.array([2, 3, 4, 6, 2, 3, 6], dtype=np.int64)\n\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\n \"from_node_id\": from_id,\n \"to_node_id\": to_id,\n \"edge_type\": 5 * [\"flow\"] + 2 * [\"control\"],\n },\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\"node_id\": [2, 2], \"level\": [0.0, 1.0], \"area\": [1000.0, 1000.0]}\n)\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [2],\n \"drainage\": [0.0],\n \"potential_evaporation\": [0.0],\n \"infiltration\": [0.0],\n \"precipitation\": [0.0],\n \"urban_runoff\": [0.0],\n }\n)\n\nstate = pd.DataFrame(\n data={\n \"node_id\": [2],\n \"level\": [6.0],\n }\n)\n\nbasin = ribasim.Basin(profile=profile, static=static, state=state)\n\nSetup the pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": [3],\n \"flow_rate\": [0.0], # Will be overwritten by PID controller\n }\n )\n)\n\nSetup the outlet:\n\noutlet = ribasim.Outlet(\n static=pd.DataFrame(\n data={\n \"node_id\": [6],\n \"flow_rate\": [0.0], # Will be overwritten by PID controller\n }\n )\n)\n\nSetup flow boundary:\n\nflow_boundary = ribasim.FlowBoundary(\n static=pd.DataFrame(data={\"node_id\": [1], \"flow_rate\": [1e-3]})\n)\n\nSetup flow boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [4],\n \"level\": [1.0], # Not relevant\n }\n )\n)\n\nSetup PID control:\n\npid_control = ribasim.PidControl(\n time=pd.DataFrame(\n data={\n \"node_id\": 4 * [5, 7],\n \"time\": [\n \"2020-01-01 00:00:00\",\n \"2020-01-01 00:00:00\",\n \"2020-05-01 00:00:00\",\n \"2020-05-01 00:00:00\",\n \"2020-07-01 00:00:00\",\n \"2020-07-01 00:00:00\",\n \"2020-12-01 00:00:00\",\n \"2020-12-01 00:00:00\",\n ],\n \"listen_node_id\": 4 * [2, 2],\n \"target\": [5.0, 5.0, 5.0, 5.0, 7.5, 7.5, 7.5, 7.5],\n \"proportional\": 4 * [-1e-3, 1e-3],\n \"integral\": 4 * [-1e-7, 1e-7],\n \"derivative\": 4 * [0.0, 0.0],\n }\n )\n)\n\nNote that the coefficients for the pump and the outlet are equal in magnitude but opposite in sign. This way the pump and the outlet equally work towards the same goal, while having opposite effects on the controlled basin due to their connectivity to this basin.\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"pid_control\",\n node=node,\n edge=edge,\n basin=basin,\n flow_boundary=flow_boundary,\n level_boundary=level_boundary,\n pump=pump,\n outlet=outlet,\n pid_control=pid_control,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2020-12-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"pid_control\")\n\nNow run the model with ribasim pid_control/pid_control.toml. After running the model, read back the output:\n\nfrom matplotlib.dates import date2num\n\ndf_basin = pd.read_feather(datadir / \"pid_control/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\nax = df_basin_wide[\"level\"].plot()\nax.set_ylabel(\"level [m]\")\n\n# Plot target level\ntarget_levels = model.pid_control.time.target.to_numpy()[::2]\ntimes = date2num(model.pid_control.time.time)[::2]\nax.plot(times, target_levels, color=\"k\", ls=\":\", label=\"target level\");"
+ "text": "1 Basic model with static forcing\n\nimport geopandas as gpd\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\n\nimport ribasim\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\n \"node_id\": [1, 1, 3, 3, 6, 6, 9, 9],\n \"area\": [0.01, 1000.0] * 4,\n \"level\": [0.0, 1.0] * 4,\n }\n)\n\n# Convert steady forcing to m/s\n# 2 mm/d precipitation, 1 mm/d evaporation\nseconds_in_day = 24 * 3600\nprecipitation = 0.002 / seconds_in_day\nevaporation = 0.001 / seconds_in_day\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [0],\n \"drainage\": [0.0],\n \"potential_evaporation\": [evaporation],\n \"infiltration\": [0.0],\n \"precipitation\": [precipitation],\n \"urban_runoff\": [0.0],\n }\n)\nstatic = static.iloc[[0, 0, 0, 0]]\nstatic[\"node_id\"] = [1, 3, 6, 9]\n\nbasin = ribasim.Basin(profile=profile, static=static)\n\nSetup linear resistance:\n\nlinear_resistance = ribasim.LinearResistance(\n static=pd.DataFrame(\n data={\"node_id\": [10, 12], \"resistance\": [5e3, (3600.0 * 24) / 100.0]}\n )\n)\n\nSetup Manning resistance:\n\nmanning_resistance = ribasim.ManningResistance(\n static=pd.DataFrame(\n data={\n \"node_id\": [2],\n \"length\": [900.0],\n \"manning_n\": [0.04],\n \"profile_width\": [6.0],\n \"profile_slope\": [3.0],\n }\n )\n)\n\nSet up a rating curve node:\n\n# Discharge: lose 1% of storage volume per day at storage = 1000.0.\nq1000 = 1000.0 * 0.01 / seconds_in_day\n\nrating_curve = ribasim.TabulatedRatingCurve(\n static=pd.DataFrame(\n data={\n \"node_id\": [4, 4],\n \"level\": [0.0, 1.0],\n \"discharge\": [0.0, q1000],\n }\n )\n)\n\nSetup fractional flows:\n\nfractional_flow = ribasim.FractionalFlow(\n static=pd.DataFrame(\n data={\n \"node_id\": [5, 8, 13],\n \"fraction\": [0.3, 0.6, 0.1],\n }\n )\n)\n\nSetup pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": [7],\n \"flow_rate\": [0.5 / 3600],\n }\n )\n)\n\nSetup level boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [11, 17],\n \"level\": [0.5, 1.5],\n }\n )\n)\n\nSetup flow boundary:\n\nflow_boundary = ribasim.FlowBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [15, 16],\n \"flow_rate\": [1e-4, 1e-4],\n }\n )\n)\n\nSetup terminal:\n\nterminal = ribasim.Terminal(\n static=pd.DataFrame(\n data={\n \"node_id\": [14],\n }\n )\n)\n\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: Basin,\n (1.0, 0.0), # 2: ManningResistance\n (2.0, 0.0), # 3: Basin\n (3.0, 0.0), # 4: TabulatedRatingCurve\n (3.0, 1.0), # 5: FractionalFlow\n (3.0, 2.0), # 6: Basin\n (4.0, 1.0), # 7: Pump\n (4.0, 0.0), # 8: FractionalFlow\n (5.0, 0.0), # 9: Basin\n (6.0, 0.0), # 10: LinearResistance\n (2.0, 2.0), # 11: LevelBoundary\n (2.0, 1.0), # 12: LinearResistance\n (3.0, -1.0), # 13: FractionalFlow\n (3.0, -2.0), # 14: Terminal\n (3.0, 3.0), # 15: FlowBoundary\n (0.0, 1.0), # 16: FlowBoundary\n (6.0, 1.0), # 17: LevelBoundary\n ]\n)\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_id, node_type = ribasim.Node.get_node_ids_and_types(\n basin,\n manning_resistance,\n rating_curve,\n pump,\n fractional_flow,\n linear_resistance,\n level_boundary,\n flow_boundary,\n terminal,\n)\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(node_id, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array(\n [1, 2, 3, 4, 4, 5, 6, 8, 7, 9, 11, 12, 4, 13, 15, 16, 10], dtype=np.int64\n)\nto_id = np.array(\n [2, 3, 4, 5, 8, 6, 7, 9, 9, 10, 12, 3, 13, 14, 6, 1, 17], dtype=np.int64\n)\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\n \"from_node_id\": from_id,\n \"to_node_id\": to_id,\n \"edge_type\": len(from_id) * [\"flow\"],\n },\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"basic\",\n node=node,\n edge=edge,\n basin=basin,\n level_boundary=level_boundary,\n flow_boundary=flow_boundary,\n pump=pump,\n linear_resistance=linear_resistance,\n manning_resistance=manning_resistance,\n tabulated_rating_curve=rating_curve,\n fractional_flow=fractional_flow,\n terminal=terminal,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2021-01-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"basic\")\n\n\n\n2 Update the basic model with transient forcing\nThis assumes you have already created the basic model with static forcing.\n\nimport numpy as np\nimport pandas as pd\nimport xarray as xr\n\nimport ribasim\n\n\nmodel = ribasim.Model.from_toml(datadir / \"basic/basic.toml\")\n\n\ntime = pd.date_range(model.starttime, model.endtime)\nday_of_year = time.day_of_year.to_numpy()\nseconds_per_day = 24 * 60 * 60\nevaporation = (\n (-1.0 * np.cos(day_of_year / 365.0 * 2 * np.pi) + 1.0) * 0.0025 / seconds_per_day\n)\nrng = np.random.default_rng(seed=0)\nprecipitation = (\n rng.lognormal(mean=-1.0, sigma=1.7, size=time.size) * 0.001 / seconds_per_day\n)\n\nWe’ll use xarray to easily broadcast the values.\n\ntimeseries = (\n pd.DataFrame(\n data={\n \"node_id\": 1,\n \"time\": time,\n \"drainage\": 0.0,\n \"potential_evaporation\": evaporation,\n \"infiltration\": 0.0,\n \"precipitation\": precipitation,\n \"urban_runoff\": 0.0,\n }\n )\n .set_index(\"time\")\n .to_xarray()\n)\n\nbasin_ids = model.basin.static[\"node_id\"].to_numpy()\nbasin_nodes = xr.DataArray(\n np.ones(len(basin_ids)), coords={\"node_id\": basin_ids}, dims=[\"node_id\"]\n)\nforcing = (timeseries * basin_nodes).to_dataframe().reset_index()\n\n\nstate = pd.DataFrame(\n data={\n \"node_id\": basin_ids,\n \"level\": 1.4,\n \"concentration\": 0.0,\n }\n)\n\n\nmodel.basin.forcing = forcing\nmodel.basin.state = state\n\n\nmodel.modelname = \"basic_transient\"\nmodel.write(datadir / \"basic_transient\")\n\nNow run the model with ribasim basic-transient/basic.toml. After running the model, read back the output:\n\ndf_basin = pd.read_feather(datadir / \"basic_transient/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\ndf_basin_wide[\"level\"].plot()\n\n<Axes: xlabel='time'>\n\n\n\n\n\n\ndf_flow = pd.read_feather(datadir / \"basic_transient/output/flow.arrow\")\ndf_flow[\"edge\"] = list(zip(df_flow.from_node_id, df_flow.to_node_id))\ndf_flow[\"flow_m3d\"] = df_flow.flow * 86400\nax = df_flow.pivot_table(index=\"time\", columns=\"edge\", values=\"flow_m3d\").plot()\nax.legend(bbox_to_anchor=(1.3, 1), title=\"Edge\")\n\n<matplotlib.legend.Legend at 0x7f14799f5c10>\n\n\n\n\n\n\ntype(df_flow)\n\npandas.core.frame.DataFrame\n\n\n\n\n3 Model with discrete control\nThe model constructed below consists of a single basin which slowly drains trough a TabulatedRatingCurve, but is held within a range around a target level (setpoint) by two connected pumps. These two pumps behave like a reversible pump. When pumping can be done in only one direction, and the other direction is only possible under gravity, use an Outlet for that direction.\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: Basin\n (1.0, 0.5), # 2: Pump\n (1.0, -0.5), # 3: Pump\n (2.0, 0.0), # 4: LevelBoundary\n (-1.0, 0.0), # 5: TabulatedRatingCurve\n (-2.0, 0.0), # 6: Terminal\n (1.0, 0.0), # 7: DiscreteControl\n ]\n)\n\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_type = [\n \"Basin\",\n \"Pump\",\n \"Pump\",\n \"LevelBoundary\",\n \"TabulatedRatingCurve\",\n \"Terminal\",\n \"DiscreteControl\",\n]\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(np.arange(len(xy)) + 1, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array([1, 3, 4, 2, 1, 5, 7, 7], dtype=np.int64)\nto_id = np.array([3, 4, 2, 1, 5, 6, 2, 3], dtype=np.int64)\n\nedge_type = 6 * [\"flow\"] + 2 * [\"control\"]\n\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\"from_node_id\": from_id, \"to_node_id\": to_id, \"edge_type\": edge_type},\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\n \"node_id\": [1, 1],\n \"area\": [1000.0, 1000.0],\n \"level\": [0.0, 1.0],\n }\n)\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [1],\n \"drainage\": [0.0],\n \"potential_evaporation\": [0.0],\n \"infiltration\": [0.0],\n \"precipitation\": [0.0],\n \"urban_runoff\": [0.0],\n }\n)\n\nstate = pd.DataFrame(data={\"node_id\": [1], \"level\": [20.0]})\n\nbasin = ribasim.Basin(profile=profile, static=static, state=state)\n\nSetup the discrete control:\n\ncondition = pd.DataFrame(\n data={\n \"node_id\": 3 * [7],\n \"listen_feature_id\": 3 * [1],\n \"variable\": 3 * [\"level\"],\n \"greater_than\": [5.0, 10.0, 15.0], # min, setpoint, max\n }\n)\n\nlogic = pd.DataFrame(\n data={\n \"node_id\": 5 * [7],\n \"truth_state\": [\"FFF\", \"U**\", \"T*F\", \"**D\", \"TTT\"],\n \"control_state\": [\"in\", \"in\", \"none\", \"out\", \"out\"],\n }\n)\n\ndiscrete_control = ribasim.DiscreteControl(condition=condition, logic=logic)\n\nThe above control logic can be summarized as follows: - If the level gets above the maximum, activate the control state “out” until the setpoint is reached; - If the level gets below the minimum, active the control state “in” until the setpoint is reached; - Otherwise activate the control state “none”.\nSetup the pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": 3 * [2] + 3 * [3],\n \"control_state\": 2 * [\"none\", \"in\", \"out\"],\n \"flow_rate\": [0.0, 2e-3, 0.0, 0.0, 0.0, 2e-3],\n }\n )\n)\n\nThe pump data defines the following:\n\n\n\nControl state\nPump #2 flow rate (m/s)\nPump #3 flow rate (m/s)\n\n\n\n\n“none”\n0.0\n0.0\n\n\n“in”\n2e-3\n0.0\n\n\n“out”\n0.0\n2e-3\n\n\n\nSetup the level boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(data={\"node_id\": [4], \"level\": [10.0]})\n)\n\nSetup the rating curve:\n\nrating_curve = ribasim.TabulatedRatingCurve(\n static=pd.DataFrame(\n data={\"node_id\": 2 * [5], \"level\": [2.0, 15.0], \"discharge\": [0.0, 1e-3]}\n )\n)\n\nSetup the terminal:\n\nterminal = ribasim.Terminal(static=pd.DataFrame(data={\"node_id\": [6]}))\n\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"level_setpoint_with_minmax\",\n node=node,\n edge=edge,\n basin=basin,\n pump=pump,\n level_boundary=level_boundary,\n tabulated_rating_curve=rating_curve,\n terminal=terminal,\n discrete_control=discrete_control,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2021-01-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nListen edges are plotted with a dashed line since they are not present in the “Edge / static” schema but only in the “Control / condition” schema.\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"level_setpoint_with_minmax\")\n\nNow run the model with level_setpoint_with_minmax/level_setpoint_with_minmax.toml. After running the model, read back the output:\n\nfrom matplotlib.dates import date2num\n\ndf_basin = pd.read_feather(datadir / \"level_setpoint_with_minmax/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\n\nax = df_basin_wide[\"level\"].plot()\n\ngreater_than = model.discrete_control.condition.greater_than\n\nax.hlines(\n greater_than,\n df_basin.time[0],\n df_basin.time.max(),\n lw=1,\n ls=\"--\",\n color=\"k\",\n)\n\ndf_control = pd.read_feather(\n datadir / \"level_setpoint_with_minmax/output/control.arrow\"\n)\n\ny_min, y_max = ax.get_ybound()\nax.fill_between(df_control.time[:2], 2 * [y_min], 2 * [y_max], alpha=0.2, color=\"C0\")\nax.fill_between(df_control.time[2:4], 2 * [y_min], 2 * [y_max], alpha=0.2, color=\"C0\")\n\nax.set_xticks(\n date2num(df_control.time).tolist(),\n df_control.control_state.tolist(),\n rotation=50,\n)\n\nax.set_yticks(greater_than, [\"min\", \"setpoint\", \"max\"])\nax.set_ylabel(\"level\")\nplt.show()\n\n\n\n\nThe highlighted regions show where a pump is active.\nLet’s print an overview of what happened with control:\n\nmodel.print_discrete_control_record(\n datadir / \"level_setpoint_with_minmax/output/control.arrow\"\n)\n\n0. At 2020-01-01 00:00:00 the control node with ID 7 reached truth state TTT:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level > 10.0\n For node ID 1 (Basin): level > 15.0\n\n This yielded control state \"out\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.002\n\n1. At 2020-02-09 01:17:29.324000 the control node with ID 7 reached truth state TFF:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level < 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"none\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.0\n\n2. At 2020-07-05 13:24:51.165000 the control node with ID 7 reached truth state FFF:\n For node ID 1 (Basin): level < 5.0\n For node ID 1 (Basin): level < 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"in\":\n For node ID 2 (Pump): flow_rate = 0.002\n For node ID 3 (Pump): flow_rate = 0.0\n\n3. At 2020-08-11 11:49:59.015000 the control node with ID 7 reached truth state TTF:\n For node ID 1 (Basin): level > 5.0\n For node ID 1 (Basin): level > 10.0\n For node ID 1 (Basin): level < 15.0\n\n This yielded control state \"none\":\n For node ID 2 (Pump): flow_rate = 0.0\n For node ID 3 (Pump): flow_rate = 0.0\n\n\n\nNote that crossing direction specific truth states (containing “U”, “D”) are not present in this overview even though they are part of the control logic. This is because in the control logic for this model these truth states are only used to sustain control states, while the overview only shows changes in control states.\n\n\n4 Model with PID control\nSet up the nodes:\n\nxy = np.array(\n [\n (0.0, 0.0), # 1: FlowBoundary\n (1.0, 0.0), # 2: Basin\n (2.0, 0.5), # 3: Pump\n (3.0, 0.0), # 4: LevelBoundary\n (1.5, 1.0), # 5: PidControl\n (2.0, -0.5), # 6: outlet\n (1.5, -1.0), # 7: PidControl\n ]\n)\n\nnode_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n\nnode_type = [\n \"FlowBoundary\",\n \"Basin\",\n \"Pump\",\n \"LevelBoundary\",\n \"PidControl\",\n \"Outlet\",\n \"PidControl\",\n]\n\n# Make sure the feature id starts at 1: explicitly give an index.\nnode = ribasim.Node(\n static=gpd.GeoDataFrame(\n data={\"type\": node_type},\n index=pd.Index(np.arange(len(xy)) + 1, name=\"fid\"),\n geometry=node_xy,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the edges:\n\nfrom_id = np.array([1, 2, 3, 4, 6, 5, 7], dtype=np.int64)\nto_id = np.array([2, 3, 4, 6, 2, 3, 6], dtype=np.int64)\n\nlines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\nedge = ribasim.Edge(\n static=gpd.GeoDataFrame(\n data={\n \"from_node_id\": from_id,\n \"to_node_id\": to_id,\n \"edge_type\": 5 * [\"flow\"] + 2 * [\"control\"],\n },\n geometry=lines,\n crs=\"EPSG:28992\",\n )\n)\n\nSetup the basins:\n\nprofile = pd.DataFrame(\n data={\"node_id\": [2, 2], \"level\": [0.0, 1.0], \"area\": [1000.0, 1000.0]}\n)\n\nstatic = pd.DataFrame(\n data={\n \"node_id\": [2],\n \"drainage\": [0.0],\n \"potential_evaporation\": [0.0],\n \"infiltration\": [0.0],\n \"precipitation\": [0.0],\n \"urban_runoff\": [0.0],\n }\n)\n\nstate = pd.DataFrame(\n data={\n \"node_id\": [2],\n \"level\": [6.0],\n }\n)\n\nbasin = ribasim.Basin(profile=profile, static=static, state=state)\n\nSetup the pump:\n\npump = ribasim.Pump(\n static=pd.DataFrame(\n data={\n \"node_id\": [3],\n \"flow_rate\": [0.0], # Will be overwritten by PID controller\n }\n )\n)\n\nSetup the outlet:\n\noutlet = ribasim.Outlet(\n static=pd.DataFrame(\n data={\n \"node_id\": [6],\n \"flow_rate\": [0.0], # Will be overwritten by PID controller\n }\n )\n)\n\nSetup flow boundary:\n\nflow_boundary = ribasim.FlowBoundary(\n static=pd.DataFrame(data={\"node_id\": [1], \"flow_rate\": [1e-3]})\n)\n\nSetup flow boundary:\n\nlevel_boundary = ribasim.LevelBoundary(\n static=pd.DataFrame(\n data={\n \"node_id\": [4],\n \"level\": [1.0], # Not relevant\n }\n )\n)\n\nSetup PID control:\n\npid_control = ribasim.PidControl(\n time=pd.DataFrame(\n data={\n \"node_id\": 4 * [5, 7],\n \"time\": [\n \"2020-01-01 00:00:00\",\n \"2020-01-01 00:00:00\",\n \"2020-05-01 00:00:00\",\n \"2020-05-01 00:00:00\",\n \"2020-07-01 00:00:00\",\n \"2020-07-01 00:00:00\",\n \"2020-12-01 00:00:00\",\n \"2020-12-01 00:00:00\",\n ],\n \"listen_node_id\": 4 * [2, 2],\n \"target\": [5.0, 5.0, 5.0, 5.0, 7.5, 7.5, 7.5, 7.5],\n \"proportional\": 4 * [-1e-3, 1e-3],\n \"integral\": 4 * [-1e-7, 1e-7],\n \"derivative\": 4 * [0.0, 0.0],\n }\n )\n)\n\nNote that the coefficients for the pump and the outlet are equal in magnitude but opposite in sign. This way the pump and the outlet equally work towards the same goal, while having opposite effects on the controlled basin due to their connectivity to this basin.\nSetup a model:\n\nmodel = ribasim.Model(\n modelname=\"pid_control\",\n node=node,\n edge=edge,\n basin=basin,\n flow_boundary=flow_boundary,\n level_boundary=level_boundary,\n pump=pump,\n outlet=outlet,\n pid_control=pid_control,\n starttime=\"2020-01-01 00:00:00\",\n endtime=\"2020-12-01 00:00:00\",\n)\n\nLet’s take a look at the model:\n\nmodel.plot()\n\n<Axes: >\n\n\n\n\n\nWrite the model to a TOML and GeoPackage:\n\ndatadir = Path(\"data\")\nmodel.write(datadir / \"pid_control\")\n\nNow run the model with ribasim pid_control/pid_control.toml. After running the model, read back the output:\n\nfrom matplotlib.dates import date2num\n\ndf_basin = pd.read_feather(datadir / \"pid_control/output/basin.arrow\")\ndf_basin_wide = df_basin.pivot_table(\n index=\"time\", columns=\"node_id\", values=[\"storage\", \"level\"]\n)\nax = df_basin_wide[\"level\"].plot()\nax.set_ylabel(\"level [m]\")\n\n# Plot target level\ntarget_levels = model.pid_control.time.target.to_numpy()[::2]\ntimes = date2num(model.pid_control.time.time)[::2]\nax.plot(times, target_levels, color=\"k\", ls=\":\", label=\"target level\");"
},
{
"objectID": "python/reference/Pump.html",