Created profiled units and tests

pull/27/head
Jun He 3 years ago
parent 0b95df25ec
commit e24f22553b

@ -4,6 +4,8 @@
module UnitCommitment module UnitCommitment
@info "PU" ##REMOVE ME
include("instance/structs.jl") include("instance/structs.jl")
include("model/formulations/base/structs.jl") include("model/formulations/base/structs.jl")
include("solution/structs.jl") include("solution/structs.jl")
@ -30,6 +32,7 @@ include("model/formulations/base/psload.jl")
include("model/formulations/base/sensitivity.jl") include("model/formulations/base/sensitivity.jl")
include("model/formulations/base/system.jl") include("model/formulations/base/system.jl")
include("model/formulations/base/unit.jl") include("model/formulations/base/unit.jl")
include("model/formulations/base/punit.jl")
include("model/formulations/CarArr2006/pwlcosts.jl") include("model/formulations/CarArr2006/pwlcosts.jl")
include("model/formulations/DamKucRajAta2016/ramp.jl") include("model/formulations/DamKucRajAta2016/ramp.jl")
include("model/formulations/Gar1962/pwlcosts.jl") include("model/formulations/Gar1962/pwlcosts.jl")

@ -17,6 +17,7 @@ function _migrate(json)
end end
version = VersionNumber(version) version = VersionNumber(version)
version >= v"0.3" || _migrate_to_v03(json) version >= v"0.3" || _migrate_to_v03(json)
version >= v"0.4" || _migrate_to_v04(json)
return return
end end
@ -36,3 +37,14 @@ function _migrate_to_v03(json)
end end
end end
end end
function _migrate_to_v04(json)
# Migrate thermal units
if json["Generators"] !== nothing
for (gen_name, gen) in json["Generators"]
if gen["Type"] === nothing
gen["Type"] = "Thermal"
end
end
end
end

@ -85,6 +85,7 @@ function _from_json(json; repair = true)
lines = TransmissionLine[] lines = TransmissionLine[]
loads = PriceSensitiveLoad[] loads = PriceSensitiveLoad[]
reserves = Reserve[] reserves = Reserve[]
profiled_units = ProfiledUnit[]
function scalar(x; default = nothing) function scalar(x; default = nothing)
x !== nothing || return default x !== nothing || return default
@ -135,6 +136,7 @@ function _from_json(json; repair = true)
length(buses), length(buses),
timeseries(dict["Load (MW)"]), timeseries(dict["Load (MW)"]),
Unit[], Unit[],
ProfiledUnit[],
PriceSensitiveLoad[], PriceSensitiveLoad[],
) )
name_to_bus[bus_name] = bus name_to_bus[bus_name] = bus
@ -161,90 +163,107 @@ function _from_json(json; repair = true)
# Read units # Read units
for (unit_name, dict) in json["Generators"] for (unit_name, dict) in json["Generators"]
# Read and validate unit type
unit_type = scalar(dict["Type"], default = nothing)
unit_type !== nothing || error("unit $unit_name has no type specified")
bus = name_to_bus[dict["Bus"]] bus = name_to_bus[dict["Bus"]]
# Read production cost curve if lowercase(unit_type) === "thermal"
K = length(dict["Production cost curve (MW)"]) # Read production cost curve
curve_mw = hcat( K = length(dict["Production cost curve (MW)"])
[timeseries(dict["Production cost curve (MW)"][k]) for k in 1:K]..., curve_mw = hcat(
) [timeseries(dict["Production cost curve (MW)"][k]) for k in 1:K]...,
curve_cost = hcat(
[timeseries(dict["Production cost curve (\$)"][k]) for k in 1:K]...,
)
min_power = curve_mw[:, 1]
max_power = curve_mw[:, K]
min_power_cost = curve_cost[:, 1]
segments = CostSegment[]
for k in 2:K
amount = curve_mw[:, k] - curve_mw[:, k-1]
cost = (curve_cost[:, k] - curve_cost[:, k-1]) ./ amount
replace!(cost, NaN => 0.0)
push!(segments, CostSegment(amount, cost))
end
# Read startup costs
startup_delays = scalar(dict["Startup delays (h)"], default = [1])
startup_costs = scalar(dict["Startup costs (\$)"], default = [0.0])
startup_categories = StartupCategory[]
for k in 1:length(startup_delays)
push!(
startup_categories,
StartupCategory(
startup_delays[k] .* time_multiplier,
startup_costs[k],
),
) )
end curve_cost = hcat(
[timeseries(dict["Production cost curve (\$)"][k]) for k in 1:K]...,
)
min_power = curve_mw[:, 1]
max_power = curve_mw[:, K]
min_power_cost = curve_cost[:, 1]
segments = CostSegment[]
for k in 2:K
amount = curve_mw[:, k] - curve_mw[:, k-1]
cost = (curve_cost[:, k] - curve_cost[:, k-1]) ./ amount
replace!(cost, NaN => 0.0)
push!(segments, CostSegment(amount, cost))
end
# Read reserve eligibility # Read startup costs
unit_reserves = Reserve[] startup_delays = scalar(dict["Startup delays (h)"], default = [1])
if "Reserve eligibility" in keys(dict) startup_costs = scalar(dict["Startup costs (\$)"], default = [0.0])
unit_reserves = startup_categories = StartupCategory[]
[name_to_reserve[n] for n in dict["Reserve eligibility"]] for k in 1:length(startup_delays)
end push!(
startup_categories,
StartupCategory(
startup_delays[k] .* time_multiplier,
startup_costs[k],
),
)
end
# Read and validate initial conditions # Read reserve eligibility
initial_power = scalar(dict["Initial power (MW)"], default = nothing) unit_reserves = Reserve[]
initial_status = scalar(dict["Initial status (h)"], default = nothing) if "Reserve eligibility" in keys(dict)
if initial_power === nothing unit_reserves =
initial_status === nothing || [name_to_reserve[n] for n in dict["Reserve eligibility"]]
error("unit $unit_name has initial status but no initial power")
else
initial_status !== nothing ||
error("unit $unit_name has initial power but no initial status")
initial_status != 0 ||
error("unit $unit_name has invalid initial status")
if initial_status < 0 && initial_power > 1e-3
error("unit $unit_name has invalid initial power")
end end
initial_status *= time_multiplier
end
unit = Unit( # Read and validate initial conditions
unit_name, initial_power = scalar(dict["Initial power (MW)"], default = nothing)
bus, initial_status = scalar(dict["Initial status (h)"], default = nothing)
max_power, if initial_power === nothing
min_power, initial_status === nothing ||
timeseries(dict["Must run?"], default = [false for t in 1:T]), error("unit $unit_name has initial status but no initial power")
min_power_cost, else
segments, initial_status !== nothing ||
scalar(dict["Minimum uptime (h)"], default = 1) * time_multiplier, error("unit $unit_name has initial power but no initial status")
scalar(dict["Minimum downtime (h)"], default = 1) * time_multiplier, initial_status != 0 ||
scalar(dict["Ramp up limit (MW)"], default = 1e6), error("unit $unit_name has invalid initial status")
scalar(dict["Ramp down limit (MW)"], default = 1e6), if initial_status < 0 && initial_power > 1e-3
scalar(dict["Startup limit (MW)"], default = 1e6), error("unit $unit_name has invalid initial power")
scalar(dict["Shutdown limit (MW)"], default = 1e6), end
initial_status, initial_status *= time_multiplier
initial_power, end
startup_categories,
unit_reserves, unit = Unit(
) unit_name,
push!(bus.units, unit) bus,
for r in unit_reserves max_power,
push!(r.units, unit) min_power,
timeseries(dict["Must run?"], default = [false for t in 1:T]),
min_power_cost,
segments,
scalar(dict["Minimum uptime (h)"], default = 1) * time_multiplier,
scalar(dict["Minimum downtime (h)"], default = 1) * time_multiplier,
scalar(dict["Ramp up limit (MW)"], default = 1e6),
scalar(dict["Ramp down limit (MW)"], default = 1e6),
scalar(dict["Startup limit (MW)"], default = 1e6),
scalar(dict["Shutdown limit (MW)"], default = 1e6),
initial_status,
initial_power,
startup_categories,
unit_reserves,
)
push!(bus.units, unit)
for r in unit_reserves
push!(r.units, unit)
end
name_to_unit[unit_name] = unit
push!(units, unit)
elseif lowercase(unit_type) === "profiled"
bus = name_to_bus[dict["Bus"]]
pu = ProfiledUnit(
unit_name,
bus,
timeseries(dict["Maximum power (MW)"]),
timeseries(dict["Cost (\$/MW)"])
)
push!(bus.profiled_units, pu)
push!(profiled_units, pu)
else
error("unit $unit_name has an invalid type")
end end
name_to_unit[unit_name] = unit
push!(units, unit)
end end
# Read transmission lines # Read transmission lines
@ -325,6 +344,8 @@ function _from_json(json; repair = true)
time = T, time = T,
units_by_name = Dict(g.name => g for g in units), units_by_name = Dict(g.name => g for g in units),
units = units, units = units,
profiled_units_by_name = Dict(pu.name => pu for pu in profiled_units),
profiled_units = profiled_units,
) )
if repair if repair
UnitCommitment.repair!(instance) UnitCommitment.repair!(instance)

@ -7,6 +7,7 @@ mutable struct Bus
offset::Int offset::Int
load::Vector{Float64} load::Vector{Float64}
units::Vector units::Vector
profiled_units::Vector
price_sensitive_loads::Vector price_sensitive_loads::Vector
end end
@ -48,6 +49,13 @@ mutable struct Unit
reserves::Vector{Reserve} reserves::Vector{Reserve}
end end
mutable struct ProfiledUnit
name::String
bus::Bus
capacity::Vector{Float64}
cost::Vector{Float64}
end
mutable struct TransmissionLine mutable struct TransmissionLine
name::String name::String
offset::Int offset::Int
@ -90,11 +98,14 @@ Base.@kwdef mutable struct UnitCommitmentInstance
time::Int time::Int
units_by_name::Dict{AbstractString,Unit} units_by_name::Dict{AbstractString,Unit}
units::Vector{Unit} units::Vector{Unit}
profiled_units_by_name::Dict{AbstractString,ProfiledUnit}
profiled_units::Vector{ProfiledUnit}
end end
function Base.show(io::IO, instance::UnitCommitmentInstance) function Base.show(io::IO, instance::UnitCommitmentInstance)
print(io, "UnitCommitmentInstance(") print(io, "UnitCommitmentInstance(")
print(io, "$(length(instance.units)) units, ") print(io, "$(length(instance.units)) units, ")
print(io, "$(length(instance.profiled_units)) profiled units, ")
print(io, "$(length(instance.buses)) buses, ") print(io, "$(length(instance.buses)) buses, ")
print(io, "$(length(instance.lines)) lines, ") print(io, "$(length(instance.lines)) lines, ")
print(io, "$(length(instance.contingencies)) contingencies, ") print(io, "$(length(instance.contingencies)) contingencies, ")

@ -90,6 +90,9 @@ function build_model(;
for ps in instance.price_sensitive_loads for ps in instance.price_sensitive_loads
_add_price_sensitive_load!(model, ps) _add_price_sensitive_load!(model, ps)
end end
for pu in instance.profiled_units
_add_profiled_unit!(model, pu)
end
_add_system_wide_eqs!(model) _add_system_wide_eqs!(model)
@objective(model, Min, model[:obj]) @objective(model, Min, model[:obj])
end end

@ -0,0 +1,27 @@
# 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.
function _add_profiled_unit!(
model::JuMP.Model,
pu::ProfiledUnit,
)::Nothing
punits = _init(model, :profiled_units)
net_injection = _init(model, :expr_net_injection)
for t in 1:model[:instance].time
# Decision variable
punits[pu.name, t] =
@variable(model, lower_bound = 0, upper_bound = pu.capacity[t])
# Objective function terms
add_to_expression!(model[:obj], punits[pu.name, t], pu.cost[t])
# Net injection
add_to_expression!(
net_injection[pu.bus.name, t],
punits[pu.name, t],
1.0,
)
end
return
end

Binary file not shown.

Binary file not shown.

@ -16,3 +16,11 @@ end
@test length(instance.buses) == 14 @test length(instance.buses) == 14
@test length(instance.lines) == 20 @test length(instance.lines) == 20
end end
@testset "read v0.4" begin
instance = UnitCommitment.read("$FIXTURES/ucjl-0.4.json.gz")
@test length(instance.units) == 6
@test length(instance.buses) == 14
@test length(instance.lines) == 20
@test length(instance.profiled_units) == 2
end

@ -124,3 +124,117 @@ end
@test unit.startup_categories[3].delay == 6 @test unit.startup_categories[3].delay == 6
@test unit.initial_status == -200 @test unit.initial_status == -200
end end
@testset "read_benchmark profiled-units" begin
instance = UnitCommitment.read("$FIXTURES/case14-profiled.json.gz")
@test length(instance.lines) == 20
@test length(instance.buses) == 14
@test length(instance.units) == 6
@test length(instance.profiled_units) == 2
@test length(instance.contingencies) == 19
@test length(instance.price_sensitive_loads) == 1
@test instance.time == 4
@test instance.lines[5].name == "l5"
@test instance.lines[5].source.name == "b2"
@test instance.lines[5].target.name == "b5"
@test instance.lines[5].reactance 0.17388
@test instance.lines[5].susceptance 10.037550333
@test instance.lines[5].normal_flow_limit == [1e8 for t in 1:4]
@test instance.lines[5].emergency_flow_limit == [1e8 for t in 1:4]
@test instance.lines[5].flow_limit_penalty == [5e3 for t in 1:4]
@test instance.lines_by_name["l5"].name == "l5"
@test instance.lines[1].name == "l1"
@test instance.lines[1].source.name == "b1"
@test instance.lines[1].target.name == "b2"
@test instance.lines[1].reactance 0.059170
@test instance.lines[1].susceptance 29.496860773945
@test instance.lines[1].normal_flow_limit == [300.0 for t in 1:4]
@test instance.lines[1].emergency_flow_limit == [400.0 for t in 1:4]
@test instance.lines[1].flow_limit_penalty == [1e3 for t in 1:4]
@test instance.buses[9].name == "b9"
@test instance.buses[9].load == [35.36638, 33.25495, 31.67138, 31.14353]
@test instance.buses_by_name["b9"].name == "b9"
@test instance.reserves[1].name == "r1"
@test instance.reserves[1].type == "spinning"
@test instance.reserves[1].amount == [100.0, 100.0, 100.0, 100.0]
@test instance.reserves_by_name["r1"].name == "r1"
unit = instance.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 instance.units_by_name["g1"].name == "g1"
unit = instance.units[2]
@test unit.name == "g2"
@test unit.must_run == [false for t in 1:4]
@test length(unit.reserves) == 1
unit = instance.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 instance.contingencies[1].lines == [instance.lines[1]]
@test instance.contingencies[1].units == []
@test instance.contingencies[1].name == "c1"
@test instance.contingencies_by_name["c1"].name == "c1"
load = instance.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 instance.price_sensitive_loads_by_name["ps1"].name == "ps1"
profiled_unit = instance.profiled_units[2]
@test profiled_unit.name == "g8"
@test profiled_unit.bus.name == "b5"
@test profiled_unit.cost == [50.0 for t in 1:4]
@test profiled_unit.capacity == [120.0 for t in 1:4]
@test instance.profiled_units_by_name["g8"].name == "g8"
end
Loading…
Cancel
Save