HarmonicFitter.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.math3.fitting;

  18. import org.apache.commons.math3.optim.nonlinear.vector.MultivariateVectorOptimizer;
  19. import org.apache.commons.math3.analysis.function.HarmonicOscillator;
  20. import org.apache.commons.math3.exception.ZeroException;
  21. import org.apache.commons.math3.exception.NumberIsTooSmallException;
  22. import org.apache.commons.math3.exception.MathIllegalStateException;
  23. import org.apache.commons.math3.exception.util.LocalizedFormats;
  24. import org.apache.commons.math3.util.FastMath;

  25. /**
  26.  * Class that implements a curve fitting specialized for sinusoids.
  27.  *
  28.  * Harmonic fitting is a very simple case of curve fitting. The
  29.  * estimated coefficients are the amplitude a, the pulsation ω and
  30.  * the phase &phi;: <code>f (t) = a cos (&omega; t + &phi;)</code>. They are
  31.  * searched by a least square estimator initialized with a rough guess
  32.  * based on integrals.
  33.  *
  34.  * @since 2.0
  35.  * @deprecated As of 3.3. Please use {@link HarmonicCurveFitter} and
  36.  * {@link WeightedObservedPoints} instead.
  37.  */
  38. @Deprecated
  39. public class HarmonicFitter extends CurveFitter<HarmonicOscillator.Parametric> {
  40.     /**
  41.      * Simple constructor.
  42.      * @param optimizer Optimizer to use for the fitting.
  43.      */
  44.     public HarmonicFitter(final MultivariateVectorOptimizer optimizer) {
  45.         super(optimizer);
  46.     }

  47.     /**
  48.      * Fit an harmonic function to the observed points.
  49.      *
  50.      * @param initialGuess First guess values in the following order:
  51.      * <ul>
  52.      *  <li>Amplitude</li>
  53.      *  <li>Angular frequency</li>
  54.      *  <li>Phase</li>
  55.      * </ul>
  56.      * @return the parameters of the harmonic function that best fits the
  57.      * observed points (in the same order as above).
  58.      */
  59.     public double[] fit(double[] initialGuess) {
  60.         return fit(new HarmonicOscillator.Parametric(), initialGuess);
  61.     }

  62.     /**
  63.      * Fit an harmonic function to the observed points.
  64.      * An initial guess will be automatically computed.
  65.      *
  66.      * @return the parameters of the harmonic function that best fits the
  67.      * observed points (see the other {@link #fit(double[]) fit} method.
  68.      * @throws NumberIsTooSmallException if the sample is too short for the
  69.      * the first guess to be computed.
  70.      * @throws ZeroException if the first guess cannot be computed because
  71.      * the abscissa range is zero.
  72.      */
  73.     public double[] fit() {
  74.         return fit((new ParameterGuesser(getObservations())).guess());
  75.     }

  76.     /**
  77.      * This class guesses harmonic coefficients from a sample.
  78.      * <p>The algorithm used to guess the coefficients is as follows:</p>
  79.      *
  80.      * <p>We know f (t) at some sampling points t<sub>i</sub> and want to find a,
  81.      * &omega; and &phi; such that f (t) = a cos (&omega; t + &phi;).
  82.      * </p>
  83.      *
  84.      * <p>From the analytical expression, we can compute two primitives :
  85.      * <pre>
  86.      *     If2  (t) = &int; f<sup>2</sup>  = a<sup>2</sup> &times; [t + S (t)] / 2
  87.      *     If'2 (t) = &int; f'<sup>2</sup> = a<sup>2</sup> &omega;<sup>2</sup> &times; [t - S (t)] / 2
  88.      *     where S (t) = sin (2 (&omega; t + &phi;)) / (2 &omega;)
  89.      * </pre>
  90.      * </p>
  91.      *
  92.      * <p>We can remove S between these expressions :
  93.      * <pre>
  94.      *     If'2 (t) = a<sup>2</sup> &omega;<sup>2</sup> t - &omega;<sup>2</sup> If2 (t)
  95.      * </pre>
  96.      * </p>
  97.      *
  98.      * <p>The preceding expression shows that If'2 (t) is a linear
  99.      * combination of both t and If2 (t): If'2 (t) = A &times; t + B &times; If2 (t)
  100.      * </p>
  101.      *
  102.      * <p>From the primitive, we can deduce the same form for definite
  103.      * integrals between t<sub>1</sub> and t<sub>i</sub> for each t<sub>i</sub> :
  104.      * <pre>
  105.      *   If2 (t<sub>i</sub>) - If2 (t<sub>1</sub>) = A &times; (t<sub>i</sub> - t<sub>1</sub>) + B &times; (If2 (t<sub>i</sub>) - If2 (t<sub>1</sub>))
  106.      * </pre>
  107.      * </p>
  108.      *
  109.      * <p>We can find the coefficients A and B that best fit the sample
  110.      * to this linear expression by computing the definite integrals for
  111.      * each sample points.
  112.      * </p>
  113.      *
  114.      * <p>For a bilinear expression z (x<sub>i</sub>, y<sub>i</sub>) = A &times; x<sub>i</sub> + B &times; y<sub>i</sub>, the
  115.      * coefficients A and B that minimize a least square criterion
  116.      * &sum; (z<sub>i</sub> - z (x<sub>i</sub>, y<sub>i</sub>))<sup>2</sup> are given by these expressions:</p>
  117.      * <pre>
  118.      *
  119.      *         &sum;y<sub>i</sub>y<sub>i</sub> &sum;x<sub>i</sub>z<sub>i</sub> - &sum;x<sub>i</sub>y<sub>i</sub> &sum;y<sub>i</sub>z<sub>i</sub>
  120.      *     A = ------------------------
  121.      *         &sum;x<sub>i</sub>x<sub>i</sub> &sum;y<sub>i</sub>y<sub>i</sub> - &sum;x<sub>i</sub>y<sub>i</sub> &sum;x<sub>i</sub>y<sub>i</sub>
  122.      *
  123.      *         &sum;x<sub>i</sub>x<sub>i</sub> &sum;y<sub>i</sub>z<sub>i</sub> - &sum;x<sub>i</sub>y<sub>i</sub> &sum;x<sub>i</sub>z<sub>i</sub>
  124.      *     B = ------------------------
  125.      *         &sum;x<sub>i</sub>x<sub>i</sub> &sum;y<sub>i</sub>y<sub>i</sub> - &sum;x<sub>i</sub>y<sub>i</sub> &sum;x<sub>i</sub>y<sub>i</sub>
  126.      * </pre>
  127.      * </p>
  128.      *
  129.      *
  130.      * <p>In fact, we can assume both a and &omega; are positive and
  131.      * compute them directly, knowing that A = a<sup>2</sup> &omega;<sup>2</sup> and that
  132.      * B = - &omega;<sup>2</sup>. The complete algorithm is therefore:</p>
  133.      * <pre>
  134.      *
  135.      * for each t<sub>i</sub> from t<sub>1</sub> to t<sub>n-1</sub>, compute:
  136.      *   f  (t<sub>i</sub>)
  137.      *   f' (t<sub>i</sub>) = (f (t<sub>i+1</sub>) - f(t<sub>i-1</sub>)) / (t<sub>i+1</sub> - t<sub>i-1</sub>)
  138.      *   x<sub>i</sub> = t<sub>i</sub> - t<sub>1</sub>
  139.      *   y<sub>i</sub> = &int; f<sup>2</sup> from t<sub>1</sub> to t<sub>i</sub>
  140.      *   z<sub>i</sub> = &int; f'<sup>2</sup> from t<sub>1</sub> to t<sub>i</sub>
  141.      *   update the sums &sum;x<sub>i</sub>x<sub>i</sub>, &sum;y<sub>i</sub>y<sub>i</sub>, &sum;x<sub>i</sub>y<sub>i</sub>, &sum;x<sub>i</sub>z<sub>i</sub> and &sum;y<sub>i</sub>z<sub>i</sub>
  142.      * end for
  143.      *
  144.      *            |--------------------------
  145.      *         \  | &sum;y<sub>i</sub>y<sub>i</sub> &sum;x<sub>i</sub>z<sub>i</sub> - &sum;x<sub>i</sub>y<sub>i</sub> &sum;y<sub>i</sub>z<sub>i</sub>
  146.      * a     =  \ | ------------------------
  147.      *           \| &sum;x<sub>i</sub>y<sub>i</sub> &sum;x<sub>i</sub>z<sub>i</sub> - &sum;x<sub>i</sub>x<sub>i</sub> &sum;y<sub>i</sub>z<sub>i</sub>
  148.      *
  149.      *
  150.      *            |--------------------------
  151.      *         \  | &sum;x<sub>i</sub>y<sub>i</sub> &sum;x<sub>i</sub>z<sub>i</sub> - &sum;x<sub>i</sub>x<sub>i</sub> &sum;y<sub>i</sub>z<sub>i</sub>
  152.      * &omega;     =  \ | ------------------------
  153.      *           \| &sum;x<sub>i</sub>x<sub>i</sub> &sum;y<sub>i</sub>y<sub>i</sub> - &sum;x<sub>i</sub>y<sub>i</sub> &sum;x<sub>i</sub>y<sub>i</sub>
  154.      *
  155.      * </pre>
  156.      * </p>
  157.      *
  158.      * <p>Once we know &omega;, we can compute:
  159.      * <pre>
  160.      *    fc = &omega; f (t) cos (&omega; t) - f' (t) sin (&omega; t)
  161.      *    fs = &omega; f (t) sin (&omega; t) + f' (t) cos (&omega; t)
  162.      * </pre>
  163.      * </p>
  164.      *
  165.      * <p>It appears that <code>fc = a &omega; cos (&phi;)</code> and
  166.      * <code>fs = -a &omega; sin (&phi;)</code>, so we can use these
  167.      * expressions to compute &phi;. The best estimate over the sample is
  168.      * given by averaging these expressions.
  169.      * </p>
  170.      *
  171.      * <p>Since integrals and means are involved in the preceding
  172.      * estimations, these operations run in O(n) time, where n is the
  173.      * number of measurements.</p>
  174.      */
  175.     public static class ParameterGuesser {
  176.         /** Amplitude. */
  177.         private final double a;
  178.         /** Angular frequency. */
  179.         private final double omega;
  180.         /** Phase. */
  181.         private final double phi;

  182.         /**
  183.          * Simple constructor.
  184.          *
  185.          * @param observations Sampled observations.
  186.          * @throws NumberIsTooSmallException if the sample is too short.
  187.          * @throws ZeroException if the abscissa range is zero.
  188.          * @throws MathIllegalStateException when the guessing procedure cannot
  189.          * produce sensible results.
  190.          */
  191.         public ParameterGuesser(WeightedObservedPoint[] observations) {
  192.             if (observations.length < 4) {
  193.                 throw new NumberIsTooSmallException(LocalizedFormats.INSUFFICIENT_OBSERVED_POINTS_IN_SAMPLE,
  194.                                                     observations.length, 4, true);
  195.             }

  196.             final WeightedObservedPoint[] sorted = sortObservations(observations);

  197.             final double aOmega[] = guessAOmega(sorted);
  198.             a = aOmega[0];
  199.             omega = aOmega[1];

  200.             phi = guessPhi(sorted);
  201.         }

  202.         /**
  203.          * Gets an estimation of the parameters.
  204.          *
  205.          * @return the guessed parameters, in the following order:
  206.          * <ul>
  207.          *  <li>Amplitude</li>
  208.          *  <li>Angular frequency</li>
  209.          *  <li>Phase</li>
  210.          * </ul>
  211.          */
  212.         public double[] guess() {
  213.             return new double[] { a, omega, phi };
  214.         }

  215.         /**
  216.          * Sort the observations with respect to the abscissa.
  217.          *
  218.          * @param unsorted Input observations.
  219.          * @return the input observations, sorted.
  220.          */
  221.         private WeightedObservedPoint[] sortObservations(WeightedObservedPoint[] unsorted) {
  222.             final WeightedObservedPoint[] observations = unsorted.clone();

  223.             // Since the samples are almost always already sorted, this
  224.             // method is implemented as an insertion sort that reorders the
  225.             // elements in place. Insertion sort is very efficient in this case.
  226.             WeightedObservedPoint curr = observations[0];
  227.             for (int j = 1; j < observations.length; ++j) {
  228.                 WeightedObservedPoint prec = curr;
  229.                 curr = observations[j];
  230.                 if (curr.getX() < prec.getX()) {
  231.                     // the current element should be inserted closer to the beginning
  232.                     int i = j - 1;
  233.                     WeightedObservedPoint mI = observations[i];
  234.                     while ((i >= 0) && (curr.getX() < mI.getX())) {
  235.                         observations[i + 1] = mI;
  236.                         if (i-- != 0) {
  237.                             mI = observations[i];
  238.                         }
  239.                     }
  240.                     observations[i + 1] = curr;
  241.                     curr = observations[j];
  242.                 }
  243.             }

  244.             return observations;
  245.         }

  246.         /**
  247.          * Estimate a first guess of the amplitude and angular frequency.
  248.          * This method assumes that the {@link #sortObservations(WeightedObservedPoint[])} method
  249.          * has been called previously.
  250.          *
  251.          * @param observations Observations, sorted w.r.t. abscissa.
  252.          * @throws ZeroException if the abscissa range is zero.
  253.          * @throws MathIllegalStateException when the guessing procedure cannot
  254.          * produce sensible results.
  255.          * @return the guessed amplitude (at index 0) and circular frequency
  256.          * (at index 1).
  257.          */
  258.         private double[] guessAOmega(WeightedObservedPoint[] observations) {
  259.             final double[] aOmega = new double[2];

  260.             // initialize the sums for the linear model between the two integrals
  261.             double sx2 = 0;
  262.             double sy2 = 0;
  263.             double sxy = 0;
  264.             double sxz = 0;
  265.             double syz = 0;

  266.             double currentX = observations[0].getX();
  267.             double currentY = observations[0].getY();
  268.             double f2Integral = 0;
  269.             double fPrime2Integral = 0;
  270.             final double startX = currentX;
  271.             for (int i = 1; i < observations.length; ++i) {
  272.                 // one step forward
  273.                 final double previousX = currentX;
  274.                 final double previousY = currentY;
  275.                 currentX = observations[i].getX();
  276.                 currentY = observations[i].getY();

  277.                 // update the integrals of f<sup>2</sup> and f'<sup>2</sup>
  278.                 // considering a linear model for f (and therefore constant f')
  279.                 final double dx = currentX - previousX;
  280.                 final double dy = currentY - previousY;
  281.                 final double f2StepIntegral =
  282.                     dx * (previousY * previousY + previousY * currentY + currentY * currentY) / 3;
  283.                 final double fPrime2StepIntegral = dy * dy / dx;

  284.                 final double x = currentX - startX;
  285.                 f2Integral += f2StepIntegral;
  286.                 fPrime2Integral += fPrime2StepIntegral;

  287.                 sx2 += x * x;
  288.                 sy2 += f2Integral * f2Integral;
  289.                 sxy += x * f2Integral;
  290.                 sxz += x * fPrime2Integral;
  291.                 syz += f2Integral * fPrime2Integral;
  292.             }

  293.             // compute the amplitude and pulsation coefficients
  294.             double c1 = sy2 * sxz - sxy * syz;
  295.             double c2 = sxy * sxz - sx2 * syz;
  296.             double c3 = sx2 * sy2 - sxy * sxy;
  297.             if ((c1 / c2 < 0) || (c2 / c3 < 0)) {
  298.                 final int last = observations.length - 1;
  299.                 // Range of the observations, assuming that the
  300.                 // observations are sorted.
  301.                 final double xRange = observations[last].getX() - observations[0].getX();
  302.                 if (xRange == 0) {
  303.                     throw new ZeroException();
  304.                 }
  305.                 aOmega[1] = 2 * Math.PI / xRange;

  306.                 double yMin = Double.POSITIVE_INFINITY;
  307.                 double yMax = Double.NEGATIVE_INFINITY;
  308.                 for (int i = 1; i < observations.length; ++i) {
  309.                     final double y = observations[i].getY();
  310.                     if (y < yMin) {
  311.                         yMin = y;
  312.                     }
  313.                     if (y > yMax) {
  314.                         yMax = y;
  315.                     }
  316.                 }
  317.                 aOmega[0] = 0.5 * (yMax - yMin);
  318.             } else {
  319.                 if (c2 == 0) {
  320.                     // In some ill-conditioned cases (cf. MATH-844), the guesser
  321.                     // procedure cannot produce sensible results.
  322.                     throw new MathIllegalStateException(LocalizedFormats.ZERO_DENOMINATOR);
  323.                 }

  324.                 aOmega[0] = FastMath.sqrt(c1 / c2);
  325.                 aOmega[1] = FastMath.sqrt(c2 / c3);
  326.             }

  327.             return aOmega;
  328.         }

  329.         /**
  330.          * Estimate a first guess of the phase.
  331.          *
  332.          * @param observations Observations, sorted w.r.t. abscissa.
  333.          * @return the guessed phase.
  334.          */
  335.         private double guessPhi(WeightedObservedPoint[] observations) {
  336.             // initialize the means
  337.             double fcMean = 0;
  338.             double fsMean = 0;

  339.             double currentX = observations[0].getX();
  340.             double currentY = observations[0].getY();
  341.             for (int i = 1; i < observations.length; ++i) {
  342.                 // one step forward
  343.                 final double previousX = currentX;
  344.                 final double previousY = currentY;
  345.                 currentX = observations[i].getX();
  346.                 currentY = observations[i].getY();
  347.                 final double currentYPrime = (currentY - previousY) / (currentX - previousX);

  348.                 double omegaX = omega * currentX;
  349.                 double cosine = FastMath.cos(omegaX);
  350.                 double sine = FastMath.sin(omegaX);
  351.                 fcMean += omega * currentY * cosine - currentYPrime * sine;
  352.                 fsMean += omega * currentY * sine + currentYPrime * cosine;
  353.             }

  354.             return FastMath.atan2(-fsMean, fcMean);
  355.         }
  356.     }
  357. }