From 95c8f7c87d705e04f865c9b93ae9c04b186b64b9 Mon Sep 17 00:00:00 2001 From: Martijn Visser Date: Fri, 13 Dec 2024 21:25:40 +0100 Subject: [PATCH] Add `Basin / subgrid_time` table --- core/src/read.jl | 6 ++++ core/src/schema.jl | 13 +++++++ docs/reference/node/basin.qmd | 15 ++++++-- python/ribasim/ribasim/config.py | 5 +++ python/ribasim/ribasim/nodes/basin.py | 6 ++++ python/ribasim/ribasim/schemas.py | 19 ++++++++++ .../ribasim_testmodels/two_basin.py | 8 +++-- ribasim_qgis/core/nodes.py | 35 ++++++++++++++----- 8 files changed, 93 insertions(+), 14 deletions(-) diff --git a/core/src/read.jl b/core/src/read.jl index e99d207e0..6c9e2a34c 100644 --- a/core/src/read.jl +++ b/core/src/read.jl @@ -187,6 +187,12 @@ function parse_static_and_time( return out, !errors end +""" +Validate the split of node IDs between static and time tables. + +For node types that can have a part of the parameters defined statically and a part dynamically, +this checks if each ID is defined exactly once in either table. +""" function static_and_time_node_ids( db::DB, static::StructVector, diff --git a/core/src/schema.jl b/core/src/schema.jl index e03d8e852..58b32088e 100644 --- a/core/src/schema.jl +++ b/core/src/schema.jl @@ -10,6 +10,7 @@ @schema "ribasim.basin.profile" BasinProfile @schema "ribasim.basin.state" BasinState @schema "ribasim.basin.subgrid" BasinSubgrid +@schema "ribasim.basin.subgridtime" BasinSubgridTime @schema "ribasim.basin.concentration" BasinConcentration @schema "ribasim.basin.concentrationexternal" BasinConcentrationExternal @schema "ribasim.basin.concentrationstate" BasinConcentrationState @@ -58,9 +59,13 @@ function nodetype( type_string = string(T) elements = split(type_string, '.'; limit = 3) last_element = last(elements) + # Special case last elements that need an underscore if startswith(last_element, "concentration") && length(last_element) > 13 elements[end] = "concentration_$(last_element[14:end])" end + if last_element == "subgridtime" + elements[end] = "subgrid_time" + end if isnode(sv) n = elements[2] k = Symbol(elements[3]) @@ -150,6 +155,14 @@ end subgrid_level::Float64 end +@version BasinSubgridTimeV1 begin + subgrid_id::Int32 + node_id::Int32 + time::DateTime + basin_level::Float64 + subgrid_level::Float64 +end + @version LevelBoundaryStaticV1 begin node_id::Int32 active::Union{Missing, Bool} diff --git a/docs/reference/node/basin.qmd b/docs/reference/node/basin.qmd index ee87fb99a..62abafde9 100644 --- a/docs/reference/node/basin.qmd +++ b/docs/reference/node/basin.qmd @@ -228,7 +228,7 @@ ax.legend() for converting the initial state in terms of levels to an initial state in terms of storages used in the core. -#### Interactive basin example +#### Interactive Basin example The profile data is not detailed enough to create a full 3D picture of the basin. However, if we assume the profile data is for a stretch of canal of given length, the following plot shows a cross section of the basin. ```{python} @@ -391,6 +391,15 @@ Water levels beyond the last `basin_level` are linearly extrapolated. Note that the interpolation to subgrid water level is not constrained by any water balance within Ribasim. Generally, to create physically meaningful subgrid water levels, the subgrid table must be parametrized properly such that the spatially integrated water volume of the subgrid elements agrees with the total storage volume of the basin. +## Subgrid time + +This table is the transient form of the Subgrid table. +The only difference is that a time column is added. +The table must by sorted by time, and per time it must be sorted by `subgrid_id`. +With this the subgrid relations can be updated over time. +Note that a `node_id` can be either in this table or in the static one, but not both. +That means for each Basin all subgrid relations are either static or dynamic. + ## Concentration {#sec-basin-conc} This table defines the concentration of substances for the inflow boundaries of a Basin node. @@ -402,7 +411,7 @@ substance | String | | can correspond to known De drainage | Float64 | $\text{g}/\text{m}^3$ | (optional) precipitation | Float64 | $\text{g}/\text{m}^3$ | (optional) -## ConcentrationState {#sec-basin-conc-state} +## Concentration state {#sec-basin-conc-state} This table defines the concentration of substances in the Basin at the start of the simulation. column | type | unit | restriction @@ -411,7 +420,7 @@ node_id | Int32 | - | sorted substance | String | - | can correspond to known Delwaq substances concentration | Float64 | $\text{g}/\text{m}^3$ | -## ConcentrationExternal +## Concentration external This table is used for (external) concentrations, that can be used for Control lookups. column | type | unit | restriction diff --git a/python/ribasim/ribasim/config.py b/python/ribasim/ribasim/config.py index a9acd3350..49060b2b9 100644 --- a/python/ribasim/ribasim/config.py +++ b/python/ribasim/ribasim/config.py @@ -23,6 +23,7 @@ BasinStateSchema, BasinStaticSchema, BasinSubgridSchema, + BasinSubgridTimeSchema, BasinTimeSchema, ContinuousControlFunctionSchema, ContinuousControlVariableSchema, @@ -405,6 +406,10 @@ class Basin(MultiNodeModel): default_factory=TableModel[BasinSubgridSchema], json_schema_extra={"sort_keys": ["subgrid_id", "basin_level"]}, ) + subgrid_time: TableModel[BasinSubgridTimeSchema] = Field( + default_factory=TableModel[BasinSubgridTimeSchema], + json_schema_extra={"sort_keys": ["subgrid_id", "time", "basin_level"]}, + ) area: SpatialTableModel[BasinAreaSchema] = Field( default_factory=SpatialTableModel[BasinAreaSchema], json_schema_extra={"sort_keys": ["node_id"]}, diff --git a/python/ribasim/ribasim/nodes/basin.py b/python/ribasim/ribasim/nodes/basin.py index 1beb21336..9da7ee1a6 100644 --- a/python/ribasim/ribasim/nodes/basin.py +++ b/python/ribasim/ribasim/nodes/basin.py @@ -8,6 +8,7 @@ BasinStateSchema, BasinStaticSchema, BasinSubgridSchema, + BasinSubgridTimeSchema, BasinTimeSchema, ) @@ -18,6 +19,7 @@ "State", "Static", "Subgrid", + "SubgridTime", "Time", ] @@ -42,6 +44,10 @@ class Subgrid(TableModel[BasinSubgridSchema]): pass +class SubgridTime(TableModel[BasinSubgridTimeSchema]): + pass + + class Area(SpatialTableModel[BasinAreaSchema]): pass diff --git a/python/ribasim/ribasim/schemas.py b/python/ribasim/ribasim/schemas.py index 2bcd22222..b80a5d466 100644 --- a/python/ribasim/ribasim/schemas.py +++ b/python/ribasim/ribasim/schemas.py @@ -116,6 +116,25 @@ class BasinStaticSchema(_BaseSchema): ) +class BasinSubgridTimeSchema(_BaseSchema): + fid: Index[Int32] = pa.Field(default=1, check_name=True, coerce=True) + subgrid_id: Series[Annotated[pd.ArrowDtype, pyarrow.int32()]] = pa.Field( + nullable=False + ) + node_id: Series[Annotated[pd.ArrowDtype, pyarrow.int32()]] = pa.Field( + nullable=False, default=0 + ) + time: Series[Annotated[pd.ArrowDtype, pyarrow.timestamp("ms")]] = pa.Field( + nullable=False + ) + basin_level: Series[Annotated[pd.ArrowDtype, pyarrow.float64()]] = pa.Field( + nullable=False + ) + subgrid_level: Series[Annotated[pd.ArrowDtype, pyarrow.float64()]] = pa.Field( + nullable=False + ) + + class BasinSubgridSchema(_BaseSchema): fid: Index[Int32] = pa.Field(default=1, check_name=True, coerce=True) subgrid_id: Series[Annotated[pd.ArrowDtype, pyarrow.int32()]] = pa.Field( diff --git a/python/ribasim_testmodels/ribasim_testmodels/two_basin.py b/python/ribasim_testmodels/ribasim_testmodels/two_basin.py index bbdab74e1..acc52c4aa 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/two_basin.py +++ b/python/ribasim_testmodels/ribasim_testmodels/two_basin.py @@ -44,10 +44,12 @@ def two_basin_model() -> Model: Node(3, Point(750, 0)), [ *basin_shared, - basin.Subgrid( + # Raise the subgrid levels by a meter after a month + basin.SubgridTime( subgrid_id=2, - basin_level=[0.0, 1.0], - subgrid_level=[0.0, 1.0], + time=["2020-01-01", "2020-01-01", "2020-02-01", "2020-02-01"], + basin_level=[0.0, 1.0, 0.0, 1.0], + subgrid_level=[0.0, 1.0, 1.0, 2.0], meta_x=750.0, meta_y=0.0, ), diff --git a/ribasim_qgis/core/nodes.py b/ribasim_qgis/core/nodes.py index 35d8f3970..d2274b3ee 100644 --- a/ribasim_qgis/core/nodes.py +++ b/ribasim_qgis/core/nodes.py @@ -348,8 +348,8 @@ def geometry_type(cls) -> str: @classmethod def attributes(cls) -> list[QgsField]: return [ - QgsField("time", QVariant.DateTime), QgsField("node_id", QVariant.Int), + QgsField("time", QVariant.DateTime), QgsField("drainage", QVariant.Double), QgsField("potential_evaporation", QVariant.Double), QgsField("infiltration", QVariant.Double), @@ -369,8 +369,8 @@ def geometry_type(cls) -> str: @classmethod def attributes(cls) -> list[QgsField]: return [ - QgsField("time", QVariant.DateTime), QgsField("node_id", QVariant.Int), + QgsField("time", QVariant.DateTime), QgsField("substance", QVariant.String), QgsField("concentration", QVariant.Double), ] @@ -388,8 +388,8 @@ def geometry_type(cls) -> str: @classmethod def attributes(cls) -> list[QgsField]: return [ - QgsField("time", QVariant.DateTime), QgsField("node_id", QVariant.Int), + QgsField("time", QVariant.DateTime), QgsField("substance", QVariant.String), QgsField("concentration", QVariant.Double), ] @@ -407,15 +407,15 @@ def geometry_type(cls) -> str: @classmethod def attributes(cls) -> list[QgsField]: return [ - QgsField("time", QVariant.DateTime), QgsField("node_id", QVariant.Int), + QgsField("time", QVariant.DateTime), QgsField("substance", QVariant.String), QgsField("drainage", QVariant.Double), QgsField("precipitation", QVariant.Double), ] -class BasinSubgridLevel(Input): +class BasinSubgrid(Input): @classmethod def input_type(cls) -> str: return "Basin / subgrid" @@ -434,6 +434,26 @@ def attributes(cls) -> list[QgsField]: ] +class BasinSubgridTime(Input): + @classmethod + def input_type(cls) -> str: + return "Basin / subgrid_time" + + @classmethod + def geometry_type(cls) -> str: + return "No Geometry" + + @classmethod + def attributes(cls) -> list[QgsField]: + return [ + QgsField("subgrid_id", QVariant.Int), + QgsField("node_id", QVariant.Int), + QgsField("time", QVariant.DateTime), + QgsField("basin_level", QVariant.Double), + QgsField("subgrid_level", QVariant.Double), + ] + + class BasinArea(Input): @classmethod def input_type(cls) -> str: @@ -505,8 +525,8 @@ def geometry_type(cls) -> str: @classmethod def attributes(cls) -> list[QgsField]: return [ - QgsField("time", QVariant.DateTime), QgsField("node_id", QVariant.Int), + QgsField("time", QVariant.DateTime), QgsField("level", QVariant.Double), QgsField("flow_rate", QVariant.Double), ] @@ -583,7 +603,6 @@ def geometry_type(cls) -> str: @classmethod def attributes(cls) -> list[QgsField]: return [ - QgsField("time", QVariant.DateTime), QgsField("node_id", QVariant.Int), QgsField("time", QVariant.DateTime), QgsField("level", QVariant.Double), @@ -663,8 +682,8 @@ def geometry_type(cls) -> str: @classmethod def attributes(cls) -> list[QgsField]: return [ - QgsField("time", QVariant.DateTime), QgsField("node_id", QVariant.Int), + QgsField("time", QVariant.DateTime), QgsField("flow_rate", QVariant.Double), ]