From 425e30866f7b31ff6e67c64088737da82b59e9ec Mon Sep 17 00:00:00 2001 From: "Olmez Turan, Merve" Date: Tue, 12 May 2026 18:36:15 -0600 Subject: [PATCH 1/6] Auto generation of ng_crf_penalty_st.csv --- inputs/state_policies/ng_st_forced.csv | 5 ++ reeds/financials.py | 77 +++++++++++++++++++ .../input_processing/calc_financial_inputs.py | 4 + reeds/input_processing/runfiles.csv | 2 +- 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 inputs/state_policies/ng_st_forced.csv diff --git a/inputs/state_policies/ng_st_forced.csv b/inputs/state_policies/ng_st_forced.csv new file mode 100644 index 00000000..dd98238e --- /dev/null +++ b/inputs/state_policies/ng_st_forced.csv @@ -0,0 +1,5 @@ +*t,st,tech +2045,DE,ng +2045,IL,ng +2045,VA,ng +2040,NY,ng diff --git a/reeds/financials.py b/reeds/financials.py index 64bd6017..3b41aaf6 100644 --- a/reeds/financials.py +++ b/reeds/financials.py @@ -788,3 +788,80 @@ def param_exporter(df, parameter, file_name, output_dir): ### Add '*' to first column name so GAMS reads it as a comment df = df.rename(columns={c: (f'*{c}' if not i else c) for i, c in enumerate(df.columns)}) df.round(5).to_csv(os.path.join(output_dir, f'{file_name}.csv'), index=False, header=True) + + +def calc_ng_crf_penalty_st(inputs_case, years, financials_sys, sw): + """ + Compute the CRF-based cost penalty for natural gas plants in states with + forced retirement policies. + + For each (state, forced-retirement year) pair in ng_st_forced.csv, this + function calculates how much of a plant's remaining economic life is cut + short by the policy. The penalty is the ratio of the CRF over the + truncated remaining life to the CRF over the full evaluation period: + - value = 100 if the plant comes online after the forced year (no penalty) + - value > 1 if the plant is forced to retire before its full life + + States not present in hierarchy.csv (i.e. outside the modeled region) are + dropped before any computation. If no forced-retirement states remain, + returns an empty DataFrame with columns ['t', 'st', 'value']. + """ + # Load state-level forced retirement years for NG plants + ng_st_forced = pd.read_csv(os.path.join(inputs_case, 'ng_st_forced.csv')) + + # Keep only states that exist in this run's modeled region + hierarchy = pd.read_csv(os.path.join(inputs_case, 'hierarchy.csv')) + valid_states = hierarchy['st'].unique() + ng_st_forced = ng_st_forced[ng_st_forced['st'].isin(valid_states)] + + # No forced retirements in this region — return empty result + if ng_st_forced.empty: + ng_crf_penalty_st=pd.DataFrame(columns=['*t', 'st', 'value']) + ng_crf_penalty_st.to_csv(os.path.join(inputs_case, 'ng_crf_penalty_st.csv'), + index=False) + return ng_crf_penalty_st + + # Years when retirements are forced, and the model's active year range + forced_years = ng_st_forced['*t'].astype(int).unique() + years_arr = np.asarray([y for y in years if int(sw['startyear']) <= y <= int(sw['endyear'])]) + + # Real discount rate and full-life CRF for each year (the baseline) + d_real_arr = financials_sys.set_index('t')['d_real'].reindex(years_arr).values + base_crf_arr = np.array([calc_crf(d, sw['sys_eval_years']) for d in d_real_arr]) + + # How many years remain between each build year t and each forced retirement year + # shape: (len(years_arr), len(forced_years)) + diff = forced_years[None, :] - years_arr[:, None] + # Effective remaining life: at least 1 year, at most the full evaluation period + n_eff = np.clip(diff, 1, sw['sys_eval_years']) + + # CRF over the truncated life, then normalize by the full-life baseline + crf_vals = np.vectorize(calc_crf)(d_real_arr[:, None], n_eff) + value_matrix = np.where(diff <= 0, 100.0, crf_vals / base_crf_arr[:, None]) + + # Wrap results in a DataFrame indexed by build year, columns = forced years + crf_table = pd.DataFrame(value_matrix, index=years_arr, + columns=forced_years, dtype=float) + crf_table.index.name = 't' + + # Reshape to long format: one row per (build year, forced year) + crf_long = ( + crf_table.stack().rename('value') + .rename_axis(['t', '*t']).reset_index() + ) + + # Attach state labels by merging on the forced retirement year + ng_crf_penalty_st = ( + ng_st_forced[['*t', 'st']].merge(crf_long, on='*t', how='left') + [['t', 'st', 'value']] + .rename(columns={'t': '*t'})) + + # Remove penalty when standalone DAC or BECCS provide general carbon removal. + # GSw_CCSFLEX_DAC enables DAC only as flexibility for fossil CCS plants (not + # atmospheric removal), so that case alone does not lift the NG penalty. + if int(sw['GSw_DAC']) or int(sw['GSw_BECCS']): + ng_crf_penalty_st['value'] = 1.0 + + ng_crf_penalty_st.to_csv(os.path.join(inputs_case, 'ng_crf_penalty_st.csv'), + index=False) + return ng_crf_penalty_st \ No newline at end of file diff --git a/reeds/input_processing/calc_financial_inputs.py b/reeds/input_processing/calc_financial_inputs.py index 56e8aec8..13c0a907 100644 --- a/reeds/input_processing/calc_financial_inputs.py +++ b/reeds/input_processing/calc_financial_inputs.py @@ -96,6 +96,10 @@ def calc_financial_inputs(inputs_case): sw['financials_sys_suffix'], inflation_df, modeled_years, years, year_map, sw['sys_eval_years'], scen_settings, scalars['co2_capture_incentive_length'],scalars['h2_ptc_length']) financials_sys.to_csv(os.path.join(inputs_case,'financials_sys.csv'),index=False) + + # Calculate the natural gas CRF penalty for states + reeds.financials.calc_ng_crf_penalty_st(inputs_case, years, financials_sys, sw) + df_ivt = df_ivt.merge( financials_sys[['t', 'pvf_capital', 'crf', 'crf_co2_incentive','crf_h2_incentive','d_real', 'd_nom', 'interest_rate_nom', 'tax_rate', 'debt_fraction', 'rroe_nom']], diff --git a/reeds/input_processing/runfiles.csv b/reeds/input_processing/runfiles.csv index 366b1025..27424ae1 100644 --- a/reeds/input_processing/runfiles.csv +++ b/reeds/input_processing/runfiles.csv @@ -262,7 +262,7 @@ nexth.csv,,1,ignore,ignore,,,,,0,,,,,, nexth_actualszn.csv,,1,ignore,ignore,,,,,0,,,,,, nextpartition.csv,,1,ignore,ignore,,,,,0,,,,,, ng_crf_penalty.csv,,1,ignore,ignore,,,,0,0,,,,,, -ng_crf_penalty_st.csv,inputs/state_policies/ng_crf_penalty_st.csv,1,ignore,ignore,st,*t,,0,0,,,,,, +ng_st_forced.csv,inputs/state_policies/ng_st_forced.csv,1,ignore,ignore,st,*t,,0,0,,,,,, ng_demand_elec.csv,inputs/fuelprices/ng_demand_{ngscen}.csv,1,ignore,ignore,wide_cendiv,year,,1,0,,,,,, ng_demand_tot.csv,inputs/fuelprices/ng_tot_demand_{ngscen}.csv,1,ignore,ignore,wide_cendiv,year,,1,0,,,,,, noretire.csv,inputs/sets/noretire.csv,1,ignore,ignore,,,,,0,,,,noretire,, From 8c165b2dc29059e4c6da4fb2d4ab0ff8934846d5 Mon Sep 17 00:00:00 2001 From: "Olmez Turan, Merve" Date: Mon, 1 Jun 2026 22:55:19 -0600 Subject: [PATCH 2/6] Update financials.py --- reeds/financials.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/reeds/financials.py b/reeds/financials.py index 3b41aaf6..8ff27fcd 100644 --- a/reeds/financials.py +++ b/reeds/financials.py @@ -799,8 +799,8 @@ def calc_ng_crf_penalty_st(inputs_case, years, financials_sys, sw): function calculates how much of a plant's remaining economic life is cut short by the policy. The penalty is the ratio of the CRF over the truncated remaining life to the CRF over the full evaluation period: - - value = 100 if the plant comes online after the forced year (no penalty) - - value > 1 if the plant is forced to retire before its full life + - value = 100 if the plant comes online after the forced year + - value > 1 if the plant is forced to retire before its full life States not present in hierarchy.csv (i.e. outside the modeled region) are dropped before any computation. If no forced-retirement states remain, @@ -856,12 +856,6 @@ def calc_ng_crf_penalty_st(inputs_case, years, financials_sys, sw): [['t', 'st', 'value']] .rename(columns={'t': '*t'})) - # Remove penalty when standalone DAC or BECCS provide general carbon removal. - # GSw_CCSFLEX_DAC enables DAC only as flexibility for fossil CCS plants (not - # atmospheric removal), so that case alone does not lift the NG penalty. - if int(sw['GSw_DAC']) or int(sw['GSw_BECCS']): - ng_crf_penalty_st['value'] = 1.0 - ng_crf_penalty_st.to_csv(os.path.join(inputs_case, 'ng_crf_penalty_st.csv'), index=False) return ng_crf_penalty_st \ No newline at end of file From 23b0283500511d8a3cf175729603438c00e07cd4 Mon Sep 17 00:00:00 2001 From: "Olmez Turan, Merve" Date: Mon, 1 Jun 2026 23:21:04 -0600 Subject: [PATCH 3/6] source and documentation update --- docs/sources.csv | 2 +- docs/sources_documentation.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/sources.csv b/docs/sources.csv index 456c17fe..b02a1fcd 100644 --- a/docs/sources.csv +++ b/docs/sources.csv @@ -562,7 +562,7 @@ RelativeFilePath,RelativeFolderPath,FileName_new,FileExtension,Description_new,I /inputs/state_policies/ces_fraction.csv,/inputs/state_policies,ces_fraction,.csv,Annual compliance for states with a CES policy,,,,, /inputs/state_policies/forced_retirements.csv,/inputs/state_policies,forced_retirements,.csv,List of regions with mandatory retirement policies for certain technologies,,,,, /inputs/state_policies/hydrofrac_policy.csv,/inputs/state_policies,hydrofrac_policy,.csv,,,,,, -/inputs/state_policies/ng_crf_penalty_st.csv,/inputs/state_policies,ng_crf_penalty_st,.csv,Cost adjustment for NG techs in states where all NG techs must be retired by a certain year,"allt,st",N/A,https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220,Inputs,rate (unitless) +/inputs/state_policies/ng_st_forced.csv,/inputs/state_policies,ng_st_forced,.csv,List of states where natural gas plants must be retired by a certain year,"t,st",N/A,https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220,Inputs,year /inputs/state_policies/nuclear_subsidies.csv,/inputs/state_policies,nuclear_subsidies,.csv,,,,,, /inputs/state_policies/offshore_req_default.csv,/inputs/state_policies,offshore_req_default,.csv,"default state mandates of offshore wind capacity, updated in November 2025","st,allt",,,Inputs,MW /inputs/state_policies/oosfrac.csv,/inputs/state_policies,oosfrac,.csv,Defines the fraction of renewable and clean energy credits can be purchased from out of state (oos). Applied for RPS and CES,,,,, diff --git a/docs/sources_documentation.md b/docs/sources_documentation.md index 4af952c0..8b29011b 100644 --- a/docs/sources_documentation.md +++ b/docs/sources_documentation.md @@ -2849,13 +2849,13 @@ - [hydrofrac_policy.csv](/inputs/state_policies/hydrofrac_policy.csv) --- - - [ng_crf_penalty_st.csv](/inputs/state_policies/ng_crf_penalty_st.csv) + - [ng_st_forced.csv](/inputs/state_policies/ng_st_forced.csv) - **File Type:** Inputs - - **Description:** Cost adjustment for NG techs in states where all NG techs must be retired by a certain year - - **Indices:** allt,st + - **Description:** The certain year for specific states that NG techs must be retired by a certain year + - **Indices:** t,st - **Dollar year:** N/A - **Citation:** [https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220](https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220) - - **Units:** rate (unitless) + - **Units:** year --- From fcce191581b1ed1c11d816284376f459bccd0073 Mon Sep 17 00:00:00 2001 From: Merve Turan <107019199+merveturan@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:51:33 -0600 Subject: [PATCH 4/6] Update docs/sources_documentation.md Co-authored-by: Wesley Cole <49044852+wesleyjcole@users.noreply.github.com> --- docs/sources_documentation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources_documentation.md b/docs/sources_documentation.md index 8b29011b..d9314fb8 100644 --- a/docs/sources_documentation.md +++ b/docs/sources_documentation.md @@ -2851,7 +2851,7 @@ - [ng_st_forced.csv](/inputs/state_policies/ng_st_forced.csv) - **File Type:** Inputs - - **Description:** The certain year for specific states that NG techs must be retired by a certain year + - **Description:** The year for specific states in which natural gas techs must be retired - **Indices:** t,st - **Dollar year:** N/A - **Citation:** [https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220](https://github.nrel.gov/ReEDS/ReEDS-2.0/pull/1220) From c5ea78def1c0cd5dd36d8010eb613cdc8e4961b7 Mon Sep 17 00:00:00 2001 From: Merve Turan <107019199+merveturan@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:51:44 -0600 Subject: [PATCH 5/6] Update reeds/financials.py Co-authored-by: Wesley Cole <49044852+wesleyjcole@users.noreply.github.com> --- reeds/financials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reeds/financials.py b/reeds/financials.py index 8ff27fcd..c62a35b9 100644 --- a/reeds/financials.py +++ b/reeds/financials.py @@ -799,7 +799,7 @@ def calc_ng_crf_penalty_st(inputs_case, years, financials_sys, sw): function calculates how much of a plant's remaining economic life is cut short by the policy. The penalty is the ratio of the CRF over the truncated remaining life to the CRF over the full evaluation period: - - value = 100 if the plant comes online after the forced year + - value = 100 for years after the forced retirement year - value > 1 if the plant is forced to retire before its full life States not present in hierarchy.csv (i.e. outside the modeled region) are From 8f1cdd8661c7594229a2bea45b093745a3d3ec5c Mon Sep 17 00:00:00 2001 From: Merve Turan <107019199+merveturan@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:52:28 -0600 Subject: [PATCH 6/6] Update reeds/financials.py Co-authored-by: Wesley Cole <49044852+wesleyjcole@users.noreply.github.com> --- reeds/financials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reeds/financials.py b/reeds/financials.py index c62a35b9..f523ee72 100644 --- a/reeds/financials.py +++ b/reeds/financials.py @@ -800,7 +800,7 @@ def calc_ng_crf_penalty_st(inputs_case, years, financials_sys, sw): short by the policy. The penalty is the ratio of the CRF over the truncated remaining life to the CRF over the full evaluation period: - value = 100 for years after the forced retirement year - - value > 1 if the plant is forced to retire before its full life + - value > 1 if the plant would be forced to retire before its full economic life States not present in hierarchy.csv (i.e. outside the modeled region) are dropped before any computation. If no forced-retirement states remain,