From e310134833a8913657b6f93ce53e8e477eebbf02 Mon Sep 17 00:00:00 2001 From: Sam West Date: Wed, 14 Feb 2024 11:47:59 +1100 Subject: [PATCH 01/26] Modified benchmark run in Windows to use direct function calls instead of subprocesses (which use way too much RAM) Added pyarrow to suppress pandas future warning --- .gitignore | 3 +- .python-version | 1 + benchmarks.yml | 488 ++++++++++++++++++++-------------------- pyproject.toml | 1 + utils/dd_to_csv.py | 8 +- utils/run_benchmarks.py | 73 +++--- xl2times/__main__.py | 16 +- 7 files changed, 311 insertions(+), 279 deletions(-) create mode 100644 .python-version diff --git a/.gitignore b/.gitignore index f2af748..0dd196b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,6 @@ ground_truth/* *.pyproj.* speedscope.json *.pkl -.venv/ +.venv*/ benchmarks/ +.idea/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..0c7d5f5 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11.4 diff --git a/benchmarks.yml b/benchmarks.yml index 3f3ed6b..1caa37b 100644 --- a/benchmarks.yml +++ b/benchmarks.yml @@ -19,79 +19,79 @@ benchmarks: dd_files: - "base" - "syssettings" - - name: DemoS_003-all - input_folder: DemoS_003 - dd_folder: DemoS_003-all - dd_files: - - "base" - - "syssettings" - - name: DemoS_004 - input_folder: DemoS_004 - inputs: - - "BY_Trans.xlsx" - - "Sets-DemoModels.xlsx" - - "SysSettings.xlsx" - - "VT_REG_PRI_V04.xlsx" - dd_folder: DemoS_004 - dd_files: - - "base" - - "syssettings" - - name: DemoS_004a - input_folder: DemoS_004 - inputs: - - "BY_Trans.xlsx" - - "Sets-DemoModels.xlsx" - - "SysSettings.xlsx" - - "SuppXLS/Scen_Peak_RSV.xlsx" - - "VT_REG_PRI_V04.xlsx" - dd_folder: DemoS_004a - dd_files: - - "base" - - "syssettings" - - "peak_rsv" - - name: DemoS_004b - input_folder: DemoS_004 - inputs: - - "BY_Trans.xlsx" - - "Sets-DemoModels.xlsx" - - "SysSettings.xlsx" - - "SuppXLS/Scen_Peak_RSV-FLX.xlsx" - - "VT_REG_PRI_V04.xlsx" - dd_folder: DemoS_004b - dd_files: - - "base" - - "syssettings" - - "peak_rsv-flx" - - name: DemoS_004-all - input_folder: DemoS_004 - dd_folder: DemoS_004-all - dd_files: - - "base" - - "syssettings" - - "peak_rsv" - - "peak_rsv-flx" - - name: DemoS_005-all - input_folder: DemoS_005 - dd_folder: DemoS_005-all - dd_files: - - "base" - - "syssettings" - - "trade_param" - - "co2_tax" - - "elc_co2_bound" - - "peak_rsv" - - "uc_co2bnd" - - name: DemoS_006-all - input_folder: DemoS_006 - dd_folder: DemoS_006-all - dd_files: - - "base" - - "newtechs" - - "syssettings" - - "trade_param" - - "elc_co2_bound" - - "peak_rsv" - - "uc_co2bnd" +# - name: DemoS_003-all +# input_folder: DemoS_003 +# dd_folder: DemoS_003-all +# dd_files: +# - "base" +# - "syssettings" +# - name: DemoS_004 +# input_folder: DemoS_004 +# inputs: +# - "BY_Trans.xlsx" +# - "Sets-DemoModels.xlsx" +# - "SysSettings.xlsx" +# - "VT_REG_PRI_V04.xlsx" +# dd_folder: DemoS_004 +# dd_files: +# - "base" +# - "syssettings" +# - name: DemoS_004a +# input_folder: DemoS_004 +# inputs: +# - "BY_Trans.xlsx" +# - "Sets-DemoModels.xlsx" +# - "SysSettings.xlsx" +# - "SuppXLS/Scen_Peak_RSV.xlsx" +# - "VT_REG_PRI_V04.xlsx" +# dd_folder: DemoS_004a +# dd_files: +# - "base" +# - "syssettings" +# - "peak_rsv" +# - name: DemoS_004b +# input_folder: DemoS_004 +# inputs: +# - "BY_Trans.xlsx" +# - "Sets-DemoModels.xlsx" +# - "SysSettings.xlsx" +# - "SuppXLS/Scen_Peak_RSV-FLX.xlsx" +# - "VT_REG_PRI_V04.xlsx" +# dd_folder: DemoS_004b +# dd_files: +# - "base" +# - "syssettings" +# - "peak_rsv-flx" +# - name: DemoS_004-all +# input_folder: DemoS_004 +# dd_folder: DemoS_004-all +# dd_files: +# - "base" +# - "syssettings" +# - "peak_rsv" +# - "peak_rsv-flx" +# - name: DemoS_005-all +# input_folder: DemoS_005 +# dd_folder: DemoS_005-all +# dd_files: +# - "base" +# - "syssettings" +# - "trade_param" +# - "co2_tax" +# - "elc_co2_bound" +# - "peak_rsv" +# - "uc_co2bnd" +# - name: DemoS_006-all +# input_folder: DemoS_006 +# dd_folder: DemoS_006-all +# dd_files: +# - "base" +# - "newtechs" +# - "syssettings" +# - "trade_param" +# - "elc_co2_bound" +# - "peak_rsv" +# - "uc_co2bnd" - name: DemoS_007-all input_folder: DemoS_007 dd_folder: DemoS_007-all @@ -108,174 +108,174 @@ benchmarks: - "tra_co2_bound" - "uc_co2bnd" - "uc_growth" - - name: DemoS_008-all - input_folder: DemoS_008 - dd_folder: DemoS_008-all - dd_files: - - "base" - - "newtechs" - - "syssettings" - - "trade_param" - - "demproj_dtcar" - - "elasticdem" - - "elc_co2_bound" - - "peak_rsv" - - "refinery" - - "tra_co2_bound" - - "uc_co2bnd" - - "uc_growth" - - "uc_nuc_maxcap" - - name: DemoS_009-all - input_folder: DemoS_009 - dd_folder: DemoS_009-all - dd_files: - - "base" - - "new-chp-dh" - - "new-ind" - - "newtechs" - - "syssettings" - - "trade_param" - - "demproj_dtcar" - - "elasticdem" - - "elc_co2_bound" - - "ind_newres" - - "peak_rsv" - - "refinery" - - "tra_co2_bound" - - "uc_co2bnd" - - "uc_dh_minprod" - - "uc_growth" - - "uc_nuc_maxcap" - - name: DemoS_010-all - input_folder: DemoS_010 - dd_folder: DemoS_010-all - dd_files: - - "base" - - "new-chp-dh" - - "new-ind" - - "newtechs" - - "syssettings" - - "dem_ref" - - "trade_param" - - "demproj_dtcar" - - "elasticdem" - - "elc_co2_bound" - - "ind_newres" - - "peak_rsv" - - "refinery" - - "tra_co2_bound" - - "uc_co2bnd" - - "uc_dh_minprod" - - "uc_growth" - - "uc_nuc_maxcap" - - name: DemoS_011-all - input_folder: DemoS_011 - dd_folder: DemoS_011-all - dd_files: - - "base" - - "new-chp-dh" - - "new-ind" - - "newtechs" - - "syssettings" - - "dem_ref" - - "trade_param" - - "bounds-uc_wsets" - - "demproj_dtcar" - - "elasticdem" - - "elc_co2_bound" - - "ind_newres" - - "peak_rsv" - - "refinery" - - "tra_co2_bound" - - "uc_co2bnd" - - "uc_dh_minprod" - - "uc_growth" - - "uc_nuc_maxcap" - - name: DemoS_012-all - input_folder: DemoS_012 - dd_folder: DemoS_012-all - dd_files: - - "base" - - "new-chp-dh" - - "new-ind" - - "newtechs" - - "syssettings" - - "dem_ref" - - "trade_param" - - "bnd_ppfossil" - - "bounds-uc_wsets" - - "co2_tax" - - "demproj_dtcar" - - "elasticdem" - - "elc_co2_bound" - - "ind_newres" - - "nuc_dscinv" - - "peak_rsv" - - "refinery" - - "solar_subsidies" - - "tra_co2_bound" - - "tradsl_tax" - - "uc_co2_regions" - - "uc_co2bnd" - - "uc_dh_minprod" - - "uc_growth" - - "uc_nuc_maxcap" - - name: Ireland - input_folder: Ireland - regions: "IE" - inputs: - - "VT_IE_AGR.xlsx" - - "VT_IE_IND.xlsx" - - "VT_IE_PWR.xlsx" - - "VT_IE_RSD.xlsx" - - "VT_IE_SRV.xlsx" - - "VT_IE_SUP.xlsx" - - "VT_IE_TRA.xlsx" - - "BY_Trans.xlsx" - - "SetRules.xlsx" - - "SuppXLS/Trades/ScenTrade__Trade_Links.xlsx" - - "SubRES_TMPL/SubRES_PWR_DH.xlsx" - - "SubRES_TMPL/SubRES_PWR_DH_Trans.xlsx" - - "SubRES_TMPL/SubRES_PWR_NewTechs.xlsx" - - "SubRES_TMPL/SubRES_PWR_NewTechs_Trans.xlsx" - - "SubRES_TMPL/SubRES_RSD_NewTechs.xlsx" - - "SubRES_TMPL/SubRES_RSD_NewTechs_Trans.xlsx" - - "SubRES_TMPL/SubRES_RSD-Retrofit.xlsx" - - "SubRES_TMPL/SubRES_RSD-Retrofit_Trans.xlsx" - - "SubRES_TMPL/SubRES_SRV_DC_ExcessHeat.xlsx" - - "SubRES_TMPL/SubRES_SRV_DC_ExcessHeat_Trans.xlsx" - - "SubRES_TMPL/SubRES_SRV_NewTechs.xlsx" - - "SubRES_TMPL/SubRES_SRV_NewTechs_Trans.xlsx" - - "SubRES_TMPL/SubRES_SUP_BioRefineries.xlsx" - - "SubRES_TMPL/SubRES_SUP_BioRefineries_Trans.xlsx" - - "SubRES_TMPL/SubRES_SUP_H2NewTechs.xlsx" - - "SubRES_TMPL/SubRES_SUP_H2NewTechs_Trans.xlsx" - - "SubRES_TMPL/SubRES_SYS_OtherNewTechs.xlsx" - - "SubRES_TMPL/SubRES_SYS_OtherNewTechs_Trans.xlsx" - - "SubRES_TMPL/SubRES_TRA_NewVehicles.xlsx" - - "SubRES_TMPL/SubRES_TRA_NewVehicles_Trans.xlsx" - - "SysSettings.xlsx" - - "SuppXLS/Scen_A_SYS_SAD_40TS.xlsx" - - "SuppXLS/Scen_B_SYS_Additional_Assumptions.xlsx" - - "SuppXLS/Scen_B_SYS_Demands.xlsx" - - "SuppXLS/Scen_B_SUP_DomBioPot_Baseline.xlsx" - - "SuppXLS/Scen_B_IND_Emi_Proc.xlsx" - - "SuppXLS/Scen_B_PWR_CCS.xlsx" - - "SuppXLS/Scen_B_SRV_DC_EH.xlsx" - - "SuppXLS/Scen_B_PWR_RNW_Potentials.xlsx" - - "SuppXLS/Scen_B_IND_Emissions.xlsx" - - "SuppXLS/Scen_B_RSD_Retrofit-Ctrl.xlsx" - - "SuppXLS/Scen_B_RSD_UC.xlsx" - - "SuppXLS/Scen_B_SRV_UC.xlsx" - - "SuppXLS/Scen_B_PWR_SNSP_Limit.xlsx" - - "SuppXLS/Scen_B_SYS_Bio_DelivCost.xlsx" - - "SuppXLS/Scen_B_SYS_Historic_Bounds.xlsx" - - "SuppXLS/Scen_B_SYS_MaxGrowthRates.xlsx" - - "SuppXLS/Scen_B_RSD_UnitBoilers.xlsx" - - "SuppXLS/Scen_B_TRA_P_ModalShares.xlsx" - - "SuppXLS/Scen_B_TRA_NewCars_Retirement.xlsx" - - "SuppXLS/Scen_B_TRA_Stock_Retirement.xlsx" - - "SuppXLS/Scen_B_TRA_Emissions.xlsx" - - "SuppXLS/Scen_B_TRA_EV_Parity.xlsx" - - "SuppXLS/Scen_B_TRA_F_ModalShares.xlsx" - dd_folder: Ireland +# - name: DemoS_008-all +# input_folder: DemoS_008 +# dd_folder: DemoS_008-all +# dd_files: +# - "base" +# - "newtechs" +# - "syssettings" +# - "trade_param" +# - "demproj_dtcar" +# - "elasticdem" +# - "elc_co2_bound" +# - "peak_rsv" +# - "refinery" +# - "tra_co2_bound" +# - "uc_co2bnd" +# - "uc_growth" +# - "uc_nuc_maxcap" +# - name: DemoS_009-all +# input_folder: DemoS_009 +# dd_folder: DemoS_009-all +# dd_files: +# - "base" +# - "new-chp-dh" +# - "new-ind" +# - "newtechs" +# - "syssettings" +# - "trade_param" +# - "demproj_dtcar" +# - "elasticdem" +# - "elc_co2_bound" +# - "ind_newres" +# - "peak_rsv" +# - "refinery" +# - "tra_co2_bound" +# - "uc_co2bnd" +# - "uc_dh_minprod" +# - "uc_growth" +# - "uc_nuc_maxcap" +# - name: DemoS_010-all +# input_folder: DemoS_010 +# dd_folder: DemoS_010-all +# dd_files: +# - "base" +# - "new-chp-dh" +# - "new-ind" +# - "newtechs" +# - "syssettings" +# - "dem_ref" +# - "trade_param" +# - "demproj_dtcar" +# - "elasticdem" +# - "elc_co2_bound" +# - "ind_newres" +# - "peak_rsv" +# - "refinery" +# - "tra_co2_bound" +# - "uc_co2bnd" +# - "uc_dh_minprod" +# - "uc_growth" +# - "uc_nuc_maxcap" +# - name: DemoS_011-all +# input_folder: DemoS_011 +# dd_folder: DemoS_011-all +# dd_files: +# - "base" +# - "new-chp-dh" +# - "new-ind" +# - "newtechs" +# - "syssettings" +# - "dem_ref" +# - "trade_param" +# - "bounds-uc_wsets" +# - "demproj_dtcar" +# - "elasticdem" +# - "elc_co2_bound" +# - "ind_newres" +# - "peak_rsv" +# - "refinery" +# - "tra_co2_bound" +# - "uc_co2bnd" +# - "uc_dh_minprod" +# - "uc_growth" +# - "uc_nuc_maxcap" +# - name: DemoS_012-all +# input_folder: DemoS_012 +# dd_folder: DemoS_012-all +# dd_files: +# - "base" +# - "new-chp-dh" +# - "new-ind" +# - "newtechs" +# - "syssettings" +# - "dem_ref" +# - "trade_param" +# - "bnd_ppfossil" +# - "bounds-uc_wsets" +# - "co2_tax" +# - "demproj_dtcar" +# - "elasticdem" +# - "elc_co2_bound" +# - "ind_newres" +# - "nuc_dscinv" +# - "peak_rsv" +# - "refinery" +# - "solar_subsidies" +# - "tra_co2_bound" +# - "tradsl_tax" +# - "uc_co2_regions" +# - "uc_co2bnd" +# - "uc_dh_minprod" +# - "uc_growth" +# - "uc_nuc_maxcap" +# - name: Ireland +# input_folder: Ireland +# regions: "IE" +# inputs: +# - "VT_IE_AGR.xlsx" +# - "VT_IE_IND.xlsx" +# - "VT_IE_PWR.xlsx" +# - "VT_IE_RSD.xlsx" +# - "VT_IE_SRV.xlsx" +# - "VT_IE_SUP.xlsx" +# - "VT_IE_TRA.xlsx" +# - "BY_Trans.xlsx" +# - "SetRules.xlsx" +# - "SuppXLS/Trades/ScenTrade__Trade_Links.xlsx" +# - "SubRES_TMPL/SubRES_PWR_DH.xlsx" +# - "SubRES_TMPL/SubRES_PWR_DH_Trans.xlsx" +# - "SubRES_TMPL/SubRES_PWR_NewTechs.xlsx" +# - "SubRES_TMPL/SubRES_PWR_NewTechs_Trans.xlsx" +# - "SubRES_TMPL/SubRES_RSD_NewTechs.xlsx" +# - "SubRES_TMPL/SubRES_RSD_NewTechs_Trans.xlsx" +# - "SubRES_TMPL/SubRES_RSD-Retrofit.xlsx" +# - "SubRES_TMPL/SubRES_RSD-Retrofit_Trans.xlsx" +# - "SubRES_TMPL/SubRES_SRV_DC_ExcessHeat.xlsx" +# - "SubRES_TMPL/SubRES_SRV_DC_ExcessHeat_Trans.xlsx" +# - "SubRES_TMPL/SubRES_SRV_NewTechs.xlsx" +# - "SubRES_TMPL/SubRES_SRV_NewTechs_Trans.xlsx" +# - "SubRES_TMPL/SubRES_SUP_BioRefineries.xlsx" +# - "SubRES_TMPL/SubRES_SUP_BioRefineries_Trans.xlsx" +# - "SubRES_TMPL/SubRES_SUP_H2NewTechs.xlsx" +# - "SubRES_TMPL/SubRES_SUP_H2NewTechs_Trans.xlsx" +# - "SubRES_TMPL/SubRES_SYS_OtherNewTechs.xlsx" +# - "SubRES_TMPL/SubRES_SYS_OtherNewTechs_Trans.xlsx" +# - "SubRES_TMPL/SubRES_TRA_NewVehicles.xlsx" +# - "SubRES_TMPL/SubRES_TRA_NewVehicles_Trans.xlsx" +# - "SysSettings.xlsx" +# - "SuppXLS/Scen_A_SYS_SAD_40TS.xlsx" +# - "SuppXLS/Scen_B_SYS_Additional_Assumptions.xlsx" +# - "SuppXLS/Scen_B_SYS_Demands.xlsx" +# - "SuppXLS/Scen_B_SUP_DomBioPot_Baseline.xlsx" +# - "SuppXLS/Scen_B_IND_Emi_Proc.xlsx" +# - "SuppXLS/Scen_B_PWR_CCS.xlsx" +# - "SuppXLS/Scen_B_SRV_DC_EH.xlsx" +# - "SuppXLS/Scen_B_PWR_RNW_Potentials.xlsx" +# - "SuppXLS/Scen_B_IND_Emissions.xlsx" +# - "SuppXLS/Scen_B_RSD_Retrofit-Ctrl.xlsx" +# - "SuppXLS/Scen_B_RSD_UC.xlsx" +# - "SuppXLS/Scen_B_SRV_UC.xlsx" +# - "SuppXLS/Scen_B_PWR_SNSP_Limit.xlsx" +# - "SuppXLS/Scen_B_SYS_Bio_DelivCost.xlsx" +# - "SuppXLS/Scen_B_SYS_Historic_Bounds.xlsx" +# - "SuppXLS/Scen_B_SYS_MaxGrowthRates.xlsx" +# - "SuppXLS/Scen_B_RSD_UnitBoilers.xlsx" +# - "SuppXLS/Scen_B_TRA_P_ModalShares.xlsx" +# - "SuppXLS/Scen_B_TRA_NewCars_Retirement.xlsx" +# - "SuppXLS/Scen_B_TRA_Stock_Retirement.xlsx" +# - "SuppXLS/Scen_B_TRA_Emissions.xlsx" +# - "SuppXLS/Scen_B_TRA_EV_Parity.xlsx" +# - "SuppXLS/Scen_B_TRA_F_ModalShares.xlsx" +# dd_folder: Ireland diff --git a/pyproject.toml b/pyproject.toml index 473c373..a5dd4d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "more-itertools", "openpyxl >= 3.0, < 3.1", "pandas >= 2.1", + "pyarrow" ] [project.optional-dependencies] diff --git a/utils/dd_to_csv.py b/utils/dd_to_csv.py index df444e7..5de10fd 100644 --- a/utils/dd_to_csv.py +++ b/utils/dd_to_csv.py @@ -216,7 +216,7 @@ def convert_dd_to_tabular( return -if __name__ == "__main__": +def main(arg_list: None | list[str] = None): args_parser = argparse.ArgumentParser() args_parser.add_argument( "input_dir", type=str, help="Input directory containing .dd files." @@ -224,5 +224,9 @@ def convert_dd_to_tabular( args_parser.add_argument( "output_dir", type=str, help="Output directory to save the .csv files in." ) - args = args_parser.parse_args() + args = args_parser.parse_args(arg_list) convert_dd_to_tabular(args.input_dir, args.output_dir, generate_headers_by_attr()) + + +if __name__ == "__main__": + main() diff --git a/utils/run_benchmarks.py b/utils/run_benchmarks.py index f2a9a91..a9bb9d7 100644 --- a/utils/run_benchmarks.py +++ b/utils/run_benchmarks.py @@ -1,4 +1,6 @@ import argparse +import os +from collections import namedtuple from concurrent.futures import ProcessPoolExecutor from functools import partial import git @@ -10,7 +12,7 @@ import sys from tabulate import tabulate import time -from typing import Any, Tuple +from typing import Any, Tuple, NamedTuple import yaml @@ -134,23 +136,32 @@ def run_benchmark( # First convert ground truth DD to csv if not skip_csv: shutil.rmtree(csv_folder, ignore_errors=True) - res = subprocess.run( - [ - "python", - "utils/dd_to_csv.py", - dd_folder, - csv_folder, - ], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - if res.returncode != 0: - # Remove partial outputs - shutil.rmtree(csv_folder, ignore_errors=True) - print(res.stdout) - print(f"ERROR: dd_to_csv failed on {benchmark['name']}") - sys.exit(1) + if os.name != "nt": + res = subprocess.run( + [ + "python", + "utils/dd_to_csv.py", + dd_folder, + csv_folder, + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + # windows needs this for subprocess to inherit venv: + shell=True if os.name == "nt" else False, + ) + if res.returncode != 0: + # Remove partial outputs + shutil.rmtree(csv_folder, ignore_errors=True) + print(res.stdout) + print(f"ERROR: dd_to_csv failed on {benchmark['name']}") + sys.exit(1) + else: + # subprocesses use too much RAM in windows, just use function call in current process instead + from utils.dd_to_csv import main + + main([dd_folder, csv_folder]) + elif not path.exists(csv_folder): print(f"ERROR: --skip_csv is true but {csv_folder} does not exist") sys.exit(1) @@ -170,22 +181,32 @@ def run_benchmark( else: args.append(xl_folder) start = time.time() - res = subprocess.run( - ["xl2times"] + args, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) + res = None + if os.name != "nt": + res = subprocess.run( + ["xl2times"] + args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + else: + # Subprocesses are heavyweight in windows use too much RAM, use function calls instead + from xl2times.__main__ import main + + summary = main(args) + # TODO this is a hack to make the return value look like it's from a subprocess. Replace subprocess calls above with function calls? + res = namedtuple("stdout", ["stdout", "stderr", "returncode"])(summary, "", 0) + runtime = time.time() - start - if verbose: + if verbose and res is not None: line = "-" * 80 print(f"\n{line}\n{benchmark['name']}\n{line}\n\n{res.stdout}") print(res.stderr if res.stderr is not None else "") else: print(".", end="", flush=True) - if res.returncode != 0: + if res is not None and res.returncode != 0: print(res.stdout) print(f"ERROR: tool failed on {benchmark['name']}") sys.exit(1) diff --git a/xl2times/__main__.py b/xl2times/__main__.py index f954525..6b05dfd 100644 --- a/xl2times/__main__.py +++ b/xl2times/__main__.py @@ -157,7 +157,7 @@ def read_csv_tables(input_dir: str) -> Dict[str, DataFrame]: def compare( data: Dict[str, DataFrame], ground_truth: Dict[str, DataFrame], output_dir: str -): +) -> str: print( f"Ground truth contains {len(ground_truth)} tables," f" {sum(df.shape[0] for _, df in ground_truth.items())} rows" @@ -220,13 +220,15 @@ def compare( os.path.join(output_dir, table_name + "_missing.csv"), index=False, ) - - print( + result = ( f"{total_correct_rows / total_gt_rows :.1%} of ground truth rows present" f" in output ({total_correct_rows}/{total_gt_rows})" f", {total_additional_rows} additional rows" ) + print(result) + return result + def produce_times_tables( config: datatypes.Config, input: Dict[str, DataFrame] @@ -382,7 +384,7 @@ def dump_tables(tables: List, filename: str) -> List: return tables -def main(): +def main(arg_list: None | list[str] = None) -> str: args_parser = argparse.ArgumentParser() args_parser.add_argument( "input", @@ -416,7 +418,7 @@ def main(): action="store_true", help="Verbose mode: print tables after every transform", ) - args = args_parser.parse_args() + args = args_parser.parse_args(arg_list) config = datatypes.Config( "times_mapping.txt", @@ -465,7 +467,9 @@ def main(): if args.ground_truth_dir: ground_truth = read_csv_tables(args.ground_truth_dir) - compare(tables, ground_truth, args.output_dir) + comparison = compare(tables, ground_truth, args.output_dir) + return comparison + return "" if __name__ == "__main__": From 43af588306a476dc2d590b406822f137b5d430d7 Mon Sep 17 00:00:00 2001 From: Sam West Date: Wed, 14 Feb 2024 12:02:14 +1100 Subject: [PATCH 02/26] testing regression code --- benchmarks.yml | 270 ++++++++++++++++++++-------------------- utils/run_benchmarks.py | 6 +- 2 files changed, 139 insertions(+), 137 deletions(-) diff --git a/benchmarks.yml b/benchmarks.yml index 1caa37b..cfdec2b 100644 --- a/benchmarks.yml +++ b/benchmarks.yml @@ -7,18 +7,18 @@ benchmarks_folder: benchmarks/ benchmarks: - - name: DemoS_001-all - input_folder: DemoS_001 - dd_folder: DemoS_001-all - dd_files: - - "base" - - "syssettings" - - name: DemoS_002-all - input_folder: DemoS_002 - dd_folder: DemoS_002-all - dd_files: - - "base" - - "syssettings" +# - name: DemoS_001-all +# input_folder: DemoS_001 +# dd_folder: DemoS_001-all +# dd_files: +# - "base" +# - "syssettings" +# - name: DemoS_002-all +# input_folder: DemoS_002 +# dd_folder: DemoS_002-all +# dd_files: +# - "base" +# - "syssettings" # - name: DemoS_003-all # input_folder: DemoS_003 # dd_folder: DemoS_003-all @@ -92,94 +92,49 @@ benchmarks: # - "elc_co2_bound" # - "peak_rsv" # - "uc_co2bnd" - - name: DemoS_007-all - input_folder: DemoS_007 - dd_folder: DemoS_007-all - dd_files: - - "base" - - "newtechs" - - "syssettings" - - "trade_param" - - "demproj_dtcar" - - "elasticdem" - - "elc_co2_bound" - - "peak_rsv" - - "refinery" - - "tra_co2_bound" - - "uc_co2bnd" - - "uc_growth" -# - name: DemoS_008-all -# input_folder: DemoS_008 -# dd_folder: DemoS_008-all -# dd_files: -# - "base" -# - "newtechs" -# - "syssettings" -# - "trade_param" -# - "demproj_dtcar" -# - "elasticdem" -# - "elc_co2_bound" -# - "peak_rsv" -# - "refinery" -# - "tra_co2_bound" -# - "uc_co2bnd" -# - "uc_growth" -# - "uc_nuc_maxcap" -# - name: DemoS_009-all -# input_folder: DemoS_009 -# dd_folder: DemoS_009-all +# - name: DemoS_007-all +# input_folder: DemoS_007 +# dd_folder: DemoS_007-all # dd_files: # - "base" -# - "new-chp-dh" -# - "new-ind" # - "newtechs" # - "syssettings" # - "trade_param" # - "demproj_dtcar" # - "elasticdem" # - "elc_co2_bound" -# - "ind_newres" # - "peak_rsv" # - "refinery" # - "tra_co2_bound" # - "uc_co2bnd" -# - "uc_dh_minprod" # - "uc_growth" -# - "uc_nuc_maxcap" -# - name: DemoS_010-all -# input_folder: DemoS_010 -# dd_folder: DemoS_010-all +# - name: DemoS_008-all +# input_folder: DemoS_008 +# dd_folder: DemoS_008-all # dd_files: # - "base" -# - "new-chp-dh" -# - "new-ind" # - "newtechs" # - "syssettings" -# - "dem_ref" # - "trade_param" # - "demproj_dtcar" # - "elasticdem" # - "elc_co2_bound" -# - "ind_newres" # - "peak_rsv" # - "refinery" # - "tra_co2_bound" # - "uc_co2bnd" -# - "uc_dh_minprod" # - "uc_growth" # - "uc_nuc_maxcap" -# - name: DemoS_011-all -# input_folder: DemoS_011 -# dd_folder: DemoS_011-all +# - name: DemoS_009-all +# input_folder: DemoS_009 +# dd_folder: DemoS_009-all # dd_files: # - "base" # - "new-chp-dh" # - "new-ind" # - "newtechs" # - "syssettings" -# - "dem_ref" # - "trade_param" -# - "bounds-uc_wsets" # - "demproj_dtcar" # - "elasticdem" # - "elc_co2_bound" @@ -191,9 +146,9 @@ benchmarks: # - "uc_dh_minprod" # - "uc_growth" # - "uc_nuc_maxcap" -# - name: DemoS_012-all -# input_folder: DemoS_012 -# dd_folder: DemoS_012-all +# - name: DemoS_010-all +# input_folder: DemoS_010 +# dd_folder: DemoS_010-all # dd_files: # - "base" # - "new-chp-dh" @@ -202,80 +157,125 @@ benchmarks: # - "syssettings" # - "dem_ref" # - "trade_param" -# - "bnd_ppfossil" -# - "bounds-uc_wsets" -# - "co2_tax" # - "demproj_dtcar" # - "elasticdem" # - "elc_co2_bound" # - "ind_newres" -# - "nuc_dscinv" # - "peak_rsv" # - "refinery" -# - "solar_subsidies" # - "tra_co2_bound" -# - "tradsl_tax" -# - "uc_co2_regions" # - "uc_co2bnd" # - "uc_dh_minprod" # - "uc_growth" # - "uc_nuc_maxcap" -# - name: Ireland -# input_folder: Ireland -# regions: "IE" -# inputs: -# - "VT_IE_AGR.xlsx" -# - "VT_IE_IND.xlsx" -# - "VT_IE_PWR.xlsx" -# - "VT_IE_RSD.xlsx" -# - "VT_IE_SRV.xlsx" -# - "VT_IE_SUP.xlsx" -# - "VT_IE_TRA.xlsx" -# - "BY_Trans.xlsx" -# - "SetRules.xlsx" -# - "SuppXLS/Trades/ScenTrade__Trade_Links.xlsx" -# - "SubRES_TMPL/SubRES_PWR_DH.xlsx" -# - "SubRES_TMPL/SubRES_PWR_DH_Trans.xlsx" -# - "SubRES_TMPL/SubRES_PWR_NewTechs.xlsx" -# - "SubRES_TMPL/SubRES_PWR_NewTechs_Trans.xlsx" -# - "SubRES_TMPL/SubRES_RSD_NewTechs.xlsx" -# - "SubRES_TMPL/SubRES_RSD_NewTechs_Trans.xlsx" -# - "SubRES_TMPL/SubRES_RSD-Retrofit.xlsx" -# - "SubRES_TMPL/SubRES_RSD-Retrofit_Trans.xlsx" -# - "SubRES_TMPL/SubRES_SRV_DC_ExcessHeat.xlsx" -# - "SubRES_TMPL/SubRES_SRV_DC_ExcessHeat_Trans.xlsx" -# - "SubRES_TMPL/SubRES_SRV_NewTechs.xlsx" -# - "SubRES_TMPL/SubRES_SRV_NewTechs_Trans.xlsx" -# - "SubRES_TMPL/SubRES_SUP_BioRefineries.xlsx" -# - "SubRES_TMPL/SubRES_SUP_BioRefineries_Trans.xlsx" -# - "SubRES_TMPL/SubRES_SUP_H2NewTechs.xlsx" -# - "SubRES_TMPL/SubRES_SUP_H2NewTechs_Trans.xlsx" -# - "SubRES_TMPL/SubRES_SYS_OtherNewTechs.xlsx" -# - "SubRES_TMPL/SubRES_SYS_OtherNewTechs_Trans.xlsx" -# - "SubRES_TMPL/SubRES_TRA_NewVehicles.xlsx" -# - "SubRES_TMPL/SubRES_TRA_NewVehicles_Trans.xlsx" -# - "SysSettings.xlsx" -# - "SuppXLS/Scen_A_SYS_SAD_40TS.xlsx" -# - "SuppXLS/Scen_B_SYS_Additional_Assumptions.xlsx" -# - "SuppXLS/Scen_B_SYS_Demands.xlsx" -# - "SuppXLS/Scen_B_SUP_DomBioPot_Baseline.xlsx" -# - "SuppXLS/Scen_B_IND_Emi_Proc.xlsx" -# - "SuppXLS/Scen_B_PWR_CCS.xlsx" -# - "SuppXLS/Scen_B_SRV_DC_EH.xlsx" -# - "SuppXLS/Scen_B_PWR_RNW_Potentials.xlsx" -# - "SuppXLS/Scen_B_IND_Emissions.xlsx" -# - "SuppXLS/Scen_B_RSD_Retrofit-Ctrl.xlsx" -# - "SuppXLS/Scen_B_RSD_UC.xlsx" -# - "SuppXLS/Scen_B_SRV_UC.xlsx" -# - "SuppXLS/Scen_B_PWR_SNSP_Limit.xlsx" -# - "SuppXLS/Scen_B_SYS_Bio_DelivCost.xlsx" -# - "SuppXLS/Scen_B_SYS_Historic_Bounds.xlsx" -# - "SuppXLS/Scen_B_SYS_MaxGrowthRates.xlsx" -# - "SuppXLS/Scen_B_RSD_UnitBoilers.xlsx" -# - "SuppXLS/Scen_B_TRA_P_ModalShares.xlsx" -# - "SuppXLS/Scen_B_TRA_NewCars_Retirement.xlsx" -# - "SuppXLS/Scen_B_TRA_Stock_Retirement.xlsx" -# - "SuppXLS/Scen_B_TRA_Emissions.xlsx" -# - "SuppXLS/Scen_B_TRA_EV_Parity.xlsx" -# - "SuppXLS/Scen_B_TRA_F_ModalShares.xlsx" -# dd_folder: Ireland + - name: DemoS_011-all + input_folder: DemoS_011 + dd_folder: DemoS_011-all + dd_files: + - "base" + - "new-chp-dh" + - "new-ind" + - "newtechs" + - "syssettings" + - "dem_ref" + - "trade_param" + - "bounds-uc_wsets" + - "demproj_dtcar" + - "elasticdem" + - "elc_co2_bound" + - "ind_newres" + - "peak_rsv" + - "refinery" + - "tra_co2_bound" + - "uc_co2bnd" + - "uc_dh_minprod" + - "uc_growth" + - "uc_nuc_maxcap" + - name: DemoS_012-all + input_folder: DemoS_012 + dd_folder: DemoS_012-all + dd_files: + - "base" + - "new-chp-dh" + - "new-ind" + - "newtechs" + - "syssettings" + - "dem_ref" + - "trade_param" + - "bnd_ppfossil" + - "bounds-uc_wsets" + - "co2_tax" + - "demproj_dtcar" + - "elasticdem" + - "elc_co2_bound" + - "ind_newres" + - "nuc_dscinv" + - "peak_rsv" + - "refinery" + - "solar_subsidies" + - "tra_co2_bound" + - "tradsl_tax" + - "uc_co2_regions" + - "uc_co2bnd" + - "uc_dh_minprod" + - "uc_growth" + - "uc_nuc_maxcap" + - name: Ireland + input_folder: Ireland + regions: "IE" + inputs: + - "VT_IE_AGR.xlsx" + - "VT_IE_IND.xlsx" + - "VT_IE_PWR.xlsx" + - "VT_IE_RSD.xlsx" + - "VT_IE_SRV.xlsx" + - "VT_IE_SUP.xlsx" + - "VT_IE_TRA.xlsx" + - "BY_Trans.xlsx" + - "SetRules.xlsx" + - "SuppXLS/Trades/ScenTrade__Trade_Links.xlsx" + - "SubRES_TMPL/SubRES_PWR_DH.xlsx" + - "SubRES_TMPL/SubRES_PWR_DH_Trans.xlsx" + - "SubRES_TMPL/SubRES_PWR_NewTechs.xlsx" + - "SubRES_TMPL/SubRES_PWR_NewTechs_Trans.xlsx" + - "SubRES_TMPL/SubRES_RSD_NewTechs.xlsx" + - "SubRES_TMPL/SubRES_RSD_NewTechs_Trans.xlsx" + - "SubRES_TMPL/SubRES_RSD-Retrofit.xlsx" + - "SubRES_TMPL/SubRES_RSD-Retrofit_Trans.xlsx" + - "SubRES_TMPL/SubRES_SRV_DC_ExcessHeat.xlsx" + - "SubRES_TMPL/SubRES_SRV_DC_ExcessHeat_Trans.xlsx" + - "SubRES_TMPL/SubRES_SRV_NewTechs.xlsx" + - "SubRES_TMPL/SubRES_SRV_NewTechs_Trans.xlsx" + - "SubRES_TMPL/SubRES_SUP_BioRefineries.xlsx" + - "SubRES_TMPL/SubRES_SUP_BioRefineries_Trans.xlsx" + - "SubRES_TMPL/SubRES_SUP_H2NewTechs.xlsx" + - "SubRES_TMPL/SubRES_SUP_H2NewTechs_Trans.xlsx" + - "SubRES_TMPL/SubRES_SYS_OtherNewTechs.xlsx" + - "SubRES_TMPL/SubRES_SYS_OtherNewTechs_Trans.xlsx" + - "SubRES_TMPL/SubRES_TRA_NewVehicles.xlsx" + - "SubRES_TMPL/SubRES_TRA_NewVehicles_Trans.xlsx" + - "SysSettings.xlsx" + - "SuppXLS/Scen_A_SYS_SAD_40TS.xlsx" + - "SuppXLS/Scen_B_SYS_Additional_Assumptions.xlsx" + - "SuppXLS/Scen_B_SYS_Demands.xlsx" + - "SuppXLS/Scen_B_SUP_DomBioPot_Baseline.xlsx" + - "SuppXLS/Scen_B_IND_Emi_Proc.xlsx" + - "SuppXLS/Scen_B_PWR_CCS.xlsx" + - "SuppXLS/Scen_B_SRV_DC_EH.xlsx" + - "SuppXLS/Scen_B_PWR_RNW_Potentials.xlsx" + - "SuppXLS/Scen_B_IND_Emissions.xlsx" + - "SuppXLS/Scen_B_RSD_Retrofit-Ctrl.xlsx" + - "SuppXLS/Scen_B_RSD_UC.xlsx" + - "SuppXLS/Scen_B_SRV_UC.xlsx" + - "SuppXLS/Scen_B_PWR_SNSP_Limit.xlsx" + - "SuppXLS/Scen_B_SYS_Bio_DelivCost.xlsx" + - "SuppXLS/Scen_B_SYS_Historic_Bounds.xlsx" + - "SuppXLS/Scen_B_SYS_MaxGrowthRates.xlsx" + - "SuppXLS/Scen_B_RSD_UnitBoilers.xlsx" + - "SuppXLS/Scen_B_TRA_P_ModalShares.xlsx" + - "SuppXLS/Scen_B_TRA_NewCars_Retirement.xlsx" + - "SuppXLS/Scen_B_TRA_Stock_Retirement.xlsx" + - "SuppXLS/Scen_B_TRA_Emissions.xlsx" + - "SuppXLS/Scen_B_TRA_EV_Parity.xlsx" + - "SuppXLS/Scen_B_TRA_F_ModalShares.xlsx" + dd_folder: Ireland diff --git a/utils/run_benchmarks.py b/utils/run_benchmarks.py index a9bb9d7..34f2c8f 100644 --- a/utils/run_benchmarks.py +++ b/utils/run_benchmarks.py @@ -12,7 +12,7 @@ import sys from tabulate import tabulate import time -from typing import Any, Tuple, NamedTuple +from typing import Any, Tuple import yaml @@ -194,7 +194,9 @@ def run_benchmark( from xl2times.__main__ import main summary = main(args) - # TODO this is a hack to make the return value look like it's from a subprocess. Replace subprocess calls above with function calls? + + # pack the results into a namedtuple pretending to be a return value from a subprocess call (as above). + # TODO Replace subprocess calls above with function calls? res = namedtuple("stdout", ["stdout", "stderr", "returncode"])(summary, "", 0) runtime = time.time() - start From 57bbb4dbd1595ea2a7ec0f475c8847a2f238b9d9 Mon Sep 17 00:00:00 2001 From: Sam West Date: Wed, 14 Feb 2024 12:50:18 +1100 Subject: [PATCH 03/26] limit max_workers on windows to reduce peak ram --- benchmarks.yml | 322 ++++++++++++++++++++-------------------- utils/run_benchmarks.py | 5 +- 2 files changed, 165 insertions(+), 162 deletions(-) diff --git a/benchmarks.yml b/benchmarks.yml index cfdec2b..3f3ed6b 100644 --- a/benchmarks.yml +++ b/benchmarks.yml @@ -7,167 +7,167 @@ benchmarks_folder: benchmarks/ benchmarks: -# - name: DemoS_001-all -# input_folder: DemoS_001 -# dd_folder: DemoS_001-all -# dd_files: -# - "base" -# - "syssettings" -# - name: DemoS_002-all -# input_folder: DemoS_002 -# dd_folder: DemoS_002-all -# dd_files: -# - "base" -# - "syssettings" -# - name: DemoS_003-all -# input_folder: DemoS_003 -# dd_folder: DemoS_003-all -# dd_files: -# - "base" -# - "syssettings" -# - name: DemoS_004 -# input_folder: DemoS_004 -# inputs: -# - "BY_Trans.xlsx" -# - "Sets-DemoModels.xlsx" -# - "SysSettings.xlsx" -# - "VT_REG_PRI_V04.xlsx" -# dd_folder: DemoS_004 -# dd_files: -# - "base" -# - "syssettings" -# - name: DemoS_004a -# input_folder: DemoS_004 -# inputs: -# - "BY_Trans.xlsx" -# - "Sets-DemoModels.xlsx" -# - "SysSettings.xlsx" -# - "SuppXLS/Scen_Peak_RSV.xlsx" -# - "VT_REG_PRI_V04.xlsx" -# dd_folder: DemoS_004a -# dd_files: -# - "base" -# - "syssettings" -# - "peak_rsv" -# - name: DemoS_004b -# input_folder: DemoS_004 -# inputs: -# - "BY_Trans.xlsx" -# - "Sets-DemoModels.xlsx" -# - "SysSettings.xlsx" -# - "SuppXLS/Scen_Peak_RSV-FLX.xlsx" -# - "VT_REG_PRI_V04.xlsx" -# dd_folder: DemoS_004b -# dd_files: -# - "base" -# - "syssettings" -# - "peak_rsv-flx" -# - name: DemoS_004-all -# input_folder: DemoS_004 -# dd_folder: DemoS_004-all -# dd_files: -# - "base" -# - "syssettings" -# - "peak_rsv" -# - "peak_rsv-flx" -# - name: DemoS_005-all -# input_folder: DemoS_005 -# dd_folder: DemoS_005-all -# dd_files: -# - "base" -# - "syssettings" -# - "trade_param" -# - "co2_tax" -# - "elc_co2_bound" -# - "peak_rsv" -# - "uc_co2bnd" -# - name: DemoS_006-all -# input_folder: DemoS_006 -# dd_folder: DemoS_006-all -# dd_files: -# - "base" -# - "newtechs" -# - "syssettings" -# - "trade_param" -# - "elc_co2_bound" -# - "peak_rsv" -# - "uc_co2bnd" -# - name: DemoS_007-all -# input_folder: DemoS_007 -# dd_folder: DemoS_007-all -# dd_files: -# - "base" -# - "newtechs" -# - "syssettings" -# - "trade_param" -# - "demproj_dtcar" -# - "elasticdem" -# - "elc_co2_bound" -# - "peak_rsv" -# - "refinery" -# - "tra_co2_bound" -# - "uc_co2bnd" -# - "uc_growth" -# - name: DemoS_008-all -# input_folder: DemoS_008 -# dd_folder: DemoS_008-all -# dd_files: -# - "base" -# - "newtechs" -# - "syssettings" -# - "trade_param" -# - "demproj_dtcar" -# - "elasticdem" -# - "elc_co2_bound" -# - "peak_rsv" -# - "refinery" -# - "tra_co2_bound" -# - "uc_co2bnd" -# - "uc_growth" -# - "uc_nuc_maxcap" -# - name: DemoS_009-all -# input_folder: DemoS_009 -# dd_folder: DemoS_009-all -# dd_files: -# - "base" -# - "new-chp-dh" -# - "new-ind" -# - "newtechs" -# - "syssettings" -# - "trade_param" -# - "demproj_dtcar" -# - "elasticdem" -# - "elc_co2_bound" -# - "ind_newres" -# - "peak_rsv" -# - "refinery" -# - "tra_co2_bound" -# - "uc_co2bnd" -# - "uc_dh_minprod" -# - "uc_growth" -# - "uc_nuc_maxcap" -# - name: DemoS_010-all -# input_folder: DemoS_010 -# dd_folder: DemoS_010-all -# dd_files: -# - "base" -# - "new-chp-dh" -# - "new-ind" -# - "newtechs" -# - "syssettings" -# - "dem_ref" -# - "trade_param" -# - "demproj_dtcar" -# - "elasticdem" -# - "elc_co2_bound" -# - "ind_newres" -# - "peak_rsv" -# - "refinery" -# - "tra_co2_bound" -# - "uc_co2bnd" -# - "uc_dh_minprod" -# - "uc_growth" -# - "uc_nuc_maxcap" + - name: DemoS_001-all + input_folder: DemoS_001 + dd_folder: DemoS_001-all + dd_files: + - "base" + - "syssettings" + - name: DemoS_002-all + input_folder: DemoS_002 + dd_folder: DemoS_002-all + dd_files: + - "base" + - "syssettings" + - name: DemoS_003-all + input_folder: DemoS_003 + dd_folder: DemoS_003-all + dd_files: + - "base" + - "syssettings" + - name: DemoS_004 + input_folder: DemoS_004 + inputs: + - "BY_Trans.xlsx" + - "Sets-DemoModels.xlsx" + - "SysSettings.xlsx" + - "VT_REG_PRI_V04.xlsx" + dd_folder: DemoS_004 + dd_files: + - "base" + - "syssettings" + - name: DemoS_004a + input_folder: DemoS_004 + inputs: + - "BY_Trans.xlsx" + - "Sets-DemoModels.xlsx" + - "SysSettings.xlsx" + - "SuppXLS/Scen_Peak_RSV.xlsx" + - "VT_REG_PRI_V04.xlsx" + dd_folder: DemoS_004a + dd_files: + - "base" + - "syssettings" + - "peak_rsv" + - name: DemoS_004b + input_folder: DemoS_004 + inputs: + - "BY_Trans.xlsx" + - "Sets-DemoModels.xlsx" + - "SysSettings.xlsx" + - "SuppXLS/Scen_Peak_RSV-FLX.xlsx" + - "VT_REG_PRI_V04.xlsx" + dd_folder: DemoS_004b + dd_files: + - "base" + - "syssettings" + - "peak_rsv-flx" + - name: DemoS_004-all + input_folder: DemoS_004 + dd_folder: DemoS_004-all + dd_files: + - "base" + - "syssettings" + - "peak_rsv" + - "peak_rsv-flx" + - name: DemoS_005-all + input_folder: DemoS_005 + dd_folder: DemoS_005-all + dd_files: + - "base" + - "syssettings" + - "trade_param" + - "co2_tax" + - "elc_co2_bound" + - "peak_rsv" + - "uc_co2bnd" + - name: DemoS_006-all + input_folder: DemoS_006 + dd_folder: DemoS_006-all + dd_files: + - "base" + - "newtechs" + - "syssettings" + - "trade_param" + - "elc_co2_bound" + - "peak_rsv" + - "uc_co2bnd" + - name: DemoS_007-all + input_folder: DemoS_007 + dd_folder: DemoS_007-all + dd_files: + - "base" + - "newtechs" + - "syssettings" + - "trade_param" + - "demproj_dtcar" + - "elasticdem" + - "elc_co2_bound" + - "peak_rsv" + - "refinery" + - "tra_co2_bound" + - "uc_co2bnd" + - "uc_growth" + - name: DemoS_008-all + input_folder: DemoS_008 + dd_folder: DemoS_008-all + dd_files: + - "base" + - "newtechs" + - "syssettings" + - "trade_param" + - "demproj_dtcar" + - "elasticdem" + - "elc_co2_bound" + - "peak_rsv" + - "refinery" + - "tra_co2_bound" + - "uc_co2bnd" + - "uc_growth" + - "uc_nuc_maxcap" + - name: DemoS_009-all + input_folder: DemoS_009 + dd_folder: DemoS_009-all + dd_files: + - "base" + - "new-chp-dh" + - "new-ind" + - "newtechs" + - "syssettings" + - "trade_param" + - "demproj_dtcar" + - "elasticdem" + - "elc_co2_bound" + - "ind_newres" + - "peak_rsv" + - "refinery" + - "tra_co2_bound" + - "uc_co2bnd" + - "uc_dh_minprod" + - "uc_growth" + - "uc_nuc_maxcap" + - name: DemoS_010-all + input_folder: DemoS_010 + dd_folder: DemoS_010-all + dd_files: + - "base" + - "new-chp-dh" + - "new-ind" + - "newtechs" + - "syssettings" + - "dem_ref" + - "trade_param" + - "demproj_dtcar" + - "elasticdem" + - "elc_co2_bound" + - "ind_newres" + - "peak_rsv" + - "refinery" + - "tra_co2_bound" + - "uc_co2bnd" + - "uc_dh_minprod" + - "uc_growth" + - "uc_nuc_maxcap" - name: DemoS_011-all input_folder: DemoS_011 dd_folder: DemoS_011-all diff --git a/utils/run_benchmarks.py b/utils/run_benchmarks.py index 34f2c8f..18b3820 100644 --- a/utils/run_benchmarks.py +++ b/utils/run_benchmarks.py @@ -246,7 +246,10 @@ def run_all_benchmarks( verbose=verbose, ) - with ProcessPoolExecutor() as executor: + max_workers = ( + 1 if os.name == "nt" else None + ) # prevent excessive number of processes in Windwows + with ProcessPoolExecutor(max_workers=max_workers) as executor: results = list(executor.map(run_a_benchmark, benchmarks)) print("\n\n" + tabulate(results, headers, floatfmt=".1f") + "\n") From 13f35ee7a213e9c7a2cf2ca72cfc0b5b86ff8031 Mon Sep 17 00:00:00 2001 From: Sam West Date: Wed, 14 Feb 2024 13:16:56 +1100 Subject: [PATCH 04/26] more process count limiting --- utils/run_benchmarks.py | 9 +++++---- xl2times/__main__.py | 4 +++- xl2times/transforms.py | 4 +++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/utils/run_benchmarks.py b/utils/run_benchmarks.py index 18b3820..dbbd82c 100644 --- a/utils/run_benchmarks.py +++ b/utils/run_benchmarks.py @@ -15,6 +15,10 @@ from typing import Any, Tuple import yaml +# prevent excessive number of processes in Windows and high cpu-count machines +# TODO make this a cli param or global setting? +max_workers: int = 4 if os.name == "nt" else min(16, os.cpu_count() or 16) + def parse_result(lastline): m = match( @@ -246,9 +250,6 @@ def run_all_benchmarks( verbose=verbose, ) - max_workers = ( - 1 if os.name == "nt" else None - ) # prevent excessive number of processes in Windwows with ProcessPoolExecutor(max_workers=max_workers) as executor: results = list(executor.map(run_a_benchmark, benchmarks)) print("\n\n" + tabulate(results, headers, floatfmt=".1f") + "\n") @@ -305,7 +306,7 @@ def run_all_benchmarks( verbose=verbose, ) - with ProcessPoolExecutor() as executor: + with ProcessPoolExecutor(max_workers) as executor: results_main = list(executor.map(run_a_benchmark, benchmarks)) # Print table with combined results to make comparison easier diff --git a/xl2times/__main__.py b/xl2times/__main__.py index 6b05dfd..ac8f501 100644 --- a/xl2times/__main__.py +++ b/xl2times/__main__.py @@ -8,6 +8,8 @@ import sys import time from typing import Dict, List + +from utils.run_benchmarks import max_workers from . import datatypes from . import excel from . import transforms @@ -31,7 +33,7 @@ def convert_xl_to_times( use_pool = True if use_pool: - with ProcessPoolExecutor() as executor: + with ProcessPoolExecutor(max_workers) as executor: for result in executor.map(excel.extract_tables, input_files): raw_tables.extend(result) else: diff --git a/xl2times/transforms.py b/xl2times/transforms.py index 3a269d2..996f00c 100644 --- a/xl2times/transforms.py +++ b/xl2times/transforms.py @@ -10,6 +10,8 @@ from concurrent.futures import ProcessPoolExecutor import time from functools import reduce + +from utils.run_benchmarks import max_workers from . import datatypes from . import utils @@ -2264,5 +2266,5 @@ def expand_rows_parallel( tables: List[datatypes.EmbeddedXlTable], model: datatypes.TimesModel, ) -> List[datatypes.EmbeddedXlTable]: - with ProcessPoolExecutor() as executor: + with ProcessPoolExecutor(max_workers) as executor: return list(executor.map(expand_rows, tables)) From 1c8e11f3fe697f10d7047c465db9686c8ae8f68d Mon Sep 17 00:00:00 2001 From: Sam West Date: Wed, 14 Feb 2024 13:25:06 +1100 Subject: [PATCH 05/26] move max_workers to utils. --- utils/run_benchmarks.py | 6 ++---- xl2times/__main__.py | 2 +- xl2times/transforms.py | 2 +- xl2times/utils.py | 5 +++++ 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/utils/run_benchmarks.py b/utils/run_benchmarks.py index dbbd82c..64f99c0 100644 --- a/utils/run_benchmarks.py +++ b/utils/run_benchmarks.py @@ -15,9 +15,7 @@ from typing import Any, Tuple import yaml -# prevent excessive number of processes in Windows and high cpu-count machines -# TODO make this a cli param or global setting? -max_workers: int = 4 if os.name == "nt" else min(16, os.cpu_count() or 16) +from xl2times.utils import max_workers def parse_result(lastline): @@ -343,7 +341,7 @@ def run_all_benchmarks( time_regressions = df[df["Time (s)"] > 2 * df["M Time (s)"]]["Benchmark"] runtime_change = df["Time (s)"].sum() - df["M Time (s)"].sum() - print(f"Change in runtime: {runtime_change:+.2f}") + print(f"Change in runtime: {runtime_change:+.2f} s") correct_change = df["Correct"].sum() - df["M Correct"].sum() print(f"Change in correct rows: {correct_change:+d}") additional_change = df["Additional"].sum() - df["M Additional"].sum() diff --git a/xl2times/__main__.py b/xl2times/__main__.py index ac8f501..4affa3e 100644 --- a/xl2times/__main__.py +++ b/xl2times/__main__.py @@ -9,7 +9,7 @@ import time from typing import Dict, List -from utils.run_benchmarks import max_workers +from xl2times.utils import max_workers from . import datatypes from . import excel from . import transforms diff --git a/xl2times/transforms.py b/xl2times/transforms.py index 996f00c..cbee122 100644 --- a/xl2times/transforms.py +++ b/xl2times/transforms.py @@ -11,7 +11,7 @@ import time from functools import reduce -from utils.run_benchmarks import max_workers +from .utils import max_workers from . import datatypes from . import utils diff --git a/xl2times/utils.py b/xl2times/utils.py index c7898d2..df157e2 100644 --- a/xl2times/utils.py +++ b/xl2times/utils.py @@ -1,3 +1,4 @@ +import os import re from dataclasses import replace from math import log10, floor @@ -10,6 +11,10 @@ from . import datatypes +# prevent excessive number of processes in Windows and high cpu-count machines +# TODO make this a cli param or global setting? +max_workers: int = 4 if os.name == "nt" else min(16, os.cpu_count() or 16) + def apply_composite_tag(table: datatypes.EmbeddedXlTable) -> datatypes.EmbeddedXlTable: """ From 552af5b8cddd60b2e4c53cea827eb08e0acfae97 Mon Sep 17 00:00:00 2001 From: Sam West Date: Wed, 14 Feb 2024 14:29:41 +1100 Subject: [PATCH 06/26] Added more detail to benchmarking section in readme --- README.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/README.md b/README.md index 72262ec..b758cf3 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,44 @@ VS Code will highlight the changes in the two files, which should correspond to See our GitHub Actions CI `.github/workflows/ci.yml` and the utility script `utils/run_benchmarks.py` to see how to run the tool on the DemoS models. +In summary, use the commands below to clone the benchmarks data into your local `benchmarks` dir. +Note that this assumes you have access to all these repositories (some are private and +you'll have to request access) - if not, comment out the inaccessible benchmarks from `benchmakrs.yml` before running. + +```bash +# Get VEDA example models and reference DD files +# XLSX files are in private repo for licensing reasons, please request access or replace with your own licensed VEDA example files. +git clone git@github.com:olejandro/demos-xlsx.git benchmarks/xlsx/ +git clone git@github.com:olejandro/demos-dd.git benchmarks/dd/ + +# Get Ireland model and reference DD files +git clone git@github.com:esma-cgep/tim.git benchmarks/xlsx/Ireland +git clone git@github.com:esma-cgep/tim-gams.git benchmarks/dd/Ireland +``` +Then to run the benchmarks: +```bash +# Run a only a single benchmark by name (see benchmarks.yml for name list) +python utils/run_benchmarks.py benchmarks.yml --verbose --run DemoS_001-all + +# Run all benchmarks (without GAMS run, just comparing CSV data) +python utils/run_benchmarks.py benchmarks.yml --verbose | tee out.txt + + +# Run benchmarks with regression tests vs main branch +git branch feature/your_new_changes --checkout +# ... make your code changes here ... +git commit -a -m "your commit message" # code must be committed for comparison to `main` branch to run. +python utils/run_benchmarks.py benchmarks.yml --verbose | tee out.txt +``` +At this point, if you haven't broken anything you should see something like: +``` +Change in runtime: +2.97s +Change in correct rows: +0 +Change in additional rows: +0 +No regressions. You're awesome! +``` +If you have a large increase in runtime, a decrease in correct rows or fewer rows being produced, then you've broken something and will need to figure out how to fix it. + ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a From 52ae6b5f427ed9b9ac3ac29cd25979b357592aab Mon Sep 17 00:00:00 2001 From: Sam West Date: Wed, 14 Feb 2024 15:21:06 +1100 Subject: [PATCH 07/26] added debug flag for running benchmarks as function calls so breakpoints in them work added traceable exit codes --- utils/run_benchmarks.py | 47 +++++++++++++--------- xl2times/__main__.py | 86 +++++++++++++++++++++++------------------ 2 files changed, 77 insertions(+), 56 deletions(-) diff --git a/utils/run_benchmarks.py b/utils/run_benchmarks.py index 64f99c0..9c58892 100644 --- a/utils/run_benchmarks.py +++ b/utils/run_benchmarks.py @@ -26,7 +26,7 @@ def parse_result(lastline): ) if not m: print(f"ERROR: could not parse output of run:\n{lastline}") - sys.exit(1) + sys.exit(2) # return (accuracy, num_correct_rows, num_additional_rows) return (float(m.groups()[0]), int(m.groups()[1]), int(m.groups()[3])) @@ -62,7 +62,7 @@ def run_gams_gdxdiff( print(res.stdout) print(res.stderr if res.stderr is not None else "") print(f"ERROR: GAMS failed on {benchmark['name']}") - sys.exit(1) + sys.exit(3) if "error" in res.stdout.lower(): print(res.stdout) print(f"ERROR: GAMS errored on {benchmark['name']}") @@ -93,7 +93,7 @@ def run_gams_gdxdiff( print(res.stdout) print(res.stderr if res.stderr is not None else "") print(f"ERROR: GAMS failed on {benchmark['name']} ground truth") - sys.exit(1) + sys.exit(4) if "error" in res.stdout.lower(): print(res.stdout) print(f"ERROR: GAMS errored on {benchmark['name']}") @@ -129,6 +129,7 @@ def run_benchmark( skip_csv: bool = False, out_folder: str = "out", verbose: bool = False, + debug: bool = False, ) -> Tuple[str, float, str, float, int, int]: xl_folder = path.join(benchmarks_folder, "xlsx", benchmark["input_folder"]) dd_folder = path.join(benchmarks_folder, "dd", benchmark["dd_folder"]) @@ -149,7 +150,7 @@ def run_benchmark( stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, - # windows needs this for subprocess to inherit venv: + # windows needs this for subprocess to inherit venv when calling python directly: shell=True if os.name == "nt" else False, ) if res.returncode != 0: @@ -157,7 +158,7 @@ def run_benchmark( shutil.rmtree(csv_folder, ignore_errors=True) print(res.stdout) print(f"ERROR: dd_to_csv failed on {benchmark['name']}") - sys.exit(1) + sys.exit(5) else: # subprocesses use too much RAM in windows, just use function call in current process instead from utils.dd_to_csv import main @@ -166,7 +167,7 @@ def run_benchmark( elif not path.exists(csv_folder): print(f"ERROR: --skip_csv is true but {csv_folder} does not exist") - sys.exit(1) + sys.exit(6) # Then run the tool args = [ @@ -184,7 +185,7 @@ def run_benchmark( args.append(xl_folder) start = time.time() res = None - if os.name != "nt": + if not debug: res = subprocess.run( ["xl2times"] + args, stdout=subprocess.PIPE, @@ -192,13 +193,12 @@ def run_benchmark( text=True, ) else: - # Subprocesses are heavyweight in windows use too much RAM, use function calls instead - from xl2times.__main__ import main + # If debug option is set, run as a function call to allow stepping with a debugger. + from xl2times.__main__ import run, parse_args - summary = main(args) + summary = run(parse_args(args)) # pack the results into a namedtuple pretending to be a return value from a subprocess call (as above). - # TODO Replace subprocess calls above with function calls? res = namedtuple("stdout", ["stdout", "stderr", "returncode"])(summary, "", 0) runtime = time.time() - start @@ -213,7 +213,7 @@ def run_benchmark( if res is not None and res.returncode != 0: print(res.stdout) print(f"ERROR: tool failed on {benchmark['name']}") - sys.exit(1) + sys.exit(7) with open(path.join(out_folder, "stdout"), "w") as f: f.write(res.stdout) @@ -236,6 +236,7 @@ def run_all_benchmarks( skip_main=False, skip_regression=False, verbose=False, + debug: bool = False, ): print("Running benchmarks", end="", flush=True) headers = ["Benchmark", "Time (s)", "GDX Diff", "Accuracy", "Correct", "Additional"] @@ -246,6 +247,7 @@ def run_all_benchmarks( skip_csv=skip_csv, run_gams=run_gams, verbose=verbose, + debug=debug, ) with ProcessPoolExecutor(max_workers=max_workers) as executor: @@ -289,7 +291,7 @@ def run_all_benchmarks( else: if repo.is_dirty(): print("Your working directory is not clean. Skipping regression tests.") - sys.exit(1) + sys.exit(8) # Re-run benchmarks on main repo.heads.main.checkout() @@ -302,6 +304,7 @@ def run_all_benchmarks( run_gams=run_gams, out_folder="out-main", verbose=verbose, + debug=debug, ) with ProcessPoolExecutor(max_workers) as executor: @@ -335,7 +338,7 @@ def run_all_benchmarks( ) if df.isna().values.any(): print(f"ERROR: number of benchmarks changed:\n{df}") - sys.exit(1) + sys.exit(9) accu_regressions = df[df["Correct"] < df["M Correct"]]["Benchmark"] addi_regressions = df[df["Additional"] > df["M Additional"]]["Benchmark"] time_regressions = df[df["Time (s)"] > 2 * df["M Time (s)"]]["Benchmark"] @@ -355,7 +358,7 @@ def run_all_benchmarks( print(f"ERROR: additional rows regressed on: {', '.join(addi_regressions)}") if not time_regressions.empty: print(f"ERROR: runtime regressed on: {', '.join(time_regressions)}") - sys.exit(1) + sys.exit(10) # TODO also check if any new tables are missing? print("No regressions. You're awesome!") @@ -410,6 +413,12 @@ def run_all_benchmarks( default=False, help="Print output of run on each benchmark", ) + args_parser.add_argument( + "--debug", + action="store_true", + default=False, + help="Run each benchmark as a function call to allow a debugger to stop at breakpoints in benchmark runs.", + ) args = args_parser.parse_args() spec = yaml.safe_load(open(args.benchmarks_yaml)) @@ -417,17 +426,17 @@ def run_all_benchmarks( benchmark_names = [b["name"] for b in spec["benchmarks"]] if len(set(benchmark_names)) != len(benchmark_names): print("ERROR: Found duplicate name in benchmarks YAML file") - sys.exit(1) + sys.exit(11) if args.dd and args.times_dir is None: print("ERROR: --times_dir is required when using --dd") - sys.exit(1) + sys.exit(12) if args.run is not None: benchmark = next((b for b in spec["benchmarks"] if b["name"] == args.run), None) if benchmark is None: print(f"ERROR: could not find {args.run} in {args.benchmarks_yaml}") - sys.exit(1) + sys.exit(13) _, runtime, gms, acc, cor, add = run_benchmark( benchmark, @@ -436,6 +445,7 @@ def run_all_benchmarks( run_gams=args.dd, skip_csv=args.skip_csv, verbose=args.verbose, + debug=args.debug, ) print( f"Ran {args.run} in {runtime:.2f}s. {acc}% ({cor} correct, {add} additional).\n" @@ -451,4 +461,5 @@ def run_all_benchmarks( skip_main=args.skip_main, skip_regression=args.skip_regression, verbose=args.verbose, + debug=args.debug, ) diff --git a/xl2times/__main__.py b/xl2times/__main__.py index 4affa3e..a57a112 100644 --- a/xl2times/__main__.py +++ b/xl2times/__main__.py @@ -386,42 +386,7 @@ def dump_tables(tables: List, filename: str) -> List: return tables -def main(arg_list: None | list[str] = None) -> str: - args_parser = argparse.ArgumentParser() - args_parser.add_argument( - "input", - nargs="*", - help="Either an input directory, or a list of input xlsx/xlsm files to process", - ) - args_parser.add_argument( - "--regions", - type=str, - default="", - help="Comma-separated list of regions to include in the model", - ) - args_parser.add_argument( - "--output_dir", type=str, default="output", help="Output directory" - ) - args_parser.add_argument( - "--ground_truth_dir", - type=str, - help="Ground truth directory to compare with output", - ) - args_parser.add_argument("--dd", action="store_true", help="Output DD files") - args_parser.add_argument( - "--only_read", - action="store_true", - help="Read xlsx/xlsm files and stop after outputting raw_tables.txt", - ) - args_parser.add_argument("--use_pkl", action="store_true") - args_parser.add_argument( - "-v", - "--verbose", - action="store_true", - help="Verbose mode: print tables after every transform", - ) - args = args_parser.parse_args(arg_list) - +def run(args) -> str | None: config = datatypes.Config( "times_mapping.txt", "times-info.json", @@ -434,7 +399,7 @@ def main(arg_list: None | list[str] = None) -> str: if not isinstance(args.input, list) or len(args.input) < 1: print(f"ERROR: expected at least 1 input. Got {args.input}") - sys.exit(1) + sys.exit(-1) elif len(args.input) == 1: assert os.path.isdir(args.input[0]) input_files = [ @@ -471,8 +436,53 @@ def main(arg_list: None | list[str] = None) -> str: ground_truth = read_csv_tables(args.ground_truth_dir) comparison = compare(tables, ground_truth, args.output_dir) return comparison - return "" + else: + return None + + +def parse_args(arg_list: None | list[str]) -> argparse.Namespace: + args_parser = argparse.ArgumentParser() + args_parser.add_argument( + "input", + nargs="*", + help="Either an input directory, or a list of input xlsx/xlsm files to process", + ) + args_parser.add_argument( + "--regions", + type=str, + default="", + help="Comma-separated list of regions to include in the model", + ) + args_parser.add_argument( + "--output_dir", type=str, default="output", help="Output directory" + ) + args_parser.add_argument( + "--ground_truth_dir", + type=str, + help="Ground truth directory to compare with output", + ) + args_parser.add_argument("--dd", action="store_true", help="Output DD files") + args_parser.add_argument( + "--only_read", + action="store_true", + help="Read xlsx/xlsm files and stop after outputting raw_tables.txt", + ) + args_parser.add_argument("--use_pkl", action="store_true") + args_parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Verbose mode: print tables after every transform", + ) + args = args_parser.parse_args(arg_list) + return args + + +def main(arg_list: None | list[str] = None): + args = parse_args(arg_list) + run(args) if __name__ == "__main__": main() + sys.exit(0) From 4572d0e5692b778e07e27eb9bddab8fa36231b89 Mon Sep 17 00:00:00 2001 From: Sam West Date: Wed, 14 Feb 2024 15:37:30 +1100 Subject: [PATCH 08/26] If benchmark fails in CI, try again without the --dd flag, to see if we're just missing a GAMS license. --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7a3b09..83d63c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,6 +92,9 @@ jobs: # printed again in the next step. # Save the return code to retcode.txt so that the next step can fail the action # if run_benchmarks.py failed. + # + # If it fails, try again without the --dd flag, to see if we're just missing a GAMS license. + # Build still fails without GAMS in final step, but we get CSV-only regression test results here - useful for testing in repo forks. run: | source .venv/bin/activate export PATH=$PATH:$GITHUB_WORKSPACE/GAMS/gams44.1_linux_x64_64_sfx @@ -100,6 +103,14 @@ jobs: --verbose \ | tee out.txt; \ echo ${PIPESTATUS[0]} > retcode.txt) + if [ $(cat retcode.txt) -ne 0 ]; then + echo "Retrying without GAMS to check CSV regressions..." + (python utils/run_benchmarks.py benchmarks.yml \ + --times_dir $GITHUB_WORKSPACE/TIMES_model \ + --verbose \ + | tee out.txt\ + echo ${PIPESTATUS[0]} > retcode.txt) + fi - name: Print summary working-directory: xl2times From beb05adeb005fa8684b9c23ed7e58bb22b37ba3d Mon Sep 17 00:00:00 2001 From: Sam West Date: Wed, 14 Feb 2024 16:15:40 +1100 Subject: [PATCH 09/26] Split into separate CI tasks with/without GAMS license --- .github/workflows/ci.yml | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83d63c3..e0b95cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,9 +3,9 @@ name: CI on: # Triggers the workflow on push or pull request events but only for the main branch push: - branches: [main] + branches: [ main ] pull_request: - branches: [main] + branches: [ main ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -87,14 +87,13 @@ jobs: # ---------- Run tool, check for regressions - name: Run tool on all benchmarks + if: secrets.GAMS_LICENSE working-directory: xl2times # Use tee to also save the output to out.txt so that the summary table can be # printed again in the next step. # Save the return code to retcode.txt so that the next step can fail the action # if run_benchmarks.py failed. # - # If it fails, try again without the --dd flag, to see if we're just missing a GAMS license. - # Build still fails without GAMS in final step, but we get CSV-only regression test results here - useful for testing in repo forks. run: | source .venv/bin/activate export PATH=$PATH:$GITHUB_WORKSPACE/GAMS/gams44.1_linux_x64_64_sfx @@ -103,14 +102,19 @@ jobs: --verbose \ | tee out.txt; \ echo ${PIPESTATUS[0]} > retcode.txt) - if [ $(cat retcode.txt) -ne 0 ]; then - echo "Retrying without GAMS to check CSV regressions..." - (python utils/run_benchmarks.py benchmarks.yml \ - --times_dir $GITHUB_WORKSPACE/TIMES_model \ - --verbose \ - | tee out.txt\ - echo ${PIPESTATUS[0]} > retcode.txt) - fi + + - name: Run CSV-only regression tests (no GAMS license) + # Run without --dd flag GAMS license secret doesn't exist to see if we're just missing a GAMS license. + # Build will fail without GAMS in final step, but we get CSV-only regression test results here - useful for testing in repo forks. + if: secrets.GAMS_LICENSE == '' + run: | + source .venv/bin/activate + export PATH=$PATH:$GITHUB_WORKSPACE/GAMS/gams44.1_linux_x64_64_sfx + (python utils/run_benchmarks.py benchmarks.yml \ + --times_dir $GITHUB_WORKSPACE/TIMES_model \ + --verbose \ + | tee out.txt; ) + echo 'Note: Pipeline will fail in final step due to missing GAMS license' - name: Print summary working-directory: xl2times From f24ca915e63f63ce03d0e967800f27fe470d83ae Mon Sep 17 00:00:00 2001 From: Sam West Date: Wed, 14 Feb 2024 16:19:47 +1100 Subject: [PATCH 10/26] update ci syntax --- .github/workflows/ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0b95cc..cc2f9ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,13 +87,11 @@ jobs: # ---------- Run tool, check for regressions - name: Run tool on all benchmarks - if: secrets.GAMS_LICENSE + if: ${{ secrets.GAMS_LICENSE }} != '' working-directory: xl2times # Use tee to also save the output to out.txt so that the summary table can be # printed again in the next step. # Save the return code to retcode.txt so that the next step can fail the action - # if run_benchmarks.py failed. - # run: | source .venv/bin/activate export PATH=$PATH:$GITHUB_WORKSPACE/GAMS/gams44.1_linux_x64_64_sfx @@ -106,7 +104,7 @@ jobs: - name: Run CSV-only regression tests (no GAMS license) # Run without --dd flag GAMS license secret doesn't exist to see if we're just missing a GAMS license. # Build will fail without GAMS in final step, but we get CSV-only regression test results here - useful for testing in repo forks. - if: secrets.GAMS_LICENSE == '' + if: ${{ secrets.GAMS_LICENSE }} == '' run: | source .venv/bin/activate export PATH=$PATH:$GITHUB_WORKSPACE/GAMS/gams44.1_linux_x64_64_sfx From 319fa1a981c0842e2629ba82ffcffe587a8daf92 Mon Sep 17 00:00:00 2001 From: Sam West Date: Wed, 14 Feb 2024 16:23:40 +1100 Subject: [PATCH 11/26] fixed secret check --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc2f9ea..b401b7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,9 @@ jobs: # ---------- Run tool, check for regressions - name: Run tool on all benchmarks - if: ${{ secrets.GAMS_LICENSE }} != '' + env: + gams_license: ${{ secrets.GAMS_LICENSE }} + if: ${{ env.gams_license != '' }} working-directory: xl2times # Use tee to also save the output to out.txt so that the summary table can be # printed again in the next step. @@ -104,7 +106,9 @@ jobs: - name: Run CSV-only regression tests (no GAMS license) # Run without --dd flag GAMS license secret doesn't exist to see if we're just missing a GAMS license. # Build will fail without GAMS in final step, but we get CSV-only regression test results here - useful for testing in repo forks. - if: ${{ secrets.GAMS_LICENSE }} == '' + env: + gams_license: ${{ secrets.GAMS_LICENSE }} + if: ${{ env.gams_license == '' }} run: | source .venv/bin/activate export PATH=$PATH:$GITHUB_WORKSPACE/GAMS/gams44.1_linux_x64_64_sfx From 8daecc4dd2324550e490d61059bd650bd787abdd Mon Sep 17 00:00:00 2001 From: Sam West Date: Wed, 14 Feb 2024 16:27:04 +1100 Subject: [PATCH 12/26] add working dir skip gams install if no license --- .github/workflows/ci.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b401b7a..da8a4e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,9 @@ jobs: # ---------- Install GAMS - name: Install GAMS + env: + GAMS_LICENSE: ${{ secrets.GAMS_LICENSE }} + if: ${{ env.GAMS_LICENSE != '' }} run: | curl https://d37drm4t2jghv5.cloudfront.net/distributions/44.1.0/linux/linux_x64_64_sfx.exe -o linux_x64_64_sfx.exe chmod +x linux_x64_64_sfx.exe @@ -81,15 +84,14 @@ jobs: mkdir -p $HOME/.local/share/GAMS echo "$GAMS_LICENSE" > $HOME/.local/share/GAMS/gamslice.txt ls -l $HOME/.local/share/GAMS/ - env: - GAMS_LICENSE: ${{ secrets.GAMS_LICENSE }} + # ---------- Run tool, check for regressions - name: Run tool on all benchmarks env: - gams_license: ${{ secrets.GAMS_LICENSE }} - if: ${{ env.gams_license != '' }} + GAMS_LICENSE: ${{ secrets.GAMS_LICENSE }} + if: ${{ env.GAMS_LICENSE != '' }} working-directory: xl2times # Use tee to also save the output to out.txt so that the summary table can be # printed again in the next step. @@ -107,8 +109,9 @@ jobs: # Run without --dd flag GAMS license secret doesn't exist to see if we're just missing a GAMS license. # Build will fail without GAMS in final step, but we get CSV-only regression test results here - useful for testing in repo forks. env: - gams_license: ${{ secrets.GAMS_LICENSE }} - if: ${{ env.gams_license == '' }} + GAMS_LICENSE: ${{ secrets.GAMS_LICENSE }} + if: ${{ env.GAMS_LICENSE == '' }} + working-directory: xl2times run: | source .venv/bin/activate export PATH=$PATH:$GITHUB_WORKSPACE/GAMS/gams44.1_linux_x64_64_sfx From 1b8b0c94663e2b269f14a677d3b1195085f626da Mon Sep 17 00:00:00 2001 From: Sam West Date: Wed, 14 Feb 2024 16:34:55 +1100 Subject: [PATCH 13/26] Pass CI if no-GAMS regression tests pass --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da8a4e1..ff12b0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,19 +106,20 @@ jobs: echo ${PIPESTATUS[0]} > retcode.txt) - name: Run CSV-only regression tests (no GAMS license) - # Run without --dd flag GAMS license secret doesn't exist to see if we're just missing a GAMS license. - # Build will fail without GAMS in final step, but we get CSV-only regression test results here - useful for testing in repo forks. env: GAMS_LICENSE: ${{ secrets.GAMS_LICENSE }} if: ${{ env.GAMS_LICENSE == '' }} working-directory: xl2times + # Run without --dd flag GAMS license secret doesn't exist to see if we're just missing a GAMS license. + # This way we still run CSV-only regression tests without GAMS - useful for testing in forks before creating PRs. run: | source .venv/bin/activate export PATH=$PATH:$GITHUB_WORKSPACE/GAMS/gams44.1_linux_x64_64_sfx (python utils/run_benchmarks.py benchmarks.yml \ --times_dir $GITHUB_WORKSPACE/TIMES_model \ --verbose \ - | tee out.txt; ) + | tee out.txt; \ + echo ${PIPESTATUS[0]} > retcode.txt) echo 'Note: Pipeline will fail in final step due to missing GAMS license' - name: Print summary From dd6fc955b6bcac4416e38b787e12c2e29d164de3 Mon Sep 17 00:00:00 2001 From: Sam West Date: Thu, 15 Feb 2024 08:35:55 +1100 Subject: [PATCH 14/26] Addressed review comments --- .github/workflows/ci.yml | 5 ++--- .gitignore | 1 + .python-version | 1 - README.md | 5 +++-- pyproject.toml | 2 +- utils/__init__.py | 0 utils/run_benchmarks.py | 8 +++----- xl2times/__main__.py | 16 ++++++++++++++-- 8 files changed, 24 insertions(+), 14 deletions(-) delete mode 100644 .python-version create mode 100644 utils/__init__.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff12b0b..7f42d5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,8 +110,8 @@ jobs: GAMS_LICENSE: ${{ secrets.GAMS_LICENSE }} if: ${{ env.GAMS_LICENSE == '' }} working-directory: xl2times - # Run without --dd flag GAMS license secret doesn't exist to see if we're just missing a GAMS license. - # This way we still run CSV-only regression tests without GAMS - useful for testing in forks before creating PRs. + # Run without --dd flag if GAMS license secret doesn't exist. + # Useful for testing for (CSV) regressions in forks before creating PRs. run: | source .venv/bin/activate export PATH=$PATH:$GITHUB_WORKSPACE/GAMS/gams44.1_linux_x64_64_sfx @@ -120,7 +120,6 @@ jobs: --verbose \ | tee out.txt; \ echo ${PIPESTATUS[0]} > retcode.txt) - echo 'Note: Pipeline will fail in final step due to missing GAMS license' - name: Print summary working-directory: xl2times diff --git a/.gitignore b/.gitignore index 0dd196b..f71874c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ speedscope.json .venv*/ benchmarks/ .idea/ +.python-version diff --git a/.python-version b/.python-version deleted file mode 100644 index 0c7d5f5..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.11.4 diff --git a/README.md b/README.md index b758cf3..bef1915 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ If you want to skip these pre-commit steps for a particular commit, if for insta git commit --no-verify ``` -### Publishing the Tool +## Publishing the Tool To publish a new version of the tool on PyPI, update the version number in `pyproject.toml`, and then run: ```bash @@ -87,6 +87,7 @@ Note that this assumes you have access to all these repositories (some are priva you'll have to request access) - if not, comment out the inaccessible benchmarks from `benchmakrs.yml` before running. ```bash +mkdir benchmarks # Get VEDA example models and reference DD files # XLSX files are in private repo for licensing reasons, please request access or replace with your own licensed VEDA example files. git clone git@github.com:olejandro/demos-xlsx.git benchmarks/xlsx/ @@ -99,7 +100,7 @@ git clone git@github.com:esma-cgep/tim-gams.git benchmarks/dd/Ireland Then to run the benchmarks: ```bash # Run a only a single benchmark by name (see benchmarks.yml for name list) -python utils/run_benchmarks.py benchmarks.yml --verbose --run DemoS_001-all +python utils/run_benchmarks.py benchmarks.yml --verbose --run DemoS_001-all | tee out.txt # Run all benchmarks (without GAMS run, just comparing CSV data) python utils/run_benchmarks.py benchmarks.yml --verbose | tee out.txt diff --git a/pyproject.toml b/pyproject.toml index a5dd4d6..5472df5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=61.0.0", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools] -packages = ["xl2times"] +packages = ["xl2times", "utils"] [project] name = "xl2times" diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/run_benchmarks.py b/utils/run_benchmarks.py index 9c58892..5ae8876 100644 --- a/utils/run_benchmarks.py +++ b/utils/run_benchmarks.py @@ -150,8 +150,6 @@ def run_benchmark( stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, - # windows needs this for subprocess to inherit venv when calling python directly: - shell=True if os.name == "nt" else False, ) if res.returncode != 0: # Remove partial outputs @@ -160,7 +158,7 @@ def run_benchmark( print(f"ERROR: dd_to_csv failed on {benchmark['name']}") sys.exit(5) else: - # subprocesses use too much RAM in windows, just use function call in current process instead + # If debug option is set, run as a function call to allow stepping with a debugger. from utils.dd_to_csv import main main([dd_folder, csv_folder]) @@ -203,14 +201,14 @@ def run_benchmark( runtime = time.time() - start - if verbose and res is not None: + if verbose: line = "-" * 80 print(f"\n{line}\n{benchmark['name']}\n{line}\n\n{res.stdout}") print(res.stderr if res.stderr is not None else "") else: print(".", end="", flush=True) - if res is not None and res.returncode != 0: + if res.returncode != 0: print(res.stdout) print(f"ERROR: tool failed on {benchmark['name']}") sys.exit(7) diff --git a/xl2times/__main__.py b/xl2times/__main__.py index a57a112..0e90ffe 100644 --- a/xl2times/__main__.py +++ b/xl2times/__main__.py @@ -387,6 +387,11 @@ def dump_tables(tables: List, filename: str) -> List: def run(args) -> str | None: + """ + Runs the xl2times conversion. + :param args: pre-parsed command line arguments + :return comparison with ground-truth string if `ground_truth_dir` is provided, else None. + """ config = datatypes.Config( "times_mapping.txt", "times-info.json", @@ -441,6 +446,10 @@ def run(args) -> str | None: def parse_args(arg_list: None | list[str]) -> argparse.Namespace: + """Parse command line arguments + :param arg_list: list of command line arguments. Uses sys.argv (default argpasrse behaviour) if `None`. + :return parsed arguments + """ args_parser = argparse.ArgumentParser() args_parser.add_argument( "input", @@ -478,11 +487,14 @@ def parse_args(arg_list: None | list[str]) -> argparse.Namespace: return args -def main(arg_list: None | list[str] = None): +def main(arg_list: None | list[str] = None) -> None: + """Main entry point for the xl2times package + :return: None. + """ args = parse_args(arg_list) run(args) if __name__ == "__main__": - main() + main(sys.argv[1:]) sys.exit(0) From 00fed2b8626be3aa987358787bb570c540c3177a Mon Sep 17 00:00:00 2001 From: Sam West Date: Thu, 15 Feb 2024 09:35:40 +1100 Subject: [PATCH 15/26] vectorised loops in transforms.generate_commodity_groups() --- xl2times/transforms.py | 135 ++++++++++++++++++++++++++++++++--------- 1 file changed, 106 insertions(+), 29 deletions(-) diff --git a/xl2times/transforms.py b/xl2times/transforms.py index 129e514..6e2244a 100644 --- a/xl2times/transforms.py +++ b/xl2times/transforms.py @@ -11,6 +11,8 @@ import time from functools import reduce +from tqdm import tqdm + from .utils import max_workers from . import datatypes from . import utils @@ -984,18 +986,11 @@ def generate_commodity_groups( # Commodity groups by process, region and commodity comm_groups = pd.merge(prc_top, comm_set, on=["region", "commodity"]) comm_groups["commoditygroup"] = 0 - # Store the number of IN/OUT commodities of the same type per Region and Process in CommodityGroup - for region in comm_groups["region"].unique(): - i_reg = comm_groups["region"] == region - for process in comm_groups[i_reg]["process"].unique(): - i_reg_prc = i_reg & (comm_groups["process"] == process) - for cset in comm_groups[i_reg_prc]["csets"].unique(): - i_reg_prc_cset = i_reg_prc & (comm_groups["csets"] == cset) - for io in ["IN", "OUT"]: - i_reg_prc_cset_io = i_reg_prc_cset & (comm_groups["io"] == io) - comm_groups.loc[i_reg_prc_cset_io, "commoditygroup"] = sum( - i_reg_prc_cset_io - ) + + # Original logic, slow for large tables + # _count_comm_group_looped() + # Much faster vectorised version + _count_comm_group_vectorised(comm_groups) def name_comm_group(df): """ @@ -1013,23 +1008,10 @@ def name_comm_group(df): comm_groups["commoditygroup"] = comm_groups.apply(name_comm_group, axis=1) # Determine default PCG according to Veda - comm_groups["DefaultVedaPCG"] = None - for region in comm_groups["region"].unique(): - i_reg = comm_groups["region"] == region - for process in comm_groups[i_reg]["process"]: - i_reg_prc = i_reg & (comm_groups["process"] == process) - default_set = False - for io in ["OUT", "IN"]: - if default_set: - break - i_reg_prc_io = i_reg_prc & (comm_groups["io"] == io) - for cset in csets_ordered_for_pcg: - i_reg_prc_io_cset = i_reg_prc_io & (comm_groups["csets"] == cset) - df = comm_groups[i_reg_prc_io_cset] - if not df.empty: - comm_groups.loc[i_reg_prc_io_cset, "DefaultVedaPCG"] = True - default_set = True - break + # original logic, slow for large tables + # comm_groups = pcg_looped(comm_groups, csets_ordered_for_pcg) + # vectorised logic, much faster + comm_groups = _process_comm_groups_vectorised(comm_groups, csets_ordered_for_pcg) # Add standard Veda PCGS named contrary to name_comm_group if reg_prc_veda_pcg.shape[0]: @@ -1063,6 +1045,101 @@ def name_comm_group(df): return tables +def _count_comm_group_looped(comm_groups): + """Store the number of IN/OUT commodities of the same type per Region and Process in CommodityGroup + TODO remove this function once _count_comm_group_vectorised is validated? + """ + for region in comm_groups["region"].unique(): + i_reg = comm_groups["region"] == region + for process in tqdm( + comm_groups[i_reg]["process"].unique(), f"Summing commodities for {region}" + ): + i_reg_prc = i_reg & (comm_groups["process"] == process) + for cset in comm_groups[i_reg_prc]["csets"].unique(): + i_reg_prc_cset = i_reg_prc & (comm_groups["csets"] == cset) + for io in ["IN", "OUT"]: + i_reg_prc_cset_io = i_reg_prc_cset & (comm_groups["io"] == io) + comm_groups.loc[i_reg_prc_cset_io, "commoditygroup"] = sum( + i_reg_prc_cset_io + ) + + +def _count_comm_group_vectorised(comm_groups): + """Much faster vectorised version + Stores the number of IN/OUT commodities of the same type per Region and Process in CommodityGroup + """ + comm_groups["commoditygroup"] = ( + comm_groups.groupby(["region", "process", "csets", "io"]).transform("count") + )["commoditygroup"] + # set comoditygroup to 0 for io rows that aren't IN or OUT + comm_groups.loc[~comm_groups["io"].isin(["IN", "OUT"]), "commoditygroup"] = 0 + + +def _process_comm_groups_looped( + comm_groups: pd.DataFrame, csets_ordered_for_pcg: list[str] +) -> pd.DataFrame: + """Original, looped version of the default pcg logic. + Sets the first commodity group in the list of csets_ordered_for_pcg as the default pcg for each region/process/io combination, + but setting the io="OUT" subset as default before "IN". + TODO remove this function once _count_comm_group_vectorised is validated? + """ + comm_groups["DefaultVedaPCG"] = None + for region in tqdm( + comm_groups["region"].unique(), + desc=f"Determining default Primary Commodity Groups", + ): + i_reg = comm_groups["region"] == region + for process in comm_groups[i_reg]["process"]: + i_reg_prc = i_reg & (comm_groups["process"] == process) + default_set = False + for io in ["OUT", "IN"]: + if default_set: + break + i_reg_prc_io = i_reg_prc & (comm_groups["io"] == io) + for cset in csets_ordered_for_pcg: + i_reg_prc_io_cset = i_reg_prc_io & (comm_groups["csets"] == cset) + df = comm_groups[i_reg_prc_io_cset] + if not df.empty: + comm_groups.loc[i_reg_prc_io_cset, "DefaultVedaPCG"] = True + default_set = True + break + return comm_groups + + +def _process_comm_groups_vectorised( + comm_groups_test: pd.DataFrame, csets_ordered_for_pcg: list[str] +) -> pd.DataFrame: + """Vectorised version of the pcg_looped() logic, for speedup (~18x faster) with large commodity tables. + See Section 3.7.2.2, pg 80. of `TIMES Documentation PART IV` for details. + :param comm_groups_test: 'Process' DataFrame with columns ["region", "process", "io", "csets", "commoditygroup"] + :param csets_ordered_for_pcg: List of csets in the order they should be considered for default pcg + """ + + def _set_default_veda_pcg(group): + """For a given [region, process] group, default group is set as the first cset in the `csets_ordered_for_pcg` list, which is an output, if + one exists, otherwise the first input.""" + if not group["csets"].isin(csets_ordered_for_pcg).all(): + return group + + for io in ["OUT", "IN"]: + for cset in csets_ordered_for_pcg: + group.loc[ + (group["io"] == io) & (group["csets"] == cset), "DefaultVedaPCG" + ] = True + if group["DefaultVedaPCG"].any(): + break + return group + + comm_groups_test["DefaultVedaPCG"] = None + comm_groups_subset = comm_groups_test.groupby( + ["region", "process"], sort=False, as_index=False + ).apply(_set_default_veda_pcg) + comm_groups_subset = comm_groups_subset.reset_index( + level=0, drop=True + ).sort_index() # back to the original index and row order + return comm_groups_subset + + def complete_commodity_groups( config: datatypes.Config, tables: Dict[str, DataFrame], From fc5ee0babdc8b030007f68e1c4d3170806e230c4 Mon Sep 17 00:00:00 2001 From: Sam West Date: Thu, 15 Feb 2024 09:40:45 +1100 Subject: [PATCH 16/26] add tqdm package for progress bars --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 484e835..32f27a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,8 @@ dependencies = [ "more-itertools", "openpyxl >= 3.0, < 3.1", "pandas >= 2.1", - "pyarrow" + "pyarrow", + "tqdm" ] [project.optional-dependencies] From e4d5bdaa82fc02d3b2e2d1891360eb454b29c8e6 Mon Sep 17 00:00:00 2001 From: Sam West Date: Thu, 15 Feb 2024 10:05:30 +1100 Subject: [PATCH 17/26] added percentage changes to benchmark results --- utils/run_benchmarks.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/utils/run_benchmarks.py b/utils/run_benchmarks.py index 5ae8876..7230dc1 100644 --- a/utils/run_benchmarks.py +++ b/utils/run_benchmarks.py @@ -341,12 +341,28 @@ def run_all_benchmarks( addi_regressions = df[df["Additional"] > df["M Additional"]]["Benchmark"] time_regressions = df[df["Time (s)"] > 2 * df["M Time (s)"]]["Benchmark"] - runtime_change = df["Time (s)"].sum() - df["M Time (s)"].sum() - print(f"Change in runtime: {runtime_change:+.2f} s") - correct_change = df["Correct"].sum() - df["M Correct"].sum() - print(f"Change in correct rows: {correct_change:+d}") - additional_change = df["Additional"].sum() - df["M Additional"].sum() - print(f"Change in additional rows: {additional_change:+d}") + our_time = df["Time (s)"].sum() + main_time = df["M Time (s)"].sum() + runtime_change = our_time - main_time + + print(f"Total runtime: {our_time:.2f}s (main: {main_time:.2f}s)") + print( + f"Change in runtime (negative == faster): {runtime_change:+.2f}s ({100*runtime_change/main_time:+.1f}%)" + ) + + our_correct = df["Correct"].sum() + main_correct = df["M Correct"].sum() + correct_change = our_correct - main_correct + print( + f"Change in correct rows (higher == better): {correct_change:+d} ({100*correct_change/main_correct:+.1f}%)" + ) + + our_additional_rows = df["Additional"].sum() + main_additional_rows = df["M Additional"].sum() + additional_change = our_additional_rows - main_additional_rows + print( + f"Change in additional rows: {additional_change:+d} ({100*additional_change/main_additional_rows:+.1f}%)" + ) if len(accu_regressions) + len(addi_regressions) + len(time_regressions) > 0: print() From a9cae653f8a2933ba0eef5ae9d482fdb2cda0018 Mon Sep 17 00:00:00 2001 From: Sam West Date: Thu, 15 Feb 2024 10:54:32 +1100 Subject: [PATCH 18/26] Added unit tests, test data, pytest lib/config and CI step removed old looped function now vectorised outputs are verified as identical --- .github/workflows/ci.yml | 6 ++ .gitignore | 1 + pyproject.toml | 34 +++++--- tests/data/austimes_pcg_test_data.parquet | Bin 0 -> 54689 bytes .../comm_groups_austimes_test_data.parquet | Bin 0 -> 44975 bytes tests/test_transforms.py | 67 ++++++++++++++++ xl2times/transforms.py | 74 ++++-------------- 7 files changed, 114 insertions(+), 68 deletions(-) create mode 100644 tests/data/austimes_pcg_test_data.parquet create mode 100644 tests/data/comm_groups_austimes_test_data.parquet create mode 100644 tests/test_transforms.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f42d5f..323aefc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,12 @@ jobs: pre-commit install pre-commit run --all-files + - name: Run unit tests + working-directory: xl2times + run: | + source .venv/bin/activate + pytest + # ---------- Prepare ETSAP Demo models - uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index 5b52706..125701c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ benchmarks/ .python-version docs/_build/ docs/api/ +/.coverage diff --git a/pyproject.toml b/pyproject.toml index 32f27a5..6939e1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,22 +14,28 @@ requires-python = ">=3.10" license = { file = "LICENSE" } keywords = [] classifiers = [ - "Development Status :: 4 - Beta", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", ] dependencies = [ - "GitPython >= 3.1.31, < 3.2", - "more-itertools", - "openpyxl >= 3.0, < 3.1", - "pandas >= 2.1", - "pyarrow", - "tqdm" + "GitPython >= 3.1.31, < 3.2", + "more-itertools", + "openpyxl >= 3.0, < 3.1", + "pandas >= 2.1", + "pyarrow", + "tqdm", ] [project.optional-dependencies] -dev = ["black", "pre-commit", "tabulate"] +dev = [ + "black", + "pre-commit", + "tabulate", + "pytest", + "pytest-cov" +] [project.urls] Documentation = "https://github.com/etsap-TIMES/xl2times#readme" @@ -38,3 +44,9 @@ Source = "https://github.com/etsap-TIMES/xl2times" [project.scripts] xl2times = "xl2times.__main__:main" + +[tool.pytest.ini_options] +# don't print runtime warnings +filterwarnings = ["ignore::DeprecationWarning", "ignore::UserWarning", "ignore::FutureWarning"] +# show output, print test coverage report +addopts = '-s --durations=0 --durations-min=5.0 --tb=native --cov-report term --cov-report html --cov=xl2times --cov=utils' diff --git a/tests/data/austimes_pcg_test_data.parquet b/tests/data/austimes_pcg_test_data.parquet new file mode 100644 index 0000000000000000000000000000000000000000..c3346d9cfdd77625389e6c62474787d0ca803f7a GIT binary patch literal 54689 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ÐQbz01D@QSTO-KMoI@kXBjlNXfODZIN!f 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≫gx+i)N@JRGvhardsnvO?@2h{|a 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

4-;Ta}iI(<{_Sr%}2ZtI|ES_SAggl zSBMxAS7cO8snZmv#g*W0aa<{)J+2I~Ev_7KRa^yPS6n6HuDB}1gK;wvkH*bHJP|h= z@pRlA#0znA5moW?5Iy5kyyqyA$!H zzV(Rj^xcJcwr>OC#lDS*a(^eHPyZ&wi2lupN&Q`jIsM&;mHk_cs%I z9><#d@lfvV--@Zv_TP>8QvWu@clz%^JllUS;>G^^5aq4g5q-AqK#bVB6ESJ)F0`Yw z@lALO)cr!dFl<$VFl^5RVb~!FXxR58YTD8ggjE+O2&=Xy2&-;O5LUe^L0ENHg0Sjc z3BsxmCJ3uOnjozDM1rvD(+R?=FC+-7RwW9n_DmF39g>Jveb48bRcVRBq>B^Lr2XuP zYwd}#YgLzWL$3Wkmm3m#Nqs+F6vVBVOX(r5@5|{SU!TA~-gnFU=qfe;k^2*Ty_(;p z<{Q*}qndZB`6e~rtma*6-mT_a)VxQ{d+~v$(JsJ06+{m~$_&CPH7$47gIO7qFPhfl^Z4^8P8=rkMVrQ zXE0vCcp>9Oj2AOr!gwj;WsH|IUcq=J<5i5$WPBFmvl*Ym_*};4F>YhLnj!BWuVK8F z@%fC`F<#Gj1LF%AU&y$F@kYj*7;k30h4Dp-9jU&Z)p#@8^umhGdrq1=vg2g*8>J5ko7+=a3MWg|)_$|jV} zC|xMsC|gi^P1S3qdb7J2jxMOy(kZ% zJdE-P%A+XzP##0sk8%Lzag>87PoO-B@)XL`D9@k_-OBi-48M}$*E0M@hTqC?2rW>C zBQhM7;g}51%kY8>FUs&c8D5g%WjtYIctwUk$nZxQUX|gv46n)Xx(p{|cteJhGQ26n zTQa;Y!#gtkNrrc2cu$7+W%#oUf05w>8BXE(B*SSL&dBhQ3?Iwzi4330a8`!D%J7*C zpUX%V4}X*4OBv3|a9)Q0{=b-4-ct=*eM)uN70rm2)@FNcTibvynwJlc@Lt!i3VY=d zDeNCv!X3^3^@-{0x~WMvCg&R6ogQieOcFj!os+L-Zt0O$6^3V&mN3^Q(uz-NM;UQ_ zfe%|>b_dKbN2Jvx<>X_w(G`OaV@t;ms>mfZ-Ox2cx>utL>p3$_5viehPzUP$M;f;M zv?}a(AF6~3!>YS(XOBEijq6f!jh!Q%9o|uyuETbber?dB4n7zz80_l>I0Q5xM<%Hg z&#u>Xh)3FO7^!adkULGn@eXDeu2zH#9pVtMSb%V%Bj|eSESaJh5)UKaqkD7nNd0;* z;TWgZj!rv}$^^k7V6lJ(1VL(37mt)YNQ;7u5p_0$*BV1h*ZLUr zkGnht1)bF%(JT6|^h$KmFiAx+sZ`uY!H4rnvpz*}Iu!EXj+qCg6B7%Fu@ecXkaB8L zb2BnD6PGg`t%rM-wHOiURkR+V=toCb2zuE;pVhd7u7ojMJc9+BkZ}cr?sS@|*_Mjl zaiXcl%SG~T(GWk&2rPzfbMzr}vf5#@gdp(~ov9JNa4bjx8|Pn0;;BXpQ5t-WfSR=J zB9Q2BB1t|Pq;RAfnytY~EqaY8ts(x&B$}Pnnn?^b3)e>JZ9EE{&qT9qc{LIz(k~RJ zBLg{v-XIFB63u04C$rPqP&Sup+_5ees@0%$u!YowdF0<~v&VUAHRDFep>j69UNaA~ zp2qNLt!c_cVi`A4HX)ACX8SvcC90D!jn*uJ>=n$s1T(*_T^(g6QS%7Km<5CJgU(ts ztOWUrV*C%`8>NS|CT7NFpOBLy>M`6Yn<8dW4bl@veUA)c@j;)db2Br(v>sA&^3&6c zYsZLfYP#N|aVGUr=giQCH!js+R9ZtG^B`7jfH(*%4k*a$U0i?ad3TOfH8CdK6Rb70 zUzjoEV%J968pXEz)cL?6er=;!5Z4Iiq`Fq4GRUuuA*47G{MtrEkZB^W=Y zfL`0;MTYpbO~!1&uPuja-w^!TA~zCjiw!btiw#n22xq}fJpHJXkz+eWjgz{Rs|RHy^#ZhSr$JlmoHB-$1mD#(iCqX@QbOCSkK#&?e@zg|>8>LMLBMC~m4h;!Smw$?2z z(3@=qmVx85s6ZcxqR>aI&*&8lwFGq{dPOEvlQ9uP*&=ng;F$N3Xyj+ADp2XSMIq$6 zl8s7Zfe@^0z3s|E-Iy)<3DJcG1;~k$FQBMEH8?IohZ;H?FtH4ys))#@^VDEJKfsDV z2nx*eat=b?uS8>BG#O*(S-WioQ3~cA-Z@fzO983I2mVO4Ry28CMSu05RdifEvIZCx zRJHo2s9G<rn_QU!f}Y3mI_+HLZ@vZvL0`tVKO*g`TxPtDdz#MbBEHXRXk)_8L8F zhxDxdNqV-To~=U9)}K|+)}NwhtI)Gm=-GOWo~=WAw*DkN?Y9&dW*aATD#ZS?Dr)~J zirR&ucA=>K8b$4ciW+|J6PBX=uUoDIHT_~V9#+TCs;J|qDC!W3I)tK*YZP@1Df)xo ze{m*Cbl?Jx6cxKX9n4KLl#a1@5=FQ)h>Z6f^l4xTBS;;di+~;$!(2WMK5S%^@ z@TESC835re!7&&koErF_Atjj}$jKfrfSobs`gj`GAQ8ET*o!OkaaI;4YCJPoiL1JA z2Y0cg%L&7pDQN@sVASwxjD5I=@*mX_;i0SW6)|xYlV}$R*o6=oJ{TwJ4MWXuN!BJ^)^(f_bGY^+!jbg$IR7 z17hq%HRwr*b)_5dWdE2j0V=CyE6dSiqwmG;F?W$TYw4U)7o2GsnH8V$R;fo%o#K2SW_v! z5!`^zIE?P8IWu(2b>T=LjjtE;_SV`O>qY-jnz=PLJ54b31gI?q+TFye`+N+pD3FEp zGe4j4B%h8nA*WN;8ilC1dG$C%ly6Ul)9ufD2eI_(=GDT zo|a^70^I_B%w}zH5 z8qHj8Np*XJNvdz{IQB1+8f*#G`jH~T`MviZt%*yq_KIOlVwo@lf>-)b#FidJ_rOl1DWNZ+Yit-&{ZBzlLj_<~4G6c8^U zRseb()zR+$WU(aFS;p!nYqWowpb65hn=nDEP4^8KdBV_tk9VkmV3FJ9H^x_|)%y8r zv|2vFLmOEp7O(ibh&~YHt9yCmgpUKU@G*_8D`@MA6&av?mnMl8w03%z_Fr1Vgbt0? zz(T!C1GRSFHVw96L!eu5eYv~L3-v{MOKqkJ{nQ9PDi^k1&syS~PRfX~Oef%tITX!BD+C zLtRv-tsvHhY2ufnZ(pc$^GN97_mGR8IW(=djA+?o_W|amF0%SQ6{;YgKz63#P^g!+ zeF^fW(f|c*Gp(5|MlNbJ_1!xdp1HOb9r9M@H0@@In1{ED-=&xzN!am!i8mfq8Q&|t z>aPhmmv>c@o2y#P|JOI^*E)PFoH$cemXpL8Ku9X?{QVKJSr6smC z#}QYc{R%uBzXW;W^WB;Cb(v4p9rHj-J(Wm|X_1f|S=XM$hIh zNcWtxNSSos<&&qF>u_>j-~- zyfDi%?`>Nay}$FAf5&@^MqpP~j0jkAW>Z$c(oc8X7O?E|!&zg0k#hdUvCF@w*`Bg> zyV{T)xWYa7_P~`IOSWfJ9?`B=Qr&SVs&5ZkYeKlqPs+d%T0ui&H~-EsebRwrhWx|> zt3BsXk5JshA;fVU&3xt=rjrqKmYjOturB<=HNoq`citYnZnZwsWTPy-AO+Pm6_C-> zC!Ja|div@E$LNiW1+E@FXA%~ej$FW+V+PFRMN(+E25ZF%*&@bdFLtXs{Bh){r5SWi z>>9l-MepWiTa8eVqVMqXvJr;G{FT*Me>q(vufr3XY0daY?Vd@#HiA6KD%X+wUzjN|$iRXIrf1>QAWHS8AlFua{Rf^X~zZlrH-tv3% ziS;qBrg+{Jd!nHDuDG}6b=?*JZp(?g68>_l=Z3`7oy8lHKH1*20Y2M*V#5tz9`f9n zeAHE0Q8K}I6`v&d{o|8l@e|}+@`dBY8>f8xLD$Bq|2%hMd5FfI@3m3y*8zL7M5(v7;Wp`lsRV68=JDmuJGENePMVPF}yVyD_kD3sbq7`_#NGw zr%gQY#^&jh4|{dx#=KV2m6vd;yDR^O^KW#`xRDyW3)1wZ-Gx~}Te^#;N1yC2o?$g^ zDY>e`dqsuCg{50c%WYe>l+9dpa!dK#6~>+l`=-*K%DNp}da4#4IN39^`LMBfR@-Z( zy|X(`ZRwqJ)A^IVbC*-^zIny23a_L0Dt%d>?Y5wvzUn)p-|Vw*w0if~bQPBM*Y?_a z`sd%h=*|AR`&M{wt>3k&Y-_`w9X(qYJbd8ItqUJJ?0vUm)m7a@y#q@R{vrNd2LfKT z46lw<%4Rk`d6MF*0rT%}e2Oy7)9){S7k9@rJ#%U;ZhK}nJ^K-E&pvQ>({q&h{ql>a z#x(!x{0kWM=FH|_{~fm%Kfb&9H(2twKYW8Fb%7eM5q@~mj}F+aQRMO4T8?1aQEkF| znALC5F?|+g>hf8-FExHRW%<-?TZDCy7j+35Iy72Pt8>6PfWX0`nx4%3r9+1B<)On-ICx0tT>$|qF$lqzSb@>i;SMwQR0@?MwKtAa*itJ zsqz(7E>Pubs{EZQ-%#aSs(eS4i&XgsRsKnp@2T(Vn)SR^GzQeq_~PEz6}B|%aWB_+w# zZT8!L{GPaD@1mpTPeWH_=>QVkAVIPOQzV!w!Hp6%XmBq@FOw&6YXpQ)wo-WGN14|!W#&85NIO2 znLs2w)b)lpS&};C5v6&jFn_qzhMbb{Gfcnhh7~YJkZ9v(GvKc#CAbm=y zJK0E6G&`kYiH;*1GbWH388M`UxuxKurc{D53CbmX=DY@V&m~4E^OX#K=1iim@LjRn z8G&9o&*e@+H7X@uCB3b|5L#j?`PglybgTa?XXP$NMt+C5R`qm>)JH-^`t1v(dQ7NYAirqoMHgQP4#Yb0VJxlq!jd;8(q(1GSk zbQg?jGtnKwKUXECNkWHN>Sjq-=^c&>#ul_;vGmiOGn#NIw+PQE!$R*0TtT*?MLXAm zxaPyPU7MtENw)`idku@cbKBA4oohv06XM!$v7~fJy5D;H;j(iHo(4o$;GK;N#if$6 zOhR43xgx~(>@NL1F#RTJe9pOubi>Uao7DHEh92IdTpC4MM-i{p%FP~!_c)Jz+&lbj z9~_2TB)|)P6@gj;Xk2Ru6cZ>RFmpw{0u2P3SCngD877p5!)*kn;V2WBtt<5Uno+1D za65tN1b>plFC^i%r;tDq9yJ1G1j-3i5SWESP4GS!*a_4SSU_N*uwlIY1&au@65s?D z6Lb~=O9|XW;AZR-)<$3jL3b&zn!s8Ds|YNY;0^+F2-Fj3M5C8rg#>MwM?wdMu#SLD zxIu(G0@>K8gf=bA7i~Hd?XOrWU1g|xaK~-ITOaI9oA6xtuDjqL;rDNv1`)frR4t2m zpnt)Hj~>{z_^)U8Y+vay;lb@IeHV1^+%Rpy-UkdzCp`2Ymc8m)=%4&`3%S0xuHefrFO_yiDNt1YRNV2LgX2@G61h1YRTXI)M`e-XL%iCvO68 z5qO)xI|Tkj;9Uam5qO`#p9%bhzy}0Q5%`e6X#!^md_>@50-q50l)za6eEh(arcHVT|VJB7s*Iw&lmu$00w3O7-> znZhj;enDY5g_brkNTu%5zQ6gE=mq_BzN)DK-0 zHc;rMu!TYogbDD0u=oeTC- zc!yIN7Ll8;vICOLB17@S3q?iu7J+CaF9E4XgL(`vyJI zmFfQ3{d&BbxW4H7S))JV8-$l(*B8BV2k-L2pIE1e>sy6^x32y!dkgQ^U)oMx->0@y z$LIUEOGhQXoANVc`Put>ro-NkHs>#R_WrJ_sG-)b4Q$2#@kh+5mrCI5{ayJGg%i#+ zx{v;ajD7!!*YhU2`V_^8-?>AoQOOU`a|ZNXd0+fQqxv4Q)%E+VZa<5x-6VZ1x=T(VhlN^cT+zv zg+1!)w-Z$m#9hoSkRO$nXP#?N-#0We*9MN@pmj+9|NFml1Rm;;Vj~{W-|Vqaecut^ z!+Z8;qGzq;_}tF5RyY)c=J>x;nV;6Hzja6%9mIZVujjSjwR=4S9Hrs&Gs!omqxj6% zi&EIF->WY_y(Zh68ylNz8u-$IXTLU_QhWcohlU;Btq$Ayk@M-VtN*dR?jv9mj8VBp zH|OJDM!XM*jHyLgVw0YcCpH=RW%!5icam|xbk+aCD$Hn7VK&V5D|-9j-@wx67b*S* zpq-QP*A}uZfz+cy0J>AsjyQ+E5(t<6=#D>4q~j|54JL6+{FEYmw*Y@s(XYlMIZe$gFmRnvvQa9x$ZbAMz36>)B<$EK?5)}-j<_j4Cf(2xu4`>>Zy7ku z$Ef!!H0;5ZEbO}#%RhV zv~)%dJ}Ego4j(!kzI)jmhsU}Gd@x3hwO^$a@kYIU{Cpf?=Q1xJr-3dEYJ9}a`_s&@ z@1-zzahRlXt4U=|D6;FLc*C z$>i2zYpSuenZ6ovQ!qZnSi5Adz1h*;*whvbfirIkUWjoR3dWnjb+y5h0)v}ujkOrj z)b4QLUK_Wya@;jL!_6%iOiYN2O>*wmirwg2X5)^l&biJ8Y6q&mvZnbBwT_y(SH`)z zHkU7L5lsfSH#OLsYh1l(Z){mQl+awgpw`Ys?;2~lt;WV}nEj^U+9u52&{QYNmX>5j zCI;U!tTR{kBG`2ziaM_7$*}nUR&T_3-7*O~HK1Ye!1K`8HaINd=WVf}`P#-@aX)W6 zLo?~M&0Uf7Gq)86{g*?)YjVC+#xDMD)%#Q|2QyV-D7B-qJZ@oSMV{4Kkdu!42|109_NuJ1*a}Pj z;_B=&Ufq<}Fry*0x_0TbRvYHcSD@U0y2kwAYj`Y0FKs zHrQi|>g`R17-#Jm*k5c<%ZblPuZzhqDU2J4bGYI%lPZcuJp=7Il3J=8iyXCCWy?fc z#YI4B3m$g-8Cbd_VM>|Bm>ZY=`>NKpUDHZ#M?MEk> zYb#QnX*o@0?Y8p##Z_6Q4Wez#%k9~Y`8C;&4rje-(-v1Ywm2$d3SH^tW#-EAqWYSw z451f}Nm+ZPC21k*-{jQaQC3>iV2;gcioUY%m&a(?s(i`93d`WJ#ABF^V`3>=W?$;;_rI%OE!D2}t~tJF zmy4Z!{rQit({AUZ4Wf--K5oJu%cId|9Ol~MA31i{=J|Jx(*nDt%sg;xt}|Bo%c?4h zT*vnR#dww1EUk_y>-aI-!gGmNm*e?VQ507dGi`BAh1Gf9*|Rcn?#Wy>+_v$YGv_ZD zI4`d~Z(FLH3N86*&f~*T=Xxw}t{j>xmJFVsORLH=&DAkE1LyOSmP(w%>`e=u=MT;W z7{-3MY|m0vF>RTxJgMDzt}n=l&Rt+_&r6$@%^r*1X7Ef@nVt}^Z(#STw|=2G5L;~ zY|*vULXp3r06|=u(93iP)@R=Y!bKUWSDxnniis8*Io!7O(%SvTrItL9013$yY&v|{r z<6a|<5hgi{3`?Ifg_r_cY^{shYk8!-r6tmNK@S|bU None: """ - for region in comm_groups["region"].unique(): - i_reg = comm_groups["region"] == region - for process in tqdm( - comm_groups[i_reg]["process"].unique(), f"Summing commodities for {region}" - ): - i_reg_prc = i_reg & (comm_groups["process"] == process) - for cset in comm_groups[i_reg_prc]["csets"].unique(): - i_reg_prc_cset = i_reg_prc & (comm_groups["csets"] == cset) - for io in ["IN", "OUT"]: - i_reg_prc_cset_io = i_reg_prc_cset & (comm_groups["io"] == io) - comm_groups.loc[i_reg_prc_cset_io, "commoditygroup"] = sum( - i_reg_prc_cset_io - ) - - -def _count_comm_group_vectorised(comm_groups): - """Much faster vectorised version - Stores the number of IN/OUT commodities of the same type per Region and Process in CommodityGroup + Store the number of IN/OUT commodities of the same type per Region and Process in CommodityGroup. + `comm_groups` is modified in-place + Args: + comm_groups: 'Process' DataFrame with additional columns "commoditygroup" """ + comm_groups["commoditygroup"] = 0 + comm_groups["commoditygroup"] = ( comm_groups.groupby(["region", "process", "csets", "io"]).transform("count") )["commoditygroup"] @@ -1075,44 +1060,19 @@ def _count_comm_group_vectorised(comm_groups): comm_groups.loc[~comm_groups["io"].isin(["IN", "OUT"]), "commoditygroup"] = 0 -def _process_comm_groups_looped( - comm_groups: pd.DataFrame, csets_ordered_for_pcg: list[str] -) -> pd.DataFrame: - """Original, looped version of the default pcg logic. - Sets the first commodity group in the list of csets_ordered_for_pcg as the default pcg for each region/process/io combination, - but setting the io="OUT" subset as default before "IN". - TODO remove this function once _count_comm_group_vectorised is validated? - """ - comm_groups["DefaultVedaPCG"] = None - for region in tqdm( - comm_groups["region"].unique(), - desc=f"Determining default Primary Commodity Groups", - ): - i_reg = comm_groups["region"] == region - for process in comm_groups[i_reg]["process"]: - i_reg_prc = i_reg & (comm_groups["process"] == process) - default_set = False - for io in ["OUT", "IN"]: - if default_set: - break - i_reg_prc_io = i_reg_prc & (comm_groups["io"] == io) - for cset in csets_ordered_for_pcg: - i_reg_prc_io_cset = i_reg_prc_io & (comm_groups["csets"] == cset) - df = comm_groups[i_reg_prc_io_cset] - if not df.empty: - comm_groups.loc[i_reg_prc_io_cset, "DefaultVedaPCG"] = True - default_set = True - break - return comm_groups - - def _process_comm_groups_vectorised( comm_groups_test: pd.DataFrame, csets_ordered_for_pcg: list[str] ) -> pd.DataFrame: - """Vectorised version of the pcg_looped() logic, for speedup (~18x faster) with large commodity tables. - See Section 3.7.2.2, pg 80. of `TIMES Documentation PART IV` for details. - :param comm_groups_test: 'Process' DataFrame with columns ["region", "process", "io", "csets", "commoditygroup"] - :param csets_ordered_for_pcg: List of csets in the order they should be considered for default pcg + """Dets the first commodity group in the list of csets_ordered_for_pcg as the default pcg for each region/process/io combination, + but setting the io="OUT" subset as default before "IN". + + See: + Section 3.7.2.2, pg 80. of `TIMES Documentation PART IV` for details. + Args: + comm_groups_test: 'Process' DataFrame with columns ["region", "process", "io", "csets", "commoditygroup"] + csets_ordered_for_pcg: List of csets in the order they should be considered for default pcg + Returns: + Processed DataFrame with a new column "DefaultVedaPCG" set to True for the default pcg in each region/process/io combination. """ def _set_default_veda_pcg(group): From 25c48ee999bfdc99ea9d93ff8bbc93afc52bdfb5 Mon Sep 17 00:00:00 2001 From: Sam West Date: Thu, 15 Feb 2024 12:46:14 +1100 Subject: [PATCH 19/26] removed commented calls, fixed typos --- xl2times/transforms.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/xl2times/transforms.py b/xl2times/transforms.py index 0c97032..6f1bb93 100644 --- a/xl2times/transforms.py +++ b/xl2times/transforms.py @@ -986,9 +986,7 @@ def generate_commodity_groups( # Commodity groups by process, region and commodity comm_groups = pd.merge(prc_top, comm_set, on=["region", "commodity"]) - # Original logic, slow for large tables - # _count_comm_group_looped() - # Much faster vectorised version + # Add columns for the number of IN/OUT commodities of each type _count_comm_group_vectorised(comm_groups) def name_comm_group(df): @@ -1006,10 +1004,7 @@ def name_comm_group(df): # Replace commodity group member count with the name comm_groups["commoditygroup"] = comm_groups.apply(name_comm_group, axis=1) - # Determine default PCG according to Veda - # original logic, slow for large tables - # comm_groups = pcg_looped(comm_groups, csets_ordered_for_pcg) - # vectorised logic, much faster + # Determine default PCG according to Veda's logic comm_groups = _process_comm_groups_vectorised(comm_groups, csets_ordered_for_pcg) # Add standard Veda PCGS named contrary to name_comm_group @@ -1063,7 +1058,7 @@ def _count_comm_group_vectorised(comm_groups: pd.DataFrame) -> None: def _process_comm_groups_vectorised( comm_groups_test: pd.DataFrame, csets_ordered_for_pcg: list[str] ) -> pd.DataFrame: - """Dets the first commodity group in the list of csets_ordered_for_pcg as the default pcg for each region/process/io combination, + """Sets the first commodity group in the list of csets_ordered_for_pcg as the default pcg for each region/process/io combination, but setting the io="OUT" subset as default before "IN". See: From cd9d830e124574ca838992514a98a30f87ff5b4c Mon Sep 17 00:00:00 2001 From: Sam West Date: Thu, 15 Feb 2024 09:35:40 +1100 Subject: [PATCH 20/26] vectorised loops in transforms.generate_commodity_groups() --- xl2times/transforms.py | 135 ++++++++++++++++++++++++++++++++--------- 1 file changed, 106 insertions(+), 29 deletions(-) diff --git a/xl2times/transforms.py b/xl2times/transforms.py index 984701f..a76a51d 100644 --- a/xl2times/transforms.py +++ b/xl2times/transforms.py @@ -11,6 +11,8 @@ import time from functools import reduce +from tqdm import tqdm + from .utils import max_workers from . import datatypes from . import utils @@ -1056,18 +1058,11 @@ def generate_commodity_groups( # Commodity groups by process, region and commodity comm_groups = pd.merge(prc_top, comm_set, on=["region", "commodity"]) comm_groups["commoditygroup"] = 0 - # Store the number of IN/OUT commodities of the same type per Region and Process in CommodityGroup - for region in comm_groups["region"].unique(): - i_reg = comm_groups["region"] == region - for process in comm_groups[i_reg]["process"].unique(): - i_reg_prc = i_reg & (comm_groups["process"] == process) - for cset in comm_groups[i_reg_prc]["csets"].unique(): - i_reg_prc_cset = i_reg_prc & (comm_groups["csets"] == cset) - for io in ["IN", "OUT"]: - i_reg_prc_cset_io = i_reg_prc_cset & (comm_groups["io"] == io) - comm_groups.loc[i_reg_prc_cset_io, "commoditygroup"] = sum( - i_reg_prc_cset_io - ) + + # Original logic, slow for large tables + # _count_comm_group_looped() + # Much faster vectorised version + _count_comm_group_vectorised(comm_groups) def name_comm_group(df): """ @@ -1085,23 +1080,10 @@ def name_comm_group(df): comm_groups["commoditygroup"] = comm_groups.apply(name_comm_group, axis=1) # Determine default PCG according to Veda - comm_groups["DefaultVedaPCG"] = None - for region in comm_groups["region"].unique(): - i_reg = comm_groups["region"] == region - for process in comm_groups[i_reg]["process"]: - i_reg_prc = i_reg & (comm_groups["process"] == process) - default_set = False - for io in ["OUT", "IN"]: - if default_set: - break - i_reg_prc_io = i_reg_prc & (comm_groups["io"] == io) - for cset in csets_ordered_for_pcg: - i_reg_prc_io_cset = i_reg_prc_io & (comm_groups["csets"] == cset) - df = comm_groups[i_reg_prc_io_cset] - if not df.empty: - comm_groups.loc[i_reg_prc_io_cset, "DefaultVedaPCG"] = True - default_set = True - break + # original logic, slow for large tables + # comm_groups = pcg_looped(comm_groups, csets_ordered_for_pcg) + # vectorised logic, much faster + comm_groups = _process_comm_groups_vectorised(comm_groups, csets_ordered_for_pcg) # Add standard Veda PCGS named contrary to name_comm_group if reg_prc_veda_pcg.shape[0]: @@ -1135,6 +1117,101 @@ def name_comm_group(df): return tables +def _count_comm_group_looped(comm_groups): + """Store the number of IN/OUT commodities of the same type per Region and Process in CommodityGroup + TODO remove this function once _count_comm_group_vectorised is validated? + """ + for region in comm_groups["region"].unique(): + i_reg = comm_groups["region"] == region + for process in tqdm( + comm_groups[i_reg]["process"].unique(), f"Summing commodities for {region}" + ): + i_reg_prc = i_reg & (comm_groups["process"] == process) + for cset in comm_groups[i_reg_prc]["csets"].unique(): + i_reg_prc_cset = i_reg_prc & (comm_groups["csets"] == cset) + for io in ["IN", "OUT"]: + i_reg_prc_cset_io = i_reg_prc_cset & (comm_groups["io"] == io) + comm_groups.loc[i_reg_prc_cset_io, "commoditygroup"] = sum( + i_reg_prc_cset_io + ) + + +def _count_comm_group_vectorised(comm_groups): + """Much faster vectorised version + Stores the number of IN/OUT commodities of the same type per Region and Process in CommodityGroup + """ + comm_groups["commoditygroup"] = ( + comm_groups.groupby(["region", "process", "csets", "io"]).transform("count") + )["commoditygroup"] + # set comoditygroup to 0 for io rows that aren't IN or OUT + comm_groups.loc[~comm_groups["io"].isin(["IN", "OUT"]), "commoditygroup"] = 0 + + +def _process_comm_groups_looped( + comm_groups: pd.DataFrame, csets_ordered_for_pcg: list[str] +) -> pd.DataFrame: + """Original, looped version of the default pcg logic. + Sets the first commodity group in the list of csets_ordered_for_pcg as the default pcg for each region/process/io combination, + but setting the io="OUT" subset as default before "IN". + TODO remove this function once _count_comm_group_vectorised is validated? + """ + comm_groups["DefaultVedaPCG"] = None + for region in tqdm( + comm_groups["region"].unique(), + desc=f"Determining default Primary Commodity Groups", + ): + i_reg = comm_groups["region"] == region + for process in comm_groups[i_reg]["process"]: + i_reg_prc = i_reg & (comm_groups["process"] == process) + default_set = False + for io in ["OUT", "IN"]: + if default_set: + break + i_reg_prc_io = i_reg_prc & (comm_groups["io"] == io) + for cset in csets_ordered_for_pcg: + i_reg_prc_io_cset = i_reg_prc_io & (comm_groups["csets"] == cset) + df = comm_groups[i_reg_prc_io_cset] + if not df.empty: + comm_groups.loc[i_reg_prc_io_cset, "DefaultVedaPCG"] = True + default_set = True + break + return comm_groups + + +def _process_comm_groups_vectorised( + comm_groups_test: pd.DataFrame, csets_ordered_for_pcg: list[str] +) -> pd.DataFrame: + """Vectorised version of the pcg_looped() logic, for speedup (~18x faster) with large commodity tables. + See Section 3.7.2.2, pg 80. of `TIMES Documentation PART IV` for details. + :param comm_groups_test: 'Process' DataFrame with columns ["region", "process", "io", "csets", "commoditygroup"] + :param csets_ordered_for_pcg: List of csets in the order they should be considered for default pcg + """ + + def _set_default_veda_pcg(group): + """For a given [region, process] group, default group is set as the first cset in the `csets_ordered_for_pcg` list, which is an output, if + one exists, otherwise the first input.""" + if not group["csets"].isin(csets_ordered_for_pcg).all(): + return group + + for io in ["OUT", "IN"]: + for cset in csets_ordered_for_pcg: + group.loc[ + (group["io"] == io) & (group["csets"] == cset), "DefaultVedaPCG" + ] = True + if group["DefaultVedaPCG"].any(): + break + return group + + comm_groups_test["DefaultVedaPCG"] = None + comm_groups_subset = comm_groups_test.groupby( + ["region", "process"], sort=False, as_index=False + ).apply(_set_default_veda_pcg) + comm_groups_subset = comm_groups_subset.reset_index( + level=0, drop=True + ).sort_index() # back to the original index and row order + return comm_groups_subset + + def complete_commodity_groups( config: datatypes.Config, tables: Dict[str, DataFrame], From e80554321828a8d731e4d7fd9dbb155aae6f9493 Mon Sep 17 00:00:00 2001 From: Sam West Date: Thu, 15 Feb 2024 09:40:45 +1100 Subject: [PATCH 21/26] add tqdm package for progress bars --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 484e835..32f27a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,8 @@ dependencies = [ "more-itertools", "openpyxl >= 3.0, < 3.1", "pandas >= 2.1", - "pyarrow" + "pyarrow", + "tqdm" ] [project.optional-dependencies] From 6078f4fdea2415acbf400e0c1757ed954aeaa496 Mon Sep 17 00:00:00 2001 From: Sam West Date: Thu, 15 Feb 2024 10:05:30 +1100 Subject: [PATCH 22/26] added percentage changes to benchmark results --- utils/run_benchmarks.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/utils/run_benchmarks.py b/utils/run_benchmarks.py index 5ae8876..7230dc1 100644 --- a/utils/run_benchmarks.py +++ b/utils/run_benchmarks.py @@ -341,12 +341,28 @@ def run_all_benchmarks( addi_regressions = df[df["Additional"] > df["M Additional"]]["Benchmark"] time_regressions = df[df["Time (s)"] > 2 * df["M Time (s)"]]["Benchmark"] - runtime_change = df["Time (s)"].sum() - df["M Time (s)"].sum() - print(f"Change in runtime: {runtime_change:+.2f} s") - correct_change = df["Correct"].sum() - df["M Correct"].sum() - print(f"Change in correct rows: {correct_change:+d}") - additional_change = df["Additional"].sum() - df["M Additional"].sum() - print(f"Change in additional rows: {additional_change:+d}") + our_time = df["Time (s)"].sum() + main_time = df["M Time (s)"].sum() + runtime_change = our_time - main_time + + print(f"Total runtime: {our_time:.2f}s (main: {main_time:.2f}s)") + print( + f"Change in runtime (negative == faster): {runtime_change:+.2f}s ({100*runtime_change/main_time:+.1f}%)" + ) + + our_correct = df["Correct"].sum() + main_correct = df["M Correct"].sum() + correct_change = our_correct - main_correct + print( + f"Change in correct rows (higher == better): {correct_change:+d} ({100*correct_change/main_correct:+.1f}%)" + ) + + our_additional_rows = df["Additional"].sum() + main_additional_rows = df["M Additional"].sum() + additional_change = our_additional_rows - main_additional_rows + print( + f"Change in additional rows: {additional_change:+d} ({100*additional_change/main_additional_rows:+.1f}%)" + ) if len(accu_regressions) + len(addi_regressions) + len(time_regressions) > 0: print() From 20becfa0e311a1cfa46a96bc6f7f09266b3b8e99 Mon Sep 17 00:00:00 2001 From: Sam West Date: Thu, 15 Feb 2024 10:54:32 +1100 Subject: [PATCH 23/26] Added unit tests, test data, pytest lib/config and CI step removed old looped function now vectorised outputs are verified as identical --- .github/workflows/ci.yml | 6 ++ pyproject.toml | 34 +++++--- tests/data/austimes_pcg_test_data.parquet | Bin 0 -> 54689 bytes .../comm_groups_austimes_test_data.parquet | Bin 0 -> 44975 bytes tests/test_transforms.py | 67 ++++++++++++++++ xl2times/transforms.py | 74 ++++-------------- 6 files changed, 113 insertions(+), 68 deletions(-) create mode 100644 tests/data/austimes_pcg_test_data.parquet create mode 100644 tests/data/comm_groups_austimes_test_data.parquet create mode 100644 tests/test_transforms.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f42d5f..323aefc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,12 @@ jobs: pre-commit install pre-commit run --all-files + - name: Run unit tests + working-directory: xl2times + run: | + source .venv/bin/activate + pytest + # ---------- Prepare ETSAP Demo models - uses: actions/checkout@v3 diff --git a/pyproject.toml b/pyproject.toml index 32f27a5..6939e1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,22 +14,28 @@ requires-python = ">=3.10" license = { file = "LICENSE" } keywords = [] classifiers = [ - "Development Status :: 4 - Beta", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", ] dependencies = [ - "GitPython >= 3.1.31, < 3.2", - "more-itertools", - "openpyxl >= 3.0, < 3.1", - "pandas >= 2.1", - "pyarrow", - "tqdm" + "GitPython >= 3.1.31, < 3.2", + "more-itertools", + "openpyxl >= 3.0, < 3.1", + "pandas >= 2.1", + "pyarrow", + "tqdm", ] [project.optional-dependencies] -dev = ["black", "pre-commit", "tabulate"] +dev = [ + "black", + "pre-commit", + "tabulate", + "pytest", + "pytest-cov" +] [project.urls] Documentation = "https://github.com/etsap-TIMES/xl2times#readme" @@ -38,3 +44,9 @@ Source = "https://github.com/etsap-TIMES/xl2times" [project.scripts] xl2times = "xl2times.__main__:main" + +[tool.pytest.ini_options] +# don't print runtime warnings +filterwarnings = ["ignore::DeprecationWarning", "ignore::UserWarning", "ignore::FutureWarning"] +# show output, print test coverage report +addopts = '-s --durations=0 --durations-min=5.0 --tb=native --cov-report term --cov-report html --cov=xl2times --cov=utils' diff --git a/tests/data/austimes_pcg_test_data.parquet b/tests/data/austimes_pcg_test_data.parquet new file mode 100644 index 0000000000000000000000000000000000000000..c3346d9cfdd77625389e6c62474787d0ca803f7a GIT binary patch literal 54689 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ÐQbz01D@QSTO-KMoI@kXBjlNXfODZIN!f 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≫gx+i)N@JRGvhardsnvO?@2h{|a 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

4-;Ta}iI(<{_Sr%}2ZtI|ES_SAggl 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 None: """ - for region in comm_groups["region"].unique(): - i_reg = comm_groups["region"] == region - for process in tqdm( - comm_groups[i_reg]["process"].unique(), f"Summing commodities for {region}" - ): - i_reg_prc = i_reg & (comm_groups["process"] == process) - for cset in comm_groups[i_reg_prc]["csets"].unique(): - i_reg_prc_cset = i_reg_prc & (comm_groups["csets"] == cset) - for io in ["IN", "OUT"]: - i_reg_prc_cset_io = i_reg_prc_cset & (comm_groups["io"] == io) - comm_groups.loc[i_reg_prc_cset_io, "commoditygroup"] = sum( - i_reg_prc_cset_io - ) - - -def _count_comm_group_vectorised(comm_groups): - """Much faster vectorised version - Stores the number of IN/OUT commodities of the same type per Region and Process in CommodityGroup + Store the number of IN/OUT commodities of the same type per Region and Process in CommodityGroup. + `comm_groups` is modified in-place + Args: + comm_groups: 'Process' DataFrame with additional columns "commoditygroup" """ + comm_groups["commoditygroup"] = 0 + comm_groups["commoditygroup"] = ( comm_groups.groupby(["region", "process", "csets", "io"]).transform("count") )["commoditygroup"] @@ -1147,44 +1132,19 @@ def _count_comm_group_vectorised(comm_groups): comm_groups.loc[~comm_groups["io"].isin(["IN", "OUT"]), "commoditygroup"] = 0 -def _process_comm_groups_looped( - comm_groups: pd.DataFrame, csets_ordered_for_pcg: list[str] -) -> pd.DataFrame: - """Original, looped version of the default pcg logic. - Sets the first commodity group in the list of csets_ordered_for_pcg as the default pcg for each region/process/io combination, - but setting the io="OUT" subset as default before "IN". - TODO remove this function once _count_comm_group_vectorised is validated? - """ - comm_groups["DefaultVedaPCG"] = None - for region in tqdm( - comm_groups["region"].unique(), - desc=f"Determining default Primary Commodity Groups", - ): - i_reg = comm_groups["region"] == region - for process in comm_groups[i_reg]["process"]: - i_reg_prc = i_reg & (comm_groups["process"] == process) - default_set = False - for io in ["OUT", "IN"]: - if default_set: - break - i_reg_prc_io = i_reg_prc & (comm_groups["io"] == io) - for cset in csets_ordered_for_pcg: - i_reg_prc_io_cset = i_reg_prc_io & (comm_groups["csets"] == cset) - df = comm_groups[i_reg_prc_io_cset] - if not df.empty: - comm_groups.loc[i_reg_prc_io_cset, "DefaultVedaPCG"] = True - default_set = True - break - return comm_groups - - def _process_comm_groups_vectorised( comm_groups_test: pd.DataFrame, csets_ordered_for_pcg: list[str] ) -> pd.DataFrame: - """Vectorised version of the pcg_looped() logic, for speedup (~18x faster) with large commodity tables. - See Section 3.7.2.2, pg 80. of `TIMES Documentation PART IV` for details. - :param comm_groups_test: 'Process' DataFrame with columns ["region", "process", "io", "csets", "commoditygroup"] - :param csets_ordered_for_pcg: List of csets in the order they should be considered for default pcg + """Dets the first commodity group in the list of csets_ordered_for_pcg as the default pcg for each region/process/io combination, + but setting the io="OUT" subset as default before "IN". + + See: + Section 3.7.2.2, pg 80. of `TIMES Documentation PART IV` for details. + Args: + comm_groups_test: 'Process' DataFrame with columns ["region", "process", "io", "csets", "commoditygroup"] + csets_ordered_for_pcg: List of csets in the order they should be considered for default pcg + Returns: + Processed DataFrame with a new column "DefaultVedaPCG" set to True for the default pcg in each region/process/io combination. """ def _set_default_veda_pcg(group): From cb82f83cbbecbf08ad7aa7faf10880645f128af5 Mon Sep 17 00:00:00 2001 From: Sam West Date: Thu, 15 Feb 2024 12:46:14 +1100 Subject: [PATCH 24/26] removed commented calls, fixed typos --- xl2times/transforms.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/xl2times/transforms.py b/xl2times/transforms.py index d3b14bf..6464b79 100644 --- a/xl2times/transforms.py +++ b/xl2times/transforms.py @@ -1058,9 +1058,7 @@ def generate_commodity_groups( # Commodity groups by process, region and commodity comm_groups = pd.merge(prc_top, comm_set, on=["region", "commodity"]) - # Original logic, slow for large tables - # _count_comm_group_looped() - # Much faster vectorised version + # Add columns for the number of IN/OUT commodities of each type _count_comm_group_vectorised(comm_groups) def name_comm_group(df): @@ -1078,10 +1076,7 @@ def name_comm_group(df): # Replace commodity group member count with the name comm_groups["commoditygroup"] = comm_groups.apply(name_comm_group, axis=1) - # Determine default PCG according to Veda - # original logic, slow for large tables - # comm_groups = pcg_looped(comm_groups, csets_ordered_for_pcg) - # vectorised logic, much faster + # Determine default PCG according to Veda's logic comm_groups = _process_comm_groups_vectorised(comm_groups, csets_ordered_for_pcg) # Add standard Veda PCGS named contrary to name_comm_group @@ -1135,7 +1130,7 @@ def _count_comm_group_vectorised(comm_groups: pd.DataFrame) -> None: def _process_comm_groups_vectorised( comm_groups_test: pd.DataFrame, csets_ordered_for_pcg: list[str] ) -> pd.DataFrame: - """Dets the first commodity group in the list of csets_ordered_for_pcg as the default pcg for each region/process/io combination, + """Sets the first commodity group in the list of csets_ordered_for_pcg as the default pcg for each region/process/io combination, but setting the io="OUT" subset as default before "IN". See: From e29c8b0294a80c32445300a5fd4841b98bb37044 Mon Sep 17 00:00:00 2001 From: Sam West Date: Thu, 15 Feb 2024 13:20:22 +1100 Subject: [PATCH 25/26] renamed input param --- xl2times/transforms.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xl2times/transforms.py b/xl2times/transforms.py index 6464b79..eb67b2d 100644 --- a/xl2times/transforms.py +++ b/xl2times/transforms.py @@ -1128,7 +1128,7 @@ def _count_comm_group_vectorised(comm_groups: pd.DataFrame) -> None: def _process_comm_groups_vectorised( - comm_groups_test: pd.DataFrame, csets_ordered_for_pcg: list[str] + comm_groups: pd.DataFrame, csets_ordered_for_pcg: list[str] ) -> pd.DataFrame: """Sets the first commodity group in the list of csets_ordered_for_pcg as the default pcg for each region/process/io combination, but setting the io="OUT" subset as default before "IN". @@ -1136,7 +1136,7 @@ def _process_comm_groups_vectorised( See: Section 3.7.2.2, pg 80. of `TIMES Documentation PART IV` for details. Args: - comm_groups_test: 'Process' DataFrame with columns ["region", "process", "io", "csets", "commoditygroup"] + comm_groups: 'Process' DataFrame with columns ["region", "process", "io", "csets", "commoditygroup"] csets_ordered_for_pcg: List of csets in the order they should be considered for default pcg Returns: Processed DataFrame with a new column "DefaultVedaPCG" set to True for the default pcg in each region/process/io combination. @@ -1157,8 +1157,8 @@ def _set_default_veda_pcg(group): break return group - comm_groups_test["DefaultVedaPCG"] = None - comm_groups_subset = comm_groups_test.groupby( + comm_groups["DefaultVedaPCG"] = None + comm_groups_subset = comm_groups.groupby( ["region", "process"], sort=False, as_index=False ).apply(_set_default_veda_pcg) comm_groups_subset = comm_groups_subset.reset_index( From 83ec9c738c9b92cf1e4af404448d20a328161197 Mon Sep 17 00:00:00 2001 From: Sam West Date: Thu, 15 Feb 2024 16:26:35 +1100 Subject: [PATCH 26/26] addressed final comments in #184 fixed bug in git code when no remote is called 'origin' --- .gitignore | 1 + README.md | 3 ++- pyproject.toml | 2 +- utils/dd_to_csv.py | 3 ++- utils/run_benchmarks.py | 6 ++++-- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 85bc0d3..2bb5d07 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ benchmarks/ docs/_build/ docs/api/ .coverage +/out.txt diff --git a/README.md b/README.md index 7804d88..00416c7 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,8 @@ python -m pip install --upgrade build python -m pip install --upgrade twine rm -rf dist python -m build -python -m twine upload dist/*``` +python -m twine upload dist/* +``` ## Contributing diff --git a/pyproject.toml b/pyproject.toml index 6939e1e..ab7f0f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=61.0.0", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools] -packages = ["xl2times", "utils"] +packages = ["xl2times"] [project] name = "xl2times" diff --git a/utils/dd_to_csv.py b/utils/dd_to_csv.py index 5de10fd..a9e1132 100644 --- a/utils/dd_to_csv.py +++ b/utils/dd_to_csv.py @@ -1,4 +1,5 @@ import argparse +import sys from collections import defaultdict import json import os @@ -229,4 +230,4 @@ def main(arg_list: None | list[str] = None): if __name__ == "__main__": - main() + main(sys.argv[1:]) diff --git a/utils/run_benchmarks.py b/utils/run_benchmarks.py index 7230dc1..74e0e53 100644 --- a/utils/run_benchmarks.py +++ b/utils/run_benchmarks.py @@ -159,7 +159,7 @@ def run_benchmark( sys.exit(5) else: # If debug option is set, run as a function call to allow stepping with a debugger. - from utils.dd_to_csv import main + from dd_to_csv import main main([dd_folder, csv_folder]) @@ -259,7 +259,9 @@ def run_all_benchmarks( # The rest of this script checks regressions against main # so skip it if we're already on main repo = git.Repo(".") # pyright: ignore - origin = repo.remotes.origin + origin = ( + repo.remotes.origin if "origin" in repo.remotes else repo.remotes[0] + ) # don't assume remote is called 'origin' origin.fetch("main") if "main" not in repo.heads: repo.create_head("main", origin.refs.main).set_tracking_branch(origin.refs.main)