mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-06 08:18:51 -06:00
Added the profiled units
This commit is contained in:
@@ -32,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
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
|||||||
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
|
||||||
@@ -182,6 +183,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
|||||||
timeseries(dict["Load (MW)"]),
|
timeseries(dict["Load (MW)"]),
|
||||||
Unit[],
|
Unit[],
|
||||||
PriceSensitiveLoad[],
|
PriceSensitiveLoad[],
|
||||||
|
ProfiledUnit[],
|
||||||
)
|
)
|
||||||
name_to_bus[bus_name] = bus
|
name_to_bus[bus_name] = bus
|
||||||
push!(buses, bus)
|
push!(buses, bus)
|
||||||
@@ -207,15 +209,25 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
|||||||
|
|
||||||
# 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"]]
|
||||||
|
|
||||||
|
if lowercase(unit_type) === "thermal"
|
||||||
# Read production cost curve
|
# Read production cost curve
|
||||||
K = length(dict["Production cost curve (MW)"])
|
K = length(dict["Production cost curve (MW)"])
|
||||||
curve_mw = hcat(
|
curve_mw = hcat(
|
||||||
[timeseries(dict["Production cost curve (MW)"][k]) for k in 1:K]...,
|
[
|
||||||
|
timeseries(dict["Production cost curve (MW)"][k]) for
|
||||||
|
k in 1:K
|
||||||
|
]...,
|
||||||
)
|
)
|
||||||
curve_cost = hcat(
|
curve_cost = hcat(
|
||||||
[timeseries(dict["Production cost curve (\$)"][k]) for k in 1:K]...,
|
[
|
||||||
|
timeseries(dict["Production cost curve (\$)"][k]) for
|
||||||
|
k in 1:K
|
||||||
|
]...,
|
||||||
)
|
)
|
||||||
min_power = curve_mw[:, 1]
|
min_power = curve_mw[:, 1]
|
||||||
max_power = curve_mw[:, K]
|
max_power = curve_mw[:, K]
|
||||||
@@ -250,14 +262,18 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Read and validate initial conditions
|
# Read and validate initial conditions
|
||||||
initial_power = scalar(dict["Initial power (MW)"], default = nothing)
|
initial_power =
|
||||||
initial_status = scalar(dict["Initial status (h)"], default = nothing)
|
scalar(dict["Initial power (MW)"], default = nothing)
|
||||||
|
initial_status =
|
||||||
|
scalar(dict["Initial status (h)"], default = nothing)
|
||||||
if initial_power === nothing
|
if initial_power === nothing
|
||||||
initial_status === nothing ||
|
initial_status === nothing || error(
|
||||||
error("unit $unit_name has initial status but no initial power")
|
"unit $unit_name has initial status but no initial power",
|
||||||
|
)
|
||||||
else
|
else
|
||||||
initial_status !== nothing ||
|
initial_status !== nothing || error(
|
||||||
error("unit $unit_name has initial power but no initial status")
|
"unit $unit_name has initial power but no initial status",
|
||||||
|
)
|
||||||
initial_status != 0 ||
|
initial_status != 0 ||
|
||||||
error("unit $unit_name has invalid initial status")
|
error("unit $unit_name has invalid initial status")
|
||||||
if initial_status < 0 && initial_power > 1e-3
|
if initial_status < 0 && initial_power > 1e-3
|
||||||
@@ -274,8 +290,10 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
|||||||
timeseries(dict["Must run?"], default = [false for t in 1:T]),
|
timeseries(dict["Must run?"], default = [false for t in 1:T]),
|
||||||
min_power_cost,
|
min_power_cost,
|
||||||
segments,
|
segments,
|
||||||
scalar(dict["Minimum uptime (h)"], default = 1) * time_multiplier,
|
scalar(dict["Minimum uptime (h)"], default = 1) *
|
||||||
scalar(dict["Minimum downtime (h)"], default = 1) * time_multiplier,
|
time_multiplier,
|
||||||
|
scalar(dict["Minimum downtime (h)"], default = 1) *
|
||||||
|
time_multiplier,
|
||||||
scalar(dict["Ramp up limit (MW)"], default = 1e6),
|
scalar(dict["Ramp up limit (MW)"], default = 1e6),
|
||||||
scalar(dict["Ramp down limit (MW)"], default = 1e6),
|
scalar(dict["Ramp down limit (MW)"], default = 1e6),
|
||||||
scalar(dict["Startup limit (MW)"], default = 1e6),
|
scalar(dict["Startup limit (MW)"], default = 1e6),
|
||||||
@@ -291,6 +309,19 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
|||||||
end
|
end
|
||||||
name_to_unit[unit_name] = unit
|
name_to_unit[unit_name] = unit
|
||||||
push!(units, 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
|
end
|
||||||
|
|
||||||
# Read transmission lines
|
# Read transmission lines
|
||||||
@@ -371,6 +402,8 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
|||||||
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,
|
||||||
isf = spzeros(Float64, length(lines), length(buses) - 1),
|
isf = spzeros(Float64, length(lines), length(buses) - 1),
|
||||||
lodf = spzeros(Float64, length(lines), length(lines)),
|
lodf = spzeros(Float64, length(lines), length(lines)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ mutable struct Bus
|
|||||||
load::Vector{Float64}
|
load::Vector{Float64}
|
||||||
units::Vector
|
units::Vector
|
||||||
price_sensitive_loads::Vector
|
price_sensitive_loads::Vector
|
||||||
|
profiled_units::Vector
|
||||||
end
|
end
|
||||||
|
|
||||||
mutable struct CostSegment
|
mutable struct CostSegment
|
||||||
@@ -73,6 +74,13 @@ mutable struct PriceSensitiveLoad
|
|||||||
revenue::Vector{Float64}
|
revenue::Vector{Float64}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
mutable struct ProfiledUnit
|
||||||
|
name::String
|
||||||
|
bus::Bus
|
||||||
|
capacity::Vector{Float64}
|
||||||
|
cost::Vector{Float64}
|
||||||
|
end
|
||||||
|
|
||||||
Base.@kwdef mutable struct UnitCommitmentScenario
|
Base.@kwdef mutable struct UnitCommitmentScenario
|
||||||
buses_by_name::Dict{AbstractString,Bus}
|
buses_by_name::Dict{AbstractString,Bus}
|
||||||
buses::Vector{Bus}
|
buses::Vector{Bus}
|
||||||
@@ -92,6 +100,8 @@ Base.@kwdef mutable struct UnitCommitmentScenario
|
|||||||
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
|
||||||
|
|
||||||
Base.@kwdef mutable struct UnitCommitmentInstance
|
Base.@kwdef mutable struct UnitCommitmentInstance
|
||||||
@@ -108,6 +118,7 @@ function Base.show(io::IO, instance::UnitCommitmentInstance)
|
|||||||
print(io, "$(length(sc.lines)) lines, ")
|
print(io, "$(length(sc.lines)) lines, ")
|
||||||
print(io, "$(length(sc.contingencies)) contingencies, ")
|
print(io, "$(length(sc.contingencies)) contingencies, ")
|
||||||
print(io, "$(length(sc.price_sensitive_loads)) price sensitive loads, ")
|
print(io, "$(length(sc.price_sensitive_loads)) price sensitive loads, ")
|
||||||
|
print(io, "$(length(sc.profiled_units)) profiled units, ")
|
||||||
print(io, "$(instance.time) time steps")
|
print(io, "$(instance.time) time steps")
|
||||||
print(io, ")")
|
print(io, ")")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ function build_model(;
|
|||||||
for g in sc.units
|
for g in sc.units
|
||||||
_add_unit_dispatch!(model, g, formulation, sc)
|
_add_unit_dispatch!(model, g, formulation, sc)
|
||||||
end
|
end
|
||||||
|
for pu in sc.profiled_units
|
||||||
|
_add_profiled_unit!(model, pu, sc)
|
||||||
|
end
|
||||||
_add_system_wide_eqs!(model, sc)
|
_add_system_wide_eqs!(model, sc)
|
||||||
end
|
end
|
||||||
@objective(model, Min, model[:obj])
|
@objective(model, Min, model[:obj])
|
||||||
|
|||||||
32
src/model/formulations/base/punit.jl
Normal file
32
src/model/formulations/base/punit.jl
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# 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,
|
||||||
|
sc::UnitCommitmentScenario,
|
||||||
|
)::Nothing
|
||||||
|
punits = _init(model, :prod_profiled)
|
||||||
|
net_injection = _init(model, :expr_net_injection)
|
||||||
|
for t in 1:model[:instance].time
|
||||||
|
# Decision variable
|
||||||
|
punits[sc.name, pu.name, t] =
|
||||||
|
@variable(model, lower_bound = 0, upper_bound = pu.capacity[t])
|
||||||
|
|
||||||
|
# Objective function terms
|
||||||
|
add_to_expression!(
|
||||||
|
model[:obj],
|
||||||
|
punits[sc.name, pu.name, t],
|
||||||
|
pu.cost[t] * sc.probability,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Net injection
|
||||||
|
add_to_expression!(
|
||||||
|
net_injection[sc.name, pu.bus.name, t],
|
||||||
|
punits[sc.name, pu.name, t],
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
@@ -65,6 +65,7 @@ function solution(model::JuMP.Model)::OrderedDict
|
|||||||
sol = OrderedDict()
|
sol = OrderedDict()
|
||||||
for sc in instance.scenarios
|
for sc in instance.scenarios
|
||||||
sol[sc.name] = OrderedDict()
|
sol[sc.name] = OrderedDict()
|
||||||
|
if !isempty(sc.units)
|
||||||
sol[sc.name]["Production (MW)"] =
|
sol[sc.name]["Production (MW)"] =
|
||||||
OrderedDict(g.name => production(g, sc) for g in sc.units)
|
OrderedDict(g.name => production(g, sc) for g in sc.units)
|
||||||
sol[sc.name]["Production cost (\$)"] =
|
sol[sc.name]["Production cost (\$)"] =
|
||||||
@@ -73,11 +74,13 @@ function solution(model::JuMP.Model)::OrderedDict
|
|||||||
OrderedDict(g.name => startup_cost(g, sc) for g in sc.units)
|
OrderedDict(g.name => startup_cost(g, sc) for g in sc.units)
|
||||||
sol[sc.name]["Is on"] = timeseries(model[:is_on], sc.units)
|
sol[sc.name]["Is on"] = timeseries(model[:is_on], sc.units)
|
||||||
sol[sc.name]["Switch on"] = timeseries(model[:switch_on], sc.units)
|
sol[sc.name]["Switch on"] = timeseries(model[:switch_on], sc.units)
|
||||||
sol[sc.name]["Switch off"] = timeseries(model[:switch_off], sc.units)
|
sol[sc.name]["Switch off"] =
|
||||||
|
timeseries(model[:switch_off], sc.units)
|
||||||
sol[sc.name]["Net injection (MW)"] =
|
sol[sc.name]["Net injection (MW)"] =
|
||||||
timeseries(model[:net_injection], sc.buses, sc = sc)
|
timeseries(model[:net_injection], sc.buses, sc = sc)
|
||||||
sol[sc.name]["Load curtail (MW)"] =
|
sol[sc.name]["Load curtail (MW)"] =
|
||||||
timeseries(model[:curtail], sc.buses, sc = sc)
|
timeseries(model[:curtail], sc.buses, sc = sc)
|
||||||
|
end
|
||||||
if !isempty(sc.lines)
|
if !isempty(sc.lines)
|
||||||
sol[sc.name]["Line overflow (MW)"] =
|
sol[sc.name]["Line overflow (MW)"] =
|
||||||
timeseries(model[:overflow], sc.lines, sc = sc)
|
timeseries(model[:overflow], sc.lines, sc = sc)
|
||||||
@@ -86,6 +89,16 @@ function solution(model::JuMP.Model)::OrderedDict
|
|||||||
sol[sc.name]["Price-sensitive loads (MW)"] =
|
sol[sc.name]["Price-sensitive loads (MW)"] =
|
||||||
timeseries(model[:loads], sc.price_sensitive_loads, sc = sc)
|
timeseries(model[:loads], sc.price_sensitive_loads, sc = sc)
|
||||||
end
|
end
|
||||||
|
if !isempty(sc.profiled_units)
|
||||||
|
sol[sc.name]["Profiled production (MW)"] =
|
||||||
|
timeseries(model[:prod_profiled], sc.profiled_units, sc = sc)
|
||||||
|
sol[sc.name]["Profiled production cost (\$)"] = OrderedDict(
|
||||||
|
pu.name => [
|
||||||
|
value(model[:prod_profiled][sc.name, pu.name, t]) *
|
||||||
|
pu.cost[t] for t in 1:instance.time
|
||||||
|
] for pu in sc.profiled_units
|
||||||
|
)
|
||||||
|
end
|
||||||
sol[sc.name]["Spinning reserve (MW)"] = OrderedDict(
|
sol[sc.name]["Spinning reserve (MW)"] = OrderedDict(
|
||||||
r.name => OrderedDict(
|
r.name => OrderedDict(
|
||||||
g.name => [
|
g.name => [
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
|||||||
|
|
||||||
@test repr(instance) == (
|
@test repr(instance) == (
|
||||||
"UnitCommitmentInstance(1 scenarios, 6 units, 14 buses, " *
|
"UnitCommitmentInstance(1 scenarios, 6 units, 14 buses, " *
|
||||||
"20 lines, 19 contingencies, 1 price sensitive loads, 4 time steps)"
|
"20 lines, 19 contingencies, 1 price sensitive loads, 0 profiled units, 4 time steps)"
|
||||||
)
|
)
|
||||||
|
|
||||||
@test length(instance.scenarios) == 1
|
@test length(instance.scenarios) == 1
|
||||||
@@ -131,3 +131,23 @@ 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")
|
||||||
|
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.capacity == [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.capacity == [120.0 for t in 1:4]
|
||||||
|
@test sc.profiled_units_by_name["g8"].name == "g8"
|
||||||
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user