From b9ad7e6dc58281a888c98e15651cf88bfe5b5b2a Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:12:00 -0600 Subject: [PATCH 01/11] add checks.check_numperiods(); runreeds.py: change skip_checks to force --- docs/source/developer_best_practices.md | 2 +- reeds/checks.py | 79 ++++++++++++++++++++++++- reeds/inputs.py | 6 +- runreeds.py | 34 +++++++---- 4 files changed, 103 insertions(+), 18 deletions(-) diff --git a/docs/source/developer_best_practices.md b/docs/source/developer_best_practices.md index 9aa3f3c57..ad24dd839 100644 --- a/docs/source/developer_best_practices.md +++ b/docs/source/developer_best_practices.md @@ -638,7 +638,7 @@ Additionally, if you want to re-run a given scenario without having to run all o | `--simult_runs` / `-r` | Number of simultaneous runs. If negative, run all simultaneously | | `--forcelocal` / `-l` | Force model to run locally instead of submitting a slurm job | | `--restart` / `-r` | Switch to restart existing ReEDS runs | - | `--skip_checks` / `-f` | Force run, skipping checks on conda environment and switches | + | `--force` / `-f` | Force run, skipping checks on conda environment and switches | | `--debug` / `-d` | Run in debug mode (same behavior as debug switch in cases.csv) | | `--debugnode` / `-n` | Run using debug specifications for slurm on an hpc system | diff --git a/reeds/checks.py b/reeds/checks.py index 848e1fc45..a90baa6fc 100644 --- a/reeds/checks.py +++ b/reeds/checks.py @@ -1,6 +1,8 @@ import pandas as pd import os import sys +from pathlib import Path +from warnings import warn sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) import reeds @@ -33,6 +35,81 @@ def check_GSw_LoadSiteReg(sw): raise ValueError(err) -def check_switches(sw): +def estimate_numperiods(sw): + """ + Estimate the number of representative periods for the supplied switch settings. + Mostly only matters if using GSw_HourlyClusterAlgorithm = 'optimized'. + + Caveats: + - Will overestimate the number of periods if the following three are true: + - GSw_HourlyClusterAlgorithm = 'optimized' + - GSw_HourlyClusterWeights has nonzero weight for 'wind-ofs' + - The combination of GSw_ZoneSet, GSw_Region, and GSw_HourlyClusterRegionLevel + specifies regions that do not include offshore wind resources + + Inputs for testing: + sw = { + 'GSw_ZoneSet': 'z90', + 'GSw_Region': 'cendiv/Pacific', + 'GSw_HourlyClusterRegionLevel': 'transgrp', + 'GSw_HourlyClusterWeights': 'load_1/upv_1/wind-ons_1/wind-ofs_0', + } + """ + if str(sw['GSw_HourlyClusterAlgorithm']).startswith(('hierarchical','kmeans','kmedoids')): + numperiods = int(sw['GSw_HourlyNumPeriods']) + elif sw['GSw_HourlyClusterAlgorithm'] == 'optimized': + zones = reeds.inputs.parse_regions(**sw) + hierarchy = reeds.io.assemble_hierarchy(**sw).set_index('r').loc[zones] + level = sw['GSw_HourlyClusterRegionLevel'] + levels = sorted(hierarchy[level].unique()) + featureweights = sw['GSw_HourlyClusterWeights'].split('/') + features = [i for i in featureweights if float(i.rsplit('_',1)[-1]) > 0] + numperiods = len(levels) * len(features) + elif str(sw['GSw_HourlyClusterAlgorithm']).startswith('user'): + fpath = Path( + reeds.io.reeds_path, 'inputs', 'temporal', + f'period_szn_{sw["GSw_HourlyClusterAlgorithm"]}.csv', + ) + period_szn = pd.read_csv(fpath) + numperiods = len(period_szn.rep_period.unique()) + else: + raise ValueError(f"GSw_HourlyClusterAlgorithm = {sw['GSw_HourlyClusterAlgorithm']}") + + return numperiods + + +def check_numperiods(sw, threshold=24, force=0): + """ + If the estimated number of representative periods is under the specified threshold, + ask the user if they want to proceed. + If force > 0, skip the prompt and print the estimated number of periods. + """ + numperiods = estimate_numperiods(sw) + if numperiods >= threshold: + return + else: + msg = ( + 'The estimated number of representative periods for your switch settings is ' + f'{numperiods}, which is less than the threshold of {threshold}.\n' + 'These conditions should only be used for testing and are not appropriate for ' + 'analysis.\nSee ' + 'https://reeds-model.github.io/ReEDS/user_guide.html#temporal-resolution-switches ' + 'for suggestions on how to increase the number of representative periods when ' + 'using a small number of regions.\n' + 'The simplest approach is to set GSw_HourlyClusteAlgorithm to "hierarchical" ' + f'and GSw_HourlyNumClusters ≥ {threshold}.' + ) + print(msg) + err = f'Insufficient representative periods: {numperiods}' + if force: + warn(err) + else: + proceed = str(input(f'Do you want to proceed with {numperiods} periods? y/[n]') or 'n') + if proceed.lower() not in ['y','yes']: + raise ValueError(err) + + +def check_switches(sw, force=0): """Run all the checks""" check_GSw_LoadSiteReg(sw) + check_numperiods(sw, force=force) diff --git a/reeds/inputs.py b/reeds/inputs.py index 256f95114..f386652e2 100644 --- a/reeds/inputs.py +++ b/reeds/inputs.py @@ -131,7 +131,7 @@ def add_intermediate_switches(dfcases:pd.DataFrame) -> pd.DataFrame: def parse_cases( cases_filename:str='cases_test.csv', single:str='', - skip_checks:bool=False, + force:int=0, ) -> pd.DataFrame: """ Read a ReEDS cases file, look up empty switch values from "Default Value" or cases.csv, @@ -140,7 +140,7 @@ def parse_cases( Args: cases_filename (str): 'cases_{something}.csv' or 'cases.csv' single (str): If not '', specifies a single column to keep from cases_filename - skip_checks (bool): Skip case validation (not recommended) + force (bool): Skip case validation (not recommended) Returns: pd.DataFrame @@ -198,7 +198,7 @@ def parse_cases( # Check to make sure the switch setting is valid for i, val in dfcases[case].items(): - if skip_checks: + if force: continue # check that the switch isn't duplicated if isinstance(choices[i], pd.Series) and len(choices[i]) > 1: diff --git a/runreeds.py b/runreeds.py index 1572ce3ef..052c7431a 100644 --- a/runreeds.py +++ b/runreeds.py @@ -215,7 +215,7 @@ def check_cases_format(df_cases): raise ValueError(err) -def check_compatibility(sw): +def check_compatibility(sw, force=0): if int(sw['startyear']) != 2010: raise ValueError(f"startyear = {sw['startyear']} but must be = 2010") @@ -531,7 +531,7 @@ def check_compatibility(sw): ) ### Contents of user-specified files - reeds.checks.check_switches(sw) + reeds.checks.check_switches(sw, force=(force>=1)) ### Uncommonly used packages if sw['GSw_HourlyClusterAlgorithm'].lower().startswith('kmedoids'): @@ -546,6 +546,7 @@ def check_compatibility(sw): ) raise ModuleNotFoundError(err) + # function to stop the model after input processing def stop_after_input_processing(OPATH, reeds_path, casedir, caseSwitches): comment('Exit after input_processing', OPATH) @@ -778,7 +779,7 @@ def setup_window( def setupEnvironment( BatchName=False, cases_suffix=False, single='', simult_runs=0, - forcelocal=0, skip_checks=False, + forcelocal=0, force=0, debug=False, debugnode=False, cases_per_node=1, dryrun=False, ): @@ -788,7 +789,7 @@ def setupEnvironment( # WORKERS = 1 # forcelocal = 0 # single = '' - # skip_checks = False + # force = 0 # debug = False # dryrun = True @@ -848,7 +849,7 @@ def setupEnvironment( quit() #%% Check whether the ReEDS conda environment is activated - if (not skip_checks) and ( + if (force < 3) and ( ('reeds' not in os.environ['CONDA_DEFAULT_ENV'].lower()) or (not pd.__version__.startswith('3')) ): @@ -872,7 +873,7 @@ def setupEnvironment( df_cases = reeds.inputs.parse_cases( cases_filename=cases_filename, single=single, - skip_checks=skip_checks, + force=(force>=2), ) ## Propagate debug setting if debug: @@ -887,7 +888,7 @@ def setupEnvironment( #%% Stop now if any switches are incompatible check_cases_format(df_cases) for sw in caseSwitches: - check_compatibility(sw) + check_compatibility(sw, force=force) if dryrun: quit() @@ -951,7 +952,7 @@ def setupEnvironment( quit() #%% User warnings - if (df_cases.loc['cleanup_level'].astype(int) > 0).any() and not skip_checks: + if (df_cases.loc['cleanup_level'].astype(int) > 0).any() and (force < 2): print( '\nWARNING: At least one case uses cleanup_level ≥ 1, which removes files ' 'used by R2X.\nIf you plan to run R2X, do not proceed; set cleanup_level ' @@ -1759,7 +1760,7 @@ def launch_single_case_run( def main( BatchName='', cases_suffix='', single='', simult_runs=0, - forcelocal=False, skip_checks=False, + forcelocal=False, force=0, debug=False, debugnode=False, cases_per_node=1, dryrun=False, ): @@ -1797,7 +1798,7 @@ def main( envVar = setupEnvironment( BatchName=BatchName, cases_suffix=cases_suffix, single=single, simult_runs=simult_runs, - forcelocal=forcelocal, skip_checks=skip_checks, + forcelocal=forcelocal, force=force, debug=debug, debugnode=debugnode, cases_per_node=cases_per_node, dryrun=dryrun, ) @@ -1832,8 +1833,6 @@ def main( help='Number of simultaneous runs. If negative, run all simultaneously.') parser.add_argument('--forcelocal', '-l', action='store_true', help='Force model to run locally instead of submitting a slurm job') - parser.add_argument('--skip_checks', '-f', action="store_true", - help="Force run, skipping checks on conda environment and switches") parser.add_argument('--debug', '-d', action='count', default=0, help="Run in debug mode (same behavior as debug switch in cases.csv)") parser.add_argument('--debugnode', '-n', action="store_true", @@ -1843,12 +1842,21 @@ def main( "If not provided, the user will be prompted to specify it.") parser.add_argument('--dryrun', '-t', action='store_true', help="Check inputs but don't start runs") + parser.add_argument( + '--force', '-f', action='count', default=0, + help=( + 'Force run, skipping the following checks: ' + '≥1: Enough representative periods for analysis; ' + '≥2: Switches within allowed values; ' + '≥3: Conda environment activated' + ), + ) args = parser.parse_args() main( BatchName=args.BatchName, cases_suffix=args.cases_suffix, single=args.single, - simult_runs=args.simult_runs, forcelocal=args.forcelocal, skip_checks=args.skip_checks, + simult_runs=args.simult_runs, forcelocal=args.forcelocal, force=args.force, debug=args.debug, debugnode=args.debugnode, cases_per_node=args.cases_per_node, dryrun=args.dryrun, ) From 917c39069045a9f772970c679ee7e68e8beb5846 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:19:13 -0600 Subject: [PATCH 02/11] move check_compatibility to reeds.checks --- reeds/checks.py | 336 +++++++++++++++++++++++++- reeds/input_processing/mcs_sampler.py | 3 +- runreeds.py | 336 +------------------------- 3 files changed, 337 insertions(+), 338 deletions(-) diff --git a/reeds/checks.py b/reeds/checks.py index a90baa6fc..755e8b39d 100644 --- a/reeds/checks.py +++ b/reeds/checks.py @@ -1,6 +1,9 @@ -import pandas as pd import os +import re import sys +import importlib +import numpy as np +import pandas as pd from pathlib import Path from warnings import warn @@ -8,6 +11,337 @@ import reeds +def check_compatibility(sw, force=0): + if int(sw['startyear']) != 2010: + raise ValueError(f"startyear = {sw['startyear']} but must be = 2010") + + if int(sw['GSw_SkipRAyear']) <= int(sw['startyear']): + raise ValueError(f"GSw_SkipRAyear = {sw['GSw_SkipRAyear']} but must be > {sw['startyear']}") + + if (sw['GSw_HourlyType'] in ['year']) and int(sw['GSw_InterDayLinkage']): + raise ValueError( + "GSw_HourlyType cannot be 'year' when GSw_InterDayLinkage is enabled. " + f"Current values: GSw_HourlyType={sw['GSw_HourlyType']}, GSw_InterDayLinkage={sw['GSw_InterDayLinkage']}" + ) + + if 24 % (int(sw['GSw_HourlyWindowOverlap']) * int(sw['GSw_HourlyChunkLengthRep'])): + raise ValueError( + ('24 must be divisible by GSw_HourlyWindowOverlap * GSw_HourlyChunkLengthRep:' + '\nGSw_HourlyWindowOverlap = {}\nGSw_HourlyChunkLengthRep = {}'.format( + sw['GSw_HourlyWindowOverlap'], sw['GSw_HourlyChunkLengthRep']))) + + if int(sw['GSw_HourlyWindow']) <= int(sw['GSw_HourlyWindowOverlap']): + raise ValueError( + ('GSw_HourlyWindow must be greater than GSw_HourlyWindowOverlap:' + '\nGSw_HourlyWindow = {}\nGSw_HourlyWindowOverlap = {}'.format( + sw['GSw_HourlyWindow'], sw['GSw_HourlyWindowOverlap']))) + + if ((sw['GSw_HourlyClusterAlgorithm'] not in ['hierarchical','optimized','kmeans','kmedoids']) + and ('user' not in sw['GSw_HourlyClusterAlgorithm']) + ): + if sw['GSw_HourlyClusterAlgorithm'].startswith('hierarchical'): + args = sw['GSw_HourlyClusterAlgorithm'].split('_') + assert len(args) == 3 + ## https://scikit-learn.org/stable/modules/generated/sklearn.cluster.AgglomerativeClustering.html + assert args[1] in ['euclidean','l1','l2','manhattan','cosine'] + assert args[2] in ['ward', 'complete', 'average', 'single'] + if args[2] == 'ward': + assert args[1] == 'euclidean' + elif sw['GSw_HourlyClusterAlgorithm'].startswith('kmedoids'): + args = sw['GSw_HourlyClusterAlgorithm'].split('_') + assert len(args) == 3 + assert args[1] in ['euclidean','l1','l2','manhattan','cosine'] + assert args[2] in ['heuristic','k-medoids++','random','build'] + else: + raise ValueError( + "GSw_HourlyClusterAlgorithm must be set to 'hierarchical', 'optimized', " + "'kmeans', or 'kmedoids', or must " + "contain the substring 'user' and match a scenario in " + "inputs/temporal/period_szn_user.csv" + ) + + if ((sw['GSw_PRM_StressModel'].lower() not in ['pras']) + and ('user' not in sw['GSw_PRM_StressModel'])): + raise ValueError( + "GSw_PRM_StressModel must be set to 'pras' or must " + "contain the substring 'user' and match a scenario at " + "inputs/temporal/stressperiods_{GSw_PRM_StressModel}.csv" + ) + + if (int(sw['GSw_H2_PTC']) == 1) and (int(sw['GSw_H2']) != 2): + raise ValueError( + 'When running with the H2 PTC enabled, GSw_H2 should be set to 2.\n' + f"GSw_H2_PTC={sw['GSw_H2_PTC']}, GSw_H2={sw['GSw_H2']}" + ) + + if int(sw['GSw_H2_SMR']) == 0 and sw['GSw_H2_Demand_Case'] in ['BAU', 'Aggressive', 'Decarb_with_BAU']: + raise ValueError( + f"GSw_H2_SMR is set to 0, but GSw_H2_Demand_Case is set to '{sw['GSw_H2_Demand_Case']}', which requires SMR set to 1.\n" + "When GSw_H2_SMR is 0, GSw_H2_Demand_Case must be one of: 'none', 'Decarb', or 'LTS'." + ) + + if ('usa' not in sw['GSw_Region'].lower()) and (int(sw['GSw_GasCurve']) != 2): + raise ValueError( + 'Should use GSw_GasCurve=2 (fixed prices) when running sub-nationally\n' + f"GSw_Region={sw['GSw_Region']}, GSw_GasCurve={sw['GSw_GasCurve']}" + ) + + if (int(sw['MCS_runs']) > 1 and int(sw['GSw_MGA_RV_runs']) > 1): + raise ValueError( + 'Running Monte Carlo analysis (MCS_runs > 1) and random vector \n' + 'MGA sampling (GSw_MGA_RV_runs > 1) simultaneously is not yet supported.' + ) + + reeds.inputs.validate_zoneset(sw['GSw_ZoneSet']) + + ### Parsed string switches + ## Automatic inputs + hierarchy = reeds.io.get_hierarchy(GSw_ZoneSet=sw['GSw_ZoneSet']).reset_index() + + ### Check that the stress metrics specified in GSw_PRM_StressThresholdMetrics + ### are allowed and have well-formed GSw_PRM_StressThreshold{metric} entries + ra_switches = { + i.lower(): f'GSw_PRM_StressThreshold{i}' + for i in ['Depth', 'Duration', 'LOLD', 'LOLE', 'LOLH', 'NEUE'] + } + used_metrics = [i.lower() for i in sw['GSw_PRM_StressThresholdMetrics'].split('/')] + allowed_levels = ['country','interconnect','nercr','transreg','transgrp','st','r'] + + for metric in used_metrics: + if metric not in ra_switches: + raise NotImplementedError(f"GSw_PRM_StressThresholdMetrics = {metric} is not supported") + + for threshold in sw[ra_switches[metric]].split('/'): + ## Example: GSw_PRM_StressThresholdNEUE = 'transgrp_1' + (hierarchy_level, stress_value) = threshold.split('_') + if hierarchy_level not in allowed_levels: + raise ValueError( + f"{ra_switches[metric]}: level={hierarchy_level} but must be in:\n" + + '\n'.join(allowed_levels) + ) + if not (float(stress_value) >= 0): + raise ValueError( + f"stress value in {ra_switches[metric]} must be a positive number " + f"but '{stress_value}' was provided" + ) + + ### GSw_PRM_UpdateMethod 1-3 (static or PRAS-informed PRM update) is computed from the + ### NEUE-based shortfall, so it requires NEUE to be an active stress metric + if int(sw['GSw_PRM_UpdateMethod']) in [1, 2, 3] and 'neue' not in used_metrics: + raise ValueError( + f"GSw_PRM_UpdateMethod={sw['GSw_PRM_UpdateMethod']} requires 'NEUE' to be included " + f"in GSw_PRM_StressThresholdMetrics (={sw['GSw_PRM_StressThresholdMetrics']}), " + "since PRM updates are computed from the NEUE-based shortfall." + ) + + if sw['GSw_PRM_StressStorageCutoff'].lower() not in ['off','0','false']: + metric, value = sw['GSw_PRM_StressStorageCutoff'].split('_') + if metric.lower()[:3] not in ['eue', 'cap', 'abs']: + raise ValueError( + "The first argument of GSw_PRM_StressStorageCutoff must be in " + f"['eue', 'cap', 'abs'] but {metric} was provided" + ) + try: + float(value) + except ValueError: + raise ValueError( + "The second argument of GSw_PRM_StressStorageCutoff must be a number " + f"but {value} was provided" + ) + if (metric.lower()[:3] == 'abs') and (int(value) != 1): + raise NotImplementedError( + "GSw_PRM_StressStorageCutoff: only abs_1 is implemented for abs but " + f"{metric}_{value} was provided" + ) + + for keyval in sw['GSw_PRM_NetImportLimitScen'].split('/'): + err = ( + "GSw_PRM_NetImportLimitScen accepts inputs in the format " + "{year1}_{'hist' or float}/{year2}_{float}/{year3}_{float} " + "or a single value given as {year1}_{'hist' or float}. Examples are " + "2024_hist/2035_40, 2025_20/2032_40, 2024_hist, 2025_20/2032_40/2050_60. " + f"You entered {sw['GSw_PRM_NetImportLimitScen']}." + ) + year, limit = keyval.split('_') + try: + int(year) + except ValueError: + raise ValueError(err) + if limit not in ['hist', 'histmax']: + try: + float(limit) + 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.\n" + "To iteratively update the PRM, set GSw_PRM_UpdateMethod to an integer between 1-3:" + "\n1: static update set by GSw_PRM_UpdateFraction; " + "\n2: dynamic update informed by PRAS; " + "\n3: dynamic update but only after all new stress periods have been added" + ) + + for bir in sw['GSw_PVB_BIR'].split('_'): + if not (float(bir) >= 0): + raise ValueError("Fix GSw_PVB_BIR") + + for ilr in sw['GSw_PVB_ILR'].split('_'): + if not (float(ilr) >= 0): + raise ValueError("Fix GSw_PVB_ILR") + + for pvbtype in sw['GSw_PVB_Types'].split('_'): + if not (1 <= int(pvbtype) <= 3): + raise ValueError("Fix GSw_PVB_Types") + + try: + prm = float(sw['GSw_PRM_scenario']) + if prm >= 1: + raise Exception( + f"GSw_PRM_scenario={sw['GSw_PRM_scenario']} but should be formatted as a " + "fraction, not a percent" + ) + except ValueError: + pass + + scalars = reeds.io.get_scalars() + ilr_upv = scalars['ilr_utility'] * 100 + + if ( + int(sw['GSw_PVB']) + and not all([np.isclose(float(ilr), ilr_upv) for ilr in sw['GSw_PVB_ILR'].split('_')]) + ): + raise ValueError( + f"GSw_PVB_ILR = {sw['GSw_PVB_ILR']} but all entries must be {int(ilr_upv)}" + ) + + allowed_years = list(range(2007,2014)) + list(range(2016,2024)) + allowed_years_string = ','.join([str(year) for year in allowed_years]) + + resource_adequacy_years = [int(y) for y in sw['resource_adequacy_years'].split('_')] + for year in resource_adequacy_years: + if year not in allowed_years: + raise ValueError( + f"resource_adequacy_years must be in {allowed_years_string} but is " + f"{sw['resource_adequacy_years']}" + ) + + for year in sw['GSw_HourlyWeatherYears'].split('_'): + if int(year) not in allowed_years: + raise ValueError( + f"GSw_HourlyWeatherYears must be in {allowed_years_string} but is " + f"{sw['GSw_HourlyWeatherYears']}" + ) + + if int(year) not in resource_adequacy_years: + raise ValueError( + "GSw_HourlyWeatherYears must be a subset of resource_adequacy_years but " + f"GSw_HourlyWeatherYears={sw['GSw_HourlyWeatherYears']} and " + f"resource_adequacy_years={sw['resource_adequacy_years']}" + ) + + solveyears = reeds.inputs.parse_yearset(sw['yearset']) + if int(sw['endyear']) not in solveyears: + err = f"`endyear` = {sw['endyear']} but must be in `yearset`: {sw['yearset']}" + raise ValueError(err) + + # Add a row for each county + county2zone = reeds.io.get_county2zone(GSw_ZoneSet=sw['GSw_ZoneSet'], as_map=False) + county2zone['county'] = 'p' + county2zone.FIPS + # Add county info to hierarchy + hierarchy = hierarchy.merge(county2zone.drop(columns=['FIPS','state']), on='r') + + # Make sure specified regions are allowed for the specified hierarchy level + region_groups = sw['GSw_Region'].split('//') if '//' in sw['GSw_Region'] else [sw['GSw_Region']] + for group in region_groups: + level, regions = group.split('/') + if level not in hierarchy: + err = ( + f"The specified hierarchy level '{level}' does not exist in the hierarchy file." + f"\nUpdate GSw_Region={sw['GSw_Region']} to specify a valid level." + ) + raise ValueError(err) + invalid_regions = [ + region for region in regions.split('.') + if region.lower() not in hierarchy[level].str.lower().values + ] + if invalid_regions: + err = f"GSw_Region: {', '.join(invalid_regions)} need to be in {hierarchy[level].unique()}" + raise Exception(err) + + ### Compatible switch combinations + if sw['GSw_LoadProfiles'] == 'historic': + if ('demand_' + sw['demandscen'] +'.csv') not in os.listdir(os.path.join(reeds.io.reeds_path, 'inputs','load')) : + raise ValueError("The demand file specified by the demandscen switch is not in the inputs/load folder") + + if ( + re.match(r'(\/|[a-zA-Z]:[\\\/]).+$', sw['GSw_LoadProfiles']) + and not Path(sw['GSw_LoadProfiles']).is_file() + ): + err = f"GSw_LoadProfiles={sw['GSw_LoadProfiles']} but the specified file does not exist" + raise FileNotFoundError(err) + + if sw['GSw_LoadProfiles'].startswith('EER2023'): + allowed_ra_years = range(2007,2014) + if not all([y in allowed_ra_years for y in resource_adequacy_years]): + err = ( + f"GSw_LoadProfiles={sw['GSw_LoadProfiles']} only supports resource_adequacy_years=" + f"{list(allowed_ra_years)} but {resource_adequacy_years} was supplied" + ) + raise ValueError(err) + + if (sw['GSw_MGA_Objective'] not in ['capacity', 'generation']) and int(sw['GSw_MGA_RV_runs']) > 0: + raise NotImplementedError( + f"GSw_MGA_Objective='{sw['GSw_MGA_Objective']}' is not yet supported for MGA random vector sampling." + ) + + ### Dependent model availability + if ( + ((int(sw['pras']) == 2) or int(sw['GSw_PRM_StressIterateMax'])) + and (not os.path.isfile(os.path.join(reeds.io.reeds_path, 'Manifest.toml'))) + ): + err = ( + "Manifest.toml does not exist. " + "Please set up julia by following the instructions at " + "https://reeds-model.github.io/ReEDS/setup.html#reeds2pras-julia-and-stress-periods-setup" + ) + raise Exception(err) + + ### Land use and reeds_to_rev + if (int(sw['land_use_analysis'])) and (not int(sw['reeds_to_rev'])): + raise ValueError( + "'reeds_to_rev' must be enable for land_use analysis to run." + ) + + disallowed_characters = ['~', '|', '*'] + invalid_switches = [ + key for key, val in sw.items() + if any([char in val for char in disallowed_characters]) + ] + if len(invalid_switches) > 0: + raise ValueError( + "The following switches have values with disallowed characters " + f"({', '.join(disallowed_characters)}): {', '.join(invalid_switches)}" + ) + + ### Contents of user-specified files + reeds.checks.check_switches(sw, force=(force>=1)) + + ### Uncommonly used packages + if sw['GSw_HourlyClusterAlgorithm'].lower().startswith('kmedoids'): + if importlib.util.find_spec("sklearn_extra") is None: + err = ( + "The scikit-learn-extra package is required for GSw_HourlyClusterAlgorithm=" + f"{sw['GSw_HourlyClusterAlgorithm']} but is not available in your conda " + "environment. Please install it by running:\n" + " pip install 'scikit-learn-extra>=0.2.0,<0.3.0'" + "\nor:\n" + " conda install -c conda-forge scikit-learn-extra=0.2" + ) + raise ModuleNotFoundError(err) + + def check_GSw_LoadSiteReg(sw): """ Ensure the region entries in loadsite_{GSw_LoadSiteTrajectory} obey the hierarchy diff --git a/reeds/input_processing/mcs_sampler.py b/reeds/input_processing/mcs_sampler.py index 7ccf3b626..dd1456f0d 100644 --- a/reeds/input_processing/mcs_sampler.py +++ b/reeds/input_processing/mcs_sampler.py @@ -1952,7 +1952,7 @@ def main_mga_rv( sampling_regions = hierarchy_val_r[sw['GSw_MGA_RV_region']].unique() ## objective (assumes sw.GSw_MGA_Objective in ['capacity', 'generation'] based on - ## check in runreeds.check_compatibility() + ## check in reeds.checks.check_compatibility() ## if the subobjective is an aggregated category, break it up into leaf-level subcategories ## derived from tech-subset-table.csv; otherwise just use the subobjective as the group subsets = get_mga_rv_subsets(reeds_path, sw.GSw_MGA_SubObjective) @@ -2065,4 +2065,3 @@ def main_mga_rv( process='input_processing/mcs_sampler.py', path=os.path.join(os.path.dirname(inputs_case)) ) - diff --git a/runreeds.py b/runreeds.py index 052c7431a..bb8e515c4 100644 --- a/runreeds.py +++ b/runreeds.py @@ -10,11 +10,9 @@ import time import shutil import csv -import importlib import numpy as np import pandas as pd import subprocess -import re from datetime import datetime import argparse from pathlib import Path @@ -215,338 +213,6 @@ def check_cases_format(df_cases): raise ValueError(err) -def check_compatibility(sw, force=0): - if int(sw['startyear']) != 2010: - raise ValueError(f"startyear = {sw['startyear']} but must be = 2010") - - if int(sw['GSw_SkipRAyear']) <= int(sw['startyear']): - raise ValueError(f"GSw_SkipRAyear = {sw['GSw_SkipRAyear']} but must be > {sw['startyear']}") - - if (sw['GSw_HourlyType'] in ['year']) and int(sw['GSw_InterDayLinkage']): - raise ValueError( - "GSw_HourlyType cannot be 'year' when GSw_InterDayLinkage is enabled. " - f"Current values: GSw_HourlyType={sw['GSw_HourlyType']}, GSw_InterDayLinkage={sw['GSw_InterDayLinkage']}" - ) - - if 24 % (int(sw['GSw_HourlyWindowOverlap']) * int(sw['GSw_HourlyChunkLengthRep'])): - raise ValueError( - ('24 must be divisible by GSw_HourlyWindowOverlap * GSw_HourlyChunkLengthRep:' - '\nGSw_HourlyWindowOverlap = {}\nGSw_HourlyChunkLengthRep = {}'.format( - sw['GSw_HourlyWindowOverlap'], sw['GSw_HourlyChunkLengthRep']))) - - if int(sw['GSw_HourlyWindow']) <= int(sw['GSw_HourlyWindowOverlap']): - raise ValueError( - ('GSw_HourlyWindow must be greater than GSw_HourlyWindowOverlap:' - '\nGSw_HourlyWindow = {}\nGSw_HourlyWindowOverlap = {}'.format( - sw['GSw_HourlyWindow'], sw['GSw_HourlyWindowOverlap']))) - - if ((sw['GSw_HourlyClusterAlgorithm'] not in ['hierarchical','optimized','kmeans','kmedoids']) - and ('user' not in sw['GSw_HourlyClusterAlgorithm']) - ): - if sw['GSw_HourlyClusterAlgorithm'].startswith('hierarchical'): - args = sw['GSw_HourlyClusterAlgorithm'].split('_') - assert len(args) == 3 - ## https://scikit-learn.org/stable/modules/generated/sklearn.cluster.AgglomerativeClustering.html - assert args[1] in ['euclidean','l1','l2','manhattan','cosine'] - assert args[2] in ['ward', 'complete', 'average', 'single'] - if args[2] == 'ward': - assert args[1] == 'euclidean' - elif sw['GSw_HourlyClusterAlgorithm'].startswith('kmedoids'): - args = sw['GSw_HourlyClusterAlgorithm'].split('_') - assert len(args) == 3 - assert args[1] in ['euclidean','l1','l2','manhattan','cosine'] - assert args[2] in ['heuristic','k-medoids++','random','build'] - else: - raise ValueError( - "GSw_HourlyClusterAlgorithm must be set to 'hierarchical', 'optimized', " - "'kmeans', or 'kmedoids', or must " - "contain the substring 'user' and match a scenario in " - "inputs/temporal/period_szn_user.csv" - ) - - if ((sw['GSw_PRM_StressModel'].lower() not in ['pras']) - and ('user' not in sw['GSw_PRM_StressModel'])): - raise ValueError( - "GSw_PRM_StressModel must be set to 'pras' or must " - "contain the substring 'user' and match a scenario at " - "inputs/temporal/stressperiods_{GSw_PRM_StressModel}.csv" - ) - - if (int(sw['GSw_H2_PTC']) == 1) and (int(sw['GSw_H2']) != 2): - raise ValueError( - 'When running with the H2 PTC enabled, GSw_H2 should be set to 2.\n' - f"GSw_H2_PTC={sw['GSw_H2_PTC']}, GSw_H2={sw['GSw_H2']}" - ) - - if int(sw['GSw_H2_SMR']) == 0 and sw['GSw_H2_Demand_Case'] in ['BAU', 'Aggressive', 'Decarb_with_BAU']: - raise ValueError( - f"GSw_H2_SMR is set to 0, but GSw_H2_Demand_Case is set to '{sw['GSw_H2_Demand_Case']}', which requires SMR set to 1.\n" - "When GSw_H2_SMR is 0, GSw_H2_Demand_Case must be one of: 'none', 'Decarb', or 'LTS'." - ) - - if ('usa' not in sw['GSw_Region'].lower()) and (int(sw['GSw_GasCurve']) != 2): - raise ValueError( - 'Should use GSw_GasCurve=2 (fixed prices) when running sub-nationally\n' - f"GSw_Region={sw['GSw_Region']}, GSw_GasCurve={sw['GSw_GasCurve']}" - ) - - if (int(sw['MCS_runs']) > 1 and int(sw['GSw_MGA_RV_runs']) > 1): - raise ValueError( - 'Running Monte Carlo analysis (MCS_runs > 1) and random vector \n' - 'MGA sampling (GSw_MGA_RV_runs > 1) simultaneously is not yet supported.' - ) - - reeds.inputs.validate_zoneset(sw['GSw_ZoneSet']) - - ### Parsed string switches - ## Automatic inputs - reeds_path = os.path.dirname(__file__) - hierarchy = reeds.io.get_hierarchy(GSw_ZoneSet=sw['GSw_ZoneSet']).reset_index() - - ### Check that the stress metrics specified in GSw_PRM_StressThresholdMetrics - ### are allowed and have well-formed GSw_PRM_StressThreshold{metric} entries - ra_switches = { - i.lower(): f'GSw_PRM_StressThreshold{i}' - for i in ['Depth', 'Duration', 'LOLD', 'LOLE', 'LOLH', 'NEUE'] - } - used_metrics = [i.lower() for i in sw['GSw_PRM_StressThresholdMetrics'].split('/')] - allowed_levels = ['country','interconnect','nercr','transreg','transgrp','st','r'] - - for metric in used_metrics: - if metric not in ra_switches: - raise NotImplementedError(f"GSw_PRM_StressThresholdMetrics = {metric} is not supported") - - for threshold in sw[ra_switches[metric]].split('/'): - ## Example: GSw_PRM_StressThresholdNEUE = 'transgrp_1' - (hierarchy_level, stress_value) = threshold.split('_') - if hierarchy_level not in allowed_levels: - raise ValueError( - f"{ra_switches[metric]}: level={hierarchy_level} but must be in:\n" - + '\n'.join(allowed_levels) - ) - if not (float(stress_value) >= 0): - raise ValueError( - f"stress value in {ra_switches[metric]} must be a positive number " - f"but '{stress_value}' was provided" - ) - - ### GSw_PRM_UpdateMethod 1-3 (static or PRAS-informed PRM update) is computed from the - ### NEUE-based shortfall, so it requires NEUE to be an active stress metric - if int(sw['GSw_PRM_UpdateMethod']) in [1, 2, 3] and 'neue' not in used_metrics: - raise ValueError( - f"GSw_PRM_UpdateMethod={sw['GSw_PRM_UpdateMethod']} requires 'NEUE' to be included " - f"in GSw_PRM_StressThresholdMetrics (={sw['GSw_PRM_StressThresholdMetrics']}), " - "since PRM updates are computed from the NEUE-based shortfall." - ) - - if sw['GSw_PRM_StressStorageCutoff'].lower() not in ['off','0','false']: - metric, value = sw['GSw_PRM_StressStorageCutoff'].split('_') - if metric.lower()[:3] not in ['eue', 'cap', 'abs']: - raise ValueError( - "The first argument of GSw_PRM_StressStorageCutoff must be in " - f"['eue', 'cap', 'abs'] but {metric} was provided" - ) - try: - float(value) - except ValueError: - raise ValueError( - "The second argument of GSw_PRM_StressStorageCutoff must be a number " - f"but {value} was provided" - ) - if (metric.lower()[:3] == 'abs') and (int(value) != 1): - raise NotImplementedError( - "GSw_PRM_StressStorageCutoff: only abs_1 is implemented for abs but " - f"{metric}_{value} was provided" - ) - - for keyval in sw['GSw_PRM_NetImportLimitScen'].split('/'): - err = ( - "GSw_PRM_NetImportLimitScen accepts inputs in the format " - "{year1}_{'hist' or float}/{year2}_{float}/{year3}_{float} " - "or a single value given as {year1}_{'hist' or float}. Examples are " - "2024_hist/2035_40, 2025_20/2032_40, 2024_hist, 2025_20/2032_40/2050_60. " - f"You entered {sw['GSw_PRM_NetImportLimitScen']}." - ) - year, limit = keyval.split('_') - try: - int(year) - except ValueError: - raise ValueError(err) - if limit not in ['hist', 'histmax']: - try: - float(limit) - 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.\n" - "To iteratively update the PRM, set GSw_PRM_UpdateMethod to an integer between 1-3:" - "\n1: static update set by GSw_PRM_UpdateFraction; " - "\n2: dynamic update informed by PRAS; " - "\n3: dynamic update but only after all new stress periods have been added" - ) - - for bir in sw['GSw_PVB_BIR'].split('_'): - if not (float(bir) >= 0): - raise ValueError("Fix GSw_PVB_BIR") - - for ilr in sw['GSw_PVB_ILR'].split('_'): - if not (float(ilr) >= 0): - raise ValueError("Fix GSw_PVB_ILR") - - for pvbtype in sw['GSw_PVB_Types'].split('_'): - if not (1 <= int(pvbtype) <= 3): - raise ValueError("Fix GSw_PVB_Types") - - try: - prm = float(sw['GSw_PRM_scenario']) - if prm >= 1: - raise Exception( - f"GSw_PRM_scenario={sw['GSw_PRM_scenario']} but should be formatted as a " - "fraction, not a percent" - ) - except ValueError: - pass - - scalars = reeds.io.get_scalars() - ilr_upv = scalars['ilr_utility'] * 100 - - if ( - int(sw['GSw_PVB']) - and not all([np.isclose(float(ilr), ilr_upv) for ilr in sw['GSw_PVB_ILR'].split('_')]) - ): - raise ValueError( - f"GSw_PVB_ILR = {sw['GSw_PVB_ILR']} but all entries must be {int(ilr_upv)}" - ) - - allowed_years = list(range(2007,2014)) + list(range(2016,2024)) - allowed_years_string = ','.join([str(year) for year in allowed_years]) - - resource_adequacy_years = [int(y) for y in sw['resource_adequacy_years'].split('_')] - for year in resource_adequacy_years: - if year not in allowed_years: - raise ValueError( - f"resource_adequacy_years must be in {allowed_years_string} but is " - f"{sw['resource_adequacy_years']}" - ) - - for year in sw['GSw_HourlyWeatherYears'].split('_'): - if int(year) not in allowed_years: - raise ValueError( - f"GSw_HourlyWeatherYears must be in {allowed_years_string} but is " - f"{sw['GSw_HourlyWeatherYears']}" - ) - - if int(year) not in resource_adequacy_years: - raise ValueError( - "GSw_HourlyWeatherYears must be a subset of resource_adequacy_years but " - f"GSw_HourlyWeatherYears={sw['GSw_HourlyWeatherYears']} and " - f"resource_adequacy_years={sw['resource_adequacy_years']}" - ) - - solveyears = reeds.inputs.parse_yearset(sw['yearset']) - if int(sw['endyear']) not in solveyears: - err = f"`endyear` = {sw['endyear']} but must be in `yearset`: {sw['yearset']}" - raise ValueError(err) - - # Add a row for each county - county2zone = reeds.io.get_county2zone(GSw_ZoneSet=sw['GSw_ZoneSet'], as_map=False) - county2zone['county'] = 'p' + county2zone.FIPS - # Add county info to hierarchy - hierarchy = hierarchy.merge(county2zone.drop(columns=['FIPS','state']), on='r') - - # Make sure specified regions are allowed for the specified hierarchy level - region_groups = sw['GSw_Region'].split('//') if '//' in sw['GSw_Region'] else [sw['GSw_Region']] - for group in region_groups: - level, regions = group.split('/') - if level not in hierarchy: - err = ( - f"The specified hierarchy level '{level}' does not exist in the hierarchy file." - f"\nUpdate GSw_Region={sw['GSw_Region']} to specify a valid level." - ) - raise ValueError(err) - invalid_regions = [ - region for region in regions.split('.') - if region.lower() not in hierarchy[level].str.lower().values - ] - if invalid_regions: - err = f"GSw_Region: {', '.join(invalid_regions)} need to be in {hierarchy[level].unique()}" - raise Exception(err) - - ### Compatible switch combinations - if sw['GSw_LoadProfiles'] == 'historic': - if ('demand_' + sw['demandscen'] +'.csv') not in os.listdir(os.path.join(reeds_path, 'inputs','load')) : - raise ValueError("The demand file specified by the demandscen switch is not in the inputs/load folder") - - if ( - re.match(r'(\/|[a-zA-Z]:[\\\/]).+$', sw['GSw_LoadProfiles']) - and not Path(sw['GSw_LoadProfiles']).is_file() - ): - err = f"GSw_LoadProfiles={sw['GSw_LoadProfiles']} but the specified file does not exist" - raise FileNotFoundError(err) - - if sw['GSw_LoadProfiles'].startswith('EER2023'): - allowed_ra_years = range(2007,2014) - if not all([y in allowed_ra_years for y in resource_adequacy_years]): - err = ( - f"GSw_LoadProfiles={sw['GSw_LoadProfiles']} only supports resource_adequacy_years=" - f"{list(allowed_ra_years)} but {resource_adequacy_years} was supplied" - ) - raise ValueError(err) - - if (sw['GSw_MGA_Objective'] not in ['capacity', 'generation']) and int(sw['GSw_MGA_RV_runs']) > 0: - raise NotImplementedError( - f"GSw_MGA_Objective='{sw['GSw_MGA_Objective']}' is not yet supported for MGA random vector sampling." - ) - - ### Dependent model availability - if ( - ((int(sw['pras']) == 2) or int(sw['GSw_PRM_StressIterateMax'])) - and (not os.path.isfile(os.path.join(reeds_path, 'Manifest.toml'))) - ): - err = ( - "Manifest.toml does not exist. " - "Please set up julia by following the instructions at " - "https://reeds-model.github.io/ReEDS/setup.html#reeds2pras-julia-and-stress-periods-setup" - ) - raise Exception(err) - - ### Land use and reeds_to_rev - if (int(sw['land_use_analysis'])) and (not int(sw['reeds_to_rev'])): - raise ValueError( - "'reeds_to_rev' must be enable for land_use analysis to run." - ) - - disallowed_characters = ['~', '|', '*'] - invalid_switches = [ - key for key, val in sw.items() - if any([char in val for char in disallowed_characters]) - ] - if len(invalid_switches) > 0: - raise ValueError( - "The following switches have values with disallowed characters " - f"({', '.join(disallowed_characters)}): {', '.join(invalid_switches)}" - ) - - ### Contents of user-specified files - reeds.checks.check_switches(sw, force=(force>=1)) - - ### Uncommonly used packages - if sw['GSw_HourlyClusterAlgorithm'].lower().startswith('kmedoids'): - if importlib.util.find_spec("sklearn_extra") is None: - err = ( - "The scikit-learn-extra package is required for GSw_HourlyClusterAlgorithm=" - f"{sw['GSw_HourlyClusterAlgorithm']} but is not available in your conda " - "environment. Please install it by running:\n" - " pip install 'scikit-learn-extra>=0.2.0,<0.3.0'" - "\nor:\n" - " conda install -c conda-forge scikit-learn-extra=0.2" - ) - raise ModuleNotFoundError(err) - - # function to stop the model after input processing def stop_after_input_processing(OPATH, reeds_path, casedir, caseSwitches): comment('Exit after input_processing', OPATH) @@ -888,7 +554,7 @@ def setupEnvironment( #%% Stop now if any switches are incompatible check_cases_format(df_cases) for sw in caseSwitches: - check_compatibility(sw, force=force) + reeds.checks.check_compatibility(sw, force=force) if dryrun: quit() From 4f0d6f6496f25945a5c72b4fdd87ed9602460c48 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:20:13 -0600 Subject: [PATCH 03/11] PR template: add reminder about about switch checks to reeds.checks --- .github/PULL_REQUEST_TEMPLATE.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a8de87f0c..99ed7c725 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -50,6 +50,7 @@ Include additional illustrative plots describing input data, methods, testing, a - postprocessing_tools.md: User/developer-facing description of scripts in postprocessing folder - README.md files: User/developer facing description of individual input folders --> +- [ ] If switches have been added or modified in `cases.csv`, allowed values are specified in the "Choices" column or checked in `reeds/checks.py` - If input data added/modified: - [ ] Dollar year recorded and converted to 2004$ for GAMS - [ ] Timeseries are in Central Time @@ -61,9 +62,7 @@ Include additional illustrative plots describing input data, methods, testing, a - [ ] Map of absolute values before - [ ] Map of absolute values after - [ ] Map of differences: (after - before) or (after / before) - - If entries are added/removed/changed in the EIA-NEMS unit database: - - [ ] Changes have been committed to [ReEDS_Input_Processing](https://github.com/ReEDS-Model/ReEDS_Input_Processing) - - [ ] `hourlize/resource.py` was rerun to regenerate the existing/prescribed VRE capacity data + - [ ] If entries are added/removed/changed in the EIA-NEMS unit database, associated changes have been committed to [ReEDS_Input_Processing](https://github.com/ReEDS-Model/ReEDS_Input_Processing) - [ ] Code formatting standardized - [ ] Reusable functions used where possible instead of copy/pasted code From 3ffaca5e30d01ffb20019d3dcb47105c82a250bc Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:29:36 -0600 Subject: [PATCH 04/11] run check_compatibility() in check_switches() rather than vice versa --- reeds/checks.py | 11 +++++------ runreeds.py | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/reeds/checks.py b/reeds/checks.py index 755e8b39d..a18de133d 100644 --- a/reeds/checks.py +++ b/reeds/checks.py @@ -11,7 +11,7 @@ import reeds -def check_compatibility(sw, force=0): +def check_compatibility(sw): if int(sw['startyear']) != 2010: raise ValueError(f"startyear = {sw['startyear']} but must be = 2010") @@ -325,9 +325,6 @@ def check_compatibility(sw, force=0): f"({', '.join(disallowed_characters)}): {', '.join(invalid_switches)}" ) - ### Contents of user-specified files - reeds.checks.check_switches(sw, force=(force>=1)) - ### Uncommonly used packages if sw['GSw_HourlyClusterAlgorithm'].lower().startswith('kmedoids'): if importlib.util.find_spec("sklearn_extra") is None: @@ -431,7 +428,7 @@ def check_numperiods(sw, threshold=24, force=0): 'for suggestions on how to increase the number of representative periods when ' 'using a small number of regions.\n' 'The simplest approach is to set GSw_HourlyClusteAlgorithm to "hierarchical" ' - f'and GSw_HourlyNumClusters ≥ {threshold}.' + f'and GSw_HourlyNumClusters ≥ {threshold}.\n' ) print(msg) err = f'Insufficient representative periods: {numperiods}' @@ -445,5 +442,7 @@ def check_numperiods(sw, threshold=24, force=0): def check_switches(sw, force=0): """Run all the checks""" - check_GSw_LoadSiteReg(sw) check_numperiods(sw, force=force) + if force < 2: + check_compatibility(sw) + check_GSw_LoadSiteReg(sw) diff --git a/runreeds.py b/runreeds.py index bb8e515c4..1a0c134a1 100644 --- a/runreeds.py +++ b/runreeds.py @@ -554,7 +554,7 @@ def setupEnvironment( #%% Stop now if any switches are incompatible check_cases_format(df_cases) for sw in caseSwitches: - reeds.checks.check_compatibility(sw, force=force) + reeds.checks.check_switches(sw, force=force) if dryrun: quit() From 81ab8a85593ceeb05c294a7e7ae1675bf4e4b74f Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:57:57 -0600 Subject: [PATCH 05/11] cases_test.csv: change Pacific_2020 to NY_365 to show how to run with 365 days --- cases_test.csv | 11 ++++++----- reeds/checks.py | 7 +++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/cases_test.csv b/cases_test.csv index 805d5829d..c84de5c41 100644 --- a/cases_test.csv +++ b/cases_test.csv @@ -1,7 +1,7 @@ -,Default Value,Pacific,USA_defaults,Mid_Case,USA_decarb,github_Pacific,github_Everything,github_MA_county_CC,Pacific_CC,Pacific_weks,Pacific_full_year,Interday_storage,Pacific_2020,Pacific_rep15,WY_county,WECC_county,PJM_county_CC,NYVT_mixed,OR_water,MonteCarlo_Random,MonteCarlo_LHS,Everything,Simple,USA_fast,USA_faster,MultiMetricRA,Pacific_DR,Pacific_MGA,Pacific_MGA_RV,Pacific_LoadSite95,MARICTNYNJPAOH_Offshore,R2P +,Default Value,Pacific,USA_defaults,Mid_Case,USA_decarb,github_Pacific,github_Everything,github_MA_county_CC,Pacific_CC,Pacific_weks,Pacific_full_year,Interday_storage,NY_365,Pacific_rep15,WY_county,WECC_county,PJM_county_CC,NYVT_mixed,OR_water,MonteCarlo_Random,MonteCarlo_LHS,Everything,Simple,USA_fast,USA_faster,MultiMetricRA,Pacific_DR,Pacific_MGA,Pacific_MGA_RV,Pacific_LoadSite95,MARICTNYNJPAOH_Offshore,R2P ignore,1,0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, GSw_Region,cendiv/Pacific,,country/USA,country/USA,country/USA,,st/ID.WY.NE.IA.IL,st/MA,,,,,,,st/WY,interconnect/western,transreg/PJM,st/NY.VT,st/OR,st/NE.NY.PA,st/NE.NY.PA,st/ID.WY.NE.IA.IL,st/KS,country/USA,country/USA,st/NY.NJ,,,,,st/MA.RI.CT.NY.NJ.PA.OH, -endyear,2035,,2050,2050,2050,2030,2060,2030,,,,,,,,,,,2035,2030,2030,2060,2035,2050,2050,2050,,,,,, +endyear,2035,,2050,2050,2050,2030,2060,2030,,,,,2030,,,,,,2035,2030,2030,2060,2035,2050,2050,2050,,,,,, yearset,,,,,,,2010..2060..10,,,,,,,,,,,,,,,2010..2060..10,2010_2015_2020..2050..3,,2010_2025_2050,,,,,,, GSw_ZoneSet,,,,,,,z54,z3109,,,,,,,z3109,z3109,z3109,PJMcounty,,,,z54,z134,z54,z48,,,,,,, GSw_GasCurve,2,,1,1,,,,,,,,,,,,,,,,,,,,1,1,,,,,,, @@ -12,7 +12,7 @@ GSw_TransHurdleRate,,,,1,,,,,,,,,,,,,,,,,,,,,,,,,,,, distpvscen,,,,,stscen2023_mid_case_95_by_2035,,,,,,,,,,,,,,,,,,,,,,,,,,, GSw_AnnualCap,,,,,2,,1,,,,,,,,,,,,,,,1,,,,,,,,,, GSw_AnnualCapScen,,,,,start2024_90pct2035_100pct2045,,start2027_95pct2035,,,,,,,,,,,,,,,start2027_95pct2035,,,,,,,,,, -GSw_LoadProfiles,,,,,EER2025_100by2050,EER2025_IRAlow,EER2025_IRAlow,EER2025_IRAlow,,,,,historic,,,,,,,,,EER2025_100by2050,,,,,historic,,,,, +GSw_LoadProfiles,,,,,EER2025_100by2050,EER2025_IRAlow,EER2025_IRAlow,EER2025_IRAlow,,,,,,,,,,,,,,EER2025_100by2050,,,,,historic,,,,, GSw_NG_CRF_penalty,,,,,ramp_2045,,ramp_2023_2035,,,,,,,,,,,,,,,ramp_2023_2035,,,,,,,,,, GSw_PRM_NetImportLimit,,,,,0,,,,,,,,,,,,,,,,,,,,,,,,,,, GSw_RetirePenalty,,,,,0,,,,,,,,,,,,,,,,,,,,,,,,,,, @@ -22,13 +22,14 @@ GSw_PRM_scenario,,,,,,,,,static,,,,,,,,static,,,,,,,,,,,,,,, GSw_PRM_UpdateMethod,,,,,,,,,1,,,,,,,,,,,,,,,,,,,,,,, GSw_HourlyType,,,,,,,,,,wek,year,,,,,,,,,,,,,,,,,,,,, GSw_InterDayLinkage,,,,,,,,,,,,1,,,,,,,,,,,,,,,,,,,, -GSw_HourlyWeatherYears,,,,,,,2012_2013,,,,,,2020,2007_2008_2009_2010_2011_2012_2013_2016_2017_2018_2019_2020_2021_2022_2023,,,,,,,,2012_2013,,,,,2018,,,,, +GSw_HourlyWeatherYears,,,,,,,2012_2013,,,,,,,2007_2008_2009_2010_2011_2012_2013_2016_2017_2018_2019_2020_2021_2022_2023,,,,,,,,2012_2013,,,,,2018,,,,, GSw_HourlyClusterMapMethod,,,,,,,,,,,,,,bestfirst,,,,,,,,,,,,,,,,,, GSw_WaterCapacity,,,,,,,,,,,,,,,,,,,1,,,,,,,,,,,,, GSw_WaterMain,,,,,,,,,,,,,,,,,,,1,,,,,,,,,,,,, GSw_WaterUse,,,,,,,,,,,,,,,,,,,1,,,,,,,,,,,,, resource_adequacy_years,,,,,,,2011_2012_2013_2021_2022_2023,,,,,,,,,,,,,,,2011_2012_2013_2021_2022_2023,,,,,,,,,, -GSw_HourlyClusterAlgorithm,,,,,,,,,,,,,,,,,,,,user,user,,,,,,,,,,, +GSw_HourlyClusterAlgorithm,,,,,,,,,,,,,hierarchical,,,,,,,user,user,,,,,,,,,,, +GSw_HourlyNumClusters,,,,,,,,,,,,,365,,,,,,,,,,,,,,,,,,, MCS_runs,,,,,,,,,,,,,,,,,,,,2,2,,,,,,,,,,, MCS_dist_groups,,,,,,,,,,,,,,,,,,,,tech.hydro.nuclear.gas.coal.load_country,upv_tri.nuclear_tri.ng_fuel_price_tri.load_country_unif,,,,,,,,,,, MCS_lhs,,,,,,,,,,,,,,,,,,,,,1,,,,,,,,,,, diff --git a/reeds/checks.py b/reeds/checks.py index a18de133d..02188f668 100644 --- a/reeds/checks.py +++ b/reeds/checks.py @@ -380,14 +380,17 @@ def estimate_numperiods(sw): Inputs for testing: sw = { + 'GSw_HourlyType': 'day', 'GSw_ZoneSet': 'z90', 'GSw_Region': 'cendiv/Pacific', 'GSw_HourlyClusterRegionLevel': 'transgrp', 'GSw_HourlyClusterWeights': 'load_1/upv_1/wind-ons_1/wind-ofs_0', } """ - if str(sw['GSw_HourlyClusterAlgorithm']).startswith(('hierarchical','kmeans','kmedoids')): - numperiods = int(sw['GSw_HourlyNumPeriods']) + if sw['GSw_HourlyType'] == 'year': + numperiods = 365 * len(sw['GSw_HourlyWeatherYears'].split('_')) + elif str(sw['GSw_HourlyClusterAlgorithm']).startswith(('hierarchical','kmeans','kmedoids')): + numperiods = int(sw['GSw_HourlyNumClusters']) elif sw['GSw_HourlyClusterAlgorithm'] == 'optimized': zones = reeds.inputs.parse_regions(**sw) hierarchy = reeds.io.assemble_hierarchy(**sw).set_index('r').loc[zones] From dafa5dc21fdc9f57938347f19ade85222ab1a4f0 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 2 Oct 2026 09:23:44 -0600 Subject: [PATCH 06/11] add check_islands() --- reeds/checks.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/reeds/checks.py b/reeds/checks.py index 02188f668..158724293 100644 --- a/reeds/checks.py +++ b/reeds/checks.py @@ -443,9 +443,57 @@ def check_numperiods(sw, threshold=24, force=0): raise ValueError(err) +def check_islands(sw, force=0): + """ + Check if the choice of region resolution results in islanded zones + (which can lead to resource adequacy challenges) or a single zone + (which is not supported by ReEDS2PRAS) + + Inputs for testing: + sw = { + 'GSw_ZoneSet': 'z90', + 'GSw_Region': 'transreg/PJM', + 'pras': '2', + } + """ + zones = reeds.inputs.parse_regions(**sw) + itls = reeds.inputs.get_itls(GSw_ZoneSet=sw['GSw_ZoneSet']) + dfitl = itls.loc[(itls.r.isin(zones)) & (itls.rr.isin(zones))].copy() + islands = [r for r in zones if r not in dfitl[['r','rr']].stack().values] + + if (len(zones) == 1) and int(sw['pras']): + err = ( + f"The system specified by GSw_ZoneSet = {sw['GSw_ZoneSet']} and " + f"GSw_Region = {sw['GSw_Region']} results in a single zone: {zones[0]}.\n" + "ReEDS2PRAS does not support single-zone systems. Please run a larger system." + ) + raise ValueError(err) + + if not len(islands): + return + else: + msg = ( + f"The system specified by GSw_ZoneSet = {sw['GSw_ZoneSet']} and " + f"GSw_Region = {sw['GSw_Region']} results in {len(islands)} islanded zones:\n " + f"{'\n '.join(islands)}\n" + f"This approach may lead to resource adequacy challenges.\n" + f"Consider running a larger system.\n" + f"If you proceed with this system, be sure to check the resource adequacy results." + ) + print(msg) + err = f'{len(islands)} islanded zones: {islands}' + if force: + warn(err) + else: + proceed = str(input(f'Do you want to proceed with {len(islands)} islanded zones? y/[n]')) + if proceed.lower() not in ['y','yes']: + raise ValueError(err) + + def check_switches(sw, force=0): """Run all the checks""" check_numperiods(sw, force=force) + check_islands(sw, force=force) if force < 2: check_compatibility(sw) check_GSw_LoadSiteReg(sw) From d7666ba4d647de96a00a987c74febec439a57515 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:39:26 -0600 Subject: [PATCH 07/11] check_islands(): add exception for ERCOT --- cases_test.csv | 2 +- reeds/checks.py | 26 +++++++++++++++----------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/cases_test.csv b/cases_test.csv index c84de5c41..67d4466e6 100644 --- a/cases_test.csv +++ b/cases_test.csv @@ -1,6 +1,6 @@ ,Default Value,Pacific,USA_defaults,Mid_Case,USA_decarb,github_Pacific,github_Everything,github_MA_county_CC,Pacific_CC,Pacific_weks,Pacific_full_year,Interday_storage,NY_365,Pacific_rep15,WY_county,WECC_county,PJM_county_CC,NYVT_mixed,OR_water,MonteCarlo_Random,MonteCarlo_LHS,Everything,Simple,USA_fast,USA_faster,MultiMetricRA,Pacific_DR,Pacific_MGA,Pacific_MGA_RV,Pacific_LoadSite95,MARICTNYNJPAOH_Offshore,R2P ignore,1,0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -GSw_Region,cendiv/Pacific,,country/USA,country/USA,country/USA,,st/ID.WY.NE.IA.IL,st/MA,,,,,,,st/WY,interconnect/western,transreg/PJM,st/NY.VT,st/OR,st/NE.NY.PA,st/NE.NY.PA,st/ID.WY.NE.IA.IL,st/KS,country/USA,country/USA,st/NY.NJ,,,,,st/MA.RI.CT.NY.NJ.PA.OH, +GSw_Region,cendiv/Pacific,,country/USA,country/USA,country/USA,,st/ID.WY.NE.IA.IL,st/MA,,,,,st/NY,,st/WY,interconnect/western,transreg/PJM,st/NY.VT,st/OR,st/NE.NY.PA,st/NE.NY.PA,st/ID.WY.NE.IA.IL,st/KS,country/USA,country/USA,st/NY.NJ,,,,,st/MA.RI.CT.NY.NJ.PA.OH, endyear,2035,,2050,2050,2050,2030,2060,2030,,,,,2030,,,,,,2035,2030,2030,2060,2035,2050,2050,2050,,,,,, yearset,,,,,,,2010..2060..10,,,,,,,,,,,,,,,2010..2060..10,2010_2015_2020..2050..3,,2010_2025_2050,,,,,,, GSw_ZoneSet,,,,,,,z54,z3109,,,,,,,z3109,z3109,z3109,PJMcounty,,,,z54,z134,z54,z48,,,,,,, diff --git a/reeds/checks.py b/reeds/checks.py index 158724293..387f141d9 100644 --- a/reeds/checks.py +++ b/reeds/checks.py @@ -11,6 +11,9 @@ import reeds +ISLAND_EXCEPTIONS = ['TX', 'ERCOT', 'TX_ERCOT'] + + def check_compatibility(sw): if int(sw['startyear']) != 2010: raise ValueError(f"startyear = {sw['startyear']} but must be = 2010") @@ -423,7 +426,7 @@ def check_numperiods(sw, threshold=24, force=0): return else: msg = ( - 'The estimated number of representative periods for your switch settings is ' + '\nThe estimated number of representative periods for your switch settings is ' f'{numperiods}, which is less than the threshold of {threshold}.\n' 'These conditions should only be used for testing and are not appropriate for ' 'analysis.\nSee ' @@ -459,13 +462,14 @@ def check_islands(sw, force=0): zones = reeds.inputs.parse_regions(**sw) itls = reeds.inputs.get_itls(GSw_ZoneSet=sw['GSw_ZoneSet']) dfitl = itls.loc[(itls.r.isin(zones)) & (itls.rr.isin(zones))].copy() - islands = [r for r in zones if r not in dfitl[['r','rr']].stack().values] + not_islands = dfitl[['r','rr']].stack().unique().tolist() + islands = [r for r in zones if r not in not_islands + ISLAND_EXCEPTIONS] if (len(zones) == 1) and int(sw['pras']): err = ( - f"The system specified by GSw_ZoneSet = {sw['GSw_ZoneSet']} and " - f"GSw_Region = {sw['GSw_Region']} results in a single zone: {zones[0]}.\n" - "ReEDS2PRAS does not support single-zone systems. Please run a larger system." + f'\nThe system specified by GSw_ZoneSet = {sw["GSw_ZoneSet"]} and ' + f'GSw_Region = {sw["GSw_Region"]} results in a single zone: {zones[0]}.\n' + 'ReEDS2PRAS does not support single-zone systems. Please run a larger system.' ) raise ValueError(err) @@ -473,12 +477,12 @@ def check_islands(sw, force=0): return else: msg = ( - f"The system specified by GSw_ZoneSet = {sw['GSw_ZoneSet']} and " - f"GSw_Region = {sw['GSw_Region']} results in {len(islands)} islanded zones:\n " - f"{'\n '.join(islands)}\n" - f"This approach may lead to resource adequacy challenges.\n" - f"Consider running a larger system.\n" - f"If you proceed with this system, be sure to check the resource adequacy results." + f'\nThe system specified by GSw_ZoneSet = {sw["GSw_ZoneSet"]} and ' + f'GSw_Region = {sw["GSw_Region"]} results in {len(islands)} islanded zones:\n ' + f'{"\n ".join(islands)}\n' + f'This approach may lead to resource adequacy challenges.\n' + f'Consider running a larger system.\n' + f'If you proceed with this system, be sure to check the resource adequacy results.' ) print(msg) err = f'{len(islands)} islanded zones: {islands}' From 5edfcb9334ce20257f614e402e8217c8cdcc7518 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:18:23 -0600 Subject: [PATCH 08/11] stress_periods.py: raise error if RA thresholds aren't met after the last iteration --- reeds/resource_adequacy/stress_periods.py | 27 ++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/reeds/resource_adequacy/stress_periods.py b/reeds/resource_adequacy/stress_periods.py index 7f41e0fd0..b46977b5a 100644 --- a/reeds/resource_adequacy/stress_periods.py +++ b/reeds/resource_adequacy/stress_periods.py @@ -3,6 +3,7 @@ import numpy as np import pandas as pd from pathlib import Path +from warnings import warn from typing import Literal import reeds @@ -809,9 +810,8 @@ def main(sw, t, iteration=0, logging=True): """ #%% Write consolidated stress metrics ra_metrics = calc_ra_metrics(case=sw.casedir, t=t, iteration=iteration) - ra_metrics.round(3).to_csv( - os.path.join(sw.casedir, 'outputs', f'ra_metrics_{t}i{iteration}.csv') - ) + ra_metrics_path = Path(sw.casedir, 'outputs', f'ra_metrics_{t}i{iteration}.csv') + ra_metrics.round(3).to_csv(ra_metrics_path) #%% Write EUE events eue_events = get_eue_events(case=sw.casedir, t=t, iteration=iteration) @@ -865,6 +865,27 @@ def main(sw, t, iteration=0, logging=True): os.path.join(sw.casedir, 'inputs_case', newstresspath, 'prm.csv'), ) + #%% Raise an error if it's the final iteration and metrics still exceed the threshold + if ( + ((iteration + 1) == int(sw.GSw_PRM_StressIterateMax)) + and (len(failed)) + ): + err = ( + '\nAt least one resource adequacy metric failed to meet the required ' + f'threshold(s) within {sw.GSw_PRM_StressIterateMax} iterations.\n' + f'Check this file for regional details:\n{ra_metrics_path}\n' + 'Consider:\n' + '- adding more iterations (GSw_PRM_StressIterateMax)\n' + '- increasing the planning reserve margin (GSw_PRM_scenario)\n' + '- relaxing the RA threshold(s) (GSw_PRM_StressThreshold*)\n' + '- revisiting other RA settings (GSw_PRM*)' + '- modeling a larger area (GSw_Region)' + ) + if int(sw['debug']): + warn(err) + else: + raise ValueError(err) + # #%%### Option to run script directly for debugging # if __name__ == '__main__': From 7bfd37c82969232e046a600197158a53bbf7ba6c Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:50:08 -0600 Subject: [PATCH 09/11] skip rep periods check on the github runner --- .github/workflows/python-app.yaml | 2 +- docs/source/user_guide.md | 2 +- reeds/checks.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml index 6b6380675..4706fdec6 100644 --- a/.github/workflows/python-app.yaml +++ b/.github/workflows/python-app.yaml @@ -195,7 +195,7 @@ jobs: - name: Run ReEDS model env: SCENARIO: ${{ matrix.scenario }} - run: python runreeds.py -b "$batch" -c test -s "$SCENARIO" + run: python runreeds.py -b "$batch" -c test -fs "$SCENARIO" - name: Print GAMS log if: runner.debug == '1' diff --git a/docs/source/user_guide.md b/docs/source/user_guide.md index eff586c19..a34d2474b 100644 --- a/docs/source/user_guide.md +++ b/docs/source/user_guide.md @@ -64,7 +64,7 @@ Here is partial list of remotely hosted files used by ReEDS: - So turning up `GSw_HourlyNumClusters` on its own won't increase the temporal coverage. If you want more temporal coverage, the options are: - Switch to `GSw_HourlyType=wek`, which increases the length of the periods from 1 day to 5 days. If all the other switches are left at their defaults, switching to `wek` would increase the coverage from 42 days to 5*42=210 days. - Reduce `GSw_HourlyClusterRegionLevel` to something smaller than transreg (like `st`), and then increase `GSw_HourlyNumClusters` - - Switch to `GSw_HourlyClusteAlgorithm=hierarchical` and then increase `GSw_HourlyNumClusters` (although that's less desirable, because hierarchical clustering doesn't do as good of a job of reproducing the actual spatial distribution of CF and load) + - Switch to `GSw_HourlyClusterAlgorithm=hierarchical` and then increase `GSw_HourlyNumClusters` (although that's less desirable, because hierarchical clustering doesn't do as good of a job of reproducing the actual spatial distribution of CF and load) - Switch to `Gsw_HourlyType=year`. Although if you're running for the whole US you'll need to turn on region aggregation (`GSw_ZoneSet` in [`z54` or `z70`]) for it to solve. - `GSw_HourlyClusterAlgorithm` - If set to 'hierarchical', then hierarchical clustering is used via diff --git a/reeds/checks.py b/reeds/checks.py index 387f141d9..2ddf6e2c3 100644 --- a/reeds/checks.py +++ b/reeds/checks.py @@ -433,7 +433,7 @@ def check_numperiods(sw, threshold=24, force=0): 'https://reeds-model.github.io/ReEDS/user_guide.html#temporal-resolution-switches ' 'for suggestions on how to increase the number of representative periods when ' 'using a small number of regions.\n' - 'The simplest approach is to set GSw_HourlyClusteAlgorithm to "hierarchical" ' + 'The simplest approach is to set GSw_HourlyClusterAlgorithm to "hierarchical" ' f'and GSw_HourlyNumClusters ≥ {threshold}.\n' ) print(msg) From 08d57e2d6fc25bf2948a69d157a3bb7e2a01bc0c Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Wed, 7 Oct 2026 07:51:47 -0600 Subject: [PATCH 10/11] docs: fix ITL figure --- docs/source/figs/docs/transmission-itl-r.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/figs/docs/transmission-itl-r.png b/docs/source/figs/docs/transmission-itl-r.png index 7e3798856..e27c47094 100644 --- a/docs/source/figs/docs/transmission-itl-r.png +++ b/docs/source/figs/docs/transmission-itl-r.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46eaf85112d6ed3e4f601f102bd33e29bc2ef9879cd4ef9ad78bf2ccf1bc3272 -size 639374 +oid sha256:7e2168780b1d38f276c9facdec2086866c0c8b788e412751e4a9d5fb1c9b6b55 +size 838641 From ebfb177f4097852ad5ca034ebca8004d83dcb641 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:34:51 -0600 Subject: [PATCH 11/11] PR template: round csvs to reasonable number of decimals --- .github/PULL_REQUEST_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 99ed7c725..7f6adc813 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -57,6 +57,7 @@ Include additional illustrative plots describing input data, methods, testing, a - [ ] Units are specified - [ ] Preprocessing steps have been documented and committed to [ReEDS_Input_Processing](https://github.com/ReEDS-Model/ReEDS_Input_Processing) - [ ] New large data files handled with .h5 instead of .csv + - [ ] New .csv data files are rounded to a reasonable number of decimal places (≤2 for costs and capacity, ≤6 for lat/lon) - [ ] If new parameters are added to `d_objective.gms`, they are included in `objective_function_params.yaml` for completeness checking - [ ] If spatially resolved inputs are modified, the following visualizations for each file are included in the PR description (time-averaged if the inputs are time-resolved): - [ ] Map of absolute values before