Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Issue 97] Sensor locations may transmit multiple sensor data types #99

Merged
merged 9 commits into from
Feb 18, 2015
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ object Sensor {
/**
* Used to model a full sensor network of locations that may transmit data to us. Instances of the case class represent
* sensor signals at a given point in time.
*
* Sensor networks relate a location and a point (an instance or position index) to the `SensorData` they produce.
*/
case class SensorNet(wrist: SensorData, waist: SensorData, foot: SensorData, chest: SensorData, unknown: SensorData) {
val toMap = Map[SensorDataSourceLocation, SensorData](
case class SensorNet(wrist: Vector[SensorData], waist: Vector[SensorData], foot: Vector[SensorData], chest: Vector[SensorData], unknown: Vector[SensorData]) {
val toMap = Map[SensorDataSourceLocation, Vector[SensorData]](
SensorDataSourceLocationWrist -> wrist,
SensorDataSourceLocationWaist -> waist,
SensorDataSourceLocationFoot -> foot,
Expand All @@ -62,7 +64,7 @@ case class SensorNet(wrist: SensorData, waist: SensorData, foot: SensorData, che
}

object SensorNet {
def apply(sensorMap: Map[SensorDataSourceLocation, SensorData]) =
def apply(sensorMap: Map[SensorDataSourceLocation, Vector[SensorData]]) =
new SensorNet(
sensorMap(SensorDataSourceLocationWrist),
sensorMap(SensorDataSourceLocationWaist),
Expand All @@ -74,9 +76,11 @@ object SensorNet {

/**
* Location or column slice through a sensor network.
*
* Sensor points produce `SensorNetValue`. They have a location and an instance or position index (the point).
*/
case class SensorNetValue(wrist: SensorValue, waist: SensorValue, foot: SensorValue, chest: SensorValue, unknown: SensorValue) {
val toMap = Map[SensorDataSourceLocation, SensorValue](
case class SensorNetValue(wrist: Vector[SensorValue], waist: Vector[SensorValue], foot: Vector[SensorValue], chest: Vector[SensorValue], unknown: Vector[SensorValue]) {
val toMap = Map[SensorDataSourceLocation, Vector[SensorValue]](
SensorDataSourceLocationWrist -> wrist,
SensorDataSourceLocationWaist -> waist,
SensorDataSourceLocationFoot -> foot,
Expand All @@ -86,7 +90,7 @@ case class SensorNetValue(wrist: SensorValue, waist: SensorValue, foot: SensorVa
}

object SensorNetValue {
def apply(sensorMap: Map[SensorDataSourceLocation, SensorValue]) =
def apply(sensorMap: Map[SensorDataSourceLocation, Vector[SensorValue]]) =
new SensorNetValue(
sensorMap(SensorDataSourceLocationWrist),
sensorMap(SensorDataSourceLocationWaist),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,19 @@ class UserExercisesClassifier(sessionProps: SessionProperties, modelProps: Props
// TODO: refactor code so that the following assumptions may be weakened further!
case sdwls: ClassifyExerciseEvt =>
require(
sdwls.sensorData.map(_.location).toSet == Sensor.sourceLocations && sdwls.sensorData.map(_.location).size == Sensor.sourceLocations.size,
"for each sensor location, there is a unique and corresponding member in the sensor data for the `ClassifyExerciseEvt` instance"
sdwls.sensorData.map(_.location).toSet == Sensor.sourceLocations && sdwls.sensorData.forall(_.data.nonEmpty),
"all sensor locations are present in the `ClassifyExerciseEvt` instance and have data"
)
val sensorMap = sdwls.sensorData.groupBy(_.location).mapValues(_.flatMap(_.data))
val blockSize = sensorMap(SensorDataSourceLocationWrist).length
// (SensorDataSourceLocation, Int) -> List[SensorData]
val sensorMap: Map[SensorDataSourceLocation, List[List[SensorData]]] = sdwls.sensorData.groupBy(_.location).mapValues(_.map(_.data))
val blockSize = sensorMap(SensorDataSourceLocationWrist).head.length
require(
sensorMap.values.forall(_.length == blockSize),
"all sensor data locations have a common data length"
sensorMap.values.forall(_.forall(_.length == blockSize)),
"all sensor data location points have a common data length"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this is a reasonable assumption now, the number of samples can differ in the future (e.g. switching the watch to a 50 Hz sampling rate).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked with Carl, re-sampling should be done, so that every sensor values are taken at the same frequency. For periods when there is no data (the sensor does not transmit anything), a good central values should be inserted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened issue 103 (#103) to address the possible sampling frequency difference of sensors.

)

(0 until blockSize).foreach { block =>
val sensorEvent = sensorMap.map { case (loc, _) => (loc, sensorMap(loc)(block)) }.toMap
val sensorEvent = sensorMap.map { case (loc, data) => (loc, (0 until data.size).map(point => sensorMap(loc)(point)(block)).toVector) }.toMap

model.tell(SensorNet(sensorEvent), sender())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,20 +385,20 @@ abstract class ExerciseModel(name: String, sessionProps: SessionProperties, toWa
// TODO: refactor code so that the following assumptions may be weakened further!
case event: SensorNet =>
require(
event.toMap.values.forall(_.values.nonEmpty),
"all sensors in a network should produce some sensor value"
event.toMap.values.forall(_.forall(_.values.nonEmpty)),
"all sensor points in a network should produce some sensor value"
)
val blockSize = event.toMap.values.head.values.length
val blockSize = event.toMap.values.head.head.values.length
require(
event.toMap.values.forall(_.values.length == blockSize),
"all sensors in a network produce the same number of sensor values"
event.toMap.values.forall(_.forall(_.values.length == blockSize)),
"all sensor points in a network produce the same number of sensor values"
)
require(
event.toMap.values.forall(_.samplingRate == samplingRate),
"all sensors have a fixed known sample rate"
event.toMap.values.forall(_.forall(_.samplingRate == samplingRate)),
"all sensor points have a fixed known sample rate"
)

val sensorEvents = (0 until blockSize).map(block => SensorNetValue(event.toMap.mapValues(_.values(block))))
val sensorEvents = (0 until blockSize).map(block => SensorNetValue(event.toMap.mapValues(data => (0 until data.size).map(point => data(point).values(block)).toVector)))

for (evt <- sensorEvents) {
self.tell(evt, sender())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,19 @@ class RandomExerciseModel(sessionProps: SessionProperties)
override def aroundReceive(receive: Receive, msg: Any) = msg match {
case event: SensorNet =>
event.toMap.foreach { x => (x: @unchecked) match {
case (location, AccelerometerData(sr, values)) =>
val xs = values.map(_.x)
val ys = values.map(_.y)
val zs = values.map(_.z)
println(s"****** Acceleration $location | X: (${xs.min}, ${xs.max}), Y: (${ys.min}, ${ys.max}), Z: (${zs.min}, ${zs.max})")

case (location, RotationData(_, values)) =>
val xs = values.map(_.x)
val ys = values.map(_.y)
val zs = values.map(_.z)
println(s"****** Rotation $location | X: (${xs.min}, ${xs.max}), Y: (${ys.min}, ${ys.max}), Z: (${zs.min}, ${zs.max})")
case (location, data: Vector[_]) =>
for ((AccelerometerData(_, values), point) <- data.zipWithIndex) {
val xs = values.map(_.x)
val ys = values.map(_.y)
val zs = values.map(_.z)
println(s"****** Acceleration $location@$point | X: (${xs.min}, ${xs.max}), Y: (${ys.min}, ${ys.max}), Z: (${zs.min}, ${zs.max})")
}
for ((RotationData(_, values), point) <- data.zipWithIndex) {
val xs = values.map(_.x)
val ys = values.map(_.y)
val zs = values.map(_.z)
println(s"****** Rotation $location@$point | X: (${xs.min}, ${xs.max}), Y: (${ys.min}, ${ys.max}), Z: (${zs.min}, ${zs.max})")
}
}}
super.aroundReceive(receive, msg)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import com.eigengo.lift.exercise.classifiers.ExerciseModel
* Essentially, we view our model traces as being streams here. As a result, all queries are evaluated (on the actual
* stream) from the time point they are received by the model.
*/
abstract class StandardExerciseModel(sessionProps: SessionProperties, toWatch: Set[Query] = Set.empty)
abstract class StandardExerciseModel(sessionProps: SessionProperties, tapSensor: SensorDataSourceLocation, toWatch: Set[Query] = Set.empty)
extends ExerciseModel("tap", sessionProps, toWatch)
with StandardEvaluation
with ActorLogging {
Expand All @@ -24,10 +24,8 @@ abstract class StandardExerciseModel(sessionProps: SessionProperties, toWatch: S
import ClassificationAssertions._
import FlowGraphImplicits._

// Workflow for recognising 'tap' gestures..
// Workflow for recognising 'tap' gestures that are detected via `tapSensor`
object Tap extends GestureWorkflows("tap", context.system.settings.config)
// ..that are detected via wrist sensors
val tapSensor = SensorDataSourceLocationWrist

/**
* Monitor wrist sensor and add in tap gesture detection.
Expand All @@ -43,7 +41,9 @@ abstract class StandardExerciseModel(sessionProps: SessionProperties, toWatch: S

in ~> split

split ~> Flow[SensorNetValue].map(_.toMap(tapSensor).asInstanceOf[AccelerometerValue]).via(classifier.map(_.toSet)) ~> merge.left
split ~> Flow[SensorNetValue]
.mapConcat(_.toMap(tapSensor).find(_.isInstanceOf[AccelerometerValue]).asInstanceOf[Option[AccelerometerValue]].toList)
.via(classifier.map(_.toSet)) ~> merge.left

split ~> merge.right

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,24 @@ trait ExerciseGenerators {
for {
sessionProps <- SessionPropertiesGen
events <- listOfN(Sensor.sourceLocations.size, SensorDataWithLocationGen(width, height)).map(_.zipWithIndex.map { case (sdwl, n) => sdwl.copy(location = Sensor.sourceLocations.toList(n)) })
} yield ClassifyExerciseEvt(sessionProps, events)
data <- SensorDataWithLocationGen(width, height)
} yield ClassifyExerciseEvt(sessionProps, events :+ data)

def SensorNetGen(size: Int): Gen[SensorNet] =
for {
sensorMap <- listOfN(Sensor.sourceLocations.size, SensorDataGen(size)).map(_.zipWithIndex.map { case (sv, n) => (Sensor.sourceLocations.toList(n), sv) }.toMap[SensorDataSourceLocation, SensorData])
sensorMap <- listOfN(Sensor.sourceLocations.size, SensorDataGen(size)).map(_.zipWithIndex.map { case (sv, n) => (Sensor.sourceLocations.toList(n), Vector(sv)) }.toMap[SensorDataSourceLocation, Vector[SensorData]])
} yield SensorNet(sensorMap)

def MultiSensorNetGen(size: Int): Gen[SensorNet] =
for {
sensorNet <- SensorNetGen(size)
location <- SensorDataSourceLocationGen
value <- SensorDataGen(size)
} yield SensorNet(sensorNet.toMap + (location -> (sensorNet.toMap(location) :+ value)))

val SensorNetValueGen: Gen[SensorNetValue] =
for {
sensorMap <- listOfN(Sensor.sourceLocations.size, SensorValueGen).map(_.zipWithIndex.map { case (sv, n) => (Sensor.sourceLocations.toList(n), sv) }.toMap[SensorDataSourceLocation, SensorValue])
sensorMap <- listOfN(Sensor.sourceLocations.size, SensorValueGen).map(_.zipWithIndex.map { case (sv, n) => (Sensor.sourceLocations.toList(n), Vector(sv)) }.toMap[SensorDataSourceLocation, Vector[SensorValue]])
} yield SensorNetValue(sensorMap)

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ class UserExercisesClassifierTest
}

property("UserExercisesClassifier should correctly 'slice up' ClassifyExerciseEvt into SensorNet events") {
val width = 2//0
val height = 3//0
val width = 20
val height = 30

forAll(ClassifyExerciseEvtGen(width, height)) { (event: ClassifyExerciseEvt) =>
val modelProbe = TestProbe()
Expand All @@ -39,10 +39,14 @@ class UserExercisesClassifierTest

val msgs = modelProbe.receiveN(width).asInstanceOf[Vector[SensorNet]].toList
for (result <- msgs) {
assert(result.toMap.values.forall(_.values.length == height))
assert(result.toMap.values.forall(_.forall(_.values.length == height)))
}
for (sensor <- Sensor.sourceLocations) {
assert(msgs.flatMap(_.toMap(sensor).values) == event.sensorData.find(_.location == sensor).get.data.flatMap(_.values))
val numberOfPoints = event.sensorData.count(_.location == sensor)

for (point <- 0 until numberOfPoints) {
assert(msgs.flatMap(_.toMap(sensor)(point).values) == event.sensorData.filter(_.location == sensor).map(_.data.flatMap(_.values)).toVector(point))
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,18 @@ class ExerciseModelTest
}
})

forAll(SensorNetGen(30)) { (rawEvent: SensorNet) =>
val event = SensorNet(rawEvent.toMap.mapValues(evt => new SensorData { val samplingRate = rate; val values = evt.values }))
forAll(MultiSensorNetGen(30)) { (rawEvent: SensorNet) =>
val event = SensorNet(rawEvent.toMap.mapValues(_.map(evt => new SensorData { val samplingRate = rate; val values = evt.values })))

model ! event

val msgs = modelProbe.receiveN(event.wrist.values.length).asInstanceOf[Vector[SensorNetValue]].toList
val msgs = modelProbe.receiveN(event.wrist.head.values.length).asInstanceOf[Vector[SensorNetValue]].toList
for (sensor <- Sensor.sourceLocations) {
assert(msgs.map(_.toMap(sensor)) == event.toMap(sensor).values)
val numberOfPoints = rawEvent.toMap(sensor).length

for (point <- 0 until numberOfPoints) {
assert(msgs.map(_.toMap(sensor)(point)) == event.toMap(sensor)(point).values)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import akka.testkit.TestActorRef
import com.eigengo.lift.exercise.UserExercisesClassifier.{Tap => TapEvent}
import com.eigengo.lift.exercise.classifiers.ExerciseModel
import com.eigengo.lift.exercise.classifiers.workflows.ClassificationAssertions.{NegGesture, Gesture, BindToSensors}
import com.eigengo.lift.exercise.{AccelerometerValue, SensorNetValue, SessionProperties}
import com.eigengo.lift.exercise.{SensorDataSourceLocationWrist, AccelerometerValue, SensorNetValue, SessionProperties}
import com.typesafe.config.ConfigFactory
import java.text.SimpleDateFormat
import scala.concurrent.{ExecutionContext, Future}
Expand Down Expand Up @@ -37,7 +37,7 @@ class StandardExerciseModelTest extends AkkaSpec(ConfigFactory.load("classificat
"StandardExerciseModel workflow" must {

def component(in: Source[SensorNetValue], out: Sink[BindToSensors]) = {
val workflow = TestActorRef(new StandardExerciseModel(sessionProps) with SMTInterface {
val workflow = TestActorRef(new StandardExerciseModel(sessionProps, SensorDataSourceLocationWrist) with SMTInterface {
def makeDecision(query: Query, value: QueryValue, result: Boolean) = TapEvent
def simplify(query: Query)(implicit ec: ExecutionContext) = Future(query)
def satisfiable(query: Query)(implicit ec: ExecutionContext) = Future(true)
Expand All @@ -46,7 +46,8 @@ class StandardExerciseModelTest extends AkkaSpec(ConfigFactory.load("classificat
}

"correctly detect wrist sensor taps" in {
val msgs: List[SensorNetValue] = accelerometerData.map(d => SensorNetValue(d, dummyValue, dummyValue, dummyValue, dummyValue))
// FIXME: is this correct?
val msgs: List[SensorNetValue] = accelerometerData.map(d => SensorNetValue(Vector(d), Vector(dummyValue), Vector(dummyValue), Vector(dummyValue), Vector(dummyValue)))
val tapIndex = List(256 until 290, 341 until 344, 379 until 408, 546 until 577).flatten.toList
// Simulate source that outputs messages and then blocks
val in = PublisherProbe[SensorNetValue]()
Expand Down