From c3357e6080cfeda0ec2af634aab121382023e3f3 Mon Sep 17 00:00:00 2001 From: Felix Oesterle Date: Thu, 13 Feb 2025 16:15:35 +0100 Subject: [PATCH 01/12] feat(comMotPSAInitial): Add initial com8MoTPSA module Initial implementation of the com8MoTPSA module. Includes a basic run script, configuration file parsing, and a function to convert the configuration file to the RCF format required by the MoT-PSA binary. A README_dev file is also added with basic instructions. --- avaframe/com8MoTPSA/README_dev.md | 7 +++ avaframe/com8MoTPSA/__init__.py | 0 avaframe/com8MoTPSA/com8MoTPSA.py | 58 ++++++++++++++++++ avaframe/com8MoTPSA/com8MoTPSACfg.ini | 86 +++++++++++++++++++++++++++ avaframe/com8MoTPSA/runCom8MoTPSA.py | 85 ++++++++++++++++++++++++++ 5 files changed, 236 insertions(+) create mode 100644 avaframe/com8MoTPSA/README_dev.md create mode 100644 avaframe/com8MoTPSA/__init__.py create mode 100644 avaframe/com8MoTPSA/com8MoTPSA.py create mode 100644 avaframe/com8MoTPSA/com8MoTPSACfg.ini create mode 100644 avaframe/com8MoTPSA/runCom8MoTPSA.py diff --git a/avaframe/com8MoTPSA/README_dev.md b/avaframe/com8MoTPSA/README_dev.md new file mode 100644 index 000000000..1f57c24fc --- /dev/null +++ b/avaframe/com8MoTPSA/README_dev.md @@ -0,0 +1,7 @@ +# com8MoTPSA + +This is the initial development / prototype stage of including control of MoTPSA in AvaFrame. + +For now you need a compiled binary, available from ??? + +This binary needs to live in the avaframe/com8MoTPSA directory \ No newline at end of file diff --git a/avaframe/com8MoTPSA/__init__.py b/avaframe/com8MoTPSA/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/avaframe/com8MoTPSA/com8MoTPSA.py b/avaframe/com8MoTPSA/com8MoTPSA.py new file mode 100644 index 000000000..4b0c006cb --- /dev/null +++ b/avaframe/com8MoTPSA/com8MoTPSA.py @@ -0,0 +1,58 @@ +import os +import subprocess +import platform + + +def _runAndCheck(command): + if os.name == "nt": + useShell = True + elif platform.system() == "Darwin": + useShell = False + else: + useShell = False + + # This starts the subprocess + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=useShell, + encoding="utf-8", + errors="replace", + universal_newlines=True, + ) + + while True: + realtimeOutput = process.stdout.readline() + + if realtimeOutput == "" and process.poll() is not None: + break + + if realtimeOutput: + line = realtimeOutput.strip() + print(line) + + +# Example usage + +def cfgToRcf(cfg, fileName): + with open(fileName, 'w') as f: + for section in cfg.sections(): + f.write(f"# {section.replace('_', ' ')}\n") + f.write("#\n") + for key, value in cfg.items(section): + key = key.replace('_', ' ') + key = key.strip() + f.write(f"{key:<40}{value}\n") + f.write("#\n") + + +def com8MoTPSAMain(cfgMain, cfgInfo=None): + # Generate command and run via subprocess.run + + rcfFile = 'outputTemplate.rcf' + + cfgToRcf(cfgInfo, rcfFile) + + command = ['./MoT-PSA', rcfFile] + _runAndCheck(command) diff --git a/avaframe/com8MoTPSA/com8MoTPSACfg.ini b/avaframe/com8MoTPSA/com8MoTPSACfg.ini new file mode 100644 index 000000000..c2c8d576e --- /dev/null +++ b/avaframe/com8MoTPSA/com8MoTPSACfg.ini @@ -0,0 +1,86 @@ +### Config File - This file contains the main settings for the simulation run +## Copy to local_com8MoTPSACfg.ini and set you parameters +; +; +;[GENERAL] +;# model type - only for file naming (psa - powder snow avalanche) +;modelType = psa + +[Run_information] +MoT-Voellmy input file version = 2023-03-06 +Area_of_Interest = Ryggfonn +UTM_zone = 33N +EPSG_geodetic_datum_code = 25833 +Run_name = Test_01 + +[File_names] +Grid_filename = ./dem.asc +Release_depth_1_filename = ./h1in08.asc +Release_depth_2_filename = ./h2in.asc +Bed_depth_filename = ./b0.asc +Bed_deposition_filename = ./T_Hsc1500_Phi050_d00.asc +Bed_shear_strength_filename = ./tau_c300.asc +Forest_density_filename = - +Tree_diameter_filename = - +Start_velocity_u_filename = - +Start_velocity_v_filename = - +Output_filename_root = ./Run_01/01 +Output_format = ESRI_ASCII_Grid + +[Physical_parameters] +Gravitational_acceleration_(m/s^2) = 9.81 +Density_kg_m3 = 250.0 +Rheology = Voellmy +Parameters = constant +Dry_friction_coefficient = 0.400 +Turbulent_drag_coefficient = 0.0010 +Effective_drag_height_m = 0.0 +Centrifugal_effects = yes +Passive_earth_pressure_coeff = 1.0 +Basal_drag_coeff_0_2 = 0.008 +Basal_drag_coeff_1_2 = 0.04 +Top_drag_coeff = 0.0001 +Conc_L2_Prof_Coeff_0 = 1.3333 +Conc_L2_Prof_Coeff_1 = -0.66667 +Conc_L2_Prof_Coeff_2 = 0.0 +Speed_L2_Prof_Coeff_0 = 1.4 +Speed_L2_Prof_Coeff_1 = 0.13333 +Speed_L2_Prof_Coeff_2 = -1.4 + +[Forest_effects] +Forest_effects = no +Tree_drag_coefficient = 1.0 +Modulus_of_rupture_MPa = 50.0 +Forest_decay_coefficient_m_s = 0.15 + +[ENTRAINMENT] +Entrainment_L1 = TJEM +Entrainment_L2 = Base+Air +Erosion_coefficient_L1 = 0.0 +Bed_strength_profile = global +Bed_friction_coefficient = 0.0 +Bed_density_kg_m3 = 140.0 +Deposition = yes +Deposition_density_L1_kg_m3 = 400.0 +Suspension_model = TJSM +Avalanche_shear_strength_Pa = 0.0 +Avalanche_shear_strength_deposit_Pa = 0.0 +Decay_coeff_snow_s_s_suspension = 1.0 +Decay_coeff_snow_s_s_deposition = 1.0 +Entrainment_coeff_m12 = 0.0 +Deposition_rate_21_m_s = 0.50 +Constant_density_L1 = yes +Evolving_geometry = no + +[NUMERICAL_PARAMETERS] +Simulation_time_s = 100.0 +Minimum_time_step_s = 0.001 +Maximum_time_step_s = 0.1 +Output_interval_s = 1.0 +Write_velocity_vectors = no +Write_maximum_pressure = yes +Write_instant_pressure = no +Minimum_flow_depth_m = 0.01 +Minimum_speed_m_s = 0.01 +Momentum_threshold = 0.05 +Initial_CFL_number = 0.8 \ No newline at end of file diff --git a/avaframe/com8MoTPSA/runCom8MoTPSA.py b/avaframe/com8MoTPSA/runCom8MoTPSA.py new file mode 100644 index 000000000..3f4d86f79 --- /dev/null +++ b/avaframe/com8MoTPSA/runCom8MoTPSA.py @@ -0,0 +1,85 @@ +""" + Run script for running python com8MoTPSA kernel +""" +# Load modules +# importing general python modules +import time +import argparse +import pathlib +import sys + +# Local imports +import avaframe.in3Utils.initializeProject as initProj +from avaframe.com8MoTPSA import com8MoTPSA +from avaframe.in3Utils import cfgUtils +from avaframe.in3Utils import logUtils +from avaframe.in3Utils import fileHandlerUtils as fU + + +def runCom8MoTPSA(avalancheDir=''): + """ Run com8MoTPSA in the default configuration with only an + avalanche directory as input + + Parameters + ---------- + avalancheDir: str + path to avalanche directory (setup eg. with init scipts) + + Returns + ------- + peakFilesDF: pandas dataframe + dataframe with info about com8MoTPSA peak file locations + """ + + # Time the whole routine + startTime = time.time() + + # log file name; leave empty to use default runLog.log + logName = 'runCom8MoTPSA' + + # Load avalanche directory from general configuration file + # More information about the configuration can be found here + # on the Configuration page in the documentation + cfgMain = cfgUtils.getGeneralConfig() + if avalancheDir != '': + cfgMain['MAIN']['avalancheDir'] = avalancheDir + else: + avalancheDir = cfgMain['MAIN']['avalancheDir'] + + # Start logging + log = logUtils.initiateLogger(avalancheDir, logName) + log.info('MAIN SCRIPT') + log.info('Current avalanche: %s', avalancheDir) + + # ---------------- + # Clean input directory(ies) of old work and output files + # If you just created the ``avalancheDir`` this one should be clean but if you + # already did some calculations you might want to clean it:: + initProj.cleanSingleAvaDir(avalancheDir, deleteOutput=False) + + # Set friction model according to cmd argument + cfgCom8MoTPSA = cfgUtils.getModuleConfig(com8MoTPSA, toPrint=False) + + # ---------------- + # Run dense flow + com8MoTPSA.com8MoTPSAMain(cfgMain, cfgInfo=cfgCom8MoTPSA) + + # # Get peakfiles to return to QGIS + # avaDir = pathlib.Path(avalancheDir) + # inputDir = avaDir / 'Outputs' / 'com8MoTPSA' / 'peakFiles' + # peakFilesDF = fU.makeSimDF(inputDir, avaDir=avaDir) + + # Print time needed + endTime = time.time() + log.info('Took %6.1f seconds to calculate.' % (endTime - startTime)) + + return peakFilesDF + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Run com8MoTPSA workflow') + parser.add_argument('avadir', metavar='avalancheDir', type=str, nargs='?', default='', + help='the avalanche directory') + + args = parser.parse_args() + runCom8MoTPSA(str(args.avadir)) From 4f1d4159757db081fd9cc749c416b7c96f4c65b6 Mon Sep 17 00:00:00 2001 From: Felix Oesterle Date: Thu, 13 Feb 2025 19:44:05 +0100 Subject: [PATCH 02/12] feat(comMotPSAInitial): Improve config file formatting and conversion (comMotPSAInitial) - First working conversion feat(com8MoTPSA): Enhance logging and configuration handling (com8MoTPSA) ; working commit feat(com1DFA, com8MoTPSA): Refactor simulation file handling and configuration management (comMotPSAInitialA) ; working commit --- avaframe/com1DFA/com1DFA.py | 4 +- avaframe/com8MoTPSA/com8MoTPSA.py | 78 +++++++-- avaframe/com8MoTPSA/com8MoTPSACfg.ini | 219 +++++++++++++++++--------- avaframe/com8MoTPSA/runCom8MoTPSA.py | 7 +- avaframe/in1Data/getInput.py | 1 - avaframe/runStandardTestsCom1DFA.py | 5 +- 6 files changed, 224 insertions(+), 90 deletions(-) diff --git a/avaframe/com1DFA/com1DFA.py b/avaframe/com1DFA/com1DFA.py index dbfa58313..46b97c007 100644 --- a/avaframe/com1DFA/com1DFA.py +++ b/avaframe/com1DFA/com1DFA.py @@ -2935,7 +2935,9 @@ def prepareVarSimDict(standardCfg, inputSimFiles, variationDict, simNameExisting log.warning("Simulation %s already exists, not repeating it" % simName) log.info("Done preparing variations -----") - inputSimFiles.pop("demFile") + # TODO: maybe treat this in some other way, i.e. adding an "finalDEM" or similar + # inputSimFiles.pop("demFile") + inputSimFiles["demFile"] = pathToDemFull return simDict diff --git a/avaframe/com8MoTPSA/com8MoTPSA.py b/avaframe/com8MoTPSA/com8MoTPSA.py index 4b0c006cb..da24ac394 100644 --- a/avaframe/com8MoTPSA/com8MoTPSA.py +++ b/avaframe/com8MoTPSA/com8MoTPSA.py @@ -1,8 +1,18 @@ import os import subprocess import platform +import logging +import pandas as pd +import avaframe.com1DFA.com1DFATools as com1DFATools +import avaframe.com1DFA.com1DFA as com1DFA +from avaframe.in3Utils import cfgUtils +# create local logger +log = logging.getLogger(__name__) + + +# TODO move to whereever? def _runAndCheck(command): if os.name == "nt": useShell = True @@ -33,26 +43,72 @@ def _runAndCheck(command): print(line) -# Example usage - +# TODO move to utils def cfgToRcf(cfg, fileName): - with open(fileName, 'w') as f: + with open(fileName, "w") as f: for section in cfg.sections(): - f.write(f"# {section.replace('_', ' ')}\n") - f.write("#\n") + if section in ("FOREST_EFFECTS", "ENTRAINMENT"): + pass + elif section in ("GENERAL", "INPUT"): + continue + else: + f.write(f"# {section.replace('_', ' ')}\n") + f.write("#\n") for key, value in cfg.items(section): - key = key.replace('_', ' ') + # key = key.replace('_', ' ') key = key.strip() f.write(f"{key:<40}{value}\n") f.write("#\n") def com8MoTPSAMain(cfgMain, cfgInfo=None): - # Generate command and run via subprocess.run + # Get all necessary information from the configuration files + + # Load avalanche directory from general configuration file + avalancheDir = cfgMain["MAIN"]["avalancheDir"] + + # fetch type of cfgInfo + typeCfgInfo = com1DFATools.checkCfgInfoType(cfgInfo) + + # preprocessing to create configuration objects for all simulations to run + simDict, outDir, inputSimFiles, simDFExisting = com1DFA.com1DFAPreprocess(cfgMain, typeCfgInfo, cfgInfo) + + log.info("The following simulations will be performed") + for key in simDict: + log.info("Simulation: %s" % key) + exportFlag = simDict[key]["cfgSim"]["EXPORTS"].getboolean("exportData") + + for key in simDict: + # Generate command and run via subprocess.run + # Configuration that needs adjustment + + simDF = pd.DataFrame() + + # load configuration object for current sim + cfg = simDict[key]["cfgSim"] + + # fetch simHash for current sim + simHash = simDict[key]["simHash"] + + # append configuration to dataframe + simDF = cfgUtils.appendCgf2DF(simHash, key, cfg, simDF) + print(simDF["DEM"]) + + cfgInfo["Run information"]["Area of Interest"] = cfgMain["MAIN"]["avalancheDir"] + cfgInfo["Run information"]["UTM zone"] = "HAUMIBLAU" + cfgInfo["Run information"]["EPSG geodetic datum code"] = "37888" + cfgInfo["Run information"]["Run name"] = cfgMain["MAIN"]["avalancheDir"] + cfgInfo["File names"]["Grid filename"] = "./" + str(inputSimFiles["demFile"]) + + # select release area input data according to chosen release scenario + # inputSimFiles = gI.selectReleaseFile(inputSimFiles, cfg["INPUT"]["releaseScenario"]) + + # create required input from input files + # demOri, inputSimLines = prepareInputData(inputSimFiles, cfg) - rcfFile = 'outputTemplate.rcf' + rcfFile = key + ".rcf" - cfgToRcf(cfgInfo, rcfFile) + cfgToRcf(cfgInfo, rcfFile) - command = ['./MoT-PSA', rcfFile] - _runAndCheck(command) + # command = ['./MoT-PSA', rcfFile] + # _runAndCheck(command) diff --git a/avaframe/com8MoTPSA/com8MoTPSACfg.ini b/avaframe/com8MoTPSA/com8MoTPSACfg.ini index c2c8d576e..49172d068 100644 --- a/avaframe/com8MoTPSA/com8MoTPSACfg.ini +++ b/avaframe/com8MoTPSA/com8MoTPSACfg.ini @@ -1,86 +1,161 @@ ### Config File - This file contains the main settings for the simulation run ## Copy to local_com8MoTPSACfg.ini and set you parameters -; -; -;[GENERAL] -;# model type - only for file naming (psa - powder snow avalanche) -;modelType = psa -[Run_information] +[GENERAL] +# model type - only for file naming (psa - powder snow avalanche) +modelType = psa + +# list of simulations that shall be performed (null, ent, res, entres, available (use all available input data)) +simTypeList = available + + +#+++++Release thickness++++ +# True if release thickness should be read from shapefile file; if False - relTh read from ini file +relThFromShp = True +# if a variation on relTh shall be performed add here +- percent and number of steps separated by $ +# for example relThPercentVariation=50$10 [%] +relThPercentVariation = +# if a variation on relTh shall be performed add here +- absolute value and number of steps separated by $ +# for example relThRangeVariation=0.5$10 [m] +relThRangeVariation = +# if a variation on relTh shall be performed add here +- ci% value and number of steps separated by $ +# for example relThRangeFromCiVariation= ci95$10 +relThRangeFromCiVariation = +# if variation on relTh shall be performed using a normal distribution in number of steps, +# value of buildType (ci95 value), min and max of dist in percent, buildType (ci95 only allowed), +# support (e.g. 10000) all separated by $: e.g. normaldistribution$numberOfSteps$0.3$95$ci95$10000 +# if relThFromShp=True ci95 is read from shp file too +relThDistVariation = +# release thickness (only considered if relThFromShp=False) [m] +relTh = +# read release thickness directly from file (relThFromShp needs to be False) +relThFromFile = False + + +#+++++Entrainment thickness++++ +# True if entrainment thickness should be read from shapefile file; if False - entTh read from ini file +entThFromShp = True +# if a thickness value is missing for the entrainment feature in the provided shp file this value is used for all features [m] +entThIfMissingInShp = 0.3 +# if a variation on entTh shall be performed add here +- percent and number of steps separated by $ +# for example entThPercentVariation=50$10 [%] +entThPercentVariation = +# if a variation on entTh shall be performed add here +- absolute value and number of steps separated by $ +# for example entThRangeVariation=0.5$10 [m] +entThRangeVariation = +# if a variation on entTh shall be performed add here +- ci% value and number of steps separated by $ +# for example entThRangeFromCiVariation= ci95$10 +entThRangeFromCiVariation = +# if variation on entTh shall be performed using a normal distribution in number of steps, +# value of buildType (ci95 value), min and max of dist in percent, buildType (ci95 only allowed), +# support (e.g. 10000) all separated by $: e.g. normaldistribution$numberOfSteps$0.3$95$ci95$10000 +# if entFromShp=True ci95 is read from shp file too +entThDistVariation = +# entrainment thickness (only considered if entThFromShp=False) [m] +entTh = + +#+++++Secondary release thickness+++++ +# if secRelArea is True - add secondary release area +secRelArea = True +# True if release thickness should be read from shapefile file; if False - secondaryRelTh read from ini file +secondaryRelThFromShp = True +# if a variation on secondaryRelTh shall be performed add here +- percent and number of steps separated by $ +# for example secondaryRelThPercentVariation=50$10 [%] +secondaryRelThPercentVariation = +# if a variation on secondaryRelTh shall be performed add here +- absolute value and number of steps separated by $ +# for example secondaryRelThRangeVariation=0.5$10 [m] +secondaryRelThRangeVariation = +# if a variation on secondaryRelTh shall be performed add here +- ci% value and number of steps separated by $ +# for example secondaryRelThRangeFromCiVariation= ci95$10 +secondaryRelThRangeFromCiVariation = +# if variation on secondaryRelTh shall be performed using a normal distribution in number of steps, +# value of buildType (ci95 value), min and max of dist in percent, buildType (ci95 only allowed), +# support (e.g. 10000) all separated by $: e.g. normaldistribution$numberOfSteps$0.3$95$ci95$10000 +# if secondaryRelThFromShp=True ci95 is read from shp file too +secondaryRelThDistVariation = +# secondary area release thickness (only considered if secondaryRelThFromShp=False) [m] +secondaryRelTh = + +[INPUT] +# specify a particular release area scenario, provide name of shapefile with or without extension .shp (optional) +releaseScenario = + +# Below are the settings for the MoT-PSA model +[Run information] MoT-Voellmy input file version = 2023-03-06 -Area_of_Interest = Ryggfonn -UTM_zone = 33N -EPSG_geodetic_datum_code = 25833 -Run_name = Test_01 +Area of Interest = Ryggfonn +UTM zone = 33N +EPSG geodetic datum code = 25833 +Run name = Test_01 -[File_names] -Grid_filename = ./dem.asc -Release_depth_1_filename = ./h1in08.asc -Release_depth_2_filename = ./h2in.asc -Bed_depth_filename = ./b0.asc -Bed_deposition_filename = ./T_Hsc1500_Phi050_d00.asc -Bed_shear_strength_filename = ./tau_c300.asc -Forest_density_filename = - -Tree_diameter_filename = - -Start_velocity_u_filename = - -Start_velocity_v_filename = - -Output_filename_root = ./Run_01/01 -Output_format = ESRI_ASCII_Grid +[File names] +Grid filename = - +Release depth 1 filename = ./h1in08.asc +Release depth 2 filename = ./h2in.asc +Bed depth filename = - +Bed deposition filename = - +Bed shear strength filename = - +Forest density filename = - +Tree diameter filename = - +Start velocity u filename = - +Start velocity v filename = - +Output filename root = ./Run_01/01 +Output format = ESRI_ASCII_Grid [Physical_parameters] -Gravitational_acceleration_(m/s^2) = 9.81 -Density_kg_m3 = 250.0 +Gravitational acceleration (m/s^2) = 9.81 +Density (kg/m^3) = 250.0 Rheology = Voellmy Parameters = constant -Dry_friction_coefficient = 0.400 -Turbulent_drag_coefficient = 0.0010 -Effective_drag_height_m = 0.0 -Centrifugal_effects = yes -Passive_earth_pressure_coeff = 1.0 -Basal_drag_coeff_0_2 = 0.008 -Basal_drag_coeff_1_2 = 0.04 -Top_drag_coeff = 0.0001 -Conc_L2_Prof_Coeff_0 = 1.3333 -Conc_L2_Prof_Coeff_1 = -0.66667 -Conc_L2_Prof_Coeff_2 = 0.0 -Speed_L2_Prof_Coeff_0 = 1.4 -Speed_L2_Prof_Coeff_1 = 0.13333 -Speed_L2_Prof_Coeff_2 = -1.4 +Dry-friction coefficient (-) = 0.400 +Turbulent drag coefficient (-) = 0.0010 +Effective drag height (m) = 0.0 +Centrifugal effects = yes +Passive earth-pressure coeff. (-) = 1.0 +Basal drag coeff. 0-2 (-) = 0.008 +Basal drag coeff. 1-2 (-) = 0.04 +Top drag coeff. (-) = 0.0001 +Conc_L2_Prof_Coeff_0 (-) = 1.3333 +Conc_L2_Prof_Coeff_1 (-) = -0.66667 +Conc_L2_Prof_Coeff_2 (-) = 0.0 +Speed_L2_Prof_Coeff_0 (-) = 1.4 +Speed_L2_Prof_Coeff_1 (-) = 0.13333 +Speed_L2_Prof_Coeff_2 (-) = -1.4 -[Forest_effects] -Forest_effects = no -Tree_drag_coefficient = 1.0 -Modulus_of_rupture_MPa = 50.0 -Forest_decay_coefficient_m_s = 0.15 +[FOREST_EFFECTS] +Forest effects = no +Tree drag coefficient (-) = 1.0 +Modulus of rupture (MPa) = 50.0 +Forest decay coefficient (m s) = 0.15 [ENTRAINMENT] -Entrainment_L1 = TJEM -Entrainment_L2 = Base+Air -Erosion_coefficient_L1 = 0.0 -Bed_strength_profile = global -Bed_friction_coefficient = 0.0 -Bed_density_kg_m3 = 140.0 +Entrainment L1 = TJEM +Entrainment L2 = Base+Air +Erosion coefficient L1 (-) = 0.0 +Bed strength profile = global +Bed friction coefficient (-) = 0.0 +Bed density (kg/m^3) = 140.0 Deposition = yes -Deposition_density_L1_kg_m3 = 400.0 -Suspension_model = TJSM -Avalanche_shear_strength_Pa = 0.0 -Avalanche_shear_strength_deposit_Pa = 0.0 -Decay_coeff_snow_s_s_suspension = 1.0 -Decay_coeff_snow_s_s_deposition = 1.0 -Entrainment_coeff_m12 = 0.0 -Deposition_rate_21_m_s = 0.50 -Constant_density_L1 = yes -Evolving_geometry = no +Deposition density L1 (kg/m^3) = 400.0 +Suspension model = TJSM +Avalanche shear strength (Pa) = 0.0 +Avalanche shear strength deposit (Pa) = 0.0 +Decay coeff. snow s. s. suspension (-) = 1.0 +Decay coeff. snow s. s. deposition (-) = 1.0 +Entrainment coeff. m12 (-) = 0.0 +Deposition rate 21 (m/s) = 0.50 +Constant density L1 = yes +Evolving geometry = no -[NUMERICAL_PARAMETERS] -Simulation_time_s = 100.0 -Minimum_time_step_s = 0.001 -Maximum_time_step_s = 0.1 -Output_interval_s = 1.0 -Write_velocity_vectors = no -Write_maximum_pressure = yes -Write_instant_pressure = no -Minimum_flow_depth_m = 0.01 -Minimum_speed_m_s = 0.01 -Momentum_threshold = 0.05 -Initial_CFL_number = 0.8 \ No newline at end of file +[Numerical parameters] +Simulation time (s) = 100.0 +Minimum time step (s) = 0.001 +Maximum time step (s) = 0.1 +Output interval (s) = 1.0 +Write velocity vectors = no +Write maximum pressure = yes +Write instant. pressure = no +Minimum flow depth (m) = 0.01 +Minimum speed (m/s) = 0.01 +Momentum threshold (-) = 0.05 +Initial CFL number (-) = 0.8 \ No newline at end of file diff --git a/avaframe/com8MoTPSA/runCom8MoTPSA.py b/avaframe/com8MoTPSA/runCom8MoTPSA.py index 3f4d86f79..b14595b4d 100644 --- a/avaframe/com8MoTPSA/runCom8MoTPSA.py +++ b/avaframe/com8MoTPSA/runCom8MoTPSA.py @@ -57,11 +57,11 @@ def runCom8MoTPSA(avalancheDir=''): # already did some calculations you might want to clean it:: initProj.cleanSingleAvaDir(avalancheDir, deleteOutput=False) - # Set friction model according to cmd argument + # Get module config cfgCom8MoTPSA = cfgUtils.getModuleConfig(com8MoTPSA, toPrint=False) # ---------------- - # Run dense flow + # Run psa com8MoTPSA.com8MoTPSAMain(cfgMain, cfgInfo=cfgCom8MoTPSA) # # Get peakfiles to return to QGIS @@ -73,7 +73,8 @@ def runCom8MoTPSA(avalancheDir=''): endTime = time.time() log.info('Took %6.1f seconds to calculate.' % (endTime - startTime)) - return peakFilesDF + # return peakFilesDF + return if __name__ == '__main__': diff --git a/avaframe/in1Data/getInput.py b/avaframe/in1Data/getInput.py index cd7b3611d..9f63385b3 100644 --- a/avaframe/in1Data/getInput.py +++ b/avaframe/in1Data/getInput.py @@ -250,7 +250,6 @@ def getInputDataCom1DFA(avaDir): demFile = getDEMPath(avaDir) # check if frictionParameter file is available - # TODO: enable this with geotiff muFile, entResInfo["mu"] = getAndCheckInputFiles(inputDir, "RASTERS", "mu parameter data", fileExt="raster", fileSuffix='_mu') diff --git a/avaframe/runStandardTestsCom1DFA.py b/avaframe/runStandardTestsCom1DFA.py index a8ad4c680..75bc7b214 100644 --- a/avaframe/runStandardTestsCom1DFA.py +++ b/avaframe/runStandardTestsCom1DFA.py @@ -47,8 +47,8 @@ # valuesList = ['resistance'] filterType = 'TAGS' valuesList = ['standardTest', 'standardTestSnowGlide'] -# filterType = 'AVANAME' -# valuesList = ['avaAlr'] +# filterType = 'NAME' +# valuesList = ['avaPyramidNullTest'] testList = tU.filterBenchmarks(testDictList, filterType, valuesList, condition='or') @@ -80,6 +80,7 @@ avaDir = test['AVADIR'] cfgMain['MAIN']['avalancheDir'] = avaDir + print("\n", 40 * "*", "\n", test["NAME"], 40 * "*") # Fetch benchmark test info benchDict = test simNameRef = test['simNameRef'] From a0ab8ba0b345e725410cd048a9720a875b008990 Mon Sep 17 00:00:00 2001 From: Felix Oesterle Date: Thu, 6 Mar 2025 16:04:33 +0100 Subject: [PATCH 03/12] feat(com8MoTPSA): Add function to rewrite DEM values and update configuration parameters (comMotPSAInitial) - Add a function to rewrite DEM NaN values to 0.0 and update the nodata value in the header. - Update MoT-PSA configuration parameters to use the modified DEM and set release depth filenames. - Convert release shape to raster with values for the current simulation. - Write release lines to raster files. - Increase precision of raster output to 3 decimal places. feat(com8MoTPSA): Enhance output handling and add bed input files (com8MoTPSA) - Adds dummy bed depth, deposition, and shear strength files to the MoT-PSA configuration. These files are currently populated with empty rasters. This prepares the framework for future incorporation of these parameters into the simulation. Working commit: squash --- avaframe/com8MoTPSA/com8MoTPSA.py | 100 ++++++++++++++++++++++++-- avaframe/com8MoTPSA/com8MoTPSACfg.ini | 6 +- avaframe/in2Trans/rasterUtils.py | 2 +- 3 files changed, 98 insertions(+), 10 deletions(-) diff --git a/avaframe/com8MoTPSA/com8MoTPSA.py b/avaframe/com8MoTPSA/com8MoTPSA.py index da24ac394..f147e7973 100644 --- a/avaframe/com8MoTPSA/com8MoTPSA.py +++ b/avaframe/com8MoTPSA/com8MoTPSA.py @@ -3,10 +3,15 @@ import platform import logging import pandas as pd +import numpy as np import avaframe.com1DFA.com1DFATools as com1DFATools import avaframe.com1DFA.com1DFA as com1DFA from avaframe.in3Utils import cfgUtils +from avaframe.in2Trans import rasterUtils as rU +from avaframe.com1DFA import particleInitialisation as pI +from avaframe.in1Data import getInput as gI +import avaframe.in3Utils.geoTrans as geoTrans # create local logger log = logging.getLogger(__name__) @@ -32,6 +37,9 @@ def _runAndCheck(command): universal_newlines=True, ) + printCounter = 0 + counter = 1 + while True: realtimeOutput = process.stdout.readline() @@ -40,7 +48,29 @@ def _runAndCheck(command): if realtimeOutput: line = realtimeOutput.strip() - print(line) + + # do not pollute output window with time step prints + # TODO: hacky for now + if "Step" in line: + counter = counter + 1 + printCounter = printCounter + 1 + if printCounter > 100: + # print('\r' + line, flush=True, end='') + msg = ( + "Process is running. Reported time steps (all sims): " + + str(counter) + ) + print(msg) + printCounter = 0 + + elif "find_dt" in line: + continue + elif "h1" in line: + continue + elif "h2" in line: + continue + else: + print(line) # TODO move to utils @@ -61,6 +91,16 @@ def cfgToRcf(cfg, fileName): f.write("#\n") +def rewriteDEMtoZeroValues(demFile): + demData = rU.readRaster(demFile) + demData["rasterData"][np.isnan(demData["rasterData"])] = 0.0 + demData["header"]["nodata_value"] = 0.0 + newFileName = demFile.parent / demFile.stem + rU.writeResultToRaster(demData["header"], demData["rasterData"], newFileName) + + + + def com8MoTPSAMain(cfgMain, cfgInfo=None): # Get all necessary information from the configuration files @@ -73,6 +113,11 @@ def com8MoTPSAMain(cfgMain, cfgInfo=None): # preprocessing to create configuration objects for all simulations to run simDict, outDir, inputSimFiles, simDFExisting = com1DFA.com1DFAPreprocess(cfgMain, typeCfgInfo, cfgInfo) + # convert DEM from nan to 0 values + # TODO: suggest MoT-PSA to handle nan values + rewriteDEMtoZeroValues(inputSimFiles["demFile"]) + + log.info("The following simulations will be performed") for key in simDict: log.info("Simulation: %s" % key) @@ -92,13 +137,55 @@ def com8MoTPSAMain(cfgMain, cfgInfo=None): # append configuration to dataframe simDF = cfgUtils.appendCgf2DF(simHash, key, cfg, simDF) - print(simDF["DEM"]) + # convert release shape to raster with values for current sim + relFile = simDict[key]["relFile"] + + # select release area input data according to chosen release scenario + inputSimFiles = gI.selectReleaseFile(inputSimFiles, cfg["INPUT"]["releaseScenario"]) + # create required input from input files + demOri, inputSimLines = com1DFA.prepareInputData(inputSimFiles, cfg) + + if cfg["GENERAL"].getboolean("iniStep"): + # append buffered release Area + inputSimLines = pI.createReleaseBuffer(cfg, inputSimLines) + + # set thickness values for the release area, entrainment and secondary release areas + relName, inputSimLines, badName = com1DFA.prepareReleaseEntrainment( + cfg, inputSimFiles["releaseScenario"], inputSimLines + ) + + releaseLine = inputSimLines["releaseLine"] + # check if release features overlap between features + # TODO: split releaseheight -> question NGI + dem = rU.readRaster(inputSimFiles["demFile"]) + dem["originalHeader"] = dem["header"].copy() + releaseLine = geoTrans.prepareArea(releaseLine, dem, np.sqrt(2), combine=True, checkOverlap=False) + releaseL1 = inputSimFiles["demFile"].parent / "releaseLine1" + releaseL2 = inputSimFiles["demFile"].parent / "releaseLine2" + bedDepth = inputSimFiles["demFile"].parent / "dummyBedDepth" + bedDepo = inputSimFiles["demFile"].parent / "dummyBedDepo" + bedShear = inputSimFiles["demFile"].parent / "dummyBedShear" + rU.writeResultToRaster(dem["header"], releaseLine["rasterData"], releaseL1) + rU.writeResultToRaster(dem["header"], releaseLine["rasterData"], releaseL2) + emptyRaster = np.full_like(releaseLine["rasterData"], 0) + rU.writeResultToRaster(dem["header"], emptyRaster, bedDepth) + rU.writeResultToRaster(dem["header"], emptyRaster, bedDepo) + rU.writeResultToRaster(dem["header"], emptyRaster, bedShear) + + # set configuration for MoT-PSA cfgInfo["Run information"]["Area of Interest"] = cfgMain["MAIN"]["avalancheDir"] - cfgInfo["Run information"]["UTM zone"] = "HAUMIBLAU" - cfgInfo["Run information"]["EPSG geodetic datum code"] = "37888" + cfgInfo["Run information"]["UTM zone"] = "32N" + cfgInfo["Run information"]["EPSG geodetic datum code"] = "31287" cfgInfo["Run information"]["Run name"] = cfgMain["MAIN"]["avalancheDir"] cfgInfo["File names"]["Grid filename"] = "./" + str(inputSimFiles["demFile"]) + cfgInfo["File names"]["Release depth 1 filename"] = "./" + str(releaseL1) + ".asc" + cfgInfo["File names"]["Release depth 2 filename"] = "./" + str(releaseL2) + ".asc" + cfgInfo["File names"]["Bed depth filename"] = "./" + str(bedDepth) + ".asc" + cfgInfo["File names"]["Bed deposition filename"] = "./" + str(bedDepo) + ".asc" + cfgInfo["File names"]["Bed shear strength filename"] = "./" + str(bedShear) + ".asc" + cfgInfo["File names"]["Output filename root"] = "./avaTest/Work/" + str(simHash) + "/" + str( + simHash) # select release area input data according to chosen release scenario # inputSimFiles = gI.selectReleaseFile(inputSimFiles, cfg["INPUT"]["releaseScenario"]) @@ -110,5 +197,6 @@ def com8MoTPSAMain(cfgMain, cfgInfo=None): cfgToRcf(cfgInfo, rcfFile) - # command = ['./MoT-PSA', rcfFile] - # _runAndCheck(command) + command = ['./MoT-PSA', rcfFile] + _runAndCheck(command) + diff --git a/avaframe/com8MoTPSA/com8MoTPSACfg.ini b/avaframe/com8MoTPSA/com8MoTPSACfg.ini index 49172d068..2e7950bfd 100644 --- a/avaframe/com8MoTPSA/com8MoTPSACfg.ini +++ b/avaframe/com8MoTPSA/com8MoTPSACfg.ini @@ -129,10 +129,10 @@ Modulus of rupture (MPa) = 50.0 Forest decay coefficient (m s) = 0.15 [ENTRAINMENT] -Entrainment L1 = TJEM -Entrainment L2 = Base+Air +Entrainment L1 = none +Entrainment L2 = none Erosion coefficient L1 (-) = 0.0 -Bed strength profile = global +Bed strength profile = constant Bed friction coefficient (-) = 0.0 Bed density (kg/m^3) = 140.0 Deposition = yes diff --git a/avaframe/in2Trans/rasterUtils.py b/avaframe/in2Trans/rasterUtils.py index 260eac856..a7d7063aa 100644 --- a/avaframe/in2Trans/rasterUtils.py +++ b/avaframe/in2Trans/rasterUtils.py @@ -189,7 +189,7 @@ class with methods that give cellsize, nrows, ncols, xllcenter width=resultArray.shape[1], count=1, dtype=resultArray.dtype, - # decimal_precision=3, + decimal_precision=3, ) if flip: rasterOut.write(np.flipud(resultArray), 1) From 4936cd7fb4f6dd76e3d7dc34e69adcc6a2e079b2 Mon Sep 17 00:00:00 2001 From: Felix Oesterle Date: Tue, 11 Mar 2025 22:48:10 +0100 Subject: [PATCH 04/12] feat(comMotPSAInitial): Implement com8MoTPSA preprocessing and postprocessing (comMotPSAInitial) - Implemented preprocessing steps for com8MoTPSA, including creating rcf files and preparing input data. - Implemented postprocessing steps for com8MoTPSA, including copying result files to the output directory. - Parallelized the execution of MoT-PSA simulations. feat(com8MoTPSA): Enhance postprocessing and add plotting (comMotPSAInitial) - Enhance postprocessing to copy ppr, pfd, and pfv files with updated naming conventions. - Update plotUtilsCfg.ini with configurations for pfd plotting. feat(com8MoTPSA): Update file handling and increase simulation time (comMotPSAInitial) - Changed working directory to the script's directory before executing MoT-PSA. - Removed "./" prefix from file paths in the configuration file to use absolute paths. - Set releaseL2 to a zero raster instead of releaseLine raster data. - Increased simulation time in `com8MoTPSACfg.ini` from 100.0s to 1000.0s. adjust how release files are used --- avaframe/com8MoTPSA/com8MoTPSA.py | 206 +++++++++++++++++++++----- avaframe/com8MoTPSA/com8MoTPSACfg.ini | 4 +- avaframe/out3Plot/plotUtils.py | 1 + avaframe/out3Plot/plotUtilsCfg.ini | 4 + 4 files changed, 173 insertions(+), 42 deletions(-) diff --git a/avaframe/com8MoTPSA/com8MoTPSA.py b/avaframe/com8MoTPSA/com8MoTPSA.py index f147e7973..09a78e985 100644 --- a/avaframe/com8MoTPSA/com8MoTPSA.py +++ b/avaframe/com8MoTPSA/com8MoTPSA.py @@ -4,6 +4,16 @@ import logging import pandas as pd import numpy as np +import pathlib +import time +import shutil + +if os.name == "nt": + from multiprocessing.pool import ThreadPool as Pool +elif platform.system() == "Darwin": + from multiprocessing.pool import ThreadPool as Pool +else: + from multiprocessing import Pool import avaframe.com1DFA.com1DFATools as com1DFATools import avaframe.com1DFA.com1DFA as com1DFA @@ -12,6 +22,10 @@ from avaframe.com1DFA import particleInitialisation as pI from avaframe.in1Data import getInput as gI import avaframe.in3Utils.geoTrans as geoTrans +import avaframe.in3Utils.fileHandlerUtils as fU +from avaframe.out1Peak import outPlotAllPeak as oP + +import avaframe.com8MoTPSA.com8MoTPSA as com8MoTPSA # create local logger log = logging.getLogger(__name__) @@ -57,10 +71,10 @@ def _runAndCheck(command): if printCounter > 100: # print('\r' + line, flush=True, end='') msg = ( - "Process is running. Reported time steps (all sims): " + "Process is running. Reported time steps: " + str(counter) ) - print(msg) + log.info(msg) printCounter = 0 elif "find_dt" in line: @@ -69,8 +83,10 @@ def _runAndCheck(command): continue elif "h2" in line: continue + elif "write_data" in line: + continue else: - print(line) + log.info(line) # TODO move to utils @@ -96,7 +112,7 @@ def rewriteDEMtoZeroValues(demFile): demData["rasterData"][np.isnan(demData["rasterData"])] = 0.0 demData["header"]["nodata_value"] = 0.0 newFileName = demFile.parent / demFile.stem - rU.writeResultToRaster(demData["header"], demData["rasterData"], newFileName) + rU.writeResultToRaster(demData["header"], demData["rasterData"], newFileName, flip=True) @@ -104,8 +120,6 @@ def rewriteDEMtoZeroValues(demFile): def com8MoTPSAMain(cfgMain, cfgInfo=None): # Get all necessary information from the configuration files - # Load avalanche directory from general configuration file - avalancheDir = cfgMain["MAIN"]["avalancheDir"] # fetch type of cfgInfo typeCfgInfo = com1DFATools.checkCfgInfoType(cfgInfo) @@ -123,6 +137,102 @@ def com8MoTPSAMain(cfgMain, cfgInfo=None): log.info("Simulation: %s" % key) exportFlag = simDict[key]["cfgSim"]["EXPORTS"].getboolean("exportData") + # Preprocess the simulations, mainly creating the rcf files + rcfFiles = com8MoTPSAPreprocess(simDict, inputSimFiles, cfgMain, cfgInfo) + + # And now we run the simulations + startTime = time.time() + + log.info("--- STARTING (potential) PARALLEL PART ----") + + # Get number of CPU Cores wanted + nCPU = cfgUtils.getNumberOfProcesses(cfgMain, len(rcfFiles)) + + # Create parallel pool and run + # with multiprocessing.Pool(processes=nCPU) as pool: + with Pool(processes=nCPU) as pool: + results = pool.map(com8MoTPSATask, rcfFiles) + pool.close() + pool.join() + + timeNeeded = "%.2f" % (time.time() - startTime) + log.info("Overall (parallel) com8MoTPSA computation took: %s s " % timeNeeded) + log.info("--- ENDING (potential) PARALLEL PART ----") + + # Postprocess the simulations + com8MoTPSAPostprocess(simDict, cfgMain, inputSimFiles) + + +def com8MoTPSAPostprocess(simDict, cfgMain, inputSimFiles): + avalancheDir = cfgMain["MAIN"]["avalancheDir"] + # Copy max files to output directory + + outputDir = pathlib.Path(avalancheDir) / "Outputs" / "com8MoTPSA" + outputDirPeakFile = pathlib.Path(avalancheDir) / "Outputs" / "com8MoTPSA" / "peakFiles" + fU.makeADir(outputDirPeakFile) + + for key in simDict: + workDir = pathlib.Path(avalancheDir) / "Work" / "com8MoTPSA" / str(key) + + # Copy DataTime.txt + dataTimeFile = workDir / "DataTime.txt" + shutil.copy2(dataTimeFile, outputDir / (str(key) + "_DataTime.txt")) + + # TODO: functionize it + # Copy ppr files + pprFiles = list(workDir.glob("*p?_max*")) + targetFiles = [pathlib.Path(str(f.name).replace('null_psa_p1_max', 'lay1_psa_ppr')) for f in pprFiles] + targetFiles = [pathlib.Path(str(f).replace('null_psa_p2_max', 'lay2_psa_ppr')) for f in targetFiles] + targetFiles = [outputDirPeakFile / f for f in targetFiles] + for source, target in zip(pprFiles, targetFiles): + shutil.copy2(source, target) + + # Copy pfd files + pfdFiles = list(workDir.glob("*h?_max*")) + targetFiles = [pathlib.Path(str(f.name).replace('null_psa_h1_max', 'lay1_psa_pfd')) for f in pfdFiles] + targetFiles = [pathlib.Path(str(f).replace('null_psa_h2_max', 'lay2_psa_pfd')) for f in targetFiles] + targetFiles = [outputDirPeakFile / f for f in targetFiles] + for source, target in zip(pfdFiles, targetFiles): + shutil.copy2(source, target) + + # Copy pfv files + pfvFiles = list(workDir.glob("*s?_max*")) + targetFiles = [pathlib.Path(str(f.name).replace('null_psa_s1_max', 'lay1_psa_pfv')) for f in pfvFiles] + targetFiles = [pathlib.Path(str(f).replace('null_psa_s2_max', 'lay2_psa_pfv')) for f in targetFiles] + targetFiles = [outputDirPeakFile / f for f in targetFiles] + for source, target in zip(pfvFiles, targetFiles): + shutil.copy2(source, target) + + # create plots and report + modName = __name__.split('.')[-1] + reportDir = pathlib.Path(avalancheDir, "Outputs", modName, "reports") + fU.makeADir(reportDir) + print(inputSimFiles["demFile"]) + + dem = rU.readRaster(inputSimFiles["demFile"]) + # Generate plots for all peakFiles + plotDict = oP.plotAllPeakFields(avalancheDir, cfgMain["FLAGS"], modName, demData=dem) + + +def com8MoTPSATask(rcfFile): + # TODO: Obvious... + os.chdir(os.path.dirname(os.path.abspath(__file__))) + command = ['./MoT-PSA', rcfFile] + # command = ['/home/felix/Versioning/AvaFrame/avaframe/com8MoTPSA/MoT-PSA', rcfFile] + log.info("Run simulation: %s" % rcfFile) + _runAndCheck(command) + return command + + +def com8MoTPSAPreprocess(simDict, inputSimFiles, cfgMain, cfgInfo): + # Load avalanche directory from general configuration file + avalancheDir = cfgMain["MAIN"]["avalancheDir"] + + workDir = pathlib.Path(avalancheDir) / "Work" / "com8MoTPSA" + cfgFileDir = pathlib.Path(avalancheDir) / "Outputs" / "com8MoTPSA" / "configurationFiles" + fU.makeADir(cfgFileDir) + rcfFiles = list() + for key in simDict: # Generate command and run via subprocess.run # Configuration that needs adjustment @@ -135,12 +245,10 @@ def com8MoTPSAMain(cfgMain, cfgInfo=None): # fetch simHash for current sim simHash = simDict[key]["simHash"] - # append configuration to dataframe - simDF = cfgUtils.appendCgf2DF(simHash, key, cfg, simDF) + # # append configuration to dataframe + # simDF = cfgUtils.appendCgf2DF(simHash, key, cfg, simDF) # convert release shape to raster with values for current sim - relFile = simDict[key]["relFile"] - # select release area input data according to chosen release scenario inputSimFiles = gI.selectReleaseFile(inputSimFiles, cfg["INPUT"]["releaseScenario"]) # create required input from input files @@ -160,43 +268,61 @@ def com8MoTPSAMain(cfgMain, cfgInfo=None): # TODO: split releaseheight -> question NGI dem = rU.readRaster(inputSimFiles["demFile"]) dem["originalHeader"] = dem["header"].copy() - releaseLine = geoTrans.prepareArea(releaseLine, dem, np.sqrt(2), combine=True, checkOverlap=False) - releaseL1 = inputSimFiles["demFile"].parent / "releaseLine1" - releaseL2 = inputSimFiles["demFile"].parent / "releaseLine2" - bedDepth = inputSimFiles["demFile"].parent / "dummyBedDepth" - bedDepo = inputSimFiles["demFile"].parent / "dummyBedDepo" - bedShear = inputSimFiles["demFile"].parent / "dummyBedShear" - rU.writeResultToRaster(dem["header"], releaseLine["rasterData"], releaseL1) - rU.writeResultToRaster(dem["header"], releaseLine["rasterData"], releaseL2) - emptyRaster = np.full_like(releaseLine["rasterData"], 0) - rU.writeResultToRaster(dem["header"], emptyRaster, bedDepth) - rU.writeResultToRaster(dem["header"], emptyRaster, bedDepo) - rU.writeResultToRaster(dem["header"], emptyRaster, bedShear) + #releaseLine = geoTrans.prepareArea(releaseLine, dem, np.sqrt(2), combine=True, checkOverlap=False) + if len(inputSimLines['relThField']) == 0: + # if no release thickness field or function - set release according to shapefile or ini file + # this is a list of release rasters that we want to combine + releaseLine = geoTrans.prepareArea( + releaseLine, dem, np.sqrt(2), thList=releaseLine["thickness"], combine=True, checkOverlap=False + ) + releaseField = releaseLine['rasterData'] + else: + # if relTh provided - set release thickness with field or function + releaseLine = geoTrans.prepareArea(releaseLine, dem, np.sqrt(2), combine=True, checkOverlap=False) + relRasterPoly = releaseLine['rasterData'].copy() + releaseRelThCombined = np.where(relRasterPoly>0, inputSimLines['relThField'], 0) + releaseField = releaseRelThCombined + + # Generate the work and data dirs for the current simHash + + cuWorkDir = workDir / key + workInputDir = cuWorkDir / "Input" + workOutputDir = cuWorkDir / key + + fU.makeADir(cuWorkDir) + fU.makeADir(workInputDir) + + zeroRaster = np.full_like(releaseLine["rasterData"], 0) + + releaseL1 = workInputDir / "releaseLayer1" + releaseL2 = workInputDir / "releaseLayer2" + bedDepth = workInputDir / "dummyBedDepth" + bedDepo = workInputDir / "dummyBedDepo" + bedShear = workInputDir / "dummyBedShear" + rU.writeResultToRaster(dem["header"], releaseField, releaseL1, flip=True) + rU.writeResultToRaster(dem["header"], zeroRaster, releaseL2, flip=True) + rU.writeResultToRaster(dem["header"], zeroRaster, bedDepth) + rU.writeResultToRaster(dem["header"], zeroRaster, bedDepo) + rU.writeResultToRaster(dem["header"], zeroRaster, bedShear) # set configuration for MoT-PSA cfgInfo["Run information"]["Area of Interest"] = cfgMain["MAIN"]["avalancheDir"] cfgInfo["Run information"]["UTM zone"] = "32N" cfgInfo["Run information"]["EPSG geodetic datum code"] = "31287" cfgInfo["Run information"]["Run name"] = cfgMain["MAIN"]["avalancheDir"] - cfgInfo["File names"]["Grid filename"] = "./" + str(inputSimFiles["demFile"]) - cfgInfo["File names"]["Release depth 1 filename"] = "./" + str(releaseL1) + ".asc" - cfgInfo["File names"]["Release depth 2 filename"] = "./" + str(releaseL2) + ".asc" - cfgInfo["File names"]["Bed depth filename"] = "./" + str(bedDepth) + ".asc" - cfgInfo["File names"]["Bed deposition filename"] = "./" + str(bedDepo) + ".asc" - cfgInfo["File names"]["Bed shear strength filename"] = "./" + str(bedShear) + ".asc" - cfgInfo["File names"]["Output filename root"] = "./avaTest/Work/" + str(simHash) + "/" + str( - simHash) - - # select release area input data according to chosen release scenario - # inputSimFiles = gI.selectReleaseFile(inputSimFiles, cfg["INPUT"]["releaseScenario"]) - - # create required input from input files - # demOri, inputSimLines = prepareInputData(inputSimFiles, cfg) + cfgInfo["File names"]["Grid filename"] = str(inputSimFiles["demFile"]) + cfgInfo["File names"]["Release depth 1 filename"] = str(releaseL1) + ".asc" + cfgInfo["File names"]["Release depth 2 filename"] = str(releaseL2) + ".asc" + cfgInfo["File names"]["Bed depth filename"] = str(bedDepth) + ".asc" + cfgInfo["File names"]["Bed deposition filename"] = str(bedDepo) + ".asc" + cfgInfo["File names"]["Bed shear strength filename"] = str(bedShear) + ".asc" + cfgInfo["File names"]["Output filename root"] = str(workOutputDir) - rcfFile = key + ".rcf" + rcfFileName = cfgFileDir / (str(key) + ".rcf") - cfgToRcf(cfgInfo, rcfFile) + cfgUtils.writeCfgFile(avalancheDir, com8MoTPSA, cfgInfo, str(key)) - command = ['./MoT-PSA', rcfFile] - _runAndCheck(command) + cfgToRcf(cfgInfo, rcfFileName) + rcfFiles.append(rcfFileName) + return rcfFiles diff --git a/avaframe/com8MoTPSA/com8MoTPSACfg.ini b/avaframe/com8MoTPSA/com8MoTPSACfg.ini index 2e7950bfd..7fd80392f 100644 --- a/avaframe/com8MoTPSA/com8MoTPSACfg.ini +++ b/avaframe/com8MoTPSA/com8MoTPSACfg.ini @@ -6,7 +6,7 @@ modelType = psa # list of simulations that shall be performed (null, ent, res, entres, available (use all available input data)) -simTypeList = available +simTypeList = null #+++++Release thickness++++ @@ -148,7 +148,7 @@ Constant density L1 = yes Evolving geometry = no [Numerical parameters] -Simulation time (s) = 100.0 +Simulation time (s) = 1000.0 Minimum time step (s) = 0.001 Maximum time step (s) = 0.1 Output interval (s) = 1.0 diff --git a/avaframe/out3Plot/plotUtils.py b/avaframe/out3Plot/plotUtils.py index 740149d50..7673ba8e3 100644 --- a/avaframe/out3Plot/plotUtils.py +++ b/avaframe/out3Plot/plotUtils.py @@ -235,6 +235,7 @@ "ppr": cmapPres, "pfv": cmapSpeed, "pft": cmapThickness, + "pfd": cmapThickness, "P": cmapPres, "FV": cmapSpeed, "FM": cmapThickness, diff --git a/avaframe/out3Plot/plotUtilsCfg.ini b/avaframe/out3Plot/plotUtilsCfg.ini index ad0585e15..6cc8cd51b 100644 --- a/avaframe/out3Plot/plotUtilsCfg.ini +++ b/avaframe/out3Plot/plotUtilsCfg.ini @@ -66,6 +66,7 @@ gravityAcc = 9.81 # units for output variables unitppr = kPa unitpft = m +unitpfd = m unitpfv = $ms^{-1}$ unitpta = ° unitpke = $kJm^{-2}$ @@ -86,6 +87,7 @@ unitdmDet = kg # threshold levels elevMaxppr = 100 elevMaxpft = 1 +elevMaxpfd = 1 elevMaxpfv = 10 elevMaxpta = 40 elevMaxP = 100 @@ -109,6 +111,7 @@ probaColorLevels = 0|0.25|0.50|0.75|1. # contour levels (when adding contour lines on a plot) contourLevelsppr = 1|3|5|10|25|50|100|250 contourLevelspft = 0.1|0.25|0.5|0.75|1 +contourLevelspfd = 0.1|0.25|0.5|0.75|1 contourLevelspfv = 0.5|1|5|10|25|50 contourLevelspta = 10|15|20|25|30|35 contourLevelsP = 1|3|5|10|25|50|100|250 @@ -120,6 +123,7 @@ contourLevelsFTDet = -0.1|-0.25|-0.5|-0.75|-1 # name for result parameters nameppr = peak pressure namepft = peak flow thickness +namepfd = peak flow depth namepfv = peak flow velocity namepta = peak travel angle namepke = peak kinetic energy From f28b716fbd1871c6dab744a411b8a8454a2994bf Mon Sep 17 00:00:00 2001 From: RolandFischbacher Date: Tue, 29 Apr 2025 13:29:21 +0200 Subject: [PATCH 05/12] Add morris sampling methode and change regex to handle white space and special characters runAna4ProbAna.py with com8MoTPSA module (runAna4ProbAnaCom8MoTPSA.py), add sections to com8MoTPSA.ini and probAnaCfg.ini, and introduce modname Pass module instead of modName and add hepler functions to simplify functions: `checkParameterInConfig` and `fetchParameterSection` Change handling of friction index to volume index for com8MoTPSA Add `renameDuplicates` to handle duplicate column names in DataFrame and Simplify configuration-appending by generalizing section (add all sections) Change naming of outputlayer (dfa/psa), revert merging to original version (merge on simName) refactor(core): improve code readability and fix minor issues - Remove unused parameters in `probAna.py` and update method names for clarity. - Fix a conditional statement in `com1DFA.py` for better compatibility. - Add a missing newline and correct whitespace formatting in `aimecTools.py`. - Maintain consistent casing in `plotUtilsCfg.ini`. refactor(com8MoTPSA): improve code formatting and readability - --- avaframe/ana4Stats/probAna.py | 141 +++++++++++++-- avaframe/ana4Stats/probAnaCfg.ini | 10 +- avaframe/com1DFA/com1DFA.py | 120 ++++++++----- avaframe/com1DFA/com1DFATools.py | 32 ++-- avaframe/com1DFA/deriveParameterSet.py | 11 +- avaframe/com8MoTPSA/com8MoTPSA.py | 164 ++++++++++++------ avaframe/com8MoTPSA/com8MoTPSACfg.ini | 58 ++++++- .../com8MoTPSA/runAna4ProbAnaCom8MoTPSA.py | 133 ++++++++++++++ avaframe/in3Utils/cfgUtils.py | 55 ++++-- avaframe/runAna4ProbAna.py | 2 +- avaframe/runScripts/runAna3AIMEC.py | 2 + avaframe/tests/test_probAna.py | 2 +- 12 files changed, 587 insertions(+), 143 deletions(-) create mode 100644 avaframe/com8MoTPSA/runAna4ProbAnaCom8MoTPSA.py diff --git a/avaframe/ana4Stats/probAna.py b/avaframe/ana4Stats/probAna.py index f69944fd2..e1b3e5287 100644 --- a/avaframe/ana4Stats/probAna.py +++ b/avaframe/ana4Stats/probAna.py @@ -9,6 +9,9 @@ import logging import pathlib from scipy.stats import qmc +from SALib.sample import morris +import pickle +from deepdiff import grep import avaframe.out3Plot.plotUtils as pU from avaframe.in3Utils import cfgUtils @@ -116,8 +119,6 @@ def cfgFilesLocalApproach(variationsDict, cfgProb, modName, outDir): dictionary with for each varName, varVariation, varSteps, and type of variation cfgProb: configParser object configuration settings - avaDir: pathlib path - path to avalanche directory modName: module computational module @@ -271,14 +272,19 @@ def checkParameterSettings(cfg, varParList): """ - # set a list of all thickness parameters that are set to be read from shp file + #set a list of all thickness parameters that are set to be read from shp file thReadFromShp = [] - # loop over all parameters and check if no variation is set and if read from shp for varPar in varParList: - if any(chars in cfg['GENERAL'][varPar] for chars in ['|', '$', ':']): + # Check if valid parameter exists in any section and check for duplicates + _ = checkIfParameterInConfig(cfg, varPar) + + # Fetch section where parameter was found + section = fetchParameterSection(cfg, varPar) + + if any(chars in cfg[section][varPar] for chars in ['|', '$', ':']): message = ('Only one reference value is allowed for %s: but %s is given' % - (varPar, cfg['GENERAL'][varPar])) + (varPar, cfg[section][varPar])) log.error(message) raise AssertionError(message) elif varPar in ['entTh', 'relTh', 'secondaryRelTh']: @@ -287,11 +293,78 @@ def checkParameterSettings(cfg, varParList): _ = checkForNumberOfReferenceValues(cfg['GENERAL'], varPar) # check if th read from shp file if cfg['GENERAL'].getboolean(thFromShp): - thReadFromShp.append(varPar) + thReadFromShp.append(varPar) return True, thReadFromShp +def checkIfParameterInConfig(cfg, varPar): + """ + Checks the existence and uniqueness of a parameter within a configuration object. + + This function searches for a specified parameter within a configuration object, ensuring the + parameter exists and is not duplicated across sections. If the parameter is found multiple + times or not found at all, an error is logged. If the parameter is found in only one section, + that section is returned. + + Parameters + ---------- + cfg : configparser object + The configuration object in which to search for the specified parameter. + varPar : str + The name of the parameter to locate within the configuration. + + Returns + ------- + True if the parameter is found and unique. + """ + + # search for parameter in cfg object + matches = cfg | grep(varPar, verbose_level=2) + + # Check if matches is empty + if not matches.get('matched_paths'): + message = "'%s' is not a valid parameter" % varPar + log.error(message) + raise AssertionError(message) + + # Exact key match (case-insensitive) + exactKeyMatch = { + path: val for path, val in matches['matched_paths'].items() + if path.lower().endswith(f"['{varPar.lower()}']") + } + + # Check for duplicates + if len(exactKeyMatch) != 1: + message = "Parameter '%s' does not uniquely match a single configuration parameter." % varPar + log.error(message) + raise AssertionError(message) + + return True + + +def fetchParameterSection(cfg, parameter): + """Fetch the section name that contains the specified parameter in a configuration file. + + Parameters + ---------- + cfg : configparser object + Configuration settings + parameter : str + Name of the parameter to find + + Returns + ------- + str + Name of the section containing the parameter or None if the parameter is not found. + """ + + for section in cfg.sections(): + if parameter in cfg[section]: + return section + + return None + def checkForNumberOfReferenceValues(cfgGen, varPar): """ check if in reference configuration no variation option of varPar is set if set - throw error @@ -507,8 +580,6 @@ def fetchThicknessInfo(avaDir): Parameters ------------ - cfg: configparser object - configuration settings avaDir: pathlib path or str path to avalanche directory @@ -565,7 +636,7 @@ def createSampleFromConfig(avaDir, cfgProb, comMod): _, thReadFromShp = checkParameterSettings(cfgStart, varParList) modNameString = str(pathlib.Path(comMod.__file__).stem) - if modNameString.lower() == "com1dfa": + if modNameString.lower() in ["com1dfa", "com8motpsa"]: # check if thickness parameters are actually read from shp file _, thReadFromShp = checkParameterSettings(cfgStart, varParList) else: @@ -580,6 +651,12 @@ def createSampleFromConfig(avaDir, cfgProb, comMod): varType) paramValuesDList = [paramValuesD] + # save dictionary to pickle file + outDir = pathlib.Path(avaDir, 'Outputs', 'ana4Stats') + fU.makeADir(outDir) + with open(outDir / 'paramValuesD.pickle', 'wb') as fi: + pickle.dump(paramValuesDList[0], fi) + return paramValuesDList @@ -615,8 +692,8 @@ def createSampleWithVariationStandardParameters(cfgProb, cfgStart, varParList, v lowerBounds = [] upperBounds = [] for idx, varPar in enumerate(varParList): - # if parameter value directly set in configuration modify the value directly - varVal = cfgStart['GENERAL'].getfloat(varPar) + section = fetchParameterSection(cfgStart, varPar) + varVal = cfgStart[section].getfloat(varPar) if varType[idx].lower() == 'percent': lB = varVal - varVal * (float(valVariationValue[idx]) / 100.) uB = varVal + varVal * (float(valVariationValue[idx]) / 100.) @@ -708,7 +785,9 @@ def createSampleWithVariationForThParameters(avaDir, cfgProb, cfgStart, varParLi # initialize lower and upper bounds required to get a sample for the parameter values # numpy arrays required to do masking as lists don't work for a list indices - varValList = np.asarray([cfgStart['GENERAL'].getfloat(varPar) if varPar in staParameter else thValues[idx] for idx, varPar in enumerate(fullListOfParameters)]) + varValList = np.asarray([cfgStart[fetchParameterSection(cfgStart, varPar)].getfloat(varPar) + if varPar in staParameter + else thValues[idx] for idx, varPar in enumerate(fullListOfParameters)]) fullValVar = np.asarray([float(valVariationValue[i]) if valVariationValue[i] != 'ci95' else np.nan for i in parentParameterId]) fullVarType = np.asarray([varType[i].lower() for i in parentParameterId]) lowerBounds = np.asarray([None]*len(fullListOfParameters)) @@ -730,13 +809,18 @@ def createSampleWithVariationForThParameters(avaDir, cfgProb, cfgStart, varParLi upperBounds[fullVarType == 'rangefromci'] = (varValList[fullVarType == 'rangefromci'] + ciValues[fullVarType == 'rangefromci']) - # create a sample of parameter values using scipy latin hypercube sampling + # create a sample of parameter values using scipy latin hypercube or morris sampling sample = createSample(cfgProb, varParList) # create a full sample including those thickness values for the potentially multiple features # however, the thickness values for one parameter (relTh or entTh or secondaryRelTh) should not # be independent for the different features within one parameter - fullSample = np.zeros((int(cfgProb['PROBRUN']['nSample']), len(fullListOfParameters))) + if cfgProb['PROBRUN']['sampleMethod'] == 'morris': + fullSample = np.zeros( + (int(cfgProb['PROBRUN']['nSample']) * (len(varParList) + 1), len(fullListOfParameters))) + else: + fullSample = np.zeros((int(cfgProb['PROBRUN']['nSample']), len(fullListOfParameters))) + for idx, varPar in enumerate(fullListOfParameters): lB = [0]*len(varParList) uB = [1]*len(varParList) @@ -777,10 +861,26 @@ def createSample(cfgProb, varParList): """ # random generator initialized with seed - randomGen = np.random.default_rng(cfgProb['PROBRUN'].getint('sampleSeed')) + sampleSeed = cfgProb['PROBRUN'].getint('sampleSeed') + randomGen = np.random.default_rng(sampleSeed) + nTrajectories = cfgProb['PROBRUN'].getint('nSample') + + # create a sample of parameter values using salib morris sampling + if cfgProb['PROBRUN']['sampleMethod'].lower() == 'morris': + param_ranges = { + 'num_vars': len(varParList), + 'names': varParList, + 'bounds': [[0, 1]] * len(varParList) + } + sample = morris.sample( + param_ranges, + N=nTrajectories, # number of trajectories + num_levels=6, # how many discrete values per parameter + seed=sampleSeed + ) # create a sample of parameter values using scipy latin hypercube sampling - if cfgProb['PROBRUN']['sampleMethod'].lower() == 'latin': + elif cfgProb['PROBRUN']['sampleMethod'].lower() == 'latin': sampler = qmc.LatinHypercube(d=len(varParList), seed=randomGen) sample = sampler.random(n=int(cfgProb['PROBRUN']['nSample'])) log.info('Parameter sample created using latin hypercube sampling') @@ -869,7 +969,12 @@ def createCfgFiles(paramValuesDList, comMod, cfg, cfgPath=''): cfgStart = fetchStartCfg(comMod, cfg) for count1, pVal in enumerate(paramValuesD['values']): for index, par in enumerate(paramValuesD['names']): - cfgStart['GENERAL'][par] = str(pVal[index]) + section = fetchParameterSection(cfgStart, par) + # If parameter not found in any section, add it to 'GENERAL'. + if section is not None: + cfgStart[section][par] = str(pVal[index]) + else: + cfgStart['GENERAL'][par] = str(pVal[index]) if modName.lower() == 'com1dfa': cfgStart['VISUALISATION']['scenario'] = str(count1) cfgStart['INPUT']['thFromIni'] = paramValuesD['thFromIni'] diff --git a/avaframe/ana4Stats/probAnaCfg.ini b/avaframe/ana4Stats/probAnaCfg.ini index adb255be1..9f2d67f25 100644 --- a/avaframe/ana4Stats/probAnaCfg.ini +++ b/avaframe/ana4Stats/probAnaCfg.ini @@ -38,9 +38,9 @@ samplingStrategy = 1 # #++++++VARIATION INFO FOR DRAW SAMPLES FROM FULL SET OF VARIATIONS # type of parameters that shall be varied -separated by | (options: float) varParType = float|float -# factor used to create the number of samples +# factor used to create the number of samples, if morris number of samples depends on number of varied variables and number of trajectories, for now use nSample as number of trajectories nSample = 40 -# sample method used to create sample (options: latin) +# sample method used to create sample (options: latin, morris) sampleMethod = latin # seed for random generator sampleSeed = 12345 @@ -74,6 +74,12 @@ frictModel = samosAT defaultConfig = True +[com8MoTPSA_com8MoTPSA_override] +# use default com1DFA config as base configuration (True) and override following parameters +# if False and local is available use local +defaultConfig = True + + [in1Data_computeFromDistribution_override] # use default config as base configuration (True) and override following parameters # if False and local is available use local diff --git a/avaframe/com1DFA/com1DFA.py b/avaframe/com1DFA/com1DFA.py index 46b97c007..1dd11d687 100644 --- a/avaframe/com1DFA/com1DFA.py +++ b/avaframe/com1DFA/com1DFA.py @@ -63,7 +63,7 @@ debugPlot = cfgAVA["FLAGS"].getboolean("debugPlot") -def com1DFAPreprocess(cfgMain, typeCfgInfo, cfgInfo): +def com1DFAPreprocess(cfgMain, typeCfgInfo, cfgInfo, module=com1DFA): """preprocess information from configuration, read input data and gather into inputSimFiles, create one config object for each of all desired simulations, create dataFrame with one line per simulations of already existing sims in avalancheDir @@ -78,6 +78,8 @@ def com1DFAPreprocess(cfgMain, typeCfgInfo, cfgInfo): path to configuration file if overwrite is desired - optional if not local (if available) or default configuration will be loaded if cfgInfo is a configparser object take this as initial config + module: module + module to be used for task (optional) Returns -------- @@ -93,17 +95,17 @@ def com1DFAPreprocess(cfgMain, typeCfgInfo, cfgInfo): # read initial configuration if typeCfgInfo in ["cfgFromFile", "cfgFromDefault"]: - cfgStart = cfgUtils.getModuleConfig(com1DFA, fileOverride=cfgInfo, toPrint=False) + cfgStart = cfgUtils.getModuleConfig(module, fileOverride=cfgInfo, toPrint=False) elif typeCfgInfo == "cfgFromObject": cfgStart = cfgInfo # fetch input data and create work and output directories inputSimFilesAll, outDir, simDFExisting, simNameExisting = com1DFATools.initializeInputs( - avalancheDir, cfgStart["GENERAL"].getboolean("cleanRemeshedRasters") + avalancheDir, cfgStart["GENERAL"].getboolean("cleanRemeshedRasters"), module ) # create dictionary with one key for each simulation that shall be performed - simDict = dP.createSimDict(avalancheDir, com1DFA, cfgStart, inputSimFilesAll, simNameExisting) + simDict = dP.createSimDict(avalancheDir, module, cfgStart, inputSimFilesAll, simNameExisting) return simDict, outDir, inputSimFilesAll, simDFExisting @@ -1151,8 +1153,6 @@ def initializeSimulation(cfg, outDir, demOri, inputSimLines, logName): ) fields["cResRaster"] = cResRaster fields["detRaster"] = detRaster - fields["cResRasterOrig"] = cResRaster - fields["detRasterOrig"] = detRaster for fric in ["mu", "xi"]: if (inputSimLines[fric + "File"] == None) or ( @@ -1332,7 +1332,6 @@ def initializeParticles(cfg, releaseLine, dem, inputSimLines="", logName="", rel particles["massPerPart"] = massPerPart particles["mTot"] = np.sum(particles["m"]) - particles["tPlot"] = 0 particles["h"] = hPartArray particles["ux"] = np.zeros(np.shape(hPartArray)) particles["uy"] = np.zeros(np.shape(hPartArray)) @@ -2843,11 +2842,12 @@ def prepareVarSimDict(standardCfg, inputSimFiles, variationDict, simNameExisting # check if DEM in Inputs has desired mesh size pathToDem = dP.checkRasterMeshSize(cfgSim, inputSimFiles["demFile"], "DEM") cfgSim["INPUT"]["DEM"] = pathToDem - if ( - cfgSim["GENERAL"]["relThFromFile"] == "True" - or cfgSim["GENERAL"]["frictModel"].lower() == "spatialvoellmy" - ): - dem = IOf.readRaster(pathlib.Path(cfgSim["GENERAL"]["avalancheDir"], "Inputs", pathToDem)) + if modName == 'com1DFA': + if cfgSim["GENERAL"]["relThFromFile"] == "True" or cfgSim["GENERAL"]["frictModel"].lower() == "spatialvoellmy": + dem = IOf.readRaster(pathlib.Path(cfgSim['GENERAL']['avalancheDir'], 'Inputs', pathToDem)) + elif modName == 'com8MoTPSA': + if cfgSim["GENERAL"]["relThFromFile"] == "True": + dem = IOf.readRaster(pathlib.Path(cfgSim['GENERAL']['avalancheDir'], 'Inputs', pathToDem)) # check if RELTH in Inputs has desired mesh size if cfgSim["GENERAL"]["relThFromFile"] == "True": @@ -2856,12 +2856,16 @@ def prepareVarSimDict(standardCfg, inputSimFiles, variationDict, simNameExisting else: cfgSim["INPUT"]["relThFile"] = "" - # check if spatialVoellmy is chosen that friction fields have correct extent - if cfgSim["GENERAL"]["frictModel"].lower() == "spatialvoellmy": - for fric in ["mu", "xi"]: - pathToFric = dP.checkExtentAndCellSize(cfgSim, inputSimFiles["%sFile" % fric], dem, fric) - cfgSim["INPUT"]["%sFile" % fric] = pathToFric + if modName == 'com1DFA': + # check if spatialVoellmy is chosen that friction fields have correct extent + if cfgSim["GENERAL"]["frictModel"].lower() == "spatialvoellmy": + for fric in ['mu', 'xi']: + pathToFric = dP.checkExtentAndCellSize(cfgSim, inputSimFiles['%sFile' % fric], dem, fric) + cfgSim['INPUT']['%sFile' % fric] = pathToFric + # add info about dam file path to the cfg + if cfgSim['GENERAL']['dam'] == 'True' and inputSimFiles['damFile'] is not None: + cfgSim['INPUT']['DAM'] = str(pathlib.Path('DAM', inputSimFiles['damFile'].name)) # add info about dam file path to the cfg if cfgSim["GENERAL"]["dam"] == "True" and inputSimFiles["damFile"] is not None: cfgSim["INPUT"]["DAM"] = str(pathlib.Path("DAM", inputSimFiles["damFile"].name)) @@ -2878,23 +2882,35 @@ def prepareVarSimDict(standardCfg, inputSimFiles, variationDict, simNameExisting cfgSim = dP.appendShpThickness(cfgSim) # check differences to default and add indicator to name - defID, _ = com1DFATools.compareSimCfgToDefaultCfgCom1DFA(cfgSim) + defID, _ = com1DFATools.compareSimCfgToDefaultCfgCom1DFA(cfgSim, module) - # if frictModel is samosATAuto compute release vol - if cfgSim["GENERAL"]["frictModel"].lower() == "samosatauto": - pathToDemFull = pathlib.Path(cfgSim["GENERAL"]["avalancheDir"], "Inputs", pathToDem) - relVolume = fetchRelVolume(rel, cfgSim, pathToDemFull, inputSimFiles["secondaryReleaseFile"]) - else: - relVolume = "" + # predefine different size classification indices + frictIndi = None + volIndi = None + + pathToDemFull = pathlib.Path(cfgSim["GENERAL"]["avalancheDir"], "Inputs", pathToDem) + + if modName == 'com1DFA': + # if frictModel is samosATAuto compute release vol + if cfgSim["GENERAL"]["frictModel"].lower() == "samosatauto": + relVolume = fetchRelVolume(rel, cfgSim, pathToDemFull, inputSimFiles["secondaryReleaseFile"]) + else: + relVolume = "" + + # check sphKernelRadius setting + cfgSim = checkCfg.checkCellSizeKernelRadius(cfgSim) - # check sphKernelRadius setting - cfgSim = checkCfg.checkCellSizeKernelRadius(cfgSim) + # only keep friction model parameters that are used + cfgSim = checkCfg.checkCfgFrictionModel(cfgSim, inputSimFiles, relVolume=relVolume) - # only keep friction model parameters that are used - cfgSim = checkCfg.checkCfgFrictionModel(cfgSim, inputSimFiles, relVolume=relVolume) + # set frictModelIndicator, this needs to happen AFTER checkCfgFrictModel + frictIndi = com1DFATools.setFrictTypeIndicator(cfgSim) - # set frictModelIndicator, this needs to happen AFTER checkCfgFrictModel - frictIndi = com1DFATools.setFrictTypeIndicator(cfgSim) + elif modName == 'com8MoTPSA': + relVolume = fetchRelVolume(rel, cfgSim, pathToDemFull, inputSimFiles["secondaryReleaseFile"]) + + # set Volume class identificator + volIndi = setVolumeIndicator(cfgSim, relVolume) # convert back to configParser object cfgSimObject = cfgUtils.convertDictToConfigParser(cfgSim) @@ -2908,7 +2924,7 @@ def prepareVarSimDict(standardCfg, inputSimFiles, variationDict, simNameExisting relNameSim, simHash, defID, - frictIndi, + frictIndi or volIndi, row._asdict()["simTypeList"], cfgSim["GENERAL"]["modelType"], ], @@ -2924,19 +2940,17 @@ def prepareVarSimDict(standardCfg, inputSimFiles, variationDict, simNameExisting "relFile": rel, "cfgSim": cfgSimObject, } - # write configuration file - cfgUtils.writeCfgFile( - cfgSimObject["GENERAL"]["avalancheDir"], - com1DFA, - cfgSimObject, - fileName=simName, - ) + if modName == 'com1DFA': + # write configuration file, dont need to write cfg file for com8MoTPSA (does this later when creating rcf file) + cfgUtils.writeCfgFile( + cfgSimObject["GENERAL"]["avalancheDir"], com1DFA, cfgSimObject, fileName=simName + ) else: log.warning("Simulation %s already exists, not repeating it" % simName) log.info("Done preparing variations -----") # TODO: maybe treat this in some other way, i.e. adding an "finalDEM" or similar - # inputSimFiles.pop("demFile") + inputSimFiles.pop("demFile") inputSimFiles["demFile"] = pathToDemFull return simDict @@ -3093,6 +3107,8 @@ def fetchRelVolume(releaseFile, cfg, pathToDem, secondaryReleaseFile, radius=0.0 demVol = geoTrans.getNormalMesh(demVol, num=methodMeshNormal) demVol = DFAtls.getAreaMesh(demVol, methodMeshNormal) + + # compute volume of release area relVolume = initializeRelVol(cfg, demVol, releaseFile, radius, releaseType="primary") @@ -3194,6 +3210,32 @@ def initializeRelVol(cfg, demVol, releaseFile, radius, releaseType="primary"): return relVolume +def setVolumeIndicator(simCfg, relVolume): + """Sets the Volume indicator for the simname based on the threshold defined in ini file + + Parameters + ----------- + simCfg: dict + simulation configuration + relVolume: float + Volume of the release area in m^3 + + Returns + -------- + VolIndi: str + S, M or L + + """ + if relVolume < float(simCfg["GENERAL"]["volClassSmall"]): + volIndi = "S" + elif relVolume > float(simCfg["GENERAL"]["volClassMedium"]): + volIndi = "L" + else: + volIndi = "M" + + return volIndi + + def saveContToPickle(contourDictXY, outDir, cuSimName): """save contourline x, y coordinates dictionary to a pickle diff --git a/avaframe/com1DFA/com1DFATools.py b/avaframe/com1DFA/com1DFATools.py index 46c570ee6..125deac61 100644 --- a/avaframe/com1DFA/com1DFATools.py +++ b/avaframe/com1DFA/com1DFATools.py @@ -98,7 +98,7 @@ def setFrictTypeIndicator(simCfg): return frictTypeIdentifier -def compareSimCfgToDefaultCfgCom1DFA(simCfg): +def compareSimCfgToDefaultCfgCom1DFA(simCfg, module=com1DFA): """Compares the given simulation configuration (as dict) to the default com1DFA configuration. Disregards values like avalancheDir that are expected to change. Returns True if it is the default + an identifier string: D = Default and @@ -108,6 +108,8 @@ def compareSimCfgToDefaultCfgCom1DFA(simCfg): ----------- simCfg: dict simulation configuration + module: module + module to be used for task (optional) Returns -------- @@ -116,6 +118,9 @@ def compareSimCfgToDefaultCfgCom1DFA(simCfg): """ + # extract the name of the module + modName = module.__name__.split(".")[-1] + log.info("Comparing simCfg to default cfg") defaultIdentifierString = "D" @@ -152,9 +157,10 @@ def compareSimCfgToDefaultCfgCom1DFA(simCfg): # sphKernelSize is set during runtime, make sure it is not reported # as changed if default is set to meshCellSize - if defCfg["GENERAL"]["sphKernelRadius"] == "meshCellSize": - if simCfg["GENERAL"]["sphKernelRadius"] == simCfg["GENERAL"]["meshCellSize"]: - excludeItems.append("root['GENERAL']['sphKernelRadius']") + if modName == "com1DFA": + if defCfg["GENERAL"]["sphKernelRadius"] == "meshCellSize": + if simCfg["GENERAL"]["sphKernelRadius"] == simCfg["GENERAL"]["meshCellSize"]: + excludeItems.append("root['GENERAL']['sphKernelRadius']") # do the diff and analyse # this is the deepdiff > 8.0 version @@ -243,7 +249,7 @@ def _cleanDiffKey(keyString): return keyStr -def createSimDictFromCfgs(cfgMain, cfgPath): +def createSimDictFromCfgs(cfgMain, cfgPath, module=com1DFA): """From multiple cfg files create a simDict with one item for each simulation to perform within these cfg files still parameter variations are allowed @@ -253,6 +259,8 @@ def createSimDictFromCfgs(cfgMain, cfgPath): main configuration of AvaFrame cfgPath: pathlib Path or str path to directory with cfg files + module: module + module to be used for task (optional) Returns -------- @@ -266,7 +274,7 @@ def createSimDictFromCfgs(cfgMain, cfgPath): # fetch input data and create work and output directories # TODO: so for now remeshed dir is cleaned before a run - inputSimFilesAll, outDir, simDFExisting, simNameExisting = initializeInputs(avalancheDir, True) + inputSimFilesAll, outDir, simDFExisting, simNameExisting = initializeInputs(avalancheDir, True, module) # save dem file path as it is deleted from input sim files dict once it is set in the config demFile = inputSimFilesAll["demFile"] @@ -287,12 +295,12 @@ def createSimDictFromCfgs(cfgMain, cfgPath): # loop over all cfgFiles and create simDict for index, cfgFile in enumerate(cfgFilesAll): # read configuration - cfgFromFile = cfgUtils.getModuleConfig(com1DFA, fileOverride=cfgFile, toPrint=False) + cfgFromFile = cfgUtils.getModuleConfig(module, fileOverride=cfgFile, toPrint=False) # create dictionary with one key for each simulation that shall be performed # NOTE: sims that are added don't need to be added to the simNameExisting list as # if new identical sims are added the simDict entry is just updated and not a duplicate one added - simDict = dP.createSimDict(avalancheDir, com1DFA, cfgFromFile, inputSimFilesAll, simNameExisting) + simDict = dP.createSimDict(avalancheDir, module, cfgFromFile, inputSimFilesAll, simNameExisting) simDictAll.update(simDict) # reset dem file @@ -301,7 +309,7 @@ def createSimDictFromCfgs(cfgMain, cfgPath): return simDictAll, inputSimFilesAll, simDFExisting, outDir -def initializeInputs(avalancheDir, cleanRemeshedRasters): +def initializeInputs(avalancheDir, cleanRemeshedRasters, module=com1DFA): """Create work and output directories, fetch input files and thickness info If cleanRemeshedRasters is true, the remesh folder contents will be deleted at the beginning @@ -311,6 +319,8 @@ def initializeInputs(avalancheDir, cleanRemeshedRasters): to avalanche directory cleanRemeshedRasters: bool flag if the remesh directory shall be cleaned + module: module + module to be used for task (optional) Returns -------- @@ -320,8 +330,8 @@ def initializeInputs(avalancheDir, cleanRemeshedRasters): path to store outputs """ - # fetch name of module - modName = str(pathlib.Path(com1DFA.__file__).stem) + # extract the name of the module + modName = module.__name__.split(".")[-1] # Create output and work directories _, outDir = inDirs.initialiseRunDirs(avalancheDir, modName, cleanRemeshedRasters) diff --git a/avaframe/com1DFA/deriveParameterSet.py b/avaframe/com1DFA/deriveParameterSet.py index 2fbaefc53..482e6216a 100644 --- a/avaframe/com1DFA/deriveParameterSet.py +++ b/avaframe/com1DFA/deriveParameterSet.py @@ -16,6 +16,7 @@ from avaframe.in1Data import getInput as gI from avaframe.in3Utils import cfgUtils from avaframe.in3Utils import geoTrans +import avaframe.com1DFA.com1DFA as com1DFA log = logging.getLogger(__name__) @@ -1034,14 +1035,14 @@ def writeToCfgLine(values): return valString -def createSimDict(avalancheDir, com1DFA, cfgInitial, inputSimFiles, simNameExisting): +def createSimDict(avalancheDir, module, cfgInitial, inputSimFiles, simNameExisting): """Create a simDict with all the simulations that shall be performed Parameters ----------- avalancheDir: pathlib path path to avalanche directory - com1DFA: module + module: module computational module cfgStart: configparser object configuration settings for com1DFA @@ -1068,19 +1069,19 @@ def createSimDict(avalancheDir, com1DFA, cfgInitial, inputSimFiles, simNameExist # create a dictionary with information on which parameter shall be varied for individual simulations # compare cfgStart to default module config for this - modCfg, variationDict = getParameterVariationInfo(avalancheDir, com1DFA, cfgInitial) + modCfg, variationDict = getParameterVariationInfo(avalancheDir, module, cfgInitial) # create a configuration object per simulation to run (from configuration) gathered in simDict # only new simulations are included in this simDict # key is simName and corresponds to one simulation simDict = {} simDict = com1DFA.prepareVarSimDict( - modCfg, inputSimFiles, variationDict, simNameExisting=simNameExisting + modCfg, inputSimFiles, variationDict, simNameExisting=simNameExisting, module=module ) # write full configuration (.ini file) to file date = datetime.today() fileName = "sourceConfiguration_" + "{:%d_%m_%Y_%H_%M_%S}".format(date) - cfgUtils.writeCfgFile(avalancheDir, com1DFA, modCfg, fileName=fileName) + cfgUtils.writeCfgFile(avalancheDir, module, modCfg, fileName=fileName) return simDict diff --git a/avaframe/com8MoTPSA/com8MoTPSA.py b/avaframe/com8MoTPSA/com8MoTPSA.py index 09a78e985..713b6d204 100644 --- a/avaframe/com8MoTPSA/com8MoTPSA.py +++ b/avaframe/com8MoTPSA/com8MoTPSA.py @@ -17,6 +17,7 @@ import avaframe.com1DFA.com1DFATools as com1DFATools import avaframe.com1DFA.com1DFA as com1DFA +import avaframe.com8MoTPSA.com8MoTPSA as com8MoTPSA from avaframe.in3Utils import cfgUtils from avaframe.in2Trans import rasterUtils as rU from avaframe.com1DFA import particleInitialisation as pI @@ -25,7 +26,6 @@ import avaframe.in3Utils.fileHandlerUtils as fU from avaframe.out1Peak import outPlotAllPeak as oP -import avaframe.com8MoTPSA.com8MoTPSA as com8MoTPSA # create local logger log = logging.getLogger(__name__) @@ -70,10 +70,7 @@ def _runAndCheck(command): printCounter = printCounter + 1 if printCounter > 100: # print('\r' + line, flush=True, end='') - msg = ( - "Process is running. Reported time steps: " - + str(counter) - ) + msg = "Process is running. Reported time steps: " + str(counter) log.info(msg) printCounter = 0 @@ -112,33 +109,26 @@ def rewriteDEMtoZeroValues(demFile): demData["rasterData"][np.isnan(demData["rasterData"])] = 0.0 demData["header"]["nodata_value"] = 0.0 newFileName = demFile.parent / demFile.stem - rU.writeResultToRaster(demData["header"], demData["rasterData"], newFileName, flip=True) - - + rU.writeResultToRaster( + demData["header"], demData["rasterData"], newFileName, flip=True + ) def com8MoTPSAMain(cfgMain, cfgInfo=None): # Get all necessary information from the configuration files - - - # fetch type of cfgInfo - typeCfgInfo = com1DFATools.checkCfgInfoType(cfgInfo) - - # preprocessing to create configuration objects for all simulations to run - simDict, outDir, inputSimFiles, simDFExisting = com1DFA.com1DFAPreprocess(cfgMain, typeCfgInfo, cfgInfo) + simDict, inputSimFiles = com8MoTPSAGenerateConfigs(cfgMain, cfgInfo) # convert DEM from nan to 0 values # TODO: suggest MoT-PSA to handle nan values rewriteDEMtoZeroValues(inputSimFiles["demFile"]) - log.info("The following simulations will be performed") for key in simDict: log.info("Simulation: %s" % key) exportFlag = simDict[key]["cfgSim"]["EXPORTS"].getboolean("exportData") # Preprocess the simulations, mainly creating the rcf files - rcfFiles = com8MoTPSAPreprocess(simDict, inputSimFiles, cfgMain, cfgInfo) + rcfFiles = com8MoTPSAPreprocess(simDict, inputSimFiles, cfgMain) # And now we run the simulations startTime = time.time() @@ -168,7 +158,9 @@ def com8MoTPSAPostprocess(simDict, cfgMain, inputSimFiles): # Copy max files to output directory outputDir = pathlib.Path(avalancheDir) / "Outputs" / "com8MoTPSA" - outputDirPeakFile = pathlib.Path(avalancheDir) / "Outputs" / "com8MoTPSA" / "peakFiles" + outputDirPeakFile = ( + pathlib.Path(avalancheDir) / "Outputs" / "com8MoTPSA" / "peakFiles" + ) fU.makeADir(outputDirPeakFile) for key in simDict: @@ -181,55 +173,77 @@ def com8MoTPSAPostprocess(simDict, cfgMain, inputSimFiles): # TODO: functionize it # Copy ppr files pprFiles = list(workDir.glob("*p?_max*")) - targetFiles = [pathlib.Path(str(f.name).replace('null_psa_p1_max', 'lay1_psa_ppr')) for f in pprFiles] - targetFiles = [pathlib.Path(str(f).replace('null_psa_p2_max', 'lay2_psa_ppr')) for f in targetFiles] + targetFiles = [ + pathlib.Path(str(f.name).replace("null_psa_p1_max", "null_dfa_ppr")) + for f in pprFiles + ] + targetFiles = [ + pathlib.Path(str(f).replace("null_psa_p2_max", "null_psa_ppr")) + for f in targetFiles + ] targetFiles = [outputDirPeakFile / f for f in targetFiles] for source, target in zip(pprFiles, targetFiles): shutil.copy2(source, target) # Copy pfd files pfdFiles = list(workDir.glob("*h?_max*")) - targetFiles = [pathlib.Path(str(f.name).replace('null_psa_h1_max', 'lay1_psa_pfd')) for f in pfdFiles] - targetFiles = [pathlib.Path(str(f).replace('null_psa_h2_max', 'lay2_psa_pfd')) for f in targetFiles] + targetFiles = [ + pathlib.Path(str(f.name).replace("null_psa_h1_max", "null_dfa_pfd")) + for f in pfdFiles + ] + targetFiles = [ + pathlib.Path(str(f).replace("null_psa_h2_max", "null_psa_pfd")) + for f in targetFiles + ] targetFiles = [outputDirPeakFile / f for f in targetFiles] for source, target in zip(pfdFiles, targetFiles): shutil.copy2(source, target) # Copy pfv files pfvFiles = list(workDir.glob("*s?_max*")) - targetFiles = [pathlib.Path(str(f.name).replace('null_psa_s1_max', 'lay1_psa_pfv')) for f in pfvFiles] - targetFiles = [pathlib.Path(str(f).replace('null_psa_s2_max', 'lay2_psa_pfv')) for f in targetFiles] + targetFiles = [ + pathlib.Path(str(f.name).replace("null_psa_s1_max", "null_dfa_pfv")) + for f in pfvFiles + ] + targetFiles = [ + pathlib.Path(str(f).replace("null_psa_s2_max", "null_psa_pfv")) + for f in targetFiles + ] targetFiles = [outputDirPeakFile / f for f in targetFiles] for source, target in zip(pfvFiles, targetFiles): shutil.copy2(source, target) # create plots and report - modName = __name__.split('.')[-1] + modName = __name__.split(".")[-1] reportDir = pathlib.Path(avalancheDir, "Outputs", modName, "reports") fU.makeADir(reportDir) print(inputSimFiles["demFile"]) dem = rU.readRaster(inputSimFiles["demFile"]) # Generate plots for all peakFiles - plotDict = oP.plotAllPeakFields(avalancheDir, cfgMain["FLAGS"], modName, demData=dem) + plotDict = oP.plotAllPeakFields( + avalancheDir, cfgMain["FLAGS"], modName, demData=dem + ) def com8MoTPSATask(rcfFile): # TODO: Obvious... os.chdir(os.path.dirname(os.path.abspath(__file__))) - command = ['./MoT-PSA', rcfFile] + command = ["./MoT-PSA", rcfFile] # command = ['/home/felix/Versioning/AvaFrame/avaframe/com8MoTPSA/MoT-PSA', rcfFile] log.info("Run simulation: %s" % rcfFile) _runAndCheck(command) return command -def com8MoTPSAPreprocess(simDict, inputSimFiles, cfgMain, cfgInfo): +def com8MoTPSAPreprocess(simDict, inputSimFiles, cfgMain): # Load avalanche directory from general configuration file avalancheDir = cfgMain["MAIN"]["avalancheDir"] workDir = pathlib.Path(avalancheDir) / "Work" / "com8MoTPSA" - cfgFileDir = pathlib.Path(avalancheDir) / "Outputs" / "com8MoTPSA" / "configurationFiles" + cfgFileDir = ( + pathlib.Path(avalancheDir) / "Outputs" / "com8MoTPSA" / "configurationFiles" + ) fU.makeADir(cfgFileDir) rcfFiles = list() @@ -250,7 +264,9 @@ def com8MoTPSAPreprocess(simDict, inputSimFiles, cfgMain, cfgInfo): # convert release shape to raster with values for current sim # select release area input data according to chosen release scenario - inputSimFiles = gI.selectReleaseFile(inputSimFiles, cfg["INPUT"]["releaseScenario"]) + inputSimFiles = gI.selectReleaseFile( + inputSimFiles, cfg["INPUT"]["releaseScenario"] + ) # create required input from input files demOri, inputSimLines = com1DFA.prepareInputData(inputSimFiles, cfg) @@ -268,23 +284,31 @@ def com8MoTPSAPreprocess(simDict, inputSimFiles, cfgMain, cfgInfo): # TODO: split releaseheight -> question NGI dem = rU.readRaster(inputSimFiles["demFile"]) dem["originalHeader"] = dem["header"].copy() - #releaseLine = geoTrans.prepareArea(releaseLine, dem, np.sqrt(2), combine=True, checkOverlap=False) - if len(inputSimLines['relThField']) == 0: + # releaseLine = geoTrans.prepareArea(releaseLine, dem, np.sqrt(2), combine=True, checkOverlap=False) + if len(inputSimLines["relThField"]) == 0: # if no release thickness field or function - set release according to shapefile or ini file # this is a list of release rasters that we want to combine releaseLine = geoTrans.prepareArea( - releaseLine, dem, np.sqrt(2), thList=releaseLine["thickness"], combine=True, checkOverlap=False + releaseLine, + dem, + np.sqrt(2), + thList=releaseLine["thickness"], + combine=True, + checkOverlap=False, ) - releaseField = releaseLine['rasterData'] + releaseField = releaseLine["rasterData"] else: # if relTh provided - set release thickness with field or function - releaseLine = geoTrans.prepareArea(releaseLine, dem, np.sqrt(2), combine=True, checkOverlap=False) - relRasterPoly = releaseLine['rasterData'].copy() - releaseRelThCombined = np.where(relRasterPoly>0, inputSimLines['relThField'], 0) + releaseLine = geoTrans.prepareArea( + releaseLine, dem, np.sqrt(2), combine=True, checkOverlap=False + ) + relRasterPoly = releaseLine["rasterData"].copy() + releaseRelThCombined = np.where( + relRasterPoly > 0, inputSimLines["relThField"], 0 + ) releaseField = releaseRelThCombined # Generate the work and data dirs for the current simHash - cuWorkDir = workDir / key workInputDir = cuWorkDir / "Input" workOutputDir = cuWorkDir / key @@ -306,23 +330,57 @@ def com8MoTPSAPreprocess(simDict, inputSimFiles, cfgMain, cfgInfo): rU.writeResultToRaster(dem["header"], zeroRaster, bedShear) # set configuration for MoT-PSA - cfgInfo["Run information"]["Area of Interest"] = cfgMain["MAIN"]["avalancheDir"] - cfgInfo["Run information"]["UTM zone"] = "32N" - cfgInfo["Run information"]["EPSG geodetic datum code"] = "31287" - cfgInfo["Run information"]["Run name"] = cfgMain["MAIN"]["avalancheDir"] - cfgInfo["File names"]["Grid filename"] = str(inputSimFiles["demFile"]) - cfgInfo["File names"]["Release depth 1 filename"] = str(releaseL1) + ".asc" - cfgInfo["File names"]["Release depth 2 filename"] = str(releaseL2) + ".asc" - cfgInfo["File names"]["Bed depth filename"] = str(bedDepth) + ".asc" - cfgInfo["File names"]["Bed deposition filename"] = str(bedDepo) + ".asc" - cfgInfo["File names"]["Bed shear strength filename"] = str(bedShear) + ".asc" - cfgInfo["File names"]["Output filename root"] = str(workOutputDir) + cfg["Run information"]["Area of Interest"] = cfgMain["MAIN"]["avalancheDir"] + cfg["Run information"]["UTM zone"] = "32N" + cfg["Run information"]["EPSG geodetic datum code"] = "31287" + cfg["Run information"]["Run name"] = cfgMain["MAIN"]["avalancheDir"] + cfg["File names"]["Grid filename"] = str(inputSimFiles["demFile"]) + cfg["File names"]["Release depth 1 filename"] = str(releaseL1) + ".asc" + cfg["File names"]["Release depth 2 filename"] = str(releaseL2) + ".asc" + cfg["File names"]["Bed depth filename"] = str(bedDepth) + ".asc" + cfg["File names"]["Bed deposition filename"] = str(bedDepo) + ".asc" + cfg["File names"]["Bed shear strength filename"] = str(bedShear) + ".asc" + cfg["File names"]["Output filename root"] = str(workOutputDir) rcfFileName = cfgFileDir / (str(key) + ".rcf") + cfgUtils.writeCfgFile(avalancheDir, com8MoTPSA, cfg, str(key)) + cfgToRcf(cfg, rcfFileName) + rcfFiles.append(rcfFileName) + return rcfFiles - cfgUtils.writeCfgFile(avalancheDir, com8MoTPSA, cfgInfo, str(key)) - cfgToRcf(cfgInfo, rcfFileName) - rcfFiles.append(rcfFileName) +def com8MoTPSAGenerateConfigs(cfgMain, cfgInfo): + """ + Creates configuration objects for com8MoTPSA. - return rcfFiles + Parameters + ------------ + cfgMain: configparser object + main configuration of AvaFrame + cfgInfo: str or pathlib Path or configparser object + path to configuration file if overwrite is desired - optional + if not local (if available) or default configuration will be loaded + if cfgInfo is a configparser object take this as initial config + + Returns + -------- + simDict: dict + dictionary with one key per simulation to perform including its config object + inputSimFiles: dict + dictionary with input files info + """ + + # fetch type of cfgInfo + typeCfgInfo = com1DFATools.checkCfgInfoType(cfgInfo) + + if typeCfgInfo == "cfgFromDir": + # preprocessing to create configuration objects for all simulations to run by reading multiple cfg files + simDict, inputSimFiles, simDFExisting, outDir = ( + com1DFATools.createSimDictFromCfgs(cfgMain, cfgInfo, module=com8MoTPSA) + ) + else: + # preprocessing to create configuration objects for all simulations to run + simDict, outDir, inputSimFiles, simDFExisting = com1DFA.com1DFAPreprocess( + cfgMain, typeCfgInfo, cfgInfo, module=com8MoTPSA + ) + return simDict, inputSimFiles diff --git a/avaframe/com8MoTPSA/com8MoTPSACfg.ini b/avaframe/com8MoTPSA/com8MoTPSACfg.ini index 7fd80392f..50fbec7d2 100644 --- a/avaframe/com8MoTPSA/com8MoTPSACfg.ini +++ b/avaframe/com8MoTPSA/com8MoTPSACfg.ini @@ -76,9 +76,60 @@ secondaryRelThDistVariation = # secondary area release thickness (only considered if secondaryRelThFromShp=False) [m] secondaryRelTh = +#+++++++++++++Volume classes [m³] +volClassSmall = 25000. +volClassMedium = 60000. + +#+++++++++++++Mesh and interpolation +# interpolation option +# 3 Options available : -0: nearest neighbour interpolation +# -1: equal weights interpolation +# -2: bilinear interpolation +interpOption = 2 +# minimum flow thickness [m] +hmin = 0.05 +# remesh the input rasters or look for remeshed rasters +# expected mesh size [m] +meshCellSize = 5 +# threshold under which no remeshing is done +meshCellSizeThreshold = 0.001 +# clean DEMremeshed directory to ensure remeshing if chosen meshCellsize is different from rasters in Inputs/ +cleanRemeshedRasters = True +# resize files read from Inputs/RASTERS to be resized to extent of DEM as resizeThreshold x meshCellSize +resizeThreshold = 3 + +# Normal computation on rectangular grid +# 4 triangles method 6 triangles method 8 triangles method +# +----U----UR---+---+--... +----+----+----+---+--... +----+----+----+---+--... +# | /|\ | /| | /| 2 /| /| |\ 2 | 3 /| /| Y +# | / | \ | / | | / | / | / | | \ | / | / | ^ +# | / | \ | / | / | / | / | / | / | \ | / | / | / | +# |/ 1 | 2 \|/ |/ |/ 1 |/ 3 |/ |/ | 1 \|/ 4 |/ |/ | +# +----P----L----+---+--... +----*----+----+---+--... +----*----+----+----+--... +-----> X +# |\ 4 | 3 /| /| | 6 /| 4 /| /| | 8 /|\ 5 | /| +# | \ | / | / | | / | / | / | | / | \ | / | +# | \ | / | / | / | / | / | / | / | / | \ | / | / +# | \|/ |/ |/ |/ 5 |/ |/ |/ |/ 7 | 6 \|/ |/ +# +----+----+----+---+--... +----+----+----+---+--... +----+----+----+---+--... +# | /| /| /| | /| /| /| | /| /| /| +# 4 Options available : -1: simple cross product method (with the diagonals P-UR and U-L) +# -4: 4 triangles method +# -6: 6 triangles method +# -8: 8 triangles method +methodMeshNormal = 1 + +#++++++++++++++ Technical values +++++++++++++ +# when checking if a point is within a polygon, one can decide to add a buffer +# arround the polygon (0 means take strictly the points inside, a very small value +# will inclune the points located on the polygon line) +thresholdPointInPoly = 0.001 + + [INPUT] # specify a particular release area scenario, provide name of shapefile with or without extension .shp (optional) releaseScenario = +# important for parameter variation through probRun +thFromIni = # Below are the settings for the MoT-PSA model [Run information] @@ -158,4 +209,9 @@ Write instant. pressure = no Minimum flow depth (m) = 0.01 Minimum speed (m/s) = 0.01 Momentum threshold (-) = 0.05 -Initial CFL number (-) = 0.8 \ No newline at end of file +Initial CFL number (-) = 0.8 + +[EXPORTS] +# peak files and plots are exported, option to turn off exports when exportData is set to False +# this affects export of peak files and also generation of peak file plots +exportData = True diff --git a/avaframe/com8MoTPSA/runAna4ProbAnaCom8MoTPSA.py b/avaframe/com8MoTPSA/runAna4ProbAnaCom8MoTPSA.py new file mode 100644 index 000000000..78eb15ac8 --- /dev/null +++ b/avaframe/com8MoTPSA/runAna4ProbAnaCom8MoTPSA.py @@ -0,0 +1,133 @@ +""" + Run script for performing an com8MoTPSA avalanche simulation with parameter variation and performing a + probability analysis with the simulation results + Define settings in ana4Stats/probAnaCfg.ini or your local copy - local_probAnaCfg.ini +""" + +# Load modules +import pathlib +import argparse + +# Local imports +from avaframe.com1DFA import com1DFA +import avaframe.com8MoTPSA.com8MoTPSA as com8MoTPSA +from avaframe.out3Plot import statsPlots as sP +from avaframe.ana4Stats import probAna +from avaframe.in3Utils import initializeProject as initProj +from avaframe.in3Utils import cfgUtils +from avaframe.in3Utils import logUtils +import avaframe.in3Utils.fileHandlerUtils as fU +from avaframe.out3Plot import outQuickPlot as oP +import configparser + + +def runProbAna(avalancheDir=''): + """ Run a com1DFA probability analysis with parameters and only an + avalanche directory as input + + Parameters + ---------- + avalancheDir: str + path to avalanche directory (setup eg. with init scipts) + + Returns + ------- + """ + + # log file name; leave empty to use default runLog.log + logName = 'runAna4ProbAna' + + # Load general configuration filee + cfgMain = cfgUtils.getGeneralConfig() + + # Load avalanche directory from general configuration file, + # if not provided as input argument + cfgMain = cfgUtils.getGeneralConfig() + if avalancheDir != '': + cfgMain['MAIN']['avalancheDir'] = avalancheDir + else: + avalancheDir = cfgMain['MAIN']['avalancheDir'] + + avalancheDir = pathlib.Path(avalancheDir) + + # Start logging + log = logUtils.initiateLogger(avalancheDir, logName) + log.info('MAIN SCRIPT') + log.info('Current avalanche: %s', avalancheDir) + + # Clean input directory(ies) of old work files + initProj.cleanSingleAvaDir(avalancheDir, deleteOutput=False) + + # Load configuration file for probabilistic run and analysis + cfgProb = cfgUtils.getModuleConfig(probAna) + + # create configuration files for com8MoTPSA simulations including parameter + # variation - defined in the probabilistic config + # prob4AnaCfg.ini or its local copy + cfgFiles, cfgPath = probAna.createComModConfig(cfgProb, avalancheDir, com8MoTPSA) + + # perform com8MoTPSA simulations + # dem, plotDict, reportDictList, simDF = com8MoTPSA.com8MoTPSAMain(cfgMain, cfgInfo=cfgPath) + com8MoTPSA.com8MoTPSAMain(cfgMain, cfgInfo=cfgPath) + + # fetch simDF of only sims that were created in above call to com8MoTPSAMain - do not include sims + # that were run previously and are still located in outDir + simDFActual, _ = cfgUtils.readAllConfigurationInfo(avalancheDir, specDir='', configCsvName='latestSims') + + # check if sampling strategy is from full sample - then only one configuration is possible + probabilityConfigurations = probAna.fetchProbConfigs(cfgProb['PROBRUN']) + + # perform probability analysis + for probConf in probabilityConfigurations: + + # filter simulations according to probability configurations + cfgProb['FILTER'] = probabilityConfigurations[probConf] + log.info('Perform proba analysis for configuration: %s' % probConf) + # provide optional filter criteria for simulations + parametersDict = fU.getFilterDict(cfgProb, 'FILTER') + + # perform probability analysis + anaPerformed, contourDict = probAna.probAnalysis(avalancheDir, + cfgProb, + 'com8MoTPSA', + parametersDict=parametersDict, + probConf=probConf, + simDFActual=simDFActual + ) + if anaPerformed is False: + log.warning('No files found for configuration: %s' % probConf) + + + # make a plot of the contours + inputDir = pathlib.Path(avalancheDir, 'Outputs', 'ana4Stats') + outName = '%s_prob_%s_%s_lim%s' % (str(avalancheDir.stem), + probConf, + cfgProb['GENERAL']['peakVar'], + cfgProb['GENERAL']['peakLim']) + pathDict = {'pathResult': str(inputDir / 'plots'), + 'avaDir': str(avalancheDir), + 'plotScenario': outName + } + oP.plotContours(contourDict, + cfgProb['GENERAL']['peakVar'], + cfgProb['GENERAL']['peakLim'], pathDict, addLegend=False) + + # plot probability maps + sP.plotProbMap(avalancheDir, inputDir, cfgProb, demPlot=True) + + # # copy outputs to folder called like probability configurations + # outputFiles = avalancheDir / 'Outputs' / 'ana4Stats' + # saveFiles = avalancheDir / 'Outputs' / ('ana4Stats_' + probConf) + # shutil.move(outputFiles, saveFiles) + + return + + +if __name__ == '__main__': + + parser = argparse.ArgumentParser(description='Run ana4ProbAna workflow') + parser.add_argument('avadir', metavar='a', type=str, nargs='?', default='', + help='the avalanche directory') + + args = parser.parse_args() + runProbAna(str(args.avadir)) diff --git a/avaframe/in3Utils/cfgUtils.py b/avaframe/in3Utils/cfgUtils.py index a52c50ff0..7232807b8 100644 --- a/avaframe/in3Utils/cfgUtils.py +++ b/avaframe/in3Utils/cfgUtils.py @@ -129,7 +129,6 @@ def getModuleConfig(module, fileOverride='', modInfo=False, toPrint=True, onlyDe # Finally read it cfg, modDict = readCompareConfig(iniFile, modName, compare, toPrint) - if modInfo: return cfg, modDict @@ -196,7 +195,6 @@ def readCompareConfig(iniFile, modName, compare, toPrint=True): # read default and local parser files defCfg.read(iniFile[0]) locCfg.read(iniFile[1]) - log.debug('Writing cfg for: %s', modName) # compare to default config and get modification dictionary and config modDict, modCfg = compareTwoConfigs(defCfg, locCfg, toPrint=toPrint) @@ -237,7 +235,7 @@ def _splitDeepDiffValuesChangedItem(inKey, inVal): newVal: str new value """ - splitKey = re.findall(r"\['?([A-Za-z0-9_]+)'?\]", inKey) + splitKey = re.findall(r"\[\s*['\"]([^'\"]+)['\"]\s*\]", inKey) section = splitKey[0] key = splitKey[1] @@ -411,7 +409,6 @@ def readCfgFile(avaDir, module='', fileName=''): return cfg - def cfgHash(cfg, typeDict=False): """ UID hash of a config. Given a configParser object cfg, or a dictionary - then typeDict=True, returns a uid hash @@ -555,7 +552,7 @@ def createConfigurationInfo(avaDir, comModule='com1DFA', standardCfg='', writeCS def appendCgf2DF(simHash, simName, cfgObject, simDF): """ append simulation configuration to the simulation dataframe - only account for sections GENERAL and INPUT + append all sections to the dataframe Parameters ----------- @@ -575,14 +572,17 @@ def appendCgf2DF(simHash, simName, cfgObject, simDF): """ indexItem = [simHash] cfgDict = convertConfigParserToDict(cfgObject) - simItemDFGeneral = pd.DataFrame(data=cfgDict['GENERAL'], index=indexItem) - simItemDFInput = pd.DataFrame(data=cfgDict['INPUT'], index=indexItem) - if 'VISUALISATION' in cfgDict: - simItemDFVisualisation = pd.DataFrame(data=cfgDict['VISUALISATION'], index=indexItem) - simItemDF = pd.concat([simItemDFGeneral, simItemDFInput, simItemDFVisualisation], axis=1) - else: - simItemDF = pd.concat([simItemDFGeneral, simItemDFInput], axis=1) + simItemDFList = [] + for section in cfgDict: + simItemDFSection = pd.DataFrame(data=cfgDict[section], index=indexItem) + simItemDFList.append(simItemDFSection) + simItemDF = pd.concat(simItemDFList, axis=1) simItemDF = simItemDF.assign(simName=simName) + + # check for duplicates: if yes, rename them by adding Dupl1 to the duplicate name + if simItemDF.columns.duplicated().any(): + renameDuplicates(simItemDF) + if isinstance(simDF, str): simDF = simItemDF else: @@ -590,6 +590,37 @@ def appendCgf2DF(simHash, simName, cfgObject, simDF): return simDF +def renameDuplicates(df): + """ + Rename duplicate column names in the given DataFrame. This ensures all column names in the DataFrame + are unique by adding a suffix 'DuplX' where X is the occurrence number, starting + from 1 for the first duplicate. + + Parameters + ---------- + df : pandas.DataFrame + The input DataFrame whose column names need to be checked for duplicates. + + Returns + ------- + bool + Returns True to indicate the renaming of duplicate column names was successful. + """ + seen = {} + new_cols = [] + + for col in df.columns: + if col not in seen: + seen[col] = 0 + new_cols.append(col) + else: + seen[col] += 1 + new_cols.append(f"{col}_{seen[col]}") + + df.columns = new_cols + return True + + def appendTcpu2DF(simHash, tCPU, tCPUDF): """ append Tcpu dictionary to the dataframe diff --git a/avaframe/runAna4ProbAna.py b/avaframe/runAna4ProbAna.py index 3c6627329..fba76fe38 100644 --- a/avaframe/runAna4ProbAna.py +++ b/avaframe/runAna4ProbAna.py @@ -127,4 +127,4 @@ def runProbAna(avalancheDir=''): help='the avalanche directory') args = parser.parse_args() - runProbAna(str(args.avadir)) + runProbAna(str(args.avadir)) \ No newline at end of file diff --git a/avaframe/runScripts/runAna3AIMEC.py b/avaframe/runScripts/runAna3AIMEC.py index 96565ecc1..49de10665 100644 --- a/avaframe/runScripts/runAna3AIMEC.py +++ b/avaframe/runScripts/runAna3AIMEC.py @@ -8,6 +8,8 @@ from avaframe.in3Utils import initializeProject as iP from avaframe.in3Utils import cfgUtils from avaframe.in3Utils import logUtils + + # create local logger log = logging.getLogger(__name__) diff --git a/avaframe/tests/test_probAna.py b/avaframe/tests/test_probAna.py index 0a6e4aab7..895d94183 100644 --- a/avaframe/tests/test_probAna.py +++ b/avaframe/tests/test_probAna.py @@ -223,7 +223,7 @@ def test_createComModConfig(tmp_path): assert cfgTest21['GENERAL']['relThPercentVariation'] == '' assert cfgTest21['GENERAL']['musamosat'] == cfgTest2['GENERAL']['musamosat'] assert cfgTest21['INPUT']['releaseScenario'] == 'relParabolaTwo' - assert cfgTest2.has_option('GENERAL', 'relTh0') + assert cfgTest21.has_option('GENERAL', 'relTh0') assert len(cfgFiles2) == 80 cfgProb = configparser.ConfigParser() From 7bcc5719a1e0110b1e6187e60400e0779ef7ebfc Mon Sep 17 00:00:00 2001 From: RolandFischbacher Date: Tue, 24 Jun 2025 16:01:38 +0200 Subject: [PATCH 06/12] feat(com8): Add code necessary to run com8 refactor(com1DFA): fix field initialization - Add `cResRasterOrig` and `detRasterOrig` fields for clarity. - Initialize `tPlot` field for particles handling. - Streamline `DAM` file path assignment to reduce redundant code. refactor(rasterUtils): remove `decimal_precision` parameter feat(cfgUtils): add `cfgToRcf` function for converting configurations to RCF format - Added a new utility function `cfgToRcf` in `cfgUtils.py` to convert `ConfigParser` objects to the RCF format required by NGI MoT. - Removed the duplicated implementation of `cfgToRcf` from `com8MoTPSA.py` and integrated the new utility. refactor(com1DFA, com8MoTPSA): qlty fmt refactor(com8MoTPSA): remove unused `pandas` import --- .gitignore | 2 + avaframe/com1DFA/com1DFA.py | 52 +++++++++------- avaframe/com1DFA/deriveParameterSet.py | 6 +- avaframe/com8MoTPSA/README_dev.md | 2 +- avaframe/com8MoTPSA/com8MoTPSA.py | 82 +++++--------------------- avaframe/in2Trans/rasterUtils.py | 2 +- avaframe/in3Utils/cfgUtils.py | 31 +++++++++- 7 files changed, 85 insertions(+), 92 deletions(-) diff --git a/.gitignore b/.gitignore index 0982d9c80..10f46188c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ Work build +dist + RELEASE-VERSION.txt *.egg* diff --git a/avaframe/com1DFA/com1DFA.py b/avaframe/com1DFA/com1DFA.py index 1dd11d687..8750f7822 100644 --- a/avaframe/com1DFA/com1DFA.py +++ b/avaframe/com1DFA/com1DFA.py @@ -1153,6 +1153,8 @@ def initializeSimulation(cfg, outDir, demOri, inputSimLines, logName): ) fields["cResRaster"] = cResRaster fields["detRaster"] = detRaster + fields["cResRasterOrig"] = cResRaster + fields["detRasterOrig"] = detRaster for fric in ["mu", "xi"]: if (inputSimLines[fric + "File"] == None) or ( @@ -1329,9 +1331,9 @@ def initializeParticles(cfg, releaseLine, dem, inputSimLines="", logName="", rel particles["idFixed"] = idFixed # initialize enthalpy particles["totalEnthalpy"] = TIni * cpIce + gravAcc * particles["z"] - particles["massPerPart"] = massPerPart particles["mTot"] = np.sum(particles["m"]) + particles["tPlot"] = 0 particles["h"] = hPartArray particles["ux"] = np.zeros(np.shape(hPartArray)) particles["uy"] = np.zeros(np.shape(hPartArray)) @@ -2738,7 +2740,7 @@ def exportFields( ) -def prepareVarSimDict(standardCfg, inputSimFiles, variationDict, simNameExisting=""): +def prepareVarSimDict(standardCfg, inputSimFiles, variationDict, simNameExisting="", module=com1DFA): """Prepare a dictionary with simulations that shall be run with varying parameters following the variation dict Parameters @@ -2752,6 +2754,8 @@ def prepareVarSimDict(standardCfg, inputSimFiles, variationDict, simNameExisting simNameExisting: list list of simulation names that already exist (optional). If provided, only carry on simulations that do not exist + module: module + module to be used for task (optional) Returns ------- @@ -2760,6 +2764,9 @@ def prepareVarSimDict(standardCfg, inputSimFiles, variationDict, simNameExisting simType and contains full configuration configparser object for simulation run """ + # extract the name of the module + modName = module.__name__.split(".")[-1] + # get list of simulation types that are desired if "simTypeList" in variationDict: simTypeList = variationDict["simTypeList"] @@ -2842,12 +2849,15 @@ def prepareVarSimDict(standardCfg, inputSimFiles, variationDict, simNameExisting # check if DEM in Inputs has desired mesh size pathToDem = dP.checkRasterMeshSize(cfgSim, inputSimFiles["demFile"], "DEM") cfgSim["INPUT"]["DEM"] = pathToDem - if modName == 'com1DFA': - if cfgSim["GENERAL"]["relThFromFile"] == "True" or cfgSim["GENERAL"]["frictModel"].lower() == "spatialvoellmy": - dem = IOf.readRaster(pathlib.Path(cfgSim['GENERAL']['avalancheDir'], 'Inputs', pathToDem)) - elif modName == 'com8MoTPSA': + if modName == "com1DFA": + if ( + cfgSim["GENERAL"]["relThFromFile"] == "True" + or cfgSim["GENERAL"]["frictModel"].lower() == "spatialvoellmy" + ): + dem = IOf.readRaster(pathlib.Path(cfgSim["GENERAL"]["avalancheDir"], "Inputs", pathToDem)) + elif modName == "com8MoTPSA": if cfgSim["GENERAL"]["relThFromFile"] == "True": - dem = IOf.readRaster(pathlib.Path(cfgSim['GENERAL']['avalancheDir'], 'Inputs', pathToDem)) + dem = IOf.readRaster(pathlib.Path(cfgSim["GENERAL"]["avalancheDir"], "Inputs", pathToDem)) # check if RELTH in Inputs has desired mesh size if cfgSim["GENERAL"]["relThFromFile"] == "True": @@ -2856,19 +2866,16 @@ def prepareVarSimDict(standardCfg, inputSimFiles, variationDict, simNameExisting else: cfgSim["INPUT"]["relThFile"] = "" - if modName == 'com1DFA': + if modName == "com1DFA": # check if spatialVoellmy is chosen that friction fields have correct extent if cfgSim["GENERAL"]["frictModel"].lower() == "spatialvoellmy": - for fric in ['mu', 'xi']: - pathToFric = dP.checkExtentAndCellSize(cfgSim, inputSimFiles['%sFile' % fric], dem, fric) - cfgSim['INPUT']['%sFile' % fric] = pathToFric + for fric in ["mu", "xi"]: + pathToFric = dP.checkExtentAndCellSize(cfgSim, inputSimFiles["%sFile" % fric], dem, fric) + cfgSim["INPUT"]["%sFile" % fric] = pathToFric # add info about dam file path to the cfg - if cfgSim['GENERAL']['dam'] == 'True' and inputSimFiles['damFile'] is not None: - cfgSim['INPUT']['DAM'] = str(pathlib.Path('DAM', inputSimFiles['damFile'].name)) - # add info about dam file path to the cfg - if cfgSim["GENERAL"]["dam"] == "True" and inputSimFiles["damFile"] is not None: - cfgSim["INPUT"]["DAM"] = str(pathlib.Path("DAM", inputSimFiles["damFile"].name)) + if cfgSim["GENERAL"]["dam"] == "True" and inputSimFiles["damFile"] is not None: + cfgSim["INPUT"]["DAM"] = str(pathlib.Path("DAM", inputSimFiles["damFile"].name)) # add info about entrainment file path to the cfg if "ent" in row._asdict()["simTypeList"] and inputSimFiles["entFile"] is not None: @@ -2890,7 +2897,7 @@ def prepareVarSimDict(standardCfg, inputSimFiles, variationDict, simNameExisting pathToDemFull = pathlib.Path(cfgSim["GENERAL"]["avalancheDir"], "Inputs", pathToDem) - if modName == 'com1DFA': + if modName == "com1DFA": # if frictModel is samosATAuto compute release vol if cfgSim["GENERAL"]["frictModel"].lower() == "samosatauto": relVolume = fetchRelVolume(rel, cfgSim, pathToDemFull, inputSimFiles["secondaryReleaseFile"]) @@ -2906,7 +2913,7 @@ def prepareVarSimDict(standardCfg, inputSimFiles, variationDict, simNameExisting # set frictModelIndicator, this needs to happen AFTER checkCfgFrictModel frictIndi = com1DFATools.setFrictTypeIndicator(cfgSim) - elif modName == 'com8MoTPSA': + elif modName == "com8MoTPSA": relVolume = fetchRelVolume(rel, cfgSim, pathToDemFull, inputSimFiles["secondaryReleaseFile"]) # set Volume class identificator @@ -2940,10 +2947,13 @@ def prepareVarSimDict(standardCfg, inputSimFiles, variationDict, simNameExisting "relFile": rel, "cfgSim": cfgSimObject, } - if modName == 'com1DFA': + if modName == "com1DFA": # write configuration file, dont need to write cfg file for com8MoTPSA (does this later when creating rcf file) cfgUtils.writeCfgFile( - cfgSimObject["GENERAL"]["avalancheDir"], com1DFA, cfgSimObject, fileName=simName + cfgSimObject["GENERAL"]["avalancheDir"], + com1DFA, + cfgSimObject, + fileName=simName, ) else: log.warning("Simulation %s already exists, not repeating it" % simName) @@ -3107,8 +3117,6 @@ def fetchRelVolume(releaseFile, cfg, pathToDem, secondaryReleaseFile, radius=0.0 demVol = geoTrans.getNormalMesh(demVol, num=methodMeshNormal) demVol = DFAtls.getAreaMesh(demVol, methodMeshNormal) - - # compute volume of release area relVolume = initializeRelVol(cfg, demVol, releaseFile, radius, releaseType="primary") diff --git a/avaframe/com1DFA/deriveParameterSet.py b/avaframe/com1DFA/deriveParameterSet.py index 482e6216a..84a8bd27d 100644 --- a/avaframe/com1DFA/deriveParameterSet.py +++ b/avaframe/com1DFA/deriveParameterSet.py @@ -1076,7 +1076,11 @@ def createSimDict(avalancheDir, module, cfgInitial, inputSimFiles, simNameExisti # key is simName and corresponds to one simulation simDict = {} simDict = com1DFA.prepareVarSimDict( - modCfg, inputSimFiles, variationDict, simNameExisting=simNameExisting, module=module + modCfg, + inputSimFiles, + variationDict, + simNameExisting=simNameExisting, + module=module, ) # write full configuration (.ini file) to file diff --git a/avaframe/com8MoTPSA/README_dev.md b/avaframe/com8MoTPSA/README_dev.md index 1f57c24fc..7f5454141 100644 --- a/avaframe/com8MoTPSA/README_dev.md +++ b/avaframe/com8MoTPSA/README_dev.md @@ -4,4 +4,4 @@ This is the initial development / prototype stage of including control of MoTPSA For now you need a compiled binary, available from ??? -This binary needs to live in the avaframe/com8MoTPSA directory \ No newline at end of file +This binary needs to live in the avaframe/com8MoTPSA directory diff --git a/avaframe/com8MoTPSA/com8MoTPSA.py b/avaframe/com8MoTPSA/com8MoTPSA.py index 713b6d204..4990a43cc 100644 --- a/avaframe/com8MoTPSA/com8MoTPSA.py +++ b/avaframe/com8MoTPSA/com8MoTPSA.py @@ -2,12 +2,13 @@ import subprocess import platform import logging -import pandas as pd import numpy as np import pathlib import time import shutil +from avaframe.in3Utils.cfgUtils import cfgToRcf + if os.name == "nt": from multiprocessing.pool import ThreadPool as Pool elif platform.system() == "Darwin": @@ -86,32 +87,12 @@ def _runAndCheck(command): log.info(line) -# TODO move to utils -def cfgToRcf(cfg, fileName): - with open(fileName, "w") as f: - for section in cfg.sections(): - if section in ("FOREST_EFFECTS", "ENTRAINMENT"): - pass - elif section in ("GENERAL", "INPUT"): - continue - else: - f.write(f"# {section.replace('_', ' ')}\n") - f.write("#\n") - for key, value in cfg.items(section): - # key = key.replace('_', ' ') - key = key.strip() - f.write(f"{key:<40}{value}\n") - f.write("#\n") - - def rewriteDEMtoZeroValues(demFile): demData = rU.readRaster(demFile) demData["rasterData"][np.isnan(demData["rasterData"])] = 0.0 demData["header"]["nodata_value"] = 0.0 newFileName = demFile.parent / demFile.stem - rU.writeResultToRaster( - demData["header"], demData["rasterData"], newFileName, flip=True - ) + rU.writeResultToRaster(demData["header"], demData["rasterData"], newFileName, flip=True) def com8MoTPSAMain(cfgMain, cfgInfo=None): @@ -125,7 +106,6 @@ def com8MoTPSAMain(cfgMain, cfgInfo=None): log.info("The following simulations will be performed") for key in simDict: log.info("Simulation: %s" % key) - exportFlag = simDict[key]["cfgSim"]["EXPORTS"].getboolean("exportData") # Preprocess the simulations, mainly creating the rcf files rcfFiles = com8MoTPSAPreprocess(simDict, inputSimFiles, cfgMain) @@ -158,9 +138,7 @@ def com8MoTPSAPostprocess(simDict, cfgMain, inputSimFiles): # Copy max files to output directory outputDir = pathlib.Path(avalancheDir) / "Outputs" / "com8MoTPSA" - outputDirPeakFile = ( - pathlib.Path(avalancheDir) / "Outputs" / "com8MoTPSA" / "peakFiles" - ) + outputDirPeakFile = pathlib.Path(avalancheDir) / "Outputs" / "com8MoTPSA" / "peakFiles" fU.makeADir(outputDirPeakFile) for key in simDict: @@ -174,13 +152,9 @@ def com8MoTPSAPostprocess(simDict, cfgMain, inputSimFiles): # Copy ppr files pprFiles = list(workDir.glob("*p?_max*")) targetFiles = [ - pathlib.Path(str(f.name).replace("null_psa_p1_max", "null_dfa_ppr")) - for f in pprFiles - ] - targetFiles = [ - pathlib.Path(str(f).replace("null_psa_p2_max", "null_psa_ppr")) - for f in targetFiles + pathlib.Path(str(f.name).replace("null_psa_p1_max", "null_dfa_ppr")) for f in pprFiles ] + targetFiles = [pathlib.Path(str(f).replace("null_psa_p2_max", "null_psa_ppr")) for f in targetFiles] targetFiles = [outputDirPeakFile / f for f in targetFiles] for source, target in zip(pprFiles, targetFiles): shutil.copy2(source, target) @@ -188,13 +162,9 @@ def com8MoTPSAPostprocess(simDict, cfgMain, inputSimFiles): # Copy pfd files pfdFiles = list(workDir.glob("*h?_max*")) targetFiles = [ - pathlib.Path(str(f.name).replace("null_psa_h1_max", "null_dfa_pfd")) - for f in pfdFiles - ] - targetFiles = [ - pathlib.Path(str(f).replace("null_psa_h2_max", "null_psa_pfd")) - for f in targetFiles + pathlib.Path(str(f.name).replace("null_psa_h1_max", "null_dfa_pfd")) for f in pfdFiles ] + targetFiles = [pathlib.Path(str(f).replace("null_psa_h2_max", "null_psa_pfd")) for f in targetFiles] targetFiles = [outputDirPeakFile / f for f in targetFiles] for source, target in zip(pfdFiles, targetFiles): shutil.copy2(source, target) @@ -202,13 +172,9 @@ def com8MoTPSAPostprocess(simDict, cfgMain, inputSimFiles): # Copy pfv files pfvFiles = list(workDir.glob("*s?_max*")) targetFiles = [ - pathlib.Path(str(f.name).replace("null_psa_s1_max", "null_dfa_pfv")) - for f in pfvFiles - ] - targetFiles = [ - pathlib.Path(str(f).replace("null_psa_s2_max", "null_psa_pfv")) - for f in targetFiles + pathlib.Path(str(f.name).replace("null_psa_s1_max", "null_dfa_pfv")) for f in pfvFiles ] + targetFiles = [pathlib.Path(str(f).replace("null_psa_s2_max", "null_psa_pfv")) for f in targetFiles] targetFiles = [outputDirPeakFile / f for f in targetFiles] for source, target in zip(pfvFiles, targetFiles): shutil.copy2(source, target) @@ -221,9 +187,7 @@ def com8MoTPSAPostprocess(simDict, cfgMain, inputSimFiles): dem = rU.readRaster(inputSimFiles["demFile"]) # Generate plots for all peakFiles - plotDict = oP.plotAllPeakFields( - avalancheDir, cfgMain["FLAGS"], modName, demData=dem - ) + oP.plotAllPeakFields(avalancheDir, cfgMain["FLAGS"], modName, demData=dem) def com8MoTPSATask(rcfFile): @@ -241,9 +205,7 @@ def com8MoTPSAPreprocess(simDict, inputSimFiles, cfgMain): avalancheDir = cfgMain["MAIN"]["avalancheDir"] workDir = pathlib.Path(avalancheDir) / "Work" / "com8MoTPSA" - cfgFileDir = ( - pathlib.Path(avalancheDir) / "Outputs" / "com8MoTPSA" / "configurationFiles" - ) + cfgFileDir = pathlib.Path(avalancheDir) / "Outputs" / "com8MoTPSA" / "configurationFiles" fU.makeADir(cfgFileDir) rcfFiles = list() @@ -251,22 +213,12 @@ def com8MoTPSAPreprocess(simDict, inputSimFiles, cfgMain): # Generate command and run via subprocess.run # Configuration that needs adjustment - simDF = pd.DataFrame() - # load configuration object for current sim cfg = simDict[key]["cfgSim"] - # fetch simHash for current sim - simHash = simDict[key]["simHash"] - - # # append configuration to dataframe - # simDF = cfgUtils.appendCgf2DF(simHash, key, cfg, simDF) - # convert release shape to raster with values for current sim # select release area input data according to chosen release scenario - inputSimFiles = gI.selectReleaseFile( - inputSimFiles, cfg["INPUT"]["releaseScenario"] - ) + inputSimFiles = gI.selectReleaseFile(inputSimFiles, cfg["INPUT"]["releaseScenario"]) # create required input from input files demOri, inputSimLines = com1DFA.prepareInputData(inputSimFiles, cfg) @@ -303,9 +255,7 @@ def com8MoTPSAPreprocess(simDict, inputSimFiles, cfgMain): releaseLine, dem, np.sqrt(2), combine=True, checkOverlap=False ) relRasterPoly = releaseLine["rasterData"].copy() - releaseRelThCombined = np.where( - relRasterPoly > 0, inputSimLines["relThField"], 0 - ) + releaseRelThCombined = np.where(relRasterPoly > 0, inputSimLines["relThField"], 0) releaseField = releaseRelThCombined # Generate the work and data dirs for the current simHash @@ -375,8 +325,8 @@ def com8MoTPSAGenerateConfigs(cfgMain, cfgInfo): if typeCfgInfo == "cfgFromDir": # preprocessing to create configuration objects for all simulations to run by reading multiple cfg files - simDict, inputSimFiles, simDFExisting, outDir = ( - com1DFATools.createSimDictFromCfgs(cfgMain, cfgInfo, module=com8MoTPSA) + simDict, inputSimFiles, simDFExisting, outDir = com1DFATools.createSimDictFromCfgs( + cfgMain, cfgInfo, module=com8MoTPSA ) else: # preprocessing to create configuration objects for all simulations to run diff --git a/avaframe/in2Trans/rasterUtils.py b/avaframe/in2Trans/rasterUtils.py index a7d7063aa..260eac856 100644 --- a/avaframe/in2Trans/rasterUtils.py +++ b/avaframe/in2Trans/rasterUtils.py @@ -189,7 +189,7 @@ class with methods that give cellsize, nrows, ncols, xllcenter width=resultArray.shape[1], count=1, dtype=resultArray.dtype, - decimal_precision=3, + # decimal_precision=3, ) if flip: rasterOut.write(np.flipud(resultArray), 1) diff --git a/avaframe/in3Utils/cfgUtils.py b/avaframe/in3Utils/cfgUtils.py index 7232807b8..7fa1b8b3e 100644 --- a/avaframe/in3Utils/cfgUtils.py +++ b/avaframe/in3Utils/cfgUtils.py @@ -924,4 +924,33 @@ def getModPathName(module): # get filename of module modName = str(pathlib.Path(module.__file__).stem) - return modPath, modName \ No newline at end of file + return modPath, modName + + +def cfgToRcf(cfg, fileName): + """Convert configuration object to RCF format file (used by NGI MoT). + + Takes a ConfigParser object and writes its contents to a file in rcf format, + excluding certain sections and formatting others according to RCF requirements. + + Parameters + ---------- + cfg : configparser.ConfigParser + Configuration object containing sections and their key-value pairs + fileName : str or pathlib.Path + Path to the output file where the RCF format will be written + """ + with open(fileName, "w") as f: + for section in cfg.sections(): + if section in ("FOREST_EFFECTS", "ENTRAINMENT"): + pass + elif section in ("GENERAL", "INPUT"): + continue + else: + f.write(f"# {section.replace('_', ' ')}\n") + f.write("#\n") + for key, value in cfg.items(section): + # key = key.replace('_', ' ') + key = key.strip() + f.write(f"{key:<40}{value}\n") + f.write("#\n") From 75300db3240f3963bf6d3cd4d2760fb0b6135a5c Mon Sep 17 00:00:00 2001 From: "qltysh[bot]" <168846912+qltysh[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 09:47:11 +0000 Subject: [PATCH 07/12] =?UTF-8?q?=F0=9F=93=9D=20qlty=20fmt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- avaframe/ana4Stats/probAna.py | 1040 ++++++++++++++++------------- avaframe/in1Data/getInput.py | 34 +- avaframe/in3Utils/cfgUtils.py | 707 ++++++++++---------- avaframe/tests/test_probAna.py | 1134 +++++++++++++++++++------------- 4 files changed, 1632 insertions(+), 1283 deletions(-) diff --git a/avaframe/ana4Stats/probAna.py b/avaframe/ana4Stats/probAna.py index e1b3e5287..0ccbb20ad 100644 --- a/avaframe/ana4Stats/probAna.py +++ b/avaframe/ana4Stats/probAna.py @@ -31,60 +31,60 @@ def createComModConfig(cfgProb, avaDir, modName): - """ create configuration file for performing sims with modName com module - - Parameters - ----------- - cfgProb: configParser object - configuration settings - avaDir: pathlib path - path to avalanche directory - modName: module - computational module - - Returns - ------- - cfgFiles: list - list of paths to newly generated configuration files for com module inlcuding - parameter variations + """create configuration file for performing sims with modName com module + + Parameters + ----------- + cfgProb: configParser object + configuration settings + avaDir: pathlib path + path to avalanche directory + modName: module + computational module + + Returns + ------- + cfgFiles: list + list of paths to newly generated configuration files for com module inlcuding + parameter variations """ # setup where configuration file is saved modNameString = str(pathlib.Path(modName.__file__).stem) - outDir = avaDir / 'Work' / ('%sConfigFiles' % modNameString) + outDir = avaDir / "Work" / ("%sConfigFiles" % modNameString) fU.makeADir(outDir) # check variation settings - variationsDict = makeDictFromVars(cfgProb['PROBRUN']) + variationsDict = makeDictFromVars(cfgProb["PROBRUN"]) - if cfgProb['PROBRUN'].getint('samplingStrategy') == 2: - log.info('Probability run performed by varying one parameter at a time - local approach.') + if cfgProb["PROBRUN"].getint("samplingStrategy") == 2: + log.info("Probability run performed by varying one parameter at a time - local approach.") cfgFiles = cfgFilesLocalApproach(variationsDict, cfgProb, modName, outDir) else: - log.info('Probability run perfromed drawing parameter set from full sample.') + log.info("Probability run perfromed drawing parameter set from full sample.") cfgFiles = cfgFilesGlobalApproach(avaDir, cfgProb, modName, outDir) return cfgFiles, outDir def cfgFilesGlobalApproach(avaDir, cfgProb, modName, outDir): - """ create configuration files with all parameter variations - drawn from full sample - for performing sims with modName comModule - - Parameters - ----------- - cfgProb: configParser object - configuration settings - avaDir: pathlib path - path to avalanche directory - modName: module - computational module - - Returns - ------- - cfgFiles: list - list of paths to newly generated configuration files for com module inlcuding parameter - variations + """create configuration files with all parameter variations - drawn from full sample + for performing sims with modName comModule + + Parameters + ----------- + cfgProb: configParser object + configuration settings + avaDir: pathlib path + path to avalanche directory + modName: module + computational module + + Returns + ------- + cfgFiles: list + list of paths to newly generated configuration files for com module inlcuding parameter + variations """ # create sample of all parameter variations @@ -92,17 +92,17 @@ def cfgFilesGlobalApproach(avaDir, cfgProb, modName, outDir): # create plot of parameter sample if variation of two parameters for paramValuesD in paramValuesDList: - if 'releaseScenario' in paramValuesD.keys(): - releaseScenario = paramValuesD['releaseScenario'] + if "releaseScenario" in paramValuesD.keys(): + releaseScenario = paramValuesD["releaseScenario"] else: - releaseScenario = '' - plotDir = avaDir / 'Outputs' / 'ana4Stats' / 'plots' - if len(paramValuesD['names']) == 2: + releaseScenario = "" + plotDir = avaDir / "Outputs" / "ana4Stats" / "plots" + if len(paramValuesD["names"]) == 2: sP.plotSample(paramValuesD, plotDir, releaseScenario=releaseScenario) - elif len(paramValuesD['varParNamesInitial']) == 2: + elif len(paramValuesD["varParNamesInitial"]) == 2: sP.plotThSampleFromVals(paramValuesD, plotDir) else: - log.debug('More or less than two parameters have been varied - no plot of sample available') + log.debug("More or less than two parameters have been varied - no plot of sample available") # write cfg files one for each parameter set drawn from full sample cfgFiles = createCfgFiles(paramValuesDList, modName, cfgProb, cfgPath=outDir) @@ -111,21 +111,21 @@ def cfgFilesGlobalApproach(avaDir, cfgProb, modName, outDir): def cfgFilesLocalApproach(variationsDict, cfgProb, modName, outDir): - """ create configuration file for performing sims with modName com module - - Parameters - ----------- - variationsDict: dict - dictionary with for each varName, varVariation, varSteps, and type of variation - cfgProb: configParser object - configuration settings - modName: module - computational module - - Returns - ------- - cfgFiles: dict - dictionary of paths to newly generated configuration files for com module for all parameters + """create configuration file for performing sims with modName com module + + Parameters + ----------- + variationsDict: dict + dictionary with for each varName, varVariation, varSteps, and type of variation + cfgProb: configParser object + configuration settings + modName: module + computational module + + Returns + ------- + cfgFiles: dict + dictionary of paths to newly generated configuration files for com module for all parameters """ @@ -134,13 +134,13 @@ def cfgFilesLocalApproach(variationsDict, cfgProb, modName, outDir): # define configuration files # get filename of module modNameString = str(pathlib.Path(modName.__file__).stem) - cfgFile = outDir / ('probRun%sCfg%s.ini' % (modNameString, varName)) + cfgFile = outDir / ("probRun%sCfg%s.ini" % (modNameString, varName)) # use cfgFile, local com module settings or default settings if local not available modCfg = fetchStartCfg(modName, cfgProb) modCfg = updateCfgRange(modCfg, cfgProb, varName, variationsDict[varName]) - with open(cfgFile, 'w') as configfile: + with open(cfgFile, "w") as configfile: modCfg.write(configfile) # append cfgFiles to list cfgFiles.append(cfgFile) @@ -149,28 +149,28 @@ def cfgFilesLocalApproach(variationsDict, cfgProb, modName, outDir): def updateCfgRange(cfg, cfgProb, varName, varDict): - """ update cfg with a range for parameters in cfgProb - - Parameters - ----------- - cfg: configparser object - configuration object to update - cfgProb: configParser object - configparser object with info on update - varName: str - name of parameter used for variation - varDict: dict - dictionary with variationValue and numberOfSteps for varName - - Returns - -------- - cfg: configParser - updated configuration object + """update cfg with a range for parameters in cfgProb + + Parameters + ----------- + cfg: configparser object + configuration object to update + cfgProb: configParser object + configparser object with info on update + varName: str + name of parameter used for variation + varDict: dict + dictionary with variationValue and numberOfSteps for varName + + Returns + -------- + cfg: configParser + updated configuration object """ # set reference values of parameters - override values in com module configurations - varParList = cfgProb['PROBRUN']['varParList'].split('|') + varParList = cfgProb["PROBRUN"]["varParList"].split("|") # also for the other parameters that are varied subsequently # first check if no parameter variation in provided for these parameters in the com module ini # if so - error @@ -178,101 +178,125 @@ def updateCfgRange(cfg, cfgProb, varName, varDict): # this is now done for parameter VARNAME from inputs # get range, steps and reference value of parameter to perform variations - valVariation = varDict['variationValue'] - valSteps = varDict['numberOfSteps'] - valVal = cfg['GENERAL'][varName] - variationType = varDict['variationType'] + valVariation = varDict["variationValue"] + valSteps = varDict["numberOfSteps"] + valVal = cfg["GENERAL"][varName] + variationType = varDict["variationType"] - if variationType.lower() == 'normaldistribution': + if variationType.lower() == "normaldistribution": # get computeFromDistribution configuration and apply override - cfgDist = cfgUtils.getModuleConfig(cP, fileOverride='', modInfo=False, toPrint=False, - onlyDefault=cfgProb['in1Data_computeFromDistribution_override'].getboolean('defaultConfig')) + cfgDist = cfgUtils.getModuleConfig( + cP, + fileOverride="", + modInfo=False, + toPrint=False, + onlyDefault=cfgProb["in1Data_computeFromDistribution_override"].getboolean("defaultConfig"), + ) cfgDist, cfgProb = cfgHandling.applyCfgOverride(cfgDist, cfgProb, cP, addModValues=False) # set variation in configuration - if varName in ['relTh', 'entTh', 'secondaryRelTh']: + if varName in ["relTh", "entTh", "secondaryRelTh"]: # if variation using normal distribution - if variationType.lower() == 'normaldistribution': - parName = varName + 'DistVariation' - if valVariation == '': - valVariation = '-' - parValue = (variationType + '$' - + valSteps + '$' + valVariation + '$' - + cfgDist['GENERAL']['minMaxInterval'] + '$' - + cfgDist['GENERAL']['buildType'] + '$' - + cfgDist['GENERAL']['support']) + if variationType.lower() == "normaldistribution": + parName = varName + "DistVariation" + if valVariation == "": + valVariation = "-" + parValue = ( + variationType + + "$" + + valSteps + + "$" + + valVariation + + "$" + + cfgDist["GENERAL"]["minMaxInterval"] + + "$" + + cfgDist["GENERAL"]["buildType"] + + "$" + + cfgDist["GENERAL"]["support"] + ) # if variation using percent - elif variationType.lower() == 'percent': - parName = varName + 'PercentVariation' - parValue = valVariation + '$' + valSteps + elif variationType.lower() == "percent": + parName = varName + "PercentVariation" + parValue = valVariation + "$" + valSteps # if variation using absolute range - elif variationType.lower() == 'range': - parName = varName + 'RangeVariation' - parValue = valVariation + '$' + valSteps - if 'ci' in valVariation: - message = ('Variation Type: range - variationValue is %s not a valid option - only \ - scalar value allowed or consider variationType rangefromci' % - valVariation) + elif variationType.lower() == "range": + parName = varName + "RangeVariation" + parValue = valVariation + "$" + valSteps + if "ci" in valVariation: + message = ( + "Variation Type: range - variationValue is %s not a valid option - only \ + scalar value allowed or consider variationType rangefromci" + % valVariation + ) log.error(message) raise AssertionError(message) - elif variationType.lower() == 'rangefromci': - parName = varName + 'RangeFromCiVariation' - parValue = valVariation + '$' + valSteps + elif variationType.lower() == "rangefromci": + parName = varName + "RangeFromCiVariation" + parValue = valVariation + "$" + valSteps else: - message = ('Variation Type: %s - not a valid option, options are: percent, range, \ - normaldistribution, rangefromci' % variationType) + message = ( + "Variation Type: %s - not a valid option, options are: percent, range, \ + normaldistribution, rangefromci" + % variationType + ) log.error(message) raise AssertionError(message) # write parameter variation for varName in config file - cfg['GENERAL'][parName] = parValue + cfg["GENERAL"][parName] = parValue else: # set variation - if variationType.lower() == 'normaldistribution': - cfgDist = {'sampleSize': valSteps, 'mean': valVal, - 'buildType': cfgProb['in1Data_computeFromDistribution_override']['buildType'], - 'buildValue': valVariation, - 'minMaxInterval': cfgDist['GENERAL']['minMaxInterval'], - 'support': cfgDist['GENERAL']['support']} + if variationType.lower() == "normaldistribution": + cfgDist = { + "sampleSize": valSteps, + "mean": valVal, + "buildType": cfgProb["in1Data_computeFromDistribution_override"]["buildType"], + "buildValue": valVariation, + "minMaxInterval": cfgDist["GENERAL"]["minMaxInterval"], + "support": cfgDist["GENERAL"]["support"], + } _, valValues, _, _ = cP.extractNormalDist(cfgDist) - cfg['GENERAL'][varName] = dP.writeToCfgLine(valValues) - elif variationType.lower() == 'percent': - cfg['GENERAL'][varName] = '%s$%s$%s' % (valVal, valVariation, valSteps) - valValues = fU.splitIniValueToArraySteps(cfg['GENERAL'][varName]) + cfg["GENERAL"][varName] = dP.writeToCfgLine(valValues) + elif variationType.lower() == "percent": + cfg["GENERAL"][varName] = "%s$%s$%s" % (valVal, valVariation, valSteps) + valValues = fU.splitIniValueToArraySteps(cfg["GENERAL"][varName]) - elif variationType.lower() == 'range': - if '-' in valVariation or '+' in valVariation: + elif variationType.lower() == "range": + if "-" in valVariation or "+" in valVariation: valStart = str(float(valVal) + float(valVariation)) valStop = float(valVal) else: valStart = str(float(valVal) - float(valVariation)) valStop = str(float(valVal) + float(valVariation)) - cfg['GENERAL'][varName] = '%s:%s:%s' % (valStart, valStop, valSteps) + cfg["GENERAL"][varName] = "%s:%s:%s" % (valStart, valStop, valSteps) valValues = np.linspace(float(valStart), float(valStop), int(valSteps)) else: - message = ('Variation Type: %s - not a valid option, options are: percent, range, \ - normaldistribution, rangefromci' % variationType) + message = ( + "Variation Type: %s - not a valid option, options are: percent, range, \ + normaldistribution, rangefromci" + % variationType + ) log.error(message) raise AssertionError(message) # add a scenario Name to VISUALISATION - cfg['VISUALISATION']['scenario'] = varName + cfg["VISUALISATION"]["scenario"] = varName return cfg def checkParameterSettings(cfg, varParList): - """ check if parameter settings in comMod configuration do not inlcude variation for parameters to be varied + """check if parameter settings in comMod configuration do not inlcude variation for parameters to be varied - Parameters - ----------- - cfg: configparser object - configuration settings - varParList: list - list of parameters (names) that shall be varied + Parameters + ----------- + cfg: configparser object + configuration settings + varParList: list + list of parameters (names) that shall be varied """ - #set a list of all thickness parameters that are set to be read from shp file + # set a list of all thickness parameters that are set to be read from shp file thReadFromShp = [] for varPar in varParList: @@ -282,18 +306,20 @@ def checkParameterSettings(cfg, varParList): # Fetch section where parameter was found section = fetchParameterSection(cfg, varPar) - if any(chars in cfg[section][varPar] for chars in ['|', '$', ':']): - message = ('Only one reference value is allowed for %s: but %s is given' % - (varPar, cfg[section][varPar])) + if any(chars in cfg[section][varPar] for chars in ["|", "$", ":"]): + message = "Only one reference value is allowed for %s: but %s is given" % ( + varPar, + cfg[section][varPar], + ) log.error(message) raise AssertionError(message) - elif varPar in ['entTh', 'relTh', 'secondaryRelTh']: - thFromShp = varPar + 'FromShp' + elif varPar in ["entTh", "relTh", "secondaryRelTh"]: + thFromShp = varPar + "FromShp" # check if reference settings have already variation of varPar - _ = checkForNumberOfReferenceValues(cfg['GENERAL'], varPar) + _ = checkForNumberOfReferenceValues(cfg["GENERAL"], varPar) # check if th read from shp file - if cfg['GENERAL'].getboolean(thFromShp): - thReadFromShp.append(varPar) + if cfg["GENERAL"].getboolean(thFromShp): + thReadFromShp.append(varPar) return True, thReadFromShp @@ -323,14 +349,15 @@ def checkIfParameterInConfig(cfg, varPar): matches = cfg | grep(varPar, verbose_level=2) # Check if matches is empty - if not matches.get('matched_paths'): + if not matches.get("matched_paths"): message = "'%s' is not a valid parameter" % varPar log.error(message) raise AssertionError(message) # Exact key match (case-insensitive) exactKeyMatch = { - path: val for path, val in matches['matched_paths'].items() + path: val + for path, val in matches["matched_paths"].items() if path.lower().endswith(f"['{varPar.lower()}']") } @@ -365,98 +392,108 @@ def fetchParameterSection(cfg, parameter): return None + def checkForNumberOfReferenceValues(cfgGen, varPar): - """ check if in reference configuration no variation option of varPar is set - if set - throw error + """check if in reference configuration no variation option of varPar is set + if set - throw error - Parameters - ----------- - cfgGen: configparser object - reference configuration settings - varPar: str - name of parameter to be checked + Parameters + ----------- + cfgGen: configparser object + reference configuration settings + varPar: str + name of parameter to be checked """ - thPV = varPar + 'PercentVariation' - thRV = varPar + 'RangeVariation' - thDV = varPar + 'DistVariation' - thRCiV = varPar + 'RangeFromCiVariation' + thPV = varPar + "PercentVariation" + thRV = varPar + "RangeVariation" + thDV = varPar + "DistVariation" + thRCiV = varPar + "RangeFromCiVariation" # check if variation is set - if cfgGen[thPV] != '' or cfgGen[thRV] != '' or cfgGen[thDV] != '' or cfgGen[thRCiV] != '': - message = ('Only one reference value is allowed for %s: but %s %s, %s %s, %s %s, %s %s is given' % - (varPar, thPV, cfgGen[thPV], thRV, cfgGen[thRV], thDV, cfgGen[thDV], thRCiV, cfgGen[thRCiV])) + if cfgGen[thPV] != "" or cfgGen[thRV] != "" or cfgGen[thDV] != "" or cfgGen[thRCiV] != "": + message = "Only one reference value is allowed for %s: but %s %s, %s %s, %s %s, %s %s is given" % ( + varPar, + thPV, + cfgGen[thPV], + thRV, + cfgGen[thRV], + thDV, + cfgGen[thDV], + thRCiV, + cfgGen[thRCiV], + ) log.error(message) raise AssertionError(message) return True -def probAnalysis(avaDir, cfg, modName, parametersDict='', inputDir='', probConf='', simDFActual=''): - """ Compute probability map of a given set of simulation result exceeding a particular threshold and save to outDir - - Parameters - ---------- - avaDir: str - path to avalanche directory - cfg : dict - configuration read from ini file of probAna function - modName - name of computational module that was used to run the simulations - to locate results files and filtering options - parametersDict: dict - dictionary with simulation parameters to filter simulations - only available if modName=com1DFA - inputDir : str - optional - path to directory where data that should be analysed can be found in - a subfolder called peakFiles and configurationFiles, required if not in module results - probConf : str - name of probability configuration - simDFActual: pandas dataFrame - dataframe of simulation configurations that shall be used for prob analysis +def probAnalysis(avaDir, cfg, modName, parametersDict="", inputDir="", probConf="", simDFActual=""): + """Compute probability map of a given set of simulation result exceeding a particular threshold and save to outDir + + Parameters + ---------- + avaDir: str + path to avalanche directory + cfg : dict + configuration read from ini file of probAna function + modName + name of computational module that was used to run the simulations - to locate results files and filtering options + parametersDict: dict + dictionary with simulation parameters to filter simulations - only available if modName=com1DFA + inputDir : str + optional - path to directory where data that should be analysed can be found in + a subfolder called peakFiles and configurationFiles, required if not in module results + probConf : str + name of probability configuration + simDFActual: pandas dataFrame + dataframe of simulation configurations that shall be used for prob analysis """ avaDir = pathlib.Path(avaDir) # set output directory - outDir = avaDir / 'Outputs' / 'ana4Stats' + outDir = avaDir / "Outputs" / "ana4Stats" fU.makeADir(outDir) # fetch all result files and filter simulations according to parametersDict - if modName.lower() == 'com1dfa': + if modName.lower() == "com1dfa": simNameList = cfgHandling.filterSims(avaDir, parametersDict, specDir=inputDir, simDF=simDFActual) filtering = True else: simNameList = [] filtering = False - log.info('No filtering available for this comMod: %s' % modName) + log.info("No filtering available for this comMod: %s" % modName) # initialize flag if analysis has been performed or e.g. no matching files found analysisPerformed = False if simNameList == [] and filtering: # no matching sims found for filtering criteria - log.warning('No matching simulations found for filtering criteria') + log.warning("No matching simulations found for filtering criteria") return analysisPerformed # if matching sims found - perform analysis - if inputDir == '': - inputDir = avaDir / 'Outputs' / modName / 'peakFiles' + if inputDir == "": + inputDir = avaDir / "Outputs" / modName / "peakFiles" peakFilesDF = fU.makeSimDF(inputDir, avaDir=avaDir) else: - inputDirPF = inputDir / 'peakFiles' + inputDirPF = inputDir / "peakFiles" peakFilesDF = fU.makeSimDF(inputDirPF, avaDir=avaDir) if len(peakFilesDF) == 0: - message = 'No peak files found in %s' % str(inputDir) + message = "No peak files found in %s" % str(inputDir) log.error(message) raise FileNotFoundError(message) # get header info from peak files - this should be the same for all peakFiles - header = IOf.readRasterHeader(peakFilesDF['files'][0]) - refData = IOf.readRaster(peakFilesDF['files'][0]) - nRows = header['nrows'] - nCols = header['ncols'] + header = IOf.readRasterHeader(peakFilesDF["files"][0]) + refData = IOf.readRaster(peakFilesDF["files"][0]) + nRows = header["nrows"] + nCols = header["ncols"] # Initialise array for computations probSum = np.zeros((nRows, nCols)) @@ -464,129 +501,144 @@ def probAnalysis(avaDir, cfg, modName, parametersDict='', inputDir='', probConf= contourDict = {} # Loop through peakFiles and compute probability - for m in range(len(peakFilesDF['names'])): - + for m in range(len(peakFilesDF["names"])): # only take simulations that match filter criteria from parametersDict - if (peakFilesDF['simName'][m] in simNameList) or filtering == False: + if (peakFilesDF["simName"][m] in simNameList) or filtering == False: # Load peak field for desired peak field parameter - if peakFilesDF['resType'][m] == cfg['GENERAL']['peakVar']: - + if peakFilesDF["resType"][m] == cfg["GENERAL"]["peakVar"]: # Load data - fileName = peakFilesDF['files'][m] + fileName = peakFilesDF["files"][m] dataLim = np.zeros((nRows, nCols)) fileData = IOf.readRaster(fileName) # check if extent is the same as first loaded dataset # if not - remesh and print warning - if fileData['header']['nrows'] != nRows or fileData['header']['ncols'] != nCols: - log.warning('datasets used to create probMap do not match in extent - remeshing: %s to cellSize %s' % - (fileName, header['cellsize'])) + if fileData["header"]["nrows"] != nRows or fileData["header"]["ncols"] != nCols: + log.warning( + "datasets used to create probMap do not match in extent - remeshing: %s to cellSize %s" + % (fileName, header["cellsize"]) + ) dataRead, _ = gT.resizeData(fileData, refData) else: - dataRead = fileData['rasterData'] + dataRead = fileData["rasterData"] data = np.flipud(dataRead) # fetch contourline info - xGrid, yGrid, _, _ = gT.makeCoordGridFromHeader(refData['header']) - contourDictXY = pU.fetchContourCoords(xGrid, yGrid, fileData['rasterData'], float(cfg['GENERAL']['peakLim'])) + xGrid, yGrid, _, _ = gT.makeCoordGridFromHeader(refData["header"]) + contourDictXY = pU.fetchContourCoords( + xGrid, + yGrid, + fileData["rasterData"], + float(cfg["GENERAL"]["peakLim"]), + ) contourDict[fileName.stem] = contourDictXY - log.info('File Name: %s , simulation parameter %s ' % (fileName, cfg['GENERAL']['peakVar'])) + log.info("File Name: %s , simulation parameter %s " % (fileName, cfg["GENERAL"]["peakVar"])) # Check if peak values exceed desired threshold - dataLim[data > float(cfg['GENERAL']['peakLim'])] = 1.0 + dataLim[data > float(cfg["GENERAL"]["peakLim"])] = 1.0 probSum = probSum + dataLim count = count + 1 # Create probability map ranging from 0-1 probMap = probSum / count - unit = pU.cfgPlotUtils['unit%s' % cfg['GENERAL']['peakVar']] - log.info('probability analysis performed for peak parameter: %s and a peak value ' - 'threshold of: %s %s' % (cfg['GENERAL']['peakVar'], cfg['GENERAL']['peakLim'], unit)) - log.info('%s peak fields added to analysis' % count) + unit = pU.cfgPlotUtils["unit%s" % cfg["GENERAL"]["peakVar"]] + log.info( + "probability analysis performed for peak parameter: %s and a peak value " + "threshold of: %s %s" % (cfg["GENERAL"]["peakVar"], cfg["GENERAL"]["peakLim"], unit) + ) + log.info("%s peak fields added to analysis" % count) # Save to raster file avaName = avaDir.name - outFileName = '%s_prob_%s_%s_lim%s' % (avaName, - probConf, - cfg['GENERAL']['peakVar'], - cfg['GENERAL']['peakLim']) + outFileName = "%s_prob_%s_%s_lim%s" % ( + avaName, + probConf, + cfg["GENERAL"]["peakVar"], + cfg["GENERAL"]["peakLim"], + ) outFile = outDir / outFileName IOf.writeResultToRaster(header, probMap, outFile) - log.info('Prob result written to %s' % outFile) + log.info("Prob result written to %s" % outFile) analysisPerformed = True return analysisPerformed, contourDict def makeDictFromVars(cfg): - """ create a dictionary with info on parameter variation for all parameter in - varParList + """create a dictionary with info on parameter variation for all parameter in + varParList - Parameters - ----------- - cfg: configparser object - configuration settings, here varParList, variationValue, numberOfSteps + Parameters + ----------- + cfg: configparser object + configuration settings, here varParList, variationValue, numberOfSteps - Returns - -------- - variationsDict: dict - dictionary with for each varName, varVariation, varSteps, and type of variation + Returns + -------- + variationsDict: dict + dictionary with for each varName, varVariation, varSteps, and type of variation """ - varParList = cfg['varParList'].split('|') - varParTypes = cfg['varParType'].split('|') - varValues = cfg['variationValue'].split('|') - varSteps = cfg['numberOfSteps'].split('|') - varTypes = cfg['variationType'].split('|') + varParList = cfg["varParList"].split("|") + varParTypes = cfg["varParType"].split("|") + varValues = cfg["variationValue"].split("|") + varSteps = cfg["numberOfSteps"].split("|") + varTypes = cfg["variationType"].split("|") # check if value is provided for each parameter - if cfg.getint('samplingStrategy') == 1: - lengthsPar = 'varParType' - elif cfg.getint('samplingStrategy') == 2: - lengthsPar = 'numberOfSteps' + if cfg.getint("samplingStrategy") == 1: + lengthsPar = "varParType" + elif cfg.getint("samplingStrategy") == 2: + lengthsPar = "numberOfSteps" else: - message = 'Chosen sampling strategy not valid: options are 1 or 2' + message = "Chosen sampling strategy not valid: options are 1 or 2" log.error(message) raise AssertionError(message) - if (len(varParList) == len(varValues) == len(cfg[lengthsPar].split('|')) == len(varTypes)) is False: - message = ('For every parameter in varParList a variationValue, %s and variationType needs to be provided' % lengthsPar) + if (len(varParList) == len(varValues) == len(cfg[lengthsPar].split("|")) == len(varTypes)) is False: + message = ( + "For every parameter in varParList a variationValue, %s and variationType needs to be provided" + % lengthsPar + ) log.error(message) raise AssertionError(message) # check if correct values provided for rangefromci - rangeFromCi = [idx for idx, v in enumerate(varTypes) if v.lower() == 'rangefromci'] + rangeFromCi = [idx for idx, v in enumerate(varTypes) if v.lower() == "rangefromci"] varValuesRCi = np.asarray(varValues)[rangeFromCi] - ciCheck = [False for ci in varValuesRCi if ci != 'ci95'] + ciCheck = [False for ci in varValuesRCi if ci != "ci95"] if len(ciCheck) > 0: - message = 'If rangefromci is chosen as variation type, ci95 is required as variationValue' + message = "If rangefromci is chosen as variation type, ci95 is required as variationValue" log.error(message) raise AssertionError(message) variationsDict = {} - if cfg.getint('samplingStrategy') == 2: + if cfg.getint("samplingStrategy") == 2: for idx, val in enumerate(varParList): - variationsDict[val] = {'variationValue': varValues[idx], 'numberOfSteps': varSteps[idx], - 'variationType': varTypes[idx]} + variationsDict[val] = { + "variationValue": varValues[idx], + "numberOfSteps": varSteps[idx], + "variationType": varTypes[idx], + } return variationsDict def fetchThicknessInfo(avaDir): - """ Fetch input data for avaDir and thickness info + """Fetch input data for avaDir and thickness info - Parameters - ------------ - avaDir: pathlib path or str - path to avalanche directory + Parameters + ------------ + avaDir: pathlib path or str + path to avalanche directory - Returns - ----------- - inputSimFilesAll: dict - dictionary with info on available input data (release areas, entrainment, and thickness info) + Returns + ----------- + inputSimFilesAll: dict + dictionary with info on available input data (release areas, entrainment, and thickness info) """ # fetch input data - dem, release-, entrainment- and resistance areas (and secondary release areas) @@ -599,39 +651,39 @@ def fetchThicknessInfo(avaDir): def createSampleFromConfig(avaDir, cfgProb, comMod): - """ Create a sample of parameters for a desired parameter variation, - and draw nSample sets of parameter values - if thickness values read from shp for comMod, convert sample values for these + """Create a sample of parameters for a desired parameter variation, + and draw nSample sets of parameter values + if thickness values read from shp for comMod, convert sample values for these - Parameters - ------------ - avaDir: pathlib path - path to avalanche directory - cfgProb: configparser object - configuration settings for parameter variation - comMod: computational module - module to perform then sims for parameter variation + Parameters + ------------ + avaDir: pathlib path + path to avalanche directory + cfgProb: configparser object + configuration settings for parameter variation + comMod: computational module + module to perform then sims for parameter variation - Returns - -------- - paramValuesDList: list - list of paramValuesD (multiple if multiple release area scenarios) + Returns + -------- + paramValuesDList: list + list of paramValuesD (multiple if multiple release area scenarios) - - names: list, list of parameter names (that are varied) - - values: numpy nd array, as many rows as sets of parameter values and as many rows as parameters - - typeList: list, list of types of parameters (float, ...) - - thFromIni: str, str of parameter names where the base value is read from shape + - names: list, list of parameter names (that are varied) + - values: numpy nd array, as many rows as sets of parameter values and as many rows as parameters + - typeList: list, list of types of parameters (float, ...) + - thFromIni: str, str of parameter names where the base value is read from shape - """ + """ # read initial configuration cfgStart = fetchStartCfg(comMod, cfgProb) # fetch parameter names for parameter variation and variation value and variation type - varParList = cfgProb['PROBRUN']['varParList'].split('|') - valVariationValue = cfgProb['PROBRUN']['variationValue'].split('|') - varType = cfgProb['PROBRUN']['variationType'].split('|') + varParList = cfgProb["PROBRUN"]["varParList"].split("|") + valVariationValue = cfgProb["PROBRUN"]["variationValue"].split("|") + varType = cfgProb["PROBRUN"]["variationType"].split("|") # check if thickness parameters are actually read from shp file _, thReadFromShp = checkParameterSettings(cfgStart, varParList) @@ -644,24 +696,32 @@ def createSampleFromConfig(avaDir, cfgProb, comMod): # create sets of parameters values for parameter variation if len(thReadFromShp) > 0: - paramValuesDList = createSampleWithVariationForThParameters(avaDir, cfgProb, cfgStart, varParList, - valVariationValue, varType, thReadFromShp) + paramValuesDList = createSampleWithVariationForThParameters( + avaDir, + cfgProb, + cfgStart, + varParList, + valVariationValue, + varType, + thReadFromShp, + ) else: - paramValuesD = createSampleWithVariationStandardParameters(cfgProb, cfgStart, varParList, valVariationValue, - varType) + paramValuesD = createSampleWithVariationStandardParameters( + cfgProb, cfgStart, varParList, valVariationValue, varType + ) paramValuesDList = [paramValuesD] # save dictionary to pickle file - outDir = pathlib.Path(avaDir, 'Outputs', 'ana4Stats') + outDir = pathlib.Path(avaDir, "Outputs", "ana4Stats") fU.makeADir(outDir) - with open(outDir / 'paramValuesD.pickle', 'wb') as fi: + with open(outDir / "paramValuesD.pickle", "wb") as fi: pickle.dump(paramValuesDList[0], fi) return paramValuesDList def createSampleWithVariationStandardParameters(cfgProb, cfgStart, varParList, valVariationValue, varType): - """ create a sample for a parameter variation using latin hypercube sampling + """create a sample for a parameter variation using latin hypercube sampling Parameters ------------ @@ -694,14 +754,14 @@ def createSampleWithVariationStandardParameters(cfgProb, cfgStart, varParList, v for idx, varPar in enumerate(varParList): section = fetchParameterSection(cfgStart, varPar) varVal = cfgStart[section].getfloat(varPar) - if varType[idx].lower() == 'percent': - lB = varVal - varVal * (float(valVariationValue[idx]) / 100.) - uB = varVal + varVal * (float(valVariationValue[idx]) / 100.) - elif varType[idx].lower() == 'range': + if varType[idx].lower() == "percent": + lB = varVal - varVal * (float(valVariationValue[idx]) / 100.0) + uB = varVal + varVal * (float(valVariationValue[idx]) / 100.0) + elif varType[idx].lower() == "range": lB = varVal - float(valVariationValue[idx]) uB = varVal + float(valVariationValue[idx]) else: - message = ('Variation method: %s not a valid option' % varType[idx]) + message = "Variation method: %s not a valid option" % varType[idx] log.error(message) raise AssertionError(message) # update bounds @@ -713,45 +773,49 @@ def createSampleWithVariationStandardParameters(cfgProb, cfgStart, varParList, v sampleWBounds = qmc.scale(sample, lowerBounds, upperBounds) # create dictionary with all the info - paramValuesD = {'names': varParList, - 'values': sampleWBounds, - 'typeList': cfgProb['PROBRUN']['varParType'].split('|'), - 'thFromIni': ''} + paramValuesD = { + "names": varParList, + "values": sampleWBounds, + "typeList": cfgProb["PROBRUN"]["varParType"].split("|"), + "thFromIni": "", + } return paramValuesD -def createSampleWithVariationForThParameters(avaDir, cfgProb, cfgStart, varParList, valVariationValue, varType, thReadFromShp): - """ Create a sample of parameters for a desired parameter variation, - and fetch thickness values from shp file and perform variation for each feature within - shapefile but treating the features of one shapefile as not-independent +def createSampleWithVariationForThParameters( + avaDir, cfgProb, cfgStart, varParList, valVariationValue, varType, thReadFromShp +): + """Create a sample of parameters for a desired parameter variation, + and fetch thickness values from shp file and perform variation for each feature within + shapefile but treating the features of one shapefile as not-independent - paramsValuesD dict in output list contains + paramsValuesD dict in output list contains - Parameters - ------------ - cfgProb: configparser object - configuration settings for parameter variation - cfgStart: configparser object - configuration settings for comMod without variation values - varParList: list - list of parameters that shall be varied - valVariationValue: list - list if value used for variation - varType: list - list of type of variation for each parameter (percent, range, rangefromci) + Parameters + ------------ + cfgProb: configparser object + configuration settings for parameter variation + cfgStart: configparser object + configuration settings for comMod without variation values + varParList: list + list of parameters that shall be varied + valVariationValue: list + list if value used for variation + varType: list + list of type of variation for each parameter (percent, range, rangefromci) - Returns - -------- - paramValuesDList: list - list of paramValuesD (multiple if multiple release area scenarios) + Returns + -------- + paramValuesDList: list + list of paramValuesD (multiple if multiple release area scenarios) - - names: list, list of parameter names (that are varied) - - values: numpy nd array, as many rows as sets of parameter values and as many rows as parameters - - typeList: list, list of types of parameters (float, ...) - - thFromIni: str, str of parameter names where the base value is read from shape + - names: list, list of parameter names (that are varied) + - values: numpy nd array, as many rows as sets of parameter values and as many rows as parameters + - typeList: list, list of types of parameters (float, ...) + - thFromIni: str, str of parameter names where the base value is read from shape """ @@ -759,7 +823,7 @@ def createSampleWithVariationForThParameters(avaDir, cfgProb, cfgStart, varParLi inputSimFiles = fetchThicknessInfo(avaDir) paramValuesDList = [] - for iRel, relF in enumerate(inputSimFiles['relFiles']): + for iRel, relF in enumerate(inputSimFiles["relFiles"]): paramValuesD = {} # create lower and upper bounds for all thickness parameters - taking into account all features fullListOfParameters = [] @@ -769,11 +833,13 @@ def createSampleWithVariationForThParameters(avaDir, cfgProb, cfgStart, varParLi ciValues = np.asarray([]) for idx1, varPar in enumerate(varParList): if varPar in thReadFromShp: - ciRequired = varType[idx1].lower() == 'rangefromci' - thV, ciV, thFeatureNames = fetchThThicknessLists(varPar, inputSimFiles, relF, ciRequired=ciRequired) + ciRequired = varType[idx1].lower() == "rangefromci" + thV, ciV, thFeatureNames = fetchThThicknessLists( + varPar, inputSimFiles, relF, ciRequired=ciRequired + ) # add to list all the parameter names fullListOfParameters = fullListOfParameters + thFeatureNames - parentParameterId = parentParameterId + [varParList.index(varPar)]*len(thFeatureNames) + parentParameterId = parentParameterId + [varParList.index(varPar)] * len(thFeatureNames) thValues = np.append(thValues, thV) ciValues = np.append(ciValues, ciV) else: @@ -785,29 +851,47 @@ def createSampleWithVariationForThParameters(avaDir, cfgProb, cfgStart, varParLi # initialize lower and upper bounds required to get a sample for the parameter values # numpy arrays required to do masking as lists don't work for a list indices - varValList = np.asarray([cfgStart[fetchParameterSection(cfgStart, varPar)].getfloat(varPar) - if varPar in staParameter - else thValues[idx] for idx, varPar in enumerate(fullListOfParameters)]) - fullValVar = np.asarray([float(valVariationValue[i]) if valVariationValue[i] != 'ci95' else np.nan for i in parentParameterId]) + varValList = np.asarray( + [ + ( + cfgStart[fetchParameterSection(cfgStart, varPar)].getfloat(varPar) + if varPar in staParameter + else thValues[idx] + ) + for idx, varPar in enumerate(fullListOfParameters) + ] + ) + fullValVar = np.asarray( + [ + float(valVariationValue[i]) if valVariationValue[i] != "ci95" else np.nan + for i in parentParameterId + ] + ) fullVarType = np.asarray([varType[i].lower() for i in parentParameterId]) - lowerBounds = np.asarray([None]*len(fullListOfParameters)) - upperBounds = np.asarray([None]*len(fullListOfParameters)) + lowerBounds = np.asarray([None] * len(fullListOfParameters)) + upperBounds = np.asarray([None] * len(fullListOfParameters)) # set lower and upper bounds depending on varType (percent, range, rangefromci) - lowerBounds[fullVarType == 'percent'] = (varValList[fullVarType == 'percent'] - - varValList[fullVarType == 'percent'] * (fullValVar[fullVarType == 'percent'] / 100.)) - upperBounds[fullVarType == 'percent'] = (varValList[fullVarType == 'percent'] + - varValList[fullVarType == 'percent'] * (fullValVar[fullVarType == 'percent'] / 100.)) - - lowerBounds[fullVarType == 'range'] = (varValList[fullVarType == 'range'] - - fullValVar[fullVarType == 'range']) - upperBounds[fullVarType == 'range'] = (varValList[fullVarType == 'range'] + - fullValVar[fullVarType == 'range']) + lowerBounds[fullVarType == "percent"] = varValList[fullVarType == "percent"] - varValList[ + fullVarType == "percent" + ] * (fullValVar[fullVarType == "percent"] / 100.0) + upperBounds[fullVarType == "percent"] = varValList[fullVarType == "percent"] + varValList[ + fullVarType == "percent" + ] * (fullValVar[fullVarType == "percent"] / 100.0) + + lowerBounds[fullVarType == "range"] = ( + varValList[fullVarType == "range"] - fullValVar[fullVarType == "range"] + ) + upperBounds[fullVarType == "range"] = ( + varValList[fullVarType == "range"] + fullValVar[fullVarType == "range"] + ) - lowerBounds[fullVarType == 'rangefromci'] = (varValList[fullVarType == 'rangefromci'] - - ciValues[fullVarType == 'rangefromci']) - upperBounds[fullVarType == 'rangefromci'] = (varValList[fullVarType == 'rangefromci'] + - ciValues[fullVarType == 'rangefromci']) + lowerBounds[fullVarType == "rangefromci"] = ( + varValList[fullVarType == "rangefromci"] - ciValues[fullVarType == "rangefromci"] + ) + upperBounds[fullVarType == "rangefromci"] = ( + varValList[fullVarType == "rangefromci"] + ciValues[fullVarType == "rangefromci"] + ) # create a sample of parameter values using scipy latin hypercube or morris sampling sample = createSample(cfgProb, varParList) @@ -815,15 +899,19 @@ def createSampleWithVariationForThParameters(avaDir, cfgProb, cfgStart, varParLi # create a full sample including those thickness values for the potentially multiple features # however, the thickness values for one parameter (relTh or entTh or secondaryRelTh) should not # be independent for the different features within one parameter - if cfgProb['PROBRUN']['sampleMethod'] == 'morris': + if cfgProb["PROBRUN"]["sampleMethod"] == "morris": fullSample = np.zeros( - (int(cfgProb['PROBRUN']['nSample']) * (len(varParList) + 1), len(fullListOfParameters))) + ( + int(cfgProb["PROBRUN"]["nSample"]) * (len(varParList) + 1), + len(fullListOfParameters), + ) + ) else: - fullSample = np.zeros((int(cfgProb['PROBRUN']['nSample']), len(fullListOfParameters))) + fullSample = np.zeros((int(cfgProb["PROBRUN"]["nSample"]), len(fullListOfParameters))) for idx, varPar in enumerate(fullListOfParameters): - lB = [0]*len(varParList) - uB = [1]*len(varParList) + lB = [0] * len(varParList) + uB = [1] * len(varParList) lB[parentParameterId[idx]] = lowerBounds[idx] uB[parentParameterId[idx]] = upperBounds[idx] parSample = qmc.scale(sample, lB, uB) @@ -831,13 +919,15 @@ def createSampleWithVariationForThParameters(avaDir, cfgProb, cfgStart, varParLi # create dictionary with all the info thFromIni = cfgUtils.convertToCfgList(list(set(varParList).symmetric_difference(set(staParameter)))) - paramValuesD = {'names': fullListOfParameters, - 'values': fullSample, - 'typeList': cfgProb['PROBRUN']['varParType'].split('|'), - 'thFromIni': thFromIni, - 'thVariationBasedOnFromShp': thReadFromShp, - 'varParNamesInitial': varParList, - 'releaseScenario': relF.stem} + paramValuesD = { + "names": fullListOfParameters, + "values": fullSample, + "typeList": cfgProb["PROBRUN"]["varParType"].split("|"), + "thFromIni": thFromIni, + "thVariationBasedOnFromShp": thReadFromShp, + "varParNamesInitial": varParList, + "releaseScenario": relF.stem, + } paramValuesDList.append(paramValuesD) @@ -845,47 +935,47 @@ def createSampleWithVariationForThParameters(avaDir, cfgProb, cfgStart, varParLi def createSample(cfgProb, varParList): - """ create a sample of parameters - - Parameters - ----------- - cfgProb: configparser object - configuration settings - varParList: list - list of parameters used for creating a sample - - Returns - -------- - sample: scipy object - sample object of given dimension that can be adjusted to desired bounds + """create a sample of parameters + + Parameters + ----------- + cfgProb: configparser object + configuration settings + varParList: list + list of parameters used for creating a sample + + Returns + -------- + sample: scipy object + sample object of given dimension that can be adjusted to desired bounds """ # random generator initialized with seed - sampleSeed = cfgProb['PROBRUN'].getint('sampleSeed') + sampleSeed = cfgProb["PROBRUN"].getint("sampleSeed") randomGen = np.random.default_rng(sampleSeed) - nTrajectories = cfgProb['PROBRUN'].getint('nSample') + nTrajectories = cfgProb["PROBRUN"].getint("nSample") # create a sample of parameter values using salib morris sampling - if cfgProb['PROBRUN']['sampleMethod'].lower() == 'morris': + if cfgProb["PROBRUN"]["sampleMethod"].lower() == "morris": param_ranges = { - 'num_vars': len(varParList), - 'names': varParList, - 'bounds': [[0, 1]] * len(varParList) + "num_vars": len(varParList), + "names": varParList, + "bounds": [[0, 1]] * len(varParList), } sample = morris.sample( param_ranges, N=nTrajectories, # number of trajectories num_levels=6, # how many discrete values per parameter - seed=sampleSeed + seed=sampleSeed, ) # create a sample of parameter values using scipy latin hypercube sampling - elif cfgProb['PROBRUN']['sampleMethod'].lower() == 'latin': + elif cfgProb["PROBRUN"]["sampleMethod"].lower() == "latin": sampler = qmc.LatinHypercube(d=len(varParList), seed=randomGen) - sample = sampler.random(n=int(cfgProb['PROBRUN']['nSample'])) - log.info('Parameter sample created using latin hypercube sampling') + sample = sampler.random(n=int(cfgProb["PROBRUN"]["nSample"])) + log.info("Parameter sample created using latin hypercube sampling") else: - message = ('Sampling method: %s not a valid option' % cfgProb['PROBRUN']['sampleMethod']) + message = "Sampling method: %s not a valid option" % cfgProb["PROBRUN"]["sampleMethod"] log.error(message) raise AssertionError(message) @@ -893,68 +983,72 @@ def createSample(cfgProb, varParList): def fetchThThicknessLists(varPar, inputSimFiles, releaseFile, ciRequired=False): - """ fetch the desired thickness shp file info on thickness, id and ci values - of all available features in shp file - - Parameters - ----------- - varPar: str - name of thickness parameter - inputSimFiles: dict - dictionary with info in input data - ciRequired: bool - if True throw error if ci Values not provided - - Returns - -------- - thicknessFeatureNames: list - list of names of thickness features - thValues: list - list of thickness values for all features - ciValues: list - list of ci values for all feature + """fetch the desired thickness shp file info on thickness, id and ci values + of all available features in shp file + + Parameters + ----------- + varPar: str + name of thickness parameter + inputSimFiles: dict + dictionary with info in input data + ciRequired: bool + if True throw error if ci Values not provided + + Returns + -------- + thicknessFeatureNames: list + list of names of thickness features + thValues: list + list of thickness values for all features + ciValues: list + list of ci values for all feature """ - if varPar == 'relTh': - thFile = [inputSimFiles['relFiles'][idx] for idx, relF in enumerate(inputSimFiles['relFiles']) if relF == releaseFile][0] - elif varPar == 'entTh': - thFile = inputSimFiles['entFile'] - elif varPar == 'secondaryRelTh': - thFile = inputSimFiles['secondaryReleaseFile'] + if varPar == "relTh": + thFile = [ + inputSimFiles["relFiles"][idx] + for idx, relF in enumerate(inputSimFiles["relFiles"]) + if relF == releaseFile + ][0] + elif varPar == "entTh": + thFile = inputSimFiles["entFile"] + elif varPar == "secondaryRelTh": + thFile = inputSimFiles["secondaryReleaseFile"] infoDict = inputSimFiles[thFile.stem] - thicknessFeatureNames = [varPar+str(id) for id in infoDict['id']] - thValues = [float(th) for th in infoDict['thickness']] - ciValues = [float(ci) if ci != 'None' else np.nan for ci in infoDict['ci95']] + thicknessFeatureNames = [varPar + str(id) for id in infoDict["id"]] + thValues = [float(th) for th in infoDict["thickness"]] + ciValues = [float(ci) if ci != "None" else np.nan for ci in infoDict["ci95"]] if np.nan in ciValues and ciRequired: - msg = ('ci95 values required in shape file but not provided for %s' % varPar) + msg = "ci95 values required in shape file but not provided for %s" % varPar log.error(msg) raise AssertionError(msg) return thValues, ciValues, thicknessFeatureNames -def createCfgFiles(paramValuesDList, comMod, cfg, cfgPath=''): - """ create all config files required to run com Module from parameter variations using paramValues +def createCfgFiles(paramValuesDList, comMod, cfg, cfgPath=""): + """create all config files required to run com Module from parameter variations using paramValues - Parameters - ----------- - paramValuesDList: list - list of dictionaries with parameter names and values (array of all sets of parameter values, - one row per value set) - multiple dictionaries if multiple release area scenarios and thFromShp - comMod: com module - computational module - cfg: configparser object - configuration settings - cfgPath: str - path where cfg files should be saved to + Parameters + ----------- + paramValuesDList: list + list of dictionaries with parameter names and values (array of all sets of parameter values, + one row per value set) + multiple dictionaries if multiple release area scenarios and thFromShp + comMod: com module + computational module + cfg: configparser object + configuration settings + cfgPath: str + path where cfg files should be saved to - Returns - -------- - cfgFiles: list - list of cfg file paths for comMod including the updated values of the parameters to vary + Returns + -------- + cfgFiles: list + list of cfg file paths for comMod including the updated values of the parameters to vary """ @@ -967,21 +1061,21 @@ def createCfgFiles(paramValuesDList, comMod, cfg, cfgPath=''): for paramValuesD in paramValuesDList: # read initial configuration cfgStart = fetchStartCfg(comMod, cfg) - for count1, pVal in enumerate(paramValuesD['values']): - for index, par in enumerate(paramValuesD['names']): + for count1, pVal in enumerate(paramValuesD["values"]): + for index, par in enumerate(paramValuesD["names"]): section = fetchParameterSection(cfgStart, par) # If parameter not found in any section, add it to 'GENERAL'. if section is not None: cfgStart[section][par] = str(pVal[index]) else: - cfgStart['GENERAL'][par] = str(pVal[index]) - if modName.lower() == 'com1dfa': - cfgStart['VISUALISATION']['scenario'] = str(count1) - cfgStart['INPUT']['thFromIni'] = paramValuesD['thFromIni'] - if 'releaseScenario' in paramValuesD.keys(): - cfgStart['INPUT']['releaseScenario'] = paramValuesD['releaseScenario'] - cfgF = pathlib.Path(cfgPath, ('%d_%sCfg.ini' % (countS, modName))) - with open(cfgF, 'w') as configfile: + cfgStart["GENERAL"][par] = str(pVal[index]) + if modName.lower() == "com1dfa": + cfgStart["VISUALISATION"]["scenario"] = str(count1) + cfgStart["INPUT"]["thFromIni"] = paramValuesD["thFromIni"] + if "releaseScenario" in paramValuesD.keys(): + cfgStart["INPUT"]["releaseScenario"] = paramValuesD["releaseScenario"] + cfgF = pathlib.Path(cfgPath, ("%d_%sCfg.ini" % (countS, modName))) + with open(cfgF, "w") as configfile: cfgStart.write(configfile) # append file path to list of cfg files cfgFiles.append(cfgF) @@ -991,28 +1085,32 @@ def createCfgFiles(paramValuesDList, comMod, cfg, cfgPath=''): def fetchStartCfg(comMod, cfgProb): - """ fetch start configuration of comMod - if onlyDefault use default comModCfg.ini and if false check if there is a local_comModCfg.ini - - Parameters - ----------- - comMod: computational module - module where configuration is read from - cfgProb: configparser object - configuration settings of probAna with collection_comMod_override section - - Returns - -------- - cfgStart: configparser object - configuration object of comMod + """fetch start configuration of comMod + if onlyDefault use default comModCfg.ini and if false check if there is a local_comModCfg.ini + + Parameters + ----------- + comMod: computational module + module where configuration is read from + cfgProb: configparser object + configuration settings of probAna with collection_comMod_override section + + Returns + -------- + cfgStart: configparser object + configuration object of comMod """ # get filename of module modName = str(pathlib.Path(comMod.__file__).stem) modP = (pathlib.Path(comMod.__file__).resolve().parent).stem # fetch comMod config - cfgStart = cfgUtils.getModuleConfig(comMod, fileOverride='', toPrint=False, - onlyDefault=cfgProb['%s_%s_override' % (modP, modName)].getboolean('defaultConfig')) + cfgStart = cfgUtils.getModuleConfig( + comMod, + fileOverride="", + toPrint=False, + onlyDefault=cfgProb["%s_%s_override" % (modP, modName)].getboolean("defaultConfig"), + ) # override with parameters set in in the cfgProb comMod_override section cfgStart, cfgProb = cfgHandling.applyCfgOverride(cfgStart, cfgProb, comMod, addModValues=False) @@ -1021,28 +1119,30 @@ def fetchStartCfg(comMod, cfgProb): def fetchProbConfigs(cfg): - """ fetch configurations of prob run in order to filter simulations - e.g. to create probability maps for different scenarios - - Parameters - ----------- - cfg: configparser object - configuration setting, here used: samplingStrategy, varParList - - Returns - -------- - probConfigs: dict - dictionary with one key per config and a dict per key with parameter and value + """fetch configurations of prob run in order to filter simulations + e.g. to create probability maps for different scenarios + + Parameters + ----------- + cfg: configparser object + configuration setting, here used: samplingStrategy, varParList + + Returns + -------- + probConfigs: dict + dictionary with one key per config and a dict per key with parameter and value """ - probConfigs = {'includeAll': {}} + probConfigs = {"includeAll": {}} - if cfg.getint('samplingStrategy') == 2: - for par in cfg['varParList'].split('|'): - probConfigs['include' + par] = {'scenario': par} - log.info('Probability maps are created for full parameter variation and for %s separately' % - cfg['varParList']) + if cfg.getint("samplingStrategy") == 2: + for par in cfg["varParList"].split("|"): + probConfigs["include" + par] = {"scenario": par} + log.info( + "Probability maps are created for full parameter variation and for %s separately" + % cfg["varParList"] + ) else: - log.info('Probability map is created for full parameter variation') + log.info("Probability map is created for full parameter variation") return probConfigs diff --git a/avaframe/in1Data/getInput.py b/avaframe/in1Data/getInput.py index 9f63385b3..8450291fc 100644 --- a/avaframe/in1Data/getInput.py +++ b/avaframe/in1Data/getInput.py @@ -1,5 +1,5 @@ """ - Fetch input data for avalanche simulations +Fetch input data for avalanche simulations """ import logging @@ -250,12 +250,14 @@ def getInputDataCom1DFA(avaDir): demFile = getDEMPath(avaDir) # check if frictionParameter file is available - muFile, entResInfo["mu"] = getAndCheckInputFiles(inputDir, "RASTERS", "mu parameter data", fileExt="raster", - fileSuffix='_mu') + muFile, entResInfo["mu"] = getAndCheckInputFiles( + inputDir, "RASTERS", "mu parameter data", fileExt="raster", fileSuffix="_mu" + ) # check if frictionParameter file is available - xiFile, entResInfo["xi"] = getAndCheckInputFiles(inputDir, "RASTERS", "xi parameter data", fileExt="raster", - fileSuffix='_xi') + xiFile, entResInfo["xi"] = getAndCheckInputFiles( + inputDir, "RASTERS", "xi parameter data", fileExt="raster", fileSuffix="_xi" + ) # return DEM, first item of release, entrainment and resistance areas inputSimFiles = { @@ -327,7 +329,12 @@ def getAndCheckInputFiles(inputDir, folder, inputType, fileExt="shp", fileSuffix if len(OutputFile) < 1: OutputFile = None elif len(OutputFile) > 1: - message = "More than one %s .%s file in %s/%s/ not allowed" % (inputType, fileExt, inputDir, folder) + message = "More than one %s .%s file in %s/%s/ not allowed" % ( + inputType, + fileExt, + inputDir, + folder, + ) log.error(message) raise AssertionError(message) else: @@ -335,7 +342,9 @@ def getAndCheckInputFiles(inputDir, folder, inputType, fileExt="shp", fileSuffix OutputFile = OutputFile[0] if OutputFile.suffix not in supportedFileFormats: - message = "Unsupported file format found for OutputFile %s; shp, asc, tif are allowed" % OutputFile + message = ( + "Unsupported file format found for OutputFile %s; shp, asc, tif are allowed" % OutputFile + ) log.error(message) raise AssertionError(message) @@ -375,7 +384,11 @@ def getThicknessInputSimFiles(inputSimFiles): for releaseA in inputSimFiles["relFiles"]: # fetch thickness and id info from input data thicknessList, idList, ci95List = shpConv.readThickness(releaseA) - inputSimFiles[releaseA.stem] = {"thickness": thicknessList, "id": idList, "ci95": ci95List} + inputSimFiles[releaseA.stem] = { + "thickness": thicknessList, + "id": idList, + "ci95": ci95List, + } # append release scenario name to list releaseScenarioList.append(releaseA.stem) @@ -443,7 +456,10 @@ def updateThicknessCfg(inputSimFiles, cfgInitial): cfgInitial["INPUT"]["entrainmentScenario"] = inputSimFiles["entFile"].stem if inputSimFiles["secondaryReleaseFile"] != None and "secondaryReleaseFile" in thTypeList: cfgInitial = dP.getThicknessValue( - cfgInitial, inputSimFiles, inputSimFiles["secondaryReleaseFile"].stem, "secondaryRelTh" + cfgInitial, + inputSimFiles, + inputSimFiles["secondaryReleaseFile"].stem, + "secondaryRelTh", ) cfgInitial["INPUT"]["secondaryReleaseScenario"] = inputSimFiles["secondaryReleaseFile"].stem diff --git a/avaframe/in3Utils/cfgUtils.py b/avaframe/in3Utils/cfgUtils.py index 7fa1b8b3e..5d06df4d3 100644 --- a/avaframe/in3Utils/cfgUtils.py +++ b/avaframe/in3Utils/cfgUtils.py @@ -1,7 +1,7 @@ -''' - Utilities for handling configuration files +""" +Utilities for handling configuration files -''' +""" import configparser import logging @@ -27,25 +27,25 @@ log = logging.getLogger(__name__) -def getGeneralConfig(nameFile=''): - ''' Returns the general configuration for avaframe +def getGeneralConfig(nameFile=""): + """Returns the general configuration for avaframe returns a configParser object Parameters ---------- nameFile: pathlib path optional full path to file, if empty use avaframeCfg from folder one level up - ''' + """ # get path of module modPath = pathlib.Path(avaf.__file__).resolve().parent if isinstance(nameFile, pathlib.Path): - localFile = nameFile.parents[0] / ('local_' + nameFile.name) + localFile = nameFile.parents[0] / ("local_" + nameFile.name) defaultFile = nameFile else: - localFile = modPath / 'local_avaframeCfg.ini' - defaultFile = modPath / 'avaframeCfg.ini' + localFile = modPath / "local_avaframeCfg.ini" + defaultFile = modPath / "avaframeCfg.ini" if localFile.is_file(): iniFile = localFile @@ -55,16 +55,16 @@ def getGeneralConfig(nameFile=''): iniFile = defaultFile compare = False else: - raise FileNotFoundError('None of the provided cfg files exist ') + raise FileNotFoundError("None of the provided cfg files exist ") # Finally read it - cfg, _ = readCompareConfig(iniFile, 'General', compare) + cfg, _ = readCompareConfig(iniFile, "General", compare) return cfg -def getModuleConfig(module, fileOverride='', modInfo=False, toPrint=True, onlyDefault=False): - ''' Returns the configuration for a given module +def getModuleConfig(module, fileOverride="", modInfo=False, toPrint=True, onlyDefault=False): + """Returns the configuration for a given module returns a configParser object module object: module : the calling function provides the already imported @@ -87,10 +87,10 @@ def getModuleConfig(module, fileOverride='', modInfo=False, toPrint=True, onlyDe Order is as follows: fileOverride -> local_MODULECfg.ini -> MODULECfg.ini - ''' + """ if isinstance(onlyDefault, bool) == False: - message = 'OnlyDefault parameter is not a boolean but %s' % type(onlyDefault) + message = "OnlyDefault parameter is not a boolean but %s" % type(onlyDefault) log.error(message) raise TypeError(message) @@ -101,11 +101,11 @@ def getModuleConfig(module, fileOverride='', modInfo=False, toPrint=True, onlyDe else: modPath, modName = getModPathName(module) - localFile = modPath / ('local_'+modName+'Cfg.ini') - defaultFile = modPath / (modName+'Cfg.ini') + localFile = modPath / ("local_" + modName + "Cfg.ini") + defaultFile = modPath / (modName + "Cfg.ini") - log.debug('localFile: %s', localFile) - log.debug('defaultFile: %s', defaultFile) + log.debug("localFile: %s", localFile) + log.debug("defaultFile: %s", defaultFile) # Decide which one to take if fileOverride: @@ -114,8 +114,7 @@ def getModuleConfig(module, fileOverride='', modInfo=False, toPrint=True, onlyDe iniFile = [defaultFile, fileOverride] compare = True else: - raise FileNotFoundError('Provided fileOverride does not exist: ' + - str(fileOverride)) + raise FileNotFoundError("Provided fileOverride does not exist: " + str(fileOverride)) elif localFile.is_file() and not onlyDefault: iniFile = localFile @@ -125,7 +124,7 @@ def getModuleConfig(module, fileOverride='', modInfo=False, toPrint=True, onlyDe iniFile = defaultFile compare = False else: - raise FileNotFoundError('None of the provided cfg files exist ') + raise FileNotFoundError("None of the provided cfg files exist ") # Finally read it cfg, modDict = readCompareConfig(iniFile, modName, compare, toPrint) @@ -136,7 +135,7 @@ def getModuleConfig(module, fileOverride='', modInfo=False, toPrint=True, onlyDe def getDefaultModuleConfig(module, toPrint=True): - ''' Returns the default configuration for a given module + """Returns the default configuration for a given module returns a configParser object module object: module : the calling function provides the already imported @@ -147,15 +146,15 @@ def getDefaultModuleConfig(module, toPrint=True): from avaframe.com2AB import com2AB as c2 leads to getModuleConfig(c2) - ''' + """ # get path to the module and its name modPath, modName = getModPathName(module) - defaultFile = modPath / (modName+'Cfg.ini') + defaultFile = modPath / (modName + "Cfg.ini") - log.info('Getting the default config for %s', modName) - log.debug('defaultFile: %s', defaultFile) + log.info("Getting the default config for %s", modName) + log.debug("defaultFile: %s", defaultFile) # Finally read it cfg, _ = readCompareConfig(defaultFile, modName, compare=False, toPrint=toPrint) @@ -164,7 +163,7 @@ def getDefaultModuleConfig(module, toPrint=True): def readCompareConfig(iniFile, modName, compare, toPrint=True): - ''' Read and optionally compare configuration files (if a local and default are both provided) + """Read and optionally compare configuration files (if a local and default are both provided) and inform user of the eventual differences. Take the default as reference. Parameters @@ -183,10 +182,10 @@ def readCompareConfig(iniFile, modName, compare, toPrint=True): contains combined config modDict: dict dictionary containing only differences from default - ''' + """ if compare: - log.info('Reading config from: %s and %s' % (iniFile[0], iniFile[1])) + log.info("Reading config from: %s and %s" % (iniFile[0], iniFile[1])) # initialize configparser object to read defCfg = configparser.ConfigParser() defCfg.optionxform = str @@ -195,12 +194,12 @@ def readCompareConfig(iniFile, modName, compare, toPrint=True): # read default and local parser files defCfg.read(iniFile[0]) locCfg.read(iniFile[1]) - log.debug('Writing cfg for: %s', modName) + log.debug("Writing cfg for: %s", modName) # compare to default config and get modification dictionary and config modDict, modCfg = compareTwoConfigs(defCfg, locCfg, toPrint=toPrint) else: - log.info('Reading config from: %s', iniFile) + log.info("Reading config from: %s", iniFile) # initialize our final configparser object modCfg = configparser.ConfigParser() modCfg.optionxform = str @@ -215,58 +214,58 @@ def readCompareConfig(iniFile, modName, compare, toPrint=True): def _splitDeepDiffValuesChangedItem(inKey, inVal): - """ splits one item of a deepdiff result into section, key, old value, new value - - Parameters - ----------- - inputKey: str - key of a deepdiff changed_values item - inputValue: dict - value of a deepdiff changed_values item - - Returns - -------- - section: str - section name of changed item - key: str - key name of changed item - oldVal: str - old value - newVal: str - new value + """splits one item of a deepdiff result into section, key, old value, new value + + Parameters + ----------- + inputKey: str + key of a deepdiff changed_values item + inputValue: dict + value of a deepdiff changed_values item + + Returns + -------- + section: str + section name of changed item + key: str + key name of changed item + oldVal: str + old value + newVal: str + new value """ splitKey = re.findall(r"\[\s*['\"]([^'\"]+)['\"]\s*\]", inKey) section = splitKey[0] key = splitKey[1] - return section, key, inVal['old_value'], inVal['new_value'] + return section, key, inVal["old_value"], inVal["new_value"] def compareTwoConfigs(defCfg, locCfg, toPrint=False): - """ compare locCfg to defCfg and return a cfg object and modification dict - Values are merged from locCfg to defCfg: - - parameters already in defCfg get the value from locCfg - - additional values in locCfg get added in the resulting Cfg - - Parameters - ----------- - defCfg: configparser object - default configuration - locCfg: configuration object - configuration that is compared to defCfg - toPrint: bool - flag if config shall be printed to log - - Returns - -------- - modInfo: dict - dictionary containing only differences from default - cfg: configParser object - contains combined config + """compare locCfg to defCfg and return a cfg object and modification dict + Values are merged from locCfg to defCfg: + - parameters already in defCfg get the value from locCfg + - additional values in locCfg get added in the resulting Cfg + + Parameters + ----------- + defCfg: configparser object + default configuration + locCfg: configuration object + configuration that is compared to defCfg + toPrint: bool + flag if config shall be printed to log + + Returns + -------- + modInfo: dict + dictionary containing only differences from default + cfg: configParser object + contains combined config """ - log.info('Comparing two configs') + log.info("Comparing two configs") # initialize modInfo and printOutInfo modInfo = dict() @@ -285,7 +284,6 @@ def compareTwoConfigs(defCfg, locCfg, toPrint=False): except ValueError: cfgDiff = DeepDiff(defCfgD, locCfgD) - # Combine them, different keys are just added, for the same keys, the # local (right) value is used modCfgD = deepcopy(defCfgD) @@ -299,12 +297,12 @@ def compareTwoConfigs(defCfg, locCfg, toPrint=False): # If toPrint is set, print full configuration: if toPrint: - for line in pformat(modCfgD, sort_dicts=False).split('\n'): + for line in pformat(modCfgD, sort_dicts=False).split("\n"): log.info(line) # Generate modInfo dictionary for output - if 'values_changed' in cfgDiff: - for key, value in cfgDiff['values_changed'].items(): + if "values_changed" in cfgDiff: + for key, value in cfgDiff["values_changed"].items(): section, itemKey, defValue, locValue = _splitDeepDiffValuesChangedItem(key, value) if section not in modInfo: @@ -314,91 +312,91 @@ def compareTwoConfigs(defCfg, locCfg, toPrint=False): modInfo[section][itemKey] = modString # Log changes - log.info('COMPARING TO DEFAULT, THESE CHANGES HAPPENED:') - for line in cfgDiff.pretty().split('\n'): - log.info(line.replace('root','')) + log.info("COMPARING TO DEFAULT, THESE CHANGES HAPPENED:") + for line in cfgDiff.pretty().split("\n"): + log.info(line.replace("root", "")) return modInfo, modCfg -def writeCfgFile(avaDir, module, cfg, fileName='', filePath=''): - """ Save configuration used to text file in Outputs/moduleName/configurationFiles/modName.ini - or optional to filePath and with fileName +def writeCfgFile(avaDir, module, cfg, fileName="", filePath=""): + """Save configuration used to text file in Outputs/moduleName/configurationFiles/modName.ini + or optional to filePath and with fileName - Parameters - ----------- - avaDir: str - path to avalanche directory - module: - module - cfg: configparser object - configuration settings - fileName: str - name of saved configuration file - optional - filePath: str or pathlib path - path where file should be saved to except file name - optional + Parameters + ----------- + avaDir: str + path to avalanche directory + module: + module + cfg: configparser object + configuration settings + fileName: str + name of saved configuration file - optional + filePath: str or pathlib path + path where file should be saved to except file name - optional """ # get filename of module name = pathlib.Path(module.__file__).name - modName = name.split('.')[0] + modName = name.split(".")[0] # set outputs - if filePath == '': - outDir = pathlib.Path(avaDir, 'Outputs', modName, 'configurationFiles') + if filePath == "": + outDir = pathlib.Path(avaDir, "Outputs", modName, "configurationFiles") fU.makeADir(outDir) else: if filePath.is_dir(): outDir = pathlib.Path(filePath) else: - message = '%s is not a valid location for saving cfg file' % str(filePath) + message = "%s is not a valid location for saving cfg file" % str(filePath) log.error(message) raise NotADirectoryError(message) # set path to file - if fileName == '': + if fileName == "": fileName = modName - pathToFile = pathlib.Path(outDir, '%s.ini' % (fileName)) + pathToFile = pathlib.Path(outDir, "%s.ini" % (fileName)) # write file - with open(pathToFile, 'w') as conf: + with open(pathToFile, "w") as conf: cfg.write(conf) return pathToFile -def readCfgFile(avaDir, module='', fileName=''): - """ Read configuration from ini file, if module is provided, module configuration is read from Ouputs, - if fileName is provided configuration is read from fileName +def readCfgFile(avaDir, module="", fileName=""): + """Read configuration from ini file, if module is provided, module configuration is read from Ouputs, + if fileName is provided configuration is read from fileName - Parameters - ----------- - avaDir: str - path to avalanche directory - module: - module - fileName: str - path to file that should be read - optional + Parameters + ----------- + avaDir: str + path to avalanche directory + module: + module + fileName: str + path to file that should be read - optional - Returns - -------- - cfg: configParser object - configuration that is from file + Returns + -------- + cfg: configParser object + configuration that is from file """ # define file that should be read - if fileName != '': + if fileName != "": inFile = fileName - elif module != '': + elif module != "": # get module name name = pathlib.Path(module.__file__).name - modName = name.split('.')[0] + modName = name.split(".")[0] # set input file - inFile = pathlib.Path(avaDir, 'Outputs', '%s_settings.ini' % (modName)) + inFile = pathlib.Path(avaDir, "Outputs", "%s_settings.ini" % (modName)) else: - log.error('Please provide either a module or a fileName to read configuration from file') + log.error("Please provide either a module or a fileName to read configuration from file") raise NameError # read configParser object from input file, case sensitive @@ -409,8 +407,9 @@ def readCfgFile(avaDir, module='', fileName=''): return cfg + def cfgHash(cfg, typeDict=False): - """ UID hash of a config. Given a configParser object cfg, + """UID hash of a config. Given a configParser object cfg, or a dictionary - then typeDict=True, returns a uid hash Parameters @@ -442,7 +441,7 @@ def cfgHash(cfg, typeDict=False): def convertConfigParserToDict(cfg): - """ create dictionary from configparser object """ + """create dictionary from configparser object""" cfgDict = {} for section in cfg.sections(): @@ -454,7 +453,7 @@ def convertConfigParserToDict(cfg): def convertDictToConfigParser(cfgDict): - """ create configParser object from dict """ + """create configParser object from dict""" cfg = configparser.ConfigParser() cfg.optionxform = str @@ -465,7 +464,7 @@ def convertDictToConfigParser(cfgDict): def writeDictToJson(inDict, outFilePath): - """ write a dictionary to a json file """ + """write a dictionary to a json file""" jsonDict = json.dumps(inDict, sort_keys=True, ensure_ascii=True) f = open(outFilePath, "w") @@ -473,41 +472,48 @@ def writeDictToJson(inDict, outFilePath): f.close() -def createConfigurationInfo(avaDir, comModule='com1DFA', standardCfg='', writeCSV=False, specDir='', simNameList=[]): - """ Read configurations from all simulations configuration ini files from directory - - Parameters - ----------- - avaDir: str - path to avalanche directory - standardCfg: dict - standard configuration for module - option - writeCSV: bool - True if configuration dataFrame shall be written to csv file - specDir: str - path to a directory where simulation configuration files can be found - optional - simNameList: list - if non-empty list only use cfgFiles that are included within simNameList - - Returns - -------- - simDF: pandas DataFrame - DF with all the simulation configurations +def createConfigurationInfo( + avaDir, + comModule="com1DFA", + standardCfg="", + writeCSV=False, + specDir="", + simNameList=[], +): + """Read configurations from all simulations configuration ini files from directory + + Parameters + ----------- + avaDir: str + path to avalanche directory + standardCfg: dict + standard configuration for module - option + writeCSV: bool + True if configuration dataFrame shall be written to csv file + specDir: str + path to a directory where simulation configuration files can be found - optional + simNameList: list + if non-empty list only use cfgFiles that are included within simNameList + + Returns + -------- + simDF: pandas DataFrame + DF with all the simulation configurations """ # collect all configuration files for this module from directory - if specDir != '': - inDir = pathlib.Path(specDir, 'configurationFiles') + if specDir != "": + inDir = pathlib.Path(specDir, "configurationFiles") else: - inDir = pathlib.Path(avaDir, 'Outputs', comModule, 'configurationFiles') - configFiles = list(inDir.glob('*.ini')) + inDir = pathlib.Path(avaDir, "Outputs", comModule, "configurationFiles") + configFiles = list(inDir.glob("*.ini")) if not inDir.is_dir(): - message = 'configuration file directory not found: %s' % (inDir) + message = "configuration file directory not found: %s" % (inDir) log.error(message) raise NotADirectoryError(message) elif configFiles == []: - message = 'No configuration file found in: %s' % (inDir) + message = "No configuration file found in: %s" % (inDir) log.error(message) raise FileNotFoundError(message) @@ -520,16 +526,16 @@ def createConfigurationInfo(avaDir, comModule='com1DFA', standardCfg='', writeCS else: # create configparser object, convert to json object, write to dataFrame # append all dataFrames - simDF = '' + simDF = "" for cFile in configFiles: - if 'sourceConfiguration' not in str(cFile): + if "sourceConfiguration" not in str(cFile): simName = pathlib.Path(cFile).stem - if '_AF_' in simName: - nameParts = simName.split('_AF_') - infoParts = nameParts[1].split('_') + if "_AF_" in simName: + nameParts = simName.split("_AF_") + infoParts = nameParts[1].split("_") else: - nameParts = simName.split('_') + nameParts = simName.split("_") infoParts = nameParts[1:] simHash = infoParts[0] cfgObject = readCfgFile(avaDir, fileName=cFile) @@ -539,9 +545,9 @@ def createConfigurationInfo(avaDir, comModule='com1DFA', standardCfg='', writeCS simDF = convertDF2numerics(simDF) # add default configuration - if standardCfg != '': + if standardCfg != "": # read default configuration of this module - simDF = appendCgf2DF('current standard', 'current standard', standardCfg, simDF) + simDF = appendCgf2DF("current standard", "current standard", standardCfg, simDF) # if writeCSV, write dataFrame to csv file if writeCSV: @@ -551,24 +557,24 @@ def createConfigurationInfo(avaDir, comModule='com1DFA', standardCfg='', writeCS def appendCgf2DF(simHash, simName, cfgObject, simDF): - """ append simulation configuration to the simulation dataframe - append all sections to the dataframe - - Parameters - ----------- - simHash: str - hash of the simulation to append - simName: str - name of the simulation - cfgObject: configParser - configuration coresponding to the simulation - simDF: pandas dataFrame - configuration dataframe - - Returns - -------- - simDF: pandas DataFrame - DFappended with the new simulation configuration + """append simulation configuration to the simulation dataframe + append all sections to the dataframe + + Parameters + ----------- + simHash: str + hash of the simulation to append + simName: str + name of the simulation + cfgObject: configParser + configuration coresponding to the simulation + simDF: pandas dataFrame + configuration dataframe + + Returns + -------- + simDF: pandas DataFrame + DFappended with the new simulation configuration """ indexItem = [simHash] cfgDict = convertConfigParserToDict(cfgObject) @@ -622,21 +628,21 @@ def renameDuplicates(df): def appendTcpu2DF(simHash, tCPU, tCPUDF): - """ append Tcpu dictionary to the dataframe - - Parameters - ----------- - simHash: str - hash of the simulation corresponding to the tCPU dict to append - tCPU: dict - cpu time dict of the simulation - tCPUDF: pandas dataFrame - tCPU dataframe - - Returns - -------- - simDF: pandas DataFrame - DFappended with the new simulation configuration + """append Tcpu dictionary to the dataframe + + Parameters + ----------- + simHash: str + hash of the simulation corresponding to the tCPU dict to append + tCPU: dict + cpu time dict of the simulation + tCPUDF: pandas dataFrame + tCPU dataframe + + Returns + -------- + simDF: pandas DataFrame + DFappended with the new simulation configuration """ indexItem = [simHash] tCPUItemDF = pd.DataFrame(data=tCPU, index=indexItem) @@ -648,163 +654,188 @@ def appendTcpu2DF(simHash, tCPU, tCPUDF): def convertDF2numerics(simDF): - """ convert a string DF to a numerical one + """convert a string DF to a numerical one - Parameters - ----------- - simDF: pandas dataFrame - dataframe + Parameters + ----------- + simDF: pandas dataFrame + dataframe - Returns - -------- - simDF: pandas DataFrame + Returns + -------- + simDF: pandas DataFrame """ for name, values in simDF.items(): - simDFTest = simDF[name].str.replace('.', '', regex=False) + simDFTest = simDF[name].str.replace(".", "", regex=False) # allow for - sign too - simDFTest = simDFTest.replace('-', '', regex=False) + simDFTest = simDFTest.replace("-", "", regex=False) # check for str(np.nan) as these cannot be converted to numerics by pd.to_numeric # but as friction model parameters are set to nans this is required here - if simDFTest.str.match('nan').any(): + if simDFTest.str.match("nan").any(): simDF = setStrnanToNan(simDF, simDFTest, name) # also include columns where nan is in first row - so check for any row - if simDFTest.str.isdigit().any() and (name != 'tSteps'): + if simDFTest.str.isdigit().any() and (name != "tSteps"): # problem here is that it finds even if not present in | although not in ini - simDFTest = simDF[name].str.replace('|', '§', regex=False) - if simDFTest.str.contains('§').any() == False: + simDFTest = simDF[name].str.replace("|", "§", regex=False) + if simDFTest.str.contains("§").any() == False: simDF[name] = pd.to_numeric(simDF[name]) - log.debug('Converted to numeric %s' % name) + log.debug("Converted to numeric %s" % name) else: - log.debug('Not converted to numeric: %s' % name) + log.debug("Not converted to numeric: %s" % name) return simDF def setStrnanToNan(simDF, simDFTest, name): - """ set pandas element to np.nan if it is a string nan - - Parameters - ----------- - simDF: pandas dataFrame - dataframe - simDFTest: pandas series - series of sim DF column named name - replaced "." with " " - name: str - name of pandas dataframe column - - Returns - -------- - simDF: pandas dataframe - updated pandas dataframe with np.nan values where string nan was + """set pandas element to np.nan if it is a string nan + + Parameters + ----------- + simDF: pandas dataFrame + dataframe + simDFTest: pandas series + series of sim DF column named name + replaced "." with " " + name: str + name of pandas dataframe column + + Returns + -------- + simDF: pandas dataframe + updated pandas dataframe with np.nan values where string nan was """ - nanIndex = simDFTest.str.match('nan', flags=re.IGNORECASE) + nanIndex = simDFTest.str.match("nan", flags=re.IGNORECASE) simIndex = simDF.index.values # loop over each row and use simDF.at to avoid copy vs view warning for index, nanInd in enumerate(nanIndex): if nanInd: simDF.at[simIndex[index], name] = np.nan - log.info('%s for index: %s set to numpy nan' % (name, index)) + log.info("%s for index: %s set to numpy nan" % (name, index)) return simDF -def readConfigurationInfoFromDone(avaDir, specDir='', latest=False): - """ Check avaName/Outputs/com1DFA/configurationFilesDone and pass - names of all files found in this directory and create corresponding simDF - this is useful if e.g. no allConfigurations.csv has - been written but already some simulations have been performed as a txt file is saved in - avaName/Outputs/com1DFA/configurationFiles after the respective simulation has been run - whereas the allConfigurations file is written at the end of a call to com1DFAMain that can - include several individual sims - if latest=True only look for latest simulations in avaName/Outputs/com1DFA/configurationFilesLatest - - Parameters - ----------- - avaDir: str - path to avalanche directory - specDir: str - path to a directory where simulation configuration files directory called configurationFiles can be found - optional - latest: bool - if True check for files found in avaName/Outputs/com1DFA/configurationFilesLatest - - Returns - -------- - simDF: pandas DataFrame - DF with all the simulation configurations - simDFName: array - simName column of the dataframe + +def readConfigurationInfoFromDone(avaDir, specDir="", latest=False): + """Check avaName/Outputs/com1DFA/configurationFilesDone and pass + names of all files found in this directory and create corresponding simDF + this is useful if e.g. no allConfigurations.csv has + been written but already some simulations have been performed as a txt file is saved in + avaName/Outputs/com1DFA/configurationFiles after the respective simulation has been run + whereas the allConfigurations file is written at the end of a call to com1DFAMain that can + include several individual sims + if latest=True only look for latest simulations in avaName/Outputs/com1DFA/configurationFilesLatest + + Parameters + ----------- + avaDir: str + path to avalanche directory + specDir: str + path to a directory where simulation configuration files directory called configurationFiles can be found - optional + latest: bool + if True check for files found in avaName/Outputs/com1DFA/configurationFilesLatest + + Returns + -------- + simDF: pandas DataFrame + DF with all the simulation configurations + simDFName: array + simName column of the dataframe """ # collect all configuration files for this module from directory - if specDir != '': - inDir = pathlib.Path(specDir, 'configurationFiles') + if specDir != "": + inDir = pathlib.Path(specDir, "configurationFiles") else: - inDir = pathlib.Path(avaDir, 'Outputs', 'com1DFA', 'configurationFiles') + inDir = pathlib.Path(avaDir, "Outputs", "com1DFA", "configurationFiles") # search inDir/configurationFilesDone or inDir/configurationFilesLatest (depending on latest flag) for already existing sims if latest: - configDir = inDir / 'configurationFilesLatest' + configDir = inDir / "configurationFilesLatest" else: - configDir = inDir / 'configurationFilesDone' + configDir = inDir / "configurationFilesDone" - existingSims = list(configDir.glob('*.ini')) + existingSims = list(configDir.glob("*.ini")) simNameExisting = [] for fName in existingSims: simNameExisting.append(fName.stem) - if list((inDir / 'configurationFilesDone').glob('*.ini')) == []: - log.info('No existing simulations in Outputs found') + if list((inDir / "configurationFilesDone").glob("*.ini")) == []: + log.info("No existing simulations in Outputs found") simDF = None else: # create simDF (dataFrame with one row per simulation of configuration files found in configDir) - simDF = createConfigurationInfo(avaDir, comModule='com1DFA', standardCfg='', writeCSV=False, specDir=specDir, - simNameList=simNameExisting) - - + simDF = createConfigurationInfo( + avaDir, + comModule="com1DFA", + standardCfg="", + writeCSV=False, + specDir=specDir, + simNameList=simNameExisting, + ) # check for allConfigurationsInfo to find computation info and add to info fetched from ini files if latest == False and isinstance(simDF, pd.DataFrame): # check if in allConfigurationsInfo also info for existing sims - simDFALL, _ = readAllConfigurationInfo(avaDir, specDir='', configCsvName='allConfigurations') + simDFALL, _ = readAllConfigurationInfo(avaDir, specDir="", configCsvName="allConfigurations") if isinstance(simDFALL, pd.DataFrame): - simDF = simDF.reset_index().merge(simDFALL[['nPart', 'timeLoop', 'timeForce', 'timeForceSPH', 'timePos', 'timeNeigh', - 'timeField', 'nSave', 'nIter', 'simName']], how='left', on='simName').set_index('index') + simDF = ( + simDF.reset_index() + .merge( + simDFALL[ + [ + "nPart", + "timeLoop", + "timeForce", + "timeForceSPH", + "timePos", + "timeNeigh", + "timeField", + "nSave", + "nIter", + "simName", + ] + ], + how="left", + on="simName", + ) + .set_index("index") + ) return simDF, simNameExisting -def readAllConfigurationInfo(avaDir, specDir='', configCsvName='allConfigurations'): - """ Read allConfigurations.csv file as dataFrame from directory - - Parameters - ----------- - avaDir: str - path to avalanche directory - specDir: str - path to a directory where simulation configuration files can be found - optional - configCsvName: str - name of configuration csv file - - Returns - -------- - simDF: pandas DataFrame - DF with all the simulation configurations - simDFName: array - simName column of the dataframe +def readAllConfigurationInfo(avaDir, specDir="", configCsvName="allConfigurations"): + """Read allConfigurations.csv file as dataFrame from directory + + Parameters + ----------- + avaDir: str + path to avalanche directory + specDir: str + path to a directory where simulation configuration files can be found - optional + configCsvName: str + name of configuration csv file + + Returns + -------- + simDF: pandas DataFrame + DF with all the simulation configurations + simDFName: array + simName column of the dataframe """ # collect all configuration files for this module from directory - if specDir != '': - inDir = pathlib.Path(specDir, 'configurationFiles') + if specDir != "": + inDir = pathlib.Path(specDir, "configurationFiles") else: - inDir = pathlib.Path(avaDir, 'Outputs', 'com1DFA', 'configurationFiles') - configFiles = inDir / ('%s.csv' % configCsvName) + inDir = pathlib.Path(avaDir, "Outputs", "com1DFA", "configurationFiles") + configFiles = inDir / ("%s.csv" % configCsvName) if configFiles.is_file(): - with open(configFiles, 'rb') as file: + with open(configFiles, "rb") as file: simDF = pd.read_csv(file, index_col=0, keep_default_na=False) - simDFName = simDF['simName'].to_numpy() + simDFName = simDF["simName"].to_numpy() else: simDF = None simDFName = [] @@ -812,31 +843,31 @@ def readAllConfigurationInfo(avaDir, specDir='', configCsvName='allConfiguration return simDF, simDFName -def writeAllConfigurationInfo(avaDir, simDF, specDir='', csvName='allConfigurations.csv'): - """ Write cfg configuration to allConfigurations.csv - - Parameters - ----------- - avaDir: str - path to avalanche directory - simDF: pandas dataFrame - daaframe of the configuration - specDir: str - path to a directory where simulation configuration shal be saved - optional - csvName: str - name of csv file in which to save to - optional - - Returns - -------- - configFiles: pathlib Path - path where the configuration dataframe was saved +def writeAllConfigurationInfo(avaDir, simDF, specDir="", csvName="allConfigurations.csv"): + """Write cfg configuration to allConfigurations.csv + + Parameters + ----------- + avaDir: str + path to avalanche directory + simDF: pandas dataFrame + daaframe of the configuration + specDir: str + path to a directory where simulation configuration shal be saved - optional + csvName: str + name of csv file in which to save to - optional + + Returns + -------- + configFiles: pathlib Path + path where the configuration dataframe was saved """ # collect all configuration files for this module from directory - if specDir != '': - inDir = pathlib.Path(specDir, 'configurationFiles') + if specDir != "": + inDir = pathlib.Path(specDir, "configurationFiles") else: - inDir = pathlib.Path(avaDir, 'Outputs', 'com1DFA', 'configurationFiles') + inDir = pathlib.Path(avaDir, "Outputs", "com1DFA", "configurationFiles") configFiles = inDir / csvName simDF.to_csv(configFiles) @@ -845,53 +876,53 @@ def writeAllConfigurationInfo(avaDir, simDF, specDir='', csvName='allConfigurati def convertToCfgList(parameterList): - """ convert a list into a string where individual list items are separated by | + """convert a list into a string where individual list items are separated by | - Parameters - ----------- - parameterList: list - list of parameter values + Parameters + ----------- + parameterList: list + list of parameter values - Returns - --------- - parameterString: str - str with parameter values separated by | + Returns + --------- + parameterString: str + str with parameter values separated by | """ if len(parameterList) == 0: - parameterString = '' + parameterString = "" else: parameterString = parameterList[0] for item in parameterList[1:]: - parameterString = parameterString + '|' + item + parameterString = parameterString + "|" + item return parameterString def getNumberOfProcesses(cfgMain, nSims): - """ Determine how many CPU cores to take for parallel tasks + """Determine how many CPU cores to take for parallel tasks - Parameters - ----------- - cfgMain: configuration object - the main avaframe configuration - nSims: integer - number of simulations that need to be calculated + Parameters + ----------- + cfgMain: configuration object + the main avaframe configuration + nSims: integer + number of simulations that need to be calculated - Returns - --------- - nCPU: int - number of cores to take + Returns + --------- + nCPU: int + number of cores to take """ maxCPU = multiprocessing.cpu_count() - if cfgMain["MAIN"]["nCPU"] == 'auto': - cpuPerc = float(cfgMain["MAIN"]["CPUPercent"]) / 100. + if cfgMain["MAIN"]["nCPU"] == "auto": + cpuPerc = float(cfgMain["MAIN"]["CPUPercent"]) / 100.0 nCPU = math.floor(maxCPU * cpuPerc) else: - nCPU = cfgMain['MAIN'].getint('nCPU') + nCPU = cfgMain["MAIN"].getint("nCPU") # if number of sims is lower than nCPU nCPU = min(nCPU, nSims) @@ -903,7 +934,7 @@ def getNumberOfProcesses(cfgMain, nSims): def getModPathName(module): - """ get the path and name of a module from imported module + """get the path and name of a module from imported module Parameters ------------ diff --git a/avaframe/tests/test_probAna.py b/avaframe/tests/test_probAna.py index 895d94183..558637ae1 100644 --- a/avaframe/tests/test_probAna.py +++ b/avaframe/tests/test_probAna.py @@ -1,9 +1,9 @@ """ - Pytest for module ana4Stats +Pytest for module ana4Stats - This file is part of Avaframe. +This file is part of Avaframe. - """ +""" # Load modules import numpy as np @@ -19,101 +19,127 @@ def test_probAnalysis(tmp_path): - """ test probAna function to compute mask for parameter exceeding threshold """ + """test probAna function to compute mask for parameter exceeding threshold""" # set input directory - avaName = 'avaParabola' - avaTestDir = 'avaParabolaStatsTest' + avaName = "avaParabola" + avaTestDir = "avaParabolaStatsTest" dirPath = pathlib.Path(__file__).parents[0] - avaDir = dirPath / '..' / '..' / 'benchmarks' / avaTestDir + avaDir = dirPath / ".." / ".." / "benchmarks" / avaTestDir avaDirtmp = pathlib.Path(tmp_path, avaName) inputDir = pathlib.Path(tmp_path, avaName) inputDir1 = avaDir shutil.copytree(inputDir1, inputDir) # set configurations - testCfg = os.path.join(inputDir, '%sProbAna_com1DFACfg.ini' % avaName) + testCfg = os.path.join(inputDir, "%sProbAna_com1DFACfg.ini" % avaName) cfgMain = cfgUtils.getModuleConfig(com1DFA, testCfg) # Initialise input in correct format cfg = configparser.ConfigParser() - cfg['GENERAL'] = {'peakLim': 1.0, 'peakVar': 'ppr'} - cfg['FILTER'] = {} + cfg["GENERAL"] = {"peakLim": 1.0, "peakVar": "ppr"} + cfg["FILTER"] = {} # provide optional filter criteria for simulations - parametersDict = fU.getFilterDict(cfg, 'FILTER') + parametersDict = fU.getFilterDict(cfg, "FILTER") # call function to test - pA.probAnalysis(avaDirtmp, cfg, 'com1DFA', parametersDict=parametersDict, inputDir='') - outputPath = os.path.join(avaDirtmp, 'Outputs', 'ana4Stats', 'avaParabola_prob__ppr_lim1.0.asc') + pA.probAnalysis(avaDirtmp, cfg, "com1DFA", parametersDict=parametersDict, inputDir="") + outputPath = os.path.join(avaDirtmp, "Outputs", "ana4Stats", "avaParabola_prob__ppr_lim1.0.asc") print(outputPath) probTest = np.loadtxt(outputPath, skiprows=6) # Load reference solution - probSol = np.loadtxt(os.path.join(inputDir1, 'avaParabola_prob__ppr_lim1.0.txt'), skiprows=6) + probSol = np.loadtxt(os.path.join(inputDir1, "avaParabola_prob__ppr_lim1.0.txt"), skiprows=6) # Compare result to reference solution - testRes = np.allclose(probTest, probSol, atol=1.e-6) + testRes = np.allclose(probTest, probSol, atol=1.0e-6) # Test - assert (testRes is True) + assert testRes is True # call function to test - testInputDir = avaDir / 'Outputs' / 'com1DFA' - avaDirtmp2 = pathlib.Path(tmp_path, 'avaTest') + testInputDir = avaDir / "Outputs" / "com1DFA" + avaDirtmp2 = pathlib.Path(tmp_path, "avaTest") avaDirtmp2.mkdir() - pA.probAnalysis(avaDirtmp2, cfg, 'com1DFA', parametersDict='', inputDir=testInputDir) - probTest2 = np.loadtxt(os.path.join(avaDirtmp2, 'Outputs', 'ana4Stats', 'avaTest_prob__ppr_lim1.0.asc'), skiprows=6) + pA.probAnalysis(avaDirtmp2, cfg, "com1DFA", parametersDict="", inputDir=testInputDir) + probTest2 = np.loadtxt( + os.path.join(avaDirtmp2, "Outputs", "ana4Stats", "avaTest_prob__ppr_lim1.0.asc"), + skiprows=6, + ) # Compare result to reference solution - testRes2 = np.allclose(probTest2, probSol, atol=1.e-6) + testRes2 = np.allclose(probTest2, probSol, atol=1.0e-6) # Test - assert (testRes2 is True) - + assert testRes2 is True # set input directory - avaName2 = 'avaTest2' - avaTestDir2 = 'avaProbTest' + avaName2 = "avaTest2" + avaTestDir2 = "avaProbTest" dirPath2 = pathlib.Path(__file__).parents[0] - avaDir2 = dirPath / 'data' / avaTestDir2 + avaDir2 = dirPath / "data" / avaTestDir2 inputDir2 = pathlib.Path(tmp_path, avaName2) shutil.copytree(avaDir2, inputDir2) cfg2 = configparser.ConfigParser() cfg2.optionxform = str - cfg2['GENERAL'] = {'peakVar': 'ppr', 'unit': 'kPa','peakLim': '1.0'} - cfg2['PLOT'] = {'name': 'probability map', 'cmapType': 'prob', 'levels': '0.95', 'unit': 'fraction', - 'zoomBuffer': 250., 'contrainBuffer': 10., 'meshCellSizeThreshold': 0.001} - modName = 'com1DFA' + cfg2["GENERAL"] = {"peakVar": "ppr", "unit": "kPa", "peakLim": "1.0"} + cfg2["PLOT"] = { + "name": "probability map", + "cmapType": "prob", + "levels": "0.95", + "unit": "fraction", + "zoomBuffer": 250.0, + "contrainBuffer": 10.0, + "meshCellSizeThreshold": 0.001, + } + modName = "com1DFA" # call function to be tested - analysisPerformed, contourDict = pA.probAnalysis(inputDir2, cfg2, modName, parametersDict='', inputDir='', probConf='', simDFActual='') + analysisPerformed, contourDict = pA.probAnalysis( + inputDir2, + cfg2, + modName, + parametersDict="", + inputDir="", + probConf="", + simDFActual="", + ) assert analysisPerformed is True def test_createComModConfig(tmp_path): - """ test creatig a config file """ + """test creatig a config file""" # set input directory - avaName = 'testCom1DFA2' + avaName = "testCom1DFA2" dirPath = pathlib.Path(__file__).parents[0] - inputDir = dirPath / 'data' / avaName + inputDir = dirPath / "data" / avaName avaDir = pathlib.Path(tmp_path, avaName) shutil.copytree(inputDir, avaDir) cfgProb = configparser.ConfigParser() cfgProb.optionxform = str - cfgProb['PROBRUN'] = {'varParList': 'musamosat|relTh', 'variationType': 'percent|percent', - 'variationValue': '60|50', 'numberOfSteps': '2|3', - 'defaultSetup': 'True', 'samplingStrategy': '2', 'varParType': 'float|float'} - cfgProb['sampling_override'] = {'defaultConfig': 'True'} - cfgProb['com1DFA_com1DFA_override'] = {'defaultConfig': 'True', 'frictModel': 'samosAT'} + cfgProb["PROBRUN"] = { + "varParList": "musamosat|relTh", + "variationType": "percent|percent", + "variationValue": "60|50", + "numberOfSteps": "2|3", + "defaultSetup": "True", + "samplingStrategy": "2", + "varParType": "float|float", + } + cfgProb["sampling_override"] = {"defaultConfig": "True"} + cfgProb["com1DFA_com1DFA_override"] = { + "defaultConfig": "True", + "frictModel": "samosAT", + } # call function to be tested cfgFiles, outDir = pA.createComModConfig(cfgProb, avaDir, com1DFA) -# print('cfgFiles', cfgFiles) + # print('cfgFiles', cfgFiles) # load cfg from file cfgMu = configparser.ConfigParser() @@ -122,21 +148,26 @@ def test_createComModConfig(tmp_path): cfgMu.read(cfgFiles[0]) cfgRelTh.read(cfgFiles[1]) -# print(cfgMu['GENERAL']['musamosat'], cfgMu['GENERAL']['relTh'], cfgRelTh['GENERAL']['musamosat'], + # print(cfgMu['GENERAL']['musamosat'], cfgMu['GENERAL']['relTh'], cfgRelTh['GENERAL']['musamosat'], # cfgRelTh['GENERAL']['relTh']) - assert cfgMu['GENERAL']['musamosat'] == '0.155$60$2' - assert cfgMu['GENERAL']['relTh'] == '' - assert cfgRelTh['GENERAL']['musamosat'] == '0.155' - assert cfgRelTh['GENERAL']['relTh'] == '' - assert cfgRelTh['GENERAL']['relThFromShp'] == 'True' - assert cfgRelTh['GENERAL']['relThPercentVariation'] == '50$3' - assert cfgRelTh['GENERAL']['relThFromShp'] == 'True' - assert cfgMu['GENERAL']['relThFromShp'] == 'True' - assert cfgRelTh.has_option('GENERAL', 'addStandardConfig') == False - - cfgProb['com1DFA_com1DFA_override'] = {'defaultConfig': 'True', 'relThFromShp': False, 'relTh': 2., - 'musamosat': 0.155, 'frictModel': 'samosAT'} + assert cfgMu["GENERAL"]["musamosat"] == "0.155$60$2" + assert cfgMu["GENERAL"]["relTh"] == "" + assert cfgRelTh["GENERAL"]["musamosat"] == "0.155" + assert cfgRelTh["GENERAL"]["relTh"] == "" + assert cfgRelTh["GENERAL"]["relThFromShp"] == "True" + assert cfgRelTh["GENERAL"]["relThPercentVariation"] == "50$3" + assert cfgRelTh["GENERAL"]["relThFromShp"] == "True" + assert cfgMu["GENERAL"]["relThFromShp"] == "True" + assert cfgRelTh.has_option("GENERAL", "addStandardConfig") == False + + cfgProb["com1DFA_com1DFA_override"] = { + "defaultConfig": "True", + "relThFromShp": False, + "relTh": 2.0, + "musamosat": 0.155, + "frictModel": "samosAT", + } # call function to be tested cfgFiles, outDir = pA.createComModConfig(cfgProb, avaDir, com1DFA) @@ -147,491 +178,606 @@ def test_createComModConfig(tmp_path): cfgMu.read(cfgFiles[0]) cfgRelTh.read(cfgFiles[1]) -# print(cfgMu['GENERAL']['musamosat'], cfgMu['GENERAL']['relTh'], cfgRelTh['GENERAL']['musamosat'], + # print(cfgMu['GENERAL']['musamosat'], cfgMu['GENERAL']['relTh'], cfgRelTh['GENERAL']['musamosat'], # cfgRelTh['GENERAL']['relTh']) - assert cfgMu['GENERAL']['musamosat'] == '0.155$60$2' - assert np.isclose(cfgMu['GENERAL'].getfloat('relTh'), 2.) - assert cfgRelTh['GENERAL']['musamosat'] == '0.155' - assert np.isclose(cfgRelTh['GENERAL'].getfloat('relTh'), 2.) - assert cfgRelTh['GENERAL']['relThFromShp'] == 'False' - assert cfgRelTh['GENERAL']['relThPercentVariation'] == '50$3' - assert cfgRelTh['GENERAL']['relThFromShp'] == 'False' - assert cfgMu['GENERAL']['relThFromShp'] == 'False' + assert cfgMu["GENERAL"]["musamosat"] == "0.155$60$2" + assert np.isclose(cfgMu["GENERAL"].getfloat("relTh"), 2.0) + assert cfgRelTh["GENERAL"]["musamosat"] == "0.155" + assert np.isclose(cfgRelTh["GENERAL"].getfloat("relTh"), 2.0) + assert cfgRelTh["GENERAL"]["relThFromShp"] == "False" + assert cfgRelTh["GENERAL"]["relThPercentVariation"] == "50$3" + assert cfgRelTh["GENERAL"]["relThFromShp"] == "False" + assert cfgMu["GENERAL"]["relThFromShp"] == "False" cfgProb = configparser.ConfigParser() cfgProb.optionxform = str - cfgProb['PROBRUN'] = {'varParList': 'musamosat|relTh', 'variationType': 'percent|percent', - 'variationValue': '60|50', 'numberOfSteps': '2|3', - 'defaultSetup': 'True', 'samplingStrategy': '1', - 'varParType': 'float|float', 'nSample': '40', 'sampleSeed': '12345', - 'sampleMethod': 'latin'} - cfgProb['com1DFA_com1DFA_override'] = {'defaultConfig': 'True', 'relThFromShp': False, 'relTh': 2., - 'musamosat': 0.155, 'frictModel': 'samosAT'} + cfgProb["PROBRUN"] = { + "varParList": "musamosat|relTh", + "variationType": "percent|percent", + "variationValue": "60|50", + "numberOfSteps": "2|3", + "defaultSetup": "True", + "samplingStrategy": "1", + "varParType": "float|float", + "nSample": "40", + "sampleSeed": "12345", + "sampleMethod": "latin", + } + cfgProb["com1DFA_com1DFA_override"] = { + "defaultConfig": "True", + "relThFromShp": False, + "relTh": 2.0, + "musamosat": 0.155, + "frictModel": "samosAT", + } # call function to be tested cfgFiles, outDir = pA.createComModConfig(cfgProb, avaDir, com1DFA) -# print('cfgFiles', cfgFiles) + # print('cfgFiles', cfgFiles) for cfgF in cfgFiles: cfgTest = configparser.ConfigParser() cfgTest.read(cfgF) -# print('cfgTest', cfgTest['GENERAL']['relThFromShp'], cfgTest['GENERAL']['relTh'], + # print('cfgTest', cfgTest['GENERAL']['relThFromShp'], cfgTest['GENERAL']['relTh'], # cfgTest['GENERAL']['relThPercentVariation'], cfgTest['GENERAL']['musamosat']) - assert cfgTest['GENERAL']['relThFromShp'] == 'False' - assert cfgTest['GENERAL'].getfloat('relTh') <= 3. - assert cfgTest['GENERAL'].getfloat('relTh') >= 1. - assert cfgTest['GENERAL'].getfloat('musamosat') <= 0.248 - assert cfgTest['GENERAL'].getfloat('musamosat') >= 0.062 + assert cfgTest["GENERAL"]["relThFromShp"] == "False" + assert cfgTest["GENERAL"].getfloat("relTh") <= 3.0 + assert cfgTest["GENERAL"].getfloat("relTh") >= 1.0 + assert cfgTest["GENERAL"].getfloat("musamosat") <= 0.248 + assert cfgTest["GENERAL"].getfloat("musamosat") >= 0.062 cfgTest = configparser.ConfigParser() cfgTest.read(cfgFiles[0]) - assert cfgTest['GENERAL']['relTh'] == '2.2719559079879' + assert cfgTest["GENERAL"]["relTh"] == "2.2719559079879" assert len(cfgFiles) == 40 cfgProb = configparser.ConfigParser() cfgProb.optionxform = str - cfgProb['PROBRUN'] = {'varParList': 'musamosat|relTh', 'variationType': 'percent|percent', - 'variationValue': '60|50', 'numberOfSteps': '2|3', - 'defaultSetup': 'True', 'samplingStrategy': '1', - 'varParType': 'float|float', 'nSample': '40', 'sampleSeed': '12345', - 'sampleMethod': 'latin'} - cfgProb['com1DFA_com1DFA_override'] = {'defaultConfig': 'True', 'frictModel': 'samosAT'} + cfgProb["PROBRUN"] = { + "varParList": "musamosat|relTh", + "variationType": "percent|percent", + "variationValue": "60|50", + "numberOfSteps": "2|3", + "defaultSetup": "True", + "samplingStrategy": "1", + "varParType": "float|float", + "nSample": "40", + "sampleSeed": "12345", + "sampleMethod": "latin", + } + cfgProb["com1DFA_com1DFA_override"] = { + "defaultConfig": "True", + "frictModel": "samosAT", + } # call function to be tested cfgFiles2, outDir = pA.createComModConfig(cfgProb, avaDir, com1DFA) -# print('cfgFiles', cfgFiles2) + # print('cfgFiles', cfgFiles2) cfgTest2 = configparser.ConfigParser() cfgTest2.read(cfgFiles2[0]) cfgTest21 = configparser.ConfigParser() cfgTest21.read(cfgFiles2[40]) -# print('cfgTest', cfgTest['GENERAL']['relThFromShp'], cfgTest['GENERAL']['relTh'], + # print('cfgTest', cfgTest['GENERAL']['relThFromShp'], cfgTest['GENERAL']['relTh'], # cfgTest['GENERAL']['relThPercentVariation'], cfgTest['GENERAL']['musamosat']) - assert cfgTest2['GENERAL']['relThFromShp'] == 'True' - assert cfgTest2['GENERAL']['relTh'] == '' - assert cfgTest2['GENERAL']['relThPercentVariation'] == '' - assert cfgTest2['INPUT']['releaseScenario'] == 'relParabola' - assert cfgTest2.has_option('GENERAL', 'relTh0') + assert cfgTest2["GENERAL"]["relThFromShp"] == "True" + assert cfgTest2["GENERAL"]["relTh"] == "" + assert cfgTest2["GENERAL"]["relThPercentVariation"] == "" + assert cfgTest2["INPUT"]["releaseScenario"] == "relParabola" + assert cfgTest2.has_option("GENERAL", "relTh0") assert len(cfgFiles2) == 80 - assert cfgTest21['GENERAL']['relThFromShp'] == 'True' - assert cfgTest21['GENERAL']['relTh'] == '' - assert cfgTest21['GENERAL']['relThPercentVariation'] == '' - assert cfgTest21['GENERAL']['musamosat'] == cfgTest2['GENERAL']['musamosat'] - assert cfgTest21['INPUT']['releaseScenario'] == 'relParabolaTwo' - assert cfgTest21.has_option('GENERAL', 'relTh0') + assert cfgTest21["GENERAL"]["relThFromShp"] == "True" + assert cfgTest21["GENERAL"]["relTh"] == "" + assert cfgTest21["GENERAL"]["relThPercentVariation"] == "" + assert cfgTest21["GENERAL"]["musamosat"] == cfgTest2["GENERAL"]["musamosat"] + assert cfgTest21["INPUT"]["releaseScenario"] == "relParabolaTwo" + assert cfgTest21.has_option("GENERAL", "relTh0") assert len(cfgFiles2) == 80 cfgProb = configparser.ConfigParser() cfgProb.optionxform = str - cfgProb['PROBRUN'] = {'varParList': 'musamosat|relTh', 'variationType': 'range|range', - 'variationValue': '0.2|1.2', 'numberOfSteps': '2|3', - 'samplingStrategy': '2', 'varParType': 'float|float|int'} - cfgProb['com1DFA_com1DFA_override'] = {'defaultConfig': 'True', 'frictModel': 'samosAT'} + cfgProb["PROBRUN"] = { + "varParList": "musamosat|relTh", + "variationType": "range|range", + "variationValue": "0.2|1.2", + "numberOfSteps": "2|3", + "samplingStrategy": "2", + "varParType": "float|float|int", + } + cfgProb["com1DFA_com1DFA_override"] = { + "defaultConfig": "True", + "frictModel": "samosAT", + } # call function to be tested cfgFiles3, outDir = pA.createComModConfig(cfgProb, avaDir, com1DFA) -# print('cfgFiles', cfgFiles3) + # print('cfgFiles', cfgFiles3) cfgTest3 = configparser.ConfigParser() cfgTest3.read(cfgFiles3[1]) - assert cfgTest3['GENERAL']['relThFromShp'] == 'True' - assert cfgTest3['GENERAL']['relTh'] == '' - assert cfgTest3['GENERAL']['relThRangeVariation'] == '1.2$3' + assert cfgTest3["GENERAL"]["relThFromShp"] == "True" + assert cfgTest3["GENERAL"]["relTh"] == "" + assert cfgTest3["GENERAL"]["relThRangeVariation"] == "1.2$3" assert len(cfgFiles3) == 2 cfgProb = configparser.ConfigParser() cfgProb.optionxform = str - cfgProb['PROBRUN'] = {'varParList': 'musamosat|relTh|entTh', 'variationType': 'percent|rangefromci|range', - 'variationValue': '60|ci95|0.15', 'numberOfSteps': '2|3|4', - 'samplingStrategy': '1', - 'varParType': 'float|float|float', 'nSample': '30', 'sampleSeed': '12345', - 'sampleMethod': 'latin'} - cfgProb['com1DFA_com1DFA_override'] = {'defaultConfig': 'True', 'frictModel': 'samosAT'} + cfgProb["PROBRUN"] = { + "varParList": "musamosat|relTh|entTh", + "variationType": "percent|rangefromci|range", + "variationValue": "60|ci95|0.15", + "numberOfSteps": "2|3|4", + "samplingStrategy": "1", + "varParType": "float|float|float", + "nSample": "30", + "sampleSeed": "12345", + "sampleMethod": "latin", + } + cfgProb["com1DFA_com1DFA_override"] = { + "defaultConfig": "True", + "frictModel": "samosAT", + } # call function to be tested cfgFiles4, outDir = pA.createComModConfig(cfgProb, avaDir, com1DFA) -# print('cfgFiles', cfgFiles) + # print('cfgFiles', cfgFiles) cfgTest4 = configparser.ConfigParser() cfgTest4.read(cfgFiles4[0]) -# print('cfgTest', cfgTest4['GENERAL']['relThFromShp'], cfgTest4['GENERAL']['relTh'], + # print('cfgTest', cfgTest4['GENERAL']['relThFromShp'], cfgTest4['GENERAL']['relTh'], # cfgTest4['GENERAL']['relThPercentVariation'], cfgTest4['GENERAL']['musamosat']) - assert cfgTest4['GENERAL']['relThFromShp'] == 'True' - assert cfgTest4['GENERAL']['relTh'] == '' - assert cfgTest4['GENERAL']['relThRangeVariation'] == '' - assert cfgTest4['GENERAL']['relTh0'] != '' - assert cfgTest4['GENERAL']['relTh1'] != '' - assert cfgTest4['GENERAL']['entTh0'] != '' - assert cfgTest4['INPUT']['releaseScenario'] == 'relParabola' + assert cfgTest4["GENERAL"]["relThFromShp"] == "True" + assert cfgTest4["GENERAL"]["relTh"] == "" + assert cfgTest4["GENERAL"]["relThRangeVariation"] == "" + assert cfgTest4["GENERAL"]["relTh0"] != "" + assert cfgTest4["GENERAL"]["relTh1"] != "" + assert cfgTest4["GENERAL"]["entTh0"] != "" + assert cfgTest4["INPUT"]["releaseScenario"] == "relParabola" assert len(cfgFiles4) == 60 def test_updateCfgRange(): - """ test updating cfg values """ + """test updating cfg values""" # setup inputs cfg = configparser.ConfigParser() - cfg['PROBRUN'] = {'varParList': 'musamosat|relTh', 'variationType': 'percent|percent', - 'variationValue': '60|50', 'numberOfSteps': '2|2', - 'defaultSetup': 'True', 'varParType': 'float', 'samplingStrategy': '2'} + cfg["PROBRUN"] = { + "varParList": "musamosat|relTh", + "variationType": "percent|percent", + "variationValue": "60|50", + "numberOfSteps": "2|2", + "defaultSetup": "True", + "varParType": "float", + "samplingStrategy": "2", + } com1DFACfg = cfgUtils.getDefaultModuleConfig(com1DFA) - varName = 'musamosat' + varName = "musamosat" - varDict = pA.makeDictFromVars(cfg['PROBRUN']) + varDict = pA.makeDictFromVars(cfg["PROBRUN"]) # call function cfgNew = pA.updateCfgRange(com1DFACfg, cfg, varName, varDict[varName]) - assert cfgNew['GENERAL']['musamosat'] == '0.155$60$2' - assert cfgNew['GENERAL']['relTh'] == '' - assert cfgNew['GENERAL']['relThFromShp'] == 'True' - assert cfgNew['GENERAL']['relThPercentVariation'] == '' + assert cfgNew["GENERAL"]["musamosat"] == "0.155$60$2" + assert cfgNew["GENERAL"]["relTh"] == "" + assert cfgNew["GENERAL"]["relThFromShp"] == "True" + assert cfgNew["GENERAL"]["relThPercentVariation"] == "" com1DFACfg = cfgUtils.getDefaultModuleConfig(com1DFA) - varName = 'relTh' + varName = "relTh" # call function cfgNew = pA.updateCfgRange(com1DFACfg, cfg, varName, varDict[varName]) - assert cfgNew['GENERAL']['musamosat'] == '0.155' - assert cfgNew['GENERAL']['relTh'] == '' - assert cfgNew['GENERAL']['relThFromShp'] == 'True' - assert cfgNew['GENERAL']['relThPercentVariation'] == '50$2' + assert cfgNew["GENERAL"]["musamosat"] == "0.155" + assert cfgNew["GENERAL"]["relTh"] == "" + assert cfgNew["GENERAL"]["relThFromShp"] == "True" + assert cfgNew["GENERAL"]["relThPercentVariation"] == "50$2" com1DFACfg = cfgUtils.getDefaultModuleConfig(com1DFA) - varName = 'musamosat' + varName = "musamosat" varDict = {} - varDict = pA.makeDictFromVars(cfg['PROBRUN']) + varDict = pA.makeDictFromVars(cfg["PROBRUN"]) # call function cfgNew = pA.updateCfgRange(com1DFACfg, cfg, varName, varDict[varName]) - assert cfgNew['GENERAL']['musamosat'] == '0.155$60$2' - assert cfgNew['GENERAL']['relTh'] == '' - assert cfgNew['GENERAL']['relThFromShp'] == 'True' - assert cfgNew['GENERAL']['relThPercentVariation'] == '' + assert cfgNew["GENERAL"]["musamosat"] == "0.155$60$2" + assert cfgNew["GENERAL"]["relTh"] == "" + assert cfgNew["GENERAL"]["relThFromShp"] == "True" + assert cfgNew["GENERAL"]["relThPercentVariation"] == "" com1DFACfg = cfgUtils.getDefaultModuleConfig(com1DFA) - varName = 'relTh' + varName = "relTh" # call function cfgNew = pA.updateCfgRange(com1DFACfg, cfg, varName, varDict[varName]) - assert cfgNew['GENERAL']['musamosat'] == '0.155' - assert cfgNew['GENERAL']['relTh'] == '' - assert cfgNew['GENERAL']['relThFromShp'] == 'True' - assert cfgNew['GENERAL']['relThPercentVariation'] == '50$2' + assert cfgNew["GENERAL"]["musamosat"] == "0.155" + assert cfgNew["GENERAL"]["relTh"] == "" + assert cfgNew["GENERAL"]["relThFromShp"] == "True" + assert cfgNew["GENERAL"]["relThPercentVariation"] == "50$2" # setup inputs cfg = configparser.ConfigParser() - cfg['PROBRUN'] = {'varParList': 'musamosat|relTh', 'variationType': 'range|range', - 'variationValue': '0.1|0.5', 'numberOfSteps': '5|12', 'varParType': 'float|float', - 'defaultSetup': 'True', 'samplingStrategy': '2'} + cfg["PROBRUN"] = { + "varParList": "musamosat|relTh", + "variationType": "range|range", + "variationValue": "0.1|0.5", + "numberOfSteps": "5|12", + "varParType": "float|float", + "defaultSetup": "True", + "samplingStrategy": "2", + } com1DFACfg = cfgUtils.getDefaultModuleConfig(com1DFA) - varName = 'musamosat' + varName = "musamosat" - varDict = pA.makeDictFromVars(cfg['PROBRUN']) + varDict = pA.makeDictFromVars(cfg["PROBRUN"]) # call function cfgNew = pA.updateCfgRange(com1DFACfg, cfg, varName, varDict[varName]) - assert np.isclose(float(cfgNew['GENERAL']['musamosat'].split(':')[0]), 0.055, rtol=1.e-5) - assert np.isclose(float(cfgNew['GENERAL']['musamosat'].split(':')[1]), 0.255, rtol=1.e-5) - assert cfgNew['GENERAL']['musamosat'].split(':')[2] == '5' - assert cfgNew['GENERAL']['relTh'] == '' - assert cfgNew['GENERAL']['relThFromShp'] == 'True' - assert cfgNew['GENERAL']['relThPercentVariation'] == '' + assert np.isclose(float(cfgNew["GENERAL"]["musamosat"].split(":")[0]), 0.055, rtol=1.0e-5) + assert np.isclose(float(cfgNew["GENERAL"]["musamosat"].split(":")[1]), 0.255, rtol=1.0e-5) + assert cfgNew["GENERAL"]["musamosat"].split(":")[2] == "5" + assert cfgNew["GENERAL"]["relTh"] == "" + assert cfgNew["GENERAL"]["relThFromShp"] == "True" + assert cfgNew["GENERAL"]["relThPercentVariation"] == "" com1DFACfg = cfgUtils.getDefaultModuleConfig(com1DFA) - varName = 'relTh' + varName = "relTh" # call function cfgNew = pA.updateCfgRange(com1DFACfg, cfg, varName, varDict[varName]) - assert cfgNew['GENERAL']['musamosat'] == '0.155' - assert cfgNew['GENERAL']['relTh'] == '' - assert cfgNew['GENERAL']['relThFromShp'] == 'True' - assert cfgNew['GENERAL']['relThRangeVariation'] == '0.5$12' + assert cfgNew["GENERAL"]["musamosat"] == "0.155" + assert cfgNew["GENERAL"]["relTh"] == "" + assert cfgNew["GENERAL"]["relThFromShp"] == "True" + assert cfgNew["GENERAL"]["relThRangeVariation"] == "0.5$12" # setup inputs cfg = configparser.ConfigParser() cfg.optionxform = str - cfg['PROBRUN'] = {'varParList': 'musamosat|relTh', 'variationType': 'normaldistribution|normaldistribution', - 'variationValue': '0.1|0.3', 'numberOfSteps': '3|12', - 'defaultSetup': 'True', 'varParType': 'float|float', 'samplingStrategy': '2'} - cfg['in1Data_computeFromDistribution_override'] = {'buildType': 'ci95', 'minMaxInterval': '95', - 'defaultConfig': 'True'} + cfg["PROBRUN"] = { + "varParList": "musamosat|relTh", + "variationType": "normaldistribution|normaldistribution", + "variationValue": "0.1|0.3", + "numberOfSteps": "3|12", + "defaultSetup": "True", + "varParType": "float|float", + "samplingStrategy": "2", + } + cfg["in1Data_computeFromDistribution_override"] = { + "buildType": "ci95", + "minMaxInterval": "95", + "defaultConfig": "True", + } com1DFACfg = cfgUtils.getDefaultModuleConfig(com1DFA) - varName = 'musamosat' + varName = "musamosat" - varDict = pA.makeDictFromVars(cfg['PROBRUN']) + varDict = pA.makeDictFromVars(cfg["PROBRUN"]) # call function cfgNew = pA.updateCfgRange(com1DFACfg, cfg, varName, varDict[varName]) - assert np.isclose(float(cfgNew['GENERAL']['musamosat'].split('|')[0]), 0.055, rtol=1.e-3) - assert np.isclose(float(cfgNew['GENERAL']['musamosat'].split('|')[1]), 0.155, rtol=1.e-3) - assert np.isclose(float(cfgNew['GENERAL']['musamosat'].split('|')[2]), 0.255, rtol=1.e-3) - assert cfgNew['GENERAL']['relTh'] == '' - assert cfgNew['GENERAL']['relThFromShp'] == 'True' - assert cfgNew['GENERAL']['relThDistVariation'] == '' + assert np.isclose(float(cfgNew["GENERAL"]["musamosat"].split("|")[0]), 0.055, rtol=1.0e-3) + assert np.isclose(float(cfgNew["GENERAL"]["musamosat"].split("|")[1]), 0.155, rtol=1.0e-3) + assert np.isclose(float(cfgNew["GENERAL"]["musamosat"].split("|")[2]), 0.255, rtol=1.0e-3) + assert cfgNew["GENERAL"]["relTh"] == "" + assert cfgNew["GENERAL"]["relThFromShp"] == "True" + assert cfgNew["GENERAL"]["relThDistVariation"] == "" com1DFACfg = cfgUtils.getDefaultModuleConfig(com1DFA) - varName = 'relTh' + varName = "relTh" - varDict = pA.makeDictFromVars(cfg['PROBRUN']) + varDict = pA.makeDictFromVars(cfg["PROBRUN"]) # call function cfgNew = pA.updateCfgRange(com1DFACfg, cfg, varName, varDict[varName]) -# print('value', cfgNew['GENERAL']['relThPercentVariation']) + # print('value', cfgNew['GENERAL']['relThPercentVariation']) - assert cfgNew['GENERAL']['musamosat'] == '0.155' - assert cfgNew['GENERAL']['relTh'] == '' - assert cfgNew['GENERAL']['relThFromShp'] == 'True' - assert cfgNew['GENERAL']['relThDistVariation'] == 'normaldistribution$12$0.3$95$ci95$10000' + assert cfgNew["GENERAL"]["musamosat"] == "0.155" + assert cfgNew["GENERAL"]["relTh"] == "" + assert cfgNew["GENERAL"]["relThFromShp"] == "True" + assert cfgNew["GENERAL"]["relThDistVariation"] == "normaldistribution$12$0.3$95$ci95$10000" # setup inputs cfg = configparser.ConfigParser() - cfg['PROBRUN'] = {'varParList': 'musamosat|relTh', 'variationType': 'percent|rangefromci', - 'variationValue': '60|ci95', 'numberOfSteps': '2|2', - 'defaultSetup': 'True', 'varParType': 'float|float', 'samplingStrategy': '2'} + cfg["PROBRUN"] = { + "varParList": "musamosat|relTh", + "variationType": "percent|rangefromci", + "variationValue": "60|ci95", + "numberOfSteps": "2|2", + "defaultSetup": "True", + "varParType": "float|float", + "samplingStrategy": "2", + } com1DFACfg = cfgUtils.getDefaultModuleConfig(com1DFA) - varName = 'relTh' + varName = "relTh" - varDict = pA.makeDictFromVars(cfg['PROBRUN']) + varDict = pA.makeDictFromVars(cfg["PROBRUN"]) # call function cfgNew = pA.updateCfgRange(com1DFACfg, cfg, varName, varDict[varName]) - assert cfgNew['GENERAL']['musamosat'] == '0.155' - assert cfgNew['GENERAL']['relTh'] == '' - assert cfgNew['GENERAL']['relThFromShp'] == 'True' - assert cfgNew['GENERAL']['relThPercentVariation'] == '' - assert cfgNew['GENERAL']['relThRangeFromCiVariation'] == 'ci95$2' + assert cfgNew["GENERAL"]["musamosat"] == "0.155" + assert cfgNew["GENERAL"]["relTh"] == "" + assert cfgNew["GENERAL"]["relThFromShp"] == "True" + assert cfgNew["GENERAL"]["relThPercentVariation"] == "" + assert cfgNew["GENERAL"]["relThRangeFromCiVariation"] == "ci95$2" # setup inputs cfg = configparser.ConfigParser() - cfg['PROBRUN'] = {'varParList': 'musamosat|relTh', 'variationType': 'percent|range', - 'variationValue': '60|0.25', 'numberOfSteps': '2|8', - 'defaultSetup': 'True', 'varParType': 'float|float', 'samplingStrategy': '2'} + cfg["PROBRUN"] = { + "varParList": "musamosat|relTh", + "variationType": "percent|range", + "variationValue": "60|0.25", + "numberOfSteps": "2|8", + "defaultSetup": "True", + "varParType": "float|float", + "samplingStrategy": "2", + } com1DFACfg = cfgUtils.getDefaultModuleConfig(com1DFA) - varName = 'relTh' + varName = "relTh" - varDict = pA.makeDictFromVars(cfg['PROBRUN']) + varDict = pA.makeDictFromVars(cfg["PROBRUN"]) # call function cfgNew = pA.updateCfgRange(com1DFACfg, cfg, varName, varDict[varName]) -# print('cfgNEW', cfgNew['GENERAL']['relThRangeVariation']) + # print('cfgNEW', cfgNew['GENERAL']['relThRangeVariation']) - assert cfgNew['GENERAL']['musamosat'] == '0.155' - assert cfgNew['GENERAL']['relTh'] == '' - assert cfgNew['GENERAL']['relThFromShp'] == 'True' - assert cfgNew['GENERAL']['relThPercentVariation'] == '' - assert cfgNew['GENERAL']['relThRangeVariation'] == '0.25$8' + assert cfgNew["GENERAL"]["musamosat"] == "0.155" + assert cfgNew["GENERAL"]["relTh"] == "" + assert cfgNew["GENERAL"]["relThFromShp"] == "True" + assert cfgNew["GENERAL"]["relThPercentVariation"] == "" + assert cfgNew["GENERAL"]["relThRangeVariation"] == "0.25$8" def test_makeDictFromVars(): - """ test creating a dict from parameter variation info """ + """test creating a dict from parameter variation info""" cfgProb = configparser.ConfigParser() cfgProb.optionxform = str - cfgProb['PROBRUN'] = {'varParList': 'musamosat|relTh|entTh', 'variationType': 'percent|rangeFromCi|range', - 'variationValue': '60|ci95|0.15', 'numberOfSteps': '2|3|4', - 'samplingStrategy': '2', - 'varParType': 'float|float', - 'varParType': 'float|float|float', 'nSample': '30', 'sampleSeed': '12345', - 'sampleMethod': 'latin'} + cfgProb["PROBRUN"] = { + "varParList": "musamosat|relTh|entTh", + "variationType": "percent|rangeFromCi|range", + "variationValue": "60|ci95|0.15", + "numberOfSteps": "2|3|4", + "samplingStrategy": "2", + "varParType": "float|float", + "varParType": "float|float|float", + "nSample": "30", + "sampleSeed": "12345", + "sampleMethod": "latin", + } # call function to be tested - variationD = pA.makeDictFromVars(cfgProb['PROBRUN']) + variationD = pA.makeDictFromVars(cfgProb["PROBRUN"]) assert len(variationD) == 3 - assert len(variationD['relTh']) == 3 - assert variationD['relTh']['numberOfSteps'] == '3' + assert len(variationD["relTh"]) == 3 + assert variationD["relTh"]["numberOfSteps"] == "3" - cfgProb['PROBRUN']['variationType'] = 'percent|range' + cfgProb["PROBRUN"]["variationType"] = "percent|range" - message = ('For every parameter in varParList a variationValue, numberOfSteps and variationType needs to be provided') + message = "For every parameter in varParList a variationValue, numberOfSteps and variationType needs to be provided" with pytest.raises(AssertionError) as e: - assert pA.makeDictFromVars(cfgProb['PROBRUN']) + assert pA.makeDictFromVars(cfgProb["PROBRUN"]) assert message in str(e.value) - cfgProb['PROBRUN']['variationType'] = 'percent|rangefromci|range' - cfgProb['PROBRUN']['variationValue'] = '60|50|0.15' + cfgProb["PROBRUN"]["variationType"] = "percent|rangefromci|range" + cfgProb["PROBRUN"]["variationValue"] = "60|50|0.15" - message = 'If rangefromci is chosen as variation type, ci95 is required as variationValue' + message = "If rangefromci is chosen as variation type, ci95 is required as variationValue" with pytest.raises(AssertionError) as e: - assert pA.makeDictFromVars(cfgProb['PROBRUN']) + assert pA.makeDictFromVars(cfgProb["PROBRUN"]) assert message in str(e.value) def test_createSampleWithVariationStandardParameters(): - """ test creation of sample for standard parameters """ + """test creation of sample for standard parameters""" cfgProb = configparser.ConfigParser() cfgProb.optionxform = str - cfgProb['PROBRUN'] = {'varParList': 'musamosat|relTh|entTh', 'variationType': 'percent|range|range', - 'variationValue': '60|0.25|0.15', 'numberOfSteps': '2|3|4', - 'samplingStrategy': '1', 'varParType': 'float|float|float', - 'varParType': 'float|float|float', 'nSample': '30', 'sampleSeed': '12345', - 'sampleMethod': 'latin'} - - varParList = cfgProb['PROBRUN']['varParList'].split('|') - valVariationValue = cfgProb['PROBRUN']['variationValue'].split('|') - varType = cfgProb['PROBRUN']['variationType'].split('|') + cfgProb["PROBRUN"] = { + "varParList": "musamosat|relTh|entTh", + "variationType": "percent|range|range", + "variationValue": "60|0.25|0.15", + "numberOfSteps": "2|3|4", + "samplingStrategy": "1", + "varParType": "float|float|float", + "varParType": "float|float|float", + "nSample": "30", + "sampleSeed": "12345", + "sampleMethod": "latin", + } + + varParList = cfgProb["PROBRUN"]["varParList"].split("|") + valVariationValue = cfgProb["PROBRUN"]["variationValue"].split("|") + varType = cfgProb["PROBRUN"]["variationType"].split("|") # read initial configuration cfgStart = cfgUtils.getDefaultModuleConfig(com1DFA, toPrint=False) - cfgStart['GENERAL']['relThFromShp'] = 'False' - cfgStart['GENERAL']['relTh'] = '1.5' - cfgStart['GENERAL']['entTh'] = '0.4' - paramValuesD = pA.createSampleWithVariationStandardParameters(cfgProb, cfgStart, varParList, valVariationValue, - varType) - - assert len(paramValuesD['values']) == 30 - assert paramValuesD['names'] == ['musamosat', 'relTh', 'entTh'] - assert paramValuesD['thFromIni'] == '' - assert np.amax(paramValuesD['values'][:, 1]) <= 1.75 - assert np.amin(paramValuesD['values'][:, 1]) >= 1.25 - assert np.amax(paramValuesD['values'][:, 0]) <= 0.248 - assert np.amin(paramValuesD['values'][:, 0]) >= 0.062 - assert np.amax(paramValuesD['values'][:, 2]) <= 0.55 - assert np.amin(paramValuesD['values'][:, 2]) >= 0.25 + cfgStart["GENERAL"]["relThFromShp"] = "False" + cfgStart["GENERAL"]["relTh"] = "1.5" + cfgStart["GENERAL"]["entTh"] = "0.4" + paramValuesD = pA.createSampleWithVariationStandardParameters( + cfgProb, cfgStart, varParList, valVariationValue, varType + ) + + assert len(paramValuesD["values"]) == 30 + assert paramValuesD["names"] == ["musamosat", "relTh", "entTh"] + assert paramValuesD["thFromIni"] == "" + assert np.amax(paramValuesD["values"][:, 1]) <= 1.75 + assert np.amin(paramValuesD["values"][:, 1]) >= 1.25 + assert np.amax(paramValuesD["values"][:, 0]) <= 0.248 + assert np.amin(paramValuesD["values"][:, 0]) >= 0.062 + assert np.amax(paramValuesD["values"][:, 2]) <= 0.55 + assert np.amin(paramValuesD["values"][:, 2]) >= 0.25 def test_createSampleWithVariationForThParameters(tmp_path): - """ test if thickness parameters are also included """ + """test if thickness parameters are also included""" # set input directory - avaName = 'testCom1DFA2' + avaName = "testCom1DFA2" dirPath = pathlib.Path(__file__).parents[0] - inputDir = dirPath / 'data' / avaName + inputDir = dirPath / "data" / avaName avaDir = pathlib.Path(tmp_path, avaName) shutil.copytree(inputDir, avaDir) cfgProb = configparser.ConfigParser() cfgProb.optionxform = str - cfgProb['PROBRUN'] = {'varParList': 'musamosat|relTh|entTh', 'variationType': 'percent|rangefromci|range', - 'variationValue': '60|ci95|0.15', 'numberOfSteps': '2|3|4', - 'samplingStrategy': '1', - 'varParType': 'float|float|float', 'nSample': '30', 'sampleSeed': '12345', - 'sampleMethod': 'latin'} - - varParList = cfgProb['PROBRUN']['varParList'].split('|') - valVariationValue = cfgProb['PROBRUN']['variationValue'].split('|') - varType = cfgProb['PROBRUN']['variationType'].split('|') + cfgProb["PROBRUN"] = { + "varParList": "musamosat|relTh|entTh", + "variationType": "percent|rangefromci|range", + "variationValue": "60|ci95|0.15", + "numberOfSteps": "2|3|4", + "samplingStrategy": "1", + "varParType": "float|float|float", + "nSample": "30", + "sampleSeed": "12345", + "sampleMethod": "latin", + } + + varParList = cfgProb["PROBRUN"]["varParList"].split("|") + valVariationValue = cfgProb["PROBRUN"]["variationValue"].split("|") + varType = cfgProb["PROBRUN"]["variationType"].split("|") # read initial configuration cfgStart = cfgUtils.getDefaultModuleConfig(com1DFA, toPrint=False) - thReadFromShp = ['relTh', 'entTh'] + thReadFromShp = ["relTh", "entTh"] - paramValuesDList = pA.createSampleWithVariationForThParameters(avaDir, cfgProb, cfgStart, varParList, - valVariationValue, varType, thReadFromShp) + paramValuesDList = pA.createSampleWithVariationForThParameters( + avaDir, cfgProb, cfgStart, varParList, valVariationValue, varType, thReadFromShp + ) paramValuesD = paramValuesDList[0] - assert len(paramValuesD['values']) == 30 - assert paramValuesD['names'] == ['musamosat', 'relTh0', 'relTh1', 'entTh0'] - assert len(paramValuesD['thFromIni'].split('|')) == 2 - assert 'relTh' in paramValuesD['thFromIni'] - assert 'entTh' in paramValuesD['thFromIni'] - assert np.amax(paramValuesD['values'][:, 1]) <= 1.75 - assert np.amin(paramValuesD['values'][:, 1]) >= 1.25 - assert np.amax(paramValuesD['values'][:, 2]) <= 1.4 - assert np.amin(paramValuesD['values'][:, 2]) >= 1.0 - assert np.amax(paramValuesD['values'][:, 0]) <= 0.248 - assert np.amin(paramValuesD['values'][:, 0]) >= 0.062 - assert np.amax(paramValuesD['values'][:, 3]) <= 0.45 - assert np.amin(paramValuesD['values'][:, 3]) >= 0.15 - - cfgProb['PROBRUN'] = {'varParList': 'musamosat|relTh|entTh', 'variationType': 'percent|rangefromci|rangefromci', - 'variationValue': '60|ci95|ci95', 'numberOfSteps': '2|3|4', - 'samplingStrategy': '1', - 'varParType': 'float|float|float', 'nSample': '30', 'sampleSeed': '12345', - 'sampleMethod': 'latin'} - - varParList = cfgProb['PROBRUN']['varParList'].split('|') - valVariationValue = cfgProb['PROBRUN']['variationValue'].split('|') - varType = cfgProb['PROBRUN']['variationType'].split('|') + assert len(paramValuesD["values"]) == 30 + assert paramValuesD["names"] == ["musamosat", "relTh0", "relTh1", "entTh0"] + assert len(paramValuesD["thFromIni"].split("|")) == 2 + assert "relTh" in paramValuesD["thFromIni"] + assert "entTh" in paramValuesD["thFromIni"] + assert np.amax(paramValuesD["values"][:, 1]) <= 1.75 + assert np.amin(paramValuesD["values"][:, 1]) >= 1.25 + assert np.amax(paramValuesD["values"][:, 2]) <= 1.4 + assert np.amin(paramValuesD["values"][:, 2]) >= 1.0 + assert np.amax(paramValuesD["values"][:, 0]) <= 0.248 + assert np.amin(paramValuesD["values"][:, 0]) >= 0.062 + assert np.amax(paramValuesD["values"][:, 3]) <= 0.45 + assert np.amin(paramValuesD["values"][:, 3]) >= 0.15 + + cfgProb["PROBRUN"] = { + "varParList": "musamosat|relTh|entTh", + "variationType": "percent|rangefromci|rangefromci", + "variationValue": "60|ci95|ci95", + "numberOfSteps": "2|3|4", + "samplingStrategy": "1", + "varParType": "float|float|float", + "nSample": "30", + "sampleSeed": "12345", + "sampleMethod": "latin", + } + + varParList = cfgProb["PROBRUN"]["varParList"].split("|") + valVariationValue = cfgProb["PROBRUN"]["variationValue"].split("|") + varType = cfgProb["PROBRUN"]["variationType"].split("|") # read initial configuration cfgStart = cfgUtils.getDefaultModuleConfig(com1DFA, toPrint=False) - thReadFromShp = ['relTh', 'entTh'] + thReadFromShp = ["relTh", "entTh"] - paramValuesDList = pA.createSampleWithVariationForThParameters(avaDir, cfgProb, cfgStart, varParList, - valVariationValue, varType, thReadFromShp) + paramValuesDList = pA.createSampleWithVariationForThParameters( + avaDir, cfgProb, cfgStart, varParList, valVariationValue, varType, thReadFromShp + ) paramValuesD = paramValuesDList[0] - assert len(paramValuesD['values']) == 30 - assert paramValuesD['names'] == ['musamosat', 'relTh0', 'relTh1', 'entTh0'] - assert len(paramValuesD['thFromIni'].split('|')) == 2 - assert 'relTh' in paramValuesD['thFromIni'] - assert 'entTh' in paramValuesD['thFromIni'] - assert np.amax(paramValuesD['values'][:, 1]) <= 1.75 - assert np.amin(paramValuesD['values'][:, 1]) >= 1.25 - assert np.amax(paramValuesD['values'][:, 2]) <= 1.4 - assert np.amin(paramValuesD['values'][:, 2]) >= 1.0 - assert np.amax(paramValuesD['values'][:, 0]) <= 0.248 - assert np.amin(paramValuesD['values'][:, 0]) >= 0.062 - assert np.amax(paramValuesD['values'][:, 3]) <= 0.4 - assert np.amin(paramValuesD['values'][:, 3]) >= 0.1 - - cfgProb['PROBRUN'] = {'varParList': 'musamosat|relTh|entTh', 'variationType': 'percent|rangefromci|percent', - 'variationValue': '60|ci95|5', 'numberOfSteps': '2|3|4', - 'samplingStrategy': '1', - 'varParType': 'float|float|float', 'nSample': '30', 'sampleSeed': '12345', - 'sampleMethod': 'latin'} - - varParList = cfgProb['PROBRUN']['varParList'].split('|') - valVariationValue = cfgProb['PROBRUN']['variationValue'].split('|') - varType = cfgProb['PROBRUN']['variationType'].split('|') + assert len(paramValuesD["values"]) == 30 + assert paramValuesD["names"] == ["musamosat", "relTh0", "relTh1", "entTh0"] + assert len(paramValuesD["thFromIni"].split("|")) == 2 + assert "relTh" in paramValuesD["thFromIni"] + assert "entTh" in paramValuesD["thFromIni"] + assert np.amax(paramValuesD["values"][:, 1]) <= 1.75 + assert np.amin(paramValuesD["values"][:, 1]) >= 1.25 + assert np.amax(paramValuesD["values"][:, 2]) <= 1.4 + assert np.amin(paramValuesD["values"][:, 2]) >= 1.0 + assert np.amax(paramValuesD["values"][:, 0]) <= 0.248 + assert np.amin(paramValuesD["values"][:, 0]) >= 0.062 + assert np.amax(paramValuesD["values"][:, 3]) <= 0.4 + assert np.amin(paramValuesD["values"][:, 3]) >= 0.1 + + cfgProb["PROBRUN"] = { + "varParList": "musamosat|relTh|entTh", + "variationType": "percent|rangefromci|percent", + "variationValue": "60|ci95|5", + "numberOfSteps": "2|3|4", + "samplingStrategy": "1", + "varParType": "float|float|float", + "nSample": "30", + "sampleSeed": "12345", + "sampleMethod": "latin", + } + + varParList = cfgProb["PROBRUN"]["varParList"].split("|") + valVariationValue = cfgProb["PROBRUN"]["variationValue"].split("|") + varType = cfgProb["PROBRUN"]["variationType"].split("|") # read initial configuration cfgStart = cfgUtils.getDefaultModuleConfig(com1DFA, toPrint=False) - cfgStart['GENERAL']['entTh'] = '0.5' - cfgStart['GENERAL']['entThFromShp'] = 'False' - thReadFromShp = ['relTh'] + cfgStart["GENERAL"]["entTh"] = "0.5" + cfgStart["GENERAL"]["entThFromShp"] = "False" + thReadFromShp = ["relTh"] - paramValuesDList = pA.createSampleWithVariationForThParameters(avaDir, cfgProb, cfgStart, varParList, - valVariationValue, varType, thReadFromShp) + paramValuesDList = pA.createSampleWithVariationForThParameters( + avaDir, cfgProb, cfgStart, varParList, valVariationValue, varType, thReadFromShp + ) paramValuesD = paramValuesDList[0] - assert len(paramValuesD['values']) == 30 - assert paramValuesD['names'] == ['musamosat', 'relTh0', 'relTh1', 'entTh'] - assert len(paramValuesD['thFromIni'].split('|')) == 1 - assert 'relTh' in paramValuesD['thFromIni'] - assert np.amax(paramValuesD['values'][:, 1]) <= 1.75 - assert np.amin(paramValuesD['values'][:, 1]) >= 1.25 - assert np.amax(paramValuesD['values'][:, 2]) <= 1.4 - assert np.amin(paramValuesD['values'][:, 2]) >= 1.0 - assert np.amax(paramValuesD['values'][:, 0]) <= 0.248 - assert np.amin(paramValuesD['values'][:, 0]) >= 0.062 - assert np.amax(paramValuesD['values'][:, 3]) <= 0.525 - assert np.amin(paramValuesD['values'][:, 3]) >= 0.475 + assert len(paramValuesD["values"]) == 30 + assert paramValuesD["names"] == ["musamosat", "relTh0", "relTh1", "entTh"] + assert len(paramValuesD["thFromIni"].split("|")) == 1 + assert "relTh" in paramValuesD["thFromIni"] + assert np.amax(paramValuesD["values"][:, 1]) <= 1.75 + assert np.amin(paramValuesD["values"][:, 1]) >= 1.25 + assert np.amax(paramValuesD["values"][:, 2]) <= 1.4 + assert np.amin(paramValuesD["values"][:, 2]) >= 1.0 + assert np.amax(paramValuesD["values"][:, 0]) <= 0.248 + assert np.amin(paramValuesD["values"][:, 0]) >= 0.062 + assert np.amax(paramValuesD["values"][:, 3]) <= 0.525 + assert np.amin(paramValuesD["values"][:, 3]) >= 0.475 def test_createCfgFiles(tmp_path): - """ test writing of cfg files """ + """test writing of cfg files""" - paramValuesD = {'names': ['relTh', 'musamosat'], 'values': np.asarray([[1.2, 0.1], [1.4, 0.12], [1.6, 0.14]]), - 'thFromIni': ''} + paramValuesD = { + "names": ["relTh", "musamosat"], + "values": np.asarray([[1.2, 0.1], [1.4, 0.12], [1.6, 0.14]]), + "thFromIni": "", + } cfgProb = configparser.ConfigParser() cfgProb.optionxform = str - cfgProb['PROBRUN'] = {} - cfgProb['com1DFA_com1DFA_override'] = {'defaultConfig': True, 'relTh': '', 'musamosat': 0.2, 'thFromIni': False} + cfgProb["PROBRUN"] = {} + cfgProb["com1DFA_com1DFA_override"] = { + "defaultConfig": True, + "relTh": "", + "musamosat": 0.2, + "thFromIni": False, + } - cfgFiles = pA.createCfgFiles([paramValuesD], com1DFA, cfgProb, cfgPath='') + cfgFiles = pA.createCfgFiles([paramValuesD], com1DFA, cfgProb, cfgPath="") -# print(cfgFiles) + # print(cfgFiles) cfgTest1 = configparser.ConfigParser() cfgTest1.read(cfgFiles[0]) @@ -641,194 +787,220 @@ def test_createCfgFiles(tmp_path): cfgTest3.read(cfgFiles[2]) assert len(cfgFiles) == 3 - assert cfgTest1['GENERAL'].getfloat('relTh') == 1.2 - assert cfgTest1['GENERAL'].getfloat('musamosat') == 0.1 - assert cfgTest1['INPUT']['thFromIni'] == '' - assert cfgTest1['VISUALISATION']['scenario'] == '0' - assert cfgTest2['GENERAL'].getfloat('relTh') == 1.4 - assert cfgTest2['GENERAL'].getfloat('musamosat') == 0.12 - assert cfgTest2['INPUT']['thFromIni'] == '' - assert cfgTest2['VISUALISATION']['scenario'] == '1' - assert cfgTest3['GENERAL'].getfloat('relTh') == 1.6 - assert cfgTest3['GENERAL'].getfloat('musamosat') == 0.14 - assert cfgTest3['INPUT']['thFromIni'] == '' - assert cfgTest3['VISUALISATION']['scenario'] == '2' - - cfgProb['PROBRUN'] = {} - cfgProb['com1DFA_override'] = {'defaultConfig': True} + assert cfgTest1["GENERAL"].getfloat("relTh") == 1.2 + assert cfgTest1["GENERAL"].getfloat("musamosat") == 0.1 + assert cfgTest1["INPUT"]["thFromIni"] == "" + assert cfgTest1["VISUALISATION"]["scenario"] == "0" + assert cfgTest2["GENERAL"].getfloat("relTh") == 1.4 + assert cfgTest2["GENERAL"].getfloat("musamosat") == 0.12 + assert cfgTest2["INPUT"]["thFromIni"] == "" + assert cfgTest2["VISUALISATION"]["scenario"] == "1" + assert cfgTest3["GENERAL"].getfloat("relTh") == 1.6 + assert cfgTest3["GENERAL"].getfloat("musamosat") == 0.14 + assert cfgTest3["INPUT"]["thFromIni"] == "" + assert cfgTest3["VISUALISATION"]["scenario"] == "2" + + cfgProb["PROBRUN"] = {} + cfgProb["com1DFA_override"] = {"defaultConfig": True} def test_fetchStartCfg(tmp_path): - """ test fetching starting cfg """ + """test fetching starting cfg""" cfg = configparser.ConfigParser() cfg.optionxform = str - cfg['com1DFA_com1DFA_override'] = {'defaultConfig': True, 'musamosat': 0.2} + cfg["com1DFA_com1DFA_override"] = {"defaultConfig": True, "musamosat": 0.2} cfgStart = pA.fetchStartCfg(com1DFA, cfg) - assert cfgStart.has_option('GENERAL', 'entTh') is True - assert cfgStart['GENERAL'].getfloat('musamosat') == 0.2 + assert cfgStart.has_option("GENERAL", "entTh") is True + assert cfgStart["GENERAL"].getfloat("musamosat") == 0.2 - cfg['com1DFA_com1DFA_override'] = {'defaultConfig': True} + cfg["com1DFA_com1DFA_override"] = {"defaultConfig": True} cfgStart = pA.fetchStartCfg(com1DFA, cfg) - assert cfgStart.has_option('GENERAL', 'entTh') is True - assert cfgStart['GENERAL'].getfloat('musamosat') == 0.155 + assert cfgStart.has_option("GENERAL", "entTh") is True + assert cfgStart["GENERAL"].getfloat("musamosat") == 0.155 def test_fetchProbConfigs(): - """ test creation of probability configurations """ + """test creation of probability configurations""" cfg = configparser.ConfigParser() cfg.optionxform = str - cfg['PROBRUN'] = {'samplingStrategy': '2', 'varParList': 'relTh|musamosat'} + cfg["PROBRUN"] = {"samplingStrategy": "2", "varParList": "relTh|musamosat"} - probConfigs = pA.fetchProbConfigs(cfg['PROBRUN']) + probConfigs = pA.fetchProbConfigs(cfg["PROBRUN"]) assert len(probConfigs) == 3 - assert 'includemusamosat' in probConfigs.keys() - assert 'includerelTh' in probConfigs.keys() + assert "includemusamosat" in probConfigs.keys() + assert "includerelTh" in probConfigs.keys() - cfg['PROBRUN'] = {'samplingStrategy': '1', 'varParList': 'relTh|musamosat'} + cfg["PROBRUN"] = {"samplingStrategy": "1", "varParList": "relTh|musamosat"} - probConfigs = pA.fetchProbConfigs(cfg['PROBRUN']) + probConfigs = pA.fetchProbConfigs(cfg["PROBRUN"]) assert len(probConfigs) == 1 - assert 'includeAll' in probConfigs.keys() + assert "includeAll" in probConfigs.keys() def test_fetchThicknessInfo(tmp_path): - """ test fetching info on thickness settings if readFromShp """ - avaName = 'testCom1DFA2' + """test fetching info on thickness settings if readFromShp""" + avaName = "testCom1DFA2" dirPath = pathlib.Path(__file__).parents[0] - inputDir = dirPath / 'data' / avaName + inputDir = dirPath / "data" / avaName avaDir = pathlib.Path(tmp_path, avaName) shutil.copytree(inputDir, avaDir) inputSimFiles = pA.fetchThicknessInfo(avaDir) - assert inputSimFiles['releaseScenarioList'] == ['relParabola', 'relParabolaTwo'] - assert inputSimFiles['relParabola']['thickness'][0] == '1.5' - assert inputSimFiles['relParabola']['thickness'][1] == '1.2' - assert inputSimFiles['relParabola']['id'][0] == '0' - assert inputSimFiles['relParabola']['id'][1] == '1' - assert inputSimFiles['relParabola']['ci95'][0] == '0.25' - assert inputSimFiles['relParabola']['ci95'][1] == '0.2' + assert inputSimFiles["releaseScenarioList"] == ["relParabola", "relParabolaTwo"] + assert inputSimFiles["relParabola"]["thickness"][0] == "1.5" + assert inputSimFiles["relParabola"]["thickness"][1] == "1.2" + assert inputSimFiles["relParabola"]["id"][0] == "0" + assert inputSimFiles["relParabola"]["id"][1] == "1" + assert inputSimFiles["relParabola"]["ci95"][0] == "0.25" + assert inputSimFiles["relParabola"]["ci95"][1] == "0.2" def test_fetchThicknessList(tmp_path): - """ test fetching the thickness info """ + """test fetching the thickness info""" - avaName = 'testCom1DFA2' + avaName = "testCom1DFA2" dirPath = pathlib.Path(__file__).parents[0] - inputDir = dirPath / 'data' / avaName + inputDir = dirPath / "data" / avaName avaDir = pathlib.Path(tmp_path, avaName) shutil.copytree(inputDir, avaDir) # fetch input files and corresponding thickness info inputSimFiles = pA.fetchThicknessInfo(avaDir) - thValues, ciValues, thicknessFeatureNames = pA.fetchThThicknessLists('relTh', inputSimFiles, - inputSimFiles['relFiles'][0], ciRequired=True) + thValues, ciValues, thicknessFeatureNames = pA.fetchThThicknessLists( + "relTh", inputSimFiles, inputSimFiles["relFiles"][0], ciRequired=True + ) assert thValues[0] == 1.5 assert thValues[1] == 1.2 assert ciValues[0] == 0.25 assert ciValues[1] == 0.2 - assert thicknessFeatureNames[0] == 'relTh0' - assert thicknessFeatureNames[1] == 'relTh1' + assert thicknessFeatureNames[0] == "relTh0" + assert thicknessFeatureNames[1] == "relTh1" def test_cfgFilesGlobalApproach(tmp_path): - """ test global approach to fetch sample and create cfg files """ + """test global approach to fetch sample and create cfg files""" # set input directory - avaName = 'testCom1DFA2' + avaName = "testCom1DFA2" dirPath = pathlib.Path(__file__).parents[0] - inputDir = dirPath / 'data' / avaName + inputDir = dirPath / "data" / avaName avaDir = pathlib.Path(tmp_path, avaName) shutil.copytree(inputDir, avaDir) - cfgFile = dirPath / 'data' / 'testCom1DFA' / 'probA_com1DFACfg.ini' - outDir = avaDir / 'Outputs' + cfgFile = dirPath / "data" / "testCom1DFA" / "probA_com1DFACfg.ini" + outDir = avaDir / "Outputs" cfgProb = configparser.ConfigParser() cfgProb.optionxform = str - cfgProb['PROBRUN'] = {'varParList': 'musamosat|relTh', 'variationType': 'percent|percent', - 'variationValue': '60|50', 'numberOfSteps': '2|3', - 'defaultSetup': 'True', 'samplingStrategy': '1', - 'varParType': 'float|float', 'nSample': '40', 'sampleSeed': '12345', - 'sampleMethod': 'latin'} - cfgProb['com1DFA_com1DFA_override'] = {'defaultConfig': 'True', 'relThFromShp': False, 'relTh': 2., - 'musamosat': 0.155, 'frictModel': 'samosAT'} + cfgProb["PROBRUN"] = { + "varParList": "musamosat|relTh", + "variationType": "percent|percent", + "variationValue": "60|50", + "numberOfSteps": "2|3", + "defaultSetup": "True", + "samplingStrategy": "1", + "varParType": "float|float", + "nSample": "40", + "sampleSeed": "12345", + "sampleMethod": "latin", + } + cfgProb["com1DFA_com1DFA_override"] = { + "defaultConfig": "True", + "relThFromShp": False, + "relTh": 2.0, + "musamosat": 0.155, + "frictModel": "samosAT", + } # call function to be tested cfgFiles = pA.cfgFilesGlobalApproach(avaDir, cfgProb, com1DFA, outDir) -# print('cfgFiles', cfgFiles) + # print('cfgFiles', cfgFiles) cfgTest = configparser.ConfigParser() cfgTest.read(cfgFiles[0]) -# print('cfgTest', cfgTest['GENERAL']['relThFromShp'], cfgTest['GENERAL']['relTh'], + # print('cfgTest', cfgTest['GENERAL']['relThFromShp'], cfgTest['GENERAL']['relTh'], # cfgTest['GENERAL']['relThPercentVariation'], cfgTest['GENERAL']['musamosat']) - assert cfgTest['GENERAL']['relThFromShp'] == 'False' - assert cfgTest['GENERAL']['relTh'] == '2.2719559079879' + assert cfgTest["GENERAL"]["relThFromShp"] == "False" + assert cfgTest["GENERAL"]["relTh"] == "2.2719559079879" assert len(cfgFiles) == 40 cfgProb = configparser.ConfigParser() cfgProb.optionxform = str - cfgProb['PROBRUN'] = {'varParList': 'musamosat|relTh', 'variationType': 'percent|percent', - 'variationValue': '60|50', 'numberOfSteps': '2|3', - 'defaultSetup': 'True', 'samplingStrategy': '1', - 'varParType': 'float|float', 'nSample': '40', 'sampleSeed': '12345', - 'sampleMethod': 'latin'} - cfgProb['com1DFA_com1DFA_override'] = {'defaultConfig': 'True'} + cfgProb["PROBRUN"] = { + "varParList": "musamosat|relTh", + "variationType": "percent|percent", + "variationValue": "60|50", + "numberOfSteps": "2|3", + "defaultSetup": "True", + "samplingStrategy": "1", + "varParType": "float|float", + "nSample": "40", + "sampleSeed": "12345", + "sampleMethod": "latin", + } + cfgProb["com1DFA_com1DFA_override"] = {"defaultConfig": "True"} # call function to be tested cfgFiles2 = pA.cfgFilesGlobalApproach(avaDir, cfgProb, com1DFA, outDir) -# print('cfgFiles', cfgFiles) + # print('cfgFiles', cfgFiles) cfgTest1 = configparser.ConfigParser() cfgTest1.read(cfgFiles2[0]) -# print('cfgTest', cfgTest1['GENERAL']['relThFromShp'], cfgTest1['GENERAL']['relTh'], + # print('cfgTest', cfgTest1['GENERAL']['relThFromShp'], cfgTest1['GENERAL']['relTh'], # cfgTest1['GENERAL']['relThPercentVariation'], cfgTest1['GENERAL']['musamosat']) - assert cfgTest1['GENERAL']['relThFromShp'] == 'True' - assert cfgTest1['GENERAL']['relTh'] == '' - assert cfgTest1['GENERAL']['relTh0'] != '' - assert cfgTest1['INPUT']['releaseScenario'] == 'relParabola' + assert cfgTest1["GENERAL"]["relThFromShp"] == "True" + assert cfgTest1["GENERAL"]["relTh"] == "" + assert cfgTest1["GENERAL"]["relTh0"] != "" + assert cfgTest1["INPUT"]["releaseScenario"] == "relParabola" assert len(cfgFiles2) == 80 def test_cfgFilesLocalApproach(tmp_path): - """ test creating cfg files from one at a time var """ + """test creating cfg files from one at a time var""" # set input directory - avaName = 'testCom1DFA2' + avaName = "testCom1DFA2" dirPath = pathlib.Path(__file__).parents[0] - inputDir = dirPath / 'data' / avaName + inputDir = dirPath / "data" / avaName avaDir = pathlib.Path(tmp_path, avaName) shutil.copytree(inputDir, avaDir) - cfgFile = dirPath / 'data' / 'testCom1DFA' / 'probA_com1DFACfg.ini' + cfgFile = dirPath / "data" / "testCom1DFA" / "probA_com1DFACfg.ini" outDir = avaDir cfgProb = configparser.ConfigParser() cfgProb.optionxform = str - cfgProb['PROBRUN'] = {'varParList': 'musamosat|relTh', 'variationType': 'percent|percent', - 'variationValue': '60|50', 'numberOfSteps': '2|3', - 'defaultSetup': 'True', 'samplingStrategy': '2', 'varParType': 'float|float'} - cfgProb['sampling_override'] = {'defaultConfig': 'True'} - cfgProb['com1DFA_com1DFA_override'] = {'defaultConfig': 'True'} + cfgProb["PROBRUN"] = { + "varParList": "musamosat|relTh", + "variationType": "percent|percent", + "variationValue": "60|50", + "numberOfSteps": "2|3", + "defaultSetup": "True", + "samplingStrategy": "2", + "varParType": "float|float", + } + cfgProb["sampling_override"] = {"defaultConfig": "True"} + cfgProb["com1DFA_com1DFA_override"] = {"defaultConfig": "True"} # check variation settings - variationsDict = pA.makeDictFromVars(cfgProb['PROBRUN']) + variationsDict = pA.makeDictFromVars(cfgProb["PROBRUN"]) # call function to be tested cfgFiles = pA.cfgFilesLocalApproach(variationsDict, cfgProb, com1DFA, outDir) -# print('cfgFiles', cfgFiles) + # print('cfgFiles', cfgFiles) # load cfg from file cfgMu = configparser.ConfigParser() @@ -837,34 +1009,44 @@ def test_cfgFilesLocalApproach(tmp_path): cfgMu.read(cfgFiles[0]) cfgRelTh.read(cfgFiles[1]) -# print(cfgMu['GENERAL']['musamosat'], cfgMu['GENERAL']['relTh'], cfgRelTh['GENERAL']['musamosat'], + # print(cfgMu['GENERAL']['musamosat'], cfgMu['GENERAL']['relTh'], cfgRelTh['GENERAL']['musamosat'], # cfgRelTh['GENERAL']['relTh']) - assert cfgMu['GENERAL']['musamosat'] == '0.155$60$2' - assert cfgMu['GENERAL']['relTh'] == '' - assert cfgRelTh['GENERAL']['musamosat'] == '0.155' - assert cfgRelTh['GENERAL']['relTh'] == '' - assert cfgRelTh['GENERAL']['relThFromShp'] == 'True' - assert cfgRelTh['GENERAL']['relThPercentVariation'] == '50$3' - assert cfgRelTh['GENERAL']['relThFromShp'] == 'True' - assert cfgMu['GENERAL']['relThFromShp'] == 'True' + assert cfgMu["GENERAL"]["musamosat"] == "0.155$60$2" + assert cfgMu["GENERAL"]["relTh"] == "" + assert cfgRelTh["GENERAL"]["musamosat"] == "0.155" + assert cfgRelTh["GENERAL"]["relTh"] == "" + assert cfgRelTh["GENERAL"]["relThFromShp"] == "True" + assert cfgRelTh["GENERAL"]["relThPercentVariation"] == "50$3" + assert cfgRelTh["GENERAL"]["relThFromShp"] == "True" + assert cfgMu["GENERAL"]["relThFromShp"] == "True" cfgProb = configparser.ConfigParser() cfgProb.optionxform = str - cfgProb['PROBRUN'] = {'varParList': 'musamosat|relTh', 'variationType': 'percent|range', - 'variationValue': '60|0.5', 'numberOfSteps': '2|3', - 'samplingStrategy': '2', 'varParType': 'float|float'} - cfgProb['sampling_override'] = {'defaultConfig': 'True'} - cfgProb['com1DFA_com1DFA_override'] = {'defaultConfig': 'True', 'relThFromShp': False, 'relTh': 2., - 'musamosat': 0.155, 'frictModel': 'samosAT'} + cfgProb["PROBRUN"] = { + "varParList": "musamosat|relTh", + "variationType": "percent|range", + "variationValue": "60|0.5", + "numberOfSteps": "2|3", + "samplingStrategy": "2", + "varParType": "float|float", + } + cfgProb["sampling_override"] = {"defaultConfig": "True"} + cfgProb["com1DFA_com1DFA_override"] = { + "defaultConfig": "True", + "relThFromShp": False, + "relTh": 2.0, + "musamosat": 0.155, + "frictModel": "samosAT", + } # check variation settings - variationsDict = pA.makeDictFromVars(cfgProb['PROBRUN']) + variationsDict = pA.makeDictFromVars(cfgProb["PROBRUN"]) # call function to be tested cfgFiles = pA.cfgFilesLocalApproach(variationsDict, cfgProb, com1DFA, outDir) -# print('cfgFiles', cfgFiles) + # print('cfgFiles', cfgFiles) # load cfg from file cfgMu = configparser.ConfigParser() @@ -873,68 +1055,88 @@ def test_cfgFilesLocalApproach(tmp_path): cfgMu.read(cfgFiles[0]) cfgRelTh.read(cfgFiles[1]) -# print(cfgMu['GENERAL']['musamosat'], cfgMu['GENERAL']['relTh'], cfgRelTh['GENERAL']['musamosat'], + # print(cfgMu['GENERAL']['musamosat'], cfgMu['GENERAL']['relTh'], cfgRelTh['GENERAL']['musamosat'], # cfgRelTh['GENERAL']['relTh']) - assert cfgMu['GENERAL']['musamosat'] == '0.155$60$2' - assert np.isclose(cfgMu['GENERAL'].getfloat('relTh'), 2.) - assert cfgRelTh['GENERAL']['musamosat'] == '0.155' - assert np.isclose(cfgRelTh['GENERAL'].getfloat('relTh'), 2.) - assert cfgRelTh['GENERAL']['relThFromShp'] == 'False' - assert cfgRelTh['GENERAL']['relThRangeVariation'] == '0.5$3' - assert cfgRelTh['GENERAL']['relThFromShp'] == 'False' - assert cfgMu['GENERAL']['relThFromShp'] == 'False' + assert cfgMu["GENERAL"]["musamosat"] == "0.155$60$2" + assert np.isclose(cfgMu["GENERAL"].getfloat("relTh"), 2.0) + assert cfgRelTh["GENERAL"]["musamosat"] == "0.155" + assert np.isclose(cfgRelTh["GENERAL"].getfloat("relTh"), 2.0) + assert cfgRelTh["GENERAL"]["relThFromShp"] == "False" + assert cfgRelTh["GENERAL"]["relThRangeVariation"] == "0.5$3" + assert cfgRelTh["GENERAL"]["relThFromShp"] == "False" + assert cfgMu["GENERAL"]["relThFromShp"] == "False" def test_checkParameterSettings(): - """ test if parameter settings are valid """ + """test if parameter settings are valid""" cfg = configparser.ConfigParser() cfg.optionxform = str - cfg['GENERAL'] = {'relTh': '', 'musamosat': '0.155', 'relThFromShp': 'True', 'relThPercentVariation': '', - 'relThRangeVariation': '', 'relThRangeFromCiVariation': '', - 'relThDistVariation': ''} - varParList = ['relTh', 'musamosat'] + cfg["GENERAL"] = { + "relTh": "", + "musamosat": "0.155", + "relThFromShp": "True", + "relThPercentVariation": "", + "relThRangeVariation": "", + "relThRangeFromCiVariation": "", + "relThDistVariation": "", + } + varParList = ["relTh", "musamosat"] check, thReadFromShp = pA.checkParameterSettings(cfg, varParList) assert check assert len(thReadFromShp) == 1 - assert thReadFromShp[0] == 'relTh' + assert thReadFromShp[0] == "relTh" - cfg['GENERAL']['musamosat'] = '0.1:0.2:10' - message = ('Only one reference value is allowed for %s: but %s is given' % - ('musamosat', '0.1:0.2:10')) + cfg["GENERAL"]["musamosat"] = "0.1:0.2:10" + message = "Only one reference value is allowed for %s: but %s is given" % ( + "musamosat", + "0.1:0.2:10", + ) with pytest.raises(AssertionError) as e: assert pA.checkParameterSettings(cfg, varParList) assert message in str(e.value) - cfg['GENERAL'] = {'relTh': '', 'musamosat': '0.155', 'relThFromShp': 'True', 'relThPercentVariation': '', - 'relThRangeVariation': '5$4', 'relThRangeFromCiVariation': '', - 'relThDistVariation': ''} + cfg["GENERAL"] = { + "relTh": "", + "musamosat": "0.155", + "relThFromShp": "True", + "relThPercentVariation": "", + "relThRangeVariation": "5$4", + "relThRangeFromCiVariation": "", + "relThDistVariation": "", + } with pytest.raises(AssertionError) as e: assert pA.checkParameterSettings(cfg, varParList) - assert 'Only one reference value is allowed for relTh' in str(e.value) - assert 'relThRangeVariation' in str(e.value) + assert "Only one reference value is allowed for relTh" in str(e.value) + assert "relThRangeVariation" in str(e.value) def test_checkForNumberOfReferenceValues(): - """ check if reference (base) value already has a variation set for thickness parameters""" + """check if reference (base) value already has a variation set for thickness parameters""" cfg = configparser.ConfigParser() cfg.optionxform = str - cfg['GENERAL'] = {'relTh': '', 'musamosat': '0.155', 'relThFromShp': 'True', 'relThPercentVariation': '', - 'relThRangeVariation': '', 'relThRangeFromCiVariation': '', - 'relThDistVariation': ''} + cfg["GENERAL"] = { + "relTh": "", + "musamosat": "0.155", + "relThFromShp": "True", + "relThPercentVariation": "", + "relThRangeVariation": "", + "relThRangeFromCiVariation": "", + "relThDistVariation": "", + } # call function to be tested - checkIs = pA.checkForNumberOfReferenceValues(cfg['GENERAL'], 'relTh') + checkIs = pA.checkForNumberOfReferenceValues(cfg["GENERAL"], "relTh") assert checkIs is True - cfg['GENERAL']['relThPercentVariation'] = '50$3' + cfg["GENERAL"]["relThPercentVariation"] = "50$3" with pytest.raises(AssertionError) as e: - assert pA.checkForNumberOfReferenceValues(cfg['GENERAL'], 'relTh') - assert 'Only one reference value is allowed for relTh' in str(e.value) + assert pA.checkForNumberOfReferenceValues(cfg["GENERAL"], "relTh") + assert "Only one reference value is allowed for relTh" in str(e.value) From c85b68cdb0753d883ce3b866b8c1ab3e30975a05 Mon Sep 17 00:00:00 2001 From: Felix Oesterle <6945681+fso42@users.noreply.github.com> Date: Wed, 25 Jun 2025 11:56:59 +0200 Subject: [PATCH 08/12] refactor(probAna): remove unused variables and redundant lines --- avaframe/ana4Stats/probAna.py | 1 - avaframe/tests/test_probAna.py | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/avaframe/ana4Stats/probAna.py b/avaframe/ana4Stats/probAna.py index 0ccbb20ad..2f7f533d7 100644 --- a/avaframe/ana4Stats/probAna.py +++ b/avaframe/ana4Stats/probAna.py @@ -582,7 +582,6 @@ def makeDictFromVars(cfg): """ varParList = cfg["varParList"].split("|") - varParTypes = cfg["varParType"].split("|") varValues = cfg["variationValue"].split("|") varSteps = cfg["numberOfSteps"].split("|") varTypes = cfg["variationType"].split("|") diff --git a/avaframe/tests/test_probAna.py b/avaframe/tests/test_probAna.py index 558637ae1..38628f9e4 100644 --- a/avaframe/tests/test_probAna.py +++ b/avaframe/tests/test_probAna.py @@ -159,7 +159,7 @@ def test_createComModConfig(tmp_path): assert cfgRelTh["GENERAL"]["relThPercentVariation"] == "50$3" assert cfgRelTh["GENERAL"]["relThFromShp"] == "True" assert cfgMu["GENERAL"]["relThFromShp"] == "True" - assert cfgRelTh.has_option("GENERAL", "addStandardConfig") == False + assert cfgRelTh.has_option("GENERAL", "addStandardConfig") is False cfgProb["com1DFA_com1DFA_override"] = { "defaultConfig": "True", @@ -553,7 +553,6 @@ def test_makeDictFromVars(): "variationValue": "60|ci95|0.15", "numberOfSteps": "2|3|4", "samplingStrategy": "2", - "varParType": "float|float", "varParType": "float|float|float", "nSample": "30", "sampleSeed": "12345", @@ -595,7 +594,6 @@ def test_createSampleWithVariationStandardParameters(): "numberOfSteps": "2|3|4", "samplingStrategy": "1", "varParType": "float|float|float", - "varParType": "float|float|float", "nSample": "30", "sampleSeed": "12345", "sampleMethod": "latin", @@ -896,7 +894,6 @@ def test_cfgFilesGlobalApproach(tmp_path): inputDir = dirPath / "data" / avaName avaDir = pathlib.Path(tmp_path, avaName) shutil.copytree(inputDir, avaDir) - cfgFile = dirPath / "data" / "testCom1DFA" / "probA_com1DFACfg.ini" outDir = avaDir / "Outputs" cfgProb = configparser.ConfigParser() @@ -977,7 +974,6 @@ def test_cfgFilesLocalApproach(tmp_path): inputDir = dirPath / "data" / avaName avaDir = pathlib.Path(tmp_path, avaName) shutil.copytree(inputDir, avaDir) - cfgFile = dirPath / "data" / "testCom1DFA" / "probA_com1DFACfg.ini" outDir = avaDir cfgProb = configparser.ConfigParser() From 3ad75257df6733d44bff38cb42fb740a06cd0e5c Mon Sep 17 00:00:00 2001 From: Felix Oesterle Date: Thu, 26 Jun 2025 11:57:45 +0200 Subject: [PATCH 09/12] feat(com9MoTVoellmy): implement MoT-Voellmy module with preprocessing and postprocessing - Added `com9MoTVoellmy` module for running MoT-Voellmy simulations - Implemented preprocessing to generate RCF configuration files and prepare input data - Integrated parallel processing for simulation execution --- .gitignore | 4 +- avaframe/com8MoTPSA/com8MoTPSA.py | 112 +----- avaframe/com9MoTVoellmy/__init__.py | 0 avaframe/com9MoTVoellmy/com9MoTVoellmy.py | 319 ++++++++++++++++++ avaframe/com9MoTVoellmy/com9MoTVoellmyCfg.ini | 192 +++++++++++ avaframe/com9MoTVoellmy/runCom9MoTVoellmy.py | 83 +++++ avaframe/in3Utils/MoTUtils.py | 191 +++++++++++ 7 files changed, 795 insertions(+), 106 deletions(-) create mode 100644 avaframe/com9MoTVoellmy/__init__.py create mode 100644 avaframe/com9MoTVoellmy/com9MoTVoellmy.py create mode 100644 avaframe/com9MoTVoellmy/com9MoTVoellmyCfg.ini create mode 100644 avaframe/com9MoTVoellmy/runCom9MoTVoellmy.py create mode 100644 avaframe/in3Utils/MoTUtils.py diff --git a/.gitignore b/.gitignore index 10f46188c..32fc1465d 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,6 @@ avaframe/com1DFA/*.so # Byte-compiled / optimized / DLL files # see https://github.com/github/gitignore/blob/main/Python.gitignore -*.py[cod] \ No newline at end of file +*.py[cod] + +*.rcf \ No newline at end of file diff --git a/avaframe/com8MoTPSA/com8MoTPSA.py b/avaframe/com8MoTPSA/com8MoTPSA.py index 4990a43cc..b44c5f0c9 100644 --- a/avaframe/com8MoTPSA/com8MoTPSA.py +++ b/avaframe/com8MoTPSA/com8MoTPSA.py @@ -1,5 +1,5 @@ import os -import subprocess +import sys import platform import logging import numpy as np @@ -16,9 +16,7 @@ else: from multiprocessing import Pool -import avaframe.com1DFA.com1DFATools as com1DFATools import avaframe.com1DFA.com1DFA as com1DFA -import avaframe.com8MoTPSA.com8MoTPSA as com8MoTPSA from avaframe.in3Utils import cfgUtils from avaframe.in2Trans import rasterUtils as rU from avaframe.com1DFA import particleInitialisation as pI @@ -26,78 +24,16 @@ import avaframe.in3Utils.geoTrans as geoTrans import avaframe.in3Utils.fileHandlerUtils as fU from avaframe.out1Peak import outPlotAllPeak as oP - +from avaframe.in3Utils.MoTUtils import rewriteDEMtoZeroValues, runAndCheckMoT, MoTGenerateConfigs # create local logger log = logging.getLogger(__name__) -# TODO move to whereever? -def _runAndCheck(command): - if os.name == "nt": - useShell = True - elif platform.system() == "Darwin": - useShell = False - else: - useShell = False - - # This starts the subprocess - process = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - shell=useShell, - encoding="utf-8", - errors="replace", - universal_newlines=True, - ) - - printCounter = 0 - counter = 1 - - while True: - realtimeOutput = process.stdout.readline() - - if realtimeOutput == "" and process.poll() is not None: - break - - if realtimeOutput: - line = realtimeOutput.strip() - - # do not pollute output window with time step prints - # TODO: hacky for now - if "Step" in line: - counter = counter + 1 - printCounter = printCounter + 1 - if printCounter > 100: - # print('\r' + line, flush=True, end='') - msg = "Process is running. Reported time steps: " + str(counter) - log.info(msg) - printCounter = 0 - - elif "find_dt" in line: - continue - elif "h1" in line: - continue - elif "h2" in line: - continue - elif "write_data" in line: - continue - else: - log.info(line) - - -def rewriteDEMtoZeroValues(demFile): - demData = rU.readRaster(demFile) - demData["rasterData"][np.isnan(demData["rasterData"])] = 0.0 - demData["header"]["nodata_value"] = 0.0 - newFileName = demFile.parent / demFile.stem - rU.writeResultToRaster(demData["header"], demData["rasterData"], newFileName, flip=True) - - def com8MoTPSAMain(cfgMain, cfgInfo=None): # Get all necessary information from the configuration files - simDict, inputSimFiles = com8MoTPSAGenerateConfigs(cfgMain, cfgInfo) + currentModule = sys.modules[__name__] + simDict, inputSimFiles = MoTGenerateConfigs(cfgMain, cfgInfo, currentModule) # convert DEM from nan to 0 values # TODO: suggest MoT-PSA to handle nan values @@ -196,7 +132,7 @@ def com8MoTPSATask(rcfFile): command = ["./MoT-PSA", rcfFile] # command = ['/home/felix/Versioning/AvaFrame/avaframe/com8MoTPSA/MoT-PSA', rcfFile] log.info("Run simulation: %s" % rcfFile) - _runAndCheck(command) + runAndCheckMoT(command) return command @@ -293,44 +229,10 @@ def com8MoTPSAPreprocess(simDict, inputSimFiles, cfgMain): cfg["File names"]["Output filename root"] = str(workOutputDir) rcfFileName = cfgFileDir / (str(key) + ".rcf") - cfgUtils.writeCfgFile(avalancheDir, com8MoTPSA, cfg, str(key)) + currentModule = sys.modules[__name__] + cfgUtils.writeCfgFile(avalancheDir, currentModule, cfg, str(key)) cfgToRcf(cfg, rcfFileName) rcfFiles.append(rcfFileName) return rcfFiles -def com8MoTPSAGenerateConfigs(cfgMain, cfgInfo): - """ - Creates configuration objects for com8MoTPSA. - - Parameters - ------------ - cfgMain: configparser object - main configuration of AvaFrame - cfgInfo: str or pathlib Path or configparser object - path to configuration file if overwrite is desired - optional - if not local (if available) or default configuration will be loaded - if cfgInfo is a configparser object take this as initial config - - Returns - -------- - simDict: dict - dictionary with one key per simulation to perform including its config object - inputSimFiles: dict - dictionary with input files info - """ - - # fetch type of cfgInfo - typeCfgInfo = com1DFATools.checkCfgInfoType(cfgInfo) - - if typeCfgInfo == "cfgFromDir": - # preprocessing to create configuration objects for all simulations to run by reading multiple cfg files - simDict, inputSimFiles, simDFExisting, outDir = com1DFATools.createSimDictFromCfgs( - cfgMain, cfgInfo, module=com8MoTPSA - ) - else: - # preprocessing to create configuration objects for all simulations to run - simDict, outDir, inputSimFiles, simDFExisting = com1DFA.com1DFAPreprocess( - cfgMain, typeCfgInfo, cfgInfo, module=com8MoTPSA - ) - return simDict, inputSimFiles diff --git a/avaframe/com9MoTVoellmy/__init__.py b/avaframe/com9MoTVoellmy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/avaframe/com9MoTVoellmy/com9MoTVoellmy.py b/avaframe/com9MoTVoellmy/com9MoTVoellmy.py new file mode 100644 index 000000000..323ad1226 --- /dev/null +++ b/avaframe/com9MoTVoellmy/com9MoTVoellmy.py @@ -0,0 +1,319 @@ +import os +import platform +import logging +import numpy as np +import pathlib +import time +import shutil +import sys + + +if os.name == "nt": + from multiprocessing.pool import ThreadPool as Pool +elif platform.system() == "Darwin": + from multiprocessing.pool import ThreadPool as Pool +else: + from multiprocessing import Pool + +import avaframe.com1DFA.com1DFA as com1DFA +from avaframe.in3Utils import cfgUtils +from avaframe.in2Trans import rasterUtils as rU +from avaframe.com1DFA import particleInitialisation as pI +from avaframe.in1Data import getInput as gI +import avaframe.in3Utils.geoTrans as geoTrans +import avaframe.in3Utils.fileHandlerUtils as fU +from avaframe.out1Peak import outPlotAllPeak as oP +from avaframe.in3Utils.cfgUtils import cfgToRcf +from avaframe.in3Utils.MoTUtils import rewriteDEMtoZeroValues, runAndCheckMoT, MoTGenerateConfigs, copyMoTFiles + + +# create a local logger +log = logging.getLogger(__name__) + + +def com9MoTVoellmyMain(cfgMain, cfgInfo=None): + """Run MoT-Voellmy simulations using specified configurations. + + This function executes MoT-Voellmy simulations by handling + preprocessing, parallel execution, and postprocessing steps. + + Parameters + ---------- + cfgMain : configparser.ConfigParser + Main configuration settings for the simulation + cfgInfo : str or pathlib.Path or configparser.ConfigParser, optional + Additional configuration information, by default None + Can be: + - Path to configuration file for overrides + - ConfigParser object with initial configuration + - None to use local/default configuration + + Returns + ------- + None + Results are written to the output directory structure + + Notes + ----- + The function performs several key steps: + - Generates configuration settings for all simulations + - Preprocesses input data and creates RCF configuration files + - Executes simulations in parallel using multiple processes + - Handles DEM preparation by converting NaN values to zeros + """ + + # Get all necessary information from the configuration files + currentModule = sys.modules[__name__] # As if you would to import com9MoTVoellmy + simDict, inputSimFiles = MoTGenerateConfigs(cfgMain, cfgInfo, currentModule) + + # convert DEM from nan to 0 values + # TODO: suggest MoT-PSA to handle nan values + rewriteDEMtoZeroValues(inputSimFiles["demFile"]) + + log.info("The following simulations will be performed") + for key in simDict: + log.info("Simulation: %s" % key) + + # Preprocess the simulations, mainly creating the rcf files + rcfFiles = com9MoTVoellmyPreprocess(simDict, inputSimFiles, cfgMain) + + # And now we run the simulations + startTime = time.time() + + log.info("--- STARTING (potential) PARALLEL PART ----") + + # Get number of CPU Cores wanted + nCPU = cfgUtils.getNumberOfProcesses(cfgMain, len(rcfFiles)) + + # Create parallel pool and run + # with multiprocessing.Pool(processes=nCPU) as pool: + with Pool(processes=nCPU) as pool: + results = pool.map(com9MoTVoellmyTask, rcfFiles) + pool.close() + pool.join() + + timeNeeded = "%.2f" % (time.time() - startTime) + log.info("Overall (parallel) com9MoTVoellmy computation took: %s s " % timeNeeded) + log.info("--- ENDING (potential) PARALLEL PART ----") + + # Postprocess the simulations + com9MoTVoellmyPostprocess(simDict, cfgMain, inputSimFiles) + + +def com9MoTVoellmyPostprocess(simDict, cfgMain, inputSimFiles): + """Post-process MoT-Voellmy simulation results. + + This function handles post-processing tasks after MoT-Voellmy simulations complete, + including copying result files to output directories and generating visualization plots. + + Parameters + ---------- + simDict : dict + Dictionary containing simulation configurations, with one entry per simulation + cfgMain : configparser.ConfigParser + Main configuration settings for the simulation + inputSimFiles : dict + Dictionary containing paths to input files (DEM, release areas, etc.) + + Returns + ------- + None + Results are written to the output directory structure + + Notes + ----- + The function performs several key tasks: + - Creates output directory structure + - Copies simulation result files (DataTime.txt, ppr, pfd, pfv files) + - Renames files according to conventions + - Generates visualization plots of peak fields + """ + avalancheDir = cfgMain["MAIN"]["avalancheDir"] + + # Copy max files to output directory + outputDirPeakFile = pathlib.Path(avalancheDir) / "Outputs" / "com9MoTVoellmy" / "peakFiles" + fU.makeADir(outputDirPeakFile) + + for key in simDict: + workDir = pathlib.Path(avalancheDir) / "Work" / "com9MoTVoellmy" / str(key) + + # Copy ppr files + copyMoTFiles(workDir, outputDirPeakFile, "p_max", "ppr") + + # Copy pfd files + copyMoTFiles(workDir, outputDirPeakFile, "h_max", "pfd") + + # Copy pfv files + copyMoTFiles(workDir, outputDirPeakFile, "s_max", "pfv") + + # create plots and report + modName = __name__.split(".")[-1] + reportDir = pathlib.Path(avalancheDir, "Outputs", modName, "reports") + fU.makeADir(reportDir) + + dem = rU.readRaster(inputSimFiles["demFile"]) + + # Generate plots for all peakFiles + oP.plotAllPeakFields(avalancheDir, cfgMain["FLAGS"], modName, demData=dem) + + +def com9MoTVoellmyTask(rcfFile): + """Execute a single MoT-PSA simulation using the provided configuration file. + + Parameters + ---------- + rcfFile : pathlib.Path + Path to the RCF configuration file for the simulation + + Returns + ------- + list + The command that was executed as a list containing the executable path + and configuration file path + + Notes + ----- + Changes to the directory containing this module before executing the simulation. + Uses runAndCheckMoT to execute and monitor the simulation process. + """ + os.chdir(os.path.dirname(os.path.abspath(__file__))) + + if os.name == "nt": + exeName = "./MoT-Voellmy_win.exe" + elif platform.system() == "Darwin": + message = "MoT-Voellmy does not support MacOS at the moment" + log.error(message) + raise OSError(message) + else: + exeName = "./MoT-Voellmy_linux.exe" + + command = [exeName, rcfFile] + log.info("Run simulation: %s" % rcfFile) + runAndCheckMoT(command) + return command + + +def com9MoTVoellmyPreprocess(simDict, inputSimFiles, cfgMain): + """Preprocess data for MoT-PSA simulations. + + This function prepares the input data and configuration files needed to run + MoT-PSA simulations. It processes release areas, creates required directories, + and generates configuration files for each simulation. + + Parameters + ---------- + simDict : dict + Dictionary containing simulation configurations with one entry per simulation + inputSimFiles : dict + Dictionary containing paths to input files (DEM, release areas, etc.) + cfgMain : configparser.ConfigParser + Main configuration settings for the simulation + + Returns + ------- + list + List of pathlib.Path objects pointing to generated RCF configuration files + that will be used to run the simulations + + Notes + ----- + The function performs several key steps: + - Creates working directories for each simulation + - Processes release areas and converts them to raster format + - Generates configuration files in RCF format + - Handles both single and multiple release scenarios + """ + # Load avalanche directory from general configuration file + avalancheDir = cfgMain["MAIN"]["avalancheDir"] + + workDir = pathlib.Path(avalancheDir) / "Work" / "com9MoTVoellmy" + cfgFileDir = pathlib.Path(avalancheDir) / "Outputs" / "com9MoTVoellmy" / "configurationFiles" + fU.makeADir(cfgFileDir) + rcfFiles = list() + + for key in simDict: + # Generate command and run via subprocess.run + # Configuration that needs adjustment + + # load configuration object for current sim + cfg = simDict[key]["cfgSim"] + + # convert release shape to raster with values for current sim + # select release area input data according to a chosen release scenario + inputSimFiles = gI.selectReleaseFile(inputSimFiles, cfg["INPUT"]["releaseScenario"]) + # create the required input from input files + demOri, inputSimLines = com1DFA.prepareInputData(inputSimFiles, cfg) + + if cfg["GENERAL"].getboolean("iniStep"): + # append buffered release Area + inputSimLines = pI.createReleaseBuffer(cfg, inputSimLines) + + # set thickness values for the release area, entrainment and secondary release areas + relName, inputSimLines, badName = com1DFA.prepareReleaseEntrainment( + cfg, inputSimFiles["releaseScenario"], inputSimLines + ) + + releaseLine = inputSimLines["releaseLine"] + # check if release features overlap between features + # TODO: split releaseheight -> question NGI + dem = rU.readRaster(inputSimFiles["demFile"]) + dem["originalHeader"] = dem["header"].copy() + # releaseLine = geoTrans.prepareArea(releaseLine, dem, np.sqrt(2), combine=True, checkOverlap=False) + if len(inputSimLines["relThField"]) == 0: + # if no release thickness field or function - set release according to shapefile or ini file + # this is a list of release rasters that we want to combine + releaseLine = geoTrans.prepareArea( + releaseLine, + dem, + np.sqrt(2), + thList=releaseLine["thickness"], + combine=True, + checkOverlap=False, + ) + releaseField = releaseLine["rasterData"] + else: + # if relTh provided - set release thickness with field or function + releaseLine = geoTrans.prepareArea( + releaseLine, dem, np.sqrt(2), combine=True, checkOverlap=False + ) + relRasterPoly = releaseLine["rasterData"].copy() + releaseRelThCombined = np.where(relRasterPoly > 0, inputSimLines["relThField"], 0) + releaseField = releaseRelThCombined + + # Generate the work and data dirs for the current simHash + cuWorkDir = workDir / key + workInputDir = cuWorkDir / "Input" + workOutputDir = cuWorkDir / key + + fU.makeADir(cuWorkDir) + fU.makeADir(workInputDir) + + zeroRaster = np.full_like(releaseLine["rasterData"], 0) + + release = workInputDir / "release" + bedDepth = workInputDir / "dummyBedDepth" + bedShear = workInputDir / "dummyBedShear" + rU.writeResultToRaster(dem["header"], releaseField, release, flip=True) + rU.writeResultToRaster(dem["header"], zeroRaster, bedDepth) + rU.writeResultToRaster(dem["header"], zeroRaster, bedShear) + + # set configuration for MoT-Voellmy + cfg["Run information"]["Area of Interest"] = cfgMain["MAIN"]["avalancheDir"] + cfg["Run information"]["UTM zone"] = "32N" + cfg["Run information"]["EPSG geodetic datum code"] = "31287" + cfg["Run information"]["Run name"] = cfgMain["MAIN"]["avalancheDir"] + cfg["File names"]["Grid filename"] = str(inputSimFiles["demFile"]) + cfg["File names"]["Release depth filename"] = str(release) + ".asc" + cfg["File names"]["Bed depth filename"] = str(bedDepth) + ".asc" + cfg["File names"]["Bed shear strength filename"] = str(bedShear) + ".asc" + cfg["File names"]["Output filename root"] = str(workOutputDir) + + rcfFileName = cfgFileDir / (str(key) + ".rcf") + + currentModule = sys.modules[__name__] + cfgUtils.writeCfgFile(avalancheDir, currentModule, cfg, str(key)) + cfgToRcf(cfg, rcfFileName) + rcfFiles.append(rcfFileName) + return rcfFiles + + diff --git a/avaframe/com9MoTVoellmy/com9MoTVoellmyCfg.ini b/avaframe/com9MoTVoellmy/com9MoTVoellmyCfg.ini new file mode 100644 index 000000000..f65f1db7f --- /dev/null +++ b/avaframe/com9MoTVoellmy/com9MoTVoellmyCfg.ini @@ -0,0 +1,192 @@ +### Config File - This file contains the main settings for the simulation run +## Copy to local_com8MoTPSACfg.ini and set you parameters + +[GENERAL] +# model type - only for file naming (psa - powder snow avalanche) +modelType = dfa + +# list of simulations that shall be performed (null, ent, res, entres, available (use all available input data)) +simTypeList = null + + +#+++++Release thickness++++ +# True if release thickness should be read from shapefile file; if False - relTh read from ini file +relThFromShp = True +# if a variation on relTh shall be performed add here +- percent and number of steps separated by $ +# for example relThPercentVariation=50$10 [%] +relThPercentVariation = +# if a variation on relTh shall be performed add here +- absolute value and number of steps separated by $ +# for example relThRangeVariation=0.5$10 [m] +relThRangeVariation = +# if a variation on relTh shall be performed add here +- ci% value and number of steps separated by $ +# for example relThRangeFromCiVariation= ci95$10 +relThRangeFromCiVariation = +# if variation on relTh shall be performed using a normal distribution in number of steps, +# value of buildType (ci95 value), min and max of dist in percent, buildType (ci95 only allowed), +# support (e.g. 10000) all separated by $: e.g. normaldistribution$numberOfSteps$0.3$95$ci95$10000 +# if relThFromShp=True ci95 is read from shp file too +relThDistVariation = +# release thickness (only considered if relThFromShp=False) [m] +relTh = +# read release thickness directly from file (relThFromShp needs to be False) +relThFromFile = False + + +#+++++Entrainment thickness++++ +# True if entrainment thickness should be read from shapefile file; if False - entTh read from ini file +entThFromShp = True +# if a thickness value is missing for the entrainment feature in the provided shp file this value is used for all features [m] +entThIfMissingInShp = 0.3 +# if a variation on entTh shall be performed add here +- percent and number of steps separated by $ +# for example entThPercentVariation=50$10 [%] +entThPercentVariation = +# if a variation on entTh shall be performed add here +- absolute value and number of steps separated by $ +# for example entThRangeVariation=0.5$10 [m] +entThRangeVariation = +# if a variation on entTh shall be performed add here +- ci% value and number of steps separated by $ +# for example entThRangeFromCiVariation= ci95$10 +entThRangeFromCiVariation = +# if variation on entTh shall be performed using a normal distribution in number of steps, +# value of buildType (ci95 value), min and max of dist in percent, buildType (ci95 only allowed), +# support (e.g. 10000) all separated by $: e.g. normaldistribution$numberOfSteps$0.3$95$ci95$10000 +# if entFromShp=True ci95 is read from shp file too +entThDistVariation = +# entrainment thickness (only considered if entThFromShp=False) [m] +entTh = + +#+++++Secondary release thickness+++++ +# if secRelArea is True - add secondary release area +secRelArea = True +# True if release thickness should be read from shapefile file; if False - secondaryRelTh read from ini file +secondaryRelThFromShp = True +# if a variation on secondaryRelTh shall be performed add here +- percent and number of steps separated by $ +# for example secondaryRelThPercentVariation=50$10 [%] +secondaryRelThPercentVariation = +# if a variation on secondaryRelTh shall be performed add here +- absolute value and number of steps separated by $ +# for example secondaryRelThRangeVariation=0.5$10 [m] +secondaryRelThRangeVariation = +# if a variation on secondaryRelTh shall be performed add here +- ci% value and number of steps separated by $ +# for example secondaryRelThRangeFromCiVariation= ci95$10 +secondaryRelThRangeFromCiVariation = +# if variation on secondaryRelTh shall be performed using a normal distribution in number of steps, +# value of buildType (ci95 value), min and max of dist in percent, buildType (ci95 only allowed), +# support (e.g. 10000) all separated by $: e.g. normaldistribution$numberOfSteps$0.3$95$ci95$10000 +# if secondaryRelThFromShp=True ci95 is read from shp file too +secondaryRelThDistVariation = +# secondary area release thickness (only considered if secondaryRelThFromShp=False) [m] +secondaryRelTh = + +#+++++++++++++Volume classes [m³] +volClassSmall = 25000. +volClassMedium = 60000. + +#+++++++++++++Mesh and interpolation +# interpolation option +# 3 Options available : -0: nearest neighbour interpolation +# -1: equal weights interpolation +# -2: bilinear interpolation +interpOption = 2 +# minimum flow thickness [m] +hmin = 0.05 +# remesh the input rasters or look for remeshed rasters +# expected mesh size [m] +meshCellSize = 5 +# threshold under which no remeshing is done +meshCellSizeThreshold = 0.001 +# clean DEMremeshed directory to ensure remeshing if chosen meshCellsize is different from rasters in Inputs/ +cleanRemeshedRasters = True +# resize files read from Inputs/RASTERS to be resized to extent of DEM as resizeThreshold x meshCellSize +resizeThreshold = 3 + +# Normal computation on rectangular grid +# 4 triangles method 6 triangles method 8 triangles method +# +----U----UR---+---+--... +----+----+----+---+--... +----+----+----+---+--... +# | /|\ | /| | /| 2 /| /| |\ 2 | 3 /| /| Y +# | / | \ | / | | / | / | / | | \ | / | / | ^ +# | / | \ | / | / | / | / | / | / | \ | / | / | / | +# |/ 1 | 2 \|/ |/ |/ 1 |/ 3 |/ |/ | 1 \|/ 4 |/ |/ | +# +----P----L----+---+--... +----*----+----+---+--... +----*----+----+----+--... +-----> X +# |\ 4 | 3 /| /| | 6 /| 4 /| /| | 8 /|\ 5 | /| +# | \ | / | / | | / | / | / | | / | \ | / | +# | \ | / | / | / | / | / | / | / | / | \ | / | / +# | \|/ |/ |/ |/ 5 |/ |/ |/ |/ 7 | 6 \|/ |/ +# +----+----+----+---+--... +----+----+----+---+--... +----+----+----+---+--... +# | /| /| /| | /| /| /| | /| /| /| +# 4 Options available : -1: simple cross product method (with the diagonals P-UR and U-L) +# -4: 4 triangles method +# -6: 6 triangles method +# -8: 8 triangles method +methodMeshNormal = 1 + +#++++++++++++++ Technical values +++++++++++++ +# when checking if a point is within a polygon, one can decide to add a buffer +# arround the polygon (0 means take strictly the points inside, a very small value +# will inclune the points located on the polygon line) +thresholdPointInPoly = 0.001 + + +[INPUT] +# specify a particular release area scenario, provide name of shapefile with or without extension .shp (optional) +releaseScenario = +# important for parameter variation through probRun +thFromIni = + +# Below are the settings for the MoT-Voellmy model +[Run information] +MoT-Voellmy input file version = 2024-09-10 +Area of Interest = Grasdalen +UTM zone = 33N +EPSG geodetic datum code = 25833 +Run name = Ryggfonn_2021-04-11_02 + +[File names] +Grid filename = - +Release depth filename = ./h0.asc +Bed depth filename = - +Bed shear strength filename = - +Forest density filename = - +Tree diameter filename = - +Start velocity u filename = - +Start velocity v filename = - +Output filename root = ./Run_02/02 +Output format = ESRI_ASCII_Grid + +[Physical_parameters] +Gravitational acceleration (m/s^2) = 9.81 +Flow density (kg/m^3) = 250.0 +Bed density (kg/m^3) = 140.0 +Deposit density (kg/m^3) = 450.0 +Rheology = Voellmy +Parameters = constant +Dry-friction coefficient (-) = 0.40 +Turbulent drag coefficient (-) = 0.001 +Effective drag height (m) = 3.0 +Centrifugal effects = yes +Passive earth-pressure coeff. (-) = 1.0 + +[FOREST_EFFECTS] +Forest effects = no +Tree drag coefficient (-) = 1.0 +Modulus of rupture (MPa) = 50.0 +Forest decay coefficient (m/s) = 0.15 + +[ENTRAINMENT] +Entrainment = none +Erosion coefficient (-) = 0.0 +Bed strength profile = global +Bed friction coefficient (-) = 0.25 +Deposition = no +Evolving geometry = no + +[Numerical parameters] +Simulation time (s) = 100.0 +Minimum time step (s) = 0.001 +Maximum time step (s) = 0.2 +Output interval (s) = 1.0 +Write velocity vectors = no +Write maximum pressure = yes +Write instant. pressure = no +Minimum flow depth (m) = 0.01 +Minimum speed (m/s) = 0.01 +Momentum threshold (kg m/s) = 100.0 +Initial CFL number (-) = 0.8 diff --git a/avaframe/com9MoTVoellmy/runCom9MoTVoellmy.py b/avaframe/com9MoTVoellmy/runCom9MoTVoellmy.py new file mode 100644 index 000000000..ade91cb76 --- /dev/null +++ b/avaframe/com9MoTVoellmy/runCom9MoTVoellmy.py @@ -0,0 +1,83 @@ +""" + Run script for running python com9MoTVoellmy kernel +""" +# Load modules +# importing general python modules +import time +import argparse + +# Local imports +import avaframe.in3Utils.initializeProject as initProj +from avaframe.com9MoTVoellmy import com9MoTVoellmy +from avaframe.in3Utils import cfgUtils +from avaframe.in3Utils import logUtils + + +def runCom9MoTVoellmy(avalancheDir=''): + """ Run com9MoTVoellmy in the default configuration with only an + avalanche directory as input + + Parameters + ---------- + avalancheDir: str + path to avalanche directory (setup eg. with init scipts) + + Returns + ------- + peakFilesDF: pandas dataframe + dataframe with info about com9MoTVoellmy peak file locations + """ + + # Time the whole routine + startTime = time.time() + + # log file name; leave empty to use default runLog.log + logName = 'runCom9MoTVoellmy' + + # Load avalanche directory from general configuration file + # More information about the configuration can be found here + # on the Configuration page in the documentation + cfgMain = cfgUtils.getGeneralConfig() + if avalancheDir != '': + cfgMain['MAIN']['avalancheDir'] = avalancheDir + else: + avalancheDir = cfgMain['MAIN']['avalancheDir'] + + # Start logging + log = logUtils.initiateLogger(avalancheDir, logName) + log.info('MAIN SCRIPT') + log.info('Current avalanche: %s', avalancheDir) + + # ---------------- + # Clean input directory(ies) of old work and output files + # If you just created the ``avalancheDir`` this one should be clean but if you + # already did some calculations you might want to clean it:: + initProj.cleanSingleAvaDir(avalancheDir, deleteOutput=False) + + # Get module config + cfgCom9MoTVoellmy = cfgUtils.getModuleConfig(com9MoTVoellmy, toPrint=False) + + # ---------------- + # Run psa + com9MoTVoellmy.com9MoTVoellmyMain(cfgMain, cfgInfo=cfgCom9MoTVoellmy) + + # # Get peakfiles to return to QGIS + # avaDir = pathlib.Path(avalancheDir) + # inputDir = avaDir / 'Outputs' / 'com8MoTPSA' / 'peakFiles' + # peakFilesDF = fU.makeSimDF(inputDir, avaDir=avaDir) + + # Print time needed + endTime = time.time() + log.info('Took %6.1f seconds to calculate.' % (endTime - startTime)) + + # return peakFilesDF + return + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Run com9MoTVoellmy workflow') + parser.add_argument('avadir', metavar='avalancheDir', type=str, nargs='?', default='', + help='the avalanche directory') + + args = parser.parse_args() + runCom9MoTVoellmy(str(args.avadir)) diff --git a/avaframe/in3Utils/MoTUtils.py b/avaframe/in3Utils/MoTUtils.py new file mode 100644 index 000000000..4e1201db9 --- /dev/null +++ b/avaframe/in3Utils/MoTUtils.py @@ -0,0 +1,191 @@ +import os +import platform +import subprocess +import logging +import pathlib +import shutil + +import numpy as np + +from avaframe.com1DFA import com1DFATools as com1DFATools, com1DFA as com1DFA +from avaframe.in2Trans import rasterUtils as rU + +log = logging.getLogger(__name__) + +def rewriteDEMtoZeroValues(demFile): + """Set all NaN values in a DEM raster to zero and update the nodata value. + + This function reads a DEM raster file, replaces all NaN values with 0.0, + updates the nodata value in the header to 0.0, and writes the modified + raster back to a new file. + + Parameters + ---------- + demFile : pathlib.Path + Path to the input DEM raster file + + Returns + ------- + None + Writes a new raster file with zero values instead of NaN values. + The output file is saved in the same directory as the input file, + using the same stem name. + + Notes + ----- + The function uses the rasterUtils module for reading and writing raster data. + The output raster is flipped during writing. + """ + demData = rU.readRaster(demFile) + demData["rasterData"][np.isnan(demData["rasterData"])] = 0.0 + demData["header"]["nodata_value"] = 0.0 + newFileName = demFile.parent / demFile.stem + rU.writeResultToRaster(demData["header"], demData["rasterData"], newFileName, flip=True) + + +def runAndCheckMoT(command): + """Execute MoT command and monitor its output. + + This function runs a MoT command as a subprocess and monitors its output, + filtering and logging specific messages while tracking time steps. + + Parameters + ---------- + command : str or list + The command to execute. Can be a string or list of arguments. + + Returns + ------- + None + Function runs the command and logs output but does not return a value. + + Notes + ----- + - Uses different shell settings based on operating system + - Filters output to reduce noise from common status messages + - Logs time step progress every 100 steps + - Handles UTF-8 encoding with replacement of invalid characters + """ + if os.name == "nt": + useShell = True + elif platform.system() == "Darwin": + useShell = False + else: + useShell = False + + # This starts the subprocess + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=useShell, + encoding="utf-8", + errors="replace", + universal_newlines=True, + ) + + printCounter = 0 + counter = 1 + + while True: + realtimeOutput = process.stdout.readline() + + if realtimeOutput == "" and process.poll() is not None: + break + + if realtimeOutput: + line = realtimeOutput.strip() + + # do not pollute output window with time step prints + # TODO: hacky for now + if "Step" in line: + counter = counter + 1 + printCounter = printCounter + 1 + if printCounter > 100: + # print('\r' + line, flush=True, end='') + msg = "Process is running. Reported time steps: " + str(counter) + log.info(msg) + printCounter = 0 + + elif "find_dt" in line: + continue + elif "h1" in line: + continue + elif "h2" in line: + continue + elif "write_data" in line: + continue + elif "update_boundaries" in line: + continue + else: + log.info(line) + + +def MoTGenerateConfigs(cfgMain, cfgInfo, currentModule): + """ + Creates configuration objects for com8MoTPSA. + + Parameters + ------------ + cfgMain: configparser object + main configuration of AvaFrame + cfgInfo: str or pathlib Path or configparser object + path to configuration file if overwrite is desired - optional + if not local (if available) or default configuration will be loaded + if cfgInfo is a configparser object take this as initial config + currentModule: module object + is being passed to cfgUtils.writeCfgFile to create the correct the cfg + + + Returns + -------- + simDict: dict + dictionary with one key per simulation to perform including its config object + inputSimFiles: dict + dictionary with input files info + """ + + # fetch type of cfgInfo + typeCfgInfo = com1DFATools.checkCfgInfoType(cfgInfo) + + if typeCfgInfo == "cfgFromDir": + # preprocessing to create configuration objects for all simulations to run by reading multiple cfg files + simDict, inputSimFiles, simDFExisting, outDir = com1DFATools.createSimDictFromCfgs( + cfgMain, cfgInfo, module=currentModule + ) + else: + # preprocessing to create configuration objects for all simulations to run + simDict, outDir, inputSimFiles, simDFExisting = com1DFA.com1DFAPreprocess( + cfgMain, typeCfgInfo, cfgInfo, module=currentModule + ) + return simDict, inputSimFiles + +def copyMoTFiles(workDir, outputDir, searchString, replaceString): + """ + Copy and rename MoT result files from work directory to output directory. + + Parameters + ---------- + workDir : pathlib.Path + Source directory containing the original p_max files + outputDir : pathlib.Path + Destination directory where renamed ppr files will be copied to + searchString : str + String pattern to search for in the original filenames + replaceString : str + String to replace the searchString with in the new filenames + + Returns + ------- + None + Files are copied to the destination directory with renamed extensions + """ + varFiles = list(workDir.glob("*"+searchString+"*")) + targetFiles = [ + pathlib.Path(str(f.name).replace(searchString, replaceString)) + for f in varFiles + ] + targetFiles = [outputDir / f for f in targetFiles] + + for source, target in zip(varFiles, targetFiles): + shutil.copy2(source, target) \ No newline at end of file From 9c7c9506b62279139c37a2b7cf71cd29b2181b00 Mon Sep 17 00:00:00 2001 From: Felix Oesterle Date: Thu, 26 Jun 2025 12:12:55 +0200 Subject: [PATCH 10/12] feat(com9MoTVoellmy): add platform-specific executables for MoT-Voellmy - Remove this in future for a direct compilation feat(MANIFEST): include MoT-Voellmy executables in package distribution --- MANIFEST.in | 1 + avaframe/com9MoTVoellmy/MoT-Voellmy_linux.exe | Bin 0 -> 79200 bytes avaframe/com9MoTVoellmy/MoT-Voellmy_win.exe | Bin 0 -> 818117 bytes 3 files changed, 1 insertion(+) create mode 100755 avaframe/com9MoTVoellmy/MoT-Voellmy_linux.exe create mode 100644 avaframe/com9MoTVoellmy/MoT-Voellmy_win.exe diff --git a/MANIFEST.in b/MANIFEST.in index 323c5fec4..4f33aed1c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ include avaframe/RELEASE-VERSION.txt include avaframe/version.py include avaframe/in3Utils/logging.conf include avaframe/com1DFA/*.pyx +include avaframe/com9MoTVoellmy/MoT-Voellmy*.exe recursive-include avaframe *Cfg.ini global-exclude local_*Cfg.ini prune benchmarks diff --git a/avaframe/com9MoTVoellmy/MoT-Voellmy_linux.exe b/avaframe/com9MoTVoellmy/MoT-Voellmy_linux.exe new file mode 100755 index 0000000000000000000000000000000000000000..c1ba9356bdcbf4a35e2f5640896343f15f193a29 GIT binary patch literal 79200 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%0AM0B}s zVFqxxAfVVy^uTDkWtAlAv1#=OqnfuJYBT+L67MfG>W-%5WX{?fnDZJJ-< z@+ABXP1G13HFV&HXAA)d$vSj^Ud|C{^cpi{NCF>ky{a687@N;e1a==o?HWH_;2({$ z#rOGG5(-J73;fymE3cYE;{(3$FVw|{>j=F+D=ELS1tL^`4Y^vA-d~MM*8;*XYp4*b zFIX|!glQu?WiMK{lEvt935JOXJI9(c;hANZ!ZQ~FOaGBj@bOs4Py+7H#Cp@Sz#*r; z2($=rhXCGX09Okj*QdteE8w>p@Go-f>;2`o&<;?rA7na6CfCNTkSWaaVfBlh5=8Yy z)lg`9Yiok$?&XMWI+(|2@`X~gT__Yg2rbAe7ZeLSpWF^DvbYu&Yr^+N4f6ple~Z;0 zQ)rPP^aBM#U*>2q!$6@L541y#Blx)zq1^ayY&Lp9s1!h~dyT>2B`l)OO3T2{aNt)s z%#EQb(K828qvlGkx0*2cJ+vP%OxBk{8-ANp{}E{B zHrt#gxGy!ipN+*z!M)LMt7BoKR~PbZCd+K2Xl>|olAFRZZy}kEu*_S57Fh{gtSYqb z=i4@fe(IbI#@uaITDCA4i_{sFZY+wRal+^$hSA@hN=6@PX!QE8*=QZh*Sd|2J|c|H zAO%)k0;ADLZ2Ip&Ggn*Npjk8_V7V!v1-o_A?kpB-vD*;4@eGJ{hQ;2_V(ZwJ+nSJh zf+_Q2&7IMVul1&W$DVxu`T;WC)ZvA&>4Qm!yGEL({{^)0_CdP5s)%MPbZU-v+{tCt!OUk!YHBHAlRx#pntUVvM z-EuE$_&SQ3(NJ?fvH~eyx*LDR*axz%{1fd~l^$D3gT%!QpJ#=iE#S);K9S*9THz-M z_-uv`WBA$l3(oD1Gv~2X=IqOm?gH||9}KyeAxRARYdO<=A|M$I`4MHBbWlJx3P^8; z>}JUO0`jI$4;3*Mmr^j{NZEMs|6%L5Q*kM*eP3juI8)(Sd%^z zNG}ScR3?9qA#VxDT>{dZ$%{b_;iIs(%{UJToSOvBP^MnVkfj1LMnKXTGMgdO1td#A zx-s7j!53Ox$~f5q$0=}bAak4<3^`pu{`if}EhmWdBg&vv53H{}#Xc(*9c93m8R)N~NOQab9yqTi zIDG^u*uf?9FCz(#yBNAcK-Wc}0~k7=q1Ov2`NyD@S-UGl`5h=HhL5A#HRmu<_^45M z2N#|p3X6@xA}%~Z6y9SLPUXTs(OzM~YNPNnF4RTg%|_vQT=g_}qo^!yD0raA8wh3^`L2e|MyQCMsge#nJ4h{9<`VHp>W6NQtF!l$?}TNGv* zg{!%+zbIsRkbVmnb`ymw=z_xOTxb`C*BXVFbK#-e*da5F!l7LFxhPCG3Qyz0Euyfe zQJBnyFN?yXbb+8hj*{&Uh{C-_;kR7q6NN7sg*&-$wkUkuD13_x3q;}FM&WZPq?VdW zgP>)H<|ZONMt&`XzJ`f)o+MlfEBzbV(2xmJv-OyWQ5m0|zzWA@*q6X29%BcbZx|c6 zzYjye`*UY8wi%f>tzpDY1aV7!2XoT~p2XndRLug#k& zK(zQU1Dy3Mz*b`nKFY9y%7&wC<49oZFYFO>7BXbgRt(_N5s>fl#F8_8U%5|?%~p>W zNTgS&G%y;6fwGen)wz})(FnhWH`2EjGTwNa0)T^aD@~oW1hv9fSMSa8Z}+CERlXoz zwC%M;=~tx$yj$wzKH0YV6BS=5Ny+-9=vSz_l%`W)cJ<`oJwxm3; z($;sZjrSz~F7JtDL}waPf_u1U2i|y&l$VVIc`>`L-+%j@i?C(2oC3KRJjwsAkf5yExEmgnZ zGlY|zO!AWB131f#^{Wu+e*T&u;i;AuvisJi5l>M}$b^Xl2^) z`Adky2l$$SN8ReLmsexShtmBNlxJ?ovTpZMyeFTVwR>Jy51N!4|2_6Cc$RP1pop)+ z4xIF?-;4Ho)UWg_5DALS-@7gH#Iz^qCS_wj^G#0N-XnCEK z0BtgrWuQp0x@=(K`{^J%U!hr;g1%Z#@8d2tr`Pe|sUVEVSNF+p zN8M?3B6G?=KROvj~C`teqx07AcQ6#l6g$A66?k6NSj+oSmE+AY0HLjtbr zDBpP*F#Wq^{|*X<5s9=;+yZGao>bKfS`B0M_E2!3)8K;-6eu44vUhwX)+sl4So2fM{bnk#50zM9Af^P1&;l zIbbT9-Bm_YD7X*bo`SbLc8pxz>-2S)P6t0>#z^1*=c?tN_O;L^SI#XLjUbw00Z}{w zic**>`<_Se<@yfUQE{vKQVUqJF1O$Q+=@eOMg^vSokaS=`5Lx{s68p_F(`>@2)4m$ z&N{sxn1M~v{@};pn8Ra;N4tYfrF$LFZTbT=2@W`}K$*S@_hv4pD>5O@BT7Y!fDiA{ z_YF(Zia~|Uo2no~ASQ9H6rG3oyoq@468nGk)ilV;Z`j`{s-qux1wGYBx%lTA3kk>u zU9<#bTD4-?SBSaPWxtOYaS3AQBM2IPpUqY-uXurOkyXnVjFr&2T;!{3zmR_NrXyKm zc7~a|#3c4F5TaQi?%x2}Cd8Ue0KNf0e*n{gGKis}N~)3XhU2gV^71plTyVi~RAae! z|L_!Cip~X7a;yHl$+U#48{RmX0rl0)Ec7bX!*B#@vG)nM?AnFGWhu!0LXc8lQ+o8` zL}--`;lW$*r!=$NP{LGwgdxQ{gZ%vlt8GXKViO9%Sqx=tKt~T@)4Q7JmlOI*E;P|o zfG%uj{eu`l_^RrIZBZO<9~R$i2b?Vl9z>i70Lxf!5PpbaGx4P(V9FQRZS~GV-Ums! z2M-E57QF8@{9!H{JQf$NWBup3>bCmNFwX3p=RfFeuV_dS>Q^)%y*e2bMf<_3h^Tqs zh{TC8UlvF?Z;CXFx%36VP(Maj3`QNuXUMS_pYcJ&ja0$<^H5V!w~`-*sYuL@RAQ>m zO{#Zp3E)nXenQqkc@meWd__;{2y`+#02UQNOyQ%@V`hIsLjg$N3|w^wO;`;)Wc6YQ zC3YJDt9MRe5sp>F6r<%cGk1kLqrXtp_er6i7;b3xEOer-N4~Njc*;lqpaTf#wtj8r&N=62^2~10u)W!ibu4hqJN@OdIk4X z$2}wYS5fy;J?ckxKpZObCoo(`XqD0uTjvmKx?q7CdJdV7WZh6|q( z%&~Wu-^gu?>KMRNmrklD;HgTdcABGQck*D|n^nCqsf~gG{LY0PX|U$Wsw(;#gL2dV zuCY8COjXn!`rP-)rFBUQZ^f$&Z7_UxXC4&#Owe;+6KY_TMZPC6o#eaPqFljpI`#Ou z0q?f-(1{U^a}o5pSx3BQh`vKnzo4Pw(e1oCa)3H|4s~QHF+PPzYa!+5bPvAUfw8U| zo^mZ4ns*iUA&W3H-l@NW8ieq%xzjEl=25qM`t8iMZ*j?QT--G;kdx4z?nt>zL%Fem=+>k{6Vg)u?HtT!fFE z^P&lGr#@P!cv&vYLM(auZO^q=z^SgT_;Uru&rlcbvmbt*8zFr+Vy`+~#N8GT+?mt{ zGjaI0t0nFX$DlvYPW!DIKc~>p$3mYJe-F}sJL>y_ysNOvRW!w;?$zJH_y|55lRNFQ zVIKQlZ5hn~P;fESL30?}#cU3l;pQ;*eRwYiV=?Vgw1@2of9Q27^r|AgwxB)iD*76d zJXnRJ>h{?8ggTPH$^X9cu9}kQ-(J)X(E>|9$B^c>Z*nxHQ=T1D9zj99eB|F1qj63k z8SMx|hwk%ER+a_%$=BYd4B$J4F8ogESD~OVzaV-#Sjtl~o*vWYFKst3-Ktc4pNb#a z+<6*$N87TmY_{6l+V8=q3zz%YLVF??wJyi!P&0>K*tc3P*38G?BF$ zpeQOvE??q=-`-S=St?eNM#_sIADrK{pf8BIm{oN zM*vxfVg96lsy>R=wgfrUz+#Y49rWH{@MNT@(s7Zw3%c-^(u$#H<}PG6F**M5T{_0F zjk>Ik=(2RD@f>irAxn4^{yfU47m$iCuocZgD}pwm&NL&|f7}KT&Wg=J2N1}|`IIko zA9J7uhf3gpCwjEsW@Dte1-O`C37+GCMTzfax@rSH1A+%%?F-CUXH2Cgh4WuIxMK3H zeya~*))1d#>%vp0$I&2Zs)QtpJ?vDE8q~WoU?ENDb%` z)#JtjU57$MF)Th3Jc~OgDEfKmWW;%)@ArwA)2Tbr7!uH#BmmKSF-iv&9V9XWcjQN4 z!_V>?mnIM@6?qRc_z;my07HXIsEqkuWRpvl!uuZedvv1uzqZ14wAPmRUSb>MPR?83 zLqoNlem`KIz*%hw?vZms{Bv0%Zj`KJ^72WnGgT$@u7(PKDZcN83T;V+q7KBT18^7q zmy}Sq1T&c=o01@^Cf&i+H;8o59Qijzs&5;L#ClO8a76VD0X$cISGSQ`W)0rM4`1ma z;sHMnN6tsHA17wZ#itm;%yLSR+;+{ai~L20IO#n^=JJ_frS-@UL!qfty_MhcP@qAY3LNjCm{a* z7$lc`gP{@KPLxZ&pj+Rup>jzr-MKM{>Occ#PutM1~U>JM&4 zq#?SpDBOy^76Cb^=%mQ_LWC{F=^*bfO!)(`0F8uWT%nDW@c+R|d1Y3}o5U}Q7ph5_ zyI?#8&gcY>dRRZ9f*pO?jS)vbhav#(P4tG)^?3T#qO~N-tI=`Wh!7Kcp0}Zs3!NU3PvoA!6(9#oZjb5~44r`P zUCeA<>J@*H)S^$ZTEXl);gl1E744((1bSAqcM=bs(cYo+p-@GHf1xYu6WM<{+3fNy zO|;4W9=H$w2Y!fRM`RRKNJAV@{_Rxa&MLsd-rIx?6B9{cxrD}pWC8{{?+2fxhf?S_9*AEE z#4f?zxQX=EJpWI>e!JcV3q{ViifK^C!+6(pcT6elp zaFa?mqj8g-gC4@SZR*vNMRP?TosIQ=tZE?!(c+f+0@;62%e&E4{MSTp5&jPH&dHtj zt4rRHb8h{xLEfZc2&1de&J?uU!yt4A+_dPItG<^vtsF6}e~3c3`@DMeXLPm|cvqn= zU`Srp5xHa_XsPTd(1NGnq108pIL$t^C=hIeCmJU~R+@d-L~r7-K}9}(O2`GP1Qgwf z-wlyNlQ<4aFib~LKH4kxN#qR)#c0sqmtaX^6KX`>a4C2cogC^@vRy7&hmvCSJ0sL8 zFjTOjw8ay+*bV_-0qpMq$w68TG!@TYm3*RpS~Ha zF;;0qXRm0Yv^6qc`$*A{M?cZ80**&J-7t)nAg+QxGWX^BhNaoOXBzXJTrFomVlGMV z4|MV1+iF)Z=)%9;tX^pgzs$uR0CM$t>438HLeKHiS z2i7j~RCE>PO4g2|{)&CyCYq@$%l_bteDN1~u%r*q04sFQTf(y5irO1YGbqTx6pq}h zD*{$@F!T-75!)YLDnr9a5ABSa4_n%2s*CHxgWy&_^8H@#y_%H5K(4Fci+OyKeirk_ zJU+M5O$NceN4?mk@2vkn*4_m^it23q-wlZ-5}1HNW5nJx>oMSHVvZPnIV?bTB7g1JBvKqQc=T)YHOm|;Z`d_w>= zzwdKqX0t)oI^-VP7i%B33mdH667N+z2XNh0~8xUC_tt_pglb(hGiu&uSz=fV@u6&Q2 zh0huu?5Fl6snL2JM=f{IPM;*PpC$nUq2o&KC!p-=q`WV zP#{*9N4uld{Ce(l`aVAt9qkX--X{}aX(yQB`*O+hXQ~IPw2L2!;p$j~wP&@UBRowi zItBWvamI3QdqM3&HN1^;)29u4s^lOmp$&VsM4?Kw0uJ}GwgN#FE169Rv?-%KRJ~%fGTd z^qypBLK<1#=<4>+$Hl6n6FXsIRlSVpQu<2$MEE(dDW(IKyvK!mTyzO`(~3pI;iLtw zdY(H`^)NK zOnyqW>;1f}ksoGfbbo2|Se~}%x;()x4%$|g^`%C_ug0Ap89OdiH&a_V$v^JWNcp%g zN5Xn^Gutz5{&5%7b<11ID<tg3*;TQf zTI>f@6t+HF_DsXc+5R{J{;^!fot*gGxG~z|2l;M@uio#MZ`#8xTC`E#*B`IdVv`U` zl-xP)T5YikiA@d}0}F9FZZP~EechQIT=C;Th3A1<}2^T5R5#YWgva z(OJhzrSVuvy>!BmqzR+KnT-~k&ya#<@451p5fp3juK{nS7CV*7@g6PyDzK!Q6wrIx z+2McU^XeN~{cq0|MA+xoV#`J&014;7{`ekWWgeA=Y#kH3B3!sJD+&+t+p<-un}?ql z-1YcKuu)t~(WmG}0O|1!^Qc*By~YB7C=_rI?j^KGzTK_GU4Sflw#GQx}ekHy*X$kuRz!y8#(9f)wuk)28GJXZ-(#7(eY%X$ZgJ%q=K|l zZ>1+rK{15R0uLT8OPxT#nv3J(KzC+nB%dm=qyo}ZKINZ*xS%iJxiZ-xGnpMSkFbz6 zVe_eynGB`MBuxZadNzV@%K(gk;Xx(|6pAYwpH-FDNK5f1LkmVqjB56+oedLnhuAHT zvoP3(A8GO5706(faCsC*pgXt;IX+)wOtq70jUIP4TeRS|flGo>)4wD?l@$ZEYpP@$ z{P`l9{kMy92^fbOo6WY9{L!vzmH38y1_KDKpOeqz;|n!G5&QqSP}8XVe=XD}ug`Bu zKkS0wL17cMtscm|b`-wqQ7WWLUu+5T3e zqRpt?X=qoyZuk$2EMc$UEGzsMniYhebDp#xDH)A=BBV;-(_7W%2SG+`80JY~f0N9? z@b)X>eMo9*Xu)ZcNW>X%wYkef89X)Ht-0WtgEBhdCoam|^}djPn^KpCLdG7>v^a87 zv6kmVj(r*mZ9zs0QV9?l(sV608DeDUHNvKTQ7rTLcclI9$WOo`aeoOCP5HCv3QY>q zVG|V>&`c|lCO;j8&Qr85YIluQNPSVUYW{O%QVjqktLCson9v3=GTXJ-gP?u#ZYclO zk{aDS8cy6QtK*P0%QHP(dpz)Z>QV*23CZlss~U5wMWUfc)t&0WJrsS{Z}l7!*f%TK z-^8nG^I+KAgSOqFCJbtUQKMu(>z@;nXJ1}j@c}YzYBerLKe!V#UkqvCSHu22;&31S zCkIjjp(8L7)q-xm$3ieVU$ws*v(=sjgBQI9^Qgw`60Hlj#8p{AG#Ikh=;$zW&(qEA z%O@wZO|h*eCBvfO3=1Z|<`g)VH)l_fAdnipzPCh+J(sKW{2D<%`)hC?S3e3iK3LKy z2ni}S@o8Pg@DhR_^Q=<7ieJDyjtFszIlMS%zL6T?=xylD_dKb%V(jPO7s!YCuN-5B z`M=hh_n`4sz*hGP#8hm4BtH9ky78?R31(gHRt=yKSk3&1mS@+!Nzsdyt%n$`YW+mT zn{HbUq7$DDQKe1s>=MkJ2W?OAI9Sdq;t*KR%1|p5(6*)k-u-!Qk2U zSb&mI;1RDt!)D=`PMO?{GBdTrs|L8 zvb^S2Q~Cs{wS-x@TyYW8i1waKJ`eEN)5q=K@f6JlLT=bAkc@U`({r&F8;f)iLLMNz zOOXwrdX2EepNg*aZz+7p*`Dx@|53`GD7-Hc0hV~y72bU!yiYeot_~F529-}FV=Pps zIahdlG9fL*d5iky=waH*$#B^lvc}ykXt0nil|$MJzzaSY+l&TB(L_P_&HakVfDXQi z;q9iD!UHwL%4Q6rrpi0^eyyh-7ofXpD@-eSPp;8U9d_56Ldom#BsKKPFa8Qh$;i zAjh1G??kuJevL%gA856{`3|TOD_O#j6=V7_tH+GhZ)=R`@$rWdAEJxC3|6^fWQ^2u z%)lm|mNoJI*G+*|)SH8kNlSNVu`ADzFSp}AMG0sFyKXD%!4H)u)<7%|9T4o}@oc3% z353rTQ1N7*j{!Z%gP{3H$;=r`q;;Iz2cA4(d*fYzauz{tUn%Eob}BI*Tp57L6$?z?ZN+g-%d_|AoY zQO5B=RSkWwUsbyqOcBQhsv6^rx)xOBr|Qo?gkV?SN()vNtC1$b>4xd(&wmt_^KWFT zt3R*%{l`i0Kh&SI`X~DVIHPzDixq}Yr#I|%)MW(g0I6yoK|?{16{p6DHvpZe9Y5d| z$-+@aMW+^9JVY&19xI^5zQNMEaQ!Ku_f(rnJPx8Ze}NeY$nFGy@t(Sls>ZK`l9T=M zzPd+gHMMFW%wI#p7vro+VBgb@4L1lMfT+l#A%9JQ{RESWoOXy#8xtV zT0DjDvM&Qy@s|jS$aKH9_Bfsjut|gJv@;j@3;y(CC)4WI<)9h0VjlarOGkVGkqVN;2>= z8g&6M)=mS+&r`ZRE&7h%?5ubtvKBs}V^u(1(w;E#-O?^-B`i${I3{6aqWq14A;}2J z%eIQDyL1#}D;Up1oM5b{%;&$dhiUQisUahh-9oi3GXG62K9q;jHT@pb;@SKP!yu(s z-F#!f6cTV-d?Q-yhy9koOknJ?@M;!W8q$eAR+C^W`gj--Ep6F<#2e`9Te^ANd_#0F zd(*o@%$YM)?6tn!7a3;+4zd=acb>4UFYOiPcu1K)?wZnV_|m?{`~tmx+@-#{7vYsl zyBLm$0Y4qs&qI^QM~5gyJ(|a&7Yh`x&;1zUuGp8J(^7EMNIDj=nCF%(0^kvOBF$i^ z^$HgIupU*fvRwRxQ zPrir9Pg8y|VV$>tH83Ig8q6haxP{H5p7m+1hCY<_7CTccE&w3ig`(|Q3Wbh=l1iuNDG5jI4VlPj zO8e|2RKbsn4KojqPbQCI2>#>J~fW#~+RNcunU9$}GqLX(@VH@fb~aO2Z*JjD7=Ole#_bJR&}XVl zq6XBJ-H7<_0f|guy4YCGQno3;)BX-?RC*dfWO_eHOBiF6>D=Ems?&AbikuXFxq(lp zQ>azcP{H4MN;ec6yx1i?Bbni`^=LLVBU3Nb7QadLC zmd%jM7^S;{E@PU2@|IR>t;;|c#VjMIp~4QA#Vn}%Ib!T@|0Ck9?)L(wc>E$EW`+#mMAP0q&1_%qMT2Wj!E-O$ZzXlWjx1eUH&;VZTCp?O&~fm<-)nj9{LDwX9m zR-dJ-jBq63Be?e_etR9Mw!Plmor3oepACEJY0ldH21UKTvP&JcGQJ;^4rMSqZL!6! zIvGSqOg%&;P7{Qg#x}l60P^$r*LFUk-XI<0>VPuhKwf)tMd`(5j4JBj-i1WB%K1?6 zzAl^VM%%=Zh(d||zDzU#m{(<(3_{5(ae9YKDi7&$%Roho7CQv26<-8U#q{8_zb}(@ z?Nd~cthQ{|MzwFiKowuXBGF?#NB?z9B*UxxX^Lpt)&BMHo-C$J6=u!DV#};_`Hnr0c;oA}ff-0SM-c!u)5^J7ZId#X?LlX<5WCs#QwBB$@^8 z%JOPI{_=m>=g~C1NDjLgdOz{Zg;b~!XOaAhT3JRi*p3efV9}7hNpVOJ)o(}fjQuB7 z&lmCYnY%LVrT@{;40{FTk(!yj4BxTHP7ebe!775(`T=NYKZ(W^6s>~TP%Zv-R#~k> zn8|v3WMU-54djS@B`68}o#+LDmf{B9eqy{3tJ^+bm665n*TZ2e9sE{Y(VDbVc#~kA z$Dt}4%ll>Cm*GH_oF+M9v+58M0o0U7Q*gE2JG1MzDhvT%pX7>2z1Wc46x zhf9GTzY)bLLrwUQR-{vd&S1?VSTmFn!O3E9GAPRa!0rQ7^xBkRJjHJ*Vo0oOZdxr2 zvyWr%RVpK5+DLjz1=MlWI+wu>1d!Tzz6iLP?Yz%V#tN;P6iY(JrW)%anNmG)+_Z$!7U-xr^}iImAS zhP9QIe%l8l9{AY)U!GuiE_ISgI{=YPjejkm- zKA&m1cKeBZI&3~sG98WzuhzvVd@(2j6N?vu7QX!yBlP%o3u?I#j__?no|y`o??8eM z>rOcBz`}z7W4g=7M+5$Tgnkz(Ck<4dINY=gt4fAD4`TE1&7(t4uPR$_zMj_Omc1-s z#fr+{TFzEEB851O))axFU7`T@)30^!D5&YzLqLF8ygf)XP+rOdEa>vK1O8!jT(x7sPK7@0DkpJ)I9^2=L~=Zv=PmYppN$WBL&IH zgvvOgdZ3)X4UyHa6Sga@<^(?~ZBB<(eAYIn+uwsEP@(Y=Ap*46S{ikDUEJ?c?ZD8N zG)sDX+h^zpv1=!xalL0PhShR-o(M)~4M7LTi_7-O=U)rvnL~atf|+6dYCX+Khrzx+pl@egFXeTblTO7eiHy-9yc=xe;leCWSLW z3l{_b`(J4c%;qT=IDVN&34!eyl(1K@9jP%plN{2tkV}`Dw7_nM(84}=u0sn1n7wT8 zMh}8re=M+)5OHe~E!Yn!l5)hZM|2rR+i$meUbG{VegBDp zeN1EuQQbl@7l)&tLL={hZraLb`%Pt(sl9$kI%K|Uekhy=U3xPMuN&PX9rq9)>`5H|AEf0Lc31E zB^h{sWd`1dH|B=Pf&}JN+UG%rOy*+7A9OY*?Tau!2IyP%13mxANUsrfNR9Nf3?N4a z@EK8Ox;^hdC~eqBrSxYW$<=5ST095aMQt0{A30Xc#i+m);3O>W;y(V_WBEb0s=Kl7 z58H=66zoGZVl}&#v=(~-nu)Ggfi9d79cW5hPj04-lWF6#tR{#%Rf>L0yR?6W=yLVX zeBpE-DGMOO<|ym5Dd(mcZAgU+?DRbs?(;fKtF-dt4HAXQE=nuKIznlYh0 z==%s<2PC7Js?Qz{Tm&TNsgl4&TAYdp<73VRyAwgA0H$o4bT^V1!>P=;@pHmmiu!!2 zPp=nxR*o=8D`N)*?-bhLtuP>u!6~k)*fp)owQM+PE3K8APULl!jr080^Dny0?sj2k zvxVcq9enn45CQSlT67V=TsWy&y^7-ZSys;82l5F_@H;F0PSvhOw5yJsUc_tmnHu}A z<1_{)iPj{V{whALYhM5b$VT#Xr)qSb-+q_&GGJq0_huSN_6b|~7?Rj8gTDiKG3RCg zi9MM%Khwyrq+O<|MY}vexywHc1VxAp0;B_0MglGaj9mCnu_t%r{GZy#FN=~udDjCb)K2JjBNPF&Hh)+yn8 zk8PwiTye#0(DZAZO3F$b^|&$X7TG^z2|e3k0J_Gm0c`de&=G!u8&*|fYFW26(PUsP z;vAn%amd`TdJvd|%pf>L(gF}BR%}fdENoaS6>i~FLp?C_>Yw`=>bJ#8MpM;q6=k-Q zd3cQPavrEuM2^6Z6RKzfI6YVUDj8()7r>v|&bI7RaeQzjN=}JUd}e>PJV^(jasqI* zd%-~`FTlCX)0fv_ZgDaLIC%jgUv={VT+I!QIbUJ5wL2^02jBNg7;x!dQLWNRGa z%&vbSe}$|tF-L~iT^{?~n9-eJ` z;q~V22e(_VH@~)h`-iV>=Rf@Zsz)ZNAR%H_pzrMCScl`zDyiu@0I+`#zdQk-7@1t= zbP0W6d6bk3evec1X$`v`T5XB$J~?_UXYL*3t*P81_*M8LlDzlrcJUhrY5hWQXFZUq ze=Jw4+A>2fIEbz_oO3f(3iI%wL$I2LA&Lt=lkO~Xs>A@ zF;`ntkE@ZY{8u$d*OT;*fAk&|aMa5bE-e&;Nl{ARblb6g2Csd19a#Fzuk zA+V^c%*`1R-)W@Oe!a0;9`dj0X{P5>{tnea>i24%&?&{y3{y$azW%tr=`?vTlk3 zl{R+T-cDZkl4&*U{*Y1D<&uexi2qgsdN_Z?ljmg*aUAhw_tHX8TVquve?XjTU*4Px zf}F<6rxl^yu)KAD!z#$bAjQep>I?6O{^0eYzE&30hR@cHsgQ~?Z>&Z!-62!}S zrRR7hQAYo`58j@XYoz-%er_B=?lf^&zxS@w!`h#Tsc#PX+XW)_ z2aulBQzljFZIIEyC!ofav>)2*-1q5WMhsnIhJDY=A5L}I4^x8k$ItLn{?()8%^ij} za|i%2j%-s|Lh+9tZ%9ML+-85tnR(XpGFt|cWsjnne)Jl6p7K-i>l{|y!LLHT4aBcw z_5UY+{h7|w_;r(D5cs7ht5gW$`GQVQ(ul12h4NC4LHAjYJC9<2p)zGX+~QTR!qTGb zkNCp1y|k2xSC8^k@ah{>_V7yZNS@uuGsxfvX`B-0_S?L404Nw1)`GpM2{J#0Ki9eV zQ#}xW1Zf=nDQmK2t61btl-3!WgF@0`RMPxr9CLK|&j9?ni@HWTssVnpt;UffAdg>= z=ks!W3*6EE{0e8t2uGP)AU;LGqkPp}8jqF(8HGQ)PQ;(Ov7l1Mx|^zi&)Ny23AMLN zpRQE6ny(qS#++94;|TJBn1!3LDLJ3JZN|BM3!S`=)n*sjv2sJrsp6`<9h}1gCbIov zwhC~}R@h{awe;x?dxW*=4JpNPm!IncPGNk=d{9odrMOKH##buLJt!;^3%fJOPM2tq z$ss$%0Ox7B_1?LH!lX=;^_elgZ(-z28A9qu@>}|&MV4S^>X)oXnA}{=Rx!#AoA-I0 zk<+C>wo`x^-pg}E_wslUkEJE*+SAkvr@btm>(8`4k2YB4l^6Z17$<4sYq>pbm36s^ zi6s>iM=v;)J>mMj#axY5JF!G>%qK5q35#h?6am87>@>Y2vaEwYKpK9^d*?j5n`5^! zoW$WExq49f+7Nu^O9a13<_p`J2){~9Du_4D%c)qWU2P>M3ya>xw9Gh)rGVYFdX9Ityjdca@z zJnbiI`poO|3E3&>=Z6$rT}&s|9&je3%)-4)yt1DFS_76jDZgh@UUuCWIl(?D-=T+e zePM4I_3QT9od<)hLgYFbqCnO%w%>C@~^TCz6Y zOR&tF>Ha|H@~T8luHIf*K(~Da?}xEr$6hGp$e(E?WA*6n;ErcpZ7@5)jc6|yS_?EX#osOjOC?A7@$i9N?QRx3nJAsqTes}Ir)d5{^R41KOQ9R4XvdGv}v|R z=}Dt6(5w@PSa+7amj;}E+5I#-{GAYo7P|n(8Q;3_ZGo3;`jL89@+tSU5xW?p@96GI zEq<6U&F ztro`%hN-W`{>3{r{Fl}6Bb#q;ld+G#j(>9n-{q_~sBc@cFO&L1g)tv#X_B$U-b+`}Vnr0`X*-Ft*?e)@ zap05Y6J6}&4d4^B7?V^4QsP2dw&czm+mqXgiLmm8XE2c5x>Hqy1&VCtx=n$v;HnSS zks9Un^C@uphU&FIu`g+mB-oA`;^C{z&u}J^xV8YWEVCJtiyZc)e0SH`VP3JTnBNg{ z-LPsLspdy|_=kEK{^zNn_?|qLb+dNz*qKbBn#@pC)X&)$P}429-71HmWRvk)9#YhQ zkaR{KKTo&w6WSrLuI?R`agISb^rY~MY}1u=ENC-@&-%RuoSBiKI~XnR>;S+5pQODp zGOmd-+`D<0IY0DiZ`@x{<4!aFg?sI9QFyspiZZOvirza^w6Ng`=sm0@MsD^pn9BBjw8n_*uwURU+$$M;dd?(u7^pLoY(rp95IRupY?F4@gJV)?%fLd; z`kave9YOU(Nj?3n;(vl4zEcoHbE=9T(6tape7mAer;CJu7fP1Edy9Qq{1Dg~Ong0G zuV~e^YB?$G@$v2_6d#92b3Uo3r9LtY;iokorGj3+dtuPLHdh|RI$$fIHKNt=uRdsU zZ)9(1^`;k`f1I76I}e|#lhaYT+aq7ZoE+n@loCln6zC0)I!NI1pt=#EWM!7#<{Mj0 zHXn0s8h#y|r3xL{g%gcv!0aCz{-#iXn4aEfQ>osFqDq#F;Up8JR_Yi1@{p-(OK)Jr z_!oj?HVpIzTFlP))&*n0uH=M1a$jYmk#&3H6O*8=vaghAj=ICx1l7q~d0(*)?n@(R z+zZA_zS}^r9%v1kU(c5(V*sgQS#h!3q{qhd1V#WlTgX^qzPDu@6n6;lT&8o`S@V@{>&DAl`wK_9u)Weqx_xr5urbnkIJyQSBr_3Bz1~KU9-h9bNWBB-7bIvz4-`e z;&1UlrX72gFU~lnYAKqp-|moS={rV9-oZw=!0mu$N|v{v*{Q-3$14#PZ|4zN?O7RD zaBSoOxop9O)g5)wi$v_KX=bLe0IO*8@$F z&kMf^)i&w8(mCd6+}6UGRL3wH3my5Mkw1FHjw(1nJ_NZV zXm$jX6SJsNRne@kVj%RmOV_Sya?a3;_L8rzK+>s_tHY7TqQ%JL0f(^CZ7q{=^~gd8=POp+eR{u3-~0K@zwemU>EI*ah>Y*HqW!6q+Y=juc@ zS&_jedcC6ezWPB3|?y#Kb98j z=I>`7_T2};lZayO4Ic?_*J7R2Ct3WtK3qi6k3O7#rAYLb7Y30~YL_x_$h@||otZAC z-)?IYq?|(7_S5Ycz>$HZ3-_i-c#J+rVt<*;gLk-k(g_X4Kty@}LZJht=_&!+jOEC4 zw3pQ840y5Hh%)(W4+U8qdQ8JnM4a6N%$?^M@cbr}A+~%yC197ktOCXoP|AXN9t}a3 zqRE}bR}|8T`6@xvr^z2(@~yQ+kN&q7YXBjt5>pG5NUBOqM;vkBEz}xq8m=E%tAs=R zI))oeT!j#72IreIgwWT2$Hh|Fj7VQabes>nLdhe8jaEhzdqNsFU(%y*6)XM}M3!Jr zkLnC2XO~8sz7ou{st7Ks|LDJTZ9==zwZCy9hx@kHy@G|I#qN;Wcu|(h$vG7ow_PMe zv)O@MTPlVWz?E>WWOxvt+t6Rq{HH_Y18=7BM%sEuM#r@@f!-j=Lzf)qr|THdqsIV) z_~G>D0A3$fIU3c|^lh+lTve64O%}t!cWup;$N@|h-bDZ@`+wXhc z5#HG?M~=`ttxw?o^^8E0y3Q*`%8#Qr0Wf3JwQweYk3Hf7*aIz0AwyoNv0pFXVUlrK zd;wT&IA7WZl{&r)Z_>%ZJaxvY2bvSt=LXHI@~g~E4gkt%q^FIesx<w_qid9~%&fMFeWfBCmUe@q0-E~0<*BjpKLjw$&GjeS?5v6b;CRYZz&{WRAetD_D^K07jKoJwOO3iQ!)$OTkeyItmn7jfGBVt4(JlF>a!n6>(`0|ZL!##_03o< z`%>B-D8z>(u3&8_6TZIT`3QrVyl z5}Z`tK;W|YF}IqXN4Dij-uqx4;`XCH)-=$>teP$C*KJ^}Xq1vMNc`Uzjg?m+skj7lpmOh!SLcc0661jxZ%P!hfe-&@@pQCYB zl`^J$fluRb+onDLA`GS~S;%P$k}6!?LKLQ>eCpc$&CN$gk{2a6$IzZ{bG3~s?)yOD z2y{qhQK6sO4$(zWNsQaqi#nnwzk_bE$!I?MdEt9?uWQdAs1o!2UdCS)=oEu2Cdz%4 z=o!l~1GM;Lek!lS9ej!I#_JI0i?~jdNoo+|u5=NeM0XWO*H5)BQIBWgHo?uB{ zR^$SBt5RMBf76u*dnK`{#XCcdg}cQidy%lhbM2o2NaQL0)gb%TjOn5o07>~jgeqbL z8=y=(Of-7*BK$1?GJ14uWRu9_Ckf1vyE^cr)CFfWeAKtFZan+&^>W^>n{DT!r!di` zzAcF5_QMWkF0b9?jfvAT)zVL&YK6lrZ0U@Kcen+JwU%2c1qJ5i(oeO2>pjO;52XQh z7gA`;{=k;zU0Fs~H&`79GF3d0c0VC>?sVyEt{e zT1Pg`07ssHxgfrNULICZqXgmm1@~f089~t865XYRj!51ZloyE`P-quw-p_4Z3vylP zQ#a^@y|lz1dn;F3vfj{wJt#`8rXx&FSJIvnr@Y!O&c|DbPCHaK<(+LtzWSw z3Uqc`nm?)E3W$mrpX7zT2yP~N&1j}$Z&sW|q}tq5p{2T^Rp{acQ1gPpf{$&3E)r-a zCvljaTNqFCTlTw8>kg3>+ZMsoj}O3283wXt@DMR?_P5jhXz?#F0NW=#HEj2Jffj{m#t;u4A?3n_K{>cfQy4TaW z`C}hrXkkvrNYXpGJ?W*ONgA{lt$*AqR6CT@bJM}Ntt4c6&^iB}yctz!R>^D#eXO(f z%Or7%IVVcE<&U#4fNDMu`)2U8kq$_avxE=%H-nR7%lYJyc%0=QUmxT{Dv0x92j9cV z3orf)=EYg)ces0ITZ@Q*b3M{hM)p&qN6)R>t*sm+9ZFDw>QT-B|CpB~PYY9VJ{yu% zWj*>ca6@0R28MEnI8^J8-8A=fZROTb?Ueg{+(y^8yB|bRt8^S0JfB1@*P|Z|(iT6= zH%`iNAYLA+?Q2ESf6Ny8&4Sl8UeB9kc6~Callm%44wM?^JMKDg)^(BKZ(@m>Fn69b ztQDYplq;?QJWM_^rD7{a`vz&Td_;X!BEN)RS&>5i;`Y5lji)cCE}P_rCKb`OvZg#i>-IwIWTbnBBPdaQR*< z_gJ{1 zpoA9d0pt#PE}>PvWsz-6p>Qj|+1oZ#_9SY{LDN$zpi?rIQAL4M&|R(S>Uj)A2zVx+ zZ0HFNOFYtas|*=rpG5snf#q$V0+XL(e2W>Z!v#>tVWGlQgYNJ! z3203O`(M4e$^MjTkZS7sRyE@@c`Ew?)5UZsSMLj8j=t1)vICuPn3zoA zcE(3Ms;D^Yu8^VZ!gZmL9wf7dzaeOlvP;-b5K%LCoYL0$npLF*58Ds$JZhKf<|+19 zUH$|VfQ3udzvaDTp@tbrKZ1V>*R300XH8yYJI6(IpgcQW?m<^>3;aOJI58{s1ypHNwEZ3Wpm>)4JiHZ;e_NJ2w38{~yikIt z|HV7hkZjt-(aFM-QQMVG(Y8Tu-Hj9x>7itey^NNs`K2VDohPG}i4t-Q2!*Pbh$PvC z1uszk=}jQS{Q!$IF#_84JQoz4Ad@;Wtz^D~ObR#PP=Zv`oH;NKHRb+s$bK0qx}=%= zXeNVc*tdbTUI>|rhbh|`J$lxHB4tsya^K%1+G@?|6&vQi&(`JrXR!uFIIASmH8UQ@ zjo%=4gS~5Zo6lPCJ=Ki5qBcl0v}(sVU9ad^c$kL5G-M>N@|#7Z)Ceby?8cgGuCs}4 zjgZ-r@+58{FadWk0SduJAGh2b>eRj- zYTlD|MRMF(A%7}ya#dnb(qPM>>BxKF<5+I@U}|?=ubb^5b4SRu%hs1|6+JbPRh8hD zzLsom*~w;Cq@8T!$Rr#Xel|$#;!vQ8eX1%`|G*&5DcTKpi9C1RbuP4twD&Y?Gg^@B;&AbNsNIk%PDFgNOpV5Wf_fXGgRbmtl$7uIv-2|T9hPlanIoi!C z$JBKY1n0AT$NexMHfM>QQ`jY3|H5y2h~32}wnbSJ_7vIQrBouYs+fk$)^iI_v9|Ot zJOlrlIdQOVVUM+?h(43osiP zS0!#JlyudxyIgKLUrPRHc~d{UA__l zcnMQHq)pAS_{nFp7`LRceRn-cR4$N$Ltb~^@UZ7lP#b%YGX@8>#mle=bdqDRb+xCk z%D*kt++7^bYZAR1jkv|!7QnqeRiwyi+T#CWe&IyUAVgBA_GKn@$9~!`$nK=Ab=snh zuw+l$(!o=EIOQ|!pWf`cD`h#wA~X^!N{aZ@S1CT89CAA2YiCcHII4+=cXho*y{ucq zTEWZ#1mVzESyO6<@C^a>a?;U!ylL8pqYY_#Selj}mW3EfJ>t~=czLu4DIZnk)lgPZ z-o8@=3j3(r?Q3DrY5u0SPiBV(vG68Otjj|q=VG3CQWbl}dzo3l!D@R-Q0aY8C zKLmcHvnYxT~Gt zXgKrve-{6bHl0V}G@WWqM_h7>ZuC%uf*2}vTT1V8zJ97csyR`WkMBmt5(%IaRjy*- zT!pEHhj?>hz-{-3wWI$3zUMCtE zHyCTN^Hdl7AmOEv7;*mZRJ}Vob@ru=pV3K43j$0WT zjR??px5`~ve1~$!HhC>h=<9EC4ox!MfAK_!Z^N9w>DaL(pSprQ>`s~t%IUe%)T!X5 zN}+T=@NwUUV9oVMDktWzmGxbYOgTT%>o?Ga#;$6t9kQ((?#Kfa1T*g;PJVO~s; z0`^Tp7R%9N6p`_SDKrV9j28B@PWaTfA5t*uh&B%rq_RfJ&Is7TsS5_tk1tw$JUsx| za?qvmcS965)Ld3os4aaN@}v*JQ~5{lvmi`4UpiVoWxu#w54@JRE?b0gFksX2{E97F zauB2eg3I8d_Mqr4uG=^W@&h0eM6mJ!7R>JwjbHmU%yX+$)qdR)+roWv63O+Vl%G-( z)cvhN@(@g6R}|8wR8WzOLlk3R2aHByz`$B4ftVI&CJROiLq0QDv0d&>ySg<%MO^T7 zLTm}}D5vcy*e%z3sMf?>WzXyqbPPfir2XkCKMU!6QB`QzIsl`A;RyVuTFZwEXPz%f z@F*#*1q1sz_dzr10Zy@gNR{Yuzm%Dxt!VY8%$;@G|1^zi6+X0V2H^J=mTO?*55Pj! zpgAZm75}a*f8ceY$zb%I?C9Q+Ix%JuxWplda>3a_#RO%zL2E3cl99}p?t)13pnnI! zID^GqI*eEJN-HznL1a~Gzg{N-^>+>f7|r)(XVP0`j9`;t(yg2jSLWDn;3g8#P}cM` zg}I+rCNGd6NO7I@7Hv~`>Gvr`2Q{1;k#$n^AV(KV{cXN7W~dOJW(0tvt*eJAGfSO0 zjS!EIJtdFJd90qfkRENFr=CQ>S!R_MA)gas{(OrpH$w#(P z%t1nXv7;}5dqKAT>p-a3PDPVKi1FVFn{zpB#wsZUYiK+OUkgjf+Hfau^7>T=5fU0P##M;c7;weY+%ctjRosCYg`*(}af zO8%OoRx|!Qwm0}{wkM~STDufSz(BLjHmd}#7bGY}^evUMSj;(xaT?Y?72^aa%yp7W z-$3A))?i1Hai0d_ZZh#w`$n+PStn}{g9bVP#@;RN950^ArF?ssgKr+Y*Oq<>w3amiqo;sg70ud` zpVKcf>EN!^ES# z{)I%-U0s9;Ua7RY)Nt%WVtdqevD(TS{59(gq8#dvp*NwMF*YSX8Vl5I(`sBa$)!GS zvC_yv`wMC@WFCqG+YjK;%$S!F%=QyJG&oj4VxAF?|5A~@QfCw!RPFK&omU`J(+?5^ zkT7o@;ztABAj=dqxaS~JSYRlcSQ>=Y?Gp0Z}k7}|olGs`Ku|7H6IFj`e-?StvX zK(5<=h7Sw+3D2?~hcLhjr7*QyFTgxx*}=VKr?WJ5dC@%_r;tNHf)OBrwsZ?vFKa6- z+x`-76v2v?SUG^s*@NX07-jTdhi;-r7Zy_cIchtcFD6kE9%Btl{!M%ascN;JjW@G% z*hDHnMI&UnwI;wNLKPp#xP>c<$tY1%#%I% z-${ni@{rsO=ddHiisV8inNqQ|Z0?n~0S?;$`|`#jyIy2j1k&Klz)T8?`#$!TjNMrr zRy7TdqXfaT_`8McDdb~}yi_y_PK{W>wri@WUq2B<0i&u+VIA`YkucjdkaH)Gz|U=< zC0T)5{9-OmkZmIDpe=PBX0x~!$q*~nTtQA9&GH>nXR0!Fw8ejbWU0GW=^%@i3NmRI zapx}RB3`YJaVDPSemaarV+%fOOH44+MSRfgN%KU%Q?^- zm=id_!>22F+2*UT&vZoCEX)@XMx*UUHoRR*5SjZHKZTDfTblx|QGs4is{K&zlF;HW z@|S^>(4x<7r@ShgX_MRY*^mX9o5UOkgZR%^}_h< zw~(1t$UxE5r4~l$Gt7%oW z*uUWkVf@!JVP^+LpQ0jBYHutwUm~qc^M^SVsRj3jliw^dY7bN=&wenJy!eUe?vYyT zJ~~aDWNvxBm@nbvr0l>eT^5(3RadlXiyUYFUh;Vps&W*Lto-?1-R71csa+<0P?az~ z(7SBP&V9`YaNalgAm<5)d|n^9LLAhopv!;s_6ax$U=-~WaCdty&Hnw8u(>Z(ad^%) zPk(!qUvrPCCbJ(52fD?3t(y}H0rs3nQcE*7TttM_UQ=i!b@og0 zMV-0pj8veXKg;0IABv5N<~hSet=bU&`|6S+;(d%VHYIGuJ3h2o# z-o06o@6&Asaq_K|p&)M~??~CJ+1B9He#ZG#WmhcD$)Ob6 z7T3_kpK#IOe;ve=zAnCe-uv=Rz-2xA+mHI%bG9Dx`4mU1eUaVKZTuoXtjPpBoeqq? zbFZcu^epN1tVGVE6cO1Tn+konzTA3&e|yd%x2@FQ#E-1@+mt_Yan5X2DGMV$>fGiH z6J~%1FmD;xw|J`t*uWPF1eZcaUv~!Z>0iOC+}uc1T{HL|J)XCK)4VFor)w{9W$L2J z`1y>)SOq)pLml=RqnfQZFXt1;o*o@5cuNR^pdN+{1gL zcB7HV-XJwDBtc|!{NJTg*v#Hg$DviY7z6$*kNQU3BBO_-MyH;V`MP5<5;=#eeZDSE z6%7ekY}{Or3$fW?yCtw5(B3ZtWK&cf#%ew~$lPl9w-^mai{Y)>;<1dILIf_3c#=l! z5WhYARrzI#opaQY|BBv|o3}unKjFBv&J>pe7l~IUR7yf+bDliNI*sq)FSdL>TiYd_ zAv3=u!Y7${+y{}GvqHA#l{@k7Y}uFT8x9kY_)~C2UB6OoSsJ)k|ZVH*NRD3vpC};7Aw5#Rd_De>sRkl8P_80Yv zTk^FBoAp6g=UU$tXq#*4MpHrX+;I#mY~_d@8Dih#ev`X#7gze6yW@Zin~WWE->Xhu zSHy)1MWN)_$3w|+YeE(KwP;MnhyEEj#?2LH7}5P_fFERMAxvWI{n@-!dv0NE!v54S z5B|bB!xb;hxw0y8rtH3Ak4I9Z%u_c0dd{~DtCFjC*^-4Zu`Syb_8$xTi9I7Sw&sHn z8?#2m=DFJ^nfb(Ps;1`tBffLt5bqu+cJCsC6YQ?azTVczLDo%{tlL?#jC3I$KXwni zlXb!b)i!ZtvKuGSI!51oJd34@++ZZe_q@qWNgbdrOyVH{go$@hbPJYmInI?7iLc02 zzd^%1f|QHAt#tBT)-|j#t}F#<*}YP(jrXnnh_o%o>o_hWgDQrJD@1a~;~>HV=pb&Z z=S0WvI{-P<{RTb1Z^T2#>Gpwse2`2g_&(ke%;5?()m`L5_fFwd>vC}%B}ZIuDi4eC zvqa&2)^-NqU@BMAK&p7JP8f?bh+%3FHSKkPY0oiiK z*{PH-3Idz9VDUMIV(xdss10-c-=!IIa-q>KEY6qutI?kGOP2sNXZ zKo4Hd18F;P52+rA9q9^iXVWckB!=(wkqim2h7m7wYEcG#<420h`P&ov+Na$&mZyoU zaxEJxHCubJvt3@k9X$hHI;WI0Y+H28WX8okd8{dRP{?G}D z!olG{vk}-b!OYJMnH{noI*Gg3Ru3f$&(NbpYgD{6w@XqUCa*lh2<&)9=B8Ex`!Bki z;0qf@wTU%A#mG;}dJw3~6I~sA32777hx{*u{F^xMI%j;fX|py;uSpDZm{W6hIKWN= zksQ~2z}C5R?R2|&?yFqw*{2qM_XjizhpQ{<_a9#YYub;>wcn3!$aO3=g_*3W^?4bn zX8ud@j)TtwA*hCgj;*$+4>Od!#MTn}YDX9FWJEd@Qfr1Vly#T$@3lH0zOjhlq}9c= zSDnm{njKXM{e$WxgV&zxj2;>pU?$lm(PO7L=TD;h2Uk~g-Tlp~#5KLJ(dp(!G(oZ< znw^0zwBGIDxw(bZB)xJ@rfc^Q30^RW8VFH|h?i@KN$T3K=WXr3;KLZzNdN=Z@EuLg zp?3%0h>;?oWkPivjR^HXXg8>x{MoSJyN_ug@G0k3@ro(p}az&Q&to~qjC$<5*p`@9URs3ONvM` zPjhOapDU(wYbeb!@Np=lxVFZnu3OHQ_g{F!K?RWTl<4>K8Q?aCB(i2YpybLR(8=hR zHR`xPC2rbuMM2-rvc9qdOgb*Hpoxjuc*U1C(=I0_S+a3@prfjyZT?-s#P``@>8rb4 z$Lr5#cdqD6Kcb4+%XW$kB(jLiW80NtKzLVIH{*~gCZ??@0>-0Stt;_#$c}0gh1->p zCp$Ke(nh>*RSgeiMYJj_RqWs!J}fC}HkTdHaV;q0y8UD+Xb$CY4x4M}O}b*F)*D}B zE!?*AM%%prpYz=n9Y6RN=%*aZT87owI2 zX7uV`)36|H(H09)MCf5^oL8RlfDOSwi!~fJ4rWQaww#G~_HoRP=H48(`VzrdQI}g= z)+)x(UG%|@HQUcPaO9=vTLsqH;)5ana+UogSc^UYs!gRVIp~|})YcTW%MMuY6TPjv zQn9|oDed67xIV{P0g4N{kHyh0>0TVT(oTWfPGq9nv&!jc{Xqi3orGlu)JX}+ zr?l(2=>oYf7>;zv6z#hN&<^f1l-s3#j^e?U$p3!lgxYU$_y=z}^x8aUuyRT4M_QT% zir=~XCz$-!NLUw_gfxGU6Rh}VuD0a2^hzQYq6M3e=0x|5jLe6JnFiL;di0_n5&}5q zYj}S>KQJmoyN)Idlc;FE3;Uy)XjfE)W+Q52$JklboSb$QHb+4~Dd-?IE8?8X)*Q|> zW4U7Mf$d%2L9Dya6#V)-4p+3?wK%oO?bk|y-$5314-039>3)E~9vJ`H@V4+liN|bW za@w$GRSN5%HY`@s%>%wHQM;0SS)q1h$zE!30%|6;!h~tc)l^|Zw&w9^{o*1){&KMv z&X=#8xDu2_k$+?XNFFPBgkSPc9MIsiRF?Zig{#Rw9A7x%aC8vug_C-nUVrSuxdmhp z-9vETTc_Sf^}jSr3*# z>dPY6lKCr>5u;OPA)}-`Ga+znt-m^1=_eWyeWgXmokC=lypMj6yI>=KwS_Mv3xAxP zQq!4I_@acF+lVdBQ5hgJ!^1Zd=J|wbof*~mt75P>L2Co%EfDmn^gu3P*fR4)hRa0c z6_*|HO#r49xQkQZElz>AI0fF~6u67iuvV6H0IP*wY0LNi4xuQM)YWv7Td*B-Wuwypb+!4&r zXzx{Z=Y^AxxC=kxF}68`xy-#lyl+~3t5g|TFAaO>5oVr(0U2O$<-7}|$f*Jpg-HYh zIKVjxZgMM{M{ab90#dGA4;I@17cdYj^#B2GP-)fBtvrzStu8swR$X^FwKWs{QdvJ{&&D9nQ8xOl32f@}*av~@*jiko?SUJ(}b@iK5A)Vxv zSwe8x8?M zL38_&?I?)47TkzLH|)WTN$K@$ViMk5<&W-uqy894ckN-acC4GT7!at?sR6MF9YzF+ z1jAz!X~4-@Z{G&{i}v0@T||d{iokw3dlBv0q7<7>vhB9!`)ZS|ou_S_vg0Xh9RqB6 z4))`(AeIGTe9E57Y-^W#a@5+cp12(1 zDb+;9>|8B*T7k6XagKbrkAZInhc1V{S#lM8K23c=r+Grlz@^0*EqbHLWMwWgGh|<) zGS@uG>qW{c?95H-Bb6M++Qlt)(5QI%?g9EOD2aS(nR_t0Yh)yURWU6EI?QIo0C1M| zQLCr6hPMJ`(WZp$scjV#Dq~v0J@kZ8NDA=o(&+Bq_nzl^xTPJ1_211}xJ8Pyb&4)xK_F-1E_cU$U zFj@5Eh_~hm6=dHhy5Era2_wJO~^|z5fD6F?!<9IL0~XK~;_@GO1}EL%R@OV7`_Nj_`q?l{BzK9<-)W z6Uq~`WcPX-=X$X7``={qW*Yl$$%R<$W-8DM9xMxEVn@E}-{DaJv>|VfGQUQixnopR`UN@y6J&@YL!=6?<)=Hqe}JYE z^IWW(cx`r9am6YjTgg#Wif}JmM_yvblBcq7KHB07!X1xG+yv%=!+A3Amvx zIsbD2ZC&7XtP8F2e}O$rduDJDmpb|16#+GE4Cn#K>ZxQN3kG%s4*FYkQgU3~q9@0l zoE+<~c-OSFtJ?L7b{Gft$bypt?SXZAK;ppgtt_|z2$Jvc9uA~(ItfgoKkF>IM{)>6 zYOFO(TB2rZ%wqxKhP7qiU>w}*oh#gdBMI=+1sXt^DH5iPcCa=Gm>$AUbHDW$l7H}k z2`m^&S3&cb^(*y=D8JqGVQI*T=)6Kd(F6;J1QV5Pd!wfWAgoaB!UB$VfL0>MREXP; zNmRnLr-O>IlOP|8F8fHxZ5uY8*;;nIqL*Zq5|e;R_BlqFDP^XB&MOo;MJgC?rH8}O zV!FdHr>f}u!RLU72OKT_2rUuQETi^I2(OqctK>>Ci0veIHqqCSsWU*yyNj>z5gew@cFpyMo$S;MQd?0a!PDSgaQTq$QT zC!c)wl$(Y~I zWQ@GlDzjOn!A>BzO{62|O|^wjMR#jna|7C9O;lTs*HfC(lTYM2O-c1Tn!uNsoFA4S z9gKAEU2^JA_-{{(Kaxi`4$lG4bGIC;+s~n&_+6B@Q!+m}XF6cM6P0kt>Av&-=lOOq z5OGN_==%RW-&UDotIYRrvaONGxr{-|Gj{wl=bNi|Y{qVBxLOs{N7jvN@&~8Aul)!Tk>^s`Dt=-pa5tY{k3p5Zc&fU3s=*eX*Um z8d214aUL7c>|bcuZ;es!>&Rg47$Sa`4Pu(61-z$|dt4ADT!h|9_V2`hNqn7&XJEg! zzZgu>z;p`3jYHo$VEK;v=P25wGP!W9*Z&txz` z7!B)tP`j_3MufdI0Ru>N{7hiRm}_XnSGKdP$7R^X{G`GWdlK~S>6g|RB!IUjh{!}` zAt$#gt}9${7JgGW>5pKiQ|>QyBiW;_;WBo3H`uPxpdC32l2qgONFM^Rg{Dqu=q;8@VSJ>mDVa!!20Ml zd2d@i^66Rnx9?Pl2$2KJRy#4E|07DHoU{X8a6b%TITU({@jYZhA5qN(IaPc;qp7SX z+FcNBD&S$I_Tx6@6r(N2Nc}p%Cd{-AlY{`f3wnhP0LOF$!4W*Ck#m#*c^!bE{3Qow z`FwYu!MzlM?Lx2Yx1|;XOn#9SLfWqM}D+LIz^!gxD>jFODe5eAx?kT;>i^3r<=^XMxF-mL+uWFd2f+ zR`F$Th#=}m%>0*XMl7X+4@KP6 zBJKvS&Bt+_{-Y>N_SRl%ztgm(1zI0fw%p?!bil8sKllj@b&>D|gaCiyOZ7PT4)E%l z8(~%oQ&-n~c1#V)sVt|nST=AW<{pfqTW2F{SZ|+!DF9sf;D}SJL6e*k9x4S}zS4Ef zJ+6BN?_*v)2|+voep4*EDYiX|H(Us^xyNzu;oJilJYW;&=Wzeva~Mx-mw!ZCZ4c8Q zEJ~i?dulsSN(>EC`}wFcQP$VsOuBItT-*3NLn~>I>?WWHLu-ID3DE}x>HC*qJdyLt zOyrX+=&$`fsn7}Nw)gXT!hOO(NcH^*I8$cLMvRV_IrswT;J|5b^7^=DANWg_v{9ut z14*hXJadJVqy_g)+*YEB(4Y@#(6p08gCG`^anqKQXhIM&sdged$FHU{6C9Ml7Gm2` ze6Xcozq#jQ6zeaDies3K?`}Ra14k!YpCNy0c};~w4!N$59MTe-a;K>WlItCg${|!K ziSyv!N!^rkOR?3l$bntfZ6N5YS*TEe)}X!H_cd&jWogBdga# zn)!Pa`%nx`jlit1`ck=w+wvyLsU~6ju)2iyF~N!i>J3Cg{KQt5-(#T$LeuO#ikn3E z@w(W4%sd{GiIA)ewu8(3mma3mehhyv&wX)_UKc@W2T#JXgy3Gl&Cd+Id}N#%%2q#8 zo`d}^EsrK7LX;=4A13i=eZGORRP2nHwniYMStI#O$~e~4rmernD7v`^g>v368dre9 zr-4UJ4g3`29drT`pY2gt8Hte(=2}9r*;&%}%s*a-;L$R1KhCaknkT3=)j4=EnBffD z+4t+gQ&51+`%K7E8M_{IDE2BApqChM+7D}{?JMyqs&@x`5Gl9+eC(n$OS_{wSRi4; zz|prBr-rq#sT?3vwjC{7p)1JozGl0AT+&=6u45e`1IH1%meTLf})nS81 zk2-;E(mrhe±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%0D!7PK)d8ESq3J?!=|M_A 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 dfc&*$jG0$r? literal 0 HcmV?d00001 From d9d6f1d000af06f7894ccf98156e501c4d73022b Mon Sep 17 00:00:00 2001 From: Felix Oesterle Date: Mon, 30 Jun 2025 17:08:03 +0200 Subject: [PATCH 11/12] chore(pyproject): update `cibuildwheel` to build on multiple CPython versions chore(pyproject): update `cibuildwheel` configuration for repair-wheel-command fix(com9MoTVoellmy): correct Windows executable path --- avaframe/com9MoTVoellmy/com9MoTVoellmy.py | 2 +- pyproject.toml | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/avaframe/com9MoTVoellmy/com9MoTVoellmy.py b/avaframe/com9MoTVoellmy/com9MoTVoellmy.py index 323ad1226..9f8ff7519 100644 --- a/avaframe/com9MoTVoellmy/com9MoTVoellmy.py +++ b/avaframe/com9MoTVoellmy/com9MoTVoellmy.py @@ -179,7 +179,7 @@ def com9MoTVoellmyTask(rcfFile): os.chdir(os.path.dirname(os.path.abspath(__file__))) if os.name == "nt": - exeName = "./MoT-Voellmy_win.exe" + exeName = "MoT-Voellmy_win.exe" elif platform.system() == "Darwin": message = "MoT-Voellmy does not support MacOS at the moment" log.error(message) diff --git a/pyproject.toml b/pyproject.toml index e028890b0..f555060fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,11 +63,15 @@ version_file = "avaframe/RELEASE-VERSION.txt" # Cibuildwhell [tool.cibuildwheel] # Only build on CPython 3.12 -#build = "cp312-*" +#build = ["cp312-*"] skip = ["*musllinux*","*-win32"] build-verbosity = 1 before-build = "pip install cython numpy" + +[tool.cibuildwheel.linux] +repair-wheel-command = "auditwheel --verbose repair -w {dest_dir} {wheel} --plat manylinux_2_34_x86_64" + #Flake8 [tool.flake8] max-line-length = 109 @@ -86,6 +90,7 @@ platforms = ["linux-64", "win-64", "osx-64"] [tool.pixi.dependencies] setuptools = "*" setuptools-scm = "*" +cibuildwheel = ">=3.0.0,<4" # Feature dev [tool.pixi.feature.dev.pypi-dependencies] From c01f4b4dd5272fa7fa306301c551231d428c7425 Mon Sep 17 00:00:00 2001 From: Felix Oesterle Date: Thu, 3 Jul 2025 10:01:03 +0200 Subject: [PATCH 12/12] test(MoTUtils;cfgUtils;probAna): add unit tests for `rewriteDEMtoZeroValues` and `copyMoTFiles` test(cfgUtils): add unit tests for configuration utilities test(probAna): add unit tests for `createSample` function with various sampling methods --- avaframe/in3Utils/cfgUtils.py | 5 +- avaframe/tests/test_MoTUtils.py | 208 ++++++++++++ avaframe/tests/test_cfgUtils.py | 567 +++++++++++++++++++++++++++++++- avaframe/tests/test_probAna.py | 129 ++++++++ 4 files changed, 906 insertions(+), 3 deletions(-) create mode 100644 avaframe/tests/test_MoTUtils.py diff --git a/avaframe/in3Utils/cfgUtils.py b/avaframe/in3Utils/cfgUtils.py index 5d06df4d3..8a62f6192 100644 --- a/avaframe/in3Utils/cfgUtils.py +++ b/avaframe/in3Utils/cfgUtils.py @@ -218,9 +218,9 @@ def _splitDeepDiffValuesChangedItem(inKey, inVal): Parameters ----------- - inputKey: str + inKey: str key of a deepdiff changed_values item - inputValue: dict + inVal: dict value of a deepdiff changed_values item Returns @@ -985,3 +985,4 @@ def cfgToRcf(cfg, fileName): key = key.strip() f.write(f"{key:<40}{value}\n") f.write("#\n") + diff --git a/avaframe/tests/test_MoTUtils.py b/avaframe/tests/test_MoTUtils.py new file mode 100644 index 000000000..ecae4bcb1 --- /dev/null +++ b/avaframe/tests/test_MoTUtils.py @@ -0,0 +1,208 @@ +import pytest +import numpy as np +import pathlib +from unittest.mock import patch, MagicMock, Mock, call +from avaframe.in3Utils import MoTUtils +import os +import platform + + +# Note 3.7.25: these tests are mostly AI-generated + +def test_rewriteDEMtoZeroValues(tmp_path): + # Create mock input data + mockDemData = { + "rasterData": np.array([[1.0, np.nan, 3.0], + [np.nan, 5.0, 6.0]]), + "header": { + "nodata_value": np.nan + } + } + + expectedRasterData = np.array([[1.0, 0.0, 3.0], + [0.0, 5.0, 6.0]]) + + # Create a mock Path object avaTestDir = "avaHockeyChannelPytest" + # mockPath = pathlib.Path(tmp_path) + # outFile = pathlib.Path(tmp_path, 'testDir', 'testDem.asc') + # print(outFile) + mockPath = MagicMock( + spec=pathlib.Path, + parent=MagicMock( + spec=pathlib.Path, + name="testDir" + ), + name="testDem.asc" + ) + # mockPath = MagicMock(spec=pathlib.Path) + # print(mockPath) + # mockPath.stem # = "testDem" + + # Mock the rasterUtils functions + with patch('avaframe.in2Trans.rasterUtils.readRaster') as mockRead: + with patch('avaframe.in2Trans.rasterUtils.writeResultToRaster') as mockWrite: + # Set up the mock return value for readRaster + mockRead.return_value = mockDemData + + # Call the function + MoTUtils.rewriteDEMtoZeroValues(mockPath) + # MoTUtils.rewriteDEMtoZeroValues(outFile) + + # Verify readRaster was called with correct argument + mockRead.assert_called_once_with(mockPath) + # mockRead.assert_called_once_with(outFile) + + # Verify the data was modified correctly + np.testing.assert_array_equal( + mockDemData["rasterData"], + expectedRasterData + ) + + # Verify nodata value was updated + assert mockDemData["header"]["nodata_value"] == 0.0 + + # Verify writeResultToRaster was called with correct arguments + mockWrite.assert_called_once() + call_args = mockWrite.call_args[0] + assert call_args[0] == mockDemData["header"] + + +def test_CopyMoTFiles(tmp_path): + # Create temporary source and destination directories + workDir = tmp_path / "work" + outputDir = tmp_path / "output" + workDir.mkdir() + outputDir.mkdir() + + # Create some test files in work directory + testFiles = [ + "test_p_max_001.txt", + "test_p_max_002.txt", + "another_p_max_file.txt" + ] + + for fileName in testFiles: + (workDir / fileName).touch() + + # Test parameters + searchString = "p_max" + replaceString = "ppr" + + # Create expected target file names + expectedTargets = [ + outputDir / name.replace(searchString, replaceString) + for name in testFiles + ] + + # Mock shutil.copy2 to avoid actual file copying + with patch('shutil.copy2') as mockCopy: + # Run the function + MoTUtils.copyMoTFiles(workDir, outputDir, searchString, replaceString) + + # Check that copy2 was called the correct number of times + assert mockCopy.call_count == len(testFiles) + + # Verify each copy operation + # Get actual calls and sort them by source path + actualCopies = sorted(mockCopy.call_args_list, + key=lambda x: str(x[0][0])) + sourceFilePaths = sorted(workDir.glob(f"*{searchString}*")) + expectedTargets = sorted(expectedTargets) + + for i in range(len(testFiles)): + callArgs = actualCopies[i][0] + assert callArgs[0] == sourceFilePaths[i] + assert callArgs[1] == expectedTargets[i] + + +def test_CopyMoTFiles_EmptyDirectory(tmp_path): + # Test behavior with empty source directory + workDir = tmp_path / "empty_work" + outputDir = tmp_path / "empty_output" + workDir.mkdir() + outputDir.mkdir() + + with patch('shutil.copy2') as mockCopy: + MoTUtils.copyMoTFiles(workDir, outputDir, "p_max", "ppr") + mockCopy.assert_not_called() + +def test_CopyMoTFiles_NoMatchingFiles(tmp_path): + # Test behavior when no files match the search string + workDir = tmp_path / "work" + outputDir = tmp_path / "output" + workDir.mkdir() + outputDir.mkdir() + + # Create files that don't match the search pattern + (workDir / "test1.txt").touch() + (workDir / "test2.txt").touch() + + with patch('shutil.copy2') as mockCopy: + MoTUtils.copyMoTFiles(workDir, outputDir, "p_max", "ppr") + mockCopy.assert_not_called() + +class MockProcess: + def __init__(self, outputLines): + self.outputLines = outputLines + self.currentLine = 0 + self.stdout = self + + def readline(self): + if self.currentLine >= len(self.outputLines): + return "" + line = self.outputLines[self.currentLine] + self.currentLine += 1 + return line + + def poll(self): + return None if self.currentLine < len(self.outputLines) else 0 + +@pytest.mark.parametrize("osName,platformSystem,expectedShell", [ + ("nt", "Windows", True), + ("posix", "Darwin", False), + ("posix", "Linux", False) +]) +def test_RunAndCheckMoT_OSSpecific(osName, platformSystem, expectedShell): + with patch('os.name', osName), \ + patch('platform.system', return_value=platformSystem), \ + patch('subprocess.Popen') as mockPopen: + + mockPopen.return_value = MockProcess([]) + MoTUtils.runAndCheckMoT("testCommand") + + mockPopen.assert_called_once() + assert mockPopen.call_args[1]['shell'] == expectedShell + + + +def test_RunAndCheckMoT_CommandTypes(): + testCases = [ + "singleCommand", + ["command", "with", "arguments"], + "command with spaces" + ] + + for command in testCases: + with patch('subprocess.Popen') as mockPopen: + mockPopen.return_value = MockProcess([]) + MoTUtils.runAndCheckMoT(command) + + mockPopen.assert_called_once() + assert mockPopen.call_args[0][0] == command + +def test_RunAndCheckMoT_ProcessExit(): + # Test proper handling of process termination + testOutput = ["Line 1\n", "Line 2\n"] + + with patch('subprocess.Popen') as mockPopen, \ + patch('avaframe.in3Utils.MoTUtils.log.info') as mockLog: + + mockPopen.return_value = MockProcess(testOutput) + MoTUtils.runAndCheckMoT("testCommand") + + # Verify all output was processed + assert mockLog.call_count == 2 + mockLog.assert_has_calls([ + call("Line 1"), + call("Line 2") + ]) \ No newline at end of file diff --git a/avaframe/tests/test_cfgUtils.py b/avaframe/tests/test_cfgUtils.py index 48168f483..96920697d 100644 --- a/avaframe/tests/test_cfgUtils.py +++ b/avaframe/tests/test_cfgUtils.py @@ -6,6 +6,13 @@ import pytest import configparser import sys +from pathlib import Path +import os +import multiprocessing +from types import ModuleType +import pandas as pd +import numpy as np + def test_getModuleConfig(): @@ -203,4 +210,562 @@ def test_readConfigurationInfoFromDone(tmp_path): simDF, simNameExisting = cfgUtils.readConfigurationInfoFromDone(configDir2, specDir="", latest=True) - assert len(simDF) == 3 \ No newline at end of file + assert len(simDF) == 3 + +def test_cfgToRcf(tmp_path): + """Test the conversion of ConfigParser object to RCF format""" + + # Create a test ConfigParser object with various sections + testCfg = configparser.ConfigParser() + testCfg['GENERAL'] = { + 'parameter1': 'value1', + 'parameter2': 'value2' + } + testCfg['INPUT'] = { + 'input1': 'value1', + 'input2': 'value2' + } + testCfg['FOREST_EFFECTS'] = { + 'forest1': 'value1', + 'forest2': 'value2' + } + testCfg['ENTRAINMENT'] = { + 'entrainment1': 'value1', + 'entrainment2': 'value2' + } + testCfg['TEST_SECTION'] = { + 'test1': 'value1', + 'test2': 'value2' + } + + # Create temporary output file + outputFile = tmp_path / "test.rcf" + + print(outputFile) + # Convert configuration to RCF format + from avaframe.in3Utils.cfgUtils import cfgToRcf + cfgToRcf(testCfg, outputFile) + + # Read the generated file + with open(outputFile, 'r') as f: + fileContent = f.read().splitlines() + + # Verify the content + expectedContent = [ + 'forest1 value1', + 'forest2 value2', + '#', + 'entrainment1 value1', + 'entrainment2 value2', + '#', + '# TEST SECTION', + '#', + 'test1 value1', + 'test2 value2', + '#' + ] + + # Check that GENERAL and INPUT sections are not included + assert '# GENERAL' not in fileContent + assert '# INPUT' not in fileContent + + # Check that FOREST_EFFECTS and ENTRAINMENT sections are handled correctly + assert '# FOREST EFFECTS' not in fileContent + assert '# ENTRAINMENT' not in fileContent + + # Check the formatting of the included section + assert fileContent == expectedContent + +def test_cfgToRcf_empty_config(tmp_path): + """Test conversion of empty ConfigParser object""" + emptyCfg = configparser.ConfigParser() + outputFile = tmp_path / "empty.rcf" + + from avaframe.in3Utils.cfgUtils import cfgToRcf + cfgToRcf(emptyCfg, outputFile) + + with open(outputFile, 'r') as f: + fileContent = f.read() + + assert fileContent == '' + + +def test_cfgToRcf_special_characters(tmp_path): + """Test handling of special characters in keys and values""" + testCfg = configparser.ConfigParser() + testCfg['TEST_SECTION'] = { + 'key with spaces': 'value with spaces', + 'key_with_underscore': 'value_with_underscore', + 'key.with.dots': 'value.with.dots' + } + + outputFile = tmp_path / "special.rcf" + + from avaframe.in3Utils.cfgUtils import cfgToRcf + cfgToRcf(testCfg, outputFile) + + with open(outputFile, 'r') as f: + fileContent = f.read().splitlines() + + # Verify handling of special characters + assert any('key with spaces' in line for line in fileContent) + assert any('key_with_underscore' in line for line in fileContent) + assert any('key.with.dots' in line for line in fileContent) + + +def test_cfgToRcf_file_handling(tmp_path): + """Test file handling edge cases""" + testCfg = configparser.ConfigParser() + testCfg['TEST'] = {'key': 'value'} + + # Test with Path object + pathObj = tmp_path / "test_path.rcf" + from avaframe.in3Utils.cfgUtils import cfgToRcf + cfgToRcf(testCfg, pathObj) + assert pathObj.exists() + + # Test with string path + strPath = str(tmp_path / "test_str.rcf") + cfgToRcf(testCfg, strPath) + assert os.path.exists(strPath) + +def test_getNumberOfProcesses(): + """Test getting the number of CPU cores for parallel processing""" + + # Create test configuration + testCfg = configparser.ConfigParser() + testCfg['MAIN'] = {} + maxCoresAvailable = multiprocessing.cpu_count() + + # Test case 1: Auto CPU with percentage + testCfg['MAIN']['nCPU'] = 'auto' + testCfg['MAIN']['CPUPercent'] = '50' + numSims = 100 + + from avaframe.in3Utils.cfgUtils import getNumberOfProcesses + resultCores = getNumberOfProcesses(testCfg, numSims) + expectedCores = int(maxCoresAvailable * 0.5) + assert resultCores == expectedCores + + # Test case 2: Explicit CPU number + testCfg['MAIN']['nCPU'] = '4' + numSims = 100 + resultCores = getNumberOfProcesses(testCfg, numSims) + assert resultCores == 4 + + # Test case 3: Number of sims less than CPU cores + testCfg['MAIN']['nCPU'] = '8' + numSims = 3 + resultCores = getNumberOfProcesses(testCfg, numSims) + assert resultCores == 3 + + # Test case 4: Auto CPU with 100% + testCfg['MAIN']['nCPU'] = 'auto' + testCfg['MAIN']['CPUPercent'] = '100' + numSims = 100 + resultCores = getNumberOfProcesses(testCfg, numSims) + assert resultCores == maxCoresAvailable + + # Test case 5: Auto CPU with low percentage + testCfg['MAIN']['nCPU'] = 'auto' + testCfg['MAIN']['CPUPercent'] = '25' + numSims = 100 + resultCores = getNumberOfProcesses(testCfg, numSims) + expectedCores = int(maxCoresAvailable * 0.25) + assert resultCores == expectedCores + + +def test_getNumberOfProcesses_edge_cases(): + """Test edge cases for getNumberOfProcesses""" + + testCfg = configparser.ConfigParser() + testCfg['MAIN'] = {} + + # Test case 1: Zero simulations + testCfg['MAIN']['nCPU'] = '4' + numSims = 0 + + from avaframe.in3Utils.cfgUtils import getNumberOfProcesses + resultCores = getNumberOfProcesses(testCfg, numSims) + assert resultCores == 0 + + # Test case 2: Very high CPU number + testCfg['MAIN']['nCPU'] = '999' + numSims = 5 + resultCores = getNumberOfProcesses(testCfg, numSims) + assert resultCores == 5 + + # Test case 3: Auto with very high percentage + testCfg['MAIN']['nCPU'] = 'auto' + testCfg['MAIN']['CPUPercent'] = '200' + numSims = 100 + resultCores = getNumberOfProcesses(testCfg, numSims) + assert resultCores == multiprocessing.cpu_count() * 2 + + +def test_getNumberOfProcesses_invalid_input(): + """Test invalid inputs for getNumberOfProcesses""" + + testCfg = configparser.ConfigParser() + testCfg['MAIN'] = {} + + # Test case: Invalid nCPU value + testCfg['MAIN']['nCPU'] = 'invalid' + numSims = 10 + + from avaframe.in3Utils.cfgUtils import getNumberOfProcesses + with pytest.raises(ValueError): + getNumberOfProcesses(testCfg, numSims) + +import pytest + + +def test_convertToCfgList(): + """Test converting lists to configuration string format""" + + # Test case 1: Basic list with strings + inputList = ['value1', 'value2', 'value3'] + + from avaframe.in3Utils.cfgUtils import convertToCfgList + resultString = convertToCfgList(inputList) + assert resultString == 'value1|value2|value3' + + # Test case 2: Empty list + emptyList = [] + resultString = convertToCfgList(emptyList) + assert resultString == '' + + # Test case 3: Single item list + singleList = ['value1'] + resultString = convertToCfgList(singleList) + assert resultString == 'value1' + + # Test case 4: List with numeric values as strings + numericList = ['1.0', '2.5', '3.7'] + resultString = convertToCfgList(numericList) + assert resultString == '1.0|2.5|3.7' + + # Test case 5: List with mixed content + mixedList = ['text', '123', 'special_chars!@#'] + resultString = convertToCfgList(mixedList) + assert resultString == 'text|123|special_chars!@#' + + +def test_convertToCfgList_edge_cases(): + """Test edge cases for convertToCfgList""" + + from avaframe.in3Utils.cfgUtils import convertToCfgList + + # Test case 1: List with empty strings + emptyStrList = ['', '', ''] + resultString = convertToCfgList(emptyStrList) + assert resultString == '||' + + # Test case 2: List with spaces + spacesList = ['item 1', 'item 2', 'item 3'] + resultString = convertToCfgList(spacesList) + assert resultString == 'item 1|item 2|item 3' + + # Test case 3: List with pipe characters in items + pipeList = ['value|1', 'value|2'] + resultString = convertToCfgList(pipeList) + assert resultString == 'value|1|value|2' + + +def test_getDefaultModuleConfig(tmp_path): + """Test getting default configuration for a module""" + + # Create a mock module + testModule = ModuleType('testModule') + testModule.__file__ = str(tmp_path / 'testModule.py') + + # Create default config file + defaultConfigPath = tmp_path / 'testModuleCfg.ini' + testConfig = configparser.ConfigParser() + testConfig['GENERAL'] = { + 'parameter1': 'value1', + 'parameter2': 'value2' + } + testConfig['TEST'] = { + 'setting1': 'test1', + 'setting2': 'test2' + } + + with open(defaultConfigPath, 'w') as configFile: + testConfig.write(configFile) + + # Test getting default config + from avaframe.in3Utils.cfgUtils import getDefaultModuleConfig + resultConfig = getDefaultModuleConfig(testModule, toPrint=False) + + # Verify the configuration + assert 'GENERAL' in resultConfig.sections() + assert 'TEST' in resultConfig.sections() + assert resultConfig['GENERAL']['parameter1'] == 'value1' + assert resultConfig['GENERAL']['parameter2'] == 'value2' + assert resultConfig['TEST']['setting1'] == 'test1' + assert resultConfig['TEST']['setting2'] == 'test2' + + +def test_getDefaultModuleConfig_empty_file(tmp_path): + """Test behavior with empty config file""" + + # Create a mock module + testModule = ModuleType('testModule') + testModule.__file__ = str(tmp_path / 'testModule.py') + + # Create empty config file + defaultConfigPath = tmp_path / 'testModuleCfg.ini' + with open(defaultConfigPath, 'w') as configFile: + configFile.write('') + + from avaframe.in3Utils.cfgUtils import getDefaultModuleConfig + resultConfig = getDefaultModuleConfig(testModule, toPrint=False) + + # Verify empty configuration + assert len(resultConfig.sections()) == 0 + + +def test_getDefaultModuleConfig_invalid_input(): + """Test handling of invalid inputs""" + + from avaframe.in3Utils.cfgUtils import getDefaultModuleConfig + + # Test with None + with pytest.raises(AttributeError): + getDefaultModuleConfig(None) + + # Test with non-module object + with pytest.raises(AttributeError): + getDefaultModuleConfig("not a module") + + # Test with module without __file__ attribute + invalidModule = ModuleType('invalidModule') + with pytest.raises(AttributeError): + getDefaultModuleConfig(invalidModule) + + + +def test_writeCfgFile(tmp_path): + """Test writing configuration to a file with default settings""" + + # Create test avalanche directory + avaDir = tmp_path / "avaTest" + avaDir.mkdir() + + # Create mock module + testModule = ModuleType('testModule') + testModule.__file__ = str(tmp_path / 'testModule.py') + + # Create test configuration + testConfig = configparser.ConfigParser() + testConfig['GENERAL'] = { + 'parameter1': 'value1', + 'parameter2': 'value2' + } + + # Write configuration file + from avaframe.in3Utils.cfgUtils import writeCfgFile + resultPath = writeCfgFile(avaDir, testModule, testConfig) + + # Check if file exists in correct location + expectedPath = avaDir / "Outputs" / "testModule" / "configurationFiles" / "testModule.ini" + assert resultPath == expectedPath + assert resultPath.exists() + + # Verify file contents + writtenConfig = configparser.ConfigParser() + writtenConfig.read(resultPath) + assert writtenConfig['GENERAL']['parameter1'] == 'value1' + assert writtenConfig['GENERAL']['parameter2'] == 'value2' + + +def test_writeCfgFile_custom_path(tmp_path): + """Test writing configuration to a custom path""" + + # Create custom directory + customPath = tmp_path / "custom" / "path" + customPath.mkdir(parents=True) + + # Create mock module + testModule = ModuleType('testModule') + testModule.__file__ = str(tmp_path / 'testModule.py') + + # Create test configuration + testConfig = configparser.ConfigParser() + testConfig['TEST'] = {'key': 'value'} + + # Write configuration file with custom path + from avaframe.in3Utils.cfgUtils import writeCfgFile + resultPath = writeCfgFile(tmp_path, testModule, testConfig, filePath=customPath) + + # Verify file location and contents + assert resultPath == customPath / "testModule.ini" + assert resultPath.exists() + + +def test_writeCfgFile_custom_filename(tmp_path): + """Test writing configuration with custom filename""" + + # Create mock module + testModule = ModuleType('testModule') + testModule.__file__ = str(tmp_path / 'testModule.py') + + # Create test configuration + testConfig = configparser.ConfigParser() + testConfig['TEST'] = {'key': 'value'} + + # Write configuration file with custom filename + customName = "customConfig" + from avaframe.in3Utils.cfgUtils import writeCfgFile + resultPath = writeCfgFile(tmp_path, testModule, testConfig, fileName=customName) + + # Verify custom filename + expectedPath = tmp_path / "Outputs" / "testModule" / "configurationFiles" / "customConfig.ini" + assert resultPath == expectedPath + assert resultPath.exists() + + +def test_writeCfgFile_invalid_path(tmp_path): + """Test writing configuration with invalid path""" + + # Create mock module + testModule = ModuleType('testModule') + testModule.__file__ = str(tmp_path / 'testModule.py') + + # Create test configuration + testConfig = configparser.ConfigParser() + testConfig['TEST'] = {'key': 'value'} + + # Create a file instead of a directory + invalidPath = tmp_path / "invalid" + invalidPath.touch() + + # Test with invalid path + from avaframe.in3Utils.cfgUtils import writeCfgFile + with pytest.raises(NotADirectoryError): + writeCfgFile(tmp_path, testModule, testConfig, filePath=invalidPath) + + +def test_writeCfgFile_empty_config(tmp_path): + """Test writing empty configuration""" + + # Create mock module + testModule = ModuleType('testModule') + testModule.__file__ = str(tmp_path / 'testModule.py') + + # Create empty configuration + emptyConfig = configparser.ConfigParser() + + # Write empty configuration + from avaframe.in3Utils.cfgUtils import writeCfgFile + resultPath = writeCfgFile(tmp_path, testModule, emptyConfig) + + # Verify empty file was created + assert resultPath.exists() + assert resultPath.stat().st_size == 0 + + + +def test_setStrnanToNan_basic(): + """Test basic functionality of converting string 'nan' to np.nan""" + + # Create test dataframe + testData = { + 'column1': ['nan', '1.0', 'NaN', '2.0', 'NAN'], + 'column2': ['value1', 'value2', 'value3', 'value4', 'value5'] + } + testDF = pd.DataFrame(testData) + testDFColumn = testDF['column1'] + + # Convert string 'nan' to np.nan + from avaframe.in3Utils.cfgUtils import setStrnanToNan + resultDF = setStrnanToNan(testDF, testDFColumn, 'column1') + + # Check results + assert pd.isna(resultDF.at[0, 'column1']) # 'nan' + assert resultDF.at[1, 'column1'] == '1.0' # number + assert pd.isna(resultDF.at[2, 'column1']) # 'NaN' + assert resultDF.at[3, 'column1'] == '2.0' # number + assert pd.isna(resultDF.at[4, 'column1']) # 'NAN' + + # Check other column remains unchanged + assert resultDF['column2'].equals(testDF['column2']) + + +def test_setStrnanToNan_no_nans(): + """Test behavior when no 'nan' strings are present""" + + # Create test dataframe without 'nan' values + testData = { + 'column1': ['1.0', '2.0', '3.0', '4.0', '5.0'], + 'column2': ['a', 'b', 'c', 'd', 'e'] + } + testDF = pd.DataFrame(testData) + testDFColumn = testDF['column1'] + + # Process dataframe + from avaframe.in3Utils.cfgUtils import setStrnanToNan + resultDF = setStrnanToNan(testDF, testDFColumn, 'column1') + + # Verify no changes were made + assert resultDF.equals(testDF) + + +def test_setStrnanToNan_empty_df(): + """Test handling of empty dataframe""" + + # Create empty dataframe + testDF = pd.DataFrame(columns=['column1', 'column2']) + testDFColumn = pd.Series(dtype='object') + + # Process empty dataframe + from avaframe.in3Utils.cfgUtils import setStrnanToNan + resultDF = setStrnanToNan(testDF, testDFColumn, 'column1') + + # Verify result is still empty + assert resultDF.empty + assert len(resultDF.columns) == 2 + + +def test_setStrnanToNan_mixed_types(): + """Test handling of mixed data types""" + + # Create test dataframe with mixed types + testData = { + 'column1': ['nan', 1.0, 'NaN', True, None], + 'column2': ['a', 'b', 'c', 'd', 'e'] + } + testDF = pd.DataFrame(testData) + testDFColumn = testDF['column1'].astype(str) + + # Process dataframe + from avaframe.in3Utils.cfgUtils import setStrnanToNan + resultDF = setStrnanToNan(testDF, testDFColumn, 'column1') + + # Check results + assert pd.isna(resultDF.at[0, 'column1']) # 'nan' + assert resultDF.at[1, 'column1'] == 1.0 # number + assert pd.isna(resultDF.at[2, 'column1']) # 'NaN' + assert resultDF.at[3, 'column1'] == True # boolean + assert pd.isna(resultDF.at[4, 'column1']) # None + + +def test_setStrnanToNan_case_sensitivity(): + """Test case-insensitive matching of 'nan' strings""" + + # Create test dataframe with different cases of 'nan' + testData = { + 'column1': ['nan', 'NaN', 'NAN', 'nAn', 'Nan'], + 'column2': ['a', 'b', 'c', 'd', 'e'] + } + testDF = pd.DataFrame(testData) + testDFColumn = testDF['column1'] + + # Process dataframe + from avaframe.in3Utils.cfgUtils import setStrnanToNan + resultDF = setStrnanToNan(testDF, testDFColumn, 'column1') + + # Verify all variations of 'nan' were converted + for i in range(5): + assert pd.isna(resultDF.at[i, 'column1']) \ No newline at end of file diff --git a/avaframe/tests/test_probAna.py b/avaframe/tests/test_probAna.py index 38628f9e4..df60c694f 100644 --- a/avaframe/tests/test_probAna.py +++ b/avaframe/tests/test_probAna.py @@ -16,6 +16,9 @@ import configparser import shutil import pathlib +from scipy.stats import qmc + + def test_probAnalysis(tmp_path): @@ -1136,3 +1139,129 @@ def test_checkForNumberOfReferenceValues(): with pytest.raises(AssertionError) as e: assert pA.checkForNumberOfReferenceValues(cfg["GENERAL"], "relTh") assert "Only one reference value is allowed for relTh" in str(e.value) + + + + +def test_createSample_latin(): + """Test Latin Hypercube sampling method""" + + # Create test configuration + testConfig = configparser.ConfigParser() + testConfig["PROBRUN"] = { + "nSample": "50", + "sampleSeed": "12345", + "sampleMethod": "latin" + } + + testParList = ["param1", "param2", "param3"] + + from avaframe.ana4Stats.probAna import createSample + resultSample = createSample(testConfig, testParList) + + # Check sample properties + assert isinstance(resultSample, np.ndarray) + assert resultSample.shape == (50, 3) # nSample x number of parameters + assert np.all(resultSample >= 0) and np.all(resultSample <= 1) # Values should be in [0,1] + + # Check Latin Hypercube properties + for colIdx in range(resultSample.shape[1]): + # Check if values are well distributed (no clustering) + binCounts = np.histogram(resultSample[:, colIdx], bins=10)[0] + assert np.all(binCounts > 0) # Each bin should have at least one sample + + +def test_createSample_morris(): + """Test Morris sampling method""" + + # Create test configuration + testConfig = configparser.ConfigParser() + testConfig["PROBRUN"] = { + "nSample": "4", + "sampleSeed": "12345", + "sampleMethod": "morris" + } + + testParList = ["param1", "param2"] + + from avaframe.ana4Stats.probAna import createSample + resultSample = createSample(testConfig, testParList) + + # Check sample properties for Morris method + expectedSampleSize = 4 * (len(testParList) + 1) # Morris method formula + assert resultSample.shape == (expectedSampleSize, 2) + assert np.all(resultSample >= 0) and np.all(resultSample <= 1) + + +def test_createSample_reproducibility(): + """Test reproducibility with same seed""" + + # Create test configuration + testConfig = configparser.ConfigParser() + testConfig["PROBRUN"] = { + "nSample": "30", + "sampleSeed": "54321", + "sampleMethod": "latin" + } + + testParList = ["param1", "param2"] + + from avaframe.ana4Stats.probAna import createSample + + # Generate two samples with same seed + firstSample = createSample(testConfig, testParList) + secondSample = createSample(testConfig, testParList) + + # Check if samples are identical + np.testing.assert_array_equal(firstSample, secondSample) + + +def test_createSample_different_seeds(): + """Test different seeds produce different samples""" + + # Create test configurations with different seeds + testConfig1 = configparser.ConfigParser() + testConfig1["PROBRUN"] = { + "nSample": "30", + "sampleSeed": "12345", + "sampleMethod": "latin" + } + + testConfig2 = configparser.ConfigParser() + testConfig2["PROBRUN"] = { + "nSample": "30", + "sampleSeed": "54321", + "sampleMethod": "latin" + } + + testParList = ["param1", "param2"] + + from avaframe.ana4Stats.probAna import createSample + + # Generate samples with different seeds + firstSample = createSample(testConfig1, testParList) + secondSample = createSample(testConfig2, testParList) + + # Check if samples are different + with pytest.raises(AssertionError): + np.testing.assert_array_equal(firstSample, secondSample) + + +def test_createSample_invalid_method(): + """Test handling of invalid sampling method""" + + # Create test configuration with invalid method + testConfig = configparser.ConfigParser() + testConfig["PROBRUN"] = { + "nSample": "30", + "sampleSeed": "12345", + "sampleMethod": "invalid_method" + } + + testParList = ["param1", "param2"] + + from avaframe.ana4Stats.probAna import createSample + + # Check if appropriate error is raised + with pytest.raises(AssertionError): + createSample(testConfig, testParList) \ No newline at end of file