From 86cbc7ccc0f355b2196dbdc2a2bf9c320d08693b Mon Sep 17 00:00:00 2001 From: Julian Straus <104911227+JulStraus@users.noreply.github.com> Date: Tue, 28 May 2024 10:05:31 +0200 Subject: [PATCH] Adaptation to changes introduced in EMB v0.7 (#19) * Adjustment to all changes from EMB v0.7 - Use StorageBehavior for representing the storage behavior - Use AbstractStorageParameter for the capacities in charge, level, and discharge --- NEWS.md | 6 + Project.toml | 6 +- docs/make.jl | 15 +- docs/src/how-to/update-models.md | 150 +++++++++ docs/src/index.md | 6 +- docs/src/library/public.md | 27 +- docs/src/manual/NEWS.md | 10 +- docs/src/manual/constraint-functions.md | 43 +-- docs/src/manual/optimization-variables.md | 2 +- examples/simple_hydro_power.jl | 14 +- src/checks.jl | 54 +++- src/constraint_functions.jl | 208 ++----------- src/datastructures.jl | 224 +++++++++----- src/legacy_constructor.jl | 360 ++++++++++++++++++---- src/model.jl | 18 +- test/runtests.jl | 1 + test/test_constructors.jl | 318 +++++++++++++++++++ test/test_hydro.jl | 190 +++++------- test/utils.jl | 2 +- 19 files changed, 1153 insertions(+), 501 deletions(-) create mode 100644 docs/src/how-to/update-models.md create mode 100644 test/test_constructors.jl diff --git a/NEWS.md b/NEWS.md index 1ea86ee..71dc108 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # Release notes +## Version 0.6.0 (2024-05-28) + +* Adjusted to changes introduced in `EnergyModelsBase` v0.7. +* Remove legacy constructor for `RegHydroStor` and provide a warning for it. +* Added constructors for `HydroStor` not requiring any longer specifying an input dictionary. + ## Version 0.5.6 (2024-05-09) * Provided a contribution section in the documentation. diff --git a/Project.toml b/Project.toml index 7bdad1d..4250d7d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "EnergyModelsRenewableProducers" uuid = "b007c34f-ba52-4995-ba37-fffe79fbde35" authors = ["Sigmund Eggen Holm , Julian Straus "] -version = "0.5.6" +version = "0.6.0" [deps] EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" @@ -10,7 +10,7 @@ Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" TimeStruct = "f9ed5ce0-9f41-4eaa-96da-f38ab8df101c" [compat] -EnergyModelsBase = "^0.6.7" +EnergyModelsBase = "^0.7.0" JuMP = "1.5" -TimeStruct = "^0.7.0" +TimeStruct = "^0.8.0" julia = "^1.6" diff --git a/docs/make.jl b/docs/make.jl index d15e1fe..345e5a4 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -27,17 +27,20 @@ makedocs( pages = [ "Home" => "index.md", "Manual" => Any[ - "Quick Start"=>"manual/quick-start.md", - "Optimization variables"=>"manual/optimization-variables.md", - "Constraint functions"=>"manual/constraint-functions.md", - "Examples"=>"manual/simple-example.md", + "Quick Start" => "manual/quick-start.md", + "Optimization variables" => "manual/optimization-variables.md", + "Constraint functions" => "manual/constraint-functions.md", + "Examples" => "manual/simple-example.md", "Release notes" => "manual/NEWS.md", ], "How to" => Any[ + "Update models" => "how-to/update-models.md", "Contribute to EnergyModelsRenewableProducers" => "how-to/contribute.md", ], - "Library" => - Any["Public"=>"library/public.md", "Internals"=>"library/internals.md"], + "Library" => Any[ + "Public" => "library/public.md", + "Internals" => "library/internals.md", + ], ], ) diff --git a/docs/src/how-to/update-models.md b/docs/src/how-to/update-models.md new file mode 100644 index 0000000..75388da --- /dev/null +++ b/docs/src/how-to/update-models.md @@ -0,0 +1,150 @@ +# [Update your model to the latest versions](@id update-models) + +`EnergyModelsRenewableProducers` is still in a pre-release version. +Hence, there are frequently breaking changes occuring, although we plan to keep backwards compatibility. +This document is designed to provide users with information regarding how they have to adjust their models to keep compatibility to the latest changes. +We will as well implement information regarding the adjustment of extension packages, although this is more difficult due to the vast majority of potential changes. + +## Adjustments from 0.4.2 + +### Key changes for nodal descriptions + +Version 0.7 of `EnergyModelsBase` introduced both *storage behaviours* resulting in a rework of the individual approach for calculating the level balance as well as the potential to have charge and discharge capacities through *storage parameters*. + +!!! note + The legacy constructors for calls of the composite type of version 0.5 will be included at least until version 0.7. + +### [`HydroStor`](@ref) + +`HydroStor` was significantly reworked due to the changes in `EnergyModelsBase` +The total rework is provided below. + +```julia +# The previous nodal description for a `HydroStor` node was given by: +HydroStor( + id, + rate_cap::TimeProfile, + stor_cap::TimeProfile, + + level_init::TimeProfile, + level_inflow::TimeProfile, + level_min::TimeProfile, + + opex_var::TimeProfile, + opex_fixed::TimeProfile, + stor_res::ResourceCarrier, + input::Dict{<:Resource, <:Real}, + output::Dict{<:Resource, <:Real}, + data::Vector{Data}, +) + +# This translates to the following new version +HydroStor{CyclicStrategic}( + id, + StorCapOpexFixed(stor_cap, opex_fixed), + StorCapOpexVar(rate_cap, opex_var), + level_init, + level_inflow, + level_min, + stor_res, + input, + output, + data, +) +``` + +### [`PumpedHydroStor`](@ref) + +`PumpedHydroStor` was significantly reworked due to the changers in `EnergyModelsBase` +The total rework is provided below. + +```julia +# The previous nodal description for a `PumpedHydroStor` node was given by: +PumpedHydroStor( + id, + rate_cap::TimeProfile, + stor_cap::TimeProfile, + + level_init::TimeProfile, + level_inflow::TimeProfile, + level_min::TimeProfile, + + opex_var::TimeProfile, + opex_var_pump::TimeProfile, + opex_fixed::TimeProfile, + stor_res::ResourceCarrier, + input::Dict{<:Resource, <:Real}, + output::Dict{<:Resource, <:Real}, + data::Vector{Data}, +) + +# This translates to the following new version +PumpedHydroStor{CyclicStrategic}( + id, + StorCapOpexVar(rate_cap, opex_var_pump), + StorCapOpexFixed(stor_cap, opex_fixed), + StorCapOpexVar(rate_cap, opex_var), + level_init, + level_inflow, + level_min, + stor_res, + input, + output, + data, +) +``` + +## Adjustments from 0.4.0 to 0.6.x + +### Key changes for nodal descriptions + +Version 0.4.1 introduced two new types that replaced the original `RegHydroStor` node with two types called [`PumpedHydroStor`](@ref) and [`HydroStor`](@ref). +The changes allowed for the introduction of a variable OPEX for pumping. +In the translation below, it is assumed that the variable OPEX for pumping is 0. + +```julia +# The previous nodal description was given by: +RegHydroStor( + id::Any, + rate_cap::TimeProfile, + stor_cap::TimeProfile, + has_pump::Bool, + level_init::TimeProfile, + level_inflow::TimeProfile, + level_min::TimeProfile, + opex_var::TimeProfile, + opex_fixed::TimeProfile, + stor_res::ResourceCarrier, + input, + output, + Data, +) + +# This translates to the following new version if has_pump == true +PumpedHydroStor( + id, + StorCapOpexVar(rate_cap, FixedProfile(0)), + StorCapOpexFixed(stor_cap, opex_fixed), + StorCapOpexVar(rate_cap, opex_var), + level_init, + level_inflow, + level_min, + stor_res, + input, + output, + Data, +) +# and the following version if has_pump == false +HydroStor( + id, + StorCapOpexFixed(stor_cap, opex_fixed), + StorCapOpexVar(rate_cap, opex_var), + level_init, + level_inflow, + level_min, + stor_res, + input, + output, + Data, +) +``` diff --git a/docs/src/index.md b/docs/src/index.md index 6f8b8d0..8152eb3 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -25,6 +25,7 @@ Pages = [ "manual/constraint-functions.md", "manual/simple-example.md" ] +Depth = 1 ``` ## How to guides @@ -32,7 +33,9 @@ Pages = [ ```@contents Pages = [ "how-to/contribute.md", + "how-to/update-models.md", ] +Depth = 1 ``` ## Library outline @@ -41,5 +44,6 @@ Pages = [ Pages = [ "library/public.md" "library/internals.md" - ] +] +Depth = 1 ``` diff --git a/docs/src/library/public.md b/docs/src/library/public.md index 3d2f4d7..1e29e88 100644 --- a/docs/src/library/public.md +++ b/docs/src/library/public.md @@ -27,9 +27,10 @@ A hydropower plant is much more flexible than, *e.g.*, a wind farm since the wat Energy can be produced (almost) whenever it is needed. Some hydropower plants also have pumps installed. These are used to pump water into the reservoir when excess and cheap energy is available in the network. +`EnergyModelsRenewableProducers` introduces hence two different types representing a regulated hydropower plant ([`HydroStor`](@ref)) and a pumped regulated hydropower plant ([`PumpedHydroStor`](@ref)) without a lower reservoir. +Both types have a `level` and `discharge` capacity while a `PumpedHydroStor` also includes a `charge` capacity. -The field `rate_cap` describes the installed production capacity of the (aggregated) hydropower plant. -The variable `level_init` represents the initial energy available in the reservoir in the beginning of each investment period, while `stor_cap` is the installed storage capacity in the reservoir. +The variable `level_init` represents the initial energy available in the reservoir in the beginning of each investment period. The variable `level_inflow` describes the inflow into the reservoir (measured in energy units), while `level_min` is the allowed minimum storage level in the dam, given as a ratio of the installed storage capacity of the reservoir at every operational period. The required minimum level is enforced by NVE and varies over the year. @@ -38,16 +39,6 @@ The resources stored in the hydro storage is set as `stor_res`, similar to a reg The five last parameters are used in the same way as in `EMB.Storage`. In the implementation of [`PumpedHydroStor`](@ref), the values set in `input` represents a loss of energy when using the pumps. A value of `1` means no energy loss, while a value of `0` represents 100% energy loss of that inflow variable. -[`PumpedHydroStor`](@ref) has in addition the field `opex_var_pump::TimeProfile`. -This field corresponds to the variable operational expenditures when pumping water into the storage reservoir. - -Since we also want to be able to model hydropower plant nodes *without* pumps, we include the boolean `has_pump` in the type describing hydropower. -For combining the behavior of a hydropower plant with and without a pump, we can disable the inflow of energy by setting the constraint - - ``\texttt{flow\_in}[n, t, p_{\texttt{Power}}] = 0,`` - -for the stored resource ``p_{\texttt{Power}}`` for the node ``n`` `::HydroStor`. -To access this variable, we therefore have to let the type `HydroStorage` be a subtype of `EMB.Storage`. The fields of the different types are listed below: @@ -55,5 +46,17 @@ The fields of the different types are listed below: HydroStorage HydroStor PumpedHydroStor +``` + +In recent version increases, we changed the individual fields of the `HydroStorage` nodes as well as their types. +Hence, we still incorporate legacy constructors that can be utilized when having a model in previous versions. +However, we removed one legacy constructor as it is no longer required. +Calling the constructor will provide you now with an error. + +This legacy constructor is: + +```@docs RegHydroStor ``` + +See the section on *[how to update models](@ref update-models)* for further information regarding how you can translate your existing model to the new model. diff --git a/docs/src/manual/NEWS.md b/docs/src/manual/NEWS.md index e380db0..71dc108 100644 --- a/docs/src/manual/NEWS.md +++ b/docs/src/manual/NEWS.md @@ -1,9 +1,15 @@ # Release notes -## Unversioned +## Version 0.6.0 (2024-05-28) + +* Adjusted to changes introduced in `EnergyModelsBase` v0.7. +* Remove legacy constructor for `RegHydroStor` and provide a warning for it. +* Added constructors for `HydroStor` not requiring any longer specifying an input dictionary. + +## Version 0.5.6 (2024-05-09) -* Updated a link in the documentation for the examples. * Provided a contribution section in the documentation. +* Fixed a link in the documentation for the examples. ## Version 0.5.5 (2024-03-21) diff --git a/docs/src/manual/constraint-functions.md b/docs/src/manual/constraint-functions.md index e9d6373..20cd07e 100644 --- a/docs/src/manual/constraint-functions.md +++ b/docs/src/manual/constraint-functions.md @@ -1,39 +1,44 @@ # [Constraint functions](@id constraint_functions) -The [`HydroStorage`](@ref) types dispatch on individual functions from within `EnergyModelsBase` ti extend the functionality +## `NonDisRES` (non-dispatchable renewable energy source) -## Storage level constraints +The introduction of the type [`NonDisRES`](@ref NonDisRES_public) does not require a new `create_node` function. +Instead, it is sufficient to dispatch on the function -All [`HydroStorage`](@ref) subtypes utilize the same function, `constraints_level(m, n::Storage, ๐’ฏ, ๐’ซ, modeltype::EnergyModel)`, for calling the two relevant subfunctions. +```julia +EMB.constraints_capacity(m, n::NonDisRES, ๐’ฏ::TimeStructure, modeltype::EnergyModel) +``` -The function +to introduce the new energy balance using the field `profile` and the variable ``\texttt{curtailment}``. +In this case, we also have to call the function ```julia -EMB.constraints_level_aux(m, n::HydroStorage, ๐’ฏ, ๐’ซ, modeltype::EnergyModel) +constraints_capacity_installed(m, n, ๐’ฏ, modeltype) ``` -is extended to account for both the provision of an initial level at the start of each strategic period as well as modifying the constraint for the variable ``\texttt{stor\_level\_}\Delta\texttt{\_op}`` to account for the introduction of the new variable ``\texttt{hydro\_spill}``. -The former is required for [`HydroStorage`](@ref) subtypes asthe initial level is frequently a function of the season (excluding small scale pumped hydro storage) while the latter is required to include spillage. +to allow for investments when coupled with `EnergyModelsInvestments`. +We do however not need to create new methods for said function. + +## `HydroStorage` (regulated hydro storage with or without pump) + +The [`HydroStorage`](@ref HydroStorage_public) types utilize the same `create_node` function for introducing new concepts. +In addition, they dispatch on individual functions from within `EnergyModelsBase` to extend the functionality. The functions ```julia -EMB.constraints_level_sp(m, n::HydroStorage, t_inv, ๐’ซ, modeltype::EnergyModel) +EMB.constraints_flow_in(m, n::HydroStor, ๐’ฏ::TimeStructure, modeltype::EnergyModel) +EMB.constraints_flow_in(m, n::PumpedHydroStor, ๐’ฏ::TimeStructure, modeltype::EnergyModel) ``` -are similar to the function used for `RefStorage{T} where {T<:ResourceCarrier}`. -It is however necessary to reintroduce it due to the declaration for `RefStorage` in `EnergyModelsBase`. -This will most likely be adjusted in later versions, although it will not impact the user directly. - -## Operational expenditure constraints +allow for a different behavior of the `HydroStorage` node through fixing the variable ``\texttt{flow\\_in}`` in the case of a [`HydroStor`](@ref) node to 0 and limiting it in the case of a [`PumpedHydroStor`](@ref) to installed charge capacity through the variable ``\texttt{stor\\_charge\\_use}``. -Variable operational expenditure (OPEX) constraints are slightly different defined in the case of [`HydroStor`](@ref) and [`PumpedHydroStor`](@ref) nodes. -Hence, dispatch is required on the individual constraints: +All `HydroStorage` subtypes utilize the introduced level balances from `EnergyModelsBase`. +MOdification to the level balance is achieved through overloading ```julia -EMB.constraints_opex_var(m, n::HydroStor, ๐’ฏ::TimeStructure, modeltype::EnergyModel) -EMB.constraints_opex_var(m, n::PumpedHydroStor, ๐’ฏ::TimeStructure, modeltype::EnergyModel) +EMB.constraints_level_aux(m, n::HydroStorage, ๐’ฏ, ๐’ซ, modeltype::EnergyModel) ``` -Within a [`HydroStor`](@ref) node, the variable OPEX is defined *via* the outflow from the hydropower plant, contrary to the definition of a `RefStorage` node in which the variable OPEX is defined *via* the inflow. -A [`PumpedHydroStor`](@ref) has contributions by both the inflow (through the field `opex_var_pump`) and the outflow (through the field `opex_var`). +to account for both the provision of an initial level at the start of each strategic period as well as modifying the constraint for the variable ``\texttt{stor\_level\_}\Delta\texttt{\_op}`` to account for the introduction of the new variable ``\texttt{hydro\_spill}``. +The former is required for [`HydroStorage`](@ref) subtypes as the initial level is frequently a function of the season (excluding small scale pumped hydro storage) while the latter is required to include spillage. diff --git a/docs/src/manual/optimization-variables.md b/docs/src/manual/optimization-variables.md index 33c3cbe..e5201db 100644 --- a/docs/src/manual/optimization-variables.md +++ b/docs/src/manual/optimization-variables.md @@ -35,7 +35,7 @@ The spillage is introduced to allow for an overflow from a reservoir if the infl The variable is used in the following constraint [`EMB.constraints_level_aux`](@ref), - ``\texttt{stor\_level\_}\Delta\texttt{\_op}[n, t] = \texttt{level\_inflow}(n, t) + \texttt{inputs}(n, p_{\texttt{Power}}) \cdot \texttt{flow\_in}[n, t] + \texttt{stor\_rate\_use}[n, t] - \texttt{hydro\_spill}[n, t]`` + ``\texttt{stor\_level\_}\Delta\texttt{\_op}[n, t] = \texttt{level\_inflow}(n, t) + \texttt{inputs}(n, p_{\texttt{Power}}) \cdot \texttt{flow\_in}[n, t] + \texttt{stor\_discharge\_use}[n, t] - \texttt{hydro\_spill}[n, t]`` for the stored resource ``p_{\texttt{Power}}``. diff --git a/examples/simple_hydro_power.jl b/examples/simple_hydro_power.jl index 1b33f65..744deec 100644 --- a/examples/simple_hydro_power.jl +++ b/examples/simple_hydro_power.jl @@ -62,15 +62,19 @@ function generate_example_data() ) # Create a regulated hydro power plant without storage capacity - hydro = HydroStor( + hydro = HydroStor{CyclicStrategic}( "hydropower", # Node ID - FixedProfile(2.0), # Rate capacity in MW - FixedProfile(90), # Storage capacity in MWh + StorCapOpexFixed(FixedProfile(90), FixedProfile(3)), + # Line above for the storage level: + # Argument 1: Storage capacity in MWh + # Argument 2: Fixed OPEX in EUR/8h + StorCapOpexVar(FixedProfile(2.0), FixedProfile(8)), + # Line above for the discharge rate: + # Argument 1: Rate capacity in MW + # Argument 2: Variable OPEX in EUR/MWh FixedProfile(10), # Initial storage level in MWh FixedProfile(1), # Inflow to the Node in MW FixedProfile(0.0), # Minimum storage level as fraction - FixedProfile(8), # Variable OPEX in EUR/MWh - FixedProfile(3), # Fixed OPEX in EUR/8h Power, # Stored resource Dict(Power => 0.9), # Input to the power plant, irrelevant in this case Dict(Power => 1), # Output from the Node, in this gase, Power diff --git a/src/checks.jl b/src/checks.jl index 0028cf3..d3aec38 100644 --- a/src/checks.jl +++ b/src/checks.jl @@ -42,11 +42,15 @@ end This method checks that the *[`HydroStorage`](@ref HydroStorage_public)* node is valid. ## Checks - - The value of the field `rate_cap` is required to be non-negative. - - The value of the field `stor_cap` is required to be non-negative. - - The value of the field `fixed_opex` is required to be non-negative and - accessible through a `StrategicPeriod` as outlined in the function - `check_fixed_opex(n, ๐’ฏแดตโฟแต›, check_timeprofiles)`. +- The `TimeProfile` of the field `capacity` in the type in the field `charge` is required + to be non-negative if the chosen composite type has the field `capacity`. +- The `TimeProfile` of the field `capacity` in the type in the field `level` is required + to be non-negative`. +- The `TimeProfile` of the field `capacity` in the type in the field `discharge` is required + to be non-negative if the chosen composite type has the field `capacity`. +- The `TimeProfile` of the field `fixed_opex` is required to be non-negative and + accessible through a `StrategicPeriod` as outlined in the function + `check_fixed_opex(n, ๐’ฏแดตโฟแต›, check_timeprofiles)` for the chosen composite type . - The field `output` can only include a single `Resource`. - The value of the field `output` is required to be smaller or equal to 1. - The value of the field `input` is required to be in the range ``[0, 1]``. @@ -58,17 +62,36 @@ This method checks that the *[`HydroStorage`](@ref HydroStorage_public)* node is function EMB.check_node(n::HydroStorage, ๐’ฏ, modeltype::EMB.EnergyModel, check_timeprofiles::Bool) ๐’ฏแดตโฟแต› = strategic_periods(๐’ฏ) - cap = capacity(n) + par_charge = charge(n) + par_level = level(n) + par_discharge = discharge(n) + if isa(par_charge, EMB.UnionCapacity) + @assert_or_log( + sum(capacity(par_charge, t) โ‰ฅ 0 for t โˆˆ ๐’ฏ) == length(๐’ฏ), + "The charge capacity must be non-negative." + ) + end + if isa(par_charge, EMB.UnionOpexFixed) + EMB.check_fixed_opex(par_charge, ๐’ฏแดตโฟแต›, check_timeprofiles) + end @assert_or_log( - sum(cap.rate[t] < 0 for t โˆˆ ๐’ฏ) == 0, - "The production capacity in field `rate_cap` has to be non-negative." - ) - @assert_or_log( - sum(cap.level[t] < 0 for t โˆˆ ๐’ฏ) == 0, - "The storage capacity in field `stor_cap` has to be non-negative." + sum(capacity(par_level, t) โ‰ฅ 0 for t โˆˆ ๐’ฏ) == length(๐’ฏ), + "The level capacity must be non-negative." ) - EMB.check_fixed_opex(n, ๐’ฏแดตโฟแต›, check_timeprofiles) + if isa(par_level, EMB.UnionOpexFixed) + EMB.check_fixed_opex(par_level, ๐’ฏแดตโฟแต›, check_timeprofiles) + end + if isa(par_discharge, EMB.UnionCapacity) + @assert_or_log( + sum(capacity(par_discharge, t) โ‰ฅ 0 for t โˆˆ ๐’ฏ) == length(๐’ฏ), + "The charge capacity must be non-negative." + ) + end + if isa(par_discharge, EMB.UnionOpexFixed) + EMB.check_fixed_opex(par_discharge, ๐’ฏแดตโฟแต›, check_timeprofiles) + end + @assert_or_log( length(outputs(n)) == 1, "Only one resource can be stored, so only this one can flow out." @@ -97,15 +120,14 @@ function EMB.check_node(n::HydroStorage, ๐’ฏ, modeltype::EMB.EnergyModel, check end @assert_or_log( - sum(level_init(n, t) โ‰ค cap.level[t] for t โˆˆ ๐’ฏ) == length(๐’ฏ), + sum(level_init(n, t) โ‰ค capacity(par_level, t) for t โˆˆ ๐’ฏ) == length(๐’ฏ), "The initial level `level_init` has to be less or equal to the max storage capacity." ) for t_inv โˆˆ ๐’ฏแดตโฟแต› - t = first(t_inv) # Check that the reservoir isn't underfilled from the start. @assert_or_log( - level_init(n, t_inv) + level_inflow(n, t) โ‰ฅ level_min(n, t) * cap.level[t], + level_init(n, t_inv) + level_inflow(n, t) โ‰ฅ level_min(n, t) * capacity(par_level, t), "The reservoir can't be underfilled from the start (" * string(t) * ").") end diff --git a/src/constraint_functions.jl b/src/constraint_functions.jl index 5565ba5..813d28e 100644 --- a/src/constraint_functions.jl +++ b/src/constraint_functions.jl @@ -22,18 +22,48 @@ function EMB.constraints_capacity(m, n::NonDisRES, ๐’ฏ::TimeStructure, modeltyp constraints_capacity_installed(m, n, ๐’ฏ, modeltype) end +""" + constraints_flow_in(m, n::HydroStor, ๐’ฏ::TimeStructure, modeltype::EnergyModel) + +When `n::HydroStor`, the the variable `:flow_in` is fixed to 0 for all potential inputs. +""" +function EMB.constraints_flow_in(m, n::HydroStor, ๐’ฏ::TimeStructure, modeltype::EnergyModel) + # Declaration of the required subsets + ๐’ซโฑโฟ = inputs(n) + + # Fix the inlet flow to a value of 0 + for t โˆˆ ๐’ฏ, p โˆˆ ๐’ซโฑโฟ + fix(m[:flow_in][n, t, p], 0; force=true) + end +end + +""" + constraints_flow_in(m, n, ๐’ฏ::TimeStructure, modeltype::EnergyModel) + +When `n::PumpedHydroStor`, the the variable `:flow_in` is used contrary to standard nodes, +that is the variable `:flow_in` is multiplied with the `inputs` value. +""" +function EMB.constraints_flow_in(m, n::PumpedHydroStor, ๐’ฏ::TimeStructure, modeltype::EnergyModel) + # Declaration of the required subsets + ๐’ซโฑโฟ = inputs(n) + + # Constraint for the individual input stream connections + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ ๐’ซโฑโฟ], + m[:flow_in][n, t, p] * inputs(n, p) == m[:stor_charge_use][n, t] + ) +end """ EMB.constraints_level_aux(m, n::HydroStorage, ๐’ฏ, ๐’ซ, modeltype) Function for creating the ฮ” constraint for the level of a `HydroStorage` node as well as -the specificaiton of the initial level in a strategic period. +the specification of the initial level in a strategic period. The change in storage level in the reservoir at operational periods `t` is the inflow through `level_inflow` plus the input `flow_in` minus the production `stor_rate_use` and the spillage of water due to overflow `hydro_spill`. """ -function EMB.constraints_level_aux(m, n::HydroStorage, ๐’ฏ, ๐’ซ, modeltype) +function EMB.constraints_level_aux(m, n::HydroStorage, ๐’ฏ, ๐’ซ, modeltype::EnergyModel) # Declaration of the required subsets p_stor = storage_resource(n) @@ -41,7 +71,7 @@ function EMB.constraints_level_aux(m, n::HydroStorage, ๐’ฏ, ๐’ซ, modeltype) @constraint(m, [t โˆˆ ๐’ฏ], m[:stor_level_ฮ”_op][n, t] == level_inflow(n, t) + inputs(n, p_stor) * m[:flow_in][n, t, p_stor] - - m[:stor_rate_use][n, t] - m[:hydro_spill][n, t] + m[:stor_discharge_use][n, t] - m[:hydro_spill][n, t] ) # The initial storage level is given by the specified initial level in the strategic @@ -54,176 +84,4 @@ function EMB.constraints_level_aux(m, n::HydroStorage, ๐’ฏ, ๐’ซ, modeltype) ) end -""" - EMB.constraints_level_sp( - m, - n::HydroStorage, - t_inv::TS.StrategicPeriod{T, U}, - ๐’ซ, - modeltype - ) where {T, U<:SimpleTimes} - -Function for creating the level constraint for a `HydroStorage` node when the -TimeStructure is given as `SimpleTimes`. -""" -function EMB.constraints_level_sp( - m, - n::HydroStorage, - t_inv::TS.StrategicPeriod{T, U}, - ๐’ซ, - modeltype - ) where {T, U<:SimpleTimes} - - # Energy balance constraints for stored hydro power. - for (t_prev, t) โˆˆ withprev(t_inv) - if isnothing(t_prev) - @constraint(m, - m[:stor_level][n, t] == - m[:stor_level][n, last(t_inv)] + - m[:stor_level_ฮ”_op][n, t] * duration(t) - ) - else - @constraint(m, - m[:stor_level][n, t] == - m[:stor_level][n, t_prev] + - m[:stor_level_ฮ”_op][n, t] * duration(t) - ) - end - end -end - -""" - EMB.constraints_level_sp( - m, - n::HydroStorage, - t_inv::TS.StrategicPeriod{T, RepresentativePeriods{U, T, SimpleTimes{T}}}, - ๐’ซ, - modeltype - ) where {T, U} - -Function for creating the level constraint for a `HydroStorage` storage node when the -operational `TimeStructure` is given as `RepresentativePeriods`. -""" -function EMB.constraints_level_sp( - m, - n::HydroStorage, - t_inv::TS.StrategicPeriod{T, RepresentativePeriods{U, T, SimpleTimes{T}}}, - ๐’ซ, - modeltype - ) where {T, U} - - # Declaration of the required subsets - ๐’ฏสณแต– = repr_periods(t_inv) - - # Constraint for the total change in the level in a given representative period - @constraint(m, [t_rp โˆˆ ๐’ฏสณแต–], - m[:stor_level_ฮ”_rp][n, t_rp] == - sum(m[:stor_level_ฮ”_op][n, t] * multiple_strat(t_inv, t) * duration(t) for t โˆˆ t_rp) - ) - - # Constraint that the total change has to be 0 - @constraint(m, sum(m[:stor_level_ฮ”_rp][n, t_rp] for t_rp โˆˆ ๐’ฏสณแต–) == 0) - - # Mass/energy balance constraints for stored energy carrier. - for (t_rp_prev, t_rp) โˆˆ withprev(๐’ฏสณแต–), (t_prev, t) โˆˆ withprev(t_rp) - if isnothing(t_rp_prev) && isnothing(t_prev) - - # Last representative period in t_inv - t_rp_last = last(๐’ฏสณแต–) - - # Constraint for the level of the first operational period in the first - # representative period in a strategic period - # The substraction of stor_level_ฮ”_op[n, first(t_rp_last)] is necessary to avoid - # treating the first operational period differently with respect to the level - # as the latter is at the end of the period - @constraint(m, - m[:stor_level][n, t] == - m[:stor_level][n, first(t_rp_last)] - - m[:stor_level_ฮ”_op][n, first(t_rp_last)] * duration(first(t_rp_last)) + - m[:stor_level_ฮ”_rp][n, t_rp_last] + - m[:stor_level_ฮ”_op][n, t] * duration(t) - ) - - # Constraint to avoid starting below 0 in this operational period - @constraint(m, - m[:stor_level][n, t] - - m[:stor_level_ฮ”_op][n, t] * duration(t) โ‰ฅ 0 - ) - - # Constraint to avoid having a level larger than the storage allows - @constraint(m, - m[:stor_level][n, t] - - m[:stor_level_ฮ”_op][n, t] * duration(t) โ‰ค m[:stor_cap_inst][n, t] - ) - - elseif isnothing(t_prev) - # Constraint for the level of the first operational period in any following - # representative period - # The substraction of stor_level_ฮ”_op[n, first(t_rp_prev)] is necessary to avoid - # treating the first operational period differently with respect to the level - # as the latter is at the end of the period - @constraint(m, - m[:stor_level][n, t] == - m[:stor_level][n, first(t_rp_prev)] - - m[:stor_level_ฮ”_op][n, first(t_rp_prev)] * duration(first(t_rp_prev)) + - m[:stor_level_ฮ”_rp][n, t_rp_prev] + - m[:stor_level_ฮ”_op][n, t] * duration(t) - ) - - # Constraint to avoid starting below 0 in this operational period - @constraint(m, - m[:stor_level][n, t] - m[:stor_level_ฮ”_op][n, t] * duration(t) โ‰ฅ - level_min(n, t) * m[:stor_cap_inst][n, t] - ) - # Constraint to avoid having a level larger than the storage allows - @constraint(m, - m[:stor_level][n, t] - m[:stor_level_ฮ”_op][n, t] * duration(t) โ‰ค - m[:stor_cap_inst][n, t] - ) - else - # Constraint for the level of a standard operational period - @constraint(m, - m[:stor_level][n, t] == - m[:stor_level][n, t_prev] + - m[:stor_level_ฮ”_op][n, t] * duration(t) - ) - end - end -end - #! format: on -""" - constraints_opex_var(m, n::HydroStor, ๐’ฏแดตโฟแต›, modeltype::EnergyModel) - -Function for creating the constraint on the variable OPEX of a `HydroStor`. -""" -function EMB.constraints_opex_var(m, n::HydroStor, ๐’ฏแดตโฟแต›, modeltype::EnergyModel) - p_stor = EMB.storage_resource(n) - @constraint( - m, - [t_inv โˆˆ ๐’ฏแดตโฟแต›], - m[:opex_var][n, t_inv] == sum( - m[:flow_out][n, t, p_stor] * opex_var(n, t) * EMB.multiple(t_inv, t) for - t โˆˆ t_inv - ) - ) -end - -""" - constraints_opex_var(m, n::PumpedHydroStor, ๐’ฏแดตโฟแต›, modeltype::EnergyModel) - -Function for creating the constraint on the variable OPEX of a `PumpedHydroStor`. -""" -function EMB.constraints_opex_var(m, n::PumpedHydroStor, ๐’ฏแดตโฟแต›, modeltype::EnergyModel) - p_stor = EMB.storage_resource(n) - @constraint( - m, - [t_inv โˆˆ ๐’ฏแดตโฟแต›], - m[:opex_var][n, t_inv] == sum( - ( - m[:flow_in][n, t, p_stor] * opex_var_pump(n, t) + - m[:flow_out][n, t, p_stor] * opex_var(n, t) - ) * EMB.multiple(t_inv, t) for t โˆˆ t_inv - ) - ) -end diff --git a/src/datastructures.jl b/src/datastructures.jl index 7415e1c..2cbe591 100644 --- a/src/datastructures.jl +++ b/src/datastructures.jl @@ -1,16 +1,20 @@ -""" A non-dispatchable renewable energy source. +""" + NonDisRES <: EMB.Source -# Fields -- **`id`** is the name/identifyer of the node.\n -- **`cap::TimeProfile`** is the installed capacity.\n -- **`profile::TimeProfile`** is the power production in each operational period as a ratio \ -of the installed capacity at that time.\n -- **`opex_var::TimeProfile`** is the variational operational costs per energy unit produced.\n -- **`opex_fixed::TimeProfile`** is the fixed operational costs.\n -- **`output::Dict{Resource, Real}`** are the generated `Resource`s, normally Power.\n -- **`data::Vector{Data}`** is the additional data (e.g. for investments). The field \ -`data` is conditional through usage of a constructor. +A non-dispatchable renewable energy source. It extends the existing `RefSource` node through +including a profile that corresponds to thr production. The profile can have variations on +the strategic level. +# Fields +- **`id`** is the name/identifyer of the node. +- **`cap::TimeProfile`** is the installed capacity. +- **`profile::TimeProfile`** is the power production in each operational period as a ratio + of the installed capacity at that time. +- **`opex_var::TimeProfile`** is the variable operating expense per energy unit produced. +- **`opex_fixed::TimeProfile`** is the fixed operating expense. +- **`output::Dict{Resource, Real}`** are the generated `Resource`s, normally Power. +- **`data::Vector{Data}`** is the additional data (e.g. for investments). The field `data` + is conditional through usage of a constructor. """ struct NonDisRES <: EMB.Source id::Any @@ -33,64 +37,118 @@ function NonDisRES( end """ An abstract type for hydro storage nodes, with or without pumping. """ -abstract type HydroStorage <: EMB.Storage end +abstract type HydroStorage{T} <: EMB.Storage{T} end -""" A regulated hydropower storage, modelled as a `Storage` node. +""" + HydroStor{T} <: HydroStorage{T} + +A regulated hydropower storage, modelled as a `Storage` node. A regulated hydro storage node +requires a capacity for the `discharge` and does not have a required inflow from the model, +except for water inflow from outside the model, although it requires a field `input`. ## Fields -- **`id`** is the name/identifyer of the node.\n -- **`rate_cap::TimeProfile`**: installed capacity.\n -- **`stor_cap::TimeProfile`** Initial installed storage capacity in the dam.\n -- **`level_init::TimeProfile`** Initial energy stored in the dam, in units of power.\n -- **`level_inflow::TimeProfile`** Inflow of power per operational period.\n -- **`level_min::TimeProfile`** Minimum fraction of the reservoir capacity that can be left.\n -- **`opex_var::TimeProfile`** Operational cost per GWh produced.\n -- **`opex_fixed::TimeProfile`** Fixed operational costs.\n -- **`stor_res::ResourceCarrier`** is the stored `Resource`.\n -- **`input::Dict{Resource, Real}`** the stored and used resources. The \ -values in the Dict is a ratio describing the energy loss when using the pumps.\n -- **`output::Dict{Resource, Real}`** can only contain one entry, the stored resource.\n -- **`data::Vector{Data}`** additional data (e.g. for investments). The field \ -`data` is conditional through usage of a constructor. -""" -struct HydroStor <: HydroStorage +- **`id`** is the name/identifyer of the node. +- **`level::EMB.UnionCapacity`** are the level parameters of the `HydroStor` node. + Depending on the chosen type, the charge parameters can include variable OPEX and/or fixed OPEX. +- **`discharge::EMB.UnionCapacity`** are the discharging parameters of the `HydroStor` node. + Depending on the chosen type, the discharge parameters can include variable OPEX, fixed OPEX, + and/or a capacity. +- **`level_init::TimeProfile`** is the initial stored energy in the dam. +- **`level_inflow::TimeProfile`** is the inflow of power per operational period. +- **`level_min::TimeProfile`** is the minimum fraction of the reservoir capacity that + has to remain in the `HydroStorage` node. +- **`stor_res::ResourceCarrier`** is the stored `Resource`. +- **`input::Dict{Resource, Real}`** are the input `Resource`s. In the case of a `HydroStor`, + this field can be left out. +- **`output::Dict{Resource, Real}`** can only contain one entry, the stored resource. +- **`data::Vector{Data}`** additional data (e.g. for investments). The field `data` is + conditional through usage of a constructor. +""" +struct HydroStor{T} <: HydroStorage{T} id::Any - rate_cap::TimeProfile - stor_cap::TimeProfile + level::EMB.UnionCapacity + discharge::EMB.UnionCapacity level_init::TimeProfile level_inflow::TimeProfile level_min::TimeProfile - opex_var::TimeProfile - opex_fixed::TimeProfile stor_res::ResourceCarrier input::Dict{<:Resource, <:Real} output::Dict{<:Resource, <:Real} data::Vector{Data} end -function HydroStor( +function HydroStor{T}( id::Any, - rate_cap::TimeProfile, - stor_cap::TimeProfile, + level::EMB.UnionCapacity, + discharge::EMB.UnionCapacity, + level_init::TimeProfile, level_inflow::TimeProfile, level_min::TimeProfile, - opex_var::TimeProfile, - opex_fixed::TimeProfile, + stor_res::ResourceCarrier, - input::Dict{<:Resource, <:Real}, output::Dict{<:Resource, <:Real}, + data::Vector{Data}, + ) where {T<:EMB.StorageBehavior} + return HydroStor{T}( + id, + level, + discharge, + level_init, + level_inflow, + level_min, + stor_res, + Dict{Resource,Real}(stor_res => 1), + output, + data, ) - return HydroStor( +end +function HydroStor{T}( + id::Any, + level::EMB.UnionCapacity, + discharge::EMB.UnionCapacity, + + level_init::TimeProfile, + level_inflow::TimeProfile, + level_min::TimeProfile, + + stor_res::ResourceCarrier, + output::Dict{<:Resource, <:Real}, + ) where {T<:EMB.StorageBehavior} + return HydroStor{T}( id, - rate_cap, - stor_cap, + level, + discharge, + level_init, + level_inflow, + level_min, + stor_res, + Dict{Resource,Real}(stor_res => 1), + output, + Data[], + ) +end +function HydroStor{T}( + id::Any, + level::EMB.UnionCapacity, + discharge::EMB.UnionCapacity, + + level_init::TimeProfile, + level_inflow::TimeProfile, + level_min::TimeProfile, + + stor_res::ResourceCarrier, + input::Dict{<:Resource, <:Real}, + output::Dict{<:Resource, <:Real}, + ) where {T<:EMB.StorageBehavior} + return HydroStor{T}( + id, + level, + discharge, level_init, level_inflow, level_min, - opex_var, - opex_fixed, stor_res, input, output, @@ -98,66 +156,72 @@ function HydroStor( ) end -""" A regulated hydropower storage with pumping capabilities, modelled as a `Storage` node. +""" + PumpedHydroStor{T} <: HydroStorage{T} + +A pumped hydropower storage, modelled as a `Storage` node. A pumped hydro storage node +allows for storing energy through pumping water into the reservoir. The current +implementation is a simplified node in which no lower reservoir is required. Instead, it is +assumed that the reservoir has an infinite size. + +A pumped hydro storage node requires a capacity for both `charge` and `discharge` to +account for the potential to store energy in the form of potential energy. ## Fields -- **`id`** is the name/identifyer of the node.\n -- **`rate_cap::TimeProfile`**: installed capacity.\n -- **`stor_cap::TimeProfile`** Initial installed storage capacity in the dam.\n -- **`level_init::TimeProfile`** Initial energy stored in the dam, in units of power.\n -- **`level_inflow::TimeProfile`** Inflow of power per operational period.\n -- **`level_min::TimeProfile`** Minimum fraction of the reservoir capacity that can be left.\n -- **`opex_var::TimeProfile`** Operational cost per GWh produced.\n -- **`opex_var_pump::TimeProfile`** Operational cost per GWh pumped into the reservoir.\n -- **`opex_fixed::TimeProfile`** Fixed operational costs.\n -- **`stor_res::ResourceCarrier`** is the stored `Resource`.\n -- **`input::Dict{Resource, Real}`** the stored and used resources. The \ -values in the Dict is a ratio describing the energy loss when using the pumps.\n -- **`output::Dict{Resource, Real}`** can only contain one entry, the stored resource.\n -- **`data::Vector{Data}`** additional data (e.g. for investments). The field \ -`data` is conditional through usage of a constructor.\n -""" -struct PumpedHydroStor <: HydroStorage +- **`id`** is the name/identifyer of the node. +- **`charge::EMB.UnionCapacity`** are the charging parameters of the `PumpedHydroStor` node. + Depending on the chosen type, the charge parameters can include variable OPEX, fixed OPEX, + and/or a capacity. +- **`level::EMB.UnionCapacity`** are the level parameters of the `HydroStor` node. + Depending on the chosen type, the charge parameters can include variable OPEX and/or fixed OPEX. +- **`discharge::EMB.UnionCapacity`** are the discharging parameters of the `HydroStor` node. + Depending on the chosen type, the discharge parameters can include variable OPEX, fixed OPEX, + and/or a capacity. +- **`level_init::TimeProfile`** is the initial stored energy in the dam. +- **`level_inflow::TimeProfile`** is the inflow of power per operational period. +- **`level_min::TimeProfile`** is the minimum fraction of the reservoir capacity that + has to remain in the `HydroStorage` node. +- **`stor_res::ResourceCarrier`** is the stored `Resource`. +- **`input::Dict{Resource, Real}`** are the input `Resource`s. +- **`output::Dict{Resource, Real}`** can only contain one entry, the stored resource. +- **`data::Vector{Data}`** additional data (e.g. for investments). The field `data` is + conditional through usage of a constructor. +""" +struct PumpedHydroStor{T} <: HydroStorage{T} id::Any - rate_cap::TimeProfile - stor_cap::TimeProfile + charge::EMB.UnionCapacity + level::EMB.UnionCapacity + discharge::EMB.UnionCapacity level_init::TimeProfile level_inflow::TimeProfile level_min::TimeProfile - opex_var::TimeProfile - opex_var_pump::TimeProfile - opex_fixed::TimeProfile stor_res::ResourceCarrier input::Dict{<:Resource, <:Real} output::Dict{<:Resource, <:Real} data::Vector{Data} end -function PumpedHydroStor( +function PumpedHydroStor{T}( id::Any, - rate_cap::TimeProfile, - stor_cap::TimeProfile, + charge::EMB.UnionCapacity, + level::EMB.UnionCapacity, + discharge::EMB.UnionCapacity, level_init::TimeProfile, level_inflow::TimeProfile, level_min::TimeProfile, - opex_var::TimeProfile, - opex_var_pump::TimeProfile, - opex_fixed::TimeProfile, stor_res::ResourceCarrier, input::Dict{<:Resource, <:Real}, output::Dict{<:Resource, <:Real}, - ) - return PumpedHydroStor( + ) where {T<:EMB.StorageBehavior} + return PumpedHydroStor{T}( id, - rate_cap, - stor_cap, + charge, + level, + discharge, level_init, level_inflow, level_min, - opex_var, - opex_var_pump, - opex_fixed, stor_res, input, output, diff --git a/src/legacy_constructor.jl b/src/legacy_constructor.jl index a6b4161..4c3cb86 100644 --- a/src/legacy_constructor.jl +++ b/src/legacy_constructor.jl @@ -1,26 +1,45 @@ """ -Legacy constructor for a regulated hydropower storage, with or without pumping \ -capabilities. This version will be discontinued in the near future and is already replaced \ -with the two new types `HydroStor` and `PumpedHydroStor`. + RegHydroStor( + id::Any, + rate_cap::TimeProfile, + stor_cap::TimeProfile, + has_pump::Bool, + level_init::TimeProfile, + level_inflow::TimeProfile, + level_min::TimeProfile, + opex_var::TimeProfile, + opex_fixed::TimeProfile, + stor_res::ResourceCarrier, + input, + output, + Data, + ) + +Original Legacy constructor for a regulated hydropower storage, with or without pumping capabilities. +This version is discontinued starting with Version 0.6.0. resulting in an error +It is replaced with the two new types [`HydroStor`](@ref) and [`PumpedHydroStor`](@ref) +to utilize the concept of multiple dispatch instead of logic. -If you are creating a new model, it is advised to directly use the types `HydroStor` and \ -`PumpedHydroStor`. +See the *[documentation](https://energymodelsx.github.io/EnergyModelsRenewableProducers.jl/stable/how-to/update-models/#Adjustments-from-0.4.0-to-0.6.x)* +for further information regarding how you can translate your existing model to the new model. ## Fields -- **`id`** is the name/identifyer of the node.\n -- **`rate_cap::TimeProfile`**: installed capacity.\n -- **`stor_cap::TimeProfile`** Initial installed storage capacity in the dam.\n -- **`has_pump::Bool`** states wheter the stored resource can flow in.\n -- **`level_init::TimeProfile`** Initial energy stored in the dam, in units of power.\n -- **`level_inflow::TimeProfile`** Inflow of power per operational period.\n -- **`level_min::TimeProfile`** Minimum fraction of the reservoir capacity that can be left.\n -- **`opex_var::TimeProfile`** Operational cost per GWh produced.\n -- **`opex_fixed::TimeProfile`** Fixed operational costs.\n -- **`stor_res::ResourceCarrier`** is the stored `Resource`.\n -- **`input::Dict{Resource, Real}`** the stored and used resources. The \ -values in the Dict is a ratio describing the energy loss when using the pumps.\n -- **`output::Dict{Resource, Real}`** can only contain one entry, the stored resource.\n -- **`data::Array{Data}`** additional data (e.g. for investments).\n +- **`id`** is the name/identifyer of the node. +- **`rate_cap::TimeProfile`** is the installed installed rate capacity. +- **`stor_cap::TimeProfile`** is the installed storage capacity in the dam. +- **`has_pump::Bool`** states wheter the stored resource can flow in. +- **`level_init::TimeProfile`** is the initial stored energy in the dam. +- **`level_inflow::TimeProfile`** is the inflow of power per operational period. +- **`level_min::TimeProfile`** is the minimum fraction of the reservoir capacity that + has to remain in the `HydroStorage` node. +- **`opex_var::TimeProfile`** are the variable operational expenses per GWh produced. +- **`opex_fixed::TimeProfile`** are the fixed operational costs of the storage caacity. +- **`stor_res::ResourceCarrier`** is the stored `Resource`. +- **`input::Dict{Resource, Real}`** are the stored and used resources. The values in the Dict + are ratios describing the energy loss when using the pumps. +- **`output::Dict{Resource, Real}`** can only contain one entry, the stored resource. +- **`data::Array{Data}`** additional data (e.g. for investments). This value is conditional + through the application of a constructor. """ function RegHydroStor( id::Any, @@ -37,44 +56,279 @@ function RegHydroStor( output, Data, ) - @warn( - "This implementation of a `RegHydroStor` will be discontinued in the near future. \n + @error( + "This implementation of a `RegHydroStor` will be discontinued in the near future. It is replaced with the type - `PumpedHydroStor` when considering a pumped hydro storage node or - `HydroStor` for a standard regulated hydro power plant. You can find the individual fields of these types in the documentation." ) +end + +""" + HydroStor( + id::Any, + rate_cap::TimeProfile, + stor_cap::TimeProfile, + level_init::TimeProfile, + level_inflow::TimeProfile, + level_min::TimeProfile, + opex_var::TimeProfile, + opex_fixed::TimeProfile, + stor_res::ResourceCarrier, + input, + output, + Data, + ) + +Legacy constructor for a regulated hydropower plant without pumping capabilities. +This version will be discontinued in the near future and replaced with the new version of +`HydroStor{StorageBehavior}` in which the parametric input defines the behavior of the +hydropower plant. +In addition, the introduction of `AbstractStorageParameters` allows for an improved +description of the individual capacities and OPEX contributions for the storage `level` and +`discharge` capacity. + +See the *[documentation](https://energymodelsx.github.io/EnergyModelsRenewableProducers.jl/stable/how-to/update-models/#Adjustments-from-0.4.0-to-0.6.x)* +for further information regarding how you can translate your existing model to the new model. + +## Fields +- **`id`** is the name/identifyer of the node. +- **`rate_cap::TimeProfile`** is the installed installed rate capacity. +- **`stor_cap::TimeProfile`** is the installed storage capacity in the dam. +- **`level_init::TimeProfile`** is the initial stored energy in the dam. +- **`level_inflow::TimeProfile`** is the inflow of power per operational period. +- **`level_min::TimeProfile`** is the minimum fraction of the reservoir capacity that + has to remain in the `HydroStorage` node. +- **`opex_var::TimeProfile`** are the variable operational expenses per GWh produced. +- **`opex_fixed::TimeProfile`** are the fixed operational costs of the storage caacity. +- **`stor_res::ResourceCarrier`** is the stored `Resource`. +- **`input::Dict{Resource, Real}`** are the stored and used resources. The values in the Dict + are ratios describing the energy loss when using the pumps. +- **`output::Dict{Resource, Real}`** can only contain one entry, the stored resource. +- **`data::Array{Data}`** additional data (e.g. for investments). This value is conditional + through the application of a constructor. +""" +function HydroStor( + id, + rate_cap::TimeProfile, + stor_cap::TimeProfile, + + level_init::TimeProfile, + level_inflow::TimeProfile, + level_min::TimeProfile, + + opex_var::TimeProfile, + opex_fixed::TimeProfile, + stor_res::ResourceCarrier, + input::Dict{<:Resource, <:Real}, + output::Dict{<:Resource, <:Real}, +) + @warn( + "The used implementation of a `HydroStor` will be discontinued in the near " * + "future. The new implementation using the types the types `StorageBehavior` and " * + "`AbstractStorageParameters` for describing a) the cyclic behavior and b) " * + "the parameters for the `level` and `discharge` capacities.\n" * + "In practice, two changes have to be incorporated: \n 1. `HydroStor{CyclicStrategic}()` " * + "instead of `HydroStor` and \n 2. the application of `StorCapOpexFixed(stor_cap, opex_fixed)` " * + "as 2โฟแตˆ field as well as `StorCapOpexVar(rate_cap, opex_var))` as 3สณแตˆ field. " * + "2โฟแตˆ, 3สณแตˆ, 7แต—สฐ and 8แต—สฐ fields are removed.\n" * + "See the documentation (https://energymodelsx.github.io/EnergyModelsRenewableProducers.jl/stable/how-to/update-models/#Adjustments-from-0.4.0-to-0.6.x) " * + "on how to update your model to the latest version.", + maxlog = 1 + ) + + return HydroStor{CyclicStrategic}( + id, + StorCapOpexFixed(stor_cap, opex_fixed), + StorCapOpexVar(rate_cap, opex_var), + level_init, + level_inflow, + level_min, + stor_res, + input, + output, + Data[], + ) +end +function HydroStor( + id, + rate_cap::TimeProfile, + stor_cap::TimeProfile, + + level_init::TimeProfile, + level_inflow::TimeProfile, + level_min::TimeProfile, + + opex_var::TimeProfile, + opex_fixed::TimeProfile, + stor_res::ResourceCarrier, + input::Dict{<:Resource, <:Real}, + output::Dict{<:Resource, <:Real}, + data::Vector{Data}, +) + @warn( + "The used implementation of a `HydroStor` will be discontinued in the near " * + "future. The new implementation using the types the types `StorageBehavior` and " * + "`AbstractStorageParameters` for describing a) the cyclic behavior and b) " * + "the parameters for the `level` and `discharge` capacities.\n" * + "In practice, two changes have to be incorporated: \n 1. `HydroStor{CyclicStrategic}()` " * + "instead of `HydroStor` and \n 2. the application of `StorCapOpexFixed(stor_cap, opex_fixed)` " * + "as 2โฟแตˆ field as well as `StorCapOpexVar(rate_cap, opex_var))` as 3สณแตˆ field. " * + "2โฟแตˆ, 3สณแตˆ, 7แต—สฐ and 8แต—สฐ fields are removed.\n" * + "See the documentation (https://energymodelsx.github.io/EnergyModelsRenewableProducers.jl/stable/how-to/update-models/#Adjustments-from-0.4.0-to-0.6.x) " * + "on how to update your model to the latest version.", + maxlog = 1 + ) - if has_pump - return PumpedHydroStor( - id, - rate_cap, - stor_cap, - level_init, - level_inflow, - level_min, - FixedProfile(0), - opex_var, - opex_fixed, - stor_res, - input, - output, - Data, - ) - else - return HydroStor( - id, - rate_cap, - stor_cap, - level_init, - level_inflow, - level_min, - FixedProfile(0), - opex_fixed, - stor_res, - input, - output, - Data, - ) - end + return HydroStor{CyclicStrategic}( + id, + StorCapOpexFixed(stor_cap, opex_fixed), + StorCapOpexVar(rate_cap, opex_var), + level_init, + level_inflow, + level_min, + stor_res, + input, + output, + data, + ) +end + +""" + PumpedHydroStor( + id::Any, + rate_cap::TimeProfile, + stor_cap::TimeProfile, + level_init::TimeProfile, + level_inflow::TimeProfile, + level_min::TimeProfile, + opex_var::TimeProfile, + opex_fixed::TimeProfile, + stor_res::ResourceCarrier, + input, + output, + Data, + ) + +Legacy constructor for a regulated pumped hydropower storage plant. +This version will be discontinued in the near future and replaced with the new version of +`HydroStor{StorageBehavior}` in which the parametric input defines the behavior of the +hydropower plant. +In addition, the introduction of `AbstractStorageParameters` allows for an improved +description of the individual capacities and OPEX contributions for the pump capacity +(`charge`), storage `level` and `discharge` capacity. + +See the *[documentation](https://energymodelsx.github.io/EnergyModelsRenewableProducers.jl/stable/how-to/update-models/#Adjustments-from-0.4.0-to-0.6.x)* +for further information regarding how you can translate your existing model to the new model. + +## Fields +- **`id`** is the name/identifyer of the node. +- **`rate_cap::TimeProfile`** is the installed installed rate capacity. +- **`stor_cap::TimeProfile`** is the installed storage capacity in the dam. +- **`level_init::TimeProfile`** is the initial stored energy in the dam. +- **`level_inflow::TimeProfile`** is the inflow of power per operational period. +- **`level_min::TimeProfile`** is the minimum fraction of the reservoir capacity that + has to remain in the `HydroStorage` node. +- **`opex_var::TimeProfile`** are the variable operational expenses per GWh produced. +- **`opex_var_pump::TimeProfile`** are the variable operational expenses per GWh pumped + into the storage. +- **`opex_fixed::TimeProfile`** are the fixed operational costs of the storage caacity. +- **`stor_res::ResourceCarrier`** is the stored `Resource`. +- **`input::Dict{Resource, Real}`** are the stored and used resources. The values in the Dict + are ratios describing the energy loss when using the pumps. +- **`output::Dict{Resource, Real}`** can only contain one entry, the stored resource. +- **`data::Array{Data}`** additional data (e.g. for investments). This value is conditional + through the application of a constructor. +""" +function PumpedHydroStor( + id, + rate_cap::TimeProfile, + stor_cap::TimeProfile, + + level_init::TimeProfile, + level_inflow::TimeProfile, + level_min::TimeProfile, + + opex_var::TimeProfile, + opex_var_pump::TimeProfile, + opex_fixed::TimeProfile, + stor_res::ResourceCarrier, + input::Dict{<:Resource, <:Real}, + output::Dict{<:Resource, <:Real}, +) + @warn( + "The used implementation of a `PumpedHydroStor` will be discontinued in the near " * + "future. The new implementation using the types the types `StorageBehavior` and " * + "`AbstractStorageParameters` for describing a) the cyclic behavior and b) " * + "the parameters for the `level`, `charge`, and `discharge` capacities.\n" * + "In practice, two changes have to be incorporated: \n 1. `PumpedHydroStor{CyclicStrategic}()` " * + "instead of `PumpedHydroStor` and \n 2. the application of `StorCapOpexVar(rate_cap, opex_var_pump)` " * + "as 2โฟแตˆ field, `StorCapOpexFixed(stor_cap, opex_fixed)` as 3สณแตˆ field, and" * + "`StorCapOpexVar(rate_cap, opex_var))` as 4แต—สฐ field. " * + "The previous 2โฟแตˆ, 3สณแตˆ, 7แต—สฐ, 8แต—สฐ, and 9แต—สฐ fields are removed.\n" * + "See the documentation (https://energymodelsx.github.io/EnergyModelsRenewableProducers.jl/stable/how-to/update-models/#Adjustments-from-0.4.0-to-0.6.x) " * + "on how to update your model to the latest version.", + maxlog = 1 + ) + + return PumpedHydroStor{CyclicStrategic}( + id, + StorCapOpexVar(rate_cap, opex_var_pump), + StorCapOpexFixed(stor_cap, opex_fixed), + StorCapOpexVar(rate_cap, opex_var), + level_init, + level_inflow, + level_min, + stor_res, + input, + output, + Data[], + ) +end +function PumpedHydroStor( + id, + rate_cap::TimeProfile, + stor_cap::TimeProfile, + + level_init::TimeProfile, + level_inflow::TimeProfile, + level_min::TimeProfile, + + opex_var::TimeProfile, + opex_var_pump::TimeProfile, + opex_fixed::TimeProfile, + stor_res::ResourceCarrier, + input::Dict{<:Resource, <:Real}, + output::Dict{<:Resource, <:Real}, + data::Vector{Data}, +) + @warn( + "The used implementation of a `PumpedHydroStor` will be discontinued in the near " * + "future. The new implementation using the types the types `StorageBehavior` and " * + "`AbstractStorageParameters` for describing a) the cyclic behavior and b) " * + "the parameters for the `level`, `charge`, and `discharge` capacities.\n" * + "In practice, two changes have to be incorporated: \n 1. `PumpedHydroStor{CyclicStrategic}()` " * + "instead of `PumpedHydroStor` and \n 2. the application of `StorCapOpexVar(rate_cap, opex_var_pump)` " * + "as 2โฟแตˆ field, `StorCapOpexFixed(stor_cap, opex_fixed)` as 3สณแตˆ field, and" * + "`StorCapOpexVar(rate_cap, opex_var))` as 4แต—สฐ field. " * + "The previous 2โฟแตˆ, 3สณแตˆ, 7แต—สฐ, 8แต—สฐ, and 9แต—สฐ fields are removed.\n" * + "See the documentation (https://energymodelsx.github.io/EnergyModelsRenewableProducers.jl/stable/how-to/update-models/#Adjustments-from-0.4.0-to-0.6.x) " * + "on how to update your model to the latest version.", + maxlog = 1 + ) + + return PumpedHydroStor{CyclicStrategic}( + id, + StorCapOpexVar(rate_cap, opex_var_pump), + StorCapOpexFixed(stor_cap, opex_fixed), + StorCapOpexVar(rate_cap, opex_var), + level_init, + level_inflow, + level_min, + stor_res, + input, + output, + data, + ) end diff --git a/src/model.jl b/src/model.jl index 27a7383..820fe58 100644 --- a/src/model.jl +++ b/src/model.jl @@ -12,13 +12,13 @@ end # method defined for a general Source node, which is located in EnergyModelsBase. """ - EMB.variables_node(m, ๐’ฉ::Vector{HydroStorage}, ๐’ฏ, modeltype::EnergyModel) + EMB.variables_node(m, ๐’ฉ::Vector{<:HydroStorage}, ๐’ฏ, modeltype::EnergyModel) Create the optimization variable `:hydro_spill` for every HydroStorage node. This variable enables hydro storage nodes to spill water from the reservoir without producing energy. Wihtout this slack variable, parameters with too much inflow would else lead to an infeasible model. """ -function EMB.variables_node(m, ๐’ฉ::Vector{HydroStorage}, ๐’ฏ, modeltype::EnergyModel) +function EMB.variables_node(m, ๐’ฉ::Vector{<:HydroStorage}, ๐’ฏ, modeltype::EnergyModel) @variable(m, hydro_spill[๐’ฉ, ๐’ฏ] >= 0) end @@ -33,30 +33,28 @@ function EMB.create_node(m, n::HydroStorage, ๐’ฏ, ๐’ซ, modeltype::EnergyModel) p_stor = EMB.storage_resource(n) ๐’ฏแดตโฟแต› = strategic_periods(๐’ฏ) - # If the reservoir has no pump, the stored resource cannot flow in. - if isa(n, HydroStor) - @constraint(m, [t โˆˆ ๐’ฏ], m[:flow_in][n, t, p_stor] == 0) - end - # Energy balance constraints for stored electricity. constraints_level(m, n, ๐’ฏ, ๐’ซ, modeltype) + # Call of the function for the inlet flow to the `HydroStorage` node + constraints_flow_in(m, n, ๐’ฏ, modeltype) + # The flow_out is equal to the production stor_rate_use. @constraint( m, [t โˆˆ ๐’ฏ], - m[:flow_out][n, t, p_stor] == m[:stor_rate_use][n, t] * outputs(n, p_stor) + m[:flow_out][n, t, p_stor] == m[:stor_discharge_use][n, t] * outputs(n, p_stor) ) # Can not produce more energy than what is availbable in the reservoir. - @constraint(m, [t โˆˆ ๐’ฏ], m[:stor_rate_use][n, t] <= m[:stor_level][n, t]) + @constraint(m, [t โˆˆ ๐’ฏ], m[:stor_discharge_use][n, t] <= m[:stor_level][n, t]) # The minimum contents of the reservoir is bounded below. Not allowed # to drain it completely. @constraint( m, [t โˆˆ ๐’ฏ], - m[:stor_level][n, t] โ‰ฅ level_min(n, t) * m[:stor_cap_inst][n, t] + m[:stor_level][n, t] โ‰ฅ level_min(n, t) * m[:stor_level_inst][n, t] ) # Iterate through all data and set up the constraints corresponding to the data diff --git a/test/runtests.jl b/test/runtests.jl index 75259f4..2ef87f7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -13,4 +13,5 @@ const EMRP = EnergyModelsRenewableProducers include("test_nondisres.jl") include("test_hydro.jl") include("test_examples.jl") + include("test_constructors.jl") end diff --git a/test/test_constructors.jl b/test/test_constructors.jl new file mode 100644 index 0000000..9349e27 --- /dev/null +++ b/test/test_constructors.jl @@ -0,0 +1,318 @@ +@testset "Legacy constructors" begin + + # Used parameters + rate_cap = FixedProfile(2.0) + stor_cap = FixedProfile(40) + + level_init = StrategicProfile([20, 25, 30, 20]) + level_inflow = FixedProfile(10) + level_min = StrategicProfile([0.1, 0.2, 0.05, 0.1]) + + opex_var = FixedProfile(10) + opex_var_pump = FixedProfile(10) + opex_fixed = FixedProfile(10) + + stor_res = Power + input = Dict(Power => 0.9) + output = Dict(Power => 1) + + data = Data[] + + # Check that `Hydrostor` nodes are correctly constructed + @testset "Hydrostor node wo data" begin + hydro_old = HydroStor( + "hydro", + rate_cap, + stor_cap, + level_init, + level_inflow, + level_min, + opex_var, + opex_fixed, + stor_res, + input, + output, + ) + + hydro_new = HydroStor{CyclicStrategic}( + "hydro", + StorCapOpexFixed(stor_cap, opex_fixed), + StorCapOpexVar(rate_cap, opex_var), + level_init, + level_inflow, + level_min, + stor_res, + input, + output, + Data[], + ) + for field โˆˆ fieldnames(HydroStor{CyclicStrategic}) + @test getproperty(hydro_old, field) == getproperty(hydro_new, field) + end + end + @testset "Hydrostor node with data" begin + hydro_old = HydroStor( + "hydro", + rate_cap, + stor_cap, + level_init, + level_inflow, + level_min, + opex_var, + opex_fixed, + stor_res, + input, + output, + data, + ) + + hydro_new = HydroStor{CyclicStrategic}( + "hydro", + StorCapOpexFixed(stor_cap, opex_fixed), + StorCapOpexVar(rate_cap, opex_var), + level_init, + level_inflow, + level_min, + stor_res, + input, + output, + data, + ) + for field โˆˆ fieldnames(HydroStor{CyclicStrategic}) + @test getproperty(hydro_old, field) == getproperty(hydro_new, field) + end + end + + + # Check that `PumpedHydroStor` nodes are correctly constructed + @testset "PumpedHydroStor node wo data" begin + hydro_old = PumpedHydroStor( + "hydro", + rate_cap, + stor_cap, + level_init, + level_inflow, + level_min, + opex_var, + opex_var_pump, + opex_fixed, + stor_res, + input, + output, + ) + + hydro_new = PumpedHydroStor{CyclicStrategic}( + "hydro", + StorCapOpexVar(rate_cap, opex_var_pump), + StorCapOpexFixed(stor_cap, opex_fixed), + StorCapOpexVar(rate_cap, opex_var), + level_init, + level_inflow, + level_min, + stor_res, + input, + output, + Data[], + ) + for field โˆˆ fieldnames(PumpedHydroStor{CyclicStrategic}) + @test getproperty(hydro_old, field) == getproperty(hydro_new, field) + end + end + @testset "PumpedHydroStor node with data" begin + hydro_old = PumpedHydroStor( + "hydro", + rate_cap, + stor_cap, + level_init, + level_inflow, + level_min, + opex_var, + opex_var_pump, + opex_fixed, + stor_res, + input, + output, + data, + ) + + hydro_new = PumpedHydroStor{CyclicStrategic}( + "hydro", + StorCapOpexVar(rate_cap, opex_var_pump), + StorCapOpexFixed(stor_cap, opex_fixed), + StorCapOpexVar(rate_cap, opex_var), + level_init, + level_inflow, + level_min, + stor_res, + input, + output, + data, + ) + for field โˆˆ fieldnames(PumpedHydroStor{CyclicStrategic}) + @test getproperty(hydro_old, field) == getproperty(hydro_new, field) + end + end +end + +@testset "Simplified constructors" begin + + # Used parameters + charge = StorCapOpex(FixedProfile(5), FixedProfile(1), FixedProfile(50)) + level = StorCapOpexFixed(FixedProfile(100), FixedProfile(10)) + discharge = StorCapOpexVar(FixedProfile(2), FixedProfile(1)) + + level_init = StrategicProfile([20, 25, 30, 20]) + level_inflow = FixedProfile(10) + level_min = StrategicProfile([0.1, 0.2, 0.05, 0.1]) + + opex_var = FixedProfile(10) + opex_var_pump = FixedProfile(10) + opex_fixed = FixedProfile(10) + + stor_res = Power + input = Dict(Power => 0.9) + output = Dict(Power => 1) + + data = Data[] + + # Check that `Hydrostor` nodes are correctly constructed when using simplified + # constructors + @testset "Hydrostor node wo data" begin + hydro_old = HydroStor{CyclicStrategic}( + "hydro", + level, + discharge, + level_init, + level_inflow, + level_min, + stor_res, + input, + output, + ) + + hydro_new = HydroStor{CyclicStrategic}( + "hydro", + level, + discharge, + level_init, + level_inflow, + level_min, + stor_res, + input, + output, + data, + ) + for field โˆˆ fieldnames(HydroStor{CyclicStrategic}) + @test getproperty(hydro_old, field) == getproperty(hydro_new, field) + end + end + @testset "Hydrostor node wo input" begin + hydro_old = HydroStor{CyclicStrategic}( + "hydro", + level, + discharge, + level_init, + level_inflow, + level_min, + stor_res, + output, + data, + ) + + hydro_new = HydroStor{CyclicStrategic}( + "hydro", + level, + discharge, + level_init, + level_inflow, + level_min, + stor_res, + Dict{Resource,Real}(stor_res => 1), + output, + data, + ) + for field โˆˆ fieldnames(HydroStor{CyclicStrategic}) + @test getproperty(hydro_old, field) == getproperty(hydro_new, field) + end + + end + @testset "Hydrostor node wo data and input" begin + hydro_old = HydroStor{CyclicStrategic}( + "hydro", + level, + discharge, + level_init, + level_inflow, + level_min, + stor_res, + output, + ) + + hydro_new = HydroStor{CyclicStrategic}( + "hydro", + level, + discharge, + level_init, + level_inflow, + level_min, + stor_res, + Dict{Resource,Real}(stor_res => 1), + output, + Data[], + ) + for field โˆˆ fieldnames(HydroStor{CyclicStrategic}) + @test getproperty(hydro_old, field) == getproperty(hydro_new, field) + end + + # Test that an empty input results in a running model + case, modeltype = small_graph() + + # Updating the nodes and the links + push!(case[:nodes], hydro_new) + link_from = EMB.Direct(41, case[:nodes][4], case[:nodes][1], EMB.Linear()) + push!(case[:links], link_from) + link_to = EMB.Direct(14, case[:nodes][1], case[:nodes][4], EMB.Linear()) + push!(case[:links], link_to) + + # Run the model + m = EMB.run_model(case, modeltype, OPTIMIZER) + + # Run of the general and node tests + general_tests(m) + general_node_tests(m, case, hydro_new) + end + + # Check that `PumpedHydroStor` nodes are correctly constructed when using simplified + # constructors + @testset "PumpedHydroStor node wo data" begin + hydro_old = PumpedHydroStor{CyclicStrategic}( + "hydro", + charge, + level, + discharge, + level_init, + level_inflow, + level_min, + stor_res, + input, + output, + ) + + hydro_new = PumpedHydroStor{CyclicStrategic}( + "hydro", + charge, + level, + discharge, + level_init, + level_inflow, + level_min, + stor_res, + input, + output, + data, + ) + for field โˆˆ fieldnames(HydroStor{CyclicStrategic}) + @test getproperty(hydro_old, field) == getproperty(hydro_new, field) + end + end +end diff --git a/test/test_hydro.jl b/test/test_hydro.jl index 7b64bb7..207a11b 100644 --- a/test/test_hydro.jl +++ b/test/test_hydro.jl @@ -4,20 +4,19 @@ function general_node_tests(m, case, n::EMRP.HydroStorage) # Extract time structure and storage node ๐’ฏ = case[:T] p_stor = EMB.storage_resource(n) - cap = EMB.capacity(n) @testset "stor_level bounds" begin # The storage level has to be greater than the required minimum. @test sum( - EMRP.level_min(n, t) * value.(m[:stor_cap_inst][n, t]) <= + EMRP.level_min(n, t) * value.(m[:stor_level_inst][n, t]) <= round(value.(m[:stor_level][n, t]), digits = ROUND_DIGITS) for t โˆˆ ๐’ฏ ) == length(๐’ฏ) - # The stor_level has to be less than stor_cap_inst in all operational periods. + # The stor_level has to be less than stor_level_inst in all operational periods. @test sum( - value.(m[:stor_level][n, t]) <= value.(m[:stor_cap_inst][n, t]) for t โˆˆ ๐’ฏ + value.(m[:stor_level][n, t]) <= value.(m[:stor_level_inst][n, t]) for t โˆˆ ๐’ฏ ) == length(๐’ฏ) - # TODO valing Storage node har negativ stor_cap_inst et par steder. + # TODO valing Storage node har negativ stor_level_inst et par steder. # TODO this is ok when inflow=1. When inflow=10 the stor_level gets too large. Why? # - Do we need some other sink in the system? Not logical to be left with too much power. @@ -26,7 +25,7 @@ function general_node_tests(m, case, n::EMRP.HydroStorage) @test sum( value.(value.(m[:stor_level_ฮ”_op][n, t])) โ‰ˆ EMRP.level_inflow(n, t) + inputs(n, p_stor) * value.(m[:flow_in][n, t, p_stor]) - - value.(m[:stor_rate_use][n, t]) - value.(m[:hydro_spill][n, t]) for t โˆˆ ๐’ฏ, + value.(m[:stor_discharge_use][n, t]) - value.(m[:hydro_spill][n, t]) for t โˆˆ ๐’ฏ, atol โˆˆ TEST_ATOL ) โ‰ˆ length(๐’ฏ) atol = TEST_ATOL @@ -38,12 +37,12 @@ function general_node_tests(m, case, n::EMRP.HydroStorage) duration(first(t_inv)) * ( EMRP.level_inflow(n, first(t_inv)) + value.(m[:flow_in][n, first(t_inv), p_stor]) - - value.(m[:stor_rate_use][n, first(t_inv)]) - + value.(m[:stor_discharge_use][n, first(t_inv)]) - value.(m[:hydro_spill][n, first(t_inv)]) ) for t_inv โˆˆ strategic_periods(๐’ฏ) ) == length(strategic_periods(๐’ฏ)) - # Check that stor_level is correct wrt. previous stor_level, inflow and stor_rate_use. + # Check that stor_level is correct wrt. previous stor_level, inflow and stor_discharge_use. if ๐’ฏ isa TwoLevel{T,T,U} where {T,U<:SimpleTimes} non_first = ๐’ฏ.len else @@ -55,42 +54,42 @@ function general_node_tests(m, case, n::EMRP.HydroStorage) duration(t) * ( EMRP.level_inflow(n, t) + inputs(n, p_stor) * value.(m[:flow_in][n, t, p_stor]) - - value.(m[:stor_rate_use][n, t]) - value.(m[:hydro_spill][n, t]) + value.(m[:stor_discharge_use][n, t]) - value.(m[:hydro_spill][n, t]) ) for t_inv โˆˆ strategic_periods(๐’ฏ) for (t_prev, t) โˆˆ withprev(t_inv) if !isnothing(t_prev) ) == length(๐’ฏ) - non_first end - @testset "stor_cap_inst bounds" begin - # Assure that the stor_cap_inst variable is non-negative. - @test sum(value.(m[:stor_cap_inst][n, t]) >= 0 for t โˆˆ ๐’ฏ) == length(๐’ฏ) + @testset "stor_level_inst bounds" begin + # Assure that the stor_level_inst variable is non-negative. + @test sum(value.(m[:stor_level_inst][n, t]) >= 0 for t โˆˆ ๐’ฏ) == length(๐’ฏ) - # Check that stor_cap_inst is set to cap.level. - @test sum(value.(m[:stor_cap_inst][n, t]) == cap.level[t] for t โˆˆ ๐’ฏ) == length(๐’ฏ) + # Check that stor_level_inst is set to cap.level. + @test sum(value.(m[:stor_level_inst][n, t]) == capacity(level(n), t) for t โˆˆ ๐’ฏ) == length(๐’ฏ) end - @testset "stor_rate_use bounds" begin + @testset "stor_discharge_use bounds" begin # Cannot produce more than what is stored in the reservoir. @test sum( - value.(m[:stor_rate_use][n, t]) <= value.(m[:stor_level][n, t]) for t โˆˆ ๐’ฏ + value.(m[:stor_discharge_use][n, t]) <= value.(m[:stor_level][n, t]) for t โˆˆ ๐’ฏ ) == length(๐’ฏ) - # Check that stor_rate_use is bounded above by stor_rate_inst. + # Check that stor_discharge_use is bounded above by stor_discharge_inst. @test sum( - round(value.(m[:stor_rate_use][n, t]), digits = ROUND_DIGITS) <= - value.(m[:stor_rate_inst][n, t]) for t โˆˆ ๐’ฏ + round(value.(m[:stor_discharge_use][n, t]), digits = ROUND_DIGITS) <= + value.(m[:stor_discharge_inst][n, t]) for t โˆˆ ๐’ฏ ) == length(๐’ฏ) end - @testset "stor_rate_inst" begin - @test sum(value.(m[:stor_rate_inst][n, t]) == cap.rate[t] for t โˆˆ ๐’ฏ) == length(๐’ฏ) + @testset "stor_discharge_inst" begin + @test sum(value.(m[:stor_discharge_inst][n, t]) == capacity(discharge(n), t) for t โˆˆ ๐’ฏ) == length(๐’ฏ) end @testset "flow variables" begin - # The flow_out corresponds to the production stor_rate_use. + # The flow_out corresponds to the production stor_discharge_use. @test sum( value.(m[:flow_out][n, t, p_stor]) == - value.(m[:stor_rate_use][n, t]) * outputs(n, Power) for t โˆˆ ๐’ฏ + value.(m[:stor_discharge_use][n, t]) * outputs(n, Power) for t โˆˆ ๐’ฏ ) == length(๐’ฏ) end end @@ -113,35 +112,31 @@ function check_node(nodetype::Type{<:EMRP.HydroStorage}) ) if hydro <: HydroStor - hydro = HydroStor( + hydro = HydroStor{CyclicStrategic}( "-hydro", - rate_cap, - stor_cap, + StorCapOpexFixed(stor_cap, opex_fixed), + StorCapOpexVar(rate_cap, opex_var), level_init, level_inflow, level_min, - opex_var, - opex_fixed, stor_res, input, output, ) elseif hydro <: PumpedHydroStor - hydro = HydroStor( + hydro = PumpedHydroStor{CyclicStrategic}( "-hydro", - rate_cap, - stor_cap, + StorCapOpexVar(rate_cap, opex_var_pump), + StorCapOpexFixed(stor_cap, opex_fixed), + StorCapOpexVar(rate_cap, opex_var), level_init, level_inflow, level_min, - opex_var, - opex_var_pump, - opex_fixed, stor_res, input, output, - ) + ) end case, modeltype = small_graph() @@ -161,45 +156,46 @@ function check_node(nodetype::Type{<:EMRP.HydroStorage}) # Set the global to true to suppress the error message EMB.TEST_ENV = true + check_graph(nodetype) # Test that a wrong capacity is caught by the checks. rate_cap = FixedProfile(-2.0) - @test_throws AssertionError check_graph(HydroStor; rate_cap) + @test_throws AssertionError check_graph(nodetype; rate_cap) stor_cap = FixedProfile(-40) - @test_throws AssertionError check_graph(HydroStor; stor_cap) + @test_throws AssertionError check_graph(nodetype; stor_cap) # Test that a wrong fixed OPEX is caught by the checks. opex_fixed = FixedProfile(-10) - @test_throws AssertionError check_graph(HydroStor; opex_fixed) + @test_throws AssertionError check_graph(nodetype; opex_fixed) # Test that a wrong output dictionary is caught by the checks. output = Dict(Power => 1, CO2 => 0.5) - @test_throws AssertionError check_graph(HydroStor; output) + @test_throws AssertionError check_graph(nodetype; output) output = Dict(Power => 1.5) - @test_throws AssertionError check_graph(HydroStor; output) + @test_throws AssertionError check_graph(nodetype; output) output = Dict(Power => -1.0) - @test_throws AssertionError check_graph(HydroStor; output) + @test_throws AssertionError check_graph(nodetype; output) # Test that a wrong input dictionary is caught by the checks. input = Dict(Power => 1.5) - @test_throws AssertionError check_graph(HydroStor; input) + @test_throws AssertionError check_graph(nodetype; input) input = Dict(Power => -0.9) - @test_throws AssertionError check_graph(HydroStor; input) + @test_throws AssertionError check_graph(nodetype; input) # Test that a wrong initial level is caught by the checks. level_init = StrategicProfile([50, 25, 45, 20]) - @test_throws AssertionError check_graph(HydroStor; level_init) + @test_throws AssertionError check_graph(nodetype; level_init) level_init = StrategicProfile([40, 25, 1, 20]) level_min = FixedProfile(.5) - @test_throws AssertionError check_graph(HydroStor; level_init, level_min) + @test_throws AssertionError check_graph(nodetype; level_init, level_min) level_init = StrategicProfile([40, 25, -5, 20]) - @test_throws AssertionError check_graph(HydroStor; level_init) + @test_throws AssertionError check_graph(nodetype; level_init) # Test that a wrong minimum level is caught by the checks. level_min = FixedProfile(-0.5) - @test_throws AssertionError check_graph(HydroStor; level_min) + @test_throws AssertionError check_graph(nodetype; level_min) level_min = FixedProfile(2) - @test_throws AssertionError check_graph(HydroStor; level_min) + @test_throws AssertionError check_graph(nodetype; level_min) # Set the global again to false EMB.TEST_ENV = false @@ -210,7 +206,7 @@ end @testset "HydroStor - regulated hydro power plant" begin # Test that the fields of a HydroStor are correctly checked - # - check_node(n::HydroStor, ๐’ฏ, modeltype::EnergyModel) + # - check_node(n::HydroStorage, ๐’ฏ, modeltype::EnergyModel) check_node(HydroStor) # Creation of the initial problem and the HydroStor node @@ -219,15 +215,13 @@ end min_level = StrategicProfile([0.1, 0.2, 0.05, 0.1]) # Regular nice hydro storage node. - hydro1 = HydroStor( + hydro1 = HydroStor{CyclicStrategic}( "-hydro", - FixedProfile(2.0), - max_storage, + StorCapOpexFixed(max_storage, FixedProfile(10)), + StorCapOpexVar(FixedProfile(2.0), FixedProfile(10)), initial_reservoir, FixedProfile(1), min_level, - FixedProfile(10), - FixedProfile(10), Power, Dict(Power => 0.9), Dict(Power => 1), @@ -235,15 +229,13 @@ end # Gives infeasible model without spill-variable (because without spill, the inflow is # much greater than what the Rate_cap can handle, given the Stor_cap of the storage). - hydro2 = HydroStor( + hydro2 = HydroStor{CyclicStrategic}( "-hydro", - FixedProfile(2.0), - FixedProfile(40), + StorCapOpexFixed(FixedProfile(40), FixedProfile(10)), + StorCapOpexVar(FixedProfile(2.0), FixedProfile(10)), initial_reservoir, FixedProfile(10), min_level, - FixedProfile(10), - FixedProfile(10), Power, Dict(Power => 0.9), Dict(Power => 1), @@ -269,31 +261,8 @@ end general_tests(m) general_node_tests(m, case, hydro) - @testset "no pump" begin - # No pump means no inflow. - @test sum( - value.(m[:flow_in][hydro, t, p]) == 0 for t โˆˆ ๐’ฏ for p โˆˆ inputs(hydro) - ) == length(๐’ฏ) - end - - @testset "flow_in" begin - # Check that the zero equality constraint is set on the flow_in variable - # when the pump is not allowed. If this false, there might be errors in - # the links to the node. The hydro node need one in and one out. - logic_1 = sum( - sum( - occursin("flow_in[n_-hydro,$t,Power] = 0", string(constraint)) for - constraint โˆˆ all_constraints(m, AffExpr, MOI.EqualTo{Float64}) - ) == 1 for t โˆˆ ๐’ฏ - ) == length(๐’ฏ) - logic_2 = sum( - sum( - occursin("flow_in[n_-hydro,$t,Power] == 0", string(constraint)) for - constraint โˆˆ all_constraints(m, AffExpr, MOI.EqualTo{Float64}) - ) == 1 for t โˆˆ ๐’ฏ - ) == length(๐’ฏ) - @test logic_1 || logic_2 - end + # Check that the input flow is fixed to 0 for Power + @test sum(is_fixed(m[:flow_in][hydro, t, Power]) for t โˆˆ ๐’ฏ) == length(๐’ฏ) if hydro == hydro2 # hydro2 should lead to spillage. @@ -330,6 +299,9 @@ end general_tests(m) general_node_tests(m, case, hydro1) + # Test the objective value + @test objective_value(m) โ‰ˆ -116160.0 + # All the tests following er for the function # - constraints_level(m, n::HydroStorage, ๐’ฏ, ๐’ซ, modeltype::EnergyModel) for t_inv โˆˆ ๐’ฏแดตโฟแต› @@ -356,7 +328,7 @@ end @test value.(m[:stor_level][n, t]) - value.(m[:stor_level_ฮ”_op][n, t]) * duration(t) โ‰ค - value.(m[:stor_cap_inst][n, t]) + TEST_ATOL + value.(m[:stor_level_inst][n, t]) + TEST_ATOL elseif isnothing(t_prev) # Test for the correct accounting in the first operational period of the @@ -378,7 +350,7 @@ end @test value.(m[:stor_level][n, t]) - value.(m[:stor_level_ฮ”_op][n, t]) * duration(t) โ‰ค - value.(m[:stor_cap_inst][n, t]) + TEST_ATOL + value.(m[:stor_level_inst][n, t]) + TEST_ATOL end end end @@ -387,17 +359,15 @@ end @testset "PumpedHydroStor - regulated hydro storage with pumped storage" begin - # Test that the fields of a HydroStor are correctly checked - # - check_node(n::HydroStor, ๐’ฏ, modeltype::EnergyModel) + # Test that the fields of a PumpedHydroStor are correctly checked + # - check_node(n::HydroStorage, ๐’ฏ, modeltype::EnergyModel) check_node(PumpedHydroStor) # Creation of the initial problem and the PumpedHydroStor node with a pump. products = [Power, CO2] source = EMB.RefSource( "-source", - OperationalProfile([ - 10 10 10 10 10 0 0 0 0 0 - ]), + OperationalProfile([10, 10, 10, 10, 10, 0, 0, 0, 0, 0]), FixedProfile(10), FixedProfile(10), Dict(Power => 1), @@ -406,7 +376,7 @@ end sink = EMB.RefSink( "-sink", FixedProfile(7), - Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e2)), Dict(Power => 1), ) @@ -415,16 +385,14 @@ end max_storage = FixedProfile(100) initial_reservoir = StrategicProfile([20, 25]) min_level = StrategicProfile([0.1, 0.2]) - hydro = EMRP.PumpedHydroStor( + hydro = PumpedHydroStor{CyclicStrategic}( "-hydro", - FixedProfile(10.0), - max_storage, + StorCapOpexVar(FixedProfile(10.0), FixedProfile(30)), + StorCapOpexFixed(max_storage, FixedProfile(10)), + StorCapOpexVar(FixedProfile(10.0), FixedProfile(5)), initial_reservoir, FixedProfile(1), min_level, - FixedProfile(0), - FixedProfile(30), - FixedProfile(10), Power, Dict(Power => 1), Dict(Power => 0.9), @@ -437,7 +405,7 @@ end link_to = EMB.Direct(14, case[:nodes][1], case[:nodes][4], EMB.Linear()) push!(case[:links], link_to) - case[:T] = TwoLevel(2, 1, SimpleTimes(10, 1)) + case[:T] = TwoLevel(2, 1, SimpleTimes(10, 1); op_per_strat=10) # Run the model m = EMB.run_model(case, modeltype, OPTIMIZER; check_timeprofiles=false) @@ -449,24 +417,12 @@ end general_tests(m) general_node_tests(m, case, hydro) - @testset "flow_in" begin - # Check that the zero equality constraint is not set on the flow_in variable - # when the pump is allowed. If this fails, there might be errors in the links - # to the node. The hydro node need one in and one out. - logic_1 = sum( - sum( - occursin("flow_in[n_-hydro,$t,Power] = 0", string(constraint)) for - constraint โˆˆ all_constraints(m, AffExpr, MOI.EqualTo{Float64}) - ) == 0 for t โˆˆ ๐’ฏ - ) == length(๐’ฏ) - logic_2 = sum( - sum( - occursin("flow_in[n_-hydro,$t,Power] == 0", string(constraint)) for - constraint โˆˆ all_constraints(m, AffExpr, MOI.EqualTo{Float64}) - ) == 0 for t โˆˆ ๐’ฏ - ) == length(๐’ฏ) - @test logic_1 || logic_2 - end + # Test the objective value + # -25 in v0.6 compared to 0.5 as opex_var now via stor_discharge_use instead of flow_out + @test objective_value(m) โ‰ˆ -6850.0 + + # Check that the input flow is not fixed to 0 for Power + @test sum(is_fixed(m[:flow_in][hydro, t, Power]) for t โˆˆ ๐’ฏ) == 0 @testset "deficit" begin if sum(value.(m[:sink_deficit][sink, t]) for t โˆˆ ๐’ฏ) > 0 diff --git a/test/utils.jl b/test/utils.jl index 3085a7e..f0a98e1 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -13,7 +13,7 @@ function small_graph(; source = nothing, sink = nothing, ops = SimpleTimes(24, 2 if isnothing(source) source = RefSource( 2, - FixedProfile(1), + FixedProfile(20), FixedProfile(30), FixedProfile(10), Dict(Power => 1),