diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala index 4363f07c19ef3f7a50c2a72c76d7faf06d52a962..793605dbdd7022e4a87d746704bcb970a8b6f14d 100644 --- a/src/main/scala/Main.scala +++ b/src/main/scala/Main.scala @@ -1,20 +1,20 @@ // Copyright (C) Maxime MORGE 2024 import org.scata.core.AssignmentProblem -import org.scata.algorithm.CBAA +import org.scata.algorithm.CBBA import org.scata.patrol.Environment object Main { def main(args: Array[String]): Unit = { val nbRobots= 4 - val nbTargets = 4 + val nbTargets = 8 val env = Environment.randomEnvironment(nbRobots, nbTargets) println(env) - val pb = env.toSAP() + val pb = env.toAP() println(pb) - val cbaa = new CBAA(pb) + val cbba = new CBBA(pb) println(pb) - val solution = cbaa.solve() + val solution = cbba.solve() println(solution) } } \ No newline at end of file diff --git a/src/main/scala/org/scata/algorithm/CBBA.scala b/src/main/scala/org/scata/algorithm/CBBA.scala new file mode 100644 index 0000000000000000000000000000000000000000..cc888b457abe5a1096a0eb8822a12ef6e04e8dbd --- /dev/null +++ b/src/main/scala/org/scata/algorithm/CBBA.scala @@ -0,0 +1,93 @@ +// Copyright (C) Maxime MORGE, 2024 +package org.scata.algorithm + +import org.scata.core._ +import scala.math.Ordering.Implicits.seqOrdering + +class CBBA(pb: AssignmentProblem) { + private val debug = true + private val maxTasksPerAgent = pb.tasks.size // You may define a limit Lt per agent + + // Initialize structures + private var bundle: Map[Worker, List[Task]] = pb.workers.map(_ -> List.empty[Task]).toMap + private var path: Map[Worker, List[Task]] = pb.workers.map(_ -> List.empty[Task]).toMap + private var winningBid: Map[Task, (Worker, Int)] = pb.tasks.map(_ -> (NoWorker, Int.MaxValue)).toMap + private var winningAgent: Map[Task, Worker] = pb.tasks.map(_ -> NoWorker).toMap + + private def marginalCost(worker: Worker, task: Task, currentPath: List[Task]): Int = { + // Here a simple cost (you can add a more complex scoring function) + pb.cost(worker, task) + } + + /** + * Phase 1: Each agent builds a bundle of tasks greedily + */ + private def buildBundle(worker: Worker): Unit = { + while (bundle(worker).size < maxTasksPerAgent) { + val candidateTasks = pb.tasks.diff(bundle(worker).toSet) + val costs = candidateTasks.map(task => task -> marginalCost(worker, task, path(worker))).toMap + + val validTasks = costs.filter { case (task, cost) => cost < winningBid(task)._2 } + + if (validTasks.nonEmpty) { + val bestTask = validTasks.minBy { case (task, gain) => (gain, task.name) }._1 + bundle = bundle.updated(worker, bundle(worker) :+ bestTask) + path = path.updated(worker, path(worker) :+ bestTask) + winningBid = winningBid.updated(bestTask, (worker, costs(bestTask))) + winningAgent = winningAgent.updated(bestTask, worker) + if (debug) println(s"${worker.name} adds ${bestTask.name} to bundle with bid ${costs(bestTask)}") + } else return + } + } + + /** + * Phase 2: Conflict resolution using consensus across all workers + */ + private def resolveConflicts(): Boolean = { + var changed = false + + for (task <- pb.tasks) { + val bids = pb.workers.map(worker => + (worker, bundle(worker).indexOf(task)) match { + case (_, -1) => (worker, Int.MaxValue) + case (_, idx) => (worker, marginalCost(worker, task, path(worker))) + } + ) + val (bestWorker, bestBid) = bids.minBy { case (w, b) => (b, w.name) } + + if (winningAgent(task) != bestWorker) { + // Task is reallocated, remove from previous owner's bundle + val oldOwner = winningAgent(task) + bundle = bundle.updated(oldOwner, bundle(oldOwner).filterNot(_ == task)) + path = path.updated(oldOwner, path(oldOwner).filterNot(_ == task)) + + bundle = bundle.updated(bestWorker, bundle(bestWorker) :+ task) + path = path.updated(bestWorker, path(bestWorker) :+ task) + + winningAgent = winningAgent.updated(task, bestWorker) + winningBid = winningBid.updated(task, (bestWorker, bestBid)) + changed = true + if (debug) println(s"Task ${task.name} reassigned to ${bestWorker.name} with bid $bestBid") + } + } + + changed + } + + /** + * Solve the assignment using CBBA + */ + def solve(): MultipleAssignment = { + var converged = false + while (!converged) { + for (worker <- pb.workers) buildBundle(worker) + converged = !resolveConflicts() + } + + val result = new MultipleAssignment(pb) + for ((worker, tasks) <- bundle; task <- tasks) { + result.bundle = result.bundle.updated(worker, result.bundle(worker) :+ task) + } + result + } +} \ No newline at end of file diff --git a/src/main/scala/org/scata/patrol/Environment.scala b/src/main/scala/org/scata/patrol/Environment.scala index 2d1fe6d285e0e92e67a237a321b1c5c7ebb66916..e1b0496a4f68804b0995c31a7f8c342af7e0e6b6 100644 --- a/src/main/scala/org/scata/patrol/Environment.scala +++ b/src/main/scala/org/scata/patrol/Environment.scala @@ -39,9 +39,7 @@ class Environment(val robots: SortedSet[Robot], /** * Generate a single assignment problem */ - def toSAP(): AssignmentProblem = { - if (n > m) - throw new RuntimeException("Cannot generate a single assignment problem since there are more targets than robots") + def toAP()(): AssignmentProblem = { // Convert robots and targets into Workers and Tasks respectively val workers: SortedSet[Worker] = robots.map(r => new Worker(r.name))