From a3a71ff5a9ec4aefc32ab0887cb7734a22603d30 Mon Sep 17 00:00:00 2001 From: oyurdakul Date: Thu, 3 Feb 2022 09:45:06 +0100 Subject: [PATCH] add flexiramp --- .DS_Store | Bin 0 -> 6148 bytes docs/format.md | 35 ++++- src/UnitCommitment.jl | 2 + src/instance/read.jl | 24 ++- src/instance/structs.jl | 4 + src/model/.DS_Store | Bin 0 -> 6148 bytes src/model/build.jl | 8 + src/model/formulations/WanHob2016/ramp.jl | 152 +++++++++++++++++++ src/model/formulations/WanHob2016/structs.jl | 18 +++ src/model/formulations/base/system.jl | 39 +++++ src/model/formulations/base/unit.jl | 1 + src/solution/solution.jl | 36 ++++- src/validation/validate.jl | 34 +++++ 13 files changed, 338 insertions(+), 15 deletions(-) create mode 100644 .DS_Store create mode 100644 src/model/.DS_Store create mode 100644 src/model/formulations/WanHob2016/ramp.jl create mode 100644 src/model/formulations/WanHob2016/structs.jl diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ab5cefd05e24dccaf15a94eedbadbff4e72f9891 GIT binary patch literal 6148 zcmeHKu};G<5WQZze!_Wpc$8FF1;*XvEchOc~%7l)_M*PEPgf8@6h18w*Np){o(JyMfpom^d? zm|;$M-)8SAr^R*sDdRbtN!Hj~7ggrIwJGU|1POMKCbdP@smgl^Cqy@CTb$8Ae47C${2)E%SHr!YMn} z4{17arRcpY;0ojlO!aXt=l?B!nb9IY5Ai2gz!mss3UE=c>LotP&epTfle0FU-Jyxe qyeI=0#@QtR53-M3CQ|tzW6Y}zqoS-L{uB=Mi$D^@J6GTr6!-#Or$Mg( literal 0 HcmV?d00001 diff --git a/docs/format.md b/docs/format.md index c13bdc7..985511e 100644 --- a/docs/format.md +++ b/docs/format.md @@ -36,6 +36,7 @@ This section describes system-wide parameters, such as power balance and reserve | `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 +| `Flexiramp penalty ($/MW)` | Penalty for system-wide shortage in meeting flexible ramping product requirements (in $/MW). This is charged per time step. | `500` | Y #### Example @@ -44,7 +45,8 @@ This section describes system-wide parameters, such as power balance and reserve "Parameters": { "Time horizon (h)": 4, "Power balance penalty ($/MW)": 1000.0, - "Reserve shortfall penalty ($/MW)": -1.0 + "Reserve shortfall penalty ($/MW)": -1.0, + "Flexiramp penalty ($/MW)": 100.0 } } ``` @@ -97,6 +99,8 @@ This section describes all generators in the system, including thermal units, re | `Initial power (MW)` | Amount of power the generator at time step `-1`, immediately before the planning horizon starts. | Required | N | `Must run?` | If `true`, the generator should be committed, even if that is not economical (Boolean). | `false` | Y | `Provides spinning reserves?` | If `true`, this generator may provide spinning reserves (Boolean). | `true` | Y +| `Provides flexible capacity?` | If `true`, this generator may provide flexible ramping product (Boolean). | `true` | Y + #### Production costs and limits @@ -136,6 +140,7 @@ Note that this curve also specifies the production limits. Specifically, the fir "Initial status (h)": 12, "Must run?": false, "Provides spinning reserves?": true, + "Provides flexible capacity?": false, }, "gen2": { "Bus": "b5", @@ -206,14 +211,16 @@ This section describes the characteristics of transmission system, such as its t ### Reserves -This section describes the hourly amount of operating reserves required. +This section describes the hourly amount of reserves required. | Key | Description | Default | Time series? | :-------------------- | :------------------------------------------------- | --------- | :----: | `Spinning (MW)` | Minimum amount of system-wide spinning reserves (in MW). Only generators which are online may provide this reserve. | `0.0` | Y +| `Up-flexiramp (MW)` | Minimum amount of system-wide upward flexible ramping product (in MW). Only generators which are online may provide this reserve. | `0.0` | Y +| `Down-flexiramp (MW)` | Minimum amount of system-wide downward flexible ramping product (in MW). Only generators which are online may provide this reserve. | `0.0` | Y -#### Example +#### Example 1 ```json { @@ -228,6 +235,27 @@ This section describes the hourly amount of operating reserves required. } ``` +#### Example 2 + +```json +{ + "Reserves": { + "up-flexiramp (MW)": [ + 20.31042, + 23.65273, + 27.41784, + 25.34057 + ], + "down-flexiramp (MW)": [ + 19.41546, + 21.45377, + 23.53402, + 24.80973 + ] + } +} +``` + ### Contingencies This section describes credible contingency scenarios in the optimization, such as the loss of a transmission line or generator. @@ -287,6 +315,7 @@ Current limitations ------------------- * All reserves are system-wide. Zonal reserves are not currently supported. +* Upward and downward flexible ramping products can only be acquired under the WanHob2016 formulation, which does not support spinning reserves. * Network topology remains the same for all time periods * Only N-1 transmission contingencies are supported. Generator contingencies are not currently supported. * Time-varying minimum production amounts are not currently compatible with ramp/startup/shutdown limits. diff --git a/src/UnitCommitment.jl b/src/UnitCommitment.jl index 9ada14c..89049a1 100644 --- a/src/UnitCommitment.jl +++ b/src/UnitCommitment.jl @@ -16,6 +16,7 @@ include("model/formulations/KnuOstWat2018/structs.jl") include("model/formulations/MorLatRam2013/structs.jl") include("model/formulations/PanGua2016/structs.jl") include("solution/methods/XavQiuWanThi2019/structs.jl") +include("model/formulations/WanHob2016/structs.jl") include("import/egret.jl") include("instance/read.jl") @@ -36,6 +37,7 @@ include("model/formulations/KnuOstWat2018/pwlcosts.jl") include("model/formulations/MorLatRam2013/ramp.jl") include("model/formulations/MorLatRam2013/scosts.jl") include("model/formulations/PanGua2016/ramp.jl") +include("model/formulations/WanHob2016/ramp.jl") include("model/jumpext.jl") include("solution/fix.jl") include("solution/methods/XavQiuWanThi2019/enforce.jl") diff --git a/src/instance/read.jl b/src/instance/read.jl index 3ca522c..2a6f448 100644 --- a/src/instance/read.jl +++ b/src/instance/read.jl @@ -108,6 +108,11 @@ function _from_json(json; repair = true) json["Parameters"]["Power balance penalty (\$/MW)"], default = [1000.0 for t in 1:T], ) + # Penalty price for shortage in meeting system-wide flexiramp requirements + flexiramp_shortfall_penalty = timeseries( + json["Parameters"]["Flexiramp penalty (\$/MW)"], + default = [500.0 for t in 1:T], + ) shortfall_penalty = timeseries( json["Parameters"]["Reserve shortfall penalty (\$/MW)"], default = [-1.0 for t in 1:T], @@ -200,6 +205,10 @@ function _from_json(json; repair = true) dict["Provides spinning reserves?"], default = [true for t in 1:T], ), + timeseries( + dict["Provides flexible capacity?"], + default = [true for t in 1:T], + ), startup_categories, ) push!(bus.units, unit) @@ -207,12 +216,16 @@ function _from_json(json; repair = true) push!(units, unit) end - # Read reserves - reserves = Reserves(zeros(T)) + # Read spinning, up-flexiramp, and down-flexiramp reserve requirements + reserves = Reserves(zeros(T), zeros(T), zeros(T)) if "Reserves" in keys(json) - reserves.spinning = - timeseries(json["Reserves"]["Spinning (MW)"], default = zeros(T)) - end + reserves.spinning = + timeseries(json["Reserves"]["Spinning (MW)"], default = zeros(T)) + reserves.upflexiramp = + timeseries(json["Reserves"]["Up-flexiramp (MW)"], default = zeros(T)) + reserves.dwflexiramp = + timeseries(json["Reserves"]["Down-flexiramp (MW)"], default = zeros(T)) + end # Read transmission lines if "Transmission lines" in keys(json) @@ -287,6 +300,7 @@ function _from_json(json; repair = true) price_sensitive_loads = loads, reserves = reserves, shortfall_penalty = shortfall_penalty, + flexiramp_shortfall_penalty = flexiramp_shortfall_penalty, time = T, units_by_name = Dict(g.name => g for g in units), units = units, diff --git a/src/instance/structs.jl b/src/instance/structs.jl index 1e4cd5c..8555316 100644 --- a/src/instance/structs.jl +++ b/src/instance/structs.jl @@ -37,6 +37,7 @@ mutable struct Unit initial_status::Union{Int,Nothing} initial_power::Union{Float64,Nothing} provides_spinning_reserves::Vector{Bool} + provides_flexiramp_reserves::Vector{Bool} # binary variable indicating whether the unit provides flexiramp startup_categories::Vector{StartupCategory} end @@ -54,6 +55,8 @@ end mutable struct Reserves spinning::Vector{Float64} + upflexiramp::Vector{Float64} # up-flexiramp reserve requirements + dwflexiramp::Vector{Float64} # down-flexiramp reserve requirements end mutable struct Contingency @@ -81,6 +84,7 @@ Base.@kwdef mutable struct UnitCommitmentInstance price_sensitive_loads::Vector{PriceSensitiveLoad} reserves::Reserves shortfall_penalty::Vector{Float64} + flexiramp_shortfall_penalty::Vector{Float64} # penalty price for flexiramp shortfall time::Int units_by_name::Dict{AbstractString,Unit} units::Vector{Unit} diff --git a/src/model/.DS_Store b/src/model/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..04abbce9608c659e5912cce00bfb1671bd6d1a6a GIT binary patch literal 6148 zcmeHK%Sr=55Ukc50(!{NNwKv*^&Jb006=&qTb zu35GY+uHzaeSEwHRsfcCM|^mgo1eSS?4mM8r1Op=c6h)$Ua+54pHDdV8c(c0;B~?u z@OIiBhTVR6&il__psW;-0#ZNOzYldUEci>LGc7Ui&> zs3--bz*K?D+^)R;-_d`V|EDDFq<|FoR|?o-v)!!uO4VCuFXz3s(eLSA^GSE(Iw%a$ lj)~EZx$$;<6Gd6qe9iM-I3@<2`JfZ^GvK<&q`+S*@CEsA7Nr0H literal 0 HcmV?d00001 diff --git a/src/model/build.jl b/src/model/build.jl index 87a9a66..a9b766e 100644 --- a/src/model/build.jl +++ b/src/model/build.jl @@ -32,6 +32,14 @@ function build_model(; formulation = Formulation(), variable_names::Bool = false, )::JuMP.Model + if formulation.ramping ==WanHob2016.Ramping() && instance.reserves.spinning!=zeros(instance.time) + error("Spinning reserves are not supported by the WanHob2016 ramping formulation") + end + @show formulation.ramping + if formulation.ramping !== WanHob2016.Ramping() && (instance.reserves.upflexiramp!=zeros(instance.time) || instance.reserves.dwflexiramp!=zeros(instance.time)) + error("Flexiramp is supported only by the WanHob2016 ramping formulation") + end + @info "Building model..." time_model = @elapsed begin model = Model() diff --git a/src/model/formulations/WanHob2016/ramp.jl b/src/model/formulations/WanHob2016/ramp.jl new file mode 100644 index 0000000..b3d7198 --- /dev/null +++ b/src/model/formulations/WanHob2016/ramp.jl @@ -0,0 +1,152 @@ +# UnitCommitmentFL.jl: Optimization Package for Security-Constrained Unit Commitment +# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +function _add_flexiramp_vars!(model::JuMP.Model, g::Unit)::Nothing + upflexiramp = _init(model, :upflexiramp) + upflexiramp_shortfall = _init(model, :upflexiramp_shortfall) + mfg=_init(model,:mfg) + dwflexiramp = _init(model, :dwflexiramp) + dwflexiramp_shortfall = _init(model, :dwflexiramp_shortfall) + for t in 1:model[:instance].time + # maximum feasible generation, \bar{g_{its}} in Wang & Hobbs (2016) + mfg[g.name,t]=@variable(model, lower_bound = 0) + if g.provides_flexiramp_reserves[t] + upflexiramp[g.name, t] = @variable(model) # up-flexiramp, ur_{it} in Wang & Hobbs (2016) + dwflexiramp[g.name, t] = @variable(model) # down-flexiramp, dr_{it} in Wang & Hobbs (2016) + else + upflexiramp[g.name, t] = 0.0 + dwflexiramp[g.name, t] = 0.0 + end + upflexiramp_shortfall[t] = + (model[:instance].flexiramp_shortfall_penalty[t] >= 0) ? + @variable(model, lower_bound = 0) : 0.0 + dwflexiramp_shortfall[t] = + (model[:instance].flexiramp_shortfall_penalty[t] >= 0) ? + @variable(model, lower_bound = 0) : 0.0 + end + return +end + + + +function _add_ramp_eqs!( + model::JuMP.Model, + g::Unit, + formulation_prod_vars::Gar1962.ProdVars, + formulation_ramping::WanHob2016.Ramping, + formulation_status_vars::Gar1962.StatusVars, +)::Nothing + is_initially_on = (g.initial_status > 0) + SU = g.startup_limit + SD = g.shutdown_limit + RU = g.ramp_up_limit + RD = g.ramp_down_limit + gn = g.name + minp=g.min_power + maxp=g.max_power + initial_power=g.initial_power + + is_on = model[:is_on] + prod_above = model[:prod_above] + upflexiramp=model[:upflexiramp] + dwflexiramp=model[:dwflexiramp] + mfg=model[:mfg] + + for t in 1:model[:instance].time + + @constraint(model, prod_above[gn, t] + (is_on[gn,t]*minp[t]) + <=mfg[gn,t]) # Eq. (19) in Wang & Hobbs (2016) + @constraint(model, mfg[gn,t]<= is_on[gn,t]* maxp[t]) # Eq. (22) in Wang & Hobbs (2016) + if t!=model[:instance].time + @constraint(model, minp[t] * (is_on[gn,t+1]+is_on[gn,t]-1) <= + prod_above[gn, t] - dwflexiramp[gn,t] +(is_on[gn,t]*minp[t]) + ) # first inequality of Eq. (20) in Wang & Hobbs (2016) + @constraint(model, prod_above[gn, t] - dwflexiramp[gn,t] + (is_on[gn,t]*minp[t]) <= + mfg[gn,t+1] + + (maxp[t] * (1-is_on[gn,t+1])) + ) # second inequality of Eq. (20) in Wang & Hobbs (2016) + @constraint(model, minp[t] * (is_on[gn,t+1]+is_on[gn,t]-1) <= + prod_above[gn, t] + upflexiramp[gn,t] + (is_on[gn,t]*minp[t]) + ) # first inequality of Eq. (21) in Wang & Hobbs (2016) + @constraint(model, prod_above[gn, t] + upflexiramp[gn,t] +(is_on[gn,t]*minp[t]) <= + mfg[gn,t+1] + (maxp[t] * (1-is_on[gn,t+1])) + ) # second inequality of Eq. (21) in Wang & Hobbs (2016) + if t!=1 + @constraint(model, mfg[gn,t]<=prod_above[gn,t-1] + (is_on[gn,t-1]*minp[t]) + + (RU * is_on[gn,t-1]) + + (SU*(is_on[gn,t] - is_on[gn,t-1])) + + maxp[t] * (1-is_on[gn,t]) + ) # Eq. (23) in Wang & Hobbs (2016) + @constraint(model, (prod_above[gn,t-1] + (is_on[gn,t-1]*minp[t])) + - (prod_above[gn,t] + (is_on[gn,t]*minp[t])) + <= RD * is_on[gn,t] + + SD * (is_on[gn,t-1] - is_on[gn,t]) + + maxp[t] * (1-is_on[gn,t-1]) + ) # Eq. (25) in Wang & Hobbs (2016) + else + @constraint(model, mfg[gn,t]<=initial_power + + (RU * is_initially_on) + + (SU*(is_on[gn,t] - is_initially_on)) + + maxp[t] * (1-is_on[gn,t]) + ) # Eq. (23) in Wang & Hobbs (2016) for the first time period + @constraint(model, initial_power + - (prod_above[gn,t] + (is_on[gn,t]*minp[t])) + <= RD * is_on[gn,t] + + SD * (is_initially_on - is_on[gn,t]) + + maxp[t] * (1-is_initially_on) + ) # Eq. (25) in Wang & Hobbs (2016) for the first time period + end + @constraint(model, mfg[gn,t]<= + (SD*(is_on[gn,t] - is_on[gn,t+1])) + + (maxp[t] * is_on[gn,t+1]) + ) # Eq. (24) in Wang & Hobbs (2016) + @constraint(model, -RD * is_on[gn,t+1] + -SD * (is_on[gn,t]-is_on[gn,t+1]) + -maxp[t] * (1-is_on[gn,t]) + <= upflexiramp[gn,t] + ) # first inequality of Eq. (26) in Wang & Hobbs (2016) + @constraint(model, upflexiramp[gn,t] <= + RU * is_on[gn,t] + + SU * (is_on[gn,t+1]-is_on[gn,t]) + + maxp[t] * (1-is_on[gn,t+1]) + ) # second inequality of Eq. (26) in Wang & Hobbs (2016) + @constraint(model, -RU * is_on[gn,t] + -SU * (is_on[gn,t+1]-is_on[gn,t]) + -maxp[t] * (1-is_on[gn,t+1]) + <= dwflexiramp[gn,t] + ) # first inequality of Eq. (27) in Wang & Hobbs (2016) + @constraint(model, dwflexiramp[gn,t] <= + RD * is_on[gn,t+1] + + SD * (is_on[gn,t]-is_on[gn,t+1]) + + maxp[t] * (1-is_on[gn,t]) + ) # second inequality of Eq. (27) in Wang & Hobbs (2016) + @constraint(model, -maxp[t] * is_on[gn,t] + +minp[t] * is_on[gn,t+1] + <= upflexiramp[gn,t] + ) # first inequality of Eq. (28) in Wang & Hobbs (2016) + @constraint(model, upflexiramp[gn,t] <= + maxp[t] * is_on[gn,t+1] + ) # second inequality of Eq. (28) in Wang & Hobbs (2016) + @constraint(model, -maxp[t] * is_on[gn,t+1] + <= dwflexiramp[gn,t] + ) # first inequality of Eq. (29) in Wang & Hobbs (2016) + @constraint(model, dwflexiramp[gn,t] <= + (maxp[t] * is_on[gn,t]) + -(minp[t] * is_on[gn,t+1]) + ) # second inequality of Eq. (29) in Wang & Hobbs (2016) + else + @constraint(model, mfg[gn,t]<=prod_above[gn,t-1] + (is_on[gn,t-1]*minp[t]) + + (RU * is_on[gn,t-1]) + + (SU*(is_on[gn,t] - is_on[gn,t-1])) + + maxp[t] * (1-is_on[gn,t]) + ) # Eq. (23) in Wang & Hobbs (2016) for the last time period + @constraint(model, (prod_above[gn,t-1] + (is_on[gn,t-1]*minp[t])) + - (prod_above[gn,t] + (is_on[gn,t]*minp[t])) + <= RD * is_on[gn,t] + + SD * (is_on[gn,t-1] - is_on[gn,t]) + + maxp[t] * (1-is_on[gn,t-1]) + ) # Eq. (25) in Wang & Hobbs (2016) for the last time period + end + end +end \ No newline at end of file diff --git a/src/model/formulations/WanHob2016/structs.jl b/src/model/formulations/WanHob2016/structs.jl new file mode 100644 index 0000000..a8c33f8 --- /dev/null +++ b/src/model/formulations/WanHob2016/structs.jl @@ -0,0 +1,18 @@ +# UnitCommitmentFL.jl: Optimization Package for Security-Constrained Unit Commitment +# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +""" +Formulation described in: + B. Wang and B. F. Hobbs, "Real-Time Markets for Flexiramp: A Stochastic + Unit Commitment-Based Analysis," in IEEE Transactions on Power Systems, + vol. 31, no. 2, pp. 846-860, March 2016, doi: 10.1109/TPWRS.2015.2411268. +""" +module WanHob2016 + +import ..RampingFormulation + + +struct Ramping <: RampingFormulation end + +end diff --git a/src/model/formulations/base/system.jl b/src/model/formulations/base/system.jl index d6bf573..d4944e1 100644 --- a/src/model/formulations/base/system.jl +++ b/src/model/formulations/base/system.jl @@ -5,6 +5,7 @@ function _add_system_wide_eqs!(model::JuMP.Model)::Nothing _add_net_injection_eqs!(model) _add_reserve_eqs!(model) + _add_flexiramp_eqs!(model) # Add system-wide flexiramp requirements return end @@ -54,3 +55,41 @@ function _add_reserve_eqs!(model::JuMP.Model)::Nothing end return end + +function _add_flexiramp_eqs!(model::JuMP.Model)::Nothing + # Note: The flexpramp requirements in Wang & Hobbs (2016) are imposed as hard constraints + # through Eq. (17) and Eq. (18). The constraints eq_min_upflexiramp[t] and eq_min_dwflexiramp[t] + # provided below are modified versions of Eq. (17) and Eq. (18), respectively, in that + # they include slack variables for flexiramp shortfall, which are penalized in the + # objective function. + eq_min_upflexiramp = _init(model, :eq_min_upflexiramp) + eq_min_dwflexiramp = _init(model, :eq_min_dwflexiramp) + instance = model[:instance] + for t in 1:instance.time + flexiramp_shortfall_penalty = instance.flexiramp_shortfall_penalty[t] + # Eq. (17) in Wang & Hobbs (2016) + eq_min_upflexiramp[t] = @constraint( + model, + sum(model[:upflexiramp][g.name, t] for g in instance.units) + + (flexiramp_shortfall_penalty >= 0 ? model[:upflexiramp_shortfall][t] : 0.0) >= + instance.reserves.upflexiramp[t] + ) + # Eq. (18) in Wang & Hobbs (2016) + eq_min_dwflexiramp[t] = @constraint( + model, + sum(model[:dwflexiramp][g.name, t] for g in instance.units) + + (flexiramp_shortfall_penalty >= 0 ? model[:dwflexiramp_shortfall][t] : 0.0) >= + instance.reserves.dwflexiramp[t] + ) + + # Account for flexiramp shortfall contribution to objective + if flexiramp_shortfall_penalty >= 0 + add_to_expression!( + model[:obj], + flexiramp_shortfall_penalty, + (model[:upflexiramp_shortfall][t]+model[:dwflexiramp_shortfall][t]), + ) + end + end + return +end diff --git a/src/model/formulations/base/unit.jl b/src/model/formulations/base/unit.jl index e701977..5da9ffa 100644 --- a/src/model/formulations/base/unit.jl +++ b/src/model/formulations/base/unit.jl @@ -13,6 +13,7 @@ function _add_unit!(model::JuMP.Model, g::Unit, formulation::Formulation) # Variables _add_production_vars!(model, g, formulation.prod_vars) _add_reserve_vars!(model, g) + _add_flexiramp_vars!(model, g) # Add variables for flexiramp _add_startup_shutdown_vars!(model, g) _add_status_vars!(model, g, formulation.status_vars) diff --git a/src/solution/solution.jl b/src/solution/solution.jl index 5fd8bbd..0e95fa0 100644 --- a/src/solution/solution.jl +++ b/src/solution/solution.jl @@ -50,13 +50,35 @@ function solution(model::JuMP.Model)::OrderedDict sol["Is on"] = timeseries(model[:is_on], instance.units) 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 - ) + if instance.reserves.upflexiramp != zeros(T) || instance.reserves.dwflexiramp != zeros(T) + # Report flexiramp solutions only if either of the up-flexiramp and + # down-flexiramp requirements is not a default array of zeros + sol["Up-flexiramp (MW)"] = timeseries(model[:upflexiramp], instance.units) + sol["Up-flexiramp shortfall (MW)"] = OrderedDict( + t => + (instance.flexiramp_shortfall_penalty[t] >= 0) ? + round(value(model[:upflexiramp_shortfall][t]), digits = 5) : 0.0 for + t in 1:instance.time + ) + sol["Down-flexiramp (MW)"] = timeseries(model[:dwflexiramp], instance.units) + sol["Down-flexiramp shortfall (MW)"] = OrderedDict( + t => + (instance.flexiramp_shortfall_penalty[t] >= 0) ? + round(value(model[:dwflexiramp_shortfall][t]), digits = 5) : 0.0 for + t in 1:instance.time + ) + else + # Report spinning reserve solutions only if both up-flexiramp and + # down-flexiramp requirements are arrays of zeros. + 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 + ) + + end 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 d342ef9..39546a9 100644 --- a/src/validation/validate.jl +++ b/src/validation/validate.jl @@ -338,6 +338,40 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01) ) err_count += 1 end + + upflexiramp = + sum(solution["Up-flexiramp (MW)"][g.name][t] for g in instance.units) + upflexiramp_shortfall = + (instance.flexiramp_shortfall_penalty[t] >= 0) ? + solution["Up-flexiramp shortfall (MW)"][t] : 0 + + if upflexiramp + upflexiramp_shortfall < instance.reserves.upflexiramp[t] - tol + @error @sprintf( + "Insufficient up-flexiramp at time %d (%.2f + %.2f should be %.2f)", + t, + upflexiramp, + upflexiramp_shortfall, + instance.reserves.upflexiramp[t], + ) + err_count += 1 + end + + dwflexiramp = + sum(solution["Down-flexiramp (MW)"][g.name][t] for g in instance.units) + dwflexiramp_shortfall = + (instance.flexiramp_shortfall_penalty[t] >= 0) ? + solution["Down-flexiramp shortfall (MW)"][t] : 0 + + if dwflexiramp + dwflexiramp_shortfall < instance.reserves.dwflexiramp[t] - tol + @error @sprintf( + "Insufficient down-flexiramp at time %d (%.2f + %.2f should be %.2f)", + t, + dwflexiramp, + dwflexiramp_shortfall, + instance.reserves.dwflexiramp[t], + ) + err_count += 1 + end end return err_count