diff --git a/src/main/scala/scim/components/Parser.scala b/src/main/scala/scim/components/Parser.scala index b3c39f8..cd31857 100644 --- a/src/main/scala/scim/components/Parser.scala +++ b/src/main/scala/scim/components/Parser.scala @@ -194,7 +194,7 @@ class SimulationConfigParser(input: Map[String, Any], simulationBehaviours: Simu val start: Int = truthObject(0).asInstanceOf[Int] // val end: Option[Int] = None // Not used truth.set(Some(truthObject(1).asInstanceOf[Double])) - Map[Time, Truth](Time(start) -> truth) + Map[Time, Truth](Time(start, config.duration) -> truth) } case truthObject: Map[_, _] => { val truth: Truth = Truth(new BroadcastObject()) @@ -206,10 +206,10 @@ class SimulationConfigParser(input: Map[String, Any], simulationBehaviours: Simu val output: mutable.Map[Time, Truth] = mutable.Map() timeLineVector.map(tuple => { - output(Time(tuple._1)) = truth + output(Time(tuple._1, config.duration)) = truth if (!tuple._2.isEmpty) { endTruth.set(None) - output(Time(tuple._2.get)) = endTruth + output(Time(tuple._2.get, config.duration)) = endTruth } }) output.toMap diff --git a/src/main/scala/scim/components/Simulation.scala b/src/main/scala/scim/components/Simulation.scala index 5914e21..b13dbb1 100644 --- a/src/main/scala/scim/components/Simulation.scala +++ b/src/main/scala/scim/components/Simulation.scala @@ -1,56 +1,82 @@ package scim.components -import scim.datastruct.{Behaviour, BehaviourVector, Cell, Configuration, FunctionArgs, GroundTruth, Group, Sensor, Time} +import scim.datastruct.{Behaviour, BehaviourVector, Cell, Configuration, FunctionArgs, GroundTruth, Group, Sensor, SensorCount, SensorResult, Time, Truth} import scim.lib.simulation.SimulationBehaviour class Simulation(config: Configuration) { private val sharedMemory = config.sharedMemory + private val sharedConfig = sharedMemory.config private val cells = config.cells - val timeVector = Vector.tabulate(sharedMemory.config.duration)(t => Time(t)) + def start(): Unit = { - if(sharedMemory.config.parCell != 0 && cells.size >= sharedMemory.config.parCell ) { + val duration = sharedConfig.duration + val timeVector = Vector.tabulate(duration)(t => Time(t, duration)) + def simTime(time: Time): Unit = { + val prevTruths = if(time.start) sharedMemory.groundTruth(time) else sharedMemory.groundTruth(time.prev) + val resultTruths = sharedMemory.groundTruth(time) + val sensorResults = sharedMemory.sensor(time) + simCells(time, prevTruths, resultTruths, sensorResults) + } + timeVector.foreach(simTime) + } + + private def simCells(time: Time, prevTruths: Map[Cell, Map[Group, Truth]], resultTruths: Map[Cell, Map[Group, Truth]], sensorResults: Map[Cell, Map[Group, Vector[SensorResult]]]) : Unit = { + // Instead of using foreach, we could map and have the result returned here and write it into the shared memory + // This would possibly result in less writes into the shared memory and allows just to return their result + // the previous ground truth result would have to be passed into the map function, this approach + // would fit better with functional programming than the current + def simCell(input: (Cell, Map[Group, List[(Group, Sensor, Option[GroundTruth])]])): Unit ={ + val cell = input._1 + val groups = input._2 + simGroups(time, cell, groups, prevTruths(cell), resultTruths(cell), sensorResults(cell)) + } + if(sharedConfig.parCell != 0 && cells.size >= sharedConfig.parCell ) { val cellList = cells.par - cellList.tasksupport = sharedMemory.config.cellThreadPool - timeVector.foreach(time => cellList.foreach(cell => simGroups(time, cell._1, cell._2))) + cellList.tasksupport = sharedConfig.cellThreadPool + cellList.foreach(simCell) } else { - timeVector.foreach(time => cells.foreach(cell => simGroups(time, cell._1, cell._2))) + cells.foreach(simCell) } } - private def simGroups(time: Time, cell: Cell, groups: Map[Group, List[(Group, Sensor, Option[GroundTruth])]]): Unit = { + private def simGroups(time: Time, cell: Cell, groups: Map[Group, List[(Group, Sensor, Option[GroundTruth])]], prevTruths: Map[Group, Truth], resultTruths: Map[Group, Truth], resultSensors: Map[Group, Vector[SensorResult]]): Unit = { // TODO: possible optimisation (see text below) // this can be further optimised as a child is only dependent on its parents ground truth and could run // at the same time as the parent in a different thread. This would allow the child to start running as soon // as the ground truth is calculated in the parent and doesn't have have to wait for the sensors to finish. // Any subsequent children can start in the same manner, when their parent is done with the ground truth. - // -- - // other possibility: - // run simulateGT foreach first, then run simulateSensor foreach (dont know if there is much benefit tho) - if(sharedMemory.config.parGroup != 0 && cells.size >= sharedMemory.config.parGroup ) { + // : + // only run ground truth in list, have sensors in their respective groups + // requires groups to be split into two elements: sensor and ground truth + // Tuple2(Map[Group, list[(Group, Option[GroundTruth])]], Map[Group, Sensor]) + def simGroup(group: (Group, List[(Group, Sensor, Option[GroundTruth])])): Unit = { + group._2.foreach(element => simulate(time, cell, prevTruths(group._1), resultTruths(group._1), resultSensors(group._1), element)) + } + if(sharedConfig.parGroup != 0 && cells.size >= sharedConfig.parGroup ) { val groupList = groups.par - groupList.tasksupport = sharedMemory.config.groupThreadPool - groupList.foreach(group => group._2.foreach(element => simulate(time, cell, element))) + groupList.tasksupport = sharedConfig.groupThreadPool + groupList.foreach(simGroup) } else { - groups.foreach(group => group._2.foreach(element => simulate(time, cell, element))) + groups.foreach(simGroup) } } - private def simulate(time: Time, cell: Cell, element: (Group, Sensor, Option[GroundTruth])): Unit = { + private def simulate(time: Time, cell: Cell, prevTruth: Truth, resultTruth: Truth, resultSensors: Vector[SensorResult], element: (Group, Sensor, Option[GroundTruth])): Unit = { val group = element._1 val sensor = element._2 val groundTruth = element._3 // simulate the ground truth if required if(!groundTruth.isEmpty) { - groundTruth.get.simulate(cell,group, time, sharedMemory) + groundTruth.get.simulate(cell, time, prevTruth, resultTruth) } else if (group.parent.isEmpty) { // calc default ground truth if none was defined and no parent given which is None // this ensures a value when the sensor accesses the gt and prevents a deadlock - val v: Vector[Option[(SimulationBehaviour, Option[FunctionArgs])]] = Vector.fill(sharedMemory.config.duration)(None) - GroundTruth(Some(Behaviour(BehaviourVector(v)))).simulate(cell, group, time, sharedMemory) + val v: Vector[Option[(SimulationBehaviour, Option[FunctionArgs])]] = Vector.fill(time.duration)(None) + GroundTruth(group, Some(Behaviour(BehaviourVector(v)))).simulate(cell, time, prevTruth, resultTruth) } // simulate the sensor - sensor.simulate(cell, group, time, sharedMemory) + sensor.simulate(time, resultTruth, resultSensors) } } \ No newline at end of file diff --git a/src/main/scala/scim/datastruct/Configuration.scala b/src/main/scala/scim/datastruct/Configuration.scala index c18daf8..b2627f9 100644 --- a/src/main/scala/scim/datastruct/Configuration.scala +++ b/src/main/scala/scim/datastruct/Configuration.scala @@ -7,19 +7,31 @@ import scala.collection.parallel.ForkJoinTaskSupport // configuration //case class Configuration(simulation: Simulation, sensors: Vector[Sensor], groundTruths: Vector[GroundTruth], behaviours: Vector[Behaviour]) -case class Time(time: Int){ - def prev: Time = { Time(time - 1) } +// Note: case classes implement equality and compare their values and not the memory address +// It only compares vals from the constructor +case class Time(time: Int, duration: Int){ + def prev: Time = { Time(time - 1, duration) } def start: Boolean = { time == 0 } def get: Int = { time } } -case class SharedMemory( sensor: Map[Time, Map[SensorID, SensorResult]], - groundTruth: Map[Cell, Map[Time, Map[Group, Truth]]], - config: SimulationConfig) { - def getTruth(cell: Cell, time: Time, group: Group): Truth = { - groundTruth(cell)(time)(group) +case class SharedMemory( sensor: Map[Time, Map[Cell, Map[Group, Vector[SensorResult]]]], + groundTruth: Map[Time, Map[Cell, Map[Group, Truth]]], + config: SimulationConfig, cache: Boolean = true) { + // This could be spatially optimized to have a function to invoke a new simulation tick + // a template of Map[SensorID, SensorResult] would have to be kept and deep copied when + // another tick is simulated. this won't work for ground truth, as it may already be defined + // through the configuration. it can however be deleted once it is no longer required, tho this seems + // to be rather unbeneficial, as the space was already preallocated. Sensors however could be replaced + // or deleted, depending if a buffer or list is used. + + def getTruth(time: Time, cell: Cell, group: Group): Truth = { + groundTruth(time)(cell)(group) } - def getResult(time: Time): Map[SensorID, SensorResult] = { + def getResult(time: Time, cell: Cell, group: Group): Vector[SensorResult] = { + sensor(time)(cell)(group) + } + def getAllResults(time: Time): Map[Cell, Map[Group, Vector[SensorResult]]] = { sensor(time) } } @@ -48,29 +60,26 @@ case class SimulationConfig(duration: Int, runs: Int, tickfactor: Int = 1, val cellThreadPool: ForkJoinTaskSupport = new ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(parCellThreads)) val groupThreadPool: ForkJoinTaskSupport = new ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(parGroupThreads)) val sensorThreadPool: ForkJoinTaskSupport = new ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(parSensorThreads)) - // TODO: give truth their own threadpool and parallize the calculation } case class SensorResult(result: BroadcastObject[(Group, Cell, Option[Double])]) extends BroadcastHandler(result) case class SensorID(group: Group, Id: Int) case class Duration(simDuration: Int) case class TickFactor(factor: Int) // processed when parsing configuration, artifically place timings case class Cell(x: Int, y: Int) -case class Group(name: String, parentIn: Option[String] = None){ +case class Group(name: String, sensorParent: Option[String] = None){ + // the sensorParent serves as indicator for the parser that a ground truth is not required // FIXME: use parent as get function and parent.set as setter to make var private - var parent: Option[String] = parentIn // have to make this mutable to reassign it during parsing - def getParentOrThis: Group = { + var parent: Option[String] = sensorParent // have to make this mutable to reassign it during parsing + def getParentOrGroup: Group = { if(parent.isEmpty) { this } else { - Group(parent.get) + Group(parent.get, None) } } - } case class Truth(value: BroadcastObject[Option[Double]]) extends BroadcastHandler(value) case class FunctionArgs(args: Map[String, Any]) -// passing the group and therefore the behaviour during the simulation adds additional processing costs during runtime. -// Benefit would be fewer sensor objects. case class BehaviourVector(function: Vector[Option[(SimulationBehaviour, Option[FunctionArgs])]], defaultFunc: SimulationBehaviour = (_: Cell, _: Time, v: Option[Double], _: Option[FunctionArgs]) => v, @@ -90,29 +99,29 @@ case class Behaviour(behaviour: BehaviourVector){ } } -case class GroundTruth(behaviour: Option[Behaviour]) { - def simulate(cell: Cell, group: Group, time: Time, sharedMemory: SharedMemory): Unit = { - val result = sharedMemory.getTruth(cell, time, group.getParentOrThis) - // only calculate a new ground truth if a behaviour is defined - if(!behaviour.isEmpty) { - val prevTruth = sharedMemory.getTruth(cell, time.prev, group) - result.set(behaviour.get.run(time, cell, prevTruth)) - } else { - result.set(None) +case class GroundTruth(group: Group, behaviour: Option[Behaviour]) { + def simulate(cell: Cell, time: Time, prevTruth: Truth, result: Truth): Unit = { + // don't need a truth, if the sensor uses another, but this should not happen + // as the parser should not create a ground truth in this case + if (group.sensorParent.isEmpty) { + // only calculate a new ground truth if a behaviour is defined + if (!behaviour.isEmpty) { + result.set(behaviour.get.run(time, cell, prevTruth)) + } else { + result.set(None) + } } } } -case class Sensor(behaviour: Behaviour, count: SensorCount) { - def simulate(cell: Cell, group: Group, time: Time, sharedMemory: SharedMemory): Unit = { - val truth = sharedMemory.getTruth(cell, time, group.getParentOrThis) - val result = sharedMemory.getResult(time) - if(sharedMemory.config.parSensor != 0 && count.count >= sharedMemory.config.parSensor ) { +case class Sensor(group: Group, cell: Cell, count: SensorCount, behaviour: Behaviour, parSensor: Int, sensorThreadPool: ForkJoinTaskSupport) { + def simulate(time: Time, truth: Truth, results: Vector[SensorResult]): Unit = { + if(parSensor != 0 && parSensor <= count.count) { val sensorList = count.getVector.par - sensorList.tasksupport = sharedMemory.config.sensorThreadPool - sensorList.foreach(i => result(SensorID(group, i)).set((group, cell, behaviour.run(time, cell, truth)))) + sensorList.tasksupport = sensorThreadPool + sensorList.foreach(i => results(i).set((group, cell, behaviour.run(time, cell, truth)))) } else { - count.getVector.foreach(i => result(SensorID(group, i)).set((group, cell, behaviour.run(time, cell, truth)))) + count.getVector.foreach(i => results(i).set((group, cell, behaviour.run(time, cell, truth)))) } } }