Kstars

curvefit.cpp
1#include "curvefit.h"
2#include "ekos/ekos.h"
3#include <ekos_focus_debug.h>
4
5// Constants used to identify the number of parameters used for different curve types
6constexpr int NUM_HYPERBOLA_PARAMS = 4;
7constexpr int NUM_PARABOLA_PARAMS = 3;
8constexpr int NUM_GAUSSIAN_PARAMS = 7;
9constexpr int NUM_PLANE_PARAMS = 3;
10// Parameters for the solver
11// MAX_ITERATIONS is used to limit the number of iterations for the solver.
12// A value needs to be entered that allows a solution to be found without iterating unnecessarily
13// There is a relationship with the tolerance parameters that follow.
14constexpr int MAX_ITERATIONS_CURVE = 5000;
15constexpr int MAX_ITERATIONS_STARS = 1000;
16constexpr int MAX_ITERATIONS_PLANE = 5000;
17// The next 3 parameters are used as tolerance for convergence
18// convergence is achieved if for each datapoint i
19// dx_i < INEPSABS + (INEPSREL * x_i)
20constexpr double INEPSXTOL = 1e-5;
21const double INEPSGTOL = pow(GSL_DBL_EPSILON, 1.0 / 3.0);
22constexpr double INEPSFTOL = 1e-5;
23
24// The functions here fit a number of different curves to the incoming data points using the Lehvensberg-Marquart
25// solver with geodesic acceleration as provided the Gnu Science Library (GSL). The following sources of information are useful:
26// www.wikipedia.org/wiki/Levenberg–Marquardt_algorithm - overview of the mathematics behind the algo
27// www.gnu.org/software/gsl/doc/html/nls.html - GSL manual describing Nonlinear Least-Squares fitting.
28//
29// Levensberg-Marquart (LM)
30// ------------------------
31// The Levensberg-Marquart algorithm is a non-linear least-squares solver and thus suitable for mnay
32// different equations. The basic is idea is to adjust the equation y = f(x,P) so that the computed
33// y values are as close as possible to the y values of the datapoints provided, so that the resultant
34// curve fits the data as best as it can. P is a set of parameters that are varied by the solver in order
35// to find the best fit. The solver measures how far away the curve is at each data point, squares the
36// result and adds them all up. This is the number to be minimised, lets call is S.
37//
38// The solver is supplied with an initial guess for the parameters, P. It calculates S, makes an adjustment
39// P and calculates a new S1. Provided S1 < S the solver has moved in the right direction and reduced S.
40// It iterates through this procedure until S1 - S is:
41// 1. less than a supplied limit (convergence has been reached), or
42// 2. The maximum number of iterations has been reached, or
43// 3. The solver encountered an error.
44//
45// The solver is capable of solving either an unweighted or weighted set of datapoints. In essence, an
46// unweighted set of data gives equal weight to each datapoint when trying to fit a curve. An alternative is to
47// weight each datapoint with a measure that corresponds to how accurate the measurement of the datapoint
48// actually is. Given we are calculating HFRs of stars we can calculate the variance (standard deviation
49// squared) in the measurement of HFR and use 1 / SD^2 as weighting. What this means is that if we have a
50// datapoint where the SD in HFR measurements is small we would give this datapoint a relatively high
51// weighting when fitting the curve, and if there was a datapoint with a higher SD in HFR measurements
52// it would receive a lower weighting when trying to fit curve to that point.
53//
54// There are several optimisations to LM that speed up convergence for certain types of equations. This
55// code uses the LM solver with geodesic acceleration; a technique to accelerate convergence by using the
56// second derivative of the fitted function. An optimisation that has been implemented is that since, in normal
57// operation, the solver is run multiple times with the same or similar parameters, the initial guess for
58// a solver run is set the solution from the previous run.
59//
60// The documents referenced provide the maths of the LM algorithm. What is required is the function to be
61// used f(x) and the derivative of f(x) with respect to the parameters of f(x). This partial derivative forms
62// a matrix called the Jacobian, J(x). LM then finds a minimum. Mathematically it will find a minimum local
63// to the initial guess, but not necessarily the global minimum but for the equations used this is not a
64// problem under normal operation as there will only be 1 minimum. If the data is poor, however, the
65// solver may find an "n" solution rather than a "u" especially at the start of the process where there
66// are a small number of points and the error in measurement is high. We'll ignore and continue and this
67// should correct itself.
68//
69// The original algorithm used in Ekos is a quadratic equation, which represents a parabolic curve. In order
70// not to disturb historic code the original solution will be called Quadratic. It uses a linear least-squares
71// model, not LM. The LM solver has been applied to a hyperbolic and a parabolic curve.
72//
73// In addition, the solver has been applied to:
74// 1. 3D Gaussian in order to fit star profiles and calculate FWHM.
75// 2. 3D Plane surface used by Aberration Inspector.
76//
77// Hyperbola
78// ---------
79// Equation y = f(x) = b * sqrt(1 + ((x - c) / a) ^2) + d
80// This can be re-written as:
81//
82// f(x) = b * phi + d
83// where phi = sqrt(1 + ((x - c) / a) ^2)
84//
85// Jacobian J = {df/da, df/db, df/dc, df/db}
86// df/da = b * (1 / 2) * (1 / phi) * -2 * ((x - c) ^2) / (a ^3)
87// = -b * ((x - c) ^2) / ((a ^ 3) * phi)
88// df/db = phi
89// df/dc = b * (1 / 2) * (1 / phi) * 2 * (x - c) / (a ^2) * (-1)
90// = -b * (x - c) / ((a ^2) * phi)
91// df/dd = 1
92//
93// Second directional derivative:
94// {
95// {ddf/dada,ddf/dadb,ddf/dadc,ddf/dadd},
96// {ddf/dbda,ddf/dbdb,ddf/dbdc,ddf/dbdd},
97// {ddf/dcda,ddf/dcdb,ddf/dcdc,ddf/dcdd},
98// {ddf/ddda,ddf/dddb,ddf/dddc,ddf/dddd}
99// }
100// Note the matrix is symmetric, as ddf/dxdy = ddf/dydx.
101//
102// ddf/dada = (b*(c-x)^2*(3*a^2+a*(c-x)^2))/(a^4*(a^2 + (c-x)^2)^3/2))
103// ddf/dadb = -((x - c) ^2) / ((a ^ 3) * phi)
104// ddf/dadc = -(b*(c-x)*(2a^2+(c-x)^2))/(a^3*(a^2 + (c-x)^2)^3/2))
105// ddf/dadd = 0
106// ddf/dbdb = 0
107// ddf/dbdc = -(x-c)/(a^2*phi)
108// ddf/dbdd = 0
109// ddf/dcdc = b/((a^2+(c-x)^2) * phi)
110// ddf/dcdd = 0
111// ddf/dddd = 0
112//
113// For a valid solution:
114// 1. c must be > 0 and within the range of travel of the focuser.
115// 2. b must be > 0 for a V shaped curve (b < 0 for an inverted V and b = 0 for a horizontal line).
116// 3. b + d > 0. The minimum solution when x = c gives f(x) = b + d which must be > 0.
117//
118// Parabola
119// --------
120// Equation y = f(x) = a + b((x - c) ^2)
121// Jacobian J = {df/da, df/db, df/dc}
122// df/da = 1
123// df/db = (x - c) ^2
124// df/dc = -2 * b * (x - c)
125//
126// For a valid solution:
127// 1. c must be > 0 and within the range of travel of the focuser.
128// 2. b must be > 0 for a V shaped curve (b < 0 for an inverted V and b = 0 for a horizontal line).
129// 3. a > 0. The minimum solution when x = c gives f(x) = a which must be > 0.
130//
131// Gaussian
132// --------
133// Generalised equation for a 2D gaussian.
134// Equation z = f(x,y) = b + a.exp-(A((x-x0)^2) + 2B(x-x0)(y-y0) + 2C((y-y0)^2))
135//
136// with parameters
137// b = background
138// a = Peak Intensity
139// x0 = Star centre in x dimension
140// y0 = Star centre in y dimension
141// A, B, C = relate to the shape of the elliptical gaussian and the orientation of the major and
142// minor ellipse axes wrt x and y
143//
144// Jacobian J = {df/db, df/da, df/dx0, df/dy0, df/dA, df/dB, df/dC}
145// Let phi = exp-(A((x-x0)^2) + 2B(x-x0)(y-y0) + C((y-y0)^2))
146// df/db = 1
147// df/da = phi
148// df/dx0 = [2A(x-x0)+2B(y-y0)].a.phi
149// df/dy0 = [2B(x-x0)+2C(y-y0)].a.phi
150// df/dA = -(x-x0)^2.a.phi
151// df/DB = -2(x-x0)(y-y0).a.phi
152// df/dC = -(y-y0)^2.a.phi
153//
154// 2nd derivatives
155// {
156// {ddf/dbdb,ddf/dbda,ddf/dbdx0,ddf/dbdy0,ddf/dbdA,ddf/dbdB,ddf/dbdC},
157// {ddf/dadb,ddf/dada,ddf/dadx0,ddf/dady0,ddf/dadA,ddf/dadB,ddf/dadC},
158// {ddf/dx0db,ddf/dx0da,ddf/dx0dx0,ddf/dx0dy0,ddf/dx0dA,ddf/dx0dB,ddf/dx0dC},
159// {ddf/dy0db,ddf/dy0da,ddf/dy0dx0,ddf/dy0dy0,ddf/dy0dA,ddf/dy0dB,ddf/dy0dC},
160// {ddf/dAdb,ddf/dAda,ddf/dAdx0,ddf/dAdy0,ddf/dAdA,ddf/dAdB,ddf/dAdC},
161// {ddf/dBdb,ddf/dBda,ddf/dBdx0,ddf/dBdy0,ddf/dBdA,ddf/dBdB,ddf/dBdC},
162// {ddf/dCdb,ddf/dCda,ddf/dCdx0,ddf/dCdy0,ddf/dCdA,ddf/dCdB,ddf/dCdC},
163// }
164// The matrix is symmetric, as ddf/dParm1dParm2 = ddf/dParm2dParm1 so we only need to differentiate
165// one half (above or below the diagonal) of the matrix elements.
166// ddf/dbdb = 0
167// ddf/dbda = 0
168// ddf/dbdx0 = 0
169// ddf/dbdy0 = 0
170// ddf/dbdA = 0
171// ddf/dbdB = 0
172// ddf/dbdC = 0
173//
174// ddf/dada = 0
175// ddf/dadx0 = [2A(x-x0) + 2B(y-y0)].phi
176// ddf/dady0 = [2B(x-x0) + 2C(y-y0)].phi
177// ddf/dadA = -(x-x0)^2.phi
178// ddf/dadB = -2(x-x0)(y-y0).phi
179// ddf/dadC = -Cy-y0)^2.phi
180//
181// ddf/dxodx0 = -2A.a.phi + [2A(x-x0)+2B(y-y0)]^2.a.phi
182// ddf/dx0dy0 = -2B.a.phi - [2A(x-x0)+2B(y-y0)].[2B(x-x0)+2C(y-y0)].a.phi
183// ddf/dx0dA = 2(x-x0).a.phi - [2A(x-x0)+2B(y-y0)].(x-x0)^2.a.phi
184// ddf/dx0dB = 2(y-y0).a.phi - [2A(x-x0)+2B(y-y0)].2(x-x0).(y-y0).a.phi
185// ddf/dx0dC = -[2A(x-x0)+2B(y-y0)].2(y-y0)^2.a.phi
186//
187// ddf/dy0dy0 = -2c.a.phi + [2B(x-x0)+2C(y-y0)]^2.a.phi
188// ddf/dy0dA = -[2B(x-x0)+2C(y-y0)].(x-x0)^2.a.phi
189// ddf/dy0dB = 2(x-x0).a.phi - [2B(x-x0)+2C(y-y0)].2(x-x0)(y-y0).a.phi
190// ddf/dy0dC = 2(y-y0).a.phi - [2B(x-x0)+2C(y-y0)].(y-y0)^2.a.phi
191//
192// ddf/dAdA = (x-x0)^4.a.phi
193// ddf/dAdB = 2(x-x0)^3.(y-y0).a.phi
194// ddf/dAdC = (x-x0)^2.(y-y0)^2.a.phi
195//
196// ddf/dBdB = 4(x-x0)^2.(y-y0)^2.a.phi
197// ddf/dBdC = 2(x-x0).(y-y0)^3.a.phi
198//
199// ddf/dCdC = (y-y0)^4.a.phi
200//
201// 3D Plane
202// Generalised equation for a 3D plane going through the origin.
203// Equation z = f(x,y) = -(A.x + B.y) / C
204// Jacobian J = {df/dA, df/dB, df/dC}
205// df/dA = -x / C
206// df/dB = -y / C
207// df/dC = (A.x + B.y) / C^2
208//
209// 2nd derivatives
210// {ddf/dAdA,ddf/dAdB,ddf/dAdC}
211// {ddf/dBdA,ddf/dBdB,ddf/dBdC}
212// {ddf/dCdA,ddf/dCdB,ddf/dCdC}
213//
214// The matrix is symmetric, as ddf/dParm1dParm2 = ddf/dParm2dParm1 so we only need to differentiate
215// one half (above or below the diagonal) of the matrix elements.
216//
217// ddf/dAdA = 0
218// ddf/dAdB = 0
219// ddf/dAdC = x / C^2
220//
221// ddf/dBdB = 0
222// ddf/dBdC = y / C^2
223//
224// ddf/dCdC = -2.(A.x + B.y) / C^3
225//
226//
227// Convergence
228// -----------
229// There following are key parameters that drive the LM solver. Currently these settings are coded into
230// the program. It would be better to store them in a configuration file somewhere, but they aren't the
231// sort of parameters users should have ready access to.
232// MAX_ITERATIONS - This is how many iterations to do before aborting. The game here is to set a
233// reasonable number to allow for convergence. In my testing with good data and a
234// good start point <10 iterations are required. With good data and a poor start point
235// <200 iterations are required. With poor data and a tight "tolerance" convergence
236// may need >1000 iterations. Setting to 500 seems a good compromise.
237// tolerance - Conceptually tolerance could be done in 2 ways:
238// - check the gradient, would be 0 at the curve minimum
239// - check the residuals (and their gradient), will minimise at the curve minimum
240// Currently we will check on residuals.
241// This is supported by 2 parameters INEPSABS and INEPSREL.
242// convergence is achieved if for each datapoint i, where:
243// dx_i < INEPSABS + (INEPSREL * x_i)
244// Setting a slack tolerance will mean a range of x (focus positions) that are deemed
245// valid solutions. Not good.
246// Setting a tighter tolerance will require more iterations to solve, but setting too
247// tight a tolerance is just wasting time if it doesn't improve the focus position, and
248// if too tight the solver may not be able to find the solution, so a balance needs to
249// be struck. For now lets just make these values constant and see where this gets to.
250// If this turns out not to work for some equipment, the next step would be to adjust
251// these parameters based on the equipment profile, or to adapt the parameters starting
252// with a loose tolerance and tightening as the curve gets nearer to a complete solution.
253// If we inadvertently overtighten the tolerance and fail to converge, the tolerance
254// could be slackened or more iterations used.
255// I have found that the following work well on my equipment and the simulator
256// for both hyperbola and parabola. The advice in the GSL documentation is to start
257// with an absolute tolerance of 10^-d where d is the number of digits required in the
258// solution precision of x (focuser position). The gradient tolerance starting point is
259// (machine precision)^1/3 which we're using a relative tolerance.
260// INEPSABS = 1e-5
261// INEPSREL = GSL_DBL_EPSILON ^ 1/3
262
263namespace Ekos
264{
265
266namespace
267{
268// Constants used to index m_coefficient arrays
269enum { A_IDX = 0, B_IDX, C_IDX, D_IDX, E_IDX, F_IDX, G_IDX };
270
271// hypPhi() is a repeating part of the function calculations for Hyperbolas.
272double hypPhi(double x, double a, double c)
273{
274 return sqrt(1.0 + pow(((x - c) / a), 2.0));
275}
276
277// Function to calculate f(x) for a hyperbola
278// y = b * hypPhi(x, a, c) + d
279double hypfx(double x, double a, double b, double c, double d)
280{
281 return b * hypPhi(x, a, c) + d;
282}
283
284// Function to calculate x for the passed in y and hyperbola parameters
285// y = b * sqrt(1 + ((x - c) / a) ^ 2) + d
286// ((y - d) / b) ^ 2 - 1 = ((x - c) / a) ^ 2
287// x = c + a.sqrt(((y - d) / b) ^ 2 - 1)
288// Note the larger x solution is returned
289double hypfy(double y, double a, double b, double c, double d)
290{
291 return c + a * sqrt(std::pow(((y - d) / b), 2.0) - 1);
292}
293
294// Calculates F(x) for each data point on the hyperbola
295int hypFx(const gsl_vector * X, void * inParams, gsl_vector * outResultVec)
296{
297 CurveFitting::DataPointT * DataPoints = ((CurveFitting::DataPointT *)inParams);
298
299 double a = gsl_vector_get (X, A_IDX);
300 double b = gsl_vector_get (X, B_IDX);
301 double c = gsl_vector_get (X, C_IDX);
302 double d = gsl_vector_get (X, D_IDX);
303
304 for(int i = 0; i < DataPoints->dps.size(); ++i)
305 {
306 // Hyperbola equation
307 double yi = hypfx(DataPoints->dps[i].x, a, b, c, d);
308
309 gsl_vector_set(outResultVec, i, (yi - DataPoints->dps[i].y));
310 }
311
312 return GSL_SUCCESS;
313}
314
315// Calculates the Jacobian (derivative) matrix for the hyperbola
316int hypJx(const gsl_vector * X, void * inParams, gsl_matrix * J)
317{
318 CurveFitting::DataPointT * DataPoints = ((struct CurveFitting::DataPointT *)inParams);
319
320 // Store current coefficients
321 double a = gsl_vector_get(X, A_IDX);
322 double b = gsl_vector_get(X, B_IDX);
323 double c = gsl_vector_get(X, C_IDX);
324
325 // Store non-changing calculations
326 const double a2 = a * a;
327 const double a3 = a * a2;
328
329 for(int i = 0; i < DataPoints->dps.size(); ++i)
330 {
331 // Calculate the Jacobian Matrix
332 const double x = DataPoints->dps[i].x;
333 const double x_minus_c = x - c;
334
335 gsl_matrix_set(J, i, A_IDX, -b * (x_minus_c * x_minus_c) / (a3 * hypPhi(x, a, c)));
336 gsl_matrix_set(J, i, B_IDX, hypPhi(x, a, c));
337 gsl_matrix_set(J, i, C_IDX, -b * x_minus_c / (a2 * hypPhi(x, a, c)));
338 gsl_matrix_set(J, i, D_IDX, 1);
339 }
340
341 return GSL_SUCCESS;
342}
343
344
345// ddf/dada = (b*(c-x)^2*(3*a^2+a*(c-x)^2))/(a^4*(a^2 + (c-x)^2)*phi)
346// ddf/dadb = -((x - c) ^2) / ((a ^ 3) * phi)
347// ddf/dadc = -(b*(c-x)*(2a^2+(c-x)^2))/(a^3*(a^2 + (c-x)^2)*phi))
348// ddf/dadd = 0
349// ddf/dbdb = 0
350// ddf/dbdc = -(x-c)/(a^2*phi)
351// ddf/dbdd = 0
352// ddf/dcdc = b/((a^2+(c-x)^2) * phi)
353// ddf/dcdd = 0
354// ddf/dddd = 0
355
356// ddf/dada = (b*(c-x)^2*/(a^4*phi).[3 - (x-c)^2/(a * phi)^2]
357// ddf/dadb = -((x - c)^2) / ((a^3) * phi)
358// ddf/dadc = -(b*(x-c)/((a * phi)^3).[2 - (x-c)^2/((a * phi)^2)]
359// ddf/dadd = 0
360// ddf/dbdb = 0
361// ddf/dbdc = -(x-c)/(a^2*phi)
362// ddf/dbdd = 0
363// ddf/dcdc = b/(phi^3 * a^2).[1 - ((x - c)^2)/((a * phi)^2)]
364// ddf/dcdd = 0
365// ddf/dddd = 0
366int hypFxx(const gsl_vector* X, const gsl_vector* v, void* inParams, gsl_vector* fvv)
367{
368 CurveFitting::DataPointT * DataPoint = ((struct CurveFitting::DataPointT *)inParams);
369
370 // Store current coefficients
371 const double a = gsl_vector_get(X, A_IDX);
372 const double b = gsl_vector_get(X, B_IDX);
373 const double c = gsl_vector_get(X, C_IDX);
374
375 const double a2 = pow(a, 2);
376 const double a4 = pow(a2, 2);
377
378 const double va = gsl_vector_get(v, A_IDX);
379 const double vb = gsl_vector_get(v, B_IDX);
380 const double vc = gsl_vector_get(v, C_IDX);
381
382 for(int i = 0; i < DataPoint->dps.size(); ++i)
383 {
384 const double x = DataPoint->dps[i].x;
385 const double xmc = x - c;
386 const double xmc2 = pow(xmc, 2);
387 const double phi = hypPhi(x, a, c);
388 const double aphi = a * phi;
389 const double aphi2 = pow(aphi, 2);
390
391 const double Daa = b * xmc2 * (3 - xmc2 / aphi2) / (a4 * phi);
392 const double Dab = -xmc2 / (a2 * aphi);
393 const double Dac = b * xmc * (2 - (xmc2 / aphi2)) / (aphi2 * aphi);
394 const double Dbc = -xmc / (a2 * phi);
395 const double Dcc = b * (1 - (xmc2 / aphi2)) / (phi * aphi2);
396
397 double sum = va * va * Daa + 2 * (va * vb * Dab + va * vc * Dac + vb * vc * Dbc) + vc * vc * Dcc;
398
399 gsl_vector_set(fvv, i, sum);
400
401 }
402
403 return GSL_SUCCESS;
404}
405
406// Function to calculate f(x) for a parabola.
407double parfx(double x, double a, double b, double c)
408{
409 return a + b * pow((x - c), 2.0);
410}
411
412// Function to calculate x for passed in t and parabola parameters
413// y = a + b.(x - c) ^ 2
414// x = c + sqrt((y - a) / b)
415// Note: the larger x solution is returned
416double parfy(double y, double a, double b, double c)
417{
418 return c + sqrt((y - a) / b);
419}
420
421// Calculates f(x) for each data point in the parabola.
422int parFx(const gsl_vector * X, void * inParams, gsl_vector * outResultVec)
423{
424 CurveFitting::DataPointT * DataPoint = ((struct CurveFitting::DataPointT *)inParams);
425
426 double a = gsl_vector_get (X, A_IDX);
427 double b = gsl_vector_get (X, B_IDX);
428 double c = gsl_vector_get (X, C_IDX);
429
430 for(int i = 0; i < DataPoint->dps.size(); ++i)
431 {
432 // Parabola equation
433 double yi = parfx(DataPoint->dps[i].x, a, b, c);
434
435 gsl_vector_set(outResultVec, i, (yi - DataPoint->dps[i].y));
436 }
437
438 return GSL_SUCCESS;
439}
440
441// Calculates the Jacobian (derivative) matrix for the parabola equation f(x) = a + b*(x-c)^2
442// dy/da = 1
443// dy/db = (x - c)^2
444// dy/dc = -2b * (x - c)
445int parJx(const gsl_vector * X, void * inParams, gsl_matrix * J)
446{
447 CurveFitting::DataPointT * DataPoint = ((struct CurveFitting::DataPointT *)inParams);
448
449 // Store current coefficients
450 double b = gsl_vector_get(X, B_IDX);
451 double c = gsl_vector_get(X, C_IDX);
452
453 for(int i = 0; i < DataPoint->dps.size(); ++i)
454 {
455 // Calculate the Jacobian Matrix
456 const double x = DataPoint->dps[i].x;
457 const double xmc = x - c;
458
459 gsl_matrix_set(J, i, A_IDX, 1);
460 gsl_matrix_set(J, i, B_IDX, xmc * xmc);
461 gsl_matrix_set(J, i, C_IDX, -2 * b * xmc);
462 }
463
464 return GSL_SUCCESS;
465}
466// Calculates the second directional derivative vector for the parabola equation f(x) = a + b*(x-c)^2
467int parFxx(const gsl_vector* X, const gsl_vector* v, void* inParams, gsl_vector* fvv)
468{
469 CurveFitting::DataPointT * DataPoint = ((struct CurveFitting::DataPointT *)inParams);
470
471 const double b = gsl_vector_get(X, B_IDX);
472 const double c = gsl_vector_get(X, C_IDX);
473
474 const double vb = gsl_vector_get(v, B_IDX);
475 const double vc = gsl_vector_get(v, C_IDX);
476
477 for(int i = 0; i < DataPoint->dps.size(); ++i)
478 {
479 const double x = DataPoint->dps[i].x;
480 double Dbc = -2 * (x - c);
481 double Dcc = 2 * b;
482 double sum = 2 * vb * vc * Dbc + vc * vc * Dcc;
483
484 gsl_vector_set(fvv, i, sum);
485
486 }
487
488 return GSL_SUCCESS;
489}
490
491// Function to calculate f(x,y) for a 2-D gaussian.
492double gaufxy(double x, double y, double a, double x0, double y0, double A, double B, double C, double b)
493{
494 return b + a * exp(-(A * (pow(x - x0, 2.0)) + 2.0 * B * (x - x0) * (y - y0) + C * (pow(y - y0, 2.0))));
495}
496
497// Calculates f(x,y) for each data point in the gaussian.
498int gauFxy(const gsl_vector * X, void * inParams, gsl_vector * outResultVec)
499{
500 CurveFitting::DataPoint3DT * DataPoint = ((struct CurveFitting::DataPoint3DT *)inParams);
501
502 double a = gsl_vector_get (X, A_IDX);
503 double x0 = gsl_vector_get (X, B_IDX);
504 double y0 = gsl_vector_get (X, C_IDX);
505 double A = gsl_vector_get (X, D_IDX);
506 double B = gsl_vector_get (X, E_IDX);
507 double C = gsl_vector_get (X, F_IDX);
508 double b = gsl_vector_get (X, G_IDX);
509
510 for(int i = 0; i < DataPoint->dps.size(); ++i)
511 {
512 // Gaussian equation
513 double zij = gaufxy(DataPoint->dps[i].x, DataPoint->dps[i].y, a, x0, y0, A, B, C, b);
514 gsl_vector_set(outResultVec, i, (zij - DataPoint->dps[i].z));
515 }
516
517 return GSL_SUCCESS;
518}
519
520// Calculates the Jacobian (derivative) matrix for the gaussian
521// Jacobian J = {df/db, df/da, df/dx0, df/dy0, df/dA, df/dB, df/dC}
522// Let phi = exp-(A((x-x0)^2) + 2B(x-x0)(y-y0) + C((y-y0)^2))
523// df/db = 1
524// df/da = phi
525// df/dx0 = [2A(x-x0)+2B(y-y0)].a.phi
526// df/dy0 = [2B(x-x0)+2C(y-y0)].a.phi
527// df/dA = -(x-x0)^2.a.phi
528// df/DB = -2(x-x0)(y-y0).a.phi
529// df/dC = -(y-y0)^2.a.phi
530int gauJxy(const gsl_vector * X, void * inParams, gsl_matrix * J)
531{
532 CurveFitting::DataPoint3DT * DataPoint = ((struct CurveFitting::DataPoint3DT *)inParams);
533
534 // Get current coefficients
535 const double a = gsl_vector_get (X, A_IDX);
536 const double x0 = gsl_vector_get (X, B_IDX);
537 const double y0 = gsl_vector_get (X, C_IDX);
538 const double A = gsl_vector_get (X, D_IDX);
539 const double B = gsl_vector_get (X, E_IDX);
540 const double C = gsl_vector_get (X, F_IDX);
541 // b is not used ... const double b = gsl_vector_get (X, G_IDX);
542
543 for(int i = 0; i < DataPoint->dps.size(); ++i)
544 {
545 // Calculate the Jacobian Matrix
546 const double x = DataPoint->dps[i].x;
547 const double xmx0 = x - x0;
548 const double xmx02 = xmx0 * xmx0;
549 const double y = DataPoint->dps[i].y;
550 const double ymy0 = y - y0;
551 const double ymy02 = ymy0 * ymy0;
552 const double phi = exp(-((A * xmx02) + (2.0 * B * xmx0 * ymy0) + (C * ymy02)));
553 const double aphi = a * phi;
554
555 gsl_matrix_set(J, i, A_IDX, phi);
556 gsl_matrix_set(J, i, B_IDX, 2.0 * aphi * ((A * xmx0) + (B * ymy0)));
557 gsl_matrix_set(J, i, C_IDX, 2.0 * aphi * ((B * xmx0) + (C * ymy0)));
558 gsl_matrix_set(J, i, D_IDX, -1.0 * aphi * xmx02);
559 gsl_matrix_set(J, i, E_IDX, -2.0 * aphi * xmx0 * ymy0);
560 gsl_matrix_set(J, i, F_IDX, -1.0 * aphi * ymy02);
561 gsl_matrix_set(J, i, G_IDX, 1.0);
562 }
563
564 return GSL_SUCCESS;
565}
566
567// Calculates the second directional derivative vector for the gaussian equation
568// The matrix is symmetric, as ddf/dParm1dParm2 = ddf/dParm2dParm1 so we only need to differentiate
569// one half (above or below the diagonal) of the matrix elements.
570// ddf/dbdb = 0
571// ddf/dbda = 0
572// ddf/dbdx0 = 0
573// ddf/dbdy0 = 0
574// ddf/dbdA = 0
575// ddf/dbdB = 0
576// ddf/dbdC = 0
577//
578// ddf/dada = 0
579// ddf/dadx0 = [2A(x-x0) + 2B(y-y0)].phi
580// ddf/dady0 = [2B(x-x0) + 2C(y-y0)].phi
581// ddf/dadA = -(x-x0)^2.phi
582// ddf/dadB = -2(x-x0)(y-y0).phi
583// ddf/dadC = -Cy-y0)^2.phi
584//
585// ddf/dxodx0 = -2A.a.phi + [2A(x-x0)+2B(y-y0)]^2.a.phi
586// ddf/dx0dy0 = -2B.a.phi - [2A(x-x0)+2B(y-y0)].[2B(x-x0)+2C(y-y0)].a.phi
587// ddf/dx0dA = 2(x-x0).a.phi - [2A(x-x0)+2B(y-y0)].(x-x0)^2.a.phi
588// ddf/dx0dB = 2(y-y0).a.phi - [2A(x-x0)+2B(y-y0)].2(x-x0).(y-y0).a.phi
589// ddf/dx0dC = -[2A(x-x0)+2B(y-y0)].2(y-y0)^2.a.phi
590//
591// ddf/dy0dy0 = -2c.a.phi + [2B(x-x0)+2C(y-y0)]^2.a.phi
592// ddf/dy0dA = -[2B(x-x0)+2C(y-y0)].(x-x0)^2.a.phi
593// ddf/dy0dB = 2(x-x0).a.phi - [2B(x-x0)+2C(y-y0)].2(x-x0)(y-y0).a.phi
594// ddf/dy0dC = 2(y-y0).a.phi - [2B(x-x0)+2C(y-y0)].(y-y0)^2.a.phi
595//
596// ddf/dAdA = (x-x0)^4.a.phi
597// ddf/dAdB = 2(x-x0)^3.(y-y0).a.phi
598// ddf/dAdC = (x-x0)^2.(y-y0)^2.a.phi
599//
600// ddf/dBdB = 4(x-x0)^2.(y-y0)^2.a.phi
601// ddf/dBdC = 2(x-x0).(y-y0)^3.a.phi
602//
603// ddf/dCdC = (y-y0)^4.a.phi
604//
605int gauFxyxy(const gsl_vector* X, const gsl_vector* v, void* inParams, gsl_vector* fvv)
606{
607 CurveFitting::DataPoint3DT * DataPoint = ((struct CurveFitting::DataPoint3DT *)inParams);
608
609 // Get current coefficients
610 const double a = gsl_vector_get (X, A_IDX);
611 const double x0 = gsl_vector_get (X, B_IDX);
612 const double y0 = gsl_vector_get (X, C_IDX);
613 const double A = gsl_vector_get (X, D_IDX);
614 const double B = gsl_vector_get (X, E_IDX);
615 const double C = gsl_vector_get (X, F_IDX);
616 // b not used ... const double b = gsl_vector_get (X, G_IDX);
617
618 const double va = gsl_vector_get(v, A_IDX);
619 const double vx0 = gsl_vector_get(v, B_IDX);
620 const double vy0 = gsl_vector_get(v, C_IDX);
621 const double vA = gsl_vector_get(v, D_IDX);
622 const double vB = gsl_vector_get(v, E_IDX);
623 const double vC = gsl_vector_get(v, F_IDX);
624 // vb not used ... const double vb = gsl_vector_get(v, G_IDX);
625
626 for(int i = 0; i < DataPoint->dps.size(); ++i)
627 {
628 double x = DataPoint->dps[i].x;
629 double xmx0 = x - x0;
630 double xmx02 = xmx0 * xmx0;
631 double y = DataPoint->dps[i].y;
632 double ymy0 = y - y0;
633 double ymy02 = ymy0 * ymy0;
634 double phi = exp(-((A * xmx02) + (2.0 * B * xmx0 * ymy0) + (C * ymy02)));
635 double aphi = a * phi;
636 double AB = 2.0 * ((A * xmx0) + (B * ymy0));
637 double BC = 2.0 * ((B * xmx0) + (C * ymy0));
638
639 double Dax0 = AB * phi;
640 double Day0 = BC * phi;
641 double DaA = -xmx02 * phi;
642 double DaB = -2.0 * xmx0 * ymy0 * phi;
643 double DaC = -ymy02 * phi;
644
645 double Dx0x0 = aphi * ((-2.0 * A) + (AB * AB));
646 double Dx0y0 = -aphi * ((2.0 * B) + (AB * BC));
647 double Dx0A = aphi * ((2.0 * xmx0) - (AB * xmx02));
648 double Dx0B = 2.0 * aphi * (ymy0 - (AB * xmx0 * ymy0));
649 double Dx0C = -2.0 * aphi * AB * ymy02;
650
651 double Dy0y0 = aphi * ((-2.0 * C) + (BC * BC));
652 double Dy0A = -aphi * BC * xmx02;
653 double Dy0B = 2.0 * aphi * (xmx0 - (BC * xmx0 * ymy0));
654 double Dy0C = aphi * ((2.0 * ymy0) - (BC * ymy02));
655
656 double DAA = aphi * xmx02 * xmx02;
657 double DAB = 2.0 * aphi * xmx02 * xmx0 * ymy0;
658 double DAC = aphi * xmx02 * ymy02;
659
660 double DBB = 4.0 * aphi * xmx02 * ymy02;
661 double DBC = 2.0 * aphi * xmx0 * ymy02 * ymy0;
662
663 double DCC = aphi * ymy02 * ymy02;
664
665 double sum = 2 * va * ((vx0 * Dax0) + (vy0 * Day0) + (vA * DaA) + (vB * DaB) + (vC * DaC)) + // a diffs
666 vx0 * ((vx0 * Dx0x0) + 2 * ((vy0 * Dx0y0) + (vA * Dx0A) + (vB * Dx0B) + (vC * Dx0C))) + // x0 diffs
667 vy0 * ((vy0 * Dy0y0) + 2 * ((vA * Dy0A) + (vB * Dy0B) + (vC * Dy0C))) + // y0 diffs
668 vA * ((vA * DAA) + 2 * ((vB * DAB) + (vC * DAC))) + // A diffs
669 vB * ((vB * DBB) + 2 * (vC * DBC)) + // B diffs
670 vC * vC * DCC; // C diffs
671
672 gsl_vector_set(fvv, i, sum);
673 }
674
675 return GSL_SUCCESS;
676}
677
678// Function to calculate f(x,y) for a 2-D plane.
679// f(x,y) = (A.x + B.y) / -C
680double plafxy(double x, double y, double A, double B, double C)
681{
682 return -(A * x + B * y) / C;
683}
684
685// Calculates f(x,y) for each data point in the gaussian.
686int plaFxy(const gsl_vector * X, void * inParams, gsl_vector * outResultVec)
687{
688 CurveFitting::DataPoint3DT * DataPoint = ((struct CurveFitting::DataPoint3DT *)inParams);
689
690 double A = gsl_vector_get (X, A_IDX);
691 double B = gsl_vector_get (X, B_IDX);
692 double C = gsl_vector_get (X, C_IDX);
693
694 for(int i = 0; i < DataPoint->dps.size(); ++i)
695 {
696 // Plane equation
697 double zi = plafxy(DataPoint->dps[i].x, DataPoint->dps[i].y, A, B, C);
698 gsl_vector_set(outResultVec, i, (zi - DataPoint->dps[i].z));
699 }
700
701 return GSL_SUCCESS;
702}
703
704// Calculates the Jacobian (derivative) matrix for the plane
705// Jacobian J = {df/dA, df/dB, df/dC}
706// df/dA = -x / C
707// df/dB = -y / C
708// df/dC = (A.x + B.y) / C^2
709int plaJxy(const gsl_vector * X, void * inParams, gsl_matrix * J)
710{
711 CurveFitting::DataPoint3DT * DataPoint = ((struct CurveFitting::DataPoint3DT *)inParams);
712
713 // Get current coefficients
714 const double A = gsl_vector_get (X, A_IDX);
715 const double B = gsl_vector_get (X, B_IDX);
716 const double C = gsl_vector_get (X, C_IDX);
717
718 for(int i = 0; i < DataPoint->dps.size(); ++i)
719 {
720 // Calculate the Jacobian Matrix
721 const double x = DataPoint->dps[i].x;
722 const double y = DataPoint->dps[i].y;
723
724 gsl_matrix_set(J, i, A_IDX, -x / C);
725 gsl_matrix_set(J, i, B_IDX, -y / C);
726 gsl_matrix_set(J, i, C_IDX, (A * x + B * y) / (C * C));
727 }
728
729 return GSL_SUCCESS;
730}
731
732// Calculates the second directional derivative vector for the plane equation
733// The matrix is symmetric, as ddf/dParm1dParm2 = ddf/dParm2dParm1 so we only need to differentiate
734// one half (above or below the diagonal) of the matrix elements.
735//
736// ddf/dAdA = 0
737// ddf/dAdB = 0
738// ddf/dAdC = x / C^2
739
740// ddf/dBdB = 0
741// ddf/dBdC = y / C^2
742//
743// ddf/dCdC = -2.(A.x + B.y) / C^3
744//
745int plaFxyxy(const gsl_vector* X, const gsl_vector* v, void* inParams, gsl_vector* fvv)
746{
747 CurveFitting::DataPoint3DT * DataPoint = ((struct CurveFitting::DataPoint3DT *)inParams);
748
749 // Get current coefficients
750 const double A = gsl_vector_get (X, A_IDX);
751 const double B = gsl_vector_get (X, B_IDX);
752 const double C = gsl_vector_get (X, C_IDX);
753
754 const double vA = gsl_vector_get(v, A_IDX);
755 const double vB = gsl_vector_get(v, B_IDX);
756 const double vC = gsl_vector_get(v, C_IDX);
757
758 for(int i = 0; i < DataPoint->dps.size(); ++i)
759 {
760 double x = DataPoint->dps[i].x;
761 double y = DataPoint->dps[i].y;
762 double C2 = C * C;
763 double C3 = C * C2;
764 double dAdC = x / C2;
765 double dBdC = y / C2;
766 double dCdC = -2 * (A * x + B * y) / C3;
767
768 double sum = 2 * (vA * vC * dAdC) + 2 * (vB * vC * dBdC) + vC * vC * dCdC;
769
770 gsl_vector_set(fvv, i, sum);
771 }
772
773 return GSL_SUCCESS;
774}
775} // namespace
776
777CurveFitting::CurveFitting()
778{
779 // Constructor just initialises variables
780 m_FirstSolverRun = true;
781}
782
783CurveFitting::CurveFitting(const QString &serialized)
784{
785 recreateFromQString(serialized);
786}
787
788void CurveFitting::fitCurve(const FittingGoal goal, const QVector<int> &x_, const QVector<double> &y_,
789 const QVector<double> &weight_, const QVector<bool> &outliers_,
790 const CurveFit curveFit, const bool useWeights, const OptimisationDirection optDir)
791{
792 if ((x_.size() != y_.size()) || (x_.size() != weight_.size()) || (x_.size() != outliers_.size()))
793 qCDebug(KSTARS_EKOS_FOCUS) << QString("CurveFitting::fitCurve inconsistent parameters. x=%1, y=%2, weight=%3, outliers=%4")
794 .arg(x_.size()).arg(y_.size()).arg(weight_.size()).arg(outliers_.size());
795
796 m_x.clear();
797 m_y.clear();
798 m_scale.clear();
799 m_outliers.clear();
800 for (int i = 0; i < x_.size(); ++i)
801 {
802 m_x.push_back(static_cast<double>(x_[i]));
803 m_y.push_back(y_[i]);
804 m_scale.push_back(weight_[i]);
805 m_outliers.push_back(outliers_[i]);
806 }
807
808 m_useWeights = useWeights;
809 m_CurveType = curveFit;
810
811 switch (m_CurveType)
812 {
813 case FOCUS_QUADRATIC :
814 m_coefficients = polynomial_fit(m_x.data(), m_y.data(), m_x.count(), 2);
815 break;
816 case FOCUS_HYPERBOLA :
817 m_coefficients = hyperbola_fit(goal, m_x, m_y, m_scale, m_outliers, useWeights, optDir);
818 break;
819 case FOCUS_PARABOLA :
820 m_coefficients = parabola_fit(goal, m_x, m_y, m_scale, m_outliers, useWeights, optDir);
821 break;
822 default :
823 // Something went wrong, log an error and reset state so solver starts from scratch if called again
824 qCDebug(KSTARS_EKOS_FOCUS) << QString("CurveFitting::fitCurve called with curveFit=%1").arg(curveFit);
825 m_FirstSolverRun = true;
826 return;
827 }
828 m_LastCoefficients = m_coefficients;
829 m_LastCurveType = m_CurveType;
830 m_FirstSolverRun = false;
831}
832
833void CurveFitting::fitCurve3D(const DataPoint3DT data, const CurveFit curveFit)
834{
835 if (data.useWeights)
836 {
837 qCDebug(KSTARS_EKOS_FOCUS) << QString("CurveFitting::fitCurve3D called with useWeights = true. Not currently supported");
838 m_FirstSolverRun = true;
839 return;
840 }
841
842 m_useWeights = data.useWeights;
843 m_CurveType = curveFit;
844 m_dataPoints = data;
845
846 switch (m_CurveType)
847 {
848 case FOCUS_PLANE :
849 m_coefficients = plane_fit(data);
850 break;
851 default :
852 // Something went wrong, log an error and reset state so solver starts from scratch if called again
853 qCDebug(KSTARS_EKOS_FOCUS) << QString("CurveFitting::fitCurve3D called with curveFit=%1").arg(curveFit);
854 m_FirstSolverRun = true;
855 return;
856 }
857 m_LastCoefficients = m_coefficients;
858 m_LastCurveType = m_CurveType;
859 m_FirstSolverRun = false;
860}
861
862double CurveFitting::curveFunction(double x, void *params)
863{
864 CurveFitting *instance = static_cast<CurveFitting *>(params);
865
866 if (instance && !instance->m_coefficients.empty())
867 return instance->f(x);
868 else
869 return -1;
870}
871
872double CurveFitting::f(double x)
873{
874 const int order = m_coefficients.size() - 1;
875 double y = 0;
876 if (m_CurveType == FOCUS_QUADRATIC)
877 {
878 for (int i = 0; i <= order; ++i)
879 y += m_coefficients[i] * pow(x, i);
880 }
881 else if (m_CurveType == FOCUS_HYPERBOLA && m_coefficients.size() == NUM_HYPERBOLA_PARAMS)
882 y = hypfx(x, m_coefficients[A_IDX], m_coefficients[B_IDX], m_coefficients[C_IDX], m_coefficients[D_IDX]);
883 else if (m_CurveType == FOCUS_PARABOLA && m_coefficients.size() == NUM_PARABOLA_PARAMS)
884 y = parfx(x, m_coefficients[A_IDX], m_coefficients[B_IDX], m_coefficients[C_IDX]);
885 else
886 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error: CurveFitting::f called with m_CurveType = %1 m_coefficients.size = %2")
887 .arg(m_CurveType).arg(m_coefficients.size());
888
889 return y;
890}
891
892double CurveFitting::fy(double y)
893{
894 double x = 0;
895 if (m_CurveType == FOCUS_HYPERBOLA && m_coefficients.size() == NUM_HYPERBOLA_PARAMS)
896 x = hypfy(y, m_coefficients[A_IDX], m_coefficients[B_IDX], m_coefficients[C_IDX], m_coefficients[D_IDX]);
897 else if (m_CurveType == FOCUS_PARABOLA && m_coefficients.size() == NUM_PARABOLA_PARAMS)
898 x = parfy(y, m_coefficients[A_IDX], m_coefficients[B_IDX], m_coefficients[C_IDX]);
899 else
900 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error: CurveFitting::fy called with m_CurveType = %1 m_coefficients.size = %2")
901 .arg(m_CurveType).arg(m_coefficients.size());
902
903 return x;
904}
905
906double CurveFitting::f3D(double x, double y)
907{
908 double z = 0;
909 if (m_CurveType == FOCUS_GAUSSIAN && m_coefficients.size() == NUM_GAUSSIAN_PARAMS)
910 z = gaufxy(x, y, m_coefficients[A_IDX], m_coefficients[B_IDX], m_coefficients[C_IDX], m_coefficients[D_IDX],
911 m_coefficients[E_IDX], m_coefficients[F_IDX], m_coefficients[G_IDX]);
912 else if (m_CurveType == FOCUS_PLANE && m_coefficients.size() == NUM_PLANE_PARAMS)
913 z = plafxy(x, y, m_coefficients[A_IDX], m_coefficients[B_IDX], m_coefficients[C_IDX]);
914 else
915 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error: CurveFitting::f3D called with m_CurveType = %1 m_coefficients.size = %2")
916 .arg(m_CurveType).arg(m_coefficients.size());
917
918 return z;
919}
920
921QVector<double> CurveFitting::polynomial_fit(const double *const data_x, const double *const data_y, const int n,
922 const int order)
923{
924 int status = 0;
925 double chisq = 0;
927 gsl_vector *y, *c;
928 gsl_matrix *X, *cov;
929 y = gsl_vector_alloc(n);
930 c = gsl_vector_alloc(order + 1);
931 X = gsl_matrix_alloc(n, order + 1);
932 cov = gsl_matrix_alloc(order + 1, order + 1);
933
934 for (int i = 0; i < n; i++)
935 {
936 for (int j = 0; j < order + 1; j++)
937 {
938 gsl_matrix_set(X, i, j, pow(data_x[i], j));
939 }
940 gsl_vector_set(y, i, data_y[i]);
941 }
942
943 // Must turn off error handler or it aborts on error
944 gsl_set_error_handler_off();
945
946 gsl_multifit_linear_workspace *work = gsl_multifit_linear_alloc(n, order + 1);
947 status = gsl_multifit_linear(X, y, c, cov, &chisq, work);
948
949 if (status != GSL_SUCCESS)
950 qDebug() << Q_FUNC_INFO << "GSL multifit error:" << gsl_strerror(status);
951 else
952 {
953 gsl_multifit_linear_free(work);
954
955 for (int i = 0; i < order + 1; i++)
956 {
957 vc.push_back(gsl_vector_get(c, i));
958 }
959 }
960
961 gsl_vector_free(y);
962 gsl_vector_free(c);
963 gsl_matrix_free(X);
964 gsl_matrix_free(cov);
965
966 return vc;
967}
968
969QVector<double> CurveFitting::hyperbola_fit(FittingGoal goal, const QVector<double> data_x, const QVector<double> data_y,
970 const QVector<double> data_weights, const QVector<bool> outliers, const bool useWeights, const OptimisationDirection optDir)
971{
973 DataPointT dataPoints;
974
975 // Fill in the data to which the curve will be fitted
976 dataPoints.useWeights = useWeights;
977 dataPoints.dir = optDir;
978 for (int i = 0; i < data_x.size(); i++)
979 if (!outliers[i])
980 dataPoints.push_back(data_x[i], data_y[i], data_weights[i]);
981
982 auto weights = gsl_vector_alloc(dataPoints.dps.size());
983 // Set the gsl error handler off as it aborts the program on error.
984 auto const oldErrorHandler = gsl_set_error_handler_off();
985
986 // Setup variables to be used by the solver
987 gsl_multifit_nlinear_parameters params = gsl_multifit_nlinear_default_parameters();
988 gsl_multifit_nlinear_workspace *w = gsl_multifit_nlinear_alloc(gsl_multifit_nlinear_trust, &params, dataPoints.dps.size(),
989 NUM_HYPERBOLA_PARAMS);
990 gsl_multifit_nlinear_fdf fdf;
991 gsl_vector *guess = gsl_vector_alloc(NUM_HYPERBOLA_PARAMS);
992 int numIters;
993 double xtol, gtol, ftol;
994
995 // Fill in function info
996 fdf.f = hypFx;
997 fdf.df = hypJx;
998 fdf.fvv = hypFxx;
999 fdf.n = dataPoints.dps.size();
1000 fdf.p = NUM_HYPERBOLA_PARAMS;
1001 fdf.params = &dataPoints;
1002
1003 // This is the callback used by the LM solver to allow some introspection of each iteration
1004 // Useful for debugging but clogs up the log
1005 // To activate, uncomment the callback lambda and change the call to gsl_multifit_nlinear_driver
1006 /*auto callback = [](const size_t iter, void* _params, const auto * w)
1007 {
1008 gsl_vector *f = gsl_multifit_nlinear_residual(w);
1009 gsl_vector *x = gsl_multifit_nlinear_position(w);
1010
1011 // compute reciprocal condition number of J(x)
1012 double rcond;
1013 gsl_multifit_nlinear_rcond(&rcond, w);
1014
1015 // ratio of accel component to velocity component
1016 double avratio = gsl_multifit_nlinear_avratio(w);
1017
1018 qCDebug(KSTARS_EKOS_FOCUS) << QString("iter %1: A=%2, B=%3, C=%4, D=%5 rcond(J)=%6, avratio=%7, |f(x)|=%8")
1019 .arg(iter)
1020 .arg(gsl_vector_get(x, A_IDX))
1021 .arg(gsl_vector_get(x, B_IDX))
1022 .arg(gsl_vector_get(x, C_IDX))
1023 .arg(gsl_vector_get(x, D_IDX))
1024 .arg(rcond)
1025 .arg(avratio)
1026 .arg(gsl_blas_dnrm2(f));
1027 };*/
1028
1029 // Start a timer to see how long the solve takes.
1030 QElapsedTimer timer;
1031 timer.start();
1032
1033 // We can sometimes have several attempts to solve based on "goal" and why the solver failed.
1034 // If the goal is STANDARD and we fail to solve then so be it. If the goal is BEST, then retry
1035 // with different parameters to really try and get a solution. A special case is if the solver
1036 // fails on its first step where we will always retry after adjusting parameters. It helps with
1037 // a situation where the solver gets "stuck" failing on first step repeatedly.
1038 for (int attempt = 0; attempt < 5; attempt++)
1039 {
1040 // Make initial guesses
1041 hypMakeGuess(attempt, dataPoints, guess);
1042
1043 // Load up the weights and guess vectors
1044 if (useWeights)
1045 {
1046 for (int i = 0; i < dataPoints.dps.size(); i++)
1047 gsl_vector_set(weights, i, dataPoints.dps[i].weight);
1048 gsl_multifit_nlinear_winit(guess, weights, &fdf, w);
1049 }
1050 else
1051 gsl_multifit_nlinear_init(guess, &fdf, w);
1052
1053 // Tweak solver parameters from default values
1054 hypSetupParams(goal, &params, &numIters, &xtol, &gtol, &ftol);
1055
1056 qCDebug(KSTARS_EKOS_FOCUS) << QString("Starting LM solver, fit=hyperbola, solver=%1, scale=%2, trs=%3, iters=%4, xtol=%5,"
1057 "gtol=%6, ftol=%7")
1058 .arg(params.solver->name).arg(params.scale->name).arg(params.trs->name).arg(numIters)
1059 .arg(xtol).arg(gtol).arg(ftol);
1060
1061 int info = 0;
1062 int status = gsl_multifit_nlinear_driver(numIters, xtol, gtol, ftol, NULL, NULL, &info, w);
1063
1064 if (status != 0)
1065 {
1066 // Solver failed so determine whether a retry is required.
1067 bool retry = false;
1068 if (goal == BEST)
1069 {
1070 // Pull out all the stops to get a solution
1071 retry = true;
1072 goal = BEST_RETRY;
1073 }
1074 else if (status == GSL_EMAXITER && info == GSL_ENOPROG && gsl_multifit_nlinear_niter(w) <= 1)
1075 // This is a special case where the solver can't take a first step
1076 // So, perturb the initial conditions and have another go.
1077 retry = true;
1078
1079 qCDebug(KSTARS_EKOS_FOCUS) <<
1080 QString("LM solver (Hyperbola): Failed after %1ms iters=%2 [attempt=%3] with status=%4 [%5] and info=%6 [%7], retry=%8")
1081 .arg(timer.elapsed()).arg(gsl_multifit_nlinear_niter(w)).arg(attempt + 1).arg(status).arg(gsl_strerror(status))
1082 .arg(info).arg(gsl_strerror(info)).arg(retry);
1083 if (!retry)
1084 break;
1085 }
1086 else
1087 {
1088 // All good so store the results - parameters A, B, C and D
1089 auto solution = gsl_multifit_nlinear_position(w);
1090 for (int j = 0; j < NUM_HYPERBOLA_PARAMS; j++)
1091 vc.push_back(gsl_vector_get(solution, j));
1092
1093 qCDebug(KSTARS_EKOS_FOCUS) <<
1094 QString("LM Solver (Hyperbola): Solution found after %1ms %2 iters (%3). A=%4, B=%5, C=%6, D=%7")
1095 .arg(timer.elapsed()).arg(gsl_multifit_nlinear_niter(w)).arg(getLMReasonCode(info)).arg(vc[A_IDX]).arg(vc[B_IDX])
1096 .arg(vc[C_IDX]).arg(vc[D_IDX]);
1097 break;
1098 }
1099 }
1100
1101 // Free GSL memory
1102 gsl_multifit_nlinear_free(w);
1103 gsl_vector_free(guess);
1104 gsl_vector_free(weights);
1105
1106 // Restore old GSL error handler
1107 gsl_set_error_handler(oldErrorHandler);
1108
1109 return vc;
1110}
1111
1112QString CurveFitting::getLMReasonCode(int info)
1113{
1114 QString reason;
1115
1116 if (info == 1)
1117 reason = QString("small step size");
1118 else if(info == 2)
1119 reason = QString("small gradient");
1120 else
1121 reason = QString("unknown reason");
1122
1123 return reason;
1124}
1125
1126// Setup the parameters for hyperbola curve fitting
1127void CurveFitting::hypSetupParams(FittingGoal goal, gsl_multifit_nlinear_parameters *params, int *numIters, double *xtol,
1128 double *gtol, double *ftol)
1129{
1130 // Trust region subproblem
1131 // - gsl_multifit_nlinear_trs_lm is the Levenberg-Marquardt algorithm without acceleration
1132 // - gsl_multifit_nlinear_trs_lmaccel is the Levenberg-Marquardt algorithm with acceleration
1133 // - gsl_multilarge_nlinear_trs_dogleg is the dogleg algorithm
1134 // - gsl_multifit_nlinear_trs_ddogleg is the double dogleg algorithm
1135 // - gsl_multifit_nlinear_trs_subspace2D is the 2D subspace algorithm
1136 params->trs = gsl_multifit_nlinear_trs_lmaccel;
1137
1138 // Scale
1139 // - gsl_multifit_nlinear_scale_more uses. More strategy. Good for problems with parameters with widely different scales
1140 // - gsl_multifit_nlinear_scale_levenberg. Levensberg strategy. Can be good but not scale invariant.
1141 // - gsl_multifit_nlinear_scale_marquardt. Marquardt strategy. Considered inferior to More and Levensberg
1142 params->scale = gsl_multifit_nlinear_scale_more;
1143
1144 // Solver
1145 // - gsl_multifit_nlinear_solver_qr produces reliable results but needs more iterations than Cholesky
1146 // - gsl_multifit_nlinear_solver_cholesky fast but not as stable as QR
1147 // - gsl_multifit_nlinear_solver_mcholesky modified Cholesky more stable than Cholesky
1148
1149 // avmax is the max allowed ratio of 2nd order term (acceleration, a) to the 1st order term (velocity, v)
1150 // GSL defaults to 0.75, but suggests reducing it in the case of convergence problems
1151 switch (goal)
1152 {
1153 case STANDARD:
1154 params->solver = gsl_multifit_nlinear_solver_qr;
1155 params->avmax = 0.75;
1156
1157 *numIters = MAX_ITERATIONS_CURVE;
1158 *xtol = INEPSXTOL;
1159 *gtol = INEPSGTOL;
1160 *ftol = INEPSFTOL;
1161 break;
1162 case BEST:
1163 params->solver = gsl_multifit_nlinear_solver_cholesky;
1164 params->avmax = 0.75;
1165
1166 *numIters = MAX_ITERATIONS_CURVE * 2.0;
1167 *xtol = INEPSXTOL / 10.0;
1168 *gtol = INEPSGTOL / 10.0;
1169 *ftol = INEPSFTOL / 10.0;
1170 break;
1171 case BEST_RETRY:
1172 params->solver = gsl_multifit_nlinear_solver_qr;
1173 params->avmax = 0.5;
1174
1175 *numIters = MAX_ITERATIONS_CURVE * 2.0;
1176 *xtol = INEPSXTOL;
1177 *gtol = INEPSGTOL;
1178 *ftol = INEPSFTOL;
1179 break;
1180 default:
1181 break;
1182 }
1183}
1184
1185// Initialise parameters before starting the solver. Its important to start with a guess as near to the solution as possible
1186// If we found a solution before and we're just adding more datapoints use the last solution as the guess.
1187// If we don't have a solution use the datapoints to approximate. Work out the min and max points and use these
1188// to find "close" values for the starting point parameters
1189void CurveFitting::hypMakeGuess(const int attempt, const DataPointT &dataPoints, gsl_vector * guess)
1190{
1191 // If we are retrying then perturb the initial conditions. The hope is that by doing this the solver
1192 // will be nudged to find a solution this time
1193 double perturbation = 1.0 + pow(-1, attempt) * (attempt * 0.1);
1194
1195 if (!m_FirstSolverRun && (m_LastCurveType == FOCUS_HYPERBOLA) && (m_LastCoefficients.size() == NUM_HYPERBOLA_PARAMS))
1196 {
1197 // Last run of the solver was a Hyperbola and the solution was good, so use that solution
1198 gsl_vector_set(guess, A_IDX, m_LastCoefficients[A_IDX] * perturbation);
1199 gsl_vector_set(guess, B_IDX, m_LastCoefficients[B_IDX] * perturbation);
1200 gsl_vector_set(guess, C_IDX, m_LastCoefficients[C_IDX] * perturbation);
1201 gsl_vector_set(guess, D_IDX, m_LastCoefficients[D_IDX] * perturbation);
1202 }
1203 else
1204 {
1205 double minX = dataPoints.dps[0].x;
1206 double minY = dataPoints.dps[0].y;
1207 double maxX = minX;
1208 double maxY = minY;
1209 for(int i = 0; i < dataPoints.dps.size(); i++)
1210 {
1211 if (dataPoints.dps[i].y <= 0.0)
1212 continue;
1213 if(minY <= 0.0 || dataPoints.dps[i].y < minY)
1214 {
1215 minX = dataPoints.dps[i].x;
1216 minY = dataPoints.dps[i].y;
1217 }
1218 if(maxY <= 0.0 || dataPoints.dps[i].y > maxY)
1219 {
1220 maxX = dataPoints.dps[i].x;
1221 maxY = dataPoints.dps[i].y;
1222 }
1223 }
1224 double A, B, C, D;
1225 if (dataPoints.dir == OPTIMISATION_MAXIMISE)
1226 {
1227 // Hyperbola equation: y = f(x) = b * sqrt(1 + ((x - c) / a) ^2) + d
1228 // For a maximum: c = maximum x = x(max)
1229 // b < 0 and b + d > 0
1230 // For the array of data, set:
1231 // => c = x(max)
1232 // Now assume maximum x is near the real curve maximum, so y = b + d
1233 // Set b = -d/2. So y(max) = -d/2 + d = d/2.
1234 // => d = 2.y(max)
1235 // => b = -y(max)
1236 // Now look at the minimum y value in the array of datapoints
1237 // y(min) = b * sqrt(1 + ((x(min) - c) / a) ^2) + d
1238 // (y(min) - d) / b) ^ 2) = 1 + ((x(min) - c) / a) ^2
1239 // sqrt((((y(min) - d) / b) ^ 2) - 1) = (x(min) - c) / a
1240 // a = (x(min) - c) / sqrt((((y(min) - d) / b) ^ 2) - 1)
1241 // use the values for b, c, d obtained above to get:
1242 // => a = (x(min) - x(max)) / sqrt((((y(min) - (2.y(max)) / (-y(max))) ^ 2) - 1)
1243 double num = minX - maxX;
1244 double denom = sqrt(pow((2.0 * maxY - minY) / maxY, 2.0) - 1.0);
1245 if(denom <= 0.0)
1246 denom = 1.0;
1247 A = num / denom * perturbation;
1248 B = -maxY * perturbation;
1249 C = maxX * perturbation;
1250 D = 2.0 * maxY * perturbation;
1251 }
1252 else
1253 {
1254 // For a minimum: c = minimum x; b > 0 and b + d > 0
1255 // For the array of data, set:
1256 // => c = x(min)
1257 // Now assume minimum x is near the real curve minimum, so y = b + d
1258 // => Set b = d = y(min) / 2
1259 // Now look at the maximum y value in the array of datapoints
1260 // y(max) = b * sqrt(1 + ((x(max) - c) / a) ^2) + d
1261 // ((y(max) - d) / b) ^2 = 1 + ((x(max) - c) / a) ^2
1262 // a = (x(max) - c) / sqrt((((y(max) - d) / b) ^ 2) - 1)
1263 // use the values for b, c, d obtained above to get:
1264 // a = (x(max) - x(min)) / sqrt((((y(max) - (y(min) / 2)) / (y(min) / 2)) ^ 2) - 1)
1265 double minYdiv2 = (minY / 2.0 <= 0.0) ? 1.0 : minY / 2.0;
1266 double num = maxX - minX;
1267 double denom = sqrt(pow((maxY - minYdiv2) / minYdiv2, 2.0) - 1.0);
1268 if(denom <= 0.0)
1269 denom = 1.0;
1270 A = num / denom * perturbation;
1271 B = minYdiv2 * perturbation;
1272 C = minX * perturbation;
1273 D = B;
1274 }
1275 qCDebug(KSTARS_EKOS_FOCUS) << QString("LM solver (hyp) initial params: perturbation=%1, A=%2, B=%3, C=%4, D=%5")
1276 .arg(perturbation).arg(A).arg(B).arg(C).arg(D);
1277
1278 gsl_vector_set(guess, A_IDX, A);
1279 gsl_vector_set(guess, B_IDX, B);
1280 gsl_vector_set(guess, C_IDX, C);
1281 gsl_vector_set(guess, D_IDX, D);
1282 }
1283}
1284
1285QVector<double> CurveFitting::parabola_fit(FittingGoal goal, const QVector<double> data_x, const QVector<double> data_y,
1286 const QVector<double> data_weights, const QVector<bool> outliers, bool useWeights, const OptimisationDirection optDir)
1287{
1288 QVector<double> vc;
1289 DataPointT dataPoints;
1290
1291 // Fill in the data to which the curve will be fitted
1292 dataPoints.useWeights = useWeights;
1293 dataPoints.dir = optDir;
1294 for (int i = 0; i < data_x.size(); i++)
1295 if (!outliers[i])
1296 dataPoints.push_back(data_x[i], data_y[i], data_weights[i]);
1297
1298 auto weights = gsl_vector_alloc(dataPoints.dps.size());
1299 // Set the gsl error handler off as it aborts the program on error.
1300 auto const oldErrorHandler = gsl_set_error_handler_off();
1301
1302 // Setup variables to be used by the solver
1303 gsl_multifit_nlinear_parameters params = gsl_multifit_nlinear_default_parameters();
1304 gsl_multifit_nlinear_workspace* w = gsl_multifit_nlinear_alloc (gsl_multifit_nlinear_trust, &params, dataPoints.dps.size(),
1305 NUM_PARABOLA_PARAMS);
1306 gsl_multifit_nlinear_fdf fdf;
1307 gsl_vector * guess = gsl_vector_alloc(NUM_PARABOLA_PARAMS);
1308 int numIters;
1309 double xtol, gtol, ftol;
1310
1311 // Fill in function info
1312 fdf.f = parFx;
1313 fdf.df = parJx;
1314 fdf.fvv = parFxx;
1315 fdf.n = dataPoints.dps.size();
1316 fdf.p = NUM_PARABOLA_PARAMS;
1317 fdf.params = &dataPoints;
1318
1319 // This is the callback used by the LM solver to allow some introspection of each iteration
1320 // Useful for debugging but clogs up the log
1321 // To activate, uncomment the callback lambda and change the call to gsl_multifit_nlinear_driver
1322 /*auto callback = [](const size_t iter, void* _params, const auto * w)
1323 {
1324 gsl_vector *f = gsl_multifit_nlinear_residual(w);
1325 gsl_vector *x = gsl_multifit_nlinear_position(w);
1326
1327 // compute reciprocal condition number of J(x)
1328 double rcond;
1329 gsl_multifit_nlinear_rcond(&rcond, w);
1330
1331 // ratio of accel component to velocity component
1332 double avratio = gsl_multifit_nlinear_avratio(w);
1333
1334 qCDebug(KSTARS_EKOS_FOCUS) << QString("iter %1: A=%2, B=%3, C=%4, rcond(J)=%5, avratio=%6 |f(x)|=%7")
1335 .arg(iter)
1336 .arg(gsl_vector_get(x, A_IDX))
1337 .arg(gsl_vector_get(x, B_IDX))
1338 .arg(gsl_vector_get(x, C_IDX))
1339 .arg(rcond)
1340 .arg(gsl_blas_dnrm2(f));
1341 };*/
1342
1343 // Start a timer to see how long the solve takes.
1344 QElapsedTimer timer;
1345 timer.start();
1346
1347 // We can sometimes have several attempts to solve based on "goal" and why the solver failed.
1348 // If the goal is STANDARD and we fail to solve then so be it. If the goal is BEST, then retry
1349 // with different parameters to really try and get a solution. A special case is if the solver
1350 // fails on its first step where we will always retry after adjusting parameters. It helps with
1351 // a situation where the solver gets "stuck" failing on first step repeatedly.
1352 for (int attempt = 0; attempt < 5; attempt++)
1353 {
1354 // Make initial guesses - here we just set all parameters to 1.0
1355 parMakeGuess(attempt, dataPoints, guess);
1356
1357 // Load up the weights and guess vectors
1358 if (useWeights)
1359 {
1360 for (int i = 0; i < dataPoints.dps.size(); i++)
1361 gsl_vector_set(weights, i, dataPoints.dps[i].weight);
1362 gsl_multifit_nlinear_winit(guess, weights, &fdf, w);
1363 }
1364 else
1365 gsl_multifit_nlinear_init(guess, &fdf, w);
1366
1367 // Tweak solver parameters from default values
1368 parSetupParams(goal, &params, &numIters, &xtol, &gtol, &ftol);
1369
1370 qCDebug(KSTARS_EKOS_FOCUS) << QString("Starting LM solver, fit=parabola, solver=%1, scale=%2, trs=%3, iters=%4, xtol=%5,"
1371 "gtol=%6, ftol=%7")
1372 .arg(params.solver->name).arg(params.scale->name).arg(params.trs->name).arg(numIters)
1373 .arg(xtol).arg(gtol).arg(ftol);
1374
1375 int info = 0;
1376 int status = gsl_multifit_nlinear_driver(numIters, xtol, gtol, ftol, NULL, NULL, &info, w);
1377
1378 if (status != 0)
1379 {
1380 // Solver failed so determine whether a retry is required.
1381 bool retry = false;
1382 if (goal == BEST)
1383 {
1384 // Pull out all the stops to get a solution
1385 retry = true;
1386 goal = BEST_RETRY;
1387 }
1388 else if (status == GSL_EMAXITER && info == GSL_ENOPROG && gsl_multifit_nlinear_niter(w) <= 1)
1389 // This is a special case where the solver can't take a first step
1390 // So, perturb the initial conditions and have another go.
1391 retry = true;
1392
1393 qCDebug(KSTARS_EKOS_FOCUS) <<
1394 QString("LM solver (Parabola): Failed after %1ms iters=%2 [attempt=%3] with status=%4 [%5] and info=%6 [%7], retry=%8")
1395 .arg(timer.elapsed()).arg(gsl_multifit_nlinear_niter(w)).arg(attempt + 1).arg(status).arg(gsl_strerror(status))
1396 .arg(info).arg(gsl_strerror(info)).arg(retry);
1397 if (!retry)
1398 break;
1399 }
1400 else
1401 {
1402 // All good so store the results - parameters A, B, and C
1403 auto solution = gsl_multifit_nlinear_position(w);
1404 for (int j = 0; j < NUM_PARABOLA_PARAMS; j++)
1405 vc.push_back(gsl_vector_get(solution, j));
1406
1407 qCDebug(KSTARS_EKOS_FOCUS) << QString("LM Solver (Parabola): Solution found after %1ms %2 iters (%3). A=%4, B=%5, C=%6")
1408 .arg(timer.elapsed()).arg(gsl_multifit_nlinear_niter(w)).arg(getLMReasonCode(info)).arg(vc[A_IDX]).arg(vc[B_IDX])
1409 .arg(vc[C_IDX]);
1410 break;
1411 }
1412 }
1413
1414 // Free GSL memory
1415 gsl_multifit_nlinear_free(w);
1416 gsl_vector_free(guess);
1417 gsl_vector_free(weights);
1418
1419 // Restore old GSL error handler
1420 gsl_set_error_handler(oldErrorHandler);
1421
1422 return vc;
1423}
1424
1425// Setup the parameters for parabola curve fitting
1426void CurveFitting::parSetupParams(FittingGoal goal, gsl_multifit_nlinear_parameters *params, int *numIters, double *xtol,
1427 double *gtol, double *ftol)
1428{
1429 // Trust region subproblem
1430 // - gsl_multifit_nlinear_trs_lm is the Levenberg-Marquardt algorithm without acceleration
1431 // - gsl_multifit_nlinear_trs_lmaccel is the Levenberg-Marquardt algorithm with acceleration
1432 // - gsl_multilarge_nlinear_trs_dogleg is the dogleg algorithm
1433 // - gsl_multifit_nlinear_trs_ddogleg is the double dogleg algorithm
1434 // - gsl_multifit_nlinear_trs_subspace2D is the 2D subspace algorithm
1435 params->trs = gsl_multifit_nlinear_trs_lmaccel;
1436
1437 // Scale
1438 // - gsl_multifit_nlinear_scale_more uses. More strategy. Good for problems with parameters with widely different scales
1439 // - gsl_multifit_nlinear_scale_levenberg. Levensberg strategy. Can be good but not scale invariant.
1440 // - gsl_multifit_nlinear_scale_marquardt. Marquardt strategy. Considered inferior to More and Levensberg
1441 params->scale = gsl_multifit_nlinear_scale_more;
1442
1443 // Solver
1444 // - gsl_multifit_nlinear_solver_qr produces reliable results but needs more iterations than Cholesky
1445 // - gsl_multifit_nlinear_solver_cholesky fast but not as stable as QR
1446 // - gsl_multifit_nlinear_solver_mcholesky modified Cholesky more stable than Cholesky
1447
1448 // avmax is the max allowed ratio of 2nd order term (acceleration, a) to the 1st order term (velocity, v)
1449 // GSL defaults to 0.75, but suggests reducing it in the case of convergence problems
1450 switch (goal)
1451 {
1452 case STANDARD:
1453 params->solver = gsl_multifit_nlinear_solver_cholesky;
1454 params->avmax = 0.75;
1455
1456 *numIters = MAX_ITERATIONS_CURVE;
1457 *xtol = INEPSXTOL;
1458 *gtol = INEPSGTOL;
1459 *ftol = INEPSFTOL;
1460 break;
1461 case BEST:
1462 params->solver = gsl_multifit_nlinear_solver_cholesky;
1463 params->avmax = 0.75;
1464
1465 *numIters = MAX_ITERATIONS_CURVE * 2.0;
1466 *xtol = INEPSXTOL / 10.0;
1467 *gtol = INEPSGTOL / 10.0;
1468 *ftol = INEPSFTOL / 10.0;
1469 break;
1470 case BEST_RETRY:
1471 params->solver = gsl_multifit_nlinear_solver_qr;
1472 params->avmax = 0.5;
1473
1474 *numIters = MAX_ITERATIONS_CURVE * 2.0;
1475 *xtol = INEPSXTOL;
1476 *gtol = INEPSGTOL;
1477 *ftol = INEPSFTOL;
1478 break;
1479 default:
1480 break;
1481 }
1482}
1483
1484// Initialise parameters before starting the solver. Its important to start with a guess as near to the solution as possible
1485// If we found a solution before and we're just adding more datapoints use the last solution as the guess.
1486// If we don't have a solution use the datapoints to approximate. Work out the min and max points and use these
1487// to find "close" values for the starting point parameters
1488void CurveFitting::parMakeGuess(const int attempt, const DataPointT &dataPoints, gsl_vector * guess)
1489{
1490 // If we are retrying then perturb the initial conditions. The hope is that by doing this the solver
1491 // will be nudged to find a solution this time
1492 double perturbation = 1.0 + pow(-1, attempt) * (attempt * 0.1);
1493
1494 if (!m_FirstSolverRun && (m_LastCurveType == FOCUS_PARABOLA) && (m_LastCoefficients.size() == NUM_PARABOLA_PARAMS))
1495 {
1496 // Last run of the solver was a Parabola and that solution was good, so use that solution
1497 gsl_vector_set(guess, A_IDX, m_LastCoefficients[A_IDX] * perturbation);
1498 gsl_vector_set(guess, B_IDX, m_LastCoefficients[B_IDX] * perturbation);
1499 gsl_vector_set(guess, C_IDX, m_LastCoefficients[C_IDX] * perturbation);
1500 }
1501 else
1502 {
1503 double minX = dataPoints.dps[0].x;
1504 double minY = dataPoints.dps[0].y;
1505 double maxX = minX;
1506 double maxY = minY;
1507 for(int i = 0; i < dataPoints.dps.size(); i++)
1508 {
1509 if (dataPoints.dps[i].y <= 0.0)
1510 continue;
1511 if(minY <= 0.0 || dataPoints.dps[i].y < minY)
1512 {
1513 minX = dataPoints.dps[i].x;
1514 minY = dataPoints.dps[i].y;
1515 }
1516 if(maxY <= 0.0 || dataPoints.dps[i].y > maxY)
1517 {
1518 maxX = dataPoints.dps[i].x;
1519 maxY = dataPoints.dps[i].y;
1520 }
1521 }
1522 double A, B, C;
1523 if (dataPoints.dir == OPTIMISATION_MAXIMISE)
1524 {
1525 // Equation y = f(x) = a + b((x - c) ^2)
1526 // For a maximum b < 0 and a > 0
1527 // At the maximum, Xmax = c, Ymax = a
1528 // Far from the maximum, b = (Ymin - a) / ((Xmin - c) ^2)
1529 A = maxY * perturbation;
1530 B = ((minY - maxY) / pow(minX - maxX, 2.0)) * perturbation;
1531 C = maxX * perturbation;
1532 }
1533 else
1534 {
1535 // For a minimum b > 0 and a > 0
1536 // At the minimum, Xmin = c, Ymin = a
1537 // Far from the minimum, b = (Ymax - a) / ((Xmax - c) ^2)
1538 A = minY * perturbation;
1539 B = ((maxY - minY) / pow(maxX - minX, 2.0)) * perturbation;
1540 C = minX * perturbation;
1541 }
1542 qCDebug(KSTARS_EKOS_FOCUS) << QString("LM solver (par) initial params: perturbation=%1, A=%2, B=%3, C=%4")
1543 .arg(perturbation).arg(A).arg(B).arg(C);
1544
1545 gsl_vector_set(guess, A_IDX, A);
1546 gsl_vector_set(guess, B_IDX, B);
1547 gsl_vector_set(guess, C_IDX, C);
1548 }
1549}
1550
1551QVector<double> CurveFitting::gaussian_fit(DataPoint3DT data, const StarParams &starParams)
1552{
1553 QVector<double> vc;
1554
1555 // Set the gsl error handler off as it aborts the program on error.
1556 auto const oldErrorHandler = gsl_set_error_handler_off();
1557
1558 // Setup variables to be used by the solver
1559 gsl_multifit_nlinear_parameters params = gsl_multifit_nlinear_default_parameters();
1560 gsl_multifit_nlinear_workspace* w = gsl_multifit_nlinear_alloc (gsl_multifit_nlinear_trust, &params, data.dps.size(),
1561 NUM_GAUSSIAN_PARAMS);
1562 gsl_multifit_nlinear_fdf fdf;
1563 int numIters;
1564 double xtol, gtol, ftol;
1565
1566 // Fill in function info
1567 fdf.f = gauFxy;
1568 fdf.df = gauJxy;
1569 fdf.fvv = gauFxyxy;
1570 fdf.n = data.dps.size();
1571 fdf.p = NUM_GAUSSIAN_PARAMS;
1572 fdf.params = &data;
1573
1574 // Allocate the guess vector
1575 gsl_vector * guess = gsl_vector_alloc(NUM_GAUSSIAN_PARAMS);
1576 // Allocate weights vector
1577 auto weights = gsl_vector_alloc(data.dps.size());
1578
1579 // Setup a timer to see how long the solve takes
1580 QElapsedTimer timer;
1581 timer.start();
1582
1583 // Setup for multiple solve attempts. We won't worry too much if the solver fails as there should be
1584 // plenty of stars, but if the solver fails on its first step then adjust parameters and retry as this
1585 // is a fast thing to do.
1586 for (int attempt = 0; attempt < 5; attempt++)
1587 {
1588 // Make initial guesses
1589 gauMakeGuess(attempt, starParams, guess);
1590
1591 // If using weights load up the GSL vector
1592 if (data.useWeights)
1593 {
1594 QVectorIterator<DataPT3D> dp(data.dps);
1595 int i = 0;
1596 while (dp.hasNext())
1597 gsl_vector_set(weights, i++, dp.next().weight);
1598
1599 gsl_multifit_nlinear_winit(guess, weights, &fdf, w);
1600 }
1601 else
1602 gsl_multifit_nlinear_init(guess, &fdf, w);
1603
1604 // This is the callback used by the LM solver to allow some introspection of each iteration
1605 // Useful for debugging but clogs up the log
1606 // To activate, uncomment the callback lambda and change the call to gsl_multifit_nlinear_driver
1607 /*auto callback = [](const size_t iter, void* _params, const gsl_multifit_nlinear_workspace * w)
1608 {
1609 gsl_vector *f = gsl_multifit_nlinear_residual(w);
1610 gsl_vector *x = gsl_multifit_nlinear_position(w);
1611 double rcond;
1612
1613 // compute reciprocal condition number of J(x)
1614 gsl_multifit_nlinear_rcond(&rcond, w);
1615
1616 // ratio of accel component to velocity component
1617 double avratio = gsl_multifit_nlinear_avratio(w);
1618
1619 qCDebug(KSTARS_EKOS_FOCUS) <<
1620 QString("iter %1: A = %2, B = %3, C = %4, D = %5, E = %6, F = %7, G = %8 "
1621 "rcond(J) = %9, avratio = %10, |f(x)| = %11")
1622 .arg(iter)
1623 .arg(gsl_vector_get(x, A_IDX))
1624 .arg(gsl_vector_get(x, B_IDX))
1625 .arg(gsl_vector_get(x, C_IDX))
1626 .arg(gsl_vector_get(x, D_IDX))
1627 .arg(gsl_vector_get(x, E_IDX))
1628 .arg(gsl_vector_get(x, F_IDX))
1629 .arg(gsl_vector_get(x, G_IDX))
1630 //.arg(1.0 / rcond)
1631 .arg(rcond)
1632 .arg(avratio)
1633 .arg(gsl_blas_dnrm2(f));
1634 };*/
1635
1636 gauSetupParams(&params, &numIters, &xtol, &gtol, &ftol);
1637
1638 qCDebug(KSTARS_EKOS_FOCUS) << QString("Starting LM solver, fit=gaussian, solver=%1, scale=%2, trs=%3, iters=%4, xtol=%5,"
1639 "gtol=%6, ftol=%7")
1640 .arg(params.solver->name).arg(params.scale->name).arg(params.trs->name).arg(numIters)
1641 .arg(xtol).arg(gtol).arg(ftol);
1642
1643 int info = 0;
1644 int status = gsl_multifit_nlinear_driver(numIters, xtol, gtol, ftol, NULL, NULL, &info, w);
1645
1646 if (status != 0)
1647 {
1648 qCDebug(KSTARS_EKOS_FOCUS) <<
1649 QString("LM solver (Gaussian): Failed after %1ms iters=%2 [attempt=%3] with status=%4 [%5] and info=%6 [%7]")
1650 .arg(timer.elapsed()).arg(gsl_multifit_nlinear_niter(w)).arg(attempt + 1).arg(status).arg(gsl_strerror(status))
1651 .arg(info).arg(gsl_strerror(info));
1652 if (status != GSL_EMAXITER || info != GSL_ENOPROG || gsl_multifit_nlinear_niter(w) > 1)
1653 break;
1654 }
1655 else
1656 {
1657 // All good so store the results - parameters A, B, C, D, E, F, G
1658 auto solution = gsl_multifit_nlinear_position(w);
1659 for (int j = 0; j < NUM_GAUSSIAN_PARAMS; j++)
1660 vc.push_back(gsl_vector_get(solution, j));
1661
1662 qCDebug(KSTARS_EKOS_FOCUS) << QString("LM Solver (Gaussian): Solution found after %1ms %2 iters (%3). A=%4, B=%5, C=%6, "
1663 "D=%7, E=%8, F=%9, G=%10").arg(timer.elapsed()).arg(gsl_multifit_nlinear_niter(w)).arg(getLMReasonCode(info))
1664 .arg(vc[A_IDX]).arg(vc[B_IDX]).arg(vc[C_IDX]).arg(vc[D_IDX]).arg(vc[E_IDX])
1665 .arg(vc[F_IDX]).arg(vc[G_IDX]);
1666 break;
1667 }
1668 }
1669
1670 // Free GSL memory
1671 gsl_multifit_nlinear_free(w);
1672 gsl_vector_free(guess);
1673 gsl_vector_free(weights);
1674
1675 // Restore old GSL error handler
1676 gsl_set_error_handler(oldErrorHandler);
1677
1678 return vc;
1679}
1680
1681// Initialise parameters before starting the solver. Its important to start with a guess as near to the solution as possible
1682// Since we have already run some HFR calcs on the star, use these values to calculate the guess
1683void CurveFitting::gauMakeGuess(const int attempt, const StarParams &starParams, gsl_vector * guess)
1684{
1685 // If we are retrying then perturb the initial conditions. The hope is that by doing this the solver
1686 // will be nudged to find a solution this time
1687 double perturbation = 1.0 + pow(-1, attempt) * (attempt * 0.1);
1688
1689 // Default from the input star details
1690 const double a = std::max(starParams.peak, 0.0) * perturbation; // Peak value
1691 const double x0 = std::max(starParams.centroid_x, 0.0) * perturbation; // Centroid x
1692 const double y0 = std::max(starParams.centroid_y, 0.0) * perturbation; // Centroid y
1693 const double b = std::max(starParams.background, 0.0) * perturbation; // Background
1694
1695 double A = 1.0, B = 0.0, C = 1.0;
1696 if (starParams.HFR > 0.0)
1697 {
1698 // Use 2*HFR value as FWHM along with theta to calc A, B, C
1699 // FWHM = 2.sqrt(2.ln(2)).sigma
1700 // Assume circular symmetry so B = 0
1701 const double sigma2 = pow(starParams.HFR / (sqrt(2.0 * log(2.0))), 2.0);
1702 const double costheta2 = pow(cos(starParams.theta), 2.0);
1703 const double sintheta2 = pow(sin(starParams.theta), 2.0);
1704
1705 A = C = (costheta2 + sintheta2) / (2 * sigma2) * perturbation;
1706 }
1707
1708 qCDebug(KSTARS_EKOS_FOCUS) <<
1709 QString("LM Solver (Gaussian): Guess perturbation=%1, A=%2, B=%3, C=%4, D=%5, E=%6, F=%7, G=%8")
1710 .arg(perturbation).arg(a).arg(x0).arg(y0).arg(A).arg(B).arg(C).arg(b);
1711
1712 gsl_vector_set(guess, A_IDX, a);
1713 gsl_vector_set(guess, B_IDX, x0);
1714 gsl_vector_set(guess, C_IDX, y0);
1715 gsl_vector_set(guess, D_IDX, A);
1716 gsl_vector_set(guess, E_IDX, B);
1717 gsl_vector_set(guess, F_IDX, C);
1718 gsl_vector_set(guess, G_IDX, b);
1719}
1720
1721// Setup the parameters for gaussian curve fitting
1722void CurveFitting::gauSetupParams(gsl_multifit_nlinear_parameters *params, int *numIters, double *xtol, double *gtol,
1723 double *ftol)
1724{
1725 // Trust region subproblem
1726 // - gsl_multifit_nlinear_trs_lm is the Levenberg-Marquardt algorithm without acceleration
1727 // - gsl_multifit_nlinear_trs_lmaccel is the Levenberg-Marquardt algorithm with acceleration
1728 // - gsl_multilarge_nlinear_trs_dogleg is the dogleg algorithm
1729 // - gsl_multifit_nlinear_trs_ddogleg is the double dogleg algorithm
1730 // - gsl_multifit_nlinear_trs_subspace2D is the 2D subspace algorithm
1731 params->trs = gsl_multifit_nlinear_trs_lmaccel;
1732
1733 // avmax is the max allowed ratio of 2nd order term (acceleration, a) to the 1st order term (velocity, v)
1734 // GSL defaults to 0.75
1735 params->avmax = 0.75;
1736
1737 // Scale
1738 // - gsl_multifit_nlinear_scale_more uses. More strategy. Good for problems with parameters with widely different scales
1739 // - gsl_multifit_nlinear_scale_levenberg. Levensberg strategy. Can be good but not scale invariant.
1740 // - gsl_multifit_nlinear_scale_marquardt. Marquardt strategy. Considered inferior to More and Levensberg
1741 params->scale = gsl_multifit_nlinear_scale_more;
1742
1743 // Solver
1744 // - gsl_multifit_nlinear_solver_qr produces reliable results but needs more iterations than Cholesky
1745 // - gsl_multifit_nlinear_solver_cholesky fast but not as stable as QR
1746 // - gsl_multifit_nlinear_solver_mcholesky modified Cholesky more stable than Cholesky
1747 params->solver = gsl_multifit_nlinear_solver_qr;
1748
1749 *numIters = MAX_ITERATIONS_STARS;
1750 *xtol = 1e-5;
1751 *gtol = INEPSGTOL;
1752 *ftol = 1e-5;
1753}
1754
1755// Curve fit a 3D plane
1756QVector<double> CurveFitting::plane_fit(const DataPoint3DT data)
1757{
1758 QVector<double> vc;
1759
1760 // Set the gsl error handler off as it aborts the program on error.
1761 auto const oldErrorHandler = gsl_set_error_handler_off();
1762
1763 // Setup variables to be used by the solver
1764 gsl_multifit_nlinear_parameters params = gsl_multifit_nlinear_default_parameters();
1765 gsl_multifit_nlinear_workspace* w = gsl_multifit_nlinear_alloc (gsl_multifit_nlinear_trust, &params, data.dps.size(),
1766 NUM_PLANE_PARAMS);
1767 gsl_multifit_nlinear_fdf fdf;
1768 int numIters;
1769 double xtol, gtol, ftol;
1770
1771 // Fill in function info
1772 fdf.f = plaFxy;
1773 fdf.df = plaJxy;
1774 fdf.fvv = plaFxyxy;
1775 fdf.n = data.dps.size();
1776 fdf.p = NUM_PLANE_PARAMS;
1777 fdf.params = (void *) &data;
1778
1779 // Allocate the guess vector
1780 gsl_vector * guess = gsl_vector_alloc(NUM_PLANE_PARAMS);
1781 // Allocate weights vector
1782 auto weights = gsl_vector_alloc(data.dps.size());
1783
1784 // Setup a timer to see how long the solve takes
1785 QElapsedTimer timer;
1786 timer.start();
1787
1788 // Setup for multiple solve attempts.
1789 for (int attempt = 0; attempt < 5; attempt++)
1790 {
1791 // Make initial guesses
1792 plaMakeGuess(attempt, guess);
1793
1794 // If using weights load up the GSL vector
1795 if (data.useWeights)
1796 {
1797 QVectorIterator<DataPT3D> dp(data.dps);
1798 int i = 0;
1799 while (dp.hasNext())
1800 gsl_vector_set(weights, i++, dp.next().weight);
1801
1802 gsl_multifit_nlinear_winit(guess, weights, &fdf, w);
1803 }
1804 else
1805 gsl_multifit_nlinear_init(guess, &fdf, w);
1806
1807 // This is the callback used by the LM solver to allow some introspection of each iteration
1808 // Useful for debugging but clogs up the log
1809 // To activate, uncomment the callback lambda and change the call to gsl_multifit_nlinear_driver
1810 /*auto callback = [](const size_t iter, void* _params, const gsl_multifit_nlinear_workspace * w)
1811 {
1812 gsl_vector *f = gsl_multifit_nlinear_residual(w);
1813 gsl_vector *x = gsl_multifit_nlinear_position(w);
1814 double rcond;
1815
1816 // compute reciprocal condition number of J(x)
1817 gsl_multifit_nlinear_rcond(&rcond, w);
1818
1819 // ratio of accel component to velocity component
1820 double avratio = gsl_multifit_nlinear_avratio(w);
1821
1822 qCDebug(KSTARS_EKOS_FOCUS) <<
1823 QString("iter %1: A = %2, B = %3, C = %4"
1824 "rcond(J) = %5, avratio = %6, |f(x)| = %7")
1825 .arg(iter)
1826 .arg(gsl_vector_get(x, A_IDX))
1827 .arg(gsl_vector_get(x, B_IDX))
1828 .arg(gsl_vector_get(x, C_IDX))
1829 //.arg(1.0 / rcond)
1830 .arg(rcond)
1831 .arg(avratio)
1832 .arg(gsl_blas_dnrm2(f));
1833 };*/
1834
1835 plaSetupParams(&params, &numIters, &xtol, &gtol, &ftol);
1836
1837 qCDebug(KSTARS_EKOS_FOCUS) << QString("Starting LM solver, fit=plane, solver=%1, scale=%2, trs=%3, iters=%4, xtol=%5,"
1838 "gtol=%6, ftol=%7")
1839 .arg(params.solver->name).arg(params.scale->name).arg(params.trs->name).arg(numIters)
1840 .arg(xtol).arg(gtol).arg(ftol);
1841
1842 int info = 0;
1843 int status = gsl_multifit_nlinear_driver(numIters, xtol, gtol, ftol, NULL, NULL, &info, w);
1844
1845 if (status != 0)
1846 {
1847 qCDebug(KSTARS_EKOS_FOCUS) <<
1848 QString("LM solver (Plane): Failed after %1ms iters=%2 [attempt=%3] with status=%4 [%5] and info=%6 [%7]")
1849 .arg(timer.elapsed()).arg(gsl_multifit_nlinear_niter(w)).arg(attempt + 1).arg(status).arg(gsl_strerror(status))
1850 .arg(info).arg(gsl_strerror(info));
1851 if (status != GSL_EMAXITER || info != GSL_ENOPROG || gsl_multifit_nlinear_niter(w) > 1)
1852 break;
1853 }
1854 else
1855 {
1856 // All good so store the results - parameters A, B, C
1857 auto solution = gsl_multifit_nlinear_position(w);
1858 for (int j = 0; j < NUM_PLANE_PARAMS; j++)
1859 vc.push_back(gsl_vector_get(solution, j));
1860
1861 qCDebug(KSTARS_EKOS_FOCUS) << QString("LM Solver (Plane): Solution found after %1ms %2 iters (%3). A=%4, B=%5, C=%6")
1862 .arg(timer.elapsed()).arg(gsl_multifit_nlinear_niter(w)).arg(getLMReasonCode(info))
1863 .arg(vc[A_IDX]).arg(vc[B_IDX]).arg(vc[C_IDX]);
1864 break;
1865 }
1866 }
1867
1868 // Free GSL memory
1869 gsl_multifit_nlinear_free(w);
1870 gsl_vector_free(guess);
1871 gsl_vector_free(weights);
1872
1873 // Restore old GSL error handler
1874 gsl_set_error_handler(oldErrorHandler);
1875
1876 return vc;
1877}
1878
1879// Initialise parameters before starting the solver. Its important to start with a guess as near to the solution as possible
1880void CurveFitting::plaMakeGuess(const int attempt, gsl_vector * guess)
1881{
1882 double A, B, C;
1883 // If we are retrying then perturb the initial conditions. The hope is that by doing this the solver
1884 // will be nudged to find a solution this time
1885 const double perturbation = 1.0 + pow(-1, attempt) * (attempt * 0.1);
1886
1887 if (!m_FirstSolverRun && (m_LastCurveType == FOCUS_PLANE) && (m_LastCoefficients.size() == NUM_PLANE_PARAMS) && attempt < 3)
1888 {
1889 // Last run of the solver was a Plane and that solution was good, so use that solution
1890 A = m_LastCoefficients[A_IDX] * perturbation;
1891 B = m_LastCoefficients[B_IDX] * perturbation;
1892 C = m_LastCoefficients[C_IDX] * perturbation;
1893 }
1894 else
1895 {
1896 A = perturbation;
1897 B = perturbation;
1898 C = perturbation;
1899 }
1900
1901 qCDebug(KSTARS_EKOS_FOCUS) << QString("LM Solver (Plane): Guess perturbation=%1, A=%2, B=%3, C=%4")
1902 .arg(perturbation).arg(A).arg(B).arg(C);
1903
1904 gsl_vector_set(guess, A_IDX, A);
1905 gsl_vector_set(guess, B_IDX, B);
1906 gsl_vector_set(guess, C_IDX, C);
1907}
1908
1909// Setup the parameters for plane curve fitting
1910void CurveFitting::plaSetupParams(gsl_multifit_nlinear_parameters *params, int *numIters, double *xtol, double *gtol,
1911 double *ftol)
1912{
1913 // Trust region subproblem
1914 // - gsl_multifit_nlinear_trs_lm is the Levenberg-Marquardt algorithm without acceleration
1915 // - gsl_multifit_nlinear_trs_lmaccel is the Levenberg-Marquardt algorithm with acceleration
1916 // - gsl_multilarge_nlinear_trs_dogleg is the dogleg algorithm
1917 // - gsl_multifit_nlinear_trs_ddogleg is the double dogleg algorithm
1918 // - gsl_multifit_nlinear_trs_subspace2D is the 2D subspace algorithm
1919 params->trs = gsl_multifit_nlinear_trs_lm;
1920
1921 // avmax is the max allowed ratio of 2nd order term (acceleration, a) to the 1st order term (velocity, v)
1922 // GSL defaults to 0.75
1923 // params->avmax = 0.75;
1924
1925 // Scale
1926 // - gsl_multifit_nlinear_scale_more uses. More strategy. Good for problems with parameters with widely different scales
1927 // - gsl_multifit_nlinear_scale_levenberg. Levensberg strategy. Can be good but not scale invariant.
1928 // - gsl_multifit_nlinear_scale_marquardt. Marquardt strategy. Considered inferior to More and Levensberg
1929 params->scale = gsl_multifit_nlinear_scale_more;
1930
1931 // Solver
1932 // - gsl_multifit_nlinear_solver_qr produces reliable results but needs more iterations than Cholesky
1933 // - gsl_multifit_nlinear_solver_cholesky fast but not as stable as QR
1934 // - gsl_multifit_nlinear_solver_mcholesky modified Cholesky more stable than Cholesky
1935 params->solver = gsl_multifit_nlinear_solver_qr;
1936
1937 *numIters = MAX_ITERATIONS_PLANE;
1938 *xtol = 1e-5;
1939 *gtol = INEPSGTOL;
1940 *ftol = 1e-5;
1941}
1942
1943bool CurveFitting::findMinMax(double expected, double minPosition, double maxPosition, double *position, double *value,
1944 CurveFit curveFit, const OptimisationDirection optDir)
1945{
1946 bool foundFit;
1947 switch (curveFit)
1948 {
1949 case FOCUS_QUADRATIC :
1950 foundFit = minimumQuadratic(expected, minPosition, maxPosition, position, value);
1951 break;
1952 case FOCUS_HYPERBOLA :
1953 foundFit = minMaxHyperbola(expected, minPosition, maxPosition, position, value, optDir);
1954 break;
1955 case FOCUS_PARABOLA :
1956 foundFit = minMaxParabola(expected, minPosition, maxPosition, position, value, optDir);
1957 break;
1958 default :
1959 foundFit = false;
1960 break;
1961 }
1962 if (!foundFit)
1963 // 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
1964 m_LastCoefficients.clear();
1965 return foundFit;
1966}
1967
1968bool CurveFitting::minimumQuadratic(double expected, double minPosition, double maxPosition, double *position,
1969 double *value)
1970{
1971 int status;
1972 int iter = 0, max_iter = 100;
1973 const gsl_min_fminimizer_type *T;
1974 gsl_min_fminimizer *s;
1975 double m = expected;
1976
1977 gsl_function F;
1978 F.function = &CurveFitting::curveFunction;
1979 F.params = this;
1980
1981 // Must turn off error handler or it aborts on error
1982 gsl_set_error_handler_off();
1983
1984 T = gsl_min_fminimizer_brent;
1985 s = gsl_min_fminimizer_alloc(T);
1986 status = gsl_min_fminimizer_set(s, &F, expected, minPosition, maxPosition);
1987
1988 if (status != GSL_SUCCESS)
1989 {
1990 qCWarning(KSTARS_EKOS_FOCUS) << "Focus GSL error:" << gsl_strerror(status);
1991 return false;
1992 }
1993
1994 do
1995 {
1996 iter++;
1997 status = gsl_min_fminimizer_iterate(s);
1998
1999 m = gsl_min_fminimizer_x_minimum(s);
2000 minPosition = gsl_min_fminimizer_x_lower(s);
2001 maxPosition = gsl_min_fminimizer_x_upper(s);
2002
2003 status = gsl_min_test_interval(minPosition, maxPosition, 0.01, 0.0);
2004
2005 if (status == GSL_SUCCESS)
2006 {
2007 *position = m;
2008 *value = curveFunction(m, this);
2009 }
2010 }
2011 while (status == GSL_CONTINUE && iter < max_iter);
2012
2013 gsl_min_fminimizer_free(s);
2014 return (status == GSL_SUCCESS);
2015}
2016
2017bool CurveFitting::minMaxHyperbola(double expected, double minPosition, double maxPosition, double *position,
2018 double *value, const OptimisationDirection optDir)
2019{
2020 Q_UNUSED(expected);
2021 if (m_coefficients.size() != NUM_HYPERBOLA_PARAMS)
2022 {
2023 if (m_coefficients.size() != 0)
2024 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::minimumHyperbola coefficients.size()=%1").arg(
2025 m_coefficients.size());
2026 return false;
2027 }
2028
2029 double b = m_coefficients[B_IDX];
2030 double c = m_coefficients[C_IDX];
2031 double d = m_coefficients[D_IDX];
2032
2033 // We need to check that the solution found is in the correct form.
2034 // Check 1: The hyperbola minimum (=c) lies within the bounds of the focuser (and is > 0)
2035 // Check 2: At the minimum position (=c), the value of f(x) (which is the HFR) given by b+d is > 0
2036 // Check 3: For a minimum: we have a "u" shaped curve, not an "n" shape. b > 0.
2037 // maximum: we have an "n" shaped curve, not a "U" shape. b < 0;
2038 if ((c >= minPosition) && (c <= maxPosition) && (b + d > 0.0) &&
2039 ((optDir == OPTIMISATION_MINIMISE && b > 0.0) || (optDir == OPTIMISATION_MAXIMISE && b < 0.0)))
2040 {
2041 *position = c;
2042 *value = b + d;
2043 return true;
2044 }
2045 else
2046 return false;
2047}
2048
2049bool CurveFitting::minMaxParabola(double expected, double minPosition, double maxPosition, double *position,
2050 double *value, const OptimisationDirection optDir)
2051{
2052 Q_UNUSED(expected);
2053 if (m_coefficients.size() != NUM_PARABOLA_PARAMS)
2054 {
2055 if (m_coefficients.size() != 0)
2056 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::minimumParabola coefficients.size()=%1").arg(
2057 m_coefficients.size());
2058 return false;
2059 }
2060
2061 double a = m_coefficients[A_IDX];
2062 double b = m_coefficients[B_IDX];
2063 double c = m_coefficients[C_IDX];
2064
2065 // We need to check that the solution found is in the correct form.
2066 // Check 1: The parabola minimum (=c) lies within the bounds of the focuser (and is > 0)
2067 // Check 2: At the minimum position (=c), the value of f(x) (which is the HFR) given by a is > 0
2068 // Check 3: For a minimum: we have a "u" shaped curve, not an "n" shape. b > 0.
2069 // maximum: we have an "n" shaped curve, not a "U" shape. b < 0;
2070 if ((c >= minPosition) && (c <= maxPosition) && (a > 0.0) &&
2071 ((optDir == OPTIMISATION_MINIMISE && b > 0.0) || (optDir == OPTIMISATION_MAXIMISE && b < 0.0)))
2072 {
2073 *position = c;
2074 *value = a;
2075 return true;
2076 }
2077 else
2078 return false;
2079}
2080
2081bool CurveFitting::getStarParams(const CurveFit curveFit, StarParams *starParams)
2082{
2083 bool foundFit;
2084 switch (curveFit)
2085 {
2086 case FOCUS_GAUSSIAN :
2087 foundFit = getGaussianParams(starParams);
2088 break;
2089 default :
2090 foundFit = false;
2091 break;
2092 }
2093 if (!foundFit)
2094 // 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
2095 m_LastCoefficients.clear();
2096 return foundFit;
2097}
2098
2099// Having solved for a Gaussian return params
2100bool CurveFitting::getGaussianParams(StarParams *starParams)
2101{
2102 if (m_coefficients.size() != NUM_GAUSSIAN_PARAMS)
2103 {
2104 if (m_coefficients.size() != 0)
2105 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::getGaussianParams coefficients.size()=%1").arg(
2106 m_coefficients.size());
2107 return false;
2108 }
2109
2110 const double a = m_coefficients[A_IDX];
2111 const double x0 = m_coefficients[B_IDX];
2112 const double y0 = m_coefficients[C_IDX];
2113 const double A = m_coefficients[D_IDX];
2114 const double B = m_coefficients[E_IDX];
2115 const double C = m_coefficients[F_IDX];
2116 const double b = m_coefficients[G_IDX];
2117
2118 // Sanity check the coefficients in case the solver produced a bad solution
2119 if (a <= 0.0 || b <= 0.0 || x0 <= 0.0 || y0 <= 0.0)
2120 {
2121 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::getGaussianParams a=%1 b=%2 x0=%3 y0=%4").arg(a).arg(b)
2122 .arg(x0).arg(y0);
2123 return false;
2124 }
2125
2126 const double AmC = A - C;
2127 double theta;
2128 abs(AmC) < 1e-10 ? theta = 0.0 : theta = 0.5 * atan(2 * B / AmC);
2129 const double costheta = cos(theta);
2130 const double costheta2 = costheta * costheta;
2131 const double sintheta = sin(theta);
2132 const double sintheta2 = sintheta * sintheta;
2133
2134 double sigmax2 = 0.5 / ((A * costheta2) + (2 * B * costheta * sintheta) + (C * sintheta2));
2135 double sigmay2 = 0.5 / ((A * costheta2) - (2 * B * costheta * sintheta) + (C * sintheta2));
2136
2137 double FWHMx = 2 * pow(2 * log(2) * sigmax2, 0.5);
2138 double FWHMy = 2 * pow(2 * log(2) * sigmay2, 0.5);
2139 double FWHM = (FWHMx + FWHMy) / 2.0;
2140
2141 if (isnan(FWHM) || FWHM < 0.0)
2142 {
2143 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::getGaussianParams FWHM=%1").arg(FWHM);
2144 return false;
2145 }
2146
2147 starParams->background = b;
2148 starParams->peak = a;
2149 starParams->centroid_x = x0;
2150 starParams->centroid_y = y0;
2151 starParams->theta = theta;
2152 starParams->FWHMx = FWHMx;
2153 starParams->FWHMy = FWHMy;
2154 starParams->FWHM = FWHM;
2155
2156 return true;
2157}
2158
2159// R2 (R squared) is the coefficient of determination gives a measure of how well the curve fits the datapoints.
2160// It lies between 0 and 1. 1 means that all datapoints will lie on the curve which therefore exactly describes the
2161// datapoints. 0 means that there is no correlation between the curve and the datapoints.
2162// See www.wikipedia.org/wiki/Coefficient_of_determination for more details.
2163double CurveFitting::calculateR2(CurveFit curveFit)
2164{
2165 double R2 = 0.0;
2166 QVector<double> dataPoints, curvePoints, scalePoints;
2167 int i;
2168
2169 switch (curveFit)
2170 {
2171 case FOCUS_QUADRATIC :
2172 // Not implemented
2173 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateR2 called for Quadratic");
2174 break;
2175
2176 case FOCUS_HYPERBOLA :
2177 // Calculate R2 for the hyperbola
2178 if (m_coefficients.size() != NUM_HYPERBOLA_PARAMS)
2179 {
2180 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateR2 hyperbola coefficients.size()=").arg(
2181 m_coefficients.size());
2182 break;
2183 }
2184
2185 for (i = 0; i < m_y.size(); i++)
2186 if (!m_outliers[i])
2187 {
2188 // Load up the dataPoints and curvePoints vector, excluding any outliers
2189 dataPoints.push_back(m_y[i]);
2190 curvePoints.push_back(hypfx(m_x[i], m_coefficients[A_IDX], m_coefficients[B_IDX], m_coefficients[C_IDX],
2191 m_coefficients[D_IDX]));
2192 scalePoints.push_back(m_scale[i]);
2193 }
2194
2195 // Do the actual R2 calculation
2196 R2 = calcR2(dataPoints, curvePoints, scalePoints, m_useWeights);
2197 break;
2198
2199 case FOCUS_PARABOLA :
2200 // Calculate R2 for the parabola
2201 if (m_coefficients.size() != NUM_PARABOLA_PARAMS)
2202 {
2203 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateR2 parabola coefficients.size()=").arg(
2204 m_coefficients.size());
2205 break;
2206 }
2207
2208 for (i = 0; i < m_y.size(); i++)
2209 if (!m_outliers[i])
2210 {
2211 dataPoints.push_back(m_y[i]);
2212 // Load up the dataPoints and curvePoints vector, excluding any outliers
2213 curvePoints.push_back(parfx(m_x[i], m_coefficients[A_IDX], m_coefficients[B_IDX], m_coefficients[C_IDX]));
2214 scalePoints.push_back(m_scale[i]);
2215 }
2216
2217 // Do the actual R2 calculation
2218 R2 = calcR2(dataPoints, curvePoints, scalePoints, m_useWeights);
2219 break;
2220
2221 case FOCUS_GAUSSIAN :
2222 // Calculate R2 for the gaussian
2223 if (m_coefficients.size() != NUM_GAUSSIAN_PARAMS)
2224 {
2225 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateR2 gaussian coefficients.size()=").arg(
2226 m_coefficients.size());
2227 break;
2228 }
2229
2230 for (i = 0; i < m_dataPoints.dps.size(); i++)
2231 {
2232 // Load up the curvePoints vector
2233 curvePoints.push_back(gaufxy(m_dataPoints.dps[i].x, m_dataPoints.dps[i].y, m_coefficients[A_IDX],
2234 m_coefficients[B_IDX], m_coefficients[C_IDX], m_coefficients[D_IDX],
2235 m_coefficients[E_IDX], m_coefficients[F_IDX], m_coefficients[G_IDX]));
2236 dataPoints.push_back(static_cast <double> (m_dataPoints.dps[i].z));
2237 }
2238
2239 // Do the actual R2 calculation
2240 R2 = calcR2(dataPoints, curvePoints, m_scale, m_dataPoints.useWeights);
2241 break;
2242
2243 case FOCUS_PLANE :
2244 // Calculate R2 for the plane
2245 if (m_coefficients.size() != NUM_PLANE_PARAMS)
2246 {
2247 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateR2 plane coefficients.size()=").arg(
2248 m_coefficients.size());
2249 break;
2250 }
2251
2252 for (i = 0; i < m_dataPoints.dps.size(); i++)
2253 {
2254 // Load up the curvePoints vector
2255 curvePoints.push_back(plafxy(m_dataPoints.dps[i].x, m_dataPoints.dps[i].y, m_coefficients[A_IDX],
2256 m_coefficients[B_IDX], m_coefficients[C_IDX]));
2257 dataPoints.push_back(static_cast <double> (m_dataPoints.dps[i].z));
2258 }
2259
2260 // Do the actual R2 calculation
2261 R2 = calcR2(dataPoints, curvePoints, m_scale, m_dataPoints.useWeights);
2262 break;
2263
2264 default :
2265 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateR2 curveFit=%1").arg(curveFit);
2266 break;
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// Do the maths to calculate R2 - how well the curve fits the datapoints
2274double CurveFitting::calcR2(const QVector<double> dataPoints, const QVector<double> curvePoints,
2275 const QVector<double> scale, const bool useWeights)
2276{
2277 double R2 = 0.0, chisq = 0.0, sum = 0.0, totalSumSquares = 0.0, weight, average;
2278
2279 for (int i = 0; i < dataPoints.size(); i++)
2280 {
2281 sum += dataPoints[i];
2282 useWeights ? weight = scale[i] : weight = 1.0;
2283 chisq += weight * pow((dataPoints[i] - curvePoints[i]), 2.0);
2284 }
2285 average = sum / dataPoints.size();
2286
2287 for (int i = 0; i < dataPoints.size(); i++)
2288 {
2289 useWeights ? weight = scale[i] : weight = 1.0;
2290 totalSumSquares += weight * pow((dataPoints[i] - average), 2.0);
2291 }
2292
2293 if (totalSumSquares > 0.0)
2294 R2 = 1 - (chisq / totalSumSquares);
2295 else
2296 {
2297 R2 = 0.0;
2298 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calcR2 tss=%1").arg(totalSumSquares);
2299 }
2300
2301 // R2 for linear function range 0<=R2<=1. For non-linear functions it is possible
2302 // for R2 to be negative. This doesn't really mean anything so force 0 in these situations.
2303 return std::max(R2, 0.0);
2304}
2305
2306void CurveFitting::calculateCurveDeltas(CurveFit curveFit, std::vector<std::pair<int, double>> &curveDeltas)
2307{
2308 curveDeltas.clear();
2309
2310 switch (curveFit)
2311 {
2312 case FOCUS_QUADRATIC :
2313 // Not implemented
2314 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateCurveDeltas called for Quadratic");
2315 break;
2316
2317 case FOCUS_HYPERBOLA :
2318 if (m_coefficients.size() != NUM_HYPERBOLA_PARAMS)
2319 {
2320 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateCurveDeltas hyperbola coefficients.size()=").arg(
2321 m_coefficients.size());
2322 break;
2323 }
2324
2325 for (int i = 0; i < m_y.size(); i++)
2326 if (!m_outliers[i])
2327 curveDeltas.push_back(std::make_pair(i, abs(m_y[i] - hypfx(m_x[i], m_coefficients[A_IDX], m_coefficients[B_IDX],
2328 m_coefficients[C_IDX], m_coefficients[D_IDX]))));
2329 break;
2330
2331 case FOCUS_PARABOLA :
2332 if (m_coefficients.size() != NUM_PARABOLA_PARAMS)
2333 {
2334 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateCurveDeltas parabola coefficients.size()=").arg(
2335 m_coefficients.size());
2336 break;
2337 }
2338
2339 for (int i = 0; i < m_y.size(); i++)
2340 if (!m_outliers[i])
2341 curveDeltas.push_back(std::make_pair(i, abs(m_y[i] - parfx(m_x[i], m_coefficients[A_IDX], m_coefficients[B_IDX],
2342 m_coefficients[C_IDX]))));
2343 break;
2344
2345 case FOCUS_GAUSSIAN :
2346 // Not implemented
2347 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateCurveDeltas called for Gaussian");
2348 break;
2349
2350 default :
2351 qCDebug(KSTARS_EKOS_FOCUS) << QString("Error in CurveFitting::calculateCurveDeltas curveFit=%1").arg(curveFit);
2352 break;
2353 }
2354}
2355
2356namespace
2357{
2358QString serializeDoubleVector(const QVector<double> &vector)
2359{
2360 QString str = QString("%1").arg(vector.size());
2361 for (const double v : vector)
2362 str.append(QString(";%1").arg(v, 0, 'g', 12));
2363 return str;
2364}
2365
2366bool decodeDoubleVector(const QString serialized, QVector<double> *vector)
2367{
2368 vector->clear();
2369 const QStringList parts = serialized.split(';');
2370 if (parts.size() == 0) return false;
2371 bool ok;
2372 int size = parts[0].toInt(&ok);
2373 if (!ok || (size != parts.size() - 1)) return false;
2374 for (int i = 0; i < size; ++i)
2375 {
2376 const double val = parts[i + 1].toDouble(&ok);
2377 if (!ok) return false;
2378 vector->append(val);
2379 }
2380 return true;
2381}
2382}
2383
2384QString CurveFitting::serialize() const
2385{
2386 QString serialized = "";
2387 if (m_FirstSolverRun) return serialized;
2388 serialized = QString("%1").arg(int(m_CurveType));
2389 serialized.append(QString("|%1").arg(serializeDoubleVector(m_x)));
2390 serialized.append(QString("|%1").arg(serializeDoubleVector(m_y)));
2391 serialized.append(QString("|%1").arg(serializeDoubleVector(m_scale)));
2392 serialized.append(QString("|%1").arg(m_useWeights ? "T" : "F"));
2393 // m_dataPoints not implemented. Not needed for graphing.
2394 serialized.append(QString("|m_dataPoints not implemented"));
2395 serialized.append(QString("|%1").arg(serializeDoubleVector(m_coefficients)));
2396 serialized.append(QString("|%1").arg(int(m_LastCurveType)));
2397 serialized.append(QString("|%1").arg(serializeDoubleVector(m_LastCoefficients)));
2398 return serialized;
2399}
2400
2401bool CurveFitting::recreateFromQString(const QString &serialized)
2402{
2403 QStringList parts = serialized.split('|');
2404 bool ok = false;
2405 if (parts.size() != 9) return false;
2406
2407 int val = parts[0].toInt(&ok);
2408 if (!ok) return false;
2409 m_CurveType = static_cast<CurveFit>(val);
2410
2411 if (!decodeDoubleVector(parts[1], &m_x)) return false;
2412 if (!decodeDoubleVector(parts[2], &m_y)) return false;
2413 if (!decodeDoubleVector(parts[3], &m_scale)) return false;
2414
2415 if (parts[4] == "T") m_useWeights = true;
2416 else if (parts[4] == "F") m_useWeights = false;
2417 else return false;
2418
2419 // parts[5]: m_dataPoints not implemented.
2420
2421 if (!decodeDoubleVector(parts[6], &m_coefficients)) return false;
2422
2423 val = parts[7].toInt(&ok);
2424 if (!ok) return false;
2425 m_LastCurveType = static_cast<CurveFit>(val);
2426
2427 if (!decodeDoubleVector(parts[8], &m_LastCoefficients)) return false;
2428
2429 m_FirstSolverRun = false;
2430 return true;
2431}
2432
2433} // namespace
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:79
NETWORKMANAGERQT_EXPORT NetworkManager::Status status()
void append(QList< T > &&value)
void clear()
void push_back(parameter_type value)
qsizetype size() const const
QString & append(QChar ch)
QString arg(Args &&... args) const const
void push_back(QChar ch)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:59:51 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.