001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.math3.fitting; 018 019import java.util.Arrays; 020import java.util.Comparator; 021import org.apache.commons.math3.analysis.function.Gaussian; 022import org.apache.commons.math3.exception.NullArgumentException; 023import org.apache.commons.math3.exception.NumberIsTooSmallException; 024import org.apache.commons.math3.exception.OutOfRangeException; 025import org.apache.commons.math3.exception.ZeroException; 026import org.apache.commons.math3.exception.NotStrictlyPositiveException; 027import org.apache.commons.math3.exception.util.LocalizedFormats; 028import org.apache.commons.math3.optim.nonlinear.vector.MultivariateVectorOptimizer; 029import org.apache.commons.math3.util.FastMath; 030 031/** 032 * Fits points to a {@link 033 * org.apache.commons.math3.analysis.function.Gaussian.Parametric Gaussian} function. 034 * <p> 035 * Usage example: 036 * <pre> 037 * GaussianFitter fitter = new GaussianFitter( 038 * new LevenbergMarquardtOptimizer()); 039 * fitter.addObservedPoint(4.0254623, 531026.0); 040 * fitter.addObservedPoint(4.03128248, 984167.0); 041 * fitter.addObservedPoint(4.03839603, 1887233.0); 042 * fitter.addObservedPoint(4.04421621, 2687152.0); 043 * fitter.addObservedPoint(4.05132976, 3461228.0); 044 * fitter.addObservedPoint(4.05326982, 3580526.0); 045 * fitter.addObservedPoint(4.05779662, 3439750.0); 046 * fitter.addObservedPoint(4.0636168, 2877648.0); 047 * fitter.addObservedPoint(4.06943698, 2175960.0); 048 * fitter.addObservedPoint(4.07525716, 1447024.0); 049 * fitter.addObservedPoint(4.08237071, 717104.0); 050 * fitter.addObservedPoint(4.08366408, 620014.0); 051 * double[] parameters = fitter.fit(); 052 * </pre> 053 * 054 * @since 2.2 055 * @deprecated As of 3.3. Please use {@link GaussianCurveFitter} and 056 * {@link WeightedObservedPoints} instead. 057 */ 058@Deprecated 059public class GaussianFitter extends CurveFitter<Gaussian.Parametric> { 060 /** 061 * Constructs an instance using the specified optimizer. 062 * 063 * @param optimizer Optimizer to use for the fitting. 064 */ 065 public GaussianFitter(MultivariateVectorOptimizer optimizer) { 066 super(optimizer); 067 } 068 069 /** 070 * Fits a Gaussian function to the observed points. 071 * 072 * @param initialGuess First guess values in the following order: 073 * <ul> 074 * <li>Norm</li> 075 * <li>Mean</li> 076 * <li>Sigma</li> 077 * </ul> 078 * @return the parameters of the Gaussian function that best fits the 079 * observed points (in the same order as above). 080 * @since 3.0 081 */ 082 public double[] fit(double[] initialGuess) { 083 final Gaussian.Parametric f = new Gaussian.Parametric() { 084 /** {@inheritDoc} */ 085 @Override 086 public double value(double x, double ... p) { 087 double v = Double.POSITIVE_INFINITY; 088 try { 089 v = super.value(x, p); 090 } catch (NotStrictlyPositiveException e) { // NOPMD 091 // Do nothing. 092 } 093 return v; 094 } 095 096 /** {@inheritDoc} */ 097 @Override 098 public double[] gradient(double x, double ... p) { 099 double[] v = { Double.POSITIVE_INFINITY, 100 Double.POSITIVE_INFINITY, 101 Double.POSITIVE_INFINITY }; 102 try { 103 v = super.gradient(x, p); 104 } catch (NotStrictlyPositiveException e) { // NOPMD 105 // Do nothing. 106 } 107 return v; 108 } 109 }; 110 111 return fit(f, initialGuess); 112 } 113 114 /** 115 * Fits a Gaussian function to the observed points. 116 * 117 * @return the parameters of the Gaussian function that best fits the 118 * observed points (in the same order as above). 119 */ 120 public double[] fit() { 121 final double[] guess = (new ParameterGuesser(getObservations())).guess(); 122 return fit(guess); 123 } 124 125 /** 126 * Guesses the parameters {@code norm}, {@code mean}, and {@code sigma} 127 * of a {@link org.apache.commons.math3.analysis.function.Gaussian.Parametric} 128 * based on the specified observed points. 129 */ 130 public static class ParameterGuesser { 131 /** Normalization factor. */ 132 private final double norm; 133 /** Mean. */ 134 private final double mean; 135 /** Standard deviation. */ 136 private final double sigma; 137 138 /** 139 * Constructs instance with the specified observed points. 140 * 141 * @param observations Observed points from which to guess the 142 * parameters of the Gaussian. 143 * @throws NullArgumentException if {@code observations} is 144 * {@code null}. 145 * @throws NumberIsTooSmallException if there are less than 3 146 * observations. 147 */ 148 public ParameterGuesser(WeightedObservedPoint[] observations) { 149 if (observations == null) { 150 throw new NullArgumentException(LocalizedFormats.INPUT_ARRAY); 151 } 152 if (observations.length < 3) { 153 throw new NumberIsTooSmallException(observations.length, 3, true); 154 } 155 156 final WeightedObservedPoint[] sorted = sortObservations(observations); 157 final double[] params = basicGuess(sorted); 158 159 norm = params[0]; 160 mean = params[1]; 161 sigma = params[2]; 162 } 163 164 /** 165 * Gets an estimation of the parameters. 166 * 167 * @return the guessed parameters, in the following order: 168 * <ul> 169 * <li>Normalization factor</li> 170 * <li>Mean</li> 171 * <li>Standard deviation</li> 172 * </ul> 173 */ 174 public double[] guess() { 175 return new double[] { norm, mean, sigma }; 176 } 177 178 /** 179 * Sort the observations. 180 * 181 * @param unsorted Input observations. 182 * @return the input observations, sorted. 183 */ 184 private WeightedObservedPoint[] sortObservations(WeightedObservedPoint[] unsorted) { 185 final WeightedObservedPoint[] observations = unsorted.clone(); 186 final Comparator<WeightedObservedPoint> cmp 187 = new Comparator<WeightedObservedPoint>() { 188 /** {@inheritDoc} */ 189 public int compare(WeightedObservedPoint p1, 190 WeightedObservedPoint p2) { 191 if (p1 == null && p2 == null) { 192 return 0; 193 } 194 if (p1 == null) { 195 return -1; 196 } 197 if (p2 == null) { 198 return 1; 199 } 200 final int cmpX = Double.compare(p1.getX(), p2.getX()); 201 if (cmpX < 0) { 202 return -1; 203 } 204 if (cmpX > 0) { 205 return 1; 206 } 207 final int cmpY = Double.compare(p1.getY(), p2.getY()); 208 if (cmpY < 0) { 209 return -1; 210 } 211 if (cmpY > 0) { 212 return 1; 213 } 214 final int cmpW = Double.compare(p1.getWeight(), p2.getWeight()); 215 if (cmpW < 0) { 216 return -1; 217 } 218 if (cmpW > 0) { 219 return 1; 220 } 221 return 0; 222 } 223 }; 224 225 Arrays.sort(observations, cmp); 226 return observations; 227 } 228 229 /** 230 * Guesses the parameters based on the specified observed points. 231 * 232 * @param points Observed points, sorted. 233 * @return the guessed parameters (normalization factor, mean and 234 * sigma). 235 */ 236 private double[] basicGuess(WeightedObservedPoint[] points) { 237 final int maxYIdx = findMaxY(points); 238 final double n = points[maxYIdx].getY(); 239 final double m = points[maxYIdx].getX(); 240 241 double fwhmApprox; 242 try { 243 final double halfY = n + ((m - n) / 2); 244 final double fwhmX1 = interpolateXAtY(points, maxYIdx, -1, halfY); 245 final double fwhmX2 = interpolateXAtY(points, maxYIdx, 1, halfY); 246 fwhmApprox = fwhmX2 - fwhmX1; 247 } catch (OutOfRangeException e) { 248 // TODO: Exceptions should not be used for flow control. 249 fwhmApprox = points[points.length - 1].getX() - points[0].getX(); 250 } 251 final double s = fwhmApprox / (2 * FastMath.sqrt(2 * FastMath.log(2))); 252 253 return new double[] { n, m, s }; 254 } 255 256 /** 257 * Finds index of point in specified points with the largest Y. 258 * 259 * @param points Points to search. 260 * @return the index in specified points array. 261 */ 262 private int findMaxY(WeightedObservedPoint[] points) { 263 int maxYIdx = 0; 264 for (int i = 1; i < points.length; i++) { 265 if (points[i].getY() > points[maxYIdx].getY()) { 266 maxYIdx = i; 267 } 268 } 269 return maxYIdx; 270 } 271 272 /** 273 * Interpolates using the specified points to determine X at the 274 * specified Y. 275 * 276 * @param points Points to use for interpolation. 277 * @param startIdx Index within points from which to start the search for 278 * interpolation bounds points. 279 * @param idxStep Index step for searching interpolation bounds points. 280 * @param y Y value for which X should be determined. 281 * @return the value of X for the specified Y. 282 * @throws ZeroException if {@code idxStep} is 0. 283 * @throws OutOfRangeException if specified {@code y} is not within the 284 * range of the specified {@code points}. 285 */ 286 private double interpolateXAtY(WeightedObservedPoint[] points, 287 int startIdx, 288 int idxStep, 289 double y) 290 throws OutOfRangeException { 291 if (idxStep == 0) { 292 throw new ZeroException(); 293 } 294 final WeightedObservedPoint[] twoPoints 295 = getInterpolationPointsForY(points, startIdx, idxStep, y); 296 final WeightedObservedPoint p1 = twoPoints[0]; 297 final WeightedObservedPoint p2 = twoPoints[1]; 298 if (p1.getY() == y) { 299 return p1.getX(); 300 } 301 if (p2.getY() == y) { 302 return p2.getX(); 303 } 304 return p1.getX() + (((y - p1.getY()) * (p2.getX() - p1.getX())) / 305 (p2.getY() - p1.getY())); 306 } 307 308 /** 309 * Gets the two bounding interpolation points from the specified points 310 * suitable for determining X at the specified Y. 311 * 312 * @param points Points to use for interpolation. 313 * @param startIdx Index within points from which to start search for 314 * interpolation bounds points. 315 * @param idxStep Index step for search for interpolation bounds points. 316 * @param y Y value for which X should be determined. 317 * @return the array containing two points suitable for determining X at 318 * the specified Y. 319 * @throws ZeroException if {@code idxStep} is 0. 320 * @throws OutOfRangeException if specified {@code y} is not within the 321 * range of the specified {@code points}. 322 */ 323 private WeightedObservedPoint[] getInterpolationPointsForY(WeightedObservedPoint[] points, 324 int startIdx, 325 int idxStep, 326 double y) 327 throws OutOfRangeException { 328 if (idxStep == 0) { 329 throw new ZeroException(); 330 } 331 for (int i = startIdx; 332 idxStep < 0 ? i + idxStep >= 0 : i + idxStep < points.length; 333 i += idxStep) { 334 final WeightedObservedPoint p1 = points[i]; 335 final WeightedObservedPoint p2 = points[i + idxStep]; 336 if (isBetween(y, p1.getY(), p2.getY())) { 337 if (idxStep < 0) { 338 return new WeightedObservedPoint[] { p2, p1 }; 339 } else { 340 return new WeightedObservedPoint[] { p1, p2 }; 341 } 342 } 343 } 344 345 // Boundaries are replaced by dummy values because the raised 346 // exception is caught and the message never displayed. 347 // TODO: Exceptions should not be used for flow control. 348 throw new OutOfRangeException(y, 349 Double.NEGATIVE_INFINITY, 350 Double.POSITIVE_INFINITY); 351 } 352 353 /** 354 * Determines whether a value is between two other values. 355 * 356 * @param value Value to test whether it is between {@code boundary1} 357 * and {@code boundary2}. 358 * @param boundary1 One end of the range. 359 * @param boundary2 Other end of the range. 360 * @return {@code true} if {@code value} is between {@code boundary1} and 361 * {@code boundary2} (inclusive), {@code false} otherwise. 362 */ 363 private boolean isBetween(double value, 364 double boundary1, 365 double boundary2) { 366 return (value >= boundary1 && value <= boundary2) || 367 (value >= boundary2 && value <= boundary1); 368 } 369 } 370}