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