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}