[mod][fix] modified the sim to not pass the entire shared memory

- [mod] this change should be faster at the cost of memory
 - each underlying call only gets the required data instead of everything
 - [mod] renamed Group param to better reflect its purpose
 - [mod] changed Time to incorporate the total duration as well
 - [fix] fixed shared memory mapping to match simulation
This commit is contained in:
2020-01-29 01:19:10 -06:00
parent bd22a441f0
commit fee44785d4
3 changed files with 90 additions and 55 deletions

View File

@@ -194,7 +194,7 @@ class SimulationConfigParser(input: Map[String, Any], simulationBehaviours: Simu
val start: Int = truthObject(0).asInstanceOf[Int] val start: Int = truthObject(0).asInstanceOf[Int]
// val end: Option[Int] = None // Not used // val end: Option[Int] = None // Not used
truth.set(Some(truthObject(1).asInstanceOf[Double])) truth.set(Some(truthObject(1).asInstanceOf[Double]))
Map[Time, Truth](Time(start) -> truth) Map[Time, Truth](Time(start, config.duration) -> truth)
} }
case truthObject: Map[_, _] => { case truthObject: Map[_, _] => {
val truth: Truth = Truth(new BroadcastObject()) 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() val output: mutable.Map[Time, Truth] = mutable.Map()
timeLineVector.map(tuple => { timeLineVector.map(tuple => {
output(Time(tuple._1)) = truth output(Time(tuple._1, config.duration)) = truth
if (!tuple._2.isEmpty) { if (!tuple._2.isEmpty) {
endTruth.set(None) endTruth.set(None)
output(Time(tuple._2.get)) = endTruth output(Time(tuple._2.get, config.duration)) = endTruth
} }
}) })
output.toMap output.toMap

View File

@@ -1,56 +1,82 @@
package scim.components 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 import scim.lib.simulation.SimulationBehaviour
class Simulation(config: Configuration) { class Simulation(config: Configuration) {
private val sharedMemory = config.sharedMemory private val sharedMemory = config.sharedMemory
private val sharedConfig = sharedMemory.config
private val cells = config.cells private val cells = config.cells
val timeVector = Vector.tabulate(sharedMemory.config.duration)(t => Time(t))
def start(): Unit = { 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 val cellList = cells.par
cellList.tasksupport = sharedMemory.config.cellThreadPool cellList.tasksupport = sharedConfig.cellThreadPool
timeVector.foreach(time => cellList.foreach(cell => simGroups(time, cell._1, cell._2))) cellList.foreach(simCell)
} else { } 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) // 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 // 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 // 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. // 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. // Any subsequent children can start in the same manner, when their parent is done with the ground truth.
// -- // :
// other possibility: // only run ground truth in list, have sensors in their respective groups
// run simulateGT foreach first, then run simulateSensor foreach (dont know if there is much benefit tho) // requires groups to be split into two elements: sensor and ground truth
if(sharedMemory.config.parGroup != 0 && cells.size >= sharedMemory.config.parGroup ) { // 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 val groupList = groups.par
groupList.tasksupport = sharedMemory.config.groupThreadPool groupList.tasksupport = sharedConfig.groupThreadPool
groupList.foreach(group => group._2.foreach(element => simulate(time, cell, element))) groupList.foreach(simGroup)
} else { } 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 group = element._1
val sensor = element._2 val sensor = element._2
val groundTruth = element._3 val groundTruth = element._3
// simulate the ground truth if required // simulate the ground truth if required
if(!groundTruth.isEmpty) { if(!groundTruth.isEmpty) {
groundTruth.get.simulate(cell,group, time, sharedMemory) groundTruth.get.simulate(cell, time, prevTruth, resultTruth)
} else if (group.parent.isEmpty) { } else if (group.parent.isEmpty) {
// calc default ground truth if none was defined and no parent given which is None // 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 // 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) val v: Vector[Option[(SimulationBehaviour, Option[FunctionArgs])]] = Vector.fill(time.duration)(None)
GroundTruth(Some(Behaviour(BehaviourVector(v)))).simulate(cell, group, time, sharedMemory) GroundTruth(group, Some(Behaviour(BehaviourVector(v)))).simulate(cell, time, prevTruth, resultTruth)
} }
// simulate the sensor // simulate the sensor
sensor.simulate(cell, group, time, sharedMemory) sensor.simulate(time, resultTruth, resultSensors)
} }
} }

View File

@@ -7,19 +7,31 @@ import scala.collection.parallel.ForkJoinTaskSupport
// configuration // configuration
//case class Configuration(simulation: Simulation, sensors: Vector[Sensor], groundTruths: Vector[GroundTruth], behaviours: Vector[Behaviour]) //case class Configuration(simulation: Simulation, sensors: Vector[Sensor], groundTruths: Vector[GroundTruth], behaviours: Vector[Behaviour])
case class Time(time: Int){ // Note: case classes implement equality and compare their values and not the memory address
def prev: Time = { Time(time - 1) } // 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 start: Boolean = { time == 0 }
def get: Int = { time } def get: Int = { time }
} }
case class SharedMemory( sensor: Map[Time, Map[SensorID, SensorResult]], case class SharedMemory( sensor: Map[Time, Map[Cell, Map[Group, Vector[SensorResult]]]],
groundTruth: Map[Cell, Map[Time, Map[Group, Truth]]], groundTruth: Map[Time, Map[Cell, Map[Group, Truth]]],
config: SimulationConfig) { config: SimulationConfig, cache: Boolean = true) {
def getTruth(cell: Cell, time: Time, group: Group): Truth = { // This could be spatially optimized to have a function to invoke a new simulation tick
groundTruth(cell)(time)(group) // 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) 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 cellThreadPool: ForkJoinTaskSupport = new ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(parCellThreads))
val groupThreadPool: ForkJoinTaskSupport = new ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(parGroupThreads)) val groupThreadPool: ForkJoinTaskSupport = new ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(parGroupThreads))
val sensorThreadPool: ForkJoinTaskSupport = new ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(parSensorThreads)) 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 SensorResult(result: BroadcastObject[(Group, Cell, Option[Double])]) extends BroadcastHandler(result)
case class SensorID(group: Group, Id: Int) case class SensorID(group: Group, Id: Int)
case class Duration(simDuration: Int) case class Duration(simDuration: Int)
case class TickFactor(factor: Int) // processed when parsing configuration, artifically place timings case class TickFactor(factor: Int) // processed when parsing configuration, artifically place timings
case class Cell(x: Int, y: Int) 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 // 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 var parent: Option[String] = sensorParent // have to make this mutable to reassign it during parsing
def getParentOrThis: Group = { def getParentOrGroup: Group = {
if(parent.isEmpty) { if(parent.isEmpty) {
this this
} else { } else {
Group(parent.get) Group(parent.get, None)
} }
} }
} }
case class Truth(value: BroadcastObject[Option[Double]]) extends BroadcastHandler(value) case class Truth(value: BroadcastObject[Option[Double]]) extends BroadcastHandler(value)
case class FunctionArgs(args: Map[String, Any]) 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])]], case class BehaviourVector(function: Vector[Option[(SimulationBehaviour, Option[FunctionArgs])]],
defaultFunc: SimulationBehaviour = (_: Cell, _: Time, v: Option[Double], _: Option[FunctionArgs]) => v, 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]) { case class GroundTruth(group: Group, behaviour: Option[Behaviour]) {
def simulate(cell: Cell, group: Group, time: Time, sharedMemory: SharedMemory): Unit = { def simulate(cell: Cell, time: Time, prevTruth: Truth, result: Truth): Unit = {
val result = sharedMemory.getTruth(cell, time, group.getParentOrThis) // 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 // only calculate a new ground truth if a behaviour is defined
if(!behaviour.isEmpty) { if (!behaviour.isEmpty) {
val prevTruth = sharedMemory.getTruth(cell, time.prev, group)
result.set(behaviour.get.run(time, cell, prevTruth)) result.set(behaviour.get.run(time, cell, prevTruth))
} else { } else {
result.set(None) result.set(None)
} }
} }
}
} }
case class Sensor(behaviour: Behaviour, count: SensorCount) { case class Sensor(group: Group, cell: Cell, count: SensorCount, behaviour: Behaviour, parSensor: Int, sensorThreadPool: ForkJoinTaskSupport) {
def simulate(cell: Cell, group: Group, time: Time, sharedMemory: SharedMemory): Unit = { def simulate(time: Time, truth: Truth, results: Vector[SensorResult]): Unit = {
val truth = sharedMemory.getTruth(cell, time, group.getParentOrThis) if(parSensor != 0 && parSensor <= count.count) {
val result = sharedMemory.getResult(time)
if(sharedMemory.config.parSensor != 0 && count.count >= sharedMemory.config.parSensor ) {
val sensorList = count.getVector.par val sensorList = count.getVector.par
sensorList.tasksupport = sharedMemory.config.sensorThreadPool sensorList.tasksupport = sensorThreadPool
sensorList.foreach(i => result(SensorID(group, i)).set((group, cell, behaviour.run(time, cell, truth)))) sensorList.foreach(i => results(i).set((group, cell, behaviour.run(time, cell, truth))))
} else { } 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))))
} }
} }
} }