package won.matcher.solr.utils; import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; import java.util.LinkedList; import java.util.List; /** * Created by hfriedrich on 19.07.2016. * * Detect knee points in a curve using the "Kneedle" algorithm as described in the paper "Finding a" Kneedle" in a * Haystack: Detecting Knee Points in System Behavior". * * NOTE: This implementation does not check the concavity of the curve. * Also no smoothing of the curve is applied by this method. * * Kneedle algorithm described in: * Satopaa, V., Albrecht, J., Irwin, D., & Raghavan, B. (2011, June). * Finding a" Kneedle" in a Haystack: Detecting Knee Points in System Behavior. * In 2011 31st International Conference on Distributed Computing Systems Workshops (pp. 166-171). IEEE. * */ public class Kneedle { private double sensitivity = 1.0; public Kneedle() { sensitivity = 1.0; } public Kneedle(final double sensitivity) { this.sensitivity = sensitivity; } public int[] detectKneePoints(final double[] x, final double[] y) { return detectKneeOrElbowPoints(x, y, false); } public int[] detectElbowPoints(final double[] x, final double[] y) { return detectKneeOrElbowPoints(x, y, true); } /** * Detect all knee points in a curve according to "Kneedle" algorithm. * Alternatively this method can detect elbow points instead of knee points. * * @param x x-coordintes of curve, must be increasing in value * @param y y-coordinates of curve * @param detectElbows if true detects elbow points, if false detects knee points * @return array of indices of knee (or elbow) points in the curve */ private int[] detectKneeOrElbowPoints(final double[] x, final double[] y, boolean detectElbows) { checkConstraints(x, y); List<Integer> kneeIndices = new LinkedList<Integer>(); List<Integer> lmxIndices = new LinkedList<Integer>(); List<Double> lmxThresholds = new LinkedList<Double>(); double[] xn = normalize(x); double[] yn = normalize(y); // compute the y difference values double[] yDiff = new double[y.length]; for (int i = 0; i < y.length; i++) { yDiff[i] = yn[i] - xn[i]; } // if we want to detect elbow points instead of knees do not compute local maxima but local minima instead. // Therefore we invert the yDiff values which means we actually find local minima instead of maxima in the // original yDiff curve if (detectElbows) { DescriptiveStatistics stats = new DescriptiveStatistics(yDiff); for (int i = 0; i < yDiff.length; i++) { yDiff[i] = stats.getMax() - yDiff[i]; } } // find local maxima, compute threshold values and detect knee points boolean detectKneeForLastLmx = false; for (int i = 1; i < y.length - 1; i++) { // check if the difference values of a point are bigger // than for its left and right neighbour => local maximum if (yDiff[i] > yDiff[i - 1] && yDiff[i] > yDiff[i + 1]) { // local maximum found lmxIndices.add(i); // compute the threshold value for this local maximum // NOTE: As stated in the paper the threshold Tlmx is computed. Since the mean distance of all consecutive // x-values summed together for a normalized function is always (1 / (n -1)) we do not have to compute the // whole sum here as stated in the paper. double tlmx = yDiff[i] - sensitivity / (xn.length - 1); lmxThresholds.add(tlmx); // try to find out if the current local maximum is a knee point detectKneeForLastLmx = true; } // check for new knee point if (detectKneeForLastLmx) { if (yDiff[i + 1] < lmxThresholds.get(lmxThresholds.size() - 1)) { // knee detected kneeIndices.add(lmxIndices.get(lmxIndices.size() - 1)); detectKneeForLastLmx = false; } } } int knees[] = new int[kneeIndices.size()]; for (int i = 0; i < kneeIndices.size(); i++) { knees[i] = kneeIndices.get(i); } return knees; } private double[] normalize(final double[] values) { double normalized[] = new double[values.length]; DescriptiveStatistics stats = new DescriptiveStatistics(values); for (int i = 0; i < values.length; i++) { normalized[i] = (values[i] - stats.getMin()) / (stats.getMax() - stats.getMin()); } return normalized; } private void checkConstraints(final double[] x, final double[] y) { if (x.length != y.length || x.length < 2) { throw new IllegalArgumentException("x and y arrays must have size > 1 and the same number of elements"); } for (int i = 0; i < x.length - 1; i++) { if (x[i + 1] <= x[i]) { throw new IllegalArgumentException("x values must be sorted and increasing"); } } } }