diff --git a/core/src/create.jl b/core/src/create.jl index 3c00709a3..7cbad5aff 100644 --- a/core/src/create.jl +++ b/core/src/create.jl @@ -491,9 +491,9 @@ function Basin(db::DB, config::Config, chunk_size::Int)::Basin area, level, storage = create_storage_tables(db, config) - # both static and forcing are optional, but we need fallback defaults + # both static and time are optional, but we need fallback defaults static = load_structvector(db, config, BasinStaticV1) - time = load_structvector(db, config, BasinForcingV1) + time = load_structvector(db, config, BasinTimeV1) set_static_value!(table, node_id, static) set_current_value!(table, node_id, time, config.starttime) diff --git a/core/src/solve.jl b/core/src/solve.jl index 676f7f9eb..5dfb64427 100644 --- a/core/src/solve.jl +++ b/core/src/solve.jl @@ -98,7 +98,7 @@ struct Basin{T, C} <: AbstractParameterNode level::Vector{Vector{Float64}} storage::Vector{Vector{Float64}} # data source for parameter updates - time::StructVector{BasinForcingV1, C, Int} + time::StructVector{BasinTimeV1, C, Int} function Basin( node_id, @@ -111,7 +111,7 @@ struct Basin{T, C} <: AbstractParameterNode area, level, storage, - time::StructVector{BasinForcingV1, C, Int}, + time::StructVector{BasinTimeV1, C, Int}, ) where {T, C} errors = valid_profiles(node_id, level, area) if isempty(errors) diff --git a/core/src/validation.jl b/core/src/validation.jl index 5b63569fb..c24e4a9be 100644 --- a/core/src/validation.jl +++ b/core/src/validation.jl @@ -5,7 +5,7 @@ @schema "ribasim.discretecontrol.condition" DiscreteControlCondition @schema "ribasim.discretecontrol.logic" DiscreteControlLogic @schema "ribasim.basin.static" BasinStatic -@schema "ribasim.basin.forcing" BasinForcing +@schema "ribasim.basin.time" BasinTime @schema "ribasim.basin.profile" BasinProfile @schema "ribasim.basin.state" BasinState @schema "ribasim.terminal.static" TerminalStatic @@ -36,9 +36,9 @@ From a SchemaVersion("ribasim.flowboundary.static", 1) return (:FlowBoundary, :s """ function nodetype(sv::SchemaVersion{T, N})::Tuple{Symbol, Symbol} where {T, N} n, k = split(string(T), ".")[2:3] - # Names derived from a schema are in underscores (basinforcing), - # so we parse the related record Ribasim.BasinForcingV1 - # to derive BasinForcing from it. + # Names derived from a schema are in underscores (basintime), + # so we parse the related record Ribasim.BasinTimeV1 + # to derive BasinTime from it. record = Legolas.record_type(sv) node = last(split(string(Symbol(record)), ".")) return Symbol(node[begin:length(n)]), Symbol(k) @@ -165,7 +165,7 @@ end urban_runoff::Float64 end -@version BasinForcingV1 begin +@version BasinTimeV1 begin node_id::Int time::DateTime drainage::Float64 @@ -314,7 +314,7 @@ function variable_nt(s::Any) NamedTuple{names}((getfield(s, x) for x in names)) end -function is_consistent(node, edge, state, static, profile, forcing) +function is_consistent(node, edge, state, static, profile, time) # Check that node ids exist # TODO Do we need to check the reverse as well? All ids in use? @@ -324,7 +324,7 @@ function is_consistent(node, edge, state, static, profile, forcing) @assert state.node_id ⊆ ids "State id not in node ids" @assert static.node_id ⊆ ids "Static id not in node ids" @assert profile.node_id ⊆ ids "Profile id not in node ids" - @assert forcing.node_id ⊆ ids "Forcing id not in node ids" + @assert time.node_id ⊆ ids "Time id not in node ids" # Check edges for uniqueness @assert allunique(edge, [:from_node_id, :to_node_id]) "Duplicate edge found" @@ -351,7 +351,7 @@ sort_by_function(table::StructVector{TabulatedRatingCurveStaticV1}) = sort_by_id sort_by_function(table::StructVector{BasinProfileV1}) = sort_by_id_level const TimeSchemas = Union{ - BasinForcingV1, + BasinTimeV1, FlowBoundaryTimeV1, LevelBoundaryTimeV1, PidControlTimeV1, diff --git a/core/test/docs.toml b/core/test/docs.toml index 90e01e399..67fc33019 100644 --- a/core/test/docs.toml +++ b/core/test/docs.toml @@ -21,7 +21,7 @@ compression_level = 6 # optional, default 6 # For large tables this can benefit from better compressed file sizes. # This is optional, tables are retrieved from the GeoPackage if not specified in the TOML. [basin] -forcing = "forcing.arrow" +time = "basin/time.arrow" [solver] algorithm = "QNDF" # optional, default "QNDF" diff --git a/core/test/io.jl b/core/test/io.jl index f66e29e17..a6f96d84b 100644 --- a/core/test/io.jl +++ b/core/test/io.jl @@ -78,7 +78,7 @@ end db = SQLite.DB(gpkg_path) # load a sorted table - table = Ribasim.load_structvector(db, config, Ribasim.BasinForcingV1) + table = Ribasim.load_structvector(db, config, Ribasim.BasinTimeV1) by = Ribasim.sort_by_function(table) @test by == Ribasim.sort_by_time_id # reverse it so it needs sorting diff --git a/core/test/testrun.toml b/core/test/testrun.toml index 9e76f97f8..1d58de637 100644 --- a/core/test/testrun.toml +++ b/core/test/testrun.toml @@ -11,7 +11,7 @@ output_dir = "../generated_testmodels/lhm" geopackage = "model.gpkg" [basin] -forcing = "forcing.arrow" +time = "basin/time.arrow" [solver] saveat = 86400 diff --git a/core/test/utils.jl b/core/test/utils.jl index 2a5360100..80f7a7284 100644 --- a/core/test/utils.jl +++ b/core/test/utils.jl @@ -36,7 +36,7 @@ end area, level, storage, - StructVector{Ribasim.BasinForcingV1}(undef, 0), + StructVector{Ribasim.BasinTimeV1}(undef, 0), ) @test basin.level[2][1] === 4.0 @@ -91,7 +91,7 @@ end [area], [level], [storage], - StructVector{Ribasim.BasinForcingV1}(undef, 0), + StructVector{Ribasim.BasinTimeV1}(undef, 0), ) logger = TestLogger() diff --git a/docs/core/usage.qmd b/docs/core/usage.qmd index 7a7278cfd..7f2f3b3f3 100644 --- a/docs/core/usage.qmd +++ b/docs/core/usage.qmd @@ -129,7 +129,7 @@ name it must have in the GeoPackage if it is stored there. - Basin: stores water - `Basin / static`: default forcing values, used if no dynamic data given in the forcing table - `Basin / profile`: geometries of the basins - - `Basin / forcing`: time series of the forcing values + - `Basin / time`: time series of the forcing values - `Basin / state`: used as initial condition of the basins - FractionalFlow: connect two of these from a Basin to get a fixed ratio bifurcation - `FractionalFlow / static`: fractions @@ -196,10 +196,10 @@ nodes in QGIS. For instance, you can draw a line connecting the two node coordin # Basin -The Basin table can be used to set the static value of variables. The forcing table has a +The Basin table can be used to set the static value of variables. The time table has a similar schema, with the time column added. A static value for a variable is only used if there is no dynamic forcing data for that variable. Specifically, if there is either no -forcing table, it is empty, or all timestamps of that variable are missing. +time table, it is empty, or all timestamps of that variable are missing. column | type | unit | restriction --------- | ------- | ------------ | ----------- @@ -214,7 +214,7 @@ Note that if variables are not set in the static table, default values are used possible. These are generally zero, e.g. no precipitation, no inflow. If it is not possible to have a reasonable and safe default, a value must be provided in the static table. -## Basin / forcing +## Basin / time This table is the transient form of the `Basin` table. The only difference is that a time column is added. diff --git a/docs/python/examples.ipynb b/docs/python/examples.ipynb index 3311a2994..eda723d95 100644 --- a/docs/python/examples.ipynb +++ b/docs/python/examples.ipynb @@ -550,7 +550,7 @@ "metadata": {}, "outputs": [], "source": [ - "model.basin.forcing = forcing\n", + "model.basin.time = forcing\n", "model.basin.state = state" ] }, diff --git a/docs/schema/BasinForcing.schema.json b/docs/schema/BasinTime.schema.json similarity index 86% rename from docs/schema/BasinForcing.schema.json rename to docs/schema/BasinTime.schema.json index 0f0ad5eb2..d505d525c 100644 --- a/docs/schema/BasinForcing.schema.json +++ b/docs/schema/BasinTime.schema.json @@ -45,8 +45,8 @@ "precipitation", "urban_runoff" ], - "$id": "https://deltares.github.io/Ribasim/schema/BasinForcing.schema.json", - "title": "BasinForcing", - "description": "A BasinForcing object based on Ribasim.BasinForcingV1", + "$id": "https://deltares.github.io/Ribasim/schema/BasinTime.schema.json", + "title": "BasinTime", + "description": "A BasinTime object based on Ribasim.BasinTimeV1", "type": "object" } diff --git a/docs/schema/Config.schema.json b/docs/schema/Config.schema.json index 2125ba6da..e35529df9 100644 --- a/docs/schema/Config.schema.json +++ b/docs/schema/Config.schema.json @@ -142,10 +142,10 @@ }, "basin": { "default": { - "forcing": null, "profile": null, "state": null, - "static": null + "static": null, + "time": null }, "$ref": "https://deltares.github.io/Ribasim/schema/basin.schema.json" }, diff --git a/docs/schema/basin.schema.json b/docs/schema/basin.schema.json index 6113db247..ab13b0573 100644 --- a/docs/schema/basin.schema.json +++ b/docs/schema/basin.schema.json @@ -13,7 +13,7 @@ ], "default": null }, - "static": { + "time": { "format": "default", "anyOf": [ { @@ -25,7 +25,7 @@ ], "default": null }, - "forcing": { + "static": { "format": "default", "anyOf": [ { diff --git a/docs/schema/root.schema.json b/docs/schema/root.schema.json index 469dcc354..078a2a82c 100644 --- a/docs/schema/root.schema.json +++ b/docs/schema/root.schema.json @@ -1,6 +1,9 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { + "BasinTime": { + "$ref": "BasinTime.schema.json" + }, "DiscreteControlLogic": { "$ref": "DiscreteControlLogic.schema.json" }, @@ -25,15 +28,12 @@ "DiscreteControlCondition": { "$ref": "DiscreteControlCondition.schema.json" }, - "BasinForcing": { - "$ref": "BasinForcing.schema.json" + "LinearResistanceStatic": { + "$ref": "LinearResistanceStatic.schema.json" }, "FractionalFlowStatic": { "$ref": "FractionalFlowStatic.schema.json" }, - "LinearResistanceStatic": { - "$ref": "LinearResistanceStatic.schema.json" - }, "PidControlStatic": { "$ref": "PidControlStatic.schema.json" }, diff --git a/python/ribasim/ribasim/config.py b/python/ribasim/ribasim/config.py index 096dcbc75..185f98877 100644 --- a/python/ribasim/ribasim/config.py +++ b/python/ribasim/ribasim/config.py @@ -87,8 +87,8 @@ class Terminal(BaseModel): class Basin(BaseModel): profile: Optional[str] = None + time: Optional[str] = None static: Optional[str] = None - forcing: Optional[str] = None state: Optional[str] = None @@ -171,7 +171,7 @@ class Config(BaseModel): ) basin: Basin = Field( default_factory=lambda: Basin.parse_obj( - {"forcing": None, "profile": None, "state": None, "static": None} + {"profile": None, "state": None, "static": None, "time": None} ) ) linear_resistance: LinearResistance = Field( diff --git a/python/ribasim/ribasim/models.py b/python/ribasim/ribasim/models.py index 4d24afe50..f1a157823 100644 --- a/python/ribasim/ribasim/models.py +++ b/python/ribasim/ribasim/models.py @@ -9,6 +9,17 @@ from pydantic import BaseModel, Field +class BasinTime(BaseModel): + remarks: str = Field("", description="a hack for pandera") + time: datetime + precipitation: float + infiltration: float + urban_runoff: float + node_id: int + potential_evaporation: float + drainage: float + + class DiscreteControlLogic(BaseModel): remarks: str = Field("", description="a hack for pandera") truth_state: str @@ -77,15 +88,12 @@ class DiscreteControlCondition(BaseModel): look_ahead: Optional[float] = None -class BasinForcing(BaseModel): +class LinearResistanceStatic(BaseModel): remarks: str = Field("", description="a hack for pandera") - time: datetime - precipitation: float - infiltration: float - urban_runoff: float + active: Optional[bool] = None node_id: int - potential_evaporation: float - drainage: float + resistance: float + control_state: Optional[str] = None class FractionalFlowStatic(BaseModel): @@ -95,14 +103,6 @@ class FractionalFlowStatic(BaseModel): control_state: Optional[str] = None -class LinearResistanceStatic(BaseModel): - remarks: str = Field("", description="a hack for pandera") - active: Optional[bool] = None - node_id: int - resistance: float - control_state: Optional[str] = None - - class PidControlStatic(BaseModel): integral: float remarks: str = Field("", description="a hack for pandera") @@ -215,6 +215,7 @@ class BasinStatic(BaseModel): class Root(BaseModel): + BasinTime: Optional[BasinTime] = None DiscreteControlLogic: Optional[DiscreteControlLogic] = None Edge: Optional[Edge] = None FlowBoundaryTime: Optional[FlowBoundaryTime] = None @@ -223,9 +224,8 @@ class Root(BaseModel): LevelBoundaryStatic: Optional[LevelBoundaryStatic] = None UserTime: Optional[UserTime] = None DiscreteControlCondition: Optional[DiscreteControlCondition] = None - BasinForcing: Optional[BasinForcing] = None - FractionalFlowStatic: Optional[FractionalFlowStatic] = None LinearResistanceStatic: Optional[LinearResistanceStatic] = None + FractionalFlowStatic: Optional[FractionalFlowStatic] = None PidControlStatic: Optional[PidControlStatic] = None PidControlTime: Optional[PidControlTime] = None ManningResistanceStatic: Optional[ManningResistanceStatic] = None diff --git a/python/ribasim/ribasim/node_types/basin.py b/python/ribasim/ribasim/node_types/basin.py index 18737e008..e87b7170b 100644 --- a/python/ribasim/ribasim/node_types/basin.py +++ b/python/ribasim/ribasim/node_types/basin.py @@ -4,10 +4,10 @@ from ribasim.input_base import TableModel from ribasim.schemas import ( # type: ignore - BasinForcingSchema, BasinProfileSchema, BasinStateSchema, BasinStaticSchema, + BasinTimeSchema, ) __all__ = ("Basin",) @@ -23,7 +23,7 @@ class Basin(TableModel): Table describing the geometry. static : pandas.DataFrame, optional Table describing the constant fluxes. - forcing : pandas.DataFrame, optional + time : pandas.DataFrame, optional Table describing the time-varying fluxes. state : pandas.DataFrame, optional Table describing the initial condition. @@ -31,16 +31,14 @@ class Basin(TableModel): profile: DataFrame[BasinProfileSchema] static: Optional[DataFrame[BasinStaticSchema]] = None - forcing: Optional[DataFrame[BasinForcingSchema]] = None + time: Optional[DataFrame[BasinTimeSchema]] = None state: Optional[DataFrame[BasinStateSchema]] = None def sort(self): self.profile.sort_values(["node_id", "level"], ignore_index=True, inplace=True) if self.static is not None: self.static.sort_values("node_id", ignore_index=True, inplace=True) - if self.forcing is not None: - self.forcing.sort_values( - ["time", "node_id"], ignore_index=True, inplace=True - ) + if self.time is not None: + self.time.sort_values(["time", "node_id"], ignore_index=True, inplace=True) if self.state is not None: self.state.sort_values("node_id", ignore_index=True, inplace=True) diff --git a/python/ribasim/tests/test_io.py b/python/ribasim/tests/test_io.py index 059ebf37b..5118bc9db 100644 --- a/python/ribasim/tests/test_io.py +++ b/python/ribasim/tests/test_io.py @@ -33,7 +33,7 @@ def test_basic(basic, tmp_path): assert_array_equal(index_a, index_b) assert_equal(model_orig.node.static, model_loaded.node.static) assert_equal(model_orig.edge.static, model_loaded.edge.static) - assert model_loaded.basin.forcing is None + assert model_loaded.basin.time is None def test_basic_transient(basic_transient, tmp_path): @@ -47,10 +47,10 @@ def test_basic_transient(basic_transient, tmp_path): assert_equal(model_orig.node.static, model_loaded.node.static) assert_equal(model_orig.edge.static, model_loaded.edge.static) - forcing = model_loaded.basin.forcing - assert model_orig.basin.forcing.time[0] == forcing.time[0] - assert_equal(model_orig.basin.forcing, forcing) - assert forcing.shape == (1468, 8) + time = model_loaded.basin.time + assert model_orig.basin.time.time[0] == time.time[0] + assert_equal(model_orig.basin.time, time) + assert time.shape == (1468, 8) def test_pydantic(): diff --git a/python/ribasim_testmodels/ribasim_testmodels/basic.py b/python/ribasim_testmodels/ribasim_testmodels/basic.py index 9da4892c2..fbbc9967f 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/basic.py +++ b/python/ribasim_testmodels/ribasim_testmodels/basic.py @@ -255,7 +255,7 @@ def basic_transient_model() -> ribasim.Model: } ) - model.basin.forcing = forcing + model.basin.time = forcing model.basin.state = state model.logging = None diff --git a/qgis/core/nodes.py b/qgis/core/nodes.py index fcb6dfdfc..26a52701e 100644 --- a/qgis/core/nodes.py +++ b/qgis/core/nodes.py @@ -305,8 +305,8 @@ class BasinStatic(Input): ] -class BasinForcing(Input): - input_type = "Basin / forcing" +class BasinTime(Input): + input_type = "Basin / time" geometry_type = "No Geometry" attributes = [ QgsField("time", QVariant.DateTime),