mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-06 08:18:51 -06:00
220 lines
7.6 KiB
Julia
220 lines
7.6 KiB
Julia
# 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 = 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["DA"] = solution_da
|
|
solution["RT"] = []
|
|
|
|
# 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)
|
|
push!(solution["RT"], 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["LMP (\$/MW)"] = lmp
|
|
else
|
|
for sc in instance.scenarios
|
|
solution[sc.name]["LMP (\$/MW)"] = 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
|