From cf0be07b6a2aee94b112812b0d4650991d2cbd55 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Tue, 14 Nov 2023 20:49:49 -0500 Subject: [PATCH 1/3] Separate reserves into creation and constraints Divide the core Reserves functionality into 1. variable and expression creation (space for potential modification) 2. constraint application This makes the module more like the CapacityReserveMargin or MinCapReq policies, allowing the expressions to be modified before constraints are applied. --- src/model/core/reserves.jl | 36 ++++++++++++++++++++++++------------ src/model/generate_model.jl | 10 +++++++--- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/model/core/reserves.jl b/src/model/core/reserves.jl index bf503acf5e..eed4e3b106 100644 --- a/src/model/core/reserves.jl +++ b/src/model/core/reserves.jl @@ -259,17 +259,29 @@ function reserves_core!(EP::Model, inputs::Dict, setup::Dict) sum(dfGen[y,:Reg_Cost]*vRSV[y,t] for y in RSV, t=1:T) + sum(dfGen[y,:Rsv_Cost]*vREG[y,t] for y in REG, t=1:T) ) add_to_expression!(EP[:eObj], eTotalCRsvPen) +end - ### Constraints ### - - ## Total system reserve constraints - # Regulation requirements as a percentage of demand and scheduled variable renewable energy production in each hour - # Note: frequencty regulation up and down requirements are symmetric and all resources contributing to regulation are assumed to contribute equal capacity to both up and down directions - if !isempty(REG) - @constraint(EP, cReg[t=1:T], sum(vREG[y,t] for y in REG) >= EP[:eRegReq][t]) - end - if !isempty(RSV) - @constraint(EP, cRsvReq[t=1:T], sum(vRSV[y,t] for y in RSV) + vUNMET_RSV[t] >= EP[:eRsvReq][t]) - end - +function reserves_constraints!(EP, inputs) + T = inputs["T"] # Number of time steps (hours) + + REG = inputs["REG"] + RSV = inputs["RSV"] + vREG = EP[:vREG] + vRSV = EP[:vRSV] + vUNMET_RSV = EP[:vUNMET_RSV] + eRegulationRequirement = EP[:eRegReq] + eReserveRequirement = EP[:eRsvReq] + + ## Total system reserve constraints + # Regulation requirements as a percentage of demand and scheduled + # variable renewable energy production in each hour. + # Note: frequency regulation up and down requirements are symmetric and all resources + # contributing to regulation are assumed to contribute equal capacity to both up + # and down directions + if !isempty(REG) + @constraint(EP, cReg[t=1:T], sum(vREG[y,t] for y in REG) >= eRegulationRequirement[t]) + end + if !isempty(RSV) + @constraint(EP, cRsvReq[t=1:T], sum(vRSV[y,t] for y in RSV) + vUNMET_RSV[t] >= eReserveRequirement[t]) + end end diff --git a/src/model/generate_model.jl b/src/model/generate_model.jl index 45c964c776..07fbc40a6f 100644 --- a/src/model/generate_model.jl +++ b/src/model/generate_model.jl @@ -94,10 +94,10 @@ function generate_model(setup::Dict,inputs::Dict,OPTIMIZER::MOI.OptimizerWithAtt EP[:eObj] = AffExpr(0.0) create_empty_expression!(EP, :eGenerationByZone, (Z, T)) - + # Energy losses related to technologies create_empty_expression!(EP, :eELOSSByZone, Z) - + # Initialize Capacity Reserve Margin Expression if setup["CapacityReserveMargin"] > 0 create_empty_expression!(EP, :eCapResMarBalance, (inputs["NCapacityReserveMargin"], T)) @@ -191,6 +191,10 @@ function generate_model(setup::Dict,inputs::Dict,OPTIMIZER::MOI.OptimizerWithAtt # Policies + if setup["Reserves"] > 0 + reserves_constraints!(EP, inputs) + end + # CO2 emissions limits if setup["CO2Cap"] > 0 co2_cap!(EP, inputs, setup) @@ -236,4 +240,4 @@ function generate_model(setup::Dict,inputs::Dict,OPTIMIZER::MOI.OptimizerWithAtt end return EP -end \ No newline at end of file +end From 543002507bc668ce60955c509d5c7f327c074d56 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Wed, 15 Nov 2023 15:25:16 -0500 Subject: [PATCH 2/3] Refactor for cleanliness --- src/model/core/reserves.jl | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/model/core/reserves.jl b/src/model/core/reserves.jl index eed4e3b106..023c3c78ca 100644 --- a/src/model/core/reserves.jl +++ b/src/model/core/reserves.jl @@ -217,6 +217,13 @@ function reserves_core!(EP::Model, inputs::Dict, setup::Dict) REG = inputs["REG"] RSV = inputs["RSV"] + STOR_ALL = inputs["STOR_ALL"] + + pDemand = inputs["pD"] + pP_Max(y, t) = inputs["pP_Max"][y, t] + + systemwide_hourly_demand = sum(pDemand, dims=2) + must_run_vre_generation(t) = sum(pP_Max(y, t) * EP[:eTotalCap][y] for y in intersect(inputs["VRE"], inputs["MUST_RUN"])) ### Variables ### @@ -228,10 +235,10 @@ function reserves_core!(EP::Model, inputs::Dict, setup::Dict) # Storage techs have two pairs of auxilary variables to reflect contributions to regulation and reserves # when charging and discharging (primary variable becomes equal to sum of these auxilary variables) - @variable(EP, vREG_discharge[y in intersect(inputs["STOR_ALL"], REG), t=1:T] >= 0) # Contribution to regulation (primary reserves) (mirrored variable used for storage devices) - @variable(EP, vRSV_discharge[y in intersect(inputs["STOR_ALL"], RSV), t=1:T] >= 0) # Contribution to operating reserves (secondary reserves) (mirrored variable used for storage devices) - @variable(EP, vREG_charge[y in intersect(inputs["STOR_ALL"], REG), t=1:T] >= 0) # Contribution to regulation (primary reserves) (mirrored variable used for storage devices) - @variable(EP, vRSV_charge[y in intersect(inputs["STOR_ALL"], RSV), t=1:T] >= 0) # Contribution to operating reserves (secondary reserves) (mirrored variable used for storage devices) + @variable(EP, vREG_discharge[y in intersect(STOR_ALL, REG), t=1:T] >= 0) # Contribution to regulation (primary reserves) (mirrored variable used for storage devices) + @variable(EP, vRSV_discharge[y in intersect(STOR_ALL, RSV), t=1:T] >= 0) # Contribution to operating reserves (secondary reserves) (mirrored variable used for storage devices) + @variable(EP, vREG_charge[y in intersect(STOR_ALL, REG), t=1:T] >= 0) # Contribution to regulation (primary reserves) (mirrored variable used for storage devices) + @variable(EP, vRSV_charge[y in intersect(STOR_ALL, RSV), t=1:T] >= 0) # Contribution to operating reserves (secondary reserves) (mirrored variable used for storage devices) @variable(EP, vUNMET_RSV[t=1:T] >= 0) # Unmet operating reserves penalty/cost @@ -239,16 +246,16 @@ function reserves_core!(EP::Model, inputs::Dict, setup::Dict) ## Total system reserve expressions # Regulation requirements as a percentage of demand and scheduled variable renewable energy production in each hour # Reg up and down requirements are symmetric - @expression(EP, eRegReq[t=1:T], inputs["pReg_Req_Demand"]*sum(inputs["pD"][t,z] for z=1:Z) + - inputs["pReg_Req_VRE"]*sum(inputs["pP_Max"][y,t]*EP[:eTotalCap][y] for y in intersect(inputs["VRE"], inputs["MUST_RUN"]))) + @expression(EP, eRegReq[t=1:T], inputs["pReg_Req_Demand"] * systemwide_hourly_demand[t] + + inputs["pReg_Req_VRE"] * must_run_vre_generation(t)) # Operating reserve up / contingency reserve requirements as ˚a percentage of demand and scheduled variable renewable energy production in each hour # and the largest single contingency (generator or transmission line outage) - @expression(EP, eRsvReq[t=1:T], inputs["pRsv_Req_Demand"]*sum(inputs["pD"][t,z] for z=1:Z) + - inputs["pRsv_Req_VRE"]*sum(inputs["pP_Max"][y,t]*EP[:eTotalCap][y] for y in intersect(inputs["VRE"], inputs["MUST_RUN"]))) + @expression(EP, eRsvReq[t=1:T], inputs["pRsv_Req_Demand"] * systemwide_hourly_demand[t] + + inputs["pRsv_Req_VRE"] * must_run_vre_generation(t)) # N-1 contingency requirement is considered only if Unit Commitment is being modeled if UCommit >= 1 && (inputs["pDynamic_Contingency"] >= 1 || inputs["pStatic_Contingency"] > 0) - EP[:eRsvReq] = EP[:eRsvReq] + EP[:eContingencyReq] + add_to_expression!(EP[:eRsvReq], EP[:eContingencyReq]) end ## Objective Function Expressions ## From 08d4258038d65a266d14d81e294ba2d225f85cc1 Mon Sep 17 00:00:00 2001 From: Jacob Schwartz Date: Wed, 15 Nov 2023 15:35:46 -0500 Subject: [PATCH 3/3] Fix sum over empty expression --- src/model/core/reserves.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/core/reserves.jl b/src/model/core/reserves.jl index 023c3c78ca..a850ff05e5 100644 --- a/src/model/core/reserves.jl +++ b/src/model/core/reserves.jl @@ -223,7 +223,7 @@ function reserves_core!(EP::Model, inputs::Dict, setup::Dict) pP_Max(y, t) = inputs["pP_Max"][y, t] systemwide_hourly_demand = sum(pDemand, dims=2) - must_run_vre_generation(t) = sum(pP_Max(y, t) * EP[:eTotalCap][y] for y in intersect(inputs["VRE"], inputs["MUST_RUN"])) + must_run_vre_generation(t) = sum(pP_Max(y, t) * EP[:eTotalCap][y] for y in intersect(inputs["VRE"], inputs["MUST_RUN"]); init=0) ### Variables ###