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.math4.legacy.fitting; 018 019import java.util.Collection; 020import org.apache.commons.math4.legacy.analysis.function.HarmonicOscillator; 021import org.apache.commons.math4.legacy.exception.MathIllegalStateException; 022import org.apache.commons.math4.legacy.exception.NumberIsTooSmallException; 023import org.apache.commons.math4.legacy.exception.ZeroException; 024import org.apache.commons.math4.legacy.exception.util.LocalizedFormats; 025import org.apache.commons.math4.core.jdkmath.JdkMath; 026 027/** 028 * Fits points to a {@link 029 * org.apache.commons.math4.legacy.analysis.function.HarmonicOscillator.Parametric harmonic oscillator} 030 * function. 031 * <br> 032 * The {@link #withStartPoint(double[]) initial guess values} must be passed 033 * in the following order: 034 * <ul> 035 * <li>Amplitude</li> 036 * <li>Angular frequency</li> 037 * <li>phase</li> 038 * </ul> 039 * The optimal values will be returned in the same order. 040 * 041 * @since 3.3 042 */ 043public final class HarmonicCurveFitter extends SimpleCurveFitter { 044 /** Parametric function to be fitted. */ 045 private static final HarmonicOscillator.Parametric FUNCTION = new HarmonicOscillator.Parametric(); 046 047 /** 048 * Constructor used by the factory methods. 049 * 050 * @param initialGuess Initial guess. If set to {@code null}, the initial guess 051 * will be estimated using the {@link ParameterGuesser}. 052 * @param maxIter Maximum number of iterations of the optimization algorithm. 053 */ 054 private HarmonicCurveFitter(double[] initialGuess, 055 int maxIter) { 056 super(FUNCTION, initialGuess, new ParameterGuesser(), maxIter); 057 } 058 059 /** 060 * Creates a default curve fitter. 061 * The initial guess for the parameters will be {@link ParameterGuesser} 062 * computed automatically, and the maximum number of iterations of the 063 * optimization algorithm is set to {@link Integer#MAX_VALUE}. 064 * 065 * @return a curve fitter. 066 * 067 * @see #withStartPoint(double[]) 068 * @see #withMaxIterations(int) 069 */ 070 public static HarmonicCurveFitter create() { 071 return new HarmonicCurveFitter(null, Integer.MAX_VALUE); 072 } 073 074 /** 075 * This class guesses harmonic coefficients from a sample. 076 * <p>The algorithm used to guess the coefficients is as follows:</p> 077 * 078 * <p>We know \( f(t) \) at some sampling points \( t_i \) and want 079 * to find \( a \), \( \omega \) and \( \phi \) such that 080 * \( f(t) = a \cos (\omega t + \phi) \). 081 * </p> 082 * 083 * <p>From the analytical expression, we can compute two primitives : 084 * \[ 085 * If2(t) = \int f^2 dt = a^2 (t + S(t)) / 2 086 * \] 087 * \[ 088 * If'2(t) = \int f'^2 dt = a^2 \omega^2 (t - S(t)) / 2 089 * \] 090 * where \(S(t) = \frac{\sin(2 (\omega t + \phi))}{2\omega}\) 091 * </p> 092 * 093 * <p>We can remove \(S\) between these expressions : 094 * \[ 095 * If'2(t) = a^2 \omega^2 t - \omega^2 If2(t) 096 * \] 097 * </p> 098 * 099 * <p>The preceding expression shows that \(If'2 (t)\) is a linear 100 * combination of both \(t\) and \(If2(t)\): 101 * \[ 102 * If'2(t) = A t + B If2(t) 103 * \] 104 * </p> 105 * 106 * <p>From the primitive, we can deduce the same form for definite 107 * integrals between \(t_1\) and \(t_i\) for each \(t_i\) : 108 * \[ 109 * If2(t_i) - If2(t_1) = A (t_i - t_1) + B (If2 (t_i) - If2(t_1)) 110 * \] 111 * </p> 112 * 113 * <p>We can find the coefficients \(A\) and \(B\) that best fit the sample 114 * to this linear expression by computing the definite integrals for 115 * each sample points. 116 * </p> 117 * 118 * <p>For a bilinear expression \(z(x_i, y_i) = A x_i + B y_i\), the 119 * coefficients \(A\) and \(B\) that minimize a least-squares criterion 120 * \(\sum (z_i - z(x_i, y_i))^2\) are given by these expressions:</p> 121 * \[ 122 * A = \frac{\sum y_i y_i \sum x_i z_i - \sum x_i y_i \sum y_i z_i} 123 * {\sum x_i x_i \sum y_i y_i - \sum x_i y_i \sum x_i y_i} 124 * \] 125 * \[ 126 * B = \frac{\sum x_i x_i \sum y_i z_i - \sum x_i y_i \sum x_i z_i} 127 * {\sum x_i x_i \sum y_i y_i - \sum x_i y_i \sum x_i y_i} 128 * 129 * \] 130 * 131 * <p>In fact, we can assume that both \(a\) and \(\omega\) are positive and 132 * compute them directly, knowing that \(A = a^2 \omega^2\) and that 133 * \(B = -\omega^2\). The complete algorithm is therefore:</p> 134 * 135 * For each \(t_i\) from \(t_1\) to \(t_{n-1}\), compute: 136 * \[ f(t_i) \] 137 * \[ f'(t_i) = \frac{f (t_{i+1}) - f(t_{i-1})}{t_{i+1} - t_{i-1}} \] 138 * \[ x_i = t_i - t_1 \] 139 * \[ y_i = \int_{t_1}^{t_i} f^2(t) dt \] 140 * \[ z_i = \int_{t_1}^{t_i} f'^2(t) dt \] 141 * and update the sums: 142 * \[ \sum x_i x_i, \sum y_i y_i, \sum x_i y_i, \sum x_i z_i, \sum y_i z_i \] 143 * 144 * Then: 145 * \[ 146 * a = \sqrt{\frac{\sum y_i y_i \sum x_i z_i - \sum x_i y_i \sum y_i z_i } 147 * {\sum x_i y_i \sum x_i z_i - \sum x_i x_i \sum y_i z_i }} 148 * \] 149 * \[ 150 * \omega = \sqrt{\frac{\sum x_i y_i \sum x_i z_i - \sum x_i x_i \sum y_i z_i} 151 * {\sum x_i x_i \sum y_i y_i - \sum x_i y_i \sum x_i y_i}} 152 * \] 153 * 154 * <p>Once we know \(\omega\) we can compute: 155 * \[ 156 * fc = \omega f(t) \cos(\omega t) - f'(t) \sin(\omega t) 157 * \] 158 * \[ 159 * fs = \omega f(t) \sin(\omega t) + f'(t) \cos(\omega t) 160 * \] 161 * </p> 162 * 163 * <p>It appears that \(fc = a \omega \cos(\phi)\) and 164 * \(fs = -a \omega \sin(\phi)\), so we can use these 165 * expressions to compute \(\phi\). The best estimate over the sample is 166 * given by averaging these expressions. 167 * </p> 168 * 169 * <p>Since integrals and means are involved in the preceding 170 * estimations, these operations run in \(O(n)\) time, where \(n\) is the 171 * number of measurements.</p> 172 */ 173 public static class ParameterGuesser extends SimpleCurveFitter.ParameterGuesser { 174 /** 175 * {@inheritDoc} 176 * 177 * @return the guessed parameters, in the following order: 178 * <ul> 179 * <li>Amplitude</li> 180 * <li>Angular frequency</li> 181 * <li>Phase</li> 182 * </ul> 183 * @throws NumberIsTooSmallException if the sample is too short. 184 * @throws ZeroException if the abscissa range is zero. 185 * @throws MathIllegalStateException when the guessing procedure cannot 186 * produce sensible results. 187 */ 188 @Override 189 public double[] guess(Collection<WeightedObservedPoint> observations) { 190 if (observations.size() < 4) { 191 throw new NumberIsTooSmallException(LocalizedFormats.INSUFFICIENT_OBSERVED_POINTS_IN_SAMPLE, 192 observations.size(), 4, true); 193 } 194 195 final WeightedObservedPoint[] sorted 196 = sortObservations(observations).toArray(new WeightedObservedPoint[0]); 197 198 final double[] aOmega = guessAOmega(sorted); 199 final double a = aOmega[0]; 200 final double omega = aOmega[1]; 201 202 final double phi = guessPhi(sorted, omega); 203 204 return new double[] { a, omega, phi }; 205 } 206 207 /** 208 * Estimate a first guess of the amplitude and angular frequency. 209 * 210 * @param observations Observations, sorted w.r.t. abscissa. 211 * @throws ZeroException if the abscissa range is zero. 212 * @throws MathIllegalStateException when the guessing procedure cannot 213 * produce sensible results. 214 * @return the guessed amplitude (at index 0) and circular frequency 215 * (at index 1). 216 */ 217 private double[] guessAOmega(WeightedObservedPoint[] observations) { 218 final double[] aOmega = new double[2]; 219 220 // initialize the sums for the linear model between the two integrals 221 double sx2 = 0; 222 double sy2 = 0; 223 double sxy = 0; 224 double sxz = 0; 225 double syz = 0; 226 227 double currentX = observations[0].getX(); 228 double currentY = observations[0].getY(); 229 double f2Integral = 0; 230 double fPrime2Integral = 0; 231 final double startX = currentX; 232 for (int i = 1; i < observations.length; ++i) { 233 // one step forward 234 final double previousX = currentX; 235 final double previousY = currentY; 236 currentX = observations[i].getX(); 237 currentY = observations[i].getY(); 238 239 // update the integrals of f<sup>2</sup> and f'<sup>2</sup> 240 // considering a linear model for f (and therefore constant f') 241 final double dx = currentX - previousX; 242 final double dy = currentY - previousY; 243 final double f2StepIntegral = 244 dx * (previousY * previousY + previousY * currentY + currentY * currentY) / 3; 245 final double fPrime2StepIntegral = dy * dy / dx; 246 247 final double x = currentX - startX; 248 f2Integral += f2StepIntegral; 249 fPrime2Integral += fPrime2StepIntegral; 250 251 sx2 += x * x; 252 sy2 += f2Integral * f2Integral; 253 sxy += x * f2Integral; 254 sxz += x * fPrime2Integral; 255 syz += f2Integral * fPrime2Integral; 256 } 257 258 // compute the amplitude and pulsation coefficients 259 double c1 = sy2 * sxz - sxy * syz; 260 double c2 = sxy * sxz - sx2 * syz; 261 double c3 = sx2 * sy2 - sxy * sxy; 262 if (c1 / c2 < 0 || c2 / c3 < 0) { 263 final int last = observations.length - 1; 264 // Range of the observations, assuming that the 265 // observations are sorted. 266 final double xRange = observations[last].getX() - observations[0].getX(); 267 if (xRange == 0) { 268 throw new ZeroException(); 269 } 270 aOmega[1] = 2 * Math.PI / xRange; 271 272 double yMin = Double.POSITIVE_INFINITY; 273 double yMax = Double.NEGATIVE_INFINITY; 274 for (int i = 1; i < observations.length; ++i) { 275 final double y = observations[i].getY(); 276 if (y < yMin) { 277 yMin = y; 278 } 279 if (y > yMax) { 280 yMax = y; 281 } 282 } 283 aOmega[0] = 0.5 * (yMax - yMin); 284 } else { 285 if (c2 == 0) { 286 // In some ill-conditioned cases (cf. MATH-844), the guesser 287 // procedure cannot produce sensible results. 288 throw new MathIllegalStateException(LocalizedFormats.ZERO_DENOMINATOR); 289 } 290 291 aOmega[0] = JdkMath.sqrt(c1 / c2); 292 aOmega[1] = JdkMath.sqrt(c2 / c3); 293 } 294 295 return aOmega; 296 } 297 298 /** 299 * Estimate a first guess of the phase. 300 * 301 * @param observations Observations, sorted w.r.t. abscissa. 302 * @param omega Angular frequency. 303 * @return the guessed phase. 304 */ 305 private double guessPhi(WeightedObservedPoint[] observations, 306 double omega) { 307 // initialize the means 308 double fcMean = 0; 309 double fsMean = 0; 310 311 double currentX = observations[0].getX(); 312 double currentY = observations[0].getY(); 313 for (int i = 1; i < observations.length; ++i) { 314 // one step forward 315 final double previousX = currentX; 316 final double previousY = currentY; 317 currentX = observations[i].getX(); 318 currentY = observations[i].getY(); 319 final double currentYPrime = (currentY - previousY) / (currentX - previousX); 320 321 double omegaX = omega * currentX; 322 double cosine = JdkMath.cos(omegaX); 323 double sine = JdkMath.sin(omegaX); 324 fcMean += omega * currentY * cosine - currentYPrime * sine; 325 fsMean += omega * currentY * sine + currentYPrime * cosine; 326 } 327 328 return JdkMath.atan2(-fsMean, fcMean); 329 } 330 } 331}