diff --git a/NEWS.md b/NEWS.md index 5c0b51f..d485c4e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,6 @@ # Release notes -## Unversioned +## Version 0.5.4 (2024-03-04) ### Examples @@ -9,12 +9,17 @@ ### NonDIsRes node -* Moved the capcity constraints through the profile to the function `constraints_capacity(n::NonDisRES, ...)`, and hence, removed the function `EMB.create_node(n::NonDisRES, ...)`. +* Moved the capacity constraints through the profile to the function `EMB.constraints_capacity(n::NonDisRES, ...)`, and hence, removed the function `EMB.create_node(n::NonDisRES, ...)`. + +### Minor updates + +* Added some checks and tests to the checks. +* Restructured the test folder. ## Version 0.5.3 (2024-01-30) -* Updated the restrictions on the fields individual types to be consistent. -* Added option to not include the field `data` for the individual `TransmissionMode`s. +* Updated the restrictions on the fields of individual types to be consistent. +* Added option to not include the field `data` for the individual introduced `Node`s. ## Version 0.5.2 (2024-01-19) diff --git a/Project.toml b/Project.toml index bbdd34b..b030add 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.3" +version = "0.5.4" [deps] EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" diff --git a/src/checks.jl b/src/checks.jl index 6b1296e..eb1095a 100644 --- a/src/checks.jl +++ b/src/checks.jl @@ -4,11 +4,37 @@ This method checks that the *[`NonDisRES`](@ref NonDisRES_public)* node is valid. ## Checks - - The field `profile` is required to be in the range ``[0, 1]`` for all time steps ``t โˆˆ \\mathcal{T}``. + - The field `cap` is required to be non-negative (similar to the `Source` check). + - The field `opex_fixed` is required to be non-negative (similar to the `Source` check). + - The values of the dictionary `output` are required to be non-negative \ + (similar to the `Source` check). + - The field `profile` is required to be in the range ``[0, 1]`` for all time steps \ + ``t โˆˆ \\mathcal{T}``. """ function EMB.check_node(n::NonDisRES, ๐’ฏ, modeltype::EMB.EnergyModel) - @assert_or_log sum(profile(n, t) โ‰ค 1 for t โˆˆ ๐’ฏ) == length(๐’ฏ) "The profile field must be less or equal to 1." - @assert_or_log sum(profile(n, t) โ‰ฅ 0 for t โˆˆ ๐’ฏ) == length(๐’ฏ) "The profile field must be non-negative." + + ๐’ฏแดตโฟแต› = strategic_periods(๐’ฏ) + + @assert_or_log( + sum(capacity(n, t) โ‰ฅ 0 for t โˆˆ ๐’ฏ) == length(๐’ฏ), + "The capacity must be non-negative." + ) + @assert_or_log( + sum(opex_fixed(n, t_inv) โ‰ฅ 0 for t_inv โˆˆ ๐’ฏแดตโฟแต›) == length(๐’ฏแดตโฟแต›), + "The fixed OPEX must be non-negative." + ) + @assert_or_log( + sum(outputs(n, p) โ‰ฅ 0 for p โˆˆ outputs(n)) == length(outputs(n)), + "The values for the Dictionary `output` must be non-negative." + ) + @assert_or_log( + sum(profile(n, t) โ‰ค 1 for t โˆˆ ๐’ฏ) == length(๐’ฏ), + "The profile field must be less or equal to 1." + ) + @assert_or_log( + sum(profile(n, t) โ‰ฅ 0 for t โˆˆ ๐’ฏ) == length(๐’ฏ), + "The profile field must be non-negative." + ) end """ @@ -17,50 +43,86 @@ end This method checks that the *[`HydroStorage`](@ref HydroStorage_public)* node is valid. ## Checks - - The field `n.output` can only include a single `Resource`.\n + - The value of the field `rate_cap` is required to be non-negative.\n + - The value of the field `stor_cap` is required to be non-negative.\n + - The value of the field `fixed_opex` is required to be non-negative.\n + - The field `output` can only include a single `Resource`.\n - The value of the field `output` is required to be smaller or equal to 1.\n - The value of the field `input` is required to be in the range ``[0, 1]``.\n - The value of the field `level_init` is required to be in the range \ ``[level\\_min, 1] \\cdot stor\\_cap(t)`` for all time steps ``t โˆˆ \\mathcal{T}``.\n - The value of the field `level_init` is required to be in the range ``[0, 1]``.\n - - The value of the field `rate_cap` is required to be non-negative.\n - The value of the field `level_min` is required to be in the range ``[0, 1]``. """ function EMB.check_node(n::HydroStorage, ๐’ฏ, modeltype::EMB.EnergyModel) - @assert_or_log length(outputs(n)) == 1 "Only one resource can be stored, so only this one can flow out." + + ๐’ฏแดตโฟแต› = strategic_periods(๐’ฏ) cap = capacity(n) + @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." + ) + @assert_or_log( + sum(opex_fixed(n, t_inv) >= 0 for t_inv โˆˆ ๐’ฏแดตโฟแต›) == length(๐’ฏแดตโฟแต›), + "The fixed OPEX must be non-negative." + ) + @assert_or_log( + length(outputs(n)) == 1, + "Only one resource can be stored, so only this one can flow out." + ) + for v โˆˆ values(n.output) - @assert_or_log v <= 1 "The value of the stored resource in n.output has to be less than or equal to 1." + @assert_or_log( + v โ‰ค 1, + "The value of the `output` resource has to be less than or equal to 1." + ) + @assert_or_log( + v โ‰ฅ 0, + "The value of the `output` resource has to be non-negative." + ) end for v โˆˆ values(n.input) - @assert_or_log v <= 1 "The values of the input variables has to be less than or equal to 1." - @assert_or_log v >= 0 "The values of the input variables has to be non-negative." + @assert_or_log( + v โ‰ค 1, + "The values of the input variables have to be less than or equal to 1." + ) + @assert_or_log( + v โ‰ฅ 0, + "The values of the input variables have to be non-negative." + ) end - @assert_or_log sum(level_init(n, t) <= cap.level[t] for t โˆˆ ๐’ฏ) == length(๐’ฏ) "The initial reservoir has to be less or equal to the max storage capacity." - - for t_inv โˆˆ strategic_periods(๐’ฏ) - for t โˆˆ t_inv - @assert_or_log level_init(n, t_inv) <= cap.level[t] "The initial level can not be greater than the dam capacity (" * - string(t) * - ")." - end + @assert_or_log( + sum(level_init(n, t) โ‰ค cap.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] "The reservoir can't be underfilled from the start (" * - string(t) * - ")." + @assert_or_log( + level_init(n, t_inv) + level_inflow(n, t) โ‰ฅ level_min(n, t) * cap.level[t], + "The reservoir can't be underfilled from the start (" * string(t) * ").") end - @assert_or_log sum(level_init(n, t) < 0 for t โˆˆ ๐’ฏ) == 0 "The level_init can not be negative." - - @assert_or_log sum(cap.rate[t] < 0 for t โˆˆ ๐’ฏ) == 0 "The production capacity n.rate_cap has to be non-negative." + @assert_or_log( + sum(level_init(n, t) < 0 for t โˆˆ ๐’ฏ) == 0, + "The field `level_init` can not be negative." + ) # level_min - @assert_or_log sum(level_min(n, t) < 0 for t โˆˆ ๐’ฏ) == 0 "The level_min can not be negative." - @assert_or_log sum(level_min(n, t) > 1 for t โˆˆ ๐’ฏ) == 0 "The level_min can not be larger than 1." + @assert_or_log( + sum(level_min(n, t) < 0 for t โˆˆ ๐’ฏ) == 0, + "The field `level_min` can not be negative." + ) + @assert_or_log( + sum(level_min(n, t) > 1 for t โˆˆ ๐’ฏ) == 0, + "The field `level_min` can not be larger than 1." + ) end diff --git a/test/runtests.jl b/test/runtests.jl index 794f1b1..75259f4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,66 +8,8 @@ using TimeStruct const EMB = EnergyModelsBase const EMRP = EnergyModelsRenewableProducers -CO2 = ResourceEmit("CO2", 1.0) -Power = ResourceCarrier("Power", 0.0) - -TEST_ATOL = 1e-6 -ROUND_DIGITS = 8 -OPTIMIZER = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) - -function small_graph(source = nothing, sink = nothing; ops = SimpleTimes(24, 2)) - - products = [Power, CO2] - # Creation of the source and sink module as well as the arrays used for nodes and links - if isnothing(source) - source = RefSource( - 2, - FixedProfile(1), - FixedProfile(30), - FixedProfile(10), - Dict(Power => 1), - ) - end - if isnothing(sink) - sink = RefSink( - 3, - FixedProfile(20), - Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), - Dict(Power => 1), - ) - end - - nodes = [GenAvailability(1, products), source, sink] - links = [ - Direct(21, nodes[2], nodes[1], Linear()) - Direct(13, nodes[1], nodes[3], Linear()) - ] - - # Creation of the time structure and the used global data - T = TwoLevel(4, 1, ops) - modeltype = OperationalModel( - Dict(CO2 => StrategicProfile([450, 400, 350, 300])), - Dict(CO2 => FixedProfile(0)), - CO2, - ) - - # Creation of the case dictionary - case = Dict(:nodes => nodes, :links => links, :products => products, :T => T) - return case, modeltype -end - -function general_tests(m) - # Check if the solution is optimal. - @testset "optimal solution" begin - @test termination_status(m) == MOI.OPTIMAL - - if termination_status(m) != MOI.OPTIMAL - @show termination_status(m) - end - end -end - @testset "RenewableProducers" begin + include("utils.jl") include("test_nondisres.jl") include("test_hydro.jl") include("test_examples.jl") diff --git a/test/test_hydro.jl b/test/test_hydro.jl index e7e64e7..d616702 100644 --- a/test/test_hydro.jl +++ b/test/test_hydro.jl @@ -95,8 +95,124 @@ function general_node_tests(m, case, n::EMRP.HydroStorage) end end +function check_node(nodetype::Type{<:EMRP.HydroStorage}) + + function check_graph( + hydro::Type{<:EMRP.HydroStorage}; + 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), + ) + + if hydro <: HydroStor + hydro = HydroStor( + "-hydro", + rate_cap, + stor_cap, + level_init, + level_inflow, + level_min, + opex_var, + opex_fixed, + stor_res, + input, + output, + ) + + elseif hydro <: PumpedHydroStor + hydro = HydroStor( + "-hydro", + rate_cap, + stor_cap, + level_init, + level_inflow, + level_min, + opex_var, + opex_var_pump, + opex_fixed, + stor_res, + input, + output, + ) + end + + case, modeltype = small_graph() + + # Updating the nodes and the links + push!(case[:nodes], hydro) + 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 + return EMB.run_model(case, modeltype, OPTIMIZER) + end + + @testset "Checks" begin + + # Set the global to true to suppress the error message + EMB.TEST_ENV = true + + # Test that a wrong capacity is caught by the checks. + rate_cap = FixedProfile(-2.0) + @test_throws AssertionError check_graph(HydroStor; rate_cap) + stor_cap = FixedProfile(-40) + @test_throws AssertionError check_graph(HydroStor; 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 that a wrong output dictionary is caught by the checks. + output = Dict(Power => 1, CO2 => 0.5) + @test_throws AssertionError check_graph(HydroStor; output) + output = Dict(Power => 1.5) + @test_throws AssertionError check_graph(HydroStor; output) + output = Dict(Power => -1.0) + @test_throws AssertionError check_graph(HydroStor; output) + + # Test that a wrong input dictionary is caught by the checks. + input = Dict(Power => 1.5) + @test_throws AssertionError check_graph(HydroStor; input) + input = Dict(Power => -0.9) + @test_throws AssertionError check_graph(HydroStor; 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) + level_init = StrategicProfile([40, 25, 1, 20]) + level_min = FixedProfile(.5) + @test_throws AssertionError check_graph(HydroStor; level_init, level_min) + level_init = StrategicProfile([40, 25, -5, 20]) + @test_throws AssertionError check_graph(HydroStor; 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) + level_min = FixedProfile(2) + @test_throws AssertionError check_graph(HydroStor; level_min) + + # Set the global again to false + EMB.TEST_ENV = false + end + +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(HydroStor) + # Creation of the initial problem and the HydroStor node max_storage = FixedProfile(100) initial_reservoir = StrategicProfile([20, 25, 30, 20]) @@ -267,10 +383,14 @@ end end end end -end # testset HydroStor +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) + check_node(PumpedHydroStor) + # Creation of the initial problem and the PumpedHydroStor node with a pump. products = [Power, CO2] source = EMB.RefSource( @@ -290,7 +410,7 @@ end # testset HydroStor Dict(Power => 1), ) - case, modeltype = small_graph(source, sink) + case, modeltype = small_graph(;source, sink) max_storage = FixedProfile(100) initial_reservoir = StrategicProfile([20, 25]) @@ -353,9 +473,9 @@ end # testset HydroStor # Check that the other source operates on its maximum if there is a deficit at the sink node, # since this should be used to fill the reservoir (if the reservoir is not full enough at the # beginning, and the inflow is too low). - @assert sum( + @test sum( value.(m[:cap_use][source, t]) == value.(m[:cap_inst][source, t]) for t โˆˆ ๐’ฏ ) == length(๐’ฏ) end end -end # testset PumpedHydroStor +end diff --git a/test/test_nondisres.jl b/test/test_nondisres.jl index b48fea0..c7ef595 100644 --- a/test/test_nondisres.jl +++ b/test/test_nondisres.jl @@ -1,49 +1,102 @@ # Test set for the non dispatchable renewable energy source type -@testset "NonDisRES" begin - - # Creation of the initial problem and the NonDisRES node - case, modeltype = small_graph() - wind = EMRP.NonDisRES( - "wind", - FixedProfile(2), - FixedProfile(0.9), - FixedProfile(10), - FixedProfile(10), - Dict(Power => 1), - ) - - # Updating the nodes and the links - push!(case[:nodes], wind) - link = EMB.Direct(41, case[:nodes][4], case[:nodes][1], EMB.Linear()) - push!(case[:links], link) - - # Run the model - m = EMB.run_model(case, modeltype, OPTIMIZER) - - # Extraction of the time structure - ๐’ฏ = case[:T] - - # Run of the general tests - general_tests(m) - - # Check that the installed capacity variable corresponds to the provided values - @testset "cap_inst" begin - @test sum(value.(m[:cap_inst][wind, t]) == EMB.capacity(wind)[t] for t โˆˆ ๐’ฏ) == - length(๐’ฏ) +@testset "Test NonDisRES" begin + + # Test that the fields of a NonDisRES are correctly checked + # - check_node(n::NonDisRES, ๐’ฏ, modeltype::EnergyModel) + @testset "Checks" begin + + # Set the global to true to suppress the error message + EMB.TEST_ENV = true + + # Test that a wrong capacity is caught by the checks. + wind = EMRP.NonDisRES( + "wind", + FixedProfile(-2), + OperationalProfile([0.9, 0.4, 0.6, 0.8]), + FixedProfile(10), + FixedProfile(10), + Dict(Power => 1), + ) + case, modeltype = small_graph(source=wind, ops=SimpleTimes(4,1)) + @test_throws AssertionError EMB.run_model(case, modeltype, OPTIMIZER) + + # Test that a wrong fixed OPEX is caught by the checks. + wind = EMRP.NonDisRES( + "wind", + FixedProfile(2), + OperationalProfile([0.9, 0.4, 0.6, 0.8]), + FixedProfile(10), + FixedProfile(-10), + Dict(Power => 1), + ) + case, modeltype = small_graph(source=wind, ops=SimpleTimes(4,1)) + @test_throws AssertionError EMB.run_model(case, modeltype, OPTIMIZER) + + # Test that a wrong output dictionary is caught by the checks. + wind = EMRP.NonDisRES( + "wind", + FixedProfile(2), + OperationalProfile([0.9, 0.4, 0.6, 0.8]), + FixedProfile(10), + FixedProfile(10), + Dict(Power => -1), + ) + case, modeltype = small_graph(source=wind, ops=SimpleTimes(4,1)) + @test_throws AssertionError EMB.run_model(case, modeltype, OPTIMIZER) + + # Test that a wrong profile is caught by the checks. + wind = EMRP.NonDisRES( + "wind", + FixedProfile(2), + OperationalProfile([-0.9, 0.4, 0.6, 0.8]), + FixedProfile(10), + FixedProfile(10), + Dict(Power => 1), + ) + case, modeltype = small_graph(source=wind, ops=SimpleTimes(4,1)) + @test_throws AssertionError EMB.run_model(case, modeltype, OPTIMIZER) + wind = EMRP.NonDisRES( + "wind", + FixedProfile(2), + OperationalProfile([0.9, 0.4, 1.6, 0.8]), + FixedProfile(10), + FixedProfile(10), + Dict(Power => 1), + ) + case, modeltype = small_graph(source=wind, ops=SimpleTimes(4,1)) + @test_throws AssertionError EMB.run_model(case, modeltype, OPTIMIZER) + + # Set the global again to false + EMB.TEST_ENV = false end - @testset "cap_use bounds" begin - # Test that cap_use is bounded by cap_inst. - @test sum( - value.(m[:cap_use][wind, t]) โ‰ค value.(m[:cap_inst][wind, t]) + TEST_ATOL for - t โˆˆ ๐’ฏ - ) == length(๐’ฏ) + @testset ":profile and :curtailment" begin + # Creation of the initial problem with the NonDisRES node + wind = EMRP.NonDisRES( + "wind", + FixedProfile(25), + OperationalProfile([0.9, 0.4, 0.6, 0.8]), + FixedProfile(10), + FixedProfile(10), + Dict(Power => 1), + ) + case, modeltype = small_graph(source=wind, ops=SimpleTimes(4,1)) + + # Run the model + m = EMB.run_model(case, modeltype, OPTIMIZER) + + # Extraction of the time structure + ๐’ฏ = case[:T] + + # Run of the general tests + general_tests(m) - # Test that cap_use is set correctly with respect to the profile. + # Test that cap_use is correctly with respect to the profile. + # - EMB.constraints_capacity(m, n::NonDisRES, ๐’ฏ::TimeStructure, modeltype::EnergyModel) @test sum( - value.(m[:cap_use][wind, t]) โ‰ค - EMRP.profile(wind, t) * value.(m[:cap_inst][wind, t] + TEST_ATOL) for t โˆˆ ๐’ฏ + value.(m[:cap_use][wind, t]) + value.(m[:curtailment][wind, t]) โ‰ˆ + EMRP.profile(wind, t) * value.(m[:cap_inst][wind, t]) for t โˆˆ ๐’ฏ, atol โˆˆ TEST_ATOL ) == length(๐’ฏ) end end diff --git a/test/utils.jl b/test/utils.jl new file mode 100644 index 0000000..3085a7e --- /dev/null +++ b/test/utils.jl @@ -0,0 +1,59 @@ + +CO2 = ResourceEmit("CO2", 1.0) +Power = ResourceCarrier("Power", 0.0) + +TEST_ATOL = 1e-6 +ROUND_DIGITS = 8 +OPTIMIZER = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) + +function small_graph(; source = nothing, sink = nothing, ops = SimpleTimes(24, 2)) + + products = [Power, CO2] + # Creation of the source and sink module as well as the arrays used for nodes and links + if isnothing(source) + source = RefSource( + 2, + FixedProfile(1), + FixedProfile(30), + FixedProfile(10), + Dict(Power => 1), + ) + end + if isnothing(sink) + sink = RefSink( + 3, + FixedProfile(20), + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), + Dict(Power => 1), + ) + end + + nodes = [GenAvailability(1, products), source, sink] + links = [ + Direct(21, nodes[2], nodes[1], Linear()) + Direct(13, nodes[1], nodes[3], Linear()) + ] + + # Creation of the time structure and the used global data + T = TwoLevel(4, 1, ops) + modeltype = OperationalModel( + Dict(CO2 => StrategicProfile([450, 400, 350, 300])), + Dict(CO2 => FixedProfile(0)), + CO2, + ) + + # Creation of the case dictionary + case = Dict(:nodes => nodes, :links => links, :products => products, :T => T) + return case, modeltype +end + +function general_tests(m) + # Check if the solution is optimal. + @testset "optimal solution" begin + @test termination_status(m) == MOI.OPTIMAL + + if termination_status(m) != MOI.OPTIMAL + @show termination_status(m) + end + end +end