View Javadoc
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  /*
19   * This is not the original file distributed by the Apache Software Foundation
20   * It has been modified by the Hipparchus project
21   */
22  package org.hipparchus.optim.nonlinear.vector.leastsquares;
23  
24  import org.hipparchus.exception.MathIllegalArgumentException;
25  import org.hipparchus.exception.MathIllegalStateException;
26  import org.hipparchus.exception.NullArgumentException;
27  import org.hipparchus.linear.ArrayRealVector;
28  import org.hipparchus.linear.MatrixDecomposer;
29  import org.hipparchus.linear.MatrixUtils;
30  import org.hipparchus.linear.QRDecomposer;
31  import org.hipparchus.linear.RealMatrix;
32  import org.hipparchus.linear.RealVector;
33  import org.hipparchus.optim.ConvergenceChecker;
34  import org.hipparchus.optim.LocalizedOptimFormats;
35  import org.hipparchus.optim.nonlinear.vector.leastsquares.LeastSquaresProblem.Evaluation;
36  import org.hipparchus.util.Incrementor;
37  import org.hipparchus.util.Pair;
38  
39  /**
40   * Gauss-Newton least-squares solver.
41   * <p>
42   * This class solve a least-square problem by solving the normal equations
43   * of the linearized problem at each iteration. Either LU decomposition or
44   * Cholesky decomposition can be used to solve the normal equations, or QR
45   * decomposition or SVD decomposition can be used to solve the linear system.
46   * Cholesky/LU decomposition is faster but QR decomposition is more robust for difficult
47   * problems, and SVD can compute a solution for rank-deficient problems.
48   */
49  public class GaussNewtonOptimizer implements LeastSquaresOptimizer {
50  
51      /**
52       * The singularity threshold for matrix decompositions. Determines when a {@link
53       * MathIllegalStateException} is thrown. The current value was the default value for {@link
54       * org.hipparchus.linear.LUDecomposition}.
55       */
56      private static final double SINGULARITY_THRESHOLD = 1e-11;
57  
58      /** Decomposer */
59      private final MatrixDecomposer decomposer;
60  
61      /** Indicates if normal equations should be formed explicitly. */
62      private final boolean formNormalEquations;
63  
64      /**
65       * Creates a Gauss Newton optimizer.
66       * <p>
67       * The default for the algorithm is to use QR decomposition and not
68       * form normal equations.
69       * </p>
70       */
71      public GaussNewtonOptimizer() {
72          this(new QRDecomposer(SINGULARITY_THRESHOLD), false);
73      }
74  
75      /**
76       * Create a Gauss Newton optimizer that uses the given matrix decomposition algorithm
77       * to solve the normal equations.
78       *
79       * @param decomposer          the decomposition algorithm to use.
80       * @param formNormalEquations whether the normal equations should be explicitly
81       *                            formed. If {@code true} then {@code decomposer} is used
82       *                            to solve J<sup>T</sup>Jx=J<sup>T</sup>r, otherwise
83       *                            {@code decomposer} is used to solve Jx=r. If {@code
84       *                            decomposer} can only solve square systems then this
85       *                            parameter should be {@code true}.
86       */
87      public GaussNewtonOptimizer(final MatrixDecomposer decomposer,
88                                  final boolean formNormalEquations) {
89          this.decomposer          = decomposer;
90          this.formNormalEquations = formNormalEquations;
91      }
92  
93      /**
94       * Get the matrix decomposition algorithm.
95       *
96       * @return the decomposition algorithm.
97       */
98      public MatrixDecomposer getDecomposer() {
99          return decomposer;
100     }
101 
102     /**
103      * Configure the matrix decomposition algorithm.
104      *
105      * @param newDecomposer the decomposition algorithm to use.
106      * @return a new instance.
107      */
108     public GaussNewtonOptimizer withDecomposer(final MatrixDecomposer newDecomposer) {
109         return new GaussNewtonOptimizer(newDecomposer, this.isFormNormalEquations());
110     }
111 
112     /**
113      * Get if the normal equations are explicitly formed.
114      *
115      * @return if the normal equations should be explicitly formed. If {@code true} then
116      * {@code decomposer} is used to solve J<sup>T</sup>Jx=J<sup>T</sup>r, otherwise
117      * {@code decomposer} is used to solve Jx=r.
118      */
119     public boolean isFormNormalEquations() {
120         return formNormalEquations;
121     }
122 
123     /**
124      * Configure if the normal equations should be explicitly formed.
125      *
126      * @param newFormNormalEquations whether the normal equations should be explicitly
127      *                               formed. If {@code true} then {@code decomposer} is used
128      *                               to solve J<sup>T</sup>Jx=J<sup>T</sup>r, otherwise
129      *                               {@code decomposer} is used to solve Jx=r. If {@code
130      *                               decomposer} can only solve square systems then this
131      *                               parameter should be {@code true}.
132      * @return a new instance.
133      */
134     public GaussNewtonOptimizer withFormNormalEquations(final boolean newFormNormalEquations) {
135         return new GaussNewtonOptimizer(this.getDecomposer(), newFormNormalEquations);
136     }
137 
138     /** {@inheritDoc} */
139     @Override
140     public Optimum optimize(final LeastSquaresProblem lsp) {
141         //create local evaluation and iteration counts
142         final Incrementor evaluationCounter = lsp.getEvaluationCounter();
143         final Incrementor iterationCounter = lsp.getIterationCounter();
144         final ConvergenceChecker<Evaluation> checker
145                 = lsp.getConvergenceChecker();
146 
147         // Computation will be useless without a checker (see "for-loop").
148         if (checker == null) {
149             throw new NullArgumentException();
150         }
151 
152         RealVector currentPoint = lsp.getStart();
153 
154         // iterate until convergence is reached
155         Evaluation current = null;
156         while (true) {
157             iterationCounter.increment();
158 
159             // evaluate the objective function and its jacobian
160             Evaluation previous = current;
161             // Value of the objective function at "currentPoint".
162             evaluationCounter.increment();
163             current = lsp.evaluate(currentPoint);
164             final RealVector currentResiduals = current.getResiduals();
165             final RealMatrix weightedJacobian = current.getJacobian();
166             currentPoint = current.getPoint();
167 
168             // Check convergence.
169             if (previous != null &&
170                 checker.converged(iterationCounter.getCount(), previous, current)) {
171                 return Optimum.of(current,
172                                   evaluationCounter.getCount(),
173                                   iterationCounter.getCount());
174             }
175 
176             // solve the linearized least squares problem
177             final RealMatrix lhs; // left hand side
178             final RealVector rhs; // right hand side
179             if (this.formNormalEquations) {
180                 final Pair<RealMatrix, RealVector> normalEquation =
181                         computeNormalMatrix(weightedJacobian, currentResiduals);
182                 lhs = normalEquation.getFirst();
183                 rhs = normalEquation.getSecond();
184             } else {
185                 lhs = weightedJacobian;
186                 rhs = currentResiduals;
187             }
188             final RealVector dX;
189             try {
190                 dX = this.decomposer.decompose(lhs).solve(rhs);
191             } catch (MathIllegalArgumentException e) {
192                 // change exception message
193                 throw new MathIllegalStateException(
194                         LocalizedOptimFormats.UNABLE_TO_SOLVE_SINGULAR_PROBLEM, e);
195             }
196             // update the estimated parameters
197             currentPoint = currentPoint.add(dX);
198         }
199     }
200 
201     /** {@inheritDoc} */
202     @Override
203     public String toString() {
204         return "GaussNewtonOptimizer{" +
205                 "decomposer=" + decomposer +
206                 ", formNormalEquations=" + formNormalEquations +
207                 '}';
208     }
209 
210     /**
211      * Compute the normal matrix, J<sup>T</sup>J.
212      *
213      * @param jacobian  the m by n jacobian matrix, J. Input.
214      * @param residuals the m by 1 residual vector, r. Input.
215      * @return  the n by n normal matrix and  the n by 1 J<sup>Tr vector.
216      */
217     private static Pair<RealMatrix, RealVector> computeNormalMatrix(final RealMatrix jacobian,
218                                                                     final RealVector residuals) {
219         //since the normal matrix is symmetric, we only need to compute half of it.
220         final int nR = jacobian.getRowDimension();
221         final int nC = jacobian.getColumnDimension();
222         //allocate space for return values
223         final RealMatrix normal = MatrixUtils.createRealMatrix(nC, nC);
224         final RealVector jTr = new ArrayRealVector(nC);
225         //for each measurement
226         for (int i = 0; i < nR; ++i) {
227             //compute JTr for measurement i
228             for (int j = 0; j < nC; j++) {
229                 jTr.setEntry(j, jTr.getEntry(j) +
230                         residuals.getEntry(i) * jacobian.getEntry(i, j));
231             }
232 
233             // add the the contribution to the normal matrix for measurement i
234             for (int k = 0; k < nC; ++k) {
235                 //only compute the upper triangular part
236                 for (int l = k; l < nC; ++l) {
237                     normal.setEntry(k, l, normal.getEntry(k, l) +
238                             jacobian.getEntry(i, k) * jacobian.getEntry(i, l));
239                 }
240             }
241         }
242         //copy the upper triangular part to the lower triangular part.
243         for (int i = 0; i < nC; i++) {
244             for (int j = 0; j < i; j++) {
245                 normal.setEntry(i, j, normal.getEntry(j, i));
246             }
247         }
248         return new Pair<RealMatrix, RealVector>(normal, jTr);
249     }
250 
251 }