diff --git a/src/scala/build.sbt b/src/scala/build.sbt index 947fa52..7203418 100644 --- a/src/scala/build.sbt +++ b/src/scala/build.sbt @@ -31,6 +31,8 @@ lazy val root: Project = (project in file(".")). dependsOn(minimal % "test->compile") // To build and run examples +// run with sbt 'examples/runMain org.openworm.trackercommons.examples.ExampleName' +// where `ExampleName` is one of the example files, e.g. `CountAnimals` lazy val examples: Project = (project in file("src/examples")). settings(commonSettings: _*). dependsOn(root) diff --git a/src/scala/src/examples/CircleTravel.scala b/src/scala/src/examples/CircleTravel.scala new file mode 100644 index 0000000..dd4a4b6 --- /dev/null +++ b/src/scala/src/examples/CircleTravel.scala @@ -0,0 +1,31 @@ +package org.openworm.trackercommons.examples + +import org.openworm.trackercommons._ + +object CircleTravel extends ExampleTemplate[String] { + val defaultLocation = "" + + def run(args: Array[String]): Either[String, String] = { + val circle = Create.wcon. + setMeta( + Create.meta("circle example").addSoftware( Create.software.name("CircleTravel.scala") ) + ). + addData({ + val w = Create.worm("1").add(0.0, Array(1.0), Array(0.0)) + for (i <- 1 until 12) + w.add(i.toDouble, Array(math.cos(i*math.Pi/6)), Array(math.sin(i*math.Pi/6))) + w + }). + setUnits(). + result + val output = defaultOutput + ReadWrite.write(circle, output) match { + case Left(err) => Left(err) + case _ => Right(output.getPath) + } + } + + def succeed(s: String) { + println(f"Wrote a circularly traveling point animal to $s") + } +} diff --git a/src/scala/src/examples/CountAnimals.scala b/src/scala/src/examples/CountAnimals.scala index c6a0889..fa5685d 100644 --- a/src/scala/src/examples/CountAnimals.scala +++ b/src/scala/src/examples/CountAnimals.scala @@ -1,11 +1,17 @@ package org.openworm.trackercommons.examples +import org.openworm.trackercommons._ + object CountAnimals extends ExampleTemplate[Int] { - val defaultPath = "../../tests/count_animals.wcon" + val defaultLocation = "../../tests/examples/count_animals.wcon" def run(args: Array[String]): Either[String, Int] = existingFile(args) match { case Left(msg) => Left(msg) - case Right(file) => Right(-1) + case Right(file) => + ReadWrite.readOne(file) match { + case Left(err) => Left(err.toString) + case Right(ds) => Right(ds.data.map(_.id).toSet.size) + } } def succeed(n: Int) { diff --git a/src/scala/src/examples/ExampleTemplate.scala b/src/scala/src/examples/ExampleTemplate.scala index eee7710..c75a8b6 100644 --- a/src/scala/src/examples/ExampleTemplate.scala +++ b/src/scala/src/examples/ExampleTemplate.scala @@ -1,12 +1,17 @@ package org.openworm.trackercommons.examples + +case class Selected(count: Int, where: String) {} + trait ExampleTemplate[A] { def run(args: Array[String]): Either[String, A] def succeed(a: A): Unit - def defaultPath: String + def defaultLocation: String + def defaultOutput: java.io.File = (new java.io.File("result.wcon")).getCanonicalFile + // Expect `run` to call this to get a single file def existingFile(args: Array[String]): Either[String, java.io.File] = { - val theFileName = args.headOption.getOrElse(defaultPath) + val theFileName = args.headOption.getOrElse(defaultLocation) val theFile = new java.io.File(theFileName) if (!theFile.exists) { Left(Seq( @@ -20,7 +25,7 @@ trait ExampleTemplate[A] { def main(args: Array[String]) { run(args) match { case Left(msg) => println(msg); sys.exit(1) - case Right(n) => + case Right(a) => succeed(a) } } } diff --git a/src/scala/src/examples/MovementSelect.scala b/src/scala/src/examples/MovementSelect.scala new file mode 100644 index 0000000..d2fe10d --- /dev/null +++ b/src/scala/src/examples/MovementSelect.scala @@ -0,0 +1,70 @@ +package org.openworm.trackercommons.examples + +import org.openworm.trackercommons._ + +object MovementSelect extends ExampleTemplate[Selected] { + val defaultLocation = "../../tests/examples/all_movements.wcon" + + def sq(x: Double) = x*x + + def movesFar(d: Data): Boolean = { + if (d.ts.length < 2) return false + + val cx, cy, l = new Array[Double](d.ts.length) + for (i <- l.indices) { + val ni = d.spineN(i) + if (ni < 1) { + cx(i) = Double.NaN + cy(i) = Double.NaN + l(i) = 0 + } + else { + cx(i) = if (d.cxs.length == 0) (d.x(i, 0) + d.x(i, ni-1))/2 else d.cxs(i) + cy(i) = if (d.cys.length == 0) (d.y(i, 0) + d.y(i, ni-1))/2 else d.cys(i) + l(i) = math.sqrt(sq(d.x(i, 0) - d.x(i, ni-1)) + sq(d.y(i, 0) - d.y(i, ni-1))) + } + } + + val longest = l.max + if (longest == 0) return false // Reject worms without a size + + var i0 = 0 + while (i0 < cx.length && (cx(i0).isNaN || cy(i0).isNaN)) i0 += 1 // i0 is at first valid position + var i1 = cx.length -1 + while (i1 > i0 && (cx(i1).isNaN || cy(i1).isNaN)) i1 -= 1 // i1 at last valid position + if (i1 <= i0) return false // Didn't go anywhere valid + + var distmax = 0.0 + var i = i0 + 1 + while (i <= i1) { + while (cx(i).isNaN || cy(i).isNaN) i += 1 + val dist = sq(cx(i) - cx(i0)) + sq(cy(i) - cy(i0)) + if (dist > distmax) distmax = dist + i += 1 + } + distmax = math.sqrt(distmax) + + distmax > longest // We have to have traveled at least one nose-to-tail distance + } + + def run(args: Array[String]): Either[String, Selected] = existingFile(args) match { + case Left(msg) => Left(msg) + case Right(file) => + ReadWrite.readOne(file) match { + case Left(err) => Left(err.toString) + case Right(ds) => + val selected = ds.groupByIDs().flatMap(d => if (movesFar(d)) Some(d) else None) + val output = defaultOutput + ReadWrite.write(selected, output) match { + case Left(err) => Left(err) + case _ => Right( + Selected(selected.data.length, output.getPath) + ) + } + } + } + + def succeed(s: Selected) { + println(f"Found ${s.count} records; wrote to ${s.where}") + } +} diff --git a/src/scala/src/main/scala/Create.scala b/src/scala/src/main/scala/Create.scala index 9124496..609d623 100644 --- a/src/scala/src/main/scala/Create.scala +++ b/src/scala/src/main/scala/Create.scala @@ -123,6 +123,9 @@ object Create { for (d <- more) w = w.addData(d) w } + def addData(builder: DataBuilder[YesData]): MakeWcon[U, YesData, F] = addData(builder.result) + def addData(builder1: DataBuilder[YesData], builder2: DataBuilder[YesData], more: DataBuilder[YesData]*): MakeWcon[U, YesData, F] = + addData(builder1.result, builder2.result, more.map(_.result): _*) def dropData: MakeWcon[U, NoData, F] = { stale = false; new MakeWcon[U, NoData, F](building.copy(data = Array.empty), 0) } def putCustom(key: String, value: Json) = { @@ -221,7 +224,8 @@ object Create { def result(implicit ev: I =:= YesID) = underlying } - def meta() = new MakeMeta[NoID](Metadata.empty) + def meta(): MakeMeta[NoID] = new MakeMeta[NoID](Metadata.empty) + def meta(id: String): MakeMeta[YesID] = meta().setID(id) final class MakeLab private[trackercommons] (val result: Laboratory) { def isEmpty = result.isEmpty diff --git a/src/scala/src/main/scala/Data.scala b/src/scala/src/main/scala/Data.scala index 5ddfbe6..1778ae3 100644 --- a/src/scala/src/main/scala/Data.scala +++ b/src/scala/src/main/scala/Data.scala @@ -279,8 +279,9 @@ extends AsJson with Customizable[Data] { } def reshaped(reshaper: Reshape, magic: Magic = Magic.expand, unshaped: Option[Unshaped] = None): Option[Data] = { - if (reshaper.length != 1) return None + if (reshaper.sizes.length != 1) return None val nts = reshaper(Array(ts)) + if (nts.length == 0) return None val nxd = reshaper(Array(xDatas)) val nyd = reshaper(Array(yDatas)) val nrx = reshaper(Array(rxs)) @@ -476,9 +477,9 @@ object Data extends FromJson[Data] { private def BAD(msg: String): Either[JastError, Nothing] = Left(JastError("Invalid data entries: " + msg)) private def IBAD(id: String, msg: String): Either[JastError, Nothing] = - BAD("Data points for " + id + " have " + msg) + BAD("Data points for " + id + " are wrong because " + msg) private def MYBAD(id: String, t: Double, msg: String): Either[JastError, Nothing] = - BAD("Data point for " + id + " at time " + t + " has " + msg) + BAD("Data point for " + id + " at time " + t + " is wrong because " + msg) private[trackercommons] val emptyD = new Array[Double](0) private[trackercommons] val zeroD = Array(0.0) @@ -783,7 +784,7 @@ object Data extends FromJson[Data] { case Some(us) => val u = new Unshaped val ans = concat(ds, magic, Some(u)) - us += u + if (u.mistakes.nonEmpty) us += u id -> ans case _ => id -> concat(ds, magic) diff --git a/src/scala/src/main/scala/DataSet.scala b/src/scala/src/main/scala/DataSet.scala index a373169..bc453ac 100644 --- a/src/scala/src/main/scala/DataSet.scala +++ b/src/scala/src/main/scala/DataSet.scala @@ -9,6 +9,26 @@ extends AsJson with Customizable[DataSet] { def map(f: Data => Data) = new DataSet(meta, unitmap, data.map(f), files, custom) def flatMap(f: Data => Option[Data]) = new DataSet(meta, unitmap, data.flatMap(x => f(x)), files, custom) + def groupByIDs(unshaped: Option[collection.mutable.ArrayBuffer[Custom.Unshaped]] = None): DataSet = { + val groups = data.zipWithIndex.groupBy(_._1.id).toMap + if (groups.size == data.length) this + else { + val groupedData = groups.map{ case (_, vs) => + val ix = vs.map(_._2).min + val ds = vs.map(_._1) + val re = Reshape.sortSet(ds.map(_.ts), deduplicate = true) + if (unshaped.nonEmpty) { + val u = new Custom.Unshaped + val join = Data.join(re, ds, unshaped = Some(u)) + if (u.mistakes.nonEmpty) unshaped.get += u + ix -> join + } + else ix -> Data.join(re, ds) + }.toArray.sortBy(_._1).flatMap(_._2) + this.copy(data = groupedData) + } + } + def customFn(f: Json.Obj => Json.Obj) = copy(custom = f(custom)) def json = unitmap.unfix( diff --git a/src/scala/src/main/scala/Reshape.scala b/src/scala/src/main/scala/Reshape.scala index f992094..aa35afe 100644 --- a/src/scala/src/main/scala/Reshape.scala +++ b/src/scala/src/main/scala/Reshape.scala @@ -71,9 +71,9 @@ object Reshape { n } val elements = { - var a = new Array[Int](count) + var a = new Array[Int](2*count) var i, j = 0 - while (i < indicator.length) { if (indicator(i)) { a(j) = i; j += 1 }; i +=1 } + while (i < indicator.length) { if (indicator(i)) { a(j) = 0; j += 1; a(j) = i; j += 1 }; i += 1 } a } new Reshape(elements, Array(indicator.length)) diff --git a/tests/examples/all_movements.wcon b/tests/examples/all_movements.wcon new file mode 100644 index 0000000..006a542 --- /dev/null +++ b/tests/examples/all_movements.wcon @@ -0,0 +1,29 @@ +{ + "metadata": { + "protocol": "Six animals that travel different distances." + }, + "units": { "t":"s", "x":"mm", "y":"mm" }, + "data": [ + { "id":"1", "t":[0, 1, 2, 3], + "x":[[1,2], [3,4], [5,6], [7,8]], + "y":[[3,4], [4,3], [3,2], [2,3]], + "comment":"Length = sqrt(2), travels more than 6 (include)" + }, + { "id":"2", "t":[0, 1, 2, 3], + "x":[[0,5], [1,6], [0,5], [-1,4]], + "y":[[-3,3], [-3,2], [-3,2], [-2,3]], + "comment":"Length over 6, travels less than 3 (reject)" + }, + { "id":"3", "t":[0], "x":[[1,3]], "y":[[3,5]] }, + { "id":"3", "t":[1], "x":[[2,4]], "y":[[3,5]] }, + { "id":"3", "t":[2], "x":[[3,5]], "y":[[3,5]] }, + { "id":"3", "t":[3], "x":[[4,6]], "y":[[3,5]] }, + { "id":"4", "t":[2], "x":[[-5,-5]], "y":[[3,4]] }, + { "id":"5", "t":[0, 1, 2, 3], + "x":[[2,2,2], [2,2,2], [2,2,2], [2,2,2]], + "y":[[4,4,4], [4,4,4], [4,4,4], [4,4,4]] }, + { "id":"6", "t":[0, 1, 2, 3], + "x":[[4], [4], [4], [4]], + "y":[[2], [2], [2], [2]] } + ] +} diff --git a/tests/examples/all_times.wcon b/tests/examples/all_times.wcon new file mode 100644 index 0000000..4367c04 --- /dev/null +++ b/tests/examples/all_times.wcon @@ -0,0 +1,27 @@ +{ + "metadata": { + "protocol": "Six animals present for different lengths of time." + }, + "units": { "t":"s", "x":"mm", "y":"mm" }, + "data": [ + { "id":"1", "t":[0, 8, 16, 24, 32, 40], + "x":[[1,2], [3,4], [5,6], [7,8], [9,8], [7,6]], + "y":[[3,4], [4,3], [3,2], [2,3], [1,2], [2,1]] + }, + { "id":"2", "t":[0, 3, 6, 9, 12, 15], + "x":[[8,9], [7,9], [6,9], [6,8], [6,7], [6,6]], + "y":[[7,6], [6,5], [5,4], [4,3], [3,2], [2,1]] + }, + { "id":"3", "t":[0, 1], "x":[[1,3],[2,3]], "y":[[3,5],[4,5]] }, + { "id":"3", "t":[5, 11], "x":[[1,3],[2,3]], "y":[[3,5],[4,5]] }, + { "id":"3", "t":[15, 16], "x":[[1,3],[2,3]], "y":[[3,5],[4,5]] }, + { "id":"3", "t":[19, 26], "x":[[1,3],[2,3]], "y":[[3,5],[4,5]] }, + { "id":"3", "t":[33, 34], "x":[[1,3],[2,3]], "y":[[3,5],[4,5]] }, + { "id":"4", "t":[2], "x":[[-5,-5]], "y":[[3,4]] }, + { "id":"5", "t":[22], "x":[[-5,-5]], "y":[[3,4]] }, + { "id":"6", "t":[14, 18, 22, 26], + "x":[[-1,-2], [-3,-4], [-5,-6], [-7,-8]], + "y":[[-3,-4], [-4,-3], [-3,-2], [-2,-3]] + } + ] +} diff --git a/tests/examples/count_animals.wcon b/tests/examples/count_animals.wcon new file mode 100644 index 0000000..d28af07 --- /dev/null +++ b/tests/examples/count_animals.wcon @@ -0,0 +1,12 @@ +{ + "metadata": { + "protocol": "Three animals created by hand in four data points." + }, + "units": { "t":"s", "x":"mm", "y":"mm" }, + "data": [ + {"id":"1", "t":[0], "x":[[1,2]], "y":[[3,4]] }, + {"id":"2", "t":[0], "x":[[8,9]], "y":[[7,6]] }, + {"id":"1", "t":[1, 2], "x":[[1,3],[2,3]], "y":[[3,5],[4,5]] }, + {"id":"3", "t":[2], "x":[[-5,-5]], "y":[[3,4]] } + ] +}