parent
6573bb7ea2
commit
6ddd3d6c00
@ -0,0 +1,215 @@
|
|||||||
|
# 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
|
||||||
|
using Clp
|
||||||
|
|
||||||
|
"""
|
||||||
|
function get_aelmp(
|
||||||
|
path::String;
|
||||||
|
optimizer = nothing,
|
||||||
|
solved_uc_model::Union{JuMP.Model, Nothing} = nothing,
|
||||||
|
allow_offline_participation::Bool = true,
|
||||||
|
consider_startup_costs::Bool = true
|
||||||
|
)
|
||||||
|
|
||||||
|
Calculates the approximate extended locational marginal prices of the given unit commitment instance.
|
||||||
|
The AELPM does the following three things:
|
||||||
|
1. It removes the minimum generation requirement for each generator
|
||||||
|
2. It averages the start-up cost over the offer blocks for each generator
|
||||||
|
3. It relaxes all the binary constraints and integrality
|
||||||
|
Returns a dictionary of AELMPs. Each key is usually a tuple of "Bus name" and time index.
|
||||||
|
|
||||||
|
NOTE: 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 series of start-up costs.
|
||||||
|
3. The method can only calculate for the first time slot if allow_offline_participation=false.
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `path`:
|
||||||
|
the file path of the input data.
|
||||||
|
|
||||||
|
- `optimizer`:
|
||||||
|
the optimizer for solving the problem. If not specified, the method will use Clp.
|
||||||
|
|
||||||
|
- `solved_uc_model`:
|
||||||
|
the original unit commitment model that has been solved. This is used ONLY with allow_offline_participation
|
||||||
|
being set to false.
|
||||||
|
|
||||||
|
- `allow_offline_participation`:
|
||||||
|
defaults to true. If true, offline assets are allowed to participate in pricing; otherwise those
|
||||||
|
assets are NOT allowed to participate.
|
||||||
|
|
||||||
|
- `consider_startup_costs`:
|
||||||
|
defaults to true. If true, the start-up costs are averaged over each unit production; otherwise the
|
||||||
|
production costs stay the same.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
```julia
|
||||||
|
|
||||||
|
using UnitCommitment
|
||||||
|
using Clp
|
||||||
|
|
||||||
|
# Get the AELMPs with the file path (default policy)
|
||||||
|
aelmp = UnitCommitment.get_aelmp(
|
||||||
|
"example.json",
|
||||||
|
optimizer = Clp.Optimizer,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the AELMPs with an alternative policy
|
||||||
|
# Do not allow offline generators for price participation
|
||||||
|
# solve the UC model first
|
||||||
|
instance = UnitCommitment.read("example.json")
|
||||||
|
model = UnitCommitment.build_model(
|
||||||
|
instance=instance,
|
||||||
|
optimizer=Clp.Optimizer,
|
||||||
|
variable_names = true,
|
||||||
|
)
|
||||||
|
UnitCommitment.optimize!(model)
|
||||||
|
# then call the AELMP method
|
||||||
|
aelmp = UnitCommitment.get_aelmp(
|
||||||
|
"example.json",
|
||||||
|
solved_uc_model = model,
|
||||||
|
allow_offline_participation = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Accessing the 'aelmp' dictionary
|
||||||
|
# Example: "b1" is the bus name, 1 is the first time slot
|
||||||
|
@show aelmp["b1", 1]
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
function get_aelmp(
|
||||||
|
path::String;
|
||||||
|
optimizer = nothing,
|
||||||
|
solved_uc_model::Union{JuMP.Model, Nothing} = nothing,
|
||||||
|
allow_offline_participation::Bool = true,
|
||||||
|
consider_startup_costs::Bool = true
|
||||||
|
)
|
||||||
|
@info "Calculating the AELMP..."
|
||||||
|
@info "Building the approximation model..."
|
||||||
|
# get the json object from file path.
|
||||||
|
json = _read_json(path)
|
||||||
|
|
||||||
|
# if optimizer is not specified, use Clp
|
||||||
|
if isnothing(optimizer)
|
||||||
|
optimizer = Clp.Optimizer
|
||||||
|
end
|
||||||
|
|
||||||
|
# CHECK: model must be solved if allow_offline_participation=false
|
||||||
|
if allow_offline_participation # do nothing
|
||||||
|
@info "Offline generators are allowed to participate in pricing."
|
||||||
|
else
|
||||||
|
if isnothing(solved_uc_model)
|
||||||
|
@warn "No UC model is detected. A solved UC model is required if allow_offline_participation == false."
|
||||||
|
@warn "Setting parameter allow_offline_participation = true"
|
||||||
|
allow_offline_participation = true # and do nothing else
|
||||||
|
elseif !has_values(solved_uc_model)
|
||||||
|
@warn "The UC model has no solution. A solved UC model is required if allow_offline_participation == false."
|
||||||
|
@warn "Setting parameter allow_offline_participation = true"
|
||||||
|
allow_offline_participation = true # and do nothing else
|
||||||
|
else
|
||||||
|
# the inputs are correct
|
||||||
|
@info "Offline generators are NOT allowed to participate in pricing."
|
||||||
|
@info "Offline generators will be removed for the approximation."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# CHECK: start up cost consideration
|
||||||
|
if consider_startup_costs
|
||||||
|
@info "Startup costs are considered."
|
||||||
|
else
|
||||||
|
@info "Startup costs are NOT considered."
|
||||||
|
end
|
||||||
|
|
||||||
|
# modify the data for each generator
|
||||||
|
for (unit_name, dict) in json["Generators"]
|
||||||
|
# 1. remove (if NOT allowing) the offline generators
|
||||||
|
if !allow_offline_participation
|
||||||
|
# remove based on the solved UC model result
|
||||||
|
# here, only look at the first time slot (TIME-SERIES-NOT-SUPPORTED)
|
||||||
|
is_on = value(solved_uc_model[:is_on][unit_name, 1])
|
||||||
|
if is_on == 0
|
||||||
|
delete!(json["Generators"], unit_name)
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# 2. set min generation requirement to 0 by adding 0 to production curve and cost
|
||||||
|
cost_curve_mw = dict["Production cost curve (MW)"]
|
||||||
|
cost_curve_dollar = dict["Production cost curve (\$)"]
|
||||||
|
if cost_curve_mw[1] != 0
|
||||||
|
pushfirst!(cost_curve_mw, 0)
|
||||||
|
pushfirst!(cost_curve_dollar, 0)
|
||||||
|
dict["Production cost curve (MW)"] = cost_curve_mw
|
||||||
|
dict["Production cost curve (\$)"] = cost_curve_dollar
|
||||||
|
end
|
||||||
|
|
||||||
|
# 3. average the start-up costs (if considering)
|
||||||
|
# for now, consider first element only (TIME-SERIES-NOT-SUPPORTED)
|
||||||
|
first_startup_cost = dict["Startup costs (\$)"][1]
|
||||||
|
if consider_startup_costs
|
||||||
|
additional_unit_cost = first_startup_cost / cost_curve_mw[end]
|
||||||
|
for i in eachindex(cost_curve_dollar)
|
||||||
|
cost_curve_dollar[i] += additional_unit_cost * cost_curve_mw[i]
|
||||||
|
end
|
||||||
|
dict["Production cost curve (\$)"] = cost_curve_dollar
|
||||||
|
dict["Startup costs (\$)"] = [0.0]
|
||||||
|
else
|
||||||
|
# or do nothing (just keep the first cost)
|
||||||
|
dict["Startup costs (\$)"] = [first_startup_cost]
|
||||||
|
end
|
||||||
|
|
||||||
|
# 4. other adjustments...
|
||||||
|
### FIXME in the future
|
||||||
|
# MISO Phase I: can ONLY solve fast-starts
|
||||||
|
# here, force all startup time to be 0
|
||||||
|
dict["Startup delays (h)"] = [0]
|
||||||
|
dict["Initial status (h)"] = -100
|
||||||
|
dict["Initial power (MW)"] = 0
|
||||||
|
dict["Minimum uptime (h)"] = 0
|
||||||
|
dict["Minimum downtime (h)"] = 0
|
||||||
|
### END FIXME
|
||||||
|
|
||||||
|
# update
|
||||||
|
json["Generators"][unit_name] = dict
|
||||||
|
end
|
||||||
|
|
||||||
|
# prepare the result dictionary and solve the model
|
||||||
|
elmp = Dict()
|
||||||
|
|
||||||
|
# init the model
|
||||||
|
@info "Solving the approximation model."
|
||||||
|
instance = _from_json(json) # obtain the instance object
|
||||||
|
model = build_model(
|
||||||
|
instance=instance,
|
||||||
|
variable_names=true,
|
||||||
|
)
|
||||||
|
|
||||||
|
# relax the binary constraint, and relax integrality
|
||||||
|
for v in all_variables(model)
|
||||||
|
if is_binary(v)
|
||||||
|
unset_binary(v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
relax_integrality(model)
|
||||||
|
set_optimizer(model, optimizer)
|
||||||
|
|
||||||
|
# solve the model
|
||||||
|
set_silent(model)
|
||||||
|
optimize!(model)
|
||||||
|
|
||||||
|
# access the dual values
|
||||||
|
@info "Getting dual values (AELMPs)."
|
||||||
|
for (key, val) in model[:eq_net_injection]
|
||||||
|
elmp[key] = dual(val)
|
||||||
|
end
|
||||||
|
return elmp
|
||||||
|
end
|
@ -0,0 +1,121 @@
|
|||||||
|
# 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, Clp
|
||||||
|
|
||||||
|
"""
|
||||||
|
function get_lmp(
|
||||||
|
model::JuMP.Model;
|
||||||
|
optimizer = nothing,
|
||||||
|
verbose::Bool=true
|
||||||
|
)
|
||||||
|
|
||||||
|
Calculates the locational marginal prices of the given unit commitment instance.
|
||||||
|
Returns a dictionary of LMPs. Each key is usually a tuple of "Bus name" and time index.
|
||||||
|
Returns false if there is an error in solving the LMPs.
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `model`:
|
||||||
|
the UnitCommitment model, must be solved before calling this function.
|
||||||
|
|
||||||
|
- `optimizer`:
|
||||||
|
the optimizer for solving the LP problem. If not specified, the method will use Clp.
|
||||||
|
|
||||||
|
- `verbose`:
|
||||||
|
defaults to true. If false, all error/info messages will be suppressed.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
```julia
|
||||||
|
# Read benchmark instance
|
||||||
|
instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
|
||||||
|
|
||||||
|
# Construct model (using state-of-the-art defaults)
|
||||||
|
model = UnitCommitment.build_model(
|
||||||
|
instance = instance,
|
||||||
|
optimizer = Cbc.Optimizer,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the LMPs before solving the UC model
|
||||||
|
# Error messages will be displayed and the returned value is false.
|
||||||
|
# lmp = UnitCommitment.get_lmp(model)
|
||||||
|
|
||||||
|
UnitCommitment.optimize!(model)
|
||||||
|
|
||||||
|
# Get the LMPs after solving the UC model (the correct way)
|
||||||
|
# DO NOT use Cbc as the optimizer here. Cbc does not support dual values.
|
||||||
|
lmp = UnitCommitment.get_lmp(
|
||||||
|
model,
|
||||||
|
optimizer=Clp.Optimizer
|
||||||
|
)
|
||||||
|
|
||||||
|
# Accessing the 'lmp' dictionary
|
||||||
|
# Example: "b1" is the bus name, 1 is the first time slot
|
||||||
|
@show lmp["b1", 1]
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
function get_lmp(
|
||||||
|
model::JuMP.Model;
|
||||||
|
optimizer = nothing,
|
||||||
|
verbose::Bool=true
|
||||||
|
)
|
||||||
|
# Validate model, the UC model must be solved beforehand
|
||||||
|
if !has_values(model)
|
||||||
|
if verbose
|
||||||
|
@error "The UC model must be solved before calculating the LMPs."
|
||||||
|
@error "The LMPs are NOT calculated."
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
# if optimizer is not specified, use Clp
|
||||||
|
if isnothing(optimizer)
|
||||||
|
optimizer = Clp.Optimizer
|
||||||
|
end
|
||||||
|
|
||||||
|
# Prepare the LMP result dictionary
|
||||||
|
lmp = Dict()
|
||||||
|
|
||||||
|
# Calculate LMPs
|
||||||
|
# Fix all binary variables to their optimal values and relax integrality
|
||||||
|
if verbose
|
||||||
|
@info "Calculating LMPs..."
|
||||||
|
@info "Fixing all binary variables to their optimal values and relax integrality."
|
||||||
|
end
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Solve the LP
|
||||||
|
if verbose
|
||||||
|
@info "Solving the LP."
|
||||||
|
end
|
||||||
|
JuMP.optimize!(model)
|
||||||
|
|
||||||
|
# Obtain dual values (LMPs) and store into the LMP dictionary
|
||||||
|
if verbose
|
||||||
|
@info "Getting dual values (LMPs)."
|
||||||
|
end
|
||||||
|
for (key, val) in model[:eq_net_injection]
|
||||||
|
lmp[key] = dual(val)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return the LMP dictionary
|
||||||
|
if verbose
|
||||||
|
@info "Calculation completed."
|
||||||
|
end
|
||||||
|
return lmp
|
||||||
|
end
|
@ -0,0 +1,30 @@
|
|||||||
|
# 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, JuMP
|
||||||
|
|
||||||
|
@testset "aelmp" begin
|
||||||
|
path = "$FIXTURES/aelmp_simple.json.gz"
|
||||||
|
|
||||||
|
# policy 1: allow offlines; consider startups
|
||||||
|
aelmp_1 = UnitCommitment.get_aelmp(path)
|
||||||
|
@test aelmp_1["B1", 1] ≈ 231.7 atol = 0.1
|
||||||
|
|
||||||
|
# policy 2: do not allow offlines; but consider startups
|
||||||
|
# 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)
|
||||||
|
aelmp_2 = UnitCommitment.get_aelmp(
|
||||||
|
path,
|
||||||
|
solved_uc_model = model,
|
||||||
|
allow_offline_participation = false,
|
||||||
|
)
|
||||||
|
@test aelmp_2["B1", 1] ≈ 274.3 atol = 0.1
|
||||||
|
end
|
@ -0,0 +1,52 @@
|
|||||||
|
# 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, Clp, JuMP
|
||||||
|
|
||||||
|
function solve_lmp_testcase(path::String)
|
||||||
|
instance = UnitCommitment.read(path)
|
||||||
|
model = UnitCommitment.build_model(
|
||||||
|
instance = instance,
|
||||||
|
optimizer = Cbc.Optimizer,
|
||||||
|
variable_names = true,
|
||||||
|
)
|
||||||
|
# set silent, solve the UC
|
||||||
|
JuMP.set_silent(model)
|
||||||
|
UnitCommitment.optimize!(model)
|
||||||
|
# get the lmp
|
||||||
|
lmp = UnitCommitment.get_lmp(
|
||||||
|
model,
|
||||||
|
optimizer=Clp.Optimizer,
|
||||||
|
verbose=false
|
||||||
|
)
|
||||||
|
return lmp
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "lmp" begin
|
||||||
|
# instance 1
|
||||||
|
path = "$FIXTURES/lmp_simple_test_1.json.gz"
|
||||||
|
lmp = solve_lmp_testcase(path)
|
||||||
|
@test lmp["A", 1] == 50.0
|
||||||
|
@test lmp["B", 1] == 50.0
|
||||||
|
|
||||||
|
# instance 2
|
||||||
|
path = "$FIXTURES/lmp_simple_test_2.json.gz"
|
||||||
|
lmp = solve_lmp_testcase(path)
|
||||||
|
@test lmp["A", 1] == 50.0
|
||||||
|
@test lmp["B", 1] == 60.0
|
||||||
|
|
||||||
|
# instance 3
|
||||||
|
path = "$FIXTURES/lmp_simple_test_3.json.gz"
|
||||||
|
lmp = solve_lmp_testcase(path)
|
||||||
|
@test lmp["A", 1] == 50.0
|
||||||
|
@test lmp["B", 1] == 70.0
|
||||||
|
@test lmp["C", 1] == 100.0
|
||||||
|
|
||||||
|
# instance 4
|
||||||
|
path = "$FIXTURES/lmp_simple_test_4.json.gz"
|
||||||
|
lmp = solve_lmp_testcase(path)
|
||||||
|
@test lmp["A", 1] == 50.0
|
||||||
|
@test lmp["B", 1] == 70.0
|
||||||
|
@test lmp["C", 1] == 90.0
|
||||||
|
end
|
Loading…
Reference in new issue