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

KDE's Doxygen guidelines are available online.