View Javadoc
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 }