package net.tuis.ubench; import net.tuis.ubench.scale.MathEquation; import net.tuis.ubench.scale.MathModel; import net.tuis.ubench.scale.Models; import org.apache.commons.math3.linear.*; import java.util.Arrays; import java.util.Comparator; import java.util.function.DoubleUnaryOperator; import java.util.function.Function; /** * @author Simon Forsberg */ public class ScaleDetect { private static final double TOLERANCE = 1e-4; private static final double H = 1e-5; private static final Comparator<MathEquation> ranking = Comparator.comparingInt((MathEquation eq) -> eq.isValid() ? 0 : 1) .thenComparingDouble(eqb -> - eqb.getRSquared()); /** * Finding the best fit using least-squares method for an equation system * * @param function Equation system to find fit for. Input: Parameters, Output: Residuals. * @param initial Initial 'guess' for function parameters * @param tolerance How much the function parameters may change before a solution is accepted * @return The parameters to the function that causes the least residuals */ private static double[] newtonSolve(Function<double[], double[]> function, double[] initial, double tolerance) { RealVector dx = new ArrayRealVector(initial.length); dx.set(tolerance + 1); int iterations = 0; int d = initial.length; double[] values = Arrays.copyOf(initial, initial.length); while (dx.getNorm() > tolerance) { double[] fx = function.apply(values); Array2DRowRealMatrix df = new Array2DRowRealMatrix(fx.length, d); ArrayRealVector fxVector = new ArrayRealVector(fx); for (int i = 0; i < d; i++) { double originalValue = values[i]; values[i] += H; double[] fxi = function.apply(values); values[i] = originalValue; ArrayRealVector fxiVector = new ArrayRealVector(fxi); RealVector result = fxiVector.subtract(fxVector); result = result.mapDivide(H); df.setColumn(i, result.toArray()); } dx = new RRQRDecomposition(df).getSolver().solve(fxVector.mapMultiply(-1)); // df has size = initial, and fx has size equal to whatever that function produces. for (int i = 0; i < values.length; i++) { values[i] += dx.getEntry(i); } iterations++; if (iterations % 100 == 0) { tolerance *= 10; } } return values; } /** * From the results of a UScale run, calculate the best-fit scaling equation * @param scale the results to analyze * @return the best-fit scaling equation. */ public static MathEquation detect(UScale scale) { return Arrays.stream(rank(scale)) .filter(eq -> eq.isValid()) .findFirst() .orElse(fit(scale, Models.CONSTANT)); // if no valid is found, it is because of constant data } private static MathEquation fit(UScale scale, MathModel model) { return detect(extractX(scale), extractY(scale), model); } private static double[] extractX(UScale scale) { return scale.getStats().stream().mapToDouble(st -> st.getIndex()).toArray(); } private static double[] extractY(UScale scale) { return scale.getStats().stream().mapToDouble(st -> st.getFastestNanos()).toArray(); } /** * From the scaling results calculate a number of scaling equations, and return their equations in descending order of relevance * @param scale the Scale results to rank * @return the possible equations in best-fit-first order. */ public static MathEquation[] rank(UScale scale) { double[] x = extractX(scale); double[] y = extractY(scale); return rank(x, y); } private static MathEquation[] rank(double[] x, double[] y) { MathModel[] models = new MathModel[]{ Models.CONSTANT, Models.LINEAR, Models.N_SQUARED, Models.createPolynom(3), Models.createPolynom(4), Models.LOG_N, Models.N_LOG_N, Models.EXPONENTIAL }; // sort by reverse rsquared, or negative r-squared... note the `-` in `eq -> - eq.getRSquared()` return Arrays.stream(models).map(m -> detect(x, y, m)).sorted(ranking).toArray(size -> new MathEquation[size]); } static MathEquation detect(double[] x, double[] y, MathModel model) { if (x.length != y.length) { throw new IllegalArgumentException("x and y size must match"); } Function<double[], double[]> function = new Function<double[], double[]>() { @Override public double[] apply(double[] doubles) { double[] result = new double[x.length]; DoubleUnaryOperator func = model.createFunction(doubles); for (int i = 0; i < x.length; i++) { result[i] = y[i] - func.applyAsDouble(x[i]); } return result; } }; double[] results = newtonSolve(function, model.getInitialValues(), TOLERANCE); DoubleUnaryOperator finalFunction = model.createFunction(results); double rSquared = calculateRSquared(finalFunction, x, y); MathEquation eq = new MathEquation(model, finalFunction, results, model.getFormat(), rSquared); return eq; } private static double calculateRSquared(DoubleUnaryOperator finalFunction, double[] x, double[] y) { double yAverage = Arrays.stream(y).average().getAsDouble(); double variance = 0; double residualSumOfSquares = 0; // double explainedSumOfSquares = 0; for (int i = 0; i < y.length; i++) { double yi = y[i]; double fi = finalFunction.applyAsDouble(x[i]); variance += (yi - yAverage) * (yi - yAverage); residualSumOfSquares += (yi - fi) * (yi - fi); // explainedSumOfSquares += (fi - yAverage) * (fi - yAverage); } double rSquared = 1 - residualSumOfSquares / variance; return rSquared; } }