From 46259f7c1c3b4ca6e034e8a9f7329ce6a7b9ab3a Mon Sep 17 00:00:00 2001 From: Jun He Date: Fri, 19 May 2023 13:31:20 -0400 Subject: [PATCH 01/17] time decomposition src code --- src/UnitCommitment.jl | 2 + .../methods/TimeDecomposition/optimize.jl | 177 ++++++++++++++++++ .../methods/TimeDecomposition/structs.jl | 35 ++++ 3 files changed, 214 insertions(+) create mode 100644 src/solution/methods/TimeDecomposition/optimize.jl create mode 100644 src/solution/methods/TimeDecomposition/structs.jl diff --git a/src/UnitCommitment.jl b/src/UnitCommitment.jl index 3191395..78018b4 100644 --- a/src/UnitCommitment.jl +++ b/src/UnitCommitment.jl @@ -20,6 +20,7 @@ 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("solution/methods/TimeDecomposition/structs.jl") include("import/egret.jl") include("instance/read.jl") @@ -49,6 +50,7 @@ include("solution/methods/XavQiuWanThi2019/enforce.jl") include("solution/methods/XavQiuWanThi2019/filter.jl") include("solution/methods/XavQiuWanThi2019/find.jl") include("solution/methods/XavQiuWanThi2019/optimize.jl") +include("solution/methods/TimeDecomposition/optimize.jl") include("solution/optimize.jl") include("solution/solution.jl") include("solution/warmstart.jl") diff --git a/src/solution/methods/TimeDecomposition/optimize.jl b/src/solution/methods/TimeDecomposition/optimize.jl new file mode 100644 index 0000000..4a93545 --- /dev/null +++ b/src/solution/methods/TimeDecomposition/optimize.jl @@ -0,0 +1,177 @@ +# 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. + +""" + optimize!( + instance::UnitCommitmentInstance, + method::TimeDecomposition; + optimizer, + )::OrderedDict + +Solve the given unit commitment instance with time decomposition. +The model solves each sub-problem of a given time length specified by method.time_window, +and proceeds to the next sub-problem by incrementing the time length of method.time_increment. + +Examples +-------- + +```julia +using UnitCommitment, Cbc + +import UnitCommitment: + TimeDecomposition, + Formulation, + XavQiuWanThi2019 + +# assume the instance is given as a 120h problem +instance = UnitCommitment.read("instance.json") + +solution = UnitCommitment.optimize!( + instance, + TimeDecomposition( + time_window = 36, # solve 36h problems + time_increment = 24, # advance by 24h each time + inner_method = XavQiuWanThi2019.Method(), + formulation = Formulation(), + ), + optimizer=Cbc.Optimizer +) +""" + +function optimize!( + instance::UnitCommitmentInstance, + method::TimeDecomposition; + optimizer, +)::OrderedDict + # get instance total length + T = instance.time + solution = OrderedDict() + iter = 0 + if length(instance.scenarios) > 1 + for sc in instance.scenarios + solution[sc.name] = OrderedDict() + end + end + # for each iteration, time increment by method.time_increment + for t_start in 1:method.time_increment:T + # set the initial status + if iter > 0 + _set_initial_status!(instance, solution, method.time_increment) + end + t_end = t_start + method.time_window - 1 + # if t_end exceed total T + t_end = t_end > T ? T : t_end + # slice the model + modified = UnitCommitment.slice(instance, t_start:t_end) + # solve the model + model = UnitCommitment.build_model( + instance = modified, + optimizer = optimizer, + formulation = method.formulation, + ) + UnitCommitment.optimize!(model, method.inner_method) + # get the result of each time period + sub_solution = UnitCommitment.solution(model) + if length(instance.scenarios) == 1 + _update_solution!(solution, sub_solution, method.time_increment) + else + for sc in instance.scenarios + _update_solution!( + solution[sc.name], + sub_solution[sc.name], + method.time_increment, + ) + end + end + iter += 1 # increment iteration counter + end + return solution +end + +function _set_initial_status!( + instance::UnitCommitmentInstance, + solution::OrderedDict, + time_increment::Int, +) + for sc in instance.scenarios + for thermal_unit in sc.thermal_units + if length(instance.scenarios) == 1 + prod = solution["Thermal production (MW)"][thermal_unit.name] + is_on = solution["Is on"][thermal_unit.name] + else + prod = + solution[sc.name]["Thermal production (MW)"][thermal_unit.name] + is_on = solution[sc.name]["Is on"][thermal_unit.name] + end + thermal_unit.initial_power = prod[time_increment] + thermal_unit.initial_status = _determine_initial_status( + thermal_unit.initial_status, + is_on, + time_increment, + ) + end + end +end + +function _determine_initial_status( + prev_initial_status::Union{Float64,Int}, + status_sequence::Vector{Float64}, + time_increment::Int, +)::Union{Float64,Int} + # initialize the two flags + on_status = prev_initial_status + off_status = prev_initial_status + # read through the status sequence + # at each time if the unit is on, reset off_status, increment on_status + # if the on_status < 0, set it to 1.0 + # at each time if the unit is off, reset on_status, decrement off_status + # if the off_status > 0, set it to -1.0 + for t in 1:time_increment + if status_sequence[t] == 1.0 + on_status = on_status < 0.0 ? 1.0 : on_status + 1.0 + off_status = 0.0 + else + on_status = 0.0 + off_status = off_status > 0.0 ? -1.0 : off_status - 1.0 + end + end + # only one of them has non-zero value + return on_status + off_status +end + +function _update_solution!( + solution::OrderedDict, + sub_solution::OrderedDict, + time_increment::Int, +) + # the solution has at most 3 layers + for (l1_k, l1_v) in sub_solution + for (l2_k, l2_v) in l1_v + if l2_v isa Array + # slice the sub_solution + values_of_interest = l2_v[1:time_increment] + sub_solution[l1_k][l2_k] = values_of_interest + # append to the solution + if !isempty(solution) + append!(solution[l1_k][l2_k], values_of_interest) + end + elseif l2_v isa OrderedDict + for (l3_k, l3_v) in l2_v + # slice the sub_solution + values_of_interest = l3_v[1:time_increment] + sub_solution[l1_k][l2_k][l3_k] = values_of_interest + # append to the solution + if !isempty(solution) + append!(solution[l1_k][l2_k][l3_k], values_of_interest) + end + end + end + end + end + + # if solution is never initialized, deep copy the sliced sub_solution + if isempty(solution) + merge!(solution, sub_solution) + end +end diff --git a/src/solution/methods/TimeDecomposition/structs.jl b/src/solution/methods/TimeDecomposition/structs.jl new file mode 100644 index 0000000..6ad170a --- /dev/null +++ b/src/solution/methods/TimeDecomposition/structs.jl @@ -0,0 +1,35 @@ +# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment +# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +import ..SolutionMethod +import ..Formulation +""" + mutable struct TimeDecomposition <: SolutionMethod + time_window::Int + time_increment::Int + inner_method::SolutionMethod + formulation::Formulation + end + +Time decomposition method to solve a problem with moving time window. + +Fields +------ + +- `time_window`: + the time window of each sub-problem during the entire optimization procedure. +- `time_increment`: + the time incremented to the next sub-problem. +- `inner_method`: + method to solve each sub-problem. +- `formulation`: + problem formulation. + +""" +Base.@kwdef mutable struct TimeDecomposition <: SolutionMethod + time_window::Int + time_increment::Int + inner_method::SolutionMethod + formulation::Formulation +end From adcaf6fc5584095d0de9552bc298a568a730c7ee Mon Sep 17 00:00:00 2001 From: Jun He Date: Fri, 19 May 2023 13:31:32 -0400 Subject: [PATCH 02/17] time decomposition tests --- test/runtests.jl | 7 + .../TimeDecomposition/initial_status_test.jl | 219 ++++++++++++++++++ .../TimeDecomposition/optimize_test.jl | 52 +++++ .../TimeDecomposition/update_solution_test.jl | 49 ++++ 4 files changed, 327 insertions(+) create mode 100644 test/solution/methods/TimeDecomposition/initial_status_test.jl create mode 100644 test/solution/methods/TimeDecomposition/optimize_test.jl create mode 100644 test/solution/methods/TimeDecomposition/update_solution_test.jl diff --git a/test/runtests.jl b/test/runtests.jl index 08ade97..a98fa79 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -28,6 +28,13 @@ FIXTURES = "$(@__DIR__)/fixtures" include("solution/methods/XavQiuWanThi19/find_test.jl") include("solution/methods/XavQiuWanThi19/sensitivity_test.jl") end + @testset "TimeDecomposition" begin + include("solution/methods/TimeDecomposition/initial_status_test.jl") + include( + "solution/methods/TimeDecomposition/update_solution_test.jl", + ) + include("solution/methods/TimeDecomposition/optimize_test.jl") + end end @testset "transform" begin include("transform/initcond_test.jl") diff --git a/test/solution/methods/TimeDecomposition/initial_status_test.jl b/test/solution/methods/TimeDecomposition/initial_status_test.jl new file mode 100644 index 0000000..cd253d8 --- /dev/null +++ b/test/solution/methods/TimeDecomposition/initial_status_test.jl @@ -0,0 +1,219 @@ +# 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, DataStructures + +@testset "determine_initial_status" begin + t_increment = 24 + t_model = 36 + hot_start = 100 + cold_start = -100 + + # all on throughout + stat_seq = ones(t_model) + # hot start + new_stat = UnitCommitment._determine_initial_status( + hot_start, + stat_seq, + t_increment, + ) + @test new_stat == 124 + # cold start + new_stat = UnitCommitment._determine_initial_status( + cold_start, + stat_seq, + t_increment, + ) + @test new_stat == 24 + + # off in the last 12 periods + stat_seq = ones(t_model) + stat_seq[25:end] .= 0 + # hot start + new_stat = UnitCommitment._determine_initial_status( + hot_start, + stat_seq, + t_increment, + ) + @test new_stat == 124 + # cold start + new_stat = UnitCommitment._determine_initial_status( + cold_start, + stat_seq, + t_increment, + ) + @test new_stat == 24 + + # off in one of the first 24 periods + stat_seq = ones(t_model) + stat_seq[10] = 0 + # hot start + new_stat = UnitCommitment._determine_initial_status( + hot_start, + stat_seq, + t_increment, + ) + @test new_stat == 14 + # cold start + new_stat = UnitCommitment._determine_initial_status( + cold_start, + stat_seq, + t_increment, + ) + @test new_stat == 14 + + # off in several of the first 24 periods + stat_seq = ones(t_model) + stat_seq[[10, 11, 20]] .= 0 + # hot start + new_stat = UnitCommitment._determine_initial_status( + hot_start, + stat_seq, + t_increment, + ) + @test new_stat == 4 + # cold start + new_stat = UnitCommitment._determine_initial_status( + cold_start, + stat_seq, + t_increment, + ) + @test new_stat == 4 + + # off in several of the first 24 periods + stat_seq = ones(t_model) + stat_seq[20:24] .= 0 + # hot start + new_stat = UnitCommitment._determine_initial_status( + hot_start, + stat_seq, + t_increment, + ) + @test new_stat == -5 + # cold start + new_stat = UnitCommitment._determine_initial_status( + cold_start, + stat_seq, + t_increment, + ) + @test new_stat == -5 + + # all off throughout + stat_seq = zeros(t_model) + # hot start + new_stat = UnitCommitment._determine_initial_status( + hot_start, + stat_seq, + t_increment, + ) + @test new_stat == -24 + # cold start + new_stat = UnitCommitment._determine_initial_status( + cold_start, + stat_seq, + t_increment, + ) + @test new_stat == -124 + + # on in the last 12 periods + stat_seq = zeros(t_model) + stat_seq[25:end] .= 1 + # hot start + new_stat = UnitCommitment._determine_initial_status( + hot_start, + stat_seq, + t_increment, + ) + @test new_stat == -24 + # cold start + new_stat = UnitCommitment._determine_initial_status( + cold_start, + stat_seq, + t_increment, + ) + @test new_stat == -124 +end + +@testset "set_initial_status" begin + # read one scenario + instance = UnitCommitment.read("$FIXTURES/case14.json.gz") + psuedo_solution = OrderedDict( + "Thermal production (MW)" => OrderedDict( + "g1" => [110.0, 112.0, 114.0, 116.0], + "g2" => [100.0, 102.0, 0.0, 0.0], + "g3" => [0.0, 0.0, 0.0, 0.0], + "g4" => [33.0, 34.0, 66.0, 99.0], + "g5" => [33.0, 34.0, 66.0, 99.0], + "g6" => [100.0, 100.0, 100.0, 100.0], + ), + "Is on" => OrderedDict( + "g1" => [1.0, 1.0, 1.0, 1.0], + "g2" => [1.0, 1.0, 0.0, 0.0], + "g3" => [0.0, 0.0, 0.0, 0.0], + "g4" => [1.0, 1.0, 1.0, 1.0], + "g5" => [1.0, 1.0, 1.0, 1.0], + "g6" => [1.0, 1.0, 1.0, 1.0], + ), + ) + UnitCommitment._set_initial_status!(instance, psuedo_solution, 3) + thermal_units = instance.scenarios[1].thermal_units + @test thermal_units[1].initial_power == 114.0 + @test thermal_units[1].initial_status == 3.0 + @test thermal_units[2].initial_power == 0.0 + @test thermal_units[2].initial_status == -1.0 + @test thermal_units[3].initial_power == 0.0 + @test thermal_units[3].initial_status == -9.0 + + # read multiple scenarios + instance = UnitCommitment.read([ + "$FIXTURES/case14.json.gz", + "$FIXTURES/case14-profiled.json.gz", + ]) + psuedo_solution = OrderedDict( + "case14" => OrderedDict( + "Thermal production (MW)" => OrderedDict( + "g1" => [110.0, 112.0, 114.0, 116.0], + "g2" => [100.0, 102.0, 0.0, 0.0], + "g3" => [0.0, 0.0, 0.0, 0.0], + "g4" => [33.0, 34.0, 66.0, 99.0], + "g5" => [33.0, 34.0, 66.0, 99.0], + "g6" => [100.0, 100.0, 100.0, 100.0], + ), + "Is on" => OrderedDict( + "g1" => [1.0, 1.0, 1.0, 1.0], + "g2" => [1.0, 1.0, 0.0, 0.0], + "g3" => [0.0, 0.0, 0.0, 0.0], + "g4" => [1.0, 1.0, 1.0, 1.0], + "g5" => [1.0, 1.0, 1.0, 1.0], + "g6" => [1.0, 1.0, 1.0, 1.0], + ), + ), + "case14-profiled" => OrderedDict( + "Thermal production (MW)" => OrderedDict( + "g1" => [112.0, 113.0, 116.0, 115.0], + "g2" => [0.0, 0.0, 0.0, 0.0], + "g3" => [0.0, 0.0, 0.0, 20.0], + "g4" => [33.0, 34.0, 66.0, 99.0], + "g5" => [33.0, 34.0, 66.0, 99.0], + "g6" => [100.0, 100.0, 100.0, 100.0], + ), + "Is on" => OrderedDict( + "g1" => [1.0, 1.0, 1.0, 1.0], + "g2" => [0.0, 0.0, 0.0, 0.0], + "g3" => [0.0, 0.0, 0.0, 0.0], + "g4" => [1.0, 1.0, 1.0, 1.0], + "g5" => [1.0, 1.0, 1.0, 1.0], + "g6" => [1.0, 1.0, 1.0, 1.0], + ), + ), + ) + UnitCommitment._set_initial_status!(instance, psuedo_solution, 3) + thermal_units_sc2 = instance.scenarios[2].thermal_units + @test thermal_units_sc2[1].initial_power == 116.0 + @test thermal_units_sc2[1].initial_status == 3.0 + @test thermal_units_sc2[2].initial_power == 0.0 + @test thermal_units_sc2[2].initial_status == -11.0 + @test thermal_units_sc2[3].initial_power == 0.0 + @test thermal_units_sc2[3].initial_status == -9.0 +end diff --git a/test/solution/methods/TimeDecomposition/optimize_test.jl b/test/solution/methods/TimeDecomposition/optimize_test.jl new file mode 100644 index 0000000..9987d1e --- /dev/null +++ b/test/solution/methods/TimeDecomposition/optimize_test.jl @@ -0,0 +1,52 @@ +# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment +# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +using UnitCommitment, DataStructures, Cbc +import UnitCommitment: TimeDecomposition, XavQiuWanThi2019, Formulation + +@testset "optimize_time_decomposition" begin + # read one scenario + instance = UnitCommitment.read("$FIXTURES/case14.json.gz") + solution = UnitCommitment.optimize!( + instance, + TimeDecomposition( + time_window = 3, + time_increment = 2, + inner_method = XavQiuWanThi2019.Method(), + formulation = Formulation(), + ), + optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0), + ) + @test length(solution["Thermal production (MW)"]["g1"]) == 4 + @test length(solution["Is on"]["g2"]) == 4 + @test length(solution["Spinning reserve (MW)"]["r1"]["g2"]) == 4 + + # read multiple scenarios + instance = UnitCommitment.read([ + "$FIXTURES/case14.json.gz", + "$FIXTURES/case14-profiled.json.gz", + ]) + solution = UnitCommitment.optimize!( + instance, + TimeDecomposition( + time_window = 3, + time_increment = 2, + inner_method = XavQiuWanThi2019.Method(), + formulation = Formulation(), + ), + optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0), + ) + @test length(solution["case14"]["Thermal production (MW)"]["g3"]) == 4 + @test length(solution["case14"]["Is on"]["g4"]) == 4 + @test length( + solution["case14-profiled"]["Thermal production (MW)"]["g5"], + ) == 4 + @test length(solution["case14-profiled"]["Is on"]["g6"]) == 4 + @test length( + solution["case14-profiled"]["Profiled production (MW)"]["g7"], + ) == 4 + @test length( + solution["case14-profiled"]["Spinning reserve (MW)"]["r1"]["g3"], + ) == 4 +end diff --git a/test/solution/methods/TimeDecomposition/update_solution_test.jl b/test/solution/methods/TimeDecomposition/update_solution_test.jl new file mode 100644 index 0000000..106002e --- /dev/null +++ b/test/solution/methods/TimeDecomposition/update_solution_test.jl @@ -0,0 +1,49 @@ +# 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, DataStructures + +@testset "update_solution" begin + psuedo_solution = OrderedDict() + time_increment = 4 + psuedo_sub_solution = OrderedDict( + "Thermal production (MW)" => + OrderedDict("g1" => [100.0, 200.0, 300.0, 400.0, 500.0, 600.0]), + "Is on" => OrderedDict("g1" => [1.0, 0.0, 1.0, 1.0, 0.0, 1.0]), + "Profiled production (MW)" => + OrderedDict("g1" => [199.0, 299.0, 399.0, 499.0, 599.0, 699.0]), + "Spinning reserve (MW)" => OrderedDict( + "r1" => OrderedDict("g1" => [31.0, 32.0, 33.0, 34.0, 35.0, 36.0]), + ), + ) + + # first update should directly copy the first 4 entries of sub solution + UnitCommitment._update_solution!( + psuedo_solution, + psuedo_sub_solution, + time_increment, + ) + @test psuedo_solution["Thermal production (MW)"]["g1"] == + [100.0, 200.0, 300.0, 400.0] + @test psuedo_solution["Is on"]["g1"] == [1.0, 0.0, 1.0, 1.0] + @test psuedo_solution["Profiled production (MW)"]["g1"] == + [199.0, 299.0, 399.0, 499.0] + @test psuedo_solution["Spinning reserve (MW)"]["r1"]["g1"] == + [31.0, 32.0, 33.0, 34.0] + + # second update should append the first 4 entries of sub solution + UnitCommitment._update_solution!( + psuedo_solution, + psuedo_sub_solution, + time_increment, + ) + @test psuedo_solution["Thermal production (MW)"]["g1"] == + [100.0, 200.0, 300.0, 400.0, 100.0, 200.0, 300.0, 400.0] + @test psuedo_solution["Is on"]["g1"] == + [1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0] + @test psuedo_solution["Profiled production (MW)"]["g1"] == + [199.0, 299.0, 399.0, 499.0, 199.0, 299.0, 399.0, 499.0] + @test psuedo_solution["Spinning reserve (MW)"]["r1"]["g1"] == + [31.0, 32.0, 33.0, 34.0, 31.0, 32.0, 33.0, 34.0] +end From eff5908b1328f9e2ba98912aae73b92f655a4d72 Mon Sep 17 00:00:00 2001 From: Jun He Date: Fri, 19 May 2023 13:31:44 -0400 Subject: [PATCH 03/17] time decomposition doc --- docs/src/usage.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/src/usage.md b/docs/src/usage.md index 26d1779..5f3914f 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -223,4 +223,35 @@ aelmp = UnitCommitment.compute_lmp( # 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] +``` + +## Time Decomposition Method + +When solving a unit commitment instance with a dense time slot structure, computational complexity can become a significant challenge. For instance, if the instance contains hourly data for an entire year (8760 hours), solving such a model can require a substantial amount of computational power. To address this issue, UC.jl provides a time_decomposition method within the `optimize!` function. This method decomposes the problem into multiple sub-problems, solving them sequentially. + +The `optimize!` function takes three parameters: a unit commitment instance, a `TimeDecomposition` method, and an optimizer. It returns a solution dictionary. The `TimeDecomposition` method itself requires four arguments: `time_window`, `time_increment`, `inner_method`, and `formulation`. These arguments define the time window for each sub-problem, the time increment to move to the next sub-problem, the method used to solve each sub-problem, and the formulation employed, respectively. + +The code snippet below illustrates an example of solving an instance by decomposing the model into multiple 36-hour sub-problems using the `XavQiuWanThi2019` method. Each sub-problem advances 24 hours at a time. The first sub-problem covers time steps 1 to 36, the second covers time steps 25 to 60, the third covers time steps 49 to 84, and so on. The initial power levels and statuses of the second and subsequent sub-problems are set based on the results of the first 24 hours from each of their immediate prior sub-problems. In essence, this approach addresses the complexity of solving a large problem by tackling it in 24-hour intervals, while incorporating an additional 12-hour buffer to mitigate the closing window effect for each sub-problem. + + +```julia +using UnitCommitment, Cbc + +import UnitCommitment: + TimeDecomposition, + Formulation, + XavQiuWanThi2019 + +instance = UnitCommitment.read("instance.json") + +solution = UnitCommitment.optimize!( + instance, + TimeDecomposition( + time_window = 36, # solve 36h problems + time_increment = 24, # advance by 24h each time + inner_method = XavQiuWanThi2019.Method(), + formulation = Formulation(), + ), + optimizer=Cbc.Optimizer +) ``` \ No newline at end of file From 6f9420874db7fb6dffee83e37cf5c7c30767b3fb Mon Sep 17 00:00:00 2001 From: Jun He Date: Fri, 19 May 2023 13:57:33 -0400 Subject: [PATCH 04/17] added more comments --- .../methods/TimeDecomposition/optimize.jl | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/solution/methods/TimeDecomposition/optimize.jl b/src/solution/methods/TimeDecomposition/optimize.jl index 4a93545..20ac850 100644 --- a/src/solution/methods/TimeDecomposition/optimize.jl +++ b/src/solution/methods/TimeDecomposition/optimize.jl @@ -89,6 +89,16 @@ function optimize!( return solution end +""" + _set_initial_status!( + instance::UnitCommitmentInstance, + solution::OrderedDict, + time_increment::Int, + ) + +Set the thermal units' initial power levels and statuses based on the first bunch of time slots +specified by time_increment in the solution dictionary. +""" function _set_initial_status!( instance::UnitCommitmentInstance, solution::OrderedDict, @@ -114,6 +124,16 @@ function _set_initial_status!( end end +""" + _determine_initial_status( + prev_initial_status::Union{Float64,Int}, + status_sequence::Vector{Float64}, + time_increment::Int, + )::Union{Float64,Int} + +Determines a thermal unit's initial status based on its previous initial status, and +the on/off statuses in first bunch of time slots. +""" function _determine_initial_status( prev_initial_status::Union{Float64,Int}, status_sequence::Vector{Float64}, @@ -140,6 +160,19 @@ function _determine_initial_status( return on_status + off_status end +""" + _update_solution!( + solution::OrderedDict, + sub_solution::OrderedDict, + time_increment::Int, + ) + +Updates the solution (of each scenario) by concatenating the first bunch of +time slots of the newly generated sub-solution to the end of the final solution dictionary. +This function traverses through the dictionary keys, finds the vector and finally +does the concatenation. For now, the function is hardcoded to traverse at most 3 layers +of depth until it finds a vector object. +""" function _update_solution!( solution::OrderedDict, sub_solution::OrderedDict, From 7c41a9761ca1b8f5d1d9bf989b22de6acb6705b4 Mon Sep 17 00:00:00 2001 From: Jun He Date: Fri, 19 May 2023 13:57:56 -0400 Subject: [PATCH 05/17] warning on nested time decomp --- docs/src/usage.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/src/usage.md b/docs/src/usage.md index 5f3914f..c0447f8 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -233,6 +233,10 @@ The `optimize!` function takes three parameters: a unit commitment instance, a ` The code snippet below illustrates an example of solving an instance by decomposing the model into multiple 36-hour sub-problems using the `XavQiuWanThi2019` method. Each sub-problem advances 24 hours at a time. The first sub-problem covers time steps 1 to 36, the second covers time steps 25 to 60, the third covers time steps 49 to 84, and so on. The initial power levels and statuses of the second and subsequent sub-problems are set based on the results of the first 24 hours from each of their immediate prior sub-problems. In essence, this approach addresses the complexity of solving a large problem by tackling it in 24-hour intervals, while incorporating an additional 12-hour buffer to mitigate the closing window effect for each sub-problem. +!!! warning + + Specifying `TimeDecomposition` as the value of the `inner_method` field of another `TimeDecomposition` causes errors when calling the `optimize!` function due to the different argument structures between the two `optimize!` functions. + ```julia using UnitCommitment, Cbc From ec2d56602b734db4dfbae2daab167bea59cca2eb Mon Sep 17 00:00:00 2001 From: Jun He Date: Sat, 20 May 2023 12:13:28 -0400 Subject: [PATCH 06/17] updated the warning block syntax --- docs/src/usage.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/src/usage.md b/docs/src/usage.md index c0447f8..7eb369b 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -233,10 +233,8 @@ The `optimize!` function takes three parameters: a unit commitment instance, a ` The code snippet below illustrates an example of solving an instance by decomposing the model into multiple 36-hour sub-problems using the `XavQiuWanThi2019` method. Each sub-problem advances 24 hours at a time. The first sub-problem covers time steps 1 to 36, the second covers time steps 25 to 60, the third covers time steps 49 to 84, and so on. The initial power levels and statuses of the second and subsequent sub-problems are set based on the results of the first 24 hours from each of their immediate prior sub-problems. In essence, this approach addresses the complexity of solving a large problem by tackling it in 24-hour intervals, while incorporating an additional 12-hour buffer to mitigate the closing window effect for each sub-problem. -!!! warning - - Specifying `TimeDecomposition` as the value of the `inner_method` field of another `TimeDecomposition` causes errors when calling the `optimize!` function due to the different argument structures between the two `optimize!` functions. - +> **Warning** +> Specifying `TimeDecomposition` as the value of the `inner_method` field of another `TimeDecomposition` causes errors when calling the `optimize!` function due to the different argument structures between the two `optimize!` functions. ```julia using UnitCommitment, Cbc From 7201acde78f8462060c1ee72818380dd967bb79f Mon Sep 17 00:00:00 2001 From: Jun He Date: Sat, 27 May 2023 14:49:43 -0400 Subject: [PATCH 07/17] time decomp bug fix --- docs/src/usage.md | 2 +- src/instance/read.jl | 21 +- src/instance/structs.jl | 1 + .../methods/TimeDecomposition/optimize.jl | 23 +- test/instance/read_test.jl | 1 + .../TimeDecomposition/initial_status_test.jl | 198 ++++++------------ 6 files changed, 93 insertions(+), 153 deletions(-) diff --git a/docs/src/usage.md b/docs/src/usage.md index 7eb369b..fde2833 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -233,7 +233,7 @@ The `optimize!` function takes three parameters: a unit commitment instance, a ` The code snippet below illustrates an example of solving an instance by decomposing the model into multiple 36-hour sub-problems using the `XavQiuWanThi2019` method. Each sub-problem advances 24 hours at a time. The first sub-problem covers time steps 1 to 36, the second covers time steps 25 to 60, the third covers time steps 49 to 84, and so on. The initial power levels and statuses of the second and subsequent sub-problems are set based on the results of the first 24 hours from each of their immediate prior sub-problems. In essence, this approach addresses the complexity of solving a large problem by tackling it in 24-hour intervals, while incorporating an additional 12-hour buffer to mitigate the closing window effect for each sub-problem. -> **Warning** +> **Warning** > Specifying `TimeDecomposition` as the value of the `inner_method` field of another `TimeDecomposition` causes errors when calling the `optimize!` function due to the different argument structures between the two `optimize!` functions. ```julia diff --git a/src/instance/read.jl b/src/instance/read.jl index 024e478..8c33657 100644 --- a/src/instance/read.jl +++ b/src/instance/read.jl @@ -142,16 +142,28 @@ function _from_json(json; repair = true)::UnitCommitmentScenario return x end - time_horizon = json["Parameters"]["Time (h)"] + time_horizon = json["Parameters"]["Time horizon (min)"] if time_horizon === nothing - time_horizon = json["Parameters"]["Time horizon (h)"] + time_horizon = json["Parameters"]["Time (h)"] + if time_horizon === nothing + time_horizon = json["Parameters"]["Time horizon (h)"] + end + if time_horizon !== nothing + time_horizon *= 60 + end end - time_horizon !== nothing || error("Missing parameter: Time horizon (h)") + time_horizon !== nothing || error("Missing parameter: Time horizon (min)") + isinteger(time_horizon) || + error("Time horizon must be an integer in minutes") + time_horizon = Int(time_horizon) time_step = scalar(json["Parameters"]["Time step (min)"], default = 60) (60 % time_step == 0) || error("Time step $time_step is not a divisor of 60") + (time_horizon % time_step == 0) || error( + "Time step $time_step is not a divisor of time horizon $time_horizon", + ) time_multiplier = 60 ÷ time_step - T = time_horizon * time_multiplier + T = time_horizon ÷ time_step probability = json["Parameters"]["Scenario weight"] probability !== nothing || (probability = 1) @@ -408,6 +420,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario reserves = reserves, reserves_by_name = name_to_reserve, time = T, + time_step = time_step, 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), diff --git a/src/instance/structs.jl b/src/instance/structs.jl index 8323a15..39725af 100644 --- a/src/instance/structs.jl +++ b/src/instance/structs.jl @@ -104,6 +104,7 @@ Base.@kwdef mutable struct UnitCommitmentScenario thermal_units_by_name::Dict{AbstractString,ThermalUnit} thermal_units::Vector{ThermalUnit} time::Int + time_step::Int end Base.@kwdef mutable struct UnitCommitmentInstance diff --git a/src/solution/methods/TimeDecomposition/optimize.jl b/src/solution/methods/TimeDecomposition/optimize.jl index 20ac850..840846e 100644 --- a/src/solution/methods/TimeDecomposition/optimize.jl +++ b/src/solution/methods/TimeDecomposition/optimize.jl @@ -47,7 +47,6 @@ function optimize!( # get instance total length T = instance.time solution = OrderedDict() - iter = 0 if length(instance.scenarios) > 1 for sc in instance.scenarios solution[sc.name] = OrderedDict() @@ -55,10 +54,6 @@ function optimize!( end # for each iteration, time increment by method.time_increment for t_start in 1:method.time_increment:T - # set the initial status - if iter > 0 - _set_initial_status!(instance, solution, method.time_increment) - end t_end = t_start + method.time_window - 1 # if t_end exceed total T t_end = t_end > T ? T : t_end @@ -84,7 +79,8 @@ function optimize!( ) end end - iter += 1 # increment iteration counter + # set the initial status for the next sub-problem + _set_initial_status!(instance, solution, method.time_increment) end return solution end @@ -96,7 +92,7 @@ end time_increment::Int, ) -Set the thermal units' initial power levels and statuses based on the first bunch of time slots +Set the thermal units' initial power levels and statuses based on the last bunch of time slots specified by time_increment in the solution dictionary. """ function _set_initial_status!( @@ -114,11 +110,10 @@ function _set_initial_status!( solution[sc.name]["Thermal production (MW)"][thermal_unit.name] is_on = solution[sc.name]["Is on"][thermal_unit.name] end - thermal_unit.initial_power = prod[time_increment] + thermal_unit.initial_power = prod[end] thermal_unit.initial_status = _determine_initial_status( thermal_unit.initial_status, - is_on, - time_increment, + is_on[end-time_increment+1:end], ) end end @@ -128,16 +123,14 @@ end _determine_initial_status( prev_initial_status::Union{Float64,Int}, status_sequence::Vector{Float64}, - time_increment::Int, )::Union{Float64,Int} Determines a thermal unit's initial status based on its previous initial status, and -the on/off statuses in first bunch of time slots. +the on/off statuses in the last operation. """ function _determine_initial_status( prev_initial_status::Union{Float64,Int}, status_sequence::Vector{Float64}, - time_increment::Int, )::Union{Float64,Int} # initialize the two flags on_status = prev_initial_status @@ -147,8 +140,8 @@ function _determine_initial_status( # if the on_status < 0, set it to 1.0 # at each time if the unit is off, reset on_status, decrement off_status # if the off_status > 0, set it to -1.0 - for t in 1:time_increment - if status_sequence[t] == 1.0 + for status in status_sequence + if status == 1.0 on_status = on_status < 0.0 ? 1.0 : on_status + 1.0 off_status = 0.0 else diff --git a/test/instance/read_test.jl b/test/instance/read_test.jl index 1ad347b..5adb774 100644 --- a/test/instance/read_test.jl +++ b/test/instance/read_test.jl @@ -20,6 +20,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip @test length(sc.contingencies) == 19 @test length(sc.price_sensitive_loads) == 1 @test instance.time == 4 + @test sc.time_step == 60 @test sc.lines[5].name == "l5" @test sc.lines[5].source.name == "b2" diff --git a/test/solution/methods/TimeDecomposition/initial_status_test.jl b/test/solution/methods/TimeDecomposition/initial_status_test.jl index cd253d8..1e0f17f 100644 --- a/test/solution/methods/TimeDecomposition/initial_status_test.jl +++ b/test/solution/methods/TimeDecomposition/initial_status_test.jl @@ -5,134 +5,66 @@ using UnitCommitment, DataStructures @testset "determine_initial_status" begin - t_increment = 24 - t_model = 36 hot_start = 100 cold_start = -100 # all on throughout - stat_seq = ones(t_model) + stat_seq = ones(36) # hot start - new_stat = UnitCommitment._determine_initial_status( - hot_start, - stat_seq, - t_increment, - ) - @test new_stat == 124 + new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) + @test new_stat == 136 # cold start - new_stat = UnitCommitment._determine_initial_status( - cold_start, - stat_seq, - t_increment, - ) - @test new_stat == 24 + new_stat = UnitCommitment._determine_initial_status(cold_start, stat_seq) + @test new_stat == 36 # off in the last 12 periods - stat_seq = ones(t_model) + stat_seq = ones(36) stat_seq[25:end] .= 0 # hot start - new_stat = UnitCommitment._determine_initial_status( - hot_start, - stat_seq, - t_increment, - ) - @test new_stat == 124 + new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) + @test new_stat == -12 # cold start - new_stat = UnitCommitment._determine_initial_status( - cold_start, - stat_seq, - t_increment, - ) - @test new_stat == 24 + new_stat = UnitCommitment._determine_initial_status(cold_start, stat_seq) + @test new_stat == -12 - # off in one of the first 24 periods - stat_seq = ones(t_model) + # off in one period + stat_seq = ones(36) stat_seq[10] = 0 # hot start - new_stat = UnitCommitment._determine_initial_status( - hot_start, - stat_seq, - t_increment, - ) - @test new_stat == 14 + new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) + @test new_stat == 26 # cold start - new_stat = UnitCommitment._determine_initial_status( - cold_start, - stat_seq, - t_increment, - ) - @test new_stat == 14 + new_stat = UnitCommitment._determine_initial_status(cold_start, stat_seq) + @test new_stat == 26 # off in several of the first 24 periods - stat_seq = ones(t_model) + stat_seq = ones(36) stat_seq[[10, 11, 20]] .= 0 # hot start - new_stat = UnitCommitment._determine_initial_status( - hot_start, - stat_seq, - t_increment, - ) - @test new_stat == 4 + new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) + @test new_stat == 16 # cold start - new_stat = UnitCommitment._determine_initial_status( - cold_start, - stat_seq, - t_increment, - ) - @test new_stat == 4 - - # off in several of the first 24 periods - stat_seq = ones(t_model) - stat_seq[20:24] .= 0 - # hot start - new_stat = UnitCommitment._determine_initial_status( - hot_start, - stat_seq, - t_increment, - ) - @test new_stat == -5 - # cold start - new_stat = UnitCommitment._determine_initial_status( - cold_start, - stat_seq, - t_increment, - ) - @test new_stat == -5 + new_stat = UnitCommitment._determine_initial_status(cold_start, stat_seq) + @test new_stat == 16 # all off throughout - stat_seq = zeros(t_model) + stat_seq = zeros(36) # hot start - new_stat = UnitCommitment._determine_initial_status( - hot_start, - stat_seq, - t_increment, - ) - @test new_stat == -24 + new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) + @test new_stat == -36 # cold start - new_stat = UnitCommitment._determine_initial_status( - cold_start, - stat_seq, - t_increment, - ) - @test new_stat == -124 + new_stat = UnitCommitment._determine_initial_status(cold_start, stat_seq) + @test new_stat == -136 # on in the last 12 periods - stat_seq = zeros(t_model) + stat_seq = zeros(36) stat_seq[25:end] .= 1 # hot start - new_stat = UnitCommitment._determine_initial_status( - hot_start, - stat_seq, - t_increment, - ) - @test new_stat == -24 + new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) + @test new_stat == 12 # cold start - new_stat = UnitCommitment._determine_initial_status( - cold_start, - stat_seq, - t_increment, - ) - @test new_stat == -124 + new_stat = UnitCommitment._determine_initial_status(cold_start, stat_seq) + @test new_stat == 12 end @testset "set_initial_status" begin @@ -140,28 +72,28 @@ end instance = UnitCommitment.read("$FIXTURES/case14.json.gz") psuedo_solution = OrderedDict( "Thermal production (MW)" => OrderedDict( - "g1" => [110.0, 112.0, 114.0, 116.0], - "g2" => [100.0, 102.0, 0.0, 0.0], + "g1" => [0.0, 112.0, 114.0, 116.0], + "g2" => [0.0, 102.0, 0.0, 0.0], "g3" => [0.0, 0.0, 0.0, 0.0], - "g4" => [33.0, 34.0, 66.0, 99.0], - "g5" => [33.0, 34.0, 66.0, 99.0], - "g6" => [100.0, 100.0, 100.0, 100.0], + "g4" => [0.0, 34.0, 66.0, 99.0], + "g5" => [0.0, 34.0, 66.0, 99.0], + "g6" => [0.0, 100.0, 100.0, 100.0], ), "Is on" => OrderedDict( - "g1" => [1.0, 1.0, 1.0, 1.0], - "g2" => [1.0, 1.0, 0.0, 0.0], + "g1" => [0.0, 1.0, 1.0, 1.0], + "g2" => [0.0, 1.0, 0.0, 0.0], "g3" => [0.0, 0.0, 0.0, 0.0], - "g4" => [1.0, 1.0, 1.0, 1.0], - "g5" => [1.0, 1.0, 1.0, 1.0], - "g6" => [1.0, 1.0, 1.0, 1.0], + "g4" => [0.0, 1.0, 1.0, 1.0], + "g5" => [0.0, 1.0, 1.0, 1.0], + "g6" => [0.0, 1.0, 1.0, 1.0], ), ) UnitCommitment._set_initial_status!(instance, psuedo_solution, 3) thermal_units = instance.scenarios[1].thermal_units - @test thermal_units[1].initial_power == 114.0 + @test thermal_units[1].initial_power == 116.0 @test thermal_units[1].initial_status == 3.0 @test thermal_units[2].initial_power == 0.0 - @test thermal_units[2].initial_status == -1.0 + @test thermal_units[2].initial_status == -2.0 @test thermal_units[3].initial_power == 0.0 @test thermal_units[3].initial_status == -9.0 @@ -173,47 +105,47 @@ end psuedo_solution = OrderedDict( "case14" => OrderedDict( "Thermal production (MW)" => OrderedDict( - "g1" => [110.0, 112.0, 114.0, 116.0], - "g2" => [100.0, 102.0, 0.0, 0.0], + "g1" => [0.0, 112.0, 114.0, 116.0], + "g2" => [0.0, 102.0, 0.0, 0.0], "g3" => [0.0, 0.0, 0.0, 0.0], - "g4" => [33.0, 34.0, 66.0, 99.0], - "g5" => [33.0, 34.0, 66.0, 99.0], - "g6" => [100.0, 100.0, 100.0, 100.0], + "g4" => [0.0, 34.0, 66.0, 99.0], + "g5" => [0.0, 34.0, 66.0, 99.0], + "g6" => [0.0, 100.0, 100.0, 100.0], ), "Is on" => OrderedDict( - "g1" => [1.0, 1.0, 1.0, 1.0], - "g2" => [1.0, 1.0, 0.0, 0.0], + "g1" => [0.0, 1.0, 1.0, 1.0], + "g2" => [0.0, 1.0, 0.0, 0.0], "g3" => [0.0, 0.0, 0.0, 0.0], - "g4" => [1.0, 1.0, 1.0, 1.0], - "g5" => [1.0, 1.0, 1.0, 1.0], - "g6" => [1.0, 1.0, 1.0, 1.0], + "g4" => [0.0, 1.0, 1.0, 1.0], + "g5" => [0.0, 1.0, 1.0, 1.0], + "g6" => [0.0, 1.0, 1.0, 1.0], ), ), "case14-profiled" => OrderedDict( "Thermal production (MW)" => OrderedDict( - "g1" => [112.0, 113.0, 116.0, 115.0], + "g1" => [0.0, 113.0, 116.0, 115.0], "g2" => [0.0, 0.0, 0.0, 0.0], "g3" => [0.0, 0.0, 0.0, 20.0], - "g4" => [33.0, 34.0, 66.0, 99.0], - "g5" => [33.0, 34.0, 66.0, 99.0], - "g6" => [100.0, 100.0, 100.0, 100.0], + "g4" => [0.0, 34.0, 66.0, 98.0], + "g5" => [0.0, 34.0, 66.0, 97.0], + "g6" => [0.0, 100.0, 100.0, 100.0], ), "Is on" => OrderedDict( - "g1" => [1.0, 1.0, 1.0, 1.0], + "g1" => [0.0, 1.0, 1.0, 1.0], "g2" => [0.0, 0.0, 0.0, 0.0], - "g3" => [0.0, 0.0, 0.0, 0.0], - "g4" => [1.0, 1.0, 1.0, 1.0], - "g5" => [1.0, 1.0, 1.0, 1.0], - "g6" => [1.0, 1.0, 1.0, 1.0], + "g3" => [0.0, 0.0, 0.0, 1.0], + "g4" => [0.0, 1.0, 1.0, 1.0], + "g5" => [0.0, 1.0, 1.0, 1.0], + "g6" => [0.0, 1.0, 1.0, 1.0], ), ), ) UnitCommitment._set_initial_status!(instance, psuedo_solution, 3) thermal_units_sc2 = instance.scenarios[2].thermal_units - @test thermal_units_sc2[1].initial_power == 116.0 + @test thermal_units_sc2[1].initial_power == 115.0 @test thermal_units_sc2[1].initial_status == 3.0 @test thermal_units_sc2[2].initial_power == 0.0 @test thermal_units_sc2[2].initial_status == -11.0 - @test thermal_units_sc2[3].initial_power == 0.0 - @test thermal_units_sc2[3].initial_status == -9.0 + @test thermal_units_sc2[3].initial_power == 20.0 + @test thermal_units_sc2[3].initial_status == 1.0 end From 53052ec8950d528bf5c6efc7a47fe9c9d2a1faf5 Mon Sep 17 00:00:00 2001 From: Jun He Date: Sat, 27 May 2023 15:43:39 -0400 Subject: [PATCH 08/17] standalone test integration --- test/instance/read_test.jl | 167 ------------------ test/runtests.jl | 53 ------ .../TimeDecomposition/initial_status_test.jl | 151 ---------------- .../TimeDecomposition/optimize_test.jl | 52 ------ .../TimeDecomposition/update_solution_test.jl | 49 ----- test/src/UnitCommitmentT.jl | 6 + test/src/instance/read_test.jl | 1 + .../TimeDecomposition/initial_status_test.jl | 159 +++++++++++++++++ .../TimeDecomposition/optimize_test.jl | 60 +++++++ .../TimeDecomposition/update_solution_test.jl | 55 ++++++ 10 files changed, 281 insertions(+), 472 deletions(-) delete mode 100644 test/instance/read_test.jl delete mode 100644 test/runtests.jl delete mode 100644 test/solution/methods/TimeDecomposition/initial_status_test.jl delete mode 100644 test/solution/methods/TimeDecomposition/optimize_test.jl delete mode 100644 test/solution/methods/TimeDecomposition/update_solution_test.jl create mode 100644 test/src/solution/methods/TimeDecomposition/initial_status_test.jl create mode 100644 test/src/solution/methods/TimeDecomposition/optimize_test.jl create mode 100644 test/src/solution/methods/TimeDecomposition/update_solution_test.jl diff --git a/test/instance/read_test.jl b/test/instance/read_test.jl deleted file mode 100644 index 5adb774..0000000 --- a/test/instance/read_test.jl +++ /dev/null @@ -1,167 +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. - -using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip - -@testset "read_benchmark" begin - instance = UnitCommitment.read("$FIXTURES/case14.json.gz") - - @test repr(instance) == ( - "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.thermal_units) == 6 - @test length(sc.contingencies) == 19 - @test length(sc.price_sensitive_loads) == 1 - @test instance.time == 4 - @test sc.time_step == 60 - - @test sc.lines[5].name == "l5" - @test sc.lines[5].source.name == "b2" - @test sc.lines[5].target.name == "b5" - @test sc.lines[5].reactance ≈ 0.17388 - @test sc.lines[5].susceptance ≈ 10.037550333 - @test sc.lines[5].normal_flow_limit == [1e8 for t in 1:4] - @test sc.lines[5].emergency_flow_limit == [1e8 for t in 1:4] - @test sc.lines[5].flow_limit_penalty == [5e3 for t in 1:4] - @test sc.lines_by_name["l5"].name == "l5" - - @test sc.lines[1].name == "l1" - @test sc.lines[1].source.name == "b1" - @test sc.lines[1].target.name == "b2" - @test sc.lines[1].reactance ≈ 0.059170 - @test sc.lines[1].susceptance ≈ 29.496860773945 - @test sc.lines[1].normal_flow_limit == [300.0 for t in 1:4] - @test sc.lines[1].emergency_flow_limit == [400.0 for t in 1:4] - @test sc.lines[1].flow_limit_penalty == [1e3 for t in 1:4] - - @test sc.buses[9].name == "b9" - @test sc.buses[9].load == [35.36638, 33.25495, 31.67138, 31.14353] - @test sc.buses_by_name["b9"].name == "b9" - - @test sc.reserves[1].name == "r1" - @test sc.reserves[1].type == "spinning" - @test sc.reserves[1].amount == [100.0, 100.0, 100.0, 100.0] - @test sc.reserves_by_name["r1"].name == "r1" - - unit = sc.thermal_units[1] - @test unit.name == "g1" - @test unit.bus.name == "b1" - @test unit.ramp_up_limit == 1e6 - @test unit.ramp_down_limit == 1e6 - @test unit.startup_limit == 1e6 - @test unit.shutdown_limit == 1e6 - @test unit.must_run == [false for t in 1:4] - @test unit.min_power_cost == [1400.0 for t in 1:4] - @test unit.min_uptime == 1 - @test unit.min_downtime == 1 - for t in 1:1 - @test unit.cost_segments[1].mw[t] == 10.0 - @test unit.cost_segments[2].mw[t] == 20.0 - @test unit.cost_segments[3].mw[t] == 5.0 - @test unit.cost_segments[1].cost[t] ≈ 20.0 - @test unit.cost_segments[2].cost[t] ≈ 30.0 - @test unit.cost_segments[3].cost[t] ≈ 40.0 - end - @test length(unit.startup_categories) == 3 - @test unit.startup_categories[1].delay == 1 - @test unit.startup_categories[2].delay == 2 - @test unit.startup_categories[3].delay == 3 - @test unit.startup_categories[1].cost == 1000.0 - @test unit.startup_categories[2].cost == 1500.0 - @test unit.startup_categories[3].cost == 2000.0 - @test length(unit.reserves) == 0 - @test sc.thermal_units_by_name["g1"].name == "g1" - - 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.thermal_units[3] - @test unit.name == "g3" - @test unit.bus.name == "b3" - @test unit.ramp_up_limit == 70.0 - @test unit.ramp_down_limit == 70.0 - @test unit.startup_limit == 70.0 - @test unit.shutdown_limit == 70.0 - @test unit.must_run == [true for t in 1:4] - @test unit.min_power_cost == [0.0 for t in 1:4] - @test unit.min_uptime == 1 - @test unit.min_downtime == 1 - for t in 1:4 - @test unit.cost_segments[1].mw[t] ≈ 33 - @test unit.cost_segments[2].mw[t] ≈ 33 - @test unit.cost_segments[3].mw[t] ≈ 34 - @test unit.cost_segments[1].cost[t] ≈ 33.75 - @test unit.cost_segments[2].cost[t] ≈ 38.04 - @test unit.cost_segments[3].cost[t] ≈ 44.77853 - end - @test length(unit.reserves) == 1 - @test unit.reserves[1].name == "r1" - - @test sc.contingencies[1].lines == [sc.lines[1]] - @test sc.contingencies[1].thermal_units == [] - @test sc.contingencies[1].name == "c1" - @test sc.contingencies_by_name["c1"].name == "c1" - - load = sc.price_sensitive_loads[1] - @test load.name == "ps1" - @test load.bus.name == "b3" - @test load.revenue == [100.0 for t in 1:4] - @test load.demand == [50.0 for t in 1:4] - @test sc.price_sensitive_loads_by_name["ps1"].name == "ps1" -end - -@testset "read_benchmark sub-hourly" begin - instance = UnitCommitment.read("$FIXTURES/case14-sub-hourly.json.gz") - @test instance.time == 4 - unit = instance.scenarios[1].thermal_units[1] - @test unit.name == "g1" - @test unit.min_uptime == 2 - @test unit.min_downtime == 2 - @test length(unit.startup_categories) == 3 - @test unit.startup_categories[1].delay == 2 - @test unit.startup_categories[2].delay == 4 - @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.min_power == [60.0 for t in 1:4] - @test first_pu.max_power == [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.min_power == [0.0 for t in 1:4] - @test second_pu.max_power == [120.0 for t in 1:4] - @test sc.profiled_units_by_name["g8"].name == "g8" -end - -@testset "read_benchmark commitmemt-status" begin - instance = UnitCommitment.read("$FIXTURES/case14-fixed-status.json.gz") - sc = instance.scenarios[1] - - @test sc.thermal_units[1].commitment_status == [nothing for t in 1:4] - @test sc.thermal_units[2].commitment_status == [true for t in 1:4] - @test sc.thermal_units[4].commitment_status == [false for t in 1:4] - @test sc.thermal_units[6].commitment_status == - [false, nothing, true, nothing] -end diff --git a/test/runtests.jl b/test/runtests.jl deleted file mode 100644 index a98fa79..0000000 --- a/test/runtests.jl +++ /dev/null @@ -1,53 +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. - -using Test -using UnitCommitment - -push!(Base.LOAD_PATH, @__DIR__) -UnitCommitment._setup_logger(level = Base.CoreLogging.Error) - -FIXTURES = "$(@__DIR__)/fixtures" - -@testset "UnitCommitment" begin - include("usage.jl") - @testset "import" begin - include("import/egret_test.jl") - end - @testset "instance" begin - include("instance/read_test.jl") - include("instance/migrate_test.jl") - end - @testset "model" begin - include("model/formulations_test.jl") - end - @testset "solution" begin - @testset "XavQiuWanThi19" begin - include("solution/methods/XavQiuWanThi19/filter_test.jl") - include("solution/methods/XavQiuWanThi19/find_test.jl") - include("solution/methods/XavQiuWanThi19/sensitivity_test.jl") - end - @testset "TimeDecomposition" begin - include("solution/methods/TimeDecomposition/initial_status_test.jl") - include( - "solution/methods/TimeDecomposition/update_solution_test.jl", - ) - include("solution/methods/TimeDecomposition/optimize_test.jl") - end - end - @testset "transform" begin - include("transform/initcond_test.jl") - include("transform/slice_test.jl") - @testset "randomize" begin - include("transform/randomize/XavQiuAhm2021_test.jl") - end - end - @testset "validation" begin - include("validation/repair_test.jl") - end - @testset "lmp" begin - include("lmp/conventional_test.jl") - include("lmp/aelmp_test.jl") - end -end diff --git a/test/solution/methods/TimeDecomposition/initial_status_test.jl b/test/solution/methods/TimeDecomposition/initial_status_test.jl deleted file mode 100644 index 1e0f17f..0000000 --- a/test/solution/methods/TimeDecomposition/initial_status_test.jl +++ /dev/null @@ -1,151 +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. - -using UnitCommitment, DataStructures - -@testset "determine_initial_status" begin - hot_start = 100 - cold_start = -100 - - # all on throughout - stat_seq = ones(36) - # hot start - new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) - @test new_stat == 136 - # cold start - new_stat = UnitCommitment._determine_initial_status(cold_start, stat_seq) - @test new_stat == 36 - - # off in the last 12 periods - stat_seq = ones(36) - stat_seq[25:end] .= 0 - # hot start - new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) - @test new_stat == -12 - # cold start - new_stat = UnitCommitment._determine_initial_status(cold_start, stat_seq) - @test new_stat == -12 - - # off in one period - stat_seq = ones(36) - stat_seq[10] = 0 - # hot start - new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) - @test new_stat == 26 - # cold start - new_stat = UnitCommitment._determine_initial_status(cold_start, stat_seq) - @test new_stat == 26 - - # off in several of the first 24 periods - stat_seq = ones(36) - stat_seq[[10, 11, 20]] .= 0 - # hot start - new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) - @test new_stat == 16 - # cold start - new_stat = UnitCommitment._determine_initial_status(cold_start, stat_seq) - @test new_stat == 16 - - # all off throughout - stat_seq = zeros(36) - # hot start - new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) - @test new_stat == -36 - # cold start - new_stat = UnitCommitment._determine_initial_status(cold_start, stat_seq) - @test new_stat == -136 - - # on in the last 12 periods - stat_seq = zeros(36) - stat_seq[25:end] .= 1 - # hot start - new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) - @test new_stat == 12 - # cold start - new_stat = UnitCommitment._determine_initial_status(cold_start, stat_seq) - @test new_stat == 12 -end - -@testset "set_initial_status" begin - # read one scenario - instance = UnitCommitment.read("$FIXTURES/case14.json.gz") - psuedo_solution = OrderedDict( - "Thermal production (MW)" => OrderedDict( - "g1" => [0.0, 112.0, 114.0, 116.0], - "g2" => [0.0, 102.0, 0.0, 0.0], - "g3" => [0.0, 0.0, 0.0, 0.0], - "g4" => [0.0, 34.0, 66.0, 99.0], - "g5" => [0.0, 34.0, 66.0, 99.0], - "g6" => [0.0, 100.0, 100.0, 100.0], - ), - "Is on" => OrderedDict( - "g1" => [0.0, 1.0, 1.0, 1.0], - "g2" => [0.0, 1.0, 0.0, 0.0], - "g3" => [0.0, 0.0, 0.0, 0.0], - "g4" => [0.0, 1.0, 1.0, 1.0], - "g5" => [0.0, 1.0, 1.0, 1.0], - "g6" => [0.0, 1.0, 1.0, 1.0], - ), - ) - UnitCommitment._set_initial_status!(instance, psuedo_solution, 3) - thermal_units = instance.scenarios[1].thermal_units - @test thermal_units[1].initial_power == 116.0 - @test thermal_units[1].initial_status == 3.0 - @test thermal_units[2].initial_power == 0.0 - @test thermal_units[2].initial_status == -2.0 - @test thermal_units[3].initial_power == 0.0 - @test thermal_units[3].initial_status == -9.0 - - # read multiple scenarios - instance = UnitCommitment.read([ - "$FIXTURES/case14.json.gz", - "$FIXTURES/case14-profiled.json.gz", - ]) - psuedo_solution = OrderedDict( - "case14" => OrderedDict( - "Thermal production (MW)" => OrderedDict( - "g1" => [0.0, 112.0, 114.0, 116.0], - "g2" => [0.0, 102.0, 0.0, 0.0], - "g3" => [0.0, 0.0, 0.0, 0.0], - "g4" => [0.0, 34.0, 66.0, 99.0], - "g5" => [0.0, 34.0, 66.0, 99.0], - "g6" => [0.0, 100.0, 100.0, 100.0], - ), - "Is on" => OrderedDict( - "g1" => [0.0, 1.0, 1.0, 1.0], - "g2" => [0.0, 1.0, 0.0, 0.0], - "g3" => [0.0, 0.0, 0.0, 0.0], - "g4" => [0.0, 1.0, 1.0, 1.0], - "g5" => [0.0, 1.0, 1.0, 1.0], - "g6" => [0.0, 1.0, 1.0, 1.0], - ), - ), - "case14-profiled" => OrderedDict( - "Thermal production (MW)" => OrderedDict( - "g1" => [0.0, 113.0, 116.0, 115.0], - "g2" => [0.0, 0.0, 0.0, 0.0], - "g3" => [0.0, 0.0, 0.0, 20.0], - "g4" => [0.0, 34.0, 66.0, 98.0], - "g5" => [0.0, 34.0, 66.0, 97.0], - "g6" => [0.0, 100.0, 100.0, 100.0], - ), - "Is on" => OrderedDict( - "g1" => [0.0, 1.0, 1.0, 1.0], - "g2" => [0.0, 0.0, 0.0, 0.0], - "g3" => [0.0, 0.0, 0.0, 1.0], - "g4" => [0.0, 1.0, 1.0, 1.0], - "g5" => [0.0, 1.0, 1.0, 1.0], - "g6" => [0.0, 1.0, 1.0, 1.0], - ), - ), - ) - UnitCommitment._set_initial_status!(instance, psuedo_solution, 3) - thermal_units_sc2 = instance.scenarios[2].thermal_units - @test thermal_units_sc2[1].initial_power == 115.0 - @test thermal_units_sc2[1].initial_status == 3.0 - @test thermal_units_sc2[2].initial_power == 0.0 - @test thermal_units_sc2[2].initial_status == -11.0 - @test thermal_units_sc2[3].initial_power == 20.0 - @test thermal_units_sc2[3].initial_status == 1.0 -end diff --git a/test/solution/methods/TimeDecomposition/optimize_test.jl b/test/solution/methods/TimeDecomposition/optimize_test.jl deleted file mode 100644 index 9987d1e..0000000 --- a/test/solution/methods/TimeDecomposition/optimize_test.jl +++ /dev/null @@ -1,52 +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. - -using UnitCommitment, DataStructures, Cbc -import UnitCommitment: TimeDecomposition, XavQiuWanThi2019, Formulation - -@testset "optimize_time_decomposition" begin - # read one scenario - instance = UnitCommitment.read("$FIXTURES/case14.json.gz") - solution = UnitCommitment.optimize!( - instance, - TimeDecomposition( - time_window = 3, - time_increment = 2, - inner_method = XavQiuWanThi2019.Method(), - formulation = Formulation(), - ), - optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0), - ) - @test length(solution["Thermal production (MW)"]["g1"]) == 4 - @test length(solution["Is on"]["g2"]) == 4 - @test length(solution["Spinning reserve (MW)"]["r1"]["g2"]) == 4 - - # read multiple scenarios - instance = UnitCommitment.read([ - "$FIXTURES/case14.json.gz", - "$FIXTURES/case14-profiled.json.gz", - ]) - solution = UnitCommitment.optimize!( - instance, - TimeDecomposition( - time_window = 3, - time_increment = 2, - inner_method = XavQiuWanThi2019.Method(), - formulation = Formulation(), - ), - optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0), - ) - @test length(solution["case14"]["Thermal production (MW)"]["g3"]) == 4 - @test length(solution["case14"]["Is on"]["g4"]) == 4 - @test length( - solution["case14-profiled"]["Thermal production (MW)"]["g5"], - ) == 4 - @test length(solution["case14-profiled"]["Is on"]["g6"]) == 4 - @test length( - solution["case14-profiled"]["Profiled production (MW)"]["g7"], - ) == 4 - @test length( - solution["case14-profiled"]["Spinning reserve (MW)"]["r1"]["g3"], - ) == 4 -end diff --git a/test/solution/methods/TimeDecomposition/update_solution_test.jl b/test/solution/methods/TimeDecomposition/update_solution_test.jl deleted file mode 100644 index 106002e..0000000 --- a/test/solution/methods/TimeDecomposition/update_solution_test.jl +++ /dev/null @@ -1,49 +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. - -using UnitCommitment, DataStructures - -@testset "update_solution" begin - psuedo_solution = OrderedDict() - time_increment = 4 - psuedo_sub_solution = OrderedDict( - "Thermal production (MW)" => - OrderedDict("g1" => [100.0, 200.0, 300.0, 400.0, 500.0, 600.0]), - "Is on" => OrderedDict("g1" => [1.0, 0.0, 1.0, 1.0, 0.0, 1.0]), - "Profiled production (MW)" => - OrderedDict("g1" => [199.0, 299.0, 399.0, 499.0, 599.0, 699.0]), - "Spinning reserve (MW)" => OrderedDict( - "r1" => OrderedDict("g1" => [31.0, 32.0, 33.0, 34.0, 35.0, 36.0]), - ), - ) - - # first update should directly copy the first 4 entries of sub solution - UnitCommitment._update_solution!( - psuedo_solution, - psuedo_sub_solution, - time_increment, - ) - @test psuedo_solution["Thermal production (MW)"]["g1"] == - [100.0, 200.0, 300.0, 400.0] - @test psuedo_solution["Is on"]["g1"] == [1.0, 0.0, 1.0, 1.0] - @test psuedo_solution["Profiled production (MW)"]["g1"] == - [199.0, 299.0, 399.0, 499.0] - @test psuedo_solution["Spinning reserve (MW)"]["r1"]["g1"] == - [31.0, 32.0, 33.0, 34.0] - - # second update should append the first 4 entries of sub solution - UnitCommitment._update_solution!( - psuedo_solution, - psuedo_sub_solution, - time_increment, - ) - @test psuedo_solution["Thermal production (MW)"]["g1"] == - [100.0, 200.0, 300.0, 400.0, 100.0, 200.0, 300.0, 400.0] - @test psuedo_solution["Is on"]["g1"] == - [1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0] - @test psuedo_solution["Profiled production (MW)"]["g1"] == - [199.0, 299.0, 399.0, 499.0, 199.0, 299.0, 399.0, 499.0] - @test psuedo_solution["Spinning reserve (MW)"]["r1"]["g1"] == - [31.0, 32.0, 33.0, 34.0, 31.0, 32.0, 33.0, 34.0] -end diff --git a/test/src/UnitCommitmentT.jl b/test/src/UnitCommitmentT.jl index f529c42..4ea0d73 100644 --- a/test/src/UnitCommitmentT.jl +++ b/test/src/UnitCommitmentT.jl @@ -13,6 +13,9 @@ include("solution/methods/XavQiuWanThi19/filter_test.jl") include("solution/methods/XavQiuWanThi19/find_test.jl") include("solution/methods/XavQiuWanThi19/sensitivity_test.jl") include("solution/methods/ProgressiveHedging/usage_test.jl") +include("solution/methods/TimeDecomposition/initial_status_test.jl") +include("solution/methods/TimeDecomposition/optimize_test.jl") +include("solution/methods/TimeDecomposition/update_solution_test.jl") include("transform/initcond_test.jl") include("transform/slice_test.jl") include("transform/randomize/XavQiuAhm2021_test.jl") @@ -39,6 +42,9 @@ function runtests() solution_methods_XavQiuWanThi19_find_test() solution_methods_XavQiuWanThi19_sensitivity_test() solution_methods_ProgressiveHedging_usage_test() + solution_methods_TimeDecomposition_initial_status_test() + solution_methods_TimeDecomposition_optimize_test() + solution_methods_TimeDecomposition_update_solution_test() transform_initcond_test() transform_slice_test() transform_randomize_XavQiuAhm2021_test() diff --git a/test/src/instance/read_test.jl b/test/src/instance/read_test.jl index 85f44ff..c9ae1fd 100644 --- a/test/src/instance/read_test.jl +++ b/test/src/instance/read_test.jl @@ -21,6 +21,7 @@ function instance_read_test() @test length(sc.contingencies) == 19 @test length(sc.price_sensitive_loads) == 1 @test instance.time == 4 + @test sc.time_step == 60 @test sc.lines[5].name == "l5" @test sc.lines[5].source.name == "b2" diff --git a/test/src/solution/methods/TimeDecomposition/initial_status_test.jl b/test/src/solution/methods/TimeDecomposition/initial_status_test.jl new file mode 100644 index 0000000..b3a127c --- /dev/null +++ b/test/src/solution/methods/TimeDecomposition/initial_status_test.jl @@ -0,0 +1,159 @@ +# 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, DataStructures + +function solution_methods_TimeDecomposition_initial_status_test() + @testset "determine_initial_status" begin + hot_start = 100 + cold_start = -100 + + # all on throughout + stat_seq = ones(36) + # hot start + new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) + @test new_stat == 136 + # cold start + new_stat = + UnitCommitment._determine_initial_status(cold_start, stat_seq) + @test new_stat == 36 + + # off in the last 12 periods + stat_seq = ones(36) + stat_seq[25:end] .= 0 + # hot start + new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) + @test new_stat == -12 + # cold start + new_stat = + UnitCommitment._determine_initial_status(cold_start, stat_seq) + @test new_stat == -12 + + # off in one period + stat_seq = ones(36) + stat_seq[10] = 0 + # hot start + new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) + @test new_stat == 26 + # cold start + new_stat = + UnitCommitment._determine_initial_status(cold_start, stat_seq) + @test new_stat == 26 + + # off in several of the first 24 periods + stat_seq = ones(36) + stat_seq[[10, 11, 20]] .= 0 + # hot start + new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) + @test new_stat == 16 + # cold start + new_stat = + UnitCommitment._determine_initial_status(cold_start, stat_seq) + @test new_stat == 16 + + # all off throughout + stat_seq = zeros(36) + # hot start + new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) + @test new_stat == -36 + # cold start + new_stat = + UnitCommitment._determine_initial_status(cold_start, stat_seq) + @test new_stat == -136 + + # on in the last 12 periods + stat_seq = zeros(36) + stat_seq[25:end] .= 1 + # hot start + new_stat = UnitCommitment._determine_initial_status(hot_start, stat_seq) + @test new_stat == 12 + # cold start + new_stat = + UnitCommitment._determine_initial_status(cold_start, stat_seq) + @test new_stat == 12 + end + + @testset "set_initial_status" begin + # read one scenario + instance = UnitCommitment.read(fixture("case14.json.gz")) + psuedo_solution = OrderedDict( + "Thermal production (MW)" => OrderedDict( + "g1" => [0.0, 112.0, 114.0, 116.0], + "g2" => [0.0, 102.0, 0.0, 0.0], + "g3" => [0.0, 0.0, 0.0, 0.0], + "g4" => [0.0, 34.0, 66.0, 99.0], + "g5" => [0.0, 34.0, 66.0, 99.0], + "g6" => [0.0, 100.0, 100.0, 100.0], + ), + "Is on" => OrderedDict( + "g1" => [0.0, 1.0, 1.0, 1.0], + "g2" => [0.0, 1.0, 0.0, 0.0], + "g3" => [0.0, 0.0, 0.0, 0.0], + "g4" => [0.0, 1.0, 1.0, 1.0], + "g5" => [0.0, 1.0, 1.0, 1.0], + "g6" => [0.0, 1.0, 1.0, 1.0], + ), + ) + UnitCommitment._set_initial_status!(instance, psuedo_solution, 3) + thermal_units = instance.scenarios[1].thermal_units + @test thermal_units[1].initial_power == 116.0 + @test thermal_units[1].initial_status == 3.0 + @test thermal_units[2].initial_power == 0.0 + @test thermal_units[2].initial_status == -2.0 + @test thermal_units[3].initial_power == 0.0 + @test thermal_units[3].initial_status == -9.0 + + # read multiple scenarios + instance = UnitCommitment.read([ + fixture("case14.json.gz"), + fixture("case14-profiled.json.gz"), + ]) + psuedo_solution = OrderedDict( + "case14" => OrderedDict( + "Thermal production (MW)" => OrderedDict( + "g1" => [0.0, 112.0, 114.0, 116.0], + "g2" => [0.0, 102.0, 0.0, 0.0], + "g3" => [0.0, 0.0, 0.0, 0.0], + "g4" => [0.0, 34.0, 66.0, 99.0], + "g5" => [0.0, 34.0, 66.0, 99.0], + "g6" => [0.0, 100.0, 100.0, 100.0], + ), + "Is on" => OrderedDict( + "g1" => [0.0, 1.0, 1.0, 1.0], + "g2" => [0.0, 1.0, 0.0, 0.0], + "g3" => [0.0, 0.0, 0.0, 0.0], + "g4" => [0.0, 1.0, 1.0, 1.0], + "g5" => [0.0, 1.0, 1.0, 1.0], + "g6" => [0.0, 1.0, 1.0, 1.0], + ), + ), + "case14-profiled" => OrderedDict( + "Thermal production (MW)" => OrderedDict( + "g1" => [0.0, 113.0, 116.0, 115.0], + "g2" => [0.0, 0.0, 0.0, 0.0], + "g3" => [0.0, 0.0, 0.0, 20.0], + "g4" => [0.0, 34.0, 66.0, 98.0], + "g5" => [0.0, 34.0, 66.0, 97.0], + "g6" => [0.0, 100.0, 100.0, 100.0], + ), + "Is on" => OrderedDict( + "g1" => [0.0, 1.0, 1.0, 1.0], + "g2" => [0.0, 0.0, 0.0, 0.0], + "g3" => [0.0, 0.0, 0.0, 1.0], + "g4" => [0.0, 1.0, 1.0, 1.0], + "g5" => [0.0, 1.0, 1.0, 1.0], + "g6" => [0.0, 1.0, 1.0, 1.0], + ), + ), + ) + UnitCommitment._set_initial_status!(instance, psuedo_solution, 3) + thermal_units_sc2 = instance.scenarios[2].thermal_units + @test thermal_units_sc2[1].initial_power == 115.0 + @test thermal_units_sc2[1].initial_status == 3.0 + @test thermal_units_sc2[2].initial_power == 0.0 + @test thermal_units_sc2[2].initial_status == -11.0 + @test thermal_units_sc2[3].initial_power == 20.0 + @test thermal_units_sc2[3].initial_status == 1.0 + end +end diff --git a/test/src/solution/methods/TimeDecomposition/optimize_test.jl b/test/src/solution/methods/TimeDecomposition/optimize_test.jl new file mode 100644 index 0000000..a087e78 --- /dev/null +++ b/test/src/solution/methods/TimeDecomposition/optimize_test.jl @@ -0,0 +1,60 @@ +# 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, DataStructures, Cbc +import UnitCommitment: TimeDecomposition, XavQiuWanThi2019, Formulation + +function solution_methods_TimeDecomposition_optimize_test() + @testset "optimize_time_decomposition" begin + # read one scenario + instance = UnitCommitment.read(fixture("case14.json.gz")) + solution = UnitCommitment.optimize!( + instance, + TimeDecomposition( + time_window = 3, + time_increment = 2, + inner_method = XavQiuWanThi2019.Method(), + formulation = Formulation(), + ), + optimizer = optimizer_with_attributes( + Cbc.Optimizer, + "logLevel" => 0, + ), + ) + @test length(solution["Thermal production (MW)"]["g1"]) == 4 + @test length(solution["Is on"]["g2"]) == 4 + @test length(solution["Spinning reserve (MW)"]["r1"]["g2"]) == 4 + + # read multiple scenarios + instance = UnitCommitment.read([ + fixture("case14.json.gz"), + fixture("case14-profiled.json.gz"), + ]) + solution = UnitCommitment.optimize!( + instance, + TimeDecomposition( + time_window = 3, + time_increment = 2, + inner_method = XavQiuWanThi2019.Method(), + formulation = Formulation(), + ), + optimizer = optimizer_with_attributes( + Cbc.Optimizer, + "logLevel" => 0, + ), + ) + @test length(solution["case14"]["Thermal production (MW)"]["g3"]) == 4 + @test length(solution["case14"]["Is on"]["g4"]) == 4 + @test length( + solution["case14-profiled"]["Thermal production (MW)"]["g5"], + ) == 4 + @test length(solution["case14-profiled"]["Is on"]["g6"]) == 4 + @test length( + solution["case14-profiled"]["Profiled production (MW)"]["g7"], + ) == 4 + @test length( + solution["case14-profiled"]["Spinning reserve (MW)"]["r1"]["g3"], + ) == 4 + end +end diff --git a/test/src/solution/methods/TimeDecomposition/update_solution_test.jl b/test/src/solution/methods/TimeDecomposition/update_solution_test.jl new file mode 100644 index 0000000..ad1be68 --- /dev/null +++ b/test/src/solution/methods/TimeDecomposition/update_solution_test.jl @@ -0,0 +1,55 @@ +# 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, DataStructures + +function solution_methods_TimeDecomposition_update_solution_test() + @testset "update_solution" begin + psuedo_solution = OrderedDict() + time_increment = 4 + psuedo_sub_solution = OrderedDict( + "Thermal production (MW)" => OrderedDict( + "g1" => [100.0, 200.0, 300.0, 400.0, 500.0, 600.0], + ), + "Is on" => OrderedDict("g1" => [1.0, 0.0, 1.0, 1.0, 0.0, 1.0]), + "Profiled production (MW)" => OrderedDict( + "g1" => [199.0, 299.0, 399.0, 499.0, 599.0, 699.0], + ), + "Spinning reserve (MW)" => OrderedDict( + "r1" => OrderedDict( + "g1" => [31.0, 32.0, 33.0, 34.0, 35.0, 36.0], + ), + ), + ) + + # first update should directly copy the first 4 entries of sub solution + UnitCommitment._update_solution!( + psuedo_solution, + psuedo_sub_solution, + time_increment, + ) + @test psuedo_solution["Thermal production (MW)"]["g1"] == + [100.0, 200.0, 300.0, 400.0] + @test psuedo_solution["Is on"]["g1"] == [1.0, 0.0, 1.0, 1.0] + @test psuedo_solution["Profiled production (MW)"]["g1"] == + [199.0, 299.0, 399.0, 499.0] + @test psuedo_solution["Spinning reserve (MW)"]["r1"]["g1"] == + [31.0, 32.0, 33.0, 34.0] + + # second update should append the first 4 entries of sub solution + UnitCommitment._update_solution!( + psuedo_solution, + psuedo_sub_solution, + time_increment, + ) + @test psuedo_solution["Thermal production (MW)"]["g1"] == + [100.0, 200.0, 300.0, 400.0, 100.0, 200.0, 300.0, 400.0] + @test psuedo_solution["Is on"]["g1"] == + [1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0] + @test psuedo_solution["Profiled production (MW)"]["g1"] == + [199.0, 299.0, 399.0, 499.0, 199.0, 299.0, 399.0, 499.0] + @test psuedo_solution["Spinning reserve (MW)"]["r1"]["g1"] == + [31.0, 32.0, 33.0, 34.0, 31.0, 32.0, 33.0, 34.0] + end +end From d602b686bc4ddc02503a2e4f9113c1ca40443817 Mon Sep 17 00:00:00 2001 From: Jun He Date: Wed, 7 Jun 2023 13:22:38 -0400 Subject: [PATCH 09/17] add default values --- src/solution/methods/TimeDecomposition/structs.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/solution/methods/TimeDecomposition/structs.jl b/src/solution/methods/TimeDecomposition/structs.jl index 6ad170a..dc5c99d 100644 --- a/src/solution/methods/TimeDecomposition/structs.jl +++ b/src/solution/methods/TimeDecomposition/structs.jl @@ -8,8 +8,8 @@ import ..Formulation mutable struct TimeDecomposition <: SolutionMethod time_window::Int time_increment::Int - inner_method::SolutionMethod - formulation::Formulation + inner_method::SolutionMethod = XavQiuWanThi2019.Method() + formulation::Formulation = Formulation() end Time decomposition method to solve a problem with moving time window. @@ -30,6 +30,6 @@ Fields Base.@kwdef mutable struct TimeDecomposition <: SolutionMethod time_window::Int time_increment::Int - inner_method::SolutionMethod - formulation::Formulation + inner_method::SolutionMethod = XavQiuWanThi2019.Method() + formulation::Formulation = Formulation() end From 2d510ca7eadbc0b1f5acc609c87827766d547ae6 Mon Sep 17 00:00:00 2001 From: Jun He Date: Wed, 7 Jun 2023 13:22:56 -0400 Subject: [PATCH 10/17] updated doc for time decomp --- docs/src/usage.md | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/docs/src/usage.md b/docs/src/usage.md index 2190d80..69f8d7e 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -287,21 +287,41 @@ aelmp = UnitCommitment.compute_lmp( When solving a unit commitment instance with a dense time slot structure, computational complexity can become a significant challenge. For instance, if the instance contains hourly data for an entire year (8760 hours), solving such a model can require a substantial amount of computational power. To address this issue, UC.jl provides a time_decomposition method within the `optimize!` function. This method decomposes the problem into multiple sub-problems, solving them sequentially. -The `optimize!` function takes three parameters: a unit commitment instance, a `TimeDecomposition` method, and an optimizer. It returns a solution dictionary. The `TimeDecomposition` method itself requires four arguments: `time_window`, `time_increment`, `inner_method`, and `formulation`. These arguments define the time window for each sub-problem, the time increment to move to the next sub-problem, the method used to solve each sub-problem, and the formulation employed, respectively. +The `optimize!` function takes 5 parameters: a unit commitment instance, a `TimeDecomposition` method, an optimizer, and two optional functions `after_build` and `after_optimize`. It returns a solution dictionary. The `TimeDecomposition` method itself requires four arguments: `time_window`, `time_increment`, `inner_method` (optional), and `formulation` (optional). These arguments define the time window for each sub-problem, the time increment to move to the next sub-problem, the method used to solve each sub-problem, and the formulation employed, respectively. The two functions, namely `after_build` and `after_optimize`, are invoked subsequent to the construction and optimization of each sub-model, respectively. It is imperative that the `after_build` function requires its two arguments to be consistently mapped to `model` and `instance`, while the `after_optimize` function necessitates its three arguments to be consistently mapped to `solution`, `model`, and `instance`. -The code snippet below illustrates an example of solving an instance by decomposing the model into multiple 36-hour sub-problems using the `XavQiuWanThi2019` method. Each sub-problem advances 24 hours at a time. The first sub-problem covers time steps 1 to 36, the second covers time steps 25 to 60, the third covers time steps 49 to 84, and so on. The initial power levels and statuses of the second and subsequent sub-problems are set based on the results of the first 24 hours from each of their immediate prior sub-problems. In essence, this approach addresses the complexity of solving a large problem by tackling it in 24-hour intervals, while incorporating an additional 12-hour buffer to mitigate the closing window effect for each sub-problem. +The code snippet below illustrates an example of solving an instance by decomposing the model into multiple 36-hour sub-problems using the `XavQiuWanThi2019` method. Each sub-problem advances 24 hours at a time. The first sub-problem covers time steps 1 to 36, the second covers time steps 25 to 60, the third covers time steps 49 to 84, and so on. The initial power levels and statuses of the second and subsequent sub-problems are set based on the results of the first 24 hours from each of their immediate prior sub-problems. In essence, this approach addresses the complexity of solving a large problem by tackling it in 24-hour intervals, while incorporating an additional 12-hour buffer to mitigate the closing window effect for each sub-problem. Furthermore, the `after_build` function imposes the restriction that `g3` and `g4` cannot be activated simultaneously during the initial time slot of each sub-problem. On the other hand, the `after_optimize` function is invoked to calculate the conventional Locational Marginal Prices (LMPs) for each sub-problem, and subsequently appends the computed values to the `lmps` vector. > **Warning** > Specifying `TimeDecomposition` as the value of the `inner_method` field of another `TimeDecomposition` causes errors when calling the `optimize!` function due to the different argument structures between the two `optimize!` functions. ```julia -using UnitCommitment, Cbc +using UnitCommitment, JuMP, Cbc, HiGHS import UnitCommitment: TimeDecomposition, - Formulation, - XavQiuWanThi2019 - + ConventionalLMP, + XavQiuWanThi2019, + Formulation + +# specifying the after_build and after_optimize functions +function after_build(model, instance) + @constraint( + model, + model[:is_on]["g3", 1] + model[:is_on]["g4", 1] <= 1, + ) +end + +lmps = [] +function after_optimize(solution, model, instance) + lmp = UnitCommitment.compute_lmp( + model, + ConventionalLMP(), + optimizer = HiGHS.Optimizer, + ) + return push!(lmps, lmp) +end + +# assume the instance is given as a 120h problem instance = UnitCommitment.read("instance.json") solution = UnitCommitment.optimize!( @@ -312,6 +332,8 @@ solution = UnitCommitment.optimize!( inner_method = XavQiuWanThi2019.Method(), formulation = Formulation(), ), - optimizer=Cbc.Optimizer + optimizer = Cbc.Optimizer, + after_build = after_build, + after_optimize = after_optimize, ) ``` \ No newline at end of file From 6ea769a68c142d0a40ca76d726aaa028c6265d7d Mon Sep 17 00:00:00 2001 From: Jun He Date: Wed, 7 Jun 2023 13:24:27 -0400 Subject: [PATCH 11/17] add in after_build and after_optimize --- .../methods/TimeDecomposition/optimize.jl | 78 ++++++++++++++++--- .../TimeDecomposition/optimize_test.jl | 53 +++++++++---- 2 files changed, 106 insertions(+), 25 deletions(-) diff --git a/src/solution/methods/TimeDecomposition/optimize.jl b/src/solution/methods/TimeDecomposition/optimize.jl index 840846e..063b077 100644 --- a/src/solution/methods/TimeDecomposition/optimize.jl +++ b/src/solution/methods/TimeDecomposition/optimize.jl @@ -7,22 +7,64 @@ instance::UnitCommitmentInstance, method::TimeDecomposition; optimizer, + after_build = nothing, + after_optimize = nothing, )::OrderedDict Solve the given unit commitment instance with time decomposition. The model solves each sub-problem of a given time length specified by method.time_window, -and proceeds to the next sub-problem by incrementing the time length of method.time_increment. +and proceeds to the next sub-problem by incrementing the time length of `method.time_increment`. + +Arguments +--------- + +- `instance`: + the UnitCommitment instance. + +- `method`: + the `TimeDecomposition` method. + +- `optimizer`: + the optimizer for solving the problem. + +- `after_build`: + a user-defined function that allows modifying the model after building, + must have 2 arguments `model` and `instance` in order. + +- `after_optimize`: + a user-defined function that allows handling additional steps after optimizing, + must have 3 arguments `solution`, `model` and `instance` in order. + Examples -------- ```julia -using UnitCommitment, Cbc +using UnitCommitment, JuMP, Cbc, HiGHS import UnitCommitment: TimeDecomposition, - Formulation, - XavQiuWanThi2019 + ConventionalLMP, + XavQiuWanThi2019, + Formulation + +# specifying the after_build and after_optimize functions +function after_build(model, instance) + @constraint( + model, + model[:is_on]["g3", 1] + model[:is_on]["g4", 1] <= 1, + ) +end + +lmps = [] +function after_optimize(solution, model, instance) + lmp = UnitCommitment.compute_lmp( + model, + ConventionalLMP(), + optimizer = HiGHS.Optimizer, + ) + return push!(lmps, lmp) +end # assume the instance is given as a 120h problem instance = UnitCommitment.read("instance.json") @@ -35,7 +77,9 @@ solution = UnitCommitment.optimize!( inner_method = XavQiuWanThi2019.Method(), formulation = Formulation(), ), - optimizer=Cbc.Optimizer + optimizer = Cbc.Optimizer, + after_build = after_build, + after_optimize = after_optimize, ) """ @@ -43,6 +87,8 @@ function optimize!( instance::UnitCommitmentInstance, method::TimeDecomposition; optimizer, + after_build = nothing, + after_optimize = nothing, )::OrderedDict # get instance total length T = instance.time @@ -58,16 +104,26 @@ function optimize!( # if t_end exceed total T t_end = t_end > T ? T : t_end # slice the model - modified = UnitCommitment.slice(instance, t_start:t_end) - # solve the model - model = UnitCommitment.build_model( - instance = modified, + @info "Solving the sub-problem of time $t_start to $t_end..." + sub_instance = UnitCommitment.slice(instance, t_start:t_end) + # build and optimize the model + sub_model = UnitCommitment.build_model( + instance = sub_instance, optimizer = optimizer, formulation = method.formulation, ) - UnitCommitment.optimize!(model, method.inner_method) + if after_build !== nothing + @info "Calling after build..." + after_build(sub_model, sub_instance) + end + UnitCommitment.optimize!(sub_model, method.inner_method) # get the result of each time period - sub_solution = UnitCommitment.solution(model) + sub_solution = UnitCommitment.solution(sub_model) + if after_optimize !== nothing + @info "Calling after optimize..." + after_optimize(sub_solution, sub_model, sub_instance) + end + # merge solution if length(instance.scenarios) == 1 _update_solution!(solution, sub_solution, method.time_increment) else diff --git a/test/src/solution/methods/TimeDecomposition/optimize_test.jl b/test/src/solution/methods/TimeDecomposition/optimize_test.jl index a087e78..dd68105 100644 --- a/test/src/solution/methods/TimeDecomposition/optimize_test.jl +++ b/test/src/solution/methods/TimeDecomposition/optimize_test.jl @@ -2,8 +2,8 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -using UnitCommitment, DataStructures, Cbc -import UnitCommitment: TimeDecomposition, XavQiuWanThi2019, Formulation +using UnitCommitment, DataStructures, Cbc, HiGHS +import UnitCommitment: TimeDecomposition, ConventionalLMP function solution_methods_TimeDecomposition_optimize_test() @testset "optimize_time_decomposition" begin @@ -11,12 +11,7 @@ function solution_methods_TimeDecomposition_optimize_test() instance = UnitCommitment.read(fixture("case14.json.gz")) solution = UnitCommitment.optimize!( instance, - TimeDecomposition( - time_window = 3, - time_increment = 2, - inner_method = XavQiuWanThi2019.Method(), - formulation = Formulation(), - ), + TimeDecomposition(time_window = 3, time_increment = 2), optimizer = optimizer_with_attributes( Cbc.Optimizer, "logLevel" => 0, @@ -26,6 +21,41 @@ function solution_methods_TimeDecomposition_optimize_test() @test length(solution["Is on"]["g2"]) == 4 @test length(solution["Spinning reserve (MW)"]["r1"]["g2"]) == 4 + # read one scenario with after_build and after_optimize + function after_build(model, instance) + @constraint( + model, + model[:is_on]["g3", 1] + model[:is_on]["g4", 1] <= 1, + ) + end + + lmps = [] + function after_optimize(solution, model, instance) + lmp = UnitCommitment.compute_lmp( + model, + ConventionalLMP(), + optimizer = HiGHS.Optimizer, + ) + return push!(lmps, lmp) + end + + instance = UnitCommitment.read(fixture("case14-profiled.json.gz")) + solution = UnitCommitment.optimize!( + instance, + TimeDecomposition(time_window = 3, time_increment = 2), + optimizer = optimizer_with_attributes( + Cbc.Optimizer, + "logLevel" => 0, + ), + after_build = after_build, + after_optimize = after_optimize, + ) + @test length(lmps) == 2 + @test lmps[1]["s1", "b1", 1] == 50.0 + @test lmps[2]["s1", "b10", 2] ≈ 38.04 atol = 0.1 + @test solution["Is on"]["g3"][1] == 1.0 + @test solution["Is on"]["g4"][1] == 0.0 + # read multiple scenarios instance = UnitCommitment.read([ fixture("case14.json.gz"), @@ -33,12 +63,7 @@ function solution_methods_TimeDecomposition_optimize_test() ]) solution = UnitCommitment.optimize!( instance, - TimeDecomposition( - time_window = 3, - time_increment = 2, - inner_method = XavQiuWanThi2019.Method(), - formulation = Formulation(), - ), + TimeDecomposition(time_window = 3, time_increment = 2), optimizer = optimizer_with_attributes( Cbc.Optimizer, "logLevel" => 0, From f31921fc4fd3838770d1ec50d666998ac5c80bcf Mon Sep 17 00:00:00 2001 From: Jun He Date: Tue, 13 Jun 2023 15:05:37 -0400 Subject: [PATCH 12/17] added `Time horizon (min)` --- docs/src/format.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/format.md b/docs/src/format.md index cf146ea..20b0a99 100644 --- a/docs/src/format.md +++ b/docs/src/format.md @@ -23,7 +23,7 @@ This section describes system-wide parameters, such as power balance penalty, an | Key | Description | Default | Time series? | Uncertain? | :----------------------------- | :------------------------------------------------ | :------: | :------------:| :----------: | `Version` | Version of UnitCommitment.jl this file was written for. Required to ensure that the file remains readable in future versions of the package. If you are following this page to construct the file, this field should equal `0.3`. | Required | No | No -| `Time horizon (h)` | Length of the planning horizon (in hours). | Required | No | No +| `Time horizon (min)` or `Time horizon (h)` | Length of the planning horizon (in minutes or hours). Either `Time horizon (min)` or `Time horizon (h)` is required, but not both. | Required | No | No | `Time step (min)` | Length of each time step (in minutes). Must be a divisor of 60 (e.g. 60, 30, 20, 15, etc). | `60` | No | No | `Power balance penalty ($/MW)` | Penalty for system-wide shortage or surplus in production (in $/MW). This is charged per time step. For example, if there is a shortage of 1 MW for three time steps, three times this amount will be charged. | `1000.0` | No | Yes | `Scenario name` | Name of the scenario. | `"s1"` | No | --- From 2fb89045cdac5d332c3e27269f2e1e611bda0bb9 Mon Sep 17 00:00:00 2001 From: Jun He Date: Fri, 16 Jun 2023 15:35:10 -0400 Subject: [PATCH 13/17] disable optimizer logging --- test/src/lmp/aelmp_test.jl | 15 ++++++++++++--- test/src/lmp/conventional_test.jl | 8 +++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/test/src/lmp/aelmp_test.jl b/test/src/lmp/aelmp_test.jl index a7494bf..d619207 100644 --- a/test/src/lmp/aelmp_test.jl +++ b/test/src/lmp/aelmp_test.jl @@ -12,7 +12,10 @@ function lmp_aelmp_test() instance = UnitCommitment.read(path) model = UnitCommitment.build_model( instance = instance, - optimizer = Cbc.Optimizer, + optimizer = optimizer_with_attributes( + Cbc.Optimizer, + "logLevel" => 0, + ), variable_names = true, ) JuMP.set_silent(model) @@ -22,7 +25,10 @@ function lmp_aelmp_test() aelmp_1 = UnitCommitment.compute_lmp( model, AELMP(), - optimizer = HiGHS.Optimizer, + optimizer = optimizer_with_attributes( + HiGHS.Optimizer, + "log_to_console" => false, + ), ) @test aelmp_1["s1", "B1", 1] ≈ 231.7 atol = 0.1 @@ -33,7 +39,10 @@ function lmp_aelmp_test() allow_offline_participation = false, consider_startup_costs = true, ), - optimizer = HiGHS.Optimizer, + optimizer = optimizer_with_attributes( + HiGHS.Optimizer, + "log_to_console" => false, + ), ) @test aelmp_2["s1", "B1", 1] ≈ 274.3 atol = 0.1 end diff --git a/test/src/lmp/conventional_test.jl b/test/src/lmp/conventional_test.jl index b1135c5..bd2529e 100644 --- a/test/src/lmp/conventional_test.jl +++ b/test/src/lmp/conventional_test.jl @@ -3,13 +3,12 @@ # Released under the modified BSD license. See COPYING.md for more details. using UnitCommitment, Cbc, HiGHS, JuMP -import UnitCommitment: ConventionalLMP function solve_conventional_testcase(path::String) instance = UnitCommitment.read(path) model = UnitCommitment.build_model( instance = instance, - optimizer = Cbc.Optimizer, + optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0), variable_names = true, ) JuMP.set_silent(model) @@ -17,7 +16,10 @@ function solve_conventional_testcase(path::String) lmp = UnitCommitment.compute_lmp( model, ConventionalLMP(), - optimizer = HiGHS.Optimizer, + optimizer = optimizer_with_attributes( + HiGHS.Optimizer, + "log_to_console" => false, + ), ) return lmp end From 0bb175078b6d6eae3d7630fc670d44c50cc85c5d Mon Sep 17 00:00:00 2001 From: Jun He Date: Fri, 16 Jun 2023 15:35:51 -0400 Subject: [PATCH 14/17] da to rt market with tests --- src/UnitCommitment.jl | 2 + src/market/market.jl | 220 +++++++++++++++++++++++++++++++++ src/market/structs.jl | 33 +++++ test/src/UnitCommitmentT.jl | 3 + test/src/market/market_test.jl | 151 ++++++++++++++++++++++ 5 files changed, 409 insertions(+) create mode 100644 src/market/market.jl create mode 100644 src/market/structs.jl create mode 100644 test/src/market/market_test.jl 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 From 3086e71611bf040f68d71e1b292dd4d9b12f75c4 Mon Sep 17 00:00:00 2001 From: Jun He Date: Fri, 16 Jun 2023 17:02:06 -0400 Subject: [PATCH 15/17] updated doc with solve_market --- docs/src/usage.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/src/usage.md b/docs/src/usage.md index 69f8d7e..71d38a7 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -336,4 +336,33 @@ solution = UnitCommitment.optimize!( after_build = after_build, after_optimize = after_optimize, ) -``` \ No newline at end of file +``` + +## Day-ahead (DA) Market to Real-time (RT) Markets +The UC.jl package offers a comprehensive set of functions for solving marketing problems. The primary function, `solve_market`, facilitates the solution of day-ahead (DA) markets, which can be either deterministic or stochastic in nature. Subsequently, it sequentially maps the commitment status obtained from the DA market to all the real-time (RT) markets, which are deterministic instances. It is essential to ensure that the time span of the DA market encompasses all the RT markets, and the file paths for the RT markets must be specified in chronological order. Each RT market should represent a single time slot, and it is recommended to include a few additional time slots to mitigate the closing window effect. + +The `solve_market` function accepts several parameters, including the file path (or a list of file paths in the case of stochastic markets) for the DA market, a list of file paths for the RT markets, the market settings specified by the `MarketSettings` structure, and an optimizer. The `MarketSettings` structure itself requires three optional arguments: `inner_method`, `lmp_method`, and `formulation`. If the computation of Locational Marginal Prices (LMPs) is not desired, the `lmp_method` can be set to `nothing`. Additional optional parameters include a linear programming optimizer for solving LMPs (if a different optimizer than the required one is desired), callback functions `after_build_da` and `after_optimize_da`, which are invoked after the construction and optimization of the DA market, and callback functions `after_build_rt` and `after_optimize_rt`, which are invoked after the construction and optimization of each RT market. It is crucial to note that the `after_build` function requires its two arguments to consistently correspond to `model` and `instance`, while the `after_optimize` function requires its three arguments to consistently correspond to `solution`, `model`, and `instance`. + +As an illustrative example, suppose the DA market predicts hourly data for a 24-hour period, while the RT markets represent 5-minute intervals. In this scenario, each RT market file corresponds to a specific 5-minute interval, with the first RT market representing the initial 5 minutes, the second RT market representing the subsequent 5 minutes, and so on. Consequently, there should be 12 RT market files for each hour. To mitigate the closing window effect, except for the last few RT markets, each RT market should contain three time slots, resulting in a total time span of 15 minutes. However, only the first time slot is considered in the final solution. The last two RT markets should only contain 2 and 1 time slot(s), respectively, to ensure that the total time covered by all RT markets does not exceed the time span of the DA market. The code snippet below demonstrates a simplified example of how to utilize the `solve_market` function. Please note that it only serves as a simplified example and may require further customization based on the specific requirements of your use case. + +```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, +) +``` From cd96b2807624924ce17f4e55385ce1247f3f7e73 Mon Sep 17 00:00:00 2001 From: Jun He Date: Fri, 16 Jun 2023 17:11:41 -0400 Subject: [PATCH 16/17] market json gz --- test/fixtures/market_da_scenario.json.gz | Bin 0 -> 414 bytes test/fixtures/market_da_simple.json.gz | Bin 0 -> 412 bytes test/fixtures/market_rt1_simple.json.gz | Bin 0 -> 411 bytes test/fixtures/market_rt2_simple.json.gz | Bin 0 -> 413 bytes test/fixtures/market_rt3_simple.json.gz | Bin 0 -> 412 bytes test/fixtures/market_rt4_simple.json.gz | Bin 0 -> 407 bytes 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/fixtures/market_da_scenario.json.gz create mode 100644 test/fixtures/market_da_simple.json.gz create mode 100644 test/fixtures/market_rt1_simple.json.gz create mode 100644 test/fixtures/market_rt2_simple.json.gz create mode 100644 test/fixtures/market_rt3_simple.json.gz create mode 100644 test/fixtures/market_rt4_simple.json.gz diff --git a/test/fixtures/market_da_scenario.json.gz b/test/fixtures/market_da_scenario.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..238e93f3f6987ea3bd6b1af19c9291ad0f0584ad GIT binary patch literal 414 zcmV;P0b%|hiwFpS-HBuX18re)Yh`p_WMN-(V`Xk(a%pcaYIARH0M(RVOT#b}#ozZS zLfJ!ws!6-n>EnD5!NI`c{KFo?8d;!iN}4+7*mw8dcI`4IY#o4emrHG&jnQ2(^jb2R6aD(A;ZE<7F?lWm5PXIGFWwYQs*wb^h5V|=HZRMfNWLl=A%ne>vZD)IO ztQMKQpuP&Ph5mdR=+(`$9uECuiANRfepE9LqC>C)yq;eJ$E1IY{N*&r!}^PbAv~7& z<^%T`MRO3apLp&t5C13f*CUYwt#_G-c&iNgXSKslKKlT-NY(9LHp=#2*I9SI0m($e IX_5s10H}z^n*aa+ literal 0 HcmV?d00001 diff --git a/test/fixtures/market_da_simple.json.gz b/test/fixtures/market_da_simple.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..caa36203ee99222272e4a6be6396cbee4e55d0cf GIT binary patch literal 412 zcmV;N0b~9jiwFqC#E4`718re)Yh`p_WMN-(X>D+9WiD!SZ*Bn9lwE7WFcgOG{S~2j z(LuK*{Ze=7F2-PjFq!+ni(oShv`tBqIVb-2<+Qd&94HJ2yE$#noA>18Nyxf`kTa%| zW=t_zIyY!-GA8$AMqK13;oz&#@#^e^XACWbXKQ-hot>CqX$6~BoCkSS-&hl(Q;Bac zxKA*cgMj?RbB}rSKaszlh}_V6=P65e%7}heE9~`WZ{QZFy4~wW*}Zk0P3IeoJO+)E G1poj9MHEYKz;O`UV>zkAZ!EOWvJGQpeBocFvZ=R65rwGawM zOtXv|u8ZIbtsnxi2iOTEArRoRiy*9{W07$*S6X~1i8`5(Cy)?hUo8yJokTGxYbytX zvfy6*0de23yHhMdo#DiKUPvE@i1d5YN?p4OH(YYfjPl!r*u*acr`VREdf0@<%beTT zc+Pdk(v2|EDk*0MrbIJU7&I&O8+Tnm4_#~*YL^&K_xak5jbLnzghbUKw0r59?+w$Y z%qwT%OuKr*Q?`VJ`S*ldDU4t#aAu}#5>Dxko~s4d%{17_Lm@?0W~j`Ky{+0#cH~4Y zq@Pe3iiwFpEm2PAJ18re)Yh`p_a&$6Zb7^gGY-KKLb8l_{)s#_d!Y~lW-~AM! z_|QSOCB}BTNB1xW6NJfh8$JY^VW3S)n$9`#yD!(;8gZa79PH)V+&}-z<#(m)7DC>L zX_j)sb?)7wwawVvvt0=#ZNkG>T`#DzW07*SP+EK`iTuTdm5D3mhG)o6g#?9St875_ z21U-D;sesW+ts6DG4h8K@Vt?JKR{iKXQjGnD%^9)H8aZX(dH&@A$tn+YpVx!YP`xg zbu(gQtHORnqN;6@$_DbgZEMP^`HdjoXj zSS_Wyp&CWdK%brmIw`?*A@oiq9+k9vQN^4_$6!+!&#!?)r+Wj4@JeBzN z1Md(FW=hFVJolJ~{}cJ^iO4OjcaiXTrwrL=dBR?Q?gj3Es^7k@mEFItwP}3=f}f#k HlLY_(;UCX0 literal 0 HcmV?d00001 diff --git a/test/fixtures/market_rt3_simple.json.gz b/test/fixtures/market_rt3_simple.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..9026d8d77203e9baa12a8990273adf9298d058c5 GIT binary patch literal 412 zcmV;N0b~9jiwFpjm2PAJ18re)Yh`p_a&$9ab7^gGY-KKLb8l_{)s)|B!Y~lW-~B5> z@xej2CB}AckM3a%CJ2+cANUY#hJm&zX*%b`|9y9@tq}(b!@*vz&3*H|Ts~L2Y7yd% zgb|6f(&XL^SwRMJPq0!tg@i{hdY)fp$10Izu8sQADe275KxVF#TbYqgqEb{i1!ax0 zHz;!H6d$nW9aaxoM5HsEpy#be1D`;cmg>5xa3@n~gw<}3kej%L;1ui^s{3_nyv!u# z#&c;B5pRW&(NQt8a78lHxh1p0e8{E?>XRn^Ps5hMRJw4M4Y3ke1MjonEB}GDt%;L0m2I$C< zUZn1ZY7~A0{rNP|SqZL(p?@s#u%z7&E9N{r1e@b{ehnNu{afTOr$HW6Uo3>+SmK)x zyvrz>IcGod++iO4PvoyhA_rRUDwfe!8H&&Hgq{A}3)~{rPW!S}w*R`;y7dhm9Ju0> G1pol7QOGL* literal 0 HcmV?d00001 diff --git a/test/fixtures/market_rt4_simple.json.gz b/test/fixtures/market_rt4_simple.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..ecf1a180e202e6e2b35729e4ff08191c315ba09a GIT binary patch literal 407 zcmV;I0cidoiwFpgm2PAJ18re)Yh`p_a&$Cbb7^gGY-KKLb8l_{)s$UJ!!Q(v@AoT0 z*~Nsa$9MHEYKz;O`UV>zkAZ!Hgm!TGQpeE=Dg=kPM(CUI|zAW zrdi4j*SU9v){ueR1B`@{knr$X;Q3W{B2tbPN{bIAQ7;wp1PUm2)!gvRkwDp4*&7x) zcjh0G<{w6Pip8imnp)3GfP+~nY#Q?oms~TW+zuf(b?1NuY_~A6U#BLkjN9B~!F9@# zRv2p)7jpv}M01rJG%xfUZyLZp>SG(I4H2I0^L1+*!PpuJ2`}x3_AeasyXMqXKKlHy&K%fBOygvq^QV@J*dh~cH~$srCU%P zh2KDbJPveHf(vXI97;SYX%C``IgR$gruKM#4cvD6x5%H4gFLKmEQIh-;$IiMPp~yp zN`B(G#XS6<$e#~H4zylH!sAvMvG;O?t^V8%TwAKW?s=`WZ(V28`2?ma-f58q000|8 B!ukLJ literal 0 HcmV?d00001 From 82cefe26522213bc2c2c388d7b8fdc80763ed39f Mon Sep 17 00:00:00 2001 From: Jun He Date: Sun, 16 Jul 2023 16:57:53 -0400 Subject: [PATCH 17/17] disable HiGHS logging --- test/src/solution/methods/TimeDecomposition/optimize_test.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/src/solution/methods/TimeDecomposition/optimize_test.jl b/test/src/solution/methods/TimeDecomposition/optimize_test.jl index dd68105..b9076fd 100644 --- a/test/src/solution/methods/TimeDecomposition/optimize_test.jl +++ b/test/src/solution/methods/TimeDecomposition/optimize_test.jl @@ -34,7 +34,10 @@ function solution_methods_TimeDecomposition_optimize_test() lmp = UnitCommitment.compute_lmp( model, ConventionalLMP(), - optimizer = HiGHS.Optimizer, + optimizer = optimizer_with_attributes( + HiGHS.Optimizer, + "log_to_console" => false, + ), ) return push!(lmps, lmp) end