Repository: DARMA-tasking/LB-analysis-framework

Issue:
Replace hard-coded 1.0 with actual local alpha values for the computation of max arrangement work statistics
## Description of the issue:
As reported by @cwschilly, currently in `LBAF_app`, the `compute_min_max_arrangements_work()` is called with all `alpha` values set to 1.

## How to fix the issue:
This must be replaced with the use of the proper rank-local `alpha` values when available

Discussion:

Patch:
diff --git a/config/conf.yaml b/config/conf.yaml
index b0e9cc9..8b823ac 100644
--- a/config/conf.yaml
+++ b/config/conf.yaml
@@ -13,7 +13,6 @@ work_model:
     gamma: 0.0
 
 # Specify algorithm
-brute_force_optimization: true
 algorithm:
   name: InformAndTransfer
   phase_id: 0
diff --git a/config/synthetic-blocks.yaml b/config/synthetic-blocks.yaml
index 38528cc..fac6b84 100644
--- a/config/synthetic-blocks.yaml
+++ b/config/synthetic-blocks.yaml
@@ -16,7 +16,6 @@ work_model:
       max_memory_usage: 45.0
 
 # Specify algorithm
-brute_force_optimization: true
 algorithm:
   name: InformAndTransfer
   phase_id: 0
diff --git a/docs/pages/configuration.rst b/docs/pages/configuration.rst
index 5f0e13f..b0f38f2 100644
--- a/docs/pages/configuration.rst
+++ b/docs/pages/configuration.rst
@@ -78,7 +78,6 @@ Example configuration
         gamma: 0.
 
     # Specify balancing algorithm
-    #brute_force_optimization: True
     algorithm:
     #  name: BruteForce
       name: InformAndTransfer
diff --git a/src/lbaf/Applications/LBAF_app.py b/src/lbaf/Applications/LBAF_app.py
index 52a2813..528a36c 100644
--- a/src/lbaf/Applications/LBAF_app.py
+++ b/src/lbaf/Applications/LBAF_app.py
@@ -552,19 +552,6 @@ class LBAFApplication:
         initial_phase = phases[min(phases.keys())]
         self.__print_statistics(initial_phase, "initial")
 
-        # Perform brute force optimization when needed
-        if ("brute_force_optimization" in self.__parameters.__dict__
-                and self.__parameters.algorithm["name"] != "BruteForce"):
-            self.__logger.info("Starting brute force optimization")
-            objects = initial_phase.get_objects()
-            beta, gamma = [
-                self.__parameters.work_model.get("parameters", {}).get(k)
-                for k in ("beta", "gamma")]
-            _n_a, _w_min_max, a_min_max = lbstats.compute_min_max_arrangements_work(
-                objects, 1.0, beta, gamma, n_ranks, logger=self.__logger)
-        else:
-            a_min_max = []
-
         # Instantiate runtime
         if self.__parameters.ranks_per_node > 1 and (
                 wmp := self.__parameters.work_model.get("parameters")):
@@ -573,7 +560,6 @@ class LBAFApplication:
             phases,
             self.__parameters.work_model,
             self.__parameters.algorithm,
-            a_min_max,
             self.__logger)
 
         # Execute runtime for specified phases
diff --git a/src/lbaf/Execution/lbsAlgorithmBase.py b/src/lbaf/Execution/lbsAlgorithmBase.py
index 4689084..d7a1919 100644
--- a/src/lbaf/Execution/lbsAlgorithmBase.py
+++ b/src/lbaf/Execution/lbsAlgorithmBase.py
@@ -200,11 +200,10 @@ class AlgorithmBase:
         self._update_statistics(statistics)
 
     @abc.abstractmethod
-    def execute(self, p_id, phases, statistics, a_min_max):
+    def execute(self, p_id, phases, statistics):
         """Execute balancing algorithm on Phase instance.
 
         :param: p_id: index of phase to be rebalanced (all if equal to _)
         :param: phases: list of Phase instances
         :param: statistics: dictionary of  statistics
-        :param: a_min_max: possibly empty list of optimal arrangements.
         """
diff --git a/src/lbaf/Execution/lbsBruteForceAlgorithm.py b/src/lbaf/Execution/lbsBruteForceAlgorithm.py
index e9cfa77..fff32a9 100644
--- a/src/lbaf/Execution/lbsBruteForceAlgorithm.py
+++ b/src/lbaf/Execution/lbsBruteForceAlgorithm.py
@@ -65,7 +65,7 @@ class BruteForceAlgorithm(AlgorithmBase):
         self._logger.info(
             f"Instantiated {'with' if self.__skip_transfer else 'without'} transfer stage skipping")
 
-    def execute(self, p_id: int, phases: list, statistics: dict, _):
+    def execute(self, p_id: int, phases: list, statistics: dict):
         """Execute brute force optimization algorithm on phase with index p_id."""
         # Perform pre-execution checks and initializations
         self._initialize(p_id, phases, statistics)
@@ -80,8 +80,9 @@ class BruteForceAlgorithm(AlgorithmBase):
             self._work_model.get_alpha() if affine_combination else 1.0,
             self._work_model.get_beta() if affine_combination else 0.0,
             self._work_model.get_gamma() if affine_combination else 0.0]
-        _n_a, _w_min_max, a_min_max = compute_min_max_arrangements_work(objects, alpha, beta, gamma, n_ranks,
-                                                                        logger=self._logger)
+        _n_a, _w_min_max, a_min_max = compute_min_max_arrangements_work(
+            objects, alpha, beta, gamma, n_ranks,
+            logger=self._logger)
 
         # Skip object transfers when requested
         if self.__skip_transfer:
diff --git a/src/lbaf/Execution/lbsCentralizedPrefixOptimizerAlgorithm.py b/src/lbaf/Execution/lbsCentralizedPrefixOptimizerAlgorithm.py
index 42c3617..7fb6437 100644
--- a/src/lbaf/Execution/lbsCentralizedPrefixOptimizerAlgorithm.py
+++ b/src/lbaf/Execution/lbsCentralizedPrefixOptimizerAlgorithm.py
@@ -62,7 +62,7 @@ class CentralizedPrefixOptimizerAlgorithm(AlgorithmBase):
         self._phase = None
         self._max_shared_ids = None
 
-    def execute(self, p_id: int, phases: list, statistics: dict, _):
+    def execute(self, p_id: int, phases: list, statistics: dict):
         """ Execute centralized prefix memory-constrained optimizer"""
 
         p_id = 0
diff --git a/src/lbaf/Execution/lbsCriterionBase.py b/src/lbaf/Execution/lbsCriterionBase.py
index 49ff5c0..1d245bd 100644
--- a/src/lbaf/Execution/lbsCriterionBase.py
+++ b/src/lbaf/Execution/lbsCriterionBase.py
@@ -114,14 +114,3 @@ class CriterionBase:
         :param o_dst: optional iterable of objects on destination for swaps.
         """
         # Must be implemented by concrete subclass
-
-    @abc.abstractmethod
-    def estimate(self, r_src, o_src, r_dst_id, o_dst: Optional[List]=None):
-        """Estimate value of criterion for candidate objects transfer
-
-        :param r_src: iterable of objects on source
-        :param o_src: Rank instance
-        :param r_dst_id: Rank instance ID
-        :param o_dst: optional iterable of objects on destination for swaps.
-        """
-        # Must be implemented by concrete subclass
diff --git a/src/lbaf/Execution/lbsInformAndTransferAlgorithm.py b/src/lbaf/Execution/lbsInformAndTransferAlgorithm.py
index 2355372..bbca68e 100644
--- a/src/lbaf/Execution/lbsInformAndTransferAlgorithm.py
+++ b/src/lbaf/Execution/lbsInformAndTransferAlgorithm.py
@@ -215,7 +215,7 @@ class InformAndTransferAlgorithm(AlgorithmBase):
         self._logger.info(
             f"Average number of peers known to ranks: {n_k} ({100 * n_k / n_r:.2f}% of {n_r})")
 
-    def execute(self, p_id: int, phases: list, statistics: dict, a_min_max):
+    def execute(self, p_id: int, phases: list, statistics: dict):
         """ Execute 2-phase information+transfer algorithm on Phase with index p_id."""
         # Perform pre-execution checks and initializations
         self._initialize(p_id, phases, statistics)
@@ -272,21 +272,6 @@ class InformAndTransferAlgorithm(AlgorithmBase):
             lb_iteration.set_communications(self._initial_communications[p_id])
             self._initial_phase.get_lb_iterations().append(lb_iteration)
 
-            # Report minimum Hamming distance when minimax optimum is available
-            if a_min_max:
-                # Compute current arrangement
-                arrangement = dict(sorted(
-                    {o.get_id(): p.get_id()
-                     for p in self._rebalanced_phase.get_ranks()
-                     for o in p.get_objects()}.items())).values()
-                self._logger.debug(f"Iteration {i + 1} arrangement: {tuple(arrangement)}")
-
-                # Compute minimum distance from arrangement to optimum
-                hd_min = min_Hamming_distance(arrangement, a_min_max)
-                self._logger.info(
-                    f"Iteration {i + 1} minimum Hamming distance to optimal arrangements: {hd_min}")
-                statistics["minimum Hamming distance to optimum"].append(hd_min)
-
             # Check if the current imbalance is within the target_imbalance range
             if stats.statistics["imbalance"] <= self.__target_imbalance:
                 self._logger.info(
diff --git a/src/lbaf/Execution/lbsPhaseStepperAlgorithm.py b/src/lbaf/Execution/lbsPhaseStepperAlgorithm.py
index ae0e7dc..332081f 100644
--- a/src/lbaf/Execution/lbsPhaseStepperAlgorithm.py
+++ b/src/lbaf/Execution/lbsPhaseStepperAlgorithm.py
@@ -58,7 +58,7 @@ class PhaseStepperAlgorithm(AlgorithmBase):
         # Call superclass init
         super().__init__(work_model, parameters, lgr)
 
-    def execute(self, _, phases: list, statistics: dict, __):
+    def execute(self, _, phases: list, statistics: dict):
         """Steps through all phases."""
 
         # Ensure that a list with at least one phase was provided
diff --git a/src/lbaf/Execution/lbsPrescribedPermutationAlgorithm.py b/src/lbaf/Execution/lbsPrescribedPermutationAlgorithm.py
index bf95721..24fd142 100644
--- a/src/lbaf/Execution/lbsPrescribedPermutationAlgorithm.py
+++ b/src/lbaf/Execution/lbsPrescribedPermutationAlgorithm.py
@@ -27,7 +27,7 @@ class PrescribedPermutationAlgorithm(AlgorithmBase):
             self._logger.error(f"Incorrect prescribed permutation: {self.__permutation}")
             raise SystemExit(1)
 
-    def execute(self, p_id: int, phases: list, statistics: dict, a_min_max):
+    def execute(self, p_id: int, phases: list, statistics: dict):
         """ Apply prescribed permutation to phase objects."""
         # Perform pre-execution checks and initializations
         self._initialize(p_id, phases, statistics)
diff --git a/src/lbaf/Execution/lbsRuntime.py b/src/lbaf/Execution/lbsRuntime.py
index 617a330..d20b60a 100644
--- a/src/lbaf/Execution/lbsRuntime.py
+++ b/src/lbaf/Execution/lbsRuntime.py
@@ -55,24 +55,17 @@ class Runtime:
             phases: dict,
             work_model: dict,
             algorithm: dict,
-            arrangements: list,
             logger: Logger):
         """Class constructor.
 
         :param phases: dictionary of Phase instances
         :param work_model: dictionary with work model name and optional parameters
         :param algorithm: dictionary with algorithm name and parameters
-        :param arrangements: arrangements that minimize maximum work
         :param logger: logger for output messages
         """
         # Assign logger to instance variable
         self.__logger = logger
 
-        # Keep track of possibly empty list of arrangements with minimax work
-        self.__logger.info(
-            f"Instantiating runtime with {len(arrangements)} optimal arrangements for Hamming distance")
-        self.__a_min_max = arrangements
-
         # If no LBS phase was provided, do not do anything
         if not phases or not isinstance(phases, dict):
             self.__logger.error(
@@ -104,19 +97,6 @@ class Runtime:
             lambda x: x.get_load())
         self.__statistics = {"average load": l_stats.get_average()}
 
-        # Compute initial arrangement
-        arrangement = dict(sorted(
-            {o.get_id(): p.get_id()
-             for p in phase_0.get_ranks()
-             for o in p.get_objects()}.items())).values()
-        self.__logger.debug(f"Initial arrangement: {tuple(arrangement)}")
-
-        # Report minimum Hamming distance when minimax optimum is available
-        if self.__a_min_max:
-            hd_min = min_Hamming_distance(arrangement, self.__a_min_max)
-            self.__statistics["minimum Hamming distance to optimum"] = [hd_min]
-            self.__logger.info(f"Phase 0 minimum Hamming distance to optimal arrangements: {hd_min}")
-
     def get_work_model(self):
         """Return runtime work model."""
         return self.__work_model
@@ -130,8 +110,7 @@ class Runtime:
         self.__algorithm.execute(
             p_id,
             self.__phases,
-            self.__statistics,
-            self.__a_min_max)
+            self.__statistics)
 
         # Retrieve possibly null rebalanced phase and return it
         if (lbp := self.__algorithm.get_rebalanced_phase()):
diff --git a/src/lbaf/Execution/lbsStrictLocalizingCriterion.py b/src/lbaf/Execution/lbsStrictLocalizingCriterion.py
index 8f06d57..ec949dc 100644
--- a/src/lbaf/Execution/lbsStrictLocalizingCriterion.py
+++ b/src/lbaf/Execution/lbsStrictLocalizingCriterion.py
@@ -85,7 +85,3 @@ class StrictLocalizingCriterion(CriterionBase):
 
         # Accept transfer if this point was reached as no locality was broken
         return 1.
-
-    def estimate(self, r_src: Rank, o_src: list, *args) -> float:
-        """Estimate is compute because all information is local for this criterion."""
-        return self.compute(r_src, o_src, *args)
diff --git a/src/lbaf/IO/lbsConfigurationValidator.py b/src/lbaf/IO/lbsConfigurationValidator.py
index 3817751..78c5ad4 100644
--- a/src/lbaf/IO/lbsConfigurationValidator.py
+++ b/src/lbaf/IO/lbsConfigurationValidator.py
@@ -103,7 +103,6 @@ class ConfigurationValidator:
                 Optional("phase_id"): int,
                 Optional("parameters"): dict},
            "output_file_stem": str,
-            Optional("brute_force_optimization"): bool,
             Optional("overwrite_validator"): bool,
             Optional("check_schema"): bool,
             Optional("log_to_file"): str,
@@ -253,7 +252,7 @@ class ConfigurationValidator:
         sections = {
             "input": ["from_data", "from_samplers", "check_schema"],
             "work model": ["work_model"],
-            "algorithm": ["brute_force_optimization", "algorithm"],
+            "algorithm": ["algorithm"],
             "output": [
                 "logging_level", "log_to_file", "overwrite_validator", "terminal_background",
                 "generate_multimedia", "output_dir", "output_file_stem",


Test patch:
diff --git a/docs/pages/testing.rst b/docs/pages/testing.rst
index 4a290fd..70de152 100644
--- a/docs/pages/testing.rst
+++ b/docs/pages/testing.rst
@@ -72,7 +72,6 @@ Synthetic Blocks Test Configuration
       gamma: 0.
 
   # Specify balancing algorithm
-  brute_force_optimization: True
   algorithm:
     name: InformAndTransfer
     parameters:
diff --git a/tests/acceptance/test_synthetic_blocks.py b/tests/acceptance/test_synthetic_blocks.py
index a9c37b6..c635b89 100644
--- a/tests/acceptance/test_synthetic_blocks.py
+++ b/tests/acceptance/test_synthetic_blocks.py
@@ -41,7 +41,6 @@ class TestSyntheticBlocksLB(unittest.TestCase):
                     }
                 }
             },
-            "brute_force_optimization": False,
             "algorithm": {
                 "name": "InformAndTransfer",
                 "phase_id": 0,
diff --git a/tests/unit/Execution/test_lbs_centralized_prefix_optimizer_algorithm.py b/tests/unit/Execution/test_lbs_centralized_prefix_optimizer_algorithm.py
index d8af946..709e163 100644
--- a/tests/unit/Execution/test_lbs_centralized_prefix_optimizer_algorithm.py
+++ b/tests/unit/Execution/test_lbs_centralized_prefix_optimizer_algorithm.py
@@ -98,15 +98,11 @@ class TestConfig(unittest.TestCase):
         self.cpoa.execute(
             self.phase.get_id(),
             self.phases,
-            self.statistics,
-            1
-        )
+            self.statistics)
         new_phase = self.cpoa.get_rebalanced_phase()
         self.assertEqual(
             new_phase.get_id(),
-            self.phase.get_id()
-        )
-
+            self.phase.get_id())
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/unit/Execution/test_lbs_runtime.py b/tests/unit/Execution/test_lbs_runtime.py
index 265e197..1ace51b 100644
--- a/tests/unit/Execution/test_lbs_runtime.py
+++ b/tests/unit/Execution/test_lbs_runtime.py
@@ -90,14 +90,12 @@ class TestConfig(unittest.TestCase):
         beta = 1.0
         gamma = 0.0
         n_ranks = 4
-        self.arrangements = compute_min_max_arrangements_work(objects, 0.0, beta, gamma,
-                                                              n_ranks, logger=self.logger)[2]
+
         # Initialize the Runtime instances
         self.runtime = Runtime(
             self.phases,
             self.work_model,
             self.algorithm,
-            self.arrangements,
             self.logger)
 
     def test_lbs_runtime_get_work_model(self):
@@ -109,7 +107,6 @@ class TestConfig(unittest.TestCase):
                 None,
                 self.work_model,
                 self.algorithm,
-                self.arrangements,
                 self.logger)
         self.assertEqual(context.exception.code, 1)
 
diff --git a/tests/unit/config/conf_correct_003.yml b/tests/unit/config/conf_correct_003.yml
index 9c8be60..7204b63 100644
--- a/tests/unit/config/conf_correct_003.yml
+++ b/tests/unit/config/conf_correct_003.yml
@@ -12,7 +12,6 @@ work_model:
     gamma: 0.0
 
 # Specify algorithm
-brute_force_optimization: true
 algorithm:
   name: InformAndTransfer
   phase_id: 0
diff --git a/tests/unit/config/conf_correct_brute_force.yml b/tests/unit/config/conf_correct_brute_force.yml
index ec363ad..5b26fa7 100644
--- a/tests/unit/config/conf_correct_brute_force.yml
+++ b/tests/unit/config/conf_correct_brute_force.yml
@@ -12,7 +12,6 @@ work_model:
     gamma: 0.0
 
 # Specify algorithm
-brute_force_optimization: true
 algorithm:
   name: BruteForce
   phase_id: 0