Use EnergyModelsInvestments

EnergyModelsInvestments was initially designed as extension package for EnergyModelsBase. Hence, many design choices are impacted by the requirements of EnergyModelsBase. We realized however that it can be beneficial to make it independent of EnergyModelsBase to use the incorporated methods in other energy system optimization models that may be under development.

Using EnergyModelsInvestments requires the following implementations in your model.

Implementation

If you are uncertain on how to best implement investment options, it can be beneficial to investigate the approaches chosen in EnergyModelsBase and EnergyModelsGeography.

Auxiliary functions

The are several additional functions which are specific for the individual types. This functions are either used as example for a simplified interface or alternatively required in EnergyModelsInvestments. The latter requires you to implement methods within your model through multiple dispatch as they are called within EnergyModelsInvestments.

Required methods

When you are using the type NoStartInvData, you have to specify the function

start_cap(element, t_inv, inv_data::NoStartInvData, cap)

for your type. The main reason is that in this case you do not specify the initial capacity in EnergyModelsInvestments, but instead deduce it from the provided initial capacity in your energiy system optimization model.

In the case of EnergyModelsBase, this is given through the methods

EMI.start_cap(n::EMB.Node, t_inv, inv_data::NoStartInvData, cap) =
    capacity(n, t_inv)
EMI.start_cap(n::Storage, t_inv, inv_data::NoStartInvData, cap) =
    capacity(getproperty(n, cap), t_inv)

and the application of the internal function capacity().

Simplified interface functions

EnergyModelsBase creates new types for the investment data instead of using NoStartInvData and StartInvData. The background for this approach is that we like to have the potential for multiple capacities within a node, e.g., charge, level, and discharge capacities in Storage nodes as well as the potential for investments in the individual capacities. Hence, we create new methods for the function investment_data() to directly access the individual fields of the new investment data types from the node level. In addition, we incorporate the function has_investment() to allow a limitation of investments to a subset of the nodes.

Both functions are not directly used in EnergyModelsInvestments in this context. Hence, it is not required to declare new methods for these functions. It can however be beneficial to create methods as it may simplify the subsequent structure.

Variable declarations

As outlined in Optimization variables, we require that the user follows a given variable naming convention. The variables used within EnergyModelsInvestments are extracted with the help of the following functions:

The provided functions utilize a prefix::Symbol argument which is the corresponding prefix for all variables. Optimization variables explains the required names for the variables with prefix = :cap.

Although it is in general possible to provide dispatch on these functions for new types, we unfortunately require in the current implementation that the naming convention has to be followed at least for the Auxiliary variables as SparseVariables requires the extraction of all variables with a given name before the insertion.

Hence, it is best to declare all variables as, e.g. using the prefix :cap:

𝒯ᴵⁿᵛ = strategic_periods(𝒯)

# Add investment variables for reference nodes for each strategic period:
@variable(m, cap_inst[𝒩ᴵⁿᵛ] >= 0)
@variable(m, cap_capex[𝒩ᴵⁿᵛ, 𝒯ᴵⁿᵛ] >= 0)
@variable(m, cap_current[𝒩ᴵⁿᵛ, 𝒯ᴵⁿᵛ] >= 0)
@variable(m, cap_add[𝒩ᴵⁿᵛ, 𝒯ᴵⁿᵛ] >= 0)
@variable(m, cap_rem[𝒩ᴵⁿᵛ, 𝒯ᴵⁿᵛ] >= 0)
@variable(m, cap_invest_b[𝒩ᴵⁿᵛ, 𝒯ᴵⁿᵛ] >= 0; container = IndexedVarArray)
@variable(m, cap_remove_b[𝒩ᴵⁿᵛ, 𝒯ᴵⁿᵛ] >= 0; container = IndexedVarArray)

with 𝒩ᴵⁿᵛ corresponding to nodes with investments.

Inclusion of investment constraints

Investment constraints are included through the function

function add_investment_constraints(
    m,                          # JuMP model
    element,                    # Element for which investments are added
    inv_data::AbstractInvData,  # Investment data for the element
    cap,                        # Capacity that has investments
    prefix,                     # Used prefix in variable declaration
    𝒯ᴵⁿᵛ::TS.StratPeriods,      # Strategic periods
    disc_rate::Float64,         # Discount rate in absolute values
)

The individual input is as well described in the documentation

This functions includes constraints on the capacity and calculates the capital expenses for each element. There are two main points one has to consider:

  1. We add the investments constraints for each individual element. In the case of multiple elements, it is necessary to iterate through the vector of elements.
  2. We add the investment constraints for each individual capacity cap. This argument is only relevant if an element has multiple capacities as it is the case for Storage nodes in EnergyModelsBase.

Consequently, you have to iterate through all elements and their capacities cap if you want to add investment constraints.

Given the example nodes above with single capacities, this would be given as:

for n ∈ 𝒩ᴵⁿᵛ
    # Extract the investment data, the discount rate, and the strategic periods
    disc_rate = discount_rate(modeltype)
    inv_data = investment_data(n, :cap)
    𝒯ᴵⁿᵛ = strategic_periods(𝒯)

    # Add the investment constraints
    EMI.add_investment_constraints(m, n, inv_data, :cap, :cap, 𝒯ᴵⁿᵛ, disc_rate)
end

Note that we included in this example a method for investment_data() as outlined above.

Updating the objective function

EnergyModelsInvestments does not change the objective function. This would require detailed knowledge regarding the individual contributing factors. Hence, we decided that it is beneficial to instead calculate the capital expense contributions in each strategic period. As a consequence, when using EnergyModelsInvestments, you have to include the variable $\texttt{cap\_capex}$ (if you used prefix = :cap) for all elements with investments to your objective function.

An illustrative example is given in the function EMB.objective(m, 𝒩, 𝒯, 𝒫, modeltype::AbstractInvestmentModel) compared to the function objective(m, 𝒩, 𝒯, 𝒫, modeltype::EnergyModel) in EnergyModelsBase.

Note

We plan to add the links to the functions once we have both updates registered.