diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cc4dbe548..37fc5999ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix factor of 0.5 when writing out transmission losses. (#480) - Fix summation error when a set of hours is empty (in thermal_commit.jl). - Fix access to eELOSSByZone expr before initialization (#541) +- Fix modeling of hydro reservoir with long duration storage (#572). ### Changed diff --git a/src/load_inputs/load_generators_data.jl b/src/load_inputs/load_generators_data.jl index 385b911b07..7ef7d091db 100644 --- a/src/load_inputs/load_generators_data.jl +++ b/src/load_inputs/load_generators_data.jl @@ -30,6 +30,7 @@ function load_generators_data!(setup::Dict, path::AbstractString, inputs_gen::Di # Set of storage resources with long duration storage capabilitites inputs_gen["STOR_HYDRO_LONG_DURATION"] = gen_in[(gen_in.LDS.==1) .& (gen_in.HYDRO.==1),:R_ID] + inputs_gen["STOR_HYDRO_SHORT_DURATION"] = gen_in[(gen_in.LDS.==0) .& (gen_in.HYDRO.==1),:R_ID] inputs_gen["STOR_LONG_DURATION"] = gen_in[(gen_in.LDS.==1) .& (gen_in.STOR.>=1),:R_ID] inputs_gen["STOR_SHORT_DURATION"] = gen_in[(gen_in.LDS.==0) .& (gen_in.STOR.>=1),:R_ID] diff --git a/src/model/resources/hydro/hydro_inter_period_linkage.jl b/src/model/resources/hydro/hydro_inter_period_linkage.jl index 8e98eb9538..1963b3f1be 100644 --- a/src/model/resources/hydro/hydro_inter_period_linkage.jl +++ b/src/model/resources/hydro/hydro_inter_period_linkage.jl @@ -54,8 +54,6 @@ function hydro_inter_period_linkage!(EP::Model, inputs::Dict) STOR_HYDRO_LONG_DURATION = inputs["STOR_HYDRO_LONG_DURATION"] - START_SUBPERIODS = inputs["START_SUBPERIODS"] - hours_per_subperiod = inputs["hours_per_subperiod"] #total number of hours per subperiod dfPeriodMap = inputs["Period_Map"] # Dataframe that maps modeled periods to representative periods @@ -81,21 +79,20 @@ function hydro_inter_period_linkage!(EP::Model, inputs::Dict) # Modified initial state of storage for long-duration storage - initialize wth value carried over from last period # Alternative to cSoCBalStart constraint which is included when not modeling operations wrapping and long duration storage # Note: tw_min = hours_per_subperiod*(w-1)+1; tw_max = hours_per_subperiod*w - @constraint(EP, cSoCBalLongDurationStorageStart_H[w=1:REP_PERIOD, y in STOR_HYDRO_LONG_DURATION], + @constraint(EP, cHydroReservoirLongDurationStorageStart[w=1:REP_PERIOD, y in STOR_HYDRO_LONG_DURATION], EP[:vS_HYDRO][y,hours_per_subperiod*(w-1)+1] == (EP[:vS_HYDRO][y,hours_per_subperiod*w]-vdSOC_HYDRO[y,w])-(1/dfGen[y,:Eff_Down]*EP[:vP][y,hours_per_subperiod*(w-1)+1])-EP[:vSPILL][y,hours_per_subperiod*(w-1)+1]+inputs["pP_Max"][y,hours_per_subperiod*(w-1)+1]*EP[:eTotalCap][y]) - # Storage at beginning of period w = storage at beginning of period w-1 + storage built up in period w (after n representative periods) ## Multiply storage build up term from prior period with corresponding weight - @constraint(EP, cSoCBalLongDurationStorage_H[y in STOR_HYDRO_LONG_DURATION, r in MODELED_PERIODS_INDEX], + @constraint(EP, cHydroReservoirLongDurationStorage[y in STOR_HYDRO_LONG_DURATION, r in MODELED_PERIODS_INDEX], vSOC_HYDROw[y, mod1(r+1, NPeriods)] == vSOC_HYDROw[y,r] + vdSOC_HYDRO[y,dfPeriodMap[r,:Rep_Period_Index]]) # Storage at beginning of each modeled period cannot exceed installed energy capacity - @constraint(EP, cSoCBalLongDurationStorageUpper_H[y in STOR_HYDRO_LONG_DURATION, r in MODELED_PERIODS_INDEX], + @constraint(EP, cHydroReservoirLongDurationStorageUpper[y in STOR_HYDRO_LONG_DURATION, r in MODELED_PERIODS_INDEX], vSOC_HYDROw[y,r] <= dfGen[y,:Hydro_Energy_to_Power_Ratio]*EP[:eTotalCap][y]) # Initial storage level for representative periods must also adhere to sub-period storage inventory balance # Initial storage = Final storage - change in storage inventory across representative period - @constraint(EP, cSoCBalLongDurationStorageSub_H[y in STOR_HYDRO_LONG_DURATION, r in REP_PERIODS_INDEX], + @constraint(EP, cHydroReservoirLongDurationStorageSub[y in STOR_HYDRO_LONG_DURATION, r in REP_PERIODS_INDEX], vSOC_HYDROw[y,r] == EP[:vS_HYDRO][y,hours_per_subperiod*dfPeriodMap[r,:Rep_Period_Index]] - vdSOC_HYDRO[y,dfPeriodMap[r,:Rep_Period_Index]]) diff --git a/src/model/resources/hydro/hydro_res.jl b/src/model/resources/hydro/hydro_res.jl index 9dc20a0194..2c56c88b69 100644 --- a/src/model/resources/hydro/hydro_res.jl +++ b/src/model/resources/hydro/hydro_res.jl @@ -70,6 +70,12 @@ function hydro_res!(EP::Model, inputs::Dict, setup::Dict) HYDRO_RES = inputs["HYDRO_RES"] # Set of all reservoir hydro resources, used for common constraints HYDRO_RES_KNOWN_CAP = inputs["HYDRO_RES_KNOWN_CAP"] # Reservoir hydro resources modeled with unknown reservoir energy capacity + STOR_HYDRO_SHORT_DURATION = inputs["STOR_HYDRO_SHORT_DURATION"] + representative_periods = inputs["REP_PERIOD"] + + START_SUBPERIODS = inputs["START_SUBPERIODS"] + INTERIOR_SUBPERIODS = inputs["INTERIOR_SUBPERIODS"] + # These variables are used in the ramp-up and ramp-down expressions reserves_term = @expression(EP, [y in HYDRO_RES, t in 1:T], 0) regulation_term = @expression(EP, [y in HYDRO_RES, t in 1:T], 0) @@ -107,6 +113,14 @@ function hydro_res!(EP::Model, inputs::Dict, setup::Dict) ### Constratints ### + if representative_periods > 1 && !isempty(inputs["STOR_HYDRO_LONG_DURATION"]) + CONSTRAINTSET = STOR_HYDRO_SHORT_DURATION + else + CONSTRAINTSET = HYDRO_RES + end + + @constraint(EP, cHydroReservoirStart[y in CONSTRAINTSET,t in START_SUBPERIODS], EP[:vS_HYDRO][y,t] == EP[:vS_HYDRO][y, hoursbefore(p,t,1)]- (1/dfGen[y,:Eff_Down]*EP[:vP][y,t]) - vSPILL[y,t] + inputs["pP_Max"][y,t]*EP[:eTotalCap][y]) + ### Constraints commmon to all reservoir hydro (y in set HYDRO_RES) ### @constraints(EP, begin ### NOTE: time coupling constraints in this block do not apply to first hour in each sample period; @@ -115,8 +129,7 @@ function hydro_res!(EP::Model, inputs::Dict, setup::Dict) # DEV NOTE: Last inputs["pP_Max"][y,t] term above is inflows; currently part of capacity factors inputs in Generators_variability.csv but should be moved to its own Hydro_inflows.csv input in future. # Constraints for reservoir hydro - cHydroReservoir[y in HYDRO_RES, t in 1:T], EP[:vS_HYDRO][y,t] == (EP[:vS_HYDRO][y, hoursbefore(p,t,1)] - - (1/dfGen[y,:Eff_Down]*EP[:vP][y,t]) - vSPILL[y,t] + inputs["pP_Max"][y,t]*EP[:eTotalCap][y]) + cHydroReservoirInterior[y in HYDRO_RES, t in INTERIOR_SUBPERIODS], EP[:vS_HYDRO][y,t] == (EP[:vS_HYDRO][y, hoursbefore(p,t,1)]- (1/dfGen[y,:Eff_Down]*EP[:vP][y,t]) - vSPILL[y,t] + inputs["pP_Max"][y,t]*EP[:eTotalCap][y]) # Maximum ramp up and down cRampUp[y in HYDRO_RES, t in 1:T], EP[:vP][y,t] + regulation_term[y,t] + reserves_term[y,t] - EP[:vP][y, hoursbefore(p,t,1)] <= dfGen[y,:Ramp_Up_Percentage]*EP[:eTotalCap][y]