diff --git a/src/main/scala/org/scata/core/SingleAssignment.scala b/src/main/scala/org/scata/core/SingleAssignment.scala new file mode 100644 index 0000000000000000000000000000000000000000..c6452f9a064b158de142e2f81593c7ef5eec6f3c --- /dev/null +++ b/src/main/scala/org/scata/core/SingleAssignment.scala @@ -0,0 +1,84 @@ +// Copyright (C) Maxime MORGE, 2024 +package org.scata.core + +import scala.collection.SortedSet + +/** + * Class representing an allocation as + * a single-assignment of the tasks to some workers. + * @param pb is a single-assignment instance + */ +class SingleAssignment(val pb: SingleAssignmentProblem) { + + var bundle: Map[Worker, Task] = Map[Worker, Task]() + + pb.workers.foreach{ + worker=> bundle += worker -> NoTask + } + + override def toString: String = + pb.workers.toList.map(worker => s"$worker: $bundle").mkString("\n") + + override def equals(that: Any): Boolean = + that match { + case that: SingleAssignment => that.canEqual(this) && this.bundle == that.bundle + case _ => false + } + + override def hashCode(): Int = this.bundle.hashCode() + + def canEqual(a: Any): Boolean = a.isInstanceOf[SingleAssignment] + + /** + * Returns a copy + */ + @throws(classOf[RuntimeException]) + private def copy(): SingleAssignment = { + val assignment = new SingleAssignment(pb) + this.bundle.foreach { + case (worker: Worker, task: Task) => + assignment.bundle = assignment.bundle.updated(worker, task) + case _ => throw new RuntimeException("Not able to copy bundle") + } + assignment + } + + /** + * Updates an assignment with a new bundle for a computing node + */ + def update(worker: Worker, task: Task): SingleAssignment = { + val allocation = this.copy() + allocation.bundle = allocation.bundle.updated(worker, task) + allocation + } + + /** + * Returns true if each task is assigned to no more than one worker + */ + private def isPartition: Boolean = { + val tasks = bundle.values.toList + tasks.distinct.size == tasks.size + } + + /** + * Returns true if the assignment is complete, i.e., every task is assigned to a worker + */ + private def isComplete: Boolean = { + pb.tasks.forall(task => bundle.values.toSet.contains(task)) + } + + /** + * Returns true if the allocation is sound, i.e a complete partition of the tasks + */ + def isSound: Boolean = isPartition && isComplete + + /** + * Returns the worker which has the task in its bundle + */ + def worker(task: Task): Option[Worker] = { + bundle.find { + case (_, t) if task.equals(t) => true + case _ => false + }.map(_._1) + } +} \ No newline at end of file diff --git a/src/main/scala/org/scata/core/SingleAssignmentProblem.scala b/src/main/scala/org/scata/core/SingleAssignmentProblem.scala new file mode 100644 index 0000000000000000000000000000000000000000..939719ca7866db89a9131959e22416e95ce82947 --- /dev/null +++ b/src/main/scala/org/scata/core/SingleAssignmentProblem.scala @@ -0,0 +1,71 @@ +// Copyright (C) Maxime MORGE, 2024 +package org.scata.core + +import scala.collection.SortedSet +import org.scata.utils.RandomUtils + +/** + * Class representing a Single Assignment Problem + * + * @param workers are the workers + * @param tasks are the tasks + */ +class SingleAssignmentProblem(val workers: SortedSet[Worker], + val tasks : SortedSet[Task], + val cost : Map[(Worker,Task),Double]) { + + /** + * 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" + } + + /** + * Returns the number of computing nodes + */ + def m: Int = workers.size + + /** + * Returns the number of tasks + */ + def n: Int = tasks.size +} + +/** + * Factory for SingleAssignmentProblem + */ +object SingleAssignmentProblem{ + + implicit val order : Ordering[Double] = Ordering.Double.TotalOrdering + // eventually Ordering.Double.IeeeOrdering + + private val MAX_COST = 100 // Maximum task cost + + /** + * Returns a random single-assignment problem instance with + * @param m nodes + * @param n tasks + */ + def randomProblem(m: Int, n: Int): SingleAssignmentProblem = { + // Workers + val workers: SortedSet[Worker] = collection.immutable.SortedSet[Worker]() ++ + (for (i <- 1 to m) yield new Worker(name = "w%02d".format(i))) + + // Tasks + val tasks : SortedSet[Task] = collection.immutable.SortedSet[Task]() ++ + (for (i <- 1 to n) yield new Task(name = "t%02d".format(i))) + + var cost : Map[(Worker,Task),Double] = Map [(Worker,Task),Double]() + // Adjust the resource sizes + workers.foreach{ worker => + tasks.foreach{ task => + cost = cost.updated((worker, task), RandomUtils.random(1, MAX_COST)) + } + } + new SingleAssignmentProblem(workers, tasks, cost) + } +} diff --git a/src/main/scala/org/scata/core/Task.scala b/src/main/scala/org/scata/core/Task.scala new file mode 100644 index 0000000000000000000000000000000000000000..2628e54607d86cc14ab42546f6f1b810be3f2517 --- /dev/null +++ b/src/main/scala/org/scata/core/Task.scala @@ -0,0 +1,41 @@ +// Copyright (C) Maxime MORGE 2024 +package org.scata.core + +/** + * Class representing a task + * @param name of the task + */ +class Task(val name : String) extends Ordered[Task]{ + + override def toString: String = name + + /** + * Returns a full description of the task + */ + def describe: String = s"$name: " + + override def equals(that: Any): Boolean = + that match { + case that: Task => that.canEqual(this) && this.name == that.name + case _ => false + } + + private def canEqual(a: Any): Boolean = a.isInstanceOf[Task] + + /** + * Returns 0 if this and that are the same, negative if this < that, and positive otherwise + * Tasks are sorted with their name + */ + def compare(that: Task) : Int = { + if (this.name == that.name) return 0 + else if (this.name > that.name) return 1 + -1 + } +} + +/** + * The default task + */ +object NoTask extends Task("NoTask"){ + override def toString: String = "θ" +} diff --git a/src/main/scala/org/scata/core/Worker.scala b/src/main/scala/org/scata/core/Worker.scala new file mode 100644 index 0000000000000000000000000000000000000000..931cbb0c4cab6374a78af3cc0c8864a5d852e6f7 --- /dev/null +++ b/src/main/scala/org/scata/core/Worker.scala @@ -0,0 +1,28 @@ +// Copyright (C) Maxime MORGE 2024 +package org.scata.core + +/** + * Class representing a worker + * @param name of the worker + */ +final class Worker(val name : String) extends Ordered[Worker]{ + + override def toString: String = name + + override def equals(that: Any): Boolean = + that match { + case that: Worker => that.canEqual(this) && this.name == that.name + case _ => false + } + private def canEqual(a: Any) : Boolean = a.isInstanceOf[Worker] + + /** + * Returns 0 if this an that are the same, negative if this < that, and positive otherwise + * Workers are sorted with their name + */ + def compare(that: Worker) : Int = { + if (this.name == that.name) return 0 + else if (this.name > that.name) return 1 + -1 + } +} diff --git a/src/main/scala/org/scata/utils/MathUtils.scala b/src/main/scala/org/scata/utils/MathUtils.scala new file mode 100644 index 0000000000000000000000000000000000000000..2f996b195e114992efe29c90b4a08153ff744efd --- /dev/null +++ b/src/main/scala/org/scata/utils/MathUtils.scala @@ -0,0 +1,265 @@ +// Copyright (C) Maxime MORGE 2024 +package org.scata.utils + +import java.util.concurrent.TimeUnit +import scala.annotation.unused +import scala.collection.SortedSet + +/** + * Compare floating-point numbers in Scala + * + */ +object MathUtils { + + /** + * Implicit class for classical list functions + */ + implicit class Count[T](list: List[T]) { + def count(n: T): Int = list.count(_ == n) + } + + /** + * Implicit class for classical mathematical functions + */ + implicit class MathUtils(x: Double) { + private val precision = 0.000001 + + /** + * Returns true if x and y are equals according to an implicit precision parameter + */ + def ~=(y: Double): Boolean = { + if ((x - y).abs <= precision) true else false + } + + /** + * Returns true if x is greater than y according to an implicit precision parameter + */ + def ~>(y: Double): Boolean = { + if (x - y > precision) true else false + } + + /** + * Returns true if y is greater than x according to an implicit precision parameter + */ + def ~<(y: Double): Boolean = { + if (y - x > precision) true else false + } + + /** + * Returns true if x is greater or equal than y according to an implicit precision parameter + */ + @unused + def ~>=(y: Double): Boolean = (x~>y) || (x~=y) + + /** + * Returns true if y is greater than x according to an implicit precision parameter + */ + def ~<=(y: Double): Boolean = (x~<y) || (x~=y) + } +} + +/** + * Random weight in Scala + * + */ +object RandomUtils { + + val r: scala.util.Random = scala.util.Random // For reproducible XP new scala.util.Random(42) + + /** + * Returns true with a probability p in [0;1] + */ + def randomBoolean(p: Double): Boolean = { + if (p < 0.0 || p > 1) throw new RuntimeException(s"The probability $p must be in [0 ; 1]") + if (math.random() < p) return true + false + } + + /** + * Returns a pseudo-randomly generated Double in ]0;1] + */ + def strictPositiveWeight(): Double = { + val number = r.nextDouble() // in [0.0;1.0[ + 1.0 - number + } + + /** + * Returns the next pseudorandom, normally distributed + * double value with mean 0.0 and standard deviation 1.0 + */ + @unused + def nextGaussian(): Double = { + r.nextGaussian() + } + + /** + * Returns a pseudo-randomly generated Double in [-1.0;1.0[ + */ + def weight(): Double = { + val number = r.nextDouble() // in [0.0;1.0[ + number * 2 - 1 + } + + /** + * Returns a shuffle list + */ + def shuffle[T](s: List[T]): List[T] = { + r.shuffle(s) + } + + /** + * Returns a shuffle list + */ + def shuffle[T](s: SortedSet[T]): List[T] = { + r.shuffle(s.toList) + } + + + /** + * Returns a random element in a non-empty list + */ + def random[T](s: Iterator[T]): T = { + val n = r.nextInt(s.size) + s.iterator.drop(n).next() + } + + /** + * Returns a random element in a non-empty set + */ + def random[T](s: Set[T]): T = { + val n = r.nextInt(s.size) + s.iterator.drop(n).next() + } + + /** + * Returns a random element in a non-empty set + */ + def random[T](s: SortedSet[T]): T = { + val n = r.nextInt(s.size) + s.iterator.drop(n).next() + } + + /** + * Returns a pseudo-randomly generated Double in [min, max] + */ + @unused + def randomDouble(min: Int, max: Int): Double = { + (min + util.Random.nextInt((max - min) + 1)).toDouble + } + + /** + * Returns a pseudo-randomly generated Double in [min, max] + */ + def random(min: Int, max: Int): Int = { + min + util.Random.nextInt((max - min) + 1) + } + + /* + * Returns a pseudo-randomly generated subset of n elm + */ + def pick[T](s: SortedSet[T], n: Int): Set[T] = r.shuffle(s.toList).take(n).toSet + +} + +/** + * Matrix in Scala + * + */ +object Matrix { + + /** + * Print + * + * @param matrix is an array of array + * @tparam T type of content + * @return string representation + **/ + def show[T](matrix: Array[Array[T]]): String = matrix.map(_.mkString("[", ", ", "]")).mkString("\n") + + /** + * Print + * + * @tparam T type of content + * @param f function + * @param L line number + * @param C column number + * @return string representation + **/ + def show[T](f: (Integer, Integer) => T, L: Integer, C: Integer): String = { + (for (i <- 0 until L) yield { + (for (j <- 0 until C) yield f(i, j).toString).mkString("[", ", ", "]") + }).mkString("[\n", ",\n", "]\n") + } +} + +/** + * List in Scala + */ +object MyList { + def insert[T](list: List[T], i: Int, value: T): List[T] = list match { + case head :: tail if i > 0 => head :: insert(tail, i - 1, value) + case _ => value :: list + } +} + +/** + * Time in Scala + */ +object MyTime { + def show(nanoseconds: Long): String = { + s"${TimeUnit.NANOSECONDS.toHours(nanoseconds)}h " + + s"${TimeUnit.NANOSECONDS.toMinutes(nanoseconds) - TimeUnit.HOURS.toMinutes(TimeUnit.NANOSECONDS.toHours(nanoseconds))}min " + + s"${TimeUnit.NANOSECONDS.toSeconds(nanoseconds) - TimeUnit.MINUTES.toSeconds(TimeUnit.NANOSECONDS.toMinutes(nanoseconds))}sec " + + s"${TimeUnit.NANOSECONDS.toMillis(nanoseconds) - TimeUnit.SECONDS.toMillis(TimeUnit.NANOSECONDS.toSeconds(nanoseconds))}ms " + + s"${TimeUnit.NANOSECONDS.toNanos(nanoseconds) - TimeUnit.MILLISECONDS.toNanos(TimeUnit.NANOSECONDS.toMillis(nanoseconds))}ns " + } +} + +/** + * Statistical tools + */ +object Stat { + + /** + * Returns the mean of a random variable + */ + def mean(values: List[Double]): Double = values.sum / values.length + + /** + * Returns the variance of a random variable with a Gaussian distribution (i.e. normally distributed) + * + * @param values of the random variable + */ + private def variance(values: List[Double]): Double = { + val mean: Double = Stat.mean(values) + values.map(a => math.pow(a - mean, 2)).sum / values.length + } + + /** + * Returns the mean and the variance of a random variable with a Gaussian distribution (i.e. normally distributed) + * + * @param values of the random variable + */ + private def normal(values: List[Double]): (Double, Double) = (mean(values), variance(values)) + + + /** + * Returns the statistic t for Welch's t-test + */ + @unused + def statistic(values1: List[Double], values2: List[Double]): Double = { + val (mean1, var1) = normal(values1) + val (mean2, var2) = normal(values2) + (mean1 - mean2) / math.sqrt(var1 / values1.length + var2 / values2.length) + } + + /** + * Returns the first, the second (median) and the third quartile of data + */ + def quartiles(data: List[Double]): (Double, Double, Double) = { + val sortedData = data.sortWith(_ < _) + val dataSize = data.size + (sortedData(dataSize / 4), sortedData(dataSize / 2), sortedData(dataSize * 3 / 4)) + } + +} \ No newline at end of file