From ffd278d2acb8e639a50bc412d2731a619a734188 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Sun, 1 Oct 2023 17:34:10 -0400 Subject: [PATCH 01/32] Initial maintenance constraints No effects on CapResMargin re: COMMIT status --- src/model/resources/maintenance.jl | 190 ++++++++++++++++++ src/model/resources/thermal/thermal_commit.jl | 4 +- src/write_outputs/write_maintenance.jl | 30 +++ src/write_outputs/write_outputs.jl | 4 + 4 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 src/model/resources/maintenance.jl create mode 100644 src/write_outputs/write_maintenance.jl diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl new file mode 100644 index 0000000000..195ffd126d --- /dev/null +++ b/src/model/resources/maintenance.jl @@ -0,0 +1,190 @@ +const MAINTENANCEDOWNVARS = "MaintenanceDownVariables" +const MAINTENANCESHUTVARS = "MaintenanceShutVariables" +const HASMAINT = "has_maiNTENANCE" + +function get_maintenance(df::DataFrame)::Vector{Int} + if "MAINT" in names(df) + df[df.MAINT.>0, :R_ID] + else + Vector{Int}[] + end +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 = "vMDOWN_" * 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) + @constraint(EP, sum(vMSHUT[t]*weights[t] for t in maintenance_begin_hours) >= + 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 + +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) + + 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 + + MAINT = get_maintenance(dfGen) + 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 diff --git a/src/model/resources/thermal/thermal_commit.jl b/src/model/resources/thermal/thermal_commit.jl index 5e1b3e8b0f..3fe5191935 100644 --- a/src/model/resources/thermal/thermal_commit.jl +++ b/src/model/resources/thermal/thermal_commit.jl @@ -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""" diff --git a/src/write_outputs/write_maintenance.jl b/src/write_outputs/write_maintenance.jl new file mode 100644 index 0000000000..313d710669 --- /dev/null +++ b/src/write_outputs/write_maintenance.jl @@ -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 diff --git a/src/write_outputs/write_outputs.jl b/src/write_outputs/write_outputs.jl index 13e7df2f57..32abd1b6f0 100644 --- a/src/write_outputs/write_outputs.jl +++ b/src/write_outputs/write_outputs.jl @@ -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() From 866c2b4d91861299ddb973b1694fdf6e1b278233 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Sun, 1 Oct 2023 19:58:50 -0400 Subject: [PATCH 02/32] Add capacity reserve margin effect --- src/model/resources/maintenance.jl | 51 ++++--------------- src/model/resources/thermal/thermal.jl | 26 +++++++++- src/model/resources/thermal/thermal_commit.jl | 39 ++++++++++++++ 3 files changed, 75 insertions(+), 41 deletions(-) diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl index 195ffd126d..9336338df6 100644 --- a/src/model/resources/maintenance.jl +++ b/src/model/resources/maintenance.jl @@ -10,6 +10,16 @@ function get_maintenance(df::DataFrame)::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"] @@ -85,7 +95,7 @@ function maintenance_constraints!(EP::Model, weights = inputs["omega"] y = r_id - down_name = "vMDOWN_" * resource_name * "_" * suffix + down_name = maintenance_down_name(resource_name, suffix) shut_name = "vMSHUT_" * resource_name * "_" * suffix down = Symbol(down_name) shut = Symbol(shut_name) @@ -149,42 +159,3 @@ end function get_maintenance_down_variables(inputs::Dict)::Set{Symbol} inputs[MAINTENANCEDOWNVARS] 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) - - 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 - - MAINT = get_maintenance(dfGen) - 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 diff --git a/src/model/resources/thermal/thermal.jl b/src/model/resources/thermal/thermal.jl index 856313673c..97d9693fbb 100644 --- a/src/model/resources/thermal/thermal.jl +++ b/src/model/resources/thermal/thermal.jl @@ -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"] + 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 @@ -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, + inputs::Dict) + dfGen = inputs["dfGen"] + T = inputs["T"] # Number of time steps (hours) + reserves = inputs["NCapacityReserveMargin"] + 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 diff --git a/src/model/resources/thermal/thermal_commit.jl b/src/model/resources/thermal/thermal_commit.jl index 3fe5191935..c5a6161b7d 100644 --- a/src/model/resources/thermal/thermal_commit.jl +++ b/src/model/resources/thermal/thermal_commit.jl @@ -338,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 From bea9f3f06bf9fb90028ad9cc57f9d7f3d4bb2028 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 10:22:52 -0400 Subject: [PATCH 03/32] fix capitaLIZATION --- src/model/resources/maintenance.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl index 9336338df6..0044941bcc 100644 --- a/src/model/resources/maintenance.jl +++ b/src/model/resources/maintenance.jl @@ -1,6 +1,6 @@ const MAINTENANCEDOWNVARS = "MaintenanceDownVariables" const MAINTENANCESHUTVARS = "MaintenanceShutVariables" -const HASMAINT = "has_maiNTENANCE" +const HASMAINT = "HAS_MAINTENANCE" function get_maintenance(df::DataFrame)::Vector{Int} if "MAINT" in names(df) From 56259fb169bbe5ac7a3eec9350e78c64d581fdaa Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 10:25:52 -0400 Subject: [PATCH 04/32] Update_to_JuMP_Style --- src/model/resources/maintenance.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl index 0044941bcc..3c789fa598 100644 --- a/src/model/resources/maintenance.jl +++ b/src/model/resources/maintenance.jl @@ -1,6 +1,6 @@ -const MAINTENANCEDOWNVARS = "MaintenanceDownVariables" -const MAINTENANCESHUTVARS = "MaintenanceShutVariables" -const HASMAINT = "HAS_MAINTENANCE" +const MAINTENANCE_DOWN_VARS = "MaintenanceDownVariables" +const MAINTENANCE_SHUT_VARS = "MaintenanceShutVariables" +const HAS_MAINT = "HAS_MAINTENANCE" function get_maintenance(df::DataFrame)::Vector{Int} if "MAINT" in names(df) @@ -143,8 +143,8 @@ function maintenance_constraints!(EP::Model, end function ensure_maintenance_variable_records!(inputs::Dict) - inputs[HASMAINT] = true - for var in (MAINTENANCEDOWNVARS, MAINTENANCESHUTVARS) + inputs[HAS_MAINT] = true + for var in (MAINTENANCE_DOWN_VARS, MAINTENANCE_SHUT_VARS) if var ∉ keys(inputs) inputs[var] = Set{Symbol}() end @@ -153,9 +153,9 @@ end function has_maintenance(inputs::Dict)::Bool rep_periods = inputs["REP_PERIOD"] - HASMAINT in keys(inputs) && rep_periods == 1 + HAS_MAINT in keys(inputs) && rep_periods == 1 end function get_maintenance_down_variables(inputs::Dict)::Set{Symbol} - inputs[MAINTENANCEDOWNVARS] + inputs[MAINTENANCE_DOWN_VARS] end From 5b6435b5f74932717ebf8b0e3b4ece5b8748ea7d Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 10:30:58 -0400 Subject: [PATCH 05/32] Rename reserves to ncapres, etc --- src/model/resources/thermal/thermal.jl | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/model/resources/thermal/thermal.jl b/src/model/resources/thermal/thermal.jl index 97d9693fbb..a17ba932df 100644 --- a/src/model/resources/thermal/thermal.jl +++ b/src/model/resources/thermal/thermal.jl @@ -30,15 +30,15 @@ function thermal!(EP::Model, inputs::Dict, setup::Dict) # Capacity Reserves Margin policy if setup["CapacityReserveMargin"] > 0 - reserves = inputs["NCapacityReserveMargin"] - 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)) + ncapres = inputs["NCapacityReserveMargin"] + capresfactor(y, capres) = dfGen[y, Symbol("CapRes_$capres")] + @expression(EP, eCapResMarBalanceThermal[capres in 1:ncapres, t in 1:T], + sum(capresfactor(y, capres) * 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) + thermal_maintenance_capacity_reserve_margin_adjustment!(EP, inputs) end end #= @@ -50,18 +50,18 @@ function thermal!(EP::Model, inputs::Dict, setup::Dict) =# ##From main end -function thermal_maintenance_capacity_reserve_margin_adj!(EP::Model, - inputs::Dict) +function thermal_maintenance_capacity_reserve_margin_adjustment!(EP::Model, + inputs::Dict) dfGen = inputs["dfGen"] T = inputs["T"] # Number of time steps (hours) - reserves = inputs["NCapacityReserveMargin"] + ncapres = inputs["NCapacityReserveMargin"] THERM_COMMIT = inputs["THERM_COMMIT"] MAINT = intersect(get_maintenance(dfGen), THERM_COMMIT) - capresfactor(y, res) = dfGen[y, Symbol("CapRes_$res")] + capresfactor(y, capres) = dfGen[y, Symbol("CapRes_$capres")] 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)) + maint_adj = @expression(EP, [capres in 1:ncapres, t in 1:T], + -sum(capresfactor(y, capres) * down_var(y)[t] * cap_size(y) for y in MAINT)) add_similar_to_expression!(EP[:eCapResMarBalance], maint_adj) end From 73b962a5a7127f4e73caed0b61ce361d2b16a0ac Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 10:34:29 -0400 Subject: [PATCH 06/32] Remove weights --- src/model/resources/maintenance.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl index 3c789fa598..e6bd34d27c 100644 --- a/src/model/resources/maintenance.jl +++ b/src/model/resources/maintenance.jl @@ -92,7 +92,6 @@ function maintenance_constraints!(EP::Model, 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) @@ -136,7 +135,7 @@ function maintenance_constraints!(EP::Model, @constraint(EP, [t in T], vMDOWN[t] == sum(vMSHUT[controlling_hours(t)])) # Plant frequire maintenance every (certain number of) year(s) - @constraint(EP, sum(vMSHUT[t]*weights[t] for t in maintenance_begin_hours) >= + @constraint(EP, sum(vMSHUT[t] for t in maintenance_begin_hours) >= ecap[y] / cap / maint_freq_years) return down, shut From cc62c2e1366470c303ac9d7ca86bdf6306454d1c Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 10:35:54 -0400 Subject: [PATCH 07/32] typo --- src/model/resources/maintenance.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl index e6bd34d27c..cf0ee4cf24 100644 --- a/src/model/resources/maintenance.jl +++ b/src/model/resources/maintenance.jl @@ -134,7 +134,7 @@ function maintenance_constraints!(EP::Model, # 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) + # Plant require maintenance every (certain number of) year(s) @constraint(EP, sum(vMSHUT[t] for t in maintenance_begin_hours) >= ecap[y] / cap / maint_freq_years) From cd8741c17ae110aa4c644701e3b5c06496c038ac Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 11:24:14 -0400 Subject: [PATCH 08/32] Apply JuliaFormatter --- src/model/resources/maintenance.jl | 65 ++++++++++++++++---------- src/write_outputs/write_maintenance.jl | 4 +- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl index cf0ee4cf24..4d7d0d7be4 100644 --- a/src/model/resources/maintenance.jl +++ b/src/model/resources/maintenance.jl @@ -40,7 +40,12 @@ end 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) +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 @@ -77,18 +82,20 @@ end 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) +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"] @@ -105,10 +112,14 @@ function maintenance_constraints!(EP::Model, 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) + 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) @@ -125,18 +136,22 @@ function maintenance_constraints!(EP::Model, 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) + @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 require maintenance every (certain number of) year(s) - @constraint(EP, sum(vMSHUT[t] for t in maintenance_begin_hours) >= - ecap[y] / cap / maint_freq_years) + @constraint( + EP, + sum(vMSHUT[t] for t in maintenance_begin_hours) >= ecap[y] / cap / maint_freq_years + ) return down, shut end diff --git a/src/write_outputs/write_maintenance.jl b/src/write_outputs/write_maintenance.jl index 313d710669..f4a4612a96 100644 --- a/src/write_outputs/write_maintenance.jl +++ b/src/write_outputs/write_maintenance.jl @@ -2,9 +2,7 @@ function write_simple_csv(filename::AbstractString, df::DataFrame) CSV.write(filename, df) end -function write_simple_csv(filename::AbstractString, - header::Vector, - matrix) +function write_simple_csv(filename::AbstractString, header::Vector, matrix) df = DataFrame(matrix, header) write_simple_csv(filename, df) end From 5017fa0239b85a3cd7bd73d0b015f269b393c634 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 11:25:08 -0400 Subject: [PATCH 09/32] Use named constant key --- src/model/resources/maintenance.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl index 4d7d0d7be4..3254fefeb7 100644 --- a/src/model/resources/maintenance.jl +++ b/src/model/resources/maintenance.jl @@ -106,8 +106,8 @@ function maintenance_constraints!( down = Symbol(down_name) shut = Symbol(shut_name) - union!(inputs["MaintenanceDownVariables"], (down,)) - union!(inputs["MaintenanceShutVariables"], (shut,)) + union!(inputs[MAINTENANCE_DOWN_VARS], (down,)) + union!(inputs[MAINTENANCE_SHUT_VARS], (shut,)) maintenance_begin_hours = 1:maint_begin_cadence:T[end] From d137c060d60036a39d64b866b2afa84ac6db87c9 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 11:42:17 -0400 Subject: [PATCH 10/32] Simplify implementation --- src/model/resources/maintenance.jl | 38 +++++++++++-------- src/model/resources/thermal/thermal.jl | 5 ++- src/model/resources/thermal/thermal_commit.jl | 10 ++--- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl index 3254fefeb7..759c8a6b3a 100644 --- a/src/model/resources/maintenance.jl +++ b/src/model/resources/maintenance.jl @@ -2,7 +2,16 @@ const MAINTENANCE_DOWN_VARS = "MaintenanceDownVariables" const MAINTENANCE_SHUT_VARS = "MaintenanceShutVariables" const HAS_MAINT = "HAS_MAINTENANCE" -function get_maintenance(df::DataFrame)::Vector{Int} +@doc raw""" + resources_with_maintenance(df::DataFrame)::Vector{Int} + + Get a vector of the R_ID's of all resources listed in a dataframe + that have maintenance requirements. If there are none, return an empty vector. + + This method takes a specific dataframe because compound resources may have their + data in multiple dataframes. +""" +function resources_with_maintenance(df::DataFrame)::Vector{Int} if "MAINT" in names(df) df[df.MAINT.>0, :R_ID] else @@ -10,14 +19,12 @@ function get_maintenance(df::DataFrame)::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) +function maintenance_down_name(resource_component::AbstractString) + "vMDOWN_" * resource_component end -function maintenance_down_name(resource::AbstractString, suffix::AbstractString) - "vMDOWN_" * resource * "_" * suffix +function maintenance_shut_name(resource_component::AbstractString) + "vMSHUT_" * resource_component end function sanity_check_maintenance(MAINTENANCE::Vector{Int}, inputs::Dict) @@ -53,8 +60,7 @@ end @doc raw""" maintenance_constraints!(EP::Model, inputs::Dict, - resource_name::AbstractString, - suffix::AbstractString, + resource_component::AbstractString, r_id::Int, maint_begin_cadence::Int, maint_dur::Int, @@ -66,9 +72,10 @@ end EP: the JuMP model inputs: main data storage - resource_name: unique resource name + resource_component: unique resource name with optional component name + If the plant has more than one component, this could identify a specific part which + is undergoing maintenance. 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 @@ -85,8 +92,7 @@ end function maintenance_constraints!( EP::Model, inputs::Dict, - resource_name::AbstractString, - suffix::AbstractString, + resource_component::AbstractString, r_id::Int, maint_begin_cadence::Int, maint_dur::Int, @@ -97,12 +103,12 @@ function maintenance_constraints!( integer_operational_unit_committment::Bool, ) - T = 1:inputs["T"] # Number of time steps (hours) + T = 1:inputs["T"] hours_per_subperiod = inputs["hours_per_subperiod"] y = r_id - down_name = maintenance_down_name(resource_name, suffix) - shut_name = "vMSHUT_" * resource_name * "_" * suffix + down_name = maintenance_down_name(resource_component) + shut_name = maintenance_shut_name(resource_component) down = Symbol(down_name) shut = Symbol(shut_name) diff --git a/src/model/resources/thermal/thermal.jl b/src/model/resources/thermal/thermal.jl index a17ba932df..c56bfc05c3 100644 --- a/src/model/resources/thermal/thermal.jl +++ b/src/model/resources/thermal/thermal.jl @@ -36,7 +36,7 @@ function thermal!(EP::Model, inputs::Dict, setup::Dict) sum(capresfactor(y, capres) * EP[:eTotalCap][y] for y in THERM_ALL)) add_similar_to_expression!(EP[:eCapResMarBalance], eCapResMarBalanceThermal) - MAINT = get_maintenance(dfGen) + MAINT = resources_with_maintenance(dfGen) if !isempty(intersect(MAINT, THERM_COMMIT)) thermal_maintenance_capacity_reserve_margin_adjustment!(EP, inputs) end @@ -58,9 +58,10 @@ function thermal_maintenance_capacity_reserve_margin_adjustment!(EP::Model, THERM_COMMIT = inputs["THERM_COMMIT"] MAINT = intersect(get_maintenance(dfGen), THERM_COMMIT) + resource_component(y) = dfGen[y, :Resource] capresfactor(y, capres) = dfGen[y, Symbol("CapRes_$capres")] cap_size(y) = dfGen[y, :Cap_Size] - down_var(y) = EP[Symbol(maintenance_down_name(inputs, y, "THERM"))] + down_var(y) = EP[Symbol(maintenance_down_name(resource_component(y)))] maint_adj = @expression(EP, [capres in 1:ncapres, t in 1:T], -sum(capresfactor(y, capres) * down_var(y)[t] * cap_size(y) for y in MAINT)) add_similar_to_expression!(EP[:eCapResMarBalance], maint_adj) diff --git a/src/model/resources/thermal/thermal_commit.jl b/src/model/resources/thermal/thermal_commit.jl index c5a6161b7d..7567de748d 100644 --- a/src/model/resources/thermal/thermal_commit.jl +++ b/src/model/resources/thermal/thermal_commit.jl @@ -218,7 +218,7 @@ 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)) + if !isempty(resources_with_maintenance(dfGen)) maintenance_constraints_thermal!(EP, inputs, setup) end end @@ -347,9 +347,8 @@ function maintenance_constraints_thermal!(EP::Model, inputs::Dict, setup::Dict) 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" + MAINT = resources_with_maintenance(dfGen) + resource_component(y) = by_rid(y, :Resource) 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))) @@ -365,8 +364,7 @@ function maintenance_constraints_thermal!(EP::Model, inputs::Dict, setup::Dict) for y in MAINT maintenance_constraints!(EP, inputs, - resource(y), - suffix, + resource_component(y), y, maint_begin_cadence(y), maint_dur(y), From 0027091c91ee35999f75d82e332506764d1d8cb4 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 13:08:40 -0400 Subject: [PATCH 11/32] Use a common name for a collection --- src/model/resources/maintenance.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl index 759c8a6b3a..e518f58dc2 100644 --- a/src/model/resources/maintenance.jl +++ b/src/model/resources/maintenance.jl @@ -27,12 +27,12 @@ function maintenance_shut_name(resource_component::AbstractString) "vMSHUT_" * resource_component end -function sanity_check_maintenance(MAINTENANCE::Vector{Int}, inputs::Dict) +function sanity_check_maintenance(MAINT::Vector{Int}, inputs::Dict) rep_periods = inputs["REP_PERIOD"] - is_maint_reqs = !isempty(MAINTENANCE) + is_maint_reqs = !isempty(MAINT) if rep_periods > 1 && is_maint_reqs - @error """Resources with R_ID $MAINTENANCE have MAINT > 0, + @error """Resources with R_ID $MAINT 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.") From c623d834f2b38fa253ae381dbd0bf31a7b1eb125 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 13:16:59 -0400 Subject: [PATCH 12/32] Simplify formulation --- src/model/resources/maintenance.jl | 17 ++++++++--------- src/model/resources/thermal/thermal_commit.jl | 6 +++--- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl index e518f58dc2..2add2f5efe 100644 --- a/src/model/resources/maintenance.jl +++ b/src/model/resources/maintenance.jl @@ -1,6 +1,5 @@ const MAINTENANCE_DOWN_VARS = "MaintenanceDownVariables" const MAINTENANCE_SHUT_VARS = "MaintenanceShutVariables" -const HAS_MAINT = "HAS_MAINTENANCE" @doc raw""" resources_with_maintenance(df::DataFrame)::Vector{Int} @@ -58,7 +57,7 @@ function controlling_maintenance_start_hours( end @doc raw""" - maintenance_constraints!(EP::Model, + maintenance_formulation!(EP::Model, inputs::Dict, resource_component::AbstractString, r_id::Int, @@ -84,12 +83,15 @@ end 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. + 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. + + Creates maintenance-tracking variables and adds their Symbols to two Sets in `inputs`. + Adds constraints which act on the vCOMMIT-like variable. """ -function maintenance_constraints!( +function maintenance_formulation!( EP::Model, inputs::Dict, resource_component::AbstractString, @@ -158,12 +160,9 @@ function maintenance_constraints!( EP, sum(vMSHUT[t] for t in maintenance_begin_hours) >= ecap[y] / cap / maint_freq_years ) - - return down, shut end function ensure_maintenance_variable_records!(inputs::Dict) - inputs[HAS_MAINT] = true for var in (MAINTENANCE_DOWN_VARS, MAINTENANCE_SHUT_VARS) if var ∉ keys(inputs) inputs[var] = Set{Symbol}() @@ -173,7 +172,7 @@ end function has_maintenance(inputs::Dict)::Bool rep_periods = inputs["REP_PERIOD"] - HAS_MAINT in keys(inputs) && rep_periods == 1 + MAINTENANCE_DOWN_VARS in keys(inputs) && rep_periods == 1 end function get_maintenance_down_variables(inputs::Dict)::Set{Symbol} diff --git a/src/model/resources/thermal/thermal_commit.jl b/src/model/resources/thermal/thermal_commit.jl index 7567de748d..41914476fe 100644 --- a/src/model/resources/thermal/thermal_commit.jl +++ b/src/model/resources/thermal/thermal_commit.jl @@ -219,7 +219,7 @@ function thermal_commit!(EP::Model, inputs::Dict, setup::Dict) ## END Constraints for thermal units subject to integer (discrete) unit commitment decisions if !isempty(resources_with_maintenance(dfGen)) - maintenance_constraints_thermal!(EP, inputs, setup) + maintenance_formulation_thermal!(EP, inputs, setup) end end @@ -339,7 +339,7 @@ function thermal_commit_reserves!(EP::Model, inputs::Dict) end -function maintenance_constraints_thermal!(EP::Model, inputs::Dict, setup::Dict) +function maintenance_formulation_thermal!(EP::Model, inputs::Dict, setup::Dict) @info "Maintenance Module for Thermal plants" @@ -362,7 +362,7 @@ function maintenance_constraints_thermal!(EP::Model, inputs::Dict, setup::Dict) sanity_check_maintenance(MAINT, inputs) for y in MAINT - maintenance_constraints!(EP, + maintenance_formulation!(EP, inputs, resource_component(y), y, From bbf20c95ef093078a3de489c305df82781e06b9e Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 13:28:17 -0400 Subject: [PATCH 13/32] Add docstrings for methods --- src/model/resources/maintenance.jl | 42 +++++++++++++++++++++----- src/write_outputs/write_maintenance.jl | 2 +- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl index 2add2f5efe..462a25767b 100644 --- a/src/model/resources/maintenance.jl +++ b/src/model/resources/maintenance.jl @@ -160,21 +160,47 @@ function maintenance_formulation!( EP, sum(vMSHUT[t] for t in maintenance_begin_hours) >= ecap[y] / cap / maint_freq_years ) + + return end -function ensure_maintenance_variable_records!(inputs::Dict) +@doc raw""" + ensure_maintenance_variable_records!(dict::Dict) + + dict: a dictionary of model data + + This should be called by each method that adds maintenance formulations, + to ensure that certain entries in the model data dict exist. +""" +function ensure_maintenance_variable_records!(dict::Dict) for var in (MAINTENANCE_DOWN_VARS, MAINTENANCE_SHUT_VARS) - if var ∉ keys(inputs) - inputs[var] = Set{Symbol}() + if var ∉ keys(dict) + dict[var] = Set{Symbol}() end end end -function has_maintenance(inputs::Dict)::Bool - rep_periods = inputs["REP_PERIOD"] - MAINTENANCE_DOWN_VARS in keys(inputs) && rep_periods == 1 +@doc raw""" + has_maintenance(dict::Dict) + + dict: a dictionary of model data + + Checks whether the dictionary contains listings of maintenance-related variable names. + This is true only after `maintenance_formulation!` has been called. +""" +function has_maintenance(dict::Dict)::Bool + rep_periods = dict["REP_PERIOD"] + MAINTENANCE_DOWN_VARS in keys(dict) && rep_periods == 1 end -function get_maintenance_down_variables(inputs::Dict)::Set{Symbol} - inputs[MAINTENANCE_DOWN_VARS] +@doc raw""" + maintenance_down_variables(dict::Dict) + + dict: a dictionary of model data + + get listings of maintenance-related variable names. + This is available only after `maintenance_formulation!` has been called. +""" +function maintenance_down_variables(dict::Dict)::Set{Symbol} + dict[MAINTENANCE_DOWN_VARS] end diff --git a/src/write_outputs/write_maintenance.jl b/src/write_outputs/write_maintenance.jl index f4a4612a96..741abdcfa8 100644 --- a/src/write_outputs/write_maintenance.jl +++ b/src/write_outputs/write_maintenance.jl @@ -23,6 +23,6 @@ end write_maintenance(path::AbstractString, inputs::Dict, EP::Model) """ function write_maintenance(path::AbstractString, inputs::Dict, EP::Model) - downvars = get_maintenance_down_variables(inputs) + downvars = maintenance_down_variables(inputs) write_timeseries_variables(EP, downvars, joinpath(path, "maint_down.csv")) end From b5cbbf186172de0fac3c35e5e6d9719ed40a1fa1 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 13:33:20 -0400 Subject: [PATCH 14/32] Add docstrings --- src/model/resources/maintenance.jl | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl index 462a25767b..1d3db0c4f7 100644 --- a/src/model/resources/maintenance.jl +++ b/src/model/resources/maintenance.jl @@ -18,11 +18,23 @@ function resources_with_maintenance(df::DataFrame)::Vector{Int} end end -function maintenance_down_name(resource_component::AbstractString) +@doc raw""" + maintenance_down_name(resource_component::AbstractString)::String + + JuMP variable name to control whether a resource-component is down for maintenance. + Here resource-component could be a whole resource or a component (for complex resources). +""" +function maintenance_down_name(resource_component::AbstractString)::String "vMDOWN_" * resource_component end -function maintenance_shut_name(resource_component::AbstractString) +@doc raw""" + maintenance_shut_name(resource_component::AbstractString)::String + + JuMP variable name to control when a resource-components begins maintenance. + Here resource-component could be a whole resource or a component (for complex resources). +""" +function maintenance_shut_name(resource_component::AbstractString)::String "vMSHUT_" * resource_component end From 38e0d1555685fe14207957b590c9a9a3d6852449 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 13:45:22 -0400 Subject: [PATCH 15/32] Add validation --- src/model/resources/resources.jl | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/model/resources/resources.jl b/src/model/resources/resources.jl index af88ac1ab8..dafac0ae12 100644 --- a/src/model/resources/resources.jl +++ b/src/model/resources/resources.jl @@ -62,6 +62,39 @@ function check_longdurationstorage_applicability(r::GenXResource) return error_strings end +@doc raw""" + check_maintenance_applicability(r::GenXResource) + +Check whether the MAINT flag is set appropriately +""" +function check_maintenance_applicability(r::GenXResource) + applicable_resources = [:THERM] + + not_set = resource_attribute_not_set() + value = get(r, :MAINT, not_set) + + error_strings = String[] + + if value == not_set + # not MAINT so the rest is not applicable + return error_strings + end + + check_for_flag_set(el) = get(r, el, not_set) == 1 + statuses = check_for_flag_set.(applicable_resources) + + if count(statuses) == 0 + e = string("Resource ", resource_name(r), " has :MAINT = ", value, ".\n", + "This setting is valid only for resources where the type is \n", + "one of $applicable_resources. \n", + "Furthermore for THERM resources, it is valid only for \n", + "those which have unit commitment (THERM==1)." + ) + push!(error_strings, e) + end + return error_strings +end + @doc raw""" check_resource(r::GenXResource)::Vector{String} @@ -72,6 +105,7 @@ function check_resource(r::GenXResource)::Vector{String} e = String[] e = [e; check_resource_type_flags(r)] e = [e; check_longdurationstorage_applicability(r)] + e = [e; check_maintenance_applicability(r)] return e end From 603274fcf92b11e786b862adc4dd3b4da8184b7d Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 13:46:46 -0400 Subject: [PATCH 16/32] Fix name --- src/model/resources/thermal/thermal.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/model/resources/thermal/thermal.jl b/src/model/resources/thermal/thermal.jl index c56bfc05c3..3ca0b77f9a 100644 --- a/src/model/resources/thermal/thermal.jl +++ b/src/model/resources/thermal/thermal.jl @@ -56,13 +56,14 @@ function thermal_maintenance_capacity_reserve_margin_adjustment!(EP::Model, T = inputs["T"] # Number of time steps (hours) ncapres = inputs["NCapacityReserveMargin"] THERM_COMMIT = inputs["THERM_COMMIT"] - MAINT = intersect(get_maintenance(dfGen), THERM_COMMIT) + MAINT = resources_with_maintenance(dfGen) + applicable_resources = intersect(MAINT, THERM_COMMIT) resource_component(y) = dfGen[y, :Resource] capresfactor(y, capres) = dfGen[y, Symbol("CapRes_$capres")] cap_size(y) = dfGen[y, :Cap_Size] down_var(y) = EP[Symbol(maintenance_down_name(resource_component(y)))] maint_adj = @expression(EP, [capres in 1:ncapres, t in 1:T], - -sum(capresfactor(y, capres) * down_var(y)[t] * cap_size(y) for y in MAINT)) + -sum(capresfactor(y, capres) * down_var(y)[t] * cap_size(y) for y in applicable_resources)) add_similar_to_expression!(EP[:eCapResMarBalance], maint_adj) end From 34b71306aaa4a723f6c5cae6a9b5e6b8c65190a0 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 13:59:06 -0400 Subject: [PATCH 17/32] More verbose resource checking --- src/model/resources/resources.jl | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/model/resources/resources.jl b/src/model/resources/resources.jl index dafac0ae12..99e142dfe8 100644 --- a/src/model/resources/resources.jl +++ b/src/model/resources/resources.jl @@ -80,18 +80,22 @@ function check_maintenance_applicability(r::GenXResource) return error_strings end - check_for_flag_set(el) = get(r, el, not_set) == 1 + check_for_flag_set(el) = get(r, el, not_set) > 0 statuses = check_for_flag_set.(applicable_resources) if count(statuses) == 0 e = string("Resource ", resource_name(r), " has :MAINT = ", value, ".\n", "This setting is valid only for resources where the type is \n", "one of $applicable_resources. \n", - "Furthermore for THERM resources, it is valid only for \n", - "those which have unit commitment (THERM==1)." ) push!(error_strings, e) end + if get(r, :THERM, not_set) == 2 + e = string("Resource ", resource_name(r), " has :MAINT = ", value, ".\n", + "This is valid only for resources with unit commitment (:THERM = 1);\n", + "this has :THERM = 2.") + push!(error_strings, e) + end return error_strings end From 1c2c1af1bcc48554142031be72e8340b06aed03a Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 16:07:46 -0400 Subject: [PATCH 18/32] Start adding docs --- docs/src/maintenance.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 docs/src/maintenance.md diff --git a/docs/src/maintenance.md b/docs/src/maintenance.md new file mode 100644 index 0000000000..0819038251 --- /dev/null +++ b/docs/src/maintenance.md @@ -0,0 +1,4 @@ +# Scheduled Maintenance + +In the real world, some types of resources require regular scheduled maintenance. +This is a concern for From ab6515ad18dcd9246e0bf1d38c30c0992a786f61 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 17:41:13 -0400 Subject: [PATCH 19/32] Write maintenance docs --- docs/src/maintenance.md | 76 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/docs/src/maintenance.md b/docs/src/maintenance.md index 0819038251..9b5a9ed317 100644 --- a/docs/src/maintenance.md +++ b/docs/src/maintenance.md @@ -1,4 +1,74 @@ -# Scheduled Maintenance +# Optimized Scheduled Maintenance +_Added in v0.4_ -In the real world, some types of resources require regular scheduled maintenance. -This is a concern for +In the real world, some types of resources (notably, fission) require regular scheduled maintenance, which often takes several weeks. +During this time, the plant produces no power. +This module allows GenX to find the best time of year for plants to undergo maintenance. + +GenX can model scheduled maintenance for only some types of plants: + +* Thermal plants with Unit Commitment (THERM=1) + +## Description of the maintenance model +A plant requires a single contiguous period of $h \ge 1$ hours of maintenance, every $y \ge 1$ years. +For each plant, the best time to start the maintenance period is determined by the optimizer. + +During maintenance, the plant cannot be "committed", and therefore + +* uses no fuel, +* produces no power, +* and does not contribute to reserves. + +Additionally, + +* the plant does not contribute to any Capacity Reserve Margin. + +### Treatment of plants that require maintenance only every few years +GenX models a long-term equilibrium, +and each problem generally represents a single full year. +If a plant requires maintenance every $y$ years, we take the simplification that at least $1/y$ of the plants must undergo maintenance in the modeled year. + +See also "Interaction with integer unit committment" below. + +### Reduction of number of possible start dates +This module creates constraints which work across long periods, and consequently can be very expensive to solve. +In order to reduce the expense, the set of possible maintenance start dates can be limited. +Rather than have maintenance potentially start every hour, one can have possible start dates which are once per day, once per week, etc. +(In reality, maintenance is likely scheduled months in advance, so optimizing down to the hour may not be realistic anyway.) + +## How to add scheduled maintenance requirements for a plant +There are four columns which need to be added to the plant data, i.e. in `Generators_data.csv`: + +1. `MAINT` should be `1` for plants that require maintenance and `0` otherwise. +2. `Maintenance_Duration` is the number of hours the maintenance period lasts. +3. `Maintenance_Frequency_Years`. If `1`, maintenance every year, if `3` maintenance every 3 years, etc. +4. `Maintenance_Begin_Cadence`. Spacing between hours in which maintenance can start. + +The last three fields must be integers which are greater than 0. +They are ignored for any plants which do not require maintenance. + +`Maintenance_Duration` must be less than the total number of hours in the year. + +If `Maintenance_Begin_Cadence` is `1` then the maintenance can begin in any hour. +If it is `168` then it can begin in hours 1, 169, 337, etc. + +## Restrictions on use +The maintenance module has these restrictions: + +- More than a single maintenance period per year (i.e. every three months) is not possible in the current formulation. +- Only full-year cases can be run; there must be only one "representative period". +It would not make sense to model a *month*-long maintenance period when the year is modeled as a series of representative *weeks*, for example. +- Multi-stage has not yet been tested (but please let us know what happens if you test it!). + +### Interaction with integer unit committment +If integer unit committment is on (`UCommit=1`), this module may not produce sensible results. +This module works on the level of individual resources (i.e. a specific type of plant in a specific zone.). +If there is only 1 unit of a given resource built in a zone, then it will undergo maintenance every year regardless of its `Maintenance_Frequency_Years`. + +## Hint: pre-scheduling maintenance +If you want to pre-schedule when maintenance occurs, you might not need this module. +Instead, you could set the maximum power output of the plant to zero for a certain period, or make its fuel extremely expensive during that time. +However, the plant would still be able to contribute to the Capacity Reserve Margin. + +## Outputs produced +If at least one plant has `MAINT=1`, a file `maint_down.csv` will be written listing how many plants are down for maintenance in each timestep. From c130ae114a8b19754d5b2df308c2f55d68aceded Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 18:06:54 -0400 Subject: [PATCH 20/32] Add developer docs --- docs/src/maintenance.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/src/maintenance.md b/docs/src/maintenance.md index 9b5a9ed317..db412b297c 100644 --- a/docs/src/maintenance.md +++ b/docs/src/maintenance.md @@ -72,3 +72,12 @@ However, the plant would still be able to contribute to the Capacity Reserve Mar ## Outputs produced If at least one plant has `MAINT=1`, a file `maint_down.csv` will be written listing how many plants are down for maintenance in each timestep. + +## Developer note: adding maintenance to a resource +The maintenance formulation is applied on a per-resource basis, by calling the function `maintenance_formulation!`. + +```@docs +GenX.maintenance_formulation! +``` + + From a62a54a7085f7cf9a0dde40ac389f571de8902fb Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 18:14:56 -0400 Subject: [PATCH 21/32] Improve docs --- docs/src/maintenance.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/src/maintenance.md b/docs/src/maintenance.md index db412b297c..cdf89b5789 100644 --- a/docs/src/maintenance.md +++ b/docs/src/maintenance.md @@ -13,7 +13,7 @@ GenX can model scheduled maintenance for only some types of plants: A plant requires a single contiguous period of $h \ge 1$ hours of maintenance, every $y \ge 1$ years. For each plant, the best time to start the maintenance period is determined by the optimizer. -During maintenance, the plant cannot be "committed", and therefore +During maintenance, the plant cannot be "commited", and therefore * uses no fuel, * produces no power, @@ -28,7 +28,7 @@ GenX models a long-term equilibrium, and each problem generally represents a single full year. If a plant requires maintenance every $y$ years, we take the simplification that at least $1/y$ of the plants must undergo maintenance in the modeled year. -See also "Interaction with integer unit committment" below. +See also "Interaction with integer unit commitment" below. ### Reduction of number of possible start dates This module creates constraints which work across long periods, and consequently can be very expensive to solve. @@ -36,7 +36,7 @@ In order to reduce the expense, the set of possible maintenance start dates can Rather than have maintenance potentially start every hour, one can have possible start dates which are once per day, once per week, etc. (In reality, maintenance is likely scheduled months in advance, so optimizing down to the hour may not be realistic anyway.) -## How to add scheduled maintenance requirements for a plant +## How to use There are four columns which need to be added to the plant data, i.e. in `Generators_data.csv`: 1. `MAINT` should be `1` for plants that require maintenance and `0` otherwise. @@ -60,8 +60,8 @@ The maintenance module has these restrictions: It would not make sense to model a *month*-long maintenance period when the year is modeled as a series of representative *weeks*, for example. - Multi-stage has not yet been tested (but please let us know what happens if you test it!). -### Interaction with integer unit committment -If integer unit committment is on (`UCommit=1`), this module may not produce sensible results. +### Interaction with integer unit commitment +If integer unit commitment is on (`UCommit=1`), this module may not produce sensible results. This module works on the level of individual resources (i.e. a specific type of plant in a specific zone.). If there is only 1 unit of a given resource built in a zone, then it will undergo maintenance every year regardless of its `Maintenance_Frequency_Years`. @@ -73,11 +73,19 @@ However, the plant would still be able to contribute to the Capacity Reserve Mar ## Outputs produced If at least one plant has `MAINT=1`, a file `maint_down.csv` will be written listing how many plants are down for maintenance in each timestep. +## Notes on mathematical formulation +The formulation of the maintenance state is very similar to the formulation of unit commitment. + +There is a variable called something like `vMSHUT` which is analogous to `vSTART` and controls the start of the maintenance period. +There is another variable called something like `vMDOWN` analogous to `vCOMMIT` which controls the maintenance status in any hour. + +A constraint ensures that the value of `vMDOWN` in any hour is always more than the number of `vMSHUT`s in the previous `Maintenance_Duration` hours. + +Another constraint ensures that the number of plants committed (`vCOMMIT`) at any one time plus the number of plants under maintenance (`vMDOWN`) is less than the total number of plants. + ## Developer note: adding maintenance to a resource The maintenance formulation is applied on a per-resource basis, by calling the function `maintenance_formulation!`. ```@docs GenX.maintenance_formulation! ``` - - From 6cce768069c9c1c87af909de83ebae6101046741 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 18:25:08 -0400 Subject: [PATCH 22/32] Improve docs --- docs/src/maintenance.md | 6 ++++ src/model/resources/thermal/thermal.jl | 17 ---------- src/model/resources/thermal/thermal_commit.jl | 32 +++++++++++++++++-- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/docs/src/maintenance.md b/docs/src/maintenance.md index cdf89b5789..a05c07765a 100644 --- a/docs/src/maintenance.md +++ b/docs/src/maintenance.md @@ -89,3 +89,9 @@ The maintenance formulation is applied on a per-resource basis, by calling the f ```@docs GenX.maintenance_formulation! ``` + +See `maintenance_formulation_thermal_commit!` for an example of how to apply it to a new resource. + +* The resource must have a `vCOMMIT`-like variable which is proportional to maximum the power output, etc at any given timestep. +* The resource must have a `eTotalCap`-like quantity and a `Cap_Size`-like parameter; only the ratio of the two is used. + diff --git a/src/model/resources/thermal/thermal.jl b/src/model/resources/thermal/thermal.jl index 3ca0b77f9a..a849a16d46 100644 --- a/src/model/resources/thermal/thermal.jl +++ b/src/model/resources/thermal/thermal.jl @@ -50,20 +50,3 @@ function thermal!(EP::Model, inputs::Dict, setup::Dict) =# ##From main end -function thermal_maintenance_capacity_reserve_margin_adjustment!(EP::Model, - inputs::Dict) - dfGen = inputs["dfGen"] - T = inputs["T"] # Number of time steps (hours) - ncapres = inputs["NCapacityReserveMargin"] - THERM_COMMIT = inputs["THERM_COMMIT"] - MAINT = resources_with_maintenance(dfGen) - applicable_resources = intersect(MAINT, THERM_COMMIT) - - resource_component(y) = dfGen[y, :Resource] - capresfactor(y, capres) = dfGen[y, Symbol("CapRes_$capres")] - cap_size(y) = dfGen[y, :Cap_Size] - down_var(y) = EP[Symbol(maintenance_down_name(resource_component(y)))] - maint_adj = @expression(EP, [capres in 1:ncapres, t in 1:T], - -sum(capresfactor(y, capres) * down_var(y)[t] * cap_size(y) for y in applicable_resources)) - add_similar_to_expression!(EP[:eCapResMarBalance], maint_adj) -end diff --git a/src/model/resources/thermal/thermal_commit.jl b/src/model/resources/thermal/thermal_commit.jl index 41914476fe..df0b405559 100644 --- a/src/model/resources/thermal/thermal_commit.jl +++ b/src/model/resources/thermal/thermal_commit.jl @@ -219,7 +219,7 @@ function thermal_commit!(EP::Model, inputs::Dict, setup::Dict) ## END Constraints for thermal units subject to integer (discrete) unit commitment decisions if !isempty(resources_with_maintenance(dfGen)) - maintenance_formulation_thermal!(EP, inputs, setup) + maintenance_formulation_thermal_commit!(EP, inputs, setup) end end @@ -338,8 +338,12 @@ function thermal_commit_reserves!(EP::Model, inputs::Dict) end +@doc raw""" + maintenance_formulation_thermal_commit!(EP::Model, inputs::Dict, setup::Dict) -function maintenance_formulation_thermal!(EP::Model, inputs::Dict, setup::Dict) + Creates maintenance variables and constraints for thermal-commit plants. +""" +function maintenance_formulation_thermal_commit!(EP::Model, inputs::Dict, setup::Dict) @info "Maintenance Module for Thermal plants" @@ -375,3 +379,27 @@ function maintenance_formulation_thermal!(EP::Model, inputs::Dict, setup::Dict) integer_operational_unit_committment) end end + +@doc raw""" + thermal_maintenance_capacity_reserve_margin_adjustment!(EP::Model, inputs::Dict) + + Eliminates the contribution of a plant to the capacity reserve margin while it is down + for maintenance. +""" +function thermal_maintenance_capacity_reserve_margin_adjustment!(EP::Model, + inputs::Dict) + dfGen = inputs["dfGen"] + T = inputs["T"] # Number of time steps (hours) + ncapres = inputs["NCapacityReserveMargin"] + THERM_COMMIT = inputs["THERM_COMMIT"] + MAINT = resources_with_maintenance(dfGen) + applicable_resources = intersect(MAINT, THERM_COMMIT) + + resource_component(y) = dfGen[y, :Resource] + capresfactor(y, capres) = dfGen[y, Symbol("CapRes_$capres")] + cap_size(y) = dfGen[y, :Cap_Size] + down_var(y) = EP[Symbol(maintenance_down_name(resource_component(y)))] + maint_adj = @expression(EP, [capres in 1:ncapres, t in 1:T], + -sum(capresfactor(y, capres) * down_var(y)[t] * cap_size(y) for y in applicable_resources)) + add_similar_to_expression!(EP[:eCapResMarBalance], maint_adj) +end From 6c1d3aa8013b798358b2e90a3b5b44f66cb8ff39 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 18:33:23 -0400 Subject: [PATCH 23/32] Add data documentation for inputs --- docs/src/data_documentation.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/src/data_documentation.md b/docs/src/data_documentation.md index 068b584302..476dd95883 100644 --- a/docs/src/data_documentation.md +++ b/docs/src/data_documentation.md @@ -355,6 +355,7 @@ This file contains cost and performance parameters for various generators and ot |Min\_Retired\_Cap\_MW |Minimum required discharge capacity retirements in the current model period. This field can be used to enforce lifetime retirements of existing capacity. Note that for co-located VRE-STOR resources, this value pertains to the grid connection (other minimum required discharge capacity retirements for different components of the resource can be found in the VRE-STOR dataframe). | |Min\_Retired\_Energy\_Cap\_MW |Minimum required energy capacity retirements in the current model period. This field can be used to enforce lifetime retirements of existing energy capacity. Note that for co-located VRE-STOR resources, this value pertains to the storage component (other minimum required capacity retirements for different components of the resource can be found in the VRE-STOR dataframe).| |Min\_Retired\_Charge\_Cap\_MW |Minimum required energy capacity retirements in the current model period. This field can be used to enforce lifetime retirements of existing charge capacity. | + ###### Table 6: Settings-specific columns in the Generators\_data.csv file --- |**Column Name** | **Description**| @@ -388,6 +389,11 @@ This file contains cost and performance parameters for various generators and ot |PWFU\_Fuel\_Usage\_Zero\_Load\_MMBTU\_per\_h|The fuel usage (MMBTU/h) for the first PWFU segemnt (y-intercept) at zero load.| |PWFU\_Heat\_Rate\_MMBTU\_per\_MWh\_*i| The slope of fuel usage function of the segment i.| |PWFU\_Load\_Point\_MW\_*i| The end of segment i (MW).| +|**Maintenance data**| +|MAINT|[0,1], toggles scheduled maintenance formulation.| +|Maintenance\_Duration| Duration of the maintenance period, in number of timesteps. Only used if `MAINT=1`.| +|Maintenance\_Frequency\_Years| (Inverse) frequency of scheduled maintenance, in years. Only used if `MAINT=1`.| +|Maintenance\_Begin\_Cadence| Cadence of timesteps in which scheduled maintenance can begin. Only used if `MAINT=1`.| |**Electrolyzer related parameters required if the set ELECTROLYZER is not empty**| |Hydrogen_MWh_Per_Tonne| Electrolyzer efficiency in megawatt-hours (MWh) of electricity per metric tonne of hydrogen produced (MWh/t)| |Electrolyzer_Min_kt| Minimum annual quantity of hydrogen that must be produced by electrolyzer in kilotonnes (kt)| From 50a19c91762bbaa2df9dfc283cf21052ae232c8b Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 18:37:47 -0400 Subject: [PATCH 24/32] Add description of output file. --- docs/src/data_documentation.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/src/data_documentation.md b/docs/src/data_documentation.md index 476dd95883..08e57042ec 100644 --- a/docs/src/data_documentation.md +++ b/docs/src/data_documentation.md @@ -1017,3 +1017,8 @@ Reports solar PV generation in AC terms by each co-located VRE and storage resou #### 3.2.15 vre_stor_wind_power.csv Reports wind generation in AC terms by each co-located VRE and storage resource in each model time step. + +#### 3.2.16 maint_down.csv + +Only written if at least one plant has the scheduled maintenance formulation enabled. +Reports the number of resource-components which are under maintenance during each model time step. From b223f8931fdcbb8237b298a738228b02d3cbad39 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 18:45:52 -0400 Subject: [PATCH 25/32] Simplify constraints --- src/model/resources/maintenance.jl | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl index 1d3db0c4f7..aa6a21ced0 100644 --- a/src/model/resources/maintenance.jl +++ b/src/model/resources/maintenance.jl @@ -79,7 +79,7 @@ end cap::Float64, vcommit::Symbol, ecap::Symbol, - integer_operational_unit_committment::Bool) + integer_operational_unit_commitment::Bool) EP: the JuMP model inputs: main data storage @@ -97,8 +97,8 @@ end 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. + integer_operational_unit_commitment: whether this plant has integer unit + commitment for operational variables. Creates maintenance-tracking variables and adds their Symbols to two Sets in `inputs`. Adds constraints which act on the vCOMMIT-like variable. @@ -114,7 +114,7 @@ function maintenance_formulation!( cap::Float64, vcommit::Symbol, ecap::Symbol, - integer_operational_unit_committment::Bool, + integer_operational_unit_commitment::Bool, ) T = 1:inputs["T"] @@ -141,7 +141,7 @@ function maintenance_formulation!( lower_bound = 0 ) - if integer_operational_unit_committment + if integer_operational_unit_commitment set_integer.(vMDOWN) set_integer.(vMSHUT) end @@ -151,12 +151,11 @@ function maintenance_formulation!( # 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]) + @constraint(EP, [t in T], vMDOWN[t] + vcommit[y, t] <= ecap[y] / cap) controlling_hours(t) = controlling_maintenance_start_hours( hours_per_subperiod, From b8e457870d73a68684fa8c8e8a1f641d54974daa Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 22:54:18 -0400 Subject: [PATCH 26/32] Address comments --- docs/src/data_documentation.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/data_documentation.md b/docs/src/data_documentation.md index 08e57042ec..5447b3b63e 100644 --- a/docs/src/data_documentation.md +++ b/docs/src/data_documentation.md @@ -391,9 +391,9 @@ This file contains cost and performance parameters for various generators and ot |PWFU\_Load\_Point\_MW\_*i| The end of segment i (MW).| |**Maintenance data**| |MAINT|[0,1], toggles scheduled maintenance formulation.| -|Maintenance\_Duration| Duration of the maintenance period, in number of timesteps. Only used if `MAINT=1`.| -|Maintenance\_Frequency\_Years| (Inverse) frequency of scheduled maintenance, in years. Only used if `MAINT=1`.| -|Maintenance\_Begin\_Cadence| Cadence of timesteps in which scheduled maintenance can begin. Only used if `MAINT=1`.| +|Maintenance\_Duration| (Positive integer, less than total length of simulation.) Duration of the maintenance period, in number of timesteps. Only used if `MAINT=1`.| +|Maintenance\_Frequency\_Years| Length of scheduled maintenance cycle, in years. `1` is maintenance every year, `3` is every three years, etc. (Positive integer. Only used if `MAINT=1`.)| +|Maintenance\_Begin\_Cadence| Cadence of timesteps in which scheduled maintenance can begin. `1` means that a maintenance period can start in any timestep, `24` means it can start only in timesteps 1, 25, 49, etc. A larger number can decrease the simulation computational cost as it limits the optimizer's choices. (Positive integer, less than total length of simulation. Only used if `MAINT=1`.)| |**Electrolyzer related parameters required if the set ELECTROLYZER is not empty**| |Hydrogen_MWh_Per_Tonne| Electrolyzer efficiency in megawatt-hours (MWh) of electricity per metric tonne of hydrogen produced (MWh/t)| |Electrolyzer_Min_kt| Minimum annual quantity of hydrogen that must be produced by electrolyzer in kilotonnes (kt)| From 84d5f4cd9fd199c2f6d6ebc904e4c05fc94f23ef Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 3 Oct 2023 22:56:52 -0400 Subject: [PATCH 27/32] Address comments --- docs/src/maintenance.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/src/maintenance.md b/docs/src/maintenance.md index a05c07765a..22d6f1d5b0 100644 --- a/docs/src/maintenance.md +++ b/docs/src/maintenance.md @@ -5,9 +5,7 @@ In the real world, some types of resources (notably, fission) require regular sc During this time, the plant produces no power. This module allows GenX to find the best time of year for plants to undergo maintenance. -GenX can model scheduled maintenance for only some types of plants: - -* Thermal plants with Unit Commitment (THERM=1) +Scheduled maintenance is implemented **only** for thermal plants with unit commitment (THERM=1). ## Description of the maintenance model A plant requires a single contiguous period of $h \ge 1$ hours of maintenance, every $y \ge 1$ years. @@ -58,7 +56,6 @@ The maintenance module has these restrictions: - More than a single maintenance period per year (i.e. every three months) is not possible in the current formulation. - Only full-year cases can be run; there must be only one "representative period". It would not make sense to model a *month*-long maintenance period when the year is modeled as a series of representative *weeks*, for example. -- Multi-stage has not yet been tested (but please let us know what happens if you test it!). ### Interaction with integer unit commitment If integer unit commitment is on (`UCommit=1`), this module may not produce sensible results. From 6e2893654f9cd923ff8a5948cd159078a3a081bb Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Wed, 4 Oct 2023 14:26:20 -0400 Subject: [PATCH 28/32] Add maintenance to doc tree --- docs/make.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/make.jl b/docs/make.jl index 9cf0d9b0b7..4c76763ebe 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -50,6 +50,7 @@ pages = OrderedDict( "Thermal No Commit" => "thermal_no_commit.md" ], "Hydrogen Electrolyzers" => "electrolyzers.md", + "Scheduled maintenance for various resources" => "maintenance.md", ], "Multi_stage" => [ "Configure multi-stage inputs" => "configure_multi_stage_inputs.md", From 391bb1e7cae22fccdd680fcdd9317d7aff4ab30c Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Wed, 4 Oct 2023 14:28:35 -0400 Subject: [PATCH 29/32] Typo --- src/model/resources/maintenance.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/resources/maintenance.jl b/src/model/resources/maintenance.jl index aa6a21ced0..1499fa09c8 100644 --- a/src/model/resources/maintenance.jl +++ b/src/model/resources/maintenance.jl @@ -166,7 +166,7 @@ function maintenance_formulation!( # Plant is down for the required number of hours @constraint(EP, [t in T], vMDOWN[t] == sum(vMSHUT[controlling_hours(t)])) - # Plant require maintenance every (certain number of) year(s) + # Plant requires maintenance every (certain number of) year(s) @constraint( EP, sum(vMSHUT[t] for t in maintenance_begin_hours) >= ecap[y] / cap / maint_freq_years From 6776b14617a9c08fcfe471b9c690f83b3660088d Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Wed, 4 Oct 2023 14:56:33 -0400 Subject: [PATCH 30/32] Better explanation --- docs/src/maintenance.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/src/maintenance.md b/docs/src/maintenance.md index 22d6f1d5b0..658d5f1550 100644 --- a/docs/src/maintenance.md +++ b/docs/src/maintenance.md @@ -58,9 +58,16 @@ The maintenance module has these restrictions: It would not make sense to model a *month*-long maintenance period when the year is modeled as a series of representative *weeks*, for example. ### Interaction with integer unit commitment -If integer unit commitment is on (`UCommit=1`), this module may not produce sensible results. -This module works on the level of individual resources (i.e. a specific type of plant in a specific zone.). -If there is only 1 unit of a given resource built in a zone, then it will undergo maintenance every year regardless of its `Maintenance_Frequency_Years`. +If integer unit commitment is on (`UCommit=1`) this module may not produce correct results; there may be more maintenance than the user wants. +This is because the formulation specifies that the number of plants that go down for maintenance in the simulated year must be at least (the number of plants in the zone)/(the maintenance cycle length in years). +As a reminder, the number of plants is `eTotalCap / Cap_Size`. + +If there were three 500 MW plants (total 1500 MW) in a zone, and they require maintenance every three years (`Maintenance_Frequency_Years=3`), +the formulation will work properly: one of the three plants will go under maintenance. + +But if there was only one 500 MW plant, and it requires maintenance every 3 years, the constraint will still make it do maintenance **every year**, because `ceil(1/3)` is `1`. The whole 500 MW plant will do maintenance. This is the unexpected behavior. + +However, if integer unit commitment was relaxed to "linearized" unit commitment (`UCommit=2`), the model will have only 500 MW / 3 = 166.6 MW worth of this plant do maintenance. ## Hint: pre-scheduling maintenance If you want to pre-schedule when maintenance occurs, you might not need this module. From b52fa3873b2242ea87451cdfcf50bd55574a316f Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Wed, 4 Oct 2023 15:08:25 -0400 Subject: [PATCH 31/32] Change maintenance column name Maintenance_Frequency_Years to Maintenance_Cycle_Length_Years Frequencies have units of inverse-time. This is quantity with units of time. I have corrected the name. --- docs/src/data_documentation.md | 2 +- docs/src/maintenance.md | 4 ++-- src/model/resources/thermal/thermal_commit.jl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/data_documentation.md b/docs/src/data_documentation.md index 5447b3b63e..a987a4b1dc 100644 --- a/docs/src/data_documentation.md +++ b/docs/src/data_documentation.md @@ -392,7 +392,7 @@ This file contains cost and performance parameters for various generators and ot |**Maintenance data**| |MAINT|[0,1], toggles scheduled maintenance formulation.| |Maintenance\_Duration| (Positive integer, less than total length of simulation.) Duration of the maintenance period, in number of timesteps. Only used if `MAINT=1`.| -|Maintenance\_Frequency\_Years| Length of scheduled maintenance cycle, in years. `1` is maintenance every year, `3` is every three years, etc. (Positive integer. Only used if `MAINT=1`.)| +|Maintenance\_Cycle\_Length\_Years| Length of scheduled maintenance cycle, in years. `1` is maintenance every year, `3` is every three years, etc. (Positive integer. Only used if `MAINT=1`.)| |Maintenance\_Begin\_Cadence| Cadence of timesteps in which scheduled maintenance can begin. `1` means that a maintenance period can start in any timestep, `24` means it can start only in timesteps 1, 25, 49, etc. A larger number can decrease the simulation computational cost as it limits the optimizer's choices. (Positive integer, less than total length of simulation. Only used if `MAINT=1`.)| |**Electrolyzer related parameters required if the set ELECTROLYZER is not empty**| |Hydrogen_MWh_Per_Tonne| Electrolyzer efficiency in megawatt-hours (MWh) of electricity per metric tonne of hydrogen produced (MWh/t)| diff --git a/docs/src/maintenance.md b/docs/src/maintenance.md index 658d5f1550..6e02c28bc5 100644 --- a/docs/src/maintenance.md +++ b/docs/src/maintenance.md @@ -39,7 +39,7 @@ There are four columns which need to be added to the plant data, i.e. in `Genera 1. `MAINT` should be `1` for plants that require maintenance and `0` otherwise. 2. `Maintenance_Duration` is the number of hours the maintenance period lasts. -3. `Maintenance_Frequency_Years`. If `1`, maintenance every year, if `3` maintenance every 3 years, etc. +3. `Maintenance_Cycle_Length_Years`. If `1`, maintenance every year, if `3` maintenance every 3 years, etc. 4. `Maintenance_Begin_Cadence`. Spacing between hours in which maintenance can start. The last three fields must be integers which are greater than 0. @@ -62,7 +62,7 @@ If integer unit commitment is on (`UCommit=1`) this module may not produce corre This is because the formulation specifies that the number of plants that go down for maintenance in the simulated year must be at least (the number of plants in the zone)/(the maintenance cycle length in years). As a reminder, the number of plants is `eTotalCap / Cap_Size`. -If there were three 500 MW plants (total 1500 MW) in a zone, and they require maintenance every three years (`Maintenance_Frequency_Years=3`), +If there were three 500 MW plants (total 1500 MW) in a zone, and they require maintenance every three years (`Maintenance_Cycle_Length_Years=3`), the formulation will work properly: one of the three plants will go under maintenance. But if there was only one 500 MW plant, and it requires maintenance every 3 years, the constraint will still make it do maintenance **every year**, because `ceil(1/3)` is `1`. The whole 500 MW plant will do maintenance. This is the unexpected behavior. diff --git a/src/model/resources/thermal/thermal_commit.jl b/src/model/resources/thermal/thermal_commit.jl index df0b405559..dce4c29aa2 100644 --- a/src/model/resources/thermal/thermal_commit.jl +++ b/src/model/resources/thermal/thermal_commit.jl @@ -355,7 +355,7 @@ function maintenance_formulation_thermal_commit!(EP::Model, inputs::Dict, setup: resource_component(y) = by_rid(y, :Resource) 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_freq(y) = Int(floor(by_rid(y, :Maintenance_Cycle_Length_Years))) maint_begin_cadence(y) = Int(floor(by_rid(y, :Maintenance_Begin_Cadence))) integer_operational_unit_committment = setup["UCommit"] == 1 From af5c32e43f3f050c67281fc1f9eae76ee4ceb83d Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Wed, 4 Oct 2023 15:10:33 -0400 Subject: [PATCH 32/32] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4159ae7dfb..76509c86c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add output for dual of capacity constraint (#473) - Add PR template (#516) - Validation ensures that resource flags (THERM, HYDRO, LDS etc) are self-consistent (#513). +- Maintenance formulation for thermal-commit plants (#556). ### Fixed - Set MUST_RUN=1 for RealSystemExample/small_hydro plants (#517).