package gdsc.smlm.fitting.nonlinear.stop;
import gdsc.core.utils.DoubleEquality;
import gdsc.smlm.fitting.nonlinear.StoppingCriteria;
/*-----------------------------------------------------------------------------
* GDSC SMLM Software
*
* Copyright (C) 2013 Alex Herbert
* Genome Damage and Stability Centre
* University of Sussex, UK
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*---------------------------------------------------------------------------*/
/**
* Defines the stopping criteria for the {@link gdsc.smlm.fitting.nonlinear.NonLinearFit } class.
* <p>
* Stop when N successive iterations reduce error by a negligible amount.
*/
public class ErrorStoppingCriteria extends StoppingCriteria
{
private int iterationCount;
private int iterationLimit = 1;
private int significantDigits;
private long maxUlps;
private boolean avoidPlateau = false;
private int insignificantImprovmentIteration;
private int improvementExponent;
private int insignificantImprovmentCounter;
private static final int CONSECUTIVE_INSIGNIFICANT_IMPROVEMENT_LIMIT = 3;
/**
* Instantiates a new error stopping criteria.
*/
public ErrorStoppingCriteria()
{
this(5);
}
/**
* Instantiates a new error stopping criteria.
*
* @param significantDigits
* the significant digits for a negligible change in error
*/
public ErrorStoppingCriteria(int significantDigits)
{
setSignificantDigits(significantDigits);
}
/*
* (non-Javadoc)
*
* @see gdsc.fitting.model.StoppingCriteria#initialise(double[])
*/
@Override
public void initialise(double[] a)
{
super.initialise(a);
iterationCount = 0;
// Used to avoid flats of insignificant improvement later in the routine
if (avoidPlateau)
{
insignificantImprovmentIteration = getMaximumIterations() / 2;
improvementExponent = 0;
insignificantImprovmentCounter = 0;
}
else
{
insignificantImprovmentIteration = getMaximumIterations();
}
}
/*
* (non-Javadoc)
*
* @see gdsc.fitting.model.StoppingCriteria#copyCoefficients(double[])
*/
protected void copyCoefficients(double[] a)
{
// Do nothing
}
/*
* (non-Javadoc)
*
* @see gdsc.smlm.fitting.nonlinear.stoppingCriteria#evaluate(double, double, double[])
*/
@Override
public void evaluate(double oldError, double newError, double[] a)
{
// Use a comparison of error to a set number of significant digits
final long c = DoubleEquality.signedComplement(newError, oldError);
int result;
if (c > 0)
{
// Fit is worse
// Setting the iteration count to zero forces the negligible improvements to be sequential.
//
// Note: The NonLinearFit algorithm may jump around a low minimum finding marginally
// higher error fits. If the count is reset each time then N-sequential improvements
// may not be found. In practice this reset is not necessary.
//iterationCount = 0;
result = 1;
increment(a, false);
}
else
{
//final double diff = oldError - newError;
// Allow debugging of the change in error
// Record how many significant digits the improvement was
//System.out.printf("Iter %d : Abs %g : Rel %g (%d)\n", getIteration(), diff,
// DoubleEquality.relativeError(newError, oldError),
// getExponent(DoubleEquality.relativeError(newError, oldError)));
// Check if equal or the fit is near perfect
boolean negligable = false;
if (Math.abs(c) < maxUlps || newError < 0.001 || false)
{
negligable = true;
}
else
{
// The desire is to avoid a lot of computation if the error is not really moving anywhere.
// Once we have reached a set number of iterations, e.g. maximumIterations/2
// then allow stopping if the absolute change is the same.
if (getIteration() > insignificantImprovmentIteration)
{
// Get the exponent of the improvement
int exp = getExponent(DoubleEquality.relativeError(newError, oldError));
// All we know is that there is an improvement but not enough to signal convergence.
// If the improvements get smaller then it may converge anyway.
// If the improvements get larger then the routine should be allowed to run.
// So check to see if the improvement is the same.
// Q. Should a check be made to see if the exponent is small?
if (improvementExponent == exp)
{
negligable = (++insignificantImprovmentCounter >= CONSECUTIVE_INSIGNIFICANT_IMPROVEMENT_LIMIT);
//System.out.printf("Tiny improvement %f = %f @ %d\n", oldError - newError,
// DoubleEquality.relativeError(newError, oldError), getIteration());
}
else
{
insignificantImprovmentCounter = 0;
improvementExponent = exp;
}
}
}
if (negligable)
{
// Improvement is negligible
result = 0;
iterationCount++;
if (iterationCount >= getIterationLimit() && getIteration() > getMinimumIterations())
{
areAchieved = true;
notSatisfied = false;
}
}
else
{
// Improvement is significant
// Setting the iteration count to zero forces negligible improvements to be successive
// (i.e. no significant jumps between insignificant jumps)
result = -1;
iterationCount = 0;
}
increment(a, true);
}
if (log != null)
{
log.info("iter = %d, error = %f -> %f : %s : Continue = %b", getIteration(), oldError, newError,
(result == 1) ? "worse"
: "Delta = " + DoubleEquality.relativeError(oldError, newError) +
((result == 0) ? " (negligible)" : ""),
notSatisfied);
}
}
/**
* Bit mask to isolate the exponent field of a <code>double</code>.
*/
private static final long EXP_BIT_MASK = 0x7FF0000000000000L;
/**
* The number of logical bits in the significand of a <code>double</code> number, including the implicit bit.
*/
private static final int SIGNIFICAND_WIDTH = 53;
/**
* Bias used in representing a <code>double</code> exponent.
*/
private static final int EXP_BIAS = 1023;
/**
* Returns unbiased exponent of a <code>double</code>.
*/
public static int getExponent(double d)
{
// Copied from java.lang.StrictMath.getExponent(double d)
// This is only available in Java 1.6
/*
* Bitwise convert d to long, mask out exponent bits, shift
* to the right and then subtract out double's bias adjust to
* get true exponent value.
*/
return (int) (((Double.doubleToRawLongBits(d) & EXP_BIT_MASK) >> (SIGNIFICAND_WIDTH - 1)) - EXP_BIAS);
}
/**
* Set the number of iterations that the fit has to improve by a negligible amount
*
* @param iterationLimit
* the iterationLimit to set
*/
public void setIterationLimit(int iterationLimit)
{
this.iterationLimit = iterationLimit;
}
/**
* @return the iterationLimit
*/
public int getIterationLimit()
{
return iterationLimit;
}
/**
* @param significantDigits
* the significant digits for a negligible change in error
*/
public void setSignificantDigits(int significantDigits)
{
this.significantDigits = significantDigits;
maxUlps = DoubleEquality.getUlps(significantDigits);
}
/**
* @return the significantDigits
*/
public int getSignificantDigits()
{
return significantDigits;
}
/**
* @return true if avoiding plateaus
*/
public boolean isAvoidPlateau()
{
return avoidPlateau;
}
/**
* During an optimisation the error can asymptote below the improvement required to achieve convergence. This
* can be avoided by setting the avoid plateau flag to true. In this case an additional check will be made for
* slowly improving error when more than half-way towards the maximum number of iterations. If 3 consecutive
* improvements in error are at the same level of floating point accuracy then convergence is achieved.
* <p>
* Note that no check is made that the amount of improvement in error is small (as is done using the standard
* significant digits check). Thus it is possible that a plateau will be detected when the error value is still
* significantly improving (but by the same amount each step). Chances of this will be minimised by using a maximum
* number of iterations approximately twice that which allows reasonable fits to converge. E.g. If typical fitted
* data converges within 10 iterations to 6 significant digits then the maximum iterations should be set to 20.
*
* @param avoidPlateau
* Set to true to avoid plateaus
*/
public void setAvoidPlateau(boolean avoidPlateau)
{
this.avoidPlateau = avoidPlateau;
}
}