From b5c50d92798654e049e34d12a2dde334c191f818 Mon Sep 17 00:00:00 2001 From: trevorb1 Date: Wed, 28 Sep 2022 13:49:52 +0200 Subject: [PATCH 1/9] updates to work with otoole/develop --- resources/otoole_files.csv | 67 +++++++++++++++++++++++++++++ workflow/Snakefile | 4 +- workflow/rules/osemosys.smk | 56 ++++++++++++++++++------ workflow/scripts/check_inputs.py | 36 ++++++++++------ workflow/scripts/create_modelrun.py | 34 +++++++++------ 5 files changed, 157 insertions(+), 40 deletions(-) create mode 100644 resources/otoole_files.csv diff --git a/resources/otoole_files.csv b/resources/otoole_files.csv new file mode 100644 index 0000000..1592269 --- /dev/null +++ b/resources/otoole_files.csv @@ -0,0 +1,67 @@ +inputs,outputs +AccumulatedAnnualDemand,AccumulatedNewCapacity +AnnualEmissionLimit,AnnualEmissions +AnnualExogenousEmission,AnnualFixedOperatingCost +AvailabilityFactor,AnnualTechnologyEmission +CapacityFactor,AnnualTechnologyEmissionByMode +CapacityOfOneTechnologyUnit,AnnualVariableOperatingCost +CapacityToActivityUnit,CapitalInvestment +CapitalCost,Demand +CapitalCostStorage,DiscountedTechnologyEmissionsPenalty +Conversionld,NewCapacity +Conversionlh,ProductionByTechnology +Conversionls,ProductionByTechnologyAnnual +DAILYTIMEBRACKET,RateOfActivity +DaysInDayType,RateOfProductionByTechnology +DaySplit,RateOfProductionByTechnologyByMode +DAYTYPE,RateOfUseByTechnology +default_values,RateOfUseByTechnologyByMode +DepreciationMethod,TotalAnnualTechnologyActivityByMode +DiscountRate,TotalCapacityAnnual +DiscountRateIdv,TotalTechnologyAnnualActivity +DiscountRateStorage,TotalTechnologyModelPeriodActivity +EMISSION,UseByTechnology +EmissionActivityRatio, +EmissionsPenalty, +FixedCost, +FUEL, +InputActivityRatio, +MinStorageCharge, +ModelPeriodEmissionLimit, +ModelPeriodExogenousEmission, +MODE_OF_OPERATION, +OperationalLife, +OperationalLifeStorage, +OutputActivityRatio, +REGION, +REMinProductionTarget, +ReserveMargin, +ReserveMarginTagFuel, +ReserveMarginTagTechnology, +ResidualCapacity, +ResidualStorageCapacity, +RETagFuel, +RETagTechnology, +SEASON, +SpecifiedAnnualDemand, +SpecifiedDemandProfile, +STORAGE, +StorageLevelStart, +StorageMaxChargeRate, +StorageMaxDischargeRate, +TECHNOLOGY, +TechnologyFromStorage, +TechnologyToStorage, +TIMESLICE, +TotalAnnualMaxCapacity, +TotalAnnualMaxCapacityInvestment, +TotalAnnualMinCapacity, +TotalAnnualMinCapacityInvestment, +TotalTechnologyAnnualActivityLowerLimit, +TotalTechnologyAnnualActivityUpperLimit, +TotalTechnologyModelPeriodActivityLowerLimit, +TotalTechnologyModelPeriodActivityUpperLimit, +TradeRoute, +VariableCost, +YEAR, +YearSplit, diff --git a/workflow/Snakefile b/workflow/Snakefile index 6722b47..cf67c0d 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -16,6 +16,8 @@ GROUPS = pd.read_csv(config['parameters'])['group'].unique() MODELRUNS = range((len(GROUPS) + 1) * config['replicates']) AGG_RESULTS = pd.read_csv(config["agg_results"]) ZIP = '.gz' if config['zip'] else '' +INPUT_FILES = pd.read_csv('resources/otoole_files.csv')['inputs'].to_list() +OUTPUT_FILES = pd.read_csv('resources/otoole_files.csv')['outputs'].dropna().to_list() include: "rules/osemosys.smk" include: "rules/results.smk" @@ -30,7 +32,7 @@ onsuccess: rule all: input: expand("results/SA_{scenario}.{extension}", scenario=SCENARIOS.index, extension=['csv', 'png']), - expand("results/{scenario}/model_{model_run}/results/{x}.csv", x=RESULTS.index, model_run=MODELRUNS, scenario=SCENARIOS.index) + expand("results/{scenario}/model_{model_run}/results/{x}.csv", x=OUTPUT_FILES, model_run=MODELRUNS, scenario=SCENARIOS.index) message: "Running pipeline to generate the files '{input}'" rule get_status: diff --git a/workflow/rules/osemosys.smk b/workflow/rules/osemosys.smk index ed9abd0..39d674d 100644 --- a/workflow/rules/osemosys.smk +++ b/workflow/rules/osemosys.smk @@ -13,26 +13,52 @@ def solver_output(wildcards): return rules.unzip_solution.output def datapackage_from_scenario(wildcards): - return SCENARIOS.loc[int(wildcards.scenario), 'path'] + return SCENARIOS.loc[int(wildcards.scenario), 'datapackage'] -rule copy_datapackage: - message: "Copying and modifying datapackage for '{output.folder}'" +def config_from_scenario(wildcards): + return SCENARIOS.loc[int(wildcards.scenario), 'config'] + +rule create_model_data: + message: "Copying and modifying data for '{params.folder}'" input: datapackage=datapackage_from_scenario, - sample="modelruns/{scenario}/model_{model_run}/sample_{model_run}.txt" + sample="modelruns/{scenario}/model_{model_run}/sample_{model_run}.txt", + config=config_from_scenario log: "results/log/copy_datapackage_{scenario}_{model_run}.log" + params: + folder=directory("results/{scenario}/model_{model_run}") conda: "../envs/otoole.yaml" group: "gen_lp" output: - folder=directory("results/{scenario}/model_{model_run}"), - datapackage="results/{scenario}/model_{model_run}/datapackage.json", + csv=expand("results/{{scenario}}/model_{{model_run}}/data/{csv}.csv", csv=INPUT_FILES) + shell: + "python workflow/scripts/create_modelrun.py {input.datapackage} {params.folder}/data {input.sample} {input.config}" + +rule copy_datapackage: + message: "Copying datapackage for '{params.folder}'" + input: + json=datapackage_from_scenario, + yaml=config_from_scenario, + csvs=rules.create_model_data.output, + log: "results/log/copy_datapackage_{scenario}_{model_run}.log" + params: + folder="results/{scenario}/model_{model_run}", + output: + json="results/{scenario}/model_{model_run}/datapackage.json", + yaml="results/{scenario}/model_{model_run}/config.yaml", + log: + "results/log/copy_datapackage_{scenario}_{model_run}.log" shell: - "python workflow/scripts/create_modelrun.py {input.datapackage} {output.folder} {input.sample}" + """ + cp {input.json} {output.json} + cp {input.yaml} {output.yaml} + """ rule generate_datafile: message: "Generating datafile for '{output}'" input: - datapackage="results/{scenario}/model_{model_run}/datapackage.json" + datapackage="results/{scenario}/model_{model_run}/datapackage.json", + config="results/{scenario}/model_{model_run}/config.yaml" output: temp("temp/{scenario}/model_{model_run}.txt") conda: "../envs/otoole.yaml" @@ -40,7 +66,7 @@ rule generate_datafile: log: "results/log/otoole_{scenario}_{model_run}.log" shell: - "otoole -v convert datapackage datafile {input} {output} 2> {log}" + "otoole -v convert datapackage datafile {input.datapackage} {output} {input.config} 2> {log}" rule modify_model_file: message: "Adding MODEX sets to model file" @@ -167,15 +193,19 @@ rule process_solution: group: 'results' input: solution=solver_output, - data="results/{scenario}/model_{model_run}/datapackage.json" - output: ["results/{{scenario}}/model_{{model_run}}/results/{}.csv".format(x) for x in RESULTS.index] + datapackage="results/{scenario}/model_{model_run}/datapackage.json", + config="results/{scenario}/model_{model_run}/config.yaml", + output: + # ["results/{{scenario}}/model_{{model_run}}/results/{}.csv".format(x) for x in RESULTS.index] + expand("results/{{scenario}}/model_{{model_run}}/results/{csv}.csv", csv=OUTPUT_FILES) conda: "../envs/otoole.yaml" log: "results/log/process_solution_{scenario}_{model_run}.log" params: folder="results/{scenario}/model_{model_run}/results" - shell: """ + shell: + """ mkdir -p {params.folder} - otoole -v results {config[solver]} csv {input.solution} {params.folder} --input_datapackage {input.data} &> {log} + otoole -v results {config[solver]} csv {input.solution} {params.folder} --input_datapackage {input.datapackage} {input.config} &> {log} """ rule get_statistics: diff --git a/workflow/scripts/check_inputs.py b/workflow/scripts/check_inputs.py index 5a1f728..d00ced4 100644 --- a/workflow/scripts/check_inputs.py +++ b/workflow/scripts/check_inputs.py @@ -14,10 +14,12 @@ from pathlib import Path from typing import Dict, List, Union, Any from otoole.read_strategies import ReadDatapackage +from otoole.utils import _read_file import csv import pandas as pd import yaml import math +import os from logging import getLogger @@ -62,17 +64,19 @@ def check_scenario_file(path : str): If scenario name is not an int """ + expected_headings = ['name','description','datapackage','config'] + if not Path(path).is_file(): raise FileNotFoundError( f"Scenario file {path} does not exist. Create the file " - "'resources/scenarios.csv' with the headings ['name','description','path']" + f"'resources/scenarios.csv' with the headings {expected_headings}" ) df = pd.read_csv(path) - if list(df) != ['name','description','path']: + if list(df) != expected_headings: raise ValueError( f"Scenario file {path} not formatted correctly. Expected the " - f"headings ['name','description','path']. Recieved headings {list(df)}" + f"headings {expected_headings}. Recieved headings {list(df)}" ) if df.empty: @@ -84,7 +88,7 @@ def check_scenario_file(path : str): f"All scenario names must be of type int. Scenario {name} is not an int" ) - for name, datapackage in zip(df['name'], df['path']): + for name, datapackage in zip(df['name'], df['datapackage']): check_datapackage(name, datapackage) def check_model_file(path: str): @@ -358,7 +362,8 @@ def check_parameter_data( def check_parameters( datapackage : str, - parameters: List[Dict[str, Union[str, int, float]]] + parameters : List[Dict[str, Union[str, int, float]]], + user_config : Dict ): """Checks parameter file. @@ -368,6 +373,7 @@ def check_parameters( path to master datapackage parameters: List[Dict[str, Union[str, int, float]]] Flattened parameter file + user_config : Raises ------ @@ -396,14 +402,12 @@ def check_parameters( ) # get model parameter definitions - reader = ReadDatapackage() - model_params, _ = reader.read(datapackage) - model_config = reader.input_config + model_params, _ = ReadDatapackage(user_config=user_config).read(datapackage) for parameter in parameters: - check_parameter_data(parameter, model_config) + check_parameter_data(parameter, user_config) check_parameter_interpolation_data(parameter) - check_parameter_index_data(parameter, model_config, model_params) + check_parameter_index_data(parameter, user_config, model_params) def read_parameters_file(path : str) -> List[Dict[str, Union[str, int, float]]]: """Reads in a flattened CSV file @@ -438,10 +442,16 @@ def main(config : Dict[str, Any]): check_solver(config['solver']) check_replicates(config['replicates']) - # check parameter file - datapackages = pd.read_csv(config['datapackage'])['path'].to_list() + # read in datapackage path and user_config file + scenario_df = pd.read_csv(config['datapackage']) + datapackages = scenario_df['datapackage'].to_list() + configs = scenario_df['config'].to_list() parameters = read_parameters_file(config['parameters']) - check_parameters(datapackages[0], parameters) + _, ending = os.path.splitext(configs[0]) + with open(configs[0], "r") as f: + user_config = _read_file(f, ending) + + check_parameters(datapackages[0], parameters, user_config) if __name__ == "__main__": diff --git a/workflow/scripts/create_modelrun.py b/workflow/scripts/create_modelrun.py index 902c277..cbf399c 100644 --- a/workflow/scripts/create_modelrun.py +++ b/workflow/scripts/create_modelrun.py @@ -5,7 +5,7 @@ Path to the master model datapackage - Path to the new model file + Path to the new model file directory Path the sample file @@ -30,7 +30,9 @@ import pandas as pd import numpy as np from otoole.read_strategies import ReadDatapackage -from otoole.write_strategies import WriteDatapackage +from otoole.write_strategies import WriteCsv +from otoole.utils import _read_file +import os from logging import getLogger @@ -288,27 +290,33 @@ def modify_parameters( return model_params -def main(input_filepath, output_filepath, parameters: List[Dict[str, Union[str, int, float]]]): +def main( + input_filepath, + output_filepath, + parameters: List[Dict[str, Union[str, int, float]]], + user_config): - reader = ReadDatapackage() - - writer = WriteDatapackage() + model_params, default_values = ReadDatapackage(user_config=user_config).read(input_filepath) logger.info("Reading datapackage {}".format(input_filepath)) - model_params, default_values = reader.read(input_filepath) for name, parameter in model_params.items(): parameter = parameter.sort_index() model_params[name] = parameter - config = reader.input_config - model_params = modify_parameters(model_params, parameters, config) - writer.write(model_params, output_filepath, default_values) + model_params = modify_parameters(model_params, parameters, user_config) + # WriteDatapackage(user_config=user_config).write(model_params, output_filepath, default_values) + WriteCsv(user_config=user_config, ).write(model_params, output_filepath, default_values) if __name__ == "__main__": - if len(sys.argv) != 4: - print("Usage: python create_modelrun.py ") + if len(sys.argv) != 5: + print("Usage: python create_modelrun.py ") else: with open(sys.argv[3], 'r') as csv_file: sample = list(csv.DictReader(csv_file)) - main(sys.argv[1], sys.argv[2], sample) + + _, ending = os.path.splitext(sys.argv[4]) + with open(sys.argv[4], "r") as f: + user_config = _read_file(f, ending) + + main(sys.argv[1], sys.argv[2], sample, user_config) From 71dd94672f543e5def73abe45effcea0988d15c2 Mon Sep 17 00:00:00 2001 From: trevorb1 Date: Wed, 28 Sep 2022 15:32:45 +0200 Subject: [PATCH 2/9] added file extension check --- workflow/scripts/check_inputs.py | 39 ++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/workflow/scripts/check_inputs.py b/workflow/scripts/check_inputs.py index d00ced4..b194a13 100644 --- a/workflow/scripts/check_inputs.py +++ b/workflow/scripts/check_inputs.py @@ -39,11 +39,15 @@ def check_datapackage(scenario : int, path: str): ------ FileNotFoundError If datapackage does not exist + ValueError + If datapackage is not a .json + """ if not Path(path).is_file(): raise FileNotFoundError( f"Datapackage {path} does not exist for scenario {scenario}." ) + check_file_extension(path, 'json') def check_scenario_file(path : str): """Checks the validity of the scenario file. @@ -427,6 +431,30 @@ def read_parameters_file(path : str) -> List[Dict[str, Union[str, int, float]]]: parameters = list(csv.DictReader(csv_file)) return parameters +def check_file_extension(file_name, extension): + """Checks the file for the correct extension. + + Parameters + ---------- + file_name : str + Name of file + extension : str + Expected file extension + + Rasies + ------ + ValueError + If the actual file extension is not the expected. + """ + _, ending = os.path.splitext(file_name) + if not extension.startswith('.'): + extension = f".{extension}" + if ending != extension: + raise ValueError( + f"Input configuration file must be a {extension} file. Recieved the" + f"file {file_name} with the extension {ending}" + ) + def main(config : Dict[str, Any]): """Runs a series of checks on input data. @@ -434,6 +462,11 @@ def main(config : Dict[str, Any]): ---------- config : Dict[str, Any] Parsed config.yaml file + + Rasies + ------ + ValueError + If the input config file is not a yaml file """ # check config file @@ -447,12 +480,14 @@ def main(config : Dict[str, Any]): datapackages = scenario_df['datapackage'].to_list() configs = scenario_df['config'].to_list() parameters = read_parameters_file(config['parameters']) - _, ending = os.path.splitext(configs[0]) with open(configs[0], "r") as f: - user_config = _read_file(f, ending) + user_config = _read_file(f, ".yaml") + # check parameter file check_parameters(datapackages[0], parameters, user_config) + # check otoole inputs + if __name__ == "__main__": From 8f393b3d4770a1e1e0de7635c899088e85e63a7e Mon Sep 17 00:00:00 2001 From: trevorb1 Date: Wed, 28 Sep 2022 16:08:43 +0200 Subject: [PATCH 3/9] added otoole input data check --- workflow/scripts/check_inputs.py | 46 +++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/workflow/scripts/check_inputs.py b/workflow/scripts/check_inputs.py index b194a13..575d3f1 100644 --- a/workflow/scripts/check_inputs.py +++ b/workflow/scripts/check_inputs.py @@ -89,12 +89,15 @@ def check_scenario_file(path : str): for name in df['name']: if not type(name) is int: raise TypeError( - f"All scenario names must be of type int. Scenario {name} is not an int" + f"All scenario names must be of type int. Scenario {name} is " + f"not an int" ) for name, datapackage in zip(df['name'], df['datapackage']): check_datapackage(name, datapackage) + + def check_model_file(path: str): """Checks for existance of model file. @@ -431,7 +434,7 @@ def read_parameters_file(path : str) -> List[Dict[str, Union[str, int, float]]]: parameters = list(csv.DictReader(csv_file)) return parameters -def check_file_extension(file_name, extension): +def check_file_extension(file_name : str, extension : str): """Checks the file for the correct extension. Parameters @@ -455,6 +458,40 @@ def check_file_extension(file_name, extension): f"file {file_name} with the extension {ending}" ) +def check_otoole_inputs( + actual_data : str, + scenario : int, + expected_data : str = 'otoole_files.csv', +): + """Checks that scenario data matches what snakemake will expect + + This is a useful sanity check on the otoole version you are using. + + Parameters + ---------- + input_data : str + path the directory holding otoole csv data + expected_data : str = 'otoole_files.csv' + name of the file in /resources holding input CSV data + scenario : int + scenario number + + Raises + ------ + FileNotFoundError + If the input csvs do not match the csvs in resources/otoole_files.csv + """ + missing_files = [] + expected_files = pd.read_csv(Path('resources', expected_data))['inputs'].to_list() + for csv in Path(actual_data).iterdir(): + if csv.stem not in expected_files: + missing_files.append(f"{csv.stem}.csv") + if missing_files: + raise FileNotFoundError( + f"The following CSV files are missing in the input data for scenario " + f"{scenario} : {missing_files}" + ) + def main(config : Dict[str, Any]): """Runs a series of checks on input data. @@ -477,6 +514,7 @@ def main(config : Dict[str, Any]): # read in datapackage path and user_config file scenario_df = pd.read_csv(config['datapackage']) + scenarios = scenario_df['name'].to_list() datapackages = scenario_df['datapackage'].to_list() configs = scenario_df['config'].to_list() parameters = read_parameters_file(config['parameters']) @@ -487,7 +525,9 @@ def main(config : Dict[str, Any]): check_parameters(datapackages[0], parameters, user_config) # check otoole inputs - + for scenario, datapackage in zip(scenarios, datapackages): + data_dir = Path(Path(datapackage).parent, 'data') + check_otoole_inputs(str(data_dir), scenario) if __name__ == "__main__": From 06ad335840c1b28d86bd145cb75b1d94e65a1976 Mon Sep 17 00:00:00 2001 From: trevorb1 Date: Sat, 1 Oct 2022 21:40:30 +0200 Subject: [PATCH 4/9] heatmap --- README.md | 3 +- config/parameters.csv | 4 + config/results.csv | 4 +- workflow/Snakefile | 12 +- workflow/rules/osemosys.smk | 1 - workflow/rules/results.smk | 50 +++++-- workflow/scripts/check_inputs.py | 3 +- workflow/scripts/extract_results.py | 113 +++++++++------- ...nalyze_results.py => objective_results.py} | 39 +++--- workflow/scripts/user_def_results.py | 126 ++++++++++++++++++ workflow/scripts/utils.py | 72 +++++++--- 11 files changed, 321 insertions(+), 106 deletions(-) rename workflow/scripts/{analyze_results.py => objective_results.py} (82%) create mode 100644 workflow/scripts/user_def_results.py diff --git a/README.md b/README.md index 5be222d..9cac8e4 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,7 @@ A YAML file `config.yaml` must be placed in the config directory. datapackage: config/scenarios.csv # Tell the workflow which model results to plot -result_params: config/results.csv -agg_results: config/agg_results.csv +results: config/results.csv # Filetype options: 'csv' or 'parquet' or 'feather' filetype: csv diff --git a/config/parameters.csv b/config/parameters.csv index 32ef533..1c68209 100644 --- a/config/parameters.csv +++ b/config/parameters.csv @@ -1 +1,5 @@ name,group,indexes,min_value_base_year,max_value_base_year,min_value_end_year,max_value_end_year,dist,interpolation_index,action +CapitalCost,capital,"R1,RENEWABLE",4500,5500,2000,3000,"unif","YEAR","interpolate" +VariableCost,variable,"R1,MINE_RENEWABLE,1",1,2,0.75,1,"unif","YEAR","interpolate" +InputActivityRatio,efficiency,"R1,RENEWABLE,RENEWABLE_FUEL,1",2.5,3.0,1.5,2,"unif","YEAR","interpolate" +OperationalLife,oplife,"R1,RENEWABLE",50,100,50,100,"unif","","fixed" diff --git a/config/results.csv b/config/results.csv index f7cbe2f..03fb4ed 100644 --- a/config/results.csv +++ b/config/results.csv @@ -1,2 +1,2 @@ -name -Demand +resultfile,filename,REGION,TECHNOLOGY,FUEL,EMISSION +TotalCapacityAnnual,annual_capacity,"R1","RENEWABLE,TRANSMISSION",, diff --git a/workflow/Snakefile b/workflow/Snakefile index cf67c0d..ed11ee2 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -3,18 +3,18 @@ import sys configfile: "config/config.yaml" localrules: all, clean wildcard_constraints: - agg_result_file="[a-zA-Z_\-]+", + result_file="[a-zA-Z_\-]+", scenario="\d+", model_run="\d+" container: "docker://condaforge/mambaforge:4.10.1-0" -RESULTS = pd.read_csv(config["result_params"]).set_index('name') SCENARIOS = pd.read_csv(config["datapackage"]).set_index('name') GROUPS = pd.read_csv(config['parameters'])['group'].unique() # Calculates number of model runs for the Method of Morris MODELRUNS = range((len(GROUPS) + 1) * config['replicates']) -AGG_RESULTS = pd.read_csv(config["agg_results"]) +RESULTS = pd.read_csv(config["results"]) +RESULT_FILES = RESULTS['filename'].to_list() ZIP = '.gz' if config['zip'] else '' INPUT_FILES = pd.read_csv('resources/otoole_files.csv')['inputs'].to_list() OUTPUT_FILES = pd.read_csv('resources/otoole_files.csv')['outputs'].dropna().to_list() @@ -31,8 +31,10 @@ onsuccess: rule all: input: - expand("results/SA_{scenario}.{extension}", scenario=SCENARIOS.index, extension=['csv', 'png']), - expand("results/{scenario}/model_{model_run}/results/{x}.csv", x=OUTPUT_FILES, model_run=MODELRUNS, scenario=SCENARIOS.index) + expand("results/{scenario}_summary/SA_objective.{extension}", scenario=SCENARIOS.index, extension=['csv', 'png']), + expand("results/{scenario}/model_{model_run}/results/{x}.csv", x=OUTPUT_FILES, model_run=MODELRUNS, scenario=SCENARIOS.index), + expand("results/{scenario}_summary/{result_file}_heatmap.png", scenario=SCENARIOS.index, result_file=RESULT_FILES), + message: "Running pipeline to generate the files '{input}'" rule get_status: diff --git a/workflow/rules/osemosys.smk b/workflow/rules/osemosys.smk index 39d674d..d4676cd 100644 --- a/workflow/rules/osemosys.smk +++ b/workflow/rules/osemosys.smk @@ -196,7 +196,6 @@ rule process_solution: datapackage="results/{scenario}/model_{model_run}/datapackage.json", config="results/{scenario}/model_{model_run}/config.yaml", output: - # ["results/{{scenario}}/model_{{model_run}}/results/{}.csv".format(x) for x in RESULTS.index] expand("results/{{scenario}}/model_{{model_run}}/results/{csv}.csv", csv=OUTPUT_FILES) conda: "../envs/otoole.yaml" log: "results/log/process_solution_{scenario}_{model_run}.log" diff --git a/workflow/rules/results.smk b/workflow/rules/results.smk index 239c0c7..c9a9ba3 100644 --- a/workflow/rules/results.smk +++ b/workflow/rules/results.smk @@ -1,19 +1,26 @@ def get_input(wildcards): - input_file = AGG_RESULTS.set_index('filename').loc[wildcards.agg_result_file]['resultfile'] - return ["results/{scenario}/{modelrun}/{input_file}.csv".format( - modelrun=x, input_file=input_file, scenario=y) for x in MODELRUNS for y in SCENARIOS.index] + # input_file = RESULTS.set_index('filename').loc[wildcards.result_file]['resultfile'] + # return ["results/{scenario}/model_{modelrun}/results/{input_file}.csv".format( + # modelrun=x, input_file=input_file, scenario=y) for x in MODELRUNS for y in SCENARIOS.index] + input_file = RESULTS.set_index('filename').loc[wildcards.result_file]['resultfile'] + return ["results/{wildcards.scenario}/model_{modelrun}/results/{input_file}.csv".format( + modelrun=x, input_file=input_file, wildcards=wildcards) for x in MODELRUNS] def get_indices(wildcards): - return AGG_RESULTS.set_index('filename').loc[wildcards.agg_result_file]['indices'] + indices = RESULTS.set_index('filename').loc[wildcards.result_file].dropna().drop('resultfile').to_dict() + return {x:indices[x].split(',') for x in indices} rule extract_results: - input: get_input + input: + csvs=get_input, + config=config_from_scenario params: - parameter = get_indices - log: "results/log/extract_{agg_result_file}.log" - output: expand("results/{{agg_result_file}}.{ext}", ext=config['filetype']) - conda: "envs/otoole.yaml" - script: "scripts/extract_results.py" + parameter = get_indices, + folder=directory("results/{{scenario}}_summary/") + log: "results/log/extract_scenarion_{scenario}_{result_file}.log" + output: expand("results/{{scenario}}_summary/{{result_file}}.{ext}", ext=config['filetype']) + conda: "../envs/otoole.yaml" + script: "../scripts/extract_results.py" rule calculate_hourly_demand: input: expand("results/annual_demand.{ext}", ext=config['filetype']) @@ -27,14 +34,29 @@ rule calculate_hourly_generation: conda: "envs/pandas.yaml" script: "scripts/calculate_hourly_generation.py" -rule calculate_SA_results: +rule calculate_SA_objective: + message: + "Calcualting objective cost sensitivity measures" params: parameters = config['parameters'] input: sample = "modelruns/{scenario}/morris_sample.txt", results = "results/{scenario}/objective_{scenario}.csv" output: - SA_csv = "results/SA_{scenario}.csv", - SA_png = "results/SA_{scenario}.png" + expand("results/{{scenario}}_summary/SA_objective.{ext}",ext=['csv','png']) conda: "../envs/sample.yaml" - shell: "python workflow/scripts/analyze_results.py {params.parameters} {input.sample} {input.results} {output.SA_csv}" \ No newline at end of file + shell: "python workflow/scripts/objective_results.py {params.parameters} {input.sample} {input.results} {output[0]}" + +rule calculate_SA_user_defined: + message: + "Calculating user defined sensitivity measures" + params: + parameters=config['parameters'] + input: + sample="modelruns/{scenario}/morris_sample.txt", + results=expand("results/{{scenario}}_summary/{{result_file}}.{ext}", ext=config['filetype']) + output: + "results/{scenario}_summary/{result_file}_heatmap.png" + shell: "python workflow/scripts/user_def_results.py {params.parameters} {input.sample} {input.results} {output}" + + diff --git a/workflow/scripts/check_inputs.py b/workflow/scripts/check_inputs.py index 575d3f1..ddc36d9 100644 --- a/workflow/scripts/check_inputs.py +++ b/workflow/scripts/check_inputs.py @@ -424,11 +424,10 @@ def read_parameters_file(path : str) -> List[Dict[str, Union[str, int, float]]]: path : str file path to parameters.csv - Returns: + Returns -------- parameters : List[Dict[str, Union[str, int, float]]] Flattened parameters file - """ with open(path, 'r') as csv_file: parameters = list(csv.DictReader(csv_file)) diff --git a/workflow/scripts/extract_results.py b/workflow/scripts/extract_results.py index 13b2fad..d5030ed 100644 --- a/workflow/scripts/extract_results.py +++ b/workflow/scripts/extract_results.py @@ -7,69 +7,92 @@ ``snakemake.input`` and ``snakemake.output[0]`` attributes on the snakemake object passed into this module at run time. """ + import pandas as pd -import pyarrow from typing import List, Tuple, Dict -from otoole.input import Strategy -from utils import get_model_run_scenario_from_filepath, get_model_run_scenario_from_input_filepath -import os +from pathlib import Path +from utils import get_model_run_scenario_from_filepath, parse_yaml import sys from utils import write_results, read_results +import itertools from logging import getLogger logger = getLogger(__name__) -def add_dtypes(config: Dict): - for name, details in config.items(): - if details["type"] == "param": - dtypes = {} - for column in details["indices"] + ["VALUE"]: - if column == "VALUE": - dtypes["VALUE"] = details["dtype"] - else: - dtypes[column] = config[column]["dtype"] - details["index_dtypes"] = dtypes - return config - - -def main(input_files: List, output_file: str, parameter: Tuple, config: Dict): - """Iterate over list of CSV files, extract defined results, write to output file +def get_indices(parameters : pd.DataFrame, filename : str) -> Dict : + indices = parameters.set_index('filename').loc[filename].dropna().drop('resultfile') + indices = indices.to_dict() + return {x:indices[x].split(',') for x in indices} + +def main(input_files: List, output_file: str, indices: Tuple, config: Dict): + """Iterate over list of CSV files, extract defined results, write to output file. + + Parameters + ---------- + input_files : List + List of CSVs to iterate over + output_file : str + Name of output file + indices : Dict + Indices to extract value over + ex. {'REGION':['SIMPLICITY], 'TECHNOLOGY':['GAS_EXTRACTION','HYD1']} + config : Dict + Datapackage holding type information """ aggregated_results = [] for filename in input_files: bits = get_model_run_scenario_from_filepath(filename) - - column_dtypes = config[bits['param']]['index_dtypes'] df_index = config[bits['param']]['indices'] - + column_dtypes = { + x:config[x]['dtype'] for x in df_index + } df = read_results(filename) df = df.astype(column_dtypes).set_index(df_index) - - try: - results = df.xs(parameter, drop_level=False) - results['SCENARIO'] = bits['scenario'] - results['MODELRUN'] = bits['model_run'] - results = results.reset_index( - ).set_index(['SCENARIO', 'MODELRUN'] + df_index) - - aggregated_results.append(results) - except KeyError: - logger.warning("No results found for %s in %s", parameter, bits['filepath']) + ################################################################ + # this method of slicing and then appending is super enefficient... + # will revist to speed this up + result_dfs = [] + parameters = tuple(itertools.product(*indices.values())) + indices_expanded = tuple([tuple(indices.keys())] * len(parameters)) + for index, param in zip(indices_expanded, parameters): + results = df.xs(param, level=index, drop_level=False) + result_dfs.append(results) + results = pd.concat(result_dfs) + results = results.reset_index(level='YEAR') + ################################################################ + # results['SCENARIO'] = bits['scenario'] + results['MODELRUN'] = bits['model_run'] + # results = results.reset_index( + # ).set_index(['SCENARIO', 'MODELRUN'] + df_index) + results = results.reset_index( + ).set_index(['MODELRUN'] + df_index) + aggregated_results.append(results) results = pd.concat(aggregated_results) - write_results(results, output_file, True) - -strategy = Strategy() -config = strategy.results_config.copy() -config.update(strategy.input_config) -logger.debug(config) -config = add_dtypes(config) - -input_files : List = snakemake.input -output_file = snakemake.output[0] -parameter = tuple(snakemake.params['parameter'].split(",")) -main(input_files, output_file, parameter, config) \ No newline at end of file +if __name__ == '__main__': + + if "snakemake" in globals(): + input_files = snakemake.input['csvs'] + yaml_config = snakemake.input['config'] + output_file_path = snakemake.output[0] + indices = snakemake.params['parameter'] + else: + if len(sys.argv) != 5: + raise ValueError( + "Usage: python extract_results.py " + ) + input_files = sys.argv[1] + if not isinstance(input_files, list): + input_files = [input_files] + yaml_config = sys.argv[2] + output_file_path = sys.argv[3] + parameters = pd.read_csv(sys.argv[4]) + output_file = Path(sys.argv[3]).stem + indices = get_indices(parameters, output_file) + + user_config = parse_yaml(yaml_config) + main(input_files, output_file_path, indices, user_config) \ No newline at end of file diff --git a/workflow/scripts/analyze_results.py b/workflow/scripts/objective_results.py similarity index 82% rename from workflow/scripts/analyze_results.py rename to workflow/scripts/objective_results.py index d7b7c27..ee717e6 100644 --- a/workflow/scripts/analyze_results.py +++ b/workflow/scripts/objective_results.py @@ -1,16 +1,13 @@ -"""Analyzes results from model +"""Analyzes objective value results from model Arguments --------- path_to_parameters : str File containing the parameters for generated sample - model_inputs : str File path to sample model inputs - model_outputs : str File path to model outputs - location_to_save : str File path to save results @@ -29,22 +26,22 @@ The ``inputs.txt`` should be the output from SALib.sample.morris.sample -The ``model/results.csv`` must have an 'OBJECTIVE' column holding results +The ``model/results.csv`` must have an 'OBJECTIVE' column holding results OR +be a formatted output of an OSeMOSYS parameter """ from math import ceil from SALib.analyze import morris as analyze_morris from SALib.plotting import morris as plot_morris -import os import numpy as np import pandas as pd import csv -from typing import List import sys import utils import matplotlib.pyplot as plt import matplotlib.lines as mlines +from pathlib import Path from logging import getLogger @@ -67,7 +64,20 @@ def plot_histogram(problem: dict, X: np.array, fig: plt.figure): fig.legend(handles=legend_handles, ncol=ncols, frameon=False, fontsize='small') fig.suptitle(' ', fontsize=(ncols * 20)) -def main(parameters: dict, X: np.array, Y: np.array, save_file: str): +def objective_results(parameters: dict, X: np.array, Y: np.array, save_file: str): + """Performs SA and plots results. + + Parameters + ---------- + parameters : Dict + Parameters for generated sample + X : np.array + Input Sample + Y : np.array + Results + save_file : str + File path to save results + """ problem = utils.create_salib_problem(parameters) @@ -78,16 +88,9 @@ def main(parameters: dict, X: np.array, Y: np.array, save_file: str): # save graphical resutls - ''' This is a temp fix for issue 19 ''' fig, axs = plt.subplots(2, figsize=(10,8)) plot_morris.horizontal_bar_plot(axs[0], Si, unit="(\$)") plot_morris.covariance_plot(axs[1], Si, unit="(\$)") - # fig = plt.figure(figsize=(16, 8), constrained_layout=True) - # subfigs = fig.subfigures(1, 2) - # plot_histogram(problem, X, subfigs[0]) - # axs_right = subfigs[1].subplots(2) - # plot_morris.horizontal_bar_plot(axs_right, Si, unit="(\$)") - # plot_morris.covariance_plot(axs_right, Si, unit="(\$)") fig.savefig(f'{save_file}.png') @@ -96,12 +99,12 @@ def main(parameters: dict, X: np.array, Y: np.array, save_file: str): parameters_file = sys.argv[1] sample = sys.argv[2] model_results = sys.argv[3] - save_file = sys.argv[4][:-4] #remove '.csv' extension + save_file = str(Path(sys.argv[4]).with_suffix('')) with open(parameters_file, 'r') as csv_file: parameters = list(csv.DictReader(csv_file)) - + X = np.loadtxt(sample, delimiter=',') Y = pd.read_csv(model_results)['OBJECTIVE'].to_numpy() + objective_results(parameters, X, Y, save_file) - main(parameters, X, Y, save_file) diff --git a/workflow/scripts/user_def_results.py b/workflow/scripts/user_def_results.py new file mode 100644 index 0000000..ac4a142 --- /dev/null +++ b/workflow/scripts/user_def_results.py @@ -0,0 +1,126 @@ +"""Creates a time series heat map from a model run. + +Arguments +--------- +path_to_parameters : str + File containing the parameters for generated sample +model_inputs : str + File path to sample model inputs +model_outputs : str + File path to model outputs +location_to_save : str + File path to save results + +Usage +----- +To run the script on the command line, type:: + + python analyze_results.py path/to/parameters.csv path/to/inputs.txt + path/to/model/results.csv path/to/save/SA/results.csv + +The ``parameters.csv`` CSV file should be formatted as follows:: + + name,group,indexes,min_value,max_value,dist,interpolation_index,action + CapitalCost,pvcapex,"GLOBAL,GCPSOUT0N",500,1900,unif,YEAR,interpolate + DiscountRate,discountrate,"GLOBAL,GCIELEX0N",0.05,0.20,unif,None,fixed + +The ``inputs.txt`` should be the output from SALib.sample.morris.sample + +The ``model/results.csv`` must have an 'OBJECTIVE' column holding results OR +be a formatted output of an OSeMOSYS parameter + +""" + +from pathlib import Path +from SALib.analyze import morris as analyze_morris +from SALib.plotting import morris as plot_morris +import numpy as np +import pandas as pd +import csv +import sys +import utils +import seaborn as sns +import matplotlib.pyplot as plt + + +from logging import getLogger + +logger = getLogger(__name__) + +def sort_results(df : pd.DataFrame, year : int) -> np.array: + """Organizes a model variable results file for a morris analysis + + Parameters + ---------- + df : pd.DataFrame + Dataframe grouped on model number and year + year : int + Year to sort results for + + Returns + ------- + Y : np.array + Results for morris analysis + """ + df = df.xs(year, level=('YEAR')).reset_index() + df['NUM'] = df['MODELRUN'].map(lambda x: int(x.split('_')[1])) + df = df.sort_values(by='NUM').reset_index(drop=True).drop(('NUM'), axis=1).set_index('MODELRUN') + Y = df.to_numpy() + return Y + +def main(parameters: dict, X: np.array, model_results: pd.DataFrame, save_file: str): + """Performs SA and plots results. + + Parameters + ---------- + parameters : Dict + Parameters for generated sample + X : np.array + Input Sample + model_results : pd.DataFrame + Model results for a OSeMOSYS variable + save_file : str + File path to save results + """ + + problem = utils.create_salib_problem(parameters) + model_results = model_results.groupby(['MODELRUN','YEAR']).sum() + + years = model_results.index.unique(level='YEAR') + SA_result_data = [] + + for year in model_results.index.unique(level='YEAR'): + Y = sort_results(model_results, year) + Si = analyze_morris.analyze(problem, X, Y, print_to_console=False) + # get_data() because returns masked array + # SA_result_data.append(Si['mu_star']) + SA_result_data.append(Si['mu_star']) + + SA_result_data = np.ma.concatenate([SA_result_data]) + # SA_result_data = np.concatenate(SA_result_data, axis=1) + # SA_results = pd.DataFrame(np.ma.getdata(SA_result_data)) + columns = [x['name'] for x in parameters] + SA_results = pd.DataFrame(np.ma.getdata(SA_result_data), columns=columns, index=years).T + + # Save figure results + title = Path(save_file).stem.capitalize() + height = len(columns) + 1.5 + width = len(years) / 5 + fig, ax = plt.subplots(figsize=(width, height)) + sns.heatmap(SA_results, cmap="coolwarm", ax=ax).set_title(title) + fig.savefig(f'{save_file}.png', bbox_inches='tight') + +if __name__ == "__main__": + + parameters_file = sys.argv[1] + sample = sys.argv[2] + result_file = sys.argv[3] + save_file = str(Path(sys.argv[4]).with_suffix('')) + with open(parameters_file, 'r') as csv_file: + parameters = list(csv.DictReader(csv_file)) + + X = np.loadtxt(sample, delimiter=',') + results = pd.read_csv(result_file) + + main(parameters, X, results, save_file) + diff --git a/workflow/scripts/utils.py b/workflow/scripts/utils.py index 611413a..21647c9 100644 --- a/workflow/scripts/utils.py +++ b/workflow/scripts/utils.py @@ -1,6 +1,8 @@ import os import pandas as pd -from typing import List +from typing import List, Dict +import yaml +from pathlib import Path from logging import getLogger @@ -19,17 +21,26 @@ def get_model_run_scenario_from_input_filepath(filename: str): return {'model_run': model_run, 'scenario': scenario, 'param': param, 'filepath': filepath} -def get_model_run_scenario_from_filepath(filename: str): - """Parses filepath to extract useful bits +def get_model_run_scenario_from_filepath(filepath: str) -> Dict: + """Parses filepath to extract useful bits. + + Input filepath is expected in the form of: + "results/{scenario}/{modelrun}/results/{input_file}.csv" + + Parameters + ---------- + file : str + file path from root directory - "results/{{scenario}}/{modelrun}/{input_file}.csv" + Returns + ------- + Dict + With model_run, scenario, OSeMOSYS parameter, and filepath directory """ - filepath, name = os.path.split(filename) - param = os.path.splitext(name)[0] - scenario_path, model_run = os.path.split(filepath) - scenario = os.path.split(scenario_path)[1] - return {'model_run': model_run, 'scenario': scenario, - 'param': param, 'filepath': filepath} + f = Path(filepath) + parts = f.parts + return {'model_run': parts[2], 'scenario': parts[1], + 'param': f.stem, 'filepath': str(f.parent)} def read_results(input_filepath: str) -> pd.DataFrame: extension = os.path.splitext(input_filepath)[1] @@ -43,7 +54,7 @@ def read_results(input_filepath: str) -> pd.DataFrame: def write_results(df: pd.DataFrame, output_filepath: str, index=None) -> None: - """Write out aggregated results to disk + """Write out aggregated results to disk by scenario Arguments --------- @@ -65,7 +76,7 @@ def write_results(df: pd.DataFrame, output_filepath: str, index=None) -> None: df.to_feather(output_filepath) def create_salib_problem(parameters: List) -> dict: - """Creates SALib problem from scenario configuration file. + """Creates SALib problem from scenario configuration. Arguments --------- @@ -88,8 +99,9 @@ def create_salib_problem(parameters: List) -> dict: problem = {} problem['num_vars'] = len(parameters) if problem['num_vars'] <= 1: - logger.error(f"Must define at least two variables in problem. User defined {problem['num_vars']} variable(s).") - raise ValueError + raise ValueError( + f"Must define at least two variables in problem. User defined " + f"{problem['num_vars']} variable(s).") names = [] bounds = [] @@ -106,7 +118,33 @@ def create_salib_problem(parameters: List) -> dict: problem['groups'] = groups num_groups = len(set(groups)) if num_groups <= 1: - logger.error(f"Must define at least two groups in problem. User defined {num_groups} group(s).") - raise ValueError + raise ValueError( + f"Must define at least two groups in problem. User defined " + f"{num_groups} group(s).") - return problem \ No newline at end of file + return problem + +def parse_yaml(path : str) -> Dict: + """Parses a YAML file to a dictionary + + Parameters + ---------- + path : str + input path the yaml file + + Returns + ------- + parsed_yaml : Dict + parsed YAML file + + Raises + ------ + YAMLError + If the yaml file can't be loaded + """ + with open(path, 'r') as stream: + try: + parsed_yaml = yaml.safe_load(stream) + except yaml.YAMLError as exc: + print(exc) + return parsed_yaml \ No newline at end of file From 2a9a0cacdab2f557ef10351df33ca5ecef52f52e Mon Sep 17 00:00:00 2001 From: trevorb1 Date: Sun, 2 Oct 2022 19:32:26 +0200 Subject: [PATCH 5/9] added variable SA results --- workflow/Snakefile | 5 +- workflow/rules/results.smk | 25 ++++-- ...ive_results.py => calculate_SA_results.py} | 50 +++++++++--- ...{user_def_results.py => create_heatmap.py} | 0 workflow/scripts/test.ipynb | 77 +++++++++++++++++++ 5 files changed, 141 insertions(+), 16 deletions(-) rename workflow/scripts/{objective_results.py => calculate_SA_results.py} (68%) rename workflow/scripts/{user_def_results.py => create_heatmap.py} (100%) create mode 100644 workflow/scripts/test.ipynb diff --git a/workflow/Snakefile b/workflow/Snakefile index ed11ee2..f8d39e5 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -3,7 +3,7 @@ import sys configfile: "config/config.yaml" localrules: all, clean wildcard_constraints: - result_file="[a-zA-Z_\-]+", + result_file="[^(objective)][a-zA-Z_\-]+", scenario="\d+", model_run="\d+" @@ -32,10 +32,11 @@ onsuccess: rule all: input: expand("results/{scenario}_summary/SA_objective.{extension}", scenario=SCENARIOS.index, extension=['csv', 'png']), + expand("results/{scenario}_summary/SA_{result_file}.{extension}", scenario=SCENARIOS.index, extension=['csv', 'png'], result_file=RESULT_FILES), expand("results/{scenario}/model_{model_run}/results/{x}.csv", x=OUTPUT_FILES, model_run=MODELRUNS, scenario=SCENARIOS.index), expand("results/{scenario}_summary/{result_file}_heatmap.png", scenario=SCENARIOS.index, result_file=RESULT_FILES), - message: "Running pipeline to generate the files '{input}'" + message: "Running pipeline to generate the sensitivity analysis results" rule get_status: params: diff --git a/workflow/rules/results.smk b/workflow/rules/results.smk index c9a9ba3..2a9a358 100644 --- a/workflow/rules/results.smk +++ b/workflow/rules/results.smk @@ -18,7 +18,7 @@ rule extract_results: parameter = get_indices, folder=directory("results/{{scenario}}_summary/") log: "results/log/extract_scenarion_{scenario}_{result_file}.log" - output: expand("results/{{scenario}}_summary/{{result_file}}.{ext}", ext=config['filetype']) + output: expand("results/{{scenario}}/{{result_file}}.{ext}", ext=config['filetype']) conda: "../envs/otoole.yaml" script: "../scripts/extract_results.py" @@ -38,25 +38,40 @@ rule calculate_SA_objective: message: "Calcualting objective cost sensitivity measures" params: - parameters = config['parameters'] + parameters=config['parameters'], + result_type='objective' input: sample = "modelruns/{scenario}/morris_sample.txt", results = "results/{scenario}/objective_{scenario}.csv" output: expand("results/{{scenario}}_summary/SA_objective.{ext}",ext=['csv','png']) conda: "../envs/sample.yaml" - shell: "python workflow/scripts/objective_results.py {params.parameters} {input.sample} {input.results} {output[0]}" + shell: "python workflow/scripts/calculate_SA_results.py {params.parameters} {input.sample} {input.results} {output[0]} {params.result_type}" rule calculate_SA_user_defined: + message: + "Calcualting user defined sensitivity measures" + params: + parameters=config['parameters'], + result_type='variable' + input: + sample = "modelruns/{scenario}/morris_sample.txt", + results=expand("results/{{scenario}}/{{result_file}}.{ext}", ext=config['filetype']) + output: + expand("results/{{scenario}}_summary/SA_{{result_file}}.{ext}",ext=['csv','png']) + conda: "../envs/sample.yaml" + shell: "python workflow/scripts/calculate_SA_results.py {params.parameters} {input.sample} {input.results} {output[0]} {params.result_type}" + +rule create_heatmap: message: "Calculating user defined sensitivity measures" params: parameters=config['parameters'] input: sample="modelruns/{scenario}/morris_sample.txt", - results=expand("results/{{scenario}}_summary/{{result_file}}.{ext}", ext=config['filetype']) + results=expand("results/{{scenario}}/{{result_file}}.{ext}", ext=config['filetype']) output: "results/{scenario}_summary/{result_file}_heatmap.png" - shell: "python workflow/scripts/user_def_results.py {params.parameters} {input.sample} {input.results} {output}" + shell: "python workflow/scripts/create_heatmap.py {params.parameters} {input.sample} {input.results} {output}" diff --git a/workflow/scripts/objective_results.py b/workflow/scripts/calculate_SA_results.py similarity index 68% rename from workflow/scripts/objective_results.py rename to workflow/scripts/calculate_SA_results.py index ee717e6..afdadc8 100644 --- a/workflow/scripts/objective_results.py +++ b/workflow/scripts/calculate_SA_results.py @@ -10,6 +10,9 @@ File path to model outputs location_to_save : str File path to save results +result_type : str + True for Objective result type + False for user defined result type Usage ----- @@ -18,16 +21,13 @@ python analyze_results.py path/to/parameters.csv path/to/inputs.txt path/to/model/results.csv path/to/save/SA/results.csv -The ``parameters.csv`` CSV file should be formatted as follows:: +The `parameters.csv` CSV file should be formatted as follows:: name,group,indexes,min_value,max_value,dist,interpolation_index,action CapitalCost,pvcapex,"GLOBAL,GCPSOUT0N",500,1900,unif,YEAR,interpolate DiscountRate,discountrate,"GLOBAL,GCIELEX0N",0.05,0.20,unif,None,fixed -The ``inputs.txt`` should be the output from SALib.sample.morris.sample - -The ``model/results.csv`` must have an 'OBJECTIVE' column holding results OR -be a formatted output of an OSeMOSYS parameter +The `inputs.txt` should be the output from SALib.sample.morris.sample """ @@ -47,6 +47,25 @@ logger = getLogger(__name__) +def parse_user_defined_results(df : pd.DataFrame) -> np.array: + """Extracts aggregated results from user defined results. + + Parameters + ---------- + df : pd.DataFrame + Aggregated result file parsed by model run + + Returns + ------- + Y : np.array + Array of reults per model run in model run order + """ + df = df.groupby(by=['MODELRUN']).sum().reset_index() + df['NUM'] = df['MODELRUN'].map(lambda x: int(x.split('_')[1])) + df = df.sort_values(by='NUM').reset_index(drop=True).drop(('NUM'), axis=1) + Y = df['VALUE'].to_numpy() + return Y + def plot_histogram(problem: dict, X: np.array, fig: plt.figure): # chnage histogram labels to legend for clarity @@ -64,7 +83,7 @@ def plot_histogram(problem: dict, X: np.array, fig: plt.figure): fig.legend(handles=legend_handles, ncol=ncols, frameon=False, fontsize='small') fig.suptitle(' ', fontsize=(ncols * 20)) -def objective_results(parameters: dict, X: np.array, Y: np.array, save_file: str): +def sa_results(parameters: dict, X: np.array, Y: np.array, save_file: str): """Performs SA and plots results. Parameters @@ -81,7 +100,7 @@ def objective_results(parameters: dict, X: np.array, Y: np.array, save_file: str problem = utils.create_salib_problem(parameters) - Si = analyze_morris.analyze(problem, X, Y, print_to_console=True) + Si = analyze_morris.analyze(problem, X, Y, print_to_console=False) # Save text based results Si.to_df().to_csv(f'{save_file}.csv') @@ -89,6 +108,7 @@ def objective_results(parameters: dict, X: np.array, Y: np.array, save_file: str # save graphical resutls fig, axs = plt.subplots(2, figsize=(10,8)) + # fig.set_title(title) plot_morris.horizontal_bar_plot(axs[0], Si, unit="(\$)") plot_morris.covariance_plot(axs[1], Si, unit="(\$)") @@ -100,11 +120,23 @@ def objective_results(parameters: dict, X: np.array, Y: np.array, save_file: str sample = sys.argv[2] model_results = sys.argv[3] save_file = str(Path(sys.argv[4]).with_suffix('')) + result_type = sys.argv[5] with open(parameters_file, 'r') as csv_file: parameters = list(csv.DictReader(csv_file)) X = np.loadtxt(sample, delimiter=',') - Y = pd.read_csv(model_results)['OBJECTIVE'].to_numpy() - objective_results(parameters, X, Y, save_file) + + if result_type == 'objective': + Y = pd.read_csv(model_results)['OBJECTIVE'].to_numpy() + elif result_type == 'variable': + results = pd.read_csv(model_results) + Y = parse_user_defined_results(results) + else: + raise ValueError( + f"Result type must be 'objective' or 'variable'. Supplied value is " + f"{result_type}" + ) + + sa_results(parameters, X, Y, save_file) diff --git a/workflow/scripts/user_def_results.py b/workflow/scripts/create_heatmap.py similarity index 100% rename from workflow/scripts/user_def_results.py rename to workflow/scripts/create_heatmap.py diff --git a/workflow/scripts/test.ipynb b/workflow/scripts/test.ipynb new file mode 100644 index 0000000..90c2d21 --- /dev/null +++ b/workflow/scripts/test.ipynb @@ -0,0 +1,77 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", + " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", + " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", + " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", + " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", + " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", + " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", + " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", + " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", + " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", + " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", + " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", + " 104228.62982, 104228.62982])" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = pd.read_csv('../../results/0_summary/annual_capacity.csv')\n", + "df = df.groupby(by=['MODELRUN']).sum().reset_index()\n", + "df['NUM'] = df['MODELRUN'].map(lambda x: int(x.split('_')[1]))\n", + "df = df.sort_values(by='NUM').reset_index(drop=True).drop(('NUM'), axis=1)\n", + "df['VALUE'].to_numpy()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.10.4 ('gsa')", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "b93cd6f2f4c0fa0206bbda95dee9346b0b1d12c2875b491cd680bb32bc7d3c94" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From f19aae5356fe7a073731aa64cef84af0f1d6db35 Mon Sep 17 00:00:00 2001 From: trevorb1 Date: Mon, 3 Oct 2022 08:28:08 +0200 Subject: [PATCH 6/9] added plot titles --- config/agg_results.csv | 5 -- config/results.csv | 2 +- resources/otoole_files.csv | 34 +++++------ workflow/scripts/calculate_SA_results.py | 6 +- workflow/scripts/test.ipynb | 77 ------------------------ 5 files changed, 21 insertions(+), 103 deletions(-) delete mode 100644 config/agg_results.csv delete mode 100644 workflow/scripts/test.ipynb diff --git a/config/agg_results.csv b/config/agg_results.csv deleted file mode 100644 index 56dc50a..0000000 --- a/config/agg_results.csv +++ /dev/null @@ -1,5 +0,0 @@ -resultfile,indices,filename -TotalCapacityAnnual,"REGION",annual_capacity -AnnualEmissions,"REGION",emissions -Demand,"REGION",annual_demand -ProductionByTechnology,"REGION",annual_generation diff --git a/config/results.csv b/config/results.csv index 03fb4ed..515f3dc 100644 --- a/config/results.csv +++ b/config/results.csv @@ -1,2 +1,2 @@ resultfile,filename,REGION,TECHNOLOGY,FUEL,EMISSION -TotalCapacityAnnual,annual_capacity,"R1","RENEWABLE,TRANSMISSION",, +TotalDiscountedCost,DiscountedCost,"R1",,, diff --git a/resources/otoole_files.csv b/resources/otoole_files.csv index 1592269..f9116fe 100644 --- a/resources/otoole_files.csv +++ b/resources/otoole_files.csv @@ -7,23 +7,23 @@ CapacityFactor,AnnualTechnologyEmissionByMode CapacityOfOneTechnologyUnit,AnnualVariableOperatingCost CapacityToActivityUnit,CapitalInvestment CapitalCost,Demand -CapitalCostStorage,DiscountedTechnologyEmissionsPenalty -Conversionld,NewCapacity -Conversionlh,ProductionByTechnology -Conversionls,ProductionByTechnologyAnnual -DAILYTIMEBRACKET,RateOfActivity -DaysInDayType,RateOfProductionByTechnology -DaySplit,RateOfProductionByTechnologyByMode -DAYTYPE,RateOfUseByTechnology -default_values,RateOfUseByTechnologyByMode -DepreciationMethod,TotalAnnualTechnologyActivityByMode -DiscountRate,TotalCapacityAnnual -DiscountRateIdv,TotalTechnologyAnnualActivity -DiscountRateStorage,TotalTechnologyModelPeriodActivity -EMISSION,UseByTechnology -EmissionActivityRatio, -EmissionsPenalty, -FixedCost, +CapitalCostStorage,DiscountedSalvageValue +Conversionld,DiscountedTechnologyEmissionsPenalty +Conversionlh,NewCapacity +Conversionls,ProductionByTechnology +DAILYTIMEBRACKET,ProductionByTechnologyAnnual +DaysInDayType,RateOfActivity +DaySplit,RateOfProductionByTechnology +DAYTYPE,RateOfProductionByTechnologyByMode +default_values,RateOfUseByTechnology +DepreciationMethod,RateOfUseByTechnologyByMode +DiscountRate,SalvageValue +DiscountRateIdv,TotalAnnualTechnologyActivityByMode +DiscountRateStorage,TotalCapacityAnnual +EMISSION,TotalDiscountedCost +EmissionActivityRatio,TotalTechnologyAnnualActivity +EmissionsPenalty,TotalTechnologyModelPeriodActivity +FixedCost,UseByTechnology FUEL, InputActivityRatio, MinStorageCharge, diff --git a/workflow/scripts/calculate_SA_results.py b/workflow/scripts/calculate_SA_results.py index afdadc8..ac7f83d 100644 --- a/workflow/scripts/calculate_SA_results.py +++ b/workflow/scripts/calculate_SA_results.py @@ -106,13 +106,13 @@ def sa_results(parameters: dict, X: np.array, Y: np.array, save_file: str): Si.to_df().to_csv(f'{save_file}.csv') # save graphical resutls - + title = Path(save_file).stem.capitalize() fig, axs = plt.subplots(2, figsize=(10,8)) - # fig.set_title(title) + fig.suptitle(title, fontsize=20) plot_morris.horizontal_bar_plot(axs[0], Si, unit="(\$)") plot_morris.covariance_plot(axs[1], Si, unit="(\$)") - fig.savefig(f'{save_file}.png') + fig.savefig(f'{save_file}.png', bbox_inches='tight') if __name__ == "__main__": diff --git a/workflow/scripts/test.ipynb b/workflow/scripts/test.ipynb deleted file mode 100644 index 90c2d21..0000000 --- a/workflow/scripts/test.ipynb +++ /dev/null @@ -1,77 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "import numpy as np" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", - " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", - " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", - " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", - " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", - " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", - " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", - " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", - " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", - " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", - " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", - " 104228.62982, 104228.62982, 104228.62982, 104228.62982,\n", - " 104228.62982, 104228.62982])" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df = pd.read_csv('../../results/0_summary/annual_capacity.csv')\n", - "df = df.groupby(by=['MODELRUN']).sum().reset_index()\n", - "df['NUM'] = df['MODELRUN'].map(lambda x: int(x.split('_')[1]))\n", - "df = df.sort_values(by='NUM').reset_index(drop=True).drop(('NUM'), axis=1)\n", - "df['VALUE'].to_numpy()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3.10.4 ('gsa')", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.4" - }, - "orig_nbformat": 4, - "vscode": { - "interpreter": { - "hash": "b93cd6f2f4c0fa0206bbda95dee9346b0b1d12c2875b491cd680bb32bc7d3c94" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From 67463a5ca65882eb4bce11e5c49a0c3cd2e57460 Mon Sep 17 00:00:00 2001 From: trevorb1 Date: Mon, 3 Oct 2022 15:43:32 +0200 Subject: [PATCH 7/9] heatmap updates --- workflow/rules/results.smk | 3 ++- workflow/scripts/extract_results.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/workflow/rules/results.smk b/workflow/rules/results.smk index 2a9a358..1679fea 100644 --- a/workflow/rules/results.smk +++ b/workflow/rules/results.smk @@ -13,7 +13,8 @@ def get_indices(wildcards): rule extract_results: input: csvs=get_input, - config=config_from_scenario + config=config_from_scenario, + # csvs=expand("results/{{scenario}}/model_{{model_run}}/results/{csv}.csv", csv=OUTPUT_FILES) params: parameter = get_indices, folder=directory("results/{{scenario}}_summary/") diff --git a/workflow/scripts/extract_results.py b/workflow/scripts/extract_results.py index d5030ed..cc94393 100644 --- a/workflow/scripts/extract_results.py +++ b/workflow/scripts/extract_results.py @@ -57,8 +57,11 @@ def main(input_files: List, output_file: str, indices: Tuple, config: Dict): parameters = tuple(itertools.product(*indices.values())) indices_expanded = tuple([tuple(indices.keys())] * len(parameters)) for index, param in zip(indices_expanded, parameters): - results = df.xs(param, level=index, drop_level=False) - result_dfs.append(results) + try: + results = df.xs(param, level=index, drop_level=False) + result_dfs.append(results) + except KeyError as ex: + raise ex results = pd.concat(result_dfs) results = results.reset_index(level='YEAR') ################################################################ From 442cd39bcd71728fe4d7330df50e6659bd919fbb Mon Sep 17 00:00:00 2001 From: trevorb1 Date: Wed, 5 Oct 2022 16:24:12 +0200 Subject: [PATCH 8/9] updated heatmap options --- workflow/Snakefile | 8 +++++++- workflow/scripts/check_inputs.py | 1 + workflow/scripts/create_heatmap.py | 9 +++------ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index f8d39e5..49666d4 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -22,9 +22,15 @@ OUTPUT_FILES = pd.read_csv('resources/otoole_files.csv')['outputs'].dropna().to_ include: "rules/osemosys.smk" include: "rules/results.smk" +args = sys.argv +try: + config_path = args[args.index("--configfile") + 1] +except ValueError: + config_path = 'config/config.yaml' + onstart: print('Checking user inputs...') - shell("python workflow/scripts/check_inputs.py config/config.yaml") + shell("python workflow/scripts/check_inputs.py {}".format(config_path)) onsuccess: print('Workflow finished successfully!') diff --git a/workflow/scripts/check_inputs.py b/workflow/scripts/check_inputs.py index ddc36d9..631687a 100644 --- a/workflow/scripts/check_inputs.py +++ b/workflow/scripts/check_inputs.py @@ -540,4 +540,5 @@ def main(config : Dict[str, Any]): parsed_yaml = yaml.safe_load(stream) except yaml.YAMLError as exc: print(exc) + raise exc main(parsed_yaml) \ No newline at end of file diff --git a/workflow/scripts/create_heatmap.py b/workflow/scripts/create_heatmap.py index ac4a142..2784b3a 100644 --- a/workflow/scripts/create_heatmap.py +++ b/workflow/scripts/create_heatmap.py @@ -92,22 +92,19 @@ def main(parameters: dict, X: np.array, model_results: pd.DataFrame, save_file: for year in model_results.index.unique(level='YEAR'): Y = sort_results(model_results, year) Si = analyze_morris.analyze(problem, X, Y, print_to_console=False) - # get_data() because returns masked array - # SA_result_data.append(Si['mu_star']) SA_result_data.append(Si['mu_star']) SA_result_data = np.ma.concatenate([SA_result_data]) - # SA_result_data = np.concatenate(SA_result_data, axis=1) - # SA_results = pd.DataFrame(np.ma.getdata(SA_result_data)) - columns = [x['name'] for x in parameters] + columns = set([x['group'] for x in parameters]) SA_results = pd.DataFrame(np.ma.getdata(SA_result_data), columns=columns, index=years).T # Save figure results title = Path(save_file).stem.capitalize() - height = len(columns) + 1.5 + height = len(columns) / 2 + 1.5 width = len(years) / 5 fig, ax = plt.subplots(figsize=(width, height)) sns.heatmap(SA_results, cmap="coolwarm", ax=ax).set_title(title) + ax.set_yticklabels(ax.get_yticklabels(), rotation = 0, fontsize = 8) fig.savefig(f'{save_file}.png', bbox_inches='tight') if __name__ == "__main__": From 672b812c508fe3b061f083e5d3c8e349aa47c22e Mon Sep 17 00:00:00 2001 From: trevorb1 Date: Thu, 13 Oct 2022 09:14:41 +0200 Subject: [PATCH 9/9] interaction plot --- workflow/Snakefile | 1 + workflow/rules/results.smk | 16 +++- workflow/scripts/plot_interactions.py | 107 ++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 workflow/scripts/plot_interactions.py diff --git a/workflow/Snakefile b/workflow/Snakefile index abd0a9b..a9a418a 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -39,6 +39,7 @@ rule all: input: expand("results/{scenario}_summary/SA_objective.{extension}", scenario=SCENARIOS.index, extension=['csv', 'png']), expand("results/{scenario}_summary/SA_{result_file}.{extension}", scenario=SCENARIOS.index, extension=['csv', 'png'], result_file=RESULT_FILES), + expand("results/{scenario}_summary/SA_interactions.png", scenario=SCENARIOS.index), expand("results/{scenario}/model_{model_run}/results/{x}.csv", x=OUTPUT_FILES, model_run=MODELRUNS, scenario=SCENARIOS.index), expand("results/{scenario}_summary/{result_file}_heatmap.png", scenario=SCENARIOS.index, result_file=RESULT_FILES), diff --git a/workflow/rules/results.smk b/workflow/rules/results.smk index 1679fea..e453616 100644 --- a/workflow/rules/results.smk +++ b/workflow/rules/results.smk @@ -73,6 +73,18 @@ rule create_heatmap: results=expand("results/{{scenario}}/{{result_file}}.{ext}", ext=config['filetype']) output: "results/{scenario}_summary/{result_file}_heatmap.png" - shell: "python workflow/scripts/create_heatmap.py {params.parameters} {input.sample} {input.results} {output}" + shell: + "python workflow/scripts/create_heatmap.py {params.parameters} {input.sample} {input.results} {output}" - +rule plot_interactions: + message: + "Creating interaction plots" + params: + parameters=config['parameters'] + input: + sample = "modelruns/{scenario}/morris_sample.txt", + results = "results/{scenario}/objective_{scenario}.csv" + output: + "results/{scenario}_summary/SA_interactions.png" + shell: + "python workflow/scripts/plot_interactions.py {params.parameters} {input.sample} {input.results} {output}" diff --git a/workflow/scripts/plot_interactions.py b/workflow/scripts/plot_interactions.py new file mode 100644 index 0000000..26ba929 --- /dev/null +++ b/workflow/scripts/plot_interactions.py @@ -0,0 +1,107 @@ +"""Plots interactions per trajectory. + +Arguments +--------- +path_to_parameters : str + File containing the parameters for generated sample +model_inputs : str + File path to sample model inputs +model_outputs : str + File path to model outputs +location_to_save : str + File path to save results +""" + +import matplotlib.pyplot as plt +import pandas as pd +import numpy as np +from math import ceil +import matplotlib.ticker as ticker +import sys +from pathlib import Path + +from logging import getLogger + +logger = getLogger(__name__) + +def main(parameters: pd.DataFrame, X: np.array, results: pd.DataFrame, save_file: str): + """Plots interactions. + + Parameters + ---------- + parameters : pd.DataFrame + Parameters for generated sample + X : np.array + Input Sample + model_results : pd.DataFrame + Model results for a OSeMOSYS variable + save_file : str + File path to save results + """ + + # Save sample data as dataframe + X = pd.DataFrame(X, columns=parameters['group'].to_list()) + X = X.loc[:,~X.columns.duplicated()] + + # Add objective cost column + Y_max = results['OBJECTIVE'].max() + Y = results['OBJECTIVE'] / Y_max + X['Objective_Cost'] = Y + + # save individual model results + models = {} + for row in range(len(X)): + models[f'model_{row}'] = X.loc[row, :].to_list() + categories = list(X) + + # plot + num_plots = int(len(X) / len(X.columns)) + num_rows = ceil(num_plots / 2) + runs_per_trajectory = len(X.columns) # + 1 taken care of by adding in result column + + fig, axs = plt.subplots(nrows=num_rows, ncols=2, sharex=True, sharey=True, figsize=(10,10), gridspec_kw = {'wspace':0.075, 'hspace':0.2}) + + counter = 0 + row = 0 + col = 0 + secondary_ax = [] + for model_num, model_data in models.items(): + axs[row, col].plot(categories, model_data, label = model_num) + axs[row, col].yaxis.set_major_locator(ticker.MultipleLocator(0.3333)) # 4 levels in morris + axs[row, col].yaxis.set_major_formatter(ticker.FormatStrFormatter('%0.2f')) + counter += 1 + if (counter) == runs_per_trajectory: + secondary_ax.append(axs[row, col].twinx()) + counter = 0 + col = (col + 1) % 2 + row = row if col == 1 else row + 1 + axs[num_rows - 1, 0].tick_params('x',labelrotation=90) + axs[num_rows - 1, 1].tick_params('x',labelrotation=90) + + row = -1 # start at -1 to put counter at start of loop + for index, _ in enumerate(secondary_ax): + row += 1 + if row % 2 != 0: + continue + secondary_ax[index].get_shared_y_axes().join(secondary_ax[index], secondary_ax[index + 1]) + secondary_ax[index].plot() + secondary_ax[index].yaxis.set_tick_params(labelright=False) + secondary_ax[index].set_ylim(0, Y_max) + + fig.supylabel('Sample Value', fontsize='x-large', x=0.05) + fig.suptitle('Interactions on Objective Cost', fontsize='x-large', y=0.93) + fig.savefig(f'{save_file}.png', bbox_inches='tight') + + +if __name__ == "__main__": + + parameters_file = sys.argv[1] + sample = sys.argv[2] + result_file = sys.argv[3] + save_file = str(Path(sys.argv[4]).with_suffix('')) + parameters = pd.read_csv(parameters_file) + + X = np.loadtxt(sample, delimiter=',') + results = pd.read_csv(result_file) + + main(parameters, X, results, save_file)