From 000215e991e78d0cf8978ef272c29b748fbd9762 Mon Sep 17 00:00:00 2001 From: Aleksandr Kazachkov Date: Fri, 23 Jul 2021 11:23:16 -0500 Subject: [PATCH] Add reserve shortfall penalty --- docs/format.md | 8 +++++--- instances/test/case14.json.gz | Bin 1775 -> 1795 bytes src/instance/read.jl | 5 +++++ src/instance/structs.jl | 2 ++ src/model/formulations/base/bus.jl | 4 ---- src/model/formulations/base/system.jl | 23 +++++++++++++++++++---- src/model/formulations/base/unit.jl | 10 ++++------ src/solution/solution.jl | 6 ++++++ src/validation/validate.jl | 9 +++++++-- test/model/formulations_test.jl | 10 ++++++++-- 10 files changed, 56 insertions(+), 21 deletions(-) diff --git a/docs/format.md b/docs/format.md index 0733fb5..c13bdc7 100644 --- a/docs/format.md +++ b/docs/format.md @@ -28,13 +28,14 @@ Each section is described in detail below. For a complete example, see [case14]( ### Parameters -This section describes system-wide parameters, such as power balance penalties, optimization parameters, such as the length of the planning horizon and the time. +This section describes system-wide parameters, such as power balance and reserve shortfall penalties, and optimization parameters, such as the length of the planning horizon and the time. | Key | Description | Default | Time series? | :----------------------------- | :------------------------------------------------ | :------: | :------------: -| `Time horizon (h)` | Length of the planning horizon (in hours). | Required | N +| `Time horizon (h)` | Length of the planning horizon (in hours). | Required | N | `Time step (min)` | Length of each time step (in minutes). Must be a divisor of 60 (e.g. 60, 30, 20, 15, etc). | `60` | N | `Power balance penalty ($/MW)` | Penalty for system-wide shortage or surplus in production (in $/MW). This is charged per time step. For example, if there is a shortage of 1 MW for three time steps, three times this amount will be charged. | `1000.0` | Y +| `Reserve shortfall penalty ($/MW)` | Penalty for system-wide shortage in meeting reserve requirements (in $/MW). This is charged per time step. Negative value implies reserve constraints must always be satisfied. | `-1` | Y #### Example @@ -42,7 +43,8 @@ This section describes system-wide parameters, such as power balance penalties, { "Parameters": { "Time horizon (h)": 4, - "Power balance penalty ($/MW)": 1000.0 + "Power balance penalty ($/MW)": 1000.0, + "Reserve shortfall penalty ($/MW)": -1.0 } } ``` diff --git a/instances/test/case14.json.gz b/instances/test/case14.json.gz index 11a33a8c61ceb7d5fd7fdb7f90e93cd88cdcec27..9876d5906b0608deb325304c32564ad3c7c54e4d 100644 GIT binary patch literal 1795 zcmV+e2mJUSiwFn_LHl3;17l%xWid1^YIARH0Nq+kZ`(!?KF7a;FmiAKWxKnx&t8hY zQJ^V;I6x0U4zxtu1SC=*DW`7W|K6cwD;7BvheBEuRRS62&dz=_oNpeRd^$_;bN(i+ z(pA=ERekU?&%@TMvkn|YHX zpm|w0$$ZlSx!4{09#5SmiBkBjVEbi1@D#kS^9ZpvKRVXb`yRdF7~MxBj$F$gK_l0jS7Bv;rdAy@usmc=WA# z^69~rx9Mt~;Fe{+%9|(4V!#cA+@kzgj7mO%7?r!-G%SU$^CDkuR*14``M2jCDb#8_ zCYJuW6HD{uebN%}=Dt08D_m@r%cJsjYg$c9(lHv2M`%A}?(#)eC-pinioCc@qhO4z~Z?(<-l zgh^>eKmG(hCA{QPf{-LBLCKMnAOv4;(2>=q7zhiJF^b2Sp4h+4828_0j2UK(o5q+^ z>2i!K|3ImP_MFqOFMpRY3{o6}blqW55E(HA+r?jZ%2b5Jb4)iZa#`h@x;#8%Hfz zsf=PF9Z9Kuqy|%plSEX3h=`MV?yYwOi|Q=JIj8FB$bf_jWh z36_O&dh*5u9Sq4)D>P<3B0(#xKoJ}f5V)c()+QF#$+=s35ir{lFA}Zr(OC+s6)|GA zBv(AhVXOzez`H6f>Q!FXZ6~tKi*94EB%=7T+*I={xf;5LdY4u=S+ig7U8KCt(s`2> zsN$l$UDc1N$wuYn=TCR^xZKq9Z2h?HvfZPxoJJR%^d68OvRkLme=n;Q_C(jq^5fwU z1+qUy{j|biCM)LmguEf-fBr0W?0)Z=uXp|==r+-xmgP^aL7Hd&)T92yAk-E)6DQ z5S5EMY8o`rSWN`Y3l)VuR~QA2)l!1DRsvwHmfl$!HPL5vQ1{;diLO?%%XkvCme_QI ziN<*g-qEOumaBuhhw3C>OQY3zYBLHa1sA1@)(6nm)1ZldZz5>V%Ox@@6+eGCw8Ru5 zoptzEMI#Mu0MU3(G$67Ju!v$j!U5;DfS`>FvJG`N;C8A9?ZEB@LkD*6iiq~R)pDBO zorv0klQQC1hvUY80pmrz2e4y+>Q$+MBpxCoSiCnl#%tU16MM4()Z?Xd6g96GK_FqA zq}c9ijZva(+ca8q`~dFpiaCm#Su(MQ9FGy4Yax2fE5>2d)c!lu0PgXk*-IZC+`W6U zp`xkmDD2qY^tAC5Fq7M1)Z^mLWniU4>jvB9=eQ%zc z0ks_GWW?8kBRg2H9sU^#lJR=k&u}NEk{B61A!cq+As`!-#`$X?O7wWW?4vp@Ouz)j zq+{DFXo+o=MnVE;c$P~JrsE&VqRESPEX=!u@tmlk-(6p4^CnyD2kHK(-wxCV59v#U zIZ^9a+BHi%QIS~Mfu)_O8!YXHrJcy{EbW%1ok+th?T)3LNS7?_o~4~gYAo%6rJcw< zEbWn{o$9H1bdo&%C)1V3SE@PX>E|()Y9e|1d7P!%CZ2vCuc-cpr=Q0xsuSVq=W&bb z7$-#2ew{s>{S6#=qu9+U007gLdTRgx literal 1775 zcmV<}K8zA9DhGW9rVa>xiT5Hvv@&uB1@Kl+ksaOI(Fwz z=at^+KvYPI0w$+0iQQ{OzQsG=+IyewZF!ZiuQJ-QD%WLuZ&`}8fss4kyssuD??Ft; zU2a>E!WU&#uD5GK*>;q=qtb{_r}2P;IK`qnmgh;5ZL(HV+UNnfJtl zVRo3}0RW&1jtGH1DNQ`Um_WJF=pOQ5k;PeQ#@zixIVFOYQi73WDZ$CHlwbs3Y{`-J zwi*cwmN6h>%s}j4W{m&uGRBTG#?NESv2;1am4BdAx^aW!|I%Rp#SETpsNUtGFWB?! z^_pZw(Q7l$m^SnGO@98MO7TuQV;ZQ(``+v=bss~04mcAQ!xgKnH(;0*R3-)Em=&Xv zjw$Rdyyc-XHSRD}0Md@e>eVFMTZjC zfS&ytwF;CWm~f*Nu+A}vlJeeI&n-9&7DPxVQtA+?rIg||6IG}Z;pCnN7d%6GR6g01 zL@;M(a7r{lsb+YpS21|YE>K8tkBKSKiBQf@-jtxDWjX4Up{&OwXrKy|&@%x+E5=c6 z5@DU4yCJAZ*_L^cWJO5caaf&7iLxcT5>St0-RlKj*Ll^f%cki%kyTms8-o=S#iyHX zy)3e`v1_Q;d3{l|59@=AlvhQ*Z1aj#JlkBZo4eHH5(NGE(;q!fx6QJ+y4!Zz?J-nN zlaF49fW!~ouhS=gZ0a@jMDJFc_xnQ>>fsdi^O}a4qFUZC@|KbR<+IdPQRS09pFo91d86Gs#Sj_}23l*TAD^7x@ zYN;_erxB@E>);)an(4E8s0Z)=NLPdIGoD3lG&S95ll8%2@H}d!|`uvg5l2VBF&eLB=R$JZxrtzF099+MzgLa;Q(8`trZdb1JK)1`9~wX7E*AaR#1q#{+($A_jJ)5q>mo%!9Cen(F8XMJ9RfbT|7m~*X-R z9hpi}67+Cl1Mv>v@>->q}__N zGx=Sl-HEg_X;`G)i?lQ8QlveIv@=Ofq&Txpl-m+_0M!Louf&ab^v z`LZsT#q*}9nzAjgi)^*Y&wC~FsyXnV8JV`^x~R6r-mU837WSuNomUSx>u&GX^%h?D R_2ko&zX39ZHgTXS008Iaf-nF8 diff --git a/src/instance/read.jl b/src/instance/read.jl index 06ebfad..01573f7 100644 --- a/src/instance/read.jl +++ b/src/instance/read.jl @@ -98,6 +98,10 @@ function _from_json(json; repair = true) json["Parameters"]["Power balance penalty (\$/MW)"], default = [1000.0 for t in 1:T], ) + shortfall_penalty = timeseries( + json["Parameters"]["Reserve shortfall penalty (\$/MW)"], + default = [-1.0 for t in 1:T], + ) # Read buses for (bus_name, dict) in json["Buses"] @@ -264,6 +268,7 @@ function _from_json(json; repair = true) instance = UnitCommitmentInstance( T, power_balance_penalty, + shortfall_penalty, units, buses, lines, diff --git a/src/instance/structs.jl b/src/instance/structs.jl index d75fba9..bf7360c 100644 --- a/src/instance/structs.jl +++ b/src/instance/structs.jl @@ -72,6 +72,8 @@ end mutable struct UnitCommitmentInstance time::Int power_balance_penalty::Vector{Float64} + "Penalty for failing to meet reserve requirement." + shortfall_penalty::Vector{Float64} units::Vector{Unit} buses::Vector{Bus} lines::Vector{TransmissionLine} diff --git a/src/model/formulations/base/bus.jl b/src/model/formulations/base/bus.jl index 00cacba..d881fc6 100644 --- a/src/model/formulations/base/bus.jl +++ b/src/model/formulations/base/bus.jl @@ -4,15 +4,11 @@ function _add_bus!(model::JuMP.Model, b::Bus)::Nothing net_injection = _init(model, :expr_net_injection) - reserve = _init(model, :expr_reserve) curtail = _init(model, :curtail) for t in 1:model[:instance].time # Fixed load net_injection[b.name, t] = AffExpr(-b.load[t]) - # Reserves - reserve[b.name, t] = AffExpr() - # Load curtailment curtail[b.name, t] = @variable(model, lower_bound = 0, upper_bound = b.load[t]) diff --git a/src/model/formulations/base/system.jl b/src/model/formulations/base/system.jl index 496ea2b..d6bf573 100644 --- a/src/model/formulations/base/system.jl +++ b/src/model/formulations/base/system.jl @@ -29,13 +29,28 @@ end function _add_reserve_eqs!(model::JuMP.Model)::Nothing eq_min_reserve = _init(model, :eq_min_reserve) - for t in 1:model[:instance].time + instance = model[:instance] + for t in 1:instance.time + # Equation (68) in Kneuven et al. (2020) + # As in Morales-España et al. (2013a) + # Akin to the alternative formulation with max_power_avail + # from Carrión and Arroyo (2006) and Ostrowski et al. (2012) + shortfall_penalty = instance.shortfall_penalty[t] eq_min_reserve[t] = @constraint( model, - sum( - model[:expr_reserve][b.name, t] for b in model[:instance].buses - ) >= model[:instance].reserves.spinning[t] + sum(model[:reserve][g.name, t] for g in instance.units) + + (shortfall_penalty >= 0 ? model[:reserve_shortfall][t] : 0.0) >= + instance.reserves.spinning[t] ) + + # Account for shortfall contribution to objective + if shortfall_penalty >= 0 + add_to_expression!( + model[:obj], + shortfall_penalty, + model[:reserve_shortfall][t], + ) + end end return end diff --git a/src/model/formulations/base/unit.jl b/src/model/formulations/base/unit.jl index ad00d44..e701977 100644 --- a/src/model/formulations/base/unit.jl +++ b/src/model/formulations/base/unit.jl @@ -44,12 +44,16 @@ _is_initially_on(g::Unit)::Float64 = (g.initial_status > 0 ? 1.0 : 0.0) function _add_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing reserve = _init(model, :reserve) + reserve_shortfall = _init(model, :reserve_shortfall) for t in 1:model[:instance].time if g.provides_spinning_reserves[t] reserve[g.name, t] = @variable(model, lower_bound = 0) else reserve[g.name, t] = 0.0 end + reserve_shortfall[t] = + (model[:instance].shortfall_penalty[t] >= 0) ? + @variable(model, lower_bound = 0) : 0.0 end return end @@ -210,11 +214,5 @@ function _add_net_injection_eqs!(model::JuMP.Model, g::Unit)::Nothing model[:is_on][g.name, t], g.min_power[t], ) - # Add to reserves expression - add_to_expression!( - model[:expr_reserve][g.bus.name, t], - model[:reserve][g.name, t], - 1.0, - ) end end diff --git a/src/solution/solution.jl b/src/solution/solution.jl index 6240d9a..5fd8bbd 100644 --- a/src/solution/solution.jl +++ b/src/solution/solution.jl @@ -51,6 +51,12 @@ function solution(model::JuMP.Model)::OrderedDict sol["Switch on"] = timeseries(model[:switch_on], instance.units) sol["Switch off"] = timeseries(model[:switch_off], instance.units) sol["Reserve (MW)"] = timeseries(model[:reserve], instance.units) + sol["Reserve shortfall (MW)"] = OrderedDict( + t => + (instance.shortfall_penalty[t] >= 0) ? + round(value(model[:reserve_shortfall][t]), digits = 5) : 0.0 for + t in 1:instance.time + ) sol["Net injection (MW)"] = timeseries(model[:net_injection], instance.buses) sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses) diff --git a/src/validation/validate.jl b/src/validation/validate.jl index fb0c8b2..d342ef9 100644 --- a/src/validation/validate.jl +++ b/src/validation/validate.jl @@ -324,11 +324,16 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01) # Verify spinning reserves reserve = sum(solution["Reserve (MW)"][g.name][t] for g in instance.units) - if reserve < instance.reserves.spinning[t] - tol + reserve_shortfall = + (instance.shortfall_penalty[t] >= 0) ? + solution["Reserve shortfall (MW)"][t] : 0 + + if reserve + reserve_shortfall < instance.reserves.spinning[t] - tol @error @sprintf( - "Insufficient spinning reserves at time %d (%.2f should be %.2f)", + "Insufficient spinning reserves at time %d (%.2f + %.2f should be %.2f)", t, reserve, + reserve_shortfall, instance.reserves.spinning[t], ) err_count += 1 diff --git a/test/model/formulations_test.jl b/test/model/formulations_test.jl index 01f2c8e..3b08dc5 100644 --- a/test/model/formulations_test.jl +++ b/test/model/formulations_test.jl @@ -20,8 +20,14 @@ if ENABLE_LARGE_TESTS end function _small_test(formulation::Formulation)::Nothing - instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01") - UnitCommitment.build_model(instance = instance, formulation = formulation) # should not crash + instances = ["matpower/case118/2017-02-01", "test/case14"] + for instance in instances + # Should not crash + UnitCommitment.build_model( + instance = UnitCommitment.read_benchmark(instance), + formulation = formulation, + ) + end return end