From 24bc66c018f566ec11c8035c34b4f5546a3e236e Mon Sep 17 00:00:00 2001 From: kodiobika Date: Fri, 24 Apr 2026 16:31:13 -0400 Subject: [PATCH 01/10] Migrate reVeal2ReEDS pipeline to hourlize --- hourlize/inputs/configs/config_base.json | 1 + hourlize/load.py | 24 +++ hourlize/reveal2reeds/config.json | 8 + hourlize/reveal2reeds/reveal2reeds.py | 239 +++++++++++++++++++++++ 4 files changed, 272 insertions(+) create mode 100644 hourlize/reveal2reeds/config.json create mode 100644 hourlize/reveal2reeds/reveal2reeds.py diff --git a/hourlize/inputs/configs/config_base.json b/hourlize/inputs/configs/config_base.json index 1409d39a..3f840d3f 100644 --- a/hourlize/inputs/configs/config_base.json +++ b/hourlize/inputs/configs/config_base.json @@ -51,6 +51,7 @@ "2045": 1, "2050": 1 }, + "custom_data_center_projection_years": [2025, 2030, 2035, 2040, 2045, 2050], "scenarios": ["IRA cons", "central", "baseline"], "sector_config_file": "{hourlize_path}/inputs/load/sector_config.json", "weather_years": [2007,2008,2009,2010,2011,2012,2013,2016,2017,2018,2019,2020,2021,2022,2023] diff --git a/hourlize/load.py b/hourlize/load.py index 0bc27802..97cbb4ed 100644 --- a/hourlize/load.py +++ b/hourlize/load.py @@ -9,6 +9,19 @@ import pandas as pd import site from types import SimpleNamespace +from reveal2reeds import reveal2reeds + +def get_reveal2reeds_config() -> dict: + configpath = "reveal2reeds/config.json" + with open(configpath, "r") as f: + config = json.load(f, object_pairs_hook=OrderedDict) + reveal2reeds_config = SimpleNamespace(**config) + reveal2reeds_config.cooling_proportions_source = ( + reveal2reeds_config.cooling_proportions_source + .format(scenario=reveal2reeds_config.scenario) + ) + + return reveal2reeds_config def get_state_name_code_map(reeds_path: str) -> dict: """ @@ -269,6 +282,17 @@ def create_hourly_state_load_for_model_year( compression='gzip', parse_dates=['weather_datetime'] ) + + # If applicable, replace data center cooling and IT projections with + # custom projections specified in reveal2reeds/config.json + if model_year in cf.custom_data_center_projection_years: + reveal2reeds_config = get_reveal2reeds_config() + df_load = reveal2reeds.apply_custom_data_center_demand_projections( + df_load, + model_year, + reveal2reeds_config + ) + # Downselect to specified weather years df_load = df_load.loc[df_load.weather_datetime.dt.year.isin(weather_years)] diff --git a/hourlize/reveal2reeds/config.json b/hourlize/reveal2reeds/config.json new file mode 100644 index 00000000..6eedc7c0 --- /dev/null +++ b/hourlize/reveal2reeds/config.json @@ -0,0 +1,8 @@ +{ + "national_demand_source": "/projects/largeload/geospatial/runs/random_forest_base_weights_01_09_2026/downscaling_2026-01-07_agg64/eer_national_central/eer_national_central_downscaled_projections.csv", + "cooling_proportions_source": "/projects/largeload/reVeal2ReEDS/files/{scenario}_dc_cooling_prop.csv", + "propagation_source": "/projects/largeload/reVeal2ReEDS/files/weather_year_propagation.csv", + "replace_existing_data_center_demand": true, + "scenario": "central", + "state_proportions_source": "/projects/eerload/source_eer_load_profiles/20250512_eer_download/shape_outputs_2025-05-12/annual_files/data center load allocation ADP 2024.xlsx" +} \ No newline at end of file diff --git a/hourlize/reveal2reeds/reveal2reeds.py b/hourlize/reveal2reeds/reveal2reeds.py new file mode 100644 index 00000000..03331fd6 --- /dev/null +++ b/hourlize/reveal2reeds/reveal2reeds.py @@ -0,0 +1,239 @@ +import numpy as np +import pandas as pd + +def get_national_model_year_data_center_demand( + national_demand_source_path: str, + model_year: int +) -> int: + data_center_demand = pd.read_csv(national_demand_source_path) + model_year_data_center_demand = ( + data_center_demand.loc[( + data_center_demand.year == model_year + )] + .copy() + ) + national_model_year_data_center_demand = ( + model_year_data_center_demand['total_data_center_mw'].sum() + ) + + return national_model_year_data_center_demand + +def get_propagation_by_weather_year( + propagation_source_path: str, + scenario: str +) -> pd.Series: + propagation_by_weather_year = pd.read_csv(propagation_source_path) + propagation_by_weather_year = ( + propagation_by_weather_year.loc[( + propagation_by_weather_year.scenario == scenario + )] + .set_index('year') + ['avg_prop'] + ) + + return propagation_by_weather_year + + +def calculate_national_data_center_demand_hourly( + df_load: pd.DataFrame, + model_year: int, + scenario: str, + national_demand_source_path: str, + propagation_source_path: str +): + # Calculate national projected data center demand for the model year + national_data_center_demand = get_national_model_year_data_center_demand( + national_demand_source_path, + model_year + ) + + # Get propagation factors by weather year for the given scenario. + # Propagation factors represent the percentage of projected national + # data center demand for the model year that is expected to be + # realized during each hour of each weather year. + propagation_by_weather_year = get_propagation_by_weather_year( + propagation_source_path, + scenario + ) + + # Estimate national hourly load values for each weather year + # by multiplying the propagation factors by national data + # center demand for the model year. + national_data_center_demand_hourly = pd.DataFrame( + index=df_load['weather_datetime'].drop_duplicates() + ) + national_data_center_demand_hourly['propagation_factor'] = ( + national_data_center_demand_hourly.index.year + .map(propagation_by_weather_year) + ) + national_data_center_demand_hourly['demand_MW'] = ( + national_data_center_demand_hourly['propagation_factor'] + * national_data_center_demand + ) + national_data_center_demand_hourly = ( + national_data_center_demand_hourly['demand_MW'] + ) + + return national_data_center_demand_hourly + +def get_data_center_cooling_weights( + cooling_proportions_source_path: str +) -> pd.DataFrame: + state_cooling_weights = pd.read_csv(cooling_proportions_source_path) + state_cooling_weights["weather_datetime"] = ( + pd.to_datetime(state_cooling_weights["weather_datetime"]) + ) + national_cooling_weights = ( + state_cooling_weights.groupby("weather_datetime") + ["cooling_prop"] + .mean() + ) + + return national_cooling_weights + +def get_data_center_state_weights( + state_proportions_source_path: str, + model_year: int, + scenario: str +) -> pd.DataFrame: + data_center_year = 2024 if model_year == 2025 else model_year + state_weights = pd.read_excel(state_proportions_source_path) + state_weights = ( + state_weights.loc[ + (state_weights['Run Name'] == scenario) + & (state_weights['Year'] == data_center_year) + ] + .set_index('State') + ["% of Total Data Center Load"] + ) + + return state_weights + + +def apply_state_and_subsector_weights( + national_demand: pd.DataFrame, + state_weights: pd.Series, + subsector_weights: pd.Series, + subsector: str, +): + national_subsector_demand = national_demand * subsector_weights + state_subsector_demand = pd.DataFrame( + np.outer(national_subsector_demand, state_weights), + index=national_subsector_demand.index, + columns=state_weights.index + ) + state_subsector_demand = ( + state_subsector_demand.reset_index() + .assign( + sector='commercial', + subsector=subsector, + dispatch_feeder='Commercial' + ) + .rename_axis(columns='') + ) + + return state_subsector_demand + +def calculate_state_subsector_data_center_demand_hourly( + df_load: pd.DataFrame, + model_year: int, + scenario: str, + national_demand_source_path: str, + cooling_proportions_source_path: str, + propagation_source_path: str, + state_proportions_source_path: str +) -> pd.DataFrame: + # Calculate hourly national data center demand + national_data_center_demand_hourly = ( + calculate_national_data_center_demand_hourly( + df_load, + model_year, + scenario, + national_demand_source_path, + propagation_source_path + ) + ) + # Calculate proportion of national demand attributable to each state + state_weights = get_data_center_state_weights( + state_proportions_source_path, + model_year, + scenario + ) + state_weights = state_weights.loc[state_weights.index.isin(df_load.columns)] + # Get proportion of hourly demand attributable to cooling + data_center_cooling_weights = get_data_center_cooling_weights( + cooling_proportions_source_path + ) + # Calculate state-by-state hourly demand for data center cooling subsector + state_data_center_cooling_demand_hourly = apply_state_and_subsector_weights( + national_demand=national_data_center_demand_hourly, + state_weights=state_weights, + subsector_weights=data_center_cooling_weights, + subsector='data center cooling', + ) + # Calculate state-by-state hourly demand for data center IT subsector + data_center_it_weights = 1 - data_center_cooling_weights + state_data_center_it_demand_hourly = apply_state_and_subsector_weights( + national_demand=national_data_center_demand_hourly, + state_weights=state_weights, + subsector_weights=data_center_it_weights, + subsector='data center it', + ) + # Concatenate all state subsector-level demand + state_subsector_data_center_demand_hourly = ( + pd.concat( + [ + state_data_center_cooling_demand_hourly, + state_data_center_it_demand_hourly + ], + ignore_index=True + ) + .fillna(0) + ) + return state_subsector_data_center_demand_hourly + +def apply_custom_data_center_demand_projections( + df_load: pd.DataFrame, + model_year: int, + cf: dict +): + state_subsector_data_center_demand_hourly = ( + calculate_state_subsector_data_center_demand_hourly( + df_load, + model_year, + cf.scenario, + cf.national_demand_source, + cf.cooling_proportions_source, + cf.propagation_source, + cf.state_proportions_source + ) + ) + + if cf.replace_existing_data_center_demand: + data_center_subsectors = ['data center cooling', 'data center it'] + df_load = pd.concat( + [ + df_load.loc[~df_load.subsector.isin(data_center_subsectors)], + state_subsector_data_center_demand_hourly + ], + ignore_index=True + ) + else: + df_load = ( + pd.concat( + [df_load, state_subsector_data_center_demand_hourly], + ignore_index=True + ) + .groupby( + [ + 'weather_datetime', + 'sector', + 'subsector', + 'dispatch_feeder' + ], + as_index=False + ) + .sum(numeric_only=True) + ) + + return df_load \ No newline at end of file From 695470929f70ed6a6f5638160fee03b223fd022c Mon Sep 17 00:00:00 2001 From: kodiobika Date: Thu, 11 Jun 2026 10:41:19 -0400 Subject: [PATCH 02/10] Use existing sector_config.json instead of new reveal2reeds config --- hourlize/inputs/load/sector_config.json | 12 ++++---- hourlize/load.py | 39 ++++++++++++++++++++----- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/hourlize/inputs/load/sector_config.json b/hourlize/inputs/load/sector_config.json index 35997a25..5d012d28 100644 --- a/hourlize/inputs/load/sector_config.json +++ b/hourlize/inputs/load/sector_config.json @@ -45,10 +45,12 @@ "subsectors": { "commercial": ["data center cooling", "data center it"] }, - "model_years": [2021, 2025, 2030, 2035, 2040, 2045, 2050], - "filepaths": ["/kfs2/projects/eerload/challoran/eer_splice/dummy_agg_op_datacenters_by_state.csv"], - "unit_conversion_factor": 1, - "timezone": "Etc/GMT+6", - "regional_scope": "state" + "model_years": [2025, 2030, 2035, 2040, 2045, 2050], + "scenario": "central", + "national_demand_source": "/projects/largeload/geospatial/runs/random_forest_base_weights_01_09_2026/downscaling_2026-01-07_agg64/eer_national_central/eer_national_central_downscaled_projections.csv", + "cooling_proportions_source": "/projects/largeload/reVeal2ReEDS/files/{scenario}_dc_cooling_prop.csv", + "propagation_source": "/projects/largeload/reVeal2ReEDS/files/weather_year_propagation.csv", + "replace_existing_data_center_demand": true, + "state_proportions_source": "/projects/eerload/source_eer_load_profiles/20250512_eer_download/shape_outputs_2025-05-12/annual_files/data center load allocation ADP 2024.xlsx" } } \ No newline at end of file diff --git a/hourlize/load.py b/hourlize/load.py index 97cbb4ed..0349b81c 100644 --- a/hourlize/load.py +++ b/hourlize/load.py @@ -283,15 +283,38 @@ def create_hourly_state_load_for_model_year( parse_dates=['weather_datetime'] ) - # If applicable, replace data center cooling and IT projections with - # custom projections specified in reveal2reeds/config.json - if model_year in cf.custom_data_center_projection_years: - reveal2reeds_config = get_reveal2reeds_config() - df_load = reveal2reeds.apply_custom_data_center_demand_projections( - df_load, - model_year, - reveal2reeds_config + # # If applicable, replace data center cooling and IT projections with + # # custom projections specified in reveal2reeds/config.json + # if model_year in cf.custom_data_center_projection_years: + # reveal2reeds_config = get_reveal2reeds_config() + # df_load = reveal2reeds.apply_custom_data_center_demand_projections( + # df_load, + # model_year, + # reveal2reeds_config + # ) + + # If applicable, replace or add to data center cooling and IT projections, + # as specified in inputs/load/sector_config.json + # Note this is handled differently and separately from the other + # 'replace_sectors'. The logic for handling those is below. + if 'Data Centers' in replace_sectors: + data_center_config = sector_config['Data Centers'] + data_center_config.cooling_proportions_source = ( + data_center_config.cooling_proportions_source + .format(scenario=data_center_config.scenario) ) + if model_year in data_center_config['model_years']: + df_load = reveal2reeds.apply_custom_data_center_demand_projections( + df_load, + model_year, + data_center_config + ) + else: + pass + + # Remove 'data centers' from 'replace_sectors' because we traverse + # through the list later to handle the other sectors. + replace_sectors.remove('Data Centers') # Downselect to specified weather years df_load = df_load.loc[df_load.weather_datetime.dt.year.isin(weather_years)] From 6ca8f679a2cabbef3996efa66faa2e1df82b7b91 Mon Sep 17 00:00:00 2001 From: kodiobika Date: Thu, 11 Jun 2026 12:03:01 -0400 Subject: [PATCH 03/10] Move reveal2reeds config to sector_config.json --- hourlize/inputs/configs/config_base.json | 1 - hourlize/load.py | 45 ++++++------------------ hourlize/reveal2reeds/config.json | 8 ----- hourlize/reveal2reeds/reveal2reeds.py | 14 ++++---- 4 files changed, 18 insertions(+), 50 deletions(-) delete mode 100644 hourlize/reveal2reeds/config.json diff --git a/hourlize/inputs/configs/config_base.json b/hourlize/inputs/configs/config_base.json index 3f840d3f..1409d39a 100644 --- a/hourlize/inputs/configs/config_base.json +++ b/hourlize/inputs/configs/config_base.json @@ -51,7 +51,6 @@ "2045": 1, "2050": 1 }, - "custom_data_center_projection_years": [2025, 2030, 2035, 2040, 2045, 2050], "scenarios": ["IRA cons", "central", "baseline"], "sector_config_file": "{hourlize_path}/inputs/load/sector_config.json", "weather_years": [2007,2008,2009,2010,2011,2012,2013,2016,2017,2018,2019,2020,2021,2022,2023] diff --git a/hourlize/load.py b/hourlize/load.py index 0349b81c..96280c6c 100644 --- a/hourlize/load.py +++ b/hourlize/load.py @@ -11,18 +11,6 @@ from types import SimpleNamespace from reveal2reeds import reveal2reeds -def get_reveal2reeds_config() -> dict: - configpath = "reveal2reeds/config.json" - with open(configpath, "r") as f: - config = json.load(f, object_pairs_hook=OrderedDict) - reveal2reeds_config = SimpleNamespace(**config) - reveal2reeds_config.cooling_proportions_source = ( - reveal2reeds_config.cooling_proportions_source - .format(scenario=reveal2reeds_config.scenario) - ) - - return reveal2reeds_config - def get_state_name_code_map(reeds_path: str) -> dict: """ Read from the ReEDS directory a file containing the mapping from state @@ -283,25 +271,13 @@ def create_hourly_state_load_for_model_year( parse_dates=['weather_datetime'] ) - # # If applicable, replace data center cooling and IT projections with - # # custom projections specified in reveal2reeds/config.json - # if model_year in cf.custom_data_center_projection_years: - # reveal2reeds_config = get_reveal2reeds_config() - # df_load = reveal2reeds.apply_custom_data_center_demand_projections( - # df_load, - # model_year, - # reveal2reeds_config - # ) - # If applicable, replace or add to data center cooling and IT projections, # as specified in inputs/load/sector_config.json - # Note this is handled differently and separately from the other - # 'replace_sectors'. The logic for handling those is below. if 'Data Centers' in replace_sectors: data_center_config = sector_config['Data Centers'] - data_center_config.cooling_proportions_source = ( - data_center_config.cooling_proportions_source - .format(scenario=data_center_config.scenario) + data_center_config['cooling_proportions_source'] = ( + data_center_config['cooling_proportions_source'] + .format(scenario=data_center_config['scenario']) ) if model_year in data_center_config['model_years']: df_load = reveal2reeds.apply_custom_data_center_demand_projections( @@ -312,10 +288,6 @@ def create_hourly_state_load_for_model_year( else: pass - # Remove 'data centers' from 'replace_sectors' because we traverse - # through the list later to handle the other sectors. - replace_sectors.remove('Data Centers') - # Downselect to specified weather years df_load = df_load.loc[df_load.weather_datetime.dt.year.isin(weather_years)] @@ -330,6 +302,10 @@ def create_hourly_state_load_for_model_year( # sectoral load from the raw load profiles replacement_load_list = [] for sector in replace_sectors: + # Skip 'data centers' sector, as it was already processed above + if sector == 'Data Centers': + continue + print(f"Removing endogenous load for '{sector}' sector...") if sector not in sector_config: raise NotImplementedError( @@ -395,6 +371,8 @@ def create_hourly_state_load_for_model_year( model_year ) for sector in replace_sectors + # Skip 'data centers' sector, as it was already processed above + if sector != 'Data Centers' ] # Aggregate the exogenous sectoral load to the state level and @@ -490,9 +468,8 @@ def main( ) output_fpath = os.path.join( - reeds_path, - "inputs", - "load", + cf.outpath, + 'results', f"demand_{scenario_outfile_prefix_map[scenario]}.h5" ) for model_year in model_years: diff --git a/hourlize/reveal2reeds/config.json b/hourlize/reveal2reeds/config.json deleted file mode 100644 index 6eedc7c0..00000000 --- a/hourlize/reveal2reeds/config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "national_demand_source": "/projects/largeload/geospatial/runs/random_forest_base_weights_01_09_2026/downscaling_2026-01-07_agg64/eer_national_central/eer_national_central_downscaled_projections.csv", - "cooling_proportions_source": "/projects/largeload/reVeal2ReEDS/files/{scenario}_dc_cooling_prop.csv", - "propagation_source": "/projects/largeload/reVeal2ReEDS/files/weather_year_propagation.csv", - "replace_existing_data_center_demand": true, - "scenario": "central", - "state_proportions_source": "/projects/eerload/source_eer_load_profiles/20250512_eer_download/shape_outputs_2025-05-12/annual_files/data center load allocation ADP 2024.xlsx" -} \ No newline at end of file diff --git a/hourlize/reveal2reeds/reveal2reeds.py b/hourlize/reveal2reeds/reveal2reeds.py index 03331fd6..651e0273 100644 --- a/hourlize/reveal2reeds/reveal2reeds.py +++ b/hourlize/reveal2reeds/reveal2reeds.py @@ -201,16 +201,16 @@ def apply_custom_data_center_demand_projections( calculate_state_subsector_data_center_demand_hourly( df_load, model_year, - cf.scenario, - cf.national_demand_source, - cf.cooling_proportions_source, - cf.propagation_source, - cf.state_proportions_source + cf['scenario'], + cf['national_demand_source'], + cf['cooling_proportions_source'], + cf['propagation_source'], + cf['state_proportions_source'] ) ) - if cf.replace_existing_data_center_demand: - data_center_subsectors = ['data center cooling', 'data center it'] + if cf['replace_existing_data_center_demand']: + data_center_subsectors = cf['subsectors']['commercial'] df_load = pd.concat( [ df_load.loc[~df_load.subsector.isin(data_center_subsectors)], From 77675c8b8667f9a5511943dc09041ad08e6cd5eb Mon Sep 17 00:00:00 2001 From: Kodi Obika Date: Wed, 8 Jul 2026 16:04:21 -0600 Subject: [PATCH 04/10] Cleanup --- hourlize/load.py | 78 ++++++++++++++------------- hourlize/reveal2reeds/reveal2reeds.py | 3 +- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/hourlize/load.py b/hourlize/load.py index 96280c6c..4bdc434f 100644 --- a/hourlize/load.py +++ b/hourlize/load.py @@ -271,23 +271,6 @@ def create_hourly_state_load_for_model_year( parse_dates=['weather_datetime'] ) - # If applicable, replace or add to data center cooling and IT projections, - # as specified in inputs/load/sector_config.json - if 'Data Centers' in replace_sectors: - data_center_config = sector_config['Data Centers'] - data_center_config['cooling_proportions_source'] = ( - data_center_config['cooling_proportions_source'] - .format(scenario=data_center_config['scenario']) - ) - if model_year in data_center_config['model_years']: - df_load = reveal2reeds.apply_custom_data_center_demand_projections( - df_load, - model_year, - data_center_config - ) - else: - pass - # Downselect to specified weather years df_load = df_load.loc[df_load.weather_datetime.dt.year.isin(weather_years)] @@ -299,31 +282,50 @@ def create_hourly_state_load_for_model_year( ] # For each sector specified in 'replace_sectors', remove endogenous - # sectoral load from the raw load profiles + # sectoral load from the raw load profiles. In general, the removal of + # endogenous load and then the exogenous replacement load is added later + # once the load profiles are aggregated across sectors. + # In the case of the "Data Centers" sector, the replacement of the load + # happens here all in one step, as executed by the function + # reveal2reeds.apply_custom_data_center_demand_projections(). replacement_load_list = [] for sector in replace_sectors: - # Skip 'data centers' sector, as it was already processed above if sector == 'Data Centers': - continue - - print(f"Removing endogenous load for '{sector}' sector...") - if sector not in sector_config: - raise NotImplementedError( - f"'{sector}' is not a recognized sector. " - "Update 'hourlize/inputs/load/sector_config.json'." - ) - - sector_settings = sector_config[sector] - if model_year in sector_settings['model_years']: - df_load = remove_sectoral_load( - df_load, - sector_settings['subsectors'], - replace_states, - replacement_share, - model_year + data_center_config = sector_config[sector] + data_center_config['cooling_proportions_source'] = ( + data_center_config['cooling_proportions_source'] + .format(scenario=data_center_config['scenario']) ) + if model_year in data_center_config['model_years']: + print(f"Replacing endogenous load for '{sector}' sector...") + df_load = ( + reveal2reeds.apply_custom_data_center_demand_projections( + df_load, + model_year, + data_center_config + ) + ) + else: + pass else: - pass + print(f"Removing endogenous load for '{sector}' sector...") + if sector not in sector_config: + raise NotImplementedError( + f"'{sector}' is not a recognized sector. " + "Update 'hourlize/inputs/load/sector_config.json'." + ) + + sector_settings = sector_config[sector] + if model_year in sector_settings['model_years']: + df_load = remove_sectoral_load( + df_load, + sector_settings['subsectors'], + replace_states, + replacement_share, + model_year + ) + else: + pass # Aggregate load across sectors to create state-level profiles df_load = ( @@ -371,7 +373,7 @@ def create_hourly_state_load_for_model_year( model_year ) for sector in replace_sectors - # Skip 'data centers' sector, as it was already processed above + # Skip 'data centers' sector, as it was already fully processed above if sector != 'Data Centers' ] diff --git a/hourlize/reveal2reeds/reveal2reeds.py b/hourlize/reveal2reeds/reveal2reeds.py index 651e0273..6c49c4eb 100644 --- a/hourlize/reveal2reeds/reveal2reeds.py +++ b/hourlize/reveal2reeds/reveal2reeds.py @@ -60,7 +60,7 @@ def calculate_national_data_center_demand_hourly( # by multiplying the propagation factors by national data # center demand for the model year. national_data_center_demand_hourly = pd.DataFrame( - index=df_load['weather_datetime'].drop_duplicates() + index=df_load['weather_datetime'].unique() ) national_data_center_demand_hourly['propagation_factor'] = ( national_data_center_demand_hourly.index.year @@ -229,7 +229,6 @@ def apply_custom_data_center_demand_projections( 'weather_datetime', 'sector', 'subsector', - 'dispatch_feeder' ], as_index=False ) From aa83ce3554a979ebe78cbcfb1158af9d63fc8c94 Mon Sep 17 00:00:00 2001 From: kodiobika Date: Thu, 9 Jul 2026 11:13:45 -0400 Subject: [PATCH 05/10] Clarify data center config settings --- hourlize/inputs/load/sector_config.json | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/hourlize/inputs/load/sector_config.json b/hourlize/inputs/load/sector_config.json index 5d012d28..2543efaf 100644 --- a/hourlize/inputs/load/sector_config.json +++ b/hourlize/inputs/load/sector_config.json @@ -46,11 +46,16 @@ "commercial": ["data center cooling", "data center it"] }, "model_years": [2025, 2030, 2035, 2040, 2045, 2050], - "scenario": "central", - "national_demand_source": "/projects/largeload/geospatial/runs/random_forest_base_weights_01_09_2026/downscaling_2026-01-07_agg64/eer_national_central/eer_national_central_downscaled_projections.csv", - "cooling_proportions_source": "/projects/largeload/reVeal2ReEDS/files/{scenario}_dc_cooling_prop.csv", - "propagation_source": "/projects/largeload/reVeal2ReEDS/files/weather_year_propagation.csv", "replace_existing_data_center_demand": true, - "state_proportions_source": "/projects/eerload/source_eer_load_profiles/20250512_eer_download/shape_outputs_2025-05-12/annual_files/data center load allocation ADP 2024.xlsx" + "national_demand_source": "/projects/largeload/geospatial/runs/random_forest_base_weights_01_09_2026/downscaling_2026-01-07_agg64/eer_national_central/eer_national_central_downscaled_projections.csv", + "cooling_proportions_source": "/projects/largeload/reVeal2ReEDS/files/central_dc_cooling_prop.csv", + "state_proportions_source": { + "filepath": "/projects/eerload/source_eer_load_profiles/20250512_eer_download/shape_outputs_2025-05-12/annual_files/data center load allocation ADP 2024.xlsx", + "scenario": "central" + }, + "weather_year_propagation_source": { + "filepath": "/projects/largeload/reVeal2ReEDS/files/weather_year_propagation.csv", + "scenario": "central" + } } } \ No newline at end of file From 27896b9973e8378b4ce79a0f03fb4a69d9c17bed Mon Sep 17 00:00:00 2001 From: kodiobika Date: Thu, 9 Jul 2026 11:14:03 -0400 Subject: [PATCH 06/10] Add readme --- hourlize/inputs/load/README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 hourlize/inputs/load/README.md diff --git a/hourlize/inputs/load/README.md b/hourlize/inputs/load/README.md new file mode 100644 index 00000000..564e271c --- /dev/null +++ b/hourlize/inputs/load/README.md @@ -0,0 +1,22 @@ +### Sectoral load replacement +Hourlize uses the settings in `sector_config.json` to replace endogenous sector-specific load with load from exogenous sources. The sectoral settings are outlined below. + +| Setting | Description | +| :------ | :---------- | +| subsectors | Dictionary listing the relevant sectors (keys) and subsectors (values) whose load should be replaced. Keys and values should correspond to entries of the "sector" and "subsector" columns of the input load profiles respectively | +| model_years | Model years in which load should be replaced. | +| filepaths | Paths to files containing exogenous sectoral load. | +| unit_conversion_factor | Factor by which to multiply exogenous load to convert to MWh. | +| timezone | Timezone of the exogenous load profiles. This is used to convert exogenous load to the timezone of the input load profiles. | +| regional_scope | Regional scope of exogenous load. This is used to convert exogenous load to the state level (the scope of the input load). | + +#### Data Centers +The "Data Centers" sector is treated as a special case, using a more complex pipeline mostly designed to ingest data center demand projections from the reVeal tool. This pipeline is contained in the `hourlize/reveal2reeds` folder. It takes annual national data center load and then splits it into hourly IT and cooling profiles for each state based on assumptions of state load participation, power usage effectiveness, and hourly propagation that are specified in the config. Unlike the other scenarios, this pipeline provides an option to add exogenous load to endogenous load rather than just replacing it. The relevant settings are outlined below. + +| Setting | Description | +| :------ | :---------- | +| replace_existing_data_center_demand | Controls whether exogenous data center demand replaces the endogeous data center demand or adds to it. Replacement is the default behavior. | +| national_demand_source | Filepath to annual data center demand projections. The file must have columns "year" and "total_data_center_mw". Sub-national scope is allowed; in these cases, demand values are summed for each year to arrive at annual national demand values. | +| cooling_proportions_source | Filepath to hourly (weather year-specific), state-level proportions of data center demand that should be attributed to cooling. The file must have columns "weather_datetime", "state", and "cooling_prop". +| state_proportions_source | Dictionary providing the filepath to annual percentages of national data center demand that should be attributed to each state and the scenario that should be taken from that file. The file must have columns "Run Name", "State", "Year", and "% of Total Data Center Load", and the "scenario" value of the dictionary must correspond to an entry in the "Run Name" column of the file. +| weather_year_propagation_source | Dictionary providing the filepath to annual propagation factors and the scenario that should be taken from that file. Propagation factors represent the percentage of projected national data center demand for a given model year that is expected to be realized during each hour of each weather year. The file must have columns "year" (representing the weather year), "scenario" and "avg_prop", and the "scenario" value of the dictionary must correspond to an entry in the "scenario" column of the file. \ No newline at end of file From 645bf9771518c6c26bf3e7b2ccfe9ee73e43f452 Mon Sep 17 00:00:00 2001 From: kodiobika Date: Thu, 9 Jul 2026 12:05:07 -0400 Subject: [PATCH 07/10] Update readme --- hourlize/inputs/load/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hourlize/inputs/load/README.md b/hourlize/inputs/load/README.md index 564e271c..c52df036 100644 --- a/hourlize/inputs/load/README.md +++ b/hourlize/inputs/load/README.md @@ -5,7 +5,7 @@ Hourlize uses the settings in `sector_config.json` to replace endogenous sector- | :------ | :---------- | | subsectors | Dictionary listing the relevant sectors (keys) and subsectors (values) whose load should be replaced. Keys and values should correspond to entries of the "sector" and "subsector" columns of the input load profiles respectively | | model_years | Model years in which load should be replaced. | -| filepaths | Paths to files containing exogenous sectoral load. | +| filepaths | Paths to files containing exogenous sectoral load. The values in these files are combined and aggregated to the state level and then added to the input load profiles after removing endogenous sectoral load and aggregating to the state level. | | unit_conversion_factor | Factor by which to multiply exogenous load to convert to MWh. | | timezone | Timezone of the exogenous load profiles. This is used to convert exogenous load to the timezone of the input load profiles. | | regional_scope | Regional scope of exogenous load. This is used to convert exogenous load to the state level (the scope of the input load). | @@ -17,6 +17,6 @@ The "Data Centers" sector is treated as a special case, using a more complex pip | :------ | :---------- | | replace_existing_data_center_demand | Controls whether exogenous data center demand replaces the endogeous data center demand or adds to it. Replacement is the default behavior. | | national_demand_source | Filepath to annual data center demand projections. The file must have columns "year" and "total_data_center_mw". Sub-national scope is allowed; in these cases, demand values are summed for each year to arrive at annual national demand values. | -| cooling_proportions_source | Filepath to hourly (weather year-specific), state-level proportions of data center demand that should be attributed to cooling. The file must have columns "weather_datetime", "state", and "cooling_prop". +| cooling_proportions_source | Filepath to hourly (weather year), state-level proportions of data center demand that should be attributed to cooling. The file must have columns "weather_datetime", "state", and "cooling_prop". | state_proportions_source | Dictionary providing the filepath to annual percentages of national data center demand that should be attributed to each state and the scenario that should be taken from that file. The file must have columns "Run Name", "State", "Year", and "% of Total Data Center Load", and the "scenario" value of the dictionary must correspond to an entry in the "Run Name" column of the file. -| weather_year_propagation_source | Dictionary providing the filepath to annual propagation factors and the scenario that should be taken from that file. Propagation factors represent the percentage of projected national data center demand for a given model year that is expected to be realized during each hour of each weather year. The file must have columns "year" (representing the weather year), "scenario" and "avg_prop", and the "scenario" value of the dictionary must correspond to an entry in the "scenario" column of the file. \ No newline at end of file +| weather_year_propagation_source | Dictionary providing the filepath to annual (weather year) propagation factors and the scenario that should be taken from that file. Propagation factors represent the percentage of projected national data center demand for a given model year that is expected to be realized during each hour of each weather year. The file must have columns "year" (representing the weather year), "scenario" and "avg_prop", and the "scenario" value of the dictionary must correspond to an entry in the "scenario" column of the file. \ No newline at end of file From 7ff85cde99fb2f741acc2dcecdc6f5e7139ea801 Mon Sep 17 00:00:00 2001 From: kodiobika Date: Thu, 9 Jul 2026 12:06:15 -0400 Subject: [PATCH 08/10] Code changes to match config updates --- hourlize/load.py | 4 ---- hourlize/reveal2reeds/reveal2reeds.py | 28 ++++++++++----------------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/hourlize/load.py b/hourlize/load.py index 4bdc434f..56f55d45 100644 --- a/hourlize/load.py +++ b/hourlize/load.py @@ -292,10 +292,6 @@ def create_hourly_state_load_for_model_year( for sector in replace_sectors: if sector == 'Data Centers': data_center_config = sector_config[sector] - data_center_config['cooling_proportions_source'] = ( - data_center_config['cooling_proportions_source'] - .format(scenario=data_center_config['scenario']) - ) if model_year in data_center_config['model_years']: print(f"Replacing endogenous load for '{sector}' sector...") df_load = ( diff --git a/hourlize/reveal2reeds/reveal2reeds.py b/hourlize/reveal2reeds/reveal2reeds.py index 6c49c4eb..fd47ed68 100644 --- a/hourlize/reveal2reeds/reveal2reeds.py +++ b/hourlize/reveal2reeds/reveal2reeds.py @@ -19,13 +19,12 @@ def get_national_model_year_data_center_demand( return national_model_year_data_center_demand def get_propagation_by_weather_year( - propagation_source_path: str, - scenario: str + propagation_source: dict[str, str], ) -> pd.Series: - propagation_by_weather_year = pd.read_csv(propagation_source_path) + propagation_by_weather_year = pd.read_csv(propagation_source['filepath']) propagation_by_weather_year = ( propagation_by_weather_year.loc[( - propagation_by_weather_year.scenario == scenario + propagation_by_weather_year.scenario == propagation_source['scenario'] )] .set_index('year') ['avg_prop'] @@ -37,7 +36,6 @@ def get_propagation_by_weather_year( def calculate_national_data_center_demand_hourly( df_load: pd.DataFrame, model_year: int, - scenario: str, national_demand_source_path: str, propagation_source_path: str ): @@ -47,13 +45,12 @@ def calculate_national_data_center_demand_hourly( model_year ) - # Get propagation factors by weather year for the given scenario. + # Get propagation factors by weather year. # Propagation factors represent the percentage of projected national # data center demand for the model year that is expected to be # realized during each hour of each weather year. propagation_by_weather_year = get_propagation_by_weather_year( - propagation_source_path, - scenario + propagation_source_path ) # Estimate national hourly load values for each weather year @@ -92,15 +89,14 @@ def get_data_center_cooling_weights( return national_cooling_weights def get_data_center_state_weights( - state_proportions_source_path: str, - model_year: int, - scenario: str + state_proportions_source: dict[str, str], + model_year: int ) -> pd.DataFrame: data_center_year = 2024 if model_year == 2025 else model_year - state_weights = pd.read_excel(state_proportions_source_path) + state_weights = pd.read_excel(state_proportions_source['filepath']) state_weights = ( state_weights.loc[ - (state_weights['Run Name'] == scenario) + (state_weights['Run Name'] == state_proportions_source['scenario']) & (state_weights['Year'] == data_center_year) ] .set_index('State') @@ -137,7 +133,6 @@ def apply_state_and_subsector_weights( def calculate_state_subsector_data_center_demand_hourly( df_load: pd.DataFrame, model_year: int, - scenario: str, national_demand_source_path: str, cooling_proportions_source_path: str, propagation_source_path: str, @@ -148,7 +143,6 @@ def calculate_state_subsector_data_center_demand_hourly( calculate_national_data_center_demand_hourly( df_load, model_year, - scenario, national_demand_source_path, propagation_source_path ) @@ -156,8 +150,7 @@ def calculate_state_subsector_data_center_demand_hourly( # Calculate proportion of national demand attributable to each state state_weights = get_data_center_state_weights( state_proportions_source_path, - model_year, - scenario + model_year ) state_weights = state_weights.loc[state_weights.index.isin(df_load.columns)] # Get proportion of hourly demand attributable to cooling @@ -201,7 +194,6 @@ def apply_custom_data_center_demand_projections( calculate_state_subsector_data_center_demand_hourly( df_load, model_year, - cf['scenario'], cf['national_demand_source'], cf['cooling_proportions_source'], cf['propagation_source'], From 646dba5c47ac6914010e518a1c02c8b9534feb84 Mon Sep 17 00:00:00 2001 From: Kodi Obika Date: Thu, 9 Jul 2026 11:47:27 -0600 Subject: [PATCH 09/10] Bugfix --- hourlize/reveal2reeds/reveal2reeds.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/hourlize/reveal2reeds/reveal2reeds.py b/hourlize/reveal2reeds/reveal2reeds.py index fd47ed68..ca32d736 100644 --- a/hourlize/reveal2reeds/reveal2reeds.py +++ b/hourlize/reveal2reeds/reveal2reeds.py @@ -37,7 +37,7 @@ def calculate_national_data_center_demand_hourly( df_load: pd.DataFrame, model_year: int, national_demand_source_path: str, - propagation_source_path: str + weather_year_propagation_source: str ): # Calculate national projected data center demand for the model year national_data_center_demand = get_national_model_year_data_center_demand( @@ -50,14 +50,14 @@ def calculate_national_data_center_demand_hourly( # data center demand for the model year that is expected to be # realized during each hour of each weather year. propagation_by_weather_year = get_propagation_by_weather_year( - propagation_source_path + weather_year_propagation_source ) # Estimate national hourly load values for each weather year # by multiplying the propagation factors by national data # center demand for the model year. national_data_center_demand_hourly = pd.DataFrame( - index=df_load['weather_datetime'].unique() + index=df_load['weather_datetime'].drop_duplicates() ) national_data_center_demand_hourly['propagation_factor'] = ( national_data_center_demand_hourly.index.year @@ -135,8 +135,8 @@ def calculate_state_subsector_data_center_demand_hourly( model_year: int, national_demand_source_path: str, cooling_proportions_source_path: str, - propagation_source_path: str, - state_proportions_source_path: str + weather_year_propagation_source: str, + state_proportions_source: str ) -> pd.DataFrame: # Calculate hourly national data center demand national_data_center_demand_hourly = ( @@ -144,12 +144,12 @@ def calculate_state_subsector_data_center_demand_hourly( df_load, model_year, national_demand_source_path, - propagation_source_path + weather_year_propagation_source ) ) # Calculate proportion of national demand attributable to each state state_weights = get_data_center_state_weights( - state_proportions_source_path, + state_proportions_source, model_year ) state_weights = state_weights.loc[state_weights.index.isin(df_load.columns)] @@ -196,7 +196,7 @@ def apply_custom_data_center_demand_projections( model_year, cf['national_demand_source'], cf['cooling_proportions_source'], - cf['propagation_source'], + cf['weather_year_propagation_source'], cf['state_proportions_source'] ) ) From 3ea396f9cf3c172a521cc9ec101a6a591a3a3b61 Mon Sep 17 00:00:00 2001 From: Kodi Obika Date: Thu, 9 Jul 2026 12:42:38 -0600 Subject: [PATCH 10/10] Cleanup --- hourlize/load.py | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/hourlize/load.py b/hourlize/load.py index 56f55d45..a05f38c5 100644 --- a/hourlize/load.py +++ b/hourlize/load.py @@ -283,36 +283,32 @@ def create_hourly_state_load_for_model_year( # For each sector specified in 'replace_sectors', remove endogenous # sectoral load from the raw load profiles. In general, the removal of - # endogenous load and then the exogenous replacement load is added later - # once the load profiles are aggregated across sectors. + # endogenous load happens here and then the exogenous replacement load + # is added later once the load profiles are aggregated across sectors. # In the case of the "Data Centers" sector, the replacement of the load # happens here all in one step, as executed by the function # reveal2reeds.apply_custom_data_center_demand_projections(). replacement_load_list = [] for sector in replace_sectors: - if sector == 'Data Centers': - data_center_config = sector_config[sector] - if model_year in data_center_config['model_years']: + if sector not in sector_config: + raise NotImplementedError( + f"'{sector}' is not a recognized sector. " + "Update 'hourlize/inputs/load/sector_config.json'." + ) + + sector_settings = sector_config[sector] + if model_year in sector_settings['model_years']: + if sector == 'Data Centers': print(f"Replacing endogenous load for '{sector}' sector...") df_load = ( reveal2reeds.apply_custom_data_center_demand_projections( df_load, model_year, - data_center_config + sector_settings ) ) else: - pass - else: - print(f"Removing endogenous load for '{sector}' sector...") - if sector not in sector_config: - raise NotImplementedError( - f"'{sector}' is not a recognized sector. " - "Update 'hourlize/inputs/load/sector_config.json'." - ) - - sector_settings = sector_config[sector] - if model_year in sector_settings['model_years']: + print(f"Removing endogenous load for '{sector}' sector...") df_load = remove_sectoral_load( df_load, sector_settings['subsectors'], @@ -320,8 +316,8 @@ def create_hourly_state_load_for_model_year( replacement_share, model_year ) - else: - pass + else: + pass # Aggregate load across sectors to create state-level profiles df_load = (