package gdsc.smlm.fitting.nonlinear.gradient; import java.util.ArrayList; import java.util.Arrays; import org.apache.commons.math3.random.RandomDataGenerator; import org.apache.commons.math3.random.RandomGenerator; import org.apache.commons.math3.random.Well19937c; import org.junit.Assert; import org.junit.Test; import gdsc.core.utils.DoubleEquality; import gdsc.smlm.TestSettings; import gdsc.smlm.function.Gradient2Function; import gdsc.smlm.function.ValueProcedure; import gdsc.smlm.function.gaussian.Gaussian2DFunction; import gdsc.smlm.function.gaussian.GaussianFunctionFactory; import gdsc.smlm.function.gaussian.HoltzerAstimatismZModel; import gdsc.smlm.function.gaussian.erf.ErfGaussian2DFunction; import gdsc.smlm.function.gaussian.erf.SingleAstigmatismErfGaussian2DFunction; import gdsc.smlm.function.gaussian.erf.SingleFreeCircularErfGaussian2DFunction; /** * Contains speed tests for the methods for calculating the Hessian and gradient vector * for use in the LVM algorithm. */ public class NewtonRaphsonGradient2ProcedureTest { boolean speedTests = true; DoubleEquality eq = new DoubleEquality(6, 1e-16); int MAX_ITER = 20000; int blockWidth = 10; double Background = 0.5; double Signal = 100; double Angle = Math.PI; double Xpos = 5; double Ypos = 5; double Xwidth = 1.2; double Ywidth = 1.2; RandomDataGenerator rdg; @Test public void gradientProcedureFactoryCreatesOptimisedProcedures() { double[] y = new double[0]; Assert.assertEquals( NewtonRaphsonGradient2ProcedureFactory.createUnrolled(y, new DummyGradientFunction(4)).getClass(), NewtonRaphsonGradient2Procedure4.class); Assert.assertEquals( NewtonRaphsonGradient2ProcedureFactory.createUnrolled(y, new DummyGradientFunction(5)).getClass(), NewtonRaphsonGradient2Procedure5.class); Assert.assertEquals( NewtonRaphsonGradient2ProcedureFactory.createUnrolled(y, new DummyGradientFunction(6)).getClass(), NewtonRaphsonGradient2Procedure6.class); } @Test public void gradientProcedureComputesSameLogLikelihoodAsMLEGradientCalculator() { gradientProcedureComputesSameLogLikelihoodAsMLEGradientCalculator(4); gradientProcedureComputesSameLogLikelihoodAsMLEGradientCalculator(5); gradientProcedureComputesSameLogLikelihoodAsMLEGradientCalculator(6); gradientProcedureComputesSameLogLikelihoodAsMLEGradientCalculator(11); gradientProcedureComputesSameLogLikelihoodAsMLEGradientCalculator(21); } @Test public void gradientProcedureIsNotSlowerThanGradientCalculator() { // Note: The procedure does not have a lot of work within loops. It is only a single loop // so unrolling does not produce performance gains. The JVM can optimise this. gradientProcedureIsNotSlowerThanGradientCalculator(4); gradientProcedureIsNotSlowerThanGradientCalculator(5); gradientProcedureIsNotSlowerThanGradientCalculator(6); gradientProcedureIsNotSlowerThanGradientCalculator(11); gradientProcedureIsNotSlowerThanGradientCalculator(21); } private void gradientProcedureComputesSameLogLikelihoodAsMLEGradientCalculator(int nparams) { int iter = 10; rdg = new RandomDataGenerator(new Well19937c(30051977)); ArrayList<double[]> paramsList = new ArrayList<double[]>(iter); ArrayList<double[]> yList = new ArrayList<double[]>(iter); createFakeData(nparams, iter, paramsList, yList); FakeGradientFunction func = new FakeGradientFunction(blockWidth, nparams); MLEGradientCalculator calc = (MLEGradientCalculator) GradientCalculatorFactory.newCalculator(nparams, true); String name = String.format("[%d]", nparams); for (int i = 0; i < paramsList.size(); i++) { NewtonRaphsonGradient2Procedure p = NewtonRaphsonGradient2ProcedureFactory.createUnrolled(yList.get(i), func); double s = p.computeLogLikelihood(paramsList.get(i)); double s2 = calc.logLikelihood(yList.get(i), paramsList.get(i), func); // Virtually the same ... Assert.assertEquals(name + " Result: Not same @ " + i, s, s2, 1e-10); } } @Test public void gradientProcedureComputesSameWithPrecomputed() { int iter = 10; rdg = new RandomDataGenerator(new Well19937c(30051977)); ErfGaussian2DFunction f1 = (ErfGaussian2DFunction) GaussianFunctionFactory.create2D(1, 10, 10, GaussianFunctionFactory.FIT_ERF_FREE_CIRCLE, null); ErfGaussian2DFunction f2 = (ErfGaussian2DFunction) GaussianFunctionFactory.create2D(2, 10, 10, GaussianFunctionFactory.FIT_ERF_FREE_CIRCLE, null); double[] a1 = new double[7]; double[] a2 = new double[13]; final double[] x = new double[f1.size()]; final double[] b = new double[f1.size()]; for (int i = 0; i < iter; i++) { a2[Gaussian2DFunction.BACKGROUND] = rdg.nextUniform(0.1, 0.3); a2[Gaussian2DFunction.SIGNAL] = rdg.nextUniform(100, 300); a2[Gaussian2DFunction.X_POSITION] = rdg.nextUniform(3, 5); a2[Gaussian2DFunction.Y_POSITION] = rdg.nextUniform(3, 5); a2[Gaussian2DFunction.X_SD] = rdg.nextUniform(1, 1.3); a2[Gaussian2DFunction.Y_SD] = rdg.nextUniform(1, 1.3); a2[6 + Gaussian2DFunction.SIGNAL] = rdg.nextUniform(100, 300); a2[6 + Gaussian2DFunction.X_POSITION] = rdg.nextUniform(5, 7); a2[6 + Gaussian2DFunction.Y_POSITION] = rdg.nextUniform(5, 7); a2[6 + Gaussian2DFunction.X_SD] = rdg.nextUniform(1, 1.3); a2[6 + Gaussian2DFunction.Y_SD] = rdg.nextUniform(1, 1.3); // Simulate Poisson data f2.initialise0(a2); f1.forEach(new ValueProcedure() { int k = 0; public void execute(double value) { x[k++] = (value > 0) ? rdg.nextPoisson(value) : 0; } }); // Precompute peak 2 (no background) a1[Gaussian2DFunction.BACKGROUND] = 0; for (int j = 1; j < 7; j++) a1[j] = a2[6 + j]; f1.initialise0(a1); f1.forEach(new ValueProcedure() { int k = 0; public void execute(double value) { b[k++] = value; } }); // Reset to peak 1 for (int j = 0; j < 7; j++) a1[j] = a2[j]; // Compute peak 1+2 NewtonRaphsonGradient2Procedure p12 = NewtonRaphsonGradient2ProcedureFactory.create(x, f2); double[] up1 = Arrays.copyOf(p12.computeUpdate(a2), f1.getNumberOfGradients()); // Compute peak 1+(precomputed 2) NewtonRaphsonGradient2Procedure p1b2 = NewtonRaphsonGradient2ProcedureFactory.create(x, b, f1); double[] up2 = p1b2.computeUpdate(a1); Assert.assertArrayEquals(" Result: Not same @ " + i, p12.u, p1b2.u, 1e-10); Assert.assertArrayEquals(" Update: Not same @ " + i, up1, up2, 1e-10); double[] v1 = p12.computeValue(a2); double[] v2 = p1b2.computeValue(a1); Assert.assertArrayEquals(" Value: Not same @ " + i, v1, v2, 1e-10); double[] d1 = Arrays.copyOf(p12.computeFirstDerivative(a2), f1.getNumberOfGradients()); double[] d2 = p1b2.computeFirstDerivative(a1); Assert.assertArrayEquals(" 1st derivative: Not same @ " + i, d1, d2, 1e-10); } } private abstract class Timer { private int loops; int min = 5; Timer() { } Timer(int min) { this.min = min; } long getTime() { // Run till stable timing long t1 = time(); for (int i = 0; i < 10; i++) { long t2 = t1; t1 = time(); if (loops >= min && DoubleEquality.relativeError(t1, t2) < 0.02) // 2% difference break; } return t1; } long time() { loops++; long t = System.nanoTime(); run(); t = System.nanoTime() - t; //System.out.printf("[%d] Time = %d\n", loops, t); return t; } abstract void run(); } private void gradientProcedureIsNotSlowerThanGradientCalculator(final int nparams) { org.junit.Assume.assumeTrue(speedTests || TestSettings.RUN_SPEED_TESTS); final int iter = 1000; rdg = new RandomDataGenerator(new Well19937c(30051977)); final ArrayList<double[]> paramsList = new ArrayList<double[]>(iter); final ArrayList<double[]> yList = new ArrayList<double[]>(iter); createFakeData(nparams, iter, paramsList, yList); final FakeGradientFunction func = new FakeGradientFunction(blockWidth, nparams); MLEGradientCalculator calc = (MLEGradientCalculator) GradientCalculatorFactory.newCalculator(nparams, true); for (int i = 0; i < paramsList.size(); i++) calc.logLikelihood(yList.get(i), paramsList.get(i), func); for (int i = 0; i < paramsList.size(); i++) { NewtonRaphsonGradient2Procedure p = NewtonRaphsonGradient2ProcedureFactory.createUnrolled(yList.get(i), func); p.computeLogLikelihood(paramsList.get(i)); } // Realistic loops for an optimisation final int loops = 15; // Run till stable timing Timer t1 = new Timer() { @Override void run() { for (int i = 0, k = 0; i < iter; i++) { MLEGradientCalculator calc = (MLEGradientCalculator) GradientCalculatorFactory .newCalculator(nparams, true); for (int j = loops; j-- > 0;) calc.logLikelihood(yList.get(i), paramsList.get(k++ % iter), func); } } }; long time1 = t1.getTime(); Timer t2 = new Timer(t1.loops) { @Override void run() { for (int i = 0, k = 0; i < iter; i++) { NewtonRaphsonGradient2Procedure p = NewtonRaphsonGradient2ProcedureFactory .createUnrolled(yList.get(i), func); for (int j = loops; j-- > 0;) p.computeLogLikelihood(paramsList.get(k++ % iter)); } } }; long time2 = t2.getTime(); log("GradientCalculator = %d : NewtonRaphsonGradient2Procedure %d = %d : %fx\n", time1, nparams, time2, (1.0 * time1) / time2); if (TestSettings.ASSERT_SPEED_TESTS) { // Add contingency Assert.assertTrue(time2 < time1 * 1.5); } } @Test public void gradientProcedureUnrolledComputesSameAsGradientProcedure() { gradientProcedureUnrolledComputesSameAsGradientProcedure(4); gradientProcedureUnrolledComputesSameAsGradientProcedure(5); gradientProcedureUnrolledComputesSameAsGradientProcedure(6); } private void gradientProcedureUnrolledComputesSameAsGradientProcedure(int nparams) { int iter = 10; rdg = new RandomDataGenerator(new Well19937c(30051977)); ArrayList<double[]> paramsList = new ArrayList<double[]>(iter); ArrayList<double[]> yList = new ArrayList<double[]>(iter); createFakeData(nparams, iter, paramsList, yList); FakeGradientFunction func = new FakeGradientFunction(blockWidth, nparams); NewtonRaphsonGradient2Procedure p1, p2; String name = String.format("[%d]", nparams); for (int i = 0; i < paramsList.size(); i++) { p1 = new NewtonRaphsonGradient2Procedure(yList.get(i), func); p2 = NewtonRaphsonGradient2ProcedureFactory.createUnrolled(yList.get(i), func); double[] a = paramsList.get(i); double ll1 = p1.computeLogLikelihood(a); double ll2 = p2.computeLogLikelihood(a); Assert.assertEquals(name + " LL: Not same @ " + i, ll1, ll2, 0); p1 = new NewtonRaphsonGradient2Procedure(yList.get(i), func); p2 = NewtonRaphsonGradient2ProcedureFactory.createUnrolled(yList.get(i), func); p1.computeFirstDerivative(a); p2.computeFirstDerivative(a); Assert.assertArrayEquals(name + " first derivative value: Not same @ " + i, p1.u, p2.u, 0); Assert.assertArrayEquals(name + " first derivative: Not same @ " + i, p1.d1, p2.d1, 0); p1 = new NewtonRaphsonGradient2Procedure(yList.get(i), func); p2 = NewtonRaphsonGradient2ProcedureFactory.createUnrolled(yList.get(i), func); p1.computeUpdate(a); p2.computeUpdate(a); Assert.assertArrayEquals(name + " update value: Not same @ " + i, p1.u, p2.u, 0); Assert.assertArrayEquals(name + " update: Not same d1 @ " + i, p1.d1, p2.d1, 0); Assert.assertArrayEquals(name + " update: Not same d2 @ " + i, p1.d2, p2.d2, 0); Assert.assertArrayEquals(name + " update: Not same update @ " + i, p1.getUpdate(), p2.getUpdate(), 0); } } @Test public void gradientProcedureIsFasterUnrolledThanGradientProcedure() { gradientProcedureLinearIsFasterThanGradientProcedure(4); gradientProcedureLinearIsFasterThanGradientProcedure(5); gradientProcedureLinearIsFasterThanGradientProcedure(6); } private void gradientProcedureLinearIsFasterThanGradientProcedure(final int nparams) { org.junit.Assume.assumeTrue(speedTests || TestSettings.RUN_SPEED_TESTS); final int iter = 100; rdg = new RandomDataGenerator(new Well19937c(30051977)); final ArrayList<double[]> paramsList = new ArrayList<double[]>(iter); final ArrayList<double[]> yList = new ArrayList<double[]>(iter); createData(1, iter, paramsList, yList); // Remove the timing of the function call by creating a dummy function final Gradient2Function func = new FakeGradientFunction(blockWidth, nparams); for (int i = 0; i < paramsList.size(); i++) { NewtonRaphsonGradient2Procedure p1 = new NewtonRaphsonGradient2Procedure(yList.get(i), func); p1.computeUpdate(paramsList.get(i)); p1.computeUpdate(paramsList.get(i)); NewtonRaphsonGradient2Procedure p2 = NewtonRaphsonGradient2ProcedureFactory.createUnrolled(yList.get(i), func); p2.computeUpdate(paramsList.get(i)); p2.computeUpdate(paramsList.get(i)); // Check they are the same Assert.assertArrayEquals("Update " + i, p1.getUpdate(), p2.getUpdate(), 0); } // Realistic loops for an optimisation final int loops = 15; // Run till stable timing Timer t1 = new Timer() { @Override void run() { for (int i = 0, k = 0; i < paramsList.size(); i++) { NewtonRaphsonGradient2Procedure p1 = new NewtonRaphsonGradient2Procedure(yList.get(i), func); for (int j = loops; j-- > 0;) p1.computeUpdate(paramsList.get(k++ % iter)); } } }; long time1 = t1.getTime(); Timer t2 = new Timer(t1.loops) { @Override void run() { for (int i = 0, k = 0; i < paramsList.size(); i++) { NewtonRaphsonGradient2Procedure p2 = NewtonRaphsonGradient2ProcedureFactory .createUnrolled(yList.get(i), func); for (int j = loops; j-- > 0;) p2.computeUpdate(paramsList.get(k++ % iter)); } } }; long time2 = t2.getTime(); log("Standard = %d : Unrolled %d = %d : %fx\n", time1, nparams, time2, (1.0 * time1) / time2); Assert.assertTrue(time2 < time1 * 1.5); } @Test public void gradientCalculatorComputesGradient() { gradientCalculatorComputesGradient(new SingleFreeCircularErfGaussian2DFunction(blockWidth, blockWidth)); // Use a reasonable z-depth function from the Smith, et al (2010) paper (page 377) double gamma = 0.389; double d = 0.531; double Ax = -0.0708; double Bx = -0.073; double Ay = 0.164; double By = 0.0417; HoltzerAstimatismZModel zModel = HoltzerAstimatismZModel.create(gamma, d, Ax, Bx, Ay, By); gradientCalculatorComputesGradient(new SingleAstigmatismErfGaussian2DFunction(blockWidth, blockWidth, zModel)); } private void gradientCalculatorComputesGradient(ErfGaussian2DFunction func) { // Check the first and second derivatives int nparams = func.getNumberOfGradients(); int[] indices = func.gradientIndices(); int iter = 100; rdg = new RandomDataGenerator(new Well19937c(30051977)); ArrayList<double[]> paramsList = new ArrayList<double[]>(iter); ArrayList<double[]> yList = new ArrayList<double[]>(iter); createData(1, iter, paramsList, yList, true); double delta = 1e-3; DoubleEquality eq = new DoubleEquality(3, 1e-3); for (int i = 0; i < paramsList.size(); i++) { double[] y = yList.get(i); double[] a = paramsList.get(i); double[] a2 = a.clone(); NewtonRaphsonGradient2Procedure p = NewtonRaphsonGradient2ProcedureFactory.create(y, func); //double ll = p.computeLogLikelihood(a); p.computeUpdate(a); double[] d1 = p.d1.clone(); double[] d2 = p.d2.clone(); for (int j = 0; j < nparams; j++) { int k = indices[j]; double d = (a[k] == 0) ? 1e-3 : a[k] * delta; a2[k] = a[k] + d; double llh = p.computeLogLikelihood(a2); p.computeFirstDerivative(a2); double[] d1h = p.d1.clone(); a2[k] = a[k] - d; double lll = p.computeLogLikelihood(a2); p.computeFirstDerivative(a2); double[] d1l = p.d1.clone(); a2[k] = a[k]; double gradient1 = (llh - lll) / (2 * d); double gradient2 = (d1h[j] - d1l[j]) / (2 * d); //System.out.printf("[%d,%d] ll - %f (%s %f+/-%f) d1 %f ?= %f : d2 %f ?= %f\n", i, k, ll, func.getName(k), a[k], d, // gradient1, d1[j], gradient2, d2[j]); Assert.assertTrue("Not same gradient1 @ " + j, eq.almostEqualComplement(gradient1, d1[j])); Assert.assertTrue("Not same gradient2 @ " + j, eq.almostEqualComplement(gradient2, d2[j])); } } } /** * Create random elliptical Gaussian data an returns the data plus an estimate of the parameters. * Only the chosen parameters are randomised and returned for a maximum of (background, amplitude, angle, xpos, * ypos, xwidth, ywidth } * * @param npeaks * the npeaks * @param params * set on output * @param randomiseParams * Set to true to randomise the params * @return the double[] */ private double[] doubleCreateGaussianData(int npeaks, double[] params, boolean randomiseParams) { int n = blockWidth * blockWidth; // Generate a 2D Gaussian ErfGaussian2DFunction func = (ErfGaussian2DFunction) GaussianFunctionFactory.create2D(npeaks, blockWidth, blockWidth, GaussianFunctionFactory.FIT_ERF_FREE_CIRCLE, null); params[0] = random(Background); for (int i = 0, j = 1; i < npeaks; i++, j += 6) { params[j] = random(Signal); params[j + 2] = random(Xpos); params[j + 3] = random(Ypos); params[j + 4] = random(Xwidth); params[j + 5] = random(Ywidth); } double[] y = new double[n]; func.initialise(params); for (int i = 0; i < y.length; i++) { // Add random Poisson noise y[i] = rdg.nextPoisson(func.eval(i)); } if (randomiseParams) { params[0] = random(params[0]); for (int i = 0, j = 1; i < npeaks; i++, j += 6) { params[j] = random(params[j]); params[j + 2] = random(params[j + 2]); params[j + 3] = random(params[j + 3]); params[j + 4] = random(params[j + 4]); params[j + 5] = random(params[j + 5]); } } return y; } private double random(double d) { return d + rdg.nextUniform(-d * 0.1, d * 0.1); } protected int[] createData(int npeaks, int iter, ArrayList<double[]> paramsList, ArrayList<double[]> yList) { return createData(npeaks, iter, paramsList, yList, true); } protected int[] createData(int npeaks, int iter, ArrayList<double[]> paramsList, ArrayList<double[]> yList, boolean randomiseParams) { int[] x = new int[blockWidth * blockWidth]; for (int i = 0; i < x.length; i++) x[i] = i; for (int i = 0; i < iter; i++) { double[] params = new double[1 + 6 * npeaks]; double[] y = doubleCreateGaussianData(npeaks, params, randomiseParams); paramsList.add(params); yList.add(y); } return x; } protected int[] createFakeData(int nparams, int iter, ArrayList<double[]> paramsList, ArrayList<double[]> yList) { int[] x = new int[blockWidth * blockWidth]; for (int i = 0; i < x.length; i++) x[i] = i; for (int i = 0; i < iter; i++) { double[] params = new double[nparams]; double[] y = createFakeData(params); paramsList.add(params); yList.add(y); } return x; } private double[] createFakeData(double[] params) { int n = blockWidth * blockWidth; RandomGenerator r = rdg.getRandomGenerator(); for (int i = 0; i < params.length; i++) { params[i] = r.nextDouble(); } double[] y = new double[n]; for (int i = 0; i < y.length; i++) { y[i] = r.nextDouble(); } return y; } protected ArrayList<double[]> copyList(ArrayList<double[]> paramsList) { ArrayList<double[]> params2List = new ArrayList<double[]>(paramsList.size()); for (int i = 0; i < paramsList.size(); i++) { params2List.add(copydouble(paramsList.get(i))); } return params2List; } private double[] copydouble(double[] d) { double[] d2 = new double[d.length]; for (int i = 0; i < d.length; i++) d2[i] = d[i]; return d2; } void log(String format, Object... args) { System.out.printf(format, args); } }