Compare commits

...

13 Commits

Author SHA1 Message Date
Aleksandr Kazachkov
5afb2363af Missed function definition 2021-07-26 18:40:09 -04:00
Aleksandr Kazachkov
860c47b7e3 Shutdown cost not in this commit 2021-07-26 18:38:38 -04:00
Aleksandr Kazachkov
37b21853be Added mising formulation_status_vars 2021-07-26 18:37:06 -04:00
Aleksandr Kazachkov
c8c7350096 Added fix_vars to src/model/formulations/Gar1962/status.jl 2021-07-26 18:32:09 -04:00
Aleksandr Kazachkov
7302fabe37 Added fix vars to unit.jl 2021-07-26 18:30:24 -04:00
Aleksandr Kazachkov
4ed13d6e95 Added fix_vars_via_constraint option 2021-07-26 18:29:15 -04:00
b1498c50b3 GitHub Actions: Test fewer combinations 2021-07-26 07:57:17 -05:00
Aleksandr Kazachkov
000215e991 Add reserve shortfall penalty 2021-07-26 07:54:45 -05:00
7a1b6f0f55 Update CHANGELOG.md 2021-07-21 11:18:22 -05:00
719143ea40 Flip coefficients in eq_net_injection; add example to the docs 2021-07-21 11:04:11 -05:00
07d7e04728 Fix bug in validation script; create large tests 2021-07-21 09:49:20 -05:00
4daf38906d Merge pull request #12 from mtanneau/mt/FixDuplicateStartup
Fix duplicated startup constraint
2021-07-19 17:14:39 -05:00
mtanneau
b2eaa0e48b Fix duplicated startup constraint 2021-07-17 15:57:03 -04:00
18 changed files with 342 additions and 65 deletions

View File

@@ -9,8 +9,8 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
julia-version: ['1.3', '1.4', '1.5', '1.6'] julia-version: ['1.4', '1.5', '1.6']
julia-arch: [x64, x86] julia-arch: [x64]
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, windows-latest, macOS-latest]
exclude: exclude:
- os: macOS-latest - os: macOS-latest

View File

@@ -11,6 +11,11 @@ All notable changes to this project will be documented in this file.
[semver]: https://semver.org/spec/v2.0.0.html [semver]: https://semver.org/spec/v2.0.0.html
[pkjjl]: https://pkgdocs.julialang.org/v1/compatibility/#compat-pre-1.0 [pkjjl]: https://pkgdocs.julialang.org/v1/compatibility/#compat-pre-1.0
## [0.2.2] - 2021-07-21
### Fixed
- Fix small bug in validation scripts related to startup costs
- Fix duplicated startup constraints (@mtanneau, #12)
## [0.2.1] - 2021-06-02 ## [0.2.1] - 2021-06-02
### Added ### Added
- Add multiple ramping formulations (ArrCon2000, MorLatRam2013, DamKucRajAta2016, PanGua2016) - Add multiple ramping formulations (ArrCon2000, MorLatRam2013, DamKucRajAta2016, PanGua2016)

View File

@@ -2,7 +2,7 @@ name = "UnitCommitment"
uuid = "64606440-39ea-11e9-0f29-3303a1d3d877" uuid = "64606440-39ea-11e9-0f29-3303a1d3d877"
authors = ["Santos Xavier, Alinson <axavier@anl.gov>"] authors = ["Santos Xavier, Alinson <axavier@anl.gov>"]
repo = "https://github.com/ANL-CEEESA/UnitCommitment.jl" repo = "https://github.com/ANL-CEEESA/UnitCommitment.jl"
version = "0.2.1" version = "0.2.2"
[deps] [deps]
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
@@ -31,6 +31,7 @@ julia = "1"
[extras] [extras]
Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76" Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b"
[targets] [targets]
test = ["Cbc", "Test"] test = ["Cbc", "Test", "Gurobi"]

View File

@@ -28,13 +28,14 @@ Each section is described in detail below. For a complete example, see [case14](
### Parameters ### 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? | 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 | `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 | `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 #### Example
@@ -42,7 +43,8 @@ This section describes system-wide parameters, such as power balance penalties,
{ {
"Parameters": { "Parameters": {
"Time horizon (h)": 4, "Time horizon (h)": 4,
"Power balance penalty ($/MW)": 1000.0 "Power balance penalty ($/MW)": 1000.0,
"Reserve shortfall penalty ($/MW)": -1.0
} }
} }
``` ```

View File

@@ -148,7 +148,7 @@ for g in instance.units
end end
``` ```
### Modifying the model ### Fixing variables, modifying objective function and adding constraints
Since we now have a direct reference to the JuMP decision variables, it is possible to fix variables, change the coefficients in the objective function, or even add new constraints to the model before solving it. The script below shows how can this be accomplished. For more information on modifying an existing model, [see the JuMP documentation](https://jump.dev/JuMP.jl/stable/manual/variables/). Since we now have a direct reference to the JuMP decision variables, it is possible to fix variables, change the coefficients in the objective function, or even add new constraints to the model before solving it. The script below shows how can this be accomplished. For more information on modifying an existing model, [see the JuMP documentation](https://jump.dev/JuMP.jl/stable/manual/variables/).
@@ -190,6 +190,54 @@ JuMP.set_objective_coefficient(
UnitCommitment.optimize!(model) UnitCommitment.optimize!(model)
``` ```
### Adding new component to a bus
The following snippet shows how to add a new grid component to a particular bus. For each time step, we create decision variables for the new grid component, add these variables to the objective function, then attach the component to a particular bus by modifying some existing model constraints.
```julia
using Cbc
using JuMP
using UnitCommitment
# Load instance and build base model
instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
model = UnitCommitment.build_model(
instance=instance,
optimizer=Cbc.Optimizer,
)
# Get the number of time steps in the original instance
T = instance.time
# Create decision variables for the new grid component.
# In this example, we assume that the new component can
# inject up to 10 MW of power at each time step, so we
# create new continuous variables 0 ≤ x[t] ≤ 10.
@variable(model, x[1:T], lower_bound=0.0, upper_bound=10.0)
# For each time step
for t in 1:T
# Add production costs to the objective function.
# In this example, we assume a cost of $5/MW.
set_objective_coefficient(model, x[t], 5.0)
# Attach the new component to bus b1, by modifying the
# constraint `eq_net_injection`.
set_normalized_coefficient(
model[:eq_net_injection]["b1", t],
x[t],
1.0,
)
end
# Solve the model
UnitCommitment.optimize!(model)
# Show optimal values for the x variables
@show value.(x)
```
References References
---------- ----------
* [KnOsWa20] **Bernard Knueven, James Ostrowski and Jean-Paul Watson.** "On Mixed-Integer Programming Formulations for the Unit Commitment Problem". INFORMS Journal on Computing (2020). [DOI: 10.1287/ijoc.2019.0944](https://doi.org/10.1287/ijoc.2019.0944) * [KnOsWa20] **Bernard Knueven, James Ostrowski and Jean-Paul Watson.** "On Mixed-Integer Programming Formulations for the Unit Commitment Problem". INFORMS Journal on Computing (2020). [DOI: 10.1287/ijoc.2019.0944](https://doi.org/10.1287/ijoc.2019.0944)

Binary file not shown.

View File

@@ -98,6 +98,10 @@ function _from_json(json; repair = true)
json["Parameters"]["Power balance penalty (\$/MW)"], json["Parameters"]["Power balance penalty (\$/MW)"],
default = [1000.0 for t in 1:T], 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 # Read buses
for (bus_name, dict) in json["Buses"] for (bus_name, dict) in json["Buses"]
@@ -264,6 +268,7 @@ function _from_json(json; repair = true)
instance = UnitCommitmentInstance( instance = UnitCommitmentInstance(
T, T,
power_balance_penalty, power_balance_penalty,
shortfall_penalty,
units, units,
buses, buses,
lines, lines,

View File

@@ -72,6 +72,8 @@ end
mutable struct UnitCommitmentInstance mutable struct UnitCommitmentInstance
time::Int time::Int
power_balance_penalty::Vector{Float64} power_balance_penalty::Vector{Float64}
"Penalty for failing to meet reserve requirement."
shortfall_penalty::Vector{Float64}
units::Vector{Unit} units::Vector{Unit}
buses::Vector{Bus} buses::Vector{Bus}
lines::Vector{TransmissionLine} lines::Vector{TransmissionLine}

View File

@@ -2,6 +2,12 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
"""
_add_status_vars!
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!( function _add_status_vars!(
model::JuMP.Model, model::JuMP.Model,
g::Unit, g::Unit,
@@ -10,15 +16,93 @@ function _add_status_vars!(
is_on = _init(model, :is_on) is_on = _init(model, :is_on)
switch_on = _init(model, :switch_on) switch_on = _init(model, :switch_on)
switch_off = _init(model, :switch_off) switch_off = _init(model, :switch_off)
FIX_VARS = !formulation_status_vars.fix_vars_via_constraint
is_initially_on = _is_initially_on(g) > 0
for t in 1:model[:instance].time for t in 1:model[:instance].time
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
else
is_on[g.name, t] = @variable(model, binary = true) is_on[g.name, t] = @variable(model, binary = true)
switch_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) 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
# Otherwise, unit is on, and will never turn off, so will never need to turn on
fix(is_on[g.name, t], 1.0; force = true)
fix(
switch_on[g.name, t],
(t == 1 ? 1.0 - _is_initially_on(g) : 0.0);
force = true,
)
fix(switch_off[g.name, t], 0.0; force = true)
elseif t == 1
if is_initially_on
# 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
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
switch_on[g.name, t] = 0.0
else
switch_off[g.name, t] = 0.0
end
end
end
# 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
# Otherwise, unit is on, and will never turn off, so will never need to turn on
fix(is_on[g.name, t], 1.0; force = true)
fix(
switch_on[g.name, t],
(t == 1 ? 1.0 - _is_initially_on(g) : 0.0);
force = true,
)
fix(switch_off[g.name, t], 0.0; force = true)
elseif t == 1
if is_initially_on
# 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
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
switch_on[g.name, t] = 0.0
else
switch_off[g.name, t] = 0.0
end
end
end end
end end
return return

View File

@@ -17,8 +17,53 @@ import ..PiecewiseLinearCostsFormulation
import ..ProductionVarsFormulation import ..ProductionVarsFormulation
import ..StatusVarsFormulation 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 ProdVars <: ProductionVarsFormulation end
struct PwlCosts <: PiecewiseLinearCostsFormulation 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 end

View File

@@ -12,14 +12,14 @@ function _add_startup_cost_eqs!(
S = length(g.startup_categories) S = length(g.startup_categories)
startup = model[:startup] startup = model[:startup]
for t in 1:model[:instance].time for t in 1:model[:instance].time
for s in 1:S
# If unit is switching on, we must choose a startup category # If unit is switching on, we must choose a startup category
eq_startup_choose[g.name, t, s] = @constraint( eq_startup_choose[g.name, t] = @constraint(
model, model,
model[:switch_on][g.name, t] == model[:switch_on][g.name, t] ==
sum(startup[g.name, t, s] for s in 1:S) sum(startup[g.name, t, s] for s in 1:S)
) )
for s in 1:S
# If unit has not switched off in the last `delay` time periods, startup category is forbidden. # If unit has not switched off in the last `delay` time periods, startup category is forbidden.
# The last startup category is always allowed. # The last startup category is always allowed.
if s < S if s < S

View File

@@ -4,15 +4,11 @@
function _add_bus!(model::JuMP.Model, b::Bus)::Nothing function _add_bus!(model::JuMP.Model, b::Bus)::Nothing
net_injection = _init(model, :expr_net_injection) net_injection = _init(model, :expr_net_injection)
reserve = _init(model, :expr_reserve)
curtail = _init(model, :curtail) curtail = _init(model, :curtail)
for t in 1:model[:instance].time for t in 1:model[:instance].time
# Fixed load # Fixed load
net_injection[b.name, t] = AffExpr(-b.load[t]) net_injection[b.name, t] = AffExpr(-b.load[t])
# Reserves
reserve[b.name, t] = AffExpr()
# Load curtailment # Load curtailment
curtail[b.name, t] = curtail[b.name, t] =
@variable(model, lower_bound = 0, upper_bound = b.load[t]) @variable(model, lower_bound = 0, upper_bound = b.load[t])

View File

@@ -11,12 +11,12 @@ end
function _add_net_injection_eqs!(model::JuMP.Model)::Nothing function _add_net_injection_eqs!(model::JuMP.Model)::Nothing
T = model[:instance].time T = model[:instance].time
net_injection = _init(model, :net_injection) net_injection = _init(model, :net_injection)
eq_net_injection_def = _init(model, :eq_net_injection_def) eq_net_injection = _init(model, :eq_net_injection)
eq_power_balance = _init(model, :eq_power_balance) eq_power_balance = _init(model, :eq_power_balance)
for t in 1:T, b in model[:instance].buses for t in 1:T, b in model[:instance].buses
n = net_injection[b.name, t] = @variable(model) n = net_injection[b.name, t] = @variable(model)
eq_net_injection_def[t, b.name] = eq_net_injection[b.name, t] =
@constraint(model, n == model[:expr_net_injection][b.name, t]) @constraint(model, -n + model[:expr_net_injection][b.name, t] == 0)
end end
for t in 1:T for t in 1:T
eq_power_balance[t] = @constraint( eq_power_balance[t] = @constraint(
@@ -29,13 +29,28 @@ end
function _add_reserve_eqs!(model::JuMP.Model)::Nothing function _add_reserve_eqs!(model::JuMP.Model)::Nothing
eq_min_reserve = _init(model, :eq_min_reserve) 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( eq_min_reserve[t] = @constraint(
model, model,
sum( sum(model[:reserve][g.name, t] for g in instance.units) +
model[:expr_reserve][b.name, t] for b in model[:instance].buses (shortfall_penalty >= 0 ? model[:reserve_shortfall][t] : 0.0) >=
) >= model[:instance].reserves.spinning[t] 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 end
return return
end end

View File

@@ -2,6 +2,15 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
"""
_add_unit!(model::JuMP.Model, g::Unit, formulation::Formulation)
Add production, reserve, startup, shutdown, and status variables,
and constraints for min uptime/downtime, net injection, production, ramping, startup, shutdown, and status.
Fix variables if a certain generator _must_ run or if a generator provides spinning reserves.
Also, add overflow penalty to objective for each transmission line.
"""
function _add_unit!(model::JuMP.Model, g::Unit, formulation::Formulation) function _add_unit!(model::JuMP.Model, g::Unit, formulation::Formulation)
if !all(g.must_run) && any(g.must_run) if !all(g.must_run) && any(g.must_run)
error("Partially must-run units are not currently supported") error("Partially must-run units are not currently supported")
@@ -35,7 +44,12 @@ function _add_unit!(model::JuMP.Model, g::Unit, formulation::Formulation)
formulation.status_vars, formulation.status_vars,
) )
_add_startup_cost_eqs!(model, g, formulation.startup_costs) _add_startup_cost_eqs!(model, g, formulation.startup_costs)
_add_startup_shutdown_limit_eqs!(model, g) _add_startup_shutdown_limit_eqs!(
model,
g,
formulation.status_vars,
formulation.prod_vars,
)
_add_status_eqs!(model, g, formulation.status_vars) _add_status_eqs!(model, g, formulation.status_vars)
return return
end end
@@ -44,12 +58,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 function _add_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing
reserve = _init(model, :reserve) reserve = _init(model, :reserve)
reserve_shortfall = _init(model, :reserve_shortfall)
for t in 1:model[:instance].time for t in 1:model[:instance].time
if g.provides_spinning_reserves[t] if g.provides_spinning_reserves[t]
reserve[g.name, t] = @variable(model, lower_bound = 0) reserve[g.name, t] = @variable(model, lower_bound = 0)
else else
reserve[g.name, t] = 0.0 reserve[g.name, t] = 0.0
end end
reserve_shortfall[t] =
(model[:instance].shortfall_penalty[t] >= 0) ?
@variable(model, lower_bound = 0) : 0.0
end end
return return
end end
@@ -72,7 +90,22 @@ function _add_startup_shutdown_vars!(model::JuMP.Model, g::Unit)::Nothing
return return
end end
function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing """
_add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
Creates startup/shutdown limit constraints below based on variables `Gar1962.StatusVars`, `prod_above` from `Gar1962.ProdVars`, and `reserve`.
Constraints
---
* :eq_startup_limit
* :eq_shutdown_limit
"""
function _add_startup_shutdown_limit_eqs!(
model::JuMP.Model,
g::Unit,
formulation_status_vars::Gar1962.StatusVars,
formulation_prod_vars::Gar1962.ProdVars,
)::Nothing
eq_shutdown_limit = _init(model, :eq_shutdown_limit) eq_shutdown_limit = _init(model, :eq_shutdown_limit)
eq_startup_limit = _init(model, :eq_startup_limit) eq_startup_limit = _init(model, :eq_startup_limit)
is_on = model[:is_on] is_on = model[:is_on]
@@ -91,8 +124,15 @@ function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
) )
# Shutdown limit # Shutdown limit
if g.initial_power > g.shutdown_limit if g.initial_power > g.shutdown_limit
# TODO check what happens with these variables when exporting the model
# Generator producing too much to be turned off in the first time period
# (can a binary variable have bounds x = 0?)
if formulation_status_vars.fix_vars_via_constraint
eq_shutdown_limit[g.name, 0] = eq_shutdown_limit[g.name, 0] =
@constraint(model, switch_off[g.name, 1] <= 0) @constraint(model, model[:switch_off][g.name, 1] <= 0.0)
else
fix(model[:switch_off][g.name, 1], 0.0; force = true)
end
end end
if t < T if t < T
eq_shutdown_limit[g.name, t] = @constraint( eq_shutdown_limit[g.name, t] = @constraint(
@@ -210,11 +250,5 @@ function _add_net_injection_eqs!(model::JuMP.Model, g::Unit)::Nothing
model[:is_on][g.name, t], model[:is_on][g.name, t],
g.min_power[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
end end

View File

@@ -51,6 +51,12 @@ function solution(model::JuMP.Model)::OrderedDict
sol["Switch on"] = timeseries(model[:switch_on], instance.units) sol["Switch on"] = timeseries(model[:switch_on], instance.units)
sol["Switch off"] = timeseries(model[:switch_off], instance.units) sol["Switch off"] = timeseries(model[:switch_off], instance.units)
sol["Reserve (MW)"] = timeseries(model[:reserve], 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)"] = sol["Net injection (MW)"] =
timeseries(model[:net_injection], instance.buses) timeseries(model[:net_injection], instance.buses)
sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses) sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses)

View File

@@ -208,12 +208,8 @@ function _validate_units(instance, solution; tol = 0.01)
break break
end end
end end
if t == time_down + 1 if (t == time_down + 1) && (unit.initial_status < 0)
initial_down = unit.min_downtime time_down -= unit.initial_status
if unit.initial_status < 0
initial_down = -unit.initial_status
end
time_down += initial_down
end end
# Calculate startup costs # Calculate startup costs
@@ -246,14 +242,6 @@ function _validate_units(instance, solution; tol = 0.01)
break break
end end
end end
if t == time_up + 1
initial_up = unit.min_uptime
if unit.initial_status > 0
initial_up = unit.initial_status
end
time_up += initial_up
end
if (t == time_up + 1) && (unit.initial_status > 0) if (t == time_up + 1) && (unit.initial_status > 0)
time_up += unit.initial_status time_up += unit.initial_status
end end
@@ -336,11 +324,16 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
# Verify spinning reserves # Verify spinning reserves
reserve = reserve =
sum(solution["Reserve (MW)"][g.name][t] for g in instance.units) 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( @error @sprintf(
"Insufficient spinning reserves at time %d (%.2f should be %.2f)", "Insufficient spinning reserves at time %d (%.2f + %.2f should be %.2f)",
t, t,
reserve, reserve,
reserve_shortfall,
instance.reserves.spinning[t], instance.reserves.spinning[t],
) )
err_count += 1 err_count += 1

View File

@@ -3,6 +3,7 @@
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
using UnitCommitment using UnitCommitment
using JuMP
import UnitCommitment: import UnitCommitment:
ArrCon2000, ArrCon2000,
CarArr2006, CarArr2006,
@@ -11,17 +12,55 @@ import UnitCommitment:
Gar1962, Gar1962,
KnuOstWat2018, KnuOstWat2018,
MorLatRam2013, MorLatRam2013,
PanGua2016 PanGua2016,
XavQiuWanThi2019
function _test(formulation::Formulation)::Nothing if ENABLE_LARGE_TESTS
instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01") using Gurobi
UnitCommitment.build_model(instance = instance, formulation = formulation) # should not crash end
function _small_test(formulation::Formulation)::Nothing
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 return
end end
function _large_test(formulation::Formulation)::Nothing
instances = ["pglib-uc/ca/Scenario400_reserves_1"]
for instance in instances
instance = UnitCommitment.read_benchmark(instance)
model = UnitCommitment.build_model(
instance = instance,
formulation = formulation,
optimizer = Gurobi.Optimizer,
)
UnitCommitment.optimize!(
model,
XavQiuWanThi2019.Method(two_phase_gap = false, gap_limit = 0.1),
)
solution = UnitCommitment.solution(model)
@test UnitCommitment.validate(instance, solution)
end
return
end
function _test(formulation::Formulation)::Nothing
_small_test(formulation)
if ENABLE_LARGE_TESTS
_large_test(formulation)
end
end
@testset "formulations" begin @testset "formulations" begin
_test(Formulation())
_test(Formulation(ramping = ArrCon2000.Ramping())) _test(Formulation(ramping = ArrCon2000.Ramping()))
_test(Formulation(ramping = DamKucRajAta2016.Ramping())) # _test(Formulation(ramping = DamKucRajAta2016.Ramping()))
_test( _test(
Formulation( Formulation(
ramping = MorLatRam2013.Ramping(), ramping = MorLatRam2013.Ramping(),

View File

@@ -7,6 +7,8 @@ using UnitCommitment
UnitCommitment._setup_logger() UnitCommitment._setup_logger()
const ENABLE_LARGE_TESTS = ("UCJL_LARGE_TESTS" in keys(ENV))
@testset "UnitCommitment" begin @testset "UnitCommitment" begin
include("usage.jl") include("usage.jl")
@testset "import" begin @testset "import" begin