Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Maintenance constraints (and for thermal generators) #556

Merged
merged 32 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ffd278d
Initial maintenance constraints
cfe316 Oct 1, 2023
866c2b4
Add capacity reserve margin effect
cfe316 Oct 1, 2023
bea9f3f
fix capitaLIZATION
cfe316 Oct 3, 2023
56259fb
Update_to_JuMP_Style
cfe316 Oct 3, 2023
5b6435b
Rename reserves to ncapres, etc
cfe316 Oct 3, 2023
73b962a
Remove weights
cfe316 Oct 3, 2023
cc62c2e
typo
cfe316 Oct 3, 2023
cd8741c
Apply JuliaFormatter
cfe316 Oct 3, 2023
5017fa0
Use named constant key
cfe316 Oct 3, 2023
d137c06
Simplify implementation
cfe316 Oct 3, 2023
0027091
Use a common name for a collection
cfe316 Oct 3, 2023
c623d83
Simplify formulation
cfe316 Oct 3, 2023
bbf20c9
Add docstrings for methods
cfe316 Oct 3, 2023
b5cbbf1
Add docstrings
cfe316 Oct 3, 2023
38e0d15
Add validation
cfe316 Oct 3, 2023
603274f
Fix name
cfe316 Oct 3, 2023
34b7130
More verbose resource checking
cfe316 Oct 3, 2023
1c2c1af
Start adding docs
cfe316 Oct 3, 2023
ab6515a
Write maintenance docs
cfe316 Oct 3, 2023
c130ae1
Add developer docs
cfe316 Oct 3, 2023
a62a54a
Improve docs
cfe316 Oct 3, 2023
6cce768
Improve docs
cfe316 Oct 3, 2023
6c1d3aa
Add data documentation for inputs
cfe316 Oct 3, 2023
50a19c9
Add description of output file.
cfe316 Oct 3, 2023
b223f89
Simplify constraints
cfe316 Oct 3, 2023
b8e4578
Address comments
cfe316 Oct 4, 2023
84d5f4c
Address comments
cfe316 Oct 4, 2023
6e28936
Add maintenance to doc tree
cfe316 Oct 4, 2023
391bb1e
Typo
cfe316 Oct 4, 2023
6776b14
Better explanation
cfe316 Oct 4, 2023
b52fa38
Change maintenance column name
cfe316 Oct 4, 2023
af5c32e
Changelog
cfe316 Oct 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions src/model/resources/maintenance.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
const MAINTENANCEDOWNVARS = "MaintenanceDownVariables"
const MAINTENANCESHUTVARS = "MaintenanceShutVariables"
const HASMAINT = "has_maiNTENANCE"
cfe316 marked this conversation as resolved.
Show resolved Hide resolved

function get_maintenance(df::DataFrame)::Vector{Int}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide more documentation for these functions.

if "MAINT" in names(df)
df[df.MAINT.>0, :R_ID]
else
Vector{Int}[]
end
end

function maintenance_down_name(inputs::Dict, y::Int, suffix::AbstractString)
dfGen = inputs["dfGen"]
resource = dfGen[y, :Resource]
maintenance_down_name(resource, suffix)
end

function maintenance_down_name(resource::AbstractString, suffix::AbstractString)
"vMDOWN_" * resource * "_" * suffix
end

function sanity_check_maintenance(MAINTENANCE::Vector{Int}, inputs::Dict)
rep_periods = inputs["REP_PERIOD"]

is_maint_reqs = !isempty(MAINTENANCE)
if rep_periods > 1 && is_maint_reqs
@error """Resources with R_ID $MAINTENANCE have MAINT > 0,
but the number of representative periods ($rep_periods) is greater than 1.
These are incompatible with a Maintenance requirement."""
error("Incompatible GenX settings and maintenance requirements.")
end
end

@doc raw"""
controlling_maintenance_start_hours(p::Int, t::Int, maintenance_duration::Int, maintenance_begin_hours::UnitRange{Int64})

p: hours_per_subperiod
t: the current hour
maintenance_duration: length of a maintenance period
maintenance_begin_hours: collection of hours in which maintenance is allowed to start
"""
function controlling_maintenance_start_hours(p::Int, t::Int, maintenance_duration::Int, maintenance_begin_hours)
controlled_hours = hoursbefore(p, t, 0:(maintenance_duration-1))
return intersect(controlled_hours, maintenance_begin_hours)
end

@doc raw"""
maintenance_constraints!(EP::Model,
inputs::Dict,
resource_name::AbstractString,
suffix::AbstractString,
r_id::Int,
maint_begin_cadence::Int,
maint_dur::Int,
maint_freq_years::Int,
cap::Float64,
vcommit::Symbol,
ecap::Symbol,
integer_operational_unit_committment::Bool)

EP: the JuMP model
inputs: main data storage
resource_name: unique resource name
r_id: Resource ID (unique resource integer)
suffix: the part of the plant which has maintenance applied
maint_begin_cadence:
It may be too expensive (from an optimization perspective) to allow maintenance
to begin at any time step during the simulation. Instead this integer describes
the cadence of timesteps in which maintenance can begin. Must be at least 1.
maint_dur: Number of timesteps that maintenance takes. Must be at least 1.
maint_freq_years: 1 is maintenannce every year,
2 is maintenance every other year, etc. Must be at least 1.
cap: Plant electrical capacity.
vcommit: symbol of vCOMMIT-like variable.
ecap: symbol of eTotalCap-like variable.
integer_operational_unit_committment: whether this plant has integer unit
committment for operational variables.
"""
function maintenance_constraints!(EP::Model,
inputs::Dict,
resource_name::AbstractString,
suffix::AbstractString,
r_id::Int,
maint_begin_cadence::Int,
maint_dur::Int,
maint_freq_years::Int,
cap::Float64,
vcommit::Symbol,
ecap::Symbol,
integer_operational_unit_committment::Bool)

T = 1:inputs["T"] # Number of time steps (hours)
hours_per_subperiod = inputs["hours_per_subperiod"]
weights = inputs["omega"]

y = r_id
down_name = maintenance_down_name(resource_name, suffix)
shut_name = "vMSHUT_" * resource_name * "_" * suffix
down = Symbol(down_name)
shut = Symbol(shut_name)

union!(inputs["MaintenanceDownVariables"], (down,))
union!(inputs["MaintenanceShutVariables"], (shut,))

maintenance_begin_hours = 1:maint_begin_cadence:T[end]

# create variables
vMDOWN = EP[down] = @variable(EP, [t in T], base_name=down_name, lower_bound=0)
vMSHUT = EP[shut] = @variable(EP, [t in maintenance_begin_hours],
base_name=shut_name,
lower_bound=0)

if integer_operational_unit_committment
set_integer.(vMDOWN)
set_integer.(vMSHUT)
end

vcommit = EP[vcommit]
ecap = EP[ecap]

# Maintenance variables are measured in # of plants
@constraints(EP, begin
[t in T], vMDOWN[t] <= ecap[y] / cap
[t in maintenance_begin_hours], vMSHUT[t] <= ecap[y] / cap
end)

# Plant is non-committed during maintenance
@constraint(EP, [t in T], ecap[y] / cap - vcommit[y,t] >= vMDOWN[t])

controlling_hours(t) = controlling_maintenance_start_hours(hours_per_subperiod,
t,
maint_dur,
maintenance_begin_hours)
# Plant is down for the required number of hours
@constraint(EP, [t in T], vMDOWN[t] == sum(vMSHUT[controlling_hours(t)]))

# Plant frequire maintenance every (certain number of) year(s)
cfe316 marked this conversation as resolved.
Show resolved Hide resolved
@constraint(EP, sum(vMSHUT[t]*weights[t] for t in maintenance_begin_hours) >=
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does this work when you are only modeling one year?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, maybe it's pointless to have weights here.

In general

  • maintenance only works when you are modeling one year
  • if a plant requires maintenance every N years then 1/N of the plants require maintenance.

ecap[y] / cap / maint_freq_years)

return down, shut
end

function ensure_maintenance_variable_records!(inputs::Dict)
inputs[HASMAINT] = true
for var in (MAINTENANCEDOWNVARS, MAINTENANCESHUTVARS)
if var ∉ keys(inputs)
inputs[var] = Set{Symbol}()
end
end
end

function has_maintenance(inputs::Dict)::Bool
rep_periods = inputs["REP_PERIOD"]
HASMAINT in keys(inputs) && rep_periods == 1
end

function get_maintenance_down_variables(inputs::Dict)::Set{Symbol}
inputs[MAINTENANCEDOWNVARS]
end
26 changes: 25 additions & 1 deletion src/model/resources/thermal/thermal.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,16 @@ function thermal!(EP::Model, inputs::Dict, setup::Dict)

# Capacity Reserves Margin policy
if setup["CapacityReserveMargin"] > 0
@expression(EP, eCapResMarBalanceThermal[res=1:inputs["NCapacityReserveMargin"], t=1:T], sum(dfGen[y,Symbol("CapRes_$res")] * EP[:eTotalCap][y] for y in THERM_ALL))
reserves = inputs["NCapacityReserveMargin"]
cfe316 marked this conversation as resolved.
Show resolved Hide resolved
capresfactor(y, res) = dfGen[y, Symbol("CapRes_$res")]
@expression(EP, eCapResMarBalanceThermal[res in 1:reserves, t in 1:T],
sum(capresfactor(y, res) * EP[:eTotalCap][y] for y in THERM_ALL))
add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceThermal)

MAINT = get_maintenance(dfGen)
if !isempty(intersect(MAINT, THERM_COMMIT))
thermal_maintenance_capacity_reserve_margin_adj!(EP, inputs)
end
end
#=
##CO2 Polcy Module Thermal Generation by zone
Expand All @@ -41,3 +49,19 @@ function thermal!(EP::Model, inputs::Dict, setup::Dict)
EP[:eGenerationByZone] += eGenerationByThermAll
=# ##From main
end

function thermal_maintenance_capacity_reserve_margin_adj!(EP::Model,
cfe316 marked this conversation as resolved.
Show resolved Hide resolved
inputs::Dict)
dfGen = inputs["dfGen"]
T = inputs["T"] # Number of time steps (hours)
reserves = inputs["NCapacityReserveMargin"]
cfe316 marked this conversation as resolved.
Show resolved Hide resolved
THERM_COMMIT = inputs["THERM_COMMIT"]
MAINT = intersect(get_maintenance(dfGen), THERM_COMMIT)

capresfactor(y, res) = dfGen[y, Symbol("CapRes_$res")]
cap_size(y) = dfGen[y, :Cap_Size]
down_var(y) = EP[Symbol(maintenance_down_name(inputs, y, "THERM"))]
maint_adj = @expression(EP, [res in 1:reserves, t in 1:T],
-sum(capresfactor(y, res) * down_var(y)[t] * cap_size(y) for y in MAINT))
add_similar_to_expression!(EP[:eCapResMarBalance], maint_adj)
end
43 changes: 42 additions & 1 deletion src/model/resources/thermal/thermal_commit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,9 @@ function thermal_commit!(EP::Model, inputs::Dict, setup::Dict)
)

## END Constraints for thermal units subject to integer (discrete) unit commitment decisions

if !isempty(get_maintenance(dfGen))
maintenance_constraints_thermal!(EP, inputs, setup)
end
end

@doc raw"""
Expand Down Expand Up @@ -336,3 +338,42 @@ function thermal_commit_reserves!(EP::Model, inputs::Dict)

end


function maintenance_constraints_thermal!(EP::Model, inputs::Dict, setup::Dict)

@info "Maintenance Module for Thermal plants"

ensure_maintenance_variable_records!(inputs)
dfGen = inputs["dfGen"]
by_rid(rid, sym) = by_rid_df(rid, sym, dfGen)

MAINT = get_maintenance(dfGen)
resource(y) = by_rid(y, :Resource)
suffix="THERM"
cap(y) = by_rid(y, :Cap_Size)
maint_dur(y) = Int(floor(by_rid(y, :Maintenance_Duration)))
maint_freq(y) = Int(floor(by_rid(y, :Maintenance_Frequency_Years)))
maint_begin_cadence(y) = Int(floor(by_rid(y, :Maintenance_Begin_Cadence)))

integer_operational_unit_committment = setup["UCommit"] == 1

vcommit = :vCOMMIT
ecap = :eTotalCap

sanity_check_maintenance(MAINT, inputs)

for y in MAINT
maintenance_constraints!(EP,
inputs,
resource(y),
suffix,
y,
maint_begin_cadence(y),
maint_dur(y),
maint_freq(y),
cap(y),
vcommit,
ecap,
integer_operational_unit_committment)
end
end
30 changes: 30 additions & 0 deletions src/write_outputs/write_maintenance.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
function write_simple_csv(filename::AbstractString, df::DataFrame)
CSV.write(filename, df)
end

function write_simple_csv(filename::AbstractString,
header::Vector,
matrix)
df = DataFrame(matrix, header)
write_simple_csv(filename, df)
end

function prepare_timeseries_variables(EP::Model, set::Set{Symbol})
# function to extract data from DenseAxisArray
data(var) = value.(EP[var]).data

return DataFrame(set .=> data.(set))
end

function write_timeseries_variables(EP, set::Set{Symbol}, filename::AbstractString)
df = prepare_timeseries_variables(EP, set)
write_simple_csv(filename, df)
end

@doc raw"""
write_maintenance(path::AbstractString, inputs::Dict, EP::Model)
"""
function write_maintenance(path::AbstractString, inputs::Dict, EP::Model)
downvars = get_maintenance_down_variables(inputs)
write_timeseries_variables(EP, downvars, joinpath(path, "maint_down.csv"))
end
4 changes: 4 additions & 0 deletions src/write_outputs/write_outputs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ function write_outputs(EP::Model, path::AbstractString, setup::Dict, inputs::Dic
println("Time elapsed for writing co2 is")
println(elapsed_time_emissions)

if has_maintenance(inputs)
write_maintenance(path, inputs, EP)
end

# Temporary! Suppress these outputs until we know that they are compatable with multi-stage modeling
if setup["MultiStage"] == 0
dfPrice = DataFrame()
Expand Down