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<>(normal, jTr);
249 }
250
251 }