diff --git a/core/src/allocation_optim.jl b/core/src/allocation_optim.jl index 7ecb3b885..6e2404862 100644 --- a/core/src/allocation_optim.jl +++ b/core/src/allocation_optim.jl @@ -706,11 +706,9 @@ function save_allocation_flows!( priority::Int32, optimization_type::OptimizationType.T, )::Nothing - (; flow, problem, subnetwork_id) = allocation_model + (; flow, subnetwork_id, sources) = allocation_model (; allocation, graph) = p (; record_flow) = allocation - F_basin_in = problem[:F_basin_in] - F_basin_out = problem[:F_basin_out] edges_allocation = keys(flow.data) @@ -765,7 +763,7 @@ function save_allocation_flows!( for node_id in graph[].node_ids[subnetwork_id] if node_id.type == NodeType.Basin && has_external_demand(graph, node_id, :level_demand)[1] - flow_rate = JuMP.value(F_basin_out[node_id]) - JuMP.value(F_basin_in[node_id]) + flow_rate = sources[(node_id, node_id)].basin_flow_rate push!(record_flow.time, t) push!(record_flow.edge_id, 0) push!(record_flow.from_node_type, string(NodeType.Basin)) @@ -885,6 +883,15 @@ function optimize_per_source!( )::Nothing (; problem, sources, subnetwork_id, flow) = allocation_model (; priorities) = allocation + F_basin_in = problem[:F_basin_in] + F_basin_out = problem[:F_basin_out] + + # Start the cumulative basin flow rates at 0 + for source in values(sources) + if source.type == AllocationSourceType.basin + source.basin_flow_rate = 0.0 + end + end priority = priorities[priority_idx] @@ -931,6 +938,15 @@ function optimize_per_source!( end end + # Add to the basin cumulative flow rate + for (edge, source) in sources + if source.type == AllocationSourceType.basin + node_id = edge[1] + source.basin_flow_rate += + JuMP.value(F_basin_out[node_id]) - JuMP.value(F_basin_in[node_id]) + end + end + # Adjust allocated flow to basins increase_allocateds!(p.basin, problem) end diff --git a/core/src/parameter.jl b/core/src/parameter.jl index 62e3f76e8..03b909799 100644 --- a/core/src/parameter.jl +++ b/core/src/parameter.jl @@ -136,12 +136,15 @@ edge: The outflow edge of the source type: The type of source (edge, basin, main_to_sub, user_return, buffer) capacity: The initial capacity of the source as determined by the physical layer capacity_reduced: The capacity adjusted by passed optimizations +basin_flow_rate: The total outflow rate of a basin when optimized over all sources for one priority. + Ignored when the source is not a basin. """ @kwdef mutable struct AllocationSource const edge::Tuple{NodeID, NodeID} const type::AllocationSourceType.T capacity::Float64 = 0.0 capacity_reduced::Float64 = 0.0 + basin_flow_rate::Float64 = 0.0 end function Base.show(io::IO, source::AllocationSource) diff --git a/docs/tutorial/natural-flow.ipynb b/docs/tutorial/natural-flow.ipynb index 3807769bf..3bb622810 100644 --- a/docs/tutorial/natural-flow.ipynb +++ b/docs/tutorial/natural-flow.ipynb @@ -48,7 +48,7 @@ "source": [ "# Crystal River Basin\n", "We will examine a straightforward example of the Crystal river basin, which includes a main river and a single tributary flowing into the sea (see @fig-crystal-basin).\n", - "An average discharge of $44.45 \\text{ m}^3/\\text{s}$ is measured at the confluence.\n", + "Between 2014 and 2023 an average discharge of $44.45 \\text{ m}^3/\\text{s}$ is measured at the confluence.\n", "In this module, the basin is free of any activities, allowing the model to simulate the natural flow.\n", "The next step is to include a demand (irrigation) that taps from a canal out of the main river.\n", "\n", @@ -89,7 +89,9 @@ "metadata": {}, "source": [ "### Setup paths and model configuration\n", - "Reference the paths of the Ribasim installation and model directory and define the time period (2022-01-01 until 2023-01-01) for the model simulation.\n", + "Reference the paths of the Ribasim installation and model directory and define the time period.\n", + "The used simulation period is defined by the `starttime` and `endtime` of the model, not by the input timeseries.\n", + "For now we will look into the period from 2022-01-01 until 2023-01-01 for the model simulation.\n", "The coordinate reference system (CRS) is also required, and set to [EPSG:4326](https://epsg.io/4326), which means all coordinates are interpreted as latitude and longitude values.\n", "The CRS is important for correctly placing Ribasim models on the map, but since this is a fictional model, it is not important." ] @@ -135,8 +137,7 @@ "data[\"total\"] = data[\"minor\"] + data[\"main\"]\n", "display(data)\n", "\n", - "# Average and max inflow of the total inflow data timeseries\n", - "# From 2014 - 2023\n", + "# Average and max inflow of the total inflow data over 2022\n", "print(\"Average inflow [m3/s]:\", data[\"total\"].mean())\n", "print(\"Maximum inflow [m3/s]:\", data[\"total\"].max())\n", "\n", diff --git a/python/ribasim/ribasim/config.py b/python/ribasim/ribasim/config.py index 78a59e85e..a9acd3350 100644 --- a/python/ribasim/ribasim/config.py +++ b/python/ribasim/ribasim/config.py @@ -80,6 +80,7 @@ class Results(ChildModel): class Solver(ChildModel): """ Defines the numerical solver options. + For more details see . Attributes diff --git a/python/ribasim/ribasim/delwaq/__init__.py b/python/ribasim/ribasim/delwaq/__init__.py index 9aaec6658..d9de44392 100644 --- a/python/ribasim/ribasim/delwaq/__init__.py +++ b/python/ribasim/ribasim/delwaq/__init__.py @@ -7,7 +7,6 @@ "generate", "parse", "run_delwaq", - "plot", "add_tracer", "plot_fraction", "plot_spatial", diff --git a/python/ribasim/ribasim/delwaq/generate.py b/python/ribasim/ribasim/delwaq/generate.py index 44c6f4355..309f7a67b 100644 --- a/python/ribasim/ribasim/delwaq/generate.py +++ b/python/ribasim/ribasim/delwaq/generate.py @@ -57,8 +57,8 @@ def _quote(value): def _make_boundary(data, boundary_type): """ Create a Delwaq boundary definition with the given data and boundary type. - Pivot our data from long to wide format, and convert the time to a string. + Pivot our data from long to wide format, and convert the time to a string. Specifically, we go from a table: `node_id, substance, time, concentration` to @@ -300,7 +300,6 @@ def generate( output_path: Path = output_path, ) -> tuple[nx.DiGraph, set[str]]: """Generate a Delwaq model from a Ribasim model and results.""" - # Read in model and results model = ribasim.Model.read(toml_path) results_folder = toml_path.parent / model.results_dir diff --git a/python/ribasim/ribasim/geometry/edge.py b/python/ribasim/ribasim/geometry/edge.py index faa2e92fa..bc473458f 100644 --- a/python/ribasim/ribasim/geometry/edge.py +++ b/python/ribasim/ribasim/geometry/edge.py @@ -75,8 +75,10 @@ def add( edge_id: Optional[NonNegativeInt] = None, **kwargs, ): - """Add an edge between nodes. The type of the edge (flow or control) - is automatically inferred from the type of the `from_node`. + """ + Add an edge between nodes. + + The type of the edge (flow or control) is automatically inferred from the type of the `from_node`. Parameters ---------- @@ -181,7 +183,6 @@ def plot(self, **kwargs) -> Axes: **kwargs : Dict Supported: 'ax', 'color_flow', 'color_control' """ - assert self.df is not None kwargs = kwargs.copy() # Avoid side-effects ax = kwargs.get("ax", None) diff --git a/python/ribasim/ribasim/model.py b/python/ribasim/ribasim/model.py index d7274a67c..39d16cb39 100644 --- a/python/ribasim/ribasim/model.py +++ b/python/ribasim/ribasim/model.py @@ -300,7 +300,6 @@ def write(self, filepath: str | PathLike[str]) -> Path: filepath : str | PathLike[str] A file path with .toml extension. """ - if self.use_validation: self._validate_model() @@ -351,7 +350,6 @@ def _has_valid_neighbor_amount( nodes, ) -> bool: """Check if the neighbor amount of the two nodes connected by the given edge meet the minimum requirements.""" - is_valid = True # filter graph by edge type @@ -419,7 +417,6 @@ def _add_source_sink_node( Specify that their occurrence in from_node table or to_node table is 0. """ - # loop over nodes, add the one that is not the downstream (from) or upstream (to) of any other nodes for index, node in enumerate(nodes): if nodes.index[index] not in node_info[f"{direction}_node_id"].to_numpy(): @@ -463,7 +460,6 @@ def _reset_contextvar(self) -> "Model": def plot_control_listen(self, ax): """Plot the implicit listen edges of the model.""" - df_listen_edge = pd.DataFrame( data={ "control_node_id": pd.Series([], dtype="int32[pyarrow]"), @@ -598,7 +594,6 @@ def to_xugrid(self, add_flow: bool = False, add_allocation: bool = False): add_allocation : bool add allocation results (Optional, defaults to False) """ - if add_flow and add_allocation: raise ValueError("Cannot add both allocation and flow results.") diff --git a/python/ribasim/ribasim/utils.py b/python/ribasim/ribasim/utils.py index 42fd638b1..c8ed18e29 100644 --- a/python/ribasim/ribasim/utils.py +++ b/python/ribasim/ribasim/utils.py @@ -32,6 +32,7 @@ def __getattr__( def _node_lookup_numpy(node_id) -> Series[Int32]: """Create a lookup table from from node_id to the node dimension index. + Used when adding data onto the nodes of an xugrid dataset. """ return pd.Series( @@ -43,6 +44,7 @@ def _node_lookup_numpy(node_id) -> Series[Int32]: def _node_lookup(uds) -> Series[Int32]: """Create a lookup table from from node_id to the node dimension index. + Used when adding data onto the nodes of an xugrid dataset. """ return pd.Series( @@ -57,7 +59,6 @@ def _edge_lookup(uds) -> Series[Int32]: Used when adding data onto the edges of an xugrid dataset. """ - return pd.Series( index=uds["edge_id"], data=uds[uds.grid.edge_dimension], diff --git a/python/ribasim_api/tests/test_bmi.py b/python/ribasim_api/tests/test_bmi.py index f11383721..a8c25b186 100644 --- a/python/ribasim_api/tests/test_bmi.py +++ b/python/ribasim_api/tests/test_bmi.py @@ -175,8 +175,9 @@ def test_get_value_ptr_allocation(libribasim, user_demand, tmp_path): def test_err_unknown_var(libribasim, basic, tmp_path): """ - Unknown or invalid variable address should trigger Python Exception, - print the kernel error, and not crash the library + Unknown or invalid variable address. + + Should trigger Python Exception, print the kernel error, and not crash the library. """ basic.write(tmp_path / "ribasim.toml") config_file = str(tmp_path / "ribasim.toml") diff --git a/python/ribasim_testmodels/ribasim_testmodels/__init__.py b/python/ribasim_testmodels/ribasim_testmodels/__init__.py index bb065778b..f65276d28 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/__init__.py +++ b/python/ribasim_testmodels/ribasim_testmodels/__init__.py @@ -7,6 +7,7 @@ import ribasim_testmodels from ribasim_testmodels.allocation import ( allocation_example_model, + allocation_training_model, fair_distribution_model, flow_demand_model, level_demand_model, @@ -63,6 +64,7 @@ from ribasim_testmodels.two_basin import two_basin_model __all__ = [ + "allocation_training_model", "allocation_example_model", "backwater_model", "basic_arrow_model", diff --git a/python/ribasim_testmodels/ribasim_testmodels/allocation.py b/python/ribasim_testmodels/ribasim_testmodels/allocation.py index da31a3ecc..dce319344 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/allocation.py +++ b/python/ribasim_testmodels/ribasim_testmodels/allocation.py @@ -22,7 +22,6 @@ def user_demand_model() -> Model: """Create a UserDemand test model with static and dynamic UserDemand on the same basin.""" - model = Model( starttime="2020-01-01", endtime="2021-01-01", @@ -90,9 +89,9 @@ def user_demand_model() -> Model: def subnetwork_model() -> Model: """Create a UserDemand testmodel representing a subnetwork. + This model is merged into main_network_with_subnetworks_model. """ - model = Model( starttime="2020-01-01", endtime="2020-04-01", @@ -175,9 +174,9 @@ def subnetwork_model() -> Model: def looped_subnetwork_model() -> Model: """Create a UserDemand testmodel representing a subnetwork containing a loop in the topology. + This model is merged into main_network_with_subnetworks_model. """ - model = Model( starttime="2020-01-01", endtime="2021-01-01", @@ -304,7 +303,6 @@ def looped_subnetwork_model() -> Model: def minimal_subnetwork_model() -> Model: """Create a subnetwork that is minimal with non-trivial allocation.""" - model = Model( starttime="2020-01-01", endtime="2021-01-01", @@ -361,7 +359,6 @@ def minimal_subnetwork_model() -> Model: def allocation_example_model() -> Model: """Generate a model that is used as an example of allocation in the docs.""" - model = Model( starttime="2020-01-01", endtime="2020-01-20", @@ -424,7 +421,6 @@ def allocation_example_model() -> Model: def main_network_with_subnetworks_model() -> Model: """Generate a model which consists of a main network and multiple connected subnetworks.""" - model = Model( starttime="2020-01-01", endtime="2020-03-01", @@ -701,7 +697,6 @@ def main_network_with_subnetworks_model() -> Model: def subnetworks_with_sources_model() -> Model: """Generate a model with subnetworks which contain sources.""" - model = main_network_with_subnetworks_model() model.flow_boundary.add( @@ -721,7 +716,6 @@ def subnetworks_with_sources_model() -> Model: def level_demand_model() -> Model: """Small model with LevelDemand nodes.""" - model = Model( starttime="2020-01-01", endtime="2020-02-01", @@ -782,7 +776,6 @@ def level_demand_model() -> Model: def flow_demand_model() -> Model: """Small model with a FlowDemand.""" - model = Model( starttime="2020-01-01 00:00:00", endtime="2021-01-01 00:00:00", @@ -857,7 +850,6 @@ def flow_demand_model() -> Model: def linear_resistance_demand_model(): """Small model with a FlowDemand for a node with a max flow rate.""" - model = Model( starttime="2020-01-01 00:00:00", endtime="2021-01-01 00:00:00", @@ -892,11 +884,7 @@ def linear_resistance_demand_model(): def fair_distribution_model(): - """ - Small model with little restrictions within the graph to see the behavior of - allocation in that case. - """ - + """See the behavior of allocation with few restrictions within the graph.""" model = Model( starttime="2020-01-01 00:00:00", endtime="2020-01-07 00:00:00", @@ -991,3 +979,181 @@ def fair_distribution_model(): model.edge.add(model.user_demand[9], model.basin[5]) return model + + +def allocation_training_model(): + model = Model( + starttime="2022-01-01", + endtime="2023-01-01", + crs="EPSG:4326", + allocation=Allocation(use_allocation=True), + ) + + flow_boundary_times = pd.date_range(start="2022-01-01", end="2023-01-01", freq="MS") + + # Flow boundaries + main = model.flow_boundary.add( + Node(1, Point(0.0, 0.0), subnetwork_id=1, name="Main"), + [ + flow_boundary.Time( + time=flow_boundary_times, + flow_rate=[ + 47.3, + 156.7, + 77.6, + 47.8, + 26.6, + 23.1, + 18.6, + 15.6, + 23.1, + 35.6, + 24.4, + 20.0, + 29.4, + ], + ) + ], + ) + + minor = model.flow_boundary.add( + Node(2, Point(-3.0, 0.0), subnetwork_id=1, name="Minor"), + [ + flow_boundary.Time( + time=flow_boundary_times, + flow_rate=[ + 0.2, + 28.3, + 16.0, + 11.2, + 8.5, + 9.6, + 9.2, + 7.9, + 7.5, + 7.2, + 7.4, + 10.0, + 8.3, + ], + ) + ], + ) + + level = model.level_demand.add( + Node(11, Point(1, 1), subnetwork_id=1, name="test"), + [ + level_demand.Static( + min_level=[2], + max_level=5, + priority=1, + ) + ], + ) + + # Confluence + conf = model.basin.add( + Node(3, Point(-1.5, -1), subnetwork_id=1, name="confluence"), + [ + basin.Profile(area=[672000, 5600000], level=[0, 6]), + basin.State(level=[4]), + ], + ) + + tbr_conf = model.tabulated_rating_curve.add( + Node(4, Point(-1.5, -1.5), subnetwork_id=1, name="tbr_conf"), + [ + tabulated_rating_curve.Static( + level=[0.0, 2, 5], + flow_rate=[0.0, 50, 200], + ) + ], + ) + + # Irrigation + irr = model.user_demand.add( + Node(6, Point(-1.5, 0.5), subnetwork_id=1, name="irrigation"), + [ + user_demand.Time( + demand=[0.0, 0.0, 10, 12, 12, 0.0], + return_factor=0, + min_level=0, + priority=3, + time=[ + "2022-01-01", + "2022-03-31", + "2022-04-01", + "2022-07-01", + "2022-09-30", + "2022-10-01", + ], + ) + ], + ) + + # Reservoir + reservoir = model.basin.add( + Node(7, Point(-0.75, -0.5), subnetwork_id=1, name="reservoir"), + [ + basin.Profile(area=[20000000, 32300000], level=[0, 7]), + basin.State(level=[3.5]), + ], + ) + + rsv_weir = model.tabulated_rating_curve.add( + Node(8, Point(-1.125, -0.75), subnetwork_id=1, name="rsv_weir"), + [ + tabulated_rating_curve.Static( + level=[0.0, 1.5, 5], + flow_rate=[0.0, 45, 200], + ) + ], + ) + + # Public water use + city = model.user_demand.add( + Node(9, Point(-0.75, -1), subnetwork_id=1, name="city"), + [ + user_demand.Time( + # Total demand in m³/s + demand=[2.0, 2.3, 2.3, 2.4, 3, 3, 4, 3, 2.5, 2.2, 2.0, 2.0], + return_factor=0.4, + min_level=0, + priority=2, + time=pd.date_range(start="2022-01-01", periods=12, freq="MS"), + ) + ], + ) + + # Industry + industry = model.user_demand.add( + Node(10, Point(0, -1.5), subnetwork_id=1, name="industry"), + [ + user_demand.Time( + # Total demand in m³/s + demand=[4, 4, 4.5, 5, 5, 6, 7.5, 8, 5, 4, 3, 2.0], + return_factor=0.5, + min_level=0, + priority=1, + time=pd.date_range(start="2022-01-01", periods=12, freq="MS"), + ) + ], + ) + + sea = model.terminal.add(Node(5, Point(-1.5, -3.0), subnetwork_id=1, name="sea")) + + model.edge.add(main, reservoir, name="main") + model.edge.add(minor, conf, name="minor") + model.edge.add(reservoir, irr, name="irr supplied") + model.edge.add(irr, conf, name="irr drain") + model.edge.add(reservoir, city, name="city supplied") + model.edge.add(city, conf, name="city returnflow") + model.edge.add(reservoir, rsv_weir, name="rsv2weir") + model.edge.add(rsv_weir, conf, name="weir2conf") + model.edge.add(conf, tbr_conf, name="conf2tbr") + model.edge.add(level, reservoir) + model.edge.add(reservoir, industry, name="industry supplied") + model.edge.add(industry, conf, name="ind2conf") + model.edge.add(tbr_conf, sea, name="sea") + + return model diff --git a/python/ribasim_testmodels/ribasim_testmodels/backwater.py b/python/ribasim_testmodels/ribasim_testmodels/backwater.py index 81f6fa2a0..686a2ad16 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/backwater.py +++ b/python/ribasim_testmodels/ribasim_testmodels/backwater.py @@ -10,8 +10,7 @@ def backwater_model(): - """Backwater curve as an integration test for ManningResistance""" - + """Backwater curve as an integration test for ManningResistance.""" node_type = np.full(102, "ManningResistance") node_type[1::2] = "Basin" node_type[0] = "FlowBoundary" diff --git a/python/ribasim_testmodels/ribasim_testmodels/basic.py b/python/ribasim_testmodels/ribasim_testmodels/basic.py index 8618631de..f50dc714a 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/basic.py +++ b/python/ribasim_testmodels/ribasim_testmodels/basic.py @@ -213,8 +213,7 @@ def basic_arrow_model() -> ribasim.Model: def basic_transient_model() -> ribasim.Model: - """Update the basic model with transient forcing""" - + """Update the basic model with transient forcing.""" model = basic_model() time = pd.date_range(model.starttime, model.endtime) day_of_year = time.day_of_year.to_numpy() @@ -265,11 +264,11 @@ def basic_transient_model() -> ribasim.Model: def tabulated_rating_curve_model() -> ribasim.Model: """ Set up a model where the upstream Basin has two TabulatedRatingCurve attached. + They both flow to the same downstream Basin, but one has a static rating curve, and the other one a time-varying rating curve. Only the upstream Basin receives a (constant) precipitation. """ - # Setup a model: model = ribasim.Model( starttime="2020-01-01", diff --git a/python/ribasim_testmodels/ribasim_testmodels/bucket.py b/python/ribasim_testmodels/ribasim_testmodels/bucket.py index 8634e8b1d..804a83f88 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/bucket.py +++ b/python/ribasim_testmodels/ribasim_testmodels/bucket.py @@ -10,7 +10,6 @@ def bucket_model() -> ribasim.Model: """Bucket model with just a single basin at Deltares' headquarter.""" - model = ribasim.Model( starttime="2020-01-01", endtime="2021-01-01", @@ -38,7 +37,6 @@ def bucket_model() -> ribasim.Model: def leaky_bucket_model() -> ribasim.Model: """Bucket model with dynamic forcing with missings at Deltares' headquarter.""" - model = ribasim.Model( starttime="2020-01-01", endtime="2020-01-05", diff --git a/python/ribasim_testmodels/ribasim_testmodels/continuous_control.py b/python/ribasim_testmodels/ribasim_testmodels/continuous_control.py index 0da7fdca2..ba18c0d88 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/continuous_control.py +++ b/python/ribasim_testmodels/ribasim_testmodels/continuous_control.py @@ -14,7 +14,6 @@ def outlet_continuous_control_model() -> Model: """Set up a small model that distributes flow over 2 branches.""" - model = Model( starttime="2020-01-01", endtime="2021-01-01", diff --git a/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py b/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py index c325fbfb5..e28d62f33 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py +++ b/python/ribasim_testmodels/ribasim_testmodels/discrete_control.py @@ -17,10 +17,10 @@ def pump_discrete_control_model() -> Model: """ - Set up a basic model with a pump controlled based on basin levels. + Set up a basic model with a Pump controlled based on Basin levels. + The LinearResistance is deactivated when the levels are almost equal. """ - model = Model( starttime="2020-01-01", endtime="2021-01-01", @@ -119,8 +119,7 @@ def pump_discrete_control_model() -> Model: def flow_condition_model() -> Model: - """Set up a basic model that involves discrete control based on a flow condition""" - + """Set up a basic model that involves discrete control based on a flow condition.""" model = Model( starttime="2020-01-01", endtime="2021-01-01", @@ -184,7 +183,6 @@ def flow_condition_model() -> Model: def level_boundary_condition_model() -> Model: """Set up a small model with a condition on a level boundary.""" - model = Model( starttime="2020-01-01", endtime="2021-01-01", @@ -259,7 +257,6 @@ def tabulated_rating_curve_control_model() -> Model: node will effectively increase the crest level to prevent further drainage at some threshold level. """ - model = Model( starttime="2020-01-01", endtime="2021-01-01", @@ -321,11 +318,7 @@ def tabulated_rating_curve_control_model() -> Model: def compound_variable_condition_model() -> Model: - """ - Set up a minimal model containing a condition on a compound variable - for discrete control. - """ - + """Model with a condition on a compound variable for DiscreteControl.""" model = Model( starttime="2020-01-01", endtime="2021-01-01", @@ -379,11 +372,10 @@ def compound_variable_condition_model() -> Model: def level_range_model() -> Model: """ - Set up a minimal model in which the level of a basin is kept within an acceptable range - around a setpoint while being affected by time-varying forcing. + Keep the level of a Basin within a range around a setpoint, under the influence of time-varying forcing. + This is done by bringing the level back to the setpoint once the level goes beyond this range. """ - model = Model( starttime="2020-01-01", endtime="2021-01-01", @@ -472,11 +464,7 @@ def level_range_model() -> Model: def connector_node_flow_condition_model() -> Model: - """ - Set up a minimal model with discrete control with a condition on - the flow through a connector node. - """ - + """DiscreteControl with a condition on the flow through a connector node.""" model = Model( starttime="2020-01-01", endtime="2021-01-01", @@ -526,11 +514,7 @@ def connector_node_flow_condition_model() -> Model: def concentration_condition_model() -> Model: - """ - Set up a minimal model with discrete control based on a - concentration condition. - """ - + """DiscreteControl based on a concentration condition.""" model = Model( starttime="2020-01-01", endtime="2021-01-01", @@ -585,10 +569,9 @@ def concentration_condition_model() -> Model: def continuous_concentration_condition_model() -> Model: """ - Set up a minimal model with discrete control based on a - continuous (calculated) concentration condition. - In this case, we setup a salt concentration and mimic - the Dutch coast. + DiscreteControl based on a continuous (calculated) concentration condition. + + In this case, we setup a salt concentration and mimic the Dutch coast. dc / | @@ -598,7 +581,6 @@ def continuous_concentration_condition_model() -> Model: | term """ - model = Model( starttime="2020-01-01", endtime="2020-02-01", diff --git a/python/ribasim_testmodels/ribasim_testmodels/doc_example.py b/python/ribasim_testmodels/ribasim_testmodels/doc_example.py index b097d9cc2..1ed15519c 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/doc_example.py +++ b/python/ribasim_testmodels/ribasim_testmodels/doc_example.py @@ -11,8 +11,7 @@ def local_pidcontrolled_cascade_model(): - """Demonstrating model for the cascade polder project from our partner""" - + """Demonstrating model for the cascade polder project from our partner.""" model = Model(starttime="2020-01-01", endtime="2021-01-01", crs="EPSG:28992") # Set up basins diff --git a/python/ribasim_testmodels/ribasim_testmodels/equations.py b/python/ribasim_testmodels/ribasim_testmodels/equations.py index cb03ce7ee..87b3e284e 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/equations.py +++ b/python/ribasim_testmodels/ribasim_testmodels/equations.py @@ -19,7 +19,6 @@ def linear_resistance_model() -> Model: """Set up a minimal model which uses a linear_resistance node.""" - model = Model( starttime="2020-01-01", endtime="2021-01-01", @@ -50,7 +49,6 @@ def linear_resistance_model() -> Model: def rating_curve_model() -> Model: """Set up a minimal model which uses a tabulated_rating_curve node.""" - model = Model( starttime="2020-01-01", endtime="2021-01-01", @@ -89,7 +87,6 @@ def rating_curve_model() -> Model: def manning_resistance_model() -> Model: """Set up a minimal model which uses a manning_resistance node.""" - model = Model( starttime="2020-01-01", endtime="2021-01-01", @@ -123,7 +120,6 @@ def manning_resistance_model() -> Model: def misc_nodes_model() -> Model: """Set up a minimal model using flow_boundary and pump nodes.""" - model = Model( starttime="2020-01-01", endtime="2021-01-01", @@ -160,8 +156,7 @@ def misc_nodes_model() -> Model: def pid_control_equation_model() -> Model: - """Set up a model with pid control for an analytical solution test""" - + """Set up a model with pid control for an analytical solution test.""" model = Model( starttime="2020-01-01", endtime="2020-01-01 00:05:00", crs="EPSG:28992" ) diff --git a/python/ribasim_testmodels/ribasim_testmodels/invalid.py b/python/ribasim_testmodels/ribasim_testmodels/invalid.py index 46473b461..1e0657f6e 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/invalid.py +++ b/python/ribasim_testmodels/ribasim_testmodels/invalid.py @@ -19,7 +19,8 @@ def invalid_qh_model() -> Model: """ - Invalid TabulatedRatingCurve Q(h) table: + Invalid TabulatedRatingCurve Q(h) table. + - levels must be unique - flow_rate must start at 0 - flow_rate must not be decreasing @@ -113,7 +114,6 @@ def invalid_discrete_control_model() -> Model: def invalid_edge_types_model() -> Model: """Set up a minimal model with invalid edge types.""" - model = Model( starttime="2020-01-01", endtime="2020-12-01", @@ -147,7 +147,6 @@ def invalid_edge_types_model() -> Model: def invalid_unstable_model() -> Model: """Model with several extremely quickly emptying basins.""" - model = Model( starttime="2020-01-01", endtime="2021-01-01", @@ -174,7 +173,6 @@ def invalid_unstable_model() -> Model: def invalid_priorities_model() -> Model: """Model with allocation active but missing priority parameter(s).""" - model = Model( starttime="2020-01-01 00:00:00", endtime="2021-01-01 00:00:00", diff --git a/python/ribasim_testmodels/ribasim_testmodels/pid_control.py b/python/ribasim_testmodels/ribasim_testmodels/pid_control.py index 435d60e7a..fd395798f 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/pid_control.py +++ b/python/ribasim_testmodels/ribasim_testmodels/pid_control.py @@ -15,7 +15,6 @@ def pid_control_model() -> Model: """Set up a basic model with a PID controlled pump controlling a basin with abundant inflow.""" - model = Model( starttime="2020-01-01", endtime="2020-12-01", @@ -87,7 +86,6 @@ def pid_control_model() -> Model: def discrete_control_of_pid_control_model() -> Model: """Set up a basic model where a discrete control node sets the target level of a pid control node.""" - model = Model( starttime="2020-01-01", endtime="2020-12-01", diff --git a/python/ribasim_testmodels/ribasim_testmodels/time.py b/python/ribasim_testmodels/ribasim_testmodels/time.py index b77f46c2f..ef8b9063a 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/time.py +++ b/python/ribasim_testmodels/ribasim_testmodels/time.py @@ -7,8 +7,7 @@ def flow_boundary_time_model() -> Model: - """Set up a minimal model with time-varying flow boundary""" - + """Set up a minimal model with time-varying flow boundary.""" model = Model( starttime="2020-01-01", endtime="2021-01-01", diff --git a/python/ribasim_testmodels/ribasim_testmodels/trivial.py b/python/ribasim_testmodels/ribasim_testmodels/trivial.py index 42ef8650f..6380e27ea 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/trivial.py +++ b/python/ribasim_testmodels/ribasim_testmodels/trivial.py @@ -5,8 +5,7 @@ def trivial_model() -> Model: - """Trivial model with just a basin, tabulated rating curve and terminal node""" - + """Trivial model with just a basin, tabulated rating curve and terminal node.""" model = Model( starttime="2020-01-01", endtime="2021-01-01", diff --git a/python/ribasim_testmodels/ribasim_testmodels/two_basin.py b/python/ribasim_testmodels/ribasim_testmodels/two_basin.py index 9cedb312f..bbdab74e1 100644 --- a/python/ribasim_testmodels/ribasim_testmodels/two_basin.py +++ b/python/ribasim_testmodels/ribasim_testmodels/two_basin.py @@ -18,7 +18,6 @@ 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.flow_boundary.add( diff --git a/ribasim_qgis/core/nodes.py b/ribasim_qgis/core/nodes.py index c638a58e0..35d8f3970 100644 --- a/ribasim_qgis/core/nodes.py +++ b/ribasim_qgis/core/nodes.py @@ -1,5 +1,5 @@ """ -This module contains the classes to represent the Ribasim node layers. +Classes to represent the Ribasim node layers. The classes specify: @@ -93,8 +93,9 @@ def create( def new_layer(self, crs: QgsCoordinateReferenceSystem) -> QgsVectorLayer: """ - Separate creation of the instance with creating the layer, since the - layer might also come from an existing geopackage. + Separate creation of the instance with creating the layer. + + Needed since the layer might also come from an existing geopackage. """ layer = QgsVectorLayer(self.geometry_type(), self.input_type(), "memory") provider = layer.dataProvider() diff --git a/ribasim_qgis/core/topology.py b/ribasim_qgis/core/topology.py index 709aa6441..c03321b7a 100644 --- a/ribasim_qgis/core/topology.py +++ b/ribasim_qgis/core/topology.py @@ -97,17 +97,15 @@ def infer_edge_type(from_node_type: str) -> str: def set_edge_properties(node: QgsVectorLayer, edge: QgsVectorLayer) -> None: """ + Set edge properties based on the node and edge geometries. + Based on the location of the first and last vertex of every edge geometry, derive which nodes it connects. Sets values for: - - * from_node_type * from_node_id - * to_node_type * to_node_id * edge_type - """ node_xy, node_index, node_identifiers = collect_node_properties(node) edge_xy = collect_edge_coordinates(edge) diff --git a/ribasim_qgis/scripts/qgis_testrunner.py b/ribasim_qgis/scripts/qgis_testrunner.py index 726341abc..fb532d6f5 100644 --- a/ribasim_qgis/scripts/qgis_testrunner.py +++ b/ribasim_qgis/scripts/qgis_testrunner.py @@ -1,7 +1,6 @@ #!/usr/bin/env python """ -*************************************************************************** Launches a unit test inside QGIS and exit the application. Arguments: @@ -51,13 +50,19 @@ import sys import traceback +# Monkey patch QGIS Python console +from console.console_output import writeOut + +# Start as soon as the initializationCompleted signal is fired +from qgis.core import QgsApplication, QgsProject, QgsProjectBadLayerHandler +from qgis.PyQt.QtCore import QDir from qgis.utils import iface assert iface is not None def __get_test_function(test_module_name): - """Load the test module and return the test function""" + """Load the test module and return the test function.""" print(f"QGIS Test Runner - Trying to import {test_module_name}") try: test_module = importlib.import_module(test_module_name) @@ -82,20 +87,12 @@ def __get_test_function(test_module_name): return getattr(test_module, function_name, None) -# Start as soon as the initializationCompleted signal is fired -from qgis.core import QgsApplication, QgsProject, QgsProjectBadLayerHandler -from qgis.PyQt.QtCore import QDir - - class QgsProjectBadLayerDefaultHandler(QgsProjectBadLayerHandler): def handleBadLayers(self, layers, dom): + # Ignore bad layers pass -# Monkey patch QGIS Python console -from console.console_output import writeOut - - def _write(self, m): sys.stdout.write(m) diff --git a/ribasim_qgis/tests/ui/test_load_plugin.py b/ribasim_qgis/tests/ui/test_load_plugin.py index 325d7772e..dd138e441 100644 --- a/ribasim_qgis/tests/ui/test_load_plugin.py +++ b/ribasim_qgis/tests/ui/test_load_plugin.py @@ -15,7 +15,6 @@ def test_plugin_is_loaded(self): def test_plugin(self): """Triggers Ribasim button and checks that Dock is added.""" - # This checks the *actual* QGIS interface, not just a stub self.assertTrue(iface is not None, "QGIS interface not available") diff --git a/ribasim_qgis/widgets/dataset_widget.py b/ribasim_qgis/widgets/dataset_widget.py index f45f12132..079fad59f 100644 --- a/ribasim_qgis/widgets/dataset_widget.py +++ b/ribasim_qgis/widgets/dataset_widget.py @@ -1,8 +1,7 @@ """ -This widgets displays the available input layers in the GeoPackage. +A widget that displays the available input layers in the GeoPackage. -This widget also allows enabling or disabling individual elements for a -computation. +It also allows enabling or disabling individual elements for a computation. """ from __future__ import annotations @@ -73,14 +72,7 @@ def add_node_layer(self, element: Input) -> QTreeWidgetItem: return item def remove_geopackage_layers(self) -> None: - """ - Remove layers from: - - * The dataset tree widget - * The QGIS layer panel - * The geopackage - """ - + """Remove layers from the dataset tree widget, QGIS layer panel and the GeoPackage.""" # Collect the selected items selection = self.selectedItems() @@ -169,7 +161,7 @@ def __init__(self, parent: QWidget): @property def path(self) -> Path: - """Returns currently active path to Ribasim model (.toml)""" + """Returns currently active path to Ribasim model (.toml).""" return Path(self.dataset_line_edit.text()) def connect_nodes(self) -> None: @@ -240,7 +232,7 @@ def add_relationship(from_layer, to_layer_id, name, fk="node_id") -> None: from_layer.setEditorWidgetSetup(field_index, setup) def load_geopackage(self) -> None: - """Load the layers of a GeoPackage into the Layers Panel""" + """Load the layers of a GeoPackage into the Layers Panel.""" self.dataset_tree.clear() geo_path = get_database_path_from_model_file(self.path) nodes = load_nodes_from_geopackage(geo_path) @@ -364,12 +356,7 @@ def _open_model(self, path: str) -> None: self.dataset_tree.sortByColumn(0, Qt.SortOrder.AscendingOrder) def remove_geopackage_layer(self) -> None: - """ - Remove layers from: - * The dataset tree widget - * The QGIS layer panel - * The geopackage - """ + """Remove layers from the dataset tree widget, QGIS layer panel and the GeoPackage.""" self.dataset_tree.remove_geopackage_layers() def suppress_popup_changed(self): diff --git a/ribasim_qgis/widgets/ribasim_widget.py b/ribasim_qgis/widgets/ribasim_widget.py index 8af4689c4..77beeded3 100644 --- a/ribasim_qgis/widgets/ribasim_widget.py +++ b/ribasim_qgis/widgets/ribasim_widget.py @@ -1,5 +1,5 @@ """ -This module forms the high level DockWidget. +High level RibasimWidget. It ensures the underlying widgets can talk to each other. It also manages the connection to the QGIS Layers Panel, and ensures there is a group for the @@ -70,7 +70,7 @@ def edge_layer(self) -> QgsVectorLayer | None: @property def crs(self) -> QgsCoordinateReferenceSystem: - """Returns coordinate reference system of current mapview""" + """Returns coordinate reference system of current mapview.""" map_canvas = self.iface.mapCanvas() assert map_canvas is not None map_settings = map_canvas.mapSettings() @@ -110,9 +110,9 @@ def create_groups(self, name: str) -> None: self.create_subgroup(name, "Ribasim Input") def add_to_group(self, maplayer: Any, destination: str, on_top: bool): - """ - Try to add to a group; it might have been deleted. In that case, we add - as many groups as required. + """Try to add to a group. + + It might have been deleted. In that case, we add as many groups as required. """ group = self.groups[destination] try: @@ -138,7 +138,7 @@ def add_layer( labels: QgsAbstractVectorLayerLabeling | None = None, ) -> QgsMapLayer | None: """ - Add a layer to the Layers Panel + Add a layer to the Layers Panel. Parameters ---------- diff --git a/ruff.toml b/ruff.toml index ab710a111..608f4db18 100644 --- a/ruff.toml +++ b/ruff.toml @@ -6,14 +6,7 @@ target-version = "py39" # See https://docs.astral.sh/ruff/rules/ select = ["C4", "D2", "D3", "D4", "E", "F", "I", "NPY", "PD", "UP"] ignore = [ - "D202", - "D205", - "D206", - "D400", - "D404", - "E402", "E501", - "E703", "PD002", "PD901", ]