diff --git a/NEWS.md b/NEWS.md index b8d710c..606ef4c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,11 +1,13 @@ # Release notes -## Unversioned +## Version 0.8.3 (2024-03-21) * Fixed a bug regarding accessing the field `limit` of a `LimitedExchangeArea`. * Moved all files declaring structures to a separate folder for improved readability. +* Allow for jumping over `TimeProfile` checks also from `EnergyModelsGeography`. +* Added possibility to provide a different type of `JuMP.Model`. -## Version 0.8.1 (2024-03-04) +## Version 0.8.2 (2024-03-04) * Fixed a bug when running the examples from a non-cloned version of `EnergyModelsGeography`. * This was achieved through a separate Project.toml in the examples. diff --git a/Project.toml b/Project.toml index 0714270..89175eb 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "EnergyModelsGeography" uuid = "3f775d88-a4da-46c4-a2cc-aa9f16db6708" authors = ["Espen Flo BΓΈdal "] -version = "0.8.2" +version = "0.8.3" [deps] EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" @@ -9,7 +9,7 @@ JuMP = "4076af6c-e467-56ae-b986-b466b2749572" TimeStruct = "f9ed5ce0-9f41-4eaa-96da-f38ab8df101c" [compat] -EnergyModelsBase = "^0.6.0" +EnergyModelsBase = "^0.6.7" JuMP = "1.5" julia = "^1.6" TimeStruct = "^0.7.0" diff --git a/src/checks.jl b/src/checks.jl index 7520c53..1b2b0ee 100644 --- a/src/checks.jl +++ b/src/checks.jl @@ -1,24 +1,125 @@ -function check_data(case, modeltype) +""" + check_data(case, modeltype, check_timeprofiles::Bool) + +Check if the case data is consistent. Use the `@assert_or_log` macro when testing. +Currently, not checking data except that the case dictionary follows the required structure. +""" +function check_data(case, modeltype, check_timeprofiles::Bool) + + global EMB.logs = [] + log_by_element = Dict() + + # Check the case data. If the case data is not in the correct format, the overall check + # is cancelled as extractions would not be possible + check_case_data(case) + log_by_element["Case data"] = EMB.logs + if EMB.ASSERTS_AS_LOG + EMB.compile_logs(case, log_by_element) + end π’œ = case[:areas] ℒᡗʳᡃⁿ˒ = case[:transmission] - β„’ = case[:links] - 𝒩 = case[:nodes] 𝒫 = case[:products] 𝒯 = case[:T] for a ∈ π’œ - check_area(a, 𝒩, β„’, 𝒯, 𝒫, modeltype) + check_area(a, 𝒯, 𝒫, modeltype, check_timeprofiles) + # Put all log messages that emerged during the check, in a dictionary with the + # area as key. + log_by_element[a] = EMB.logs end for l ∈ ℒᡗʳᡃⁿ˒ - check_transmission(l, 𝒩, 𝒯, 𝒫, modeltype) + check_transmission(l, 𝒯, 𝒫, modeltype, check_timeprofiles) + + β„³ = modes(l) + for m ∈ β„³ + check_mode(m, 𝒯, 𝒫, modeltype, check_timeprofiles) + if check_timeprofiles + check_time_structure(m, 𝒯) + end + # Put all log messages that emerged during the check, in a dictionary with the + # corridor as key. + log_by_element[l] = EMB.logs + end + end + + if EMB.ASSERTS_AS_LOG + EMB.compile_logs(case, log_by_element) + end +end + +""" + check_case_data(case) + +Checks the `case` dictionary is in the correct format. The function is only checking the +new, additional data as we do not yet consider dispatch on the case data. + +## Checks +- The dictionary requires the keys `:areas` and `:transmission`. +- The individual keys are of the correct type, that is + - `:areas::Area` and + - `:transmission::Vector{<:Transmission}`. +""" +function check_case_data(case) + + case_keys = [:areas, :transmission] + key_map = Dict( + :areas => Vector{<:Area}, + :transmission => Vector{<:Transmission}, + ) + for key ∈ case_keys + @assert_or_log( + haskey(case, key), + "The `case` dictionary requires the key `:" * string(key) * "` which is " * + "not included." + ) + if haskey(case, key) + @assert_or_log( + isa(case[key], key_map[key]), + "The key `" * string(key) * "` in the `case` dictionary contains " * + "other types than the allowed." + ) + end end end +""" + check_area(a::Area, 𝒯, 𝒫, modeltype::EnergyModel, check_timeprofiles::Bool) + +Check that the fields of an `Area` corresponds to required structure. +""" +function check_area(a::Area, 𝒯, 𝒫, modeltype::EnergyModel, check_timeprofiles::Bool) +end + +""" + check_transmission(l::Transmission, 𝒯, 𝒫, modeltype::EnergyModel, check_timeprofiles::Bool) -function check_area(a::Area, 𝒩, β„’, 𝒯, 𝒫, modeltype) +Check that the fields of a `Transmission` corridor corresponds to required structure. +""" +function check_transmission(l::Transmission, 𝒯, 𝒫, modeltype::EnergyModel, check_timeprofiles::Bool) end -function check_transmission(l::Transmission, 𝒩, 𝒯, 𝒫, modeltype) + +""" + check_mode(m::TransmissionMode, 𝒯, 𝒫, modeltype::EnergyModel, check_timeprofiles::Bool) + +Check that the fields of a `TransmissionMode` corresponds to required structure. +""" +function check_mode(l::TransmissionMode, 𝒯, 𝒫, modeltype::EnergyModel, check_timeprofiles::Bool) +end + +""" + check_time_structure(m::TransmissionMode, 𝒯) + +Check that all fields of a `TransmissionMode` that are of type `TimeProfile` correspond to +the time structure `𝒯`. +""" +function check_time_structure(m::TransmissionMode, 𝒯) + for fieldname ∈ fieldnames(typeof(m)) + value = getfield(m, fieldname) + if isa(value, TimeProfile) + EMB.check_profile(fieldname, value, 𝒯) + end + end end diff --git a/src/model.jl b/src/model.jl index b5f2609..66bc069 100644 --- a/src/model.jl +++ b/src/model.jl @@ -1,14 +1,29 @@ """ - create_model(case, modeltype::EnergyModel) - -Create the model and call all requried functions based on provided 'modeltype' -and case data. + create_model(case, modeltype::EnergyModel; check_timeprofiles::Bool=true) + +Create the model and call all required functions. + +## Input +- `case` - The case dictionary requiring the keys `:T`, `:nodes`, `:links`, `products` as + it is the case for standard `EnergyModelsBase` models. In addition, the keys `:areas` and + `:transmission` are required for extending the existing model. + If the input is not provided in the correct form, the checks will identify the problem. + In the case of a +- `modeltype::EnergyModel` - Used modeltype, that is a subtype of the type `EnergyModel`. +- `m` - the empty `JuMP.Model` instance. If it is not provided, then it is assumed that the + input is a standard `JuMP.Model`. + +## Conditional input +- `check_timeprofiles::Bool=true` - A boolean indicator whether the time profiles of the individual + nodes should be checked or not. It is advised to not deactivate the check, except if you + are testing new components. It may lead to unexpected behaviour and potential + inconsistencies in the input data, if the time profiles are not checked. """ -function create_model(case, modeltype) +function create_model(case, modeltype::EnergyModel, m::JuMP.Model; check_timeprofiles::Bool=true) @debug "Construct model" # Call of the basic model - m = EMB.create_model(case, modeltype) - check_data(case, modeltype) + m = EMB.create_model(case, modeltype, m; check_timeprofiles) + check_data(case, modeltype, check_timeprofiles) # Data structure π’œ = case[:areas] @@ -35,7 +50,10 @@ function create_model(case, modeltype) return m end - +function create_model(case, modeltype::EnergyModel; check_timeprofiles::Bool=true) + m = JuMP.Model() + create_model(case, modeltype, m; check_timeprofiles) +end """ variables_area(m, π’œ, 𝒯, ℒᡗʳᡃⁿ˒, modeltype::EnergyModel) diff --git a/test/runtests.jl b/test/runtests.jl index 279148d..eac69dc 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -24,4 +24,5 @@ const ROUND_DIGITS = 8 include("test_simplepipe.jl") include("test_simplelinepack.jl") include("test_utils.jl") + include("test_checks.jl") end diff --git a/test/test_checks.jl b/test/test_checks.jl new file mode 100644 index 0000000..e69b1a7 --- /dev/null +++ b/test/test_checks.jl @@ -0,0 +1,95 @@ +# Set the global to true to suppress the error message +EMB.TEST_ENV = true + +@testset "Test checks - case dictionary" begin + # Resources used in the analysis + Power = ResourceCarrier("Power", 0.0) + CO2 = ResourceEmit("CO2", 1.0) + + function small_graph() + products = [Power, CO2] + + # Creation of the source and sink module as well as the arrays used for nodes and links + source = RefSource( + "src", + FixedProfile(25), + FixedProfile(10), + FixedProfile(5), + Dict(Power => 1), + ) + sink = RefSink( + "sink", + FixedProfile(20), + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), + Dict(Power => 1), + ) + + nodes = [ + GeoAvailability(1, products), + EMG.GeoAvailability(2, products), + source, + sink, + ] + links = [ + Direct(31, nodes[3], nodes[1], Linear()), + Direct(24, nodes[2], nodes[4], Linear()), + ] + + # Creation of the two areas and potential transmission lines + areas = [ + RefArea(1, "Factory", 10.751, 59.921, nodes[1]), + RefArea(2, "North Sea", 10.398, 63.4366, nodes[2]), + ] + + transmission_line = RefStatic( + "Transline", + Power, + FixedProfile(30.0), + FixedProfile(0.05), + FixedProfile(0.05), + FixedProfile(0.05), + 1, + ) + transmissions = [Transmission(areas[1], areas[2], [transmission_line])] + + # Creation of the time structure and the used global data + T = TwoLevel(4, 1, SimpleTimes(4, 1)) + 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, + :areas => areas, + :transmission => transmissions, + :T => T, + ) + return case, modeltype + end + + # Check that the keys are present + # - EMG.check_case_data(case) + case, model = small_graph() + for key ∈ [:areas, :transmission] + case_test = deepcopy(case) + pop!(case_test, key) + @test_throws AssertionError EMG.create_model(case_test, model) + end + + # Check that the keys are of the correct format and do not include any unwanted types + # - EMG.check_case_data(case) + case_test = deepcopy(case) + case_test[:areas] = [case[:areas], case[:areas], 10] + @test_throws AssertionError EMG.create_model(case_test, model) + case_test = deepcopy(case) + case_test[:transmission] = [case[:transmission], case[:transmission], 10] + @test_throws AssertionError EMG.create_model(case_test, model) +end + +# Set the global again to false +EMB.TEST_ENV = false