GaussNewtonOptimizer.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      https://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */

  17. /*
  18.  * This is not the original file distributed by the Apache Software Foundation
  19.  * It has been modified by the Hipparchus project
  20.  */
  21. package org.hipparchus.optim.nonlinear.vector.leastsquares;

  22. import org.hipparchus.exception.MathIllegalArgumentException;
  23. import org.hipparchus.exception.MathIllegalStateException;
  24. import org.hipparchus.exception.NullArgumentException;
  25. import org.hipparchus.linear.ArrayRealVector;
  26. import org.hipparchus.linear.MatrixDecomposer;
  27. import org.hipparchus.linear.MatrixUtils;
  28. import org.hipparchus.linear.QRDecomposer;
  29. import org.hipparchus.linear.RealMatrix;
  30. import org.hipparchus.linear.RealVector;
  31. import org.hipparchus.optim.ConvergenceChecker;
  32. import org.hipparchus.optim.LocalizedOptimFormats;
  33. import org.hipparchus.optim.nonlinear.vector.leastsquares.LeastSquaresProblem.Evaluation;
  34. import org.hipparchus.util.Incrementor;
  35. import org.hipparchus.util.Pair;

  36. /**
  37.  * Gauss-Newton least-squares solver.
  38.  * <p>
  39.  * This class solve a least-square problem by solving the normal equations
  40.  * of the linearized problem at each iteration. Either LU decomposition or
  41.  * Cholesky decomposition can be used to solve the normal equations, or QR
  42.  * decomposition or SVD decomposition can be used to solve the linear system.
  43.  * Cholesky/LU decomposition is faster but QR decomposition is more robust for difficult
  44.  * problems, and SVD can compute a solution for rank-deficient problems.
  45.  */
  46. public class GaussNewtonOptimizer implements LeastSquaresOptimizer {

  47.     /**
  48.      * The singularity threshold for matrix decompositions. Determines when a {@link
  49.      * MathIllegalStateException} is thrown. The current value was the default value for {@link
  50.      * org.hipparchus.linear.LUDecomposition}.
  51.      */
  52.     private static final double SINGULARITY_THRESHOLD = 1e-11;

  53.     /** Decomposer */
  54.     private final MatrixDecomposer decomposer;

  55.     /** Indicates if normal equations should be formed explicitly. */
  56.     private final boolean formNormalEquations;

  57.     /**
  58.      * Creates a Gauss Newton optimizer.
  59.      * <p>
  60.      * The default for the algorithm is to use QR decomposition and not
  61.      * form normal equations.
  62.      * </p>
  63.      */
  64.     public GaussNewtonOptimizer() {
  65.         this(new QRDecomposer(SINGULARITY_THRESHOLD), false);
  66.     }

  67.     /**
  68.      * Create a Gauss Newton optimizer that uses the given matrix decomposition algorithm
  69.      * to solve the normal equations.
  70.      *
  71.      * @param decomposer          the decomposition algorithm to use.
  72.      * @param formNormalEquations whether the normal equations should be explicitly
  73.      *                            formed. If {@code true} then {@code decomposer} is used
  74.      *                            to solve J<sup>T</sup>Jx=J<sup>T</sup>r, otherwise
  75.      *                            {@code decomposer} is used to solve Jx=r. If {@code
  76.      *                            decomposer} can only solve square systems then this
  77.      *                            parameter should be {@code true}.
  78.      */
  79.     public GaussNewtonOptimizer(final MatrixDecomposer decomposer,
  80.                                 final boolean formNormalEquations) {
  81.         this.decomposer          = decomposer;
  82.         this.formNormalEquations = formNormalEquations;
  83.     }

  84.     /**
  85.      * Get the matrix decomposition algorithm.
  86.      *
  87.      * @return the decomposition algorithm.
  88.      */
  89.     public MatrixDecomposer getDecomposer() {
  90.         return decomposer;
  91.     }

  92.     /**
  93.      * Configure the matrix decomposition algorithm.
  94.      *
  95.      * @param newDecomposer the decomposition algorithm to use.
  96.      * @return a new instance.
  97.      */
  98.     public GaussNewtonOptimizer withDecomposer(final MatrixDecomposer newDecomposer) {
  99.         return new GaussNewtonOptimizer(newDecomposer, this.isFormNormalEquations());
  100.     }

  101.     /**
  102.      * Get if the normal equations are explicitly formed.
  103.      *
  104.      * @return if the normal equations should be explicitly formed. If {@code true} then
  105.      * {@code decomposer} is used to solve J<sup>T</sup>Jx=J<sup>T</sup>r, otherwise
  106.      * {@code decomposer} is used to solve Jx=r.
  107.      */
  108.     public boolean isFormNormalEquations() {
  109.         return formNormalEquations;
  110.     }

  111.     /**
  112.      * Configure if the normal equations should be explicitly formed.
  113.      *
  114.      * @param newFormNormalEquations whether the normal equations should be explicitly
  115.      *                               formed. If {@code true} then {@code decomposer} is used
  116.      *                               to solve J<sup>T</sup>Jx=J<sup>T</sup>r, otherwise
  117.      *                               {@code decomposer} is used to solve Jx=r. If {@code
  118.      *                               decomposer} can only solve square systems then this
  119.      *                               parameter should be {@code true}.
  120.      * @return a new instance.
  121.      */
  122.     public GaussNewtonOptimizer withFormNormalEquations(final boolean newFormNormalEquations) {
  123.         return new GaussNewtonOptimizer(this.getDecomposer(), newFormNormalEquations);
  124.     }

  125.     /** {@inheritDoc} */
  126.     @Override
  127.     public Optimum optimize(final LeastSquaresProblem lsp) {
  128.         //create local evaluation and iteration counts
  129.         final Incrementor evaluationCounter = lsp.getEvaluationCounter();
  130.         final Incrementor iterationCounter = lsp.getIterationCounter();
  131.         final ConvergenceChecker<Evaluation> checker
  132.                 = lsp.getConvergenceChecker();

  133.         // Computation will be useless without a checker (see "for-loop").
  134.         if (checker == null) {
  135.             throw new NullArgumentException();
  136.         }

  137.         RealVector currentPoint = lsp.getStart();

  138.         // iterate until convergence is reached
  139.         Evaluation current = null;
  140.         while (true) {
  141.             iterationCounter.increment();

  142.             // evaluate the objective function and its jacobian
  143.             Evaluation previous = current;
  144.             // Value of the objective function at "currentPoint".
  145.             evaluationCounter.increment();
  146.             current = lsp.evaluate(currentPoint);
  147.             final RealVector currentResiduals = current.getResiduals();
  148.             final RealMatrix weightedJacobian = current.getJacobian();
  149.             currentPoint = current.getPoint();

  150.             // Check convergence.
  151.             if (previous != null &&
  152.                 checker.converged(iterationCounter.getCount(), previous, current)) {
  153.                 return Optimum.of(current,
  154.                                   evaluationCounter.getCount(),
  155.                                   iterationCounter.getCount());
  156.             }

  157.             // solve the linearized least squares problem
  158.             final RealMatrix lhs; // left hand side
  159.             final RealVector rhs; // right hand side
  160.             if (this.formNormalEquations) {
  161.                 final Pair<RealMatrix, RealVector> normalEquation =
  162.                         computeNormalMatrix(weightedJacobian, currentResiduals);
  163.                 lhs = normalEquation.getFirst();
  164.                 rhs = normalEquation.getSecond();
  165.             } else {
  166.                 lhs = weightedJacobian;
  167.                 rhs = currentResiduals;
  168.             }
  169.             final RealVector dX;
  170.             try {
  171.                 dX = this.decomposer.decompose(lhs).solve(rhs);
  172.             } catch (MathIllegalArgumentException e) {
  173.                 // change exception message
  174.                 throw new MathIllegalStateException(
  175.                         LocalizedOptimFormats.UNABLE_TO_SOLVE_SINGULAR_PROBLEM, e);
  176.             }
  177.             // update the estimated parameters
  178.             currentPoint = currentPoint.add(dX);
  179.         }
  180.     }

  181.     /** {@inheritDoc} */
  182.     @Override
  183.     public String toString() {
  184.         return "GaussNewtonOptimizer{" +
  185.                 "decomposer=" + decomposer +
  186.                 ", formNormalEquations=" + formNormalEquations +
  187.                 '}';
  188.     }

  189.     /**
  190.      * Compute the normal matrix, J<sup>T</sup>J.
  191.      *
  192.      * @param jacobian  the m by n jacobian matrix, J. Input.
  193.      * @param residuals the m by 1 residual vector, r. Input.
  194.      * @return  the n by n normal matrix and  the n by 1 J<sup>Tr vector.
  195.      */
  196.     private static Pair<RealMatrix, RealVector> computeNormalMatrix(final RealMatrix jacobian,
  197.                                                                     final RealVector residuals) {
  198.         //since the normal matrix is symmetric, we only need to compute half of it.
  199.         final int nR = jacobian.getRowDimension();
  200.         final int nC = jacobian.getColumnDimension();
  201.         //allocate space for return values
  202.         final RealMatrix normal = MatrixUtils.createRealMatrix(nC, nC);
  203.         final RealVector jTr = new ArrayRealVector(nC);
  204.         //for each measurement
  205.         for (int i = 0; i < nR; ++i) {
  206.             //compute JTr for measurement i
  207.             for (int j = 0; j < nC; j++) {
  208.                 jTr.setEntry(j, jTr.getEntry(j) +
  209.                         residuals.getEntry(i) * jacobian.getEntry(i, j));
  210.             }

  211.             // add the the contribution to the normal matrix for measurement i
  212.             for (int k = 0; k < nC; ++k) {
  213.                 //only compute the upper triangular part
  214.                 for (int l = k; l < nC; ++l) {
  215.                     normal.setEntry(k, l, normal.getEntry(k, l) +
  216.                             jacobian.getEntry(i, k) * jacobian.getEntry(i, l));
  217.                 }
  218.             }
  219.         }
  220.         //copy the upper triangular part to the lower triangular part.
  221.         for (int i = 0; i < nC; i++) {
  222.             for (int j = 0; j < i; j++) {
  223.                 normal.setEntry(i, j, normal.getEntry(j, i));
  224.             }
  225.         }
  226.         return new Pair<>(normal, jTr);
  227.     }

  228. }