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..d9314fb8 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 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) - - **Units:** rate (unitless) + - **Units:** year --- 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..f523ee72 100644 --- a/reeds/financials.py +++ b/reeds/financials.py @@ -788,3 +788,74 @@ 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 for years after the forced retirement year + - 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, + 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'})) + + 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 f2611a52..01e81333 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 ac9f66ea..c72251fa 100644 --- a/reeds/input_processing/runfiles.csv +++ b/reeds/input_processing/runfiles.csv @@ -150,7 +150,7 @@ natgas_price_cendiv.csv,inputs/fuelprices/ng_{ngscen}.csv,1,ignore,ignore,wide_c national_rps_frac_allScen.csv,inputs/national_generation/national_rps_frac_allScen.csv,int(sw.GSw_StateRPS) != 0,ignore,ignore,,,,1,0,,,,,, net_gen_existing_hydro.csv,inputs/hydro/net_gen_existing_hydro.csv,1,ignore,ignore,,"t,month",,0,0,,,,,, peak_net_imports.csv,inputs/reserves/peak_net_imports.csv,1,ignore,ignore,nercr,t,,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,,,set,noretire,technologies that will never be retired,