Compare commits

..

1 Commits

Author SHA1 Message Date
2978ad665e Fix bug in validation script; create large tests 2021-06-26 08:44:27 -05:00
14 changed files with 34 additions and 118 deletions

View File

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

View File

@@ -2,7 +2,7 @@ name = "UnitCommitment"
uuid = "64606440-39ea-11e9-0f29-3303a1d3d877"
authors = ["Santos Xavier, Alinson <axavier@anl.gov>"]
repo = "https://github.com/ANL-CEEESA/UnitCommitment.jl"
version = "0.2.3"
version = "0.2.2"
[deps]
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"

View File

@@ -28,14 +28,13 @@ Each section is described in detail below. For a complete example, see [case14](
### Parameters
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.
This section describes system-wide parameters, such as power balance penalties, 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. Negative value implies reserve constraints must always be satisfied. | `-1` | Y
#### Example
@@ -43,8 +42,7 @@ 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
"Power balance penalty ($/MW)": 1000.0
}
}
```

View File

@@ -148,7 +148,7 @@ for g in instance.units
end
```
### Fixing variables, modifying objective function and adding constraints
### Modifying the model
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,54 +190,6 @@ JuMP.set_objective_coefficient(
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
----------
* [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,10 +98,6 @@ function _from_json(json; repair = true)
json["Parameters"]["Power balance penalty (\$/MW)"],
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
for (bus_name, dict) in json["Buses"]
@@ -268,7 +264,6 @@ function _from_json(json; repair = true)
instance = UnitCommitmentInstance(
T,
power_balance_penalty,
shortfall_penalty,
units,
buses,
lines,

View File

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

View File

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

View File

@@ -4,11 +4,15 @@
function _add_bus!(model::JuMP.Model, b::Bus)::Nothing
net_injection = _init(model, :expr_net_injection)
reserve = _init(model, :expr_reserve)
curtail = _init(model, :curtail)
for t in 1:model[:instance].time
# Fixed load
net_injection[b.name, t] = AffExpr(-b.load[t])
# Reserves
reserve[b.name, t] = AffExpr()
# Load curtailment
curtail[b.name, 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
T = model[:instance].time
net_injection = _init(model, :net_injection)
eq_net_injection = _init(model, :eq_net_injection)
eq_net_injection_def = _init(model, :eq_net_injection_def)
eq_power_balance = _init(model, :eq_power_balance)
for t in 1:T, b in model[:instance].buses
n = net_injection[b.name, t] = @variable(model)
eq_net_injection[b.name, t] =
@constraint(model, -n + model[:expr_net_injection][b.name, t] == 0)
eq_net_injection_def[t, b.name] =
@constraint(model, n == model[:expr_net_injection][b.name, t])
end
for t in 1:T
eq_power_balance[t] = @constraint(
@@ -29,28 +29,13 @@ 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]
for t in 1:model[:instance].time
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]
sum(
model[:expr_reserve][b.name, t] for b in model[:instance].buses
) >= model[: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
return
end

View File

@@ -44,16 +44,12 @@ _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
return
end
@@ -214,5 +210,11 @@ function _add_net_injection_eqs!(model::JuMP.Model, g::Unit)::Nothing
model[:is_on][g.name, 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

View File

@@ -51,12 +51,6 @@ function solution(model::JuMP.Model)::OrderedDict
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)

View File

@@ -324,16 +324,11 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
# 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
if reserve < instance.reserves.spinning[t] - tol
@error @sprintf(
"Insufficient spinning reserves at time %d (%.2f + %.2f should be %.2f)",
"Insufficient spinning reserves at time %d (%.2f should be %.2f)",
t,
reserve,
reserve_shortfall,
instance.reserves.spinning[t],
)
err_count += 1

View File

@@ -20,14 +20,8 @@ if ENABLE_LARGE_TESTS
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
instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
UnitCommitment.build_model(instance = instance, formulation = formulation) # should not crash
return
end