commit
81d4ff5b9d
@ -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
|
@ -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
|
@ -0,0 +1,259 @@
|
|||||||
|
# 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,
|
||||||
|
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`.
|
||||||
|
|
||||||
|
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, JuMP, Cbc, HiGHS
|
||||||
|
|
||||||
|
import UnitCommitment:
|
||||||
|
TimeDecomposition,
|
||||||
|
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!(
|
||||||
|
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,
|
||||||
|
after_build = after_build,
|
||||||
|
after_optimize = after_optimize,
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
function optimize!(
|
||||||
|
instance::UnitCommitmentInstance,
|
||||||
|
method::TimeDecomposition;
|
||||||
|
optimizer,
|
||||||
|
after_build = nothing,
|
||||||
|
after_optimize = nothing,
|
||||||
|
)::OrderedDict
|
||||||
|
# get instance total length
|
||||||
|
T = instance.time
|
||||||
|
solution = OrderedDict()
|
||||||
|
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
|
||||||
|
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
|
||||||
|
@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,
|
||||||
|
)
|
||||||
|
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(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
|
||||||
|
for sc in instance.scenarios
|
||||||
|
_update_solution!(
|
||||||
|
solution[sc.name],
|
||||||
|
sub_solution[sc.name],
|
||||||
|
method.time_increment,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# set the initial status for the next sub-problem
|
||||||
|
_set_initial_status!(instance, solution, method.time_increment)
|
||||||
|
end
|
||||||
|
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 last bunch of time slots
|
||||||
|
specified by time_increment in the solution dictionary.
|
||||||
|
"""
|
||||||
|
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[end]
|
||||||
|
thermal_unit.initial_status = _determine_initial_status(
|
||||||
|
thermal_unit.initial_status,
|
||||||
|
is_on[end-time_increment+1:end],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
_determine_initial_status(
|
||||||
|
prev_initial_status::Union{Float64,Int},
|
||||||
|
status_sequence::Vector{Float64},
|
||||||
|
)::Union{Float64,Int}
|
||||||
|
|
||||||
|
Determines a thermal unit's initial status based on its previous initial status, and
|
||||||
|
the on/off statuses in the last operation.
|
||||||
|
"""
|
||||||
|
function _determine_initial_status(
|
||||||
|
prev_initial_status::Union{Float64,Int},
|
||||||
|
status_sequence::Vector{Float64},
|
||||||
|
)::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 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
|
||||||
|
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
|
||||||
|
|
||||||
|
"""
|
||||||
|
_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,
|
||||||
|
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
|
@ -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 = XavQiuWanThi2019.Method()
|
||||||
|
formulation::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 = XavQiuWanThi2019.Method()
|
||||||
|
formulation::Formulation = Formulation()
|
||||||
|
end
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
@ -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
|
@ -0,0 +1,88 @@
|
|||||||
|
# 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, HiGHS
|
||||||
|
import UnitCommitment: TimeDecomposition, ConventionalLMP
|
||||||
|
|
||||||
|
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),
|
||||||
|
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 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 = optimizer_with_attributes(
|
||||||
|
HiGHS.Optimizer,
|
||||||
|
"log_to_console" => false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
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"),
|
||||||
|
fixture("case14-profiled.json.gz"),
|
||||||
|
])
|
||||||
|
solution = UnitCommitment.optimize!(
|
||||||
|
instance,
|
||||||
|
TimeDecomposition(time_window = 3, time_increment = 2),
|
||||||
|
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
|
@ -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
|
Loading…
Reference in new issue