Repository: DARMA-tasking/LB-analysis-framework

Issue:
Refactor distributed information stage 
This issue is motivated by that fact that, over time, implicit assumptions have crept in regarding what's know/unknown by the sender regarding the destination rank. 

The information stage must be refactored to be fully distributed again (as it was when load-only information was contained)

Discussion:
cwschilly: @ppebay I fixed the PR so it passes the CI tests; is this ready to be taken out of draft?
ppebay: Thanks @cwschilly for fixing the CI on this PR

It's not ready yet as I still have to complete the implementation of the distributed information usage for issue #323 

But please continue monitoring this issue as subsequent pushes might continue to break the CI. 
ppebay: @cwschilly your CI-fixing commit is good. Please consider adding those tests removed from `lbsRank` testing, to `lbsInformAndTransferAlgorithm` testing (as these methods have been moved there).
cwschilly: @ppebay There doesn't seem to be any unit testing done on `lbsInformandTransferAlgorithm`--does a new test file need to be written?
ppebay: @cwschilly handing this over to you, for the remaining Tox/CI testing errors (some updates are needed). Thanks!
ppebay: Example with user-defined memory toy problem:

![Screen Shot 2023-06-11 at 2 30 21 PM](https://github.com/DARMA-tasking/LB-analysis-framework/assets/17439260/c0bae4c8-f7df-41f2-bbc4-87dc6b33a414)

![Screen Shot 2023-06-11 at 2 30 36 PM](https://github.com/DARMA-tasking/LB-analysis-framework/assets/17439260/e0d78fba-3552-4383-b429-c7e08265828f)

ppebay: Conflicts due to mergers with PR #400 & #402 now resolved.

@cwschilly can you please have a look at the code quality CI test errors in this PR? Thanks
cwschilly: > Conflicts due to mergers with PR #400 & #402 now resolved.
> 
> @cwschilly can you please have a look at the code quality CI test errors in this PR? Thanks

Yes I'll figure these out ASAP
cwschilly: @ppebay The code now passes all CI tests apart from commit formatting on "[Resolved merge conflicts](https://github.com/DARMA-tasking/LB-analysis-framework/pull/390/commits/4f8330b920c6bc4254e820c333805b19ba338ec1)"  
lifflander: This looks great to me. We still need to actually simulate the locking. It looks like this mostly just adjusts the peers so that they are based on the known PEs during the information stage.

Patch:
diff --git a/config/challenging-toy-fewer-tasks.yaml b/config/challenging-toy-fewer-tasks.yaml
new file mode 100644
index 0000000..32b80c9
--- /dev/null
+++ b/config/challenging-toy-fewer-tasks.yaml
@@ -0,0 +1,43 @@
+# Specify input
+from_data:
+  data_stem: "../data/challenging_toy_fewer_tasks/toy"
+  phase_ids:
+    - 0
+check_schema: true
+
+# Specify work model
+work_model:
+  name: AffineCombination
+  parameters:
+    alpha: 1.
+    beta: 0.
+    gamma: 0.
+    upper_bounds:
+      max_memory_usage: 8.0e+9
+
+# Specify balancing algorithm
+algorithm:
+  name: InformAndTransfer
+  phase_id: 0
+  parameters:
+    n_iterations: 8
+    n_rounds: 4
+    fanout: 4
+    order_strategy: arbitrary
+    transfer_strategy: Clustering
+    criterion: Tempered
+    max_objects_per_transfer: 100
+    deterministic_transfer: True
+
+# Specify output
+output_dir: ../output
+output_file_stem: output_file
+LBAF_Viz:
+  x_ranks: 4
+  y_ranks: 4
+  z_ranks: 1
+  object_jitter: 0.5
+  rank_qoi: homed_blocks_ratio
+  #rank_qoi: max_memory_usage
+  object_qoi: shared_block_id
+  save_meshes: False
diff --git a/config/conf.yaml b/config/conf.yaml
index 43f549b..2d74811 100644
--- a/config/conf.yaml
+++ b/config/conf.yaml
@@ -1,59 +1,3 @@
-# Docs
-# parameter_name [type]        description
-# ---------------------------  -----------------------------------------------------------------------
-# from_data:
-#   data_stem [str]            base file name of VT load logs
-#   phase_ids [list or str]    list of ids of phase to be read in VT load logs e.g. [1, 2, 3] or "1-3"
-# from_samplers [dict]
-#   n_objects [int]            number of objects
-#   n_mapped_ranks [int]       number of initially mapped processors
-#   communication_degree [int] object communication degree (no communication if 0)
-#   load_sampler               description of object load sampler:
-#     name [str]               in uniform, lognormal
-#     parameters [list]        parameters e.g. 1.0,10.0 for lognormal
-#   volume_sampler             description of object communication volumes sampler:
-#     name [str]               in uniform, lognormal
-#     parameters [list]        parameters e.g. 1.0,10.0 for lognormal
-# check_schema  [bool]         validates that configuration is valid with the configuration schema
-# work_model [dict]            work model to be used
-#   name [str]                 in LoadOnly, AffineCombination
-#   parameters [dict]          optional parameters specific to each work model
-# algorithm [dict]             balancing algorithm to be used
-#   name [str]                 in InformAndTransfer, BruteForce, PhaseStepper
-#   parameters [dict]          parameters specific to each algorithm:
-#     InformAndtransfer [dict] InformAndtransfer algorithm parameters
-#       criterion [str]        in Tempered (default), StrictLocalizer
-#       n_iterations [int]     number of load-balancing iterations
-#       deterministic_transfer [bool] for deterministic transfer (default: False)
-#       n_rounds [int]         number of information rounds
-#       fanout [int]           information fanout index
-#       order_strategy [str]   ordering of objects for transfer
-#                              in arbitrary (default), element_id, increasing_times
-#                              decreasing_times, increasing_connectivity,
-#                              fewest_migrations, small_objects
-#      BruteForce [dict]       BruteForce algorithm parameters
-#        skip_transfer [bool]  skip transfer phase (default: False)
-#      PhaseStepper [dict]     PhaseStepper algorithm parameters
-# logging_level [str]          set to `info`, `debug`, `warning` or `error`
-# log_to_file [str]            filepath to save the log file (optional)
-# output_dir [str]             output directory (default: '.')
-# LBAF_Viz [dict]              Visualization parameters (optional)
-#   x_ranks [int]              number of ranks in x direction for rank visualization
-#   y_ranks [int]              number of ranks in y direction for rank visualization
-#   z_ranks [int]              number of ranks in z direction for rank visualization
-#   object_jitter [float]      coefficient of random jitter with magnitude < 1
-#   rank_qoi [str]             in load, work, None
-#   object_qoi [str]           in load, work, None
-#   save_meshes [bool]         generate mesh outputs (default: False)
-#   force_continuous_object_qoi [bool] always treat object QOI as continuous or not
-# file_suffix [str]            file suffix of VT data files (default: "json")
-# communication_degree [int]   object communication degree (no communication if 0)
-# write_JSON                   write load directives for VT as JSON files
-#   compressed [bool]          compress json files using brotli
-#   suffix [str]               suffix for generates files. (default: "json")
-#   communications [bool]      use communications (default: False)
-#   offline_LB_compatible [bool] (default: False)
-
 # Specify input
 from_data:
   data_stem: ../data/synthetic_lb_data/data
@@ -75,13 +19,13 @@ algorithm:
   phase_id: 0
   parameters:
     n_iterations: 8
-    n_rounds: 4
-    fanout: 4
+    n_rounds: 2
+    fanout: 2
     order_strategy: arbitrary
     transfer_strategy: Recursive
     criterion: Tempered
     max_objects_per_transfer: 8
-    deterministic_transfer: True
+    deterministic_transfer: False
 
 # Specify output
 output_dir: ../output
diff --git a/config/user-defined-memory-toy-problem.yaml b/config/user-defined-memory-toy-problem.yaml
index 109c432..50873f8 100644
--- a/config/user-defined-memory-toy-problem.yaml
+++ b/config/user-defined-memory-toy-problem.yaml
@@ -3,7 +3,7 @@ from_data:
   data_stem: ../data/user-defined-memory-toy-problem/toy_mem
   phase_ids:
     - 0
-check_schema: True
+check_schema: False
 
 # Specify work model
 work_model:
@@ -21,13 +21,13 @@ algorithm:
   phase_id: 0
   parameters:
     n_iterations: 4
-    n_rounds: 4
-    fanout: 4
+    n_rounds: 2
+    fanout: 2
     order_strategy: arbitrary
     transfer_strategy: Clustering
     criterion: Tempered
     max_objects_per_transfer: 32
-    deterministic_transfer: True
+    deterministic_transfer: False
 
 # Specify output
 output_dir: ../output
diff --git a/src/lbaf/Execution/lbsClusteringTransferStrategy.py b/src/lbaf/Execution/lbsClusteringTransferStrategy.py
index 8cd2e99..d48af3a 100644
--- a/src/lbaf/Execution/lbsClusteringTransferStrategy.py
+++ b/src/lbaf/Execution/lbsClusteringTransferStrategy.py
@@ -85,7 +85,7 @@ class ClusteringTransferStrategy(TransferStrategyBase):
             f"Found {len(suitable_subclusters)} suitable subclusters amongst {n_inspect} inspected")
         return sorted(suitable_subclusters.keys(), key=suitable_subclusters.get)
 
-    def execute(self, phase: Phase, ave_load: float):
+    def execute(self, known_peers, phase: Phase, ave_load: float):
         """Perform object transfer stage."""
         # Initialize transfer stage
         self.__average_load = ave_load
@@ -95,7 +95,7 @@ class ClusteringTransferStrategy(TransferStrategyBase):
         # Iterate over ranks
         for r_src in phase.get_ranks():
             # Retrieve potential targets
-            targets = r_src.get_targets()
+            targets = known_peers.get(r_src, set()).difference({r_src})
             if not targets:
                 n_ignored += 1
                 continue
@@ -110,7 +110,7 @@ class ClusteringTransferStrategy(TransferStrategyBase):
             n_swaps = 0
             for o_src in clusters_src.values():
                 swapped_cluster = False
-                for r_try in targets.keys():
+                for r_try in targets:
                     # Iterate over target clusters
                     for o_try in self.__cluster_objects(r_try).values():
                         # Decide whether swap is beneficial
@@ -148,7 +148,7 @@ class ClusteringTransferStrategy(TransferStrategyBase):
                     l_dst = math.inf
 
                     # Select best destination with respect to criterion
-                    for r_try in targets.keys():
+                    for r_try in targets:
                         c_try = self._criterion.compute(
                             r_src, o_src, r_try)
                         if c_try <= 0.0:
@@ -163,8 +163,8 @@ class ClusteringTransferStrategy(TransferStrategyBase):
                             r_dst = r_try
                 else:
                     # Compute transfer CMF given information known to source
-                    p_cmf, c_values = r_src.compute_transfer_cmf(
-                        self._criterion, o_src, targets, False)
+                    p_cmf, c_values = self._compute_transfer_cmf(
+                        r_src, o_src, targets, False)
                     self._logger.debug(f"CMF = {p_cmf}")
                     if not p_cmf:
                         n_rejects += 1
diff --git a/src/lbaf/Execution/lbsCriterionBase.py b/src/lbaf/Execution/lbsCriterionBase.py
index df3c3f8..829abbb 100644
--- a/src/lbaf/Execution/lbsCriterionBase.py
+++ b/src/lbaf/Execution/lbsCriterionBase.py
@@ -63,8 +63,8 @@ class CriterionBase:
             raise SystemExit(1) from e
 
     @abc.abstractmethod
-    def compute(self, r_src, o_src, r_dst, o_dst: Optional[List]=None):
-        """Return value of criterion for candidate objects transfer
+    def compute(self, r_src, o_src, r_dst, o_dst: Optional[List]=[]):
+        """Compute value of criterion for candidate objects transfer
 
         :param r_src: iterable of objects on source
         :param o_src: Rank instance
@@ -72,7 +72,17 @@ class CriterionBase:
         :param o_dst: optional iterable of objects on destination for swaps.
         """
 
-        if o_dst is None:
-            o_dst = []
+        # Must be implemented by concrete subclass
+        pass
+
+    @abc.abstractmethod
+    def estimate(self, r_src, o_src, r_dst_id, o_dst: Optional[List]=[]):
+        """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 b61363d..c854aca 100644
--- a/src/lbaf/Execution/lbsInformAndTransferAlgorithm.py
+++ b/src/lbaf/Execution/lbsInformAndTransferAlgorithm.py
@@ -1,9 +1,13 @@
+import random
 from logging import Logger
 
 from ..IO.lbsStatistics import min_Hamming_distance, print_function_statistics
 from .lbsAlgorithmBase import AlgorithmBase
 from .lbsCriterionBase import CriterionBase
 from .lbsTransferStrategyBase import TransferStrategyBase
+from ..Model.lbsRank import Rank
+from ..Model.lbsMessage import Message
+from ..IO.lbsStatistics import print_function_statistics, min_Hamming_distance
 
 
 class InformAndTransferAlgorithm(AlgorithmBase):
@@ -64,72 +68,114 @@ class InformAndTransferAlgorithm(AlgorithmBase):
             self._logger.error(f"Could not instantiate a transfer strategy of type {strat_name}")
             raise SystemExit(1)
 
-    def __information_stage(self):
+        # No information about peers is known initially
+        self.__known_peers = {}
+
+    def get_known_peers(self):
+        """Return all known peers."""
+        return self.__known_peers
+
+    def __process_message(self, r_rcv: Rank, m: Message):
+        """Process message received by rank."""
+        # Make rank aware of itself
+        if r_rcv not in self.__known_peers:
+            self.__known_peers[r_rcv] = {r_rcv}
+
+        # Process the message
+        self.__known_peers[r_rcv].update(m.get_support())
+
+    def __forward_message(self, i: int, r_snd: Rank, f:int):
+        """Forward information message to rank peers sampled from known ones."""
+        # Make rank aware of itself
+        if r_snd not in self.__known_peers:
+            self.__known_peers[r_snd] = {r_snd}
+
+        # Create load message tagged at given information round
+        msg = Message(i, self.__known_peers[r_snd])
+
+        # Compute complement of set of known peers
+        complement = self.__known_peers[r_snd].difference({r_snd})
+
+        # Forward message to pseudo-random sample of ranks
+        return random.sample(
+            list(complement), min(f, len(complement))), msg
+
+    def __execute_information_stage(self):
         """Execute information stage."""
         # Build set of all ranks in the phase
         rank_set = set(self._rebalanced_phase.get_ranks())
 
-        # Initialize information messages
+        # Initialize information messages and known peers
+        messages, self.__known_peers = {}, {}
+        n_r = len(rank_set)
+        for r_snd in rank_set:
+            # Make rank aware of itself
+            self.__known_peers[r_snd] = {r_snd}
+
+            # Create initial message spawned from rank
+            msg = Message(0, {r_snd})
+
+            # Broadcast message to random sample of ranks excluding self
+            for r_rcv in random.sample(
+                list(rank_set.difference({r_snd})), min(self.__fanout, n_r - 1)):
+                messages.setdefault(r_rcv, []).append(msg)
+
+        # Sanity check prior to forwarding iterations
+        if (n_m := sum([len(m) for m in messages.values()])) != (n_c := n_r * self.__fanout):
+            self._logger.error(
+                f"Incorrect number of initial messages: {n_m} <> {n_c}")
         self._logger.info(
-            f"Initializing information messages with fanout={self.__fanout}")
-        information_round = 1
-        messages = {}
-
-        # Iterate over all ranks
-        for p_snd in rank_set:
-            # Reset load information known by sender
-            p_snd.reset_all_load_information()
-
-            # Collect message when destination list is not empty
-            dst, msg = p_snd.initialize_message(rank_set, self.__fanout)
-            for p_rcv in dst:
-                messages.setdefault(p_rcv, []).append(msg)
-
-        # Process all messages of first round
-        for p_rcv, msg_lst in messages.items():
-            for m in msg_lst:
-                p_rcv.process_message(m)
-
-        # Report on gossiping status when requested
-        for p in rank_set:
-            self._logger.debug(f"information known to rank {p.get_id()}: "
-                                f"{[p_u.get_id() for p_u in p.get_known_loads()]}")
+            f"Sent {n_m} initial information messages with fanout={self.__fanout}")
+
+        # Process all received initial messages
+        for r_rcv, m_rcv in messages.items():
+            for m in m_rcv:
+                # Process message by recipient
+                self.__process_message(r_rcv, m)
+
+        # Perform sanity check on first round of information aggregation
+        n_k = 0
+        for r in rank_set:
+            # Retrieve and tally peers known to rank
+            k_p = self.__known_peers.get(r, {})
+            n_k += len(k_p)
+            self._logger.debug(
+                f"Peers known to rank {r.get_id()}: {[r_k.get_id() for r_k in k_p]}")
+        if n_k != (n_c := n_c + n_r):
+            self._logger.error(
+                f"Incorrect total number of aggregated initial known peers: {n_k} <> {n_c}")
 
         # Forward messages for as long as necessary and requested
-        while information_round < self.__n_rounds:
-            # Initiate next gossiping round
-            self._logger.debug(f"Performing message forwarding round {information_round}")
-            information_round += 1
+        for i in range(1, self.__n_rounds):
+            # Initiate next information round
+            self._logger.debug(f"Performing message forwarding round {i}")
             messages.clear()
 
             # Iterate over all ranks
-            for p_snd in rank_set:
-                # Check whether rank must relay previously received message
-                if p_snd.round_last_received + 1 == information_round:
-                    # Collect message when destination list is not empty
-                    dst, msg = p_snd.forward_message(information_round, rank_set, self.__fanout)
-                    for p_rcv in dst:
-                        messages.setdefault(p_rcv, []).append(msg)
+            for r_snd in rank_set:
+                # Collect message when destination list is not empty
+                dst, msg = self.__forward_message(
+                    i, r_snd, self.__fanout)
+                for r_rcv in dst:
+                    messages.setdefault(r_rcv, []).append(msg)
 
             # Process all messages of first round
-            for p_rcv, msg_lst in messages.items():
-                for msg in msg_lst:
-                    p_rcv.process_message(msg)
+            for r_rcv, msg_lst in messages.items():
+                for m in msg_lst:
+                    self.__process_message(r_rcv, m)
 
-            # Report on gossiping status when requested
+            # Report on known peers when requested
             for rank in rank_set:
                 self._logger.debug(
-                    f"information known to rank {rank.get_id()}: "
-                    f"{[p_u.get_id() for p_u in rank.get_known_loads()]}")
+                    f"Peers known to rank {r.get_id()}: {[r_k.get_id() for r_k in k_p]}")
 
-        # Build reverse lookup of ranks to those aware of them
-        for rank in rank_set:
-            # Skip non-loaded ranks
-            if not rank.get_load():
-                continue
+        # Report on final know information ratio
+        n_k = sum([len(k_p) for k_p in self.__known_peers.values() if k_p]) / n_r
+        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, distributions: dict, statistics: dict, a_min_max):
-        """ Execute 2-phase gossip+transfer algorithm on Phase with index p_id."""
+        """ Execute 2-phase information+transfer algorithm on Phase with index p_id."""
         # Perform pre-execution checks and initializations
         self._initialize(p_id, phases, distributions, statistics)
 
@@ -144,11 +190,11 @@ class InformAndTransferAlgorithm(AlgorithmBase):
             self._logger.info(f"Starting iteration {i + 1} with total work of {total_work}")
 
             # Start with information stage
-            self.__information_stage()
+            self.__execute_information_stage()
 
             # Then execute transfer stage
             n_ignored, n_transfers, n_rejects = self.__transfer_strategy.execute(
-                self._rebalanced_phase, statistics["average load"])
+                self.__known_peers, self._rebalanced_phase, statistics["average load"])
             n_proposed = n_transfers + n_rejects
             if n_proposed:
                 self._logger.info(
diff --git a/src/lbaf/Execution/lbsRecursiveTransferStrategy.py b/src/lbaf/Execution/lbsRecursiveTransferStrategy.py
index 18701c8..d7a2ac4 100644
--- a/src/lbaf/Execution/lbsRecursiveTransferStrategy.py
+++ b/src/lbaf/Execution/lbsRecursiveTransferStrategy.py
@@ -65,7 +65,7 @@ class RecursiveTransferStrategy(TransferStrategyBase):
             # Succeed when criterion is satisfied
             return True
 
-    def execute(self, phase: Phase, ave_load: float):
+    def execute(self, known_peers, phase: Phase, ave_load: float):
         """Perform object transfer stage."""
         # Initialize transfer stage
         self.__average_load = ave_load
@@ -75,11 +75,11 @@ class RecursiveTransferStrategy(TransferStrategyBase):
         # Iterate over ranks
         for r_src in phase.get_ranks():
             # Retrieve potential targets
-            targets = r_src.get_targets()
+            targets = known_peers.get(r_src, set()).difference({r_src})
             if not targets:
                 n_ignored += 1
                 continue
-            self._logger.debug(f"Trying to offload from rank {r_src.get_id()} to {[p.get_id() for p in targets]}:")
+            self._logger.debug(f"Trying to offload rank {r_src.get_id()} onto {[r.get_id() for r in targets]}:")
 
             # Offload objects for as long as necessary and possible
             srt_rank_obj = list(self.__order_strategy(
@@ -89,7 +89,7 @@ class RecursiveTransferStrategy(TransferStrategyBase):
                 # Pick next object in ordered list
                 o = srt_rank_obj.pop()
                 o_src = [o]
-                self._logger.debug(f"* object {o.get_id()}:")
+                self._logger.debug(f"\tobject {o.get_id()}:")
 
                 # Initialize destination information
                 r_dst = None
@@ -98,7 +98,7 @@ class RecursiveTransferStrategy(TransferStrategyBase):
                 # Use deterministic or probabilistic transfer method
                 if self._deterministic_transfer:
                     # Select best destination with respect to criterion
-                    for r_try in targets.keys():
+                    for r_try in targets:
                         c_try = self._criterion.compute(
                             r_src, o_src, r_try)
                         if c_try > c_dst:
@@ -106,8 +106,8 @@ class RecursiveTransferStrategy(TransferStrategyBase):
                             r_dst = r_try
                 else:
                     # Compute transfer CMF given information known to source
-                    p_cmf, c_values = r_src.compute_transfer_cmf(
-                        self._criterion, o_src, targets, False)
+                    p_cmf, c_values = self._compute_transfer_cmf(
+                        r_src, o_src, targets, False)
                     self._logger.debug(f"CMF = {p_cmf}")
                     if not p_cmf:
                         n_rejects += 1
diff --git a/src/lbaf/Execution/lbsStrictLocalizingCriterion.py b/src/lbaf/Execution/lbsStrictLocalizingCriterion.py
index cefb4da..9d4865a 100644
--- a/src/lbaf/Execution/lbsStrictLocalizingCriterion.py
+++ b/src/lbaf/Execution/lbsStrictLocalizingCriterion.py
@@ -43,3 +43,7 @@ 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/Execution/lbsTransferStrategyBase.py b/src/lbaf/Execution/lbsTransferStrategyBase.py
index 80a4a61..00aeb18 100644
--- a/src/lbaf/Execution/lbsTransferStrategyBase.py
+++ b/src/lbaf/Execution/lbsTransferStrategyBase.py
@@ -36,6 +36,47 @@ class TransferStrategyBase:
         logger.info(
             f"Created {'' if self._deterministic_transfer else 'non'}deterministic transfer strategy, max. {self._max_objects_per_transfer} objects")
 
+
+    def _compute_transfer_cmf(self, r_src, objects: list, targets: set, strict=False):
+        """Compute CMF for the sampling of transfer targets."""
+        # Initialize criterion values
+        c_values = {}
+        c_min, c_max = math.inf, -math.inf
+
+        # Iterate over potential targets
+        for r_dst in targets:
+            # Compute value of criterion for current target
+            c_dst = self._criterion.compute(r_src, objects, r_dst)
+
+            # Do not include rejected targets for strict CMF
+            if strict and c_dst < 0.:
+                continue
+
+            # Update criterion values
+            c_values[r_dst] = c_dst
+            if c_dst < c_min:
+                c_min = c_dst
+            if c_dst > c_max:
+                c_max = c_dst
+
+        # Initialize CMF depending on singleton or non-singleton support
+        if c_min == c_max:
+            # Sample uniformly if all criteria have same value
+            cmf = {k: 1.0 / len(c_values) for k in c_values.keys()}
+        else:
+            # Otherwise, use relative weights
+            c_range = c_max - c_min
+            cmf = {k: (v - c_min) / c_range for k, v in c_values.items()}
+
+        # Compute CMF
+        sum_p = 0.0
+        for k, v in cmf.items():
+            sum_p += v
+            cmf[k] = sum_p
+
+        # Return normalized CMF and criterion values
+        return {k: v / sum_p for k, v in cmf.items()}, c_values
+
     @staticmethod
     def factory(
             strategy_name: str,
@@ -60,10 +101,10 @@ class TransferStrategyBase:
             raise SystemExit(1) from error
 
     @abc.abstractmethod
-    def execute(self, phase, ave_load):
-        """Excecute transfer strategy on Phase instance
-
+    def execute(self, phase, known_peers: dict, ave_load: float):
+        """Execute transfer strategy on Phase instance
         :param phase: a Phase instance
+        :param known_peers: a dictionary of sets of known rank peers
         :param ave_load: average load in current phase.
         """
         # Must be implemented by concrete subclass
diff --git a/src/lbaf/Model/lbsMessage.py b/src/lbaf/Model/lbsMessage.py
index 0629449..30b3598 100644
--- a/src/lbaf/Model/lbsMessage.py
+++ b/src/lbaf/Model/lbsMessage.py
@@ -1,18 +1,18 @@
 class Message:
     """A class representing information sent between ranks."""
 
-    def __init__(self, r, c):
+    def __init__(self, r: int, s: set):
         # Member variables passed by constructor
         self.__round = r
-        self.__content = c
+        self.__support = s
 
     def __repr__(self):
-        return f"Message round: {self.__round}, Content: {self.__content}"
+        return f"Message at round: {self.__round}, support: {self.__support}"
 
     def get_round(self):
         """Return message round index."""
         return self.__round
 
-    def get_content(self):
-        """Return message content."""
-        return self.__content
+    def get_support(self):
+        """Return message support."""
+        return self.__support
diff --git a/src/lbaf/Model/lbsRank.py b/src/lbaf/Model/lbsRank.py
index 953faa6..033e4bc 100644
--- a/src/lbaf/Model/lbsRank.py
+++ b/src/lbaf/Model/lbsRank.py
@@ -1,10 +1,8 @@
 import copy
 import math
-import random as rnd
 from logging import Logger
 
 from .lbsBlock import Block
-from .lbsMessage import Message
 from .lbsObject import Object
 
 
@@ -35,25 +33,17 @@ class Rank:
         # Initialize other instance variables
         self.__size = 0.0
 
-        # Start with empty shared blokck information
+        # Start with empty shared block information
         self.__shared_blocks = {}
 
-        # No information about peers is known initially
-        self.__known_loads = {}
-
-        # No message was received initially
-        self.round_last_received = 0
-
     def copy(self, rank):
         """Specialized copy method."""
         # Copy all flat member variables
         self.__index = rank.get_id()
         self.__size = rank.get_size()
-        self.round_last_received = rank.round_last_received
 
         # Shallow copy owned objects
         self.__shared_blocks = copy.copy(rank.__shared_blocks)
-        self.__known_loads = copy.copy(rank.__known_loads)
         self.__sentinel_objects = copy.copy(rank.__sentinel_objects)
         self.__migratable_objects = copy.copy(rank.__migratable_objects)
 
@@ -178,39 +168,12 @@ class Rank:
 
     def is_sentinel(self, o: Object) -> list:
         """Return whether given object is sentinel of rank."""
-        if o in self.__sentinel_objects:
-            return True
-        else:
-            return False
-
-    def get_known_loads(self) -> dict:
-        """Return loads of peers know to self."""
-        return self.__known_loads
-
-    def add_known_load(self, rank):
-        """Make rank known to self if not already known."""
-        self.__known_loads.setdefault(rank, rank.get_load())
-
-    def get_targets(self) -> list:
-        """Return list of potential targets for object transfers."""
-        # No potential targets for loadless ranks
-        if not self.get_load() > 0.:
-            return []
-
-        # Remove self from list of targets
-        targets = self.get_known_loads()
-        del targets[self]
-        return targets
+        return (o in self.__sentinel_objects)
 
     def remove_migratable_object(self, o: Object, r_dst: "Rank"):
         """Remove migratable able object from self object sent to peer."""
-        # Remove object from those assigned to self
         self.__migratable_objects.remove(o)
 
-        # Update known load when destination is already known
-        if self.__known_loads and r_dst in self.__known_loads:
-            self.__known_loads[r_dst] += o.get_load()
-
     def get_load(self) -> float:
         """Return total load on rank."""
         return sum([o.get_load() for o in self.__migratable_objects.union(self.__sentinel_objects)])
@@ -220,7 +183,7 @@ class Rank:
         return sum([o.get_load() for o in self.__migratable_objects])
 
     def get_sentinel_load(self) -> float:
-        """Return sentinel load oon rank."""
+        """Return sentinel load on rank."""
         return sum([o.get_load() for o in self.__sentinel_objects])
 
     def get_received_volume(self):
@@ -275,85 +238,3 @@ class Rank:
     def get_max_memory_usage(self) -> float:
         """Return maximum memory usage on rank."""
         return self.__size + self.get_shared_memory() + self.get_max_object_level_memory()
-
-    def reset_all_load_information(self):
-        """Reset all load information known to self."""
-        # Reset information about known peers
-        self.__known_loads = {}
-
-    def initialize_message(self, loads: set, f: int):
-        """Initialize message to be sent to selected peers."""
-        # Retrieve current load on this rank
-        l = self.get_load()
-
-        # Make rank aware of own load
-        self.__known_loads[self] = l
-
-        # Create load message tagged at first round
-        msg = Message(1, self.__known_loads)
-
-        # Broadcast message to pseudo-random sample of ranks excluding self
-        return rnd.sample(list(loads.difference([self])), min(f, len(loads) - 1)), msg
-
-    def forward_message(self, information_round, _rank_set, fanout):
-        """Forward information message to sample of selected peers."""
-        # Create load message tagged at current round
-        msg = Message(information_round, self.__known_loads)
-
-        # Compute complement of set of known peers
-        complement = set(self.__known_loads).difference([self])
-
-        # Forward message to pseudo-random sample of ranks
-        return rnd.sample(list(complement), min(fanout, len(complement))), msg
-
-    def process_message(self, msg):
-        """Update internals when message is received."""
-        # Assert that message has the expected type
-        if not isinstance(msg, Message):
-            self.__logger.warning(f"Attempted to pass message of incorrect type {type(msg)}. Ignoring it.")
-
-        # Update load information
-        self.__known_loads.update(msg.get_content())
-
-        # Update last received message index
-        self.round_last_received = msg.get_round()
-
-    def compute_transfer_cmf(self, transfer_criterion, objects: list, targets: dict, strict=False):
-        """Compute CMF for the sampling of transfer targets."""
-        # Initialize criterion values
-        c_values = {}
-        c_min, c_max = math.inf, -math.inf
-
-        # Iterate over potential targets
-        for r_dst in targets.keys():
-            # Compute value of criterion for current target
-            c_dst = transfer_criterion.compute(self, objects, r_dst)
-
-            # Do not include rejected targets for strict CMF
-            if strict and c_dst < 0.:
-                continue
-
-            # Update criterion values
-            c_values[r_dst] = c_dst
-            if c_dst < c_min:
-                c_min = c_dst
-            if c_dst > c_max:
-                c_max = c_dst
-
-        # Initialize CMF depending on singleton or non-singleton support
-        if c_min == c_max:
-            # Sample uniformly if all criteria have same value
-            cmf = {k: 1.0 / len(c_values) for k in c_values.keys()}
-        else:
-            # Otherwise, use relative weights
-            c_range = c_max - c_min
-            cmf = {k: (v - c_min) / c_range for k, v in c_values.items()}
-
-        # Compute CMF
-        sum_p = 0.0
-        for k, v in cmf.items():
-            sum_p += v
-            cmf[k] = sum_p
-
-        # Return normalized CMF and criterion values
-        return {k: v / sum_p for k, v in cmf.items()}, c_values


Test patch:
diff --git a/tests/test_lbs_inform_and_transfer_algorithm.py b/tests/test_lbs_inform_and_transfer_algorithm.py
new file mode 100644
index 0000000..7f3223d
--- /dev/null
+++ b/tests/test_lbs_inform_and_transfer_algorithm.py
@@ -0,0 +1,70 @@
+import logging
+import random
+import unittest
+from unittest.mock import patch
+
+from lbaf.Model.lbsMessage import Message
+from lbaf.Model.lbsObject import Object
+from lbaf.Model.lbsRank import Rank
+from lbaf.Execution.lbsInformAndTransferAlgorithm import InformAndTransferAlgorithm
+from lbaf.Model.lbsWorkModelBase import WorkModelBase
+
+class TestConfig(unittest.TestCase):
+    def setUp(self):
+        self.logger = logging.getLogger()
+        self.migratable_objects = {Object(i=0, load=1.0), Object(i=1, load=0.5), Object(i=2, load=0.5), Object(i=3, load=0.5)}
+        self.sentinel_objects = {Object(i=15, load=4.5), Object(i=18, load=2.5)}
+        self.rank = Rank(r_id=0, mo=self.migratable_objects, so=self.sentinel_objects, logger=self.logger)
+        self.inform_and_transfer = InformAndTransferAlgorithm(
+            work_model=WorkModelBase(),
+            parameters={
+                "n_iterations": 8,
+                "n_rounds": 4,
+                "fanout": 4,
+                "order_strategy": "element_id",
+                "transfer_strategy": "Recursive",
+                "criterion": "Tempered",
+                "max_objects_per_transfer": 8,
+                "deterministic_transfer": True
+            },
+            lgr=self.logger,
+            rank_qoi=None,
+            object_qoi=None)
+
+    @patch.object(random, "sample")
+    def test_lbs_inform_and_transfer_forward_message(self, random_mock):
+        temp_rank_1 = Rank(r_id=1, logger=self.logger)
+        temp_rank_2 = Rank(r_id=2, logger=self.logger)
+        random_mock.return_value = [temp_rank_1, temp_rank_2]
+
+        self.assertEqual(
+            self.inform_and_transfer._InformAndTransferAlgorithm__forward_message(
+                i=2,
+                r_snd=self.rank,
+                f=4)[0],
+            [temp_rank_1, temp_rank_2]
+        )
+        self.assertEqual(
+            self.inform_and_transfer._InformAndTransferAlgorithm__forward_message(
+                i=2,
+                r_snd=self.rank,
+                f=4)[1].get_round(),
+            Message(2, {"loads": self.inform_and_transfer.get_known_peers()}).get_round()
+        )
+        self.assertEqual(
+            self.inform_and_transfer._InformAndTransferAlgorithm__forward_message(
+                i=2,
+                r_snd=self.rank,
+                f=4)[1].get_support(),
+            Message(2, self.inform_and_transfer.get_known_peers()[self.rank]).get_support()
+        )
+    def test_lbs_inform_and_transfer_process_message(self):
+        temp_rank_1 = Rank(r_id=1, logger=self.logger)
+        self.inform_and_transfer._InformAndTransferAlgorithm__process_message(
+            self.rank, Message(1,{temp_rank_1: 4.0})
+        )
+        known_peers = self.inform_and_transfer.get_known_peers()
+        self.assertEqual(known_peers, {self.rank: {self.rank, temp_rank_1}})
+
+if __name__ == "__main__":
+    unittest.main()
\ No newline at end of file
diff --git a/tests/unit/test_lbs_message.py b/tests/unit/test_lbs_message.py
index 55ed3b4..00859c2 100644
--- a/tests/unit/test_lbs_message.py
+++ b/tests/unit/test_lbs_message.py
@@ -8,16 +8,16 @@ class TestConfig(unittest.TestCase):
 
     def test_message_initialization_001(self):
         self.assertEqual(self.msg._Message__round, 1)
-        self.assertEqual(self.msg._Message__content, "something")
+        self.assertEqual(self.msg._Message__support, "something")
 
     def test_object_repr(self):
-        self.assertEqual(str(self.msg), "Message round: 1, Content: something")
+        self.assertEqual(str(self.msg), "Message at round: 1, support: something")
 
     def test_message_get_round(self):
         self.assertEqual(self.msg.get_round(), 1)
 
-    def test_message_get_content(self):
-        self.assertEqual(self.msg.get_content(), "something")
+    def test_message_get_support(self):
+        self.assertEqual(self.msg.get_support(), "something")
 
 
 if __name__ == "__main__":
diff --git a/tests/unit/test_lbs_rank.py b/tests/unit/test_lbs_rank.py
index 4c8dc12..817f2d1 100644
--- a/tests/unit/test_lbs_rank.py
+++ b/tests/unit/test_lbs_rank.py
@@ -19,8 +19,6 @@ class TestConfig(unittest.TestCase):
     def test_lbs_rank_initialization(self):
         self.assertEqual(self.rank._Rank__index, 0)
         self.assertEqual(self.rank._Rank__migratable_objects, self.migratable_objects)
-        self.assertEqual(self.rank._Rank__known_loads, {})
-        self.assertEqual(self.rank.round_last_received, 0)
         self.assertEqual(self.rank._Rank__sentinel_objects, self.sentinel_objects)
 
     def test_lbs_rank_repr(self):
@@ -53,9 +51,6 @@ class TestConfig(unittest.TestCase):
     def test_lbs_rank_get_sentinel_object_ids(self):
         self.assertEqual(sorted(self.rank.get_sentinel_object_ids()), [15, 18])
 
-    def test_lbs_rank_get_known_loads(self):
-        self.assertEqual(self.rank.get_known_loads(), {})
-
     def test_lbs_rank_get_load(self):
         self.assertEqual(self.rank.get_load(), 9.5)
 
@@ -91,57 +86,9 @@ class TestConfig(unittest.TestCase):
         self.rank.add_migratable_object(temp_object)
         self.migratable_objects.add(temp_object)
         self.assertEqual(self.rank.get_migratable_objects(), self.migratable_objects)
-        self.rank._Rank__known_loads[temp_rank] = 4.0
         self.rank.remove_migratable_object(temp_object, temp_rank)
         self.migratable_objects.remove(temp_object)
         self.assertEqual(self.rank.get_migratable_objects(), self.migratable_objects)
 
-    def test_lbs_rank_reset_all_load_information(self):
-        temp_rank = Rank(r_id=1, logger=self.logger)
-        self.rank._Rank__known_loads[temp_rank] = 4.0
-        self.assertEqual(self.rank.get_known_loads(), {temp_rank: 4.0})
-        self.rank.reset_all_load_information()
-        self.assertEqual(self.rank.get_known_loads(), {})
-
-    @patch.object(random, "sample")
-    def test_lbs_rank_initialize_message(self, random_mock):
-        self.rank._Rank__known_loads[self.rank] = self.rank.get_load()
-        temp_rank_1 = Rank(r_id=1, logger=self.logger)
-        temp_rank_1._Rank__known_loads[temp_rank_1] = 4.0
-        temp_rank_2 = Rank(r_id=2, logger=self.logger)
-        temp_rank_2._Rank__known_loads[temp_rank_2] = 5.0
-        random_mock.return_value = [temp_rank_1, temp_rank_2]
-        self.assertEqual(self.rank.initialize_message(loads={self.rank, temp_rank_1, temp_rank_2}, f=4)[0],
-                         [temp_rank_1, temp_rank_2])
-        self.assertEqual(self.rank.initialize_message(loads={self.rank, temp_rank_1, temp_rank_2}, f=4)[1].get_round(),
-                         Message(1, self.rank._Rank__known_loads).get_round())
-        self.assertEqual(self.rank.initialize_message(loads={self.rank, temp_rank_1, temp_rank_2}, f=4)[1].get_content(),
-                         Message(1, self.rank._Rank__known_loads).get_content())
-
-    @patch.object(random, "sample")
-    def test_lbs_rank_forward_message(self, random_mock):
-        self.rank._Rank__known_loads[self.rank] = self.rank.get_load()
-        temp_rank_1 = Rank(r_id=1, logger=self.logger)
-        temp_rank_1._Rank__known_loads[temp_rank_1] = 4.0
-        temp_rank_2 = Rank(r_id=2, logger=self.logger)
-        temp_rank_2._Rank__known_loads[temp_rank_2] = 5.0
-        random_mock.return_value = [temp_rank_1, temp_rank_2]
-        self.assertEqual(self.rank.forward_message(information_round=2, _rank_set=set(), fanout=4)[0],
-                         [temp_rank_1, temp_rank_2])
-        self.assertEqual(self.rank.forward_message(information_round=2, _rank_set=set(), fanout=4)[1].get_round(),
-                         Message(2, self.rank._Rank__known_loads).get_round())
-        self.assertEqual(self.rank.forward_message(information_round=2, _rank_set=set(), fanout=4)[1].get_content(),
-                         Message(2, self.rank._Rank__known_loads).get_content())
-
-    def test_lbs_rank_process_message(self):
-        self.rank._Rank__known_loads[self.rank] = self.rank.get_load()
-        temp_rank_1 = Rank(r_id=1, logger=self.logger)
-        temp_rank_1._Rank__known_loads[temp_rank_1] = 4.0
-        self.assertEqual(self.rank.get_load(), 9.5)
-        self.rank.process_message(Message(1, {temp_rank_1: 4.0}))
-        self.assertEqual(self.rank._Rank__known_loads, {self.rank: 9.5, temp_rank_1: 4.0})
-        self.assertEqual(self.rank.round_last_received, 1)
-
-
 if __name__ == "__main__":
     unittest.main()