HarmonicCurveFitter.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.  *      https://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. /*
  18.  * This is not the original file distributed by the Apache Software Foundation
  19.  * It has been modified by the Hipparchus project
  20.  */
  21. package org.hipparchus.fitting;

  22. import java.util.ArrayList;
  23. import java.util.Collection;
  24. import java.util.List;

  25. import org.hipparchus.analysis.function.HarmonicOscillator;
  26. import org.hipparchus.exception.LocalizedCoreFormats;
  27. import org.hipparchus.exception.MathIllegalArgumentException;
  28. import org.hipparchus.exception.MathIllegalStateException;
  29. import org.hipparchus.linear.DiagonalMatrix;
  30. import org.hipparchus.optim.nonlinear.vector.leastsquares.LeastSquaresBuilder;
  31. import org.hipparchus.optim.nonlinear.vector.leastsquares.LeastSquaresProblem;
  32. import org.hipparchus.util.FastMath;
  33. import org.hipparchus.util.SinCos;

  34. /**
  35.  * Fits points to a {@link
  36.  * org.hipparchus.analysis.function.HarmonicOscillator.Parametric harmonic oscillator}
  37.  * function.
  38.  * <br>
  39.  * The {@link #withStartPoint(double[]) initial guess values} must be passed
  40.  * in the following order:
  41.  * <ul>
  42.  *  <li>Amplitude</li>
  43.  *  <li>Angular frequency</li>
  44.  *  <li>phase</li>
  45.  * </ul>
  46.  * The optimal values will be returned in the same order.
  47.  *
  48.  */
  49. public class HarmonicCurveFitter extends AbstractCurveFitter {
  50.     /** Parametric function to be fitted. */
  51.     private static final HarmonicOscillator.Parametric FUNCTION = new HarmonicOscillator.Parametric();
  52.     /** Initial guess. */
  53.     private final double[] initialGuess;
  54.     /** Maximum number of iterations of the optimization algorithm. */
  55.     private final int maxIter;

  56.     /**
  57.      * Constructor used by the factory methods.
  58.      *
  59.      * @param initialGuess Initial guess. If set to {@code null}, the initial guess
  60.      * will be estimated using the {@link ParameterGuesser}.
  61.      * @param maxIter Maximum number of iterations of the optimization algorithm.
  62.      */
  63.     private HarmonicCurveFitter(double[] initialGuess, int maxIter) {
  64.         this.initialGuess = initialGuess == null ? null : initialGuess.clone();
  65.         this.maxIter = maxIter;
  66.     }

  67.     /**
  68.      * Creates a default curve fitter.
  69.      * The initial guess for the parameters will be {@link ParameterGuesser}
  70.      * computed automatically, and the maximum number of iterations of the
  71.      * optimization algorithm is set to {@link Integer#MAX_VALUE}.
  72.      *
  73.      * @return a curve fitter.
  74.      *
  75.      * @see #withStartPoint(double[])
  76.      * @see #withMaxIterations(int)
  77.      */
  78.     public static HarmonicCurveFitter create() {
  79.         return new HarmonicCurveFitter(null, Integer.MAX_VALUE);
  80.     }

  81.     /**
  82.      * Configure the start point (initial guess).
  83.      * @param newStart new start point (initial guess)
  84.      * @return a new instance.
  85.      */
  86.     public HarmonicCurveFitter withStartPoint(double[] newStart) {
  87.         return new HarmonicCurveFitter(newStart.clone(),
  88.                                        maxIter);
  89.     }

  90.     /**
  91.      * Configure the maximum number of iterations.
  92.      * @param newMaxIter maximum number of iterations
  93.      * @return a new instance.
  94.      */
  95.     public HarmonicCurveFitter withMaxIterations(int newMaxIter) {
  96.         return new HarmonicCurveFitter(initialGuess,
  97.                                        newMaxIter);
  98.     }

  99.     /** {@inheritDoc} */
  100.     @Override
  101.     protected LeastSquaresProblem getProblem(Collection<WeightedObservedPoint> observations) {
  102.         // Prepare least-squares problem.
  103.         final int len = observations.size();
  104.         final double[] target  = new double[len];
  105.         final double[] weights = new double[len];

  106.         int i = 0;
  107.         for (WeightedObservedPoint obs : observations) {
  108.             target[i]  = obs.getY();
  109.             weights[i] = obs.getWeight();
  110.             ++i;
  111.         }

  112.         final AbstractCurveFitter.TheoreticalValuesFunction model
  113.             = new AbstractCurveFitter.TheoreticalValuesFunction(FUNCTION,
  114.                                                                 observations);

  115.         final double[] startPoint = initialGuess != null ?
  116.             initialGuess :
  117.             // Compute estimation.
  118.             new ParameterGuesser(observations).guess();

  119.         // Return a new optimizer set up to fit a Gaussian curve to the
  120.         // observed points.
  121.         return new LeastSquaresBuilder().
  122.                 maxEvaluations(Integer.MAX_VALUE).
  123.                 maxIterations(maxIter).
  124.                 start(startPoint).
  125.                 target(target).
  126.                 weight(new DiagonalMatrix(weights)).
  127.                 model(model.getModelFunction(), model.getModelFunctionJacobian()).
  128.                 build();

  129.     }

  130.     /**
  131.      * This class guesses harmonic coefficients from a sample.
  132.      * <p>The algorithm used to guess the coefficients is as follows:</p>
  133.      *
  134.      * <p>We know \( f(t) \) at some sampling points \( t_i \) and want
  135.      * to find \( a \), \( \omega \) and \( \phi \) such that
  136.      * \( f(t) = a \cos (\omega t + \phi) \).
  137.      * </p>
  138.      *
  139.      * <p>From the analytical expression, we can compute two primitives :
  140.      * \[
  141.      *     If2(t) = \int f^2 dt  = a^2 (t + S(t)) / 2
  142.      * \]
  143.      * \[
  144.      *     If'2(t) = \int f'^2 dt = a^2 \omega^2 (t - S(t)) / 2
  145.      * \]
  146.      * where \(S(t) = \frac{\sin(2 (\omega t + \phi))}{2\omega}\)
  147.      * </p>
  148.      *
  149.      * <p>We can remove \(S\) between these expressions :
  150.      * \[
  151.      *     If'2(t) = a^2 \omega^2 t - \omega^2 If2(t)
  152.      * \]
  153.      * </p>
  154.      *
  155.      * <p>The preceding expression shows that \(If'2 (t)\) is a linear
  156.      * combination of both \(t\) and \(If2(t)\):
  157.      * \[
  158.      *   If'2(t) = A t + B If2(t)
  159.      * \]
  160.      * </p>
  161.      *
  162.      * <p>From the primitive, we can deduce the same form for definite
  163.      * integrals between \(t_1\) and \(t_i\) for each \(t_i\) :
  164.      * \[
  165.      *   If2(t_i) - If2(t_1) = A (t_i - t_1) + B (If2 (t_i) - If2(t_1))
  166.      * \]
  167.      * </p>
  168.      *
  169.      * <p>We can find the coefficients \(A\) and \(B\) that best fit the sample
  170.      * to this linear expression by computing the definite integrals for
  171.      * each sample points.
  172.      * </p>
  173.      *
  174.      * <p>For a bilinear expression \(z(x_i, y_i) = A x_i + B y_i\), the
  175.      * coefficients \(A\) and \(B\) that minimize a least-squares criterion
  176.      * \(\sum (z_i - z(x_i, y_i))^2\) are given by these expressions:</p>
  177.      * \[
  178.      *   A = \frac{\sum y_i y_i \sum x_i z_i - \sum x_i y_i \sum y_i z_i}
  179.      *            {\sum x_i x_i \sum y_i y_i - \sum x_i y_i \sum x_i y_i}
  180.      * \]
  181.      * \[
  182.      *   B = \frac{\sum x_i x_i \sum y_i z_i - \sum x_i y_i \sum x_i z_i}
  183.      *            {\sum x_i x_i \sum y_i y_i - \sum x_i y_i \sum x_i y_i}
  184.      *
  185.      * \]
  186.      *
  187.      * <p>In fact, we can assume that both \(a\) and \(\omega\) are positive and
  188.      * compute them directly, knowing that \(A = a^2 \omega^2\) and that
  189.      * \(B = -\omega^2\). The complete algorithm is therefore:</p>
  190.      *
  191.      * For each \(t_i\) from \(t_1\) to \(t_{n-1}\), compute:
  192.      * \[ f(t_i) \]
  193.      * \[ f'(t_i) = \frac{f (t_{i+1}) - f(t_{i-1})}{t_{i+1} - t_{i-1}} \]
  194.      * \[ x_i = t_i  - t_1 \]
  195.      * \[ y_i = \int_{t_1}^{t_i} f^2(t) dt \]
  196.      * \[ z_i = \int_{t_1}^{t_i} f'^2(t) dt \]
  197.      * and update the sums:
  198.      * \[ \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 \]
  199.      *
  200.      * Then:
  201.      * \[
  202.      *  a = \sqrt{\frac{\sum y_i y_i  \sum x_i z_i - \sum x_i y_i \sum y_i z_i }
  203.      *                 {\sum x_i y_i  \sum x_i z_i - \sum x_i x_i \sum y_i z_i }}
  204.      * \]
  205.      * \[
  206.      *  \omega = \sqrt{\frac{\sum x_i y_i \sum x_i z_i - \sum x_i x_i \sum y_i z_i}
  207.      *                      {\sum x_i x_i \sum y_i y_i - \sum x_i y_i \sum x_i y_i}}
  208.      * \]
  209.      *
  210.      * <p>Once we know \(\omega\) we can compute:
  211.      * \[
  212.      *    fc = \omega f(t) \cos(\omega t) - f'(t) \sin(\omega t)
  213.      * \]
  214.      * \[
  215.      *    fs = \omega f(t) \sin(\omega t) + f'(t) \cos(\omega t)
  216.      * \]
  217.      * </p>
  218.      *
  219.      * <p>It appears that \(fc = a \omega \cos(\phi)\) and
  220.      * \(fs = -a \omega \sin(\phi)\), so we can use these
  221.      * expressions to compute \(\phi\). The best estimate over the sample is
  222.      * given by averaging these expressions.
  223.      * </p>
  224.      *
  225.      * <p>Since integrals and means are involved in the preceding
  226.      * estimations, these operations run in \(O(n)\) time, where \(n\) is the
  227.      * number of measurements.</p>
  228.      */
  229.     public static class ParameterGuesser {
  230.         /** Amplitude. */
  231.         private final double a;
  232.         /** Angular frequency. */
  233.         private final double omega;
  234.         /** Phase. */
  235.         private final double phi;

  236.         /**
  237.          * Simple constructor.
  238.          *
  239.          * @param observations Sampled observations.
  240.          * @throws MathIllegalArgumentException if the sample is too short.
  241.          * @throws MathIllegalArgumentException if the abscissa range is zero.
  242.          * @throws MathIllegalStateException when the guessing procedure cannot
  243.          * produce sensible results.
  244.          */
  245.         public ParameterGuesser(Collection<WeightedObservedPoint> observations) {
  246.             if (observations.size() < 4) {
  247.                 throw new MathIllegalArgumentException(LocalizedCoreFormats.INSUFFICIENT_OBSERVED_POINTS_IN_SAMPLE,
  248.                                                     observations.size(), 4, true);
  249.             }

  250.             final WeightedObservedPoint[] sorted
  251.                 = sortObservations(observations).toArray(new WeightedObservedPoint[0]);

  252.             final double[] aOmega = guessAOmega(sorted);
  253.             a = aOmega[0];
  254.             omega = aOmega[1];

  255.             phi = guessPhi(sorted);
  256.         }

  257.         /**
  258.          * Gets an estimation of the parameters.
  259.          *
  260.          * @return the guessed parameters, in the following order:
  261.          * <ul>
  262.          *  <li>Amplitude</li>
  263.          *  <li>Angular frequency</li>
  264.          *  <li>Phase</li>
  265.          * </ul>
  266.          */
  267.         public double[] guess() {
  268.             return new double[] { a, omega, phi };
  269.         }

  270.         /**
  271.          * Sort the observations with respect to the abscissa.
  272.          *
  273.          * @param unsorted Input observations.
  274.          * @return the input observations, sorted.
  275.          */
  276.         private List<WeightedObservedPoint> sortObservations(Collection<WeightedObservedPoint> unsorted) {
  277.             final List<WeightedObservedPoint> observations = new ArrayList<>(unsorted);

  278.             // Since the samples are almost always already sorted, this
  279.             // method is implemented as an insertion sort that reorders the
  280.             // elements in place. Insertion sort is very efficient in this case.
  281.             WeightedObservedPoint curr = observations.get(0);
  282.             final int len = observations.size();
  283.             for (int j = 1; j < len; j++) {
  284.                 WeightedObservedPoint prec = curr;
  285.                 curr = observations.get(j);
  286.                 if (curr.getX() < prec.getX()) {
  287.                     // the current element should be inserted closer to the beginning
  288.                     int i = j - 1;
  289.                     WeightedObservedPoint mI = observations.get(i);
  290.                     while ((i >= 0) && (curr.getX() < mI.getX())) {
  291.                         observations.set(i + 1, mI);
  292.                         if (i != 0) {
  293.                             mI = observations.get(i - 1);
  294.                         }
  295.                         --i;
  296.                     }
  297.                     observations.set(i + 1, curr);
  298.                     curr = observations.get(j);
  299.                 }
  300.             }

  301.             return observations;
  302.         }

  303.         /**
  304.          * Estimate a first guess of the amplitude and angular frequency.
  305.          *
  306.          * @param observations Observations, sorted w.r.t. abscissa.
  307.          * @throws MathIllegalArgumentException if the abscissa range is zero.
  308.          * @throws MathIllegalStateException when the guessing procedure cannot
  309.          * produce sensible results.
  310.          * @return the guessed amplitude (at index 0) and circular frequency
  311.          * (at index 1).
  312.          */
  313.         private double[] guessAOmega(WeightedObservedPoint[] observations) {
  314.             final double[] aOmega = new double[2];

  315.             // initialize the sums for the linear model between the two integrals
  316.             double sx2 = 0;
  317.             double sy2 = 0;
  318.             double sxy = 0;
  319.             double sxz = 0;
  320.             double syz = 0;

  321.             double currentX = observations[0].getX();
  322.             double currentY = observations[0].getY();
  323.             double f2Integral = 0;
  324.             double fPrime2Integral = 0;
  325.             final double startX = currentX;
  326.             for (int i = 1; i < observations.length; ++i) {
  327.                 // one step forward
  328.                 final double previousX = currentX;
  329.                 final double previousY = currentY;
  330.                 currentX = observations[i].getX();
  331.                 currentY = observations[i].getY();

  332.                 // update the integrals of f<sup>2</sup> and f'<sup>2</sup>
  333.                 // considering a linear model for f (and therefore constant f')
  334.                 final double dx = currentX - previousX;
  335.                 final double dy = currentY - previousY;
  336.                 final double f2StepIntegral =
  337.                     dx * (previousY * previousY + previousY * currentY + currentY * currentY) / 3;
  338.                 final double fPrime2StepIntegral = dy * dy / dx;

  339.                 final double x = currentX - startX;
  340.                 f2Integral += f2StepIntegral;
  341.                 fPrime2Integral += fPrime2StepIntegral;

  342.                 sx2 += x * x;
  343.                 sy2 += f2Integral * f2Integral;
  344.                 sxy += x * f2Integral;
  345.                 sxz += x * fPrime2Integral;
  346.                 syz += f2Integral * fPrime2Integral;
  347.             }

  348.             // compute the amplitude and pulsation coefficients
  349.             double c1 = sy2 * sxz - sxy * syz;
  350.             double c2 = sxy * sxz - sx2 * syz;
  351.             double c3 = sx2 * sy2 - sxy * sxy;
  352.             if ((c1 / c2 < 0) || (c2 / c3 < 0)) {
  353.                 final int last = observations.length - 1;
  354.                 // Range of the observations, assuming that the
  355.                 // observations are sorted.
  356.                 final double xRange = observations[last].getX() - observations[0].getX();
  357.                 if (xRange == 0) {
  358.                     throw new MathIllegalArgumentException(LocalizedCoreFormats.ZERO_NOT_ALLOWED);
  359.                 }
  360.                 aOmega[1] = 2 * Math.PI / xRange;

  361.                 double yMin = Double.POSITIVE_INFINITY;
  362.                 double yMax = Double.NEGATIVE_INFINITY;
  363.                 for (int i = 1; i < observations.length; ++i) {
  364.                     final double y = observations[i].getY();
  365.                     if (y < yMin) {
  366.                         yMin = y;
  367.                     }
  368.                     if (y > yMax) {
  369.                         yMax = y;
  370.                     }
  371.                 }
  372.                 aOmega[0] = 0.5 * (yMax - yMin);
  373.             } else {
  374.                 if (c2 == 0) {
  375.                     // In some ill-conditioned cases (cf. MATH-844), the guesser
  376.                     // procedure cannot produce sensible results.
  377.                     throw new MathIllegalStateException(LocalizedCoreFormats.ZERO_DENOMINATOR);
  378.                 }

  379.                 aOmega[0] = FastMath.sqrt(c1 / c2);
  380.                 aOmega[1] = FastMath.sqrt(c2 / c3);
  381.             }

  382.             return aOmega;
  383.         }

  384.         /**
  385.          * Estimate a first guess of the phase.
  386.          *
  387.          * @param observations Observations, sorted w.r.t. abscissa.
  388.          * @return the guessed phase.
  389.          */
  390.         private double guessPhi(WeightedObservedPoint[] observations) {
  391.             // initialize the means
  392.             double fcMean = 0;
  393.             double fsMean = 0;

  394.             double currentX = observations[0].getX();
  395.             double currentY = observations[0].getY();
  396.             for (int i = 1; i < observations.length; ++i) {
  397.                 // one step forward
  398.                 final double previousX = currentX;
  399.                 final double previousY = currentY;
  400.                 currentX = observations[i].getX();
  401.                 currentY = observations[i].getY();
  402.                 final double currentYPrime = (currentY - previousY) / (currentX - previousX);

  403.                 double omegaX = omega * currentX;
  404.                 SinCos sc     = FastMath.sinCos(omegaX);
  405.                 fcMean += omega * currentY * sc.cos() - currentYPrime * sc.sin();
  406.                 fsMean += omega * currentY * sc.sin() + currentYPrime * sc.cos();
  407.             }

  408.             return FastMath.atan2(-fsMean, fcMean);
  409.         }
  410.     }
  411. }