001/* An actor that identifies peaks in an array. 002 003 Copyright (c) 2003-2014 The Regents of the University of California. 004 All rights reserved. 005 Permission is hereby granted, without written agreement and without 006 license or royalty fees, to use, copy, modify, and distribute this 007 software and its documentation for any purpose, provided that the above 008 copyright notice and the following two paragraphs appear in all copies 009 of this software. 010 011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 015 SUCH DAMAGE. 016 017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 022 ENHANCEMENTS, OR MODIFICATIONS. 023 024 PT_COPYRIGHT_VERSION_2 025 COPYRIGHTENDKEY 026 */ 027package ptolemy.actor.lib; 028 029import java.util.ArrayList; 030 031import ptolemy.actor.TypedAtomicActor; 032import ptolemy.actor.TypedIOPort; 033import ptolemy.actor.parameters.PortParameter; 034import ptolemy.data.ArrayToken; 035import ptolemy.data.BooleanToken; 036import ptolemy.data.DoubleToken; 037import ptolemy.data.IntToken; 038import ptolemy.data.Token; 039import ptolemy.data.expr.Parameter; 040import ptolemy.data.expr.SingletonParameter; 041import ptolemy.data.expr.StringParameter; 042import ptolemy.data.type.ArrayType; 043import ptolemy.data.type.BaseType; 044import ptolemy.data.type.Type; 045import ptolemy.kernel.CompositeEntity; 046import ptolemy.kernel.util.IllegalActionException; 047import ptolemy.kernel.util.NameDuplicationException; 048import ptolemy.kernel.util.StringAttribute; 049import ptolemy.kernel.util.Workspace; 050 051/////////////////////////////////////////////////////////////////// 052//// ArrayPeakSearch 053 054/** 055 <p>This actor outputs the indices and values of peaks in an input array.</p> 056 057 <p>The <i>dip</i> and <i>squelch</i> parameters control the 058 sensitivity to noise. These are given either as absolute numbers 059 or as relative numbers. If they are absolute numbers, then a peak 060 is detected if a rise above <i>dip</i> is detected before the peak 061 and a dip below <i>dip</i> is detected after the peak. 062 If they are given as relative numbers, then a peak is detected when 063 a rise by a factor <i>dip</i> above the most recently seen minimum 064 (if there has been one) is seen before the peak, and if a dip by a 065 factor <i>dip</i> relative to the peak is seen after the peak. 066 Relative numbers can be either linear (a fraction) or in decibels. 067 This is determined by the value of the <i>scale</i> parameter. For 068 example, if <i>dip</i> is given as 2.0 and <i>scale</i> has value 069 "relative linear", then a dip must drop to half of a local peak 070 value to be considered a dip.</p> 071 072 <p>If <i>squelch</i> is given as 10.0 and <i>scale</i> has value 073 "relative linear", then any peaks that lie below 1/10 of the global 074 peak are ignored. Note that <i>dip</i> is relative to the most 075 recently seen peak or valley, and <i>squelch</i> is relative to the 076 global peak in the array, when relative values are used. If 077 <i>scale</i> has value "relative amplitude decibels", then a value 078 of 6.0 is equivalent to the linear value 2.0. If <i>scale</i> has 079 value "relative power decibels", then a value of 3.0 is equivalent 080 to the linear value 2.0. In either decibel scale, 0.0 is 081 equivalent to 0.0 linear. Other parameters control how the search 082 is conducted.</p> 083 084 <p>This actor is based on Matlab code developed by John Signorotti of 085 Southwest Research Institute.</p> 086 087 @author Edward A. Lee 088 @version $Id$ 089 @since Ptolemy II 4.0 090 @Pt.ProposedRating Yellow (eal) 091 @Pt.AcceptedRating Red (cxh) 092 */ 093public class ArrayPeakSearch extends TypedAtomicActor { 094 /** Construct an actor with the given container and name. 095 * @param container The container. 096 * @param name The name of this actor. 097 * @exception IllegalActionException If the actor cannot be contained 098 * by the proposed container. 099 * @exception NameDuplicationException If the container already has an 100 * actor with this name. 101 */ 102 public ArrayPeakSearch(CompositeEntity container, String name) 103 throws NameDuplicationException, IllegalActionException { 104 super(container, name); 105 106 // Set Parameters. 107 dip = new Parameter(this, "dip"); 108 dip.setExpression("0.0"); 109 dip.setTypeEquals(BaseType.DOUBLE); 110 111 squelch = new Parameter(this, "squelch"); 112 squelch.setExpression("-10.0"); 113 squelch.setTypeEquals(BaseType.DOUBLE); 114 115 scale = new StringParameter(this, "scale"); 116 scale.setExpression("absolute"); 117 scale.addChoice("absolute"); 118 scale.addChoice("relative linear"); 119 scale.addChoice("relative amplitude decibels"); 120 scale.addChoice("relative power decibels"); 121 122 startIndex = new PortParameter(this, "startIndex"); 123 startIndex.setExpression("0"); 124 startIndex.setTypeEquals(BaseType.INT); 125 new SingletonParameter(startIndex.getPort(), "_showName") 126 .setToken(BooleanToken.TRUE); 127 new StringAttribute(startIndex.getPort(), "_cardinal") 128 .setExpression("SOUTH"); 129 130 endIndex = new PortParameter(this, "endIndex"); 131 endIndex.setExpression("MaxInt"); 132 endIndex.setTypeEquals(BaseType.INT); 133 new SingletonParameter(endIndex.getPort(), "_showName") 134 .setToken(BooleanToken.TRUE); 135 new StringAttribute(endIndex.getPort(), "_cardinal") 136 .setExpression("SOUTH"); 137 138 maximumNumberOfPeaks = new Parameter(this, "maximumNumberOfPeaks"); 139 maximumNumberOfPeaks.setExpression("MaxInt"); 140 maximumNumberOfPeaks.setTypeEquals(BaseType.INT); 141 142 // Ports. 143 input = new TypedIOPort(this, "input", true, false); 144 peakValues = new TypedIOPort(this, "peakValues", false, true); 145 peakIndices = new TypedIOPort(this, "peakIndices", false, true); 146 147 new SingletonParameter(peakValues, "_showName") 148 .setToken(BooleanToken.TRUE); 149 new SingletonParameter(peakIndices, "_showName") 150 .setToken(BooleanToken.TRUE); 151 152 // Set Type Constraints. 153 input.setTypeEquals(new ArrayType(BaseType.DOUBLE)); 154 peakValues.setTypeAtLeast(input); 155 peakIndices.setTypeEquals(new ArrayType(BaseType.INT)); 156 157 // NOTE: Consider constraining input element types. 158 // This is a bit complicated to do, however. 159 } 160 161 /////////////////////////////////////////////////////////////////// 162 //// ports and parameters //// 163 164 /** The amount that the signal must drop below a local maximum before a 165 * peak is detected. This is a double that can be interpreted as an 166 * absolute threshold or relative to the local peak, and if relative, on 167 * a linear or decibel scale, depending on the <i>scale</i> 168 * parameter. It defaults to 0.0. 169 */ 170 public Parameter dip; 171 172 /** The end point of the search. If this number is larger than 173 * the length of the input array, then the search is to the end 174 * of the array. This is an integer that defaults to MaxInt. 175 */ 176 public PortParameter endIndex; 177 178 /** The input port. This is required to be an array of doubles 179 */ 180 public TypedIOPort input; 181 182 /** The maximum number of peaks to report. 183 * This is an integer that defaults to MaxInt. 184 */ 185 public Parameter maximumNumberOfPeaks; 186 187 /** The output port for the indices of the peaks. The type is 188 * {int} (array of int). 189 */ 190 public TypedIOPort peakIndices; 191 192 /** The output port for the values of the peaks. The type is the 193 * same as the input port. 194 */ 195 public TypedIOPort peakValues; 196 197 /** An indicator of whether <i>dip</i> and <i>squelch</i> should 198 * be interpreted as absolute or relative, and if relative, then 199 * on a linear scale, in amplitude decibels, or power decibels. 200 * If decibels are used, then the corresponding linear threshold 201 * is 10^(<i>threshold</i>/<i>N</i>), where <i>N</i> is 20 (for 202 * amplitude decibels) or 10 (for power decibels). 203 * This parameter is a string with possible values "absolute", 204 * "relative linear", "relative amplitude decibels" or "relative 205 * power decibels". The default value is "absolute". 206 */ 207 public StringParameter scale; 208 209 /** The value below which the input is ignored by the 210 * algorithm. This is a double that can be interpreted as an 211 * absolute number or a relative number, and if relative, on a 212 * linear or decibel scale, depending on the <i>scale</i> 213 * parameter. For the relative case, the number is relative 214 * to the global peak. It defaults to -10.0. 215 */ 216 public Parameter squelch; 217 218 /** The starting point of the search. If this number is larger than 219 * the value of <i>endIndex</i>, the search is conducted backwards 220 * (and the results presented in reverse order). If this number is 221 * larger than the length of the input array, then the search is 222 * started at the end of the input array. 223 * This is an integer that defaults to 0. 224 */ 225 public PortParameter startIndex; 226 227 /////////////////////////////////////////////////////////////////// 228 //// public methods //// 229 230 /** Override the base class to set type constraints. 231 * @param workspace The workspace for the new object. 232 * @return A new instance of ArrayPeakSearch. 233 * @exception CloneNotSupportedException If a derived class contains 234 * an attribute that cannot be cloned. 235 */ 236 @Override 237 public Object clone(Workspace workspace) throws CloneNotSupportedException { 238 ArrayPeakSearch newObject = (ArrayPeakSearch) super.clone(workspace); 239 newObject.input.setTypeEquals(new ArrayType(BaseType.DOUBLE)); 240 newObject.peakValues.setTypeAtLeast(newObject.input); 241 return newObject; 242 } 243 244 /** Consume at most one array from the input port and produce 245 * two arrays containing the indices and values of the identified 246 * peaks. 247 * If there is no token on the input, then no output is produced. 248 * If the input is an empty array, then the same empty array token 249 * is produced on both outputs. 250 * @exception IllegalActionException If there is no director, or 251 * if sorting is not supported for the input array. 252 */ 253 @Override 254 public void fire() throws IllegalActionException { 255 super.fire(); 256 startIndex.update(); 257 endIndex.update(); 258 259 if (input.hasToken(0)) { 260 ArrayToken inputArray = (ArrayToken) input.get(0); 261 Type inputElementType = inputArray.getElementType(); 262 int inputSize = inputArray.length(); 263 264 if (inputSize == 0) { 265 peakValues.send(0, inputArray); 266 peakIndices.send(0, inputArray); 267 return; 268 } 269 270 int start = ((IntToken) startIndex.getToken()).intValue(); 271 int end = ((IntToken) endIndex.getToken()).intValue(); 272 int maxPeaks = ((IntToken) maximumNumberOfPeaks.getToken()) 273 .intValue(); 274 275 // Constrain start and end. 276 if (end >= inputSize) { 277 end = inputSize - 1; 278 } 279 280 if (start >= inputSize) { 281 start = inputSize - 1; 282 } 283 284 if (end < 0) { 285 end = 0; 286 } 287 288 if (start < 0) { 289 start = 0; 290 } 291 292 int increment = 1; 293 294 if (end < start) { 295 increment = -1; 296 } 297 298 boolean searchValley = false; 299 boolean searchPeak = true; 300 301 int localMaxIndex = start; 302 double localMax = ((DoubleToken) inputArray.getElement(start)) 303 .doubleValue(); 304 double localMin = localMax; 305 306 double dipValue = ((DoubleToken) dip.getToken()).doubleValue(); 307 double squelchValue = ((DoubleToken) squelch.getToken()) 308 .doubleValue(); 309 310 // The following values change since they are relative to 311 // most recently peaks or values. 312 double dipThreshold = dipValue; 313 double riseThreshold = dipValue; 314 315 String scaleValue = scale.stringValue(); 316 317 // Index of what scale we are dealing with. 318 int scaleIndicator = _ABSOLUTE; 319 320 if (!scaleValue.equals("absolute")) { 321 // Scale is relative so we adjust the thresholds. 322 // Search for the global maximum value so squelch 323 // works properly. 324 double maxValue = localMax; 325 326 for (int i = 0; i <= inputSize - 1; i = i + increment) { 327 double indata = ((DoubleToken) inputArray.getElement(i)) 328 .doubleValue(); 329 330 if (indata > maxValue) { 331 maxValue = indata; 332 } 333 } 334 335 if (scaleValue.equals("relative amplitude decibels")) { 336 scaleIndicator = _RELATIVE_DB; 337 dipThreshold = localMax * Math.pow(10.0, -dipValue / 20); 338 riseThreshold = localMin * Math.pow(10.0, dipValue / 20); 339 squelchValue = maxValue 340 * Math.pow(10.0, -squelchValue / 20); 341 } else if (scaleValue.equals("relative power decibels")) { 342 scaleIndicator = _RELATIVE_DB_POWER; 343 dipThreshold = localMax * Math.pow(10.0, -dipValue / 10); 344 riseThreshold = localMin * Math.pow(10.0, dipValue / 10); 345 squelchValue = maxValue 346 * Math.pow(10.0, -squelchValue / 10); 347 } else if (scaleValue.equals("relative linear")) { 348 scaleIndicator = _RELATIVE_LINEAR; 349 dipThreshold = localMax - dipValue; 350 riseThreshold = localMin + dipValue; 351 squelchValue = maxValue - squelchValue; 352 } 353 } 354 355 ArrayList resultIndices = new ArrayList(); 356 ArrayList resultPeaks = new ArrayList(); 357 358 for (int i = start; i <= end; i = i + increment) { 359 double indata = ((DoubleToken) inputArray.getElement(i)) 360 .doubleValue(); 361 362 if (_debugging) { 363 _debug("-- Checking input with value " + indata 364 + " at index " + i); 365 } 366 367 if (searchValley) { 368 if (indata < localMin) { 369 localMin = indata; 370 371 switch (scaleIndicator) { 372 case _RELATIVE_DB: 373 riseThreshold = localMin 374 * Math.pow(10.0, dipValue / 20); 375 break; 376 377 case _RELATIVE_DB_POWER: 378 riseThreshold = localMin 379 * Math.pow(10.0, dipValue / 10); 380 break; 381 382 case _RELATIVE_LINEAR: 383 riseThreshold = localMin + dipValue; 384 break; 385 } 386 } 387 388 if (_debugging) { 389 _debug("-- Looking for a value above " + riseThreshold); 390 } 391 392 if (indata > riseThreshold && indata > squelchValue) { 393 localMax = indata; 394 395 switch (scaleIndicator) { 396 case _RELATIVE_DB: 397 dipThreshold = localMax 398 * Math.pow(10.0, -dipValue / 20); 399 break; 400 401 case _RELATIVE_DB_POWER: 402 dipThreshold = localMax 403 * Math.pow(10.0, -dipValue / 10); 404 break; 405 406 case _RELATIVE_LINEAR: 407 dipThreshold = localMax - dipValue; 408 break; 409 } 410 411 localMaxIndex = i; 412 searchValley = false; 413 searchPeak = true; 414 } 415 } else if (searchPeak) { 416 if (indata > localMax && indata > squelchValue) { 417 localMax = indata; 418 419 switch (scaleIndicator) { 420 case _RELATIVE_DB: 421 dipThreshold = localMax 422 * Math.pow(10.0, -dipValue / 20); 423 break; 424 425 case _RELATIVE_DB_POWER: 426 dipThreshold = localMax 427 * Math.pow(10.0, -dipValue / 10); 428 break; 429 430 case _RELATIVE_LINEAR: 431 dipThreshold = localMax - dipValue; 432 break; 433 } 434 435 localMaxIndex = i; 436 } 437 438 if (_debugging) { 439 _debug("-- Looking for a value below " + dipThreshold); 440 } 441 442 if (indata < dipThreshold && localMax > squelchValue) { 443 if (_debugging) { 444 _debug("** Found a peak with value " + localMax 445 + " at index " + localMaxIndex); 446 } 447 448 resultIndices.add(new IntToken(localMaxIndex)); 449 resultPeaks.add(new DoubleToken(localMax)); 450 451 if (resultPeaks.size() > maxPeaks) { 452 break; 453 } 454 455 localMin = indata; 456 457 switch (scaleIndicator) { 458 case _RELATIVE_DB: 459 riseThreshold = localMin 460 * Math.pow(10.0, dipValue / 20); 461 break; 462 463 case _RELATIVE_DB_POWER: 464 riseThreshold = localMin 465 * Math.pow(10.0, dipValue / 10); 466 break; 467 468 case _RELATIVE_LINEAR: 469 riseThreshold = localMin + dipValue; 470 break; 471 } 472 473 searchValley = true; 474 searchPeak = false; 475 } 476 } 477 } 478 479 if (resultPeaks.isEmpty()) { 480 resultPeaks.add(inputArray.getElement(start)); 481 resultIndices.add(startIndex.getToken()); 482 } 483 484 Token[] resultPeaksArray = (Token[]) resultPeaks 485 .toArray(new Token[resultPeaks.size()]); 486 Token[] resultIndicesArray = (Token[]) resultIndices 487 .toArray(new Token[resultIndices.size()]); 488 489 peakValues.send(0, 490 new ArrayToken(inputElementType, resultPeaksArray)); 491 peakIndices.send(0, 492 new ArrayToken(BaseType.INT, resultIndicesArray)); 493 } 494 } 495 496 /////////////////////////////////////////////////////////////////// 497 //// private variables //// 498 private static final int _ABSOLUTE = 0; 499 500 private static final int _RELATIVE_DB = 1; 501 502 private static final int _RELATIVE_DB_POWER = 2; 503 504 private static final int _RELATIVE_LINEAR = 3; 505}