apache-commons-math

How do I fix a org.apache.commons.math3.exception.ConvergenceException


I am trying to make a 4 parameter Hill equation fit using Commons Math version 3.6.1 . I have also tried this with the 4.0-SNAPSHOT version as of 20 June 2018. I get the same result. I have a simple test that runs and doesn't throw an exception. However a more complex bit of data fails. I grokked the data from a couple of web sights dealing with Hill / Sigmoidal fitting. I am not sure what to do next to resolve this, any suggestions?

I am getting this:

org.apache.commons.math3.exception.ConvergenceException: illegal state: unable to perform Q.R decomposition on the 9x4 jacobian matrix

    at org.apache.commons.math3.fitting.leastsquares.LevenbergMarquardtOptimizer.qrDecomposition(LevenbergMarquardtOptimizer.java:975)
    at org.apache.commons.math3.fitting.leastsquares.LevenbergMarquardtOptimizer.optimize(LevenbergMarquardtOptimizer.java:342)
    at org.apache.commons.math3.fitting.AbstractCurveFitter.fit(AbstractCurveFitter.java:63)
    at com.adarza.curve.fit.FourParamHillFitterTest.largerDataTest(FourParamHillFitterTest.java:42)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

My code follows.

Initial Params:

import lombok.Data;

@Data
public class FourParamHillEqInitParams {
    double initialHighVarD = Double.MIN_VALUE;
    double initialLowVarA = Double.MAX_VALUE;
    double midInflectionPointVarC = 0.0;
    double initialHillSlopeVarB = 0.0;
}

The curve fitter:

import org.apache.commons.math3.fitting.AbstractCurveFitter;
import org.apache.commons.math3.fitting.WeightedObservedPoint;
import org.apache.commons.math3.fitting.leastsquares.LeastSquaresBuilder;
import org.apache.commons.math3.fitting.leastsquares.LeastSquaresProblem;
import org.apache.commons.math3.linear.DiagonalMatrix;

import java.util.*;

public class FourParamHillFitter  extends AbstractCurveFitter {
    protected LeastSquaresProblem getProblem(Collection<WeightedObservedPoint> points) {
        final int len = points.size();
        final double[] target  = new double[len];
        final double[] weights = new double[len];

        FourParamHillEqInitParams initialGuesses = guessInitialCoefficents(points);

        final double[] initialGuess = { initialGuesses.initialLowVarA,
                                        initialGuesses.initialHillSlopeVarB,
                                        initialGuesses.midInflectionPointVarC,
                                        initialGuesses.initialHighVarD };

        System.out.println("Initial Guesses: " + Arrays.toString(initialGuess));

        int i = 0;
        for(WeightedObservedPoint point : points) {
            target[i]  = point.getY();
            weights[i] = point.getWeight();
            i += 1;
        }

        final AbstractCurveFitter.TheoreticalValuesFunction model = new
                AbstractCurveFitter.TheoreticalValuesFunction(new FourParamHillFunction(), points);

        return new LeastSquaresBuilder().
                maxEvaluations(Integer.MAX_VALUE).
                maxIterations(Integer.MAX_VALUE).
                start(initialGuess).
                target(target).
                weight(new DiagonalMatrix(weights)).
                model(model.getModelFunction(), model.getModelFunctionJacobian()).
                build();
    }

    private FourParamHillEqInitParams guessInitialCoefficents(Collection<WeightedObservedPoint> points) {
        FourParamHillEqInitParams initParams = new FourParamHillEqInitParams();
        double sum = 0.0;
        for (Iterator<WeightedObservedPoint> iterator = points.iterator(); iterator.hasNext(); ) {
            WeightedObservedPoint p =  iterator.next();
            if (p.getY() > initParams.initialHighVarD) {
                initParams.initialHighVarD = p.getY();
            }
            if (p.getY() < initParams.initialLowVarA){
                initParams.initialLowVarA = p.getY();
            }
            sum += p.getY();
        }
        initParams.midInflectionPointVarC = sum / points.size(); // mean
        initParams.initialHillSlopeVarB = 25.0;
        return initParams;
    }
}

The function:

import org.apache.commons.math3.analysis.ParametricUnivariateFunction;
import org.apache.commons.math3.analysis.differentiation.DerivativeStructure;

public class FourParamHillFunction implements ParametricUnivariateFunction {
    public double value(double x, double... parm) {
//        return parameters[0] * Math.pow(t, parameters[1]) * Math.exp(-parameters[2] * t);
        double a = parm[0];
        double b = parm[1];
        double c = parm[2];
        double d = parm[3];

        return d+ ((a-d)/ (1 + Math.pow( (x/c), b)));
    }


    // Jacobian matrix of the above. In this case, this is just an array of
    // partial derivatives of the above function, with one element for each parameter.
    public double[] gradient(double t, double... parameters) {
        final double a = parameters[0];
        final double b = parameters[1];
        final double c = parameters[2];
        final double d = parameters[3];

        // Jacobian Matrix Edit

        // Using Derivative Structures...
        // constructor takes 4 arguments - the number of parameters in your
        // equation to be differentiated (4 in this case), the order of
        // differentiation for the DerivativeStructure, the index of the
        // parameter represented by the DS, and the value of the parameter itself
        DerivativeStructure aDev = new DerivativeStructure(4, 1, 0, a);
        DerivativeStructure bDev = new DerivativeStructure(4, 1, 1, b);
        DerivativeStructure cDev = new DerivativeStructure(4, 1, 2, c);
        DerivativeStructure dDev = new DerivativeStructure(4, 1, 3, d);

        // define the equation to be differentiated using another DerivativeStructure
//        DerivativeStructure y = aDev.multiply(DerivativeStructure.pow(t, bDev))
//                .multiply(cDev.negate().multiply(t).exp());

        //y = d+(a-d)/(1+(x/c)^b)
        DerivativeStructure numerator = aDev.subtract(dDev);
        DerivativeStructure xPart = cDev.reciprocal().multiply(t).pow(bDev);
        DerivativeStructure denominator = xPart.add(1.0);
        DerivativeStructure y = dDev.add( numerator.divide(denominator) );

        // then return the partial derivatives required
        // notice the format, 4 arguments for the method since 4 parameters were
        // specified first order derivative of the first parameter, then the second,
        // then the third
        return new double[] {
                y.getPartialDerivative(1, 0, 0, 0),
                y.getPartialDerivative(0, 1, 0, 0),
                y.getPartialDerivative(0, 0, 1, 0),
                y.getPartialDerivative(0, 0, 0, 1)
        };
    }
}

The tests:

import org.apache.commons.math3.fitting.WeightedObservedPoint;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import static org.junit.Assert.assertEquals;

public class FourParamHillFitterTest {

    @Test
    public void basicTest() {
        FourParamHillFitter fitter = new FourParamHillFitter();

        double[] xValues = { 1.0, 2.0, 3.0, 4.0, 5.0 };
        double[] yValues = { 1.0, 1.2, 3.0, 7.0, 7.0 };

        ArrayList<WeightedObservedPoint> points = createPointsFromArray(xValues, yValues);

        double coeffs[] = fitter.fit(points);

        System.out.println(Arrays.toString(coeffs));

        assertEquals(4, coeffs.length);
        assertEquals(1.099995, coeffs[0], 0.1);
        assertEquals(31.03071, coeffs[1], 0.01);
        assertEquals(3.072862, coeffs[2], 0.01);
        assertEquals(7.000825, coeffs[3], 0.01);
    }

    @Test
    public void largerDataTest() {
        FourParamHillFitter fitter = new FourParamHillFitter();

        double[] xValues = { 0, 1.3, 2.8, 5, 10.2, 16.5, 21.3, 31.8, 52.2};
        double[] yValues = { 0.1, 0.5, 0.9, 2.6, 7.1, 12.3, 15.3, 20.4, 24.4 };

        ArrayList<WeightedObservedPoint> points = createPointsFromArray(xValues, yValues);

        final double coeffs[] = fitter.fit(points);

        System.out.println(Arrays.toString(coeffs));

        assertEquals(4, coeffs.length);
        assertEquals(0.1536, coeffs[0], 0.01);
        assertEquals(1.7718, coeffs[1], 0.01);
        assertEquals(19.3494, coeffs[2], 0.01);
        assertEquals(28.4479, coeffs[3], 0.01);
    }

    public ArrayList<WeightedObservedPoint> createPointsFromArray(double[] xs, double[] ys){
        ArrayList<WeightedObservedPoint> points = new ArrayList<WeightedObservedPoint>();

        for(int i=0; i < xs.length; i++){
            WeightedObservedPoint point = new WeightedObservedPoint(0, xs[i], ys[i]);
            points.add(point);
        }
        return points;
    }

}

Solution

  • Not sure but I assume, because if you calculate the gradient of your function:

    d+((a-d)/(1 + Math.pow( (x/c), b)))
    

    the partial derivative for b involves a Log() (natural logarithm) expression

    -(((a - d)*(x/c)^b*Log(x/c))/((x/c)^b + 1)^2)
    

    The Log(0) is -Infinity.

    So avoid x-values equal 0. x-values near 0 like 0.0001 may help.

    In my own project I implemented a symbolic gradient in the FindFit function, which may also improve the result.