Minimizer.java |
package ij.measure; import ij.*; import ij.gui.*; import ij.macro.*; import ij.gui.Roi; import ij.gui.PolygonRoi; import ij.gui.Line; import ij.util.Tools; import ij.plugin.frame.RoiManager; import java.util.Random; import java.util.Arrays; import java.util.Vector; import java.util.Hashtable; /** Minimizer based on Nelder-Mead simplex method (also known as polytope method), * including the 'outside contraction' as described in: * J.C. Lagarias, J.A. Reeds, M.H. Wright, P. Wright: * Convergence properties of the Nelder-Mead simplex algorithm in low dimensions. * SIAM J. Optim. 9, 112-147 (1998). * Differences w.r.t. this publication: * - If outside contraction is rejected, instead of shrinking the whole simplex, an inside * contraction is tried first. Own experiments show that this results in slightly better * performance for some test functions (Perm, Mc Kinnon's function with tau=2, theta=6, * Osborne1 curve-fitting problem). In most cases, there is no difference, however. * - This implementation does not include any 'ordering rules' in case of equal function values. * - When checking for convergence, a special iteration step may be performed, improving * the best vertex of the simplex. * * Re-initialization within a minimization run: * In some cases, the simplex algorithm may fail to find the minimum or the convergence * check might stop it prematurely. Therefore, the search is initialized again, keeping the * best vertex of the simplex and setting the other vertices to random values, but keeping * variations of the parameters w.r.t. the best vertex at a similar value. If re-initializing * the simplex does not lead to a significant improvement, the value is accepted as true * (local) minimum. * * Multiple minimization runs: * In spite of re-initializing (see above), there are rare cases where minimization is stopped * too early. Also, minimization may result in a local minimum. Therefore, unless determined * otherwise by setting 'setRestarts', two minimization runs with different initialization * of the simplex are started in parallel threads. If the results don't agree within the * error limit, two more minimization runs are started. This is repeated until the two best * results agree within the error limits or the number of restarts (determined by 'setRestarts'; * default 2, i.e., up to 3 runs with two threads each) is exceeded. * This does not guarantee that the minimum is a global minimum, however: A local minimum * will be accepted if the minimizer finds a local minimum twice (or two different local * minima with the same function value within the error bounds), but no better minimum has * been found at that time. * * The user-supplied target function should return NaN for out-of-bounds parameters instead * of a high (penalty) value (minimization is faster and more reliable with NaNs). * The region where the function is defined (e.g. not returning NaN) must be convex. * Sharp corners of the region where the function value is defined (especially in higher dimensions) * may cause a problem with finding suitable test points when (re-)initializing the simplex. * If all attempts to find initial points result in NaN, the status returned is * INITIALIZATION_FAILURE. * * Versions: * Michael Schmid 2012-01-30: first version, based on previous CurveFitter * 2012-11-20: mor tries to find initial params not leading to NaN * 2013-09-24: 50% higher maximum iteration count, and never uses more than 0.4*maxIter * iterations per minimization to avoid trying too few sets of initial params * 2013-10-13: setStatusAndEscape to show iterations and enable abort by ESC * 2014-09-16: normalization bug fixed. makeNewParamVariations checks for paramResolutions * */ public class Minimizer { /** Status returned: successful completion */ public final static int SUCCESS = 0; /** Status returned: Could not initialize the simplex because either the initialParams * resulted in the target function returning NaN or all attempts to find starting * parameters for the other simplex points resulted in the target function returning NaN. * No minimization was possible. */ public final static int INITIALIZATION_FAILURE = 1; /** Status returned: aborted by call to abort method. */ public final static int ABORTED = 2; /** Status returned: Could not reinitialize the simplex because all attempts to find restarting * parameters resulted in the target function returning NaN. Reinitialization is * needed to obtain a reliable result; so the result may be inaccurate or wrong. */ public final static int REINITIALIZATION_FAILURE = 3; /** Status returned: no convergence detected after maximum iteration count */ public final static int MAX_ITERATIONS_EXCEEDED = 4; /** Status returned: not two equal solutions after maximum number of restarts */ public final static int MAX_RESTARTS_EXCEEDED = 5; /** Strings describing the status codes */ public final static String[] STATUS_STRING = { "Success", "Initialization failure; no result", "Aborted", "Re-initialization failure (inaccurate result?)", "Max. no. of iterations reached (inaccurate result?)", "Max. no. of restarts reached (inaccurate result?)"}; private final static double C_REFLECTION = 1.0; // reflection coefficient private final static double C_CONTRACTION = 0.5; // contraction coefficient private final static double C_EXPANSION = 2.0; // expansion coefficient private final static double C_SHRINK = 0.5; // shrink coefficient private final static int ITER_FACTOR = 750; // maximum number of iterations per numParams^2; twice that value for 2 threads private final static int WORST=0, NEXT_WORST=1, BEST=2;//indices in array to pass the numbers of the respective vertices private int numParams; // number of independent variables (parameters) private int numVertices; // numParams+1, number of vertices of the simplex private int numExtraArrayElements; // number of extra array elements for private use in user function private UserFunction userFunction; // target function to minimize private double maxRelError = 1e-10; // max relative error private double maxAbsError = 1e-100; // max absolute error private double[] paramResolutions; // differences of parameters less than these values are considered 0 private int maxIter; // stops after this number of iterations private int totalNumIter; // number of iterations performed in all tries so far private int numCompletedMinimizations; // number of minimizations completed private int maxRestarts = 2; // number of times to try finding local minima; each uses 2 threads if restarts>0 private int randomSeed; // for starting the random number generator private boolean useSingleThread = // single thread if one processor or set by useSingleThread(true) Runtime.getRuntime().availableProcessors() <= 1; private int status; // SUCCESS or error code private boolean wasInitialized; // initialization was successful at least once private double[] result; // result data+function value private Vector<double[]> resultsVector; // holds several results if multiple tries; the best one is kept. private String ijStatusString = null; // shown together with iteration count in status, no display if null private boolean checkEscape; // whether to stop when Escape is pressed private int nextIterationForStatus = 10;// next iteration when we should display the status private long startTime; // of the whole minimization process /*private Hashtable<Thread, double[][]> simpTable = new Hashtable<Thread, double[][]>(); //for each thread, holds a reference to its simplex */ //----------------------------------------------------------------------------- // We can't set the function in the constructor because the CurveFitter // allows specifying the fit function after other variables /** * Set the the target function, i.e. function whose value should be minimized. * @param userFunction The class having a function to minimize (implementing * the UserFunction interface). * This function must allow simultaneous calls in multiple threads unless * setMaximumThreads(1); has been called. * Note that the function will be called with at least numParams+1 array * elements; the last one is needed to store the function value. Further * array elements for private use in the user function may be added by * calling setExtraArrayElements. * @param numParams Number of independent variables (also called parameters) * of the function. */ public void setFunction(UserFunction userFunction, int numParams) { if (maxIter<=0) { maxIter = ITER_FACTOR*numParams*numParams; if (maxRestarts > 0) maxIter *= 2; } this.userFunction = userFunction; this.numParams = numParams; this.numVertices = numParams+1; } /** Perform minimization with the gradient-enhanced simplex method once or a few * times, depending on the value of 'restarts'. Running it several times helps * to reduce the probability of finding local minima or accepting one of the rare * results where the minimizer has got stuck before finding the true minimum. * We are using two threads and terminate after two equal results. Thus, apart * from the overhead of starting a new thread (typically < 1 ms), for unproblematic * minimization problems on a dual-core machine this is almost as fast as running * it once. * * Use 'setFunction' first to define the function and number of parameters. * Afterwards, use the 'getParams' method to access the result. * * @param initialParams Array with starting values of the parameters (variables). * When null, the starting values are assumed to be 0. * The target function must be defined (not returning NaN) for * the values specified as initialParams. * @param initialParamVariations Parameters (variables) are initially varied by up to +/- * this value. If not given (null), initial variations are taken as * 10% of initial parameter value or 0.01 for parameters that are zero. * When this array is given, all elements must be positive (nonzero). * If one or several initial parameters are zero, is advisable to set the initialParamVariations * array to useful values indicating the typical order of magnitude of the parameters. * For target functions with only one minimum, convergence is fastest with large values of * initialParamVariations, so that the expected value is within initialParam+/-initialParamVariations. * If local minima can occur, it is best to use a value close to the expected global minimum, * and rather small initialParamVariations, much lower than the distance to the nearest local * minimum. * * @return status code; SUCCESS if two attempts have found minima with the * same value (within the error bounds); so a minimum has been found * with very high probability. */ public int minimize(final double[] initialParams, final double[] initialParamVariations) { status = SUCCESS; resultsVector = new Vector<double[]>(); int maxLoopCount = maxRestarts+1; if (useSingleThread) maxLoopCount*=2; // if we have only one thread, loop twice as many times for (int i=0; i<maxLoopCount; i++) { // try several times, until we have twice the same result Thread secondThread = null; if (maxRestarts>0 && !useSingleThread) { // set up 2nd thread to minimize final int seed = randomSeed+1000000+i; final Thread thread = new Thread( new Runnable() { final public void run() { minimizeOnce(initialParams, initialParamVariations, seed); } }, "Minimizer-1" ); thread.setPriority( Thread.currentThread().getPriority() ); thread.start(); secondThread = thread; } minimizeOnce(initialParams, initialParamVariations, randomSeed+i); //minimize in main thread if (secondThread != null) try { secondThread.join(); // wait until send thread is done } catch (InterruptedException e) {} if (resultsVector.size() == 0 && result==null) return status; if (result==null) result = (double[])resultsVector.get(0); for (double[] r : resultsVector) // find best result so far if (value(r) < value(result)) result = r; if (status != SUCCESS && status != REINITIALIZATION_FAILURE && status != MAX_ITERATIONS_EXCEEDED) return status; // no more tries if permanent error or aborted if (totalNumIter >= maxIter) return MAX_ITERATIONS_EXCEEDED; // no more tries if too many iterations for (int ir=0; ir<resultsVector.size(); ir++) if (!belowErrorLimit(value((double[])resultsVector.get(ir)), value(result), 1.0)) { resultsVector.remove(ir); // discard results that are significantly worse ir --; } if (resultsVector.size() >= 2) return SUCCESS; // if we have two (almost) equal results, it's enough } //for i <= maxRestarts if (ijStatusString != null) IJ.showStatus(""); // reset status display return maxRestarts>0 ? MAX_RESTARTS_EXCEEDED : // number of restarts exceeded without two equal results status; // if only one run was required, we can't have 2 equal results } /** Perform minimization with the simplex method once, including re-initialization until * we have a stable solution. * Use the 'getParams' method to access the result. * * @param initialParams Array with starting values of the parameters (variables). * When null, the starting values are assumed to be 0. * The target function must be defined (not returning NaN) for * the values specified as initialParams. * @param initialParamVariations Parameters (variables) are initially varied by up to +/- * this value. If not given (null), iniital variations are taken as * 10% of inial parameter value or 0.01 for parameters that are zero. * When this array is given, all elements must be positive (nonzero). * If one or several initial parameters are zero, is advisable to set the initialParamVariations * array to useful values indicating the typical order of magnitude of the parameters. * For target functions with only one minimum, convergence is fastest with large values of * initialParamVariations, so that the expected value is within initialParam+/-initialParamVariations. * If local minima can occur, it is best to use a value close to the expected global minimum, * and rather small initialParamVariations, much lower than the distance to the nearest local * minimum. * * @return status code; SUCCESS if it is considered likely that a minimum of the * target function has been found. */ public int minimizeOnce(double[] initialParams, double[] initialParamVariations) { status = SUCCESS; minimizeOnce(initialParams, initialParamVariations, randomSeed); return status; } /** Get the result, i.e., the set of parameter values (i.e., variable values) * from the best corner of the simplex. Note that the array returned may have more * elements than numParams; ignore the rest. * May return an array with only NaN values in case the minimize call has returned * an INITIALIZATION_FAILURE status or that abort() has been called at the very * beginning of the minimization. * Do not call this method before minimization. */ public double[] getParams() { if (result == null) { result = new double[numParams+1+numExtraArrayElements]; Arrays.fill(result, Double.NaN); } return result; } /** Get the value of the minimum, i.e. the value associated with the resulting parameters * as obtained by getParams(). May return NaN in case the minimize call has returned * an INITIALIZATION_FAILURE status or that abort() has been called at the very * beginning of the minimization. * Do not call this method before minimization. */ public double getFunctionValue() { if (result == null) { result = new double[numParams+1]; Arrays.fill(result, Double.NaN); } return value(result); } /** Get number of iterations performed (includes all restarts). One iteration needs * between one and numParams+3 calls of the target function (typically two calls * per iteration) */ public int getIterations() { return totalNumIter; } /** Set maximum number of iterations allowed (including all restarts and all threads). * The number of function calls will be higher, up to about twice the number of * iterations. * Note that the number of iterations needed typically scales with the square of * the dimensions (i.e., numParams^2). * Default value is 1000 * numParams^2 (half that value if maxRestarts=0), which is * enough for >99% of all cases (if the maximum number of restarts is set to 2); * typical number of iterations are below 10 and 20% of this value. */ public void setMaxIterations(int x) { maxIter = x; } /** Get maximum number of iterations allowed. Unless given by 'setMaxIterations', * this value is defined only after running 'setFunction' */ public int getMaxIterations() { return maxIter; } /** Set maximum number of minimization restarts to do. * With n=0, the minimizer is run once in a single thread. * With n>0, two threads are used, and if the two results do not agree within * the error bounds, additional optimizations are done up to n times, each * with two threads. In any case, if the two best results are within the error * bounds, the best result is accepted. * Thus, on dual-core machines running no other jobs, values of n=1 or n=2 (default) * do not cause a notable increase of computing time for 'easy' optimization problems, * while greatly reducing the risk of running into spurious local minima or non- * optimal results due to the minimizer getting stuck. In problematic cases, the * improved * The 'n' value does not refer to the restarts within one minimization run * (there, at least one restart is performed, and restart is repeated until the result * does not change within the error bounds). * This value does not affect the 'minimizeOnce' function call. * When setting the maximum number of restarts to a value much higher than 3, remember * to adjust the maximum number of iterations (see setMaxIterations). */ public void setMaxRestarts(int n) { maxRestarts = n; } /** Get maximum number of minimization restarts to do */ public int getMaxRestarts() { return maxRestarts; } /** Get number of minimizations completed (i.e. not aborted or stopped because the * number of minimization was exceeded). After a minimize(..) call, typically 2 * for unproblematic cases. Higher number indicate a functin that is difficult to * minimize or the existence of more than one minimum. */ public int getCompletedMinimizations() { return numCompletedMinimizations; } /** Set a seed to initialize the random number generator, which is used for initialization * of the simplex. */ public void setRandomSeed(int n) { randomSeed = n; } /** Sets the accuracy of convergence. Minimizing is done as long as the * relative error of the function value is more than this number (Default: 1e-10). */ public void setMaxError(double maxRelError) { this.maxRelError = maxRelError; } /** Sets the accuracy of convergence. Minimizing is done as long as the * relative error of the function value is more than maxRelError (Default: 1e-10) * and the maximum absolute error is more than maxAbsError * (i.e. it is enough to fulfil one of these two criteria) */ public void setMaxError(double maxRelError, double maxAbsError) { this.maxRelError = maxRelError; this.maxAbsError = maxAbsError; } /** Set the resolution of the parameters, for problems where the target function is not smooth * but suffers from numerical noise. If all parameters of all vertices are closer to the * best value than the respective resolution value, minimization is finished, irrespective * of the difference of the target function values at the vertices */ public void setParamResolutions(double[] paramResolutions) { this.paramResolutions = paramResolutions; } /** Call setMaximuThreads(1) to avoid multi-threaded execution (in case the user-provided * target function does not allow moultithreading). Currently a maximum of 2 thread is used * irrespective of any higher value. */ public void setMaximumThreads (int numThreads) { useSingleThread = numThreads <= 1; } /** Aborts minimization. Calls to getParams() will return the best solution found so far. * This method may be called from the user-supplied target function. * If displayStatusAndCheckEsc has been called before, the Minimizer itself checks for the ESC key. */ public void abort() { status = ABORTED; } /** Create output on the number of iterations in the ImageJ Status line, e.g. * "<ijStatusString> 50 (max 750); ESC to stop" * @param ijStatusString Displayed in the beginning of the status message. No display if null. * E.g. "Optimization: Iteration " * @param checkEscape When true, the Minimizer stops if escape is pressed and the status * becomes ABORTED. Note that checking for ESC does not work in the Event Queue thread. */ public void setStatusAndEsc(String ijStatusString, boolean checkEscape) { this.ijStatusString = ijStatusString; this.checkEscape = checkEscape; } /** Add a given number of extra elements to array of parameters (independent vaiables) * for private use in the user function. Note that the first numParams+1 elements * should not be touched.*/ public void setExtraArrayElements(int numExtraArrayElements) { this.numExtraArrayElements = numExtraArrayElements; } /** Get the full simplex of the current thread. This may be useful if the target function * wants to modify the simplex */ /* public double[][] getSimplex() { return simpTable.get(Thread.currentThread()); } */ /** One minimization run (including reinitializations of the simplex until the result is stable) */ private void minimizeOnce(double[] initialParams, double[] initialParamVariations, int seed) { Random random = new Random(seed); double[][] simp = makeSimplex(initialParams, initialParamVariations, random); if (simp == null) { status = wasInitialized ? REINITIALIZATION_FAILURE : INITIALIZATION_FAILURE; return; } wasInitialized = true; if (startTime == 0) startTime = System.currentTimeMillis(); //if (IJ.debugMode) showSimplex(simp, seed+" Initialized:"); int bestVertexNumber = minimize(simp); // first minimization double bestValueSoFar = value(simp[bestVertexNumber]); //reinitialize until converged or error/aborted (don't care about reinitialization failure in other thread) boolean reinitialisationFailure = false; while (status == SUCCESS || status == REINITIALIZATION_FAILURE) { double[] paramVariations = makeNewParamVariations(simp, bestVertexNumber, initialParams, initialParamVariations); if (!reInitializeSimplex(simp, bestVertexNumber, paramVariations, random)) { reinitialisationFailure = true; break; } //if (IJ.debugMode) showSimplex(simp, seed+" Reinitialized:"); bestVertexNumber = minimize(simp); // minimize with reinitialized simplex if (belowErrorLimit(value(simp[bestVertexNumber]), bestValueSoFar, 2.0)) break; bestValueSoFar = value(simp[bestVertexNumber]); } if (reinitialisationFailure) status = REINITIALIZATION_FAILURE; else if (status == SUCCESS || status == REINITIALIZATION_FAILURE) //i.e. not aborted, not max iterations exceeded numCompletedMinimizations++; // it was a complete minimization //if (IJ.debugMode) showSimplex(simp, seed+" Final:"); if (resultsVector != null) synchronized(resultsVector) { resultsVector.add(simp[bestVertexNumber]); } else result = simp[bestVertexNumber]; } /** Minimizes the target function by variation of the simplex. * Note that one call to this function never does more than 0.4*maxIter iterations. * @return index of the best value in simp */ private int minimize(double[][] simp) { int[] worstNextBestArray = new int[3]; // used to pass these indices from 'order' function double[] center = new double[numParams+1+numExtraArrayElements]; // center of all vertices except worst double[] reflected = new double[numParams+1+numExtraArrayElements]; // the 1st new vertex to try double[] secondTry = new double[numParams+1+numExtraArrayElements]; // expanded or contracted vertex order(simp, worstNextBestArray); int worst = worstNextBestArray[WORST]; int nextWorst = worstNextBestArray[NEXT_WORST]; int best = worstNextBestArray[BEST]; //showSimplex(simp, "before minimization, value="+value(simp[best])); //String operation="ini"; int thisNumIter=0; while (true) { totalNumIter++; // global count over all threads thisNumIter++; // local count for this minimize call // THE MINIMIZAION ALGORITHM IS HERE iteration: { getCenter(simp, worst, center); // centroid of vertices except worst // Reflect worst vertex through centriod of not-worst getVertexAndEvaluate(center, simp[worst], -C_REFLECTION, reflected); if (value(reflected) <= value(simp[best])) { // if it's better than the best... // try expanding it getVertexAndEvaluate(center, simp[worst], -C_EXPANSION, secondTry); if (value(secondTry) <= value(reflected)) { copyVertex(secondTry, simp[worst]); // if expanded is better than reflected, keep it //operation="expa"; break iteration; } } if (value(reflected) < value(simp[nextWorst])) { copyVertex(reflected, simp[worst]); // keep reflected if better than 2nd worst //operation="refl"; break iteration; } else if (value(reflected) < value(simp[worst])) { // try outer contraction getVertexAndEvaluate(center, simp[worst], -C_CONTRACTION, secondTry); if (value(secondTry) <= value(reflected)) { copyVertex(secondTry, simp[worst]); // keep outer contraction //operation="outC"; break iteration; } } else if (value(reflected) > value(simp[worst]) || Double.isNaN(value(reflected))) { // else inner contraction getVertexAndEvaluate(center, simp[worst], C_CONTRACTION, secondTry); if (value(secondTry) < value(simp[worst])) { copyVertex(secondTry, simp[worst]); // keep contracted if better than 2nd worst //operation="innC"; break iteration; } } // if everything else has failed, contract simplex in on best shrinkSimplexAndEvaluate(simp, best); //operation="shri"; break iteration; } // iteration: boolean checkParamResolution = // if new 'worst' is not close to 'best', don't check any further paramResolutions!=null && belowResolutionLimit(simp[worst], simp[best]); order(simp, worstNextBestArray); worst = worstNextBestArray[WORST]; nextWorst = worstNextBestArray[NEXT_WORST]; best = worstNextBestArray[BEST]; if (checkParamResolution) if (belowResolutionLimit(simp, best)) // check whether all parameters are within the resolution limit break; if (belowErrorLimit(value(simp[best]), value(simp[worst]), 4.0)) { // make sure we are at the minimum: getCenter(simp, -1, secondTry); // check center of the simplex evaluate(secondTry); if (value(secondTry) < value(simp[best])) copyVertex(secondTry, simp[best]); // better than best: keep } if (belowErrorLimit(value(simp[best]), value(simp[worst]), 4.0)) // no large spread of values break; // looks like we are at the minimum if (totalNumIter > maxIter || thisNumIter>4*(maxIter/10)) status = MAX_ITERATIONS_EXCEEDED; if (status != SUCCESS) break; if ((ijStatusString != null || checkEscape) && totalNumIter > nextIterationForStatus) { long time = System.currentTimeMillis(); nextIterationForStatus = totalNumIter + (int)(totalNumIter*500L/(time-startTime+1)); //next display 0.5 sec later if (time - startTime > 1000L) { // display status and check for ESC after the first second if (checkEscape && IJ.escapePressed()) { status = ABORTED; IJ.resetEscape(); IJ.showStatus(ijStatusString+" ABORTED"); break; } if (ijStatusString != null) { String statusString = ijStatusString+totalNumIter+" ("+maxIter+" max)"; if (checkEscape) statusString += " ESC to stop"; IJ.showStatus(statusString); } } } } //showSimplex(simp, "after "+totalNumIter+" iterations: value="+value(simp[best])); return best; } /** Move along the line between center and worst, where +1 corresponds to worstVertex * The new point is written to'newVertex' and target function is evaluated there */ private void getVertexAndEvaluate(double[] center, double[] worstVertex, double howFar, double[] newVertex) { for (int i = 0; i < numParams; i++) newVertex[i] = (1.-howFar)*center[i] + howFar*worstVertex[i]; evaluate(newVertex); } /** Get center of all vertices except one to exclude * (may be -1 to exclude none) * Does not care about function values (i.e., last array elements) */ private void getCenter(double[][]simp, int excludeVertex, double[] center) { Arrays.fill(center, 0.); int nV = 0; for (int v=0; v<numVertices; v++) if (v != excludeVertex) { for (int i=0; i<numParams; i++) center[i] += simp[v][i]; nV++; } double norm = 1.0/nV; for (int i=0; i<numParams; i++) center[i] *= norm; } private void shrinkSimplexAndEvaluate(double[][] simp, int best) { for (int v=0; v<numVertices; v++) if (v != best) { for (int i=0; i<numParams; i++) simp[v][i] = C_SHRINK*simp[v][i] + (1-C_SHRINK)*simp[best][i]; evaluate(simp[v]); } } /** Whether the highest and lowest values fulfill the error limit criterion. * Error limits are reduced by a factor of 'sensitivity' */ private boolean belowErrorLimit(double highest, double lowest, double sensitivity) { double absError = sensitivity*Math.abs(highest - lowest); double relError = absError/(Math.max(Math.abs(highest),Math.abs(lowest))+1e-100); return relError < maxRelError || absError < maxAbsError; } /** Initialise the simplex and evaluate it. Returns null on failure. */ private double[][] makeSimplex(double[] initialParams, double[] initialParamVariations, Random random) { double[][] simp = new double[numVertices][numParams+1+numExtraArrayElements]; /* simpTable.put(Thread.currentThread(), simp); */ if (initialParams!=null) { for (int i=0; i<numParams; i++) if (Double.isNaN(initialParams[i])) if (IJ.debugMode) IJ.log("Warning: Initial Parameter["+i+"] is NaN"); System.arraycopy(initialParams, 0, simp[0], 0, Math.min(initialParams.length, numParams)); } evaluate(simp[0]); if (Double.isNaN(value(simp[0]))) { if (IJ.debugMode) showVertex(simp[0], "Warning: Initial Parameters yield NaN:"); findValidInitalParams(simp[0], initialParamVariations, random); } if (Double.isNaN(value(simp[0]))) { if (IJ.debugMode) IJ.log("Error: Could not find initial parameters not yielding NaN:"); return null; } if (initializeSimplex(simp, initialParamVariations, random)) return simp; else { if (IJ.debugMode) showSimplex(simp, "Error: Could not make simplex vertices not yielding NaN"); return null; } } /** Whether the distance between the best and any other simplex vertex * is less than the resolution of the parameters */ private boolean belowResolutionLimit(double[][] simp, int best) { for (int v=0; v<numVertices; v++) if (v!= best && !belowResolutionLimit(simp[v], simp[best])) return false; return true; } /** Whether the distance between two vertices is less than the resolution of the parameters */ private boolean belowResolutionLimit(double[] vertex1, double[] vertex2) { for (int i=0; i<numParams; i++) if (Math.abs(vertex1[i]-vertex2[i]) >= paramResolutions[i]) return false; return true; } /** Find initial parameters not yielding a result of NaN in case those given yield NaN * Called with params containing the initial parameters that have been tried previously */ private void findValidInitalParams(double[] params, double[] initialParamVariations, Random random) { final int maxAttempts = 50*numParams*numParams; //max number of attempts to find params that do not lead to NaN double rangeFactor = 1; // will gradually become larger to handle different orders of magnitude final double rangeMultiplyLog = Math.log(1e20)/(maxAttempts-1); //will try up to 1e-20 to 1e20*initialParamVariations double[] firstParams = new double[numParams]; // remember starting params (which may be modified) double[] variations = new double[numParams]; // new values of parameter variations for (int i=0; i<numParams; i++) { firstParams[i] = Double.isNaN(params[i]) ? 0 : params[i]; variations[i] = initialParamVariations!=null ? initialParamVariations[i] : 0.1*firstParams[i]; if (Double.isNaN(variations[i]) || Math.abs(variations[i])<1e-10 || Math.abs(variations[i])>1e10) variations[i] = 0.1; } for (int attempt=0; attempt<maxAttempts; attempt++) { for (int i=0; i<numParams; i++) { double multiplier = attempt<maxAttempts/10 ? 1 : //after a while try different orders of magnitude Math.exp(rangeMultiplyLog*attempt*2*(random.nextDouble()-0.5)); params[i] = multiplier*(firstParams[i]+2*(random.nextDouble()-0.5)*variations[i]); } evaluate(params); //showVertex(params,"findValidInitalParams attempt "+attempt); if (!Double.isNaN(value(params))) return; //found a valid parameter set } } /** Reinitialize an existing simplex: Create new vertices around the best one, keeping the rough size of * the simplex. This helps to avoid premature termination or cases where the simplex has become degenerate. * All points of the new simplex are evaluated already. * Returns false on error (if it could not create a simplex not resulting in NaN). */ private boolean reInitializeSimplex(double[][] simp, int bestVertexNumber, double[] paramVariations, Random random) { if (bestVertexNumber != 0) { double[] swap = simp[0]; simp[0] = simp[bestVertexNumber]; simp[bestVertexNumber] = swap; } return initializeSimplex(simp, paramVariations, random); } /** Initialize simplex vertices except the number 0, which will be taken as starting point. * All points of the new simplex are evaluated already. * Returns false on error (if it could not create a simplex not resulting in NaN). */ private boolean initializeSimplex(double[][] simp, double[] paramVariations, Random random) { double[] variations = new double[numParams]; // improved values of parameter variations for (int i=0; i<numParams; i++) { double range = paramVariations!=null && i<paramVariations.length ? paramVariations[i] : 0.1 * Math.abs(simp[0][i]); // if nothing else given, based on initial parameter if (range < 1e-100 || Double.isNaN(range)) range = 0.01; //range must be nonzero if (Math.abs(range/simp[0][i])<1e-10) range = Math.abs(simp[0][i]*1e-10); //range must be more than very last digits variations[i] = range; //IJ.log("param#"+i+" variation="+(float)variations[i]); } final int maxAttempts = 100*numParams; //max number of attempts to find params that do not lead to NaN for (int v=1; v<numVertices; v++) { int numTries = 0; do { //try finding a vertex that does not yield NaN if (numTries++ > maxAttempts) return false; // Create a vector orthogonal to all others in a coordinate system where every value is // normalized to its respective variations value. We first use this coordinate system where // every parameter is in the [-1, +1] range. // Don't orthogonalize after getting only NaN function values in many attempts; it might be // easier to find viable parameters without normalization. for (int i=0; i<numParams; i++) simp[v][i] = 2*random.nextFloat() - 1.0; if (numTries < maxAttempts/3) // (don't orthogonalize if finding valid params was very difficult //IJ.log("orthogonalize vertex "+v); for (int v2=1; v2<v; v2++) { // to avoid a degenerate simplex, make orthogonal to all others double lengthSqr = 0; double innerProduct = 0; for (int i=0; i<numParams; i++) { double x2 = (simp[v2][i] - simp[0][i])/variations[i]; // convert to [-1, +1] range lengthSqr += x2*x2; innerProduct += x2*simp[v][i]; } for (int i=0; i<numParams; i++) //subtract component parallel to the other vector simp[v][i] -= ((simp[v2][i] - simp[0][i])/variations[i])*(innerProduct/lengthSqr); } double sumSqr = 0; // normalize the 'excursion vector' to length = 1 for (int i=0; i<numParams; i++) sumSqr += simp[v][i]*simp[v][i]; if (sumSqr < 1e-14) continue; // bad luck with random numbers, excursion vector almost zero if (numTries > 2 && sumSqr > 1e-6) sumSqr = 1; // don't normalize if attempts with normalized were unsuccessful for (int i=0; i<numParams; i++) simp[v][i] = simp[0][i] + simp[v][i]/Math.sqrt(sumSqr)*variations[i]; evaluate(simp[v]); } while (Double.isNaN(value(simp[v])) && (status == SUCCESS || status == REINITIALIZATION_FAILURE)); } //showSimplex(simp, 0); return true; } /** Calculate ranges for parameter variations for re-initializing: * Note that the simplex should have adapted in size to the useful range of each * parameter, but in special cases it might have become degenerate [i.e. the * (hyper-)volume spanned by the vertices might be zero; in such a case it may * happen that one parameter has the same value for all vertices of the simplex]. * * For each parameter, as a variation value we use the typical difference w.r.t. the best * vertex of the simplex. * To avoid a degenerate simplex, we have to know what the the relative orders of magnitude of * the parameter variations should be. If initialParamVariations are given, we use them as * indication of the typoical relative values; otherwise we use the parameter values themselves * and assume that the variation of each parameter should be roughly the same fraction of the value. * If the variation of a parameter in the simplex w.r.t. this estimated variation is much * smaller (<10^3) than typical for the parameters, we increase the variation for this parameter. */ private double[] makeNewParamVariations(double[][] simp, int bestVertexNumber, double[] initialParams, double[] initialParamVariations) { double[] paramVariations = new double[numParams]; // will be the return value double[] relatedTo = new double[numParams]; // parameter variation should be related to (in size) these values double logTypicalRelativeVariation = 0; for (int i=0; i<numParams; i++) { // for all parameters double variation = 0; // roughly size of simplex in direction of that parameter for (int v=0; v<numVertices; v++) if (v != bestVertexNumber) { double delta = Math.abs(simp[v][i] - simp[bestVertexNumber][i]); paramVariations[i] += delta*delta; } paramVariations[i] = 10*Math.sqrt(paramVariations[i]); // make the new simplex larger than the old one relatedTo[i] = initialParamVariations!=null && initialParamVariations.length>= numParams ? initialParamVariations[i] : // relate to initialParamVariations (if given), Math.max(Math.abs(initialParams[i]), Math.abs(simp[bestVertexNumber][i])); double logRelativeVariation = paramVariations[i]>relatedTo[i] ? 0 : Math.log(paramVariations[i]/relatedTo[i]); logTypicalRelativeVariation += logRelativeVariation; } logTypicalRelativeVariation /= numParams; double typicalRelativeVariation = Math.exp(logTypicalRelativeVariation); final double WORST_RATIO = 1e-3; //parameter variation should not be lower than typical value by more than this for (int i=0; i<numParams; i++) { if (paramVariations[i]<relatedTo[i] && paramVariations[i]/relatedTo[i] < typicalRelativeVariation*WORST_RATIO) paramVariations[i] = relatedTo[i]*typicalRelativeVariation*WORST_RATIO; if (paramResolutions != null && paramVariations[i] < 10*paramResolutions[i]) paramVariations[i] = 10*paramResolutions[i];// parameter variation must be well above numeric noise limit } return paramVariations; } /** Evaluate the target function for the parameters given by 'vertex' * The function value is stored into the last (extra) array element. */ private void evaluate(double[] vertex) { vertex[numParams] = userFunction.userFunction(vertex, 0); } /** Function value of a vertex after call to 'evaluate' * (stored it as last array element) */ private double value(double[] vertex) { return vertex[numParams]; } /** Keep the newVertex: replace a given Vertex with it */ void copyVertex(double[] newVertex, double[] vertex) { System.arraycopy(newVertex, 0, vertex, 0, newVertex.length); } /** Find the simplex vertices with the worst, nextWorst and best values * of the target function */ private void order(double[][] simp, int[] worstNextBestArray) { int worst = 0, nextWorst = 0, best = 0; for (int i = 0; i < numVertices; i++) { if (value(simp[i]) < value(simp[best])) best = i; if (value(simp[i]) > value(simp[worst])) worst = i; } nextWorst = best; for (int i = 0; i < numVertices; i++) if (i != worst && value(simp[i]) > value(simp[nextWorst])) nextWorst = i; worstNextBestArray[WORST] = worst; worstNextBestArray[NEXT_WORST] = nextWorst; worstNextBestArray[BEST] = best; } // Display simplex [Iteration: s0(p1, p2....), s1(),....] in Log window private synchronized void showSimplex(double[][] simp, String heading) { IJ.log("Minimizer: "+heading); for (int i = 0; i < numVertices; i++) showVertex(simp[i], null); } private synchronized void showVertex(double[] vertex, String heading) { if (heading != null) IJ.log(heading); String s = ""; for (int j=0; j < numParams; j++) s += " " + IJ.d2s(vertex[j], 8,12); s += " -> " + IJ.d2s(value(vertex), 8,12); IJ.log(s); } }