Flexible demand

This example uses the following two nodes from EnergyModelsFlex:

  • PeriodDemandSink to set a demand per day instead of per operational period and
  • MinUpDownTimeNode to force the production to run for a minimum number of hours once it has started, and to be shut off for a minimum number of hours once it has stopped.

Prerequisites

The example requires EnergyModelsBase, EnergyModelsFlex, and TimeStruct for building the model, JuMP and the solver HiGHS for solving it, and PrettyTables for displaying the results. All packages are included in the environment of the examples folder.

using EnergyModelsBase
using EnergyModelsFlex
using TimeStruct

using HiGHS
using JuMP
using PrettyTables

Construction of the model

Declare the required resources

power = ResourceCarrier("Power", 0)
product = ResourceCarrier("Product", 0)
co2 = ResourceEmit("CO₂", 0)
𝒫 = [power, product, co2]
3-element Vector{Resource}:
 Power
 Product
 CO₂

Define a time structure for a single week, modelled with hourly resolution

𝒯 = TwoLevel(1, 1, SimpleTimes(7 * 24, 1))
TwoLevel{Int64, Int64, SimpleTimes{Int64}}(1, [1], SimpleTimes{Int64}[SimpleTimes{Int64}(168, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1  …  1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 168)], 1.0)

Some arbitrary electricity prices. Note that we let the electricity be free during the weekend. This would be a huge incentive to produce during the weekend, if we allowed the PeriodDemandSink to have capacity during the weekend.

day = [1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 9, 8, 7, 6, 5, 4, 3, 2]
el_cost = vcat(repeat(day, 5), zeros(48))

grid = RefSource(
    "grid",                         # Node id
    FixedProfile(1e12),             # Capacity in kW, virtually infinite
    OperationalProfile(el_cost),    # Variable OPEX in EUR/kWh
    FixedProfile(0),                # Fixed OPEX in EUR/kW/week
    Dict(power => 1),               # Output from the node with output ratio
)
n_grid

The production can only run between 6-20 on weekdays, with a capacity of 300 unit/h. First, define the maximum capacity for a regular weekday (24 hours). The capacity is 0 between midnight and 6 am, 300 unit/h between 6 am and 8 pm, and 0 again between 8 pm and midnight.

weekday_prod = vcat(zeros(6), fill(300, 14), zeros(4))
24-element Vector{Float64}:
   0.0
   0.0
   0.0
   0.0
   0.0
   0.0
 300.0
 300.0
 300.0
 300.0
   ⋮
 300.0
 300.0
 300.0
 300.0
 300.0
   0.0
   0.0
   0.0
   0.0

Repeat the weekday 5 times for a workweek, followed by no production during the weekend

week_prod = vcat(repeat(weekday_prod, 5), fill(0, 2 * 24))

demand = PeriodDemandSink(
    "demand_product",               # Node id
    OperationalProfile(week_prod),  # Capacity in unit/h
    24,                             # Duration of a demand period in hours (one day)
    PartitionProfile(vcat(ones(5)*1500, [0, 0])), # Demand in each demand period in unit
    # Line above: Demand of 1500 unit per weekday and no demand (0) during the weekend
    Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e8)),
    # Line above: Surplus and deficit penalty for each demand period in EUR/unit
    Dict(product => 1),             # Energy demand and corresponding ratio
)
n_demand_product

Define the production line using MinUpDownTimeNode

min_up_time = 8
min_down_time = 5
line = MinUpDownTimeNode(
    "line",                         # Node id
    FixedProfile(300),              # Capacity in unit/h
    FixedProfile(0),                # Variable OPEX in EUR/unit
    FixedProfile(0),                # Fixed OPEX in EUR/(unit/h)/week
    Dict(power => 1),               # Input to the node with input ratio
    Dict(product => 1),             # Output from the node with output ratio
    min_up_time,                    # Minimum up time in hours
    min_down_time,                  # Minimum down time in hours
    50,                             # Minimum load when the line is running in unit/h
    300,                            # Maximum load when the line is running in unit/h
)
n_line

Define the simple energy system

𝒩 = [grid, line, demand]
ℒ = [Direct("grid-line", grid, line), Direct("line-demand", line, demand)]
case = Case(𝒯, 𝒫, [𝒩, ℒ])
Case(TwoLevel{Int64, Int64, SimpleTimes{Int64}}(1, [1], SimpleTimes{Int64}[SimpleTimes{Int64}(168, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1  …  1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 168)], 1.0), Resource[Power, Product, CO₂], Vector[EnergyModelsBase.Node[n_grid, n_line, n_demand_product], Direct[l_n_grid-n_line, l_n_line-n_demand_product]], Vector{Function}[[EnergyModelsBase.get_nodes, EnergyModelsBase.get_links]], Dict{Any, Any}())

Define an operational energy model

modeltype = OperationalModel(
    Dict(co2 => FixedProfile(1e6)), # Emission cap for CO₂ in t/week
    Dict(co2 => FixedProfile(100)), # Emission price for CO₂ in EUR/t
    co2,                            # CO₂ instance
)
OperationalModel(Dict{ResourceEmit{Int64}, FixedProfile{Float64}}(CO₂ => FixedProfile{Float64}(1.0e6)), Dict{ResourceEmit{Int64}, FixedProfile{Int64}}(CO₂ => FixedProfile{Int64}(100)), CO₂)

Solving of the model

Create and optimize the model using the function run_model with HiGHS as solver

m = run_model(case, modeltype, HiGHS.Optimizer)
A JuMP Model
├ solver: HiGHS
├ objective_sense: MAX_SENSE
│ └ objective_function_type: JuMP.AffExpr
├ num_variables: 3381
├ num_constraints: 6919
│ ├ JuMP.AffExpr in MOI.EqualTo{Float64}: 2197
│ ├ JuMP.AffExpr in MOI.GreaterThan{Float64}: 168
│ ├ JuMP.AffExpr in MOI.LessThan{Float64}: 1344
│ ├ JuMP.VariableRef in MOI.EqualTo{Float64}: 673
│ ├ JuMP.VariableRef in MOI.GreaterThan{Float64}: 2032
│ ├ JuMP.VariableRef in MOI.LessThan{Float64}: 1
│ └ JuMP.VariableRef in MOI.ZeroOne: 504
└ Names registered in the model
  └ :cap_inst, :cap_use, :con_em_tot, :demand_sink_deficit, :demand_sink_surplus, :emissions_link, :emissions_node, :emissions_strategic, :emissions_total, :flow_in, :flow_out, :link_cap_inst, :link_in, :link_opex_fixed, :link_opex_var, :link_out, :offswitch, :on_off, :onswitch, :opex_fixed, :opex_var, :sink_deficit, :sink_surplus, :stor_charge_inst, :stor_charge_use, :stor_discharge_inst, :stor_discharge_use, :stor_level, :stor_level_inst, :stor_level_Δ_op, :stor_level_Δ_sp

Show the status, which should be optimal

@show termination_status(m)
OPTIMAL::TerminationStatusCode = 1

Result presentation

Get the full row table of the capacity usage of all nodes

table = JuMP.Containers.rowtable(value, m[:cap_use]; header = [:Node, :TimePeriod, :CapUse]);

Filter only for Node == line

line = get_nodes(case)[2]
filtered = filter(row -> row.Node == line, table);

Display the filtered table with the resulting optimal production.

  • Note that the demand is only satisfied during the set work hours (6-20) on weekdays. This is caused by the restrictions put on PeriodDemandSink with the capacity limited to these time periods. This is also the reason that there is no production during the weekend, even though the electricity is free.
  • Also note that the production always runs for at least 8 hours, even though the daily demand of 1500 units could be reached in 5 hours running at full capacity. This can be explained by the constraint for a minimum run time of 8 hours on the MinUpDownTimeNode. To maximize production at low prices, it runs at the minimum capacity of 50 when the electricity is more expensive.
pretty_table(
    filtered;
    fit_table_in_display_horizontally = false,
    fit_table_in_display_vertically   = false,
    maximum_number_of_rows            = -1,
    maximum_number_of_columns         = -1,
)
┌────────┬────────────┬────────┐
│   Node │ TimePeriod │ CapUse │
├────────┼────────────┼────────┤
│ n_line │     sp1-t1 │    0.0 │
│ n_line │     sp1-t2 │    0.0 │
│ n_line │     sp1-t3 │    0.0 │
│ n_line │     sp1-t4 │    0.0 │
│ n_line │     sp1-t5 │    0.0 │
│ n_line │     sp1-t6 │    0.0 │
│ n_line │     sp1-t7 │  300.0 │
│ n_line │     sp1-t8 │  300.0 │
│ n_line │     sp1-t9 │  300.0 │
│ n_line │    sp1-t10 │  300.0 │
│ n_line │    sp1-t11 │  150.0 │
│ n_line │    sp1-t12 │   50.0 │
│ n_line │    sp1-t13 │   50.0 │
│ n_line │    sp1-t14 │   50.0 │
│ n_line │    sp1-t15 │    0.0 │
│ n_line │    sp1-t16 │    0.0 │
│ n_line │    sp1-t17 │    0.0 │
│ n_line │    sp1-t18 │    0.0 │
│ n_line │    sp1-t19 │    0.0 │
│ n_line │    sp1-t20 │    0.0 │
│ n_line │    sp1-t21 │    0.0 │
│ n_line │    sp1-t22 │    0.0 │
│ n_line │    sp1-t23 │    0.0 │
│ n_line │    sp1-t24 │    0.0 │
│ n_line │    sp1-t25 │    0.0 │
│ n_line │    sp1-t26 │    0.0 │
│ n_line │    sp1-t27 │    0.0 │
│ n_line │    sp1-t28 │    0.0 │
│ n_line │    sp1-t29 │    0.0 │
│ n_line │    sp1-t30 │    0.0 │
│ n_line │    sp1-t31 │  300.0 │
│ n_line │    sp1-t32 │  300.0 │
│ n_line │    sp1-t33 │  300.0 │
│ n_line │    sp1-t34 │  300.0 │
│ n_line │    sp1-t35 │  150.0 │
│ n_line │    sp1-t36 │   50.0 │
│ n_line │    sp1-t37 │   50.0 │
│ n_line │    sp1-t38 │   50.0 │
│ n_line │    sp1-t39 │    0.0 │
│ n_line │    sp1-t40 │    0.0 │
│ n_line │    sp1-t41 │    0.0 │
│ n_line │    sp1-t42 │    0.0 │
│ n_line │    sp1-t43 │    0.0 │
│ n_line │    sp1-t44 │    0.0 │
│ n_line │    sp1-t45 │    0.0 │
│ n_line │    sp1-t46 │    0.0 │
│ n_line │    sp1-t47 │    0.0 │
│ n_line │    sp1-t48 │    0.0 │
│ n_line │    sp1-t49 │    0.0 │
│ n_line │    sp1-t50 │    0.0 │
│ n_line │    sp1-t51 │    0.0 │
│ n_line │    sp1-t52 │    0.0 │
│ n_line │    sp1-t53 │    0.0 │
│ n_line │    sp1-t54 │    0.0 │
│ n_line │    sp1-t55 │  300.0 │
│ n_line │    sp1-t56 │  300.0 │
│ n_line │    sp1-t57 │  300.0 │
│ n_line │    sp1-t58 │  300.0 │
│ n_line │    sp1-t59 │  150.0 │
│ n_line │    sp1-t60 │   50.0 │
│ n_line │    sp1-t61 │   50.0 │
│ n_line │    sp1-t62 │   50.0 │
│ n_line │    sp1-t63 │    0.0 │
│ n_line │    sp1-t64 │    0.0 │
│ n_line │    sp1-t65 │    0.0 │
│ n_line │    sp1-t66 │    0.0 │
│ n_line │    sp1-t67 │    0.0 │
│ n_line │    sp1-t68 │    0.0 │
│ n_line │    sp1-t69 │    0.0 │
│ n_line │    sp1-t70 │    0.0 │
│ n_line │    sp1-t71 │    0.0 │
│ n_line │    sp1-t72 │    0.0 │
│ n_line │    sp1-t73 │    0.0 │
│ n_line │    sp1-t74 │    0.0 │
│ n_line │    sp1-t75 │    0.0 │
│ n_line │    sp1-t76 │    0.0 │
│ n_line │    sp1-t77 │    0.0 │
│ n_line │    sp1-t78 │    0.0 │
│ n_line │    sp1-t79 │  300.0 │
│ n_line │    sp1-t80 │  300.0 │
│ n_line │    sp1-t81 │  300.0 │
│ n_line │    sp1-t82 │  300.0 │
│ n_line │    sp1-t83 │  150.0 │
│ n_line │    sp1-t84 │   50.0 │
│ n_line │    sp1-t85 │   50.0 │
│ n_line │    sp1-t86 │   50.0 │
│ n_line │    sp1-t87 │    0.0 │
│ n_line │    sp1-t88 │    0.0 │
│ n_line │    sp1-t89 │    0.0 │
│ n_line │    sp1-t90 │    0.0 │
│ n_line │    sp1-t91 │    0.0 │
│ n_line │    sp1-t92 │    0.0 │
│ n_line │    sp1-t93 │    0.0 │
│ n_line │    sp1-t94 │    0.0 │
│ n_line │    sp1-t95 │    0.0 │
│ n_line │    sp1-t96 │    0.0 │
│ n_line │    sp1-t97 │    0.0 │
│ n_line │    sp1-t98 │    0.0 │
│ n_line │    sp1-t99 │    0.0 │
│ n_line │   sp1-t100 │    0.0 │
│ n_line │   sp1-t101 │    0.0 │
│ n_line │   sp1-t102 │    0.0 │
│ n_line │   sp1-t103 │  300.0 │
│ n_line │   sp1-t104 │  300.0 │
│ n_line │   sp1-t105 │  300.0 │
│ n_line │   sp1-t106 │  300.0 │
│ n_line │   sp1-t107 │  150.0 │
│ n_line │   sp1-t108 │   50.0 │
│ n_line │   sp1-t109 │   50.0 │
│ n_line │   sp1-t110 │   50.0 │
│ n_line │   sp1-t111 │    0.0 │
│ n_line │   sp1-t112 │    0.0 │
│ n_line │   sp1-t113 │    0.0 │
│ n_line │   sp1-t114 │    0.0 │
│ n_line │   sp1-t115 │    0.0 │
│ n_line │   sp1-t116 │    0.0 │
│ n_line │   sp1-t117 │    0.0 │
│ n_line │   sp1-t118 │    0.0 │
│ n_line │   sp1-t119 │    0.0 │
│ n_line │   sp1-t120 │    0.0 │
│ n_line │   sp1-t121 │    0.0 │
│ n_line │   sp1-t122 │    0.0 │
│ n_line │   sp1-t123 │    0.0 │
│ n_line │   sp1-t124 │    0.0 │
│ n_line │   sp1-t125 │    0.0 │
│ n_line │   sp1-t126 │    0.0 │
│ n_line │   sp1-t127 │    0.0 │
│ n_line │   sp1-t128 │    0.0 │
│ n_line │   sp1-t129 │    0.0 │
│ n_line │   sp1-t130 │    0.0 │
│ n_line │   sp1-t131 │    0.0 │
│ n_line │   sp1-t132 │    0.0 │
│ n_line │   sp1-t133 │    0.0 │
│ n_line │   sp1-t134 │    0.0 │
│ n_line │   sp1-t135 │    0.0 │
│ n_line │   sp1-t136 │    0.0 │
│ n_line │   sp1-t137 │    0.0 │
│ n_line │   sp1-t138 │    0.0 │
│ n_line │   sp1-t139 │    0.0 │
│ n_line │   sp1-t140 │    0.0 │
│ n_line │   sp1-t141 │    0.0 │
│ n_line │   sp1-t142 │    0.0 │
│ n_line │   sp1-t143 │    0.0 │
│ n_line │   sp1-t144 │    0.0 │
│ n_line │   sp1-t145 │    0.0 │
│ n_line │   sp1-t146 │    0.0 │
│ n_line │   sp1-t147 │    0.0 │
│ n_line │   sp1-t148 │    0.0 │
│ n_line │   sp1-t149 │    0.0 │
│ n_line │   sp1-t150 │    0.0 │
│ n_line │   sp1-t151 │    0.0 │
│ n_line │   sp1-t152 │    0.0 │
│ n_line │   sp1-t153 │    0.0 │
│ n_line │   sp1-t154 │    0.0 │
│ n_line │   sp1-t155 │    0.0 │
│ n_line │   sp1-t156 │    0.0 │
│ n_line │   sp1-t157 │    0.0 │
│ n_line │   sp1-t158 │    0.0 │
│ n_line │   sp1-t159 │    0.0 │
│ n_line │   sp1-t160 │    0.0 │
│ n_line │   sp1-t161 │    0.0 │
│ n_line │   sp1-t162 │    0.0 │
│ n_line │   sp1-t163 │    0.0 │
│ n_line │   sp1-t164 │    0.0 │
│ n_line │   sp1-t165 │    0.0 │
│ n_line │   sp1-t166 │    0.0 │
│ n_line │   sp1-t167 │    0.0 │
│ n_line │   sp1-t168 │    0.0 │
└────────┴────────────┴────────┘

This page was generated using Literate.jl.