diff --git a/c_module/cli/cli.py b/c_module/cli/cli.py index 519dad9..e7d6330 100644 --- a/c_module/cli/cli.py +++ b/c_module/cli/cli.py @@ -9,8 +9,12 @@ default=user_input[ParamNames.add_on_activated.value], show_default=True, required=True, is_flag=True, help="Flag to use the carbon module as a standalone module or as a TiMBA add-on.") @click.option('-SC', '--sc_num', "sc_num", - default=user_input[ParamNames.sc_num.value], show_default=True, required=True, type=int, + default=user_input[ParamNames.sc_num.value], show_default=True, required=False, type=int, help="Flag to control the number of processed scenarios.") +@click.option('-IFP', '--input_folder_path', 'input_folder_path', default=user_input[ParamNames.input_folder_path.value], + show_default=True, required=False, type=str, help="Path to input folder.") +@click.option('-OFP', '--input_folder_path', 'output_folder_path', default=user_input[ParamNames.output_folder_path.value], + show_default=True, required=False, type=str, help="Path to output folder.") @click.option('-SY', '--start_year', 'start_year', default=user_input[ParamNames.start_year.value], show_default=True, required=True, type=int, help="Start year of carbon calculations.") @@ -44,12 +48,9 @@ @click.option('-UD', '--fao_data_update', 'fao_data_update', default=user_input[ParamNames.fao_data_update.value], show_default=True, required=False, is_flag=True, help="Flag to update FAO data.") -@click.option('-FP', '--folderpath', 'folderpath', default=user_input[ParamNames.folderpath.value], - show_default=True, required=False, type=str, help="Path to directory with Input/Output folder.") - def cli(add_on_activated, sc_num, start_year, end_year, calc_c_forest_agb, calc_c_forest_bgb, calc_c_forest_soil, calc_c_forest_dwl, calc_c_hwp, c_hwp_accounting_approach, read_in_pkl, show_carbon_dashboard, fao_data_update, - folderpath): + input_folder_path, output_folder_path): user_input_cli = { ParamNames.add_on_activated.value: add_on_activated, @@ -65,7 +66,8 @@ def cli(add_on_activated, sc_num, start_year, end_year, calc_c_forest_agb, calc_ ParamNames.c_hwp_accounting_approach.value: c_hwp_accounting_approach, ParamNames.show_carbon_dashboard.value: show_carbon_dashboard, ParamNames.fao_data_update.value: fao_data_update, - ParamNames.folderpath.value: folderpath, + ParamNames.input_folder_path.value: input_folder_path, + ParamNames.output_folder_path.value: output_folder_path, # Adavanced settings not available via CLI ParamNames.historical_c_hwp.value: user_input[ParamNames.historical_c_hwp.value], ParamNames.hist_hwp_start_year.value: user_input[ParamNames.hist_hwp_start_year.value], diff --git a/c_module/data_management/data_manager.py b/c_module/data_management/data_manager.py index 636a6a6..e25f371 100644 --- a/c_module/data_management/data_manager.py +++ b/c_module/data_management/data_manager.py @@ -1,4 +1,4 @@ -from c_module.parameters.paths import cmodule_is_standalone, extract_scenarios +from c_module.parameters.paths import extract_scenarios from c_module.parameters.defines import (VarNames, ParamNames, CountryConstants, FolderNames, PathNames) from c_module.user_io.default_parameters import user_input import pandas as pd @@ -16,16 +16,15 @@ class DataManager: @staticmethod def set_sc_paths(self): - TIMBADIR_INPUT = self.paths[PathNames.TIMBADIR_INPUT.value] - TIMBADIR_OUTPUT = self.paths[PathNames.TIMBADIR_OUTPUT.value] INPUT_FOLDER = self.paths[PathNames.INPUT_FOLDER.value] + OUTPUT_FOLDER = self.paths[PathNames.OUTPUT_FOLDER.value] - if user_input[ParamNames.add_on_activated.value] or not cmodule_is_standalone(debug=False): + if user_input[ParamNames.add_on_activated.value]: # input paths for add-on c-module - scenarios = extract_scenarios(input_folder=TIMBADIR_INPUT, - output_folder=TIMBADIR_OUTPUT, + scenarios = extract_scenarios(output_folder=OUTPUT_FOLDER, sc_num=user_input[ParamNames.sc_num.value]) PKL_RESULTS_INPUT = scenarios + input_folder = INPUT_FOLDER else: # input paths for standalone c-module scenarios = list((INPUT_FOLDER / Path("projection_data")).glob(r"*.pkl")) @@ -33,6 +32,10 @@ def set_sc_paths(self): scenarios.sort(key=lambda f: f.stat().st_mtime) scenarios = scenarios[-user_input[ParamNames.sc_num.value]:] PKL_RESULTS_INPUT = scenarios + input_folder = INPUT_FOLDER / Path("projection_data") + + if len(PKL_RESULTS_INPUT) == 0: + self.logger.info(f"No scenarios found in {input_folder}") self.sc_path = PKL_RESULTS_INPUT @@ -54,10 +57,10 @@ def check_input_data_structure(self): INPUT_FOLDER = self.paths[PathNames.INPUT_FOLDER.value] INPUT_FOLDER.mkdir(parents=True, exist_ok=True) - if cmodule_is_standalone(debug=False): - required = {FolderNames.additional_info.value, FolderNames.projection_data.value} - else: + if self.UserInput[ParamNames.add_on_activated.value]: required = {FolderNames.additional_info.value} + else: + required = {FolderNames.additional_info.value, FolderNames.projection_data.value} existing = {p.name for p in Path(INPUT_FOLDER).iterdir() if p.is_dir()} missing = list(required - existing) if len(missing) > 0: @@ -238,10 +241,7 @@ def save_data(self): carbon_data_ext = DataManager.add_additional_info(self, data=carbon_data_ext, sc=sc) self.timba_data[sc][VarNames.timba_data_carbon.value] = self.carbon_data[sc][VarNames.carbon_total.value] self.timba_data[sc][VarNames.timba_data_carbon_flat.value] = carbon_data_ext - if not self.UserInput[ParamNames.add_on_activated.value]: - DataManager.serialize_to_pickle(self.timba_data[sc], OUTPUT_FOLDER / Path(f"{sc}.pkl")) - else: - DataManager.serialize_to_pickle(self.carbon_data[sc], OUTPUT_FOLDER / Path(f"{sc}.pkl")) + DataManager.serialize_to_pickle(self.timba_data[sc], OUTPUT_FOLDER / Path(f"{sc}.pkl")) for df_key in self.carbon_data[sc].keys(): carbon_data = self.carbon_data[sc][df_key] diff --git a/c_module/parameters/defines.py b/c_module/parameters/defines.py index db6d365..18a9621 100644 --- a/c_module/parameters/defines.py +++ b/c_module/parameters/defines.py @@ -28,7 +28,8 @@ class ParamNames(Enum): start_year = "start_year" end_year = "end_year" read_in_pkl = "read_in_pkl" - folderpath = "folderpath" + input_folder_path = "input_folder_path" + output_folder_path = "output_folder_path" save_data_as = "save_data_as" calc_c_forest_agb = "calc_c_forest_agb" calc_c_forest_bgb = "calc_c_forest_bgb" diff --git a/c_module/parameters/paths.py b/c_module/parameters/paths.py index 217fdd9..10e828b 100644 --- a/c_module/parameters/paths.py +++ b/c_module/parameters/paths.py @@ -6,10 +6,9 @@ current_dt = dt.datetime.now().strftime("%Y%m%dT%H-%M-%S") -def extract_scenarios(input_folder, output_folder, sc_num): +def extract_scenarios(output_folder, sc_num): """ Extract scenario names from Excel files in a folder and merge them with 'DataContainer_Sc_' prefix. - :param input_folder: Path to the folder containing scenario files. :param output_folder: Path to the folder where the output files will be stored. :param sc_num: Number of scenarios to extract. :return: List of merged scenario names. @@ -18,9 +17,9 @@ def extract_scenarios(input_folder, output_folder, sc_num): files = list(folder_path.glob("*.pkl")) files.sort(key=lambda f: f.stat().st_mtime) if sc_num is None: - folder_path = Path(input_folder) + folder_path = Path(output_folder) try: - sc_num = len(list(folder_path.glob("*.xlsx"))) + sc_num = len(list(folder_path.glob("*.pkl"))) except FileNotFoundError: sc_num = 1 files = files[-sc_num:] @@ -32,132 +31,26 @@ def extract_scenarios(input_folder, output_folder, sc_num): return scenarios -def cmodule_is_standalone(debug: bool = False) -> bool: +def set_paths(user_input: dict) -> dict: """ - Check if cmodule is standalone or not, covering if the code is run as the main program, covering CLI, script, IDE, - and entry point runs. - :param debug: Flag to enable debug mode. - :return: Bool if cmodule is standalone or not. + Set paths dynamically based on user input. + :param user_input: dictionary with user input. + :return: dictionary of paths. """ - import sys - import inspect - import __main__ - - reasons = [] - - # Running under a typical test runner => treat as imported - if "pytest" in sys.modules: - reasons.append("pytest detected in sys.modules") - if debug: - print("DEBUG: pytest present -> treated as imported") - return True - if any("unittest" in mod for mod in sys.modules): - reasons.append("unittest detected in sys.modules") - if debug: - print("DEBUG: unittest present -> treated as imported") - return True - - # Simple and reliable check for most cases - if __name__ == "__main__": - reasons.append("__name__ == '__main__'") - if debug: - print("DEBUG: __name__ == '__main__' -> standalone") - return True - - # Inspect stack: some IDEs or runners execute a wrapper that sets __name__ == '__main__' - # in a different frame. If any frame was executed as __main__, assume standalone entry. - for frame_info in inspect.stack(): - g = frame_info.frame.f_globals - frame_name = g.get("__name__") - frame_file = g.get("__file__", None) - if frame_name == "__main__": - reasons.append(f"found frame with __name__ == '__main__' (file={frame_file})") - if debug: - print("DEBUG: stack frame with __name__ == '__main__' -> standalone") - print(f"DEBUG: frame file: {frame_file}") - return True - - # Compare the top-level script path with this package path: - # if the top-level entry script is outside this package, it likely invoked/imported the package. - main_file = getattr(__main__, "__file__", None) - if main_file: - try: - main_path = Path(main_file).resolve() - package_root = Path(__file__).resolve().parents[1] - # If the top-level script is the module file itself -> standalone - if main_path == Path(__file__).resolve(): - reasons.append("main_file equals this module file") - if debug: - print("DEBUG: main_file equals this module file -> standalone") - return True - # If the top-level script is *inside* the package: often still a standalone run (python -m) - if package_root in main_path.parents: - reasons.append("main_file is located inside package root (likely -m or IDE module run)") - if debug: - print("DEBUG: main_file inside package root -> standalone") - print(f"DEBUG: main_file={main_path}, package_root={package_root}") - return True - # Otherwise treat as imported - reasons.append("main_file exists but is outside package -> treated as imported") - if debug: - print("DEBUG: main_file outside package -> treated as imported") - print(f"DEBUG: main_file={main_path}, package_root={package_root}") - return False - except Exception as e: - # fallback - if debug: - print("DEBUG: error resolving main_file or package_root:", e) - pass - - # If none of the above matched, assume imported - if debug: - print("DEBUG: no indication of standalone execution; reasons:", reasons) - return False - - -def set_paths(user_input: dict) -> dict: PACKAGEDIR = Path(__file__).parent.parent.absolute() - if cmodule_is_standalone(debug=False): - if user_input[ParamNames.add_on_activated.value]: - import sys - print("Inconsistent settings:") - print(f"C-Module is executed as standalone: {cmodule_is_standalone(debug=False)}") - print(f"But parameter add_on_activated: {user_input[ParamNames.add_on_activated.value]}") - print(f"Harmonize settings to proceed") - sys.exit("Stopping execution.") - - if user_input[ParamNames.add_on_activated.value] or not cmodule_is_standalone(debug=False): - # input and output paths for add-on c-module - if user_input[ParamNames.folderpath.value] is None: - # If user-defined path does not exists, use default path - # For compatibility with other modules, paths must be adapted - TIMBADIR = Path(__file__).parent.parent.parent.parent.parent.parent.absolute() - TARGETDIR = TIMBADIR - else: - # If user-defined path exist - USER_PATH = Path(user_input[ParamNames.folderpath.value]).absolute() - TARGETDIR = USER_PATH - - TIMBADIR_INPUT = TARGETDIR / Path("TiMBA") / Path("data") / Path("input") / Path("01_Input_Files") - TIMBADIR_OUTPUT = TARGETDIR / Path("TiMBA") / Path("data") / Path("output") / Path("data") - + if user_input["input_folder_path"] is None: + # take standard paths from the package INPUT_FOLDER = PACKAGEDIR / Path("data") / Path("input") - OUTPUT_FOLDER = TIMBADIR_OUTPUT - else: - # input and output paths for standalone c-module - if user_input[ParamNames.folderpath.value] is None: - TARGETDIR = PACKAGEDIR - else: - USER_PATH = Path(user_input[ParamNames.folderpath.value]).absolute() - TARGETDIR = USER_PATH - - TIMBADIR_INPUT = None - TIMBADIR_OUTPUT = None - - INPUT_FOLDER = TARGETDIR / Path("data") / Path("input") - OUTPUT_FOLDER = TARGETDIR / Path("data") / Path("output") + # take paths as defined by the user + INPUT_FOLDER = Path(user_input["input_folder_path"]).absolute() + if user_input["output_folder_path"] is None: + # take standard paths from the package + OUTPUT_FOLDER = PACKAGEDIR / Path("data") / Path("output") + else: + # take paths as defined by the user + OUTPUT_FOLDER = Path(user_input["output_folder_path"]).absolute() # Official statistics from the Food and Agriculture Organization FAO_DIR = INPUT_FOLDER / Path("historical_data") @@ -181,8 +74,6 @@ def set_paths(user_input: dict) -> dict: path_dict = { PathNames.INPUT_FOLDER.value: INPUT_FOLDER, PathNames.OUTPUT_FOLDER.value: OUTPUT_FOLDER, - PathNames.TIMBADIR_INPUT.value: TIMBADIR_INPUT, - PathNames.TIMBADIR_OUTPUT.value: TIMBADIR_OUTPUT, PathNames.FAO_DIR.value: FAO_DIR, PathNames.FAOSTAT_URL.value: FAOSTAT_URL, PathNames.FAOSTAT_DATA.value: FAOSTAT_DATA, @@ -198,4 +89,5 @@ def set_paths(user_input: dict) -> dict: PathNames.DEFAULT_PROJECTION_DIR.value: DEFAULT_PROJECTION_DIR, PathNames.LOGGING_OUTPUT_FOLDER.value: LOGGING_OUTPUT_FOLDER } - return path_dict \ No newline at end of file + return path_dict + diff --git a/c_module/user_io/default_parameters.py b/c_module/user_io/default_parameters.py index 5a3c309..95e4c4a 100644 --- a/c_module/user_io/default_parameters.py +++ b/c_module/user_io/default_parameters.py @@ -9,7 +9,8 @@ end_year = 2050 # Not activated read_in_pkl = True # Caution False option is not implemented yet -folderpath = None +input_folder_path = None +output_folder_path = None # Forest carbon related parameters calc_c_forest_agb = True @@ -36,7 +37,8 @@ ParamNames.start_year.value: start_year, ParamNames.end_year.value: end_year, ParamNames.read_in_pkl.value: read_in_pkl, - ParamNames.folderpath.value: folderpath, + ParamNames.input_folder_path.value: input_folder_path, + ParamNames.output_folder_path.value: output_folder_path, ParamNames.calc_c_forest_agb.value: calc_c_forest_agb, ParamNames.calc_c_forest_bgb.value: calc_c_forest_bgb, ParamNames.calc_c_forest_soil.value: calc_c_forest_soil,