From 3b83e2dba753f3059c0a63b400a5e340f69ecc37 Mon Sep 17 00:00:00 2001 From: Arriaga Gonzalez <63330@icf.com> Date: Fri, 14 Nov 2025 14:32:46 -0800 Subject: [PATCH 1/5] Docs: Updated README.md --- .../Emissions/Off Model Calculators/Readme.md | 95 ++++++++++-------- .../assets/off-model calculator workflow.png | Bin 0 -> 31381 bytes 2 files changed, 54 insertions(+), 41 deletions(-) create mode 100644 utilities/RTP/Emissions/Off Model Calculators/assets/off-model calculator workflow.png diff --git a/utilities/RTP/Emissions/Off Model Calculators/Readme.md b/utilities/RTP/Emissions/Off Model Calculators/Readme.md index 345a4e841..a348ad87e 100644 --- a/utilities/RTP/Emissions/Off Model Calculators/Readme.md +++ b/utilities/RTP/Emissions/Off Model Calculators/Readme.md @@ -1,41 +1,54 @@ -## Off-model calculation - -Travel Model One is not sensitive to the full range of policies MTC and ABAG may choose to pursue in RTP. Marketing and education campaigns, as well as non-capacity-increasing transportation investments likebikeshare programs, are examples of strategies with the potential to change behavior in ways that result in reduced vehicle emissions. Travel Model 1.5 and EMFAC do not estimate reductions in emissions in response to these types of changes in traveler behavior. As such, “off-model” approaches are used to quantify the GHG reduction benefits of these important climate initiatives. - -The off-model calculation process follows Travel Model One run. It contains the following steps, not fully automated. - -### Prepare model data for off-model calculation - -1. Run [trip-distance-by-mode-superdistrict.R](https://github.com/BayAreaMetro/travel-model-one/tree/master/utilities/bespoke-requests/trip-distance-by-mode-superdistrict) to create a super-district trip distance summary, which is not a standard model output. - * run the script on modeling server where the full run data is stored: - ``` - 2050_TM160_DBP_Plan_04/ - core_summaries/ - trip-distance-by-mode-superdistrict.csv - ``` - * copy the output file into M: - ``` - 2050_TM160_DBP_Plan_04/ - OUTPUT/ - bespoke/ - trip-distance-by-mode-superdistrict.csv - ``` - -2. Run the following scripts. Example call: `Rscript BikeInfrastructure.R "X:\travel-model-one-master\utilities\RTP\config_RTP2025\ModelRuns_RTP2025.xlsx" output_dir` - * [BikeInfrastructure.R](BikeInfrastructure.R) - * [Bikeshare.R](Bikeshare.R) - * [Carshare.R](Carshare.R) - * [EmployerShuttles.R](EmployerShuttles.R) (vanpools strategy) - * [TargetedTransportationAlternatives.R](TargetedTransportationAlternatives.R) - -### Create off-model calculation Excel worbooks for a given run set (including a 2035 run and a 2050 run) - -Start with a master version off-model calculation Excel workbook, e.g. `OffModel_Bikeshare.xlsx`. -Run [update_offmodel_calculator_workbooks_with_TM_output.py](update_offmodel_calculator_workbooks_with_TM_output.py) with model_run_id as the arguments to create a copy of the master workbook, fill in the relevant model output data, and update the index in "Main Sheet". - * Example call: `python update_offmodel_calculator_workbooks.py bike_share 2035_TM160_DBP_Plan_04 2050_TM160_DBP_Plan_04`. This creates `OffModel_Bikeshare__2035_TM160_DBP_Plan_04__2050_TM160_DBP_Plan_04.xlsx`. - -### Manually open and save the newly created Excel workbook to trigger the calculation - -### Summarize off-model calculation results - -Once all the off-model calculation Excel workbooks for a set of model run have been created, run [summarize_offmodel_results.py](summarize_offmodel_results.py) to pull data from the Excel workbooks, summarize the results, and create summary tables, which will be automatically loaded into Travel Model Tableau dashboard ([internal link](https://10ay.online.tableau.com/#/site/metropolitantransportationcommission/views/across_RTP2025_runs/EmissionsandOff-model)). +# Off-model calculation +Travel Model One is not sensitive to the full range of policies MTC and ABAG may choose to pursue in RTP. Marketing and education campaigns, as well as non-capacity-increasing transportation investments like bike share programs, are examples of strategies with the potential to change behavior in ways that result in reduced vehicle emissions. Travel Model 1.5 and EMFAC do not estimate reductions in emissions in response to these types of changes in traveler behavior. As such, “off-model” approaches are used to quantify the GHG reduction benefits of these important climate initiatives. + +## Current Off-Model Calculators +In Plan Bay Area 2050, MTC/ABAG conducted off-model analysis to evaluate the GHG impacts of initiatives that could not be captured in the regional travel model. These initiatives constituted most of the key subcomponents of Strategy EN8: Expand Clean Vehicle Initiatives and Strategy EN9: Expand Transportation Demand Management Initiatives: + +- Strategy EN8: Initiative EN8a – Regional Electric Vehicle Chargers +- Strategy EN8: Initiative EN8b – Vehicle Buyback Program +- Strategy EN8: Initiative EN8c – Electric Bicycle Rebate Program - NEW +- Strategy EN9: Initiative EN9a – Bike Share +- Strategy EN9: Initiative EN9b – Car Share +- Strategy EN9: Initiative EN9c – Targeted Transportation Alternatives +- Strategy EN9: Initiative EN9d – Vanpools + + +The off-model calculation process follows Travel Model One run. It contains the following steps. The process is fully automated. + +## Latest Updates +- **Full automation of the process:** pulling from the RTP config files to summarizing outputs ready to be used in a Tableau dashboard. +- **Simplification of the output tab:** the ‘Output’ tab of each calculator will no longer append a running history of the calculator ‘system state’ – only the current run – instead, this duty of storing a running history will be taken on by Tableau Online (eventually). +- **Added to 'Variable_locations.csv':** the csv has now two columns, pointing the automated scripts to the appropriate cell locations based on whether the Travel Model run is a year 2035 or 2050 run. + +# General Process Workflow +Below is the general process workflow triggered when running the main script `update_offmodel_calculator_workbooks_with_TM_output.py`: + +![Off-model calculator workflow](assets/off-model%20calculator%20workflow.png) + +## Identify model run +The program reads the `ModelRuns_RTP2025.xlsx` file located in `utilities\RTP\config_RTP2025` and filters its data based on the condition that the column J, or Off-Model Calculator, has a "Y". + +## Get model data +The program then gets the model data belonging to the model run (e.g. 2050_TM160_DBP_Plan_04) which is required in the off-model calculator (when applicable) by creating a bespoke request. + +The program gets the model data from two R scripts, which run automatically: + +## Get SB 375 calculation +The SB 375 tab in each calculator now lists 2005 data. There is a `SB375.csv` which holds 2005 data and will be updated manually as needed. This value is pushed automatically to each calculator. + +The 2005 change is only triggered by a Travel Model update, therefore, its considered a type of "off-model calculator update." When a new version of the Travel Model exists, it would recalculate 2005 and create a new version of the off-model calculators. + +## Calculator updates +Each Excel Off-model calculator is updated with the following data: +- Model run +- Year +- Model data +- SB 375 data +- Variable locations + +Note: the variable locations is a file that specifies all variables used in each calculator and their corresponding cell locations in Excel format (e.g. A1). + +Then, the results are saved in new excel workbooks. + +## Results from the run +The program then stores the outputs of the Excel workbooks into a centralized logger in the server. It also creates a summarized table with the outputs of each calculator, which will be automatically loaded into the Travel Model Tableau dashboard ([internal link](https://10ay.online.tableau.com/#/site/metropolitantransportationcommission/views/across_RTP2025_runs/EmissionsandOff-model)). \ No newline at end of file diff --git a/utilities/RTP/Emissions/Off Model Calculators/assets/off-model calculator workflow.png b/utilities/RTP/Emissions/Off Model Calculators/assets/off-model calculator workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..45e41301faa2391d09104c1f2fd097e0f70c2927 GIT binary patch literal 31381 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 Date: Fri, 5 Dec 2025 14:58:26 -0800 Subject: [PATCH 2/5] Refactor: Eliminated the need of external variable location csv --- .../Off Model Calculators/helper/calcs.py | 52 ++++++++++++++----- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/utilities/RTP/Emissions/Off Model Calculators/helper/calcs.py b/utilities/RTP/Emissions/Off Model Calculators/helper/calcs.py index 2ccec33be..a2e248cee 100644 --- a/utilities/RTP/Emissions/Off Model Calculators/helper/calcs.py +++ b/utilities/RTP/Emissions/Off Model Calculators/helper/calcs.py @@ -20,8 +20,7 @@ class OffModelCalculator: masterWbName: string, name of offModelCalculator of interest (e.g. bikeshare) dataFileName: string, name of model data file (input). verbose: print each method calculations. - varsDir: master file with all variable locations in all OffModelCalculators. - v: dictionary, all variable names and values for the OffModelCalculator chosen. + v: dictionary, all variable names and locations for the OffModelCalculator chosen. """ def __init__(self, model_run_id, uid, verbose=False): @@ -32,7 +31,6 @@ def __init__(self, model_run_id, uid, verbose=False): self.dataFileName="" # self.masterLogPath=common.get_master_log_path() self.verbose=verbose - self.varsDir=common.get_vars_directory() def copy_workbook(self): # Start run @@ -95,17 +93,47 @@ def write_model_data_to_excel(self, data): OffModelCalculator.write_sbdata_to_excel(self) def get_variable_locations(self): + """ + Read variable locations from the Output_test tab in the master workbook. + Handles both year-specific variables (e.g., variable_2035) and year-agnostic variables. + """ + # Get year from run ID (first 4 characters) + year = self.runs[:4] - # allVars=pd.read_excel(self.varsDir) - allVars=pd.read_csv(self.varsDir) - calcVars=allVars.loc[allVars.Workbook.isin([self.masterWbName])] - calcVars=calcVars[['Sheet', 'Variable Name', 'Location_{}'.format(self.runs[:4])]] - calcVars.rename(columns={'Location_{}'.format(self.runs[:4]): 'Location'}, inplace=True) - groups=set(calcVars.Sheet) - self.v={} + # Read Output_test tab from master workbook + allVars = pd.read_excel(self.master_workbook_file, sheet_name='Output_test') + + # Select relevant columns: Sheet, Variable Name, Location + allVars = allVars[['Sheet', 'Variable Name', 'Location']].copy() + + # Split variables into two groups: + # 1. Variables with year suffix (e.g., variable_2035) + # 2. Variables without year suffix (year-agnostic) + + year_suffix = f'_{year}' + + # Get variables WITH year suffix for this specific year + vars_with_year = allVars[allVars['Variable Name'].str.endswith(year_suffix)].copy() + # Remove year suffix from these variables + vars_with_year['Variable Name'] = vars_with_year['Variable Name'].str.replace(year_suffix, '', regex=False) + + # Get variables WITHOUT any year suffix (year-agnostic) + # A variable is year-agnostic if it doesn't end with _YYYY pattern + vars_without_year = allVars[~allVars['Variable Name'].str.match(r'.*_\d{4}$')].copy() + + # Combine both sets of variables + calcVars = pd.concat([vars_with_year, vars_without_year], ignore_index=True) + + # Remove duplicates (keep first occurrence) + calcVars = calcVars.drop_duplicates(subset=['Sheet', 'Variable Name'], keep='first') + + # Group by sheet and create dictionary + groups = set(calcVars['Sheet']) + self.v = {} for group in groups: - self.v.setdefault(group,dict()) - self.v[group]=dict(zip(calcVars['Variable Name'],calcVars['Location'])) + self.v.setdefault(group, dict()) + group_vars = calcVars[calcVars['Sheet'] == group] + self.v[group] = dict(zip(group_vars['Variable Name'], group_vars['Location'])) if self.verbose: print("Calculator variables and locations in Excel:") From 5fc0ae127896cef7ea77932fda9986d4756c6d4e Mon Sep 17 00:00:00 2001 From: Arriaga Gonzalez <63330@icf.com> Date: Fri, 5 Dec 2025 15:02:53 -0800 Subject: [PATCH 3/5] Refactor: Handles vertical data in output tabs (Excels) --- .../extract_offmodel_results.py | 64 +++++++++++++++++-- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/utilities/RTP/Emissions/Off Model Calculators/extract_offmodel_results.py b/utilities/RTP/Emissions/Off Model Calculators/extract_offmodel_results.py index e3e9aea1e..bf29a85f0 100644 --- a/utilities/RTP/Emissions/Off Model Calculators/extract_offmodel_results.py +++ b/utilities/RTP/Emissions/Off Model Calculators/extract_offmodel_results.py @@ -58,13 +58,63 @@ def get_runs_with_off_model(modelruns_xlsx): modelruns_with_off_model_FBP = modelruns_df.loc[modelruns_df['run_offmodel']=='FBP'] return modelruns_with_off_model_IP, modelruns_with_off_model_FBP +def read_output_data(off_model_wb, run_id, output_tab_name='Output_test'): + """ + Read output data from Excel workbook. + Supports vertical format (Output_test/Output tab with Sheet, Variable Name, Value, Location columns). + Returns DataFrame in horizontal format for compatibility with existing pipeline. + + Args: + off_model_wb: Path to Excel workbook + run_id: Model run ID (e.g., '2035_TM160_IPA_16') + output_tab_name: Name of output tab to read from (default: 'Output_test') + + Returns: + DataFrame with columns: Horizon Run ID, Out_daily_GHG_reduced_{year}, Out_per_capita_GHG_reduced_{year} + """ + year = run_id[:4] + + # Read vertical format from Output tab + df = pd.read_excel(off_model_wb, sheet_name=output_tab_name) + print("Read first 15 rows from {} tab:\n{}".format(output_tab_name, df.head(15))) + print("") + # Filter for output variables (those starting with 'Out_') + # In the Output_test tab, output variables are identified by their variable name, not by Sheet column + output_vars = df[df['Variable Name'].str.startswith('Out_', na=False)].copy() + + # Get year-specific variables + year_suffix = f'_{year}' + output_vars_year = output_vars[output_vars['Variable Name'].str.endswith(year_suffix)] + + # Create lookup dictionary: variable_name -> value + var_lookup = dict(zip(output_vars_year['Variable Name'], output_vars_year['Value'])) + + # Extract specific variables + daily_ghg_key = f'Out_daily_GHG_reduced_{year}' + per_capita_ghg_key = f'Out_per_capita_GHG_reduced_{year}' + + # Reshape to horizontal format (same structure as old Output tab) + result_df = pd.DataFrame({ + 'Horizon Run ID': [run_id], + f'Out_daily_GHG_reduced_{year}': [var_lookup.get(daily_ghg_key, None)], + f'Out_per_capita_GHG_reduced_{year}': [var_lookup.get(per_capita_ghg_key, None)] + }) + + # Log warnings if variables are missing + if daily_ghg_key not in var_lookup: + LOGGER.warning(f'Variable {daily_ghg_key} not found in {output_tab_name} tab') + if per_capita_ghg_key not in var_lookup: + LOGGER.warning(f'Variable {per_capita_ghg_key} not found in {output_tab_name} tab') + + return result_df + def extract_off_model_calculator_result(run_directory, run_id, calculator_name): """ Extract the result from one off-model calculator workbook. """ off_model_output_dir = os.path.join( run_directory, 'OUTPUT', 'offmodel', 'offmodel_output') - + # for calculator_name in calculator_names: off_model_wb = os.path.join(off_model_output_dir, 'PBA50+_OffModel_{}__{}.xlsx'.format(calculator_name, run_id)) @@ -73,12 +123,12 @@ def extract_off_model_calculator_result(run_directory, run_id, calculator_name): return None else: refresh_excelworkbook(off_model_wb) - off_model_df = pd.read_excel(off_model_wb, sheet_name='Output', skiprows=1) - off_model_df = off_model_df[[ - 'Horizon Run ID', - # 'Out_daily_VMT_reduced_{}'.format(run_id[:4]), - 'Out_daily_GHG_reduced_{}'.format(run_id[:4]), - 'Out_per_capita_GHG_reduced_{}'.format(run_id[:4])]] + + # Read from vertical format Output_test tab (will be renamed to 'Output' after migration) + # TODO: Change 'Output_test' to 'Output' after Excel tab is renamed + off_model_df = read_output_data(off_model_wb, run_id, output_tab_name='Output_test') + + # Rename columns to match expected format off_model_df.rename( columns={'Horizon Run ID': 'directory', 'Out_daily_GHG_reduced_{}'.format(run_id[:4]): 'daily_ghg_reduction', From d80d9e5413eb253f8eb6e4720702dc4eacc09862 Mon Sep 17 00:00:00 2001 From: Arriaga Gonzalez <63330@icf.com> Date: Fri, 5 Dec 2025 15:04:58 -0800 Subject: [PATCH 4/5] Tests: Added tests to check correct handling of new I/O --- .../tests/test_config.py | 29 ++ .../tests/test_extract_refactor.py | 167 +++++++++++ .../tests/test_new_variable_locations.py | 271 ++++++++++++++++++ .../tests/test_variable_locations.py | 204 +++++++++++++ .../tests/validate_output_tabs.py | 207 +++++++++++++ .../tests/verify_excel_structure.py | 50 ++++ 6 files changed, 928 insertions(+) create mode 100644 utilities/RTP/Emissions/Off Model Calculators/tests/test_config.py create mode 100644 utilities/RTP/Emissions/Off Model Calculators/tests/test_extract_refactor.py create mode 100644 utilities/RTP/Emissions/Off Model Calculators/tests/test_new_variable_locations.py create mode 100644 utilities/RTP/Emissions/Off Model Calculators/tests/test_variable_locations.py create mode 100644 utilities/RTP/Emissions/Off Model Calculators/tests/validate_output_tabs.py create mode 100644 utilities/RTP/Emissions/Off Model Calculators/tests/verify_excel_structure.py diff --git a/utilities/RTP/Emissions/Off Model Calculators/tests/test_config.py b/utilities/RTP/Emissions/Off Model Calculators/tests/test_config.py new file mode 100644 index 000000000..a255fd770 --- /dev/null +++ b/utilities/RTP/Emissions/Off Model Calculators/tests/test_config.py @@ -0,0 +1,29 @@ +""" +Configuration file for testing off-model calculator functions locally. +Update these paths to point to your local test data files. +""" + +import os + +# Base directory for test data +TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), 'test_data') + +# Path to Variable_locations.csv file +# Update this to point to your local copy of the Variable_locations.csv file +VARIABLE_LOCATIONS_CSV = os.path.join(TEST_DATA_DIR, 'Variable_locations.csv') + +# Example master workbook name (bike_share, car_share, etc.) +TEST_WORKBOOK_NAME = 'PBA50+_OffModel_Bikeshare' + +# Example model run ID (format: YYYY_TMXXX_RunName) +TEST_MODEL_RUN_ID = '2035_TM160_IPA_16' + +# Path to test Excel workbook +# For testing with actual file, you can also use the models directory: +# TEST_EXCEL_WORKBOOK = os.path.join(os.path.dirname(__file__), '..', 'models', f'{TEST_WORKBOOK_NAME}.xlsx') +TEST_EXCEL_WORKBOOK = os.path.join(TEST_DATA_DIR, f'{TEST_WORKBOOK_NAME}.xlsx') + +# Create test_data directory if it doesn't exist +if not os.path.exists(TEST_DATA_DIR): + os.makedirs(TEST_DATA_DIR) + print(f"Created test data directory: {TEST_DATA_DIR}") diff --git a/utilities/RTP/Emissions/Off Model Calculators/tests/test_extract_refactor.py b/utilities/RTP/Emissions/Off Model Calculators/tests/test_extract_refactor.py new file mode 100644 index 000000000..4c9ce084a --- /dev/null +++ b/utilities/RTP/Emissions/Off Model Calculators/tests/test_extract_refactor.py @@ -0,0 +1,167 @@ +""" +Test the refactored extract_offmodel_results.py with vertical format support. +""" + +import sys +import os +import pandas as pd + +# Add parent directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +# Import the function to test +from extract_offmodel_results import read_output_data + +def test_read_output_data(): + """ + Test the read_output_data function with a sample Excel file. + """ + print("=" * 80) + print("Testing read_output_data() function") + print("=" * 80) + + # Path to test Excel file + script_dir = os.path.dirname(os.path.abspath(__file__)) + excel_file = os.path.join(script_dir, 'test_data', 'PBA50+_OffModel_Bikeshare.xlsx') + + if not os.path.exists(excel_file): + print(f"\n❌ ERROR: Test file not found: {excel_file}") + return False + + # Test parameters + run_id = '2035_TM160_IPA_16' + year = run_id[:4] + + print(f"\nTest Configuration:") + print(f" Excel File: {excel_file}") + print(f" Run ID: {run_id}") + print(f" Year: {year}") + print(f" Reading from: Output_test tab") + + try: + # Call the function + print("\n1. Calling read_output_data()...") + result_df = read_output_data(excel_file, run_id, output_tab_name='Output_test') + + print("\n2. Result DataFrame:") + print("-" * 80) + print(result_df) + print("-" * 80) + + # Validate structure + print("\n3. Validation:") + expected_columns = [ + 'Horizon Run ID', + f'Out_daily_GHG_reduced_{year}', + f'Out_per_capita_GHG_reduced_{year}' + ] + + print(f" Expected columns: {expected_columns}") + print(f" Actual columns: {list(result_df.columns)}") + + all_columns_present = all(col in result_df.columns for col in expected_columns) + + if all_columns_present: + print(" ✓ All expected columns present") + else: + print(" ❌ Missing columns!") + return False + + # Check values + print("\n4. Values:") + print(f" Horizon Run ID: {result_df['Horizon Run ID'].iloc[0]}") + print(f" Out_daily_GHG_reduced_{year}: {result_df[f'Out_daily_GHG_reduced_{year}'].iloc[0]}") + print(f" Out_per_capita_GHG_reduced_{year}: {result_df[f'Out_per_capita_GHG_reduced_{year}'].iloc[0]}") + + # Check for NaN values + has_nan = result_df.isna().any().any() + if has_nan: + print("\n ❌ ERROR: Some values are NaN (variables are missing!)") + print(f" NaN columns: {result_df.columns[result_df.isna().any()].tolist()}") + print("\n" + "=" * 80) + print("❌ TEST FAILED: Variables not found in Output_test tab") + print("=" * 80) + return False + else: + print("\n ✓ No NaN values - all variables found") + + print("\n" + "=" * 80) + print("✓ TEST PASSED: Function works correctly with vertical format") + print("=" * 80) + return True + + except Exception as e: + print(f"\n❌ ERROR: {e}") + import traceback + traceback.print_exc() + print("\n" + "=" * 80) + print("❌ TEST FAILED") + print("=" * 80) + return False + +def test_transformation(): + """ + Test that vertical → horizontal transformation works correctly. + """ + print("\n" + "=" * 80) + print("Testing Vertical → Horizontal Transformation") + print("=" * 80) + + script_dir = os.path.dirname(os.path.abspath(__file__)) + excel_file = os.path.join(script_dir, 'test_data', 'PBA50+_OffModel_Bikeshare.xlsx') + + if not os.path.exists(excel_file): + print(f"\n❌ ERROR: Test file not found") + return False + + try: + # Show vertical format + print("\n1. VERTICAL FORMAT (Input - Output_test tab):") + print("-" * 80) + df_vertical = pd.read_excel(excel_file, sheet_name='Output_test') + output_vars = df_vertical[df_vertical['Sheet'] == 'Output'] + print(output_vars[['Sheet', 'Variable Name', 'Value']].head(10).to_string()) + + # Show horizontal format + print("\n2. HORIZONTAL FORMAT (Output - after transformation):") + print("-" * 80) + df_horizontal = read_output_data(excel_file, '2035_TM160_IPA_16', output_tab_name='Output_test') + print(df_horizontal.to_string()) + + print("\n3. Transformation Summary:") + print(f" Vertical rows (Output sheet only): {len(output_vars)}") + print(f" Horizontal rows: {len(df_horizontal)}") + print(f" Horizontal columns: {len(df_horizontal.columns)}") + + print("\n✓ Transformation works - vertical data converted to horizontal format") + return True + + except Exception as e: + print(f"\n❌ ERROR: {e}") + import traceback + traceback.print_exc() + return False + +if __name__ == '__main__': + print("\nRefactored extract_offmodel_results.py Test Suite") + print("=" * 80) + + result1 = test_read_output_data() + result2 = test_transformation() + + print("\n" + "=" * 80) + print("TEST SUMMARY") + print("=" * 80) + print(f"read_output_data() test: {'✓ PASSED' if result1 else '❌ FAILED'}") + print(f"Transformation test: {'✓ PASSED' if result2 else '❌ FAILED'}") + + if result1 and result2: + print("\n✓✓✓ ALL TESTS PASSED ✓✓✓") + print("\nThe refactored extraction script is ready to use!") + print("\nNext steps:") + print("1. Test with actual workbook files") + print("2. After testing, change 'Output_test' to 'Output' in line 127") + sys.exit(0) + else: + print("\n❌❌❌ SOME TESTS FAILED ❌❌❌") + sys.exit(1) diff --git a/utilities/RTP/Emissions/Off Model Calculators/tests/test_new_variable_locations.py b/utilities/RTP/Emissions/Off Model Calculators/tests/test_new_variable_locations.py new file mode 100644 index 000000000..136033adb --- /dev/null +++ b/utilities/RTP/Emissions/Off Model Calculators/tests/test_new_variable_locations.py @@ -0,0 +1,271 @@ +""" +Test script for the updated get_variable_locations() method. +This tests reading from Output_test tab in Excel instead of Variable_locations.csv. +""" + +import sys +import os +import pandas as pd + +# Add parent directory to path to import helper modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +import test_config + +def test_new_get_variable_locations(): + """ + Test the new get_variable_locations() logic that reads from Output_test tab. + This simulates what the updated method does. + """ + + print("=" * 80) + print("Testing NEW get_variable_locations() - Reading from Excel Output_test tab") + print("=" * 80) + + excel_file = test_config.TEST_EXCEL_WORKBOOK + runs = test_config.TEST_MODEL_RUN_ID + + # Check if Excel file exists + if not os.path.exists(excel_file): + print(f"\nERROR: Excel workbook not found at:") + print(f" {excel_file}") + print("\nPlease update test_config.TEST_EXCEL_WORKBOOK with path to your Excel file.") + return None + + print(f"\n1. Reading from Excel workbook:") + print(f" File: {excel_file}") + print(f" Sheet: Output_test") + print(f" Model Run: {runs}") + + # Get year from run ID (first 4 characters) + year = runs[:4] + print(f" Year extracted: {year}") + + try: + # Read Output_test tab from Excel + allVars = pd.read_excel(excel_file, sheet_name='Output_test') + print(f"\n2. Total rows in Output_test: {len(allVars)}") + print(f" Columns: {list(allVars.columns)}") + + # Show sample of all data + print(f"\n3. Sample of Output_test (first 10 rows):") + print(allVars.head(10).to_string()) + + # Select relevant columns first + allVars = allVars[['Sheet', 'Variable Name', 'Location']].copy() + + # Split variables into two groups: + # 1. Variables with year suffix (e.g., variable_2035) + # 2. Variables without year suffix (year-agnostic) + + year_suffix = f'_{year}' + print(f"\n4. Processing variables:") + print(f" Year suffix to filter: '{year_suffix}'") + + # Get variables WITH year suffix for this specific year + vars_with_year = allVars[allVars['Variable Name'].str.endswith(year_suffix)].copy() + print(f" Variables WITH year suffix ({year}): {len(vars_with_year)}") + + # Get variables WITHOUT any year suffix (year-agnostic) + # A variable is year-agnostic if it doesn't end with _YYYY pattern + vars_without_year = allVars[~allVars['Variable Name'].str.match(r'.*_\d{4}$')].copy() + print(f" Variables WITHOUT year suffix (year-agnostic): {len(vars_without_year)}") + + if len(vars_with_year) == 0 and len(vars_without_year) == 0: + print(f"\n WARNING: No variables found!") + return None + + print(f"\n5. Variables WITH year suffix (before removing suffix):") + print(vars_with_year.head(10).to_string()) + + # Remove year suffix from year-specific variables + vars_with_year['Variable Name'] = vars_with_year['Variable Name'].str.replace(year_suffix, '', regex=False) + + print(f"\n6. Variables WITHOUT year suffix (year-agnostic):") + print(vars_without_year.to_string()) + + # Combine both sets of variables + calcVars = pd.concat([vars_with_year, vars_without_year], ignore_index=True) + + print(f"\n7. Combined variables (total: {len(calcVars)}):") + print(calcVars.to_string()) + + # Check for duplicates + duplicates = calcVars[calcVars.duplicated(subset=['Sheet', 'Variable Name'], keep=False)] + if len(duplicates) > 0: + print(f"\n ⚠ WARNING: Found {len(duplicates)} duplicate variable entries:") + print(duplicates.sort_values(['Sheet', 'Variable Name']).to_string()) + print("\n Removing duplicates (keeping first occurrence)...") + calcVars = calcVars.drop_duplicates(subset=['Sheet', 'Variable Name'], keep='first') + + # Group by sheet + groups = set(calcVars['Sheet']) + print(f"\n8. Sheets found: {groups} (after deduplication)") + + # Create dictionary structure + v = {} + for group in groups: + v.setdefault(group, dict()) + group_vars = calcVars[calcVars['Sheet'] == group] + v[group] = dict(zip(group_vars['Variable Name'], group_vars['Location'])) + + print(f"\n9. Final dictionary structure (v):") + print("-" * 80) + for sheet, variables in v.items(): + print(f"\nSheet: '{sheet}'") + for var_name, location in variables.items(): + print(f" {var_name:40s} -> {location}") + + print("\n" + "=" * 80) + print("Test completed successfully!") + print("=" * 80) + print("\nKey changes from CSV method:") + print(" ✓ Reads from Excel Output_test tab (not external CSV)") + print(" ✓ Filters by year suffix in variable name (not column name)") + print(" ✓ Removes year suffix for clean variable names") + print(" ✓ No dependency on external Variable_locations.csv file") + + return v + + except Exception as e: + print(f"\n❌ ERROR: {str(e)}") + import traceback + traceback.print_exc() + return None + + +def compare_csv_vs_excel(): + """ + Compare results from old CSV method vs new Excel method. + """ + print("\n" + "=" * 80) + print("COMPARISON: CSV method vs Excel method") + print("=" * 80) + + # Test old method (CSV) + csv_path = test_config.VARIABLE_LOCATIONS_CSV + excel_path = test_config.TEST_EXCEL_WORKBOOK + runs = test_config.TEST_MODEL_RUN_ID + workbook_name = test_config.TEST_WORKBOOK_NAME + year = runs[:4] + + csv_result = None + excel_result = None + + # Try old CSV method + if os.path.exists(csv_path): + print("\n1. OLD METHOD (CSV):") + print(f" Reading from: {csv_path}") + try: + allVars = pd.read_csv(csv_path) + calcVars = allVars.loc[allVars['Workbook'].isin([workbook_name])] + location_col = f'Location_{year}' + if location_col in calcVars.columns: + calcVars = calcVars[['Sheet', 'Variable Name', location_col]].copy() + calcVars.rename(columns={location_col: 'Location'}, inplace=True) + + csv_result = {} + for sheet in set(calcVars['Sheet']): + sheet_vars = calcVars[calcVars['Sheet'] == sheet] + csv_result[sheet] = dict(zip(sheet_vars['Variable Name'], sheet_vars['Location'])) + + print(f" ✓ Found {len(calcVars)} variables") + else: + print(f" ✗ Column '{location_col}' not found") + except Exception as e: + print(f" ✗ Error: {e}") + else: + print(f"\n1. OLD METHOD (CSV): Skipped (file not found)") + + # Try new Excel method + if os.path.exists(excel_path): + print("\n2. NEW METHOD (Excel):") + print(f" Reading from: {excel_path} -> Output_test") + try: + allVars = pd.read_excel(excel_path, sheet_name='Output_test') + allVars = allVars[['Sheet', 'Variable Name', 'Location']].copy() + + year_suffix = f'_{year}' + + # Get variables WITH year suffix for this specific year + vars_with_year = allVars[allVars['Variable Name'].str.endswith(year_suffix)].copy() + vars_with_year['Variable Name'] = vars_with_year['Variable Name'].str.replace(year_suffix, '', regex=False) + + # Get variables WITHOUT any year suffix (year-agnostic) + vars_without_year = allVars[~allVars['Variable Name'].str.match(r'.*_\d{4}$')].copy() + + # Combine both sets + calcVars = pd.concat([vars_with_year, vars_without_year], ignore_index=True) + + # Remove duplicates (keep first occurrence) + calcVars = calcVars.drop_duplicates(subset=['Sheet', 'Variable Name'], keep='first') + + excel_result = {} + for sheet in set(calcVars['Sheet']): + sheet_vars = calcVars[calcVars['Sheet'] == sheet] + excel_result[sheet] = dict(zip(sheet_vars['Variable Name'], sheet_vars['Location'])) + + print(f" ✓ Found {len(calcVars)} unique variables ({len(vars_with_year)} with year, {len(vars_without_year)} year-agnostic)") + except Exception as e: + print(f" ✗ Error: {e}") + else: + print(f"\n2. NEW METHOD (Excel): Skipped (file not found)") + + # Compare + if csv_result and excel_result: + print("\n3. COMPARISON:") + + # Get all unique sheet names + all_sheets = set(list(csv_result.keys()) + list(excel_result.keys())) + + for sheet_name in sorted(all_sheets): + csv_vars = csv_result.get(sheet_name, {}) + excel_vars = excel_result.get(sheet_name, {}) + + all_var_names = set(list(csv_vars.keys()) + list(excel_vars.keys())) + + print(f"\n Sheet: '{sheet_name}'") + print(f" {'Variable Name':40s} {'CSV Location':15s} {'Excel Location':15s} {'Match':10s}") + print(f" {'-'*80}") + + for var_name in sorted(all_var_names): + csv_loc = csv_vars.get(var_name, 'N/A') + excel_loc = excel_vars.get(var_name, 'N/A') + match = '✓' if csv_loc == excel_loc else '✗' + print(f" {var_name:40s} {csv_loc:15s} {excel_loc:15s} {match:10s}") + + # Print summary + print("\n" + "=" * 80) + print("SUMMARY:") + total_matches = 0 + total_vars = 0 + for sheet_name in all_sheets: + csv_vars = csv_result.get(sheet_name, {}) + excel_vars = excel_result.get(sheet_name, {}) + all_var_names = set(list(csv_vars.keys()) + list(excel_vars.keys())) + + for var_name in all_var_names: + total_vars += 1 + if csv_vars.get(var_name) == excel_vars.get(var_name) and csv_vars.get(var_name) != None: + total_matches += 1 + + print(f"Matching variables: {total_matches}/{total_vars}") + print("=" * 80) + + +if __name__ == '__main__': + print("\nUpdated get_variable_locations() Test") + print("=" * 80) + print(f"Test configuration:") + print(f" Workbook: {test_config.TEST_WORKBOOK_NAME}") + print(f" Model Run: {test_config.TEST_MODEL_RUN_ID}") + print(f" Excel Path: {test_config.TEST_EXCEL_WORKBOOK}") + print("=" * 80) + + # Run main test + result = test_new_get_variable_locations() + + # Run comparison if both files exist + if os.path.exists(test_config.VARIABLE_LOCATIONS_CSV) and os.path.exists(test_config.TEST_EXCEL_WORKBOOK): + print("\n") + compare_csv_vs_excel() diff --git a/utilities/RTP/Emissions/Off Model Calculators/tests/test_variable_locations.py b/utilities/RTP/Emissions/Off Model Calculators/tests/test_variable_locations.py new file mode 100644 index 000000000..f0101aa47 --- /dev/null +++ b/utilities/RTP/Emissions/Off Model Calculators/tests/test_variable_locations.py @@ -0,0 +1,204 @@ +""" +Test script to understand how get_variable_locations() works. +This script isolates and tests the get_variable_locations() function +without needing access to all production paths. +""" + +import sys +import os +import pandas as pd + +# Add parent directory to path to import helper modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +import test_config + +def test_get_variable_locations(): + """ + Mock version of get_variable_locations() from calcs.py + This demonstrates how the function reads and processes Variable_locations.csv + """ + + print("=" * 80) + print("Testing get_variable_locations() function") + print("=" * 80) + + # Check if Variable_locations.csv exists + if not os.path.exists(test_config.VARIABLE_LOCATIONS_CSV): + print(f"\nERROR: Variable_locations.csv not found at:") + print(f" {test_config.VARIABLE_LOCATIONS_CSV}") + print("\nPlease create a test_data folder and add your Variable_locations.csv file.") + print(f"Expected location: {test_config.TEST_DATA_DIR}") + return None + + print(f"\n1. Reading Variable_locations.csv from:") + print(f" {test_config.VARIABLE_LOCATIONS_CSV}") + + # Read the CSV file + allVars = pd.read_csv(test_config.VARIABLE_LOCATIONS_CSV) + + print(f"\n2. Total variables in CSV: {len(allVars)}") + print(f" Columns: {list(allVars.columns)}") + + # Show sample of all data + print(f"\n3. Sample of all variables (first 10 rows):") + print(allVars.head(10).to_string()) + + # Filter by workbook name + masterWbName = test_config.TEST_WORKBOOK_NAME + runs = test_config.TEST_MODEL_RUN_ID + year = runs[:4] # Extract year from run ID (e.g., "2035" from "2035_TM160_IPA_16") + + print(f"\n4. Filtering for workbook: '{masterWbName}'") + print(f" Model run: {runs}") + print(f" Year: {year}") + + calcVars = allVars.loc[allVars['Workbook'].isin([masterWbName])] + + print(f"\n5. Variables found for this workbook: {len(calcVars)}") + + if len(calcVars) == 0: + print(f"\n WARNING: No variables found for workbook '{masterWbName}'") + print(f" Available workbooks in CSV:") + if 'Workbook' in allVars.columns: + for wb in allVars['Workbook'].unique(): + print(f" - {wb}") + return None + + # Select relevant columns + location_col = f'Location_{year}' + + print(f"\n6. Looking for location column: '{location_col}'") + + if location_col not in calcVars.columns: + print(f"\n WARNING: Column '{location_col}' not found!") + print(f" Available location columns:") + location_cols = [col for col in calcVars.columns if col.startswith('Location_')] + for col in location_cols: + print(f" - {col}") + + if location_cols: + location_col = location_cols[0] + print(f"\n Using first available column: '{location_col}'") + else: + print("\n No location columns found!") + return None + + # Select and rename columns + calcVars = calcVars[['Sheet', 'Variable Name', location_col]].copy() + calcVars.rename(columns={location_col: 'Location'}, inplace=True) + + print(f"\n7. Processed variables (all rows):") + print(calcVars.to_string()) + + # Group by sheet + groups = set(calcVars['Sheet']) + print(f"\n8. Sheets found: {groups}") + + # Create dictionary structure + v = {} + for group in groups: + v.setdefault(group, dict()) + group_vars = calcVars[calcVars['Sheet'] == group] + v[group] = dict(zip(group_vars['Variable Name'], group_vars['Location'])) + + print(f"\n9. Final dictionary structure (v):") + print("-" * 80) + for sheet, variables in v.items(): + print(f"\nSheet: '{sheet}'") + for var_name, location in variables.items(): + print(f" {var_name:30s} -> {location}") + + print("\n" + "=" * 80) + print("Test completed successfully!") + print("=" * 80) + + return v + + +def create_sample_csv(): + """ + Create a sample Variable_locations.csv file for testing. + This shows the expected structure of the file. + """ + sample_data = { + 'Workbook': [ + 'PBA50+_OffModel_Bikeshare', + 'PBA50+_OffModel_Bikeshare', + 'PBA50+_OffModel_Bikeshare', + 'PBA50+_OffModel_Carshare', + 'PBA50+_OffModel_Carshare', + ], + 'Sheet': [ + 'Main Sheet', + 'Main Sheet', + 'Calculations', + 'Main Sheet', + 'Main Sheet', + ], + 'Variable Name': [ + 'Run_directory', + 'year', + 'total_trips', + 'Run_directory', + 'year', + ], + 'Location_2035': [ + 'B5', + 'B6', + 'C10', + 'B5', + 'B6', + ], + 'Location_2050': [ + 'B5', + 'B6', + 'C10', + 'B5', + 'B6', + ] + } + + df = pd.DataFrame(sample_data) + sample_path = os.path.join(test_config.TEST_DATA_DIR, 'Variable_locations_SAMPLE.csv') + df.to_csv(sample_path, index=False) + + print(f"\nSample Variable_locations.csv created at:") + print(f" {sample_path}") + print("\nSample structure:") + print(df.to_string()) + print(f"\nRename this file to 'Variable_locations.csv' to use it for testing.") + + +if __name__ == '__main__': + print("\nOff-Model Calculator Variable Locations Test") + print("=" * 80) + print(f"Test configuration:") + print(f" Workbook: {test_config.TEST_WORKBOOK_NAME}") + print(f" Model Run: {test_config.TEST_MODEL_RUN_ID}") + print(f" CSV Path: {test_config.VARIABLE_LOCATIONS_CSV}") + print("=" * 80) + + # Check if Variable_locations.csv exists + if not os.path.exists(test_config.VARIABLE_LOCATIONS_CSV): + print("\n[INFO] Variable_locations.csv not found. Creating sample file...") + create_sample_csv() + print("\n[ACTION REQUIRED]") + print("1. Check the test_data folder") + print("2. Either:") + print(" a) Copy your actual Variable_locations.csv to test_data/, OR") + print(" b) Rename Variable_locations_SAMPLE.csv to Variable_locations.csv") + print("3. Update test_config.py with correct workbook name and run ID") + print("4. Run this script again") + else: + # Run the test + result = test_get_variable_locations() + + if result: + print("\n[SUCCESS] Variable locations loaded successfully!") + print("\nYou can now understand how the data flows:") + print("1. CSV file contains mappings for all calculators") + print("2. Filtered by workbook name (calculator type)") + print("3. Organized by sheet name") + print("4. Each variable maps to an Excel cell location") + print("5. Used by write_runid_to_mainsheet() to write data to Excel") diff --git a/utilities/RTP/Emissions/Off Model Calculators/tests/validate_output_tabs.py b/utilities/RTP/Emissions/Off Model Calculators/tests/validate_output_tabs.py new file mode 100644 index 000000000..28b80a3ac --- /dev/null +++ b/utilities/RTP/Emissions/Off Model Calculators/tests/validate_output_tabs.py @@ -0,0 +1,207 @@ +""" +Validation script to compare Output tab (old horizontal format) vs Output_test tab (new vertical format). +Ensures no variables are lost in the transition. +""" + +import pandas as pd +import os +import sys + +def get_variables_from_output_tab(excel_file, year): + """ + Extract variable names from old Output tab (horizontal format). + Returns set of variable names. + """ + try: + # Read Output tab (skip first row which is usually headers) + df = pd.read_excel(excel_file, sheet_name='Output', skiprows=1) + + # Get column names (these are the variables in horizontal format) + variables = set(df.columns) + + # Filter for year-specific variables + year_variables = {col for col in variables if str(year) in str(col)} + + return variables, year_variables + except Exception as e: + print(f"Error reading Output tab: {e}") + return set(), set() + +def get_variables_from_output_test_tab(excel_file, year): + """ + Extract variable names from new Output_test tab (vertical format). + Returns set of variable names. + """ + try: + # Read Output_test tab + df = pd.read_excel(excel_file, sheet_name='Output_test') + + # Filter for Output sheet + output_vars = df[df['Sheet'] == 'Output'].copy() + + # Get all variable names + all_variables = set(output_vars['Variable Name'].dropna()) + + # Get year-specific variables (ending with _YYYY) + year_suffix = f'_{year}' + year_variables = {var for var in all_variables if var.endswith(year_suffix)} + + # Get year-agnostic variables (not ending with _YYYY) + agnostic_variables = {var for var in all_variables if not var[-5:].startswith('_') or not var[-4:].isdigit()} + + return all_variables, year_variables, agnostic_variables + except Exception as e: + print(f"Error reading Output_test tab: {e}") + return set(), set(), set() + +def compare_output_tabs(excel_file, year): + """ + Compare Output and Output_test tabs to ensure no variables are lost. + """ + print("=" * 80) + print(f"Validating Output Tabs for Year {year}") + print("=" * 80) + print(f"File: {excel_file}\n") + + # Get variables from old Output tab + print("1. Reading Output tab (horizontal format)...") + output_all, output_year = get_variables_from_output_tab(excel_file, year) + print(f" Total columns: {len(output_all)}") + print(f" Year-specific variables ({year}): {len(output_year)}") + + # Get variables from new Output_test tab + print("\n2. Reading Output_test tab (vertical format)...") + test_all, test_year, test_agnostic = get_variables_from_output_test_tab(excel_file, year) + print(f" Total variables: {len(test_all)}") + print(f" Year-specific variables ({year}): {len(test_year)}") + print(f" Year-agnostic variables: {len(test_agnostic)}") + + # Remove year suffix from Output_test variables for comparison + test_year_normalized = {var.replace(f'_{year}', '') for var in test_year} + + # Compare + print("\n3. COMPARISON:") + print("-" * 80) + + # Show Output tab variables + print(f"\nOutput tab variables (year {year}):") + for var in sorted(output_year): + print(f" - {var}") + + # Show Output_test tab year-specific variables (before normalization) + print(f"\nOutput_test tab year-specific variables (year {year}):") + for var in sorted(test_year): + print(f" - {var}") + + # Show Output_test tab year-agnostic variables + if test_agnostic: + print(f"\nOutput_test tab year-agnostic variables:") + for var in sorted(test_agnostic): + print(f" - {var}") + + # Check for missing variables + print("\n4. VALIDATION:") + print("-" * 80) + + # Normalize Output tab variable names (remove year suffix if present) + output_year_normalized = {var.replace(f'_{year}', '') for var in output_year if f'_{year}' in var} + + # Variables in Output but not in Output_test + missing_in_test = output_year_normalized - test_year_normalized - test_agnostic + + # Variables in Output_test but not in Output + new_in_test = test_year_normalized - output_year_normalized + + if missing_in_test: + print(f"\n❌ MISSING VARIABLES in Output_test (present in Output):") + for var in sorted(missing_in_test): + print(f" - {var}") + else: + print(f"\n✓ All Output tab variables are present in Output_test") + + if new_in_test: + print(f"\n✓ NEW VARIABLES in Output_test (not in Output):") + for var in sorted(new_in_test): + print(f" - {var}") + + # Summary + print("\n5. SUMMARY:") + print("=" * 80) + print(f"Output tab variables: {len(output_year_normalized)}") + print(f"Output_test year-specific: {len(test_year_normalized)}") + print(f"Output_test year-agnostic: {len(test_agnostic)}") + print(f"Output_test total (normalized): {len(test_year_normalized) + len(test_agnostic)}") + print(f"Missing variables: {len(missing_in_test)}") + print(f"New variables: {len(new_in_test)}") + + if missing_in_test: + print("\n❌ VALIDATION FAILED: Some variables are missing!") + return False + else: + print("\n✓ VALIDATION PASSED: All variables accounted for!") + return True + +def validate_specific_extraction_variables(excel_file, year): + """ + Validate that the specific variables needed by extract_offmodel_results.py are present. + """ + print("\n" + "=" * 80) + print("Validating Required Extraction Variables") + print("=" * 80) + + required_vars = [ + f'Out_daily_GHG_reduced_{year}', + f'Out_per_capita_GHG_reduced_{year}' + ] + + try: + df = pd.read_excel(excel_file, sheet_name='Output_test') + output_vars = df[df['Sheet'] == 'Output'] + available_vars = set(output_vars['Variable Name'].dropna()) + + print("\nRequired variables for extraction:") + all_present = True + for var in required_vars: + if var in available_vars: + print(f" ✓ {var}") + else: + print(f" ❌ {var} - MISSING!") + all_present = False + + if all_present: + print("\n✓ All required variables are present") + return True + else: + print("\n❌ Some required variables are missing!") + return False + + except Exception as e: + print(f"\n❌ Error: {e}") + return False + +if __name__ == '__main__': + # Path to test file + script_dir = os.path.dirname(os.path.abspath(__file__)) + excel_file = os.path.join(script_dir, 'test_data', 'PBA50+_OffModel_Bikeshare.xlsx') + + if not os.path.exists(excel_file): + print(f"ERROR: Excel file not found: {excel_file}") + sys.exit(1) + + # Test for year 2035 (adjust as needed) + year = '2035' + + # Run comparisons + result1 = compare_output_tabs(excel_file, year) + result2 = validate_specific_extraction_variables(excel_file, year) + + if result1 and result2: + print("\n" + "=" * 80) + print("✓✓✓ ALL VALIDATIONS PASSED ✓✓✓") + print("=" * 80) + sys.exit(0) + else: + print("\n" + "=" * 80) + print("❌❌❌ VALIDATION FAILED ❌❌❌") + print("=" * 80) + sys.exit(1) diff --git a/utilities/RTP/Emissions/Off Model Calculators/tests/verify_excel_structure.py b/utilities/RTP/Emissions/Off Model Calculators/tests/verify_excel_structure.py new file mode 100644 index 000000000..44e5bfcd6 --- /dev/null +++ b/utilities/RTP/Emissions/Off Model Calculators/tests/verify_excel_structure.py @@ -0,0 +1,50 @@ +""" +Quick script to verify Excel file has the Output_test tab with expected structure. +""" + +import pandas as pd +import os + +# Path to Excel file (relative to this script's location) +script_dir = os.path.dirname(os.path.abspath(__file__)) +excel_file = os.path.join(script_dir, 'test_data', 'PBA50+_OffModel_Bikeshare.xlsx') + +if not os.path.exists(excel_file): + print(f"ERROR: File not found: {excel_file}") +else: + print(f"Checking Excel file: {excel_file}\n") + + # Read Excel file to get sheet names + xl = pd.ExcelFile(excel_file) + print(f"Available sheets:") + for i, sheet in enumerate(xl.sheet_names, 1): + print(f" {i}. {sheet}") + + # Check if Output_test exists + if 'Output_test' in xl.sheet_names: + print(f"\n✓ Output_test sheet found!") + + # Read Output_test tab + df = pd.read_excel(excel_file, sheet_name='Output_test') + + print(f"\nOutput_test structure:") + print(f" Rows: {len(df)}") + print(f" Columns: {list(df.columns)}") + + print(f"\nFirst 15 rows:") + print(df.head(15).to_string()) + + # Check for year suffixes + print(f"\nVariable names with year suffixes:") + year_vars = df[df['Variable Name'].str.contains('_20', na=False)] + if len(year_vars) > 0: + unique_vars = year_vars['Variable Name'].unique()[:10] + for var in unique_vars: + print(f" - {var}") + print(f" ... (showing first 10 of {len(year_vars)} variables)") + else: + print(" No variables with year suffixes found") + + else: + print(f"\n✗ Output_test sheet NOT found!") + print(f"Please add Output_test sheet to the Excel file.") From 4ead49f2bb1f37dcba13d846624f6acee1dd21f6 Mon Sep 17 00:00:00 2001 From: Arriaga Gonzalez <63330@icf.com> Date: Fri, 5 Dec 2025 15:07:52 -0800 Subject: [PATCH 5/5] Testing: data to test the changes --- .../test_data/PBA50+_OffModel_Bikeshare.xlsx | Bin 0 -> 50428 bytes .../tests/test_data/Variable_locations.csv | 160 ++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 utilities/RTP/Emissions/Off Model Calculators/tests/test_data/PBA50+_OffModel_Bikeshare.xlsx create mode 100644 utilities/RTP/Emissions/Off Model Calculators/tests/test_data/Variable_locations.csv diff --git a/utilities/RTP/Emissions/Off Model Calculators/tests/test_data/PBA50+_OffModel_Bikeshare.xlsx b/utilities/RTP/Emissions/Off Model Calculators/tests/test_data/PBA50+_OffModel_Bikeshare.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..57fc1fb1d46d2c30b08b8b4852b3a9e584275dc1 GIT binary patch literal 50428 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

DkD2ZBiW=9 z+Q=^;$(j(D9E^au&&ByzkZ>RAQ~_SbFWavF%7V;b9*rb*y>>r!Z4B1jQVkdgb>V+i@8gegEl$Ekd&&y!r*_N=o zTQNh!Q-E0|tnh`Y+ocj<(GVfPx#mZc^nOT_NwFxeF6DHG^%Zy&<=A6u2wB49HRSE7 zCblH`lpJT)hrpLKIgD(Mn?Yc|)O5d`7P&hVC8y2u)fS`#l?)dfBTh3Y?=UO!Loq8E zPLy2Z>g-ag-$PNAo}m*TP4 z@%2Edx%`(;_n&jj=zfXJt9~>5W$6L{d{2h|VQ+VEbh9*a_{U$`(E46)7(wuci~Zu8 z5v#=#>K7y?nTj5|HKo$^i_+_XjST^TmdIxsxRWCvTe7VE;$Gb33g27hV%kSn)yN;Y zI^*arr+M1y z`E{Rj6+PQv;p1L^DdtP?XJ5cGJiot+7>8B1*#yVNKj5;Twgdm661*$esVj+^DLO#hP-i1*`54fl24u0P}wqlu=TgEK!#M`E_jOu2cPW;FQ& zHz*PM34I zWR|as9_9qAF*|6p|AXaLttexfZQHFKT9G96C5L5NexWiujt0a~&l>b;Fi0+OHX2`aRIqD>oG0Po|BU$5Yqr&RkzjjKHPC9I|VIq=E z1k@3|(;=&Kx^Mork;hu#7=D&62?~0{f;NKkBTPmlD`t;9oCX_!{{a3sbDT|ay4e)W zB3)z}SH>CRy#y0E1k4?bobFUp8tF8X7IP_oOc*<83@tco6t<45fj+C8a>0xfcrEq? z!Ivk<&^_o5=y3AAMBr6OU(KA7 z7GypSubial9xB<9_!EdIQ9pwLh!;+uGc9dZZi?BITT$;0i9F%xRZUr3-s`iltSwl= z0AC99BV!Wz&V+@v%m*XRcg453zB+B;Jc>8vwN`Ok$BafgqfGJ)a7bs_yh$NtJT42~ z-j!;P?EEJ{JD^tf>s}W)d)U@ith-OB)j9q~-lZ-sPiX91qr}e`SnXiAK z?~n&Z)}-MP+Dkq$lZ*@rKgcblYSLMsZvM_}>he^DN^P>ARbs$?=jH&3t#EaIV&wQc zqhQ(c-1zYt2aNajlRKA*N)!3e5@{K7j;XEgAXqw=_idOqVWk!klwVPP4apx5BUg}3 zyIQb|+B&UgB#67OtwWZHY0kzpm?EwEb&03RQPC0h{TOJ0QP_0;7el;UYMUI(fP z?_=_7mGOJBxmj>(&f*gK|JJCgJNyZZ|B$&vZ!Kge1)k6{kEDjSPLe%h$NcCCvv5r`T2qRG%pAH{G0lM;L~9~V zkcztiL39EiBYwn29Z~=X0MmVV{7yG+pWAFSGEPZ$!`Bp#fY8JmQbDl2Ovx4yosvkt zU({Lz_6HoDFnN@3DhH{sRubGTG>7dlS>1wBQb`mHnNT7L*qA)??eD;i>G|`?R5Ns+>sH%RGcW7bKu?xZ@p*=xy zhv+3c+dt1j7UDm(euzx|7I8DR(0;ZP_ye9hULI@l&a1A3g%4VNzyV>6G*&7BIW*lv zX`9Z+QX*E=zIvWJN8>p`M*@2#OB_ht0$xZmk)We?Q( z3z1O^eo_hSO}i<1dq$2Fxr)F4cvH-A?Tfu z*G%+X^UXq(J-?c~B+hoojnPnjiUy+H`W24-v6O174!~s%&JV9Vpp*9Z_DI-pF`<+& zHrS>xj^7m_TwlOwXc9cskKNRs@>Y^V+V@RD?aujYxYy9~(=2!y$edSR^rIa^>t9t@ zOfH(it=rpPymocwi9eYgej8Pa-A`W|Q{RB@fVb_?K?bhl&u&RhY5!tPEi@ek6|Nx< z=@*m67Fku;-;z_vFXw}^YJOAS*`sk+0fWd$1ZL}LuuE%-VZK@>BYLLbkL3{Es181& z6cyJx+9#*dUuE7IcgEZR8BsZ6a|&2=yh$#>b_!^s`KfrxxOXXkRWFWX)VXdb4x4PQ zVNvX~m7@rDnJ9c!vHnTB<%FR?Ghe=@iKdmUOp>Ps`U?~Vpe0GG;0GIyDcKh!bI)mP zwh>1u=<^d*>1|#osEZ5LhX?DV#sYILMZG=ns2}Dk&>6u+X&a0KnJLm0p1TFpA&?>;UfJT~C!dr%| zs&Qag$XKGu1+gg$!B5ZPc9vy)2kl}5>QMI>Zn&TpwU)_-gB-#}uu>kN zv*+G}8m-0B+xpfMfLp#&vY^SwkzR5Wx=+ZSQYve8zH-50NNf;>nLP1pezuEMz$07$ zG|quB{1iK_K|z_v{K>OM;0F$YQ79LC>z}ol!~nd~!EI8*_luCo-k-yZox_NYd}V1% zjg0jz^RpGcVfw4FWok4aZLfoeLnj~U)A}$ z)?2T56oI>Ke3gR#dy^-Im@X=D6x_9<6Kxv_^@pH>+czk|s!#Tw5 zo7I79^Z83y7qPm3jema6a=Nfs7;)jn7p3hN?|&OcB2Y2fM*U7O(Ed*SLH@TXxT&7K zk>M{#doyd3zccaQYBsV&tVljzVjr*tVqS^AkXS~OI<`<0g5cdnY2o?D3cA8GwFEVP zpI2SNq`W_5-GX9T(xPb zHA zB`HTik^=Y)i?l-bR$YnXa5Si%%aAE6;7Bb=U1*q3*{&8c zxiC7c%pu?*jjqU$Vj3T?LKwnI^b6QqF9_iY#Zj*^oS8P1B7v_3$~yOedl~1&x5tL# z28-;E5!mPW-JJ=Wo_mOB_H0`?w?O45T@`HACle#fXqP~ZWc8w5ROdSbClG zsbSkOs(`2UsoizKV`R`|ti4AAuon|1mooFw1Ezqj^M1a;UxrDSsBTZvM)?^BgTu3GWz+EO;--a!nb)y0Np(m!_sIfGUs# z+RVoIbx1-01-&`zPtL&9UJRAj4caRNPIWZ@nX$FEZ`pX#{YWCMG(@7Q%z`?flh|G( z5gJ@eB1xN0&26GHqKK2KGi8#(*ovZQZ0GX&iyC;);)#p4Xf0L}jLc<4wixE3d;3@R%Kp+=7p@RQ|R?P zM8{=bXI&o=+7tZ)OhFm)A@cZP);GTrLWQv3zX8!G)+BKA#e#7(9EkR8j~=(MBvYy% zIKew647F;=Dpp6fwd*(@tdEp^Iio37@3PhKxl{~)FCE1;IE)*GRH@6k(XCnvf77Vg zcV#i}8CiXDB3A;l49MFmcC=@cFX7a%%rqp)x(uSudz%Ui)sZ94h{^Fah$*W zZa~r__;O!X4r{>{~{6os*!?t3Xx3L5$H)F_%DRd(84Ih^SafoRc=| zlgFhxZ8)syO~i%w*%aPU5ceO-jf~4pfx$W1QA4V`@3lk9uetzM@;w)Zjw|PU!cnt@ zq$dVYJLFrzc|;_pQk$UXB&a~iSmg9UM}!mXj8#kEVj>^??BiuPy=xxTwG#RTw|-_M zT9ixIPGC0e0n32tf2ytNhf{!Ol;q_+Sl9!akunYl<*sPrB+n|F;ec}TG)Yx~weefc zBkh2GOx!FpVBkXhQGDUIScyY+0m)UtYmsVVq5v|` zXf^3ti1^^~nm0*3J_ET}9aMl)?=(8m%5Zn%g9<~^3t2C2;lJqC!;v+kTLseYHYjCc z^WYWPL>U}JLgu{weaXI!*8=_LkWBD3Z5?nM;EC0bt1qPZ4`?V>-p9}W8URTMDRlR2 zWbv70p_Ph5p;E^Opux znwj9#>q+DA+=>rAp|a2|RR*CsQC}weN`DwnIaJ8CWfOjdjooF}Gg7WY^k6W^r2X+H zeQb6<4QVh!_r=wD-kZfa6+Dlgvtzu?qXj{q8~RMIRVuW(~9#AUvG7W}FDm{jh=ZT5EPzd^M3;x|xK)}7vY7(bI zHIrs)S{e`a;L-Z6r=iq)fL`PkQ8ttFV)UNfD3i>2Kf*n?ZY9Ky$W8D@cl#6!*IhN$ zI6GJ!`O6q7l6I8(7tNu+5yq6;zV?0Um}gjyB4$o8ZXxcEQ;HRNjfG{v7Y3mD?Eo>cRod!oA?qjw~7A68hlBbWG11DQI76wCs-Hg(y#kL-z?bRgxCu4kOu z>vf?$86-5Ob`1c{jJ1Gs4te8>H8*Tww$^(D6;Ah(+RABmswE3-uWnoKeUXbX_*i65qteZ#KYKQegRq<{G##yp*-Ea`x zuu@LZ?>B!SYcpg$ADgwj;+rosg40*m4VY*wQo4FxTku1mpsWQFSKqof=LVzFtLdBx zLeKohj>;zrKhq5pJ05YFPHt7NvSFxSAdQa1{9HgCId4i09^asi;e&uav!HdA0|Aqp z@}5L^__yb31O&@kKx-pEnvU4?#rj+Hj8C+AD`+Ou&>g>LkoFgYs*bsZ7cuq*uh?Ks zE3KxIAR3gxY6!bz1H0tWh2>Pe>_c$U1k-p*fNQo?EZV?nxVpBs`1xb1?Tt`=zzZy# z5hkIYgROS_2&RfL837#z0ZY1Q`0X-BS;m`ykLcmQmMH%-LOHD%BzwP+De#S7qJJUu zpH`Xw)+6p;?O^|-Q{3NXuz@kX;eB+-!q32;g03D3OF__rzgP)dm5zb+-_}6aQ(~Wy zKRg<+jd~6bZKn@eGTyn$u#92js219Rf({JOOlxmE)~LJr>)6gVkRhnKV)f=}y3&DV zWF*3dV^vY0>Vo8nKhXQV2UhE%AA8!nqFm6R_ez%o!pVPNE}0uuS>^AgsNf|0H4BLh|~4 zvJxl|-EXi(r{$xD_81XlKnPW3%|{T#B$Q6&nZ?swldH-F<1HSDT0Jt;F)8^R7LlU9 z$8M~=C}63FI%+dyn`-x1`AekSP|E00!5~fQX?I=L)LFWL^{@tbBkLCqDAXZcW3J4y zW=c-SJll}(HN~IGe?%}!60ggiFUCEhbU7aQEdtS}RF^kwh{*GaO$V31H3$D01Wb7l z@&79k{3VO{KSkj8R|I513jT+h0{F`;9{@_)7eOf^Fw<{uGUo_b_2prJ;G zQC&CzLyng4DAe9~cU)-meWPGA5&Rnky9-vVmy|Lx=3&FBYD#}mfET65yLX{l5~(;E zJ9bexgec|wc?Fa%IM0=Fw+^`3S61B_WPkabYaOGR^5s&Z7m7PQ>hq0);lCnq6O40@ ztyoL?2L;R;$7CM}5K_zOgZEc|X$JnY2;ey;82)J=uK4}6{=e$M->j{_BB+d?lmMnj z7J2r+hQHd{5C=gqD~5`gsagO7AI-w!5D(r^yuzn1ZH{4Mw83Jo|1Z4FyA*BE9iQ6d?dnnK$m{DP*`F z$4a#?mPaftwRKQ0MIAkaD<#=l{ykIU#gLm$)jwt`%<2HC3neCE>L_0Gm zqZj6yf5-eR%lrJbe-dKFaDDh`LU_ex@MkKCYI}Zm=Yo16=UZL=t0Iw>8rEUMuLsbM3s*o#}0AaYCvO9Waf`AdBB;NtyCqgH?C_e|1B zIweDBRiC1(TrSr!mZ=bGV}B zI}th^&U3hwY%ZVNIg=*5le)AWV88n3RrSI0&nJ3&XQ6js;t)|@lsP!63A*?!V8xbR ze+b|KQi>brLZ0s8cfevkC{|5mw~wWdA(*ccz%Lj^Q&jN#4gqh~gYSX_E#^iJLX+CC zGpHE!olh=?$egiurO6-~JE3iooieRG9ZXsB0KYjSeGvir$4j5*Sq53#5@raEgPVe1 zf~CKs2k*WY_8~QFVcN&9RkE9n$ST}5cqg{5?UL9J{`cX8n8APsrEg^XgAVe4A;Zzs z-bl~T$WZX#5b~X%@_!8cMT}wGxOKoc^Uib98(i}9&D9D@`0^gx^G)(gYP-7L%q=xSt6WMl;dgJ`v` zIo!M?leNAVxs*`~xmm&$V(Z9oQ{&@J4Ln?&U2pADcErk1rh0oa)3 zuNe6^wOSNdaXxP^r{F{w1GH@(jpgUmrm2R(a`1kUh(VkFQylTY zg>@5fnYz~A4@N`CTGFR4vqgHJD*SQVal#d1Ct4&mCaz=wL2#lFQ@zRm3Dj$W>!Zh{ zy=sc3NJ1&75IE}>$PN{@qKGgGHMN(T5~piv)S(f5@rIqLwFF%tj1~)}uy1Euic`n~ zWDjb``-G6WWXpTNq{TI@A^mw(N<&@r)ft!F%nf6Md*3yzI1I-q%4%CUyL3Sz zuJ%KDH}|s1_}ZqV)eynW42-=XC|%8P_~nf`ZaM6tXap+msKW*oq!ET*v0?Q6u!p@2BC_xYT5T`(EXHYoqe}FC zDT-sMGVszpl{8fKk}oS7V}tSey~hV6(dKy7+Svnd`0N&*@H{aNRjJ?$^)X}> z9QIcmz_vO3zVzT1AB)#>;L(3;M_3CMg}1*YEkp260@i<)=ie#B|5lU}>XJ6=Z17!Z zCqD3vCK`8?$x$4_m@Ry@(pZ-MgIkd4`Ic5B3M6F%pU<4gkIhD-5~!vQrc`#p_t`c> zuQzshphJwieI|}Z(ZS7T@Zo9a^Os|hMXa_HG0S+hn))Ind3UGqaiYJ82pD#DW0K4; z3C6x?ao&Oj>KT@yhY4ayN+fKqmni4A)`?!~YDiViM;Drp6CU+{XGfzxx%23(L{NCX z{BdI`pa0Af=d9{hL-K3G8x=~?kclM*WgvH>-%yO!%Q`@406&nzEism>cWY=1u{3GN zcm3U)u)2^PeLLWQ_Je&t2$;cdvs6{Tjlo4hL~R^L1>6v$O~N?H5&^w#`G7ZBeh&}aF!dsR}0tsZ-CCvi6>;OFm5xdcF-Z#es@0WOyJ zVlfE=IbXKH-VRso8|>5pL>_|5)@4cVoV^$+9!Q~!Xx_11+lP* z`2@hz4p~XVsxdjjei}Y<{W4Rdwl;DyS0ZT{+heG}u50UI5N+&^aXKSmWSs`L-6oND zm^c9f#zFexLH*eh*vH}fyjJ;(%yKN_tgS_kYrGl$vKz5IiE|$VguZaem4(tsiQ+Nb z*@`@uo2cn>pcA;R&ePo6{cWMN#YrIgN#%^(;_9Dp(kamSvGL;*u#_eRu=BLlLMZ zso2&583t2h@=`jAcZAgjV=K$!JH{##^vf9CoPnCe?odWg2!BGtckKj~s;;Wu2M|nN zk}Q>n`KrPApkV1MNV(-jQwg$n#5vNwl2IuOe$kL1Pk36e06pdX)2ZmC@YbxP90SPf za7aU=HPAkdwm$P7gLZ!xUFpsTyjWcKmX~TAL{M{r8j_QSThz;Z2pgkhTyj2 zHGH^+?fmdthZDiq+gXUxvb9ds55E*=n{nd7%v(dK^V2zaJJ;SvKk+Hm^v0T1*v{5& zz@sBP4n<3fe6QF!sI;4WIeGD#C*N>t=QO5PPiMBw_*2%3*M#wOYoJb3yExS)smD8# zI!8=;h}j5#YoZ}pQ!`a7L7N;~sF*CUqBo?Db+}Xh_GK|+TlDu4s`^oO8LHc$t#-sb zULK+Q|Fn13QB`eS!>18YLQ0U5Mx>=vI;9(=4$_^H64Ko*Qc8z{NQZPvcXtZX`EI$- zeZh0N&v)EIQ7gWAW1P}Y~u?@ny^sn9dE|EefL#Z>0@F8vK=ju}4uS;45Ira;?>K(+2nh^t}nq*i2Ag8j={U>T#@rrZhR*bcSf z?8A3chmS#1%1f#bHy>iGqo>qVnX*(=uu?CsUwoa}0Qx4-w}8Ig;s16(JkudkZouLD z=2x6Nr)QyS3Qf7CkFirNvgPhZ6j;AXA#?aVm(40#!r$z6p_p12%^1!!{YLHm1@YX; z?It2mi^q5}V1w`hp0dBOL;ls9=O_7Z&nM_nT?)VQ3c5KY;ngGop**u{2-%{Fkp9Xo zqZ6vZo2^rOYKrHF9FX3x1>lWOLHYT~@yV)hlP>5rckx|%yJJYDnIc{*7JReUj7VIU zRtO_a{S>^B^d>?@=sEXH+~0QyyV$NC=QY_w6M>>zV^J{dR6!XiQ1P1W~H9d@?MaE zrZM`k@&XyGpR-6hWP3CZl?@yFt)o~R&pn$A7d0(uvfAUFJvp_ZDO*8zNRs^n@fbO^f*0G=!0ShpO`mtOl;UAkHxRJIQEfL zuTlS0kSmOrp87#FD$jXRhSWI1W|zZZ?b{yr>D{?fhEXK%56PP0y3L|+N*tMH)x^BR z!^iAx#E7}xR#-|IUZS))kthZ2rlhh`On!GcMs zVF;5&KQMrF{KPnwOz*8BG-VXuGiU8B3gS`X=%g?=^iBww2u}~VC&Fr-h!w-c-sCmo z^a{vP!Oh+8SmOKr>W0ew{kjAKYb87=o)3!nb~|=dA}aVtf}roWKxS@M(EIl@kLE&6kZTe zy5X^e($2RL&BfufwT_bnVAitwrPTqMZV2Cd znFqoBD}J;U7=*mvqnR<)88e;JV_4ClIKWRq8=U0;}!BXhrXTJA|S;G+y0>3XJH zKhM%yUBTR*tZ0jY5IRn!B|&MqptFZxELC*y2(5$Rv>WNiUqVN5YJY>>gN5odVD*k&n_DkEu&1*}naD zl(=g6PG`uHoV8^{QC?L(4%6gE7c6CDh9hbF{bp%jsHl5ItXQ=sc8-|mwW8hZLkF83 z=XdNxDuimf>vh+Lb4K4~)mfZ7jWk4do)vfl$KfUpbp815*smPG>rwPwdcTm|XZ{(Z z_!;eIV=lMT+OKrCK0I(~j7Rr?_kakXqu+dZ{_vOmetvE|KVdhWwjsXCerTx7aiWuU z;r5|*q@qWUHhNQYE?=bclx**=b>k(+ySPV^Bs78G^AYh(AxPe-P2OA6m>KR5-=Hrk zkkw=|CPS>c6L`2dFL1$VL_*GbCy*6BM; zjnB$dQR$EBV#<`)Kann>_}0D}-M9$cu&o>${oJoxWgdT7hdm&{Y72WYBi8f^3IV}H zDICHRS_e%6VFRK;Km~tP{@g!Y)mI=$f#iHOTB+Nluy?sz-XJ!3X|f{Q`=O&xE&13k zC7dWpH$MUVUB?7Q?P3(WWEl49?S;MJoX*wr_(kknr3fn#&*BvL$xml1Ch!>u2e8z) zwy`j``OoK{as&#i|Ig;)h1W=B{)iYlx~^||@Vm^e>~jd6d_9j0W;JHgs0(LB+?7Wy znlGn2H(om1D<#nvJ<<{g@8L_`7JKNPH#=Y5@fZ~^dbmBJ1(&w9$ZTtyMZV(Er7>H8 z_jirP2lJm|B2Lkg=O*HLQ2SATq$FkN&~uBYKt1jv(X~~I`Ir`Ab*B05`D0%m#E}|{t2}j#Yl1tuiXSdE)wYM%w z81R#Ep#ODUaJ513PuB&eA)Lq!^r*0fo_&!tE4_11JrHFVp_oLb${fv!?Fw9?^nEEO{L#FvQ>?$#VMj z(PJ+A@1;hi^(vZu4>r6Mv>3Q6IR^*WH9mIfi#<9cBCvQiOhM?0eW}rc#CODLK6UF0 z4THE>lm&>yB=GJGRM7e<7H0akIv^ceohyNmm5_eOfLfzG$E#9fQS}Ie|5GI;)S@7n z0_ECZaf`oVEy&qON%7LzpxPUbZFC;T{RekKq8IZ5tZv^_sVz?~6(mK~fcbDAp>(3L zm{|LSn0b{~4070tl*2)0aoCrIcyt)c?99`o#(-kd1MOD{#}LSz?IImaF;*>des%g| z&>)kMSDIRk9i{2$4QoWl6d1Wedu_Q~=u?b^QCQy*4BVO)X4txS9~|>lvYJZ^ycYD{ z))Y1)#D%F%O?`Xlu)j;ZSIH1=1!Qy1BGNBC z#O1B|SfJB_8qixH>$*h}1Wguy2qkSgaX!=U3F z*O!fp?#yxi zt5y{&HZByl0^TsLpAHyFC&m@J`!g+G0NJ#{lr^kVnzdhw%3rsCOYg$6?d8dgTY02>2UrJdc zbbayh;TJcYFkDu}@Ko`qxSK)tLV64evj|gNQi_f!+l#Ju6&Lv2=y{_EW9)LiTOs6> zqq{^%?--@L{6K8SLt-#*!$`5b=&2Dz+opdkTF|PX#@H5%?ojA9Tu!$?@xM@w{3u4Xje8B z)zSAqhv8(Wp)ERUOB1LUNY?St@puJv9gk|dUS$>^SRdFWp!>icz{NN6R&NLvq1OW= z&^81|`Y50t!1cFK$zzEOfNY+4%6(@e8}lkzVvizBJL~9i8Ya(Qg)ZWc!e->k;|k*H zr65v-RhGSb_r4iq?mMy3BiTSE4?pY3t*6v)_o-#a;4kLSJ|u?_nf5O4Q@`!fdYhO} zSS3^VnLVNV{L6{Q^iGspLlKn8m^wX zIH+l3QgmaNE)=YNKns{cQAc`C+7T_Q;q!=u9zH$MWMT;ga?Ncw8V`;OXXooV`wqbvqh#y_6NQ9`FzN(Dv81QFgym2ifyC zD|ge?5e!p6ohLWBeZ3eq3K_fSt3{^%>4_y;%6F;n{v2-6*6_z$CjMg`hq2p*Jgylc z19yzZu;~-<3BHPY!QWeyuW}v>%lnAane>%K_%$r_pUq1f^3O|XXy=$u%gck<@CR`~ zwKE|&EF2Zk)rC4K;}KxF{nZm!LL7h+_^;GC;~(nW%tGO(I`5oxHn6CiG^?bc!Kk1q z+a;#+q*E$y?l+V2#p3@!&B{ilzSng@Wvn-4Qo zwS!^1O|zU0mb&Aa47#~&wl+T^?J-YgRN4kIs?|+b?r~3M(OK@(XbP;d(DYTyNJ~rM zNuivH)oN~C=GIF}hW3-ao#lM@keGBOy;PZKb}Gg?h1PXlP1uHq|DyVM>!hnGQ%E5M z&!Q~V>wxO3;H4n1q)h$;DPg-#_wbl7lekSg4#9rg5FVlI=M*asc4ayGsyIBxOp4?s zv}u*?RK)x)KAdv|pfTDvbPLIavFSD2vFB)af?Vq>iQi96!SmScRL`t4n3>xk)Clm!7mnJ)$ zr;0sQwB|18#N4uT?W0pP&mn#mZAdzXzv(Zm_n~3FDmF0(IHR@){y|RbyA4ZwQqIQ@ z6=oyvD_Ffhdir+uBUh7*(3;Eh?iOs25^Z~uMgVOG=CSC~Vh&*~IU&-Qi8n)%o%O2a ziC*;z9TVC+d|VH<{d^;4K@{6_Xj5IzjsyC+#@2S8xse)VPqGNnrzPGj?V_ED&b+9( ze=NS0i2QsVk@kU4+wxZXhh06Zq^~@*%|;WL4lS7~nphp=t_<~3kKV)Np3KfPjRYM> zEF&j!wvF8#m}G#i7GpMsDQr$vy>D>bnQV+SXgygJIMAU6)!^n~^7)g=t9NCm!m$>% zr@|w0_xB>*razoCzy8pICtZq{LT3z zLH3MvMU%QJ+Hpn{p2GRlM^m+f^ z>GR_rtb(5I0hFG>b+eEx8}|aY83!()USb}keDzZ*EmnFRr56OrmwHm}o@X4InXfAN zY=|(ETLvl06W|`@Ozx$f)7e{-(9qEpuALW^&cKw!x)Cqf-*-8|JJQzjblAr)+vB#( ziF%cFMz5df3cEB~CLYHKU2G14ZApgG0P^y2FI@3mK`VPK_viscKQ*xj{7~}~HI^hJ zn`RT1ZEyeky~`<44~(6Dz`>RpNCzxGls8Z9rUO!)mh#e33UYg>{0?Q)Rr`N*<@^{+ z{JM`txqy_;*RW8Om;3ce?^L^Z#XLfa#(vAQC)S*AH$e*__{*2d8mU$Tfx4*jnTSwY zcP`yxhZw9$BoP*5CGd-1C;Sk@W0^PC_vlV3SQnWa@)>-j=20)~=4vLdu*dNvT2&iT zw(#PD2&uNgkOJhgcm}m$7%pHf}T3|%J4K*zL#p_NUZM^Gu4Apq5t1@2kU5B}+ zoZVhHTWNXi_snx!UUsLS z`3sbQvL^t%^VdPFe zFYezjfwRCOj8u&)A||QVp(1WH?UwZ(3yxEqf0~Zj*Ew$R9+Ptb9E;*T#Psy3mTz^( zd;7)EO}cs(1_6dM9(R51tBu_(%j9Alcl{M*i66!Z_PsE%M~CacBn#4KltES!j~7YK zs~khyFDu}+b7M|*Wl|h|*s56zY3Gy?aaJqUID=2nMgy6ChW2i}k z-8v*60~7UV=VzaT-A)V7ffe~*#lZOoRQmr(46gH-ev1LP80cJyf$dK*=%56|0KO7e z#Hv%bLuF0n4>92WDF*w#)8#pYPw*&lRuZr<$VKEur|OnhkE#GM=m#l_8_swFQCLR( z?lZ=jv^)11b8|>piu@L;hc>W+Vji}~t%0)c=MR)PCg-1)5$uTd+%hccJ}Jj z5%7er%*!sMWWYC10uGaE_AR8PQA$H@HWu8Qr5x`%)U1VDK?=ZBdk3^T931nd^8`ozO?7P%i8aIBtnV;wv4tc&b)n-8?T zY&>3fJQxby+J<{uBb4kC-~FP$%_U|(y;hX~pTOm;h*y$AfLFZb>Um(`gOflwB1Kk_eeJP z)4Izk=S&9TF?iFqPF@!xM};3mSeun1ri~OlF0izV=prPaHd7cVs&3!Nd(&)0oIYGyDG?jK&*%GNx~+_av(n@jc66k`ZOhVvp_hStm}0=weuE(Q~R0GR<5EFPnMhq|h>V=9u#H=KL6y-JT8&`FFw5g>efU zz>JcBP{Jk!?BPdgK&)Y!0`{<@WFYLYWC44)Q7RBc7{0&(dI?D@To`O?yZV|p3t!Hh{zatlx#AVsWboZ8z{(JY~N?102Q)k~ z9^1dafP{xahQYhThH6PP!&m3=8v37o<258D1dKMso!5Z!_~Ek8q0X8g(mca$P$9$rq{>d9BjF+{xehd zE-}~st)=~~`s>(WNGKYh@(dgVaQqa+BZ%z`muf6vcl7k`&#hqarY>Ls?5vsFYAL+3 z)VI<6`E<)j{0cySQKSDey>l9X0LH_OCJh7}|6wIw8HeBlSvzYT;(>tzK<+S5Q~5^o z1oD4?o!URf{>+)ywXiVx`L}Q75Pedn90UH85>V0d=N|8MQ%Ahw+$4bCME<6q`4?a& zf(U;Gg5GSp01LQ@8!6k@$=cwQ9L507Nm3xydjgyy3qWt7Tp3H?6!0VEukQxLeJkba zy%wATevb4trS#dYl&e#v!71Q}lwDI&q;I8M9a#oW0Y6RYn!+i6E9L5JC2$J(fe+V| zDYaWESBb>n6!7}p*AzvaD++iuZ*Ty3@ziSoj^P#Hx|AyTQ1GHp*F%$R{ul~g0t%b} zUhd+Wu;6q>_`Bj4;2`j-1lJ%R*DKHsfxopz0XPsm$^RPY=k{NK*UJTPAb86BH89Zq zzW}cdPjDc30{S&DLhPuaa5z3z228Z2Me=GFvW zaO$twrq@ge2%k3)f17&>PWd&*@+M`-2b^-N4*NCPDmYJ)BA9%68ts3|0aMq92{_U8@Ez^jbOh?@ri(d_)9!H_+n?K8;siAaDS9{M9uA zG5P8qZboB)4+h^#xE?&2^2ZDK2XQ~(Q1JccYiM4^KY?EDK7&L5{!U$T{t5JI3j`ec ztLyFhJ|Q3^if&H+ZH_o_)~^nnn=ILq+gX3FpMUiN++?YIx}Ej+su_GKc}>$QyP7z? bn+r=B30NT010 (2035),C40, +PBA50+_OffModel_Carshare,Main Sheet,Total_pop_lodensity,Total population in TAZs with density <10 (2035),C41, +PBA50+_OffModel_Carshare,Main Sheet,Adult_pop_hidensity,Adult pop (age 20-64) in TAZs with density >10 (2035),C42, +PBA50+_OffModel_Carshare,Main Sheet,Adult_pop_lodensity,Adult pop (age 20-64) in TAZs with density <10 (2035),C43, +PBA50+_OffModel_Carshare,Main Sheet,TradCarshare_members,Number of traditional car share members (2035),C44, +PBA50+_OffModel_Carshare,Main Sheet,OneWayCarshare_members,Number of one-way car share members (2035),C45, +PBA50+_OffModel_Carshare,Main Sheet,Out_daily_VMT_reduced,Total daily VMT reductions from car sharing members driving less (2035),C47, +PBA50+_OffModel_Carshare,Main Sheet,Out_daily_GHG_reduced_VMT,Daily GHG reductions from car sharing members driving less (tons) (2035),C48, +PBA50+_OffModel_Carshare,Main Sheet,Out_daily_GHG_reduced_FE,Daily GHG reductions from car sharing members driving more fuel efficient vehicles (tons) (2035),C49, +PBA50+_OffModel_Carshare,Main Sheet,Out_daily_GHG_reduced,Total daily GHG reductions (tons) (2035),C50, +PBA50+_OffModel_Carshare,Main Sheet,Out_per_capita_GHG_reduced,Per capita GHG reductions (2035),C51, +PBA50+_OffModel_Carshare,Main Sheet,Run_directory,,C36,D36 +PBA50+_OffModel_Carshare,Main Sheet,year,,C37,D37 +PBA50+_OffModel_Carshare,Main Sheet,k_min_pop_density,,B5, +PBA50+_OffModel_Ebike,Main Sheet,Total_Funding,Total amount of funding,B5, +PBA50+_OffModel_Ebike,Main Sheet,Incentive_Amount,Incentive amount,B6, +PBA50+_OffModel_Ebike,Main Sheet,Ebikes_Incentivized,Number of e-bike purchases incentivized,B7, +PBA50+_OffModel_Ebike,Main Sheet,Start_year,Start year,B8, +PBA50+_OffModel_Ebike,Main Sheet,End_year,End year,B9, +PBA50+_OffModel_Ebike,Main Sheet,Incentive_Caused_Fraction,Fraction of incentives going to buyers who wouldn't have purchased without them,B12, +PBA50+_OffModel_Ebike,Main Sheet,Monthly_Car_Replacing_VMT,Car replacing VMT (miles per month per bike),B15, +PBA50+_OffModel_Ebike,Main Sheet,Disuse_Factor,Longterm Dis-use factor,B18, +PBA50+_OffModel_Ebike,Main Sheet,Out_daily_VMT_reduced,Total daily VMT reductions - all (2035),C24, +PBA50+_OffModel_Ebike,Main Sheet,Out_daily_GHG_reduced,Total daily GHG reductions (tons) (2035),C25, +PBA50+_OffModel_Ebike,Main Sheet,Out_per_capita_GHG_reduced,Per capita GHG reductions (2035),C26, +PBA50+_OffModel_Ebike,Main Sheet,year,,C30,D30 +PBA50+_OffModel_Ebike,Main Sheet,Run_directory,,C29,D29 +PBA50+_OffModel_RegionalCharger,Main Sheet,Num_charger_incentives,Chargers to fund (ports),B5, +PBA50+_OffModel_RegionalCharger,Main Sheet,Program_start_year,Start year of program,B6, +PBA50+_OffModel_RegionalCharger,Main Sheet,Program_end_year,End year of program,B7, +PBA50+_OffModel_RegionalCharger,Main Sheet,ACCII_toggle,,B9, +PBA50+_OffModel_RegionalCharger,Main Sheet,L2_charger_share,Share of incentivzed L2 chargers,B11, +PBA50+_OffModel_RegionalCharger,Main Sheet,DCFC_50kW_charger_share,Share of incentivzed DCFC (50 kW) chargers,B12, +PBA50+_OffModel_RegionalCharger,Main Sheet,DCFC_150kW_charger_share,Share of incentivzed DCFC (150 kW) chargers,B13, +PBA50+_OffModel_RegionalCharger,Main Sheet,DCFC_250kW_charger_share,Share of incentivzed DCFC (250 kW) chargers,B14, +PBA50+_OffModel_RegionalCharger,Main Sheet,DCFC_350kW_charger_share,Share of incentivzed DCFC (350 kW) chargers,B15, +PBA50+_OffModel_RegionalCharger,Main Sheet,L2_charger_events_daily,Average L2 charger events per day,B17, +PBA50+_OffModel_RegionalCharger,Main Sheet,DCFC_50kW_charger_events_daily,Average DCFC 50kW charger events per day,B18, +PBA50+_OffModel_RegionalCharger,Main Sheet,DCFC_150kW_charger_events_daily,Average DCFC 150kW charger events per day,B19, +PBA50+_OffModel_RegionalCharger,Main Sheet,DCFC_250kW_charger_events_daily,Average DCFC 250kW charger events per day,B20, +PBA50+_OffModel_RegionalCharger,Main Sheet,DCFC_350kW_charger_events_daily,Average DCFC 350kW charger events per day,B21, +PBA50+_OffModel_RegionalCharger,Main Sheet,L2_charger_incentive,Incentive amount for L2 chargers,B23, +PBA50+_OffModel_RegionalCharger,Main Sheet,DCFC_charger_incentive,Incentive amount for DCFC chargers,B24, +PBA50+_OffModel_RegionalCharger,Main Sheet,Out_daily_GHG_reduced,Total daily GHG reductions (tons) (2035),C34, +PBA50+_OffModel_RegionalCharger,Main Sheet,Out_per_capita_GHG_reduced,Per capita GHG reductions (2035),C35, +PBA50+_OffModel_RegionalCharger,Main Sheet,Total_program_cost,Total cumulative program cost (YOE$) (2035),C36, +PBA50+_OffModel_RegionalCharger,Main Sheet,Number_L2_incentivized,,B26, +PBA50+_OffModel_RegionalCharger,Main Sheet,Number_DCFC_50kW_incentivized,,B27, +PBA50+_OffModel_RegionalCharger,Main Sheet,Number_DCFC_150kW_incentivized,,B28, +PBA50+_OffModel_RegionalCharger,Main Sheet,Number_DCFC_250kW_incentivized,,B29, +PBA50+_OffModel_RegionalCharger,Main Sheet,Number_DCFC_350kW_incentivized,,B30, +PBA50+_OffModel_RegionalCharger,Main Sheet,year,,C33,D33 +PBA50+_OffModel_RegionalCharger,Main Sheet,Run_directory,,C32,D32 +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Run_directory,,C26,D26 +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Annual_investment_residential,Annual investment in residential programs ($2020),B5, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Annual_investment_employee,Annual investment in employee programs ($2020),B6, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Start_year,Start year of programs,B7, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Average_cost_household,Average cost/year of marketing to a household,B9, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Average_cost_employee,Average cost/year of marketing to an employee,B10, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Average_residential_penetration,Average residential penetration rate,B11, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Average_employee_penetration,Average employee penetration rate,B12, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Average_reduction_in_households,Average reduction in SOV mode share for participating households,B13, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Average_reduction_in_employees,Average reduction in SOV mode share for participating employees,B14, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Average_daily_one_way_trips_near_transit,Average daily one-way driving trips per household for households living near transit,B15, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Average_daily_commute_trips,Average daily one-way employee commute trips,B16, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Average_years_behavior_persists,Average number of years for which behavior change persists,B17, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Total_households_baseline,"Total households, 2015",B19, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Total_jobs_baseline,"Total jobs, 2015",B20, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Current_households_near_transit,Current total households living near transit stations,B22, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Current_employees_near_transit,Current total employees living near transit stations,B23, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Total_households,Total households (2035),C29, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Total_jobs,Total jobs (2035),C30, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Average_one_way_trip_length,Average one-way trip length for all trips (miles) (2035),C31, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Average_one_way_trip_length_for_home_based_work_drive_trips,Average one-way trip length for home-based work drive alone trips (miles) (2035),C32, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Total_households_that_are_likely_change,Total households that are likely to change behavior (2035),C34, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Total_employees_that_are_likely_change,Total employees that are likely to change behavior (2035),C35, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Total_households_change,Total households who change behavior (2035),C36, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Total_employees_change,Total employees who change behavior (2035),C37, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Total_daily_trip_reductions_HH,Total daily vehicle trip reductions - households (2035),C38, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Total_daily_trip_reductions_emp,Total daily vehicle trip reductions - employees (2035),C39, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Total_daily_trip_reductions,Total daily vehicle trip reductions - all (2035),C40, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Total_daily_VMT_reductions_HH,Total daily VMT reductions - households (2035),C41, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Total_daily_VMT_reductions_emp,Total daily VMT reductions - employees (2035),C42, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Out_daily_VMT_reduced,Total daily VMT reductions - all (2035),C43, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Out_daily_GHG_reduced,Total daily GHG reductions (tons) (2035),C44, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Out_per_capita_GHG_reduced,Per capita GHG reductions (2035),C45, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Total_program_cost_res,"Total cumulative program cost, residential (YOE$) (2035)",C46, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Total_program_cost_employee,"Total cumulative program cost, employee (YOE$) (2035)",C47, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,Total_program_cost,"Total cumulative program cost, all (YOE$) (2035)",C48, +PBA50+_OffModel_TargetedTransAlt,Main Sheet,year,,C27,D27 +PBA50+_OffModel_Vanpools,Main Sheet,Baseline_vanpool_vans,"Baseline number of vans, 2005",B4, +PBA50+_OffModel_Vanpools,Main Sheet,OneWay_trip_distance,One-way trip mileage (2035),C7, +PBA50+_OffModel_Vanpools,Main Sheet,Vanpool_occupancy,Average vanpool occupancy (2035),C8, +PBA50+_OffModel_Vanpools,Main Sheet,Vanpool_vans,Vanpool Program vans (2035),C9, +PBA50+_OffModel_Vanpools,Main Sheet,Vanpool_one_way_vehicle_trip_reductions,Total daily one-way vehicle trip reductions (2035),C15, +PBA50+_OffModel_Vanpools,Main Sheet,Out_daily_VMT_reduced,Total daily VMT reductions (2035),C16, +PBA50+_OffModel_Vanpools,Main Sheet,Out_daily_GHG_reduced,Total daily GHG reductions (tons) (2035),C17, +PBA50+_OffModel_Vanpools,Main Sheet,Out_per_capita_GHG_reduced,Per capita GHG reductions (2035),C18, +PBA50+_OffModel_Vanpools,Main Sheet,Run_directory,,C12,D12 +PBA50+_OffModel_Vanpools,Main Sheet,year,,C13,D13 +PBA50+_OffModel_VehicleBuyback,Main Sheet,Num_EV_incentives,Number of EVs to deploy,B5, +PBA50+_OffModel_VehicleBuyback,Main Sheet,Vehicle_replacement_age,Age of vehicle being replaced,B6, +PBA50+_OffModel_VehicleBuyback,Main Sheet,Program_start_year,Start year of program,B7, +PBA50+_OffModel_VehicleBuyback,Main Sheet,Program_end_year,End year of program,B8, +PBA50+_OffModel_VehicleBuyback,Main Sheet,Credit_sharing_toggle,Apply credit sharing adjustment,B10, +PBA50+_OffModel_VehicleBuyback,Main Sheet,PHEV_incentive_share,Share of incentivized PHEVs,B12, +PBA50+_OffModel_VehicleBuyback,Main Sheet,Average_PHEV_incentive,Average PHEV incentive,B15, +PBA50+_OffModel_VehicleBuyback,Main Sheet,Average_BEV_incentive,Average BEV incentive,B16, +PBA50+_OffModel_VehicleBuyback,Main Sheet,Out_daily_GHG_reduced,Total daily GHG reductions (tons) (2035),C23, +PBA50+_OffModel_VehicleBuyback,Main Sheet,Out_per_capita_GHG_reduced,Per capita GHG reductions (2035),C24, +PBA50+_OffModel_VehicleBuyback,Main Sheet,Total_program_cost,Total cumulative program cost (YOE$) (2035),C25, +PBA50+_OffModel_VehicleBuyback,Main Sheet,Incentive_fraction_PHEV,MPO EV Incentive Fraction for PHEV,L36, +PBA50+_OffModel_VehicleBuyback,Main Sheet,Incentive_fraction_BEV,MPO EV Incentive Fraction for BEV,M36, +PBA50+_OffModel_VehicleBuyback,Main Sheet,Rebate_count_Q1,Rebate Counts Q1,C30, +PBA50+_OffModel_VehicleBuyback,Main Sheet,Rebate_count_Q2,Rebate Counts Q2,C31, +PBA50+_OffModel_VehicleBuyback,Main Sheet,Rebate_count_Q3,Rebate Counts Q3,C32, +PBA50+_OffModel_VehicleBuyback,Main Sheet,Rebate_count_Q4,Rebate Counts Q4,C33, +PBA50+_OffModel_VehicleBuyback,Main Sheet,BEV_Rebate_amount_Q1,Rebate Amount BEV Q1,E30, +PBA50+_OffModel_VehicleBuyback,Main Sheet,BEV_Rebate_amount_Q2,Rebate Amount BEV Q2,E31, +PBA50+_OffModel_VehicleBuyback,Main Sheet,BEV_Rebate_amount_Q3,Rebate Amount BEV Q3,E32, +PBA50+_OffModel_VehicleBuyback,Main Sheet,BEV_Rebate_amount_Q4,Rebate Amount BEV Q4,E33, +PBA50+_OffModel_VehicleBuyback,Main Sheet,PHEV_Rebate_amount_Q1,Rebate Amount PHEV Q1,F30, +PBA50+_OffModel_VehicleBuyback,Main Sheet,PHEV_Rebate_amount_Q2,Rebate Amount PHEV Q2,F31, +PBA50+_OffModel_VehicleBuyback,Main Sheet,PHEV_Rebate_amount_Q3,Rebate Amount PHEV Q3,F32, +PBA50+_OffModel_VehicleBuyback,Main Sheet,PHEV_Rebate_amount_Q4,Rebate Amount PHEV Q4,F33, +PBA50+_OffModel_VehicleBuyback,Main Sheet,year,,C22,D22 +PBA50+_OffModel_VehicleBuyback,Main Sheet,Run_directory,,C21,D21