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.optimization.fitting;

  18. import org.apache.commons.math3.optimization.DifferentiableMultivariateVectorOptimizer;
  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.  * @deprecated As of 3.1 (to be removed in 4.0).
  35.  * @since 2.0
  36.  */
  37. @Deprecated
  38. public class HarmonicFitter extends CurveFitter<HarmonicOscillator.Parametric> {
  39.     /**
  40.      * Simple constructor.
  41.      * @param optimizer Optimizer to use for the fitting.
  42.      */
  43.     public HarmonicFitter(final DifferentiableMultivariateVectorOptimizer optimizer) {
  44.         super(optimizer);
  45.     }

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

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

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

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

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

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

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

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

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

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

  243.             return observations;
  244.         }

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

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

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

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

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

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

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

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

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

  326.             return aOmega;
  327.         }

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

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

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

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