commit
319a787904
@ -1,4 +1,5 @@
|
|||||||
[deps]
|
[deps]
|
||||||
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
|
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
|
||||||
|
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
|
||||||
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
||||||
UnitCommitment = "64606440-39ea-11e9-0f29-3303a1d3d877"
|
UnitCommitment = "64606440-39ea-11e9-0f29-3303a1d3d877"
|
||||||
|
@ -0,0 +1,212 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
using JuMP
|
||||||
|
|
||||||
|
"""
|
||||||
|
function compute_lmp(
|
||||||
|
model::JuMP.Model,
|
||||||
|
method::AELMP;
|
||||||
|
optimizer,
|
||||||
|
)::OrderedDict{Tuple{String,Int},Float64}
|
||||||
|
|
||||||
|
Calculates the approximate extended locational marginal prices of the given unit commitment instance.
|
||||||
|
|
||||||
|
The AELPM does the following three things:
|
||||||
|
|
||||||
|
1. It sets the minimum power output of each generator to zero
|
||||||
|
2. It averages the start-up cost over the offer blocks for each generator
|
||||||
|
3. It relaxes all integrality constraints
|
||||||
|
|
||||||
|
Returns a dictionary mapping `(bus_name, time)` to the marginal price.
|
||||||
|
|
||||||
|
WARNING: This approximation method is not fully developed. The implementation is based on MISO Phase I only.
|
||||||
|
|
||||||
|
1. It only supports Fast Start resources. More specifically, the minimum up/down time has to be zero.
|
||||||
|
2. The method does NOT support time-varying start-up costs.
|
||||||
|
3. An asset is considered offline if it is never on throughout all time periods.
|
||||||
|
4. The method does NOT support multiple scenarios.
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `model`:
|
||||||
|
the UnitCommitment model, must be solved before calling this function if offline participation is not allowed.
|
||||||
|
|
||||||
|
- `method`:
|
||||||
|
the AELMP method.
|
||||||
|
|
||||||
|
- `optimizer`:
|
||||||
|
the optimizer for solving the LP problem.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using UnitCommitment
|
||||||
|
using HiGHS
|
||||||
|
|
||||||
|
import UnitCommitment: AELMP
|
||||||
|
|
||||||
|
# Read benchmark instance
|
||||||
|
instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
|
||||||
|
|
||||||
|
# Build the model
|
||||||
|
model = UnitCommitment.build_model(
|
||||||
|
instance = instance,
|
||||||
|
optimizer = HiGHS.Optimizer,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optimize the model
|
||||||
|
UnitCommitment.optimize!(model)
|
||||||
|
|
||||||
|
# Compute the AELMPs
|
||||||
|
aelmp = UnitCommitment.compute_lmp(
|
||||||
|
model,
|
||||||
|
AELMP(
|
||||||
|
allow_offline_participation = false,
|
||||||
|
consider_startup_costs = true
|
||||||
|
),
|
||||||
|
optimizer = HiGHS.Optimizer
|
||||||
|
)
|
||||||
|
|
||||||
|
# Access the AELMPs
|
||||||
|
# Example: "s1" is the scenario name, "b1" is the bus name, 1 is the first time slot
|
||||||
|
# Note: although scenario is supported, the query still keeps the scenario keys for consistency.
|
||||||
|
@show aelmp["s1", "b1", 1]
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
function compute_lmp(
|
||||||
|
model::JuMP.Model,
|
||||||
|
method::AELMP;
|
||||||
|
optimizer,
|
||||||
|
)::OrderedDict{Tuple{String,String,Int},Float64}
|
||||||
|
@info "Building the approximation model..."
|
||||||
|
instance = deepcopy(model[:instance])
|
||||||
|
_aelmp_check_parameters(instance, model, method)
|
||||||
|
_modify_scenario!(instance.scenarios[1], model, method)
|
||||||
|
|
||||||
|
# prepare the result dictionary and solve the model
|
||||||
|
elmp = OrderedDict()
|
||||||
|
@info "Solving the approximation model."
|
||||||
|
approx_model = build_model(instance = instance, variable_names = true)
|
||||||
|
|
||||||
|
# relax the binary constraint, and relax integrality
|
||||||
|
for v in all_variables(approx_model)
|
||||||
|
if is_binary(v)
|
||||||
|
unset_binary(v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
relax_integrality(approx_model)
|
||||||
|
set_optimizer(approx_model, optimizer)
|
||||||
|
|
||||||
|
# solve the model
|
||||||
|
set_silent(approx_model)
|
||||||
|
optimize!(approx_model)
|
||||||
|
|
||||||
|
# access the dual values
|
||||||
|
@info "Getting dual values (AELMPs)."
|
||||||
|
for (key, val) in approx_model[:eq_net_injection]
|
||||||
|
elmp[key] = dual(val)
|
||||||
|
end
|
||||||
|
return elmp
|
||||||
|
end
|
||||||
|
|
||||||
|
function _aelmp_check_parameters(
|
||||||
|
instance::UnitCommitmentInstance,
|
||||||
|
model::JuMP.Model,
|
||||||
|
method::AELMP,
|
||||||
|
)
|
||||||
|
# CHECK: model cannot have multiple scenarios
|
||||||
|
if length(instance.scenarios) > 1
|
||||||
|
error("The method does NOT support multiple scenarios.")
|
||||||
|
end
|
||||||
|
sc = instance.scenarios[1]
|
||||||
|
# CHECK: model must be solved if allow_offline_participation=false
|
||||||
|
if !method.allow_offline_participation
|
||||||
|
if isnothing(model) || !has_values(model)
|
||||||
|
error(
|
||||||
|
"A solved UC model is required if allow_offline_participation=false.",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
all_units = sc.thermal_units
|
||||||
|
# CHECK: model cannot handle non-fast-starts (MISO Phase I: can ONLY solve fast-starts)
|
||||||
|
if any(u -> u.min_uptime > 1 || u.min_downtime > 1, all_units)
|
||||||
|
error(
|
||||||
|
"The minimum up/down time of all generators must be 1. AELMP only supports fast-starts.",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
if any(u -> u.initial_power > 0, all_units)
|
||||||
|
error("The initial power of all generators must be 0.")
|
||||||
|
end
|
||||||
|
if any(u -> u.initial_status >= 0, all_units)
|
||||||
|
error("The initial status of all generators must be negative.")
|
||||||
|
end
|
||||||
|
# CHECK: model does not support startup costs (in time series)
|
||||||
|
if any(u -> length(u.startup_categories) > 1, all_units)
|
||||||
|
error("The method does NOT support time-varying start-up costs.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function _modify_scenario!(
|
||||||
|
sc::UnitCommitmentScenario,
|
||||||
|
model::JuMP.Model,
|
||||||
|
method::AELMP,
|
||||||
|
)
|
||||||
|
# this function modifies the sc units (generators)
|
||||||
|
if !method.allow_offline_participation
|
||||||
|
# 1. remove (if NOT allowing) the offline generators
|
||||||
|
units_to_remove = []
|
||||||
|
for unit in sc.thermal_units
|
||||||
|
# remove based on the solved UC model result
|
||||||
|
# remove the unit if it is never on
|
||||||
|
if all(t -> value(model[:is_on][unit.name, t]) == 0, sc.time)
|
||||||
|
# unregister from the bus
|
||||||
|
filter!(x -> x.name != unit.name, unit.bus.thermal_units)
|
||||||
|
# unregister from the reserve
|
||||||
|
for r in unit.reserves
|
||||||
|
filter!(x -> x.name != unit.name, r.thermal_units)
|
||||||
|
end
|
||||||
|
# append the name to the remove list
|
||||||
|
push!(units_to_remove, unit.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# unregister the units from the remove list
|
||||||
|
filter!(x -> !(x.name in units_to_remove), sc.thermal_units)
|
||||||
|
end
|
||||||
|
|
||||||
|
for unit in sc.thermal_units
|
||||||
|
# 2. set min generation requirement to 0 by adding 0 to production curve and cost
|
||||||
|
# min_power & min_costs are vectors with dimension T
|
||||||
|
if unit.min_power[1] != 0
|
||||||
|
first_cost_segment = unit.cost_segments[1]
|
||||||
|
pushfirst!(
|
||||||
|
unit.cost_segments,
|
||||||
|
CostSegment(
|
||||||
|
ones(size(first_cost_segment.mw)) * unit.min_power[1],
|
||||||
|
ones(size(first_cost_segment.cost)) *
|
||||||
|
unit.min_power_cost[1] / unit.min_power[1],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
unit.min_power = zeros(size(first_cost_segment.mw))
|
||||||
|
unit.min_power_cost = zeros(size(first_cost_segment.cost))
|
||||||
|
end
|
||||||
|
|
||||||
|
# 3. average the start-up costs (if considering)
|
||||||
|
# if consider_startup_costs = false, then use the current first_startup_cost
|
||||||
|
first_startup_cost = unit.startup_categories[1].cost
|
||||||
|
if method.consider_startup_costs
|
||||||
|
additional_unit_cost = first_startup_cost / unit.max_power[1]
|
||||||
|
for i in eachindex(unit.cost_segments)
|
||||||
|
unit.cost_segments[i].cost .+= additional_unit_cost
|
||||||
|
end
|
||||||
|
first_startup_cost = 0.0 # zero out the start up cost
|
||||||
|
end
|
||||||
|
unit.startup_categories =
|
||||||
|
StartupCategory[StartupCategory(0, first_startup_cost)]
|
||||||
|
end
|
||||||
|
return sc.thermal_units_by_name =
|
||||||
|
Dict(g.name => g for g in sc.thermal_units)
|
||||||
|
end
|
@ -0,0 +1,92 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
using JuMP
|
||||||
|
|
||||||
|
"""
|
||||||
|
function compute_lmp(
|
||||||
|
model::JuMP.Model,
|
||||||
|
method::ConventionalLMP;
|
||||||
|
optimizer,
|
||||||
|
)::OrderedDict{Tuple{String,String,Int},Float64}
|
||||||
|
|
||||||
|
Calculates conventional locational marginal prices of the given unit commitment
|
||||||
|
instance. Returns a dictionary mapping `(bus_name, time)` to the marginal price.
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `model`:
|
||||||
|
the UnitCommitment model, must be solved before calling this function.
|
||||||
|
|
||||||
|
- `method`:
|
||||||
|
the LMP method.
|
||||||
|
|
||||||
|
- `optimizer`:
|
||||||
|
the optimizer for solving the LP problem.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using UnitCommitment
|
||||||
|
using HiGHS
|
||||||
|
|
||||||
|
import UnitCommitment: ConventionalLMP
|
||||||
|
|
||||||
|
# Read benchmark instance
|
||||||
|
instance = UnitCommitment.read_benchmark("matpower/case118/2018-01-01")
|
||||||
|
|
||||||
|
# Build the model
|
||||||
|
model = UnitCommitment.build_model(
|
||||||
|
instance = instance,
|
||||||
|
optimizer = HiGHS.Optimizer,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optimize the model
|
||||||
|
UnitCommitment.optimize!(model)
|
||||||
|
|
||||||
|
# Compute the LMPs using the conventional method
|
||||||
|
lmp = UnitCommitment.compute_lmp(
|
||||||
|
model,
|
||||||
|
ConventionalLMP(),
|
||||||
|
optimizer = HiGHS.Optimizer,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Access the LMPs
|
||||||
|
# Example: "s1" is the scenario name, "b1" is the bus name, 1 is the first time slot
|
||||||
|
@show lmp["s1", "b1", 1]
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
function compute_lmp(
|
||||||
|
model::JuMP.Model,
|
||||||
|
::ConventionalLMP;
|
||||||
|
optimizer,
|
||||||
|
)::OrderedDict{Tuple{String,String,Int},Float64}
|
||||||
|
if !has_values(model)
|
||||||
|
error("The UC model must be solved before calculating the LMPs.")
|
||||||
|
end
|
||||||
|
lmp = OrderedDict()
|
||||||
|
|
||||||
|
@info "Fixing binary variables and relaxing integrality..."
|
||||||
|
vals = Dict(v => value(v) for v in all_variables(model))
|
||||||
|
for v in all_variables(model)
|
||||||
|
if is_binary(v)
|
||||||
|
unset_binary(v)
|
||||||
|
fix(v, vals[v])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
relax_integrality(model)
|
||||||
|
set_optimizer(model, optimizer)
|
||||||
|
|
||||||
|
@info "Solving the LP..."
|
||||||
|
JuMP.optimize!(model)
|
||||||
|
|
||||||
|
@info "Getting dual values (LMPs)..."
|
||||||
|
for (key, val) in model[:eq_net_injection]
|
||||||
|
lmp[key] = dual(val)
|
||||||
|
end
|
||||||
|
|
||||||
|
return lmp
|
||||||
|
end
|
@ -0,0 +1,28 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
abstract type PricingMethod end
|
||||||
|
|
||||||
|
struct ConventionalLMP <: PricingMethod end
|
||||||
|
|
||||||
|
"""
|
||||||
|
struct AELMP <: PricingMethod
|
||||||
|
allow_offline_participation::Bool = true
|
||||||
|
consider_startup_costs::Bool = true
|
||||||
|
end
|
||||||
|
|
||||||
|
Approximate Extended LMPs.
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `allow_offline_participation`:
|
||||||
|
If true, offline assets are allowed to participate in pricing.
|
||||||
|
- `consider_startup_costs`:
|
||||||
|
If true, the start-up costs are averaged over each unit production; otherwise the production costs stay the same.
|
||||||
|
"""
|
||||||
|
Base.@kwdef struct AELMP <: PricingMethod
|
||||||
|
allow_offline_participation::Bool = true
|
||||||
|
consider_startup_costs::Bool = true
|
||||||
|
end
|
@ -0,0 +1,32 @@
|
|||||||
|
# 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_profiled_unit!(
|
||||||
|
model::JuMP.Model,
|
||||||
|
pu::ProfiledUnit,
|
||||||
|
sc::UnitCommitmentScenario,
|
||||||
|
)::Nothing
|
||||||
|
punits = _init(model, :prod_profiled)
|
||||||
|
net_injection = _init(model, :expr_net_injection)
|
||||||
|
for t in 1:model[:instance].time
|
||||||
|
# Decision variable
|
||||||
|
punits[sc.name, pu.name, t] =
|
||||||
|
@variable(model, lower_bound = 0, upper_bound = pu.capacity[t])
|
||||||
|
|
||||||
|
# Objective function terms
|
||||||
|
add_to_expression!(
|
||||||
|
model[:obj],
|
||||||
|
punits[sc.name, pu.name, t],
|
||||||
|
pu.cost[t] * sc.probability,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Net injection
|
||||||
|
add_to_expression!(
|
||||||
|
net_injection[sc.name, pu.bus.name, t],
|
||||||
|
punits[sc.name, pu.name, t],
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,35 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
using UnitCommitment, Cbc, HiGHS, JuMP
|
||||||
|
import UnitCommitment: AELMP
|
||||||
|
|
||||||
|
@testset "aelmp" begin
|
||||||
|
path = "$FIXTURES/aelmp_simple.json.gz"
|
||||||
|
# model has to be solved first
|
||||||
|
instance = UnitCommitment.read(path)
|
||||||
|
model = UnitCommitment.build_model(
|
||||||
|
instance = instance,
|
||||||
|
optimizer = Cbc.Optimizer,
|
||||||
|
variable_names = true,
|
||||||
|
)
|
||||||
|
JuMP.set_silent(model)
|
||||||
|
UnitCommitment.optimize!(model)
|
||||||
|
|
||||||
|
# policy 1: allow offlines; consider startups
|
||||||
|
aelmp_1 =
|
||||||
|
UnitCommitment.compute_lmp(model, AELMP(), optimizer = HiGHS.Optimizer)
|
||||||
|
@test aelmp_1["s1", "B1", 1] ≈ 231.7 atol = 0.1
|
||||||
|
|
||||||
|
# policy 2: do not allow offlines; but consider startups
|
||||||
|
aelmp_2 = UnitCommitment.compute_lmp(
|
||||||
|
model,
|
||||||
|
AELMP(
|
||||||
|
allow_offline_participation = false,
|
||||||
|
consider_startup_costs = true,
|
||||||
|
),
|
||||||
|
optimizer = HiGHS.Optimizer,
|
||||||
|
)
|
||||||
|
@test aelmp_2["s1", "B1", 1] ≈ 274.3 atol = 0.1
|
||||||
|
end
|
@ -0,0 +1,51 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
using UnitCommitment, Cbc, HiGHS, JuMP
|
||||||
|
import UnitCommitment: ConventionalLMP
|
||||||
|
|
||||||
|
function solve_conventional_testcase(path::String)
|
||||||
|
instance = UnitCommitment.read(path)
|
||||||
|
model = UnitCommitment.build_model(
|
||||||
|
instance = instance,
|
||||||
|
optimizer = Cbc.Optimizer,
|
||||||
|
variable_names = true,
|
||||||
|
)
|
||||||
|
JuMP.set_silent(model)
|
||||||
|
UnitCommitment.optimize!(model)
|
||||||
|
lmp = UnitCommitment.compute_lmp(
|
||||||
|
model,
|
||||||
|
ConventionalLMP(),
|
||||||
|
optimizer = HiGHS.Optimizer,
|
||||||
|
)
|
||||||
|
return lmp
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "conventional" begin
|
||||||
|
# instance 1
|
||||||
|
path = "$FIXTURES/lmp_simple_test_1.json.gz"
|
||||||
|
lmp = solve_conventional_testcase(path)
|
||||||
|
@test lmp["s1", "A", 1] == 50.0
|
||||||
|
@test lmp["s1", "B", 1] == 50.0
|
||||||
|
|
||||||
|
# instance 2
|
||||||
|
path = "$FIXTURES/lmp_simple_test_2.json.gz"
|
||||||
|
lmp = solve_conventional_testcase(path)
|
||||||
|
@test lmp["s1", "A", 1] == 50.0
|
||||||
|
@test lmp["s1", "B", 1] == 60.0
|
||||||
|
|
||||||
|
# instance 3
|
||||||
|
path = "$FIXTURES/lmp_simple_test_3.json.gz"
|
||||||
|
lmp = solve_conventional_testcase(path)
|
||||||
|
@test lmp["s1", "A", 1] == 50.0
|
||||||
|
@test lmp["s1", "B", 1] == 70.0
|
||||||
|
@test lmp["s1", "C", 1] == 100.0
|
||||||
|
|
||||||
|
# instance 4
|
||||||
|
path = "$FIXTURES/lmp_simple_test_4.json.gz"
|
||||||
|
lmp = solve_conventional_testcase(path)
|
||||||
|
@test lmp["s1", "A", 1] == 50.0
|
||||||
|
@test lmp["s1", "B", 1] == 70.0
|
||||||
|
@test lmp["s1", "C", 1] == 90.0
|
||||||
|
end
|
Loading…
Reference in new issue