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 }