-
Notifications
You must be signed in to change notification settings - Fork 7
Derate thermal capacity reserve margin contribution with FOR #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ba7df78
4156c65
15736fe
3c7eb06
92aa3a9
af170bf
8d65647
64ae31d
a8a5192
4888795
35554e5
15994c7
c355a9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2460,11 +2460,12 @@ Adapted from {cite}`maiIncorporatingStressfulGrid2024`. | |||||
|
|
||||||
| The calculation of capacity credit for VRE is described in the [VRE capacity credit](#vre-capacity-credit) section; | ||||||
| the method for storage is described in the [storage capacity credit](#storage-capacity-credit) section. | ||||||
| Thermal generators are given a capacity credit of 100% in ReEDS; | ||||||
| technology-specific [outage rates](#outage-rates) for thermal generators are not considered in this method | ||||||
| and are instead assumed to be factored into the [planning reserve margin](#planning-reserve-margins). | ||||||
|
|
||||||
|
|
||||||
| #### Thermal generation capacity credit | ||||||
|
|
||||||
| For thermal generators (i.e. combined cycle, combustion turbine, nuclear (conventional and SMR), and steam (coal)), ReEDS estimates a seasonal capacity credit for each region/technology combination based on temperature-dependent forced outage rates (described in detail in [outage rates](#outage-rates)). To calculate each technology's contribution to the seasonal reserve margin, its nameplate capacity for each technology is multiplied by $(1 - \bar{FOR})$, where $\bar{FOR}$ is the mean forced outage rate during the top 20 net load hours cross all modeled [weather years](#weather-years) for each season. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
|
|
||||||
| #### VRE capacity credit | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -152,13 +152,35 @@ def reeds_cc(t, tnext, casedir): | |||||
| ### Get the non-duplicated profiles | ||||||
| resource_profiles = resources.drop_duplicates('resource') | ||||||
|
|
||||||
| ### Get forced outage profiles, flatten, and melt | ||||||
| forced_outage_rate = reeds.io.get_outage_hourly(inputs_case,'forced') | ||||||
|
|
||||||
| # drop techs that won't use this capacity credit before stacking | ||||||
| techs_to_drop = ['battery_li','can-imports','distpv', 'electrolyzer', | ||||||
| 'hydd', 'hyded', 'hydend', 'hydnd', 'hydnpd', 'hydnpnd', | ||||||
| 'hydro', 'hydsd', 'hydsnd', 'hydud', 'hydund', 'pumped-hydro'] | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whenever possible, we'd like to avoid hard-coding technology names within Python/GAMS scripts, as it increases the number of places that need to be modified when technologies are added/changed. (I know we do it now but we'd at least like to avoid adding it in new places.) Is there a tech group (or groups) in |
||||||
|
|
||||||
| forced_outage_rate = forced_outage_rate.drop(columns=techs_to_drop, errors='ignore') | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| forced_outage_rate = (forced_outage_rate | ||||||
| .stack(level=1) | ||||||
| .reset_index() | ||||||
| .rename(columns={"level_0": "timestamp"}) | ||||||
| ) | ||||||
|
|
||||||
| forced_outage_rate = pd.melt( | ||||||
| forced_outage_rate, | ||||||
| id_vars=['timestamp','r'], | ||||||
| ) | ||||||
|
|
||||||
|
Comment on lines
+165
to
+175
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general it's best to avoid reshaping hourly dataframes with thousands of columns and multiple weather years, as it can be super slow/memory-intensive. It looks like you could instead:
hours = top_net_load_hours.groupby(['ccseason', 'r'])['*timestamp'].agg(list)
mean_forced_outage_rate = pd.concat({
(ccseason, r): forced_outage_rate.loc[h].xs(r, 1, 'r').mean()
for (ccseason, r), h in hours.items()
}, names=('ccseason', 'r'))There might need to be a few other adjustments but the general idea is to keep the forced outage rates in the original hours x (techs, regions) format since it's much faster to query the data you need than to reshape the whole thing. Could you give that a shot and see if it speeds up |
||||||
| # Remove the "8760" safety valve bin | ||||||
| safety_bin = max(sdb['bin'].values) | ||||||
| sdb = sdb[sdb['bin'] != max(sdb['bin'])] | ||||||
| sdb = [int(x) for x in sdb['bin']] | ||||||
|
|
||||||
| # Temporal definitions | ||||||
| h_dt_szn = pd.read_csv(os.path.join('inputs_case', 'rep', 'h_dt_szn.csv')) | ||||||
| hmap_allyrs = pd.read_csv(os.path.join('inputs_case','rep','hmap_allyrs.csv')) | ||||||
|
|
||||||
| ccseasons = [] | ||||||
| if sw['cc_calc_annual']: | ||||||
|
|
@@ -464,6 +486,39 @@ def pivot_melt_data(df): | |||||
| ### Reorder to match ReEDS convention | ||||||
| net_load_2012 = net_load_2012.reindex(['ccreg','ccseason','year','h','hour','t','value'], axis=1) | ||||||
|
|
||||||
| # export mean forced outage rate for top load hours in each region and ccseason | ||||||
| top_net_load_hours = net_load.groupby(['ccreg','ccseason']).head(int(sw['GSw_PRM_CapCreditHours'])) | ||||||
| # map back to regions from ccreg for ReEDS input | ||||||
| top_net_load_hours = top_net_load_hours.merge( | ||||||
| hierarchy[['r','ccreg']], on='ccreg', how='left' | ||||||
| ).drop(['ccreg','year','h','t','value'], axis=1) | ||||||
|
|
||||||
| # map hour to timestamp | ||||||
| top_net_load_hours = top_net_load_hours.merge( | ||||||
| hmap_allyrs[['hour','*timestamp','actual_h']], on='hour', how='left' | ||||||
| ).drop('hour', axis=1) | ||||||
| top_net_load_hours['*timestamp'] = pd.to_datetime(top_net_load_hours['*timestamp']) | ||||||
| top_net_load_hours['h'] = top_net_load_hours['actual_h'] | ||||||
|
|
||||||
| # join top hours to outage rates based on timestamp and region | ||||||
| forced_outage_rate_top_hours = top_net_load_hours.merge( | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you explain the reasoning for using net load instead of load (since net load is a VRE concept but here we're looking at non-VRE)? For context, the RHS of |
||||||
| forced_outage_rate, | ||||||
| left_on=['*timestamp','r'], | ||||||
| right_on=['timestamp','r'], | ||||||
| how='left' | ||||||
| ).drop('*timestamp', axis=1) | ||||||
|
|
||||||
| # Add t index | ||||||
| forced_outage_rate_top_hours['t'] = str(tnext) | ||||||
|
|
||||||
| # Find mean forced outage rate across top hours for each region and ccseason | ||||||
| mean_forced_outage_rate = ( | ||||||
| forced_outage_rate_top_hours | ||||||
| .groupby(['i','r','ccseason','t'], as_index=False).mean('value') | ||||||
| .sort_values(['i','r','ccseason','t']) | ||||||
| .reset_index(drop=True) | ||||||
| ) | ||||||
|
|
||||||
| if int(sw['GSw_EVMC']): | ||||||
| cc_evmc = ( | ||||||
| pd.concat(dict_cc_evmc, axis=0) | ||||||
|
|
@@ -484,6 +539,7 @@ def pivot_melt_data(df): | |||||
| 'sdbin_size': sdbin_size, | ||||||
| 'net_load': net_load, | ||||||
| 'net_load_2012': net_load_2012, | ||||||
| 'mean_forced_outage_rate' : mean_forced_outage_rate, | ||||||
| } | ||||||
|
|
||||||
| return cc_results | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -138,6 +138,7 @@ def main(t, tnext, casedir, iteration=0): | |||||||||||||||||||||||||||
| 'cc_old': pd.DataFrame(columns=['i','r','ccreg','szn','t','Value']), | ||||||||||||||||||||||||||||
| 'cc_evmc': pd.DataFrame(columns=['i','r','szn','t','Value']), | ||||||||||||||||||||||||||||
| 'sdbin_size': pd.DataFrame(columns=['ccreg','szn','bin','t','Value']), | ||||||||||||||||||||||||||||
| 'mean_forced_outage_rate': pd.DataFrame(columns=['i','r','szn','t','Value']), | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| reeds.log.toc(tic=tic, year=t, process='ra/capacity_credit.py') | ||||||||||||||||||||||||||||
|
|
@@ -165,8 +166,18 @@ def main(t, tnext, casedir, iteration=0): | |||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| #%% Identify stress periods | ||||||||||||||||||||||||||||
| print('identifying new stress periods...') | ||||||||||||||||||||||||||||
| tic = datetime.datetime.now() | ||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||
| int(sw['GSw_PRM_CapCredit']) == 0 | ||||||||||||||||||||||||||||
| and ('user' not in sw['GSw_PRM_StressModel'].lower()) | ||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||
| print('identifying new stress periods...') | ||||||||||||||||||||||||||||
| tic = datetime.datetime.now() | ||||||||||||||||||||||||||||
| elif ( | ||||||||||||||||||||||||||||
| int(sw['GSw_PRM_CapCredit']) and (int(sw['GSw_PRM_StressIterateMax']) > 0) | ||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||
| print('updating PRM values...') | ||||||||||||||||||||||||||||
| tic = datetime.datetime.now() | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
Comment on lines
+169
to
+180
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kind of a lot of logic just for a status message, and
Suggested change
|
||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||
| ('user' not in sw['GSw_PRM_StressModel'].lower()) | ||||||||||||||||||||||||||||
| or ((int(sw.GSw_PRM_StressIterateMax)) and int(sw['GSw_PRM_CapCredit'])) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -386,6 +386,16 @@ def check_compatibility(sw): | |||||||||||||||||||||||||
| except ValueError: | ||||||||||||||||||||||||||
| raise ValueError(err) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if int(sw['GSw_PRM_UpdateMethod']) == 0 and int(sw['GSw_PRM_CapCredit']) == 1 and int(sw['GSw_PRM_StressIterateMax']) > 0: | ||||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||||
| "The combination of GSw_PRM_UpdateMethod=0, GSw_PRM_CapCredit=1, " | ||||||||||||||||||||||||||
| "and GSw_PRM_StressIterateMax>0 is not supported." | ||||||||||||||||||||||||||
| "To iteratively update the PRM, set GSw_PRM_UpdateMethod to one of: " | ||||||||||||||||||||||||||
| "(1) static update set by GSw_PRM_UpdateFraction; " | ||||||||||||||||||||||||||
| "(2) dynamic update informed by PRAS; " | ||||||||||||||||||||||||||
| "(3) dynamic update but only after all new stress periods have been added" | ||||||||||||||||||||||||||
|
Comment on lines
+391
to
+396
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| for bir in sw['GSw_PVB_BIR'].split('_'): | ||||||||||||||||||||||||||
| if not (float(bir) >= 0): | ||||||||||||||||||||||||||
| raise ValueError("Fix GSw_PVB_BIR") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is for Pacific_CC, right? It will slow down the test but I agree it's good to have to demonstrate best practice with capacity credit