diff --git a/instances/test/case14.json.gz b/instances/test/case14.json.gz index 682622f..0b7b54d 100644 Binary files a/instances/test/case14.json.gz and b/instances/test/case14.json.gz differ diff --git a/src/instance/read.jl b/src/instance/read.jl index 5606adb..ad033e7 100644 --- a/src/instance/read.jl +++ b/src/instance/read.jl @@ -66,7 +66,7 @@ function _from_json(json; repair = true) contingencies = Contingency[] lines = TransmissionLine[] loads = PriceSensitiveLoad[] - reserves2 = Reserve[] + reserves = Reserve[] function scalar(x; default = nothing) x !== nothing || return default @@ -119,9 +119,9 @@ function _from_json(json; repair = true) end # Read reserves - if "Reserves2" in keys(json) - for (reserve_name, dict) in json["Reserves2"] - reserve = Reserve( + if "Reserves" in keys(json) + for (reserve_name, dict) in json["Reserves"] + r = Reserve( name = reserve_name, type = lowercase(dict["Type"]), amount = timeseries(dict["Amount (MW)"]), @@ -131,8 +131,8 @@ function _from_json(json; repair = true) default = -1, ), ) - name_to_reserve[reserve_name] = reserve - push!(reserves2, reserve) + name_to_reserve[reserve_name] = r + push!(reserves, r) end end @@ -173,7 +173,7 @@ function _from_json(json; repair = true) ) end - # Read reserves + # Read reserve eligibility unit_reserves = Reserve[] if "Reserve eligibility" in keys(dict) unit_reserves = @@ -213,10 +213,6 @@ function _from_json(json; repair = true) scalar(dict["Shutdown limit (MW)"], default = 1e6), initial_status, initial_power, - timeseries( - dict["Provides spinning reserves?"], - default = [true for t in 1:T], - ), startup_categories, unit_reserves, ) @@ -228,13 +224,6 @@ function _from_json(json; repair = true) push!(units, unit) end - # Read reserves - reserves = Reserves(zeros(T)) - if "Reserves" in keys(json) - reserves.spinning = - timeseries(json["Reserves"]["Spinning (MW)"], default = zeros(T)) - end - # Read transmission lines if "Transmission lines" in keys(json) for (line_name, dict) in json["Transmission lines"] @@ -307,7 +296,6 @@ function _from_json(json; repair = true) price_sensitive_loads_by_name = Dict(ps.name => ps for ps in loads), price_sensitive_loads = loads, reserves = reserves, - reserves2 = reserves2, reserves_by_name = name_to_reserve, shortfall_penalty = shortfall_penalty, time = T, diff --git a/src/instance/structs.jl b/src/instance/structs.jl index 33827ba..91e87bb 100644 --- a/src/instance/structs.jl +++ b/src/instance/structs.jl @@ -44,7 +44,6 @@ mutable struct Unit shutdown_limit::Float64 initial_status::Union{Int,Nothing} initial_power::Union{Float64,Nothing} - provides_spinning_reserves::Vector{Bool} startup_categories::Vector{StartupCategory} reserves::Vector{Reserve} end @@ -61,10 +60,6 @@ mutable struct TransmissionLine flow_limit_penalty::Vector{Float64} end -mutable struct Reserves - spinning::Vector{Float64} -end - mutable struct Contingency name::String lines::Vector{TransmissionLine} @@ -88,8 +83,7 @@ Base.@kwdef mutable struct UnitCommitmentInstance power_balance_penalty::Vector{Float64} price_sensitive_loads_by_name::Dict{AbstractString,PriceSensitiveLoad} price_sensitive_loads::Vector{PriceSensitiveLoad} - reserves::Reserves - reserves2::Vector{Reserve} + reserves::Vector{Reserve} reserves_by_name::Dict{AbstractString,Reserve} shortfall_penalty::Vector{Float64} time::Int diff --git a/src/model/formulations/base/system.jl b/src/model/formulations/base/system.jl index 7fef70f..a265b2e 100644 --- a/src/model/formulations/base/system.jl +++ b/src/model/formulations/base/system.jl @@ -28,42 +28,18 @@ function _add_net_injection_eqs!(model::JuMP.Model)::Nothing end function _add_reserve_eqs!(model::JuMP.Model)::Nothing - eq_min_reserve = _init(model, :eq_min_reserve) 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[: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 - - eq_min_reserve2 = _init(model, :eq_min_reserve2) - for r in instance.reserves2 + eq_min_reserve = _init(model, :eq_min_reserve) + for r in instance.reserves 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) - eq_min_reserve2[r.name, t] = @constraint( + eq_min_reserve[r.name, t] = @constraint( model, - sum(model[:reserve2][r.name, g.name, t] for g in r.units) + - model[:reserve_shortfall2][r.name, t] >= r.amount[t] + sum(model[:reserve][r.name, g.name, t] for g in r.units) + + model[:reserve_shortfall][r.name, t] >= r.amount[t] ) # Account for shortfall contribution to objective @@ -71,7 +47,7 @@ function _add_reserve_eqs!(model::JuMP.Model)::Nothing add_to_expression!( model[:obj], r.shortfall_penalty, - model[:reserve_shortfall2][r.name, t], + model[:reserve_shortfall][r.name, t], ) end end diff --git a/src/model/formulations/base/unit.jl b/src/model/formulations/base/unit.jl index 51de4f2..1e08a90 100644 --- a/src/model/formulations/base/unit.jl +++ b/src/model/formulations/base/unit.jl @@ -45,26 +45,13 @@ _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 - - reserve2 = _init(model, :reserve2) - reserve_shortfall2 = _init(model, :reserve_shortfall2) for r in g.reserves for t in 1:model[:instance].time - reserve2[r.name, g.name, t] = @variable(model, lower_bound = 0) - if (r.name, t) ∉ keys(reserve_shortfall2) - reserve_shortfall2[r.name, t] = @variable(model, lower_bound = 0) + reserve[r.name, g.name, t] = @variable(model, lower_bound = 0) + if (r.name, t) ∉ keys(reserve_shortfall) + reserve_shortfall[r.name, t] = @variable(model, lower_bound = 0) if r.shortfall_penalty < 0 - set_upper_bound(reserve_shortfall2[r.name, t], 0.0) + set_upper_bound(reserve_shortfall[r.name, t], 0.0) end end end @@ -225,10 +212,10 @@ end function _total_reserves(model, g)::Vector T = model[:instance].time - reserve = [model[:reserve][g.name, t] for t in 1:T] + reserve = 0.0 if !isempty(g.reserves) reserve += [ - sum(model[:reserve2][r.name, g.name, t] for r in g.reserves) for + sum(model[:reserve][r.name, g.name, t] for r in g.reserves) for t in 1:model[:instance].time ] end diff --git a/src/solution/solution.jl b/src/solution/solution.jl index 35de2b3..57d6ef7 100644 --- a/src/solution/solution.jl +++ b/src/solution/solution.jl @@ -50,13 +50,6 @@ 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 - ) sol["Net injection (MW)"] = timeseries(model[:net_injection], instance.buses) sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses) @@ -67,19 +60,19 @@ function solution(model::JuMP.Model)::OrderedDict sol["Price-sensitive loads (MW)"] = timeseries(model[:loads], instance.price_sensitive_loads) end - sol["Reserve 2 (MW)"] = OrderedDict( + sol["Reserve (MW)"] = OrderedDict( r.name => OrderedDict( g.name => [ - value(model[:reserve2][r.name, g.name, t]) for + value(model[:reserve][r.name, g.name, t]) for t in 1:instance.time ] for g in r.units - ) for r in instance.reserves2 + ) for r in instance.reserves ) - sol["Reserve shortfall 2 (MW)"] = OrderedDict( + sol["Reserve shortfall (MW)"] = OrderedDict( r.name => [ - value(model[:reserve_shortfall2][r.name, t]) for + value(model[:reserve_shortfall][r.name, t]) for t in 1:instance.time - ] for r in instance.reserves2 + ] for r in instance.reserves ) return sol end diff --git a/src/validation/validate.jl b/src/validation/validate.jl index 62def52..5d69bc7 100644 --- a/src/validation/validate.jl +++ b/src/validation/validate.jl @@ -329,30 +329,12 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01) err_count += 1 end - # Verify spinning reserves - reserve = - sum(solution["Reserve (MW)"][g.name][t] for g in instance.units) - 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 + %.2f should be %.2f)", - t, - reserve, - reserve_shortfall, - instance.reserves.spinning[t], - ) - err_count += 1 - end - # Verify reserves - for r in instance.reserves2 + for r in instance.reserves provided = sum( - solution["Reserve 2 (MW)"][r.name][g.name][t] for g in r.units + solution["Reserve (MW)"][r.name][g.name][t] for g in r.units ) - shortfall = solution["Reserve shortfall 2 (MW)"][r.name][t] + shortfall = solution["Reserve shortfall (MW)"][r.name][t] required = r.amount[t] if provided + shortfall < required - tol