Warning, file /education/kstars/kstars/ekos/focus/curvefit.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 #include "curvefit.h"
0002 #include "ekos/ekos.h"
0003 #include <ekos_focus_debug.h>
0004 
0005 // Constants used to identify the number of parameters used for different curve types
0006 constexpr int NUM_HYPERBOLA_PARAMS = 4;
0007 constexpr int NUM_PARABOLA_PARAMS = 3;
0008 constexpr int NUM_GAUSSIAN_PARAMS = 7;
0009 constexpr int NUM_PLANE_PARAMS = 3;
0010 // Parameters for the solver
0011 // MAX_ITERATIONS is used to limit the number of iterations for the solver.
0012 // A value needs to be entered that allows a solution to be found without iterating unnecessarily
0013 // There is a relationship with the tolerance parameters that follow.
0014 constexpr int MAX_ITERATIONS_CURVE = 5000;
0015 constexpr int MAX_ITERATIONS_STARS = 1000;
0016 constexpr int MAX_ITERATIONS_PLANE = 5000;
0017 // The next 3 parameters are used as tolerance for convergence
0018 // convergence is achieved if for each datapoint i
0019 //     dx_i < INEPSABS + (INEPSREL * x_i)
0020 constexpr double INEPSXTOL = 1e-5;
0021 const double INEPSGTOL = pow(GSL_DBL_EPSILON, 1.0 / 3.0);
0022 constexpr double INEPSFTOL = 1e-5;
0023 
0024 // The functions here fit a number of different curves to the incoming data points using the Lehvensberg-Marquart
0025 // solver with geodesic acceleration as provided the Gnu Science Library (GSL). The following sources of information are useful:
0026 // www.wikipedia.org/wiki/Levenberg–Marquardt_algorithm - overview of the mathematics behind the algo
0027 // www.gnu.org/software/gsl/doc/html/nls.html - GSL manual describing Nonlinear Least-Squares fitting.
0028 //
0029 // Levensberg-Marquart (LM)
0030 // ------------------------
0031 // The Levensberg-Marquart algorithm is a non-linear least-squares solver and thus suitable for mnay
0032 // different equations. The basic is idea is to adjust the equation y = f(x,P) so that the computed
0033 // y values are as close as possible to the y values of the datapoints provided, so that the resultant
0034 // curve fits the data as best as it can. P is a set of parameters that are varied by the solver in order
0035 // to find the best fit. The solver measures how far away the curve is at each data point, squares the
0036 // result and adds them all up. This is the number to be minimised, lets call is S.
0037 //
0038 // The solver is supplied with an initial guess for the parameters, P. It calculates S, makes an adjustment
0039 // P and calculates a new S1. Provided S1 < S the solver has moved in the right direction and reduced S.
0040 // It iterates through this procedure until S1 - S is:
0041 // 1. less than a supplied limit (convergence has been reached), or
0042 // 2. The maximum number of iterations has been reached, or
0043 // 3. The solver encountered an error.
0044 //
0045 // The solver is capable of solving either an unweighted or weighted set of datapoints. In essence, an
0046 // unweighted set of data gives equal weight to each datapoint when trying to fit a curve. An alternative is to
0047 // weight each datapoint with a measure that corresponds to how accurate the measurement of the datapoint
0048 // actually is. Given we are calculating HFRs of stars we can calculate the variance (standard deviation
0049 // squared) in the measurement of HFR and use 1 / SD^2 as weighting. What this means is that if we have a
0050 // datapoint where the SD in HFR measurements is small we would give this datapoint a relatively high
0051 // weighting when fitting the curve, and if there was a datapoint with a higher SD in HFR measurements
0052 // it would receive a lower weighting when trying to fit curve to that point.
0053 //
0054 // There are several optimisations to LM that speed up convergence for certain types of equations. This
0055 // code uses the LM solver with geodesic acceleration; a technique to accelerate convergence by using the
0056 // second derivative of the fitted function. An optimisation that has been implemented is that since, in normal
0057 // operation, the solver is run multiple times with the same or similar parameters, the initial guess for
0058 // a solver run is set the solution from the previous run.
0059 //
0060 // The documents referenced provide the maths of the LM algorithm. What is required is the function to be
0061 // used f(x) and the derivative of f(x) with respect to the parameters of f(x). This partial derivative forms
0062 // a matrix called the Jacobian, J(x). LM then finds a minimum. Mathematically it will find a minimum local
0063 // to the initial guess, but not necessarily the global minimum but for the equations used this is not a
0064 // problem under normal operation as there will only be 1 minimum. If the data is poor, however, the
0065 // solver may find an "n" solution rather than a "u" especially at the start of the process where there
0066 // are a small number of points and the error in measurement is high. We'll ignore and continue and this
0067 // should correct itself.
0068 //
0069 // The original algorithm used in Ekos is a quadratic equation, which represents a parabolic curve. In order
0070 // not to disturb historic code the original solution will be called Quadratic. It uses a linear least-squares
0071 // model, not LM. The LM solver has been applied to a hyperbolic and a parabolic curve.
0072 //
0073 // In addition, the solver has been applied to:
0074 // 1. 3D Gaussian in order to fit star profiles and calculate FWHM.
0075 // 2. 3D Plane surface used by Aberration Inspector.
0076 //
0077 // Hyperbola
0078 // ---------
0079 // Equation y = f(x) = b * sqrt(1 + ((x - c) / a) ^2) + d
0080 // This can be re-written as:
0081 //
0082 // f(x) = b * phi + d
0083 // where phi = sqrt(1 + ((x - c) / a) ^2)
0084 //
0085 // Jacobian J = {df/da, df/db, df/dc, df/db}
0086 // df/da      = b * (1 / 2) * (1 / phi) * -2 * ((x - c) ^2) / (a ^3)
0087 //            = -b * ((x - c) ^2) / ((a ^ 3) * phi)
0088 // df/db      = phi
0089 // df/dc      = b * (1 / 2) * (1 / phi) * 2 * (x - c) / (a ^2) * (-1)
0090 //            = -b * (x - c) / ((a ^2) * phi)
0091 // df/dd      = 1
0092 //
0093 // Second directional derivative:
0094 // {
0095 //   {ddf/dada,ddf/dadb,ddf/dadc,ddf/dadd},
0096 //   {ddf/dbda,ddf/dbdb,ddf/dbdc,ddf/dbdd},
0097 //   {ddf/dcda,ddf/dcdb,ddf/dcdc,ddf/dcdd},
0098 //   {ddf/ddda,ddf/dddb,ddf/dddc,ddf/dddd}
0099 // }
0100 // Note the matrix is symmetric, as ddf/dxdy = ddf/dydx.
0101 //
0102 // ddf/dada = (b*(c-x)^2*(3*a^2+a*(c-x)^2))/(a^4*(a^2 + (c-x)^2)^3/2))
0103 // ddf/dadb = -((x - c) ^2) / ((a ^ 3) * phi)
0104 // ddf/dadc = -(b*(c-x)*(2a^2+(c-x)^2))/(a^3*(a^2 + (c-x)^2)^3/2))
0105 // ddf/dadd = 0
0106 // ddf/dbdb = 0
0107 // ddf/dbdc = -(x-c)/(a^2*phi)
0108 // ddf/dbdd = 0
0109 // ddf/dcdc = b/((a^2+(c-x)^2) * phi)
0110 // ddf/dcdd = 0
0111 // ddf/dddd = 0
0112 //
0113 // For a valid solution:
0114 // 1. c must be > 0 and within the range of travel of the focuser.
0115 // 2. b must be > 0 for a V shaped curve (b < 0 for an inverted V and b = 0 for a horizontal line).
0116 // 3. b + d > 0. The minimum solution when x = c gives f(x) = b + d which must be > 0.
0117 //
0118 // Parabola
0119 // --------
0120 // Equation y = f(x) = a + b((x - c) ^2)
0121 // Jacobian J = {df/da, df/db, df/dc}
0122 // df/da      = 1
0123 // df/db      = (x - c) ^2
0124 // df/dc      = -2 * b * (x - c)
0125 //
0126 // For a valid solution:
0127 // 1. c must be > 0 and within the range of travel of the focuser.
0128 // 2. b must be > 0 for a V shaped curve (b < 0 for an inverted V and b = 0 for a horizontal line).
0129 // 3. a > 0. The minimum solution when x = c gives f(x) = a which must be > 0.
0130 //
0131 // Gaussian
0132 // --------
0133 // Generalised equation for a 2D gaussian.
0134 // Equation z = f(x,y) = b + a.exp-(A((x-x0)^2) + 2B(x-x0)(y-y0) + 2C((y-y0)^2))
0135 //
0136 // with parameters
0137 // b          = background
0138 // a          = Peak Intensity
0139 // x0         = Star centre in x dimension
0140 // y0         = Star centre in y dimension
0141 // A, B, C    = relate to the shape of the elliptical gaussian and the orientation of the major and
0142 //              minor ellipse axes wrt x and y
0143 //
0144 // Jacobian J = {df/db, df/da, df/dx0, df/dy0, df/dA, df/dB, df/dC}
0145 // Let phi    = exp-(A((x-x0)^2) + 2B(x-x0)(y-y0) + C((y-y0)^2))
0146 // df/db      = 1
0147 // df/da      = phi
0148 // df/dx0     = [2A(x-x0)+2B(y-y0)].a.phi
0149 // df/dy0     = [2B(x-x0)+2C(y-y0)].a.phi
0150 // df/dA      = -(x-x0)^2.a.phi
0151 // df/DB      = -2(x-x0)(y-y0).a.phi
0152 // df/dC      = -(y-y0)^2.a.phi
0153 //
0154 // 2nd derivatives
0155 // {
0156 //   {ddf/dbdb,ddf/dbda,ddf/dbdx0,ddf/dbdy0,ddf/dbdA,ddf/dbdB,ddf/dbdC},
0157 //   {ddf/dadb,ddf/dada,ddf/dadx0,ddf/dady0,ddf/dadA,ddf/dadB,ddf/dadC},
0158 //   {ddf/dx0db,ddf/dx0da,ddf/dx0dx0,ddf/dx0dy0,ddf/dx0dA,ddf/dx0dB,ddf/dx0dC},
0159 //   {ddf/dy0db,ddf/dy0da,ddf/dy0dx0,ddf/dy0dy0,ddf/dy0dA,ddf/dy0dB,ddf/dy0dC},
0160 //   {ddf/dAdb,ddf/dAda,ddf/dAdx0,ddf/dAdy0,ddf/dAdA,ddf/dAdB,ddf/dAdC},
0161 //   {ddf/dBdb,ddf/dBda,ddf/dBdx0,ddf/dBdy0,ddf/dBdA,ddf/dBdB,ddf/dBdC},
0162 //   {ddf/dCdb,ddf/dCda,ddf/dCdx0,ddf/dCdy0,ddf/dCdA,ddf/dCdB,ddf/dCdC},
0163 // }
0164 // The matrix is symmetric, as ddf/dParm1dParm2 = ddf/dParm2dParm1 so we only need to differentiate
0165 // one half (above or below the diagonal) of the matrix elements.
0166 // ddf/dbdb   = 0
0167 // ddf/dbda   = 0
0168 // ddf/dbdx0  = 0
0169 // ddf/dbdy0  = 0
0170 // ddf/dbdA   = 0
0171 // ddf/dbdB   = 0
0172 // ddf/dbdC   = 0
0173 //
0174 // ddf/dada   = 0
0175 // ddf/dadx0  = [2A(x-x0) + 2B(y-y0)].phi
0176 // ddf/dady0  = [2B(x-x0) + 2C(y-y0)].phi
0177 // ddf/dadA   = -(x-x0)^2.phi
0178 // ddf/dadB   = -2(x-x0)(y-y0).phi
0179 // ddf/dadC   = -Cy-y0)^2.phi
0180 //
0181 // ddf/dxodx0 = -2A.a.phi + [2A(x-x0)+2B(y-y0)]^2.a.phi
0182 // ddf/dx0dy0 = -2B.a.phi - [2A(x-x0)+2B(y-y0)].[2B(x-x0)+2C(y-y0)].a.phi
0183 // ddf/dx0dA  = 2(x-x0).a.phi - [2A(x-x0)+2B(y-y0)].(x-x0)^2.a.phi
0184 // ddf/dx0dB  = 2(y-y0).a.phi - [2A(x-x0)+2B(y-y0)].2(x-x0).(y-y0).a.phi
0185 // ddf/dx0dC  = -[2A(x-x0)+2B(y-y0)].2(y-y0)^2.a.phi
0186 //
0187 // ddf/dy0dy0 = -2c.a.phi + [2B(x-x0)+2C(y-y0)]^2.a.phi
0188 // ddf/dy0dA  = -[2B(x-x0)+2C(y-y0)].(x-x0)^2.a.phi
0189 // ddf/dy0dB  = 2(x-x0).a.phi - [2B(x-x0)+2C(y-y0)].2(x-x0)(y-y0).a.phi
0190 // ddf/dy0dC  = 2(y-y0).a.phi - [2B(x-x0)+2C(y-y0)].(y-y0)^2.a.phi
0191 //
0192 // ddf/dAdA   = (x-x0)^4.a.phi
0193 // ddf/dAdB   = 2(x-x0)^3.(y-y0).a.phi
0194 // ddf/dAdC   = (x-x0)^2.(y-y0)^2.a.phi
0195 //
0196 // ddf/dBdB   = 4(x-x0)^2.(y-y0)^2.a.phi
0197 // ddf/dBdC   = 2(x-x0).(y-y0)^3.a.phi
0198 //
0199 // ddf/dCdC   = (y-y0)^4.a.phi
0200 //
0201 // 3D Plane
0202 // Generalised equation for a 3D plane going through the origin.
0203 // Equation z = f(x,y) = -(A.x + B.y) / C
0204 // Jacobian J = {df/dA, df/dB, df/dC}
0205 // df/dA      = -x / C
0206 // df/dB      = -y / C
0207 // df/dC      = (A.x + B.y) / C^2
0208 //
0209 // 2nd derivatives
0210 //   {ddf/dAdA,ddf/dAdB,ddf/dAdC}
0211 //   {ddf/dBdA,ddf/dBdB,ddf/dBdC}
0212 //   {ddf/dCdA,ddf/dCdB,ddf/dCdC}
0213 //
0214 // The matrix is symmetric, as ddf/dParm1dParm2 = ddf/dParm2dParm1 so we only need to differentiate
0215 // one half (above or below the diagonal) of the matrix elements.
0216 //
0217 // ddf/dAdA = 0
0218 // ddf/dAdB = 0
0219 // ddf/dAdC = x / C^2
0220 //
0221 // ddf/dBdB = 0
0222 // ddf/dBdC = y / C^2
0223 //
0224 // ddf/dCdC = -2.(A.x + B.y) / C^3
0225 //
0226 //
0227 // Convergence
0228 // -----------
0229 // There following are key parameters that drive the LM solver. Currently these settings are coded into
0230 // the program. It would be better to store them in a configuration file somewhere, but they aren't the
0231 // sort of parameters users should have ready access to.
0232 // MAX_ITERATIONS - This is how many iterations to do before aborting. The game here is to set a
0233 //                  reasonable number to allow for convergence. In my testing with good data and a
0234 //                  good start point <10 iterations are required. With good data and a poor start point
0235 //                  <200 iterations are required. With poor data and a tight "tolerance" convergence
0236 //                  may need >1000 iterations. Setting to 500 seems a good compromise.
0237 // tolerance      - Conceptually tolerance could be done in 2 ways:
0238 //                  - check the gradient, would be 0 at the curve minimum
0239 //                  - check the residuals (and their gradient), will minimise at the curve minimum
0240 //                  Currently we will check on residuals.
0241 //                  This is supported by 2 parameters INEPSABS and INEPSREL.
0242 //                      convergence is achieved if for each datapoint i, where:
0243 //                        dx_i < INEPSABS + (INEPSREL * x_i)
0244 //                  Setting a slack tolerance will mean a range of x (focus positions) that are deemed
0245 //                  valid solutions. Not good.
0246 //                  Setting a tighter tolerance will require more iterations to solve, but setting too
0247 //                  tight a tolerance is just wasting time if it doesn't improve the focus position, and
0248 //                  if too tight the solver may not be able to find the solution, so a balance needs to
0249 //                  be struck. For now lets just make these values constant and see where this gets to.
0250 //                  If this turns out not to work for some equipment, the next step would be to adjust
0251 //                  these parameters based on the equipment profile, or to adapt the parameters starting
0252 //                  with a loose tolerance and tightening as the curve gets nearer to a complete solution.
0253 //                  If we inadvertently overtighten the tolerance and fail to converge, the tolerance
0254 //                  could be slackened or more iterations used.
0255 //                  I have found that the following work well on my equipment and the simulator
0256 //                  for both hyperbola and parabola. The advice in the GSL documentation is to start
0257 //                  with an absolute tolerance of 10^-d where d is the number of digits required in the
0258 //                  solution precision of x (focuser position). The gradient tolerance starting point is
0259 //                  (machine precision)^1/3 which we're using a relative tolerance.
0260 //                      INEPSABS = 1e-5
0261 //                      INEPSREL = GSL_DBL_EPSILON ^ 1/3
0262 
0263 namespace Ekos
0264 {
0265 
0266 namespace
0267 {
0268 // Constants used to index m_coefficient arrays
0269 enum { A_IDX = 0, B_IDX, C_IDX, D_IDX, E_IDX, F_IDX, G_IDX };
0270 
0271 // hypPhi() is a repeating part of the function calculations for Hyperbolas.
0272 double hypPhi(double x, double a, double c)
0273 {
0274     return sqrt(1.0 + pow(((x - c) / a), 2.0));
0275 }
0276 
0277 // Function to calculate f(x) for a hyperbola
0278 // y = b * hypPhi(x, a, c) + d
0279 double hypfx(double x, double a, double b, double c, double d)
0280 {
0281     return b * hypPhi(x, a, c) + d;
0282 }
0283 
0284 // Calculates F(x) for each data point on the hyperbola
0285 int hypFx(const gsl_vector * X, void * inParams, gsl_vector * outResultVec)
0286 {
0287     CurveFitting::DataPointT * DataPoints = ((CurveFitting::DataPointT *)inParams);
0288 
0289     double a = gsl_vector_get (X, A_IDX);
0290     double b = gsl_vector_get (X, B_IDX);
0291     double c = gsl_vector_get (X, C_IDX);
0292     double d = gsl_vector_get (X, D_IDX);
0293 
0294     for(int i = 0; i < DataPoints->dps.size(); ++i)
0295     {
0296         // Hyperbola equation
0297         double yi = hypfx(DataPoints->dps[i].x, a, b, c, d);
0298 
0299         gsl_vector_set(outResultVec, i, (yi - DataPoints->dps[i].y));
0300     }
0301 
0302     return GSL_SUCCESS;
0303 }
0304 
0305 // Calculates the Jacobian (derivative) matrix for the hyperbola
0306 int hypJx(const gsl_vector * X, void * inParams, gsl_matrix * J)
0307 {
0308     CurveFitting::DataPointT * DataPoints = ((struct CurveFitting::DataPointT *)inParams);
0309 
0310     // Store current coefficients
0311     double a = gsl_vector_get(X, A_IDX);
0312     double b = gsl_vector_get(X, B_IDX);
0313     double c = gsl_vector_get(X, C_IDX);
0314 
0315     // Store non-changing calculations
0316     const double a2 = a * a;
0317     const double a3 = a * a2;
0318 
0319     for(int i = 0; i < DataPoints->dps.size(); ++i)
0320     {
0321         // Calculate the Jacobian Matrix
0322         const double x = DataPoints->dps[i].x;
0323         const double x_minus_c = x - c;
0324 
0325         gsl_matrix_set(J, i, A_IDX, -b * (x_minus_c * x_minus_c) / (a3 * hypPhi(x, a, c)));
0326         gsl_matrix_set(J, i, B_IDX, hypPhi(x, a, c));
0327         gsl_matrix_set(J, i, C_IDX, -b * x_minus_c / (a2 * hypPhi(x, a, c)));
0328         gsl_matrix_set(J, i, D_IDX, 1);
0329     }
0330 
0331     return GSL_SUCCESS;
0332 }
0333 
0334 
0335 // ddf/dada = (b*(c-x)^2*(3*a^2+a*(c-x)^2))/(a^4*(a^2 + (c-x)^2)*phi)
0336 // ddf/dadb = -((x - c) ^2) / ((a ^ 3) * phi)
0337 // ddf/dadc = -(b*(c-x)*(2a^2+(c-x)^2))/(a^3*(a^2 + (c-x)^2)*phi))
0338 // ddf/dadd = 0
0339 // ddf/dbdb = 0
0340 // ddf/dbdc = -(x-c)/(a^2*phi)
0341 // ddf/dbdd = 0
0342 // ddf/dcdc = b/((a^2+(c-x)^2) * phi)
0343 // ddf/dcdd = 0
0344 // ddf/dddd = 0
0345 
0346 // ddf/dada = (b*(c-x)^2*/(a^4*phi).[3 - (x-c)^2/(a * phi)^2]
0347 // ddf/dadb = -((x - c)^2) / ((a^3) * phi)
0348 // ddf/dadc = -(b*(x-c)/((a * phi)^3).[2 - (x-c)^2/((a * phi)^2)]
0349 // ddf/dadd = 0
0350 // ddf/dbdb = 0
0351 // ddf/dbdc = -(x-c)/(a^2*phi)
0352 // ddf/dbdd = 0
0353 // ddf/dcdc = b/(phi^3 * a^2).[1 - ((x - c)^2)/((a * phi)^2)]
0354 // ddf/dcdd = 0
0355 // ddf/dddd = 0
0356 int hypFxx(const gsl_vector* X,  const gsl_vector* v, void* inParams, gsl_vector* fvv)
0357 {
0358     CurveFitting::DataPointT * DataPoint = ((struct CurveFitting::DataPointT *)inParams);
0359 
0360     // Store current coefficients
0361     const double a = gsl_vector_get(X, A_IDX);
0362     const double b = gsl_vector_get(X, B_IDX);
0363     const double c = gsl_vector_get(X, C_IDX);
0364 
0365     const double a2 = pow(a, 2);
0366     const double a4 = pow(a2, 2);
0367 
0368     const double va = gsl_vector_get(v, A_IDX);
0369     const double vb = gsl_vector_get(v, B_IDX);
0370     const double vc = gsl_vector_get(v, C_IDX);
0371 
0372     for(int i = 0; i < DataPoint->dps.size(); ++i)
0373     {
0374         const double x = DataPoint->dps[i].x;
0375         const double xmc   = x - c;
0376         const double xmc2  = pow(xmc, 2);
0377         const double phi   = hypPhi(x, a, c);
0378         const double aphi  = a * phi;
0379         const double aphi2 = pow(aphi, 2);
0380 
0381         const double Daa = b * xmc2 * (3 - xmc2 / aphi2) / (a4 * phi);
0382         const double Dab = -xmc2 / (a2 * aphi);
0383         const double Dac = b * xmc * (2 - (xmc2 / aphi2)) / (aphi2 * aphi);
0384         const double Dbc = -xmc / (a2 * phi);
0385         const double Dcc = b * (1 - (xmc2 / aphi2)) / (phi * aphi2);
0386 
0387         double sum = va * va * Daa + 2 * (va * vb * Dab + va * vc * Dac + vb * vc * Dbc) + vc * vc * Dcc;
0388 
0389         gsl_vector_set(fvv, i, sum);
0390 
0391     }
0392 
0393     return GSL_SUCCESS;
0394 }
0395 
0396 // Function to calculate f(x) for a parabola.
0397 double parfx(double x, double a, double b, double c)
0398 {
0399     return a + b * pow((x - c), 2.0);
0400 }
0401 
0402 // Calculates f(x) for each data point in the parabola.
0403 int parFx(const gsl_vector * X, void * inParams, gsl_vector * outResultVec)
0404 {
0405     CurveFitting::DataPointT * DataPoint = ((struct CurveFitting::DataPointT *)inParams);
0406 
0407     double a = gsl_vector_get (X, A_IDX);
0408     double b = gsl_vector_get (X, B_IDX);
0409     double c = gsl_vector_get (X, C_IDX);
0410 
0411     for(int i = 0; i < DataPoint->dps.size(); ++i)
0412     {
0413         // Parabola equation
0414         double yi = parfx(DataPoint->dps[i].x, a, b, c);
0415 
0416         gsl_vector_set(outResultVec, i, (yi - DataPoint->dps[i].y));
0417     }
0418 
0419     return GSL_SUCCESS;
0420 }
0421 
0422 // Calculates the Jacobian (derivative) matrix for the parabola equation f(x) = a + b*(x-c)^2
0423 // dy/da = 1
0424 // dy/db = (x - c)^2
0425 // dy/dc = -2b * (x - c)
0426 int parJx(const gsl_vector * X, void * inParams, gsl_matrix * J)
0427 {
0428     CurveFitting::DataPointT * DataPoint = ((struct CurveFitting::DataPointT *)inParams);
0429 
0430     // Store current coefficients
0431     double b = gsl_vector_get(X, B_IDX);
0432     double c = gsl_vector_get(X, C_IDX);
0433 
0434     for(int i = 0; i < DataPoint->dps.size(); ++i)
0435     {
0436         // Calculate the Jacobian Matrix
0437         const double x = DataPoint->dps[i].x;
0438         const double xmc = x - c;
0439 
0440         gsl_matrix_set(J, i, A_IDX, 1);
0441         gsl_matrix_set(J, i, B_IDX, xmc * xmc);
0442         gsl_matrix_set(J, i, C_IDX, -2 * b * xmc);
0443     }
0444 
0445     return GSL_SUCCESS;
0446 }
0447 // Calculates the second directional derivative vector for the parabola equation f(x) = a + b*(x-c)^2
0448 int parFxx(const gsl_vector* X,  const gsl_vector* v, void* inParams, gsl_vector* fvv)
0449 {
0450     CurveFitting::DataPointT * DataPoint = ((struct CurveFitting::DataPointT *)inParams);
0451 
0452     const double b = gsl_vector_get(X, B_IDX);
0453     const double c = gsl_vector_get(X, C_IDX);
0454 
0455     const double vb = gsl_vector_get(v, B_IDX);
0456     const double vc = gsl_vector_get(v, C_IDX);
0457 
0458     for(int i = 0; i < DataPoint->dps.size(); ++i)
0459     {
0460         const double x = DataPoint->dps[i].x;
0461         double Dbc = -2 * (x - c);
0462         double Dcc = 2 * b;
0463         double sum = 2 * vb * vc * Dbc + vc * vc * Dcc;
0464 
0465         gsl_vector_set(fvv, i, sum);
0466 
0467     }
0468 
0469     return GSL_SUCCESS;
0470 }
0471 
0472 // Function to calculate f(x,y) for a 2-D gaussian.
0473 double gaufxy(double x, double y, double a, double x0, double y0, double A, double B, double C, double b)
0474 {
0475     return b + a * exp(-(A * (pow(x - x0, 2.0)) + 2.0 * B * (x - x0) * (y - y0) + C * (pow(y - y0, 2.0))));
0476 }
0477 
0478 // Calculates f(x,y) for each data point in the gaussian.
0479 int gauFxy(const gsl_vector * X, void * inParams, gsl_vector * outResultVec)
0480 {
0481     CurveFitting::DataPoint3DT * DataPoint = ((struct CurveFitting::DataPoint3DT *)inParams);
0482 
0483     double a  = gsl_vector_get (X, A_IDX);
0484     double x0  = gsl_vector_get (X, B_IDX);
0485     double y0 = gsl_vector_get (X, C_IDX);
0486     double A = gsl_vector_get (X, D_IDX);
0487     double B  = gsl_vector_get (X, E_IDX);
0488     double C  = gsl_vector_get (X, F_IDX);
0489     double b  = gsl_vector_get (X, G_IDX);
0490 
0491     for(int i = 0; i < DataPoint->dps.size(); ++i)
0492     {
0493         // Gaussian equation
0494         double zij = gaufxy(DataPoint->dps[i].x, DataPoint->dps[i].y, a, x0, y0, A, B, C, b);
0495         gsl_vector_set(outResultVec, i, (zij - DataPoint->dps[i].z));
0496     }
0497 
0498     return GSL_SUCCESS;
0499 }
0500 
0501 // Calculates the Jacobian (derivative) matrix for the gaussian
0502 // Jacobian J = {df/db, df/da, df/dx0, df/dy0, df/dA, df/dB, df/dC}
0503 // Let phi    = exp-(A((x-x0)^2) + 2B(x-x0)(y-y0) + C((y-y0)^2))
0504 // df/db      = 1
0505 // df/da      = phi
0506 // df/dx0     = [2A(x-x0)+2B(y-y0)].a.phi
0507 // df/dy0     = [2B(x-x0)+2C(y-y0)].a.phi
0508 // df/dA      = -(x-x0)^2.a.phi
0509 // df/DB      = -2(x-x0)(y-y0).a.phi
0510 // df/dC      = -(y-y0)^2.a.phi
0511 int gauJxy(const gsl_vector * X, void * inParams, gsl_matrix * J)
0512 {
0513     CurveFitting::DataPoint3DT * DataPoint = ((struct CurveFitting::DataPoint3DT *)inParams);
0514 
0515     // Get current coefficients
0516     const double a  = gsl_vector_get (X, A_IDX);
0517     const double x0  = gsl_vector_get (X, B_IDX);
0518     const double y0 = gsl_vector_get (X, C_IDX);
0519     const double A = gsl_vector_get (X, D_IDX);
0520     const double B  = gsl_vector_get (X, E_IDX);
0521     const double C  = gsl_vector_get (X, F_IDX);
0522     // b is not used ... const double b  = gsl_vector_get (X, G_IDX);
0523 
0524     for(int i = 0; i < DataPoint->dps.size(); ++i)
0525     {
0526         // Calculate the Jacobian Matrix
0527         const double x = DataPoint->dps[i].x;
0528         const double xmx0 = x - x0;
0529         const double xmx02 = xmx0 * xmx0;
0530         const double y = DataPoint->dps[i].y;
0531         const double ymy0 = y - y0;
0532         const double ymy02 = ymy0 * ymy0;
0533         const double phi = exp(-((A * xmx02) + (2.0 * B * xmx0 * ymy0) + (C * ymy02)));
0534         const double aphi = a * phi;
0535 
0536         gsl_matrix_set(J, i, A_IDX, phi);
0537         gsl_matrix_set(J, i, B_IDX, 2.0 * aphi * ((A * xmx0) + (B * ymy0)));
0538         gsl_matrix_set(J, i, C_IDX, 2.0 * aphi * ((B * xmx0) + (C * ymy0)));
0539         gsl_matrix_set(J, i, D_IDX, -1.0 * aphi * xmx02);
0540         gsl_matrix_set(J, i, E_IDX, -2.0 * aphi * xmx0 * ymy0);
0541         gsl_matrix_set(J, i, F_IDX, -1.0 * aphi * ymy02);
0542         gsl_matrix_set(J, i, G_IDX, 1.0);
0543     }
0544 
0545     return GSL_SUCCESS;
0546 }
0547 
0548 // Calculates the second directional derivative vector for the gaussian equation
0549 // The matrix is symmetric, as ddf/dParm1dParm2 = ddf/dParm2dParm1 so we only need to differentiate
0550 // one half (above or below the diagonal) of the matrix elements.
0551 // ddf/dbdb   = 0
0552 // ddf/dbda   = 0
0553 // ddf/dbdx0  = 0
0554 // ddf/dbdy0  = 0
0555 // ddf/dbdA   = 0
0556 // ddf/dbdB   = 0
0557 // ddf/dbdC   = 0
0558 //
0559 // ddf/dada   = 0
0560 // ddf/dadx0  = [2A(x-x0) + 2B(y-y0)].phi
0561 // ddf/dady0  = [2B(x-x0) + 2C(y-y0)].phi
0562 // ddf/dadA   = -(x-x0)^2.phi
0563 // ddf/dadB   = -2(x-x0)(y-y0).phi
0564 // ddf/dadC   = -Cy-y0)^2.phi
0565 //
0566 // ddf/dxodx0 = -2A.a.phi + [2A(x-x0)+2B(y-y0)]^2.a.phi
0567 // ddf/dx0dy0 = -2B.a.phi - [2A(x-x0)+2B(y-y0)].[2B(x-x0)+2C(y-y0)].a.phi
0568 // ddf/dx0dA  = 2(x-x0).a.phi - [2A(x-x0)+2B(y-y0)].(x-x0)^2.a.phi
0569 // ddf/dx0dB  = 2(y-y0).a.phi - [2A(x-x0)+2B(y-y0)].2(x-x0).(y-y0).a.phi
0570 // ddf/dx0dC  = -[2A(x-x0)+2B(y-y0)].2(y-y0)^2.a.phi
0571 //
0572 // ddf/dy0dy0 = -2c.a.phi + [2B(x-x0)+2C(y-y0)]^2.a.phi
0573 // ddf/dy0dA  = -[2B(x-x0)+2C(y-y0)].(x-x0)^2.a.phi
0574 // ddf/dy0dB  = 2(x-x0).a.phi - [2B(x-x0)+2C(y-y0)].2(x-x0)(y-y0).a.phi
0575 // ddf/dy0dC  = 2(y-y0).a.phi - [2B(x-x0)+2C(y-y0)].(y-y0)^2.a.phi
0576 //
0577 // ddf/dAdA   = (x-x0)^4.a.phi
0578 // ddf/dAdB   = 2(x-x0)^3.(y-y0).a.phi
0579 // ddf/dAdC   = (x-x0)^2.(y-y0)^2.a.phi
0580 //
0581 // ddf/dBdB   = 4(x-x0)^2.(y-y0)^2.a.phi
0582 // ddf/dBdC   = 2(x-x0).(y-y0)^3.a.phi
0583 //
0584 // ddf/dCdC   = (y-y0)^4.a.phi
0585 //
0586 int gauFxyxy(const gsl_vector* X,  const gsl_vector* v, void* inParams, gsl_vector* fvv)
0587 {
0588     CurveFitting::DataPoint3DT * DataPoint = ((struct CurveFitting::DataPoint3DT *)inParams);
0589 
0590     // Get current coefficients
0591     const double a  = gsl_vector_get (X, A_IDX);
0592     const double x0 = gsl_vector_get (X, B_IDX);
0593     const double y0 = gsl_vector_get (X, C_IDX);
0594     const double A  = gsl_vector_get (X, D_IDX);
0595     const double B  = gsl_vector_get (X, E_IDX);
0596     const double C  = gsl_vector_get (X, F_IDX);
0597     // b not used ... const double b  = gsl_vector_get (X, G_IDX);
0598 
0599     const double va  = gsl_vector_get(v, A_IDX);
0600     const double vx0 = gsl_vector_get(v, B_IDX);
0601     const double vy0 = gsl_vector_get(v, C_IDX);
0602     const double vA  = gsl_vector_get(v, D_IDX);
0603     const double vB  = gsl_vector_get(v, E_IDX);
0604     const double vC  = gsl_vector_get(v, F_IDX);
0605     // vb not used ... const double vb  = gsl_vector_get(v, G_IDX);
0606 
0607     for(int i = 0; i < DataPoint->dps.size(); ++i)
0608     {
0609         double x = DataPoint->dps[i].x;
0610         double xmx0 = x - x0;
0611         double xmx02 = xmx0 * xmx0;
0612         double y = DataPoint->dps[i].y;
0613         double ymy0 = y - y0;
0614         double ymy02 = ymy0 * ymy0;
0615         double phi = exp(-((A * xmx02) + (2.0 * B * xmx0 * ymy0) + (C * ymy02)));
0616         double aphi = a * phi;
0617         double AB = 2.0 * ((A * xmx0) + (B * ymy0));
0618         double BC = 2.0 * ((B * xmx0) + (C * ymy0));
0619 
0620         double Dax0 = AB * phi;
0621         double Day0 = BC * phi;
0622         double DaA  = -xmx02 * phi;
0623         double DaB  = -2.0 * xmx0 * ymy0 * phi;
0624         double DaC  = -ymy02 * phi;
0625 
0626         double Dx0x0 = aphi * ((-2.0 * A) + (AB * AB));
0627         double Dx0y0 = -aphi * ((2.0 * B) + (AB * BC));
0628         double Dx0A  = aphi * ((2.0 * xmx0) - (AB * xmx02));
0629         double Dx0B  = 2.0 * aphi * (ymy0 - (AB * xmx0 * ymy0));
0630         double Dx0C  = -2.0 * aphi * AB * ymy02;
0631 
0632         double Dy0y0 = aphi * ((-2.0 * C) + (BC * BC));
0633         double Dy0A  = -aphi * BC * xmx02;
0634         double Dy0B  = 2.0 * aphi * (xmx0 - (BC * xmx0 * ymy0));
0635         double Dy0C  = aphi * ((2.0 * ymy0) - (BC * ymy02));
0636 
0637         double DAA   = aphi * xmx02 * xmx02;
0638         double DAB   = 2.0 * aphi * xmx02 * xmx0 * ymy0;
0639         double DAC   = aphi * xmx02 * ymy02;
0640 
0641         double DBB   = 4.0 * aphi * xmx02 * ymy02;
0642         double DBC   = 2.0 * aphi * xmx0 * ymy02 * ymy0;
0643 
0644         double DCC   = aphi * ymy02 * ymy02;
0645 
0646         double sum = 2 * va * ((vx0 * Dax0) + (vy0 * Day0) + (vA * DaA) + (vB * DaB) + (vC * DaC)) + // a diffs
0647                      vx0 * ((vx0 * Dx0x0) + 2 * ((vy0 * Dx0y0) + (vA * Dx0A) + (vB * Dx0B) + (vC * Dx0C))) + // x0 diffs
0648                      vy0 * ((vy0 * Dy0y0) + 2 * ((vA * Dy0A) + (vB * Dy0B) + (vC * Dy0C))) + // y0 diffs
0649                      vA * ((vA * DAA) + 2 * ((vB * DAB) + (vC * DAC))) + // A diffs
0650                      vB * ((vB * DBB) + 2 * (vC * DBC)) + // B diffs
0651                      vC * vC * DCC; // C diffs
0652 
0653         gsl_vector_set(fvv, i, sum);
0654     }
0655 
0656     return GSL_SUCCESS;
0657 }
0658 
0659 // Function to calculate f(x,y) for a 2-D plane.
0660 // f(x,y) = (A.x + B.y) / -C
0661 double plafxy(double x, double y, double A, double B, double C)
0662 {
0663     return -(A * x + B * y) / C;
0664 }
0665 
0666 // Calculates f(x,y) for each data point in the gaussian.
0667 int plaFxy(const gsl_vector * X, void * inParams, gsl_vector * outResultVec)
0668 {
0669     CurveFitting::DataPoint3DT * DataPoint = ((struct CurveFitting::DataPoint3DT *)inParams);
0670 
0671     double A  = gsl_vector_get (X, A_IDX);
0672     double B  = gsl_vector_get (X, B_IDX);
0673     double C = gsl_vector_get (X, C_IDX);
0674 
0675     for(int i = 0; i < DataPoint->dps.size(); ++i)
0676     {
0677         // Plane equation
0678         double zi = plafxy(DataPoint->dps[i].x, DataPoint->dps[i].y, A, B, C);
0679         gsl_vector_set(outResultVec, i, (zi - DataPoint->dps[i].z));
0680     }
0681 
0682     return GSL_SUCCESS;
0683 }
0684 
0685 // Calculates the Jacobian (derivative) matrix for the plane
0686 // Jacobian J = {df/dA, df/dB, df/dC}
0687 // df/dA      = -x / C
0688 // df/dB      = -y / C
0689 // df/dC      = (A.x + B.y) / C^2
0690 int plaJxy(const gsl_vector * X, void * inParams, gsl_matrix * J)
0691 {
0692     CurveFitting::DataPoint3DT * DataPoint = ((struct CurveFitting::DataPoint3DT *)inParams);
0693 
0694     // Get current coefficients
0695     const double A = gsl_vector_get (X, A_IDX);
0696     const double B  = gsl_vector_get (X, B_IDX);
0697     const double C  = gsl_vector_get (X, C_IDX);
0698 
0699     for(int i = 0; i < DataPoint->dps.size(); ++i)
0700     {
0701         // Calculate the Jacobian Matrix
0702         const double x = DataPoint->dps[i].x;
0703         const double y = DataPoint->dps[i].y;
0704 
0705         gsl_matrix_set(J, i, A_IDX, -x / C);
0706         gsl_matrix_set(J, i, B_IDX, -y / C);
0707         gsl_matrix_set(J, i, C_IDX, (A * x + B * y) / (C * C));
0708     }
0709 
0710     return GSL_SUCCESS;
0711 }
0712 
0713 // Calculates the second directional derivative vector for the plane equation
0714 // The matrix is symmetric, as ddf/dParm1dParm2 = ddf/dParm2dParm1 so we only need to differentiate
0715 // one half (above or below the diagonal) of the matrix elements.
0716 //
0717 // ddf/dAdA = 0
0718 // ddf/dAdB = 0
0719 // ddf/dAdC = x / C^2
0720 
0721 // ddf/dBdB = 0
0722 // ddf/dBdC = y / C^2
0723 //
0724 // ddf/dCdC = -2.(A.x + B.y) / C^3
0725 //
0726 int plaFxyxy(const gsl_vector* X,  const gsl_vector* v, void* inParams, gsl_vector* fvv)
0727 {
0728     CurveFitting::DataPoint3DT * DataPoint = ((struct CurveFitting::DataPoint3DT *)inParams);
0729 
0730     // Get current coefficients
0731     const double A  = gsl_vector_get (X, A_IDX);
0732     const double B  = gsl_vector_get (X, B_IDX);
0733     const double C  = gsl_vector_get (X, C_IDX);
0734 
0735     const double vA  = gsl_vector_get(v, A_IDX);
0736     const double vB  = gsl_vector_get(v, B_IDX);
0737     const double vC  = gsl_vector_get(v, C_IDX);
0738 
0739     for(int i = 0; i < DataPoint->dps.size(); ++i)
0740     {
0741         double x = DataPoint->dps[i].x;
0742         double y = DataPoint->dps[i].y;
0743         double C2 = C * C;
0744         double C3 = C * C2;
0745         double dAdC = x / C2;
0746         double dBdC = y / C2;
0747         double dCdC = -2 * (A * x + B * y) / C3;
0748 
0749         double sum = 2 * (vA * vC * dAdC) + 2 * (vB * vC * dBdC) + vC * vC * dCdC;
0750 
0751         gsl_vector_set(fvv, i, sum);
0752     }
0753 
0754     return GSL_SUCCESS;
0755 }
0756 }  // namespace
0757 
0758 CurveFitting::CurveFitting()
0759 {
0760     // Constructor just initialises variables
0761     m_FirstSolverRun = true;
0762 }
0763 
0764 CurveFitting::CurveFitting(const QString &serialized)
0765 {
0766     recreateFromQString(serialized);
0767 }
0768 
0769 void CurveFitting::fitCurve(const FittingGoal goal, const QVector<int> &x_, const QVector<double> &y_,
0770                             const QVector<double> &weight_, const QVector<bool> &outliers_,
0771                             const CurveFit curveFit, const bool useWeights, const OptimisationDirection optDir)
0772 {
0773     if ((x_.size() != y_.size()) || (x_.size() != weight_.size()) || (x_.size() != outliers_.size()))
0774         qCDebug(KSTARS_EKOS_FOCUS) << QString("CurveFitting::fitCurve inconsistent parameters. x=%1, y=%2, weight=%3, outliers=%4")
0775                                    .arg(x_.size()).arg(y_.size()).arg(weight_.size()).arg(outliers_.size());
0776 
0777     m_x.clear();
0778     m_y.clear();
0779     m_scale.clear();
0780     for (int i = 0; i < x_.size(); ++i)
0781     {
0782         if (!outliers_[i])
0783         {
0784             m_x.push_back(static_cast<double>(x_[i]));
0785             m_y.push_back(y_[i]);
0786             m_scale.push_back(weight_[i]);
0787         }
0788     }
0789 
0790     m_useWeights = useWeights;
0791     m_CurveType = curveFit;
0792 
0793     switch (m_CurveType)
0794     {
0795         case FOCUS_QUADRATIC :
0796             m_coefficients = polynomial_fit(m_x.data(), m_y.data(), m_x.count(), 2);
0797             break;
0798         case FOCUS_HYPERBOLA :
0799             m_coefficients = hyperbola_fit(goal, m_x, m_y, m_scale, useWeights, optDir);
0800             break;
0801         case FOCUS_PARABOLA :
0802             m_coefficients = parabola_fit(goal, m_x, m_y, m_scale, useWeights, optDir);
0803             break;
0804         default :
0805             // Something went wrong, log an error and reset state so solver starts from scratch if called again
0806             qCDebug(KSTARS_EKOS_FOCUS) << QString("CurveFitting::fitCurve called with curveFit=%1").arg(curveFit);
0807             m_FirstSolverRun = true;
0808             return;
0809     }
0810     m_LastCoefficients = m_coefficients;
0811     m_LastCurveType    = m_CurveType;
0812     m_FirstSolverRun   = false;
0813 }
0814 
0815 void CurveFitting::fitCurve3D(const DataPoint3DT data, const CurveFit curveFit)
0816 {
0817     if (data.useWeights)
0818     {
0819         qCDebug(KSTARS_EKOS_FOCUS) << QString("CurveFitting::fitCurve3D called with useWeights = true. Not currently supported");
0820         m_FirstSolverRun = true;
0821         return;
0822     }
0823 
0824     m_useWeights = data.useWeights;
0825     m_CurveType = curveFit;
0826     m_dataPoints = data;
0827 
0828     switch (m_CurveType)
0829     {
0830         case FOCUS_PLANE :
0831             m_coefficients = plane_fit(data);
0832             break;
0833         default :
0834             // Something went wrong, log an error and reset state so solver starts from scratch if called again
0835             qCDebug(KSTARS_EKOS_FOCUS) << QString("CurveFitting::fitCurve3D called with curveFit=%1").arg(curveFit);
0836             m_FirstSolverRun = true;
0837             return;
0838     }
0839     m_LastCoefficients = m_coefficients;
0840     m_LastCurveType    = m_CurveType;
0841     m_FirstSolverRun   = false;
0842 }
0843 
0844 double CurveFitting::curveFunction(double x, void *params)
0845 {
0846     CurveFitting *instance = static_cast<CurveFitting *>(params);
0847 
0848     if (instance && !instance->m_coefficients.empty())
0849         return instance->f(x);
0850     else
0851         return -1;
0852 }
0853 
0854 double CurveFitting::f(double x)
0855 {
0856     const int order = m_coefficients.size() - 1;
0857     double y = 0;
0858     if (m_CurveType == FOCUS_QUADRATIC)
0859     {
0860         for (int i = 0; i <= order; ++i)
0861             y += m_coefficients[i] * pow(x, i);
0862     }
0863     else if (m_CurveType == FOCUS_HYPERBOLA && m_coefficients.size() == NUM_HYPERBOLA_PARAMS)
0864         y = hypfx(x, m_coefficients[A_IDX], m_coefficients[B_IDX], m_coefficients[C_IDX], m_coefficients[D_IDX]);
0865     else if (m_CurveType == FOCUS_PARABOLA && m_coefficients.size() == NUM_PARABOLA_PARAMS)
0866         y = parfx(x, m_coefficients[A_IDX], m_coefficients[B_IDX], m_coefficients[C_IDX]);
0867     else
0868         qCDebug(KSTARS_EKOS_FOCUS) << QString("Error: CurveFitting::f called with m_CurveType = %1 m_coefficients.size = %2")
0869                                    .arg(m_CurveType).arg(m_coefficients.size());
0870 
0871     return y;
0872 }
0873 
0874 double CurveFitting::f3D(double x, double y)
0875 {
0876     double z = 0;
0877     if (m_CurveType == FOCUS_GAUSSIAN && m_coefficients.size() == NUM_GAUSSIAN_PARAMS)
0878         z = gaufxy(x, y, m_coefficients[A_IDX], m_coefficients[B_IDX], m_coefficients[C_IDX], m_coefficients[D_IDX],
0879                    m_coefficients[E_IDX], m_coefficients[F_IDX], m_coefficients[G_IDX]);
0880     else if (m_CurveType == FOCUS_PLANE && m_coefficients.size() == NUM_PLANE_PARAMS)
0881         z = plafxy(x, y, m_coefficients[A_IDX], m_coefficients[B_IDX], m_coefficients[C_IDX]);
0882     else
0883         qCDebug(KSTARS_EKOS_FOCUS) << QString("Error: CurveFitting::f3D called with m_CurveType = %1 m_coefficients.size = %2")
0884                                    .arg(m_CurveType).arg(m_coefficients.size());
0885 
0886     return z;
0887 }
0888 
0889 QVector<double> CurveFitting::polynomial_fit(const double *const data_x, const double *const data_y, const int n,
0890         const int order)
0891 {
0892     int status = 0;
0893     double chisq = 0;
0894     QVector<double> vc;
0895     gsl_vector *y, *c;
0896     gsl_matrix *X, *cov;
0897     y   = gsl_vector_alloc(n);
0898     c   = gsl_vector_alloc(order + 1);
0899     X   = gsl_matrix_alloc(n, order + 1);
0900     cov = gsl_matrix_alloc(order + 1, order + 1);
0901 
0902     for (int i = 0; i < n; i++)
0903     {
0904         for (int j = 0; j < order + 1; j++)
0905         {
0906             gsl_matrix_set(X, i, j, pow(data_x[i], j));
0907         }
0908         gsl_vector_set(y, i, data_y[i]);
0909     }
0910 
0911     // Must turn off error handler or it aborts on error
0912     gsl_set_error_handler_off();
0913 
0914     gsl_multifit_linear_workspace *work = gsl_multifit_linear_alloc(n, order + 1);
0915     status                              = gsl_multifit_linear(X, y, c, cov, &chisq, work);
0916 
0917     if (status != GSL_SUCCESS)
0918         qDebug() << Q_FUNC_INFO << "GSL multifit error:" << gsl_strerror(status);
0919     else
0920     {
0921         gsl_multifit_linear_free(work);
0922 
0923         for (int i = 0; i < order + 1; i++)
0924         {
0925             vc.push_back(gsl_vector_get(c, i));
0926         }
0927     }
0928 
0929     gsl_vector_free(y);
0930     gsl_vector_free(c);
0931     gsl_matrix_free(X);
0932     gsl_matrix_free(cov);
0933 
0934     return vc;
0935 }
0936 
0937 QVector<double> CurveFitting::hyperbola_fit(FittingGoal goal, const QVector<double> data_x, const QVector<double> data_y,
0938         const QVector<double> data_weights, const bool useWeights, const OptimisationDirection optDir)
0939 {
0940     QVector<double> vc;
0941     DataPointT dataPoints;
0942 
0943     auto weights = gsl_vector_alloc(data_weights.size());
0944     // Fill in the data to which the curve will be fitted
0945     dataPoints.useWeights = useWeights;
0946     for (int i = 0; i < data_x.size(); i++)
0947         dataPoints.push_back(data_x[i], data_y[i], data_weights[i]);
0948 
0949     // Set the gsl error handler off as it aborts the program on error.
0950     auto const oldErrorHandler = gsl_set_error_handler_off();
0951 
0952     // Setup variables to be used by the solver
0953     gsl_multifit_nlinear_parameters params = gsl_multifit_nlinear_default_parameters();
0954     gsl_multifit_nlinear_workspace *w = gsl_multifit_nlinear_alloc(gsl_multifit_nlinear_trust, &params, data_x.size(),
0955                                         NUM_HYPERBOLA_PARAMS);
0956     gsl_multifit_nlinear_fdf fdf;
0957     gsl_vector *guess = gsl_vector_alloc(NUM_HYPERBOLA_PARAMS);
0958     int numIters;
0959     double xtol, gtol, ftol;
0960 
0961     // Fill in function info
0962     fdf.f = hypFx;
0963     fdf.df = hypJx;
0964     fdf.fvv = hypFxx;
0965     fdf.n = data_x.size();
0966     fdf.p = NUM_HYPERBOLA_PARAMS;
0967     fdf.params = &dataPoints;
0968 
0969     // This is the callback used by the LM solver to allow some introspection of each iteration
0970     // Useful for debugging but clogs up the log
0971     // To activate, uncomment the callback lambda and change the call to gsl_multifit_nlinear_driver
0972     /*auto callback = [](const size_t iter, void* _params, const auto * w)
0973     {
0974         gsl_vector *f = gsl_multifit_nlinear_residual(w);
0975         gsl_vector *x = gsl_multifit_nlinear_position(w);
0976 
0977         // compute reciprocal condition number of J(x)
0978         double rcond;
0979         gsl_multifit_nlinear_rcond(&rcond, w);
0980 
0981         // ratio of accel component to velocity component
0982         double avratio = gsl_multifit_nlinear_avratio(w);
0983 
0984         qCDebug(KSTARS_EKOS_FOCUS) << QString("iter %1: A=%2, B=%3, C=%4, D=%5 rcond(J)=%6, avratio=%7, |f(x)|=%8")
0985                                    .arg(iter)
0986                                    .arg(gsl_vector_get(x, A_IDX))
0987                                    .arg(gsl_vector_get(x, B_IDX))
0988                                    .arg(gsl_vector_get(x, C_IDX))
0989                                    .arg(gsl_vector_get(x, D_IDX))
0990                                    .arg(rcond)
0991                                    .arg(avratio)
0992                                    .arg(gsl_blas_dnrm2(f));
0993     };*/
0994 
0995     // Start a timer to see how long the solve takes.
0996     QElapsedTimer timer;
0997     timer.start();
0998 
0999     // We can sometimes have several attempts to solve based on "goal" and why the solver failed.
1000     // If the goal is STANDARD and we fail to solve then so be it. If the goal is BEST, then retry
1001     // with different parameters to really try and get a solution. A special case is if the solver
1002     // fails on its first step where we will always retry after adjusting parameters. It helps with
1003     // a situation where the solver gets "stuck" failing on first step repeatedly.
1004     for (int attempt = 0; attempt < 5; attempt++)
1005     {
1006         // Make initial guesses
1007         hypMakeGuess(attempt, data_x, data_y, optDir, guess);
1008 
1009         // Load up the weights and guess vectors
1010         if (useWeights)
1011         {
1012             for (int i = 0; i < data_weights.size(); i++)
1013                 gsl_vector_set(weights, i, data_weights[i]);
1014             gsl_multifit_nlinear_winit(guess, weights, &fdf, w);
1015         }
1016         else
1017             gsl_multifit_nlinear_init(guess, &fdf, w);
1018 
1019         // Tweak solver parameters from default values
1020         hypSetupParams(goal, &params, &numIters, &xtol, &gtol, &ftol);
1021 
1022         qCDebug(KSTARS_EKOS_FOCUS) << QString("Starting LM solver, fit=hyperbola, solver=%1, scale=%2, trs=%3, iters=%4, xtol=%5,"
1023                                               "gtol=%6, ftol=%7")
1024                                    .arg(params.solver->name).arg(params.scale->name).arg(params.trs->name).arg(numIters)
1025                                    .arg(xtol).arg(gtol).arg(ftol);
1026 
1027         int info = 0;
1028         int status = gsl_multifit_nlinear_driver(numIters, xtol, gtol, ftol, NULL, NULL, &info, w);
1029 
1030         if (status != 0)
1031         {
1032             // Solver failed so determine whether a retry is required.
1033             bool retry = false;
1034             if (goal == BEST)
1035             {
1036                 // Pull out all the stops to get a solution
1037                 retry = true;
1038                 goal = BEST_RETRY;
1039             }
1040             else if (status == GSL_EMAXITER && info == GSL_ENOPROG && gsl_multifit_nlinear_niter(w) <= 1)
1041                 // This is a special case where the solver can't take a first step
1042                 // So, perturb the initial conditions and have another go.
1043                 retry = true;
1044 
1045             qCDebug(KSTARS_EKOS_FOCUS) <<
1046                                        QString("LM solver (Hyperbola): Failed after %1ms iters=%2 [attempt=%3] with status=%4 [%5] and info=%6 [%7], retry=%8")
1047                                        .arg(timer.elapsed()).arg(gsl_multifit_nlinear_niter(w)).arg(attempt + 1).arg(status).arg(gsl_strerror(status))
1048                                        .arg(info).arg(gsl_strerror(info)).arg(retry);
1049             if (!retry)
1050                 break;
1051         }
1052         else
1053         {
1054             // All good so store the results - parameters A, B, C and D
1055             auto solution = gsl_multifit_nlinear_position(w);
1056             for (int j = 0; j < NUM_HYPERBOLA_PARAMS; j++)
1057                 vc.push_back(gsl_vector_get(solution, j));
1058 
1059             qCDebug(KSTARS_EKOS_FOCUS) <<
1060                                        QString("LM Solver (Hyperbola): Solution found after %1ms %2 iters (%3). A=%4, B=%5, C=%6, D=%7")
1061                                        .arg(timer.elapsed()).arg(gsl_multifit_nlinear_niter(w)).arg(getLMReasonCode(info)).arg(vc[A_IDX]).arg(vc[B_IDX])
1062                                        .arg(vc[C_IDX]).arg(vc[D_IDX]);
1063             break;
1064         }
1065     }
1066 
1067     // Free GSL memory
1068     gsl_multifit_nlinear_free(w);
1069     gsl_vector_free(guess);
1070     gsl_vector_free(weights);
1071 
1072     // Restore old GSL error handler
1073     gsl_set_error_handler(oldErrorHandler);
1074 
1075     return vc;
1076 }
1077 
1078 QString CurveFitting::getLMReasonCode(int info)
1079 {
1080     QString reason;
1081 
1082     if (info == 1)
1083         reason = QString("small step size");
1084     else if(info == 2)
1085         reason = QString("small gradient");
1086     else
1087         reason = QString("unknown reason");
1088 
1089     return reason;
1090 }
1091 
1092 // Setup the parameters for hyperbola curve fitting
1093 void CurveFitting::hypSetupParams(FittingGoal goal, gsl_multifit_nlinear_parameters *params, int *numIters, double *xtol,
1094                                   double *gtol, double *ftol)
1095 {
1096     // Trust region subproblem
1097     // - gsl_multifit_nlinear_trs_lm is the Levenberg-Marquardt algorithm without acceleration
1098     // - gsl_multifit_nlinear_trs_lmaccel is the Levenberg-Marquardt algorithm with acceleration
1099     // - gsl_multilarge_nlinear_trs_dogleg is the dogleg algorithm
1100     // - gsl_multifit_nlinear_trs_ddogleg is the double dogleg algorithm
1101     // - gsl_multifit_nlinear_trs_subspace2D is the 2D subspace algorithm
1102     params->trs = gsl_multifit_nlinear_trs_lmaccel;
1103 
1104     // Scale
1105     // - gsl_multifit_nlinear_scale_more uses. More strategy. Good for problems with parameters with widely different scales
1106     // - gsl_multifit_nlinear_scale_levenberg. Levensberg strategy. Can be good but not scale invariant.
1107     // - gsl_multifit_nlinear_scale_marquardt. Marquardt strategy. Considered inferior to More and Levensberg
1108     params->scale = gsl_multifit_nlinear_scale_more;
1109 
1110     // Solver
1111     // - gsl_multifit_nlinear_solver_qr produces reliable results but needs more iterations than Cholesky
1112     // - gsl_multifit_nlinear_solver_cholesky fast but not as stable as QR
1113     // - gsl_multifit_nlinear_solver_mcholesky modified Cholesky more stable than Cholesky
1114 
1115     // avmax is the max allowed ratio of 2nd order term (acceleration, a) to the 1st order term (velocity, v)
1116     // GSL defaults to 0.75, but suggests reducing it in the case of convergence problems
1117     switch (goal)
1118     {
1119         case STANDARD:
1120             params->solver = gsl_multifit_nlinear_solver_qr;
1121             params->avmax = 0.75;
1122 
1123             *numIters = MAX_ITERATIONS_CURVE;
1124             *xtol = INEPSXTOL;
1125             *gtol = INEPSGTOL;
1126             *ftol = INEPSFTOL;
1127             break;
1128         case BEST:
1129             params->solver = gsl_multifit_nlinear_solver_cholesky;
1130             params->avmax = 0.75;
1131 
1132             *numIters = MAX_ITERATIONS_CURVE * 2.0;
1133             *xtol = INEPSXTOL / 10.0;
1134             *gtol = INEPSGTOL / 10.0;
1135             *ftol = INEPSFTOL / 10.0;
1136             break;
1137         case BEST_RETRY:
1138             params->solver = gsl_multifit_nlinear_solver_qr;
1139             params->avmax = 0.5;
1140 
1141             *numIters = MAX_ITERATIONS_CURVE * 2.0;
1142             *xtol = INEPSXTOL;
1143             *gtol = INEPSGTOL;
1144             *ftol = INEPSFTOL;
1145             break;
1146         default:
1147             break;
1148     }
1149 }
1150 
1151 // Initialise parameters before starting the solver. Its important to start with a guess as near to the solution as possible
1152 // If we found a solution before and we're just adding more datapoints use the last solution as the guess.
1153 // If we don't have a solution use the datapoints to approximate. Work out the min and max points and use these
1154 // to find "close" values for the starting point parameters
1155 void CurveFitting::hypMakeGuess(const int attempt, const QVector<double> inX, const QVector<double> inY,
1156                                 const OptimisationDirection optDir, gsl_vector * guess)
1157 {
1158     if (inX.size() < 1 || inX.size() != inY.size())
1159     {
1160         qCDebug(KSTARS_EKOS_FOCUS) << QString("Bad call to hypMakeGuess: inX.size=%1, inY.size=%2").arg(inX.size()).arg(inY.size());
1161         return;
1162     }
1163 
1164     // If we are retrying then perturb the initial conditions. The hope is that by doing this the solver
1165     // will be nudged to find a solution this time
1166     double perturbation = 1.0 + pow(-1, attempt) * (attempt * 0.1);
1167 
1168     if (!m_FirstSolverRun && (m_LastCurveType == FOCUS_HYPERBOLA) && (m_LastCoefficients.size() == NUM_HYPERBOLA_PARAMS))
1169     {
1170         // Last run of the solver was a Hyperbola and the solution was good, so use that solution
1171         gsl_vector_set(guess, A_IDX, m_LastCoefficients[A_IDX] * perturbation);
1172         gsl_vector_set(guess, B_IDX, m_LastCoefficients[B_IDX] * perturbation);
1173         gsl_vector_set(guess, C_IDX, m_LastCoefficients[C_IDX] * perturbation);
1174         gsl_vector_set(guess, D_IDX, m_LastCoefficients[D_IDX] * perturbation);
1175     }
1176     else
1177     {
1178         double minX = inX[0];
1179         double minY = inY[0];
1180         double maxX = minX;
1181         double maxY = minY;
1182         for(int i = 0; i < inX.size(); i++)
1183         {
1184             if (inY[i] <= 0.0)
1185                 continue;
1186             if(minY <= 0.0 || inY[i] < minY)
1187             {
1188                 minX = inX[i];
1189                 minY = inY[i];
1190             }
1191             if(maxY <= 0.0 || inY[i] > maxY)
1192             {
1193                 maxX = inX[i];
1194                 maxY = inY[i];
1195             }
1196         }
1197         double A, B, C, D;
1198         if (optDir == OPTIMISATION_MAXIMISE)
1199         {
1200             // Hyperbola equation: y = f(x) = b * sqrt(1 + ((x - c) / a) ^2) + d
1201             // For a maximum: c = maximum x = x(max)
1202             //                b < 0 and b + d > 0
1203             // For the array of data, set:
1204             // => c = x(max)
1205             // Now assume maximum x is near the real curve maximum, so y = b + d
1206             // Set b = -d/2. So y(max) = -d/2 + d = d/2.
1207             // => d = 2.y(max)
1208             // => b = -y(max)
1209             // Now look at the minimum y value in the array of datapoints
1210             // y(min) = b * sqrt(1 + ((x(min) - c) / a) ^2) + d
1211             // (y(min) - d) / b) ^ 2) = 1 + ((x(min) - c) / a) ^2
1212             // sqrt((((y(min) - d) / b) ^ 2) - 1) = (x(min) - c) / a
1213             // a = (x(min) - c) / sqrt((((y(min) - d) / b) ^ 2) - 1)
1214             // use the values for b, c, d obtained above to get:
1215             // => a = (x(min) - x(max)) / sqrt((((y(min) - (2.y(max)) / (-y(max))) ^ 2) - 1)
1216             double num = minX - maxX;
1217             double denom = sqrt(pow((2.0 * maxY - minY) / maxY, 2.0) - 1.0);
1218             if(denom <= 0.0)
1219                 denom = 1.0;
1220             A = num / denom * perturbation;
1221             B = -maxY * perturbation;
1222             C = maxX * perturbation;
1223             D = 2.0 * maxY * perturbation;
1224         }
1225         else
1226         {
1227             // For a minimum: c = minimum x; b > 0 and b + d > 0
1228             // For the array of data, set:
1229             // => c = x(min)
1230             // Now assume minimum x is near the real curve minimum, so y = b + d
1231             // => Set b = d = y(min) / 2
1232             // Now look at the maximum y value in the array of datapoints
1233             // y(max) = b * sqrt(1 + ((x(max) - c) / a) ^2) + d
1234             // ((y(max) - d) / b) ^2 = 1 + ((x(max) - c) / a) ^2
1235             // a = (x(max) - c) / sqrt((((y(max) - d) / b) ^ 2) - 1)
1236             // use the values for b, c, d obtained above to get:
1237             // a = (x(max) - x(min)) / sqrt((((y(max) - (y(min) / 2)) / (y(min) / 2)) ^ 2) - 1)
1238             double minYdiv2 = (minY / 2.0 <= 0.0) ? 1.0 : minY / 2.0;
1239             double num = maxX - minX;
1240             double denom = sqrt(pow((maxY - minYdiv2) / minYdiv2, 2.0) - 1.0);
1241             if(denom <= 0.0)
1242                 denom = 1.0;
1243             A = num / denom * perturbation;
1244             B = minYdiv2 * perturbation;
1245             C = minX * perturbation;
1246             D = B;
1247         }
1248         qCDebug(KSTARS_EKOS_FOCUS) << QString("LM solver (hyp) initial params: perturbation=%1, A=%2, B=%3, C=%4, D=%5")
1249                                    .arg(perturbation).arg(A).arg(B).arg(C).arg(D);
1250 
1251         gsl_vector_set(guess, A_IDX, A);
1252         gsl_vector_set(guess, B_IDX, B);
1253         gsl_vector_set(guess, C_IDX, C);
1254         gsl_vector_set(guess, D_IDX, D);
1255     }
1256 }
1257 
1258 QVector<double> CurveFitting::parabola_fit(FittingGoal goal, const QVector<double> data_x, const QVector<double> data_y,
1259         const QVector<double> data_weights, bool useWeights, const OptimisationDirection optDir)
1260 {
1261     QVector<double> vc;
1262     DataPointT dataPoints;
1263 
1264     auto weights = gsl_vector_alloc(data_weights.size());
1265     // Fill in the data to which the curve will be fitted
1266     dataPoints.useWeights = useWeights;
1267     for (int i = 0; i < data_x.size(); i++)
1268         dataPoints.push_back(data_x[i], data_y[i], data_weights[i]);
1269 
1270     // Set the gsl error handler off as it aborts the program on error.
1271     auto const oldErrorHandler = gsl_set_error_handler_off();
1272 
1273     // Setup variables to be used by the solver
1274     gsl_multifit_nlinear_parameters params = gsl_multifit_nlinear_default_parameters();
1275     gsl_multifit_nlinear_workspace* w = gsl_multifit_nlinear_alloc (gsl_multifit_nlinear_trust, &params, data_x.size(),
1276                                         NUM_PARABOLA_PARAMS);
1277     gsl_multifit_nlinear_fdf fdf;
1278     gsl_vector * guess = gsl_vector_alloc(NUM_PARABOLA_PARAMS);
1279     int numIters;
1280     double xtol, gtol, ftol;
1281 
1282     // Fill in function info
1283     fdf.f = parFx;
1284     fdf.df = parJx;
1285     fdf.fvv = parFxx;
1286     fdf.n = data_x.size();
1287     fdf.p = NUM_PARABOLA_PARAMS;
1288     fdf.params = &dataPoints;
1289 
1290     // This is the callback used by the LM solver to allow some introspection of each iteration
1291     // Useful for debugging but clogs up the log
1292     // To activate, uncomment the callback lambda and change the call to gsl_multifit_nlinear_driver
1293     /*auto callback = [](const size_t iter, void* _params, const auto * w)
1294     {
1295         gsl_vector *f = gsl_multifit_nlinear_residual(w);
1296         gsl_vector *x = gsl_multifit_nlinear_position(w);
1297 
1298         // compute reciprocal condition number of J(x)
1299         double rcond;
1300         gsl_multifit_nlinear_rcond(&rcond, w);
1301 
1302         // ratio of accel component to velocity component
1303         double avratio = gsl_multifit_nlinear_avratio(w);
1304 
1305         qCDebug(KSTARS_EKOS_FOCUS) << QString("iter %1: A=%2, B=%3, C=%4, rcond(J)=%5, avratio=%6 |f(x)|=%7")
1306                                    .arg(iter)
1307                                    .arg(gsl_vector_get(x, A_IDX))
1308                                    .arg(gsl_vector_get(x, B_IDX))
1309                                    .arg(gsl_vector_get(x, C_IDX))
1310                                    .arg(rcond)
1311                                    .arg(gsl_blas_dnrm2(f));
1312     };*/
1313 
1314     // Start a timer to see how long the solve takes.
1315     QElapsedTimer timer;
1316     timer.start();
1317 
1318     // We can sometimes have several attempts to solve based on "goal" and why the solver failed.
1319     // If the goal is STANDARD and we fail to solve then so be it. If the goal is BEST, then retry
1320     // with different parameters to really try and get a solution. A special case is if the solver
1321     // fails on its first step where we will always retry after adjusting parameters. It helps with
1322     // a situation where the solver gets "stuck" failing on first step repeatedly.
1323     for (int attempt = 0; attempt < 5; attempt++)
1324     {
1325         // Make initial guesses - here we just set all parameters to 1.0
1326         parMakeGuess(attempt, data_x, data_y, optDir, guess);
1327 
1328         // Load up the weights and guess vectors
1329         if (useWeights)
1330         {
1331             for (int i = 0; i < data_weights.size(); i++)
1332                 gsl_vector_set(weights, i, data_weights[i]);
1333             gsl_multifit_nlinear_winit(guess, weights, &fdf, w);
1334         }
1335         else
1336             gsl_multifit_nlinear_init(guess, &fdf, w);
1337 
1338         // Tweak solver parameters from default values
1339         parSetupParams(goal, &params, &numIters, &xtol, &gtol, &ftol);
1340 
1341         qCDebug(KSTARS_EKOS_FOCUS) << QString("Starting LM solver, fit=parabola, solver=%1, scale=%2, trs=%3, iters=%4, xtol=%5,"
1342                                               "gtol=%6, ftol=%7")
1343                                    .arg(params.solver->name).arg(params.scale->name).arg(params.trs->name).arg(numIters)
1344                                    .arg(xtol).arg(gtol).arg(ftol);
1345 
1346         int info = 0;
1347         int status = gsl_multifit_nlinear_driver(numIters, xtol, gtol, ftol, NULL, NULL, &info, w);
1348 
1349         if (status != 0)
1350         {
1351             // Solver failed so determine whether a retry is required.
1352             bool retry = false;
1353             if (goal == BEST)
1354             {
1355                 // Pull out all the stops to get a solution
1356                 retry = true;
1357                 goal = BEST_RETRY;
1358             }
1359             else if (status == GSL_EMAXITER && info == GSL_ENOPROG && gsl_multifit_nlinear_niter(w) <= 1)
1360                 // This is a special case where the solver can't take a first step
1361                 // So, perturb the initial conditions and have another go.
1362                 retry = true;
1363 
1364             qCDebug(KSTARS_EKOS_FOCUS) <<
1365                                        QString("LM solver (Parabola): Failed after %1ms iters=%2 [attempt=%3] with status=%4 [%5] and info=%6 [%7], retry=%8")
1366                                        .arg(timer.elapsed()).arg(gsl_multifit_nlinear_niter(w)).arg(attempt + 1).arg(status).arg(gsl_strerror(status))
1367                                        .arg(info).arg(gsl_strerror(info)).arg(retry);
1368             if (!retry)
1369                 break;
1370         }
1371         else
1372         {
1373             // All good so store the results - parameters A, B, and C
1374             auto solution = gsl_multifit_nlinear_position(w);
1375             for (int j = 0; j < NUM_PARABOLA_PARAMS; j++)
1376                 vc.push_back(gsl_vector_get(solution, j));
1377 
1378             qCDebug(KSTARS_EKOS_FOCUS) << QString("LM Solver (Parabola): Solution found after %1ms %2 iters (%3). A=%4, B=%5, C=%6")
1379                                        .arg(timer.elapsed()).arg(gsl_multifit_nlinear_niter(w)).arg(getLMReasonCode(info)).arg(vc[A_IDX]).arg(vc[B_IDX])
1380                                        .arg(vc[C_IDX]);
1381             break;
1382         }
1383     }
1384 
1385     // Free GSL memory
1386     gsl_multifit_nlinear_free(w);
1387     gsl_vector_free(guess);
1388     gsl_vector_free(weights);
1389 
1390     // Restore old GSL error handler
1391     gsl_set_error_handler(oldErrorHandler);
1392 
1393     return vc;
1394 }
1395 
1396 // Setup the parameters for parabola curve fitting
1397 void CurveFitting::parSetupParams(FittingGoal goal, gsl_multifit_nlinear_parameters *params, int *numIters, double *xtol,
1398                                   double *gtol, double *ftol)
1399 {
1400     // Trust region subproblem
1401     // - gsl_multifit_nlinear_trs_lm is the Levenberg-Marquardt algorithm without acceleration
1402     // - gsl_multifit_nlinear_trs_lmaccel is the Levenberg-Marquardt algorithm with acceleration
1403     // - gsl_multilarge_nlinear_trs_dogleg is the dogleg algorithm
1404     // - gsl_multifit_nlinear_trs_ddogleg is the double dogleg algorithm
1405     // - gsl_multifit_nlinear_trs_subspace2D is the 2D subspace algorithm
1406     params->trs = gsl_multifit_nlinear_trs_lmaccel;
1407 
1408     // Scale
1409     // - gsl_multifit_nlinear_scale_more uses. More strategy. Good for problems with parameters with widely different scales
1410     // - gsl_multifit_nlinear_scale_levenberg. Levensberg strategy. Can be good but not scale invariant.
1411     // - gsl_multifit_nlinear_scale_marquardt. Marquardt strategy. Considered inferior to More and Levensberg
1412     params->scale = gsl_multifit_nlinear_scale_more;
1413 
1414     // Solver
1415     // - gsl_multifit_nlinear_solver_qr produces reliable results but needs more iterations than Cholesky
1416     // - gsl_multifit_nlinear_solver_cholesky fast but not as stable as QR
1417     // - gsl_multifit_nlinear_solver_mcholesky modified Cholesky more stable than Cholesky
1418 
1419     // avmax is the max allowed ratio of 2nd order term (acceleration, a) to the 1st order term (velocity, v)
1420     // GSL defaults to 0.75, but suggests reducing it in the case of convergence problems
1421     switch (goal)
1422     {
1423         case STANDARD:
1424             params->solver = gsl_multifit_nlinear_solver_cholesky;
1425             params->avmax = 0.75;
1426 
1427             *numIters = MAX_ITERATIONS_CURVE;
1428             *xtol = INEPSXTOL;
1429             *gtol = INEPSGTOL;
1430             *ftol = INEPSFTOL;
1431             break;
1432         case BEST:
1433             params->solver = gsl_multifit_nlinear_solver_cholesky;
1434             params->avmax = 0.75;
1435 
1436             *numIters = MAX_ITERATIONS_CURVE * 2.0;
1437             *xtol = INEPSXTOL / 10.0;
1438             *gtol = INEPSGTOL / 10.0;
1439             *ftol = INEPSFTOL / 10.0;
1440             break;
1441         case BEST_RETRY:
1442             params->solver = gsl_multifit_nlinear_solver_qr;
1443             params->avmax = 0.5;
1444 
1445             *numIters = MAX_ITERATIONS_CURVE * 2.0;
1446             *xtol = INEPSXTOL;
1447             *gtol = INEPSGTOL;
1448             *ftol = INEPSFTOL;
1449             break;
1450         default:
1451             break;
1452     }
1453 }
1454 
1455 // Initialise parameters before starting the solver. Its important to start with a guess as near to the solution as possible
1456 // If we found a solution before and we're just adding more datapoints use the last solution as the guess.
1457 // If we don't have a solution use the datapoints to approximate. Work out the min and max points and use these
1458 // to find "close" values for the starting point parameters
1459 void CurveFitting::parMakeGuess(const int attempt, const QVector<double> inX, const QVector<double> inY,
1460                                 const OptimisationDirection optDir, gsl_vector * guess)
1461 {
1462     if (inX.size() < 1 || inX.size() != inY.size())
1463     {
1464         qCDebug(KSTARS_EKOS_FOCUS) << QString("Bad call to parMakeGuess: inX.size=%1, inY.size=%2").arg(inX.size()).arg(inY.size());
1465         return;
1466     }
1467 
1468     // If we are retrying then perturb the initial conditions. The hope is that by doing this the solver
1469     // will be nudged to find a solution this time
1470     double perturbation = 1.0 + pow(-1, attempt) * (attempt * 0.1);
1471 
1472     if (!m_FirstSolverRun && (m_LastCurveType == FOCUS_PARABOLA) && (m_LastCoefficients.size() == NUM_PARABOLA_PARAMS))
1473     {
1474         // Last run of the solver was a Parabola and that solution was good, so use that solution
1475         gsl_vector_set(guess, A_IDX, m_LastCoefficients[A_IDX] * perturbation);
1476         gsl_vector_set(guess, B_IDX, m_LastCoefficients[B_IDX] * perturbation);
1477         gsl_vector_set(guess, C_IDX, m_LastCoefficients[C_IDX] * perturbation);
1478     }
1479     else
1480     {
1481         double minX = inX[0];
1482         double minY = inY[0];
1483         double maxX = minX;
1484         double maxY = minY;
1485         for(int i = 0; i < inX.size(); i++)
1486         {
1487             if (inY[i] <= 0.0)
1488                 continue;
1489             if(minY <= 0.0 || inY[i] < minY)
1490             {
1491                 minX = inX[i];
1492                 minY = inY[i];
1493             }
1494             if(maxY <= 0.0 || inY[i] > maxY)
1495             {
1496                 maxX = inX[i];
1497                 maxY = inY[i];
1498             }
1499         }
1500         double A, B, C;
1501         if (optDir == OPTIMISATION_MAXIMISE)
1502         {
1503             // Equation y = f(x) = a + b((x - c) ^2)
1504             // For a maximum b < 0 and a > 0
1505             // At the maximum, Xmax = c, Ymax = a
1506             // Far from the maximum, b = (Ymin - a) / ((Xmin - c) ^2)
1507             A = maxY * perturbation;
1508             B = ((minY - maxY) / pow(minX - maxX, 2.0)) * perturbation;
1509             C = maxX * perturbation;
1510         }
1511         else
1512         {
1513             // For a minimum b > 0 and a > 0
1514             // At the minimum, Xmin = c, Ymin = a
1515             // Far from the minimum, b = (Ymax - a) / ((Xmax - c) ^2)
1516             A = minY * perturbation;
1517             B = ((maxY - minY) / pow(maxX - minX, 2.0)) * perturbation;
1518             C = minX * perturbation;
1519         }
1520         qCDebug(KSTARS_EKOS_FOCUS) << QString("LM solver (par) initial params: perturbation=%1, A=%2, B=%3, C=%4")
1521                                    .arg(perturbation).arg(A).arg(B).arg(C);
1522 
1523         gsl_vector_set(guess, A_IDX, A);
1524         gsl_vector_set(guess, B_IDX, B);
1525         gsl_vector_set(guess, C_IDX, C);
1526     }
1527 }
1528 
1529 QVector<double> CurveFitting::gaussian_fit(DataPoint3DT data, const StarParams &starParams)
1530 {
1531     QVector<double> vc;
1532 
1533     // Set the gsl error handler off as it aborts the program on error.
1534     auto const oldErrorHandler = gsl_set_error_handler_off();
1535 
1536     // Setup variables to be used by the solver
1537     gsl_multifit_nlinear_parameters params = gsl_multifit_nlinear_default_parameters();
1538     gsl_multifit_nlinear_workspace* w = gsl_multifit_nlinear_alloc (gsl_multifit_nlinear_trust, &params, data.dps.size(),
1539                                         NUM_GAUSSIAN_PARAMS);
1540     gsl_multifit_nlinear_fdf fdf;
1541     int numIters;
1542     double xtol, gtol, ftol;
1543 
1544     // Fill in function info
1545     fdf.f = gauFxy;
1546     fdf.df = gauJxy;
1547     fdf.fvv = gauFxyxy;
1548     fdf.n = data.dps.size();
1549     fdf.p = NUM_GAUSSIAN_PARAMS;
1550     fdf.params = &data;
1551 
1552     // Allocate the guess vector
1553     gsl_vector * guess = gsl_vector_alloc(NUM_GAUSSIAN_PARAMS);
1554     // Allocate weights vector
1555     auto weights = gsl_vector_alloc(data.dps.size());
1556 
1557     // Setup a timer to see how long the solve takes
1558     QElapsedTimer timer;
1559     timer.start();
1560 
1561     // Setup for multiple solve attempts. We won't worry too much if the solver fails as there should be
1562     // plenty of stars, but if the solver fails on its first step then adjust parameters and retry as this
1563     // is a fast thing to do.
1564     for (int attempt = 0; attempt < 5; attempt++)
1565     {
1566         // Make initial guesses
1567         gauMakeGuess(attempt, starParams, guess);
1568 
1569         // If using weights load up the GSL vector
1570         if (data.useWeights)
1571         {
1572             QVectorIterator<DataPT3D> dp(data.dps);
1573             int i = 0;
1574             while (dp.hasNext())
1575                 gsl_vector_set(weights, i++, dp.next().weight);
1576 
1577             gsl_multifit_nlinear_winit(guess, weights, &fdf, w);
1578         }
1579         else
1580             gsl_multifit_nlinear_init(guess, &fdf, w);
1581 
1582         // This is the callback used by the LM solver to allow some introspection of each iteration
1583         // Useful for debugging but clogs up the log
1584         // To activate, uncomment the callback lambda and change the call to gsl_multifit_nlinear_driver
1585         /*auto callback = [](const size_t iter, void* _params, const gsl_multifit_nlinear_workspace * w)
1586         {
1587             gsl_vector *f = gsl_multifit_nlinear_residual(w);
1588             gsl_vector *x = gsl_multifit_nlinear_position(w);
1589             double rcond;
1590 
1591             // compute reciprocal condition number of J(x)
1592             gsl_multifit_nlinear_rcond(&rcond, w);
1593 
1594             // ratio of accel component to velocity component
1595             double avratio = gsl_multifit_nlinear_avratio(w);
1596 
1597             qCDebug(KSTARS_EKOS_FOCUS) <<
1598                                    QString("iter %1: A = %2, B = %3, C = %4, D = %5, E = %6, F = %7, G = %8 "
1599                                            "rcond(J) = %9, avratio = %10, |f(x)| = %11")
1600                                    .arg(iter)
1601                                    .arg(gsl_vector_get(x, A_IDX))
1602                                    .arg(gsl_vector_get(x, B_IDX))
1603                                    .arg(gsl_vector_get(x, C_IDX))
1604                                    .arg(gsl_vector_get(x, D_IDX))
1605                                    .arg(gsl_vector_get(x, E_IDX))
1606                                    .arg(gsl_vector_get(x, F_IDX))
1607                                    .arg(gsl_vector_get(x, G_IDX))
1608                                    //.arg(1.0 / rcond)
1609                                    .arg(rcond)
1610                                    .arg(avratio)
1611                                    .arg(gsl_blas_dnrm2(f));
1612         };*/
1613 
1614         gauSetupParams(&params, &numIters, &xtol, &gtol, &ftol);
1615 
1616         qCDebug(KSTARS_EKOS_FOCUS) << QString("Starting LM solver, fit=gaussian, solver=%1, scale=%2, trs=%3, iters=%4, xtol=%5,"
1617                                               "gtol=%6, ftol=%7")
1618                                    .arg(params.solver->name).arg(params.scale->name).arg(params.trs->name).arg(numIters)
1619                                    .arg(xtol).arg(gtol).arg(ftol);
1620 
1621         int info = 0;
1622         int status = gsl_multifit_nlinear_driver(numIters, xtol, gtol, ftol, NULL, NULL, &info, w);
1623 
1624         if (status != 0)
1625         {
1626             qCDebug(KSTARS_EKOS_FOCUS) <<
1627                                        QString("LM solver (Gaussian): Failed after %1ms iters=%2 [attempt=%3] with status=%4 [%5] and info=%6 [%7]")
1628                                        .arg(timer.elapsed()).arg(gsl_multifit_nlinear_niter(w)).arg(attempt + 1).arg(status).arg(gsl_strerror(status))
1629                                        .arg(info).arg(gsl_strerror(info));
1630             if (status != GSL_EMAXITER || info != GSL_ENOPROG || gsl_multifit_nlinear_niter(w) > 1)
1631                 break;
1632         }
1633         else
1634         {
1635             // All good so store the results - parameters A, B, C, D, E, F, G
1636             auto solution = gsl_multifit_nlinear_position(w);
1637             for (int j = 0; j < NUM_GAUSSIAN_PARAMS; j++)
1638                 vc.push_back(gsl_vector_get(solution, j));
1639 
1640             qCDebug(KSTARS_EKOS_FOCUS) << QString("LM Solver (Gaussian): Solution found after %1ms %2 iters (%3). A=%4, B=%5, C=%6, "
1641                                                   "D=%7, E=%8, F=%9, G=%10").arg(timer.elapsed()).arg(gsl_multifit_nlinear_niter(w)).arg(getLMReasonCode(info))
1642                                        .arg(vc[A_IDX]).arg(vc[B_IDX]).arg(vc[C_IDX]).arg(vc[D_IDX]).arg(vc[E_IDX])
1643                                        .arg(vc[F_IDX]).arg(vc[G_IDX]);
1644             break;
1645         }
1646     }
1647 
1648     // Free GSL memory
1649     gsl_multifit_nlinear_free(w);
1650     gsl_vector_free(guess);
1651     gsl_vector_free(weights);
1652 
1653     // Restore old GSL error handler
1654     gsl_set_error_handler(oldErrorHandler);
1655 
1656     return vc;
1657 }
1658 
1659 // Initialise parameters before starting the solver. Its important to start with a guess as near to the solution as possible
1660 // Since we have already run some HFR calcs on the star, use these values to calculate the guess
1661 void CurveFitting::gauMakeGuess(const int attempt, const StarParams &starParams, gsl_vector * guess)
1662 {
1663     // If we are retrying then perturb the initial conditions. The hope is that by doing this the solver
1664     // will be nudged to find a solution this time
1665     double perturbation = 1.0 + pow(-1, attempt) * (attempt * 0.1);
1666 
1667     // Default from the input star details
1668     const double a  = std::max(starParams.peak, 0.0) * perturbation;       // Peak value
1669     const double x0 = std::max(starParams.centroid_x, 0.0) * perturbation; // Centroid x
1670     const double y0 = std::max(starParams.centroid_y, 0.0) * perturbation; // Centroid y
1671     const double b  = std::max(starParams.background, 0.0) * perturbation; // Background
1672 
1673     double A = 1.0, B = 0.0, C = 1.0;
1674     if (starParams.HFR > 0.0)
1675     {
1676         // Use 2*HFR value as FWHM along with theta to calc A, B, C
1677         // FWHM = 2.sqrt(2.ln(2)).sigma
1678         // Assume circular symmetry so B = 0
1679         const double sigma2 = pow(starParams.HFR / (sqrt(2.0 * log(2.0))), 2.0);
1680         const double costheta2 = pow(cos(starParams.theta), 2.0);
1681         const double sintheta2 = pow(sin(starParams.theta), 2.0);
1682 
1683         A = C = (costheta2 + sintheta2) / (2 * sigma2) * perturbation;
1684     }
1685 
1686     qCDebug(KSTARS_EKOS_FOCUS) <<
1687                                QString("LM Solver (Gaussian): Guess perturbation=%1, A=%2, B=%3, C=%4, D=%5, E=%6, F=%7, G=%8")
1688                                .arg(perturbation).arg(a).arg(x0).arg(y0).arg(A).arg(B).arg(C).arg(b);
1689 
1690     gsl_vector_set(guess, A_IDX, a);
1691     gsl_vector_set(guess, B_IDX, x0);
1692     gsl_vector_set(guess, C_IDX, y0);
1693     gsl_vector_set(guess, D_IDX, A);
1694     gsl_vector_set(guess, E_IDX, B);
1695     gsl_vector_set(guess, F_IDX, C);
1696     gsl_vector_set(guess, G_IDX, b);
1697 }
1698 
1699 // Setup the parameters for gaussian curve fitting
1700 void CurveFitting::gauSetupParams(gsl_multifit_nlinear_parameters *params, int *numIters, double *xtol, double *gtol,
1701                                   double *ftol)
1702 {
1703     // Trust region subproblem
1704     // - gsl_multifit_nlinear_trs_lm is the Levenberg-Marquardt algorithm without acceleration
1705     // - gsl_multifit_nlinear_trs_lmaccel is the Levenberg-Marquardt algorithm with acceleration
1706     // - gsl_multilarge_nlinear_trs_dogleg is the dogleg algorithm
1707     // - gsl_multifit_nlinear_trs_ddogleg is the double dogleg algorithm
1708     // - gsl_multifit_nlinear_trs_subspace2D is the 2D subspace algorithm
1709     params->trs = gsl_multifit_nlinear_trs_lmaccel;
1710 
1711     // avmax is the max allowed ratio of 2nd order term (acceleration, a) to the 1st order term (velocity, v)
1712     // GSL defaults to 0.75
1713     params->avmax = 0.75;
1714 
1715     // Scale
1716     // - gsl_multifit_nlinear_scale_more uses. More strategy. Good for problems with parameters with widely different scales
1717     // - gsl_multifit_nlinear_scale_levenberg. Levensberg strategy. Can be good but not scale invariant.
1718     // - gsl_multifit_nlinear_scale_marquardt. Marquardt strategy. Considered inferior to More and Levensberg
1719     params->scale = gsl_multifit_nlinear_scale_more;
1720 
1721     // Solver
1722     // - gsl_multifit_nlinear_solver_qr produces reliable results but needs more iterations than Cholesky
1723     // - gsl_multifit_nlinear_solver_cholesky fast but not as stable as QR
1724     // - gsl_multifit_nlinear_solver_mcholesky modified Cholesky more stable than Cholesky
1725     params->solver = gsl_multifit_nlinear_solver_qr;
1726 
1727     *numIters = MAX_ITERATIONS_STARS;
1728     *xtol = 1e-5;
1729     *gtol = INEPSGTOL;
1730     *ftol = 1e-5;
1731 }
1732 
1733 // Curve fit a 3D plane
1734 QVector<double> CurveFitting::plane_fit(const DataPoint3DT data)
1735 {
1736     QVector<double> vc;
1737 
1738     // Set the gsl error handler off as it aborts the program on error.
1739     auto const oldErrorHandler = gsl_set_error_handler_off();
1740 
1741     // Setup variables to be used by the solver
1742     gsl_multifit_nlinear_parameters params = gsl_multifit_nlinear_default_parameters();
1743     gsl_multifit_nlinear_workspace* w = gsl_multifit_nlinear_alloc (gsl_multifit_nlinear_trust, &params, data.dps.size(),
1744                                         NUM_PLANE_PARAMS);
1745     gsl_multifit_nlinear_fdf fdf;
1746     int numIters;
1747     double xtol, gtol, ftol;
1748 
1749     // Fill in function info
1750     fdf.f = plaFxy;
1751     fdf.df = plaJxy;
1752     fdf.fvv = plaFxyxy;
1753     fdf.n = data.dps.size();
1754     fdf.p = NUM_PLANE_PARAMS;
1755     fdf.params = (void *) &data;
1756 
1757     // Allocate the guess vector
1758     gsl_vector * guess = gsl_vector_alloc(NUM_PLANE_PARAMS);
1759     // Allocate weights vector
1760     auto weights = gsl_vector_alloc(data.dps.size());
1761 
1762     // Setup a timer to see how long the solve takes
1763     QElapsedTimer timer;
1764     timer.start();
1765 
1766     // Setup for multiple solve attempts.
1767     for (int attempt = 0; attempt < 5; attempt++)
1768     {
1769         // Make initial guesses
1770         plaMakeGuess(attempt, guess);
1771 
1772         // If using weights load up the GSL vector
1773         if (data.useWeights)
1774         {
1775             QVectorIterator<DataPT3D> dp(data.dps);
1776             int i = 0;
1777             while (dp.hasNext())
1778                 gsl_vector_set(weights, i++, dp.next().weight);
1779 
1780             gsl_multifit_nlinear_winit(guess, weights, &fdf, w);
1781         }
1782         else
1783             gsl_multifit_nlinear_init(guess, &fdf, w);
1784 
1785         // This is the callback used by the LM solver to allow some introspection of each iteration
1786         // Useful for debugging but clogs up the log
1787         // To activate, uncomment the callback lambda and change the call to gsl_multifit_nlinear_driver
1788         /*auto callback = [](const size_t iter, void* _params, const gsl_multifit_nlinear_workspace * w)
1789         {
1790             gsl_vector *f = gsl_multifit_nlinear_residual(w);
1791             gsl_vector *x = gsl_multifit_nlinear_position(w);
1792             double rcond;
1793 
1794             // compute reciprocal condition number of J(x)
1795             gsl_multifit_nlinear_rcond(&rcond, w);
1796 
1797             // ratio of accel component to velocity component
1798             double avratio = gsl_multifit_nlinear_avratio(w);
1799 
1800             qCDebug(KSTARS_EKOS_FOCUS) <<
1801                                    QString("iter %1: A = %2, B = %3, C = %4"
1802                                            "rcond(J) = %5, avratio = %6, |f(x)| = %7")
1803                                    .arg(iter)
1804                                    .arg(gsl_vector_get(x, A_IDX))
1805                                    .arg(gsl_vector_get(x, B_IDX))
1806                                    .arg(gsl_vector_get(x, C_IDX))
1807                                    //.arg(1.0 / rcond)
1808                                    .arg(rcond)
1809                                    .arg(avratio)
1810                                    .arg(gsl_blas_dnrm2(f));
1811         };*/
1812 
1813         plaSetupParams(&params, &numIters, &xtol, &gtol, &ftol);
1814 
1815         qCDebug(KSTARS_EKOS_FOCUS) << QString("Starting LM solver, fit=plane, solver=%1, scale=%2, trs=%3, iters=%4, xtol=%5,"
1816                                               "gtol=%6, ftol=%7")
1817                                    .arg(params.solver->name).arg(params.scale->name).arg(params.trs->name).arg(numIters)
1818                                    .arg(xtol).arg(gtol).arg(ftol);
1819 
1820         int info = 0;
1821         int status = gsl_multifit_nlinear_driver(numIters, xtol, gtol, ftol, NULL, NULL, &info, w);
1822 
1823         if (status != 0)
1824         {
1825             qCDebug(KSTARS_EKOS_FOCUS) <<
1826                                        QString("LM solver (Plane): Failed after %1ms iters=%2 [attempt=%3] with status=%4 [%5] and info=%6 [%7]")
1827                                        .arg(timer.elapsed()).arg(gsl_multifit_nlinear_niter(w)).arg(attempt + 1).arg(status).arg(gsl_strerror(status))
1828                                        .arg(info).arg(gsl_strerror(info));
1829             if (status != GSL_EMAXITER || info != GSL_ENOPROG || gsl_multifit_nlinear_niter(w) > 1)
1830                 break;
1831         }
1832         else
1833         {
1834             // All good so store the results - parameters A, B, C
1835             auto solution = gsl_multifit_nlinear_position(w);
1836             for (int j = 0; j < NUM_PLANE_PARAMS; j++)
1837                 vc.push_back(gsl_vector_get(solution, j));
1838 
1839             qCDebug(KSTARS_EKOS_FOCUS) << QString("LM Solver (Plane): Solution found after %1ms %2 iters (%3). A=%4, B=%5, C=%6")
1840                                        .arg(timer.elapsed()).arg(gsl_multifit_nlinear_niter(w)).arg(getLMReasonCode(info))
1841                                        .arg(vc[A_IDX]).arg(vc[B_IDX]).arg(vc[C_IDX]);
1842             break;
1843         }
1844     }
1845 
1846     // Free GSL memory
1847     gsl_multifit_nlinear_free(w);
1848     gsl_vector_free(guess);
1849     gsl_vector_free(weights);
1850 
1851     // Restore old GSL error handler
1852     gsl_set_error_handler(oldErrorHandler);
1853 
1854     return vc;
1855 }
1856 
1857 // Initialise parameters before starting the solver. Its important to start with a guess as near to the solution as possible
1858 void CurveFitting::plaMakeGuess(const int attempt, gsl_vector * guess)
1859 {
1860     double A, B, C;
1861     // If we are retrying then perturb the initial conditions. The hope is that by doing this the solver
1862     // will be nudged to find a solution this time
1863     const double perturbation = 1.0 + pow(-1, attempt) * (attempt * 0.1);
1864 
1865     if (!m_FirstSolverRun && (m_LastCurveType == FOCUS_PLANE) && (m_LastCoefficients.size() == NUM_PLANE_PARAMS) && attempt < 3)
1866     {
1867         // Last run of the solver was a Plane and that solution was good, so use that solution
1868         A = m_LastCoefficients[A_IDX] * perturbation;
1869         B = m_LastCoefficients[B_IDX] * perturbation;
1870         C = m_LastCoefficients[C_IDX] * perturbation;
1871     }
1872     else
1873     {
1874         A = perturbation;
1875         B = perturbation;
1876         C = perturbation;
1877     }
1878 
1879     qCDebug(KSTARS_EKOS_FOCUS) << QString("LM Solver (Plane): Guess perturbation=%1, A=%2, B=%3, C=%4")
1880                                .arg(perturbation).arg(A).arg(B).arg(C);
1881 
1882     gsl_vector_set(guess, A_IDX, A);
1883     gsl_vector_set(guess, B_IDX, B);
1884     gsl_vector_set(guess, C_IDX, C);
1885 }
1886 
1887 // Setup the parameters for plane curve fitting
1888 void CurveFitting::plaSetupParams(gsl_multifit_nlinear_parameters *params, int *numIters, double *xtol, double *gtol,
1889                                   double *ftol)
1890 {
1891     // Trust region subproblem
1892     // - gsl_multifit_nlinear_trs_lm is the Levenberg-Marquardt algorithm without acceleration
1893     // - gsl_multifit_nlinear_trs_lmaccel is the Levenberg-Marquardt algorithm with acceleration
1894     // - gsl_multilarge_nlinear_trs_dogleg is the dogleg algorithm
1895     // - gsl_multifit_nlinear_trs_ddogleg is the double dogleg algorithm
1896     // - gsl_multifit_nlinear_trs_subspace2D is the 2D subspace algorithm
1897     params->trs = gsl_multifit_nlinear_trs_lm;
1898 
1899     // avmax is the max allowed ratio of 2nd order term (acceleration, a) to the 1st order term (velocity, v)
1900     // GSL defaults to 0.75
1901     // params->avmax = 0.75;
1902 
1903     // Scale
1904     // - gsl_multifit_nlinear_scale_more uses. More strategy. Good for problems with parameters with widely different scales
1905     // - gsl_multifit_nlinear_scale_levenberg. Levensberg strategy. Can be good but not scale invariant.
1906     // - gsl_multifit_nlinear_scale_marquardt. Marquardt strategy. Considered inferior to More and Levensberg
1907     params->scale = gsl_multifit_nlinear_scale_more;
1908 
1909     // Solver
1910     // - gsl_multifit_nlinear_solver_qr produces reliable results but needs more iterations than Cholesky
1911     // - gsl_multifit_nlinear_solver_cholesky fast but not as stable as QR
1912     // - gsl_multifit_nlinear_solver_mcholesky modified Cholesky more stable than Cholesky
1913     params->solver = gsl_multifit_nlinear_solver_qr;
1914 
1915     *numIters = MAX_ITERATIONS_PLANE;
1916     *xtol = 1e-5;
1917     *gtol = INEPSGTOL;
1918     *ftol = 1e-5;
1919 }
1920 
1921 bool CurveFitting::findMinMax(double expected, double minPosition, double maxPosition, double *position, double *value,
1922                               CurveFit curveFit, const OptimisationDirection optDir)
1923 {
1924     bool foundFit;
1925     switch (curveFit)
1926     {
1927         case FOCUS_QUADRATIC :
1928             foundFit = minimumQuadratic(expected, minPosition, maxPosition, position, value);
1929             break;
1930         case FOCUS_HYPERBOLA :
1931             foundFit = minMaxHyperbola(expected, minPosition, maxPosition, position, value, optDir);
1932             break;
1933         case FOCUS_PARABOLA :
1934             foundFit = minMaxParabola(expected, minPosition, maxPosition, position, value, optDir);
1935             break;
1936         default :
1937             foundFit = false;
1938             break;
1939     }
1940     if (!foundFit)
1941         // If we couldn't fit a curve then something's wrong so reset coefficients which will force the next run of the LM solver to start from scratch
1942         m_LastCoefficients.clear();
1943     return foundFit;
1944 }
1945 
1946 bool CurveFitting::minimumQuadratic(double expected, double minPosition, double maxPosition, double *position,
1947                                     double *value)
1948 {
1949     int status;
1950     int iter = 0, max_iter = 100;
1951     const gsl_min_fminimizer_type *T;
1952     gsl_min_fminimizer *s;
1953     double m = expected;
1954 
1955     gsl_function F;
1956     F.function = &CurveFitting::curveFunction;
1957     F.params   = this;
1958 
1959     // Must turn off error handler or it aborts on error
1960     gsl_set_error_handler_off();
1961 
1962     T      = gsl_min_fminimizer_brent;
1963     s      = gsl_min_fminimizer_alloc(T);
1964     status = gsl_min_fminimizer_set(s, &F, expected, minPosition, maxPosition);
1965 
1966     if (status != GSL_SUCCESS)
1967     {
1968         qCWarning(KSTARS_EKOS_FOCUS) << "Focus GSL error:" << gsl_strerror(status);
1969         return false;
1970     }
1971 
1972     do
1973     {
1974         iter++;
1975         status = gsl_min_fminimizer_iterate(s);
1976 
1977         m = gsl_min_fminimizer_x_minimum(s);
1978         minPosition = gsl_min_fminimizer_x_lower(s);
1979         maxPosition = gsl_min_fminimizer_x_upper(s);
1980 
1981         status = gsl_min_test_interval(minPosition, maxPosition, 0.01, 0.0);
1982 
1983         if (status == GSL_SUCCESS)
1984         {
1985             *position = m;
1986             *value    = curveFunction(m, this);
1987         }
1988     }
1989     while (status == GSL_CONTINUE && iter < max_iter);
1990 
1991     gsl_min_fminimizer_free(s);
1992     return (status == GSL_SUCCESS);
1993 }
1994 
1995 bool CurveFitting::minMaxHyperbola(double expected, double minPosition, double maxPosition, double *position,
1996                                    double *value, const OptimisationDirection optDir)
1997 {
1998     Q_UNUSED(expected);
1999     if (m_coefficients.size() != NUM_HYPERBOLA_PARAMS)
2000     {
2001         if (m_coefficients.size() != 0)
2002             qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::minimumHyperbola coefficients.size()=%1").arg(
2003                                            m_coefficients.size());
2004         return false;
2005     }
2006 
2007     double b = m_coefficients[B_IDX];
2008     double c = m_coefficients[C_IDX];
2009     double d = m_coefficients[D_IDX];
2010 
2011     // We need to check that the solution found is in the correct form.
2012     // Check 1: The hyperbola minimum (=c) lies within the bounds of the focuser (and is > 0)
2013     // Check 2: At the minimum position (=c), the value of f(x) (which is the HFR) given by b+d is > 0
2014     // Check 3: For a minimum: we have a "u" shaped curve, not an "n" shape. b > 0.
2015     //                maximum: we have an "n" shaped curve, not a "U" shape. b < 0;
2016     if ((c >= minPosition) && (c <= maxPosition) && (b + d > 0.0) &&
2017             ((optDir == OPTIMISATION_MINIMISE && b > 0.0) || (optDir == OPTIMISATION_MAXIMISE && b < 0.0)))
2018     {
2019         *position = c;
2020         *value = b + d;
2021         return true;
2022     }
2023     else
2024         return false;
2025 }
2026 
2027 bool CurveFitting::minMaxParabola(double expected, double minPosition, double maxPosition, double *position,
2028                                   double *value, const OptimisationDirection optDir)
2029 {
2030     Q_UNUSED(expected);
2031     if (m_coefficients.size() != NUM_PARABOLA_PARAMS)
2032     {
2033         if (m_coefficients.size() != 0)
2034             qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::minimumParabola coefficients.size()=%1").arg(
2035                                            m_coefficients.size());
2036         return false;
2037     }
2038 
2039     double a = m_coefficients[A_IDX];
2040     double b = m_coefficients[B_IDX];
2041     double c = m_coefficients[C_IDX];
2042 
2043     // We need to check that the solution found is in the correct form.
2044     // Check 1: The parabola minimum (=c) lies within the bounds of the focuser (and is > 0)
2045     // Check 2: At the minimum position (=c), the value of f(x) (which is the HFR) given by a is > 0
2046     // Check 3: For a minimum: we have a "u" shaped curve, not an "n" shape. b > 0.
2047     //                maximum: we have an "n" shaped curve, not a "U" shape. b < 0;
2048     if ((c >= minPosition) && (c <= maxPosition) && (a > 0.0) &&
2049             ((optDir == OPTIMISATION_MINIMISE && b > 0.0) || (optDir == OPTIMISATION_MAXIMISE && b < 0.0)))
2050     {
2051         *position = c;
2052         *value = a;
2053         return true;
2054     }
2055     else
2056         return false;
2057 }
2058 
2059 bool CurveFitting::getStarParams(const CurveFit curveFit, StarParams *starParams)
2060 {
2061     bool foundFit;
2062     switch (curveFit)
2063     {
2064         case FOCUS_GAUSSIAN :
2065             foundFit = getGaussianParams(starParams);
2066             break;
2067         default :
2068             foundFit = false;
2069             break;
2070     }
2071     if (!foundFit)
2072         // If we couldn't fit a curve then something's wrong so reset coefficients which will force the next run of the LM solver to start from scratch
2073         m_LastCoefficients.clear();
2074     return foundFit;
2075 }
2076 
2077 // Having solved for a Gaussian return params
2078 bool CurveFitting::getGaussianParams(StarParams *starParams)
2079 {
2080     if (m_coefficients.size() != NUM_GAUSSIAN_PARAMS)
2081     {
2082         if (m_coefficients.size() != 0)
2083             qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::getGaussianParams coefficients.size()=%1").arg(
2084                                            m_coefficients.size());
2085         return false;
2086     }
2087 
2088     const double a  = m_coefficients[A_IDX];
2089     const double x0 = m_coefficients[B_IDX];
2090     const double y0 = m_coefficients[C_IDX];
2091     const double A  = m_coefficients[D_IDX];
2092     const double B  = m_coefficients[E_IDX];
2093     const double C  = m_coefficients[F_IDX];
2094     const double b  = m_coefficients[G_IDX];
2095 
2096     // Sanity check the coefficients in case the solver produced a bad solution
2097     if (a <= 0.0 || b <= 0.0 || x0 <= 0.0 || y0 <= 0.0)
2098     {
2099         qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::getGaussianParams a=%1 b=%2 x0=%3 y0=%4").arg(a).arg(b)
2100                                    .arg(x0).arg(y0);
2101         return false;
2102     }
2103 
2104     const double AmC = A - C;
2105     double theta;
2106     abs(AmC) < 1e-10 ? theta = 0.0 : theta = 0.5 * atan(2 * B / AmC);
2107     const double costheta = cos(theta);
2108     const double costheta2 = costheta * costheta;
2109     const double sintheta = sin(theta);
2110     const double sintheta2 = sintheta * sintheta;
2111 
2112     double sigmax2 = 0.5 / ((A * costheta2) + (2 * B * costheta * sintheta) + (C * sintheta2));
2113     double sigmay2 = 0.5 / ((A * costheta2) - (2 * B * costheta * sintheta) + (C * sintheta2));
2114 
2115     double FWHMx = 2 * pow(2 * log(2) * sigmax2, 0.5);
2116     double FWHMy = 2 * pow(2 * log(2) * sigmay2, 0.5);
2117     double FWHM  = (FWHMx + FWHMy) / 2.0;
2118 
2119     if (isnan(FWHM) || FWHM < 0.0)
2120     {
2121         qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::getGaussianParams FWHM=%1").arg(FWHM);
2122         return false;
2123     }
2124 
2125     starParams->background = b;
2126     starParams->peak = a;
2127     starParams->centroid_x = x0;
2128     starParams->centroid_y = y0;
2129     starParams->theta = theta;
2130     starParams->FWHMx = FWHMx;
2131     starParams->FWHMy = FWHMy;
2132     starParams->FWHM = FWHM;
2133 
2134     return true;
2135 }
2136 
2137 // R2 (R squared) is the coefficient of determination gives a measure of how well the curve fits the datapoints.
2138 // It lies between 0 and 1. 1 means that all datapoints will lie on the curve which therefore exactly describes the
2139 // datapoints. 0 means that there is no correlation between the curve and the datapoints.
2140 // See www.wikipedia.org/wiki/Coefficient_of_determination for more details.
2141 double CurveFitting::calculateR2(CurveFit curveFit)
2142 {
2143     double R2 = 0.0;
2144     QVector<double> dataPoints, curvePoints;
2145     int i;
2146 
2147     switch (curveFit)
2148     {
2149         case FOCUS_QUADRATIC :
2150             // Not implemented
2151             qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateR2 called for Quadratic");
2152             break;
2153 
2154         case FOCUS_HYPERBOLA :
2155             // Calculate R2 for the hyperbola
2156             if (m_coefficients.size() != NUM_HYPERBOLA_PARAMS)
2157             {
2158                 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateR2 hyperbola coefficients.size()=").arg(
2159                                                m_coefficients.size());
2160                 break;
2161             }
2162 
2163             for (i = 0; i < m_y.size(); i++)
2164                 // Load up the curvePoints vector
2165                 curvePoints.push_back(hypfx(m_x[i], m_coefficients[A_IDX], m_coefficients[B_IDX], m_coefficients[C_IDX],
2166                                             m_coefficients[D_IDX]));
2167             // Do the actual R2 calculation
2168             R2 = calcR2(m_y, curvePoints, m_scale, m_useWeights);
2169             break;
2170 
2171         case FOCUS_PARABOLA :
2172             // Calculate R2 for the parabola
2173             if (m_coefficients.size() != NUM_PARABOLA_PARAMS)
2174             {
2175                 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateR2 parabola coefficients.size()=").arg(
2176                                                m_coefficients.size());
2177                 break;
2178             }
2179 
2180             for (i = 0; i < m_y.size(); i++)
2181                 // Load up the curvePoints vector
2182                 curvePoints.push_back(parfx(m_x[i], m_coefficients[A_IDX], m_coefficients[B_IDX], m_coefficients[C_IDX]));
2183 
2184             // Do the actual R2 calculation
2185             R2 = calcR2(m_y, curvePoints, m_scale, m_useWeights);
2186             break;
2187 
2188         case FOCUS_GAUSSIAN :
2189             // Calculate R2 for the gaussian
2190             if (m_coefficients.size() != NUM_GAUSSIAN_PARAMS)
2191             {
2192                 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateR2 gaussian coefficients.size()=").arg(
2193                                                m_coefficients.size());
2194                 break;
2195             }
2196 
2197             for (i = 0; i < m_dataPoints.dps.size(); i++)
2198             {
2199                 // Load up the curvePoints vector
2200                 curvePoints.push_back(gaufxy(m_dataPoints.dps[i].x, m_dataPoints.dps[i].y, m_coefficients[A_IDX],
2201                                              m_coefficients[B_IDX], m_coefficients[C_IDX], m_coefficients[D_IDX],
2202                                              m_coefficients[E_IDX], m_coefficients[F_IDX], m_coefficients[G_IDX]));
2203                 dataPoints.push_back(static_cast <double> (m_dataPoints.dps[i].z));
2204             }
2205 
2206             // Do the actual R2 calculation
2207             R2 = calcR2(dataPoints, curvePoints, m_scale, m_dataPoints.useWeights);
2208             break;
2209 
2210         case FOCUS_PLANE :
2211             // Calculate R2 for the plane
2212             if (m_coefficients.size() != NUM_PLANE_PARAMS)
2213             {
2214                 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateR2 plane coefficients.size()=").arg(
2215                                                m_coefficients.size());
2216                 break;
2217             }
2218 
2219             for (i = 0; i < m_dataPoints.dps.size(); i++)
2220             {
2221                 // Load up the curvePoints vector
2222                 curvePoints.push_back(plafxy(m_dataPoints.dps[i].x, m_dataPoints.dps[i].y, m_coefficients[A_IDX],
2223                                              m_coefficients[B_IDX], m_coefficients[C_IDX]));
2224                 dataPoints.push_back(static_cast <double> (m_dataPoints.dps[i].z));
2225             }
2226 
2227             // Do the actual R2 calculation
2228             R2 = calcR2(dataPoints, curvePoints, m_scale, m_dataPoints.useWeights);
2229             break;
2230 
2231         default :
2232             qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateR2 curveFit=%1").arg(curveFit);
2233             break;
2234     }
2235     // R2 for linear function range 0<=R2<=1. For non-linear functions it is possible
2236     // for R2 to be negative. This doesn't really mean anything so force 0 in these situations.
2237     return std::max(R2, 0.0);
2238 }
2239 
2240 // Do the maths to calculate R2 - how well the curve fits the datapoints
2241 double CurveFitting::calcR2(const QVector<double> dataPoints, const QVector<double> curvePoints,
2242                             const QVector<double> scale, const bool useWeights)
2243 {
2244     double R2 = 0.0, chisq = 0.0, sum = 0.0, totalSumSquares = 0.0, weight, average;
2245 
2246     for (int i = 0; i < dataPoints.size(); i++)
2247     {
2248         sum += dataPoints[i];
2249         useWeights ? weight = scale[i] : weight = 1.0;
2250         chisq += weight * pow((dataPoints[i] - curvePoints[i]), 2.0);
2251     }
2252     average = sum / dataPoints.size();
2253 
2254     for (int i = 0; i < dataPoints.size(); i++)
2255     {
2256         useWeights ? weight = scale[i] : weight = 1.0;
2257         totalSumSquares += weight * pow((dataPoints[i] - average), 2.0);
2258     }
2259 
2260     if (totalSumSquares > 0.0)
2261         R2 = 1 - (chisq / totalSumSquares);
2262     else
2263     {
2264         R2 = 0.0;
2265         qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calcR2 tss=%1").arg(totalSumSquares);
2266     }
2267 
2268     // R2 for linear function range 0<=R2<=1. For non-linear functions it is possible
2269     // for R2 to be negative. This doesn't really mean anything so force 0 in these situations.
2270     return std::max(R2, 0.0);
2271 }
2272 
2273 void CurveFitting::calculateCurveDeltas(CurveFit curveFit, std::vector<std::pair<int, double>> &curveDeltas)
2274 {
2275     curveDeltas.clear();
2276 
2277     switch (curveFit)
2278     {
2279         case FOCUS_QUADRATIC :
2280             // Not implemented
2281             qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateCurveDeltas called for Quadratic");
2282             break;
2283 
2284         case FOCUS_HYPERBOLA :
2285             if (m_coefficients.size() != NUM_HYPERBOLA_PARAMS)
2286             {
2287                 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateCurveDeltas hyperbola coefficients.size()=").arg(
2288                                                m_coefficients.size());
2289                 break;
2290             }
2291 
2292             for (int i = 0; i < m_y.size(); i++)
2293                 curveDeltas.push_back(std::make_pair(i, abs(m_y[i] - hypfx(m_x[i], m_coefficients[A_IDX], m_coefficients[B_IDX],
2294                                                      m_coefficients[C_IDX],
2295                                                      m_coefficients[D_IDX]))));
2296             break;
2297 
2298         case FOCUS_PARABOLA :
2299             if (m_coefficients.size() != NUM_PARABOLA_PARAMS)
2300             {
2301                 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateCurveDeltas parabola coefficients.size()=").arg(
2302                                                m_coefficients.size());
2303                 break;
2304             }
2305 
2306             for (int i = 0; i < m_y.size(); i++)
2307                 curveDeltas.push_back(std::make_pair(i, abs(m_y[i] - parfx(m_x[i], m_coefficients[A_IDX], m_coefficients[B_IDX],
2308                                                      m_coefficients[C_IDX]))));
2309             break;
2310 
2311         case FOCUS_GAUSSIAN :
2312             // Not implemented
2313             qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateCurveDeltas called for Gaussian");
2314             break;
2315 
2316         default :
2317             qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateCurveDeltas curveFit=%1").arg(curveFit);
2318             break;
2319     }
2320 }
2321 
2322 namespace
2323 {
2324 QString serializeDoubleVector(const QVector<double> &vector)
2325 {
2326     QString str = QString("%1").arg(vector.size());
2327     for (const double v : vector)
2328         str.append(QString(";%1").arg(v, 0, 'g', 12));
2329     return str;
2330 }
2331 
2332 bool decodeDoubleVector(const QString serialized, QVector<double> *vector)
2333 {
2334     vector->clear();
2335     const QStringList parts = serialized.split(';');
2336     if (parts.size() == 0) return false;
2337     bool ok;
2338     int size = parts[0].toInt(&ok);
2339     if (!ok || (size != parts.size() - 1)) return false;
2340     for (int i = 0; i < size; ++i)
2341     {
2342         const double val = parts[i + 1].toDouble(&ok);
2343         if (!ok) return false;
2344         vector->append(val);
2345     }
2346     return true;
2347 }
2348 }
2349 
2350 QString CurveFitting::serialize() const
2351 {
2352     QString serialized = "";
2353     if (m_FirstSolverRun) return serialized;
2354     serialized = QString("%1").arg(int(m_CurveType));
2355     serialized.append(QString("|%1").arg(serializeDoubleVector(m_x)));
2356     serialized.append(QString("|%1").arg(serializeDoubleVector(m_y)));
2357     serialized.append(QString("|%1").arg(serializeDoubleVector(m_scale)));
2358     serialized.append(QString("|%1").arg(m_useWeights ? "T" : "F"));
2359     // m_dataPoints not implemented. Not needed for graphing.
2360     serialized.append(QString("|m_dataPoints not implemented"));
2361     serialized.append(QString("|%1").arg(serializeDoubleVector(m_coefficients)));
2362     serialized.append(QString("|%1").arg(int(m_LastCurveType)));
2363     serialized.append(QString("|%1").arg(serializeDoubleVector(m_LastCoefficients)));
2364     return serialized;
2365 }
2366 
2367 bool CurveFitting::recreateFromQString(const QString &serialized)
2368 {
2369     QStringList parts = serialized.split('|');
2370     bool ok = false;
2371     if (parts.size() != 9) return false;
2372 
2373     int val = parts[0].toInt(&ok);
2374     if (!ok) return false;
2375     m_CurveType = static_cast<CurveFit>(val);
2376 
2377     if (!decodeDoubleVector(parts[1], &m_x)) return false;
2378     if (!decodeDoubleVector(parts[2], &m_y)) return false;
2379     if (!decodeDoubleVector(parts[3], &m_scale)) return false;
2380 
2381     if (parts[4] == "T") m_useWeights = true;
2382     else if (parts[4] == "F") m_useWeights = false;
2383     else return false;
2384 
2385     // parts[5]: m_dataPoints not implemented.
2386 
2387     if (!decodeDoubleVector(parts[6], &m_coefficients)) return false;
2388 
2389     val = parts[7].toInt(&ok);
2390     if (!ok) return false;
2391     m_LastCurveType = static_cast<CurveFit>(val);
2392 
2393     if (!decodeDoubleVector(parts[8], &m_LastCoefficients)) return false;
2394 
2395     m_FirstSolverRun = false;
2396     return true;
2397 }
2398 
2399 }  // namespace