package ij.measure;
import ij.*;
import ij.gui.*;
import ij.macro.*;
import ij.util.Tools;
import ij.util.IJMath;
import java.util.Arrays;
import java.util.Hashtable;
import java.awt.Color;
public class CurveFitter implements UserFunction{
public static final int STRAIGHT_LINE=0, POLY2=1, POLY3=2, POLY4=3,
EXPONENTIAL=4, POWER=5, LOG=6, RODBARD=7, GAMMA_VARIATE=8, LOG2=9,
RODBARD2=10, EXP_WITH_OFFSET=11, GAUSSIAN=12, EXP_RECOVERY=13,
INV_RODBARD=14, EXP_REGRESSION=15, POWER_REGRESSION=16,
POLY5=17, POLY6=18, POLY7=19, POLY8=20,
GAUSSIAN_NOOFFSET=21, EXP_RECOVERY_NOOFFSET=22, CHAPMAN=23, ERF=24;
public static final int[] sortedTypes = { STRAIGHT_LINE, POLY2, POLY3, POLY4,
POLY5, POLY6, POLY7, POLY8,
POWER, POWER_REGRESSION,
EXPONENTIAL, EXP_REGRESSION, EXP_WITH_OFFSET,
EXP_RECOVERY, EXP_RECOVERY_NOOFFSET,
LOG, LOG2,
GAUSSIAN, GAUSSIAN_NOOFFSET, ERF,
RODBARD, RODBARD2, INV_RODBARD,
GAMMA_VARIATE,CHAPMAN
};
public static final String[] fitList = {"Straight Line","2nd Degree Polynomial",
"3rd Degree Polynomial", "4th Degree Polynomial","Exponential","Power",
"Log","Rodbard", "Gamma Variate", "y = a+b*ln(x-c)","Rodbard (NIH Image)",
"Exponential with Offset","Gaussian", "Exponential Recovery",
"Inverse Rodbard", "Exponential (linear regression)", "Power (linear regression)",
"5th Degree Polynomial","6th Degree Polynomial","7th Degree Polynomial","8th Degree Polynomial",
"Gaussian (no offset)", "Exponential Recovery (no offset)",
"Chapman-Richards", "Error Function"
};
public static final String[] fList = {
"y = a+bx","y = a+bx+cx^2", "y = a+bx+cx^2+dx^3","y = a+bx+cx^2+dx^3+ex^4",
"y = a*exp(bx)","y = a*x^b", "y = a*ln(bx)", "y = d+(a-d)/(1+(x/c)^b)", "y = b*(x-a)^c*exp(-(x-a)/d)", "y = a+b*ln(x-c)", "x = d+(a-d)/(1+(y/c)^b) [y = c*((x-a)/(d-x))^(1/b)]", "y = a*exp(-bx) + c", "y = a + (b-a)*exp(-(x-c)*(x-c)/(2*d*d))", "y = a*(1-exp(-b*x)) + c", "y = c*((x-a)/(d-x))^(1/b)", "y = a*exp(bx)", "y = a*x^b", "y = a+bx+cx^2+dx^3+ex^4+fx^5", "y = a+bx+cx^2+dx^3+ex^4+fx^5+gx^6",
"y = a+bx+cx^2+dx^3+ex^4+fx^5+gx^6+hx^7", "y = a+bx+cx^2+dx^3+ex^4+fx^5+gx^6+hx^7+ix^8",
"y = a*exp(-(x-b)*(x-b)/(2*c*c)))", "y = a*(1-exp(-b*x))", "y = a*(1-exp(-b*x))^c", "y = a+b*erf((x-c)/d)" };
public static final String[] fMacro = {
"y = a+x*b","y = a+x*(b+x*c)", "y = a+x*(b+x*(c+x*d))","y = a+x*(b+x*(c+x*(d+x*e)))",
"y = a*Math.exp(b*x)","y = a*Math.pow(x,b)", "y = a*Math.log(b*x)", "y = d+(a-d)/(1+Math.pow(x/c,b))", "y = b*Math.pow(x-a,c)*Math.exp(-(x-a)/d)", "y = a+b*Math.log(x-c)", "y = c*Math.pow((x-a)/(d-x),1/b)", "y = a*Math.exp(-b*x)+c", "y = a+(b-a)*Math.exp(-(x-c)*(x-c)/(2*d*d))", "y = a*(1-Math.exp(-b*x))+c", "y = c*Math.pow((x-a)/(d-x),1/b)", "y = a*Math.exp(b*x)", "y = a*Math.pow(x,b)", "y = a+x*(b+x*(c+x*(d+x*(e+x*f))))", "y = a+x*(b+x*(c+x*(d+x*(e+x*(f+x*g)))))",
"y = a+x*(b+x*(c+x*(d+x*(e+x*(f+x*(g+x*h))))))", "y = a+x*(b+x*(c+x*(d+x*(e+x*(f+x*(g+x*(h+x*i)))))))",
"y = a*Math.exp(-(x-b)*(x-b)/(2*c*c))", "y = a*(1-Math.exp(-b*x))", "y = a*Math.pow(1-Math.exp(-b*x),c)", "y = a+b*Math.erf((x-c)/d)" };
public static final int IterFactor = 500;
private static final int CUSTOM = 100; private static final int GAUSSIAN_INTERNAL = 101; private static final int RODBARD_INTERNAL = 102;
private int fitType = -1; private double[] xData, yData; private double[] weights; private double[] xDataSave, yDataSave; private int numPoints; private double ySign = 0; private double sumY = Double.NaN, sumY2 = Double.NaN; private double sumWeights = Double.NaN; private int numParams; private double[] initialParams; private double[] initialParamVariations; private double[] minimizerInitialParams; private double[] minimizerInitialParamVariations; private double maxRelError = 1e-10; private long time; private int customParamCount; private String customFormula; private UserFunction userFunction; private Interpreter macro; private int macroStartProgramCounter; private int numRegressionParams; private int offsetParam = -1; private int factorParam = -1; private boolean hasSlopeParam; private double[] finalParams; private boolean linearRegressionUsed; private boolean restrictPower; private Minimizer minimizer = new Minimizer();
private int minimizerStatus = Minimizer.INITIALIZATION_FAILURE; private String errorString; private static String[] sortedFitList; private static Hashtable<String, Integer> namesTable;
public CurveFitter (double[] xData, double[] yData) {
int cleanPoints = 0;
for (int jj=0; jj<xData.length; jj++) {
if (!Double.isNaN(xData[jj] + yData[jj]))
cleanPoints++;
}
if (cleanPoints==xData.length) {
this.xData = xData;
this.yData = yData;
} else { double[] cleanX = new double[cleanPoints];
double[] cleanY = new double[cleanPoints];
int ptr = 0;
for (int jj=0; jj<xData.length; jj++) {
if (!Double.isNaN(xData[jj] + yData[jj])) {
cleanX[ptr] = xData[jj];
cleanY[ptr] = yData[jj];
ptr++;
}
}
this.xData = cleanX;
this.yData = cleanY;
}
numPoints = this.xData.length;
}
public void doFit(int fitType) {
doFit(fitType, false);
}
public void doFit(int fitType, boolean showSettings) {
if (!(fitType>=STRAIGHT_LINE && fitType<fitList.length || fitType==CUSTOM))
throw new IllegalArgumentException("Invalid fit type");
if (fitType==CUSTOM && macro==null && userFunction==null)
throw new IllegalArgumentException("No custom formula!");
this.fitType = fitType;
if (isModifiedFitType(fitType)) if (!prepareModifiedFitType(fitType)) return;
numParams = getNumParams();
if (fitType != CUSTOM)
getOffsetAndFactorParams();
calculateSumYandY2(); long startTime = System.currentTimeMillis();
if (this.fitType == STRAIGHT_LINE) { finalParams = new double[] {0, 0, 0}; doRegression(finalParams); linearRegressionUsed = true;
} else { minimizer.setFunction(this, numParams-numRegressionParams);
minimizer.setExtraArrayElements(numRegressionParams); if (macro != null) minimizer.setMaximumThreads(1); if (!makeInitialParamsAndVariations(fitType)) return; if (showSettings) settingsDialog();
if (numRegressionParams >0)
modifyInitialParamsAndVariations();
else {
minimizerInitialParams = initialParams;
minimizerInitialParamVariations = initialParamVariations;
}
startTime = System.currentTimeMillis();
double maxAbsError = Math.min(1e-6,maxRelError)*Math.sqrt(sumY2);
minimizer.setMaxError(maxRelError, maxAbsError);
minimizerStatus = minimizer.minimize(minimizerInitialParams, minimizerInitialParamVariations);
finalParams = minimizer.getParams();
if (numRegressionParams > 0)
minimizerParamsToFullParams(finalParams, false);
}
if (isModifiedFitType(fitType)) postProcessModifiedFitType(fitType);
if (fitType == ERF) if (finalParams[3] < 0) { finalParams[1] = -finalParams[1];
finalParams[3] = -finalParams[3];
}
switch (fitType) { case GAUSSIAN: finalParams[3] = Math.abs(finalParams[3]); break;
case GAUSSIAN_NOOFFSET:
finalParams[2] = Math.abs(finalParams[2]); break;
}
time = System.currentTimeMillis()-startTime;
}
public int doCustomFit(String equation, double[] initialParams, boolean showSettings) {
customFormula = null;
customParamCount = getNumParams(equation);
if (customParamCount==0)
return 0;
customFormula = equation;
String code =
"var x, a, b, c, d, e, f;\n"+
"function dummy() {}\n"+
equation+";\n"; macroStartProgramCounter = 21;
macro = new Interpreter();
try {
macro.run(code, null);
} catch (Exception e) {
if (!Macro.MACRO_CANCELED.equals(e.getMessage()))
IJ.handleException(e);
}
if (macro.wasError())
return 0;
this.initialParams = initialParams;
doFit(CUSTOM, showSettings);
return customParamCount;
}
public void doCustomFit(UserFunction userFunction, int numParams, String formula,
double[] initialParams, double[] initialParamVariations, boolean showSettings) {
this.userFunction = userFunction;
this.customParamCount = numParams;
this.initialParams = initialParams;
this.initialParamVariations = initialParamVariations;
customFormula = formula==null ? "(defined in plugin)" : formula;
doFit(CUSTOM, showSettings);
}
public void setInitialParameters(double[] initialParams) {
this.initialParams = initialParams;
}
public void setWeights(double[] weights) {
this.weights = weights;
}
public Minimizer getMinimizer() {
return minimizer;
}
public void setOffsetMultiplySlopeParams(int offsetParam, int multiplyParam, int slopeParam) {
if (multiplyParam >= 0 && slopeParam>=0)
throw new IllegalArgumentException("CurveFitter: only one of multiplyParam and slopeParam may be given (i.e., >=0)");
this.offsetParam = offsetParam;
hasSlopeParam = slopeParam >= 0;
factorParam = hasSlopeParam ? slopeParam : multiplyParam;
numRegressionParams = 0;
if (factorParam >= 0 && factorParam==offsetParam)
throw new IllegalArgumentException("CurveFitter: offsetParam and slopeParam/factorParam must be different");
if (offsetParam >=0) numRegressionParams++;
if (factorParam >=0) numRegressionParams++;
}
public int getNumParams() {
if (fitType == CUSTOM)
return customParamCount;
else
return getNumParams(fitType);
}
public static int getNumParams(int fitType) {
switch (fitType) {
case STRAIGHT_LINE: return 2;
case POLY2: return 3;
case POLY3: return 4;
case POLY4: return 5;
case POLY5: return 6;
case POLY6: return 7;
case POLY7: return 8;
case POLY8: return 9;
case EXPONENTIAL: case EXP_REGRESSION: return 2;
case POWER: case POWER_REGRESSION: return 2;
case EXP_RECOVERY_NOOFFSET: return 2;
case LOG: return 2;
case LOG2: return 3;
case GAUSSIAN_NOOFFSET: return 3;
case EXP_RECOVERY: return 3;
case CHAPMAN: return 3;
case EXP_WITH_OFFSET: return 3;
case RODBARD: case RODBARD2: case INV_RODBARD: case RODBARD_INTERNAL: return 4;
case GAMMA_VARIATE: return 4;
case GAUSSIAN: case GAUSSIAN_INTERNAL: return 4;
case ERF: return 4;
}
return 0;
}
public static int getNumParams(String customFormula) {
Program pgm = (new Tokenizer()).tokenize(customFormula);
if (!pgm.hasWord("y") || !pgm.hasWord("x"))
return 0;
String[] params = {"a","b","c","d","e","f"};
int customParamCount = 0;
for (int i=0; i<params.length; i++) {
if (pgm.hasWord(params[i])) {
customParamCount++;
}
}
return customParamCount;
}
public final double f(double x) {
if (finalParams==null)
finalParams = minimizer.getParams();
return f(finalParams, x);
}
public final double f(double[] p, double x) {
if (fitType!=CUSTOM)
return f(fitType, p, x);
else {
if (macro==null) { return userFunction.userFunction(p, x);
} else { macro.setVariable("x", x);
macro.setVariable("a", p[0]);
if (customParamCount>1) macro.setVariable("b", p[1]);
if (customParamCount>2) macro.setVariable("c", p[2]);
if (customParamCount>3) macro.setVariable("d", p[3]);
if (customParamCount>4) macro.setVariable("e", p[4]);
if (customParamCount>5) macro.setVariable("f", p[5]);
macro.run(macroStartProgramCounter);
return macro.getVariable("y");
}
}
}
public static double f(int fitType, double[] p, double x) {
switch (fitType) {
case STRAIGHT_LINE:
return p[0] + x*p[1];
case POLY2:
return p[0] + x*(p[1] + x*p[2]);
case POLY3:
return p[0] + x*(p[1] + x*(p[2] + x*p[3]));
case POLY4:
return p[0] + x*(p[1] + x*(p[2] + x*(p[3] + x*p[4])));
case POLY5:
return p[0] + x*(p[1] + x*(p[2] + x*(p[3] + x*(p[4] + x*p[5]))));
case POLY6:
return p[0] + x*(p[1] + x*(p[2] + x*(p[3] + x*(p[4] + x*(p[5] + x*p[6])))));
case POLY7:
return p[0] + x*(p[1] + x*(p[2] + x*(p[3] + x*(p[4] + x*(p[5] + x*(p[6] + x*p[7]))))));
case POLY8:
return p[0] + x*(p[1] + x*(p[2] + x*(p[3] + x*(p[4] + x*(p[5] + x*(p[6] + x*(p[7]+x*p[8])))))));
case EXPONENTIAL:
case EXP_REGRESSION:
return p[0]*Math.exp(p[1]*x);
case EXP_WITH_OFFSET:
return p[0]*Math.exp(-p[1]*x)+p[2];
case EXP_RECOVERY:
return p[0]*(1-Math.exp(-p[1]*x))+p[2];
case EXP_RECOVERY_NOOFFSET:
return p[0]*(1-Math.exp(-p[1]*x));
case CHAPMAN: double value = p[0]*(Math.pow((1-(Math.exp(-p[1]*x))), p[2]));
return value;
case GAUSSIAN:
return p[0]+(p[1]-p[0])*Math.exp(-(x-p[2])*(x-p[2])/(2.0*p[3]*p[3]));
case GAUSSIAN_INTERNAL: return p[0]+p[1]*Math.exp(-(x-p[2])*(x-p[2])/(2.0*p[3]*p[3]));
case GAUSSIAN_NOOFFSET:
return p[0]*Math.exp(-(x-p[1])*(x-p[1])/(2.0*p[2]*p[2]));
case POWER: case POWER_REGRESSION:
return p[0]*Math.pow(x,p[1]);
case LOG:
if (x == 0.0)
return -1000*p[0];
return p[0]*Math.log(p[1]*x);
case RODBARD: { double ex = Math.pow(x/p[2], p[1]); return p[3]+(p[0]-p[3])/(1.0+ex); }
case RODBARD_INTERNAL: { double ex = Math.pow(x/p[2], p[1]); return p[3]+p[0]/(1.0+ex); }
case GAMMA_VARIATE: if (p[0] >= x) return 0.0;
if (p[1] <= 0) return Double.NaN;
if (p[2] <= 0) return Double.NaN;
if (p[3] <= 0) return Double.NaN;
double pw = Math.pow((x - p[0]), p[2]);
double e = Math.exp((-(x - p[0]))/p[3]);
return p[1]*pw*e;
case LOG2:
double tmp = x-p[2];
if (tmp<=0)
return Double.NaN;
return p[0]+p[1]*Math.log(tmp);
case INV_RODBARD: case RODBARD2: double y;
if (p[3]-x < 2*Double.MIN_VALUE || x<p[0]) y = fitType==INV_RODBARD ? Double.NaN : 0.0;
else {
y = (x-p[0])/(p[3]-x); y = Math.pow(y,1.0/p[1]); y = y*p[2];
}
return y;
case ERF:
return p[0] + p[1]*IJMath.erf((x-p[2])/p[3]); default:
return 0.0;
}
}
public double[] getParams() {
return finalParams==null ? minimizer.getParams() : finalParams; }
public double[] getResiduals() {
double[] params = getParams();
double[] residuals = new double[xData.length];
for (int i=0; i<xData.length; i++)
residuals[i] = yData[i] - f(params, xData[i]);
return residuals;
}
public double getSumResidualsSqr() {
return getParams()[numParams]; }
public double getSD() {
double[] residuals = getResiduals();
int n = residuals.length;
double sum=0.0, sum2=0.0;
for (int i=0; i<n; i++) {
sum += residuals[i];
sum2 += residuals[i]*residuals[i];
}
double stdDev = (sum2-sum*sum/n); return Math.sqrt(stdDev/(n-1.0));
}
public double getRSquared() {
if (Double.isNaN(sumY)) calculateSumYandY2();
double sumMeanDiffSqr = sumY2 - sumY*sumY/sumWeights;
double rSquared = 0.0;
if (sumMeanDiffSqr > 0.0)
rSquared = 1.0 - getSumResidualsSqr()/sumMeanDiffSqr;
return rSquared;
}
public double getFitGoodness() {
if (Double.isNaN(sumY)) calculateSumYandY2();
double sumMeanDiffSqr = sumY2 - sumY*sumY/sumWeights;
double fitGoodness = 0.0;
int degreesOfFreedom = numPoints - getNumParams();
if (sumMeanDiffSqr > 0.0 && degreesOfFreedom > 0)
fitGoodness = 1.0 - (getSumResidualsSqr()/ sumMeanDiffSqr) * numPoints / (double)degreesOfFreedom;
return fitGoodness;
}
public int getStatus() {
return linearRegressionUsed ? Minimizer.SUCCESS : minimizerStatus;
}
public String getStatusString() {
return errorString != null ? errorString : minimizer.STATUS_STRING[getStatus()];
}
public String getResultString() {
String resultS = "\nFormula: " + getFormula()
+ "\nMacro code: "+getMacroCode()
+ "\nStatus: "+getStatusString();
if (getStatus()==Minimizer.INITIALIZATION_FAILURE)
return resultS;
if (!linearRegressionUsed) resultS += "\nNumber of completed minimizations: " + minimizer.getCompletedMinimizations();
resultS += "\nNumber of iterations: " + getIterations();
if (!linearRegressionUsed) resultS += " (max: " + minimizer.getMaxIterations() + ")";
resultS += "\nTime: "+time+" ms" +
"\nSum of residuals squared: " + IJ.d2s(getSumResidualsSqr(),5,9) +
"\nStandard deviation: " + IJ.d2s(getSD(),5,9) +
"\nR^2: " + IJ.d2s(getRSquared(),5) +
"\nParameters:";
char pChar = 'a';
double[] pVal = getParams();
for (int i = 0; i < numParams; i++) {
resultS += "\n " + pChar + " = " + IJ.d2s(pVal[i],5,9);
pChar++;
}
return resultS;
}
public void setRestarts(int maxRestarts) {
minimizer.setMaxRestarts(maxRestarts);
}
public void setMaxError(double maxRelError) {
if (Double.isNaN(maxRelError)) return;
if (maxRelError > 0.1) maxRelError = 0.1;
if (maxRelError < 1e-16) maxRelError = 1e-16; this.maxRelError = maxRelError;
}
public void setStatusAndEsc(String ijStatusString, boolean checkEscape) {
minimizer.setStatusAndEsc(ijStatusString, checkEscape);
}
public int getIterations() {
return linearRegressionUsed ? 1 : minimizer.getIterations();
}
public int getMaxIterations() {
return minimizer.getMaxIterations();
}
public void setMaxIterations(int maxIter) {
minimizer.setMaxIterations(maxIter);
}
public int getRestarts() {
return minimizer.getMaxRestarts();
}
public double[] getXPoints() {
return xData;
}
public double[] getYPoints() {
return yData;
}
public int getFit() {
return fitType;
}
public String getName() {
if (fitType==CUSTOM)
return "User-defined";
if (fitType==GAUSSIAN_INTERNAL)
fitType = GAUSSIAN;
else if (fitType==RODBARD_INTERNAL)
fitType = RODBARD;
return fitList[fitType];
}
public String getFormula() {
if (fitType==CUSTOM)
return customFormula;
if (fitType==GAUSSIAN_INTERNAL)
fitType = GAUSSIAN;
else if (fitType==RODBARD_INTERNAL)
fitType = RODBARD;
return fList[fitType];
}
public String getMacroCode() {
if (fitType==CUSTOM)
return customFormula;
if (fitType==GAUSSIAN_INTERNAL)
fitType = GAUSSIAN;
else if (fitType==RODBARD_INTERNAL)
fitType = RODBARD;
return fMacro[fitType];
}
public static String[] getSortedFitList() {
if (sortedFitList == null) {
String[] tmpList = new String[fitList.length];
for (int i=0; i<fitList.length; i++)
tmpList[i] = fitList[sortedTypes[i]];
sortedFitList = tmpList;
}
return sortedFitList;
}
public static int getFitCode(String fitName) {
if (namesTable == null) {
Hashtable<String,Integer> h = new Hashtable<String,Integer>();
for (int i=0; i<fitList.length; i++)
h.put(fitList[i], Integer.valueOf(i));
namesTable = h;
}
Integer i = (Integer)namesTable.get(fitName);
return i!=null? i.intValue() : -1;
}
public final double userFunction(double[] params, double dummy) {
double sumResidualsSqr = 0.0;
if (numRegressionParams == 0) { for (int i=0; i<numPoints; i++) {
double fValue = f(params,xData[i]);
double resSqr = sqr(fValue-yData[i]);
if (weights != null) resSqr *= weights[i];
sumResidualsSqr += resSqr;
}
} else { minimizerParamsToFullParams(params, true);
doRegression(params);
sumResidualsSqr = fullParamsToMinimizerParams(params);
}
return sumResidualsSqr;
}
private void minimizerParamsToFullParams(double[] params, boolean forRegression) {
boolean shouldTransformToSmallerParams = false;
double offset = 0;
double factor = hasSlopeParam ? 0 : 1; double sumResidualsSqr = 0;
if (!forRegression) { int i = params.length - 1;
if (factorParam >= 0)
factor = params[i--];
if (offsetParam >= 0)
offset = params[i];
sumResidualsSqr = params[numParams-numRegressionParams]; params[numParams] = sumResidualsSqr; }
for (int i=numParams-1, iM=numParams-numRegressionParams-1; i>=0; i--) {
if (i == offsetParam)
params[i] = offset;
else if (i == factorParam)
params[i] = factor;
else if (iM>=0)
params[i] = params[iM--];
else
params[i] = Double.NaN;
}
params[numParams] = sumResidualsSqr;
}
private void doRegression(double[] params) {
double sumX=0, sumX2=0, sumXY=0; double sumY=0, sumY2=0; double sumWeights=0;
for (int i=0; i<numPoints; i++) {
double fValue = fitType == STRAIGHT_LINE ? 0 : f(params, xData[i]); if (Double.isNaN(fValue)) { params[numParams] = Double.NaN;
return; }
double w = weights==null ? 1 : weights[i];
sumWeights += w;
if (hasSlopeParam) { double x = xData[i];
double y = yData[i] - fValue;
sumX += x*w;
sumX2 += x*x*w;
sumXY += x*y*w;
sumY2 += y*y*w;
sumY += y*w;
} else { double x = fValue;
double y = yData[i];
sumX += fValue*w;
sumX2 += fValue*fValue*w;
sumXY += fValue*yData[i]*w;
}
}
if (!hasSlopeParam) {
sumY = this.sumY;
sumY2 = this.sumY2;
}
double factor = 0; double sumResidualsSqr = 0;
if (offsetParam<0) { factor = sumXY/sumX2;
if (Double.isNaN(factor) || Double.isInfinite(factor))
factor = 0; sumResidualsSqr = sumY2 + factor*(factor*sumX2 - 2*sumXY);
if (sumResidualsSqr < 2e-15*sumY2)
sumResidualsSqr = 2e-15*sumY2;
} else { if (factorParam >= 0) {
factor = (sumXY-sumX*sumY/sumWeights)/(sumX2-sumX*sumX/sumWeights);
if (restrictPower & factor<=0) factor = 1e-100;
else if (Double.isNaN(factor) || Double.isInfinite(factor))
factor = 0; }
double offset = (sumY-factor*sumX)/sumWeights;
params[offsetParam] = offset;
sumResidualsSqr = sqr(factor)*sumX2 + sumWeights*sqr(offset) + sumY2 +
2*factor*offset*sumX - 2*factor*sumXY - 2*offset*sumY;
if (sumResidualsSqr < 2e-15*(sqr(factor)*sumX2 + sumWeights*sqr(offset) + sumY2))
sumResidualsSqr = 2e-15*(sqr(factor)*sumX2 + sumWeights*sqr(offset) + sumY2);
}
params[numParams] = sumResidualsSqr;
if (factorParam >= 0)
params[factorParam] = factor;
}
private double fullParamsToMinimizerParams(double[] params) {
double offset = offsetParam >=0 ? params[offsetParam] : 0;
double factor = factorParam >=0 ? params[factorParam] : 0;
double sumResidualsSqr = params[numParams];
for (int i=0, iNew=0; i<numParams; i++) if (i != factorParam && i != offsetParam)
params[iNew++] = params[i];
int i = params.length - 1;
if (factorParam >= 0)
params[i--] = factor;
if (offsetParam >= 0)
params[i--] = offset;
params[i--] = sumResidualsSqr;
return sumResidualsSqr;
}
private void modifyInitialParamsAndVariations() {
minimizerInitialParams = initialParams.clone();
minimizerInitialParamVariations = initialParamVariations.clone();
if (numRegressionParams > 0) for (int i=0, iNew=0; i<numParams; i++)
if (i != factorParam && i != offsetParam) {
minimizerInitialParams[iNew] = minimizerInitialParams[i];
minimizerInitialParamVariations[iNew] = minimizerInitialParamVariations[i];
iNew++;
}
}
private boolean makeInitialParamsAndVariations(int fitType) {
if (numPoints == 0) {
errorString = "No data to fit";
return false;
}
boolean hasInitialParams = initialParams != null;
boolean hasInitialParamVariations = initialParamVariations != null;
if (!hasInitialParams) {
initialParams = new double[numParams];
if (fitType==CUSTOM) {
for (int i=0; i<numParams; i++)
initialParams[i] = 1.0;
}
}
if (!hasInitialParamVariations)
initialParamVariations = new double[numParams];
if (fitType==CUSTOM) {
for (int i=0; i<numParams; i++) {
initialParamVariations[i] = 0.1 * initialParams[i];
if (initialParamVariations[i] == 0) initialParamVariations[i] = 0.01; }
return true; }
double firstx = xData[0];
double firsty = yData[0];
double lastx = xData[numPoints-1];
double lasty = yData[numPoints-1];
double xMin=firstx, xMax=firstx; double yMin=firsty, yMax=firsty;
double xMean=firstx, yMean=firsty;
double xOfMax = firstx;
for (int i=1; i<numPoints; i++) {
xMean += xData[i];
yMean += yData[i];
if (xData[i]>xMax) xMax = xData[i];
if (xData[i]<xMin) xMin = xData[i];
if (yData[i]>yMax) { yMax = yData[i]; xOfMax = xData[i]; }
if (yData[i]<yMin) yMin = yData[i];
}
xMean /= numPoints;
yMean /= numPoints;
double slope = (lasty - firsty)/(lastx - firstx);
if (Double.isNaN(slope) || Double.isInfinite(slope)) slope = 0;
double yIntercept = yMean - slope * xMean;
if (xMin < 0 && (fitType==POWER||fitType==CHAPMAN)) {
errorString = "Cannot fit "+fitList[fitType]+" when x<0";
return false;
} else if (xMin < 0 && xMax > 0 && fitType==RODBARD) {
errorString = "Cannot fit "+fitList[fitType]+" to mixture of x<0 and x>0";
return false;
} else if (xMin <= 0 && fitType==LOG) {
errorString = "Cannot fit "+fitList[fitType]+" when x<=0";
return false;
}
if (!hasInitialParams) {
switch (fitType) {
case EXPONENTIAL: initialParams[1] = 1.0/(xMax-xMin+1e-100) * Math.signum(yMean) * Math.signum(slope);
initialParams[0] = yMean/Math.exp(initialParams[1]*xMean); break;
case EXP_WITH_OFFSET: case EXP_RECOVERY: initialParams[1] = 1./(xMax-xMin+1e-100);
initialParams[0] = (yMax-yMin+1e-100)/Math.exp(initialParams[1]*xMean) * Math.signum(slope) *
fitType==EXP_RECOVERY ? 1 : -1; initialParams[2] = 0.5*yMean; break;
case EXP_RECOVERY_NOOFFSET: initialParams[1] = 1.0/(xMax-xMin+1e-100) * Math.signum(yMean) * Math.signum(slope);
initialParams[0] = yMean/Math.exp(initialParams[1]*xMean); break;
case POWER: initialParams[0] = yMean/(Math.abs(xMean+1e-100)); initialParams[1] = 1.0;
break;
case LOG: initialParams[0] = yMean; initialParams[1] = Math.E/(xMax+1e-100);
break;
case LOG2: initialParams[0] = yMean; initialParams[1] = (yMax-yMin+1e-100)/(xMax-xMin+1e-100); initialParams[2] = Math.min(0., -xMin-0.1*(xMax-xMin)-1e-100);
break;
case RODBARD: initialParams[0] = firsty; initialParams[1] = 1.0;
initialParams[2] = xMin < 0 ? xMin : xMax; initialParams[3] = lasty; break;
case INV_RODBARD: case RODBARD2: initialParams[0] = xMin - 0.1 * (xMax-xMin);
initialParams[1] = slope >= 0 ? 1.0 : -1.0;
initialParams[2] = yMax; initialParams[3] = xMax + (xMax - xMin);
break;
case GAMMA_VARIATE: initialParams[0] = xMin;
double cd = xOfMax - xMin;
if (cd < 0.1*(xMax-xMin)) cd = 0.1*(xMax-xMin);
initialParams[2] = Math.sqrt(cd);
initialParams[3] = Math.sqrt(cd);
initialParams[1] = yMax / (Math.pow(cd, initialParams[2]) * Math.exp(-cd/initialParams[3])); break;
case GAUSSIAN: initialParams[0] = yMin; initialParams[1] = yMax; initialParams[2] = xOfMax;
initialParams[3] = 0.39894 * (xMax-xMin) * (yMean-yMin)/(yMax-yMin+1e-100);
break;
case GAUSSIAN_NOOFFSET: initialParams[0] = yMax; initialParams[1] = xOfMax; initialParams[2] = 0.39894 * (xMax-xMin) * yMean/(yMax+1e-100);
break;
case CHAPMAN: initialParams[0] = yMax;
initialParams[2] = 1.5; for (int i=1; i<numPoints; i++) if (yData[i]>0.5*yMax) { initialParams[1] = 1./xData[i];
break;
}
if(Double.isNaN(initialParams[1]) || initialParams[1]>1000./xMax) initialParams[1] = 10./xMax;
break;
case ERF: initialParams[0] = 0.5*(yMax+yMin); initialParams[1] = 0.5*(yMax-yMin+1e-100) * (lasty>firsty ? 1 : -1); initialParams[2] = xMin + (xMax-xMin)*(lasty>firsty ? yMax - yMean : yMean - yMin)/(yMax-yMin+1e-100);
initialParams[3] = 0.1 * (xMax-xMin+1e-100);
break;
}
}
if (!hasInitialParamVariations) { for (int i=0; i<numParams; i++)
initialParamVariations[i] = 0.1 * initialParams[i]; switch (fitType) {
case POLY2: case POLY3: case POLY4: case POLY5: case POLY6: case POLY7: case POLY8:
double xFactor = 0.5*Math.max(Math.abs(xMax+xMin), (xMax-xMin));
initialParamVariations[numParams-1] = (yMax-yMin)/(Math.pow(0.5*(xMax-xMin), numParams-1)+1e-100);
for (int i=numParams-2; i>=0; i--)
initialParamVariations[i] = initialParamVariations[i+1]*xFactor;
break;
case EXPONENTIAL: case EXP_WITH_OFFSET: case EXP_RECOVERY: initialParamVariations[1] = 0.1/(xMax-xMin+1e-100);
break;
case RODBARD: initialParamVariations[2] = 0.5*Math.max((xMax-xMin), Math.abs(xMean));
initialParamVariations[3] = 0.5*Math.max(yMax-yMin, Math.abs(yMax));
break;
case INV_RODBARD: initialParamVariations[0] = 0.01*Math.max(xMax-xMin, Math.abs(xMax));
initialParamVariations[2] = 0.1*Math.max(yMax-yMin, Math.abs(yMax));
initialParamVariations[3] = 0.1*Math.max((xMax-xMin), Math.abs(xMean));
break;
case GAMMA_VARIATE: initialParamVariations[0] = 0.1*Math.max(yMax-yMin, Math.abs(yMax));
double ab = xOfMax - firstx + 0.1*(xMax-xMin+1e-100);
initialParamVariations[2] = 0.1*Math.sqrt(ab);
initialParamVariations[3] = 0.1*Math.sqrt(ab);
break;
case GAUSSIAN: initialParamVariations[2] = 0.2*initialParams[3]; break;
case GAUSSIAN_NOOFFSET: initialParamVariations[1] = 0.2*initialParams[2]; break;
case ERF: initialParamVariations[2] = 0.1 * (xMax-xMin+1e-100);
initialParamVariations[3] = 0.5 * initialParams[3];
break;
}
}
return true;
}
private void getOffsetAndFactorParams() {
offsetParam = -1;
factorParam = -1;
hasSlopeParam = false;
switch (fitType) {
case STRAIGHT_LINE:
case POLY2: case POLY3: case POLY4: case POLY5: case POLY6: case POLY7: case POLY8:
offsetParam = 0;
factorParam = 1;
hasSlopeParam = true;
break;
case EXPONENTIAL: factorParam = 0;
break;
case EXP_WITH_OFFSET: case EXP_RECOVERY: offsetParam = 2;
factorParam = 0;
break;
case EXP_RECOVERY_NOOFFSET: factorParam = 0;
break;
case CHAPMAN: factorParam = 0;
break;
case POWER: factorParam = 0;
break;
case LOG: factorParam = 0;
break;
case LOG2: offsetParam = 0;
factorParam = 1;
break;
case RODBARD_INTERNAL: offsetParam = 3;
factorParam = 0;
break;
case INV_RODBARD: factorParam = 2;
break;
case GAMMA_VARIATE: factorParam = 1;
break;
case GAUSSIAN_INTERNAL: offsetParam = 0;
factorParam = 1;
break;
case GAUSSIAN_NOOFFSET: factorParam = 0;
break;
case ERF: offsetParam = 0;
factorParam = 1;
break;
}
numRegressionParams = 0;
if (offsetParam >= 0) numRegressionParams++;
if (factorParam >= 0) numRegressionParams++;
}
private void calculateSumYandY2() {
sumY = 0.0; sumY2 = 0.0; sumWeights = 0.0;
double w = 1.0;
for (int i=0; i<numPoints; i++) {
double y = yData[i];
if (weights != null) w = weights[i];
sumY += y*w;
sumY2 += y*y*w;
sumWeights += w;
}
}
private boolean isModifiedFitType(int fitType) {
return fitType == POWER_REGRESSION || fitType == EXP_REGRESSION || fitType == RODBARD ||
fitType == RODBARD2 || fitType == GAUSSIAN;
}
private boolean prepareModifiedFitType(int fitType) {
if (fitType == GAUSSIAN) {
this.fitType = GAUSSIAN_INTERNAL; return true;
} else if (fitType == RODBARD) {
this.fitType = RODBARD_INTERNAL; return true;
} else if (fitType == POWER_REGRESSION || fitType == EXP_REGRESSION) {
if (fitType == POWER_REGRESSION) {
xDataSave = xData;
xData = new double[numPoints];
}
yDataSave = yData;
yData = new double[numPoints];
ySign = 0;
numPoints=0; for (int i=0; i<xData.length; i++) {
double y = yDataSave[i];
if (fitType == POWER_REGRESSION) {
double x = xDataSave[i];
if (x==0 && y==0) {
restrictPower = true;
continue; }
if (x<=0) {
errorString = "Cannot fit x<=0";
return false;
}
xData[numPoints] = Math.log(x);
}
if (ySign == 0) ySign = Math.signum(y); if (y*ySign<=0) {
errorString = "Cannot fit y=0 or mixture of y>0, y<0";
return false;
}
yData[numPoints] = Math.log(y*ySign);
numPoints++;
}
this.fitType = STRAIGHT_LINE;
} else if (fitType == RODBARD2) { xDataSave = xData;
yDataSave = yData;
xData = yDataSave; yData = xDataSave;
this.fitType = RODBARD_INTERNAL;
}
return true;
}
private void postProcessModifiedFitType(int fitType) {
if (fitType == POWER_REGRESSION || fitType == EXP_REGRESSION) finalParams[0] = ySign * Math.exp(finalParams[0]); if (fitType == GAUSSIAN) finalParams[1] += finalParams[0];
else if (fitType == RODBARD || fitType == RODBARD2) finalParams[0] += finalParams[3];
if (xDataSave != null) {
xData = xDataSave;
numPoints = xData.length;
}
if (yDataSave != null) yData = yDataSave;
this.fitType = fitType;
}
private final double sqr(double d) { return d * d; }
private void settingsDialog() {
if (initialParamVariations == null)
initialParamVariations = new double[numParams];
GenericDialog gd = new GenericDialog("Simplex Fitting Options");
gd.addMessage("Function name: " + getName() + "\n" +
"Formula: " + getFormula());
char pChar = 'a';
for (int i = 0; i < numParams; i++)
gd.addNumericField("Initial_"+(char)(pChar+i)+":", initialParams[i], 2);
gd.addNumericField("Maximum iterations:", minimizer.getMaxIterations(), 0);
gd.addNumericField("Number of restarts:", minimizer.getMaxRestarts(), 0);
gd.addNumericField("Error tolerance [1*10^(-x)]:", -(Math.log(maxRelError)/Math.log(10)), 0);
gd.showDialog();
if (gd.wasCanceled())
return;
for (int i = 0; i < numParams; i++) {
double p = gd.getNextNumber();
if (!Double.isNaN(p)) {
initialParams[i] = p;
initialParamVariations[i] = Math.max(0.01*p, 0.001*initialParamVariations[i]); }
}
double n = gd.getNextNumber();
if (n>0)
minimizer.setMaxIterations((int)n);
n = gd.getNextNumber();
if (n>=0)
minimizer.setMaxRestarts((int)n);
n = gd.getNextNumber();
setMaxError(Math.pow(10.0, -n));
}
public static int getMax(double[] array) {
double max = array[0];
int index = 0;
for(int i = 1; i < array.length; i++) {
if(max < array[i]) {
max = array[i];
index = i;
}
}
return index;
}
public Plot getPlot() {
return getPlot(100);
}
public Plot getPlot(int points) {
int PLOT_WIDTH=600, PLOT_HEIGHT=350;
double[] x = getXPoints();
double[] y = getYPoints();
if (getStatus()==Minimizer.INITIALIZATION_FAILURE) {
Plot plot = new Plot(getFormula(),"X","Y");
plot.setColor(Color.RED, Color.RED);
plot.addPoints(x, y, PlotWindow.CIRCLE);
plot.setColor(Color.BLUE);
plot.setFrameSize(PLOT_WIDTH, PLOT_HEIGHT);
plot.addLabel(0.02, 0.1, getName());
plot.addLabel(0.02, 0.2, getStatusString());
return plot;
}
int npoints = points;
if (npoints<x.length)
npoints = x.length; if (npoints>1000)
npoints = 1000;
double[] a = Tools.getMinMax(x);
double xmin=a[0], xmax=a[1];
if (points==256) {
npoints = points;
xmin = 0;
xmax = 255;
}
a = Tools.getMinMax(y);
double ymin=a[0], ymax=a[1]; float[] px = new float[npoints];
float[] py = new float[npoints];
double inc = (xmax-xmin)/(npoints-1);
double tmp = xmin;
for (int i=0; i<npoints; i++) {
px[i]=(float)tmp;
tmp += inc;
}
double[] params = getParams();
for (int i=0; i<npoints; i++)
py[i] = (float)f(params, px[i]);
a = Tools.getMinMax(py);
double dataRange = ymax - ymin;
ymin = Math.max(ymin - dataRange, Math.min(ymin, a[0])); ymax = Math.min(ymax + dataRange, Math.max(ymax, a[1]));
Plot plot = new Plot(getFormula(), "X", "Y", px, py);
plot.setLabel(0, "fit");
plot.setLimits(xmin, xmax, ymin, ymax);
plot.setFrameSize(PLOT_WIDTH, PLOT_HEIGHT);
plot.setColor(Color.RED, Color.RED);
plot.addPoints(x, y, PlotWindow.CIRCLE);
plot.setLabel(1, "data");
plot.setColor(Color.BLUE); StringBuilder legend = new StringBuilder(100);
legend.append(getName()); legend.append('\n');
legend.append(getFormula()); legend.append('\n');
double[] p = getParams();
int n = getNumParams();
char pChar = 'a';
for (int i = 0; i < n; i++) {
legend.append(pChar+" = "+IJ.d2s(p[i],5,9)+'\n');
pChar++;
}
legend.append("R^2 = "+IJ.d2s(getRSquared(),4)); legend.append('\n');
plot.addLabel(0.02, 0.1, legend.toString());
plot.setColor(Color.BLUE);
return plot;
}
}