Skip to content

Commit

Permalink
Merge branch 'main' into allocation_level_control_node
Browse files Browse the repository at this point in the history
  • Loading branch information
SouthEndMusic committed Feb 13, 2024
2 parents 724caa0 + 45ad647 commit 5e763b8
Show file tree
Hide file tree
Showing 7 changed files with 51 additions and 37 deletions.
4 changes: 2 additions & 2 deletions core/src/allocation_init.jl
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ Add the flow conservation constraints to the allocation problem.
The constraint indices are user node IDs.
Constraint:
sum(flows out of node node) <= flows into node + flow from storage and vertical fluxes
sum(flows out of node node) == flows into node + flow from storage and vertical fluxes
"""
function add_constraints_flow_conservation!(
problem::JuMP.Model,
Expand All @@ -575,7 +575,7 @@ function add_constraints_flow_conservation!(
sum([
F[(node_id, outneighbor_id)] for
outneighbor_id in outflow_ids_allocation(graph, node_id)
]) + get_basin_inflow(problem, node_id) <=
]) ==
sum([
F[(inneighbor_id, node_id)] for
inneighbor_id in inflow_ids_allocation(graph, node_id)
Expand Down
15 changes: 0 additions & 15 deletions core/src/allocation_optim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -171,21 +171,6 @@ function set_objective_priority!(
add_user_term!(ex, edge_id, objective_type, d, problem)
end

# Add flow cost
if objective_type == :linear_absolute
cost_per_flow = 0.5 / length(F)
for flow in F
JuMP.add_to_expression!(ex, cost_per_flow * flow)
end
elseif objective_type == :linear_relative
if demand_max > 0.0
cost_per_flow = 0.5 / (demand_max * length(F))
for flow in F
JuMP.add_to_expression!(ex, cost_per_flow * flow)
end
end
end

# Terms for basins
F_basin_in = problem[:F_basin_in]
for node_id in only(F_basin_in.axes)
Expand Down
51 changes: 37 additions & 14 deletions core/test/allocation_test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
@test Ribasim.get_user_demand(user, NodeID(:User, 11), 2) π
end

@testitem "Allocation objective types" begin
@testitem "Allocation objective: quadratic absolute" skip = true begin
using DataFrames: DataFrame
using SciMLBase: successful_retcode
using Ribasim: NodeID
Expand All @@ -74,6 +74,17 @@ end
F[(NodeID(:Basin, 4), NodeID(:User, 6))],
F[(NodeID(:Basin, 4), NodeID(:User, 6))],
) in keys(objective.terms) # F[4,6]^2 term
end

@testitem "Allocation objective: quadratic relative" begin
using DataFrames: DataFrame
using SciMLBase: successful_retcode
using Ribasim: NodeID
import JuMP

toml_path =
normpath(@__DIR__, "../../generated_testmodels/minimal_subnetwork/ribasim.toml")
@test ispath(toml_path)

config = Ribasim.Config(toml_path; allocation_objective_type = "quadratic_relative")
model = Ribasim.run(config)
Expand All @@ -91,6 +102,17 @@ end
F[(NodeID(:Basin, 4), NodeID(:User, 6))],
F[(NodeID(:Basin, 4), NodeID(:User, 6))],
) in keys(objective.terms) # F[4,6]^2 term
end

@testitem "Allocation objective: linear absolute" begin
using DataFrames: DataFrame
using SciMLBase: successful_retcode
using Ribasim: NodeID
import JuMP

toml_path =
normpath(@__DIR__, "../../generated_testmodels/minimal_subnetwork/ribasim.toml")
@test ispath(toml_path)

config = Ribasim.Config(toml_path; allocation_objective_type = "linear_absolute")
model = Ribasim.run(config)
Expand All @@ -104,10 +126,17 @@ end

@test objective.terms[F_abs_user[NodeID(:User, 5)]] == 1.0
@test objective.terms[F_abs_user[NodeID(:User, 6)]] == 1.0
@test objective.terms[F[(NodeID(:Basin, 4), NodeID(:User, 6))]] 0.125
@test objective.terms[F[(NodeID(:FlowBoundary, 1), NodeID(:Basin, 2))]] 0.125
@test objective.terms[F[(NodeID(:Basin, 4), NodeID(:User, 5))]] 0.125
@test objective.terms[F[(NodeID(:Basin, 2), NodeID(:Basin, 4))]] 0.125
end

@testitem "Allocation objective: linear relative" begin
using DataFrames: DataFrame
using SciMLBase: successful_retcode
using Ribasim: NodeID
import JuMP

toml_path =
normpath(@__DIR__, "../../generated_testmodels/minimal_subnetwork/ribasim.toml")
@test ispath(toml_path)

config = Ribasim.Config(toml_path; allocation_objective_type = "linear_relative")
model = Ribasim.run(config)
Expand All @@ -121,11 +150,6 @@ end

@test objective.terms[F_abs_user[NodeID(:User, 5)]] == 1.0
@test objective.terms[F_abs_user[NodeID(:User, 6)]] == 1.0
@test objective.terms[F[(NodeID(:Basin, 4), NodeID(:User, 6))]] 62.585499316005475
@test objective.terms[F[(NodeID(:FlowBoundary, 1), NodeID(:Basin, 2))]]
62.585499316005475
@test objective.terms[F[(NodeID(:Basin, 4), NodeID(:User, 5))]] 62.585499316005475
@test objective.terms[F[(NodeID(:Basin, 2), NodeID(:Basin, 4))]] 62.585499316005475
end

@testitem "Allocation with controlled fractional flow" begin
Expand Down Expand Up @@ -263,8 +287,7 @@ end
end

@test subnetwork_demands[(NodeID(:Basin, 2), NodeID(:Pump, 11))] [4.0, 4.0, 0.0]
@test subnetwork_demands[(NodeID(:Basin, 6), NodeID(:Pump, 24))]
[0.001333333333, 0.0, 0.0]
@test subnetwork_demands[(NodeID(:Basin, 6), NodeID(:Pump, 24))] [0.004, 0.0, 0.0]
@test subnetwork_demands[(NodeID(:Basin, 10), NodeID(:Pump, 38))]
[0.001, 0.002, 0.002]

Expand All @@ -288,9 +311,9 @@ end
Ribasim.update_allocation!((; p, t, u))

@test subnetwork_allocateds[NodeID(:Basin, 2), NodeID(:Pump, 11)]
[4.0, 0.49766666, 0.0]
[4.0, 0.49500000, 0.0]
@test subnetwork_allocateds[NodeID(:Basin, 6), NodeID(:Pump, 24)]
[0.00133333333, 0.0, 0.0]
[0.00399999999, 0.0, 0.0]
@test subnetwork_allocateds[NodeID(:Basin, 10), NodeID(:Pump, 38)] [0.001, 0.0, 0.0]

@test user.allocated[2] [4.0, 0.0, 0.0]
Expand Down
8 changes: 3 additions & 5 deletions docs/core/allocation.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,11 @@ $$
$$
- `linear_absolute` (default):
$$
E_{\text{user}} = \sum_{(i,j)\in E_S\;:\; i\in U_S} \left| F_{ij} - d_j^p(t)\right| + c \sum_{e \in E_S} F_e
E_{\text{user}} = \sum_{(i,j)\in E_S\;:\; i\in U_S} \left| F_{ij} - d_j^p(t)\right|
$$
- `linear_relative`:
$$
E_{\text{user}} = \sum_{(i,j)\in E_S\;:\; i\in U_S} \left|1 - \frac{F_{ij}}{d_j^p(t)}\right| + c \sum_{e \in E_S} F_e
E_{\text{user}} = \sum_{(i,j)\in E_S\;:\; i\in U_S} \left|1 - \frac{F_{ij}}{d_j^p(t)}\right|
$$

:::{.callout-note}
Expand All @@ -155,8 +155,6 @@ To avoid division by $0$ errors, if a `*_relative` objective is used and a deman

For `*_absolute` objectives the optimizer cares about the actual amount of water allocated to a user, for `*_relative` objectives it cares about the fraction of the demand allocated to the user. For `quadratic_*` objectives the optimizer cares about avoiding large shortages, for `linear_*` objectives it treats all deviations equally.

The second sum in the `linear_*` objectives adds a small cost to using flows. This incentivizes the solver to use as little flow as possible. The cost $c > 0$ is small enough such that it is always better to bring water to users than to not use flow at all. This can be achieved for `linear_*` objectives but not for `quadratic_*` objectives, and therefore this cost term is only added to the former. Therefore the `linear_*` objectives make the solver more conservative with flow than the `quadratic_*` objectives.

:::{.callout-note}
These options for objectives for allocation to users have not been tested thoroughly, and might change in the future.
:::
Expand Down Expand Up @@ -189,7 +187,7 @@ $$
## The optimization constraints
- Flow conservation: For the basins in the allocation graph we have that
$$
F^\text{basin in}_k + \sum_{j=1}^{n'} F_{kj} \le F^\text{basin out}_k + \sum_{i=1}^{n'} F_{ik}, \quad \forall k \in B_S .
F^\text{basin in}_k + \sum_{j=1}^{n'} F_{kj} = F^\text{basin out}_k + \sum_{i=1}^{n'} F_{ik}, \quad \forall k \in B_S .
$$ {#eq-flowconservationconstraint}
Note that we do not require equality here; in the allocation we do not mind that excess flow is 'forgotten' if it cannot contribute to the allocation to the users.
Expand Down
1 change: 0 additions & 1 deletion mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ strict_concatenate = True
disallow_subclassing_any = True
disallow_untyped_decorators = True
disallow_any_generics = True
mypy_path=.pixi/envs/default/Library/python:.pixi/envs/default/share/qgis/python

# Ignore errors for imported packages.
[mypy-console.*]
Expand Down
6 changes: 6 additions & 0 deletions utils/env_setup.bat
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
set JULIAUP_DEPOT_PATH=%~dp0
set QUARTO_PYTHON=python

setlocal EnableDelayedExpansion
set "current_dir=%CD%\"
set "conda_prefix=%CONDA_PREFIX%\"
set "relative_conda_prefix=!conda_prefix:%CD%=.!"
endlocal & set MYPYPATH="%relative_conda_prefix%Library\python"
3 changes: 3 additions & 0 deletions utils/env_setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ JULIAUP_DEPOT_PATH=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null &
export JULIAUP_DEPOT_PATH
QUARTO_PYTHON=python
export QUARTO_PYTHON
relative_conda_prefix=$(realpath --relative-to="$PWD" "$CONDA_PREFIX")
MYPYPATH=$relative_conda_prefix/share/qgis/python
export MYPYPATH

0 comments on commit 5e763b8

Please sign in to comment.