diff --git a/core/src/Ribasim.jl b/core/src/Ribasim.jl index 1c02e67b5..a8bba63f0 100644 --- a/core/src/Ribasim.jl +++ b/core/src/Ribasim.jl @@ -65,6 +65,7 @@ using PreallocationTools: LazyBufferCache # basin profiles and TabulatedRatingCurve. See also the node # references in the docs. using DataInterpolations: + ConstantInterpolation, LinearInterpolation, LinearInterpolationIntInv, invert_integral, diff --git a/core/src/callback.jl b/core/src/callback.jl index 467e71060..d332d7a00 100644 --- a/core/src/callback.jl +++ b/core/src/callback.jl @@ -671,12 +671,28 @@ function apply_parameter_update!(parameter_update)::Nothing end function update_subgrid_level!(integrator)::Nothing - (; p) = integrator + (; p, t) = integrator du = get_du(integrator) basin_level = p.basin.current_properties.current_level[parent(du)] subgrid = integrator.p.subgrid - for (i, (index, interp)) in enumerate(zip(subgrid.basin_index, subgrid.interpolations)) - subgrid.level[i] = interp(basin_level[index]) + + # First update the all the subgrids with static h(h) relations + for (level_index, basin_index, hh_itp) in zip( + subgrid.level_index_static, + subgrid.basin_index_static, + subgrid.interpolations_static, + ) + subgrid.level[level_index] = hh_itp(basin_level[basin_index]) + end + # Then update the subgrids with dynamic h(h) relations + for (level_index, basin_index, lookup) in zip( + subgrid.level_index_time, + subgrid.basin_index_time, + subgrid.current_interpolation_index, + ) + itp_index = lookup(t) + hh_itp = subgrid.interpolations_time[itp_index] + subgrid.level[level_index] = hh_itp(basin_level[basin_index]) end end diff --git a/core/src/parameter.jl b/core/src/parameter.jl index 072005bf7..95c8fd3f0 100644 --- a/core/src/parameter.jl +++ b/core/src/parameter.jl @@ -96,6 +96,7 @@ end Base.to_index(id::NodeID) = Int(id.value) +"LinearInterpolation from a Float64 to a Float64" const ScalarInterpolation = LinearInterpolation{ Vector{Float64}, Vector{Float64}, @@ -105,6 +106,10 @@ const ScalarInterpolation = LinearInterpolation{ (1,), } +"ConstantInterpolation from a Float64 to an Int, used to look up indices over time" +const IndexLookup = + ConstantInterpolation{Vector{Int64}, Vector{Float64}, Vector{Float64}, Int64, (1,)} + set_zero!(v) = v .= zero(eltype(v)) const Cache = LazyBufferCache{Returns{Int}, typeof(set_zero!)} @@ -867,10 +872,30 @@ end "Subgrid linearly interpolates basin levels." @kwdef struct Subgrid - subgrid_id::Vector{Int32} - basin_index::Vector{Int32} - interpolations::Vector{ScalarInterpolation} + # current level of each subgrid (static and dynamic) ordered by subgrid_id level::Vector{Float64} + + # Static part + # Static subgrid ids + subgrid_id_static::Vector{Int32} + # index into the basin.current_level vector for each static subgrid_id + basin_index_static::Vector{Int} + # index into the subgrid.level vector for each static subgrid_id + level_index_static::Vector{Int} + # per subgrid one relation + interpolations_static::Vector{ScalarInterpolation} + + # Dynamic part + # Dynamic subgrid ids + subgrid_id_time::Vector{Int32} + # index into the basin.current_level vector for each dynamic subgrid_id + basin_index_time::Vector{Int} + # index into the subgrid.level vector for each dynamic subgrid_id + level_index_time::Vector{Int} + # per subgrid n relations, n being the number of timesteps for that subgrid + interpolations_time::Vector{ScalarInterpolation} + # per subgrid 1 lookup from t to an index in interpolations_time + current_interpolation_index::Vector{IndexLookup} end """ diff --git a/core/src/read.jl b/core/src/read.jl index 255043371..475634ea7 100644 --- a/core/src/read.jl +++ b/core/src/read.jl @@ -187,11 +187,22 @@ function parse_static_and_time( return out, !errors end +""" +Retrieve and 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. + +The `is_complete` argument allows disabling the check that all Node IDs of type `node_type` +are either in the `static` or `time` table. +This is not required for Subgrid since not all Basins need to have subgrids. +""" function static_and_time_node_ids( db::DB, static::StructVector, time::StructVector, - node_type::NodeType.T, + node_type::NodeType.T; + is_complete::Bool = true, )::Tuple{Set{NodeID}, Set{NodeID}, Vector{NodeID}, Bool} node_ids = get_node_ids(db, node_type) ids = Int32.(node_ids) @@ -205,7 +216,7 @@ function static_and_time_node_ids( errors = true @error "$node_type cannot be in both static and time tables, found these node IDs in both: $doubles." end - if !issetequal(node_ids, union(static_node_ids, time_node_ids)) + if is_complete && !issetequal(node_ids, union(static_node_ids, time_node_ids)) errors = true @error "$node_type node IDs don't match." end @@ -1227,46 +1238,152 @@ function FlowDemand(db::DB, config::Config)::FlowDemand ) end +function push_lookup!( + current_interpolation_index::Vector{IndexLookup}, + lookup_index::Vector{Int}, + lookup_time::Vector{Float64}, +) + index_lookup = ConstantInterpolation( + lookup_index, + lookup_time; + extrapolate = true, + cache_parameters = true, + ) + push!(current_interpolation_index, index_lookup) +end + function Subgrid(db::DB, config::Config, basin::Basin)::Subgrid - node_to_basin = Dict(node_id => index for (index, node_id) in enumerate(basin.node_id)) - tables = load_structvector(db, config, BasinSubgridV1) - node_table = get_node_ids(db, NodeType.Basin) + time = load_structvector(db, config, BasinSubgridTimeV1) + static = load_structvector(db, config, BasinSubgridV1) - subgrid_ids = Int32[] - basin_index = Int32[] - interpolations = ScalarInterpolation[] - has_error = false - for group in IterTools.groupby(row -> row.subgrid_id, tables) + # Since not all Basins need to have subgrids, don't enforce completeness. + _, _, _, valid = + static_and_time_node_ids(db, static, time, NodeType.Basin; is_complete = false) + if !valid + error("Problems encountered when parsing Subgrid static and time node IDs.") + end + + node_to_basin = Dict{Int32, Int}( + Int32(node_id) => index for (index, node_id) in enumerate(basin.node_id) + ) + subgrid_id_static = Int32[] + basin_index_static = Int[] + interpolations_static = ScalarInterpolation[] + + # In the static table, each subgrid ID has 1 h(h) relation. We process one relation + # at a time and push the results to the respective vectors. + for group in IterTools.groupby(row -> row.subgrid_id, static) subgrid_id = first(getproperty.(group, :subgrid_id)) - node_id = NodeID(NodeType.Basin, first(getproperty.(group, :node_id)), node_table) + node_id = first(getproperty.(group, :node_id)) basin_level = getproperty.(group, :basin_level) subgrid_level = getproperty.(group, :subgrid_level) is_valid = valid_subgrid(subgrid_id, node_id, node_to_basin, basin_level, subgrid_level) + !is_valid && error("Invalid Basin / subgrid table.") + + # Ensure it doesn't extrapolate before the first value. + pushfirst!(subgrid_level, first(subgrid_level)) + pushfirst!(basin_level, nextfloat(-Inf)) + hh_itp = LinearInterpolation( + subgrid_level, + basin_level; + extrapolate = true, + cache_parameters = true, + ) + push!(subgrid_id_static, subgrid_id) + push!(basin_index_static, node_to_basin[node_id]) + push!(interpolations_static, hh_itp) + end - if is_valid - # Ensure it doesn't extrapolate before the first value. - pushfirst!(subgrid_level, first(subgrid_level)) - pushfirst!(basin_level, nextfloat(-Inf)) - new_interp = LinearInterpolation( - subgrid_level, - basin_level; - extrapolate = true, - cache_parameters = true, - ) - push!(subgrid_ids, subgrid_id) - push!(basin_index, node_to_basin[node_id]) - push!(interpolations, new_interp) - else - has_error = true + subgrid_id_time = Int32[] + basin_index_time = Int[] + interpolations_time = ScalarInterpolation[] + current_interpolation_index = IndexLookup[] + + # Push the first subgrid_id and basin_index + if length(time) > 0 + push!(subgrid_id_time, first(time.subgrid_id)) + push!(basin_index_time, node_to_basin[first(time.node_id)]) + end + + # Initialize index_lookup contents + lookup_time = Float64[] + lookup_index = Int[] + + interpolation_index = 0 + # In the time table, each subgrid ID can have a different number of relations over time. + # We group over the combination of subgrid ID and time such that this group has 1 h(h) relation. + # We process one relation at a time and push the results to the respective vectors. + # Some vectors are pushed only when the subgrid_id has changed. This can be done in + # sequence since it is first sorted by subgrid_id and then by time. + for group in IterTools.groupby(row -> (row.subgrid_id, row.time), time) + interpolation_index += 1 + subgrid_id = first(getproperty.(group, :subgrid_id)) + time_group = seconds_since(first(getproperty.(group, :time)), config.starttime) + node_id = first(getproperty.(group, :node_id)) + basin_level = getproperty.(group, :basin_level) + subgrid_level = getproperty.(group, :subgrid_level) + + is_valid = + valid_subgrid(subgrid_id, node_id, node_to_basin, basin_level, subgrid_level) + !is_valid && error("Invalid Basin / subgrid_time table.") + + # Ensure it doesn't extrapolate before the first value. + pushfirst!(subgrid_level, first(subgrid_level)) + pushfirst!(basin_level, nextfloat(-Inf)) + hh_itp = LinearInterpolation( + subgrid_level, + basin_level; + extrapolate = true, + cache_parameters = true, + ) + # These should only be pushed when the subgrid_id has changed + if subgrid_id_time[end] != subgrid_id + # Push the completed index_lookup of the previous subgrid_id + push_lookup!(current_interpolation_index, lookup_index, lookup_time) + # Push the new subgrid_id and basin_index + push!(subgrid_id_time, subgrid_id) + push!(basin_index_time, node_to_basin[node_id]) + # Start new index_lookup contents + lookup_time = Float64[] + lookup_index = Int[] end + push!(lookup_index, interpolation_index) + push!(lookup_time, time_group) + push!(interpolations_time, hh_itp) + end + + # Push completed IndexLookup of the last group + if interpolation_index > 0 + push_lookup!(current_interpolation_index, lookup_index, lookup_time) end - has_error && error("Invalid Basin / subgrid table.") - level = fill(NaN, length(subgrid_ids)) + level = fill(NaN, length(subgrid_id_static) + length(subgrid_id_time)) - return Subgrid(; subgrid_id = subgrid_ids, basin_index, interpolations, level) + # Find the level indices + level_index_static = zeros(Int, length(subgrid_id_static)) + level_index_time = zeros(Int, length(subgrid_id_time)) + subgrid_ids = sort(vcat(subgrid_id_static, subgrid_id_time)) + for (i, subgrid_id) in enumerate(subgrid_id_static) + level_index_static[i] = findsorted(subgrid_ids, subgrid_id) + end + for (i, subgrid_id) in enumerate(subgrid_id_time) + level_index_time[i] = findsorted(subgrid_ids, subgrid_id) + end + + return Subgrid(; + level, + subgrid_id_static, + basin_index_static, + level_index_static, + interpolations_static, + subgrid_id_time, + basin_index_time, + level_index_time, + interpolations_time, + current_interpolation_index, + ) end function Allocation(db::DB, config::Config, graph::MetaGraph)::Allocation diff --git a/core/src/schema.jl b/core/src/schema.jl index e03d8e852..4e5d926bb 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,8 +59,11 @@ 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])" + elseif last_element == "subgridtime" + elements[end] = "subgrid_time" end if isnode(sv) n = elements[2] @@ -150,6 +154,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/core/src/validation.jl b/core/src/validation.jl index 81a372421..fb0777e37 100644 --- a/core/src/validation.jl +++ b/core/src/validation.jl @@ -321,8 +321,8 @@ Validate the entries for a single subgrid element. """ function valid_subgrid( subgrid_id::Int32, - node_id::NodeID, - node_to_basin::Dict{NodeID, Int}, + node_id::Int32, + node_to_basin::Dict{Int32, Int}, basin_level::Vector{Float64}, subgrid_level::Vector{Float64}, )::Bool diff --git a/core/src/write.jl b/core/src/write.jl index 6edc3013b..91e2e2775 100644 --- a/core/src/write.jl +++ b/core/src/write.jl @@ -376,11 +376,14 @@ function subgrid_level_table( (; t, saveval) = saved.subgrid_level subgrid = integrator.p.subgrid - nelem = length(subgrid.subgrid_id) + nelem = length(subgrid.level) ntsteps = length(t) time = repeat(datetime_since.(t, config.starttime); inner = nelem) - subgrid_id = repeat(subgrid.subgrid_id; outer = ntsteps) + subgrid_id = repeat( + sort(vcat(subgrid.subgrid_id_static, subgrid.subgrid_id_time)); + outer = ntsteps, + ) subgrid_level = FlatVector(saveval) return (; time, subgrid_id, subgrid_level) end @@ -412,9 +415,9 @@ function write_arrow( mkpath(dirname(path)) try Arrow.write(path, table; compress, metadata) - catch + catch e @error "Failed to write results, file may be locked." path - error("Failed to write results.") + rethrow(e) end return nothing end diff --git a/core/test/run_models_test.jl b/core/test/run_models_test.jl index 696676119..2dc929c73 100644 --- a/core/test/run_models_test.jl +++ b/core/test/run_models_test.jl @@ -583,3 +583,30 @@ end @test all(isapprox.(Δinf[1:2:end], 25.0; atol = 1e-10)) @test all(Δinf[2:2:end] .== 0.0) end + +@testitem "two_basin" begin + using DataFrames: DataFrame, nrow + using Dates: DateTime + import BasicModelInterface as BMI + + toml_path = normpath(@__DIR__, "../../generated_testmodels/two_basin/ribasim.toml") + model = Ribasim.run(toml_path) + df = DataFrame(Ribasim.subgrid_level_table(model)) + + ntime = 367 + @test nrow(df) == ntime * 2 + @test df.subgrid_id == repeat(1:2; outer = ntime) + @test extrema(df.time) == (DateTime(2020), DateTime(2021)) + @test allunique(df.time[1:2:(end - 1)]) + @test all(df.subgrid_level[1:2] .== 0.01) + + # After a month the h(h) of subgrid_id 2 increases by a meter + i_change = searchsortedfirst(df.time, DateTime(2020, 2)) + @test df.subgrid_level[i_change + 1] - df.subgrid_level[i_change - 1] ≈ 1.0f0 + + # Besides the 1 meter shift the h(h) relations are 1:1 + basin_level = copy(BMI.get_value_ptr(model, "basin.level")) + basin_level[2] += 1 + @test basin_level ≈ df.subgrid_level[(end - 1):end] + @test basin_level ≈ model.integrator.p.subgrid.level +end diff --git a/core/test/validation_test.jl b/core/test/validation_test.jl index bf596539c..d3373eb63 100644 --- a/core/test/validation_test.jl +++ b/core/test/validation_test.jl @@ -285,30 +285,24 @@ end using Ribasim: valid_subgrid, NodeID using Logging - node_to_basin = Dict(NodeID(:Basin, 9, 1) => 1) + node_to_basin = Dict(Int32(9) => 1) logger = TestLogger() with_logger(logger) do - @test !valid_subgrid( - Int32(1), - NodeID(:Basin, 10, 1), - node_to_basin, - [-1.0, 0.0], - [-1.0, 0.0], - ) + @test !valid_subgrid(Int32(1), Int32(10), node_to_basin, [-1.0, 0.0], [-1.0, 0.0]) end @test length(logger.logs) == 1 @test logger.logs[1].level == Error @test logger.logs[1].message == "The node_id of the Basin / subgrid does not exist." - @test logger.logs[1].kwargs[:node_id] == NodeID(:Basin, 10, 1) + @test logger.logs[1].kwargs[:node_id] == Int32(10) @test logger.logs[1].kwargs[:subgrid_id] == 1 logger = TestLogger() with_logger(logger) do @test !valid_subgrid( Int32(1), - NodeID(:Basin, 9, 1), + Int32(9), node_to_basin, [-1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], diff --git a/docs/reference/node/basin.qmd b/docs/reference/node/basin.qmd index ee87fb99a..a7fea057b 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 `subgrid_id`, and per `subgrid_id` it must be sorted by `time`. +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 6a0555235..30d4f17d6 100644 --- a/python/ribasim/ribasim/config.py +++ b/python/ribasim/ribasim/config.py @@ -23,6 +23,7 @@ BasinStateSchema, BasinStaticSchema, BasinSubgridSchema, + BasinSubgridTimeSchema, BasinTimeSchema, ContinuousControlFunctionSchema, ContinuousControlVariableSchema, @@ -404,6 +405,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 ac0292ccb..e910b22a0 100644 --- a/python/ribasim/ribasim/schemas.py +++ b/python/ribasim/ribasim/schemas.py @@ -121,6 +121,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..0aecdc47e 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/two_basin.py +++ b/python/ribasim_testmodels/ribasim_testmodels/two_basin.py @@ -1,6 +1,6 @@ from typing import Any -from ribasim.config import Node +from ribasim.config import Node, Results from ribasim.input_base import TableModel from ribasim.model import Model from ribasim.nodes import basin, flow_boundary, tabulated_rating_curve @@ -18,7 +18,12 @@ def two_basin_model() -> Model: infiltrates in the left basin, and exfiltrates in the right basin. The right basin fills up and discharges over the rating curve. """ - model = Model(starttime="2020-01-01", endtime="2021-01-01", crs="EPSG:28992") + model = Model( + starttime="2020-01-01", + endtime="2021-01-01", + crs="EPSG:28992", + results=Results(subgrid=True), + ) model.flow_boundary.add( Node(1, Point(0, 0)), [flow_boundary.Static(flow_rate=[1e-2])] @@ -44,10 +49,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), ]