Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dutch waterways model #446

Merged
merged 32 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e486a81
Inferring node type list
SouthEndMusic Jul 19, 2023
8c1d126
First model draft
SouthEndMusic Jul 19, 2023
605da90
Add pid control and fix listen edge plotting
SouthEndMusic Jul 19, 2023
9d5c4cb
Updated parameters
SouthEndMusic Jul 19, 2023
18ddbc6
Start of adding control
SouthEndMusic Jul 20, 2023
5ac98b2
Merge branch 'main' into Dutch_waterways_model
SouthEndMusic Jul 21, 2023
679dbd0
Add first DiscreteControl part
SouthEndMusic Jul 21, 2023
72b61ed
Model updates
SouthEndMusic Jul 21, 2023
7322fb6
Merge branch 'main' into Dutch_waterways_model
SouthEndMusic Jul 25, 2023
66af98c
Merge branch 'main' into Dutch_waterways_model
SouthEndMusic Aug 3, 2023
31188ab
Fix conftest
SouthEndMusic Aug 3, 2023
f6f51bb
Merge branch 'main' into Dutch_waterways_model
SouthEndMusic Aug 9, 2023
ca4a143
Remove expand_logic function
SouthEndMusic Aug 10, 2023
ab686b7
Merge branch 'main' into Dutch_waterways_model
SouthEndMusic Aug 15, 2023
d0509db
Merge branch 'main' into Dutch_waterways_model
SouthEndMusic Aug 15, 2023
250cafb
Update model
SouthEndMusic Aug 15, 2023
b39269b
Merge branch 'main' into Dutch_waterways_model
SouthEndMusic Aug 15, 2023
6acede6
Update test model
SouthEndMusic Aug 15, 2023
b3aab4d
Julia core fixes
SouthEndMusic Aug 15, 2023
f07ff54
Merge branch 'main' into Dutch_waterways_model
SouthEndMusic Aug 16, 2023
e472784
Provide initial levels
SouthEndMusic Aug 16, 2023
ade955d
Merge branch 'main' into Dutch_waterways_model
SouthEndMusic Aug 16, 2023
7899337
Fix test
SouthEndMusic Aug 16, 2023
7cde4e0
Merge branch 'main' into Dutch_waterways_model
SouthEndMusic Aug 17, 2023
727638e
Improved resistances
SouthEndMusic Aug 17, 2023
d31b9eb
Merge branch 'main' into Dutch_waterways_model
SouthEndMusic Aug 21, 2023
24f95d5
Merge branch 'main' into Dutch_waterways_model
SouthEndMusic Aug 21, 2023
02f1e69
Merge branch 'main' into Dutch_waterways_model
SouthEndMusic Aug 21, 2023
ed60e07
Use finiteduff jac
SouthEndMusic Aug 21, 2023
91b1bb9
Merge branch 'main' into Dutch_waterways_model
SouthEndMusic Aug 21, 2023
4fbd0fe
Merge branch 'main' into Dutch_waterways_model
SouthEndMusic Aug 23, 2023
56867fd
Comments adressed
SouthEndMusic Aug 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion core/src/create.jl
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ function TabulatedRatingCurve(db::DB, config::Config)::TabulatedRatingCurve
is_active = coalesce(first(group).active, true)
interpolation, is_valid = qh_interpolation(node_id, StructVector(group))
if !ismissing(control_state)
control_mapping[(node_id, control_state)] = (; tables = interpolation)
control_mapping[(node_id, control_state)] =
(; tables = interpolation, active = is_active)
end
end
push!(interpolations, interpolation)
Expand Down
2 changes: 1 addition & 1 deletion core/src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ function valid_discrete_control(p::Parameters, config::Config)::Bool

if !isempty(undefined_control_states)
undefined_list = collect(undefined_control_states)
node_type = typeof(node)
node_type = typeof(node).name.name
@error "These control states from DiscreteControl node #$id are not defined for controlled $node_type #$id_outneighbor: $undefined_list."
errors = true
end
Expand Down
2 changes: 1 addition & 1 deletion core/test/validation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ end
"DiscreteControl node #5 has 3 condition(s), which is inconsistent with these truth state(s): [\"FFFF\"]."
@test logger.logs[2].level == Error
@test logger.logs[2].message ==
"These control states from DiscreteControl node #5 are not defined for controlled Ribasim.Pump #2: [\"foo\"]."
"These control states from DiscreteControl node #5 are not defined for controlled Pump #2: [\"foo\"]."
@test logger.logs[3].level == Error
@test logger.logs[3].message ==
"Look ahead supplied for non-timeseries listen variable 'level' from listen node #1."
Expand Down
6 changes: 4 additions & 2 deletions docs/core/usage.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -461,14 +461,16 @@ DiscreteControl is applied in the Julia core as follows:

- During the simulation it is checked whether the truth of any of the conditions changes.
- When a condition changes, the corresponding discrrete_control node id is retrieved (node_id in the condition schema above).
- The truth value of all the conditions this discrete_control node lisens to are retrieved, in the order as they are specified in the condition schema. This is then converted into a string of "T" for true and "F" for false. This string we call the truth state.
- The truth value of all the conditions this discrete_control node lisens to are retrieved, in the order as they are specified in the condition schema. This is then converted into a string of "T" for true and "F" for false. This string we call the truth state.*
- The table below determines for the given discrete_control node ID and truth state what the corresponding control state is.
- For all the nodes this discrete_control node affects (as given by the "control" edges in [Edges / static](usage.qmd#edge)), their parameters are set to those parameters in `NodeType / static` corresponding to the determined control state.

*. There is also a second truth state created in which for the last condition that changed it is specified whether it was an upcrossing ("U") or downcrossing ("D") of the threshold (greater than) value. If a control state is specified for a truth state that is crossing-specific, this takes precedence over the control state for the truth state that contains only "T" and "F".

column | type | unit | restriction
-------------- | -------- | ---- | -----------
node_id | Int | - | sorted
truth_state | String | - | Consists of the characters "T" (true), "F" (false) and "*" (any)
truth_state | String | - | Consists of the characters "T" (true), "F" (false), "U" (upcrossing), "D" (downcrossing) and "*" (any)
control_state | String | - |


Expand Down
194 changes: 92 additions & 102 deletions docs/python/examples.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -44,108 +44,6 @@
"import ribasim"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"Set up the nodes:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"xy = 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",
")\n",
"node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n",
"\n",
"node_type = [\n",
" \"Basin\",\n",
" \"ManningResistance\",\n",
" \"Basin\",\n",
" \"TabulatedRatingCurve\",\n",
" \"FractionalFlow\",\n",
" \"Basin\",\n",
" \"Pump\",\n",
" \"FractionalFlow\",\n",
" \"Basin\",\n",
" \"LinearResistance\",\n",
" \"LevelBoundary\",\n",
" \"LinearResistance\",\n",
" \"FractionalFlow\",\n",
" \"Terminal\",\n",
" \"FlowBoundary\",\n",
" \"FlowBoundary\",\n",
" \"LevelBoundary\",\n",
"]\n",
"\n",
"# Make sure the feature id starts at 1: explicitly give an index.\n",
"node = 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",
")"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"Setup the edges:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from_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",
")\n",
"to_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",
")\n",
"lines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\n",
"edge = 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",
")"
]
},
{
"attachments": {},
"cell_type": "markdown",
Expand Down Expand Up @@ -385,6 +283,98 @@
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Set up the nodes:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"xy = 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",
")\n",
"node_xy = gpd.points_from_xy(x=xy[:, 0], y=xy[:, 1])\n",
"\n",
"node_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.\n",
"node = 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",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Setup the edges:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from_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",
")\n",
"to_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",
")\n",
"lines = ribasim.utils.geometry_from_connectivity(node, from_id, to_id)\n",
"edge = 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",
")"
]
},
{
"attachments": {},
"cell_type": "markdown",
Expand Down
32 changes: 32 additions & 0 deletions python/ribasim/ribasim/geometry/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import geopandas as gpd
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pandera as pa
from geopandas import GeoDataFrame
from pandera.typing import DataFrame, Series
Expand Down Expand Up @@ -43,6 +44,37 @@ def _layername(cls, field) -> str:
def hasfid(cls):
return True

@staticmethod
def get_node_ids_and_types(*nodes):
data_types = {"node_id": int, "node_type": str}
node_type = pd.DataFrame(
{col: pd.Series(dtype=dtype) for col, dtype in data_types.items()}
)

for node in nodes:
if not node:
continue

for table_type in ["static", "time", "condition"]:
if hasattr(node, table_type):
table = getattr(node, table_type)
if table is not None:
node_type_table = pd.DataFrame(
data={
"node_id": table.node_id,
"node_type": len(table) * [node.get_input_type()],
}
)
node_type = node_type._append(node_type_table)

node_type = node_type.drop_duplicates(subset="node_id")
node_type = node_type.sort_values("node_id")

node_id = node_type.node_id.tolist()
node_type = node_type.node_type.tolist()

return node_id, node_type

def write(self, directory: FilePath, modelname: str) -> None:
"""
Write the contents of the input to a GeoPackage.
Expand Down
58 changes: 35 additions & 23 deletions python/ribasim/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
basic_model,
basic_transient_model,
bucket_model,
dutch_waterways_model,
flow_boundary_time_model,
flow_condition_model,
invalid_discrete_control_model,
Expand Down Expand Up @@ -51,26 +52,37 @@ def backwater() -> ribasim.Model:
# write models to disk for Julia tests to use
if __name__ == "__main__":
datadir = Path("data")
trivial_model().write(datadir / "trivial")
bucket_model().write(datadir / "bucket")
basic_model().write(datadir / "basic")
basic_transient_model(basic_model()).write(datadir / "basic_transient")
tabulated_rating_curve_model().write(datadir / "tabulated_rating_curve")
tabulated_rating_curve_control_model().write(
datadir / "tabulated_rating_curve_control"
)
pump_discrete_control_model().write(datadir / "pump_discrete_control")
flow_condition_model().write(datadir / "flow_condition")
backwater_model().write(datadir / "backwater")
linear_resistance_model().write(datadir / "linear_resistance")
rating_curve_model().write(datadir / "rating_curve")
manning_resistance_model().write(datadir / "manning_resistance")
pid_control_model().write(datadir / "pid_control")
misc_nodes_model().write(datadir / "misc_nodes")
invalid_qh_model().write(datadir / "invalid_qh")
invalid_fractional_flow_model().write(datadir / "invalid_fractional_flow")
flow_boundary_time_model().write(datadir / "flow_boundary_time")
level_setpoint_with_minmax_model().write(datadir / "level_setpoint_with_minmax")
pid_control_equation_model().write(datadir / "pid_control_equation")
invalid_discrete_control_model().write(datadir / "invalid_discrete_control")
invalid_edge_types_model().write(datadir / "invalid_edge_types")

models = [
model_generator()
for model_generator in (
backwater_model,
basic_model,
bucket_model,
dutch_waterways_model,
flow_boundary_time_model,
flow_condition_model,
invalid_discrete_control_model,
invalid_edge_types_model,
invalid_fractional_flow_model,
invalid_qh_model,
level_setpoint_with_minmax_model,
linear_resistance_model,
manning_resistance_model,
misc_nodes_model,
pid_control_equation_model,
pid_control_model,
pump_discrete_control_model,
rating_curve_model,
tabulated_rating_curve_control_model,
tabulated_rating_curve_model,
trivial_model,
)
]

for model in models:
model.write(datadir / model.modelname)

if model.modelname == "basic":
model = basic_transient_model(model)
model.write(datadir / model.modelname)
12 changes: 0 additions & 12 deletions python/ribasim/tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,6 @@ def test_node_id_duplicate(basic):
model.validate_model_node_field_IDs()


def test_missing_node_id(basic):
model = basic

# Add entry in node but not in pump
model.node.static = model.node.static._append(
{"type": "Pump", "geometry": Point(0, 0)}, ignore_index=True
)

with pytest.raises(ValueError, match="Expected node IDs from.+"):
model.validate_model_node_field_IDs()


def test_node_ids_misassigned(basic):
model = basic

Expand Down
Loading