1 /*
2 * Copyright (C) 2012 Alberto Irurueta Carro (alberto@irurueta.com)
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package com.irurueta.numerical.optimization;
17
18 import com.irurueta.algebra.AlgebraException;
19 import com.irurueta.algebra.Matrix;
20 import com.irurueta.algebra.WrongSizeException;
21 import com.irurueta.numerical.EvaluationException;
22 import com.irurueta.numerical.LockedException;
23 import com.irurueta.numerical.MultiDimensionFunctionEvaluatorListener;
24 import com.irurueta.numerical.NotAvailableException;
25 import com.irurueta.numerical.NotReadyException;
26
27 /**
28 * This class searches for a multi dimension function local minimum.
29 * The local minimum is searched by starting the algorithm at a start point
30 * and a given direction which should be close and point to the local minimum to
31 * be found to achieve the best accuracy with the lowest number of iterations.
32 * The implementation of this class is based on Numerical Recipes 3rd ed.
33 * Section 10.7 page 509.
34 */
35 public class PowellMultiOptimizer extends LineMultiOptimizer {
36 /**
37 * Constant defining default tolerance or accuracy to be achieved on the
38 * minimum being estimated by this class.
39 */
40 public static final double DEFAULT_TOLERANCE = 3e-8;
41
42 /**
43 * Minimum allowed tolerance value.
44 */
45 public static final double MIN_TOLERANCE = 0.0;
46
47 /**
48 * Maximum allowed iterations.
49 */
50 public static final int ITMAX = 200;
51
52 /**
53 * A small number.
54 */
55 public static final double TINY = 1e-25;
56
57 /**
58 * The fractional tolerance in the function value such that failure to
59 * decrease by more than this amount on one iteration signals doneness.
60 */
61 private double tolerance;
62
63 /**
64 * Member contains number of iterations that were needed to estimate a
65 * minimum.
66 */
67 private int iter;
68
69 /**
70 * Value of the function at the minimum.
71 */
72 private double fret;
73
74 /**
75 * Set of directions.
76 */
77 private Matrix ximat;
78
79 /**
80 * Empty constructor.
81 */
82 public PowellMultiOptimizer() {
83 super();
84 tolerance = DEFAULT_TOLERANCE;
85 iter = 0;
86 fret = 0.0;
87 ximat = null;
88 }
89
90 /**
91 * Constructor.
92 *
93 * @param listener Listener to evaluate a multidimensional function.
94 * @param tolerance Tolerance or accuracy to be expected on estimated local
95 * minimum.
96 * @throws IllegalArgumentException Raised if tolerance is negative.
97 */
98 public PowellMultiOptimizer(final MultiDimensionFunctionEvaluatorListener listener, final double tolerance) {
99 super(listener);
100 internalSetTolerance(tolerance);
101 iter = 0;
102 fret = 0.0;
103 ximat = null;
104 }
105
106 /**
107 * Constructor.
108 *
109 * @param listener Listener to evaluate a multidimensional function.
110 * @param point Start point where algorithm will be started. Start point
111 * should be close to the local minimum to be found. Provided array must
112 * have a length equal to the number of dimensions of the function being
113 * evaluated, otherwise and exception will be raised when searching for the
114 * minimum.
115 * @param directions Set of directions to start looking for a minimum.
116 * Provided matrix must have the same rows as the number of dimensions of
117 * the function being evaluated. Each column will be a direction to search
118 * for a minimum.
119 * @param tolerance Tolerance or accuracy to be expected on estimated local
120 * minimum.
121 * @throws IllegalArgumentException Raised if provided point and direction
122 * don't have the same length or if provided tolerance is negative.
123 */
124 public PowellMultiOptimizer(
125 final MultiDimensionFunctionEvaluatorListener listener, final double[] point, final Matrix directions,
126 final double tolerance) {
127 super(listener);
128 internalSetPointAndDirections(point, directions);
129 internalSetTolerance(tolerance);
130 iter = 0;
131 fret = 0.0;
132 }
133
134 /**
135 * Returns set of directions to start looking for a minimum.
136 * Returned matrix will have the same number of rows as the number of
137 * dimensions of the function (or the start point).
138 * Each column of the matrix will represent a vector containing a direction
139 * to search for a minimum.
140 *
141 * @return Set of directions.
142 * @throws NotAvailableException Raised if this has not yet been provided or
143 * computed.
144 */
145 public Matrix getDirections() throws NotAvailableException {
146 if (!areDirectionsAvailable()) {
147 throw new NotAvailableException();
148 }
149 return ximat;
150 }
151
152 /**
153 * Returns boolean indicating whether set of directions is available for
154 * retrieval.
155 *
156 * @return True if available, false otherwise.
157 */
158 public boolean areDirectionsAvailable() {
159 return ximat != null;
160 }
161
162 /**
163 * Sets start point and set of directions to start looking for minimum.
164 *
165 * @param point Start point where algorithm will be started. Start point
166 * should be close to the local minimum to be found. Provided array must
167 * have a length equal to the number of dimensions of the function being
168 * evaluated, otherwise and exception will be raised when searching for the
169 * minimum.
170 * @param directions Set of directions to start looking for a minimum.
171 * Provided matrix must have the same rows as the number of dimensions of
172 * the function being evaluated. Each column will be a direction to search
173 * for a minimum.
174 * @throws LockedException Raised if this instance is locked.
175 * @throws IllegalArgumentException Raised if provided point and direction
176 * don't have the same length.
177 */
178 public void setPointAndDirections(final double[] point, final Matrix directions) throws LockedException {
179 if (isLocked()) {
180 throw new LockedException();
181 }
182 internalSetPointAndDirections(point, directions);
183 }
184
185 /**
186 * Sets start point where local minimum is searched nearby.
187 *
188 * @param startPoint Start point to search for a local minimum
189 * @throws LockedException Raised if this instance is locked.
190 */
191 public void setStartPoint(final double[] startPoint) throws LockedException {
192 if (isLocked()) {
193 throw new LockedException();
194 }
195 p = startPoint;
196 }
197
198 /**
199 * Returns tolerance or accuracy to be expected on estimated local minimum.
200 *
201 * @return Tolerance or accuracy to be expected on estimated local minimum.
202 */
203 public double getTolerance() {
204 return tolerance;
205 }
206
207 /**
208 * Internal method to set tolerance or accuracy to be expected on estimated
209 * local minimum.
210 * This method does not check whether this instance is locked.
211 *
212 * @param tolerance Tolerance or accuracy to be expected on estimated local
213 * minimum.
214 * @throws IllegalArgumentException Raised if provided tolerance is
215 * negative.
216 */
217 private void internalSetTolerance(final double tolerance) {
218 if (tolerance < MIN_TOLERANCE) {
219 throw new IllegalArgumentException();
220 }
221 this.tolerance = tolerance;
222 }
223
224 /**
225 * Sets tolerance or accuracy to be expected on estimated local minimum.
226 *
227 * @param tolerance Tolerance or accuracy to be expected on estimated local
228 * minimum.
229 * @throws LockedException Raised if this instance is locked.
230 * @throws IllegalArgumentException Raised if provided tolerance is
231 * negative.
232 */
233 public void setTolerance(final double tolerance) throws LockedException {
234 if (isLocked()) {
235 throw new LockedException();
236 }
237 internalSetTolerance(tolerance);
238 }
239
240 /**
241 * This function estimates a function minimum.
242 * Implementations of this class will usually search a local minimum close
243 * to a start point and will start looking into provided start direction.
244 * Minimization of a function f. Input consists of an initial starting
245 * point p. The initial matrix ximat, whose columns contain the initial
246 * set of directions, is set to the identity. Returned is the best point
247 * found, at which point fret is the minimum function value and iter is
248 * the number of iterations taken.
249 *
250 * @throws LockedException Raised if this instance is locked, because
251 * estimation is being computed.
252 * @throws NotReadyException Raised if this instance is not ready, because
253 * a listener, a gradient listener and a start point haven't been provided.
254 * @throws OptimizationException Raised if the algorithm failed because of
255 * lack of convergence or because function couldn't be evaluated.
256 */
257 @Override
258 public void minimize() throws LockedException, NotReadyException, OptimizationException {
259
260 if (isLocked()) {
261 throw new LockedException();
262 }
263 if (!isReady()) {
264 throw new NotReadyException();
265 }
266
267 locked = true;
268
269 buildDirections();
270
271 // in case that set of directions have been directly provided, we check
272 // for correctness
273 final int n = p.length;
274 if (ximat.getRows() != ximat.getColumns() || ximat.getRows() != n) {
275 locked = false;
276 throw new OptimizationException();
277 }
278
279 double fptt;
280 final var pt = new double[n];
281 final var ptt = new double[n];
282
283 // set vector of directions
284 if (!isDirectionAvailable() || xi.length != n) {
285 xi = new double[n];
286 }
287
288 try {
289 fret = listener.evaluate(p);
290
291 // save the initial point
292 System.arraycopy(p, 0, pt, 0, n);
293
294 for (iter = 0; ; ++iter) {
295 final var fp = fret;
296 var ibig = 0;
297 // Will be the biggest function decrease
298 var del = 0.0;
299 // In each iteration, loop over all directions in the set.
300 for (var i = 0; i < n; i++) {
301 // Copy the direction contained in i-th column of ximat
302 ximat.getSubmatrixAsArray(0, i, n - 1, i, xi);
303 fptt = fret;
304
305 // minimize along it, and record it if it is the largest
306 // decrease so far.
307 fret = linmin();
308 if (fptt - fret > del) {
309 del = fptt - fret;
310 ibig = i + 1;
311 }
312 }
313
314 // Here comes the termination criterion.
315 if (2.0 * (fp - fret) <= tolerance * (Math.abs(fp) +
316 Math.abs(fret)) + TINY) {
317 // minimum has been found
318 break;
319 }
320 if (iter == ITMAX) {
321 // too many iterations
322 locked = false;
323 throw new OptimizationException();
324 }
325
326 // Construct the extrapolated point and the average direction
327 // moved. Save the old starting point
328 for (var j = 0; j < n; j++) {
329 ptt[j] = 2.0 * p[j] - pt[j];
330 xi[j] = p[j] - pt[j];
331 pt[j] = p[j];
332 }
333
334 // Function value at extrapolated point
335 fptt = listener.evaluate(ptt);
336
337 if (fptt < fp) {
338 final var t = 2.0 * (fp - 2.0 * fret + fptt) * sqr(fp - fret - del) - del * sqr(fp - fptt);
339
340 if (t < 0.0) {
341 // Move to the minimum of the new direction, and save the
342 // new direction
343 fret = linmin();
344
345 ximat.setSubmatrix(0, ibig - 1,
346 n - 1, ibig - 1, ximat,
347 0, n - 1,
348 n - 1, n - 1);
349 ximat.setSubmatrix(0, n - 1,
350 n - 1, n - 1, xi);
351 }
352 }
353
354 if (iterationCompletedListener != null) {
355 iterationCompletedListener.onIterationCompleted(this, iter, ITMAX);
356 }
357 }
358 } catch (final AlgebraException | EvaluationException e) {
359 throw new OptimizationException(e);
360 } finally {
361 locked = false;
362 }
363
364 // set result
365 xmin = p;
366 resultAvailable = true;
367 fmin = fret;
368 }
369
370 /**
371 * Returns boolean indicating whether this instance is ready to start the
372 * estimation of a local minimum.
373 * An instance is ready once a listener and a start point are provided.
374 *
375 * @return True if this instance is ready, false otherwise.
376 */
377 @Override
378 public boolean isReady() {
379 return isListenerAvailable() && isStartPointAvailable();
380 }
381
382 /**
383 * Internal method to set start point and set of directions to start looking
384 * for minimum.
385 * This method does not check whether this instance is locked.
386 *
387 * @param point Start point where algorithm will be started. Start point
388 * should be close to the local minimum to be found. Provided array must
389 * have a length equal to the number of dimensions of the function being
390 * evaluated, otherwise and exception will be raised when searching for the
391 * minimum.
392 * @param directions Set of directions to start looking for a minimum.
393 * Provided matrix must have the same rows as the number of dimensions of
394 * the function being evaluated. Each column will be a direction to search
395 * for a minimum.
396 * @throws IllegalArgumentException Raised if provided point and direction
397 * don't have the same length.
398 */
399 private void internalSetPointAndDirections(final double[] point, final Matrix directions) {
400 if ((point.length != directions.getRows()) || (point.length != directions.getColumns())) {
401 throw new IllegalArgumentException();
402 }
403 p = point;
404 ximat = directions;
405 xi = null;
406 }
407
408 /**
409 * Internal method to build or rebuild the set of directions if needed.
410 *
411 * @throws NotReadyException Raised if no start point has yet been provided.
412 */
413 private void buildDirections() throws NotReadyException {
414 if (!isStartPointAvailable()) {
415 throw new NotReadyException();
416 }
417
418 final var n = p.length;
419
420 if (areDirectionsAvailable() && (ximat.getRows() == n) && (ximat.getColumns() == n)) {
421 return;
422 }
423
424 try {
425 ximat = Matrix.identity(n, n);
426 } catch (final WrongSizeException ignore) {
427 // never happens
428 }
429 }
430
431 /**
432 * Computes the squared value of provided double.
433 *
434 * @param x Value to be squared.
435 * @return Squared value.
436 */
437 private double sqr(double x) {
438 return x * x;
439 }
440 }