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 if it has first started, and be shut off for a minimum number of hours if it has first stopped.
using EnergyModelsBase
using EnergyModelsFlex
using TimeStruct

using HiGHS
using JuMP
using PrettyTables

Declare the required resources.

Power = ResourceCarrier("Power", 0)
Product = ResourceCarrier("Product", 0)
CO2 = ResourceEmit("CO2", 0)
CO2

Define a timestructure for a single week.

T = 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])], 1.0)

Some arbitrary electricity prices. Note we let the energy be free in the weekend. This would be a huge incentive to produce during the weekend, if we allowed the PeriodDemandSink 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 = [repeat(day, 5)..., fill(0, 2 * 24)...]

grid = RefSource(
    "grid",
    FixedProfile(1e12), # kW - virtually infinite
    OperationalProfile(el_cost),
    FixedProfile(0),
    Dict(Power => 1),
)
n_grid

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

weekday_prod = [fill(0, 6)..., fill(300, 14)..., fill(0, 4)...]
@assert length(weekday_prod) == 24

Repeat a weekday 5 times, for a workweek, then no production on the weekends.

week_prod = [repeat(weekday_prod, 5)..., fill(0, 2 * 24)...]

demand = PeriodDemandSink(
    "demand_product",
    24, # 24 hours per day.
    [fill(1500, 5)..., 0, 0], # Demand of 1500 units per day, and nothing (0) in the weekend.
    OperationalProfile(week_prod), # kW - installed capacity
    Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e8)), # € / Demand - Price for not delivering products
    Dict(Product => 1),
)
n_demand_product

Define the production line using MinUpDownTimeNode

min_up_time = 8
min_down_time = 5
line = MinUpDownTimeNode(
    "line",
    FixedProfile(300), # kW - installed capacity for both lines
    FixedProfile(0),
    FixedProfile(0),
    Dict(Power => 1),
    Dict(Product => 1),
    min_up_time, # minUpTime
    min_down_time, # minDownTime
    50, # minCapacity
    300, # maxCapacity
    [],
)
n_line

Define the simple energy system

nodes = [grid, line, demand]
links = [Direct("grid-line", grid, line), Direct("line-demand", line, demand)]
case = Dict(:T => T, :nodes => nodes, :products => [Power, Product], :links => links)
Dict{Symbol, Any} with 4 entries:
  :T        => TwoLevel{Int64, Int64, SimpleTimes{Int64}}(1, [1], SimpleTimes{I…
  :links    => Direct[l_n_grid-n_line, l_n_line-n_demand_product]
  :products => ResourceCarrier{Int64}[Power, Product]
  :nodes    => Node[n_grid, n_line, n_demand_product]

Define as operational energy model

modeltype = OperationalModel(
    Dict(CO2 => FixedProfile(1e6)),
    Dict(CO2 => FixedProfile(100)),
    CO2,
)
OperationalModel(Dict{ResourceEmit{Int64}, FixedProfile{Float64}}(CO2 => FixedProfile{Float64}(1.0e6)), Dict{ResourceEmit{Int64}, FixedProfile{Int64}}(CO2 => FixedProfile{Int64}(100)), CO2)

Optimize the model

m = run_model(case, modeltype, HiGHS.Optimizer)
A JuMP Model
├ solver: HiGHS
├ objective_sense: MAX_SENSE
│ └ objective_function_type: JuMP.AffExpr
├ num_variables: 3212
├ num_constraints: 6917
│ ├ JuMP.AffExpr in MOI.EqualTo{Float64}: 2028
│ ├ JuMP.AffExpr in MOI.GreaterThan{Float64}: 168
│ ├ JuMP.AffExpr in MOI.LessThan{Float64}: 1512
│ ├ JuMP.VariableRef in MOI.EqualTo{Float64}: 505
│ ├ JuMP.VariableRef in MOI.GreaterThan{Float64}: 2200
│ └ 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 status, should be optimal

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

Get the full row table

table = JuMP.Containers.rowtable(value, m[:cap_use]; header = [:Node, :TimePeriod, :CapUse])
504-element Vector{NamedTuple{(:Node, :TimePeriod, :CapUse)}}:
 (Node = n_grid, TimePeriod = sp1-t1, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t1, CapUse = 0.0)
 (Node = n_demand_product, TimePeriod = sp1-t1, CapUse = 0.0)
 (Node = n_grid, TimePeriod = sp1-t2, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t2, CapUse = 0.0)
 (Node = n_demand_product, TimePeriod = sp1-t2, CapUse = 0.0)
 (Node = n_grid, TimePeriod = sp1-t3, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t3, CapUse = 0.0)
 (Node = n_demand_product, TimePeriod = sp1-t3, CapUse = 0.0)
 (Node = n_grid, TimePeriod = sp1-t4, CapUse = 0.0)
 ⋮
 (Node = n_grid, TimePeriod = sp1-t166, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t166, CapUse = 0.0)
 (Node = n_demand_product, TimePeriod = sp1-t166, CapUse = 0.0)
 (Node = n_grid, TimePeriod = sp1-t167, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t167, CapUse = 0.0)
 (Node = n_demand_product, TimePeriod = sp1-t167, CapUse = 0.0)
 (Node = n_grid, TimePeriod = sp1-t168, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t168, CapUse = 0.0)
 (Node = n_demand_product, TimePeriod = sp1-t168, CapUse = 0.0)

Filter only for Node == line

line = case[:nodes][2]
filtered = filter(row -> row.Node == line, table)
168-element Vector{NamedTuple{(:Node, :TimePeriod, :CapUse)}}:
 (Node = n_line, TimePeriod = sp1-t1, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t2, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t3, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t4, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t5, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t6, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t7, CapUse = 300.0)
 (Node = n_line, TimePeriod = sp1-t8, CapUse = 300.0)
 (Node = n_line, TimePeriod = sp1-t9, CapUse = 300.0)
 (Node = n_line, TimePeriod = sp1-t10, CapUse = 300.0)
 ⋮
 (Node = n_line, TimePeriod = sp1-t160, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t161, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t162, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t163, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t164, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t165, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t166, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t167, CapUse = 0.0)
 (Node = n_line, TimePeriod = sp1-t168, CapUse = 0.0)

Display the filtered table with the resulting optimal production.

  • Note that the demand is only satisfied during the set workhours (6-20) on weekdays. This is cause 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 is always run 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 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; crop = :none)
┌────────┬────────────┬────────┐
│   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.