# UnitCommitment.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_ramp_eqs!( model::JuMP.Model, g::Unit, formulation::MorLatRam2013, )::Nothing # TODO: Move upper case constants to model[:instance] RESERVES_WHEN_START_UP = true RESERVES_WHEN_RAMP_UP = true RESERVES_WHEN_RAMP_DOWN = true RESERVES_WHEN_SHUT_DOWN = true 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 eq_ramp_down = _init(model, :eq_ramp_down) eq_ramp_up = _init(model, :eq_str_ramp_up) is_on = model[:is_on] prod_above = model[:prod_above] reserve = model[:reserve] switch_off = model[:switch_off] switch_on = model[:switch_on] for t in 1:model[:instance].time time_invariant = (t > 1) ? (abs(g.min_power[t] - g.min_power[t-1]) < 1e-7) : true # Ramp up limit if t == 1 if is_initially_on eq_ramp_up[gn, t] = @constraint( model, g.min_power[t] + prod_above[gn, t] + (RESERVES_WHEN_RAMP_UP ? reserve[gn, t] : 0.0) <= g.initial_power + RU ) end else # amk: without accounting for time-varying min power terms, # we might get an infeasible schedule, e.g. if min_power[t-1] = 0, min_power[t] = 10 # and ramp_up_limit = 5, the constraint (p'(t) + r(t) <= p'(t-1) + RU) # would be satisfied with p'(t) = r(t) = p'(t-1) = 0 # Note that if switch_on[t] = 1, then eqns (20) or (21) go into effect if !time_invariant # Use equation (24) instead SU = g.startup_limit max_prod_this_period = g.min_power[t] * is_on[gn, t] + prod_above[gn, t] + ( RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ? reserve[gn, t] : 0.0 ) min_prod_last_period = g.min_power[t-1] * is_on[gn, t-1] + prod_above[gn, t-1] eq_ramp_up[gn, t] = @constraint( model, max_prod_this_period - min_prod_last_period <= RU * is_on[gn, t-1] + SU * switch_on[gn, t] ) else # Equation (26) in Kneuven et al. (2020) # TODO: what if RU < SU? places too stringent upper bound # prod_above[gn, t] when starting up, and creates diff with (24). eq_ramp_up[gn, t] = @constraint( model, prod_above[gn, t] + (RESERVES_WHEN_RAMP_UP ? reserve[gn, t] : 0.0) - prod_above[gn, t-1] <= RU ) end end # Ramp down limit if t == 1 if is_initially_on # TODO If RD < SD, or more specifically if # min_power + RD < initial_power < SD # then the generator should be able to shut down at time t = 1, # but the constraint below will force the unit to produce power eq_ramp_down[gn, t] = @constraint( model, g.initial_power - (g.min_power[t] + prod_above[gn, t]) <= RD ) end else # amk: similar to ramp_up, need to account for time-dependent min_power if !time_invariant # Revert to (25) SD = g.shutdown_limit max_prod_last_period = g.min_power[t-1] * is_on[gn, t-1] + prod_above[gn, t-1] + ( RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ? reserve[gn, t-1] : 0.0 ) min_prod_this_period = g.min_power[t] * is_on[gn, t] + prod_above[gn, t] eq_ramp_down[gn, t] = @constraint( model, max_prod_last_period - min_prod_this_period <= RD * is_on[gn, t] + SD * switch_off[gn, t] ) else # Equation (27) in Kneuven et al. (2020) # TODO: Similar to above, what to do if shutting down in time t # and RD < SD? There is a difference with (25). eq_ramp_down[gn, t] = @constraint( model, prod_above[gn, t-1] + (RESERVES_WHEN_RAMP_DOWN ? reserve[gn, t-1] : 0.0) - prod_above[gn, t] <= RD ) end end end end