Skip to content

Commit

Permalink
Adjusted checks based on EMB changes (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
JulStraus authored Mar 22, 2024
1 parent 2dc39b1 commit db44cf5
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 19 deletions.
6 changes: 4 additions & 2 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
name = "EnergyModelsGeography"
uuid = "3f775d88-a4da-46c4-a2cc-aa9f16db6708"
authors = ["Espen Flo Bødal <[email protected]>"]
version = "0.8.2"
version = "0.8.3"

[deps]
EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50"
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"
115 changes: 108 additions & 7 deletions src/checks.jl
Original file line number Diff line number Diff line change
@@ -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
34 changes: 26 additions & 8 deletions src/model.jl
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
95 changes: 95 additions & 0 deletions test/test_checks.jl
Original file line number Diff line number Diff line change
@@ -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

2 comments on commit db44cf5

@JulStraus
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/103412

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.8.3 -m "<description of version>" db44cf57d83d4e2127b18cac5f4459378086766c
git push origin v0.8.3

Please sign in to comment.