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, ¶ms, 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, ¶ms, &numIters, &xtol, >ol, &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, ¶ms, 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, ¶ms, &numIters, &xtol, >ol, &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, ¶ms, 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(¶ms, &numIters, &xtol, >ol, &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, ¶ms, 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(¶ms, &numIters, &xtol, >ol, &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