|
| 1 | +#! /usr/bin/env python3 |
| 2 | + |
| 3 | +""" |
| 4 | +This script serves as the main entry point for running all pieces of functionality in a consequential way by |
| 5 | +following the settings given in the configuration file 'configuration.json'. |
| 6 | +
|
| 7 | +Usage (from root directory): |
| 8 | + $ python3 -m mcda.mcda_run -c configuration.json |
| 9 | +""" |
| 10 | + |
| 11 | +import time |
| 12 | +from ProMCDA.mcda.utils.application_enums import * |
| 13 | +from ProMCDA.mcda.utils.utils_for_main import * |
| 14 | +from ProMCDA.mcda.utils.utils_for_plotting import * |
| 15 | +from ProMCDA.mcda.utils.utils_for_parallelization import * |
| 16 | +from ProMCDA.models.configuration import Configuration |
| 17 | + |
| 18 | +log = logging.getLogger(__name__) |
| 19 | + |
| 20 | +FORMATTER: str = '%(levelname)s: %(asctime)s - %(name)s - %(message)s' |
| 21 | +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format=FORMATTER) |
| 22 | +logger = logging.getLogger("ProMCDA") |
| 23 | + |
| 24 | +# randomly assign seed if not specified as environment variable |
| 25 | +RANDOM_SEED = os.environ.get('random_seed') if os.environ.get('random_seed') else 67 |
| 26 | +NUM_CORES = os.environ.get('num_cores') if os.environ.get('num_cores') else 1 |
| 27 | + |
| 28 | + |
| 29 | +# noinspection PyTypeChecker |
| 30 | +def main_using_model(input_config: dict) -> dict: |
| 31 | + """ |
| 32 | + Execute the ProMCDA (Probabilistic Multi-Criteria Decision Analysis) process. |
| 33 | +
|
| 34 | + Parameters: |
| 35 | + - input_config : Configuration parameters for the ProMCDA process. |
| 36 | +
|
| 37 | + Raises: |
| 38 | + - ValueError: If there are issues with the input matrix, weights, or indicators. |
| 39 | +
|
| 40 | + This function performs the ProMCDA process based on the provided configuration. |
| 41 | + It handles various aspects such as the sensitivity analysis and the robustness analysis. |
| 42 | + The results are saved in output files, and plots are generated to visualize the scores and rankings. |
| 43 | +
|
| 44 | + Note: Ensure that the input matrix, weights, polarities and indicators (with or without uncertainty) |
| 45 | + are correctly specified in the input configuration. |
| 46 | +
|
| 47 | + :param input_config: dict |
| 48 | + :return: None |
| 49 | + """ |
| 50 | + is_robustness_indicators = 0 |
| 51 | + is_robustness_weights = 0 |
| 52 | + f_norm = None |
| 53 | + f_agg = None |
| 54 | + marginal_pdf = [] |
| 55 | + |
| 56 | + # Extracting relevant configuration values |
| 57 | + config = Configuration.from_dict(input_config) |
| 58 | + input_matrix = pd.DataFrame(config.input_matrix) |
| 59 | + index_column_name = input_matrix.index.name |
| 60 | + index_column_values = input_matrix.index.tolist() |
| 61 | + polar = config.polarity |
| 62 | + robustness = config.robustness.robustness |
| 63 | + mc_runs = config.monte_carlo_sampling.monte_carlo_runs |
| 64 | + |
| 65 | + f_agg, f_norm, is_robustness_indicators, is_robustness_weights, marginal_pdf = verify_input(config, f_agg, f_norm, |
| 66 | + is_robustness_indicators, |
| 67 | + is_robustness_weights, |
| 68 | + marginal_pdf, mc_runs, |
| 69 | + robustness) |
| 70 | + |
| 71 | + # Check the input matrix for duplicated rows in the alternatives, rescale negative indicator values and |
| 72 | + # drop the column containing the alternatives |
| 73 | + input_matrix_no_alternatives = check_input_matrix(input_matrix) |
| 74 | + |
| 75 | + if is_robustness_indicators == 0: |
| 76 | + num_indicators = input_matrix_no_alternatives.shape[1] |
| 77 | + # Process indicators and weights based on input parameters in the configuration |
| 78 | + polar, weights = get_polar_and_weights(config, input_matrix_no_alternatives, is_robustness_indicators, |
| 79 | + is_robustness_weights, mc_runs, num_indicators, polar) |
| 80 | + return run_mcda_without_indicator_uncertainty(config, index_column_name, index_column_values, |
| 81 | + input_matrix_no_alternatives, weights, f_norm, f_agg, |
| 82 | + is_robustness_weights) |
| 83 | + else: |
| 84 | + num_non_exact_and_non_poisson = len(marginal_pdf) - marginal_pdf.count('exact') - marginal_pdf.count('poisson') |
| 85 | + num_indicators = (input_matrix_no_alternatives.shape[1] - num_non_exact_and_non_poisson) |
| 86 | + polar, weights = get_polar_and_weights(config, input_matrix_no_alternatives, is_robustness_indicators, |
| 87 | + is_robustness_weights, mc_runs, num_indicators, polar) |
| 88 | + return run_mcda_with_indicator_uncertainty(config, input_matrix_no_alternatives, index_column_name, |
| 89 | + index_column_values, mc_runs, RANDOM_SEED, |
| 90 | + config.sensitivity.sensitivity_on, f_agg, f_norm, |
| 91 | + weights, polar, marginal_pdf) |
| 92 | + |
| 93 | + |
| 94 | +def get_polar_and_weights(config, input_matrix_no_alternatives, is_robustness_indicators, is_robustness_weights, |
| 95 | + mc_runs, num_indicators, polar): |
| 96 | + # Process indicators and weights based on input parameters in the configuration |
| 97 | + polar, weights = process_indicators_and_weights(config, input_matrix_no_alternatives, is_robustness_indicators, |
| 98 | + is_robustness_weights, polar, mc_runs, num_indicators) |
| 99 | + try: |
| 100 | + check_indicator_weights_polarities(num_indicators, polar, config) |
| 101 | + except ValueError as e: |
| 102 | + logging.error(str(e), stack_info=True) |
| 103 | + raise |
| 104 | + return polar, weights |
| 105 | + |
| 106 | + |
| 107 | +def verify_input(config, f_agg, f_norm, is_robustness_indicators, is_robustness_weights, marginal_pdf, mc_runs, |
| 108 | + robustness): |
| 109 | + # Check for sensitivity-related configuration errors |
| 110 | + if config.sensitivity.sensitivity_on == SensitivityAnalysis.NO.value: |
| 111 | + f_norm = config.sensitivity.normalization |
| 112 | + f_agg = config.sensitivity.aggregation |
| 113 | + check_valid_values(config.sensitivity.normalization, SensitivityNormalization, |
| 114 | + 'The available normalization functions are: minmax, target, standardized, rank.') |
| 115 | + check_valid_values(config.sensitivity.aggregation, SensitivityAggregation, |
| 116 | + """The available aggregation functions are: weighted_sum, geometric, harmonic, minimum. |
| 117 | + Watch the correct spelling in the configuration file.""") |
| 118 | + logger.info("ProMCDA will only use one pair of norm/agg functions: " + f_norm + '/' + f_agg) |
| 119 | + else: |
| 120 | + logger.info("ProMCDA will use a set of different pairs of norm/agg functions") |
| 121 | + |
| 122 | + # Check for robustness-related configuration errors |
| 123 | + if robustness == RobustnessAnalysis.NONE.value: |
| 124 | + logger.info("ProMCDA will without uncertainty on the indicators or weights") |
| 125 | + logger.info("Read input matrix without uncertainties!") |
| 126 | + else: |
| 127 | + check_config_error((config.robustness.robustness == RobustnessAnalysis.NONE.value and |
| 128 | + config.robustness.on_weights_level != RobustnessWightLevels.NONE.value), |
| 129 | + 'Robustness analysis is expected using weights but none is specified! Please clarify.') |
| 130 | + |
| 131 | + check_config_error((config.robustness.robustness == RobustnessAnalysis.WEIGHTS.value and |
| 132 | + config.robustness.on_weights_level == RobustnessWightLevels.NONE.value), |
| 133 | + 'Robustness analysis is requested on the weights: but on all or single? Please clarify.') |
| 134 | + |
| 135 | + check_config_error((config.robustness.robustness == RobustnessAnalysis.INDICATORS.value and |
| 136 | + config.robustness.on_weights_level != RobustnessWightLevels.NONE.value), |
| 137 | + 'Robustness analysis is requested: but on weights or indicators? Please clarify.') |
| 138 | + |
| 139 | + # Check settings for robustness analysis on weights or indicators |
| 140 | + if config.robustness.robustness == RobustnessAnalysis.WEIGHTS.value and config.robustness.on_weights_level != RobustnessWightLevels.NONE.value: |
| 141 | + logger.info(f"""ProMCDA will consider uncertainty on the weights. |
| 142 | + Number of Monte Carlo runs: {mc_runs} |
| 143 | + logger.info("The random seed used is: {RANDOM_SEED}""") |
| 144 | + is_robustness_weights = 1 |
| 145 | + |
| 146 | + if config.robustness.robustness == RobustnessAnalysis.INDICATORS.value and config.robustness.on_weights_level == RobustnessWightLevels.NONE.value: |
| 147 | + logger.info(f"""ProMCDA will consider uncertainty on the indicators. |
| 148 | + Number of Monte Carlo runs: {mc_runs} |
| 149 | + logger.info("The random seed used is: {RANDOM_SEED}""") |
| 150 | + is_robustness_indicators = 1 |
| 151 | + |
| 152 | + marginal_pdf = config.monte_carlo_sampling.marginal_distributions |
| 153 | + logger.info("Read input matrix with uncertainty of the indicators!") |
| 154 | + return f_agg, f_norm, is_robustness_indicators, is_robustness_weights, marginal_pdf |
| 155 | + |
| 156 | + |
| 157 | +def read_matrix_from_file(column_names_list: list[str], file_from_stream) -> {}: |
| 158 | + result_dict = utils_for_main.read_matrix_from_file(file_from_stream).to_dict() |
| 159 | + if len(column_names_list) != len(result_dict.keys()): |
| 160 | + return {"error": "Number of provided column names does not match the CSV columns"}, 400 |
| 161 | + return {col: result_dict[col] for col in column_names_list if col in result_dict} |
| 162 | + |
| 163 | + |
| 164 | +if __name__ == '__main__': |
| 165 | + t = time.time() |
| 166 | + config_path = parse_args() |
| 167 | + input_config = get_config(config_path) |
| 168 | + main_using_model(input_config) |
| 169 | + elapsed = time.time() - t |
| 170 | + logger.info("All calculations finished in seconds {}".format(elapsed)) |
0 commit comments