You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
387 lines
13 KiB
387 lines
13 KiB
# 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 Printf
|
|
|
|
bin(x) = [xi > 0.5 for xi in x]
|
|
|
|
function validate(instance_filename::String, solution_filename::String)
|
|
instance = UnitCommitment.read(instance_filename)
|
|
solution = JSON.parse(open(solution_filename))
|
|
return validate(instance, solution)
|
|
end
|
|
|
|
"""
|
|
validate(instance, solution)::Bool
|
|
|
|
Verifies that the given solution is feasible for the problem. If feasible,
|
|
silently returns true. In infeasible, returns false and prints the validation
|
|
errors to the screen.
|
|
|
|
This function is implemented independently from the optimization model in
|
|
`model.jl`, and therefore can be used to verify that the model is indeed
|
|
producing valid solutions. It can also be used to verify the solutions produced
|
|
by other optimization packages.
|
|
"""
|
|
function validate(
|
|
instance::UnitCommitmentInstance,
|
|
solution::Union{Dict,OrderedDict},
|
|
)::Bool
|
|
err_count = 0
|
|
err_count += _validate_units(instance, solution)
|
|
err_count += _validate_reserve_and_demand(instance, solution)
|
|
|
|
if err_count > 0
|
|
@error "Found $err_count validation errors"
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
function _validate_units(instance, solution; tol = 0.01)
|
|
err_count = 0
|
|
|
|
for unit in instance.units
|
|
production = solution["Production (MW)"][unit.name]
|
|
reserve = solution["Reserve (MW)"][unit.name]
|
|
actual_production_cost = solution["Production cost (\$)"][unit.name]
|
|
actual_startup_cost = solution["Startup cost (\$)"][unit.name]
|
|
is_on = bin(solution["Is on"][unit.name])
|
|
|
|
for t in 1:instance.time
|
|
# Auxiliary variables
|
|
if t == 1
|
|
is_starting_up = (unit.initial_status < 0) && is_on[t]
|
|
is_shutting_down = (unit.initial_status > 0) && !is_on[t]
|
|
ramp_up =
|
|
max(0, production[t] + reserve[t] - unit.initial_power)
|
|
ramp_down = max(0, unit.initial_power - production[t])
|
|
else
|
|
is_starting_up = !is_on[t-1] && is_on[t]
|
|
is_shutting_down = is_on[t-1] && !is_on[t]
|
|
ramp_up = max(0, production[t] + reserve[t] - production[t-1])
|
|
ramp_down = max(0, production[t-1] - production[t])
|
|
end
|
|
|
|
# Compute production costs
|
|
production_cost, startup_cost = 0, 0
|
|
if is_on[t]
|
|
production_cost += unit.min_power_cost[t]
|
|
residual = max(0, production[t] - unit.min_power[t])
|
|
for s in unit.cost_segments
|
|
cleared = min(residual, s.mw[t])
|
|
production_cost += cleared * s.cost[t]
|
|
residual = max(0, residual - s.mw[t])
|
|
end
|
|
end
|
|
|
|
# Production should be non-negative
|
|
if production[t] < -tol
|
|
@error @sprintf(
|
|
"Unit %s produces negative amount of power at time %d (%.2f)",
|
|
unit.name,
|
|
t,
|
|
production[t]
|
|
)
|
|
err_count += 1
|
|
end
|
|
|
|
# Verify must-run
|
|
if !is_on[t] && unit.must_run[t]
|
|
@error @sprintf(
|
|
"Must-run unit %s is offline at time %d",
|
|
unit.name,
|
|
t
|
|
)
|
|
err_count += 1
|
|
end
|
|
|
|
# Verify reserve eligibility
|
|
if !unit.provides_spinning_reserves[t] && reserve[t] > tol
|
|
@error @sprintf(
|
|
"Unit %s is not eligible to provide spinning reserves at time %d",
|
|
unit.name,
|
|
t
|
|
)
|
|
err_count += 1
|
|
end
|
|
|
|
# If unit is on, must produce at least its minimum power
|
|
if is_on[t] && (production[t] < unit.min_power[t] - tol)
|
|
@error @sprintf(
|
|
"Unit %s produces below its minimum limit at time %d (%.2f < %.2f)",
|
|
unit.name,
|
|
t,
|
|
production[t],
|
|
unit.min_power[t]
|
|
)
|
|
err_count += 1
|
|
end
|
|
|
|
# If unit is on, must produce at most its maximum power
|
|
if is_on[t] &&
|
|
(production[t] + reserve[t] > unit.max_power[t] + tol)
|
|
@error @sprintf(
|
|
"Unit %s produces above its maximum limit at time %d (%.2f + %.2f> %.2f)",
|
|
unit.name,
|
|
t,
|
|
production[t],
|
|
reserve[t],
|
|
unit.max_power[t]
|
|
)
|
|
err_count += 1
|
|
end
|
|
|
|
# If unit is off, must produce zero
|
|
if !is_on[t] && production[t] + reserve[t] > tol
|
|
@error @sprintf(
|
|
"Unit %s produces power at time %d while off",
|
|
unit.name,
|
|
t
|
|
)
|
|
err_count += 1
|
|
end
|
|
|
|
# Startup limit
|
|
if is_starting_up && (ramp_up > unit.startup_limit + tol)
|
|
@error @sprintf(
|
|
"Unit %s exceeds startup limit at time %d (%.2f > %.2f)",
|
|
unit.name,
|
|
t,
|
|
ramp_up,
|
|
unit.startup_limit
|
|
)
|
|
err_count += 1
|
|
end
|
|
|
|
# Shutdown limit
|
|
if is_shutting_down && (ramp_down > unit.shutdown_limit + tol)
|
|
@error @sprintf(
|
|
"Unit %s exceeds shutdown limit at time %d (%.2f > %.2f)",
|
|
unit.name,
|
|
t,
|
|
ramp_down,
|
|
unit.shutdown_limit
|
|
)
|
|
err_count += 1
|
|
end
|
|
|
|
# Ramp-up limit
|
|
if !is_starting_up &&
|
|
!is_shutting_down &&
|
|
(ramp_up > unit.ramp_up_limit + tol)
|
|
@error @sprintf(
|
|
"Unit %s exceeds ramp up limit at time %d (%.2f > %.2f)",
|
|
unit.name,
|
|
t,
|
|
ramp_up,
|
|
unit.ramp_up_limit
|
|
)
|
|
err_count += 1
|
|
end
|
|
|
|
# Ramp-down limit
|
|
if !is_starting_up &&
|
|
!is_shutting_down &&
|
|
(ramp_down > unit.ramp_down_limit + tol)
|
|
@error @sprintf(
|
|
"Unit %s exceeds ramp down limit at time %d (%.2f > %.2f)",
|
|
unit.name,
|
|
t,
|
|
ramp_down,
|
|
unit.ramp_down_limit
|
|
)
|
|
err_count += 1
|
|
end
|
|
|
|
# Verify startup costs & minimum downtime
|
|
if is_starting_up
|
|
|
|
# Calculate how much time the unit has been offline
|
|
time_down = 0
|
|
for k in 1:(t-1)
|
|
if !is_on[t-k]
|
|
time_down += 1
|
|
else
|
|
break
|
|
end
|
|
end
|
|
if (t == time_down + 1) && (unit.initial_status < 0)
|
|
time_down -= unit.initial_status
|
|
end
|
|
|
|
# Calculate startup costs
|
|
for c in unit.startup_categories
|
|
if time_down >= c.delay
|
|
startup_cost = c.cost
|
|
end
|
|
end
|
|
|
|
# Check minimum downtime
|
|
if time_down < unit.min_downtime
|
|
@error @sprintf(
|
|
"Unit %s violates minimum downtime at time %d",
|
|
unit.name,
|
|
t
|
|
)
|
|
err_count += 1
|
|
end
|
|
end
|
|
|
|
# Verify minimum uptime
|
|
if is_shutting_down
|
|
|
|
# Calculate how much time the unit has been online
|
|
time_up = 0
|
|
for k in 1:(t-1)
|
|
if is_on[t-k]
|
|
time_up += 1
|
|
else
|
|
break
|
|
end
|
|
end
|
|
if (t == time_up + 1) && (unit.initial_status > 0)
|
|
time_up += unit.initial_status
|
|
end
|
|
|
|
# Check minimum uptime
|
|
if time_up < unit.min_uptime
|
|
@error @sprintf(
|
|
"Unit %s violates minimum uptime at time %d",
|
|
unit.name,
|
|
t
|
|
)
|
|
err_count += 1
|
|
end
|
|
end
|
|
|
|
# Verify production costs
|
|
if abs(actual_production_cost[t] - production_cost) > 1.00
|
|
@error @sprintf(
|
|
"Unit %s has unexpected production cost at time %d (%.2f should be %.2f)",
|
|
unit.name,
|
|
t,
|
|
actual_production_cost[t],
|
|
production_cost
|
|
)
|
|
err_count += 1
|
|
end
|
|
|
|
# Verify startup costs
|
|
if abs(actual_startup_cost[t] - startup_cost) > 1.00
|
|
@error @sprintf(
|
|
"Unit %s has unexpected startup cost at time %d (%.2f should be %.2f)",
|
|
unit.name,
|
|
t,
|
|
actual_startup_cost[t],
|
|
startup_cost
|
|
)
|
|
err_count += 1
|
|
end
|
|
end
|
|
end
|
|
|
|
return err_count
|
|
end
|
|
|
|
function _validate_reserve_and_demand(instance, solution, tol = 0.01)
|
|
err_count = 0
|
|
for t in 1:instance.time
|
|
load_curtail = 0
|
|
fixed_load = sum(b.load[t] for b in instance.buses)
|
|
ps_load = 0
|
|
if length(instance.price_sensitive_loads) > 0
|
|
ps_load = sum(
|
|
solution["Price-sensitive loads (MW)"][ps.name][t] for
|
|
ps in instance.price_sensitive_loads
|
|
)
|
|
end
|
|
production =
|
|
sum(solution["Production (MW)"][g.name][t] for g in instance.units)
|
|
if "Load curtail (MW)" in keys(solution)
|
|
load_curtail = sum(
|
|
solution["Load curtail (MW)"][b.name][t] for
|
|
b in instance.buses
|
|
)
|
|
end
|
|
balance = fixed_load - load_curtail - production + ps_load
|
|
|
|
# Verify that production equals demand
|
|
if abs(balance) > tol
|
|
@error @sprintf(
|
|
"Non-zero power balance at time %d (%.2f + %.2f - %.2f - %.2f != 0)",
|
|
t,
|
|
fixed_load,
|
|
ps_load,
|
|
load_curtail,
|
|
production,
|
|
)
|
|
err_count += 1
|
|
end
|
|
|
|
|
|
# Verify flexiramp solutions only if either of the up-flexiramp and
|
|
# down-flexiramp requirements is not a default array of zeros
|
|
if instance.reserves.upflexiramp != zeros(instance.time) || instance.reserves.dwflexiramp != zeros(instance.time)
|
|
upflexiramp =
|
|
sum(solution["Up-flexiramp (MW)"][g.name][t] for g in instance.units)
|
|
upflexiramp_shortfall =
|
|
(instance.flexiramp_shortfall_penalty[t] >= 0) ?
|
|
solution["Up-flexiramp shortfall (MW)"][t] : 0
|
|
|
|
if upflexiramp + upflexiramp_shortfall < instance.reserves.upflexiramp[t] - tol
|
|
@error @sprintf(
|
|
"Insufficient up-flexiramp at time %d (%.2f + %.2f should be %.2f)",
|
|
t,
|
|
upflexiramp,
|
|
upflexiramp_shortfall,
|
|
instance.reserves.upflexiramp[t],
|
|
)
|
|
err_count += 1
|
|
end
|
|
|
|
|
|
dwflexiramp =
|
|
sum(solution["Down-flexiramp (MW)"][g.name][t] for g in instance.units)
|
|
dwflexiramp_shortfall =
|
|
(instance.flexiramp_shortfall_penalty[t] >= 0) ?
|
|
solution["Down-flexiramp shortfall (MW)"][t] : 0
|
|
|
|
if dwflexiramp + dwflexiramp_shortfall < instance.reserves.dwflexiramp[t] - tol
|
|
@error @sprintf(
|
|
"Insufficient down-flexiramp at time %d (%.2f + %.2f should be %.2f)",
|
|
t,
|
|
dwflexiramp,
|
|
dwflexiramp_shortfall,
|
|
instance.reserves.dwflexiramp[t],
|
|
)
|
|
err_count += 1
|
|
end
|
|
# Verify spinning reserve solutions only if both up-flexiramp and
|
|
# down-flexiramp requirements are arrays of zeros.
|
|
else
|
|
reserve =
|
|
sum(solution["Reserve (MW)"][g.name][t] for g in instance.units)
|
|
reserve_shortfall =
|
|
(instance.shortfall_penalty[t] >= 0) ?
|
|
solution["Reserve shortfall (MW)"][t] : 0
|
|
|
|
if reserve + reserve_shortfall < instance.reserves.spinning[t] - tol
|
|
@error @sprintf(
|
|
"Insufficient spinning reserves at time %d (%.2f + %.2f should be %.2f)",
|
|
t,
|
|
reserve,
|
|
reserve_shortfall,
|
|
instance.reserves.spinning[t],
|
|
)
|
|
err_count += 1
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
return err_count
|
|
end
|