From e34f545e7b292696290bf8f7195c26f5839d9949 Mon Sep 17 00:00:00 2001 From: hellemo Date: Tue, 20 Aug 2024 13:45:24 +0200 Subject: [PATCH] Support EMB-independent EMI (#23) * Move support for EMI to extension in EMG (from EMGExt in EMI) - Move example and test from EMI to EMG - Added TransInvData constructor - Included parts in docs * Included redirect of output for examples in tests --------- Co-authored-by: Julian Straus --- NEWS.md | 8 + Project.toml | 15 +- docs/Project.toml | 2 + docs/make.jl | 35 +- docs/src/how-to/contribute.md | 12 +- docs/src/index.md | 1 + .../src/library/internals/reference_EMIExt.md | 31 ++ docs/src/library/public.md | 37 +- docs/src/manual/constraint-functions.md | 12 +- docs/src/manual/investments.md | 27 + docs/src/manual/optimization-variables.md | 10 +- docs/src/manual/philosophy.md | 10 +- docs/src/manual/quick-start.md | 27 +- docs/src/manual/simple-example.md | 2 +- docs/src/manual/transmission-mode.md | 2 +- examples/Project.toml | 1 + examples/investments.jl | 500 ++++++++++++++++++ ext/EMIExt/EMIExt.jl | 19 + ext/EMIExt/legacy_constructor.jl | 86 +++ ext/EMIExt/model.jl | 97 ++++ ext/EMIExt/utils.jl | 24 + src/EnergyModelsGeography.jl | 4 + src/structures/data.jl | 26 + test/test_examples.jl | 8 +- test/test_investments.jl | 291 ++++++++++ 25 files changed, 1225 insertions(+), 62 deletions(-) create mode 100644 docs/src/library/internals/reference_EMIExt.md create mode 100644 docs/src/manual/investments.md create mode 100644 examples/investments.jl create mode 100644 ext/EMIExt/EMIExt.jl create mode 100644 ext/EMIExt/legacy_constructor.jl create mode 100644 ext/EMIExt/model.jl create mode 100644 ext/EMIExt/utils.jl create mode 100644 src/structures/data.jl create mode 100644 test/test_investments.jl diff --git a/NEWS.md b/NEWS.md index d34e308..5f7d4ad 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,13 @@ # Release notes +## Unversioned + +### Introduced `EnergyModelsInvestments` as extension + +* `EnergyModelsInvestments` was switched to be an independent package in [PR #28](https://github.com/EnergyModelsX/EnergyModelsInvestments.jl/pull/28). +* This approach required `EnergyModelsGeography` to include all functions and type declarations internally. +* An extension was introduced to handle these problems. + ## Version 0.9.1 (2024-05-24) ### Bugfix diff --git a/Project.toml b/Project.toml index 1f261a5..8c637d8 100644 --- a/Project.toml +++ b/Project.toml @@ -1,15 +1,24 @@ name = "EnergyModelsGeography" uuid = "3f775d88-a4da-46c4-a2cc-aa9f16db6708" authors = ["Espen Flo BΓΈdal "] -version = "0.9.1" +version = "0.10.0" [deps] EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +SparseVariables = "2749762c-80ed-4b14-8f33-f0736679b02b" TimeStruct = "f9ed5ce0-9f41-4eaa-96da-f38ab8df101c" +[weakdeps] +EnergyModelsInvestments = "fca3f8eb-b383-437d-8e7b-aac76bb2004f" + +[extensions] +EMIExt = "EnergyModelsInvestments" + [compat] -EnergyModelsBase = "^0.7" +EnergyModelsBase = "^0.8" +EnergyModelsInvestments = "0.7" +SparseVariables = "0.7.3" JuMP = "1.5" -julia = "^1.6" TimeStruct = "^0.8" +julia = "1.9" diff --git a/docs/Project.toml b/docs/Project.toml index 8bbabb6..c37f2bb 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,6 +1,8 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" EnergyModelsGeography = "3f775d88-a4da-46c4-a2cc-aa9f16db6708" +EnergyModelsInvestments = "fca3f8eb-b383-437d-8e7b-aac76bb2004f" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" TimeStruct = "f9ed5ce0-9f41-4eaa-96da-f38ab8df101c" diff --git a/docs/make.jl b/docs/make.jl index cd829df..98efd86 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,9 +1,21 @@ using Documenter +using DocumenterInterLinks using EnergyModelsGeography +using EnergyModelsBase +using EnergyModelsInvestments const EMG = EnergyModelsGeography +# Copy the NEWS.md file +cp("NEWS.md", "docs/src/manual/NEWS.md"; force=true) + +links = InterLinks( + "TimeStruct" => "https://sintefore.github.io/TimeStruct.jl/stable/", + "EnergyModelsBase" => "https://energymodelsx.github.io/EnergyModelsBase.jl/stable/", + "EnergyModelsInvestments" => "https://energymodelsx.github.io/EnergyModelsInvestments.jl/stable/", +) + DocMeta.setdocmeta!( EnergyModelsGeography, :DocTestSetup, @@ -11,18 +23,19 @@ DocMeta.setdocmeta!( recursive = true, ) -# Copy the NEWS.md file -news = "docs/src/manual/NEWS.md" -cp("NEWS.md", news; force=true) - makedocs( - modules = [EnergyModelsGeography], sitename = "EnergyModelsGeography", format = Documenter.HTML( prettyurls = get(ENV, "CI", "false") == "true", edit_link = "main", assets = String[], ), + modules = [ + EMG, + isdefined(Base, :get_extension) ? + Base.get_extension(EMG, :EMIExt) : + EMG.EMIExt + ], pages = [ "Home" => "index.md", "Manual" => Any[ @@ -32,6 +45,7 @@ makedocs( "Constraint functions" => "manual/constraint-functions.md", "TransmissionMode structure" => "manual/transmission-mode.md", "Example" => "manual/simple-example.md", + "Investment options"=>"manual/investments.md", "Release notes" => "manual/NEWS.md", ], "How to" => Any[ @@ -39,10 +53,13 @@ makedocs( ], "Library" => Any[ "Public" => "library/public.md", - "Internals" => Any[ - "Reference" => "library/internals/reference.md"] - ] - ] + "Internals"=>Any[ + "Reference"=>"library/internals/reference.md", + "Reference EMIExt"=>"library/internals/reference_EMIExt.md", + ], + ], + ], + plugins=[links], ) deploydocs(; diff --git a/docs/src/how-to/contribute.md b/docs/src/how-to/contribute.md index fa576da..6d66bbe 100644 --- a/docs/src/how-to/contribute.md +++ b/docs/src/how-to/contribute.md @@ -1,8 +1,8 @@ -# Contribute to EnergyModelsGeography +# [Contribute to EnergyModelsGeography](@id how_to-con) Contributing to `EnergyModelsGeography` can be achieved in several different ways. -## Creating new extensions +## [Create new extensions](@id how_to-con-ext) The main focus of `EnergyModelsGeography` is to provide [`EnergyModelsBase`](https://energymodelsx.github.io/EnergyModelsBase.jl/stable/) with geographical representation using the concepts of [`Area`](@ref)s, [`Transmission`](@ref) corridors, or [`TransmissionMode`](@ref)s. Hence, a first approach to contributing to `EnergyModelsGeography` is to create a new package with, _e.g._, the introduction of new `Area`, `Transmission`, or `TransmissionMode` descriptions. @@ -12,9 +12,9 @@ These descriptions can, _e.g._, include constraints for an `Area` or provide the We are currently working on guidelines for the best approach for `EnergyModelsGeography`, similar to the section [_Extensions to the model_](https://energymodelsx.github.io/EnergyModelsBase.jl/stable/manual/philosophy/#sec_phil_ext) in `EnergyModelsBase`. This section will provide you eventually with additional information regarding to how you can develop new `Area`, `Transmission`, or `TransmissionMode` descriptions. -## File a bug report +## [File a bug report](@id how_to-con-bug_rep) -Another approach to contributing to `EnergyModelsGeography` is through filing a bug report as an _[issue](https://github.com/EnergyModelsX/EnergyModelsGeography.jl/issues/new)_ when unexpected behaviour is occuring. +Another approach to contributing to `EnergyModelsGeography` is through filing a bug report as an [_issue_](https://github.com/EnergyModelsX/EnergyModelsGeography.jl/issues/new) when unexpected behaviour is occuring. When filing a bug report, please follow the following guidelines: @@ -39,10 +39,10 @@ When filing a bug report, please follow the following guidelines: In order to improve the code, we welcome any reports of potential method ambiguities to help us improving the structure of the framework. -## Feature requests +## [Feature requests](@id how_to-feat_req) Although `EnergyModelsGeography` was designed with the aim of flexibility, it sometimes still requires additional features to account for potential extensions. -Feature requests for `EnergyModelsGeography` should follow the guidelines developed for [`EnergyModelsBase`](https://energymodelsx.github.io/EnergyModelsBase.jl/stable/how-to/contribute/). +Feature requests for `EnergyModelsGeography` should follow the guidelines developed for [_`EnergyModelsBase`_](https://energymodelsx.github.io/EnergyModelsBase.jl/stable/how-to/contribute/). !!! note `EnergyModelsGeography` should not include everything. diff --git a/docs/src/index.md b/docs/src/index.md index 7d6b9eb..68e5891 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -17,6 +17,7 @@ Pages = [ "manual/optimization-variables.md", "manual/constraint-functions.md", "manual/transmission-mode.md", + "manual/investments.md", "manual/simple-example.md" ] Depth = 1 diff --git a/docs/src/library/internals/reference_EMIExt.md b/docs/src/library/internals/reference_EMIExt.md new file mode 100644 index 0000000..931e43a --- /dev/null +++ b/docs/src/library/internals/reference_EMIExt.md @@ -0,0 +1,31 @@ +# Internals - EnergyModelsInvestment extensions + +## Index + +```@index +Pages = ["reference_EMIExt.md"] +``` + +## Types + +```@autodocs +Modules = [ + isdefined(Base, :get_extension) ? + Base.get_extension(EMG, :EMIExt) : + EMG.EMIExt +] +Public = false +Order = [:type] +``` + +## Methods + +```@autodocs +Modules = [ + isdefined(Base, :get_extension) ? + Base.get_extension(EMG, :EMIExt) : + EMG.EMIExt +] +Public = false +Order = [:function] +``` diff --git a/docs/src/library/public.md b/docs/src/library/public.md index 3429846..6a6f809 100644 --- a/docs/src/library/public.md +++ b/docs/src/library/public.md @@ -1,6 +1,6 @@ -# [Public interface](@id sec_lib_public) +# [Public interface](@id lib-pub) -## `Area` +## [`Area`](@id lib-pub-area) A geographical `Area` consist of a location and a connection to a local energy system *via* a specialized `Availability` node called `GeoAvailability`. The specialized `Availability` node is required to modify the energy/mass balance to allow for imports and exports. @@ -8,7 +8,7 @@ Constraints related to the area keep track of a resource's export and import to Multiple dispatch is used on the `Area` type for imposing specific constraints. Hence, other restrictions can be applied on a area level, such as electricity generation reserves, COβ‚‚ emission limits or resource limits (wind power, natural gas etc.). -### `Area` types +### [`Area` types](@id lib-pub-area-types) The following types are inmplemented: @@ -19,7 +19,7 @@ LimitedExchangeArea GeoAvailability ``` -### Functions for accessing fields of `Area` types +### [Functions for accessing fields of `Area` types](@id lib-pub-area-fun_field) The following functions are defined for accessing fields from an `Area`: @@ -32,21 +32,21 @@ exchange_resources getnodesinarea ``` -## `Transmission` +## [`Transmission`](@id lib-pub-transmission) `Transmission` occurs on specified transmission corridors `from` one area `to` another. On each corridor, there can exist several `TransmissionMode`s that are transporting resources using a range of technologies. It is important to consider the `from` and `to` `Area` when specifying a `Transmission` corridor. The chosen direction has an influence on whether the variables ``\texttt{trans\_in}[m, t]`` and ``\texttt{trans\_out}[m, t]`` are positive or negative for exports in the case of bidirectional transport. -This is also explained on the page *[Optimization variables](@ref optimization_variables)*. +This is also explained on the page *[Optimization variables](@ref man-opt_var)*. -### `Transmission` types +### [`Transmission` types](@id lib-pub-transmission-types) ```@docs Transmission ``` -### Functions for accessing fields of `Transmission` types +### [Functions for accessing fields of `Transmission` types](@id lib-pub-transmission-fun_fields) The following functions are defined for accessing fields from a `Transmission` as well as finding a subset of `Transmission` corridors: @@ -59,7 +59,7 @@ corr_from_to modes_of_dir ``` -## `TransmissionMode` +## [`TransmissionMode`](@id lib-pub-transmission_mode) `TransmissionMode` describes how resources are transported, for example by dynamic transmission modes on ship, truck or railway (represented generically by `RefDynamic`, although not implemented in the current version) or by static transmission modes on overhead power lines or gas pipelines (respresented generically by `RefStatic`). `TransmissionMode`s includes capacity limits (`trans_cap`), losses (`trans_loss`) and directions (`directions`) for the generic transmission modes `RefDynamic` and `RefStatic`. @@ -74,7 +74,7 @@ All `TransmissionMode`s can also include both fixed (`opex_fixed`) and variable They are independent of the distance. The reasoning for this approach is that it allows the modeller to have a non-linear, distance dependent OPEX or loss function for providing the input to the model. -### `TransmissionMode` types +### [`TransmissionMode` types](@id lib-pub-transmission_mode-types) The following `TransmissionMode`s are implemented and exported: @@ -87,7 +87,7 @@ PipeSimple PipeLinepackSimple ``` -### Functions for accessing fields of `TransmissionMode` types +### [Functions for accessing fields of `TransmissionMode` types](@id lib-pub-transmission_mode-fun_fields) The following functions are defined and exported for accessing fields from a `TransmissionMode`: @@ -98,3 +98,18 @@ directions consumption_rate energy_share ``` + +## [Investment data](@id lib-pub-inv_data) + +### [`InvestmentData` types](@id lib-pub-inv_data-types) + +Transmission mode investmentments utilize the same investment data type ([`SingleInvData](@extref EnergyModelsBase.SingleInvData)) as investments in node capacities. + +### [Legacy constructors](@id lib-pub-inv_data-leg) + +We provide a legacy constructor, `TransInvData`, that uses the same input as in version 0.5.x. +If you want to adjust your model to the latest changes, please refer to the section *[Update your model to the latest version of EnergyModelsInvestments](@extref EnergyModelsInvestments how_to-update-05)*. + +```@docs +TransInvData +``` diff --git a/docs/src/manual/constraint-functions.md b/docs/src/manual/constraint-functions.md index b868240..97353ef 100644 --- a/docs/src/manual/constraint-functions.md +++ b/docs/src/manual/constraint-functions.md @@ -1,11 +1,11 @@ -# [Constraint functions](@id constraint_functions) +# [Constraint functions](@id man-con) The package provides standard constraint functions that can be use for new developed `TransmissionMode`s. The general approach is similar to `EnergyModelsBase`. Bidirectional transport requires at the time being the introduciton of an *if*-loop. In later implementation, it is planned to also use dispatch for this analysis as well. -## Capacity constraints +## [Capacity constraints](@id man-con-cap) ```julia constraints_capacity(m, tm::TransmissionMode, 𝒯::TimeStructure, modeltype::EnergyModel) @@ -34,11 +34,11 @@ This functions is also used to subsequently dispatch on model type for the intro ``` without adding a function for ```julia - constraints_capacity_installed(m, tm::TransmissionMode, 𝒯::TimeStructure, modeltype::EMI.AbstractInvestmentModel) + constraints_capacity_installed(m, tm::TransmissionMode, 𝒯::TimeStructure, modeltype::EMB.AbstractInvestmentModel) ``` as this can lead to a method ambiguity error. -## Transmission loss functions +## [Transmission loss functions](@id man-con-trans_loss) ```julia constraints_trans_loss(m, tm::TransmissionMode, 𝒯ᴡⁿᡛ, modeltype::EnergyModel) @@ -49,7 +49,7 @@ It is implemented for both the `TransmissionMode` and `PipeMode` abstract types. The key difference between the two is related that `PipeMode` does not allows for bidirectional transport. The loss is calculated for the provided `TransmissionMode`s as relative loss of the transported energy. -## Balance functions +## [Balance functions](@id man-con-balance) ```julia constraints_trans_balance(m, tm::TransmissionMode, 𝒯ᴡⁿᡛ, modeltype::EnergyModel) @@ -66,7 +66,7 @@ The standard approach only relies on the conservation of mass/energy, while stor The implementation is working with the assumption that the initial level in a representative period is equal to the final level in the last representative period of a strategic period. This implies that it does not account correctly for the remaining level at the end of a representative period. -## Operational expenditure constraints +## [Operational expenditure constraints](@id man-con-opex) ```julia constraints_opex_fixed(m, tm::TransmissionMode, 𝒯ᴡⁿᡛ, modeltype::EnergyModel) diff --git a/docs/src/manual/investments.md b/docs/src/manual/investments.md new file mode 100644 index 0000000..067f2df --- /dev/null +++ b/docs/src/manual/investments.md @@ -0,0 +1,27 @@ +# [Adding investments](@id man-emi) + +Investment options are added through loading the package [`EnergyModelsInvestments`](https://energymodelsx.github.io/EnergyModelsInvestments.jl/). +`EnergyModelsInvestments` was previously seen as extension package to `EnergyModelsBase`, that it was dependent on `EnergyModelsBase` and only allowed investment options in `EnergyModelsBase`. +This approach was reversed from version 0.7 onwards and `EnergyModelsInvestments` is now a standalone package and provides an extension to `EnergyModelsBase`. + +As a consequence, it was not also necessary to move the `EnergyModelsGeography` extension of `EnergyModelsInvestments` to an `EnergyModelsInvestments` extension of `EnergyModelsGeography`. + +## [General concept](@id man-emi-gen) + +Investment options are added separately to each individual transmission mode through the field `data`. +This is similar to the approach used in +Hence, it is possible to use different prices for the same technology in different regions or allow investments only in a limited subset of technologies. + +Transmission mode investments utilize the same data type [`SingleInvData`](@extref EnergyModelsBase.SingleInvData) as the majority of the node investments. +This type inludes as fields [`AbstractInvData`](@extref EnergyModelsInvestments.AbstractInvData) which can be either in the form of [`StartInvData`](@extref EnergyModelsInvestments.StartInvData) or [`NoStartInvData`](@extref EnergyModelsInvestments.NoStartInvData). +The exact description of the individual investment data and their fields can be found in the *[public library](@extref EnergyModelsInvestments lib-pub)* of `EnergyModelsInvestments`. + +Investments require the application of an [`InvestmentModel`](@extref EnergyModelsBase.InvestmentModel) instead of an [`OperationalModel`](@extref EnergyModelsBase.OperationalModel). +This allows us to provide two core functions with new methods, `constraints_capacity_installed` (as described on *[Constraint functions](@ref man-con)*), `variables_trans_capex`, a function previously not declaring any variables, and the update to the objective functoin `update_objective`. + +## [Added variables](@id man-emi-var) + +Investment options increase the number of variables. +The individual variables are described in the *[documentation of `EnergyModelsInvestments`](@extref EnergyModelsInvestments man-opt_var)*. + +All transmission modes with investments use the prefix `:trans`. diff --git a/docs/src/manual/optimization-variables.md b/docs/src/manual/optimization-variables.md index 349339c..e499433 100644 --- a/docs/src/manual/optimization-variables.md +++ b/docs/src/manual/optimization-variables.md @@ -1,4 +1,4 @@ -# [Optimization variables](@id optimization_variables) +# [Optimization variables](@id man-opt_var) `EnergyModelsGeography` adds additional variables to `EnergyModelsBase`. These variables are required for being able to extend the model with geographic information. @@ -9,7 +9,7 @@ The additional variables can be differentiated between `Area` variables and `Tra Variables that are energy/mass based have that property highlighted in the documentation below. This is only the case for the storage level in a `PipeLinepackSimple`. -## [`Area`](@ref) +## [`Area`](@id man-opt_var-area) `Area`s create only a single additional variable: @@ -21,14 +21,14 @@ The exchange resources are automatically deduced from the coupled `TransmissionM The area exchange is negative when exporting energy/mass and positive when importing. This implies that for ``\texttt{area\_exchange}[a, t, p_\texttt{ex}] > 0``, the area imports product ``p``, and for ``\texttt{area\_exchange}[a, t, p_\texttt{ex}] < 0``, the area exports product ``p_\texttt{ex}``. -## [`TransmissionMode`](@ref) +## [`TransmissionMode`](@id man-opt_var-transmission_mode) !!! warning "'Inheritance' of optimization variables" Note that for all subtypes of [`TransmissionMode`](@ref) the variables created for the parent `TransmissionMode`-type will be created, in addition to the variables created for that type. This means that the type [`PipeLinepackSimple`](@ref) will not only have access to the optimization variable ``\texttt{linepack\_stor\_level}[m, t]``, but also all the optimization variables created for [`TransmissionMode`](@ref). -### General variables for all `TransmissionMode` +### [General variables for all `TransmissionMode`](@id man-opt_var-transmission_mode-gen) All variables described in this section are included for all subtypes of [`TransmissionMode`](@ref). In general, we can differentiate between capacity variables, flow variables, cost variables, and helper variables. @@ -67,7 +67,7 @@ In addition, both ``\texttt{trans\_in}[m, t]`` and ``\texttt{trans\_out}[m, t]`` If the energy/mass is transported **opposite to the direction** of the `Transmission` corridor, then both variables are negative and ``\texttt{trans\_in}[m, t]`` corresponds to the **outlet** to the `TransmissionMode`. -### [`PipeLinepackSimple`](@ref) <: `Pipeline` <: `TransmissionMode` +### [[`PipeLinepackSimple`](@ref) <: `Pipeline` <: `TransmissionMode`](@id man-opt_var-transmission_mode-linepack) `PipeLinepackSimple` adds one additional variable: diff --git a/docs/src/manual/philosophy.md b/docs/src/manual/philosophy.md index b84401b..f675069 100644 --- a/docs/src/manual/philosophy.md +++ b/docs/src/manual/philosophy.md @@ -1,6 +1,6 @@ -# Philosophy +# [Philosophy](@id man-phil) -## General design philosophy +## [General design philosophy](@id man-phil-gen) This package extends `EnergyModelsBase` with geographical functionalities. The geographical representation is achieved through individual `Area`s coupled with `Transmission` corridors. @@ -24,7 +24,7 @@ This can be beneficial for the case of power lines *vs.* pipelines which may req We included in an internal version one approach to extend the emission constraints, but are not yet satisfied with the exact implementation. This implies that including emissions in mass/energy transport is on the agenda for future improvements. -## Extensions to the model +## [Extensions to the model](@id man-phil-ext) `EnergyModelsGeography` is also designed to be extended by the user. Extensions of `EnergyModelsGeography` are possible through @@ -32,14 +32,14 @@ Extensions of `EnergyModelsGeography` are possible through 1. specialized [`Area`](@ref)s (like the [`LimitedExchangeArea`](@ref)) and 2. new [`TransmissionMode`](@ref)s. -### Specialized `Area`s +### [Specialized `Area`s](@id man-phil-ext-area) Specialized [`Area`](@ref)s are areas with additional constraints. In general, it is possible to import and export as much as possible through the connected `Transmission` corridors. Introducing specialized [`Area`](@ref)s allow the introduciton of additional constraints within an `Area`. These constraints can include, among others, limits on the COβ‚‚ emissions within an `Area`, export limits both on, *e.g.* operational or strategic periods as well as many other modifications. -### New [`TransmissionMode`](@ref)s +### [New [`TransmissionMode`](@ref)s](@id man-phil-ext-transmissioN_mode) New [`TransmissionMode`](@ref)s may be relevant for describing a dynamic transport mechanism or for adding a distinctive description for an energy carrier. [`TransmissionMode`](@ref)s can be developed similar to new `Node`s as described in `EnergyModelsBase`. diff --git a/docs/src/manual/quick-start.md b/docs/src/manual/quick-start.md index 85b9200..ae3c642 100644 --- a/docs/src/manual/quick-start.md +++ b/docs/src/manual/quick-start.md @@ -1,13 +1,16 @@ -# [Quick Start](@id quick_start) +# [Quick Start](@id man-quick) -> 1. Install the most recent version of [Julia](https://julialang.org/downloads/) -> 2. Install the package [`EnergyModelsBase`](https://energymodelsx.github.io/EnergyModelsBase.jl/) and the time package [`TimeStruct`](https://sintefore.github.io/TimeStruct.jl/), by running: -> ``` -> ] add TimeStruct -> ] add EnergyModelsBase -> ``` -> These packages are required as we do not only use them internally, but also for building a model. -> 3. Install the package [`EnergyModelsGeography`](https://energymodelsx.github.io/EnergyModelsGeography.jl/) -> ``` -> ] add EnergyModelsGeography -> ``` +> 1. Install the most recent version of [Julia](https://julialang.org/downloads/) +> 2. Install the package [`EnergyModelsBase`](https://energymodelsx.github.io/EnergyModelsBase.jl/) and the time package [`TimeStruct`](https://sintefore.github.io/TimeStruct.jl/), by running: +> +> ```julia +> ] add TimeStruct +> ] add EnergyModelsBase +> ``` +> +> These packages are required as we do not only use them internally, but also for building a model. +> 3. Install the package [`EnergyModelsGeography`](https://energymodelsx.github.io/EnergyModelsGeography.jl/) +> +> ```julia +> ] add EnergyModelsGeography +> ``` diff --git a/docs/src/manual/simple-example.md b/docs/src/manual/simple-example.md index f906ca4..8f6b130 100644 --- a/docs/src/manual/simple-example.md +++ b/docs/src/manual/simple-example.md @@ -1,4 +1,4 @@ -# Examples +# [Examples](@id man-exampl) For the content of the example, see the *[examples](https://github.com/EnergyModelsX/EnergyModelsGeography.jl/tree/main/examples)* directory in the project repository. diff --git a/docs/src/manual/transmission-mode.md b/docs/src/manual/transmission-mode.md index 9a9d14b..c50654b 100644 --- a/docs/src/manual/transmission-mode.md +++ b/docs/src/manual/transmission-mode.md @@ -22,4 +22,4 @@ TransmissionMode The leaf `TransmissionMode`s of the above type hierarchy tree are `composite type`s, while the inner vertices are `abstract type`s. -The individual `TransmissionMode` and their fields are explained in *[the public library](@ref sec_lib_public)*. +The individual `TransmissionMode` and their fields are explained in *[the public library](@ref lib-pub)*. diff --git a/examples/Project.toml b/examples/Project.toml index 4eace23..de7fd79 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -1,6 +1,7 @@ [deps] EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" EnergyModelsGeography = "3f775d88-a4da-46c4-a2cc-aa9f16db6708" +EnergyModelsInvestments = "fca3f8eb-b383-437d-8e7b-aac76bb2004f" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" diff --git a/examples/investments.jl b/examples/investments.jl new file mode 100644 index 0000000..e57a2fb --- /dev/null +++ b/examples/investments.jl @@ -0,0 +1,500 @@ +using Pkg +# Activate the local environment including EnergyModelsInvestments, HiGHS, PrettyTables +Pkg.activate(@__DIR__) +# Use dev version if run as part of tests +haskey(ENV, "EMX_TEST") && Pkg.develop(path=joinpath(@__DIR__,"..")) +# Install the dependencies. +Pkg.instantiate() + +# Import the required packages +using EnergyModelsBase +using EnergyModelsGeography +using EnergyModelsInvestments +using HiGHS +using JuMP +using TimeStruct + +const EMB = EnergyModelsBase +const EMG = EnergyModelsGeography +const EMI = EnergyModelsInvestments + +""" + generate_example_data_geo() + +Generate the data for an example consisting of a simple electricity network. The simple \ +network is existing within 5 regions with differing demand. Each region has the same \ +technologies. + +The example is partly based on the provided example `network.jl` in `EnergyModelsGeography`. +It will be repalced in the near future with a simplified example. +""" + +function generate_example_data_geo() + @debug "Generate case data" + @info "Generate data coded dummy model for now (Investment Model)" + + # Retrieve the products + products = get_resources_inv() + NG = products[1] + Power = products[3] + CO2 = products[4] + + # Create input data for the areas + area_ids = [1, 2, 3, 4] + d_scale = Dict(1 => 3.0, 2 => 1.5, 3 => 1.0, 4 => 0.5) + mc_scale = Dict(1 => 2.0, 2 => 2.0, 3 => 1.5, 4 => 0.5) + gen_scale = Dict(1 => 1.0, 2 => 1.0, 3 => 1.0, 4 => 0.5) + + # Create identical areas with index according to input array + an = Dict() + transmission = [] + nodes = [] + links = [] + for a_id in area_ids + n, l = get_sub_system_data_inv( + a_id, + products; + gen_scale = gen_scale[a_id], + mc_scale = mc_scale[a_id], + d_scale = d_scale[a_id], + ) + append!(nodes, n) + append!(links, l) + + # Add area node for each subsystem + an[a_id] = n[1] + end + + # Create the individual areas + areas = [ + RefArea(1, "Oslo", 10.751, 59.921, an[1]), + RefArea(2, "Bergen", 5.334, 60.389, an[2]), + RefArea(3, "Trondheim", 10.398, 63.437, an[3]), + RefArea(4, "TromsΓΈ", 18.953, 69.669, an[4]), + ] + + # Create the investment data for the different power line investment modes + inv_data_12 = SingleInvData( + FixedProfile(500), + FixedProfile(50), + 0, + BinaryInvestment(FixedProfile(50.0)), + ) + + inv_data_13 = SingleInvData( + FixedProfile(10), + FixedProfile(100), + 0, + SemiContinuousInvestment(FixedProfile(10), FixedProfile(100)), + ) + + inv_data_23 = SingleInvData( + FixedProfile(10), + FixedProfile(50), + 20, + DiscreteInvestment(FixedProfile(6)), + ) + + inv_data_34 = SingleInvData( + FixedProfile(10), + FixedProfile(50), + 0, + ContinuousInvestment(FixedProfile(1), FixedProfile(100)), + ) + + # Create the TransmissionModes and the Transmission corridors + OverheadLine_50MW_12 = RefStatic( + "PowerLine_50", + Power, + FixedProfile(50.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [inv_data_12], + ) + OverheadLine_50MW_13 = RefStatic( + "PowerLine_50", + Power, + FixedProfile(50.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [inv_data_13], + ) + OverheadLine_50MW_23 = RefStatic( + "PowerLine_50", + Power, + FixedProfile(50.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [inv_data_23], + ) + OverheadLine_50MW_34 = RefStatic( + "PowerLine_50", + Power, + FixedProfile(50.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [inv_data_34], + ) + LNG_Ship_100MW = RefDynamic( + "LNG_100", + NG, + FixedProfile(100.0), + FixedProfile(0.05), + FixedProfile(0), + FixedProfile(0), + 2, + [], + ) + + transmission = [ + Transmission(areas[1], areas[2], [OverheadLine_50MW_12]), + Transmission(areas[1], areas[3], [OverheadLine_50MW_13]), + Transmission(areas[2], areas[3], [OverheadLine_50MW_23]), + Transmission(areas[3], areas[4], [OverheadLine_50MW_34]), + Transmission(areas[4], areas[2], [LNG_Ship_100MW]), + ] + + # Creation of the time structure and global data + T = TwoLevel(4, 1, SimpleTimes(24, 1)) + em_limits = Dict(NG => FixedProfile(1e6), CO2 => StrategicProfile([450, 400, 350, 300])) + em_cost = Dict(NG => FixedProfile(0), CO2 => FixedProfile(0)) + modeltype = InvestmentModel(em_limits, em_cost, CO2, 0.07) + + + # WIP data structure + case = Dict( + :areas => Array{Area}(areas), + :transmission => Array{Transmission}(transmission), + :nodes => Array{EMB.Node}(nodes), + :links => Array{Link}(links), + :products => products, + :T => T, + ) + return case, modeltype +end + + +function get_resources_inv() + + # Define the different resources + NG = ResourceEmit("NG", 0.2) + Coal = ResourceCarrier("Coal", 0.35) + Power = ResourceCarrier("Power", 0.0) + CO2 = ResourceEmit("CO2", 1.0) + products = [NG, Coal, Power, CO2] + + return products +end + + +function get_sub_system_data_inv( + i, + products; + gen_scale::Float64 = 1.0, + mc_scale::Float64 = 1.0, + d_scale::Float64 = 1.0, + demand = false, +) + + NG, Coal, Power, CO2 = products + + if demand == false + demand = [ + OperationalProfile([ + 20, + 20, + 20, + 20, + 25, + 30, + 35, + 35, + 40, + 40, + 40, + 40, + 40, + 35, + 35, + 30, + 25, + 30, + 35, + 30, + 25, + 20, + 20, + 20, + ]), + OperationalProfile([ + 20, + 20, + 20, + 20, + 25, + 30, + 35, + 35, + 40, + 40, + 40, + 40, + 40, + 35, + 35, + 30, + 25, + 30, + 35, + 30, + 25, + 20, + 20, + 20, + ]), + OperationalProfile([ + 20, + 20, + 20, + 20, + 25, + 30, + 35, + 35, + 40, + 40, + 40, + 40, + 40, + 35, + 35, + 30, + 25, + 30, + 35, + 30, + 25, + 20, + 20, + 20, + ]), + OperationalProfile([ + 20, + 20, + 20, + 20, + 25, + 30, + 35, + 35, + 40, + 40, + 40, + 40, + 40, + 35, + 35, + 30, + 25, + 30, + 35, + 30, + 25, + 20, + 20, + 20, + ]), + ] + demand *= d_scale + end + + j = (i - 1) * 100 + nodes = [ + GeoAvailability(j + 1, products), + RefSink( + j + 2, + StrategicProfile(demand), + Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), + Dict(Power => 1), + ), + RefSource( + j + 3, + FixedProfile(30), + FixedProfile(30 * mc_scale), + FixedProfile(100), + Dict(NG => 1), + [ + SingleInvData( + FixedProfile(1000), # capex [€/kW] + FixedProfile(200), # max installed capacity [kW] + ContinuousInvestment(FixedProfile(10), FixedProfile(200)), # investment mode + ), + ], + ), + RefSource( + j + 4, + FixedProfile(9), + FixedProfile(9 * mc_scale), + FixedProfile(100), + Dict(Coal => 1), + [ + SingleInvData( + FixedProfile(1000), # capex [€/kW] + FixedProfile(200), # max installed capacity [kW] + 0, + ContinuousInvestment(FixedProfile(10), FixedProfile(200)), # investment mode + ), + ], + ), + RefNetworkNode( + j + 5, + FixedProfile(0), + FixedProfile(5.5 * mc_scale), + FixedProfile(100), + Dict(NG => 2), + Dict(Power => 1, CO2 => 0), + [ + SingleInvData( + FixedProfile(600), # capex [€/kW] + FixedProfile(25), # max installed capacity [kW] + ContinuousInvestment(FixedProfile(0), FixedProfile(25)), # investment mode + ), + CaptureEnergyEmissions(0.9), + ], + ), + RefNetworkNode( + j + 6, + FixedProfile(0), + FixedProfile(6 * mc_scale), + FixedProfile(100), + Dict(Coal => 2.5), + Dict(Power => 1), + [ + SingleInvData( + FixedProfile(800), # capex [€/kW] + FixedProfile(25), # max installed capacity [kW] + ContinuousInvestment(FixedProfile(0), FixedProfile(25)), # investment mode + ), + EmissionsEnergy(), + ], + ), + RefStorage{AccumulatingEmissions}( + j + 7, + StorCapOpex(FixedProfile(0), FixedProfile(9.1 * mc_scale), FixedProfile(100)), + StorCap(FixedProfile(0)), + CO2, + Dict(CO2 => 1, Power => 0.02), + Dict(CO2 => 1), + [ + StorageInvData( + charge = NoStartInvData( + FixedProfile(500), + FixedProfile(600), + ContinuousInvestment(FixedProfile(0), FixedProfile(600)), + ), + level = NoStartInvData( + FixedProfile(500), + FixedProfile(600), + ContinuousInvestment(FixedProfile(0), FixedProfile(600)), + ), + ), + ], + ), + RefNetworkNode( + j + 8, + FixedProfile(0), + FixedProfile(0 * mc_scale), + FixedProfile(0), + Dict(Coal => 2.5), + Dict(Power => 1), + [ + SingleInvData( + FixedProfile(10000), # capex [€/kW] + FixedProfile(25), # max installed capacity [kW] + ContinuousInvestment(FixedProfile(0), FixedProfile(2)), # investment mode + ), + EmissionsEnergy(), + ], + ), + RefStorage{AccumulatingEmissions}( + j + 9, + StorCapOpex(FixedProfile(3), FixedProfile(0 * mc_scale), FixedProfile(0)), + StorCap(FixedProfile(5)), + CO2, + Dict(CO2 => 1, Power => 0.02), + Dict(CO2 => 1), + [ + StorageInvData( + charge = NoStartInvData( + FixedProfile(500), + FixedProfile(30), + ContinuousInvestment(FixedProfile(0), FixedProfile(3)), + ), + level = NoStartInvData( + FixedProfile(500), + FixedProfile(50), + ContinuousInvestment(FixedProfile(0), FixedProfile(2)), + ), + ), + ], + ), + RefNetworkNode( + j + 10, + FixedProfile(0), + FixedProfile(0 * mc_scale), + FixedProfile(0), + Dict(Coal => 2.5), + Dict(Power => 1), + [ + SingleInvData( + FixedProfile(10000), # capex [€/kW] + FixedProfile(10000), # max installed capacity [kW] + ContinuousInvestment(FixedProfile(0), FixedProfile(10000)), # investment mode + ), + EmissionsEnergy(), + ], + ), + ] + + links = [ + Direct(j * 10 + 15, nodes[1], nodes[5], Linear()) + Direct(j * 10 + 16, nodes[1], nodes[6], Linear()) + Direct(j * 10 + 17, nodes[1], nodes[7], Linear()) + Direct(j * 10 + 18, nodes[1], nodes[8], Linear()) + Direct(j * 10 + 19, nodes[1], nodes[9], Linear()) + Direct(j * 10 + 110, nodes[1], nodes[10], Linear()) + Direct(j * 10 + 12, nodes[1], nodes[2], Linear()) + Direct(j * 10 + 31, nodes[3], nodes[1], Linear()) + Direct(j * 10 + 41, nodes[4], nodes[1], Linear()) + Direct(j * 10 + 51, nodes[5], nodes[1], Linear()) + Direct(j * 10 + 61, nodes[6], nodes[1], Linear()) + Direct(j * 10 + 71, nodes[7], nodes[1], Linear()) + Direct(j * 10 + 81, nodes[8], nodes[1], Linear()) + Direct(j * 10 + 91, nodes[9], nodes[1], Linear()) + Direct(j * 10 + 101, nodes[10], nodes[1], Linear()) + ] + return nodes, links +end + + +# Generate case data +case, model = generate_example_data_geo() +optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) +m = EMG.create_model(case, model) +set_optimizer(m, optimizer) +optimize!(m) + +solution_summary(m) + +# Uncomment to print all the constraints set in the model. +# print(m) + +solution_summary(m) diff --git a/ext/EMIExt/EMIExt.jl b/ext/EMIExt/EMIExt.jl new file mode 100644 index 0000000..dd7eda9 --- /dev/null +++ b/ext/EMIExt/EMIExt.jl @@ -0,0 +1,19 @@ +module EMIExt + +using EnergyModelsBase +using EnergyModelsGeography +using EnergyModelsInvestments +using JuMP +using TimeStruct +using SparseVariables + +const EMB = EnergyModelsBase +const EMG = EnergyModelsGeography +const EMI = EnergyModelsInvestments +const TS = TimeStruct + +include("model.jl") +include("utils.jl") +include("legacy_constructor.jl") + +end diff --git a/ext/EMIExt/legacy_constructor.jl b/ext/EMIExt/legacy_constructor.jl new file mode 100644 index 0000000..b3c8aa9 --- /dev/null +++ b/ext/EMIExt/legacy_constructor.jl @@ -0,0 +1,86 @@ + +""" + TransInvData(; + capex_trans::TimeProfile, + trans_max_inst::TimeProfile, + trans_max_add::TimeProfile, + trans_min_add::TimeProfile, + inv_mode::Investment = ContinuousInvestment(), + trans_start::Union{Real, Nothing} = nothing, + trans_increment::TimeProfile = FixedProfile(0), + capex_trans_offset::TimeProfile = FixedProfile(0), + ) + +Legacy constructor for a `TransInvData`. + +The new storage descriptions allows now for a reduction in functions which is used +to make `EnergModelsInvestments` less dependent on `EnergyModelsBase`. + +The core changes to the existing structure is the move of the required parameters to the +type Investment (_e.g._, the minimum and maximum added capacity is only required +for investment mdodes that require these parameters) as well as moving the `lifetime` to the +type [`LifetimeMode`], when required. + +See the _[documentation](https://energymodelsx.github.io/EnergyModelsInvestments.jl/stable/how-to/update-models)_ +for further information regarding how you can translate your existing model to the new model. +""" +function TransInvData(; + capex_trans::TimeProfile, + trans_max_inst::TimeProfile, + trans_max_add::TimeProfile, + trans_min_add::TimeProfile, + inv_mode::Investment = ContinuousInvestment(), + trans_start::Union{Real, Nothing} = nothing, + trans_increment::TimeProfile = FixedProfile(0), + capex_trans_offset::TimeProfile = FixedProfile(0), +) + # Create the new investment mode structures + if isa(inv_mode, BinaryInvestment) + @error( + "BinaryInvestment() cannot use the constructor as it is not possible to " * + "deduce the capacity for the investment. You have to instead use the new " * + "types as outlined in the documentation (https://energymodelsx.github.io/EnergyModelsInvestments.jl/stable/how-to/update-models)" + ) + return + elseif isa(inv_mode, FixedInvestment) + @error( + "FixedInvestment() cannot use the constructor as it is not possible to " * + "deduce the capacity for the investment. You have to instead use the new " * + "types as outlined in the documentation (https://energymodelsx.github.io/EnergyModelsInvestments.jl/stable/how-to/update-models)" + ) + return + elseif isa(inv_mode, DiscreteInvestment) + tmp_inv_mode = DiscreteInvestment(trans_increment) + elseif isa(inv_mode, ContinuousInvestment) + tmp_inv_mode = ContinuousInvestment(trans_min_add, trans_max_add) + elseif isa(inv_mode, SemiContinuousInvestment) + tmp_inv_mode = SemiContinuousInvestment(trans_min_add, trans_max_add) + elseif isa(inv_mode, SemiContinuousOffsetInvestment) + tmp_inv_mode = SemiContinuousOffsetInvestment(trans_min_add, trans_max_add, capex_trans_offset) + end + + @warn( + "The used implementation of a `TransInvData` will be discontinued in the near " * + "future. See the documentation for the new implementation using the type " * + "`SingleInvData` in the section on _How to update your model to the latest versions_.\n" * + "The core change is that we allow the individual parameters are moved to the " * + "field `inv_mode` and we allow now for `life_mode`.\n", + maxlog = 1, + ) + + # Create the new generalized investment data + if isnothing(trans_start) + return SingleInvData( + capex_trans, + trans_max_inst, + tmp_inv_mode, + ) + else + return SingleInvData( + capex_trans, + trans_max_inst, + trans_start, + tmp_inv_mode, + ) + end +end diff --git a/ext/EMIExt/model.jl b/ext/EMIExt/model.jl new file mode 100644 index 0000000..0bcc715 --- /dev/null +++ b/ext/EMIExt/model.jl @@ -0,0 +1,97 @@ +""" + EMG.update_objective(m, 𝒩, 𝒯, 𝒫, ℒᡗʳᡃⁿ˒, modeltype::EMB.AbstractInvestmentModel) + +Create objective function overloading the default from EMB for EMB.AbstractInvestmentModel. + +Maximize Net Present Value from revenues, investments (CAPEX) and operations (OPEX) + +## TODO: +# * consider passing expression around for updating +# * consider reading objective and adding terms/coefficients (from model object `m`) +""" +function EMG.update_objective(m, 𝒯, β„³, modeltype::EMB.AbstractInvestmentModel) + + # Extraction of data + 𝒯ᴡⁿᡛ = strategic_periods(𝒯) + ℳᴡⁿᡛ = filter(has_investment, β„³) + obj = JuMP.objective_function(m) + disc = Discounter(discount_rate(modeltype), 𝒯) + + # Update of the cost function for modes with investments + for t_inv ∈ 𝒯ᴡⁿᡛ, tm ∈ β„³ + if tm ∈ ℳᴡⁿᡛ + obj -= objective_weight(t_inv, disc) * m[:trans_cap_capex][tm, t_inv] + end + obj -= duration_strat(t_inv) * objective_weight(t_inv, disc, type="avg") * + m[:trans_opex_fixed][tm, t_inv] + obj -= duration_strat(t_inv) * objective_weight(t_inv, disc, type="avg") * + m[:trans_opex_var][tm, t_inv] + end + + @objective(m, Max, obj) + +end + +""" + EMG.variables_trans_capex(m, 𝒯, β„³,, modeltype::EMB.AbstractInvestmentModel) + +Create variables for the capital costs for the investments in transmission. + +Additional variables for investment in capacity: +* `:trans_cap_capex` - CAPEX costs for increases in the capacity of a transmission mode +* `:trans_cap_current` - installed capacity for storage in each strategic period +* `:trans_cap_add` - added capacity +* `:trans_cap_rem` - removed capacity +* `:trans_cap_invest_b` - binary variable whether investments in capacity are happening +* `:trans_cap_remove_b` - binary variable whether investments in capacity are removed +""" +function EMG.variables_trans_capex(m, 𝒯, β„³, modeltype::EMB.AbstractInvestmentModel) + + ℳᴡⁿᡛ = filter(has_investment, β„³) + 𝒯ᴡⁿᡛ = strategic_periods(𝒯) + + # Add transmission specific investment variables for each strategic period: + @variable(m, trans_cap_capex[ℳᴡⁿᡛ, 𝒯ᴡⁿᡛ] >= 0) + @variable(m, trans_cap_current[ℳᴡⁿᡛ, 𝒯ᴡⁿᡛ] >= 0) # Installed capacity + @variable(m, trans_cap_add[ℳᴡⁿᡛ, 𝒯ᴡⁿᡛ] >= 0) # Add capacity + @variable(m, trans_cap_rem[ℳᴡⁿᡛ, 𝒯ᴡⁿᡛ] >= 0) # Remove capacity + @variable(m, trans_cap_invest_b[ℳᴡⁿᡛ, 𝒯ᴡⁿᡛ]; container=IndexedVarArray) + @variable(m, trans_cap_remove_b[ℳᴡⁿᡛ, 𝒯ᴡⁿᡛ]; container=IndexedVarArray) +end + +""" + EMG.constraints_capacity_installed( + m, + tm::TransmissionMode, + 𝒯::TimeStructure, + modeltype::EMB.AbstractInvestmentModel, + ) + +When the modeltype is an investment model, the function introduces the related constraints +for the capacity expansion. The investment mode and lifetime mode are used for adding +constraints. + +The default function only accepts nodes with [`SingleInvData`](@extref EnergyModelsBase.SingleInvData). If you have several +capacities for investments, you have to dispatch specifically on the function. This is +implemented for `Storage` nodes. +""" +function EMG.constraints_capacity_installed( + m, + tm::TransmissionMode, + 𝒯::TimeStructure, + modeltype::EMB.AbstractInvestmentModel, +) + if has_investment(tm) + # Extract the investment data and the discount rate + disc_rate = discount_rate(modeltype) + inv_data = EMI.investment_data(tm, :cap) + 𝒯ᴡⁿᡛ = strategic_periods(𝒯) + + # Add the investment constraints + EMI.add_investment_constraints(m, tm, inv_data, :cap, :trans_cap, 𝒯ᴡⁿᡛ, disc_rate) + else + for t ∈ 𝒯 + fix(m[:trans_cap][tm, t], EMB.capacity(tm, t); force=true) + end + end +end diff --git a/ext/EMIExt/utils.jl b/ext/EMIExt/utils.jl new file mode 100644 index 0000000..b65fee2 --- /dev/null +++ b/ext/EMIExt/utils.jl @@ -0,0 +1,24 @@ +""" + EMI.get_var_inst(m, prefix::Symbol, tm::EMG.TransmissionMode) + +When the transmission mode `tm` is used as conditional input, it extracts only the variable +for the specified transmission mode. +""" +EMI.get_var_inst(m, prefix::Symbol, tm::EMG.TransmissionMode) = m[Symbol(prefix)][tm, :] + + +function EMI.has_investment(tm::EMG.TransmissionMode) + ( + hasproperty(tm, :data) && + !isnothing(findfirst(data -> typeof(data) <: InvestmentData, tm.data)) # TODO: access function for data + ) +end + +EMI.investment_data(tm::EMG.TransmissionMode) = + tm.data[findfirst(data -> typeof(data) <: InvestmentData, tm.data)] + +EMI.investment_data(n::EMG.TransmissionMode, field::Symbol) = getproperty(investment_data(n), field) + + +EMI.start_cap(tm::EMG.TransmissionMode, t_inv, inv_data::NoStartInvData, cap) = + capacity(tm, t_inv) diff --git a/src/EnergyModelsGeography.jl b/src/EnergyModelsGeography.jl index 7dd3f8c..defad0f 100644 --- a/src/EnergyModelsGeography.jl +++ b/src/EnergyModelsGeography.jl @@ -14,6 +14,7 @@ using TimeStruct include(joinpath("structures", "area.jl")) include(joinpath("structures", "mode.jl")) include(joinpath("structures", "transmission.jl")) +include(joinpath("structures", "data.jl")) include("checks.jl") include("model.jl") include("constraint_functions.jl") @@ -28,6 +29,9 @@ export Transmission, TransmissionMode export RefStatic, RefDynamic export PipeMode, PipeSimple, PipeLinepackSimple +# Export the legacy constructor for transmission investment data +export TransInvData + # Export commonly used functions for extracting fields in `Area`s export name, availability_node, limit_resources, exchange_limit, exchange_resources diff --git a/src/structures/data.jl b/src/structures/data.jl new file mode 100644 index 0000000..ab67368 --- /dev/null +++ b/src/structures/data.jl @@ -0,0 +1,26 @@ +""" + TransInvData(; + capex_trans::TimeProfile, + trans_max_inst::TimeProfile, + trans_max_add::TimeProfile, + trans_min_add::TimeProfile, + inv_mode::Investment = ContinuousInvestment(), + trans_start::Union{Real, Nothing} = nothing, + trans_increment::TimeProfile = FixedProfile(0), + capex_trans_offset::TimeProfile = FixedProfile(0), + ) + +Legacy constructor for a `InvData`. + +The new storage descriptions allows now for a reduction in functions which is used +to make `EnergModelsInvestments` less dependent on `EnergyModelsBase`. + +The core changes to the existing structure is the move of the required parameters to the +type Investment (_e.g._, the minimum and maximum added capacity is only required +for investment mdodes that require these parameters) as well as moving the `lifetime` to the +type [`LifetimeMode`], when required. + +See the _[documentation](https://energymodelsx.github.io/EnergyModelsInvestments.jl/stable/how-to/update-models)_ +for further information regarding how you can translate your existing model to the new model. +""" +TransInvData(nothing) = nothing diff --git a/test/test_examples.jl b/test/test_examples.jl index 7d6c35f..86565f4 100644 --- a/test/test_examples.jl +++ b/test/test_examples.jl @@ -1,11 +1,13 @@ ENV["EMX_TEST"] = true # Set flag for example scripts to check if they are run as part of the tests @testset "Run examples" begin - exdir = joinpath(@__DIR__, "../examples") + exdir = joinpath(@__DIR__, "..", "examples") files = filter(endswith(".jl"), readdir(exdir)) - for file in files + for file ∈ files @testset "Example $file" begin - include(joinpath(exdir, file)) + redirect_stdio(stdout=devnull, stderr=devnull) do + include(joinpath(exdir, file)) + end @test termination_status(m) == MOI.OPTIMAL end end diff --git a/test/test_investments.jl b/test/test_investments.jl new file mode 100644 index 0000000..a05b507 --- /dev/null +++ b/test/test_investments.jl @@ -0,0 +1,291 @@ +""" + optimize_geo(case) + +Optimize the `case`. +""" +function optimize_geo(case, modeltype) + m = EMG.create_model(case, modeltype) + set_optimizer(m, OPTIMIZER) + optimize!(m) + return m +end + +# Test set for analysing the proper behaviour when no investment was included +@testset "Unidirectional transmission without investments" begin + + # Creation and run of the optimization problem + case, modeltype = small_graph_geo() + m = optimize_geo(case, modeltype) + + general_tests(m) + + # Extraction of required data + 𝒯 = case[:T] + 𝒯ᴡⁿᡛ = strategic_periods(𝒯) + sink = case[:nodes][4] + tr_osl_trd = case[:transmission][1] + tm = modes(tr_osl_trd)[1] + + # Test identifying that the proper deficit is calculated + @test sum( + value.(m[:sink_deficit][sink, t]) β‰ˆ capacity(sink, t) - capacity(tm, t) for t ∈ 𝒯 + ) == length(𝒯) + + # Test showing that no investment variables are created + @test isempty((m[:trans_cap_current])) + @test isempty((m[:trans_cap_add])) + @test isempty((m[:trans_cap_rem])) + @test isempty((m[:trans_cap_invest_b])) + @test isempty((m[:trans_cap_remove_b])) +end + +# Test set for continuous investments +@testset "Unidirectional transmission with ContinuousInvestment" begin + + # Creation and run of the optimization problem + inv_data = SingleInvData( + FixedProfile(10), # capex [€/kW] + FixedProfile(250), # max installed capacity [kW] + 0, # initial capacity [kW] + ContinuousInvestment(FixedProfile(0), FixedProfile(30)), + ) + + case, modeltype = small_graph_geo(;inv_data) + m = optimize_geo(case, modeltype) + + general_tests(m) + + # Extraction of required data + 𝒯 = case[:T] + 𝒯ᴡⁿᡛ = strategic_periods(𝒯) + sink = case[:nodes][4] + tr_osl_trd = case[:transmission][1] + tm = modes(tr_osl_trd)[1] + inv_data = EMI.investment_data(tm, :cap) + + # Test identifying that the there is no deficit + @test sum(value.(m[:sink_deficit][sink, t]) == 0 for t ∈ 𝒯) == length(𝒯) + + # Test showing that the investments are as expected + for (t_inv_prev, t_inv) ∈ withprev(𝒯ᴡⁿᡛ) + if isnothing(t_inv_prev) + @testset "First investment period" begin + for t ∈ t_inv + @test ( + value.(m[:trans_cap_add][tm, t_inv]) β‰ˆ + capacity(sink, t) - inv_data.initial + ) atol = TEST_ATOL + end + end + else + @testset "Subsequent investment periods" begin + for t ∈ t_inv + @test ( + value.(m[:trans_cap_add][tm, t_inv]) β‰ˆ + capacity(sink, t) - value.(m[:trans_cap_current][tm, t_inv_prev]) + ) atol = TEST_ATOL + end + end + end + end + +end + +# Test set for semicontinuous investments +@testset "Unidirectional transmission with SemiContinuousInvestment" begin + + # Creation and run of the optimization problem + inv_data = SingleInvData( + FixedProfile(10), # capex [€/kW] + FixedProfile(250), # max installed capacity [kW] + 0, # initial capacity [kW] + SemiContinuousInvestment(FixedProfile(10), FixedProfile(30)), + ) + + case, modeltype = small_graph_geo(;inv_data) + m = optimize_geo(case, modeltype) + + general_tests(m) + + # Extraction of required data + 𝒯 = case[:T] + 𝒯ᴡⁿᡛ = strategic_periods(𝒯) + sink = case[:nodes][4] + tr_osl_trd = case[:transmission][1] + tm = modes(tr_osl_trd)[1] + inv_data = EMI.investment_data(tm, :cap) + + # Test identifying that the there is no deficit + @test sum(value.(m[:sink_deficit][sink, t]) == 0 for t ∈ 𝒯) == length(𝒯) + + # Test showing that the investments are as expected + for (t_inv_prev, t_inv) ∈ withprev(𝒯ᴡⁿᡛ) + @testset "Investment period $(t_inv.sp)" begin + @testset "Invested capacity" begin + if isnothing(t_inv_prev) + for t ∈ t_inv + @test ( + value.(m[:trans_cap_add][tm, t_inv]) >= max( + capacity(sink, t) - inv_data.initial, + EMI.min_add(inv_data, t) * + value.(m[:trans_cap_invest_b][tm, t_inv]), + ) + ) + end + else + for t ∈ t_inv + @test ( + value.(m[:trans_cap_add][tm, t_inv]) βͺ† max( + capacity(sink, t) - + value.(m[:trans_cap_current][tm, t_inv_prev]), + EMI.min_add(inv_data, t) * + value.(m[:trans_cap_invest_b][tm, t_inv]), + ) + ) + end + end + end + + # Test that the binary value is regulating the investments + @testset "Binary value" begin + if value.(m[:trans_cap_invest_b][tm, t_inv]) β‰ˆ 0 + atol = TEST_ATOL + @test value.(m[:trans_cap_add][tm, t_inv]) β‰ˆ 0 atol = TEST_ATOL + else + @test value.(m[:trans_cap_add][tm, t_inv]) βͺ† 0 + end + end + end + end + + # Test that the variable cap_invest_b is a binary + @test sum(is_binary(m[:trans_cap_invest_b][tm, t_inv]) for t_inv ∈ 𝒯ᴡⁿᡛ) == length(𝒯ᴡⁿᡛ) +end + +# Test set for semicontinuous investments with offsets in the cost +@testset "Unidirectional transmission with SemiContinuousOffsetInvestment" begin + + # Creation and run of the optimization problem + inv_data = SingleInvData( + FixedProfile(10), # capex [€/kW] + FixedProfile(250), # max installed capacity [kW] + 0, # initial capacity [kW] + SemiContinuousOffsetInvestment( + FixedProfile(10), + FixedProfile(30), + FixedProfile(10), + ), + ) + + case, modeltype = small_graph_geo(;inv_data) + m = optimize_geo(case, modeltype) + + general_tests(m) + + # Extraction of required data + 𝒯 = case[:T] + 𝒯ᴡⁿᡛ = strategic_periods(𝒯) + sink = case[:nodes][4] + tr_osl_trd = case[:transmission][1] + tm = modes(tr_osl_trd)[1] + inv_data = EMI.investment_data(tm, :cap) + inv_mode = EMI.investment_mode(inv_data) + + # Test identifying that the there is no deficit + @test sum(value.(m[:sink_deficit][sink, t]) == 0 for t ∈ 𝒯) == length(𝒯) + + # Test showing that the investments are as expected + for (t_inv_prev, t_inv) ∈ withprev(𝒯ᴡⁿᡛ) + @testset "Investment period $(t_inv.sp)" begin + @testset "Invested capacity" begin + if isnothing(t_inv_prev) + for t ∈ t_inv + @test ( + value.(m[:trans_cap_add][tm, t_inv]) >= max( + capacity(sink, t) - inv_data.initial, + EMI.min_add(inv_data, t) * + value.(m[:trans_cap_invest_b][tm, t_inv]), + ) + ) + end + else + for t ∈ t_inv + @test ( + value.(m[:trans_cap_add][tm, t_inv]) βͺ† max( + capacity(sink, t) - + value.(m[:trans_cap_current][tm, t_inv_prev]), + EMI.min_add(inv_data, t) * + value.(m[:trans_cap_invest_b][tm, t_inv]), + ) + ) + end + end + end + + # Test that the binary value is regulating the investments + @testset "Binary value" begin + if value.(m[:trans_cap_invest_b][tm, t_inv]) β‰ˆ 0 + @test value.(m[:trans_cap_add][tm, t_inv]) β‰ˆ 0 atol = TEST_ATOL + else + @test value.(m[:trans_cap_add][tm, t_inv]) βͺ† 0 + end + end + end + end + @testset "Investment costs" begin + @test sum( + value(m[:trans_cap_add][tm, t_inv]) * EMI.capex(inv_data, t_inv) + + EMI.capex_offset(inv_mode, t_inv) * value(m[:trans_cap_invest_b][tm, t_inv]) β‰ˆ + value(m[:trans_cap_capex][tm, t_inv]) for t_inv ∈ 𝒯ᴡⁿᡛ, atol in TEST_ATOL + ) == length(𝒯ᴡⁿᡛ) + end + + # Test that the variable cap_invest_b is a binary + @test sum(is_binary(m[:trans_cap_invest_b][tm, t_inv]) for t_inv ∈ 𝒯ᴡⁿᡛ) == length(𝒯ᴡⁿᡛ) +end + +# Test set for discrete investments +@testset "Unidirectional transmission with DiscreteInvestment" begin + + # Creation and run of the optimization problem + inv_data = SingleInvData( + FixedProfile(10), # capex [€/kW] + FixedProfile(250), # max installed capacity [kW] + 0, # initial capacity [kW] + DiscreteInvestment(FixedProfile(5)), + ) + + case, modeltype = small_graph_geo(;inv_data) + m = optimize_geo(case, modeltype) + + general_tests(m) + + # Extraction of required data + 𝒯 = case[:T] + 𝒯ᴡⁿᡛ = strategic_periods(𝒯) + sink = case[:nodes][4] + tr_osl_trd = case[:transmission][1] + tm = modes(tr_osl_trd)[1] + inv_data = EMI.investment_data(tm, :cap) + + # Test identifying that the there is no deficit + @test sum(value.(m[:sink_deficit][sink, t]) == 0 for t ∈ 𝒯) == length(𝒯) + + # Test showing that the investments are as expected + for t_inv ∈ 𝒯ᴡⁿᡛ + @testset "Invested capacity $(t_inv.sp)" begin + if value.(m[:trans_cap_invest_b][tm, t_inv]) β‰ˆ 0 + atol = TEST_ATOL + @test value.(m[:trans_cap_add][tm, t_inv]) β‰ˆ 0 atol = TEST_ATOL + else + @test value.(m[:trans_cap_add][tm, t_inv]) β‰ˆ + EMI.increment(inv_data, t_inv) * + value.(m[:trans_cap_invest_b][tm, t_inv]) + end + end + end + + # Test that the variable cap_invest_b is a binary + @test sum(is_integer(m[:trans_cap_invest_b][tm, t_inv]) for t_inv ∈ 𝒯ᴡⁿᡛ) == + length(𝒯ᴡⁿᡛ) +end