From 1c85bd4c2d8caeebb89e1807364668260c7f82be Mon Sep 17 00:00:00 2001
From: Maxime MORGE <maxime.morge@univ-lille.fr>
Date: Fri, 7 Mar 2025 10:21:25 +0100
Subject: [PATCH] First centralize version of CBAA

---
 src/main/scala/Main.scala                     | 13 ++-
 src/main/scala/org/scata/algorithm/CBAA.scala | 98 +++++++++++++------
 .../org/scata/core/SingleAssignment.scala     |  2 +-
 .../scata/core/SingleAssignmentProblem.scala  | 17 +++-
 4 files changed, 92 insertions(+), 38 deletions(-)

diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala
index 6650aa2..ddb5e0f 100644
--- a/src/main/scala/Main.scala
+++ b/src/main/scala/Main.scala
@@ -1,5 +1,16 @@
+// Copyright (C) Maxime MORGE 2024
+import org.scata.core.SingleAssignmentProblem
+import org.scata.algorithm.CBAA
+
 object Main {
   def main(args: Array[String]): Unit = {
-    println("Hello world!")
+
+    val nbWorkers = 4
+    val nbTasks = 3
+    val pb = SingleAssignmentProblem.randomProblem(nbWorkers, nbTasks)
+    val cbaa = new CBAA(pb)
+    println(pb)
+    val solution = cbaa.solve()
+    println(solution)
   }
 }
\ No newline at end of file
diff --git a/src/main/scala/org/scata/algorithm/CBAA.scala b/src/main/scala/org/scata/algorithm/CBAA.scala
index b11eb46..cd2b02b 100644
--- a/src/main/scala/org/scata/algorithm/CBAA.scala
+++ b/src/main/scala/org/scata/algorithm/CBAA.scala
@@ -1,6 +1,5 @@
 // Copyright (C) Maxime MORGE, 2024
 package org.scata.algorithm
-
 import org.scata.core._
 
 /**
@@ -8,11 +7,12 @@ import org.scata.core._
  * a single-assignment strategy
  */
 class CBAA(pb : SingleAssignmentProblem) {
+  private val debug = true
 
-  var neighbours : Map[Worker, List[Worker]] = Map[Worker, List[Worker]]()
- /* pb.workers.foreach{ worker =>
-    neighbours = neighbours.updated(worker, pb.workers.toList)
- }*/
+  // Generate a fully connected communication network
+  private val neighbours: Map[Worker, List[Worker]] = pb.workers.toList.map { worker =>
+    worker -> pb.workers.toList // Each worker is connected to all workers, including themselves
+  }.toMap
 
   private var workerTaskList: Map[Worker, Map[Task, Boolean]] = pb.workers.toList.map { worker =>
     worker -> pb.tasks.toList.map { task =>
@@ -22,42 +22,31 @@ class CBAA(pb : SingleAssignmentProblem) {
 
   private var winningBid: Map[Worker, Map[Task, (Worker, Int)]] = pb.workers.toList.map { worker =>
     worker -> pb.tasks.toList.map { task =>
-      task -> (NoWorker, 0)
+      task -> (NoWorker, Int.MaxValue)
     }.toMap
   }.toMap
 
-  /**
-   * Generate a fully connect communication network
-   */
-  //private def fullyConnected :  Map[Worker, List[Worker]] =
 
   /**
    * Returns true if the worker has no task assigned
    */
   private def isFree(worker : Worker) : Boolean = !pb.tasks.exists(task => workerTaskList(worker)(task))
 
-  /**
-   * Checks if two workers are neighbors
-   * @param worker1 the first worker
-   * @param worker2 the second worker
-   * @return true if they are neighbors, false otherwise
-   */
-  private def isNeighbor(worker1: Worker, worker2: Worker): Boolean = neighbours(worker1).contains(worker2)
-
   /**
    * CBAA Phase 1 for worker i
    * Selects the best task for the worker based on the cost
    * @param i index of the worker in the workers set
    * */
-  def selectTask(i : Int) : Unit = {
+  private def selectTask(i : Int) : Unit = {
     val worker = pb.workers.toIndexedSeq(i)
     // Check if the worker has no tasks assigned
     if (isFree(worker)) {
-
+      if (debug) println(s"Worker ${worker.name} is free")
       // Determine valid tasks based on cost comparison
       var validTasks: Map[Task, Boolean] = Map[Task, Boolean]()
       pb.tasks.foreach { task =>
         if (pb.cost(worker, task) < winningBid(worker)(task)._2) {
+          if (debug) println(s"Worker ${worker.name} can perform task ${task.name}")
           validTasks = validTasks.updated(task, true)
         } else {
           validTasks = validTasks.updated(task, false)
@@ -66,26 +55,71 @@ class CBAA(pb : SingleAssignmentProblem) {
       // If there are valid tasks, select the one with the minimum cost
       if (pb.tasks.exists(task => validTasks(task))) {
         val bestTask = validTasks.filter(_._2).keys.minBy(task => pb.cost(worker, task))
-        workerTaskList(worker) = workerTaskList(worker).updated(bestTask, true)
-        winningBid(worker) = winningBid(worker).updated(bestTask, (worker, pb.cost(worker, bestTask)))
+        if (debug) println(s"Worker ${worker.name} selects task ${bestTask.name} with cost ${pb.cost(worker, bestTask)}")
+        workerTaskList = workerTaskList.updated(worker, workerTaskList(worker).updated(bestTask, true))
+        winningBid = winningBid.updated(worker, winningBid(worker).updated(bestTask, (worker, pb.cost(worker, bestTask))))
       }
     }
   }
+
   /**
-   * TODO CBAA Phase 1 for worker i
+   * CBAA Phase 2 for worker i
+   * Updates the winning bids of the worker based on the minimum winning bid
+   * among its neighbors.
+   * Returns true if the worker's winning bid is updated
    * @param i index of the worker in the workers set
    */
-  def consensus(i: Int): Unit = {
+  private def consensus(i: Int): Boolean = {
+    var isWinningBidUpdated = false
     val worker = pb.workers.toIndexedSeq(i)
     // Iterate over all tasks
-    pb.tasks.foreach { task =>
-      // Find the minimum winning bid among the neighbors of the worker
-      val minWinningBid = pb.workers
-        .filter(neighbor => isNeighbor(worker, neighbor))
-        .map(neighbor => winningBid((neighbor, task)))
-        .min
-      // Update the winning bid for the worker and task
-      winningBid = winningBid.updated((worker, task), minWinningBid)
+    pb.tasks.foreach { task : Task =>
+      // Find the minimum winning bid among the worker's neighbors (including itself)
+      val minWinningBid = neighbours(worker)
+        .map(neighbor => winningBid(neighbor)(task))
+        .minBy(_._2)
+      println(s"Worker ${worker.name} minimum winning bid for task ${task.name} is ${minWinningBid}")
+      // Update the worker's winning bid for the task if a lower bid is found
+      if (minWinningBid._2 < winningBid(worker)(task)._2) {
+        println(s"Worker ${worker.name} updates winning bid for task ${task.name} to ${minWinningBid}")
+        isWinningBidUpdated = true
+        winningBid = winningBid.updated(worker, winningBid(worker).updated(task, minWinningBid))
+        // Update the worker's task list to reflect the new winning bid
+        workerTaskList = workerTaskList.updated(worker, workerTaskList(worker).updated(task, minWinningBid._1 == worker))
+      }
+    }
+    isWinningBidUpdated
+  }
+
+  /**
+   * Solves the single-assignment problem using the CBAA algorithm
+   * @return the final assignment
+   */
+  def solve() : SingleAssignment = {
+    var hasConverged = false
+    while (!hasConverged) {
+      if (debug) println("CBAA Phase 1")
+      for (i <- 0 until pb.m) {
+        selectTask(i)
+      }
+      hasConverged = true
+      if (debug) println("CBAA Phase 2")
+      for (i <- 0 until pb.m) {
+        if (consensus(i)) {
+          hasConverged = false
+        }
+        if (debug) println(s"CBAA Phase 2 has converged: ${hasConverged}")
+      }
+    }
+    // Generate the final assignment
+    val assignment = new SingleAssignment(pb)
+    pb.workers.foreach { worker =>
+      pb.tasks.foreach { task =>
+        if (workerTaskList(worker)(task)) {
+          assignment.bundle = assignment.bundle.updated(worker, task)
+        }
+      }
     }
+    return assignment
   }
 }
diff --git a/src/main/scala/org/scata/core/SingleAssignment.scala b/src/main/scala/org/scata/core/SingleAssignment.scala
index c6452f9..4133822 100644
--- a/src/main/scala/org/scata/core/SingleAssignment.scala
+++ b/src/main/scala/org/scata/core/SingleAssignment.scala
@@ -17,7 +17,7 @@ class SingleAssignment(val pb: SingleAssignmentProblem) {
   }
 
   override def toString: String =
-    pb.workers.toList.map(worker => s"$worker: $bundle").mkString("\n")
+    pb.workers.toList.map(worker => s"$worker: ${bundle(worker)}").mkString("\n")
 
   override def equals(that: Any): Boolean =
     that match {
diff --git a/src/main/scala/org/scata/core/SingleAssignmentProblem.scala b/src/main/scala/org/scata/core/SingleAssignmentProblem.scala
index c81de55..3da60cc 100644
--- a/src/main/scala/org/scata/core/SingleAssignmentProblem.scala
+++ b/src/main/scala/org/scata/core/SingleAssignmentProblem.scala
@@ -18,10 +18,19 @@ class SingleAssignmentProblem(val workers: SortedSet[Worker],
     * Returns a string describing the MASTAPlus problem
     */
   override def toString: String = {
-    s"m: ${workers.size}\n" +
-    s"n: ${tasks.size}\n" +
-      "workers: " + workers.mkString(", ") + "\n" +
-      "tasks: " + tasks.mkString(", ") + "\n"
+    val workersStr = workers.map(_.name).mkString(", ")
+    val tasksStr = tasks.map(_.name).mkString(", ")
+
+    val costStr = workers.map { worker =>
+      val taskCosts = tasks.map { task =>
+        s"${task.name}: ${cost((worker, task))}"
+      }.mkString(", ")
+      s"${worker.name} -> [$taskCosts]"
+    }.mkString("\n")
+
+    s"Workers (${workers.size}): $workersStr\n" +
+      s"Tasks (${tasks.size}): $tasksStr\n" +
+      s"Costs:\n$costStr"
   }
 
   /**
-- 
GitLab