diff --git a/src/UnitCommitment.jl b/src/UnitCommitment.jl index 1c7bc9f..033c6df 100644 --- a/src/UnitCommitment.jl +++ b/src/UnitCommitment.jl @@ -10,6 +10,7 @@ include("instance/structs.jl") include("model/formulations/base/structs.jl") include("solution/structs.jl") include("lmp/structs.jl") +include("market/structs.jl") include("model/formulations/ArrCon2000/structs.jl") include("model/formulations/CarArr2006/structs.jl") @@ -68,5 +69,6 @@ include("validation/repair.jl") include("validation/validate.jl") include("lmp/conventional.jl") include("lmp/aelmp.jl") +include("market/market.jl") end diff --git a/src/market/market.jl b/src/market/market.jl new file mode 100644 index 0000000..a0d08f2 --- /dev/null +++ b/src/market/market.jl @@ -0,0 +1,220 @@ +# 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. + +""" + solve_market( + da_path::Union{String, Vector{String}}, + rt_paths::Vector{String}, + settings::MarketSettings; + optimizer, + lp_optimizer = nothing, + after_build_da = nothing, + after_optimize_da = nothing, + after_build_rt = nothing, + after_optimize_rt = nothing, + )::OrderedDict + +Solve the day-ahead and the real-time markets by the means of commitment status mapping. +The method firstly acquires the commitment status outcomes through the resolution of the day-ahead market; +and secondly resolves each real-time market based on the corresponding results obtained previously. + +Arguments +--------- + +- `da_path`: + the data file path of the day-ahead market, can be stochastic. + +- `rt_paths`: + the list of data file paths of the real-time markets, must be deterministic for each market. + +- `settings`: + the MarketSettings which include the problem formulation, the solving method, and LMP method. + +- `optimizer`: + the optimizer for solving the problem. + +- `lp_optimizer`: + the linear programming optimizer for solving the LMP problem, defaults to `nothing`. + If not specified by the user, the program uses `optimizer` instead. + +- `after_build_da`: + a user-defined function that allows modifying the DA model after building, + must have 2 arguments `model` and `instance` in order. + +- `after_optimize_da`: + a user-defined function that allows handling additional steps after optimizing the DA model, + must have 3 arguments `solution`, `model` and `instance` in order. + +- `after_build_rt`: + a user-defined function that allows modifying each RT model after building, + must have 2 arguments `model` and `instance` in order. + +- `after_optimize_rt`: + a user-defined function that allows handling additional steps after optimizing each RT model, + must have 3 arguments `solution`, `model` and `instance` in order. + + +Examples +-------- + +```julia +using UnitCommitment, Cbc, HiGHS + +import UnitCommitment: + MarketSettings, + XavQiuWanThi2019, + ConventionalLMP, + Formulation + +solution = UnitCommitment.solve_market( + "da_instance.json", + ["rt_instance_1.json", "rt_instance_2.json", "rt_instance_3.json"], + MarketSettings( + inner_method = XavQiuWanThi2019.Method(), + lmp_method = ConventionalLMP(), + formulation = Formulation(), + ), + optimizer = Cbc.Optimizer, + lp_optimizer = HiGHS.Optimizer, +) +""" + +function solve_market( + da_path::Union{String,Vector{String}}, + rt_paths::Vector{String}, + settings::MarketSettings; + optimizer, + lp_optimizer = nothing, + after_build_da = nothing, + after_optimize_da = nothing, + after_build_rt = nothing, + after_optimize_rt = nothing, +)::OrderedDict + # solve da instance as usual + @info "Solving the day-ahead market with file $da_path..." + instance_da = UnitCommitment.read(da_path) + # LP optimizer is optional: if not specified, use optimizer + lp_optimizer = lp_optimizer === nothing ? optimizer : lp_optimizer + # build and optimize the DA market + model_da, solution_da = _build_and_optimize( + instance_da, + settings, + optimizer = optimizer, + lp_optimizer = lp_optimizer, + after_build = after_build_da, + after_optimize = after_optimize_da, + ) + # prepare the final solution + solution = OrderedDict() + solution["Day-ahead market"] = solution_da + solution["Real-time markets"] = OrderedDict() + + # count the time, sc.time = n-slots, sc.time_step = slot-interval + # sufficient to look at only one scenario + sc = instance_da.scenarios[1] + # max time (min) of the DA market + max_time = sc.time * sc.time_step + # current time increments through the RT market list + current_time = 0 + # DA market time slots in (min) + da_time_intervals = [sc.time_step * ts for ts in 1:sc.time] + + # get the uc status and set each uc fixed + solution_rt = OrderedDict() + prev_initial_status = OrderedDict() + for rt_path in rt_paths + @info "Solving the real-time market with file $rt_path..." + instance_rt = UnitCommitment.read(rt_path) + # check instance time + sc = instance_rt.scenarios[1] + # check each time slot in the RT model + for ts in 1:sc.time + slot_t_end = current_time + ts * sc.time_step + # ensure this RT's slot time ub never exceeds max time of DA + slot_t_end <= max_time || error( + "The time of the real-time market cannot exceed the time of the day-ahead market.", + ) + # get the slot start time to determine commitment status + slot_t_start = slot_t_end - sc.time_step + # find the index of the first DA time slot that covers slot_t_start + da_time_slot = findfirst(ti -> slot_t_start < ti, da_time_intervals) + # update thermal unit commitment status + for g in sc.thermal_units + g.commitment_status[ts] = + value(model_da[:is_on][g.name, da_time_slot]) == 1.0 + end + end + # update current time by ONE slot only + current_time += sc.time_step + # set initial status for all generators in all scenarios + if !isempty(solution_rt) && !isempty(prev_initial_status) + for g in sc.thermal_units + g.initial_power = + solution_rt["Thermal production (MW)"][g.name][1] + g.initial_status = UnitCommitment._determine_initial_status( + prev_initial_status[g.name], + [solution_rt["Is on"][g.name][1]], + ) + end + end + # build and optimize the RT market + _, solution_rt = _build_and_optimize( + instance_rt, + settings, + optimizer = optimizer, + lp_optimizer = lp_optimizer, + after_build = after_build_rt, + after_optimize = after_optimize_rt, + ) + prev_initial_status = + OrderedDict(g.name => g.initial_status for g in sc.thermal_units) + # rt_name = first(split(last(split(rt_path, "/")), ".")) + solution["Real-time markets"][rt_path] = solution_rt + end # end of for-loop that checks each RT market + return solution +end + +function _build_and_optimize( + instance::UnitCommitmentInstance, + settings::MarketSettings; + optimizer, + lp_optimizer, + after_build = nothing, + after_optimize = nothing, +)::Tuple{JuMP.Model,OrderedDict} + # build model with after build + model = UnitCommitment.build_model( + instance = instance, + optimizer = optimizer, + formulation = settings.formulation, + ) + if after_build !== nothing + after_build(model, instance) + end + # optimize model + UnitCommitment.optimize!(model, settings.inner_method) + solution = UnitCommitment.solution(model) + # compute lmp and add to solution + if settings.lmp_method !== nothing + lmp = UnitCommitment.compute_lmp( + model, + settings.lmp_method, + optimizer = lp_optimizer, + ) + if length(instance.scenarios) == 1 + solution["Locational marginal price"] = lmp + else + for sc in instance.scenarios + solution[sc.name]["Locational marginal price"] = OrderedDict( + key => val for (key, val) in lmp if key[1] == sc.name + ) + end + end + end + # run after optimize with solution + if after_optimize !== nothing + after_optimize(solution, model, instance) + end + return model, solution +end diff --git a/src/market/structs.jl b/src/market/structs.jl new file mode 100644 index 0000000..764a835 --- /dev/null +++ b/src/market/structs.jl @@ -0,0 +1,33 @@ +# 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. + +import ..SolutionMethod +import ..PricingMethod +import ..Formulation + +""" + struct MarketSettings + inner_method::SolutionMethod = XavQiuWanThi2019.Method() + lmp_method::Union{PricingMethod, Nothing} = ConventionalLMP() + formulation::Formulation = Formulation() + end + +Market setting struct, typically used to map a day-ahead market to real-time markets. + +Arguments +--------- + +- `inner_method`: + method to solve each marketing problem. +- `lmp_method`: + a PricingMethod method to calculate the locational marginal prices. + If it is set to `nothing`, the LMPs will not be calculated. +- `formulation`: + problem formulation. +""" +Base.@kwdef struct MarketSettings + inner_method::SolutionMethod = XavQiuWanThi2019.Method() + lmp_method::Union{PricingMethod,Nothing} = ConventionalLMP() + formulation::Formulation = Formulation() +end diff --git a/test/src/UnitCommitmentT.jl b/test/src/UnitCommitmentT.jl index 4ea0d73..ae28329 100644 --- a/test/src/UnitCommitmentT.jl +++ b/test/src/UnitCommitmentT.jl @@ -22,6 +22,7 @@ include("transform/randomize/XavQiuAhm2021_test.jl") include("validation/repair_test.jl") include("lmp/conventional_test.jl") include("lmp/aelmp_test.jl") +include("market/market_test.jl") basedir = dirname(@__FILE__) @@ -51,6 +52,8 @@ function runtests() validation_repair_test() lmp_conventional_test() lmp_aelmp_test() + simple_market_test() + stochastic_market_test() end return end diff --git a/test/src/market/market_test.jl b/test/src/market/market_test.jl new file mode 100644 index 0000000..40a8d56 --- /dev/null +++ b/test/src/market/market_test.jl @@ -0,0 +1,151 @@ +# 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: MarketSettings + +function simple_market_test() + @testset "da-to-rt simple market" begin + da_path = fixture("market_da_simple.json.gz") + rt_paths = [ + fixture("market_rt1_simple.json.gz"), + fixture("market_rt2_simple.json.gz"), + fixture("market_rt3_simple.json.gz"), + fixture("market_rt4_simple.json.gz"), + ] + # solve market with default setting + solution = UnitCommitment.solve_market( + da_path, + rt_paths, + MarketSettings(), # keep everything default + optimizer = optimizer_with_attributes( + Cbc.Optimizer, + "logLevel" => 0, + ), + lp_optimizer = optimizer_with_attributes( + HiGHS.Optimizer, + "log_to_console" => false, + ), + ) + + # the commitment status must agree with DA market + da_solution = solution["Day-ahead market"] + @test da_solution["Is on"]["GenY"] == [0.0, 1.0] + @test da_solution["Locational marginal price"][("s1", "B1", 1)] == 50.0 + @test da_solution["Locational marginal price"][("s1", "B1", 2)] == 56.0 + + rt_solution = solution["Real-time markets"] + @test length(rt_solution) == 4 + @test rt_solution[rt_paths[1]]["Is on"]["GenY"] == [0.0, 0.0] + @test rt_solution[rt_paths[2]]["Is on"]["GenY"] == [0.0, 1.0] + @test rt_solution[rt_paths[3]]["Is on"]["GenY"] == [1.0, 1.0] + @test rt_solution[rt_paths[4]]["Is on"]["GenY"] == [1.0] + @test length(rt_solution[rt_paths[1]]["Locational marginal price"]) == 2 + @test length(rt_solution[rt_paths[2]]["Locational marginal price"]) == 2 + @test length(rt_solution[rt_paths[3]]["Locational marginal price"]) == 2 + @test length(rt_solution[rt_paths[4]]["Locational marginal price"]) == 1 + + # solve market with no lmp method + solution_no_lmp = UnitCommitment.solve_market( + da_path, + rt_paths, + MarketSettings(lmp_method = nothing), # no lmp + optimizer = optimizer_with_attributes( + Cbc.Optimizer, + "logLevel" => 0, + ), + ) + + # the commitment status must agree with DA market + da_solution = solution_no_lmp["Day-ahead market"] + @test haskey(da_solution, "Locational marginal price") == false + rt_solution = solution_no_lmp["Real-time markets"] + @test haskey(rt_solution, "Locational marginal price") == false + end +end + +function stochastic_market_test() + @testset "da-to-rt stochastic market" begin + da_path = [ + fixture("market_da_simple.json.gz"), + fixture("market_da_scenario.json.gz"), + ] + rt_paths = [ + fixture("market_rt1_simple.json.gz"), + fixture("market_rt2_simple.json.gz"), + fixture("market_rt3_simple.json.gz"), + fixture("market_rt4_simple.json.gz"), + ] + # after build and after optimize + function after_build(model, instance) + @constraint(model, model[:is_on]["GenY", 1] == 1,) + end + + lmps_da = [] + lmps_rt = [] + + function after_optimize_da(solution, model, instance) + lmp = UnitCommitment.compute_lmp( + model, + ConventionalLMP(), + optimizer = optimizer_with_attributes( + HiGHS.Optimizer, + "log_to_console" => false, + ), + ) + return push!(lmps_da, lmp) + end + + function after_optimize_rt(solution, model, instance) + lmp = UnitCommitment.compute_lmp( + model, + ConventionalLMP(), + optimizer = optimizer_with_attributes( + HiGHS.Optimizer, + "log_to_console" => false, + ), + ) + return push!(lmps_rt, lmp) + end + + # solve the stochastic market with callbacks + solution = UnitCommitment.solve_market( + da_path, + rt_paths, + MarketSettings(), # keep everything default + optimizer = optimizer_with_attributes( + Cbc.Optimizer, + "logLevel" => 0, + ), + lp_optimizer = optimizer_with_attributes( + HiGHS.Optimizer, + "log_to_console" => false, + ), + after_build_da = after_build, + after_optimize_da = after_optimize_da, + after_optimize_rt = after_optimize_rt, + ) + # the commitment status must agree with DA market + da_solution_sp = solution["Day-ahead market"]["market_da_simple"] + da_solution_sc = solution["Day-ahead market"]["market_da_scenario"] + @test da_solution_sc["Is on"]["GenY"] == [1.0, 1.0] + @test da_solution_sp["Locational marginal price"][( + "market_da_simple", + "B1", + 1, + )] == 25.0 + @test da_solution_sc["Locational marginal price"][( + "market_da_scenario", + "B1", + 2, + )] == 0.0 + + rt_solution = solution["Real-time markets"] + @test rt_solution[rt_paths[1]]["Is on"]["GenY"] == [1.0, 1.0] + @test rt_solution[rt_paths[2]]["Is on"]["GenY"] == [1.0, 1.0] + @test rt_solution[rt_paths[3]]["Is on"]["GenY"] == [1.0, 1.0] + @test rt_solution[rt_paths[4]]["Is on"]["GenY"] == [1.0] + @test length(lmps_rt) == 4 + end +end