/* Copyright 2010 Aaron J. Radke Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cc.drx /** * A helper class for time series signals */ object Signal{ type TVS = Seq[(Time,Double)] private val ConsPat = """(^|\s)(-?\d)""".r //pattern to auto add a C for constant private val SpecPat = { val num = """[0-9\-.E]+""" val ws = """[\s,]*""" val kind = "[cCrRhHzZ]" val tspec = "[/@%]" //Spec pat reads like: Add piecewise signal of <kind> with param <num> with a width of by <time spec> // req. opt. opt. s"$ws($kind)$ws($num)?$ws($tspec$num)?$ws".r } private val empty:TVS = Seq() def main(args:Array[String]):Unit = { if(args.isEmpty) printExampleSpecs else { val tvs = Signal.fromSpec(args.toSeq.mkSpaces, frameWidth=1.s, dt=30.hz.inv) Plot.println(tvs) } } def printExampleSpecs:Unit = { val specs = Kson(""" zCCCccCcz desc: 3211 pulse z%0.5CCCccCcz desc: 3211 pulse with frameWidth 3 zCz desc: pulse (square) zCcz desc: doublet (square) zRrz desc: pulse (triangle) zRrrRz desc: doublet (triangle) C1 Z desc: start at constant drop to zero Z@3C1/3 desc: start at 3s a pulse Z@0.2C1Z desc: start at 0.2s a pulse Z R1/.25 H H H R-1/.25 H/1 desc: doublet (trapezoid) C1 C0 desc: start at constant drop to zero 1 0 desc: start at constant drop to zero """) for(line <- specs.lines; spec <- line.root; desc <- line.get("desc")){ println(s"#=== '$spec' $desc") val tvs = Signal.fromSpec(spec, frameWidth=1.s, dt=30.hz.inv) Plot.println(tvs) } } /** Signal spec is a mini DSL language (i.e. svg d paths) to generated time series data in a concise string like svg path language * * Spaces are irrelevant * Spec strings are a sequence of piecwise signals (pieces). * Each piece reads like: * Add piecewise signal of <kind> with param <num> with a width of by <time spec> * Each * [kind][value][timespec timevalue] * * kind: * C desc:constant default:1 * c desc:constant inverted default:1 * R desc:Rate slope to value default:1 * r desc:constant inverted default:1 * H desc:hold last value default:n/a * z desc:a constant of zero default:n/a * * timespec: * / desc:alternate frame width * @ desc:stretch frame to an absolute time * % desc:specify future frame widths * * */ def fromSpec(spec:String, gain:Double=1d, frameWidth:Time=1.s, dt:Time=30.hz.inv):TVS = { var _frameWidth:Time = frameWidth val specPrime = ConsPat.replaceAllIn(spec,{m => " C" + m.group(2)}) //prepend the C the force the constant SpecPat.findAllMatchIn(specPrime).toSeq.foldLeft(empty){case (tvs,m) => val (t0,v0) = tvs.lastOption.getOrElse(0.s -> 0d) def group(i:Int):Option[String] = Option(m group i) val kind:String = group(1).getOrElse("Z") val v:Double = group(2).getOrElse("1").toDouble //cacluate piecewise time width val tw:Time = group(3).map{tspec => val t:Time = tspec.drop(1).toDouble.s tspec.take(1) match { case "%" => _frameWidth=t; t //set this and all future timewidths case "/" => t case "@" => t - t0 } }.getOrElse(_frameWidth) // println(f"tw:${tw.ms}%5.0fms kind:$kind v:$v% 2.2f") val tStart = if(tvs.isEmpty) t0 else t0 + dt val tEnd = t0+tw tvs ++ { kind match { case "C" => Seq(tStart -> v, tEnd -> ( v ) ) case "c" => Seq(tStart -> -v, tEnd -> (-v ) ) case "R" => Seq( tEnd -> (v0 + v) ) case "r" => Seq( tEnd -> (v0 - v) ) case "h"|"H" => Seq( tEnd -> (v0 ) ) case "z"|"Z" => Seq(tStart -> 0d, tEnd -> (0d ) ) } } } //--optimze (remove redundancies) .groupRunsBy(_._2).flatMap{constants => if(constants.size > 2) Seq(constants.head, constants.last) else constants } }.map{case (t,v) => t -> v*gain} //apply gains }