diff --git a/docs/Project.toml b/docs/Project.toml index f9afcb4..01a33ce 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,4 +1,5 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +JuMP = "4076af6c-e467-56ae-b986-b466b2749572" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" UnitCommitment = "64606440-39ea-11e9-0f29-3303a1d3d877" diff --git a/docs/make.jl b/docs/make.jl index d7a587e..f266eb3 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,4 +1,4 @@ -using Documenter, UnitCommitment +using Documenter, UnitCommitment, JuMP makedocs( sitename="UnitCommitment.jl", diff --git a/docs/src/api.md b/docs/src/api.md index c237b7b..3445717 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -12,6 +12,20 @@ UnitCommitment.validate UnitCommitment.write ``` +## Locational Marginal Prices + +### Conventional LMPs +```@docs +UnitCommitment.compute_lmp(::JuMP.Model,::UnitCommitment.ConventionalLMP) +``` + +### Approximated Extended LMPs +```@docs +UnitCommitment.AELMP +UnitCommitment.compute_lmp(::JuMP.Model,::UnitCommitment.AELMP) +``` + + ## Modify instance ```@docs diff --git a/src/UnitCommitment.jl b/src/UnitCommitment.jl index 4e6d17c..484e5be 100644 --- a/src/UnitCommitment.jl +++ b/src/UnitCommitment.jl @@ -18,8 +18,6 @@ include("model/formulations/MorLatRam2013/structs.jl") include("model/formulations/PanGua2016/structs.jl") include("solution/methods/XavQiuWanThi2019/structs.jl") include("model/formulations/WanHob2016/structs.jl") -include("lmp/lmp/structs.jl") -include("lmp/aelmp/structs.jl") include("import/egret.jl") include("instance/read.jl") @@ -59,7 +57,7 @@ include("utils/log.jl") include("utils/benchmark.jl") include("validation/repair.jl") include("validation/validate.jl") -include("lmp/lmp/compute.jl") -include("lmp/aelmp/compute.jl") +include("lmp/conventional.jl") +include("lmp/aelmp.jl") end diff --git a/src/lmp/aelmp/compute.jl b/src/lmp/aelmp.jl similarity index 75% rename from src/lmp/aelmp/compute.jl rename to src/lmp/aelmp.jl index 4b92ddc..2ec0cfe 100644 --- a/src/lmp/aelmp/compute.jl +++ b/src/lmp/aelmp.jl @@ -3,21 +3,26 @@ # Released under the modified BSD license. See COPYING.md for more details. using JuMP + """ function compute_lmp( model::JuMP.Model, - method::AELMP.Method; + method::AELMP; optimizer = nothing, ) 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 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 series of start-up costs. 3. The method can only calculate for the first time slot if allow_offline_participation=false. @@ -29,7 +34,7 @@ Arguments the UnitCommitment model, must be solved before calling this function if offline participation is not allowed. - `method`: - the AELMP method, must be specified. + the AELMP method. - `optimizer`: the optimizer for solving the LP problem. @@ -38,60 +43,77 @@ Examples -------- ```julia - using UnitCommitment -using Cbc using HiGHS -import UnitCommitment: - AELMP +import UnitCommitment: AELMP # Read benchmark instance -instance = UnitCommitment.read("instance.json") +instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01") -# Construct model (using state-of-the-art defaults) +# Build the model model = UnitCommitment.build_model( instance = instance, - optimizer = Cbc.Optimizer, - variable_names = true, + optimizer = HiGHS.Optimizer, ) -# Get the AELMP with the default policy: -# 1. Offline generators are allowed to participate in pricing -# 2. Start-up costs are considered. -# DO NOT use Cbc as the optimizer here. Cbc does not support dual values. -my_aelmp_default = UnitCommitment.compute_lmp( - model, # pre-solving is optional if allowing offline participation - AELMP.Method(), - optimizer = HiGHS.Optimizer -) - -# Get the AELMPs with an alternative policy -# 1. Offline generators are NOT allowed to participate in pricing -# 2. Start-up costs are considered. -# UC model must be solved first if offline generators are NOT allowed +# Optimize the model UnitCommitment.optimize!(model) -# then call the AELMP method -my_aelmp_alt = UnitCommitment.compute_lmp( - model, # pre-solving is required here - AELMP.Method( - allow_offline_participation=false, - consider_startup_costs=true +# Compute the AELMPs +aelmp = UnitCommitment.compute_lmp( + model, + AELMP( + allow_offline_participation = false, + consider_startup_costs = true ), optimizer = HiGHS.Optimizer ) -# Accessing the 'my_aelmp_alt' dictionary +# Access the AELMPs # Example: "b1" is the bus name, 1 is the first time slot -@show my_aelmp_alt["b1", 1] - +@show aelmp["b1", 1] ``` - """ +function compute_lmp( + model::JuMP.Model, + method::AELMP; + optimizer, +)::OrderedDict{Tuple{String,Int},Float64} + @info "Calculating the AELMP..." + @info "Building the approximation model..." + instance = deepcopy(model[:instance]) + _preset_aelmp_parameters!(method, model) + _modify_instance!(instance, 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 _preset_aelmp_parameters!( - method::AELMP.Method, + method::AELMP, model::JuMP.Model ) # this function corrects the allow_offline_participation parameter to match the model status @@ -125,7 +147,7 @@ end function _modify_instance!( instance::UnitCommitmentInstance, model::JuMP.Model, - method::AELMP.Method + method::AELMP ) # this function modifies the instance units (generators) # 1. remove (if NOT allowing) the offline generators @@ -182,50 +204,4 @@ function _modify_instance!( ### END FIXME end instance.units_by_name = Dict(g.name => g for g in instance.units) -end - -function compute_lmp( - model::JuMP.Model, - method::AELMP.Method; - optimizer = nothing -) - # Error if a linear optimizer is not specified - if isnothing(optimizer) - @error "Please supply a linear optimizer." - return nothing - end - - @info "Calculating the AELMP..." - @info "Building the approximation model..." - # get the instance and make a deep copy - instance = deepcopy(model[:instance]) - # preset the method to match the model status (solved, unsolved, not supplied) - _preset_aelmp_parameters!(method, model) - # modify the instance (generator) - _modify_instance!(instance, 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 \ No newline at end of file diff --git a/src/lmp/aelmp/structs.jl b/src/lmp/aelmp/structs.jl deleted file mode 100644 index 301c6dd..0000000 --- a/src/lmp/aelmp/structs.jl +++ /dev/null @@ -1,41 +0,0 @@ -# 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. - -module AELMP - -import ..PricingMethod - -""" - mutable struct Method - allow_offline_participation::Bool, - consider_startup_costs::Bool - end - ------- - -- `allow_offline_participation`: - defaults to true. - If true, offline assets are allowed to participate in pricing. -- `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. - -""" - -mutable struct Method <: PricingMethod - allow_offline_participation::Bool - consider_startup_costs::Bool - - function Method(; - allow_offline_participation::Bool = true, - consider_startup_costs::Bool = true - ) - return new( - allow_offline_participation, - consider_startup_costs - ) - end -end - -end \ No newline at end of file diff --git a/src/lmp/lmp/compute.jl b/src/lmp/conventional.jl similarity index 62% rename from src/lmp/lmp/compute.jl rename to src/lmp/conventional.jl index efcf539..e6336c1 100644 --- a/src/lmp/lmp/compute.jl +++ b/src/lmp/conventional.jl @@ -7,13 +7,12 @@ using JuMP """ function compute_lmp( model::JuMP.Model, - method::LMP.Method; - optimizer = nothing - ) + method::ConventionalLMP; + optimizer, + )::OrderedDict{Tuple{String,Int},Float64} -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 nothing if there is an error in solving the LMPs. +Calculates conventional locational marginal prices of the given unit commitment +instance. Returns a dictionary mapping `(bus_name, time)` to the marginal price. Arguments --------- @@ -22,7 +21,7 @@ Arguments the UnitCommitment model, must be solved before calling this function. - `method`: - the LMP method, must be specified. + the LMP method. - `optimizer`: the optimizer for solving the LP problem. @@ -31,57 +30,40 @@ Examples -------- ```julia - using UnitCommitment -using Cbc using HiGHS -import UnitCommitment: - LMP - +import UnitCommitment: ConventionalLMP + # Read benchmark instance -instance = UnitCommitment.read("instance.json") +instance = UnitCommitment.read_benchmark("matpower/case118/2018-01-01") -# Construct model (using state-of-the-art defaults) +# Build the model model = UnitCommitment.build_model( instance = instance, - optimizer = Cbc.Optimizer, + optimizer = HiGHS.Optimizer, ) -# Get the LMPs before solving the UC model -# Error messages will be displayed and the returned value is nothing. -# lmp = UnitCommitment.compute_lmp(model, LMP.Method(), optimizer = HiGHS.Optimizer) # DO NOT RUN - +# Optimize the 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. -# Compute regular LMP -my_lmp = UnitCommitment.compute_lmp( +# Compute the LMPs using the conventional method +lmp = UnitCommitment.compute_lmp( model, - LMP.Method(), + ConventionalLMP(), optimizer = HiGHS.Optimizer, ) -# Accessing the 'my_lmp' dictionary +# Access the LMPs # Example: "b1" is the bus name, 1 is the first time slot -@show my_lmp["b1", 1] - +@show lmp["b1", 1] ``` - """ - function compute_lmp( model::JuMP.Model, - method::LMP.Method; - optimizer = nothing -) - # Error if a linear optimizer is not specified - if isnothing(optimizer) - @error "Please supply a linear optimizer." - return nothing - end - + ::ConventionalLMP; + optimizer, +)::OrderedDict{Tuple{String,Int},Float64} # Validate model, the UC model must be solved beforehand if !has_values(model) @error "The UC model must be solved before calculating the LMPs." diff --git a/src/lmp/lmp/structs.jl b/src/lmp/lmp/structs.jl deleted file mode 100644 index 63cf601..0000000 --- a/src/lmp/lmp/structs.jl +++ /dev/null @@ -1,18 +0,0 @@ -# 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. - -""" -Formulation described in: - - Arroyo, J. M., & Conejo, A. J. (2000). Optimal response of a thermal unit - to an electricity spot market. IEEE Transactions on power systems, 15(3), - 1098-1104. DOI: https://doi.org/10.1109/59.871739 -""" -module LMP - -import ..PricingMethod - -struct Method <: PricingMethod end - -end diff --git a/src/lmp/structs.jl b/src/lmp/structs.jl index 77d17f7..f6edd5e 100644 --- a/src/lmp/structs.jl +++ b/src/lmp/structs.jl @@ -3,3 +3,26 @@ # 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 \ No newline at end of file diff --git a/test/lmp/aelmp_test.jl b/test/lmp/aelmp_test.jl index cfa80ba..9807752 100644 --- a/test/lmp/aelmp_test.jl +++ b/test/lmp/aelmp_test.jl @@ -21,7 +21,7 @@ import UnitCommitment: # policy 1: allow offlines; consider startups aelmp_1 = UnitCommitment.compute_lmp( model, - AELMP.Method(), + AELMP(), optimizer=HiGHS.Optimizer ) @test aelmp_1["B1", 1] ≈ 231.7 atol = 0.1 @@ -29,7 +29,7 @@ import UnitCommitment: # policy 2: do not allow offlines; but consider startups aelmp_2 = UnitCommitment.compute_lmp( model, - AELMP.Method( + AELMP( allow_offline_participation=false, consider_startup_costs=true ), diff --git a/test/lmp/lmp_test.jl b/test/lmp/lmp_test.jl index e219501..329c8ed 100644 --- a/test/lmp/lmp_test.jl +++ b/test/lmp/lmp_test.jl @@ -3,8 +3,7 @@ # Released under the modified BSD license. See COPYING.md for more details. using UnitCommitment, Cbc, HiGHS, JuMP -import UnitCommitment: - LMP +import UnitCommitment: ConventionalLMP function solve_lmp_testcase(path::String) instance = UnitCommitment.read(path) @@ -13,13 +12,11 @@ function solve_lmp_testcase(path::String) optimizer = Cbc.Optimizer, variable_names = true, ) - # set silent, solve the UC JuMP.set_silent(model) UnitCommitment.optimize!(model) - # get the lmp lmp = UnitCommitment.compute_lmp( model, - LMP.Method(), + ConventionalLMP(), optimizer=HiGHS.Optimizer, ) return lmp