Documented reserve shortfall, soved comments on variables/constraints to structs.jl files, simplified loops, removed extra comments, started replacement of constant-subsitution with @constraint (with option to use fix).

pull/13/head
Aleksandr Kazachkov 4 years ago
parent 77f2f625fd
commit 9649387561

@ -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. | `0` | Y
#### Example
@ -43,6 +44,7 @@ This section describes system-wide parameters, such as power balance penalties,
"Parameters": {
"Time horizon (h)": 4,
"Power balance penalty ($/MW)": 1000.0
"Reserve shortfall penalty ($/MW)": 0.0
}
}
```

@ -3,24 +3,13 @@
# Released under the modified BSD license. See COPYING.md for more details.
"""
_add_ramp_eqs!
_add_ramp_eqs!(model, unit, formulation_prod_vars, formulation_ramping, formulation_status_vars)::Nothing
Ensure constraints on ramping are met.
Based on Arroyo and Conejo (2000).
Eqns. (24), (25) in Kneuven et al. (2020).
Eqns. (24), (25) in Knueven et al. (2020).
Variables
---
* :is_on
* :switch_off
* :switch_on
* :prod_above
* :reserve
Constraints
---
* :eq_ramp_up
* :eq_ramp_down
Adds constraints identified by `ArrCon200.Ramping` to `model` using variables `Gar1962.ProdVars` and `is_on` from `Gar1962.StatusVars`.
"""
function _add_ramp_eqs!(
model::JuMP.Model,
@ -75,7 +64,7 @@ function _add_ramp_eqs!(
min_prod_last_period =
g.min_power[t-1] * is_on[gn, t-1] + prod_above[gn, t-1]
# Equation (24) in Kneuven et al. (2020)
# Equation (24) in Knueven et al. (2020)
eq_ramp_up[gn, t] = @constraint(
model,
max_prod_this_period - min_prod_last_period <=
@ -106,7 +95,7 @@ function _add_ramp_eqs!(
min_prod_this_period =
g.min_power[t] * is_on[gn, t] + prod_above[gn, t]
# Equation (25) in Kneuven et al. (2020)
# Equation (25) in Knueven et al. (2020)
eq_ramp_down[gn, t] = @constraint(
model,
max_prod_last_period - min_prod_this_period <=

@ -13,6 +13,12 @@ module ArrCon2000
import ..RampingFormulation
"""
Constraints
---
* `eq_ramp_up`: Equation (24) in Knueven et al. (2020)
* `eq_ramp_down`: Equation (25) in Knueven et al. (2020)
"""
struct Ramping <: RampingFormulation end
end

@ -6,23 +6,7 @@
_add_production_piecewise_linear_eqs!
Ensure respect of production limits along each segment.
Based on Garver (1962) and Carrión and Arryo (2006),
which replaces (42) in Kneuven et al. (2020) with a weaker version missing the on/off variable.
Equations (45), (43), (44) in Kneuven et al. (2020).
NB: when reading instance, UnitCommitment.jl already calculates difference between max power for segments k and k-1
so the value of cost_segments[k].mw[t] is the max production *for that segment*.
===
Variables
* :segprod
* :is_on
* :prod_above
===
Constraints
* :eq_prod_above_def
* :eq_segprod_limit
Creates constraints `CarArr2006.PwlCosts` using variables `Gar1962.StatusVars`
"""
function _add_production_piecewise_linear_eqs!(
model::JuMP.Model,
@ -43,7 +27,7 @@ function _add_production_piecewise_linear_eqs!(
for t in 1:model[:instance].time
gn = g.name
for k in 1:K
# Equation (45) in Kneuven et al. (2020)
# Equation (45) in Knueven et al. (2020)
# NB: when reading instance, UnitCommitment.jl already calculates
# difference between max power for segments k and k-1 so the
# value of cost_segments[k].mw[t] is the max production *for
@ -58,14 +42,14 @@ function _add_production_piecewise_linear_eqs!(
set_upper_bound(segprod[gn, t, k], g.cost_segments[k].mw[t])
# Definition of production
# Equation (43) in Kneuven et al. (2020)
# Equation (43) in Knueven et al. (2020)
eq_prod_above_def[gn, t] = @constraint(
model,
prod_above[gn, t] == sum(segprod[gn, t, k] for k in 1:K)
)
# Objective function
# Equation (44) in Kneuven et al. (2020)
# Equation (44) in Knueven et al. (2020)
add_to_expression!(
model[:obj],
segprod[gn, t, k],

@ -14,6 +14,16 @@ module CarArr2006
import ..PiecewiseLinearCostsFormulation
"""
Based on Garver (1962) and Carrión and Arryo (2006),
which replaces (42) in Knueven et al. (2020) with a weaker version missing the on/off variable.
Equations (45), (43), (44) in Knueven et al. (2020).
Constraints
---
* `eq_prod_above_def`: Equation (43) in Knueven et al. (2020)
* `eq_segprod_limit`: Equation (45) in Knueven et al. (2020)
"""
struct PwlCosts <: PiecewiseLinearCostsFormulation end
end

@ -7,7 +7,7 @@
Ensure constraints on ramping are met.
Based on Damcı-Kurt et al. (2016).
Eqns. (35), (36) in Kneuven et al. (2020).
Eqns. (35), (36) in Knueven et al. (2020).
Variables
---
@ -78,7 +78,7 @@ function _add_ramp_eqs!(
if t > 1 && time_invariant
min_prod_last_period = prod_above[gn, t-1]
# Equation (35) in Kneuven et al. (2020)
# Equation (35) in Knueven et al. (2020)
# Sparser version of (24)
eq_str_ramp_up[gn, t] = @constraint(
model,
@ -98,7 +98,7 @@ function _add_ramp_eqs!(
# (instead of using the amount above minimum, as min prod for t < 1 is unknown)
max_prod_this_period += g.min_power[t] * is_on[gn, t]
# Modified version of equation (35) in Kneuven et al. (2020)
# Modified version of equation (35) in Knueven et al. (2020)
# Equivalent to (24)
eq_str_ramp_up[gn, t] = @constraint(
model,
@ -121,7 +121,7 @@ function _add_ramp_eqs!(
end
if t > 1 && time_invariant
# Equation (36) in Kneuven et al. (2020)
# Equation (36) in Knueven et al. (2020)
eq_str_ramp_down[gn, t] = @constraint(
model,
max_prod_last_period - min_prod_this_period <=
@ -132,7 +132,7 @@ function _add_ramp_eqs!(
# Add back in min power
min_prod_this_period += g.min_power[t] * is_on[gn, t]
# Modified version of equation (36) in Kneuven et al. (2020)
# Modified version of equation (36) in Knueven et al. (2020)
# Equivalent to (25)
eq_str_ramp_down[gn, t] = @constraint(
model,

@ -5,7 +5,7 @@
"""
_add_production_vars!(model, unit, formulation_prod_vars)
Creates variables `:prod_above` and `:segprod`.
Adds symbols identified by `Gar1962.ProdVars` to `model`.
"""
function _add_production_vars!(
model::JuMP.Model,
@ -28,7 +28,7 @@ end
Ensure production limit constraints are met.
Based on Garver (1962) and Morales-España et al. (2013).
Eqns. (18), part of (69) in Kneuven et al. (2020).
Eqns. (18), part of (69) in Knueven et al. (2020).
===
Variables
@ -52,13 +52,13 @@ function _add_production_limit_eqs!(
gn = g.name
for t in 1:model[:instance].time
# Objective function terms for production costs
# Part of (69) of Kneuven et al. (2020) as C^R_g * u_g(t) term
# Part of (69) of Knueven et al. (2020) as C^R_g * u_g(t) term
add_to_expression!(model[:obj], is_on[gn, t], g.min_power_cost[t])
# Production limit
# Equation (18) in Kneuven et al. (2020)
# Equation (18) in Knueven et al. (2020)
# as \bar{p}_g(t) \le \bar{P}_g u_g(t)
# amk: this is a weaker version of (20) and (21) in Kneuven et al. (2020)
# amk: this is a weaker version of (20) and (21) in Knueven et al. (2020)
# but keeping it here in case those are not present
power_diff = max(g.max_power[t], 0.0) - max(g.min_power[t], 0.0)
if power_diff < 1e-7

@ -7,7 +7,7 @@
Ensure respect of production limits along each segment.
Based on Garver (1962).
Equations (42), (43), (44) in Kneuven et al. (2020).
Equations (42), (43), (44) in Knueven et al. (2020).
NB: when reading instance, UnitCommitment.jl already calculates difference between max power for segments k and k-1,
so the value of cost_segments[k].mw[t] is the max production *for that segment*.
@ -45,14 +45,14 @@ function _add_production_piecewise_linear_eqs!(
K = length(g.cost_segments)
for t in 1:model[:instance].time
# Definition of production
# Equation (43) in Kneuven et al. (2020)
# Equation (43) in Knueven et al. (2020)
eq_prod_above_def[gn, t] = @constraint(
model,
prod_above[gn, t] == sum(segprod[gn, t, k] for k in 1:K)
)
for k in 1:K
# Equation (42) in Kneuven et al. (2020)
# Equation (42) in Knueven et al. (2020)
# Without this, solvers will add a lot of implied bound cuts to
# have this same effect.
# NB: when reading instance, UnitCommitment.jl already calculates
@ -69,7 +69,7 @@ function _add_production_piecewise_linear_eqs!(
set_upper_bound(segprod[gn, t, k], g.cost_segments[k].mw[t])
# Objective function
# Equation (44) in Kneuven et al. (2020)
# Equation (44) in Knueven et al. (2020)
add_to_expression!(
model[:obj],
segprod[gn, t, k],

@ -5,39 +5,26 @@
"""
_add_status_vars!
Create `is_on`, `switch_on`, and `switch_off` variables.
Adds symbols identified by `Gar1962.StatusVars` to `model`.
Fix variables if a certain generator _must_ run or based on initial conditions.
"""
function _add_status_vars!(
model::JuMP.Model,
g::Unit,
formulation_status_vars::Gar1962.StatusVars,
ALWAYS_CREATE_VARS = false,
)::Nothing
is_on = _init(model, :is_on)
switch_on = _init(model, :switch_on)
switch_off = _init(model, :switch_off)
FIX_VARS = !formulation_status_vars.fix_vars_via_constraint
for t in 1:model[:instance].time
if ALWAYS_CREATE_VARS || !g.must_run[t]
is_on[g.name, t] = @variable(model, binary = true)
switch_on[g.name, t] = @variable(model, binary = true)
switch_off[g.name, t] = @variable(model, binary = true)
end
if ALWAYS_CREATE_VARS
# If variables are created, use initial conditions to fix some values
if t == 1
if _is_initially_on(g)
# Generator was on (for g.initial_status time periods),
# so cannot be more switched on until the period after the first time it can be turned off
fix(switch_on[g.name, 1], 0.0; force = true)
else
# Generator is initially off (for -g.initial_status time periods)
# Cannot be switched off more
fix(switch_off[g.name, 1], 0.0; force = true)
end
end
is_on[g.name, t] = @variable(model, binary = true)
switch_on[g.name, t] = @variable(model, binary = true)
switch_off[g.name, t] = @variable(model, binary = true)
# Use initial conditions and whether a unit must run to fix variables
if FIX_VARS
# Fix variables using fix function
if g.must_run[t]
# If the generator _must_ run, then it is obviously on and cannot be switched off
# In the first time period, force unit to switch on if was off before
@ -49,23 +36,32 @@ function _add_status_vars!(
force = true,
)
fix(switch_off[g.name, t], 0.0; force = true)
end
else
# If vars are not created, then replace them by a constant
if t == 1
elseif t == 1
if _is_initially_on(g)
switch_on[g.name, t] = 0.0
# Generator was on (for g.initial_status time periods),
# so cannot be more switched on until the period after the first time it can be turned off
fix(switch_on[g.name, 1], 0.0; force = true)
else
switch_off[g.name, t] = 0.0
# Generator is initially off (for -g.initial_status time periods)
# Cannot be switched off more
fix(switch_off[g.name, 1], 0.0; force = true)
end
end
else
# Add explicit constraint if !FIX_VARS
if g.must_run[t]
is_on[g.name, t] = 1.0
switch_on[g.name, t] =
(t == 1 ? 1.0 - _is_initially_on(g) : 0.0)
switch_off[g.name, t] = 0.0
elseif t == 1
if _is_initially_on(g)
switch_on[g.name, t] = 0.0
else
switch_off[g.name, t] = 0.0
end
end
end # check if ALWAYS_CREATE_VARS
end
end
return
end
@ -73,16 +69,12 @@ end
"""
_add_status_eqs!
Variables
---
* is_on
* switch_off
* switch_on
Creates constraints `eq_binary_link` and `eq_switch_on_off` using variables in `Gar1962.StatusVars`.
Constraints
---
* eq_binary_link
* eq_switch_on_off
* `eq_binary_link`
* `eq_switch_on_off`
"""
function _add_status_eqs!(
model::JuMP.Model,
@ -100,7 +92,7 @@ function _add_status_eqs!(
end
# Link binary variables
# Equation (2) in Kneuven et al. (2020), originally from Garver (1962)
# Equation (2) in Knueven et al. (2020), originally from Garver (1962)
if t == 1
eq_binary_link[g.name, t] = @constraint(
model,
@ -116,7 +108,7 @@ function _add_status_eqs!(
end
# Cannot switch on and off at the same time
# amk: I am not sure this is in Kneuven et al. (2020)
# amk: I am not sure this is in Knueven et al. (2020)
eq_switch_on_off[g.name, t] = @constraint(
model,
switch_on[g.name, t] + switch_off[g.name, t] <= 1

@ -17,8 +17,53 @@ import ..PiecewiseLinearCostsFormulation
import ..ProductionVarsFormulation
import ..StatusVarsFormulation
"""
Variables
---
* `prod_above`:
[gen, t];
*production above minimum required level*;
lb: 0, ub: Inf.
KnuOstWat2020: `p'_g(t)`
* `segprod`:
[gen, segment, t];
*how much generator produces on cost segment in time t*;
lb: 0, ub: Inf.
KnuOstWat2020: `p_g^l(t)`
"""
struct ProdVars <: ProductionVarsFormulation end
struct PwlCosts <: PiecewiseLinearCostsFormulation end
struct StatusVars <: StatusVarsFormulation end
"""
Variables
---
* `is_on`:
[gen, t];
*is generator on at time t?*
lb: 0, ub: 1, binary.
KnuOstWat2020: `u_g(t)`
* `switch_on`:
[gen, t];
*indicator that generator will be turned on at t*;
lb: 0, ub: 1, binary.
KnuOstWat2020: `v_g(t)`
* `switch_off`: binary;
[gen, t];
*indicator that generator will be turned off at t*;
lb: 0, ub: 1, binary.
KnuOstWat2020: `w_g(t)`
Arguments
---
* `fix_vars_via_constraint`:
indicator for whether to set vars to a constant using `fix` or by adding an explicit constraint
(particulary useful for debugging purposes).
"""
struct StatusVars <: StatusVarsFormulation
fix_vars_via_constraint::Bool
StatusVars() = new(false)
end
end

@ -6,23 +6,21 @@
_add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
Startup and shutdown limits from Gentile et al. (2017).
Eqns. (20), (23a), and (23b) in Kneuven et al. (2020).
Eqns. (20), (23a), and (23b) in Knueven et al. (2020).
Variables
---
* :is_on
* :prod_above
* :reserve
* :switch_on
* :switch_off
Creates constraints `eq_startstop_limit`, `eq_startup_limit`, and `eq_shutdown_limit`
using variables `Gar1962.StatusVars`, `prod_above` from `Gar1962.ProdVars`, and `reserve`.
Constraints
---
* :eq_startstop_limit
* :eq_startup_limit
* :eq_shutdown_limit
* `eq_startstop_limit`
* `eq_startup_limit`
* `eq_shutdown_limit`
"""
function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
function _add_startup_shutdown_limit_eqs!(
model::JuMP.Model,
g::Unit,
formulation_status_vars::Gar1962.StatusVars)::Nothing
# TODO: Move upper case constants to model[:instance]
RESERVES_WHEN_START_UP = true
RESERVES_WHEN_RAMP_UP = true
@ -44,14 +42,19 @@ function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
if g.initial_power > g.shutdown_limit
#eqs.shutdown_limit[gi, 0] = @constraint(mip, vars.switch_off[gi, 1] <= 0)
fix(switch_off[gi, 1], 0.0; force = true)
if formulation_status_vars.always_create_vars
fix(switch_off[gi, 1], 0.0; force = true)
@constraint(mip, vars.switch_off[gi, 1] <= 0)
else
switch_off[gi, 1] = 0.0
end
end
for t in 1:T
## 2020-10-09 amk: added eqn (20) and check of g.min_uptime
# Not present in (23) in Kneueven et al.
if g.min_uptime > 1
# Equation (20) in Kneuven et al. (2020)
# Equation (20) in Knueven et al. (2020)
eqs.startstop_limit[gi, t] = @constraint(
model,
prod_above[gi, t] + reserve[gi, t] <=
@ -64,7 +67,7 @@ function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
)
else
## Startup limits
# Equation (23a) in Kneuven et al. (2020)
# Equation (23a) in Knueven et al. (2020)
eqs.startup_limit[gi, t] = @constraint(
model,
prod_above[gi, t] + reserve[gi, t] <=
@ -78,7 +81,7 @@ function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
## Shutdown limits
if t < T
# Equation (23b) in Kneuven et al. (2020)
# Equation (23b) in Knueven et al. (2020)
eqs.shutdown_limit[gi, t] = @constraint(
model,
prod_above[gi, t] + reserve[gi, t] <=
@ -91,6 +94,6 @@ function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
switch_on[gi, t]
)
end
end # check if g.min_uptime > 1
end # loop over time
end # _add_startup_shutdown_limit_eqs!
end
end
end

@ -6,8 +6,8 @@
_add_production_piecewise_linear_eqs!
Ensure respect of production limits along each segment.
Based on Kneuven et al. (2018b).
Eqns. (43), (44), (46), (48) in Kneuven et al. (2020).
Based on Knueven et al. (2018b).
Eqns. (43), (44), (46), (48) in Knueven et al. (2020).
NB: when reading instance, UnitCommitment.jl already calculates difference between max power for segments k and k-1
so the value of cost_segments[k].mw[t] is the max production *for that segment*.
@ -81,7 +81,7 @@ function _add_production_piecewise_linear_eqs!(
end
if g.min_uptime > 1
# Equation (46) in Kneuven et al. (2020)
# Equation (46) in Knueven et al. (2020)
eq_segprod_limit_a[gn, t, k] = @constraint(
model,
segprod[gn, t, k] <=
@ -90,7 +90,7 @@ function _add_production_piecewise_linear_eqs!(
(t < T ? Cw * switch_off[gn, t+1] : 0.0)
)
else
# Equation (47a)/(48a) in Kneuven et al. (2020)
# Equation (47a)/(48a) in Knueven et al. (2020)
eq_segprod_limit_b[gn, t, k] = @constraint(
model,
segprod[gn, t, k] <=
@ -99,7 +99,7 @@ function _add_production_piecewise_linear_eqs!(
(t < T ? max(0, Cv - Cw) * switch_off[gn, t+1] : 0.0)
)
# Equation (47b)/(48b) in Kneuven et al. (2020)
# Equation (47b)/(48b) in Knueven et al. (2020)
eq_segprod_limit_c[gn, t, k] = @constraint(
model,
segprod[gn, t, k] <=
@ -110,14 +110,14 @@ function _add_production_piecewise_linear_eqs!(
end
# Definition of production
# Equation (43) in Kneuven et al. (2020)
# Equation (43) in Knueven et al. (2020)
eq_prod_above_def[gn, t] = @constraint(
model,
prod_above[gn, t] == sum(segprod[gn, t, k] for k in 1:K)
)
# Objective function
# Equation (44) in Kneuven et al. (2020)
# Equation (44) in Knueven et al. (2020)
add_to_expression!(
model[:obj],
segprod[gn, t, k],

@ -6,7 +6,7 @@
_add_startup_cost_eqs!
Extended formulation of startup costs using indicator variables
based on Kneuven, Ostrowski, and Watson, 2020
based on Knueven, Ostrowski, and Watson, 2020
--- equations (59), (60), (61).
Variables
@ -58,7 +58,7 @@ function _add_startup_cost_eqs!(
# fix(vars.downtime_arc[gn, t, tmp_t], 0.; force = true)
#end
# Equation (59) in Kneuven et al. (2020)
# Equation (59) in Knueven et al. (2020)
# Relate downtime_arc with switch_on
# "switch_on[g,t] >= x_g(t',t) for all t' \in [t-TC+1, t-DT]"
eq_startup_at_t[gn, t] = @constraint(
@ -69,7 +69,7 @@ function _add_startup_cost_eqs!(
)
)
# Equation (60) in Kneuven et al. (2020)
# Equation (60) in Knueven et al. (2020)
# "switch_off[g,t] >= x_g(t,t') for all t' \in [t+DT, t+TC-1]"
eqs.shutdown_at_t[gn, t] = @constraint(
model,
@ -80,7 +80,7 @@ function _add_startup_cost_eqs!(
)
# Objective function terms for start-up costs
# Equation (61) in Kneuven et al. (2020)
# Equation (61) in Knueven et al. (2020)
default_category = S
if initial_time_shutdown > 0 && t + initial_time_shutdown - 1 < TC
for s in 1:S-1
@ -104,7 +104,7 @@ function _add_startup_cost_eqs!(
for s in 1:S-1
# Objective function terms for start-up costs
# Equation (61) in Kneuven et al. (2020)
# Equation (61) in Knueven et al. (2020)
# Says to replace the cost of last category with cost of category s
start_range = max((t - g.startup_categories[s+1].delay + 1), 1)
end_range = min((t - g.startup_categories[s].delay), T - 1)

@ -6,9 +6,9 @@
_add_ramp_eqs!
Ensure constraints on ramping are met.
Needs to be used in combination with shutdown rate constraints, e.g., (21b) in Kneuven et al. (2020).
Needs to be used in combination with shutdown rate constraints, e.g., (21b) in Knueven et al. (2020).
Based on Morales-España, Latorre, and Ramos, 2013.
Eqns. (26)+(27) [replaced by (24)+(25) if time-varying min demand] in Kneuven et al. (2020).
Eqns. (26)+(27) [replaced by (24)+(25) if time-varying min demand] in Knueven et al. (2020).
Variables
---
@ -92,7 +92,7 @@ function _add_ramp_eqs!(
RU * is_on[gn, t-1] + SU * switch_on[gn, t]
)
else
# Equation (26) in Kneuven et al. (2020)
# Equation (26) in Knueven 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(
@ -136,7 +136,7 @@ function _add_ramp_eqs!(
RD * is_on[gn, t] + SD * switch_off[gn, t]
)
else
# Equation (27) in Kneuven et al. (2020)
# Equation (27) in Knueven 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(

@ -8,7 +8,7 @@
Extended formulation of startup costs using indicator variables
based on Muckstadt and Wilson, 1968;
this version by Morales-España, Latorre, and Ramos, 2013.
Eqns. (54), (55), and (56) in Kneuven et al. (2020).
Eqns. (54), (55), and (56) in Knueven et al. (2020).
Note that the last 'constraint' is actually setting the objective.
\tstartup[gi,s,t] sum_{i=s.delay}^{(s+1).delay-1} switch_off[gi,t-i]
@ -48,7 +48,7 @@ function _add_startup_cost_eqs!(
gn = g.name
for t in 1:model[:instance].time
# If unit is switching on, we must choose a startup category
# Equation (55) in Kneuven et al. (2020)
# Equation (55) in Knueven et al. (2020)
eq_startup_choose[gn, t] = @constraint(
model,
switch_on[gn, t] == sum(startup[gn, t, s] for s in 1:S)
@ -65,7 +65,7 @@ function _add_startup_cost_eqs!(
initial_sum = (
g.initial_status < 0 && (g.initial_status + 1 in range) ? 1.0 : 0.0
)
# Change of index version of equation (54) in Kneuven et al. (2020):
# Change of index version of equation (54) in Knueven et al. (2020):
# startup[gi,s,t] ≤ sum_{i=s.delay}^{(s+1).delay-1} switch_off[gi,t-i]
eq_startup_restrict[gn, t, s] = @constraint(
model,
@ -76,7 +76,7 @@ function _add_startup_cost_eqs!(
end # if s < S (not the last category)
# Objective function terms for start-up costs
# Equation (56) in Kneuven et al. (2020)
# Equation (56) in Knueven et al. (2020)
add_to_expression!(
model[:obj],
startup[gn, t, s],

@ -6,7 +6,7 @@
_add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
Startup and shutdown limits from Morales-España et al. (2013a).
Eqns. (20), (21a), and (21b) in Kneuven et al. (2020).
Eqns. (20), (21a), and (21b) in Knueven et al. (2020).
Variables
---
@ -44,7 +44,7 @@ function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
for t in 1:T
## 2020-10-09 amk: added eqn (20) and check of g.min_uptime
if g.min_uptime > 1 && t < T
# Equation (20) in Kneuven et al. (2020)
# Equation (20) in Knueven et al. (2020)
# UT > 1 required, to guarantee that vars.switch_on[gi, t] and vars.switch_off[gi, t+1] are not both = 1 at the same time
eq_startstop_limit[gi, t] = @constraint(
model,
@ -55,7 +55,7 @@ function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
)
else
## Startup limits
# Equation (21a) in Kneuven et al. (2020)
# Equation (21a) in Knueven et al. (2020)
# Proposed by Morales-España et al. (2013a)
eqs_startup_limit[gi, t] = @constraint(
model,
@ -66,7 +66,7 @@ function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
## Shutdown limits
if t < T
# Equation (21b) in Kneuven et al. (2020)
# Equation (21b) in Knueven et al. (2020)
# TODO different from what was in previous model, due to reserve variable
# ax: ideally should have reserve_up and reserve_down variables
# i.e., the generator should be able to increase/decrease production as specified

@ -7,7 +7,7 @@
Ensure constraints on ramping are met.
Based on Ostrowski, Anjos, Vannelli (2012).
Eqn (37) in Kneuven et al. (2020).
Eqn (37) in Knueven et al. (2020).
Variables
---
@ -59,7 +59,7 @@ function _add_ramp_eqs!(
Pbar = g.max_power[t]
#TRD = floor((Pbar - SU)/RD)
# TODO check amk changed TRD wrt Kneuven et al.
# TODO check amk changed TRD wrt Knueven et al.
TRD = ceil((Pbar - SD) / RD) # ramp down time
if Pbar < 1e-7
@ -68,7 +68,7 @@ function _add_ramp_eqs!(
end
if UT >= 1
# Equation (37) in Kneuven et al. (2020)
# Equation (37) in Knueven et al. (2020)
KSD = min(TRD, UT - 1, T - t - 1)
eq_str_prod_limit[gn, t] = @constraint(
model,

@ -7,8 +7,8 @@
Add tighter upper bounds on production based on ramp-down trajectory.
Based on (28) in Pan and Guan (2016).
But there is an extra time period covered using (40) of Kneuven et al. (2020).
Eqns. (38), (40), (41) in Kneuven et al. (2020).
But there is an extra time period covered using (40) of Knueven et al. (2020).
Eqns. (38), (40), (41) in Knueven et al. (2020).
Variables
---
@ -63,14 +63,14 @@ function _add_ramp_eqs!(
end
#TRD = floor((Pbar - SU) / RD) # ramp down time
# TODO check amk changed TRD wrt Kneuven et al.
# TODO check amk changed TRD wrt Knueven et al.
TRD = ceil((Pbar - SD) / RD) # ramp down time
TRU = floor((Pbar - SU) / RU) # ramp up time, can be negative if Pbar < SU
# TODO check initial time periods: what if generator has been running for x periods?
# But maybe ok as long as (35) and (36) are also used...
if UT > 1
# Equation (38) in Kneuven et al. (2020)
# Equation (38) in Knueven et al. (2020)
# Generalization of (20)
# Necessary that if any of the switch_on = 1 in the sum,
# then switch_off[gn, t+1] = 0
@ -87,7 +87,7 @@ function _add_ramp_eqs!(
)
if UT - 2 < TRU
# Equation (40) in Kneuven et al. (2020)
# Equation (40) in Knueven et al. (2020)
# Covers an additional time period of the ramp-up trajectory, compared to (38)
eq_prod_limit_ramp_up_extra_period[gn, t] = @constraint(
model,
@ -105,7 +105,7 @@ function _add_ramp_eqs!(
KSD = min(TRD, UT - 1, T - t - 1)
if KSD > 0
KSU = min(TRU, UT - 2 - KSD, t - 1)
# Equation (41) in Kneuven et al. (2020)
# Equation (41) in Knueven et al. (2020)
eq_prod_limit_shutdown_trajectory[gn, t] = @constraint(
model,
prod_above[gn, t] +

@ -2,6 +2,11 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
"""
_add_bus!(model::JuMP.Model, b::Bus)::Nothing
Creates `expr_net_injection` and adds `curtail` variable to `model`.
"""
function _add_bus!(model::JuMP.Model, b::Bus)::Nothing
net_injection = _init(model, :expr_net_injection)
curtail = _init(model, :curtail)

@ -5,7 +5,7 @@
"""
_add_system_wide_eqs!(model::JuMP.Model)::Nothing
Calls `_add_net_injection_eqs!` and `add_reserve_eqs!`.
Adds constraints that apply to the whole system, such as relating to net injection and reserves.
"""
function _add_system_wide_eqs!(model::JuMP.Model)::Nothing
_add_net_injection_eqs!(model)
@ -20,13 +20,13 @@ Adds `net_injection`, `eq_net_injection_def`, and `eq_power_balance` identifiers
Variables
---
* expr_net_injection
* net_injection
* `expr_net_injection`
* `net_injection`
Constraints
---
* eq_net_injection_def
* eq_power_balance
* `eq_net_injection_def`
* `eq_power_balance`
"""
function _add_net_injection_eqs!(model::JuMP.Model)::Nothing
T = model[:instance].time
@ -52,24 +52,24 @@ end
Ensure constraints on reserves are met.
Based on Morales-España et al. (2013a).
Eqn. (68) from Kneuven et al. (2020).
Eqn. (68) from Knueven et al. (2020).
Adds `eq_min_reserve` identifier to `model`, and corresponding constraint.
Variables
---
* reserve
* reserve_shortfall
* `reserve`
* `reserve_shortfall`
Constraints
---
* eq_min_reserve
* `eq_min_reserve`
"""
function _add_reserve_eqs!(model::JuMP.Model)::Nothing
instance = model[:instance]
eq_min_reserve = _init(model, :eq_min_reserve)
for t in 1:instance.time
# Equation (68) in Kneuven et al. (2020)
# Equation (68) in Knueven 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)

@ -159,11 +159,11 @@ function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
end
"""
_add_shutdown_cost_eqs!
_add_shutdown_cost_eqs!(model::JuMP.Model, g::Unit)::Nothing
Variables
---
* :switch_off
* `switch_off`
"""
function _add_shutdown_cost_eqs!(model::JuMP.Model, g::Unit)::Nothing
T = model[:instance].time
@ -171,7 +171,7 @@ function _add_shutdown_cost_eqs!(model::JuMP.Model, g::Unit)::Nothing
for t in 1:T
shutdown_cost = 0.0
if shutdown_cost > 1e-7
# Equation (62) in Kneuven et al. (2020)
# Equation (62) in Knueven et al. (2020)
add_to_expression!(
model[:obj],
model[:switch_off][gi, t],
@ -179,7 +179,7 @@ function _add_shutdown_cost_eqs!(model::JuMP.Model, g::Unit)::Nothing
)
end
end # loop over time
end # _add_shutdown_cost_eqs!
end
"""
_add_ramp_eqs!(model, unit, formulation)
@ -235,20 +235,19 @@ end
Ensure constraints on up/down time are met.
Based on Garver (1962), Malkin (2003), and Rajan and Takritti (2005).
Eqns. (3), (4), (5) in Kneuven et al. (2020).
Eqns. (3), (4), (5) in Knueven et al. (2020).
Variables
---
* :is_on
* :switch_off
* :switch_on
* `is_on`
* `switch_off`
* `switch_on`
Constraints
---
* :eq_min_uptime
* :eq_min_downtime
* `eq_min_uptime`
* `eq_min_downtime`
"""
function _add_min_uptime_downtime_eqs!(model::JuMP.Model, g::Unit)::Nothing
is_on = model[:is_on]
@ -259,14 +258,14 @@ function _add_min_uptime_downtime_eqs!(model::JuMP.Model, g::Unit)::Nothing
T = model[:instance].time
for t in 1:T
# Minimum up-time
# Equation (4) in Kneuven et al. (2020)
# Equation (4) in Knueven et al. (2020)
eq_min_uptime[g.name, t] = @constraint(
model,
sum(switch_on[g.name, i] for i in (t-g.min_uptime+1):t if i >= 1) <= is_on[g.name, t]
)
# Minimum down-time
# Equation (5) in Kneuven et al. (2020)
# Equation (5) in Knueven et al. (2020)
eq_min_downtime[g.name, t] = @constraint(
model,
sum(
@ -275,7 +274,7 @@ function _add_min_uptime_downtime_eqs!(model::JuMP.Model, g::Unit)::Nothing
)
# Minimum up/down-time for initial periods
# Equations (3a) and (3b) in Kneuven et al. (2020)
# Equations (3a) and (3b) in Knueven et al. (2020)
# (using :switch_on and :switch_off instead of :is_on)
if t == 1
if g.initial_status > 0

Loading…
Cancel
Save