From 5c91dc2ac9801b488fe7c41afff274c77b18b351 Mon Sep 17 00:00:00 2001 From: Jun He Date: Wed, 8 Mar 2023 13:33:47 -0500 Subject: [PATCH 01/19] re-designed the LMP methods The LMP and AELMP methods are re-designed to be dependent on the instance object instead of input files, and to have a unified API style for purposes of flexibility and consistency. --- src/UnitCommitment.jl | 5 + src/lmp/aelmp/compute.jl | 231 +++++++++++++++++++++++++++++++++++++++ src/lmp/aelmp/structs.jl | 41 +++++++ src/lmp/lmp/compute.jl | 123 +++++++++++++++++++++ src/lmp/lmp/structs.jl | 18 +++ src/lmp/structs.jl | 5 + 6 files changed, 423 insertions(+) create mode 100644 src/lmp/aelmp/compute.jl create mode 100644 src/lmp/aelmp/structs.jl create mode 100644 src/lmp/lmp/compute.jl create mode 100644 src/lmp/lmp/structs.jl create mode 100644 src/lmp/structs.jl diff --git a/src/UnitCommitment.jl b/src/UnitCommitment.jl index 57e795b..4e6d17c 100644 --- a/src/UnitCommitment.jl +++ b/src/UnitCommitment.jl @@ -7,6 +7,7 @@ module UnitCommitment include("instance/structs.jl") include("model/formulations/base/structs.jl") include("solution/structs.jl") +include("lmp/structs.jl") include("model/formulations/ArrCon2000/structs.jl") include("model/formulations/CarArr2006/structs.jl") @@ -17,6 +18,8 @@ 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") @@ -56,5 +59,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") end diff --git a/src/lmp/aelmp/compute.jl b/src/lmp/aelmp/compute.jl new file mode 100644 index 0000000..4b92ddc --- /dev/null +++ b/src/lmp/aelmp/compute.jl @@ -0,0 +1,231 @@ +# 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.Method; + 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 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 +--------- + +- `model`: + the UnitCommitment model, must be solved before calling this function if offline participation is not allowed. + +- `method`: + the AELMP method, must be specified. + +- `optimizer`: + the optimizer for solving the LP problem. + +Examples +-------- + +```julia + +using UnitCommitment +using Cbc +using HiGHS + +import UnitCommitment: + AELMP + +# Read benchmark instance +instance = UnitCommitment.read("instance.json") + +# Construct model (using state-of-the-art defaults) +model = UnitCommitment.build_model( + instance = instance, + optimizer = Cbc.Optimizer, + variable_names = true, +) + +# 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 +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 + ), + optimizer = HiGHS.Optimizer +) + +# Accessing the 'my_aelmp_alt' dictionary +# Example: "b1" is the bus name, 1 is the first time slot +@show my_aelmp_alt["b1", 1] + +``` + +""" + +function _preset_aelmp_parameters!( + method::AELMP.Method, + model::JuMP.Model +) + # this function corrects the allow_offline_participation parameter to match the model status + # CHECK: model must be solved if allow_offline_participation=false + if method.allow_offline_participation # do nothing + @info "Offline generators are allowed to participate in pricing." + else + if isnothing(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" + method.allow_offline_participation = true # and do nothing else + elseif !has_values(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" + method.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 method.consider_startup_costs + @info "Startup costs are considered." + else + @info "Startup costs are NOT considered." + end +end + +function _modify_instance!( + instance::UnitCommitmentInstance, + model::JuMP.Model, + method::AELMP.Method +) + # this function modifies the instance units (generators) + # 1. remove (if NOT allowing) the offline generators + if !method.allow_offline_participation + for unit in instance.units + # remove based on the solved UC model result + # here, only look at the first time slot (TIME-SERIES-NOT-SUPPORTED) + if value(model[:is_on][unit.name, 1]) == 0 + # unregister from the bus + filter!(x -> x.name != unit.name, unit.bus.units) + # unregister from the reserve + for r in unit.reserves + filter!(x -> x.name != unit.name, r.units) + end + end + end + # unregister the units + filter!(x -> value(model[:is_on][x.name, 1]) != 0, instance.units) + end + + for unit in instance.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) + # for now, consider first element only (TIME-SERIES-NOT-SUPPORTED) + # 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 + + # 4. other adjustments... + ### FIXME in the future + # MISO Phase I: can ONLY solve fast-starts, force all startup time to be 0 + unit.startup_categories = StartupCategory[StartupCategory(0, first_startup_cost)] + unit.initial_status = -100 + unit.initial_power = 0 + unit.min_uptime = 0 + unit.min_downtime = 0 + ### 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 new file mode 100644 index 0000000..301c6dd --- /dev/null +++ b/src/lmp/aelmp/structs.jl @@ -0,0 +1,41 @@ +# 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/lmp/compute.jl new file mode 100644 index 0000000..efcf539 --- /dev/null +++ b/src/lmp/lmp/compute.jl @@ -0,0 +1,123 @@ +# 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::LMP.Method; + optimizer = nothing + ) + +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. + +Arguments +--------- + +- `model`: + the UnitCommitment model, must be solved before calling this function. + +- `method`: + the LMP method, must be specified. + +- `optimizer`: + the optimizer for solving the LP problem. + +Examples +-------- + +```julia + +using UnitCommitment +using Cbc +using HiGHS + +import UnitCommitment: + LMP + +# Read benchmark instance +instance = UnitCommitment.read("instance.json") + +# 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 nothing. +# lmp = UnitCommitment.compute_lmp(model, LMP.Method(), optimizer = HiGHS.Optimizer) # DO NOT RUN + +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( + model, + LMP.Method(), + optimizer = HiGHS.Optimizer, +) + +# Accessing the 'my_lmp' dictionary +# Example: "b1" is the bus name, 1 is the first time slot +@show my_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 + + # Validate model, the UC model must be solved beforehand + if !has_values(model) + @error "The UC model must be solved before calculating the LMPs." + @error "The LMPs are NOT calculated." + return nothing + end + + # Prepare the LMP result dictionary + lmp = OrderedDict() + + # Calculate LMPs + # Fix all binary variables to their optimal values and relax integrality + @info "Calculating LMPs..." + @info "Fixing all binary variables to their optimal values and relax 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 + # fix!(model, model[:solution]) + relax_integrality(model) + set_optimizer(model, optimizer) + + # Solve the LP + @info "Solving the LP..." + JuMP.optimize!(model) + + # Obtain dual values (LMPs) and store into the LMP dictionary + @info "Getting dual values (LMPs)..." + for (key, val) in model[:eq_net_injection] + lmp[key] = dual(val) + end + + # Return the LMP dictionary + @info "Calculation completed." + return lmp +end \ No newline at end of file diff --git a/src/lmp/lmp/structs.jl b/src/lmp/lmp/structs.jl new file mode 100644 index 0000000..63cf601 --- /dev/null +++ b/src/lmp/lmp/structs.jl @@ -0,0 +1,18 @@ +# 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 new file mode 100644 index 0000000..77d17f7 --- /dev/null +++ b/src/lmp/structs.jl @@ -0,0 +1,5 @@ +# 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 From 415732f0ec1c79ac5f2f6b4c071263bdc5fcf2dc Mon Sep 17 00:00:00 2001 From: Jun He Date: Wed, 8 Mar 2023 13:34:10 -0500 Subject: [PATCH 02/19] updated the doc with LMP and AELMP --- docs/src/usage.md | 104 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/docs/src/usage.md b/docs/src/usage.md index 3c65b75..8c2f993 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -139,3 +139,107 @@ solution = JSON.parsefile("solution.json") # Validate solution and print validation errors UnitCommitment.validate(instance, solution) ``` + +### Computing Locational Marginal Prices (LMPs) + +### Conventional LMPs + +The locational marginal price (LMP) refers to the cost of withdrawing one additional unit of energy at a bus. UC.jl computes the LMPs of a system using a three-step approach: (1) solving the UC model as usual, (2) fixing the values for all binary variables, and (3) re-solving the model. The LMPs are the dual variables' values associated with the net injection constraints. Step (1) is considered the pre-stage and the model must be solved before calling the `compute_lmp` method, in which Step (2) and (3) take place. + +The `compute_lmp` method calculates the locational marginal prices of the given unit commitment instance. The method accepts 3 arguments, which are(1) a solved UC model, (2) an LMP method object, and (3) a linear optimizer. Note that the LMP method is a struct that inherits the abstract type `PricingMethod`. For conventional (vanilla) LMP, the method is defined under the `LMP` module and contains no fields. Thus, one only needs to specify `LMP.Method()` for the second argument. This particular method style is designed to provide users with more flexibility to design their own pricing calculation methods (see [Approximate Extended LMPs](#approximate-extended-lmps) for more details.) Finally, the last argument requires a linear optimizer. Open-source optimizers such as `Clp` and `HiGHS` can be used here, but solvers such as `Cbc` do not support dual value evaluations and should be avoided in this method. The method returns a dictionary of LMPs. Each key is usually a tuple of "Bus name" and time index. It returns nothing if there is an error in solving the LMPs. Example usage can be found below. + +```julia +using UnitCommitment +using Cbc +using HiGHS + +import UnitCommitment: + LMP + +# Read benchmark instance +instance = UnitCommitment.read("instance.json") + +# 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 nothing. +# lmp = UnitCommitment.compute_lmp(model, LMP.Method(), optimizer = HiGHS.Optimizer) # DO NOT RUN + +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( + model, + LMP.Method(), + optimizer = HiGHS.Optimizer, +) + +# Accessing the 'my_lmp' dictionary +# Example: "b1" is the bus name, 1 is the first time slot +@show my_lmp["b1", 1] +``` + +### Approximate Extended LMPs + +UC.jl also provides an alternative method to calculate the approximate extended LMPs (AELMPs). The method is the same as the conventional name `compute_lmp` with the exception that the second argument takes the struct from the `AELMP` module. Similar to the conventional LMP, the AELMP method is a struct that inherits the abstract type `PricingMethod`. The AELMP method is defined under the `AELMP` module and contains two boolean fields: `allow_offline_participation` and `consider_startup_costs`. If `allow_offline_participation = true`, then offline generators are allowed to participate in the pricing. If instead `allow_offline_participation = false`, offline generators are not allowed and therefore are excluded from the system. A solved UC model is optional if offline participation is allowed, but is required if not allowed. The method forces offline participation to be allowed if the UC model supplied by the user is not solved. For the second field, If `consider_startup_costs = true`, then start-up costs are integrated and averaged over each unit production; otherwise the production costs stay the same. By default, both fields are set to `true`. The AELMP method can be used as an example for users to define their own pricing method. + +The method calculates the approximate extended locational marginal prices of the given unit commitment instance, which modifies the instance data in 3 ways: (1) it removes the minimum generation requirement for each generator, (2) it averages the start-up cost over the offer blocks for each generator, and (3) it relaxes all the binary constraints and integrality. Similarly, the method returns a dictionary of AELMPs. Each key is usually a tuple of "Bus name" and time index. + +However, this approximation method is not fully developed. The implementation is based on MISO Phase I only. It only supports fast start resources. More specifically, the minimum up/down time has to be zero. The method does not support time series of start-up costs. The method can only calculate for the first time slot if offline participation is not allowed. Example usage can be found below. + +```julia + +using UnitCommitment +using Cbc +using HiGHS + +import UnitCommitment: + AELMP + +# Read benchmark instance +instance = UnitCommitment.read("instance.json") + +# Construct model (using state-of-the-art defaults) +model = UnitCommitment.build_model( + instance = instance, + optimizer = Cbc.Optimizer, + variable_names = true, +) + +# 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 +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 + ), + optimizer = HiGHS.Optimizer +) + +# Accessing the 'my_aelmp_alt' dictionary +# Example: "b1" is the bus name, 1 is the first time slot +@show my_aelmp_alt["b1", 1] + +``` \ No newline at end of file From bc3aee38f81d9a2221de46972852fca1d4014606 Mon Sep 17 00:00:00 2001 From: Jun He Date: Wed, 8 Mar 2023 13:35:33 -0500 Subject: [PATCH 03/19] modified the tests for LMP and AELMP --- test/Project.toml | 1 + test/fixtures/aelmp_simple.json.gz | Bin 0 -> 364 bytes test/fixtures/lmp_simple_test_1.json.gz | Bin 0 -> 374 bytes test/fixtures/lmp_simple_test_2.json.gz | Bin 0 -> 391 bytes test/fixtures/lmp_simple_test_3.json.gz | Bin 0 -> 435 bytes test/fixtures/lmp_simple_test_4.json.gz | Bin 0 -> 439 bytes test/lmp/aelmp_test.jl | 39 +++++++++++++++++ test/lmp/lmp_test.jl | 54 ++++++++++++++++++++++++ test/runtests.jl | 4 ++ 9 files changed, 98 insertions(+) create mode 100644 test/fixtures/aelmp_simple.json.gz create mode 100644 test/fixtures/lmp_simple_test_1.json.gz create mode 100644 test/fixtures/lmp_simple_test_2.json.gz create mode 100644 test/fixtures/lmp_simple_test_3.json.gz create mode 100644 test/fixtures/lmp_simple_test_4.json.gz create mode 100644 test/lmp/aelmp_test.jl create mode 100644 test/lmp/lmp_test.jl diff --git a/test/Project.toml b/test/Project.toml index b87293d..5a175be 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -3,6 +3,7 @@ Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" GZip = "92fee26a-97fe-5a0c-ad85-20a5f3185b63" +HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" diff --git a/test/fixtures/aelmp_simple.json.gz b/test/fixtures/aelmp_simple.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..aaa932592338f32fe1fba42980433dd2b560458c GIT binary patch literal 364 zcmV-y0h9h8iwFo#?CW9x17T%sZE#<6X>D+9WiD!SZ*BnHl;2CkFc8Pz_g94CLxoO~ z#&*-ky@=popg2W*2yI}2H7WVw(9!?hBwe?)wcub+>%*1aeeW)pPtxrO*e6&JNeZf| zRKY#idY|{dS`QQ4IsVFd*%A=!`MAY5Hr3F?NGrX>gq#$7apK3dhHU}hBU zG@GH;xA#2>l7n!*bQN%#?1NRS*aPKM5-qyRU-k>yzHS=ZD{2Q8QsgGnc3O}Lr9o!o z7j0U9zzD?98SG37KmV6~)g&VbT{=R^oqOSU(kt<-iPWa_`D&j!q{``8g_hpZq zX-3u-0nw(BjqNH1bfwr(*-YCN%^7EfDS#=p!&(;`66IpUk7WmBN5|vsY+#Vz)>~($ zr%_2gk!f0U!F{HG0>KXCWkPvB8GH9H4=+HA{s=B}6MaAckvlB9>x1@&^@E{qNXqlDZ0riG|I+_p$HpJ$pMKBzgn| z91SWR-I1+}xY)DXF)3UiqNA&5c!(|;M^Z|~KBXXWdG6#Sl(fN`#GDDQq|_-pFGrcx z7+gN>mTvPPp&;>m<&V&PaMxU!{e&g70lNe<6sW++u!R|f=UoZDnQ7CCZ(GU2=EikN za;Xi;t$N2c#S0z!H1zd1{H&$N^aNiRP{!7KR(DumFR+3S-Iy5y_ktM)mGcygZGj6{ zQ;(?S232QaC(oH++;U=T<7acYpf4A4BSPS01he%w9%g^$VfwE;>|WJ3dR1QU2d$PEa0D3&GH`}89^o+LL%o2<^UZlHxHE+?bw UNqUm2L`Un&yx-V6i)0R9WB_5c6? literal 0 HcmV?d00001 diff --git a/test/fixtures/lmp_simple_test_2.json.gz b/test/fixtures/lmp_simple_test_2.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..f23d08a78edd166d8616a1be30c3fe491eecb99a GIT binary patch literal 391 zcmV;20eJo&iwFp&2J~V818i+@Uvp_~aBO8?bY*jNUotLgb8l_{-IF~}!!Qtr_x=hi zVhB>H9H4=+wM&IW5uz0gh`~(`YALoOe^5cy|IWEhjH`f{SlH})AN%g!vo}3Lk~^lE z01MhAS7d_`j6L9vD~W-G4$hMPKDyun$VzGcp(IIH$0%p9WGvJq6JbboJ^d7 z=<;z`;^s!NlBBaoe}wLW+vdv2Pgs^UU`Q|p37T0Iw=iY#c`U&Qg05VuwTyv-5lolgcsThp5A%QJVR)Sj&7=|B_#Z_o zF5|yYWwFC^<#YjL*?FuBreDFfLNtMUV1;E852b1)%r5U>1H?Q-MWh*I1xAWLcEA=bb%%*`Ecmie>drt(zs4R7>X8J$T`nR-o7WCRSTFW zxJCj|tgw*5Iarl36TLNS!kGyLu+t0LE_>A^ApyV zQDurXQSeNs$~Xp6E)__%c){+td)T%|!iTR5jvODu4+S%YgwjPx%L;5FGMuAVxowV* zTri>t%1w`u(hfN>+8UUGFR`eaQ3Ixg5vnQBg|hwLY|;c|e97v_PKzdwjm{%pYob9Bv+uR&K_P~**0&X-0kCCVApjdw0i-u+B_ dvS0i77ijN&)Ayg#-g002z610q^w*IG002^<%838~ literal 0 HcmV?d00001 diff --git a/test/fixtures/lmp_simple_test_4.json.gz b/test/fixtures/lmp_simple_test_4.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..cddf8afc412c21365f6778ddc64b232c90e8601e GIT binary patch literal 439 zcmV;o0Z9HIiwFqv2lQe918i+@Uvp_~aBO8?bY*jNUocRCt<3PP&zMYS%FPNiZk>fx6N^p z8%7jCnduQyx6(^Y9-qN)%!y z(dP>)i()!390dKkJ}ncRZ?UOmx`WQ;Ai9cb&>a`lxOvU^(iBUHDg|}Jy@!)^zpFhy hT>I$6wKq)stoKXrr>;HPc~z~={|lN1D~6E=005hs%+mk> literal 0 HcmV?d00001 diff --git a/test/lmp/aelmp_test.jl b/test/lmp/aelmp_test.jl new file mode 100644 index 0000000..cfa80ba --- /dev/null +++ b/test/lmp/aelmp_test.jl @@ -0,0 +1,39 @@ +# 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.Method(), + optimizer=HiGHS.Optimizer + ) + @test aelmp_1["B1", 1] ≈ 231.7 atol = 0.1 + + # policy 2: do not allow offlines; but consider startups + aelmp_2 = UnitCommitment.compute_lmp( + model, + AELMP.Method( + allow_offline_participation=false, + consider_startup_costs=true + ), + optimizer=HiGHS.Optimizer + ) + @test aelmp_2["B1", 1] ≈ 274.3 atol = 0.1 +end \ No newline at end of file diff --git a/test/lmp/lmp_test.jl b/test/lmp/lmp_test.jl new file mode 100644 index 0000000..e219501 --- /dev/null +++ b/test/lmp/lmp_test.jl @@ -0,0 +1,54 @@ +# 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: + LMP + +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.compute_lmp( + model, + LMP.Method(), + optimizer=HiGHS.Optimizer, + ) + 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 \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 9a61bf2..4203495 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -39,4 +39,8 @@ FIXTURES = "$(@__DIR__)/fixtures" @testset "validation" begin include("validation/repair_test.jl") end + @testset "lmp" begin + include("lmp/lmp_test.jl") + include("lmp/aelmp_test.jl") + end end From 34ca6952fbfe8134a2f80894340d1115db0552b3 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 15 Mar 2023 11:34:50 -0500 Subject: [PATCH 04/19] Revise docs --- docs/src/usage.md | 102 +++++++++++++++++++--------------------------- 1 file changed, 41 insertions(+), 61 deletions(-) diff --git a/docs/src/usage.md b/docs/src/usage.md index 8c2f993..d9e5f65 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -58,10 +58,7 @@ using UnitCommitment instance = UnitCommitment.read_benchmark("matpower/case3375wp/2017-02-01") ``` -Advanced usage --------------- - -### Customizing the formulation +## Customizing the formulation By default, `build_model` uses a formulation that combines modeling components from different publications, and that has been carefully tested, using our own benchmark scripts, to provide good performance across a wide variety of instances. This default formulation is expected to change over time, as new methods are proposed in the literature. You can, however, construct your own formulation, based on the modeling components that you choose, as shown in the next example. @@ -94,7 +91,7 @@ model = UnitCommitment.build_model( ) ``` -### Generating initial conditions +## Generating initial conditions When creating random unit commitment instances for benchmark purposes, it is often hard to compute, in advance, sensible initial conditions for all generators. Setting initial conditions naively (for example, making all generators initially off and producing no power) can easily cause the instance to become infeasible due to excessive ramping. Initial conditions can also make it hard to modify existing instances. For example, increasing the system load without carefully modifying the initial conditions may make the problem infeasible or unrealistically challenging to solve. @@ -122,7 +119,7 @@ UnitCommitment.optimize!(model) The function `generate_initial_conditions!` may return different initial conditions after each call, even if the same instance and the same optimizer is provided. The particular algorithm may also change in a future version of UC.jl. For these reasons, it is recommended that you generate initial conditions exactly once for each instance and store them for later use. -### Verifying solutions +## Verifying solutions When developing new formulations, it is very easy to introduce subtle errors in the model that result in incorrect solutions. To help with this, UC.jl includes a utility function that verifies if a given solution is feasible, and, if not, prints all the validation errors it found. The implementation of this function is completely independent from the implementation of the optimization model, and therefore can be used to validate it. The function can also be used to verify solutions produced by other optimization packages, as long as they follow the [UC.jl data format](format.md). @@ -140,106 +137,89 @@ solution = JSON.parsefile("solution.json") UnitCommitment.validate(instance, solution) ``` -### Computing Locational Marginal Prices (LMPs) +## Computing Locational Marginal Prices + +Locational marginal prices (LMPs) refer to the cost of supplying electricity at a particular location of the network. Multiple methods for computing LMPs have been proposed in the literature. UnitCommitment.jl implements two commonly-used methods: conventional LMPs and Approximated Extended LMPs (AELMPs). To compute LMPs for a given unit commitment instance, the `compute_lmp` function can be used, as shown in the examples below. The function accepts three arguments -- a solved SCUC model, an LMP method, and a linear optimizer -- and it returns a dictionary mapping `(bus_name, time)` to the marginal price. + + +!!! warning + + Most mixed-integer linear optimizers, such as `HiGHS`, `Gurobi` and `CPLEX` can be used with `compute_lmp`, with the notable exception of `Cbc`, which does not support dual value evaluations. If using `Cbc`, please provide `Clp` as the linear optimizer. ### Conventional LMPs -The locational marginal price (LMP) refers to the cost of withdrawing one additional unit of energy at a bus. UC.jl computes the LMPs of a system using a three-step approach: (1) solving the UC model as usual, (2) fixing the values for all binary variables, and (3) re-solving the model. The LMPs are the dual variables' values associated with the net injection constraints. Step (1) is considered the pre-stage and the model must be solved before calling the `compute_lmp` method, in which Step (2) and (3) take place. +LMPs are conventionally computed by: (1) solving the SCUC model, (2) fixing all binary variables to their optimal values, and (3) re-solving the resulting linear programming model. In this approach, the LMPs are defined as the dual variables' values associated with the net injection constraints. The example below shows how to compute conventional LMPs for a given unit commitment instance. First, we build and optimize the SCUC model. Then, we call the `compute_lmp` function, providing as the second argument `ConventionalLMP()`. -The `compute_lmp` method calculates the locational marginal prices of the given unit commitment instance. The method accepts 3 arguments, which are(1) a solved UC model, (2) an LMP method object, and (3) a linear optimizer. Note that the LMP method is a struct that inherits the abstract type `PricingMethod`. For conventional (vanilla) LMP, the method is defined under the `LMP` module and contains no fields. Thus, one only needs to specify `LMP.Method()` for the second argument. This particular method style is designed to provide users with more flexibility to design their own pricing calculation methods (see [Approximate Extended LMPs](#approximate-extended-lmps) for more details.) Finally, the last argument requires a linear optimizer. Open-source optimizers such as `Clp` and `HiGHS` can be used here, but solvers such as `Cbc` do not support dual value evaluations and should be avoided in this method. The method returns a dictionary of LMPs. Each key is usually a tuple of "Bus name" and time index. It returns nothing if there is an error in solving the LMPs. Example usage can be found below. ```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] ``` ### Approximate Extended LMPs -UC.jl also provides an alternative method to calculate the approximate extended LMPs (AELMPs). The method is the same as the conventional name `compute_lmp` with the exception that the second argument takes the struct from the `AELMP` module. Similar to the conventional LMP, the AELMP method is a struct that inherits the abstract type `PricingMethod`. The AELMP method is defined under the `AELMP` module and contains two boolean fields: `allow_offline_participation` and `consider_startup_costs`. If `allow_offline_participation = true`, then offline generators are allowed to participate in the pricing. If instead `allow_offline_participation = false`, offline generators are not allowed and therefore are excluded from the system. A solved UC model is optional if offline participation is allowed, but is required if not allowed. The method forces offline participation to be allowed if the UC model supplied by the user is not solved. For the second field, If `consider_startup_costs = true`, then start-up costs are integrated and averaged over each unit production; otherwise the production costs stay the same. By default, both fields are set to `true`. The AELMP method can be used as an example for users to define their own pricing method. +Approximate Extended LMPs (AELMPs) are an alternative method to calculate locational marginal prices which attemps to minimize uplift payments. The method internally works by modifying the instance data in three ways: (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, and (3) it relaxes all integrality constraints. To compute AELMPs, as shown in the example below, we call `compute_lmp` and provide `AELMP()` as the second argument. -The method calculates the approximate extended locational marginal prices of the given unit commitment instance, which modifies the instance data in 3 ways: (1) it removes the minimum generation requirement for each generator, (2) it averages the start-up cost over the offer blocks for each generator, and (3) it relaxes all the binary constraints and integrality. Similarly, the method returns a dictionary of AELMPs. Each key is usually a tuple of "Bus name" and time index. +This method has two configurable parameters: `allow_offline_participation` and `consider_startup_costs`. If `allow_offline_participation = true`, then offline generators are allowed to participate in the pricing. If instead `allow_offline_participation = false`, offline generators are not allowed and therefore are excluded from the system. A solved UC model is optional if offline participation is allowed, but is required if not allowed. The method forces offline participation to be allowed if the UC model supplied by the user is not solved. For the second field, If `consider_startup_costs = true`, then start-up costs are integrated and averaged over each unit production; otherwise the production costs stay the same. By default, both fields are set to `true`. -However, this approximation method is not fully developed. The implementation is based on MISO Phase I only. It only supports fast start resources. More specifically, the minimum up/down time has to be zero. The method does not support time series of start-up costs. The method can only calculate for the first time slot if offline participation is not allowed. Example usage can be found below. +!!! warning -```julia + This approximation method is still under active research, and has several limitations. The implementation provided in the package is based on MISO Phase I only. It only supports fast start resources. More specifically, the minimum up/down time of all generators must be 1. The method does not support time-varying start-up costs. AELMPs are only calculated for the first time period if offline participation is not allowed. +```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, -) - -# 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 + 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] ``` \ No newline at end of file From d2e11eee42064291fc7a6d542b88e318cf9fa9aa Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 15 Mar 2023 12:08:35 -0500 Subject: [PATCH 05/19] Flatten dir structure, update docstrings --- docs/Project.toml | 1 + docs/make.jl | 2 +- docs/src/api.md | 14 ++ src/UnitCommitment.jl | 6 +- src/lmp/{aelmp/compute.jl => aelmp.jl} | 150 ++++++++------------ src/lmp/aelmp/structs.jl | 41 ------ src/lmp/{lmp/compute.jl => conventional.jl} | 58 +++----- src/lmp/lmp/structs.jl | 18 --- src/lmp/structs.jl | 23 +++ test/lmp/aelmp_test.jl | 4 +- test/lmp/lmp_test.jl | 7 +- 11 files changed, 128 insertions(+), 196 deletions(-) rename src/lmp/{aelmp/compute.jl => aelmp.jl} (75%) delete mode 100644 src/lmp/aelmp/structs.jl rename src/lmp/{lmp/compute.jl => conventional.jl} (62%) delete mode 100644 src/lmp/lmp/structs.jl 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 From 784ebfa19971fad3406dd07a65bda0a63f1e2dfb Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 15 Mar 2023 12:15:57 -0500 Subject: [PATCH 06/19] ConventionalLMP: turn warnings into errors, remove some inline comments --- src/lmp/conventional.jl | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/lmp/conventional.jl b/src/lmp/conventional.jl index e6336c1..2d26b63 100644 --- a/src/lmp/conventional.jl +++ b/src/lmp/conventional.jl @@ -64,20 +64,12 @@ function compute_lmp( ::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." - @error "The LMPs are NOT calculated." - return nothing + error("The UC model must be solved before calculating the LMPs.") end - - # Prepare the LMP result dictionary lmp = OrderedDict() - # Calculate LMPs - # Fix all binary variables to their optimal values and relax integrality - @info "Calculating LMPs..." - @info "Fixing all binary variables to their optimal values and relax integrality." + @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) @@ -85,21 +77,16 @@ function compute_lmp( fix(v, vals[v]) end end - # fix!(model, model[:solution]) relax_integrality(model) set_optimizer(model, optimizer) - # Solve the LP @info "Solving the LP..." JuMP.optimize!(model) - # Obtain dual values (LMPs) and store into the LMP dictionary @info "Getting dual values (LMPs)..." for (key, val) in model[:eq_net_injection] lmp[key] = dual(val) end - # Return the LMP dictionary - @info "Calculation completed." return lmp -end \ No newline at end of file +end From d7d2a3fcf6dd1f8835f872acf39ae5d8b1489691 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 15 Mar 2023 12:23:18 -0500 Subject: [PATCH 07/19] AELMP: Convert warnings into errors; update docstrings --- src/lmp/aelmp.jl | 35 +++++++---------------------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/src/lmp/aelmp.jl b/src/lmp/aelmp.jl index 2ec0cfe..a2d5b07 100644 --- a/src/lmp/aelmp.jl +++ b/src/lmp/aelmp.jl @@ -24,8 +24,8 @@ 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. +2. The method does NOT support time-varying start-up costs. +3. AELMPs are only calculated for the first time period if offline participation is not allowed. Arguments --------- @@ -80,10 +80,9 @@ function compute_lmp( 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) + _aelmp_check_parameters(method, model) _modify_instance!(instance, model, method) # prepare the result dictionary and solve the model @@ -112,36 +111,16 @@ function compute_lmp( return elmp end -function _preset_aelmp_parameters!( +function _aelmp_check_parameters( method::AELMP, model::JuMP.Model ) - # this function corrects the allow_offline_participation parameter to match the model status # CHECK: model must be solved if allow_offline_participation=false - if method.allow_offline_participation # do nothing - @info "Offline generators are allowed to participate in pricing." - else - if isnothing(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" - method.allow_offline_participation = true # and do nothing else - elseif !has_values(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" - method.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." + 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 - - # CHECK: start up cost consideration - if method.consider_startup_costs - @info "Startup costs are considered." - else - @info "Startup costs are NOT considered." - end end function _modify_instance!( From 19e84bac07a24d2fa05c82c25a4a9d0df5454d03 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 15 Mar 2023 12:27:43 -0500 Subject: [PATCH 08/19] Reformat source code --- src/lmp/aelmp.jl | 32 ++++++++++++++++++-------------- src/lmp/conventional.jl | 2 +- src/lmp/structs.jl | 4 ++-- test/lmp/aelmp_test.jl | 24 ++++++++++-------------- test/lmp/lmp_test.jl | 4 ++-- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/lmp/aelmp.jl b/src/lmp/aelmp.jl index a2d5b07..f69f052 100644 --- a/src/lmp/aelmp.jl +++ b/src/lmp/aelmp.jl @@ -88,7 +88,7 @@ function compute_lmp( # prepare the result dictionary and solve the model elmp = OrderedDict() @info "Solving the approximation model." - approx_model = build_model(instance=instance, variable_names=true) + approx_model = build_model(instance = instance, variable_names = true) # relax the binary constraint, and relax integrality for v in all_variables(approx_model) @@ -111,14 +111,13 @@ function compute_lmp( return elmp end -function _aelmp_check_parameters( - method::AELMP, - model::JuMP.Model -) +function _aelmp_check_parameters(method::AELMP, model::JuMP.Model) # 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.") + error( + "A solved UC model is required if allow_offline_participation=false.", + ) end end end @@ -126,7 +125,7 @@ end function _modify_instance!( instance::UnitCommitmentInstance, model::JuMP.Model, - method::AELMP + method::AELMP, ) # this function modifies the instance units (generators) # 1. remove (if NOT allowing) the offline generators @@ -152,10 +151,14 @@ function _modify_instance!( # 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] - )) + 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 @@ -175,12 +178,13 @@ function _modify_instance!( # 4. other adjustments... ### FIXME in the future # MISO Phase I: can ONLY solve fast-starts, force all startup time to be 0 - unit.startup_categories = StartupCategory[StartupCategory(0, first_startup_cost)] + unit.startup_categories = + StartupCategory[StartupCategory(0, first_startup_cost)] unit.initial_status = -100 unit.initial_power = 0 unit.min_uptime = 0 unit.min_downtime = 0 ### END FIXME end - instance.units_by_name = Dict(g.name => g for g in instance.units) -end \ No newline at end of file + return instance.units_by_name = Dict(g.name => g for g in instance.units) +end diff --git a/src/lmp/conventional.jl b/src/lmp/conventional.jl index 2d26b63..005cf60 100644 --- a/src/lmp/conventional.jl +++ b/src/lmp/conventional.jl @@ -82,7 +82,7 @@ function compute_lmp( @info "Solving the LP..." JuMP.optimize!(model) - + @info "Getting dual values (LMPs)..." for (key, val) in model[:eq_net_injection] lmp[key] = dual(val) diff --git a/src/lmp/structs.jl b/src/lmp/structs.jl index f6edd5e..a2816d2 100644 --- a/src/lmp/structs.jl +++ b/src/lmp/structs.jl @@ -22,7 +22,7 @@ Arguments - `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 +Base.@kwdef struct AELMP <: PricingMethod allow_offline_participation::Bool = true consider_startup_costs::Bool = true -end \ No newline at end of file +end diff --git a/test/lmp/aelmp_test.jl b/test/lmp/aelmp_test.jl index 9807752..29a3194 100644 --- a/test/lmp/aelmp_test.jl +++ b/test/lmp/aelmp_test.jl @@ -3,37 +3,33 @@ # Released under the modified BSD license. See COPYING.md for more details. using UnitCommitment, Cbc, HiGHS, JuMP -import UnitCommitment: - AELMP +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, + 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 - ) + aelmp_1 = + UnitCommitment.compute_lmp(model, AELMP(), optimizer = HiGHS.Optimizer) @test aelmp_1["B1", 1] ≈ 231.7 atol = 0.1 # policy 2: do not allow offlines; but consider startups aelmp_2 = UnitCommitment.compute_lmp( - model, + model, AELMP( - allow_offline_participation=false, - consider_startup_costs=true + allow_offline_participation = false, + consider_startup_costs = true, ), - optimizer=HiGHS.Optimizer + optimizer = HiGHS.Optimizer, ) @test aelmp_2["B1", 1] ≈ 274.3 atol = 0.1 -end \ No newline at end of file +end diff --git a/test/lmp/lmp_test.jl b/test/lmp/lmp_test.jl index 329c8ed..ddd3411 100644 --- a/test/lmp/lmp_test.jl +++ b/test/lmp/lmp_test.jl @@ -17,7 +17,7 @@ function solve_lmp_testcase(path::String) lmp = UnitCommitment.compute_lmp( model, ConventionalLMP(), - optimizer=HiGHS.Optimizer, + optimizer = HiGHS.Optimizer, ) return lmp end @@ -48,4 +48,4 @@ end @test lmp["A", 1] == 50.0 @test lmp["B", 1] == 70.0 @test lmp["C", 1] == 90.0 -end \ No newline at end of file +end From 4827c2923046abb32a60a8d80e397cc6cac1e6f6 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 15 Mar 2023 12:41:09 -0500 Subject: [PATCH 09/19] Add Jun to authors --- README.md | 1 + docs/src/index.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index ddbadcc..8b74be1 100755 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ UnitCommitment.write("/tmp/output.json", solution) * **Alinson S. Xavier** (Argonne National Laboratory) * **Aleksandr M. Kazachkov** (University of Florida) * **Ogün Yurdakul** (Technische Universität Berlin) +* **Jun He** (Purdue University) * **Feng Qiu** (Argonne National Laboratory) ## Acknowledgments diff --git a/docs/src/index.md b/docs/src/index.md index f5cb261..9a8a013 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -20,6 +20,7 @@ Depth = 3 * **Alinson S. Xavier** (Argonne National Laboratory) * **Aleksandr M. Kazachkov** (University of Florida) * **Ogün Yurdakul** (Technische Universität Berlin) +* **Jun He** (Purdue University) * **Feng Qiu** (Argonne National Laboratory) ## Acknowledgments From 5f5c8b66ebf7695f62314545fa3973ad4cfc71e0 Mon Sep 17 00:00:00 2001 From: Jun He Date: Sun, 19 Mar 2023 14:28:39 -0400 Subject: [PATCH 10/19] more condition checking on AELMP --- docs/src/usage.md | 2 +- src/lmp/aelmp.jl | 62 +++++++++++++++++++++++++++++++---------------- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/docs/src/usage.md b/docs/src/usage.md index d9e5f65..08fe545 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -189,7 +189,7 @@ This method has two configurable parameters: `allow_offline_participation` and ` !!! warning - This approximation method is still under active research, and has several limitations. The implementation provided in the package is based on MISO Phase I only. It only supports fast start resources. More specifically, the minimum up/down time of all generators must be 1. The method does not support time-varying start-up costs. AELMPs are only calculated for the first time period if offline participation is not allowed. + This approximation method is still under active research, and has several limitations. The implementation provided in the package is based on MISO Phase I only. It only supports fast start resources. More specifically, the minimum up/down time of all generators must be 1, the initial power of all generators must be 0, and the initial status of all generators must be negative. The method does not support time-varying start-up costs. If offline participation is not allowed, AELMPs treats an asset to be offline if it is never on throughout all time periods. ```julia using UnitCommitment diff --git a/src/lmp/aelmp.jl b/src/lmp/aelmp.jl index f69f052..93041da 100644 --- a/src/lmp/aelmp.jl +++ b/src/lmp/aelmp.jl @@ -8,8 +8,8 @@ using JuMP function compute_lmp( model::JuMP.Model, method::AELMP; - optimizer = nothing, - ) + optimizer, + )::OrderedDict{Tuple{String,Int},Float64} Calculates the approximate extended locational marginal prices of the given unit commitment instance. @@ -25,7 +25,7 @@ WARNING: This approximation method is not fully developed. The implementation is 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. AELMPs are only calculated for the first time period if offline participation is not allowed. +3. An asset is considered offline if it is never on throughout all time periods. Arguments --------- @@ -82,7 +82,7 @@ function compute_lmp( )::OrderedDict{Tuple{String,Int},Float64} @info "Building the approximation model..." instance = deepcopy(model[:instance]) - _aelmp_check_parameters(method, model) + _aelmp_check_parameters(instance, model, method) _modify_instance!(instance, model, method) # prepare the result dictionary and solve the model @@ -111,7 +111,11 @@ function compute_lmp( return elmp end -function _aelmp_check_parameters(method::AELMP, model::JuMP.Model) +function _aelmp_check_parameters( + instance::UnitCommitmentInstance, + model::JuMP.Model, + method::AELMP, +) # CHECK: model must be solved if allow_offline_participation=false if !method.allow_offline_participation if isnothing(model) || !has_values(model) @@ -120,6 +124,29 @@ function _aelmp_check_parameters(method::AELMP, model::JuMP.Model) ) end end + all_units = instance.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_instance!( @@ -128,22 +155,25 @@ function _modify_instance!( method::AELMP, ) # this function modifies the instance units (generators) - # 1. remove (if NOT allowing) the offline generators if !method.allow_offline_participation + # 1. remove (if NOT allowing) the offline generators + units_to_remove = [] for unit in instance.units # remove based on the solved UC model result - # here, only look at the first time slot (TIME-SERIES-NOT-SUPPORTED) - if value(model[:is_on][unit.name, 1]) == 0 + # remove the unit if it is never on + if all(t -> value(model[:is_on][unit.name, t]) == 0, instance.time) # unregister from the bus filter!(x -> x.name != unit.name, unit.bus.units) # unregister from the reserve for r in unit.reserves filter!(x -> x.name != unit.name, r.units) end + # append the name to the remove list + push!(units_to_remove, unit.name) end end - # unregister the units - filter!(x -> value(model[:is_on][x.name, 1]) != 0, instance.units) + # unregister the units from the remove list + filter!(x -> !(x.name in units_to_remove), instance.units) end for unit in instance.units @@ -164,7 +194,6 @@ function _modify_instance!( end # 3. average the start-up costs (if considering) - # for now, consider first element only (TIME-SERIES-NOT-SUPPORTED) # 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 @@ -174,17 +203,8 @@ function _modify_instance!( end first_startup_cost = 0.0 # zero out the start up cost end - - # 4. other adjustments... - ### FIXME in the future - # MISO Phase I: can ONLY solve fast-starts, force all startup time to be 0 unit.startup_categories = - StartupCategory[StartupCategory(0, first_startup_cost)] - unit.initial_status = -100 - unit.initial_power = 0 - unit.min_uptime = 0 - unit.min_downtime = 0 - ### END FIXME + StartupCategory[StartupCategory(0, first_startup_cost)] end return instance.units_by_name = Dict(g.name => g for g in instance.units) end From 0b95df25eca29c47baa04ea409034ea78b8dd29f Mon Sep 17 00:00:00 2001 From: Jun He Date: Fri, 24 Mar 2023 10:56:41 -0400 Subject: [PATCH 11/19] typo fix in generator json example --- docs/src/format.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/src/format.md b/docs/src/format.md index 3f9887c..4efd322 100644 --- a/docs/src/format.md +++ b/docs/src/format.md @@ -126,14 +126,17 @@ Note that this curve also specifies the production limits. Specifically, the fir "Minimum downtime (h)": 4, "Minimum uptime (h)": 4, "Initial status (h)": 12, + "Initial power (MW)": 115, "Must run?": false, - "Reserve eligibility": ["r1"], + "Reserve eligibility": ["r1"] }, "gen2": { "Bus": "b5", "Production cost curve (MW)": [0.0, [10.0, 8.0, 0.0, 3.0]], "Production cost curve ($)": [0.0, 0.0], - "Reserve eligibility": ["r1", "r2"], + "Initial status (h)": -100, + "Initial power (MW)": 0, + "Reserve eligibility": ["r1", "r2"] } } } From 71ed55cb4089564c15f6643073a137f6f0eeaff5 Mon Sep 17 00:00:00 2001 From: Jun He Date: Thu, 30 Mar 2023 14:30:10 -0400 Subject: [PATCH 12/19] Formatted codes on the LMP dev branch --- src/lmp/aelmp.jl | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/lmp/aelmp.jl b/src/lmp/aelmp.jl index 93041da..0601a62 100644 --- a/src/lmp/aelmp.jl +++ b/src/lmp/aelmp.jl @@ -124,28 +124,22 @@ function _aelmp_check_parameters( ) end end - all_units = instance.units; + all_units = instance.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) + 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.", - ) + 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.", - ) + 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.", - ) + error("The method does NOT support time-varying start-up costs.") end end @@ -204,7 +198,7 @@ function _modify_instance!( first_startup_cost = 0.0 # zero out the start up cost end unit.startup_categories = - StartupCategory[StartupCategory(0, first_startup_cost)] + StartupCategory[StartupCategory(0, first_startup_cost)] end return instance.units_by_name = Dict(g.name => g for g in instance.units) end From 2a6c206e08d7259a8458b65d2034053dd9a6a890 Mon Sep 17 00:00:00 2001 From: Jun He Date: Thu, 30 Mar 2023 23:19:24 -0400 Subject: [PATCH 13/19] updated LMP for UC scenario --- docs/src/usage.md | 9 ++--- src/lmp/aelmp.jl | 35 ++++++++++++------- src/lmp/conventional.jl | 8 ++--- test/lmp/aelmp_test.jl | 4 +-- .../lmp/{lmp_test.jl => conventional_test.jl} | 32 ++++++++--------- test/runtests.jl | 2 +- 6 files changed, 50 insertions(+), 40 deletions(-) rename test/lmp/{lmp_test.jl => conventional_test.jl} (61%) diff --git a/docs/src/usage.md b/docs/src/usage.md index 08fe545..aababa0 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -177,8 +177,8 @@ lmp = UnitCommitment.compute_lmp( ) # Access the LMPs -# Example: "b1" is the bus name, 1 is the first time slot -@show lmp["b1", 1] +# Example: "s1" is the scenario name, "b1" is the bus name, 1 is the first time slot +@show lmp["s1","b1", 1] ``` ### Approximate Extended LMPs @@ -220,6 +220,7 @@ aelmp = UnitCommitment.compute_lmp( ) # Access the AELMPs -# Example: "b1" is the bus name, 1 is the first time slot -@show aelmp["b1", 1] +# 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] ``` \ No newline at end of file diff --git a/src/lmp/aelmp.jl b/src/lmp/aelmp.jl index 0601a62..8b4e44c 100644 --- a/src/lmp/aelmp.jl +++ b/src/lmp/aelmp.jl @@ -26,6 +26,7 @@ WARNING: This approximation method is not fully developed. The implementation is 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 --------- @@ -71,19 +72,20 @@ aelmp = UnitCommitment.compute_lmp( ) # Access the AELMPs -# Example: "b1" is the bus name, 1 is the first time slot -@show aelmp["b1", 1] +# 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,Int},Float64} +)::OrderedDict{Tuple{String,String,Int},Float64} @info "Building the approximation model..." instance = deepcopy(model[:instance]) _aelmp_check_parameters(instance, model, method) - _modify_instance!(instance, model, method) + _modify_scenario!(instance.scenarios[1], model, method) # prepare the result dictionary and solve the model elmp = OrderedDict() @@ -116,6 +118,13 @@ function _aelmp_check_parameters( 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) @@ -124,7 +133,7 @@ function _aelmp_check_parameters( ) end end - all_units = instance.units + all_units = sc.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( @@ -143,19 +152,19 @@ function _aelmp_check_parameters( end end -function _modify_instance!( - instance::UnitCommitmentInstance, +function _modify_scenario!( + sc::UnitCommitmentScenario, model::JuMP.Model, method::AELMP, ) - # this function modifies the instance units (generators) + # 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 instance.units + for unit in sc.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, instance.time) + 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.units) # unregister from the reserve @@ -167,10 +176,10 @@ function _modify_instance!( end end # unregister the units from the remove list - filter!(x -> !(x.name in units_to_remove), instance.units) + filter!(x -> !(x.name in units_to_remove), sc.units) end - for unit in instance.units + for unit in sc.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 @@ -200,5 +209,5 @@ function _modify_instance!( unit.startup_categories = StartupCategory[StartupCategory(0, first_startup_cost)] end - return instance.units_by_name = Dict(g.name => g for g in instance.units) + return sc.units_by_name = Dict(g.name => g for g in sc.units) end diff --git a/src/lmp/conventional.jl b/src/lmp/conventional.jl index 005cf60..38d07c5 100644 --- a/src/lmp/conventional.jl +++ b/src/lmp/conventional.jl @@ -9,7 +9,7 @@ using JuMP model::JuMP.Model, method::ConventionalLMP; optimizer, - )::OrderedDict{Tuple{String,Int},Float64} + )::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. @@ -55,15 +55,15 @@ lmp = UnitCommitment.compute_lmp( ) # Access the LMPs -# Example: "b1" is the bus name, 1 is the first time slot -@show lmp["b1", 1] +# 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,Int},Float64} +)::OrderedDict{Tuple{String,String,Int},Float64} if !has_values(model) error("The UC model must be solved before calculating the LMPs.") end diff --git a/test/lmp/aelmp_test.jl b/test/lmp/aelmp_test.jl index 29a3194..5f236da 100644 --- a/test/lmp/aelmp_test.jl +++ b/test/lmp/aelmp_test.jl @@ -20,7 +20,7 @@ import UnitCommitment: AELMP # policy 1: allow offlines; consider startups aelmp_1 = UnitCommitment.compute_lmp(model, AELMP(), optimizer = HiGHS.Optimizer) - @test aelmp_1["B1", 1] ≈ 231.7 atol = 0.1 + @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( @@ -31,5 +31,5 @@ import UnitCommitment: AELMP ), optimizer = HiGHS.Optimizer, ) - @test aelmp_2["B1", 1] ≈ 274.3 atol = 0.1 + @test aelmp_2["s1","B1", 1] ≈ 274.3 atol = 0.1 end diff --git a/test/lmp/lmp_test.jl b/test/lmp/conventional_test.jl similarity index 61% rename from test/lmp/lmp_test.jl rename to test/lmp/conventional_test.jl index ddd3411..d78d443 100644 --- a/test/lmp/lmp_test.jl +++ b/test/lmp/conventional_test.jl @@ -5,7 +5,7 @@ using UnitCommitment, Cbc, HiGHS, JuMP import UnitCommitment: ConventionalLMP -function solve_lmp_testcase(path::String) +function solve_conventional_testcase(path::String) instance = UnitCommitment.read(path) model = UnitCommitment.build_model( instance = instance, @@ -22,30 +22,30 @@ function solve_lmp_testcase(path::String) return lmp end -@testset "lmp" begin +@testset "conventional" 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 + 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_lmp_testcase(path) - @test lmp["A", 1] == 50.0 - @test lmp["B", 1] == 60.0 + 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_lmp_testcase(path) - @test lmp["A", 1] == 50.0 - @test lmp["B", 1] == 70.0 - @test lmp["C", 1] == 100.0 + 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_lmp_testcase(path) - @test lmp["A", 1] == 50.0 - @test lmp["B", 1] == 70.0 - @test lmp["C", 1] == 90.0 + 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 diff --git a/test/runtests.jl b/test/runtests.jl index 4203495..08ade97 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -40,7 +40,7 @@ FIXTURES = "$(@__DIR__)/fixtures" include("validation/repair_test.jl") end @testset "lmp" begin - include("lmp/lmp_test.jl") + include("lmp/conventional_test.jl") include("lmp/aelmp_test.jl") end end From b2ed0f67c1e0391657937529d2c92032d33285dc Mon Sep 17 00:00:00 2001 From: Jun He Date: Fri, 31 Mar 2023 15:11:37 -0400 Subject: [PATCH 14/19] Added the profiled units --- src/UnitCommitment.jl | 1 + src/instance/migrate.jl | 12 ++ src/instance/read.jl | 185 ++++++++++++++++----------- src/instance/structs.jl | 11 ++ src/model/build.jl | 3 + src/model/formulations/base/punit.jl | 32 +++++ src/solution/solution.jl | 39 ++++-- test/instance/read_test.jl | 22 +++- 8 files changed, 215 insertions(+), 90 deletions(-) create mode 100644 src/model/formulations/base/punit.jl diff --git a/src/UnitCommitment.jl b/src/UnitCommitment.jl index 5d0bf24..3191395 100644 --- a/src/UnitCommitment.jl +++ b/src/UnitCommitment.jl @@ -32,6 +32,7 @@ include("model/formulations/base/psload.jl") include("model/formulations/base/sensitivity.jl") include("model/formulations/base/system.jl") include("model/formulations/base/unit.jl") +include("model/formulations/base/punit.jl") include("model/formulations/CarArr2006/pwlcosts.jl") include("model/formulations/DamKucRajAta2016/ramp.jl") include("model/formulations/Gar1962/pwlcosts.jl") diff --git a/src/instance/migrate.jl b/src/instance/migrate.jl index becb912..317a309 100644 --- a/src/instance/migrate.jl +++ b/src/instance/migrate.jl @@ -17,6 +17,7 @@ function _migrate(json) end version = VersionNumber(version) version >= v"0.3" || _migrate_to_v03(json) + version >= v"0.4" || _migrate_to_v04(json) return end @@ -36,3 +37,14 @@ function _migrate_to_v03(json) end end end + +function _migrate_to_v04(json) + # Migrate thermal units + if json["Generators"] !== nothing + for (gen_name, gen) in json["Generators"] + if gen["Type"] === nothing + gen["Type"] = "Thermal" + end + end + end +end diff --git a/src/instance/read.jl b/src/instance/read.jl index b823dc1..635be8d 100644 --- a/src/instance/read.jl +++ b/src/instance/read.jl @@ -135,6 +135,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario lines = TransmissionLine[] loads = PriceSensitiveLoad[] reserves = Reserve[] + profiled_units = ProfiledUnit[] function scalar(x; default = nothing) x !== nothing || return default @@ -182,6 +183,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario timeseries(dict["Load (MW)"]), Unit[], PriceSensitiveLoad[], + ProfiledUnit[], ) name_to_bus[bus_name] = bus push!(buses, bus) @@ -207,90 +209,119 @@ function _from_json(json; repair = true)::UnitCommitmentScenario # Read units for (unit_name, dict) in json["Generators"] + # Read and validate unit type + unit_type = scalar(dict["Type"], default = nothing) + unit_type !== nothing || error("unit $unit_name has no type specified") bus = name_to_bus[dict["Bus"]] - # Read production cost curve - K = length(dict["Production cost curve (MW)"]) - curve_mw = hcat( - [timeseries(dict["Production cost curve (MW)"][k]) for k in 1:K]..., - ) - curve_cost = hcat( - [timeseries(dict["Production cost curve (\$)"][k]) for k in 1:K]..., - ) - min_power = curve_mw[:, 1] - max_power = curve_mw[:, K] - min_power_cost = curve_cost[:, 1] - segments = CostSegment[] - for k in 2:K - amount = curve_mw[:, k] - curve_mw[:, k-1] - cost = (curve_cost[:, k] - curve_cost[:, k-1]) ./ amount - replace!(cost, NaN => 0.0) - push!(segments, CostSegment(amount, cost)) - end - - # Read startup costs - startup_delays = scalar(dict["Startup delays (h)"], default = [1]) - startup_costs = scalar(dict["Startup costs (\$)"], default = [0.0]) - startup_categories = StartupCategory[] - for k in 1:length(startup_delays) - push!( - startup_categories, - StartupCategory( - startup_delays[k] .* time_multiplier, - startup_costs[k], - ), + if lowercase(unit_type) === "thermal" + # Read production cost curve + K = length(dict["Production cost curve (MW)"]) + curve_mw = hcat( + [ + timeseries(dict["Production cost curve (MW)"][k]) for + k in 1:K + ]..., ) - end + curve_cost = hcat( + [ + timeseries(dict["Production cost curve (\$)"][k]) for + k in 1:K + ]..., + ) + min_power = curve_mw[:, 1] + max_power = curve_mw[:, K] + min_power_cost = curve_cost[:, 1] + segments = CostSegment[] + for k in 2:K + amount = curve_mw[:, k] - curve_mw[:, k-1] + cost = (curve_cost[:, k] - curve_cost[:, k-1]) ./ amount + replace!(cost, NaN => 0.0) + push!(segments, CostSegment(amount, cost)) + end - # Read reserve eligibility - unit_reserves = Reserve[] - if "Reserve eligibility" in keys(dict) - unit_reserves = - [name_to_reserve[n] for n in dict["Reserve eligibility"]] - end + # Read startup costs + startup_delays = scalar(dict["Startup delays (h)"], default = [1]) + startup_costs = scalar(dict["Startup costs (\$)"], default = [0.0]) + startup_categories = StartupCategory[] + for k in 1:length(startup_delays) + push!( + startup_categories, + StartupCategory( + startup_delays[k] .* time_multiplier, + startup_costs[k], + ), + ) + end - # Read and validate initial conditions - initial_power = scalar(dict["Initial power (MW)"], default = nothing) - initial_status = scalar(dict["Initial status (h)"], default = nothing) - if initial_power === nothing - initial_status === nothing || - error("unit $unit_name has initial status but no initial power") - else - initial_status !== nothing || - error("unit $unit_name has initial power but no initial status") - initial_status != 0 || - error("unit $unit_name has invalid initial status") - if initial_status < 0 && initial_power > 1e-3 - error("unit $unit_name has invalid initial power") + # Read reserve eligibility + unit_reserves = Reserve[] + if "Reserve eligibility" in keys(dict) + unit_reserves = + [name_to_reserve[n] for n in dict["Reserve eligibility"]] end - initial_status *= time_multiplier - end - unit = Unit( - unit_name, - bus, - max_power, - min_power, - timeseries(dict["Must run?"], default = [false for t in 1:T]), - min_power_cost, - segments, - scalar(dict["Minimum uptime (h)"], default = 1) * time_multiplier, - scalar(dict["Minimum downtime (h)"], default = 1) * time_multiplier, - scalar(dict["Ramp up limit (MW)"], default = 1e6), - scalar(dict["Ramp down limit (MW)"], default = 1e6), - scalar(dict["Startup limit (MW)"], default = 1e6), - scalar(dict["Shutdown limit (MW)"], default = 1e6), - initial_status, - initial_power, - startup_categories, - unit_reserves, - ) - push!(bus.units, unit) - for r in unit_reserves - push!(r.units, unit) + # Read and validate initial conditions + initial_power = + scalar(dict["Initial power (MW)"], default = nothing) + initial_status = + scalar(dict["Initial status (h)"], default = nothing) + if initial_power === nothing + initial_status === nothing || error( + "unit $unit_name has initial status but no initial power", + ) + else + initial_status !== nothing || error( + "unit $unit_name has initial power but no initial status", + ) + initial_status != 0 || + error("unit $unit_name has invalid initial status") + if initial_status < 0 && initial_power > 1e-3 + error("unit $unit_name has invalid initial power") + end + initial_status *= time_multiplier + end + + unit = Unit( + unit_name, + bus, + max_power, + min_power, + timeseries(dict["Must run?"], default = [false for t in 1:T]), + min_power_cost, + segments, + scalar(dict["Minimum uptime (h)"], default = 1) * + time_multiplier, + scalar(dict["Minimum downtime (h)"], default = 1) * + time_multiplier, + scalar(dict["Ramp up limit (MW)"], default = 1e6), + scalar(dict["Ramp down limit (MW)"], default = 1e6), + scalar(dict["Startup limit (MW)"], default = 1e6), + scalar(dict["Shutdown limit (MW)"], default = 1e6), + initial_status, + initial_power, + startup_categories, + unit_reserves, + ) + push!(bus.units, unit) + for r in unit_reserves + push!(r.units, unit) + end + name_to_unit[unit_name] = unit + push!(units, unit) + elseif lowercase(unit_type) === "profiled" + bus = name_to_bus[dict["Bus"]] + pu = ProfiledUnit( + unit_name, + bus, + timeseries(dict["Maximum power (MW)"]), + timeseries(dict["Cost (\$/MW)"]), + ) + push!(bus.profiled_units, pu) + push!(profiled_units, pu) + else + error("unit $unit_name has an invalid type") end - name_to_unit[unit_name] = unit - push!(units, unit) end # Read transmission lines @@ -371,6 +402,8 @@ function _from_json(json; repair = true)::UnitCommitmentScenario time = T, units_by_name = Dict(g.name => g for g in units), units = units, + profiled_units_by_name = Dict(pu.name => pu for pu in profiled_units), + profiled_units = profiled_units, isf = spzeros(Float64, length(lines), length(buses) - 1), lodf = spzeros(Float64, length(lines), length(lines)), ) diff --git a/src/instance/structs.jl b/src/instance/structs.jl index 0c72a23..88a64fd 100644 --- a/src/instance/structs.jl +++ b/src/instance/structs.jl @@ -8,6 +8,7 @@ mutable struct Bus load::Vector{Float64} units::Vector price_sensitive_loads::Vector + profiled_units::Vector end mutable struct CostSegment @@ -73,6 +74,13 @@ mutable struct PriceSensitiveLoad revenue::Vector{Float64} end +mutable struct ProfiledUnit + name::String + bus::Bus + capacity::Vector{Float64} + cost::Vector{Float64} +end + Base.@kwdef mutable struct UnitCommitmentScenario buses_by_name::Dict{AbstractString,Bus} buses::Vector{Bus} @@ -92,6 +100,8 @@ Base.@kwdef mutable struct UnitCommitmentScenario time::Int units_by_name::Dict{AbstractString,Unit} units::Vector{Unit} + profiled_units_by_name::Dict{AbstractString,ProfiledUnit} + profiled_units::Vector{ProfiledUnit} end Base.@kwdef mutable struct UnitCommitmentInstance @@ -108,6 +118,7 @@ function Base.show(io::IO, instance::UnitCommitmentInstance) print(io, "$(length(sc.lines)) lines, ") print(io, "$(length(sc.contingencies)) contingencies, ") print(io, "$(length(sc.price_sensitive_loads)) price sensitive loads, ") + print(io, "$(length(sc.profiled_units)) profiled units, ") print(io, "$(instance.time) time steps") print(io, ")") return diff --git a/src/model/build.jl b/src/model/build.jl index fdfc27e..1bd1987 100644 --- a/src/model/build.jl +++ b/src/model/build.jl @@ -96,6 +96,9 @@ function build_model(; for g in sc.units _add_unit_dispatch!(model, g, formulation, sc) end + for pu in sc.profiled_units + _add_profiled_unit!(model, pu, sc) + end _add_system_wide_eqs!(model, sc) end @objective(model, Min, model[:obj]) diff --git a/src/model/formulations/base/punit.jl b/src/model/formulations/base/punit.jl new file mode 100644 index 0000000..ced3ba9 --- /dev/null +++ b/src/model/formulations/base/punit.jl @@ -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 diff --git a/src/solution/solution.jl b/src/solution/solution.jl index b170f30..25666a2 100644 --- a/src/solution/solution.jl +++ b/src/solution/solution.jl @@ -65,19 +65,22 @@ function solution(model::JuMP.Model)::OrderedDict sol = OrderedDict() for sc in instance.scenarios sol[sc.name] = OrderedDict() - sol[sc.name]["Production (MW)"] = - OrderedDict(g.name => production(g, sc) for g in sc.units) - sol[sc.name]["Production cost (\$)"] = - OrderedDict(g.name => production_cost(g, sc) for g in sc.units) - sol[sc.name]["Startup cost (\$)"] = - OrderedDict(g.name => startup_cost(g, sc) for g in sc.units) - sol[sc.name]["Is on"] = timeseries(model[:is_on], sc.units) - sol[sc.name]["Switch on"] = timeseries(model[:switch_on], sc.units) - sol[sc.name]["Switch off"] = timeseries(model[:switch_off], sc.units) - sol[sc.name]["Net injection (MW)"] = - timeseries(model[:net_injection], sc.buses, sc = sc) - sol[sc.name]["Load curtail (MW)"] = - timeseries(model[:curtail], sc.buses, sc = sc) + if !isempty(sc.units) + sol[sc.name]["Production (MW)"] = + OrderedDict(g.name => production(g, sc) for g in sc.units) + sol[sc.name]["Production cost (\$)"] = + OrderedDict(g.name => production_cost(g, sc) for g in sc.units) + sol[sc.name]["Startup cost (\$)"] = + OrderedDict(g.name => startup_cost(g, sc) for g in sc.units) + sol[sc.name]["Is on"] = timeseries(model[:is_on], sc.units) + sol[sc.name]["Switch on"] = timeseries(model[:switch_on], sc.units) + sol[sc.name]["Switch off"] = + timeseries(model[:switch_off], sc.units) + sol[sc.name]["Net injection (MW)"] = + timeseries(model[:net_injection], sc.buses, sc = sc) + sol[sc.name]["Load curtail (MW)"] = + timeseries(model[:curtail], sc.buses, sc = sc) + end if !isempty(sc.lines) sol[sc.name]["Line overflow (MW)"] = timeseries(model[:overflow], sc.lines, sc = sc) @@ -86,6 +89,16 @@ function solution(model::JuMP.Model)::OrderedDict sol[sc.name]["Price-sensitive loads (MW)"] = timeseries(model[:loads], sc.price_sensitive_loads, sc = sc) end + if !isempty(sc.profiled_units) + sol[sc.name]["Profiled production (MW)"] = + timeseries(model[:prod_profiled], sc.profiled_units, sc = sc) + sol[sc.name]["Profiled production cost (\$)"] = OrderedDict( + pu.name => [ + value(model[:prod_profiled][sc.name, pu.name, t]) * + pu.cost[t] for t in 1:instance.time + ] for pu in sc.profiled_units + ) + end sol[sc.name]["Spinning reserve (MW)"] = OrderedDict( r.name => OrderedDict( g.name => [ diff --git a/test/instance/read_test.jl b/test/instance/read_test.jl index 5fa4a93..81e16d1 100644 --- a/test/instance/read_test.jl +++ b/test/instance/read_test.jl @@ -9,7 +9,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip @test repr(instance) == ( "UnitCommitmentInstance(1 scenarios, 6 units, 14 buses, " * - "20 lines, 19 contingencies, 1 price sensitive loads, 4 time steps)" + "20 lines, 19 contingencies, 1 price sensitive loads, 0 profiled units, 4 time steps)" ) @test length(instance.scenarios) == 1 @@ -131,3 +131,23 @@ end @test unit.startup_categories[3].delay == 6 @test unit.initial_status == -200 end + +@testset "read_benchmark profiled-units" begin + instance = UnitCommitment.read("$FIXTURES/case14-profiled.json.gz") + sc = instance.scenarios[1] + @test length(sc.profiled_units) == 2 + + first_pu = sc.profiled_units[1] + @test first_pu.name == "g7" + @test first_pu.bus.name == "b4" + @test first_pu.cost == [100.0 for t in 1:4] + @test first_pu.capacity == [100.0 for t in 1:4] + @test sc.profiled_units_by_name["g7"].name == "g7" + + second_pu = sc.profiled_units[2] + @test second_pu.name == "g8" + @test second_pu.bus.name == "b5" + @test second_pu.cost == [50.0 for t in 1:4] + @test second_pu.capacity == [120.0 for t in 1:4] + @test sc.profiled_units_by_name["g8"].name == "g8" +end From 3564358a6319b2113ba324298d25da44155d7639 Mon Sep 17 00:00:00 2001 From: Jun He Date: Fri, 31 Mar 2023 15:11:47 -0400 Subject: [PATCH 15/19] Re-formatted the codes --- src/lmp/aelmp.jl | 4 +--- test/lmp/aelmp_test.jl | 4 ++-- test/lmp/conventional_test.jl | 12 ++++++------ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/lmp/aelmp.jl b/src/lmp/aelmp.jl index 8b4e44c..71278f1 100644 --- a/src/lmp/aelmp.jl +++ b/src/lmp/aelmp.jl @@ -120,9 +120,7 @@ function _aelmp_check_parameters( ) # CHECK: model cannot have multiple scenarios if length(instance.scenarios) > 1 - error( - "The method does NOT support multiple scenarios.", - ) + error("The method does NOT support multiple scenarios.") end sc = instance.scenarios[1] # CHECK: model must be solved if allow_offline_participation=false diff --git a/test/lmp/aelmp_test.jl b/test/lmp/aelmp_test.jl index 5f236da..484275c 100644 --- a/test/lmp/aelmp_test.jl +++ b/test/lmp/aelmp_test.jl @@ -20,7 +20,7 @@ import UnitCommitment: AELMP # 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 + @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( @@ -31,5 +31,5 @@ import UnitCommitment: AELMP ), optimizer = HiGHS.Optimizer, ) - @test aelmp_2["s1","B1", 1] ≈ 274.3 atol = 0.1 + @test aelmp_2["s1", "B1", 1] ≈ 274.3 atol = 0.1 end diff --git a/test/lmp/conventional_test.jl b/test/lmp/conventional_test.jl index d78d443..80a4ce5 100644 --- a/test/lmp/conventional_test.jl +++ b/test/lmp/conventional_test.jl @@ -38,14 +38,14 @@ end # 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 + @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 + @test lmp["s1", "A", 1] == 50.0 + @test lmp["s1", "B", 1] == 70.0 + @test lmp["s1", "C", 1] == 90.0 end From f2c0388cac749c1927d4e969ae1ddfb5d9699afa Mon Sep 17 00:00:00 2001 From: Jun He Date: Fri, 31 Mar 2023 15:11:59 -0400 Subject: [PATCH 16/19] Updated the docs --- docs/src/format.md | 22 +++++++++++++++++++++- docs/src/model.md | 9 +++++++++ docs/src/usage.md | 2 +- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/docs/src/format.md b/docs/src/format.md index 4efd322..9ae9f60 100644 --- a/docs/src/format.md +++ b/docs/src/format.md @@ -70,11 +70,14 @@ This section describes the characteristics of each bus in the system. ### Generators -This section describes all generators in the system, including thermal units, renewable units and virtual units. +This section describes all generators in the system, including thermal units, renewable units and virtual units. Two types of generators can be specified - thermal units and profiled units. A thermal unit consists of different fields, while a profiled unit is a simple generator with only a production capacity and a per-unit cost. + +#### Thermal Units | Key | Description | Default | Time series? | :------------------------ | :------------------------------------------------| ------- | :-----------: | `Bus` | Identifier of the bus where this generator is located (string). | Required | N +| `Type` | Type of the generator (string). For thermal generators, this must be `Thermal`. | Required | N | `Production cost curve (MW)` and `Production cost curve ($)` | Parameters describing the piecewise-linear production costs. See below for more details. | Required | Y | `Startup costs ($)` and `Startup delays (h)` | Parameters describing how much it costs to start the generator after it has been shut down for a certain amount of time. If `Startup costs ($)` and `Startup delays (h)` are set to `[300.0, 400.0]` and `[1, 4]`, for example, and the generator is shut down at time `00:00` (h:min), then it costs \$300 to start up the generator at any time between `01:00` and `03:59`, and \$400 to start the generator at time `04:00` or any time after that. The number of startup cost points is unlimited, and may be different for each generator. Startup delays must be strictly increasing and the first entry must equal `Minimum downtime (h)`. | `[0.0]` and `[1]` | N | `Minimum uptime (h)` | Minimum amount of time the generator must stay operational after starting up (in hours). For example, if the generator starts up at time `00:00` (h:min) and `Minimum uptime (h)` is set to 4, then the generator can only shut down at time `04:00`. | `1` | N @@ -88,6 +91,15 @@ This section describes all generators in the system, including thermal units, re | `Must run?` | If `true`, the generator should be committed, even if that is not economical (Boolean). | `false` | Y | `Reserve eligibility` | List of reserve products this generator is eligibe to provide. By default, the generator is not eligible to provide any reserves. | `[]` | N +#### Profiled Units + +| Key | Description | Default | Time series? +| :---------------- | :------------------------------------------------ | :------: | :------------: +| `Bus` | Identifier of the bus where this generator is located (string). | Required | N +| `Type` | Type of the generator (string). For profiled generators, this must be `Profiled`. | Required | N +| `Cost ($/MW)` | Cost incurred for serving each MW of power by this generator. | Required | Y +| `Maximum Capacity (MW)` | Maximum amount of power to be supplied by this generator. Any amount lower than this may be supplied. | Required | Y + #### Production costs and limits Production costs are represented as piecewise-linear curves. Figure 1 shows an example cost curve with three segments, where it costs \$1400, \$1600, \$2200 and \$2400 to generate, respectively, 100, 110, 130 and 135 MW of power. To model this generator, `Production cost curve (MW)` should be set to `[100, 110, 130, 135]`, and `Production cost curve ($)` should be set to `[1400, 1600, 2200, 2400]`. @@ -115,6 +127,7 @@ Note that this curve also specifies the production limits. Specifically, the fir "Generators": { "gen1": { "Bus": "b1", + "Type": "Thermal", "Production cost curve (MW)": [100.0, 110.0, 130.0, 135.0], "Production cost curve ($)": [1400.0, 1600.0, 2200.0, 2400.0], "Startup costs ($)": [300.0, 400.0], @@ -132,11 +145,18 @@ Note that this curve also specifies the production limits. Specifically, the fir }, "gen2": { "Bus": "b5", + "Type": "Thermal", "Production cost curve (MW)": [0.0, [10.0, 8.0, 0.0, 3.0]], "Production cost curve ($)": [0.0, 0.0], "Initial status (h)": -100, "Initial power (MW)": 0, "Reserve eligibility": ["r1", "r2"] + }, + "gen3": { + "Bus": "b6", + "Type": "Profiled", + "Maximum power (MW)": 120.0, + "Cost ($/MW)": 100.0 } } } diff --git a/docs/src/model.md b/docs/src/model.md index cb8e15d..d944b07 100644 --- a/docs/src/model.md +++ b/docs/src/model.md @@ -8,6 +8,8 @@ Decision variables ### Generators +#### Thermal Units + Name | Symbol | Description | Unit :-----|:--------:|:-------------|:------: `is_on[g,t]` | $u_{g}(t)$ | True if generator `g` is on at time `t`. | Binary @@ -19,6 +21,13 @@ Name | Symbol | Description | Unit `startup[g,t,s]` | $\delta^s_g(t)$ | True if generator `g` switches on at time `t` incurring start-up costs from start-up category `s`. | Binary +#### Profiled Units + +Name | Symbol | Description | Unit +:-----|:------:|:-------------|:------: +`prod_profiled[s,t]` | $p^{\dagger}_{g}(t)$ | Amount of power produced by profiled unit `g` at time `t`. | MW + + ### Buses Name | Symbol | Description | Unit diff --git a/docs/src/usage.md b/docs/src/usage.md index aababa0..26d1779 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -189,7 +189,7 @@ This method has two configurable parameters: `allow_offline_participation` and ` !!! warning - This approximation method is still under active research, and has several limitations. The implementation provided in the package is based on MISO Phase I only. It only supports fast start resources. More specifically, the minimum up/down time of all generators must be 1, the initial power of all generators must be 0, and the initial status of all generators must be negative. The method does not support time-varying start-up costs. If offline participation is not allowed, AELMPs treats an asset to be offline if it is never on throughout all time periods. + This approximation method is still under active research, and has several limitations. The implementation provided in the package is based on MISO Phase I only. It only supports fast start resources. More specifically, the minimum up/down time of all generators must be 1, the initial power of all generators must be 0, and the initial status of all generators must be negative. The method does not support time-varying start-up costs. The method does not support multiple scenarios. If offline participation is not allowed, AELMPs treats an asset to be offline if it is never on throughout all time periods. ```julia using UnitCommitment From 51f6aa9a80e9affceaa766aee7f8700993889c2b Mon Sep 17 00:00:00 2001 From: Jun He Date: Fri, 31 Mar 2023 15:19:46 -0400 Subject: [PATCH 17/19] Create case14-profiled.json.gz --- test/fixtures/case14-profiled.json.gz | Bin 0 -> 1921 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/fixtures/case14-profiled.json.gz diff --git a/test/fixtures/case14-profiled.json.gz b/test/fixtures/case14-profiled.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..c98d7d5e85add5d38e5aeeee8a922beff2643dff GIT binary patch literal 1921 zcmV-{2Y&b;iwFn>2O(qt17l%xWid1@aB^>EX>4U=E^2dcZUF6C$&TAb5ItwUf)IR| z0CuPMC6~Z)76ga`!($|eAP0_yGedwP1uk|bhW|Z9%_g#oVoA1~1c>Z|-CZiauC98u zQ14F?_$*$fb-K#htZo)xC+|#W~-bDh0!{s;i4` z*#g33Sv764?CQ5!a{BVO9?tIr+5RIqe;O|eY*C?$X0Y}L4b77YFd9W;u~jM2RnV{# z&{><-ZMRM?vLd}{?rsS|f~by{6zNg@!JFwbbIuudYJ;!Kjr`6B|5WB}o)$^drfqkB z#%Iw@Kgg}CcUgV!B9E#+Y`ON8oc2(8Tn|NTpk2HcG-5-&+fe&?C+dX8GRzOPQ;I1B zrc=Q%6^`Vi`V%;$ns7)x?|84%)jEL(6!|J|?{75tv9?>ei|SoDD|rujR_?lMNeW-) zWxncGfU@n``i`Y1gnEs;#L_=+#nQaENqTO-y6GRi5iYx;*eT!iM{96NJc2zbe#f@X znymNgtjMqO^SsF08}!;2wO9-<7bgNWO$6$*6M<6knzaZK%w&a+CITUZV$L88p=@9r zAp+%tV1^d32M|HLDdnHxkK>TV$5nktzG5?d#s2>*-r)SNU$OniuJ{vQ9p5Ca zW42gFz6m#d6ME_T+>Oj^0Xx}( z*Tcw0Q<-yQD)V!dUOZeQ!%umR^vc`ci!W9}6Df>DSV3y0trW=YK`JJLlNiNF7Pt?N z;40@xHNP9G5TIR2i?FKTmWjYsz_tVv3hReMU|M^jDPWO0VK-RgnBvAD)-#HEuO$N2 z2&RNr9@*h=7mAbYisZZ9Py_2z23EE+V;u&^Ghu>8YJX^YrP7iD6XEQc>sL%mC$Za? z%t~VsJg~#1M%_X%FIDgqs0gW%f)Sw{()Yr?rcs6|Yq6VB5UNH8Bvh4P)(P}G5HMO9 zMKO)wdtW1G4j2L(nQ?foF$ki-td$z6oB}gkXhA|cL#Z)R4U}Rz;#Gm4R8r;86w3S63YF_8wlEKG)4`RzULw{Xk@+aebUr*Od<469)w09&FvPVs51gIeHCotDiiZ~E6fNs*W1!k|FB z_^j&cWtN;zzO_l~tE_$4k7nktvvk>}C8#*9u2;<+HJKnBfBcN61!rBe%+_~bJL|tA zA)N;6Eq6{qeDHCeUi?z^1$S~;RPTm!4;4>v7T>QjKwU0x5P6Nr|L`ca&dRiCZw58t z6xS9=xb11rsCq>0DW&3i%bs%9p5TPiK+FWA6a$W@^F55s#vl9ExtGgSO5nXK~a>$u&Y9x08q1 znrkWMsZ9&$6l~x&80VEV4hN0YdwYU*G%n#mi17HKge6c2x5mO>5j5BM0g%FT&jSR~ z0Sus+_izP$n+MR^dfvOboiICAgN|Uw9sUq@H0#CfZY1B&?v@2 z3EcA;a~3x-WkL-(?<3IHs^EZEw1uLne@Q=qdp>DK^fAI64MV4rrZBUxLqF;1{mBE9 zn`zYZS#u6GjZ=i66sb^*dka;SlaOSUw0g?9=hJ2cYwRSPmVx}ZMZ;=q&VUD<2j&Lk zTud?HeA=8vP0RKGVmvxPLlXLA4$Z!mzRy{HBJYU#%o$trPzIFaS0Jc!a}O=MGEQ3f zGvy@n`7&m>1F0kgLQM$Ft%UawjY2~IHQ^<4K3~SD4s#QwJ)zSv{Ro;vStTJLm6BUO z>>GB+FRHT5%f2nl$Bpq4$)V3LFSBKvT|6|>$a{@~^j>3$uXqcqklC&dqNz(2}+7VJCY4;@U2=|b*2ac2(V8rbRJ78NS7M^5NufUKpTi Hf-wL9B*&aE literal 0 HcmV?d00001 From 19534a128fa0f918ae450a77a2a8cab25472b304 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 4 Apr 2023 15:40:44 -0500 Subject: [PATCH 18/19] Rename Unit to ThermalUnit --- src/instance/read.jl | 22 +++++++------- src/instance/structs.jl | 20 ++++++------- src/lmp/aelmp.jl | 15 +++++----- src/model/build.jl | 4 +-- src/model/formulations/ArrCon2000/ramp.jl | 2 +- src/model/formulations/CarArr2006/pwlcosts.jl | 2 +- .../formulations/DamKucRajAta2016/ramp.jl | 2 +- src/model/formulations/Gar1962/prod.jl | 4 +-- src/model/formulations/Gar1962/pwlcosts.jl | 2 +- src/model/formulations/Gar1962/status.jl | 4 +-- .../formulations/KnuOstWat2018/pwlcosts.jl | 2 +- src/model/formulations/MorLatRam2013/ramp.jl | 2 +- .../formulations/MorLatRam2013/scosts.jl | 2 +- src/model/formulations/PanGua2016/ramp.jl | 2 +- src/model/formulations/WanHob2016/ramp.jl | 2 +- src/model/formulations/base/system.jl | 6 ++-- src/model/formulations/base/unit.jl | 23 +++++++------- src/solution/fix.jl | 4 +-- src/solution/solution.jl | 30 +++++++++++-------- src/solution/warmstart.jl | 2 +- src/transform/initcond.jl | 2 +- src/transform/randomize/XavQiuAhm2021.jl | 4 +-- src/transform/slice.jl | 2 +- src/validation/repair.jl | 2 +- src/validation/validate.jl | 12 ++++---- test/instance/migrate_test.jl | 4 +-- test/instance/read_test.jl | 18 +++++------ test/transform/initcond_test.jl | 4 +-- .../transform/randomize/XavQiuAhm2021_test.jl | 2 +- test/transform/slice_test.jl | 2 +- 30 files changed, 106 insertions(+), 98 deletions(-) diff --git a/src/instance/read.jl b/src/instance/read.jl index 635be8d..b095b0a 100644 --- a/src/instance/read.jl +++ b/src/instance/read.jl @@ -129,7 +129,7 @@ end function _from_json(json; repair = true)::UnitCommitmentScenario _migrate(json) - units = Unit[] + thermal_units = ThermalUnit[] buses = Bus[] contingencies = Contingency[] lines = TransmissionLine[] @@ -160,7 +160,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario name_to_bus = Dict{String,Bus}() name_to_line = Dict{String,TransmissionLine}() - name_to_unit = Dict{String,Unit}() + name_to_unit = Dict{String,ThermalUnit}() name_to_reserve = Dict{String,Reserve}() function timeseries(x; default = nothing) @@ -181,7 +181,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario bus_name, length(buses), timeseries(dict["Load (MW)"]), - Unit[], + ThermalUnit[], PriceSensitiveLoad[], ProfiledUnit[], ) @@ -196,7 +196,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario name = reserve_name, type = lowercase(dict["Type"]), amount = timeseries(dict["Amount (MW)"]), - units = [], + thermal_units = [], shortfall_penalty = scalar( dict["Shortfall penalty (\$/MW)"], default = -1, @@ -282,7 +282,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario initial_status *= time_multiplier end - unit = Unit( + unit = ThermalUnit( unit_name, bus, max_power, @@ -303,12 +303,12 @@ function _from_json(json; repair = true)::UnitCommitmentScenario startup_categories, unit_reserves, ) - push!(bus.units, unit) + push!(bus.thermal_units, unit) for r in unit_reserves - push!(r.units, unit) + push!(r.thermal_units, unit) end name_to_unit[unit_name] = unit - push!(units, unit) + push!(thermal_units, unit) elseif lowercase(unit_type) === "profiled" bus = name_to_bus[dict["Bus"]] pu = ProfiledUnit( @@ -355,7 +355,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario # Read contingencies if "Contingencies" in keys(json) for (cont_name, dict) in json["Contingencies"] - affected_units = Unit[] + affected_units = ThermalUnit[] affected_lines = TransmissionLine[] if "Affected lines" in keys(dict) affected_lines = @@ -400,8 +400,8 @@ function _from_json(json; repair = true)::UnitCommitmentScenario reserves = reserves, reserves_by_name = name_to_reserve, time = T, - units_by_name = Dict(g.name => g for g in units), - units = units, + thermal_units_by_name = Dict(g.name => g for g in thermal_units), + thermal_units = thermal_units, profiled_units_by_name = Dict(pu.name => pu for pu in profiled_units), profiled_units = profiled_units, isf = spzeros(Float64, length(lines), length(buses) - 1), diff --git a/src/instance/structs.jl b/src/instance/structs.jl index 88a64fd..9d6cc5c 100644 --- a/src/instance/structs.jl +++ b/src/instance/structs.jl @@ -6,7 +6,7 @@ mutable struct Bus name::String offset::Int load::Vector{Float64} - units::Vector + thermal_units::Vector price_sensitive_loads::Vector profiled_units::Vector end @@ -25,11 +25,11 @@ Base.@kwdef mutable struct Reserve name::String type::String amount::Vector{Float64} - units::Vector + thermal_units::Vector shortfall_penalty::Float64 end -mutable struct Unit +mutable struct ThermalUnit name::String bus::Bus max_power::Vector{Float64} @@ -64,7 +64,7 @@ end mutable struct Contingency name::String lines::Vector{TransmissionLine} - units::Vector{Unit} + thermal_units::Vector{ThermalUnit} end mutable struct PriceSensitiveLoad @@ -95,13 +95,13 @@ Base.@kwdef mutable struct UnitCommitmentScenario price_sensitive_loads_by_name::Dict{AbstractString,PriceSensitiveLoad} price_sensitive_loads::Vector{PriceSensitiveLoad} probability::Float64 + profiled_units_by_name::Dict{AbstractString,ProfiledUnit} + profiled_units::Vector{ProfiledUnit} reserves_by_name::Dict{AbstractString,Reserve} reserves::Vector{Reserve} + thermal_units_by_name::Dict{AbstractString,ThermalUnit} + thermal_units::Vector{ThermalUnit} time::Int - units_by_name::Dict{AbstractString,Unit} - units::Vector{Unit} - profiled_units_by_name::Dict{AbstractString,ProfiledUnit} - profiled_units::Vector{ProfiledUnit} end Base.@kwdef mutable struct UnitCommitmentInstance @@ -113,12 +113,12 @@ function Base.show(io::IO, instance::UnitCommitmentInstance) sc = instance.scenarios[1] print(io, "UnitCommitmentInstance(") print(io, "$(length(instance.scenarios)) scenarios, ") - print(io, "$(length(sc.units)) units, ") + print(io, "$(length(sc.thermal_units)) thermal units, ") + print(io, "$(length(sc.profiled_units)) profiled units, ") print(io, "$(length(sc.buses)) buses, ") print(io, "$(length(sc.lines)) lines, ") print(io, "$(length(sc.contingencies)) contingencies, ") print(io, "$(length(sc.price_sensitive_loads)) price sensitive loads, ") - print(io, "$(length(sc.profiled_units)) profiled units, ") print(io, "$(instance.time) time steps") print(io, ")") return diff --git a/src/lmp/aelmp.jl b/src/lmp/aelmp.jl index 71278f1..7afd350 100644 --- a/src/lmp/aelmp.jl +++ b/src/lmp/aelmp.jl @@ -131,7 +131,7 @@ function _aelmp_check_parameters( ) end end - all_units = sc.units + 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( @@ -159,25 +159,25 @@ function _modify_scenario!( if !method.allow_offline_participation # 1. remove (if NOT allowing) the offline generators units_to_remove = [] - for unit in sc.units + 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.units) + 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.units) + 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.units) + filter!(x -> !(x.name in units_to_remove), sc.thermal_units) end - for unit in sc.units + 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 @@ -207,5 +207,6 @@ function _modify_scenario!( unit.startup_categories = StartupCategory[StartupCategory(0, first_startup_cost)] end - return sc.units_by_name = Dict(g.name => g for g in sc.units) + return sc.thermal_units_by_name = + Dict(g.name => g for g in sc.thermal_units) end diff --git a/src/model/build.jl b/src/model/build.jl index 1bd1987..a812011 100644 --- a/src/model/build.jl +++ b/src/model/build.jl @@ -77,7 +77,7 @@ function build_model(; end model[:obj] = AffExpr() model[:instance] = instance - for g in instance.scenarios[1].units + for g in instance.scenarios[1].thermal_units _add_unit_commitment!(model, g, formulation) end for sc in instance.scenarios @@ -93,7 +93,7 @@ function build_model(; for ps in sc.price_sensitive_loads _add_price_sensitive_load!(model, ps, sc) end - for g in sc.units + for g in sc.thermal_units _add_unit_dispatch!(model, g, formulation, sc) end for pu in sc.profiled_units diff --git a/src/model/formulations/ArrCon2000/ramp.jl b/src/model/formulations/ArrCon2000/ramp.jl index a043311..3bd48b5 100644 --- a/src/model/formulations/ArrCon2000/ramp.jl +++ b/src/model/formulations/ArrCon2000/ramp.jl @@ -4,7 +4,7 @@ function _add_ramp_eqs!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, formulation_prod_vars::Gar1962.ProdVars, formulation_ramping::ArrCon2000.Ramping, formulation_status_vars::Gar1962.StatusVars, diff --git a/src/model/formulations/CarArr2006/pwlcosts.jl b/src/model/formulations/CarArr2006/pwlcosts.jl index 9b236f7..2e133f3 100644 --- a/src/model/formulations/CarArr2006/pwlcosts.jl +++ b/src/model/formulations/CarArr2006/pwlcosts.jl @@ -4,7 +4,7 @@ function _add_production_piecewise_linear_eqs!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, formulation_prod_vars::Gar1962.ProdVars, formulation_pwl_costs::CarArr2006.PwlCosts, formulation_status_vars::StatusVarsFormulation, diff --git a/src/model/formulations/DamKucRajAta2016/ramp.jl b/src/model/formulations/DamKucRajAta2016/ramp.jl index fa380c3..304430c 100644 --- a/src/model/formulations/DamKucRajAta2016/ramp.jl +++ b/src/model/formulations/DamKucRajAta2016/ramp.jl @@ -4,7 +4,7 @@ function _add_ramp_eqs!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, formulation_prod_vars::Gar1962.ProdVars, formulation_ramping::DamKucRajAta2016.Ramping, formulation_status_vars::Gar1962.StatusVars, diff --git a/src/model/formulations/Gar1962/prod.jl b/src/model/formulations/Gar1962/prod.jl index 3e70a04..c7b8308 100644 --- a/src/model/formulations/Gar1962/prod.jl +++ b/src/model/formulations/Gar1962/prod.jl @@ -4,7 +4,7 @@ function _add_production_vars!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, formulation_prod_vars::Gar1962.ProdVars, sc::UnitCommitmentScenario, )::Nothing @@ -21,7 +21,7 @@ end function _add_production_limit_eqs!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, formulation_prod_vars::Gar1962.ProdVars, sc::UnitCommitmentScenario, )::Nothing diff --git a/src/model/formulations/Gar1962/pwlcosts.jl b/src/model/formulations/Gar1962/pwlcosts.jl index 62d0b5c..94ac942 100644 --- a/src/model/formulations/Gar1962/pwlcosts.jl +++ b/src/model/formulations/Gar1962/pwlcosts.jl @@ -4,7 +4,7 @@ function _add_production_piecewise_linear_eqs!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, formulation_prod_vars::Gar1962.ProdVars, formulation_pwl_costs::Gar1962.PwlCosts, formulation_status_vars::Gar1962.StatusVars, diff --git a/src/model/formulations/Gar1962/status.jl b/src/model/formulations/Gar1962/status.jl index 2a4a911..ea57258 100644 --- a/src/model/formulations/Gar1962/status.jl +++ b/src/model/formulations/Gar1962/status.jl @@ -4,7 +4,7 @@ function _add_status_vars!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, formulation_status_vars::Gar1962.StatusVars, )::Nothing is_on = _init(model, :is_on) @@ -27,7 +27,7 @@ end function _add_status_eqs!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, formulation_status_vars::Gar1962.StatusVars, )::Nothing eq_binary_link = _init(model, :eq_binary_link) diff --git a/src/model/formulations/KnuOstWat2018/pwlcosts.jl b/src/model/formulations/KnuOstWat2018/pwlcosts.jl index 0da8217..d3d4bdb 100644 --- a/src/model/formulations/KnuOstWat2018/pwlcosts.jl +++ b/src/model/formulations/KnuOstWat2018/pwlcosts.jl @@ -4,7 +4,7 @@ function _add_production_piecewise_linear_eqs!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, formulation_prod_vars::Gar1962.ProdVars, formulation_pwl_costs::KnuOstWat2018.PwlCosts, formulation_status_vars::Gar1962.StatusVars, diff --git a/src/model/formulations/MorLatRam2013/ramp.jl b/src/model/formulations/MorLatRam2013/ramp.jl index 90afd71..29d56e3 100644 --- a/src/model/formulations/MorLatRam2013/ramp.jl +++ b/src/model/formulations/MorLatRam2013/ramp.jl @@ -4,7 +4,7 @@ function _add_ramp_eqs!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, formulation_prod_vars::Gar1962.ProdVars, formulation_ramping::MorLatRam2013.Ramping, formulation_status_vars::Gar1962.StatusVars, diff --git a/src/model/formulations/MorLatRam2013/scosts.jl b/src/model/formulations/MorLatRam2013/scosts.jl index 2d68747..ce565c7 100644 --- a/src/model/formulations/MorLatRam2013/scosts.jl +++ b/src/model/formulations/MorLatRam2013/scosts.jl @@ -4,7 +4,7 @@ function _add_startup_cost_eqs!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, formulation::MorLatRam2013.StartupCosts, )::Nothing eq_startup_choose = _init(model, :eq_startup_choose) diff --git a/src/model/formulations/PanGua2016/ramp.jl b/src/model/formulations/PanGua2016/ramp.jl index 62a83ba..511f91f 100644 --- a/src/model/formulations/PanGua2016/ramp.jl +++ b/src/model/formulations/PanGua2016/ramp.jl @@ -4,7 +4,7 @@ function _add_ramp_eqs!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, formulation_prod_vars::Gar1962.ProdVars, formulation_ramping::PanGua2016.Ramping, formulation_status_vars::Gar1962.StatusVars, diff --git a/src/model/formulations/WanHob2016/ramp.jl b/src/model/formulations/WanHob2016/ramp.jl index 417d2a3..e634fc2 100644 --- a/src/model/formulations/WanHob2016/ramp.jl +++ b/src/model/formulations/WanHob2016/ramp.jl @@ -4,7 +4,7 @@ function _add_ramp_eqs!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, ::Gar1962.ProdVars, ::WanHob2016.Ramping, ::Gar1962.StatusVars, diff --git a/src/model/formulations/base/system.jl b/src/model/formulations/base/system.jl index 1591db4..7291648 100644 --- a/src/model/formulations/base/system.jl +++ b/src/model/formulations/base/system.jl @@ -53,7 +53,7 @@ function _add_spinning_reserve_eqs!( model, sum( model[:reserve][sc.name, r.name, g.name, t] for - g in r.units + g in r.thermal_units ) + model[:reserve_shortfall][sc.name, r.name, t] >= r.amount[t] ) @@ -91,7 +91,7 @@ function _add_flexiramp_reserve_eqs!( model, sum( model[:upflexiramp][sc.name, r.name, g.name, t] for - g in r.units + g in r.thermal_units ) + model[:upflexiramp_shortfall][sc.name, r.name, t] >= r.amount[t] ) @@ -100,7 +100,7 @@ function _add_flexiramp_reserve_eqs!( model, sum( model[:dwflexiramp][sc.name, r.name, g.name, t] for - g in r.units + g in r.thermal_units ) + model[:dwflexiramp_shortfall][sc.name, r.name, t] >= r.amount[t] ) diff --git a/src/model/formulations/base/unit.jl b/src/model/formulations/base/unit.jl index 27cbba6..06b18a2 100644 --- a/src/model/formulations/base/unit.jl +++ b/src/model/formulations/base/unit.jl @@ -6,7 +6,7 @@ # related to the binary commitment, startup and shutdown decisions of units function _add_unit_commitment!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, formulation::Formulation, ) if !all(g.must_run) && any(g.must_run) @@ -31,7 +31,7 @@ end # related to the continuous dispatch decisions of units function _add_unit_dispatch!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, formulation::Formulation, sc::UnitCommitmentScenario, ) @@ -64,11 +64,11 @@ function _add_unit_dispatch!( return end -_is_initially_on(g::Unit)::Float64 = (g.initial_status > 0 ? 1.0 : 0.0) +_is_initially_on(g::ThermalUnit)::Float64 = (g.initial_status > 0 ? 1.0 : 0.0) function _add_spinning_reserve_vars!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, sc::UnitCommitmentScenario, )::Nothing reserve = _init(model, :reserve) @@ -92,7 +92,7 @@ end function _add_flexiramp_reserve_vars!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, sc::UnitCommitmentScenario, )::Nothing upflexiramp = _init(model, :upflexiramp) @@ -128,7 +128,7 @@ function _add_flexiramp_reserve_vars!( return end -function _add_startup_shutdown_vars!(model::JuMP.Model, g::Unit)::Nothing +function _add_startup_shutdown_vars!(model::JuMP.Model, g::ThermalUnit)::Nothing startup = _init(model, :startup) for t in 1:model[:instance].time for s in 1:length(g.startup_categories) @@ -140,7 +140,7 @@ end function _add_startup_shutdown_limit_eqs!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, sc::UnitCommitmentScenario, )::Nothing eq_shutdown_limit = _init(model, :eq_shutdown_limit) @@ -179,7 +179,7 @@ end function _add_ramp_eqs!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, formulation::RampingFormulation, sc::UnitCommitmentScenario, )::Nothing @@ -224,7 +224,10 @@ function _add_ramp_eqs!( end end -function _add_min_uptime_downtime_eqs!(model::JuMP.Model, g::Unit)::Nothing +function _add_min_uptime_downtime_eqs!( + model::JuMP.Model, + g::ThermalUnit, +)::Nothing is_on = model[:is_on] switch_off = model[:switch_off] switch_on = model[:switch_on] @@ -269,7 +272,7 @@ end function _add_net_injection_eqs!( model::JuMP.Model, - g::Unit, + g::ThermalUnit, sc::UnitCommitmentScenario, )::Nothing expr_net_injection = model[:expr_net_injection] diff --git a/src/solution/fix.jl b/src/solution/fix.jl index ca7703d..3ce6170 100644 --- a/src/solution/fix.jl +++ b/src/solution/fix.jl @@ -16,7 +16,7 @@ function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing prod_above = model[:prod_above] reserve = model[:reserve] for sc in instance.scenarios - for g in sc.units + for g in sc.thermal_units for t in 1:T is_on_value = round(solution[sc.name]["Is on"][g.name][t]) prod_value = round( @@ -33,7 +33,7 @@ function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing end for r in sc.reserves r.type == "spinning" || continue - for g in r.units + for g in r.thermal_units for t in 1:T reserve_value = round( solution[sc.name]["Spinning reserve (MW)"][r.name][g.name][t], diff --git a/src/solution/solution.jl b/src/solution/solution.jl index 25666a2..3ee9dbc 100644 --- a/src/solution/solution.jl +++ b/src/solution/solution.jl @@ -65,17 +65,21 @@ function solution(model::JuMP.Model)::OrderedDict sol = OrderedDict() for sc in instance.scenarios sol[sc.name] = OrderedDict() - if !isempty(sc.units) - sol[sc.name]["Production (MW)"] = - OrderedDict(g.name => production(g, sc) for g in sc.units) - sol[sc.name]["Production cost (\$)"] = - OrderedDict(g.name => production_cost(g, sc) for g in sc.units) - sol[sc.name]["Startup cost (\$)"] = - OrderedDict(g.name => startup_cost(g, sc) for g in sc.units) - sol[sc.name]["Is on"] = timeseries(model[:is_on], sc.units) - sol[sc.name]["Switch on"] = timeseries(model[:switch_on], sc.units) + if !isempty(sc.thermal_units) + sol[sc.name]["Production (MW)"] = OrderedDict( + g.name => production(g, sc) for g in sc.thermal_units + ) + sol[sc.name]["Production cost (\$)"] = OrderedDict( + g.name => production_cost(g, sc) for g in sc.thermal_units + ) + sol[sc.name]["Startup cost (\$)"] = OrderedDict( + g.name => startup_cost(g, sc) for g in sc.thermal_units + ) + sol[sc.name]["Is on"] = timeseries(model[:is_on], sc.thermal_units) + sol[sc.name]["Switch on"] = + timeseries(model[:switch_on], sc.thermal_units) sol[sc.name]["Switch off"] = - timeseries(model[:switch_off], sc.units) + timeseries(model[:switch_off], sc.thermal_units) sol[sc.name]["Net injection (MW)"] = timeseries(model[:net_injection], sc.buses, sc = sc) sol[sc.name]["Load curtail (MW)"] = @@ -103,7 +107,7 @@ function solution(model::JuMP.Model)::OrderedDict r.name => OrderedDict( g.name => [ value(model[:reserve][sc.name, r.name, g.name, t]) for t in 1:instance.time - ] for g in r.units + ] for g in r.thermal_units ) for r in sc.reserves if r.type == "spinning" ) sol[sc.name]["Spinning reserve shortfall (MW)"] = OrderedDict( @@ -116,7 +120,7 @@ function solution(model::JuMP.Model)::OrderedDict r.name => OrderedDict( g.name => [ value(model[:upflexiramp][sc.name, r.name, g.name, t]) for t in 1:instance.time - ] for g in r.units + ] for g in r.thermal_units ) for r in sc.reserves if r.type == "flexiramp" ) sol[sc.name]["Up-flexiramp shortfall (MW)"] = OrderedDict( @@ -128,7 +132,7 @@ function solution(model::JuMP.Model)::OrderedDict r.name => OrderedDict( g.name => [ value(model[:dwflexiramp][sc.name, r.name, g.name, t]) for t in 1:instance.time - ] for g in r.units + ] for g in r.thermal_units ) for r in sc.reserves if r.type == "flexiramp" ) sol[sc.name]["Down-flexiramp shortfall (MW)"] = OrderedDict( diff --git a/src/solution/warmstart.jl b/src/solution/warmstart.jl index 678f250..c9fba50 100644 --- a/src/solution/warmstart.jl +++ b/src/solution/warmstart.jl @@ -5,7 +5,7 @@ function set_warm_start!(model::JuMP.Model, solution::AbstractDict)::Nothing instance, T = model[:instance], model[:instance].time is_on = model[:is_on] - for g in instance.units + for g in instance.thermal_units for t in 1:T JuMP.set_start_value(is_on[g.name, t], solution["Is on"][g.name][t]) JuMP.set_start_value( diff --git a/src/transform/initcond.jl b/src/transform/initcond.jl index cfbce02..697d4fd 100644 --- a/src/transform/initcond.jl +++ b/src/transform/initcond.jl @@ -15,7 +15,7 @@ function generate_initial_conditions!( sc::UnitCommitmentScenario, optimizer, )::Nothing - G = sc.units + G = sc.thermal_units B = sc.buses t = 1 mip = JuMP.Model(optimizer) diff --git a/src/transform/randomize/XavQiuAhm2021.jl b/src/transform/randomize/XavQiuAhm2021.jl index b60cafd..630c7f0 100644 --- a/src/transform/randomize/XavQiuAhm2021.jl +++ b/src/transform/randomize/XavQiuAhm2021.jl @@ -123,7 +123,7 @@ function _randomize_costs( sc::UnitCommitmentScenario, distribution, )::Nothing - for unit in sc.units + for unit in sc.thermal_units α = rand(rng, distribution) unit.min_power_cost *= α for k in unit.cost_segments @@ -168,7 +168,7 @@ function _randomize_load_profile( ) push!(system_load, system_load[t-1] * gamma) end - capacity = sum(maximum(u.max_power) for u in sc.units) + capacity = sum(maximum(u.max_power) for u in sc.thermal_units) peak_load = rand(rng, params.peak_load) * capacity system_load = system_load ./ maximum(system_load) .* peak_load diff --git a/src/transform/slice.jl b/src/transform/slice.jl index e1c6799..051e105 100644 --- a/src/transform/slice.jl +++ b/src/transform/slice.jl @@ -29,7 +29,7 @@ function slice( for r in sc.reserves r.amount = r.amount[range] end - for u in sc.units + for u in sc.thermal_units u.max_power = u.max_power[range] u.min_power = u.min_power[range] u.must_run = u.must_run[range] diff --git a/src/validation/repair.jl b/src/validation/repair.jl index 50ee614..440854e 100644 --- a/src/validation/repair.jl +++ b/src/validation/repair.jl @@ -15,7 +15,7 @@ Returns the number of validation errors found. function repair!(sc::UnitCommitmentScenario)::Int n_errors = 0 - for g in sc.units + for g in sc.thermal_units # Startup costs and delays must be increasing for s in 2:length(g.startup_categories) diff --git a/src/validation/validate.jl b/src/validation/validate.jl index be8234d..88ee36f 100644 --- a/src/validation/validate.jl +++ b/src/validation/validate.jl @@ -45,7 +45,7 @@ end function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01) err_count = 0 for sc in instance.scenarios - for unit in sc.units + for unit in sc.thermal_units production = solution[sc.name]["Production (MW)"][unit.name] reserve = [0.0 for _ in 1:instance.time] spinning_reserves = @@ -114,7 +114,7 @@ function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01) # Verify reserve eligibility for r in sc.reserves if r.type == "spinning" - if unit ∉ r.units && ( + if unit ∉ r.thermal_units && ( unit in keys( solution[sc.name]["Spinning reserve (MW)"][r.name], ) @@ -324,7 +324,7 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01) end production = sum( solution[sc.name]["Production (MW)"][g.name][t] for - g in sc.units + g in sc.thermal_units ) if "Load curtail (MW)" in keys(solution) load_curtail = sum( @@ -352,7 +352,7 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01) if r.type == "spinning" provided = sum( solution[sc.name]["Spinning reserve (MW)"][r.name][g.name][t] - for g in r.units + for g in r.thermal_units ) shortfall = solution[sc.name]["Spinning reserve shortfall (MW)"][r.name][t] @@ -371,7 +371,7 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01) elseif r.type == "flexiramp" upflexiramp = sum( solution[sc.name]["Up-flexiramp (MW)"][r.name][g.name][t] - for g in r.units + for g in r.thermal_units ) upflexiramp_shortfall = solution[sc.name]["Up-flexiramp shortfall (MW)"][r.name][t] @@ -389,7 +389,7 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01) dwflexiramp = sum( solution[sc.name]["Down-flexiramp (MW)"][r.name][g.name][t] - for g in r.units + for g in r.thermal_units ) dwflexiramp_shortfall = solution[sc.name]["Down-flexiramp shortfall (MW)"][r.name][t] diff --git a/test/instance/migrate_test.jl b/test/instance/migrate_test.jl index 3fe1e09..1176a01 100644 --- a/test/instance/migrate_test.jl +++ b/test/instance/migrate_test.jl @@ -9,14 +9,14 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip @test length(instance.scenarios) == 1 sc = instance.scenarios[1] @test length(sc.reserves_by_name["r1"].amount) == 4 - @test sc.units_by_name["g2"].reserves[1].name == "r1" + @test sc.thermal_units_by_name["g2"].reserves[1].name == "r1" end @testset "read v0.3" begin instance = UnitCommitment.read("$FIXTURES/ucjl-0.3.json.gz") @test length(instance.scenarios) == 1 sc = instance.scenarios[1] - @test length(sc.units) == 6 + @test length(sc.thermal_units) == 6 @test length(sc.buses) == 14 @test length(sc.lines) == 20 end diff --git a/test/instance/read_test.jl b/test/instance/read_test.jl index 81e16d1..b7e27f0 100644 --- a/test/instance/read_test.jl +++ b/test/instance/read_test.jl @@ -8,15 +8,15 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip instance = UnitCommitment.read("$FIXTURES/case14.json.gz") @test repr(instance) == ( - "UnitCommitmentInstance(1 scenarios, 6 units, 14 buses, " * - "20 lines, 19 contingencies, 1 price sensitive loads, 0 profiled units, 4 time steps)" + "UnitCommitmentInstance(1 scenarios, 6 thermal units, 0 profiled units, 14 buses, " * + "20 lines, 19 contingencies, 1 price sensitive loads, 4 time steps)" ) @test length(instance.scenarios) == 1 sc = instance.scenarios[1] @test length(sc.lines) == 20 @test length(sc.buses) == 14 - @test length(sc.units) == 6 + @test length(sc.thermal_units) == 6 @test length(sc.contingencies) == 19 @test length(sc.price_sensitive_loads) == 1 @test instance.time == 4 @@ -49,7 +49,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip @test sc.reserves[1].amount == [100.0, 100.0, 100.0, 100.0] @test sc.reserves_by_name["r1"].name == "r1" - unit = sc.units[1] + unit = sc.thermal_units[1] @test unit.name == "g1" @test unit.bus.name == "b1" @test unit.ramp_up_limit == 1e6 @@ -76,14 +76,14 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip @test unit.startup_categories[2].cost == 1500.0 @test unit.startup_categories[3].cost == 2000.0 @test length(unit.reserves) == 0 - @test sc.units_by_name["g1"].name == "g1" + @test sc.thermal_units_by_name["g1"].name == "g1" - unit = sc.units[2] + unit = sc.thermal_units[2] @test unit.name == "g2" @test unit.must_run == [false for t in 1:4] @test length(unit.reserves) == 1 - unit = sc.units[3] + unit = sc.thermal_units[3] @test unit.name == "g3" @test unit.bus.name == "b3" @test unit.ramp_up_limit == 70.0 @@ -106,7 +106,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip @test unit.reserves[1].name == "r1" @test sc.contingencies[1].lines == [sc.lines[1]] - @test sc.contingencies[1].units == [] + @test sc.contingencies[1].thermal_units == [] @test sc.contingencies[1].name == "c1" @test sc.contingencies_by_name["c1"].name == "c1" @@ -121,7 +121,7 @@ end @testset "read_benchmark sub-hourly" begin instance = UnitCommitment.read("$FIXTURES/case14-sub-hourly.json.gz") @test instance.time == 4 - unit = instance.scenarios[1].units[1] + unit = instance.scenarios[1].thermal_units[1] @test unit.name == "g1" @test unit.min_uptime == 2 @test unit.min_downtime == 2 diff --git a/test/transform/initcond_test.jl b/test/transform/initcond_test.jl index 11304bb..744eaf7 100644 --- a/test/transform/initcond_test.jl +++ b/test/transform/initcond_test.jl @@ -10,7 +10,7 @@ using UnitCommitment, Cbc, JuMP optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) sc = instance.scenarios[1] # All units should have unknown initial conditions - for g in sc.units + for g in sc.thermal_units @test g.initial_power === nothing @test g.initial_status === nothing end @@ -19,7 +19,7 @@ using UnitCommitment, Cbc, JuMP UnitCommitment.generate_initial_conditions!(sc, optimizer) # All units should now have known initial conditions - for g in sc.units + for g in sc.thermal_units @test g.initial_power !== nothing @test g.initial_status !== nothing end diff --git a/test/transform/randomize/XavQiuAhm2021_test.jl b/test/transform/randomize/XavQiuAhm2021_test.jl index 226db47..2c1f473 100644 --- a/test/transform/randomize/XavQiuAhm2021_test.jl +++ b/test/transform/randomize/XavQiuAhm2021_test.jl @@ -21,7 +21,7 @@ test_approx(x, y) = @test isapprox(x, y, atol = 1e-3) @testset "cost and load share" begin sc = get_scenario() # Check original costs - unit = sc.units[10] + unit = sc.thermal_units[10] test_approx(unit.min_power_cost[1], 825.023) test_approx(unit.cost_segments[1].cost[1], 36.659) test_approx(unit.startup_categories[1].cost[1], 7570.42) diff --git a/test/transform/slice_test.jl b/test/transform/slice_test.jl index 173ebbb..f4bd8f5 100644 --- a/test/transform/slice_test.jl +++ b/test/transform/slice_test.jl @@ -13,7 +13,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip @test modified.time == 2 @test length(sc.power_balance_penalty) == 2 @test length(sc.reserves_by_name["r1"].amount) == 2 - for u in sc.units + for u in sc.thermal_units @test length(u.max_power) == 2 @test length(u.min_power) == 2 @test length(u.must_run) == 2 From b1c963f217ae11d056930802c01c2368c3c48182 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 4 Apr 2023 15:59:41 -0500 Subject: [PATCH 19/19] Rename 'production' to 'thermal production' --- src/import/egret.jl | 4 ++-- src/solution/fix.jl | 6 +++--- src/solution/solution.jl | 4 ++-- src/validation/validate.jl | 10 +++++----- test/import/egret_test.jl | 5 +++-- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/import/egret.jl b/src/import/egret.jl index d11bdea..3acb32c 100644 --- a/src/import/egret.jl +++ b/src/import/egret.jl @@ -18,9 +18,9 @@ function read_egret_solution(path::String)::OrderedDict solution = OrderedDict() is_on = solution["Is on"] = OrderedDict() - production = solution["Production (MW)"] = OrderedDict() + production = solution["Thermal production (MW)"] = OrderedDict() reserve = solution["Reserve (MW)"] = OrderedDict() - production_cost = solution["Production cost (\$)"] = OrderedDict() + production_cost = solution["Thermal production cost (\$)"] = OrderedDict() startup_cost = solution["Startup cost (\$)"] = OrderedDict() for (gen_name, gen_dict) in egret["elements"]["generator"] diff --git a/src/solution/fix.jl b/src/solution/fix.jl index 3ce6170..4193889 100644 --- a/src/solution/fix.jl +++ b/src/solution/fix.jl @@ -10,8 +10,8 @@ solution. Useful for computing LMPs. """ function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing instance, T = model[:instance], model[:instance].time - "Production (MW)" ∈ keys(solution) ? solution = Dict("s1" => solution) : - nothing + "Thermal production (MW)" ∈ keys(solution) ? + solution = Dict("s1" => solution) : nothing is_on = model[:is_on] prod_above = model[:prod_above] reserve = model[:reserve] @@ -20,7 +20,7 @@ function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing for t in 1:T is_on_value = round(solution[sc.name]["Is on"][g.name][t]) prod_value = round( - solution[sc.name]["Production (MW)"][g.name][t], + solution[sc.name]["Thermal production (MW)"][g.name][t], digits = 5, ) JuMP.fix(is_on[g.name, t], is_on_value, force = true) diff --git a/src/solution/solution.jl b/src/solution/solution.jl index 3ee9dbc..3cb6f41 100644 --- a/src/solution/solution.jl +++ b/src/solution/solution.jl @@ -66,10 +66,10 @@ function solution(model::JuMP.Model)::OrderedDict for sc in instance.scenarios sol[sc.name] = OrderedDict() if !isempty(sc.thermal_units) - sol[sc.name]["Production (MW)"] = OrderedDict( + sol[sc.name]["Thermal production (MW)"] = OrderedDict( g.name => production(g, sc) for g in sc.thermal_units ) - sol[sc.name]["Production cost (\$)"] = OrderedDict( + sol[sc.name]["Thermal production cost (\$)"] = OrderedDict( g.name => production_cost(g, sc) for g in sc.thermal_units ) sol[sc.name]["Startup cost (\$)"] = OrderedDict( diff --git a/src/validation/validate.jl b/src/validation/validate.jl index 88ee36f..5447980 100644 --- a/src/validation/validate.jl +++ b/src/validation/validate.jl @@ -28,8 +28,8 @@ function validate( instance::UnitCommitmentInstance, solution::Union{Dict,OrderedDict}, )::Bool - "Production (MW)" ∈ keys(solution) ? solution = Dict("s1" => solution) : - nothing + "Thermal production (MW)" ∈ keys(solution) ? + solution = Dict("s1" => solution) : nothing err_count = 0 err_count += _validate_units(instance, solution) err_count += _validate_reserve_and_demand(instance, solution) @@ -46,7 +46,7 @@ function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01) err_count = 0 for sc in instance.scenarios for unit in sc.thermal_units - production = solution[sc.name]["Production (MW)"][unit.name] + production = solution[sc.name]["Thermal production (MW)"][unit.name] reserve = [0.0 for _ in 1:instance.time] spinning_reserves = [r for r in unit.reserves if r.type == "spinning"] @@ -57,7 +57,7 @@ function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01) ) end actual_production_cost = - solution[sc.name]["Production cost (\$)"][unit.name] + solution[sc.name]["Thermal production cost (\$)"][unit.name] actual_startup_cost = solution[sc.name]["Startup cost (\$)"][unit.name] is_on = bin(solution[sc.name]["Is on"][unit.name]) @@ -323,7 +323,7 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01) ) end production = sum( - solution[sc.name]["Production (MW)"][g.name][t] for + solution[sc.name]["Thermal production (MW)"][g.name][t] for g in sc.thermal_units ) if "Load curtail (MW)" in keys(solution) diff --git a/test/import/egret_test.jl b/test/import/egret_test.jl index 0ca54ab..a0a61bf 100644 --- a/test/import/egret_test.jl +++ b/test/import/egret_test.jl @@ -7,12 +7,13 @@ using UnitCommitment @testset "read_egret_solution" begin solution = UnitCommitment.read_egret_solution("$FIXTURES/egret_output.json.gz") - for attr in ["Is on", "Production (MW)", "Production cost (\$)"] + for attr in + ["Is on", "Thermal production (MW)", "Thermal production cost (\$)"] @test attr in keys(solution) @test "115_STEAM_1" in keys(solution[attr]) @test length(solution[attr]["115_STEAM_1"]) == 48 end - @test solution["Production cost (\$)"]["315_CT_6"][15:20] == + @test solution["Thermal production cost (\$)"]["315_CT_6"][15:20] == [0.0, 0.0, 884.44, 1470.71, 1470.71, 884.44] @test solution["Startup cost (\$)"]["315_CT_6"][15:20] == [0.0, 0.0, 5665.23, 0.0, 0.0, 0.0]