# Kstars

focusalgorithms.cpp
1/*
2 SPDX-FileCopyrightText: 2019 Hy Murveit <hy-1@murveit.com>
3
5*/
6
7#include "focusalgorithms.h"
8#include "curvefit.h"
9
10#include "polynomialfit.h"
11#include <QVector>
12#include "kstars.h"
13#include "fitsviewer/fitsstardetector.h"
14#include "focusstars.h"
15#include <ekos_focus_debug.h>
16
17namespace Ekos
18{
19/**
20 * @class LinearFocusAlgorithm
21 * @short Autofocus algorithm that linearly samples HFR values.
22 *
23 * @author Hy Murveit
24 * @version 1.0
25 */
26class LinearFocusAlgorithm : public FocusAlgorithmInterface
27{
28 public:
29
30 // Constructor initializes a linear autofocus algorithm, starting at some initial position,
31 // sampling HFR values, decreasing the position by some step size, until the algorithm believe's
32 // it's seen a minimum. It then either:
33 // a) tries to find that minimum in a 2nd pass. This is the LINEAR algorithm, or
34 // b) moves to the minimum. This is the LINEAR 1 PASS algorithm.
35 LinearFocusAlgorithm(const FocusParams &params);
36
37 // After construction, this should be called to get the initial position desired by the
38 // focus algorithm. It returns the initial position passed to the constructor if
39 // it has no movement request.
40 int initialPosition() override
41 {
42 return requestedPosition;
43 }
44
45 // Pass in the measurement for the last requested position. Returns the position for the next
46 // requested measurement, or -1 if the algorithm's done or if there's an error.
47 // If stars is not nullptr, then the relativeHFR scheme is used to modify the HFR value.
48 int newMeasurement(int position, double value, const double starWeight, const QList<Edge*> *stars) override;
49
50 FocusAlgorithmInterface *Copy() override;
51
52 void getMeasurements(QVector<int> *pos, QVector<double> *val, QVector<double> *sds) const override
53 {
54 *pos = positions;
55 *val = values;
56 *sds = weights;
57 }
58
59 void getPass1Measurements(QVector<int> *pos, QVector<double> *val, QVector<double> *sds, QVector<bool> *out) const override
60 {
61 *pos = pass1Positions;
62 *val = pass1Values;
63 *sds = pass1Weights;
64 *out = pass1Outliers;
65 }
66
67 double latestValue() const override
68 {
69 if (values.size() > 0)
70 return values.last();
71 return -1;
72 }
73
74 bool isInFirstPass() const override
75 {
76 if (params.focusAlgorithm == Focus::FOCUS_LINEAR || params.focusAlgorithm == Focus::FOCUS_LINEAR1PASS)
77 return inFirstPass;
78 else
79 return true;
80 }
81
82 QString getTextStatus(double R2 = 0) const override;
83
84 int currentStep() const override
85 {
86 return numSteps;
87 }
88
89 private:
90
91 // Called in newMeasurement. Sets up the next iteration.
92 int completeIteration(int step, bool foundFit, double minPos, double minVal);
93
94 // Does the bookkeeping for the final focus solution.
95 int setupSolution(int position, double value, double sigma);
96
97 // Perform the linear walk for FOCUS_WALK_FIXED_STEPS and FOCUS_WALK_CFZ_SHUFFLE
98 int linearWalk(int position, double value, const double starWeight);
99
100 // Calculate the curve fitting goal based on the algorithm parameters and progress
101 CurveFitting::FittingGoal getGoal(int numSteps);
102
103 // Get the minimum number of datapoints before attempting the first curve fit
104 int getCurveMinPoints();
105
106 // Analyze data for donuts are remove
107 void removeDonuts();
108
109 // Calc the next step size for Linear1Pass for FOCUS_WALK_FIXED_STEPS and FOCUS_WALK_CFZ_SHUFFLE
110 int getNextStepSize();
111
112 // Called when we've found a solution, e.g. the HFR value is within tolerance of the desired value.
113 // It it returns true, then it's decided tht we should try one more sample for a possible improvement.
114 // If it returns false, then "play it safe" and use the current position as the solution.
115 bool setupPendingSolution(int position);
116
117 // Determines the desired focus position for the first sample.
118 void computeInitialPosition();
119
120 // Get the first step focus position
121 int getFirstPosition();
122
123 // Sets the internal state for re-finding the minimum, and returns the requested next
124 // position to sample.
125 // Called by Linear. L1P calls finishFirstPass instead
126 int setupSecondPass(int position, double value, double margin = 2.0);
127
128 // Called by L1P to finish off the first pass, setting state variables.
129 int finishFirstPass(int position, double value);
130
131 // Peirce's criterion
132 // Returns the squared threshold error deviation for outlier identification
133 // using Peirce's criterion based on Gould's methodology.
134 // Arguments:
135 // N = total number of observations
136 // n = max number of outliers to be removed
137 // m = number of model unknowns
138 // Returns: squared error threshold (x2)
139 double peirce_criterion(double N, double n, double m);
140
141 // Used in the 2nd pass. Focus is getting worse. Requires several consecutive samples getting worse.
142 bool gettingWorse();
143
144 // If one of the last 2 samples are as good or better than the 2nd best so far, return true.
145 bool bestSamplesHeuristic();
146
147 // Adds to the debug log a line summarizing the result of running this algorithm.
148 void debugLog();
149
150 // Use the detailed star measurements to adjust the HFR value.
151 // See comment above method definition.
152 double relativeHFR(double origHFR, const QList<Edge*> *stars);
153
154 // Calculate the standard deviation (=sigma) of the star HFRs
155 double calculateStarSigma(const bool useWeights, const QList<Edge*> *stars);
156
157 // Used to time the focus algorithm.
158 QElapsedTimer stopWatch;
159
160 // A vector containing the HFR values sampled by this algorithm so far.
161 QVector<double> values;
162 QVector<double> pass1Values;
163 // A vector containing star weights
164 QVector<double> weights;
165 QVector<double> pass1Weights;
166 // A vector containing the focus positions corresponding to the HFR values stored above.
167 QVector<int> positions;
168 QVector<int> pass1Positions;
169 // A vector containing whether or not the datapoint has been identified as an outlier.
170 QVector<bool> pass1Outliers;
171
172 // Focus position requested by this algorithm the previous step.
173 int requestedPosition;
174 // The position where the first pass is begun. Usually requestedPosition unless there's a restart.
175 int passStartPosition;
176 // Number of iterations processed so far. Used to see it doesn't exceed params.maxIterations.
177 int numSteps;
178 // The best value in the first pass. The 2nd pass attempts to get within
179 // tolerance of this value.
180 double firstPassBestValue;
181 // The position of the minimum found in the first pass.
182 int firstPassBestPosition;
183 // The sampling interval--the recommended number of focuser steps moved inward each iteration
184 // of the first pass.
185 int stepSize;
186 // The minimum focus position to use. Computed from the focuser limits and maxTravel.
187 int minPositionLimit;
188 // The maximum focus position to use. Computed from the focuser limits and maxTravel.
189 int maxPositionLimit;
190 // Counter for the number of times a v-curve minimum above the current position was found,
191 // which implies the initial focus sweep has passed the minimum and should be terminated.
192 int numPolySolutionsFound;
193 // Counter for the number of times a v-curve minimum above the passStartPosition was found,
194 // which implies the sweep should restart at a higher position.
195 int numRestartSolutionsFound;
196 // The index (into values and positions) when the most recent 2nd pass started.
197 int secondPassStartIndex;
198 // True if performing the first focus sweep.
199 bool inFirstPass;
200 // True if the 2nd pass has found a solution, and it's now optimizing the solution.
201 bool solutionPending;
202 // When we're near done, but the HFR just got worse, we may retry the current position
203 // in case the HFR value was noisy.
204 int retryNumber = 0;
205
206 // Keep the solution-pending position/value for the status messages.
207 double solutionPendingValue = 0;
208 int solutionPendingPosition = 0;
209
210 // RelativeHFR variables
211 bool relativeHFREnabled = false;
212 QVector<FocusStars> starLists;
213 double bestRelativeHFR = 0;
214 int bestRelativeHFRIndex = 0;
215};
216
217// Copies the object. Used in testing to examine alternate possible inputs given
218// the current state.
219FocusAlgorithmInterface *LinearFocusAlgorithm::Copy()
220{
221 LinearFocusAlgorithm *alg = new LinearFocusAlgorithm(params);
222 *alg = *this;
223 return dynamic_cast<FocusAlgorithmInterface*>(alg);
224}
225
226FocusAlgorithmInterface *MakeLinearFocuser(const FocusAlgorithmInterface::FocusParams &params)
227{
228 return new LinearFocusAlgorithm(params);
229}
230
231LinearFocusAlgorithm::LinearFocusAlgorithm(const FocusParams &focusParams)
232 : FocusAlgorithmInterface(focusParams)
233{
234 stopWatch.start();
235 // These variables don't get reset if we restart the algorithm.
236 numSteps = 0;
237 maxPositionLimit = std::min(params.maxPositionAllowed, params.startPosition + params.maxTravel);
238 minPositionLimit = std::max(params.minPositionAllowed, params.startPosition - params.maxTravel);
239 computeInitialPosition();
240}
241
242QString LinearFocusAlgorithm::getTextStatus(double R2) const
243{
244 if (params.focusAlgorithm == Focus::FOCUS_LINEAR)
245 {
246 if (done)
247 {
248 if (focusSolution > 0)
249 return QString("Solution: %1").arg(focusSolution);
250 else
251 return "Failed";
252 }
253 if (solutionPending)
254 return QString("Pending: %1, %2").arg(solutionPendingPosition).arg(solutionPendingValue, 0, 'f', 2);
255 if (inFirstPass)
256 return "1st Pass";
257 else
258 return QString("2nd Pass. 1st: %1, %2")
259 .arg(firstPassBestPosition).arg(firstPassBestValue, 0, 'f', 2);
260 }
261 else // Linear 1 Pass
262 {
263 QString text = "L1P";
264 if (params.filterName != "")
265 text.append(QString(" [%1]").arg(params.filterName));
268 else if (params.curveFit == CurveFitting::FOCUS_HYPERBOLA)
269 {
270 text.append(": Hyperbola");
271 text.append(params.useWeights ? " (W)" : " (U)");
272 }
273 else if (params.curveFit == CurveFitting::FOCUS_PARABOLA)
274 {
275 text.append(": Parabola");
276 text.append(params.useWeights ? " (W)" : " (U)");
277 }
278
279 if (inFirstPass)
280 return text;
281 else if (!done)
282 return text.append(". Moving to Solution");
283 else
284 {
285 if (focusSolution > 0)
286 {
288 return text.append(" Solution: %1").arg(focusSolution);
289 else
290 // Add R2 to 2 decimal places. Round down to be conservative
291 return text.append(" Solution: %1, RÂ²=%2").arg(focusSolution).arg(trunc(R2 * 100.0) / 100.0, 0, 'f', 2);
292 }
293 else
294 return text.append(" Failed");
295 }
296 }
297}
298
299void LinearFocusAlgorithm::computeInitialPosition()
300{
301 // These variables get reset if the algorithm is restarted.
302 stepSize = params.initialStepSize;
303 inFirstPass = true;
304 solutionPending = false;
305 retryNumber = 0;
306 firstPassBestValue = -1;
307 firstPassBestPosition = 0;
308 numPolySolutionsFound = 0;
309 numRestartSolutionsFound = 0;
310 secondPassStartIndex = -1;
311
312 qCDebug(KSTARS_EKOS_FOCUS)
313 << QString("Linear: Travel %1 initStep %2 pos %3 min %4 max %5 maxIters %6 tolerance %7 minlimit %8 maxlimit %9 init#steps %10 algo %11 backlash %12 curvefit %13 useweights %14")
314 .arg(params.maxTravel).arg(params.initialStepSize).arg(params.startPosition).arg(params.minPositionAllowed)
315 .arg(params.maxPositionAllowed).arg(params.maxIterations).arg(params.focusTolerance).arg(minPositionLimit)
316 .arg(maxPositionLimit).arg(params.initialOutwardSteps).arg(params.focusAlgorithm).arg(params.backlash)
317 .arg(params.curveFit).arg(params.useWeights);
318
319 requestedPosition = std::min(maxPositionLimit, getFirstPosition());
320 passStartPosition = requestedPosition;
321 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: initialPosition %1 sized %2")
322 .arg(requestedPosition).arg(params.initialStepSize);
323}
324
325// Calculate the first position relative to the start position
326// Generally this is just the number of outward points * step size.
327// For LINEAR1PASS most walks have a constant step size so again it is the number of outward points * step size.
328// For LINEAR1PASS and FOCUS_WALK_CFZ_SHUFFLE the middle 3 or 4 points are separated by half step size
329int LinearFocusAlgorithm::getFirstPosition()
330{
331 if (params.focusAlgorithm != Focus::FOCUS_LINEAR1PASS)
332 return static_cast<int>(params.startPosition + params.initialOutwardSteps * params.initialStepSize);
333
334 int firstPosition;
336
337 switch (params.focusWalk)
338 {
339 case Focus::FOCUS_WALK_CLASSIC:
340 firstPosition = static_cast<int>(params.startPosition + params.initialOutwardSteps * params.initialStepSize);
341 break;
342
343 case Focus::FOCUS_WALK_FIXED_STEPS:
344 outSteps = (params.numSteps - 1) / 2.0f;
345 firstPosition = params.startPosition + (outSteps * params.initialStepSize);
346 break;
347
348 case Focus::FOCUS_WALK_CFZ_SHUFFLE:
349 if (params.numSteps % 2)
350 {
351 // Odd number of steps
352 numHalfStepsOut = 1.0f;
353 numFullStepsOut = ((params.numSteps - 1) / 2.0f) - numHalfStepsOut;
354 }
355 else
356 {
357 // Even number of steps
358 numHalfStepsOut = 1.5f;
359 numFullStepsOut = (params.numSteps / 2.0f) - 2.0f;
360 }
361 firstPosition = params.startPosition + (numFullStepsOut * params.initialStepSize) + (numHalfStepsOut *
362 (params.initialStepSize / 2.0f));
363
364 break;
365
366 default:
367 firstPosition = static_cast<int>(params.startPosition + params.initialOutwardSteps * params.initialStepSize);
368 break;
369 }
370
371 return firstPosition;
372}
373
374// The Problem:
375// The HFR values passed in to these focus algorithms are simply the mean or median HFR values
376// for all stars detected (with some other constraints). However, due to randomness in the
377// star detection scheme, two sets of star measurements may use different sets of actual stars to
378// compute their mean HFRs. This adds noise to determining best focus (e.g. including a
379// wider star in one set but not the other will tend to increase the HFR for that star set).
380// relativeHFR tries to correct for this by comparing two sets of stars only using their common stars.
381//
382// The Solution:
383// We maintain a reference set of stars, along with the HFR computed for those reference stars (bestRelativeHFR).
384// We find the common set of stars between an input star set and those reference stars. We compute
385// HFRs for the input stars in that common set, and for the reference common set as well.
386// The ratio of those common-set HFRs multiply bestRelativeHFR to generate the relative HFR for this
387// input star set--that is, it's the HFR "relative to the reference set". E.g. if the common-set HFR
388// for the input stars is twice worse than that from the reference common stars, then the relative HFR
389// would be 2 * bestRelativeHFR.
390// The reference set is the best HFR set of stars seen so far, but only from the 1st pass.
391// Thus, in the 2nd pass, the reference stars would be the best HFR set from the 1st pass.
392//
393// In unusual cases, e.g. when the common set can't be computed, we just return the input origHFR.
394double LinearFocusAlgorithm::relativeHFR(double origHFR, const QList<Edge*> *stars)
395{
396 constexpr double minHFR = 0.3;
397 if (origHFR < minHFR) return origHFR;
398
399 const int currentIndex = values.size();
400 const bool isFirstSample = (currentIndex == 0);
401 double relativeHFR = origHFR;
402
403 if (isFirstSample && stars != nullptr)
404 relativeHFREnabled = true;
405
406 if (relativeHFREnabled && stars == nullptr)
407 {
408 // Error--we have been getting relativeHFR stars, and now we're not.
409 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: Error: Inconsistent relativeHFR, disabling.");
410 relativeHFREnabled = false;
411 }
412
413 if (!relativeHFREnabled)
414 return origHFR;
415
416 if (starLists.size() != positions.size())
417 {
418 // Error--inconsistent data structures!
419 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: Error: Inconsistent relativeHFR data structures, disabling.");
420 relativeHFREnabled = false;
421 return origHFR;
422 }
423
424 // Copy the input stars.
425 // FocusStars enables us to measure HFR relatively consistently,
426 // by using the same stars when comparing two measurements.
427 constexpr int starDistanceThreshold = 5; // Max distance (pixels) for two positions to be considered the same star.
428 FocusStars fs(*stars, starDistanceThreshold);
429 starLists.push_back(fs);
430 auto &currentStars = starLists.back();
431
432 if (isFirstSample)
433 {
434 relativeHFR = currentStars.getHFR();
435 if (relativeHFR <= 0)
436 {
437 // Fall back to the simpler scheme.
438 relativeHFREnabled = false;
439 return origHFR;
440 }
441 // 1st measurement. By definition this is the best HFR.
442 bestRelativeHFR = relativeHFR;
443 bestRelativeHFRIndex = currentIndex;
444 }
445 else
446 {
447 // HFR computed relative to the best measured so far.
448 auto &bestStars = starLists[bestRelativeHFRIndex];
449 double hfr = currentStars.relativeHFR(bestStars, bestRelativeHFR);
450 if (hfr > 0)
451 {
452 relativeHFR = hfr;
453 }
454 else
455 {
456 relativeHFR = currentStars.getHFR();
457 if (relativeHFR <= 0)
458 return origHFR;
459 }
460
461 // In the 1st pass we compute the current HFR relative to the best HFR measured yet.
462 // In the 2nd pass we compute the current HFR relative to the best HFR in the 1st pass.
463 if (inFirstPass && relativeHFR <= bestRelativeHFR)
464 {
465 bestRelativeHFR = relativeHFR;
466 bestRelativeHFRIndex = currentIndex;
467 }
468 }
469
470 qCDebug(KSTARS_EKOS_FOCUS) << QString("RelativeHFR: orig %1 computed %2 relative %3")
471 .arg(origHFR).arg(currentStars.getHFR()).arg(relativeHFR);
472
473 return relativeHFR;
474}
475
476int LinearFocusAlgorithm::newMeasurement(int position, double value, const double starWeight, const QList<Edge*> *stars)
477{
478 if (params.focusAlgorithm == Focus::FOCUS_LINEAR1PASS && (params.focusWalk == Focus::FOCUS_WALK_FIXED_STEPS
479 || params.focusWalk == Focus::FOCUS_WALK_CFZ_SHUFFLE))
480 return linearWalk(position, value, starWeight);
481
482 double minPos = -1.0, minVal = 0;
483 bool foundFit = false;
484
485 double origValue = value;
486 // For QUADRATIC continue to use the relativeHFR functionality
487 // For HYPERBOLA and PARABOLA the stars used for the HDR calculation and the sigma calculation
488 // should be the same. For now, we will use the full set of stars and therefore not adjust the HFR
490 value = relativeHFR(value, stars);
491
492 // For LINEAR 1 PASS don't bother with a full 2nd pass just jump to the solution
493 // Do the step out and back in to deal with backlash
494 if (params.focusAlgorithm == Focus::FOCUS_LINEAR1PASS && !inFirstPass)
495 {
496 return setupSolution(position, value, starWeight);
497 }
498
499 int thisStepSize = stepSize;
500 ++numSteps;
501 if (inFirstPass)
502 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: step %1, newMeasurement(%2, %3 -> %4, %5)")
503 .arg(numSteps).arg(position).arg(origValue).arg(value)
504 .arg(stars == nullptr ? 0 : stars->size());
505 else
506 qCDebug(KSTARS_EKOS_FOCUS)
507 << QString("Linear: step %1, newMeasurement(%2, %3 -> %4, %5) 1stBest %6 %7").arg(numSteps)
508 .arg(position).arg(origValue).arg(value).arg(stars == nullptr ? 0 : stars->size())
509 .arg(firstPassBestPosition).arg(firstPassBestValue, 0, 'f', 3);
510
511 const int LINEAR_POSITION_TOLERANCE = params.initialStepSize;
512 if (abs(position - requestedPosition) > LINEAR_POSITION_TOLERANCE)
513 {
514 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: error didn't get the requested position");
515 return requestedPosition;
516 }
517 // Have we already found a solution?
518 if (focusSolution != -1)
519 {
520 doneString = i18n("Called newMeasurement after a solution was found.");
521 failCode = Ekos::FOCUS_FAIL_INTERNAL;
522 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: error %1").arg(doneString);
523 debugLog();
524 return -1;
525 }
526
527 // Store the sample values.
528 positions.push_back(position);
529 values.push_back(value);
530 weights.push_back(starWeight);
531 if (inFirstPass)
532 {
533 pass1Positions.push_back(position);
534 pass1Values.push_back(value);
535 pass1Weights.push_back(starWeight);
536 pass1Outliers.push_back(false);
537 }
538
539 // If we're in the 2nd pass and either
540 // - the current value is within tolerance, or
541 // - we're optimizing because we've previously found a within-tolerance solution,
542 // then setupPendingSolution decides whether to continue optimizing or to complete.
543 if (solutionPending ||
544 (!inFirstPass && (value < firstPassBestValue * (1.0 + params.focusTolerance))))
545 {
546 if (setupPendingSolution(position))
547 // We can continue to look a little further.
548 return completeIteration(retryNumber > 0 ? 0 : stepSize, false, -1.0f, -1.0f);
549 else
550 // Finish now
551 return setupSolution(position, value, starWeight);
552 }
553
554 if (inFirstPass)
555 {
556 constexpr int kNumPolySolutionsRequired = 2;
557 constexpr int kNumRestartSolutionsRequired = 3;
558 //constexpr double kDecentValue = 3.0;
559
560 if (values.size() >= getCurveMinPoints())
561 {
563 {
564 PolynomialFit fit(2, positions, values);
565 foundFit = fit.findMinimum(position, 0, params.maxPositionAllowed, &minPos, &minVal);
566 if (!foundFit)
567 {
568 // I've found that the first sample can be odd--perhaps due to backlash.
569 // Try again skipping the first sample, if we have sufficient points.
570 if (values.size() > getCurveMinPoints())
571 {
572 PolynomialFit fit2(2, positions.mid(1), values.mid(1));
573 foundFit = fit2.findMinimum(position, 0, params.maxPositionAllowed, &minPos, &minVal);
574 minPos = minPos + 1;
575 }
576 }
577 }
578 else // Hyperbola or Parabola so use the LM solver
579 {
580 auto goal = getGoal(numSteps);
581 params.curveFitting->fitCurve(goal, positions, values, weights, pass1Outliers, params.curveFit, params.useWeights,
582 params.optimisationDirection);
583
584 foundFit = params.curveFitting->findMinMax(position, 0, params.maxPositionAllowed, &minPos, &minVal,
585 static_cast<CurveFitting::CurveFit>(params.curveFit),
586 params.optimisationDirection);
587 }
588
589 if (foundFit)
590 {
591 const int distanceToMin = static_cast<int>(position - minPos);
592 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: fit(%1): %2 = %3 @ %4 distToMin %5")
593 .arg(positions.size()).arg(minPos).arg(minVal).arg(position).arg(distanceToMin);
594 if (distanceToMin >= 0)
595 {
596 // The minimum is further inward.
597 numPolySolutionsFound = 0;
598 numRestartSolutionsFound = 0;
599 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: Solutions reset %1 = %2").arg(minPos).arg(minVal);
600
601 // The next code block is an optimisation where the user starts focus off a long way from prime focus.
602 // The idea is that by detecting this situation we can warp quickly to the solution.
603 // However, when the solver is solving on a small number of points (e.g. 6 or 7) its possible that the
604 // best fit curve produces a solution much further in than what turns out to be the actual focus point.
605 // This code therefore results in inappropriate warping. So this code is now disabled.
606 /*if (value / minVal > kDecentValue)
607 {
608 // Only skip samples if the value is a long way from the minimum.
609 const int stepsToMin = distanceToMin / stepSize;
610 // Temporarily increase the step size if the minimum is very far inward.
611 if (stepsToMin >= 8)
612 thisStepSize = stepSize * 4;
613 else if (stepsToMin >= 4)
614 thisStepSize = stepSize * 2;
615 }*/
616 }
617 else if (!bestSamplesHeuristic())
618 {
619 // We have potentially passed the bottom of the curve,
620 // but it's possible it is further back than the start of our sweep.
621 if (minPos > passStartPosition)
622 {
623 numRestartSolutionsFound++;
624 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: RESTART Solution #%1 %2 = %3 @ %4")
625 .arg(numRestartSolutionsFound).arg(minPos).arg(minVal).arg(position);
626 }
627 else
628 {
629 numPolySolutionsFound++;
630 numRestartSolutionsFound = 0;
631 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: Solution #%1: %2 = %3 @ %4")
632 .arg(numPolySolutionsFound).arg(minPos).arg(minVal).arg(position);
633 }
634 }
635
636 // The LINEAR algo goes >2 steps past the minimum. The LINEAR 1 PASS algo uses the InitialOutwardSteps parameter
637 // in order to have some user control over the number of steps past the minimum when fitting the curve.
638 // With LINEAR 1 PASS setupSecondPass with a margin of 0.0 as no need to move the focuser further
639 // This will result in a step out, past the solution of either "backlash" id set, or 5 * step size, followed
640 // by a step in to the solution. By stepping out and in, backlash will be neutralised.
641 int howFarToGo;
642 if (params.focusAlgorithm == Focus::FOCUS_LINEAR)
644 else
645 howFarToGo = std::max(1, static_cast<int> (params.initialOutwardSteps) - 1);
646
647 if (numPolySolutionsFound >= howFarToGo)
648 {
649 // We found a minimum. Setup the 2nd pass. We could use either the polynomial min or the
650 // min measured star as the target HFR. I've seen using the polynomial minimum to be
651 // sometimes too conservative, sometimes too low. For now using the min sample.
652 double minMeasurement = *std::min_element(values.begin(), values.end());
653 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: 1stPass solution @ %1: pos %2 val %3, min measurement %4")
654 .arg(position).arg(minPos).arg(minVal).arg(minMeasurement);
655
656 // For Linear setup the second pass with a margin of 2
657 if (params.focusAlgorithm == Focus::FOCUS_LINEAR)
658 return setupSecondPass(static_cast<int>(minPos), minMeasurement, 2.0);
659 else
660 // For Linear 1 Pass do a round on the double minPos parameter before casting to an int
661 // as the cast will truncate and potentially change the position down by 1
662 // The code to display v-curve rounds so this keeps things consistent.
663 // Could make this change for Linear as well but this will then need testfocus to change
664 return finishFirstPass(static_cast<int>(round(minPos)), minMeasurement);
665 }
666 else if (numRestartSolutionsFound >= kNumRestartSolutionsRequired)
667 {
668 // We need to restart--that is the error is rising and thus our initial position
669 // was already past the minimum. Restart the algorithm with a greater initial position.
670 // If the min position from the polynomial solution is not too far from the current start position
671 // use that, but don't let it go too far away.
672 const double highestStartPosition = params.startPosition + params.initialOutwardSteps * params.initialStepSize;
673 params.startPosition = std::min(minPos, highestStartPosition);
674 computeInitialPosition();
675 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: Restart. Current pos %1, min pos %2, min val %3, re-starting at %4")
676 .arg(position).arg(minPos).arg(minVal).arg(requestedPosition);
677 return requestedPosition;
678 }
679
680 }
681 else
682 {
683 // Minimum failed indicating the 2nd-order polynomial is an inverted U--it has a maximum,
684 // but no minimum. This is, of course, not a sensible solution for the focuser, but can
685 // happen with noisy data and perhaps small step sizes. We still might be able to take advantage,
686 // and notice whether the polynomial is increasing or decreasing locally. For now, do nothing.
687 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: ******** No poly min: Poly must be inverted");
688 }
689 }
690 else
691 {
692 // Don't have enough samples to reliably fit a polynomial.
693 // Simply step the focus in one more time and iterate.
694 }
695 }
696 else if (gettingWorse())
697 {
698 // Doesn't look like we'll find something close to the min. Retry the 2nd pass.
699 // NOTE: this branch will only be executed for the 2 pass version of Linear
700 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: getting worse, re-running 2nd pass");
701 return setupSecondPass(firstPassBestPosition, firstPassBestValue);
702 }
703
704 return completeIteration(thisStepSize, foundFit, minPos, minVal);
705}
706
707// Get the curve fitting goal, based on the "walk".
708// Start with STANDARD but at the end of the pass use BEST
709CurveFitting::FittingGoal LinearFocusAlgorithm::getGoal(int numSteps)
710{
711 if (params.focusWalk == Focus::FOCUS_WALK_CLASSIC)
712 {
713 if (numSteps > 2.0 * params.initialOutwardSteps)
714 return CurveFitting::FittingGoal::BEST;
715 else
716 return CurveFitting::FittingGoal::STANDARD;
717 }
718 else // FOCUS_WALK_FIXED_STEPS or FOCUS_WALK_CFZ_SHUFFLE
719 {
720 if (numSteps >= params.numSteps)
721 return CurveFitting::FittingGoal::BEST;
722 else
723 return CurveFitting::FittingGoal::STANDARD;
724 }
725}
726
727// Get the minimum number of points to start fitting a curve
728// The default is 5. Try to get past the minimum so the curve shape is hyperbolic/parabolic
729int LinearFocusAlgorithm::getCurveMinPoints()
730{
731 if (params.focusAlgorithm != Focus::FOCUS_LINEAR1PASS)
732 return 5;
733
734 if (params.focusWalk == Focus::FOCUS_WALK_CLASSIC)
735 return params.initialOutwardSteps + 2;
736
737 // For donut busting don't try drawing an intermediate curve... as we need to scrub data first
738 if (params.donutBuster)
739 return params.numSteps;
740
741 return params.numSteps / 2 + 2;
742}
743
744// Process next step for LINEAR1PASS for walks: FOCUS_WALK_FIXED_STEPS and FOCUS_WALK_CFZ_SHUFFLE
745int LinearFocusAlgorithm::linearWalk(int position, double value, const double starWeight)
746{
747 if (!inFirstPass)
748 {
749 return setupSolution(position, value, starWeight);
750 }
751
752 bool foundFit = false;
753 double minPos = -1.0, minVal = 0;
754 ++numSteps;
755 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear1Pass: step %1, linearWalk %2, %3")
756 .arg(numSteps).arg(position).arg(value);
757
758 // If we are within stepsize ticks of where we should be then fine, otherwise try again
759 if (abs(position - requestedPosition) > params.initialStepSize)
760 {
761 qCDebug(KSTARS_EKOS_FOCUS) << QString("linearWalk error: position %1, requested position %2").arg(position)
762 .arg(requestedPosition);
763 return requestedPosition;
764 }
765
766 // Store the sample values.
767 positions.push_back(position);
768 values.push_back(value);
769 weights.push_back(starWeight);
770 pass1Positions.push_back(position);
771 pass1Values.push_back(value);
772 pass1Weights.push_back(starWeight);
773 pass1Outliers.push_back(false);
774
775 if (values.size() < getCurveMinPoints())
776 {
777 // Don't have enough samples to reliably fit a curve.
778 // Simply step the focus in one more time and iterate.
779 }
780 else
781 {
783 qCDebug(KSTARS_EKOS_FOCUS) << QString("linearWalk called incorrectly for FOCUS_QUADRATIC");
784 else
785 {
786 // Hyperbola or Parabola so use the LM solver
787 auto goal = getGoal(numSteps);
788 // Check for Donuts
789 if (params.donutBuster)
790 removeDonuts();
791 params.curveFitting->fitCurve(goal, positions, values, weights, pass1Outliers, params.curveFit, params.useWeights,
792 params.optimisationDirection);
793
794 foundFit = params.curveFitting->findMinMax(position, 0, params.maxPositionAllowed, &minPos, &minVal,
795 static_cast<CurveFitting::CurveFit>(params.curveFit), params.optimisationDirection);
796
797 if (numSteps >= params.numSteps)
798 {
799 // linearWalk has now completed either successfully (foundFit=true) or not
800 if (foundFit)
801 return finishFirstPass(static_cast<int>(round(minPos)), minVal);
802 else
803 {
804 done = true;
805 doneString = i18n("Failed to fit curve to data.");
806 failCode = Ekos::FOCUS_FAIL_CURVEFIT;
807 qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearWalk: %1").arg(doneString);
808 debugLog();
809 return -1;
810 }
811 }
812 }
813 }
814
815 int nextStepSize = getNextStepSize();
816 return completeIteration(nextStepSize, foundFit, minPos, minVal);
817}
818
819// Check for donuts
820// Assumption is that we are starting near to focus so that we should have a reasonable v-curve
821// near the central datapoints but the wings may contain donuts=poor quality data.
822// 1. Make sure we have enough data to work with
823// 2. Find the central minimum datapoint
824// 3. Move outwards on each side of the minimum checking the next datapoint. If value drops this could be bad data
825// Only remove the wings - bad data near the centre won't be filtered out.
826void LinearFocusAlgorithm::removeDonuts()
827{
828 // 1. We need at least 7 datapoints to do any analysis
829 if (values.size() <= 7)
830 return;
831
832 QVector<bool> queryOutliers = pass1Outliers;
833
834 // 2. Firstly find the central minimum datapoint
835 int centre = values.size() / 2;
836 int minElement = centre;
837 double minValue = values[centre];
838 for (int i = 1; i < 4; i++)
839 {
840 if (values[centre + i] < minValue)
841 {
842 minValue = values[centre + i];
843 minElement = centre + i;
844 }
845 if (values[centre - i] < minValue)
846 {
847 minValue = values[centre - i];
848 minElement = centre - i;
849 }
850 }
851
852 // 3a. Move outward from the central minimum checking for increasing values
853 double threshold = values[minElement];
854 double nextValue, deltaValue;
855 double tolerance = 0.5; //
856 for (int i = minElement; i > 0; i--)
857 {
858 nextValue = values[i - 1];
859 if (nextValue < threshold)
860 // Smaller value indicates bad data
861 queryOutliers[i - 1] = true;
862 else
863 {
864 deltaValue = (nextValue - values[i]) * tolerance;
865 threshold = nextValue + deltaValue;
866 }
867 }
868
869 // Starting at the furthest outward point, move inward to minimum point, marking bad data (donuts) as outliers
870 for (int i = 0; i < minElement - 3; i++)
871 {
872 if (queryOutliers[i])
873 pass1Outliers[i] = true;
874 else
875 // Stop the process at the first good datapoint - don't discard all bad data
876 break;
877 }
878
879 // 3b. Move inward from the central minimum checking for increasing values
880 threshold = values[minElement];
881 for (int i = minElement; i < values.size() - 1; i++)
882 {
883 nextValue = values[i + 1];
884 if (nextValue < threshold)
885 // Smaller value indicates bad data
886 queryOutliers[i + 1] = true;
887 else
888 {
889 deltaValue = (nextValue - values[i]) * tolerance;
890 threshold = nextValue + deltaValue;
891 }
892 }
893
894 // Starting at the furthest outward point, move inward to minimum point, marking bad data (donuts) as outliers
895 for (int i = values.size() - 1; i > minElement + 3; i--)
896 {
897 if (queryOutliers[i])
898 pass1Outliers[i] = true;
899 else
900 // Stop the process at the first good datapoint - don't discard all bad data
901 break;
902 }
903
904 QString str;
905 for (int i = pass1Outliers.size() - 1; i >= 0; i--)
906 str.append((pass1Outliers[i]) ? 'Y' : 'N');
907 qCDebug(KSTARS_EKOS_FOCUS) << QString("Donut analysis: %1").arg(str);
908
909}
910
911// Function to calculate the next step size for LINEAR1PASS for walks: FOCUS_WALK_FIXED_STEPS and FOCUS_WALK_CFZ_SHUFFLE
912int LinearFocusAlgorithm::getNextStepSize()
913{
914 int nextStepSize, lower, upper;
915
916 switch (params.focusWalk)
917 {
918 case Focus::FOCUS_WALK_CLASSIC:
919 nextStepSize = stepSize;
920 break;
921 case Focus::FOCUS_WALK_FIXED_STEPS:
922 nextStepSize = stepSize;
923 break;
924 case Focus::FOCUS_WALK_CFZ_SHUFFLE:
925 if (params.numSteps % 2)
926 {
927 // Odd number of steps
928 lower = (params.numSteps - 3) / 2;
929 upper = params.numSteps - lower;
930 }
931 else
932 {
933 // Evem number of steps
934 lower = (params.numSteps - 4) / 2;
935 upper = (params.numSteps - lower);
936 }
937
938 if (numSteps <= lower)
939 nextStepSize = stepSize;
940 else if (numSteps >= upper)
941 nextStepSize = stepSize;
942 else
943 nextStepSize = stepSize / 2;
944 break;
945 default:
946 nextStepSize = stepSize;
947 break;
948 }
949
950 return nextStepSize;
951}
952
953int LinearFocusAlgorithm::setupSolution(int position, double value, double weight)
954{
955 focusSolution = position;
956 focusValue = value;
957 focusWeight = weight;
958 done = true;
959 doneString = i18n("Solution found.");
960 failCode = Ekos::FOCUS_FAIL_NONE;
961 if (params.focusAlgorithm == Focus::FOCUS_LINEAR)
962 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: solution @ %1 = %2 (best %3)")
963 .arg(position).arg(value).arg(firstPassBestValue);
964 else // Linear 1 Pass
965 {
966 double delta = fabs(value - firstPassBestValue);
967 QString str("Linear: solution @ ");
968 str.append(QString("%1 = %2 (expected %3) delta=%4").arg(position).arg(value).arg(firstPassBestValue).arg(delta));
969
970 if (params.useWeights && weight > 0.0)
971 {
972 // TODO fix this for weights
973 double numSigmas = delta * weight;
974 str.append(QString(" or %1 sigma %2 than expected")
975 .arg(numSigmas).arg(value <= firstPassBestValue ? "better" : "worse"));
976 if (value <= firstPassBestValue || numSigmas < 1)
977 str.append(QString(". GREAT result"));
978 else if (numSigmas < 3)
979 str.append(QString(". OK result"));
980 else
981 str.append(QString(". POOR result. If this happens repeatedly it may be a sign of poor backlash compensation."));
982 }
983
984 qCInfo(KSTARS_EKOS_FOCUS) << str;
985 }
986 debugLog();
987 return -1;
988}
989
990int LinearFocusAlgorithm::completeIteration(int step, bool foundFit, double minPos, double minVal)
991{
992 if (params.focusAlgorithm == Focus::FOCUS_LINEAR && numSteps == params.maxIterations - 2)
993 {
994 // If we're close to exceeding the iteration limit, retry this pass near the old minimum position.
995 // For Linear 1 Pass we are still in the first pass so no point retrying the 2nd pass. Just carry on and
996 // fail in 2 more datapoints if necessary.
997 const int minIndex = static_cast<int>(std::min_element(values.begin(), values.end()) - values.begin());
998 return setupSecondPass(positions[minIndex], values[minIndex], 0.5);
999 }
1000 else if (numSteps > params.maxIterations)
1001 {
1002 // Fail. Exceeded our alloted number of iterations.
1003 done = true;
1004 doneString = i18n("Too many steps.");
1005 failCode = Ekos::FOCUS_FAIL_MAX_ITERS;
1006 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: error %1").arg(doneString);
1007 debugLog();
1008 return -1;
1009 }
1010
1011 // Setup the next sample.
1012 requestedPosition = requestedPosition - step;
1013
1014 // Make sure the next sample is within bounds.
1015 if (requestedPosition < minPositionLimit)
1016 {
1017 // The position is too low. Pick the min value and go to (or retry) a 2nd iteration.
1018 if (params.focusAlgorithm == Focus::FOCUS_LINEAR)
1019 {
1020 const int minIndex = static_cast<int>(std::min_element(values.begin(), values.end()) - values.begin());
1021 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: reached end without Vmin. Restarting %1 pos %2 value %3")
1022 .arg(minIndex).arg(positions[minIndex]).arg(values[minIndex]);
1023 return setupSecondPass(positions[minIndex], values[minIndex]);
1024 }
1025 else // Linear 1 Pass
1026 {
1027 if (foundFit && minPos >= minPositionLimit)
1028 // Although we ran out of room, we have got a viable, although not perfect, solution
1029 return finishFirstPass(static_cast<int>(round(minPos)), minVal);
1030 else
1031 {
1032 // We can't travel far enough to find a solution so fail as focuser parameters require user attention
1033 done = true;
1034 doneString = i18n("Solution lies outside max travel.");
1035 failCode = Ekos::FOCUS_FAIL_FOCUSER_OOB;
1036 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: error %1").arg(doneString);
1037 debugLog();
1038 return -1;
1039 }
1040 }
1041 }
1042 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: requesting position %1").arg(requestedPosition);
1043 return requestedPosition;
1044}
1045
1046// This is called in the 2nd pass, when we've found a sample point that's within tolerance
1047// of the 1st pass' best value. Since it's within tolerance, the 2nd pass might finish, using
1048// the current value as the final solution. However, this method checks to see if we might go
1049// a bit further. Once solutionPending is true, it's committed to finishing soon.
1050// It goes further if:
1051// - the current HFR value is an improvement on the previous 2nd-pass value,
1052// - the number of steps taken so far is not close the max number of allowable steps.
1053// If instead the HFR got worse, we retry the current position a few times to see if it might
1054// get better before giving up.
1055bool LinearFocusAlgorithm::setupPendingSolution(int position)
1056{
1057 const int length = values.size();
1058 const int secondPassIndex = length - secondPassStartIndex;
1059 const double thisValue = values[length - 1];
1060
1061 // ReferenceValue is the value used in the notGettingWorse computation.
1062 // Basically, it's the previous value, or the value before retries.
1063 double referenceValue = (secondPassIndex <= 1) ? 1e6 : values[length - 2];
1064 if (retryNumber > 0 && length - (2 + retryNumber) >= 0)
1065 referenceValue = values[length - (2 + retryNumber)];
1066
1067 // NotGettingWorse: not worse than the previous (non-repeat) 2nd pass sample, or the 1st 2nd pass sample.
1068 const bool notGettingWorse = (secondPassIndex <= 1) || (thisValue <= referenceValue);
1069
1070 // CouldGoFurther: Not near a boundary in position or number of steps.
1071 const bool couldGoFather = (numSteps < params.maxIterations - 2) && (position - stepSize > minPositionLimit);
1072
1073 // Allow passing the 1st pass' minimum HFR position, but take smaller steps and don't retry as much.
1074 int maxNumRetries = 3;
1075 if (position - stepSize / 2 < firstPassBestPosition)
1076 {
1077 stepSize = std::max(2, params.initialStepSize / 4);
1078 maxNumRetries = 1;
1079 }
1080
1082 {
1083 qCDebug(KSTARS_EKOS_FOCUS)
1084 << QString("Linear: %1: Position(%2) & HFR(%3) -- Pass1: %4 %5, solution pending, searching further")
1085 .arg(length).arg(position).arg(thisValue, 0, 'f', 3).arg(firstPassBestPosition).arg(firstPassBestValue, 0, 'f', 3);
1086 solutionPending = true;
1087 solutionPendingPosition = position;
1088 solutionPendingValue = thisValue;
1089 retryNumber = 0;
1090 return true;
1091 }
1092 else if (solutionPending && couldGoFather && retryNumber < maxNumRetries &&
1093 (secondPassIndex > 1) && (thisValue >= referenceValue))
1094 {
1095 qCDebug(KSTARS_EKOS_FOCUS)
1096 << QString("Linear: %1: Position(%2) & HFR(%3) -- Pass1: %4 %5, solution pending, got worse, retrying")
1097 .arg(length).arg(position).arg(thisValue, 0, 'f', 3).arg(firstPassBestPosition).arg(firstPassBestValue, 0, 'f', 3);
1098 // Try this poisition again.
1099 retryNumber++;
1100 return true;
1101 }
1102 qCDebug(KSTARS_EKOS_FOCUS)
1103 << QString("Linear: %1: Position(%2) & HFR(%3) -- Pass1: %4 %5, finishing, can't go further")
1104 .arg(length).arg(position).arg(thisValue, 0, 'f', 3).arg(firstPassBestPosition).arg(firstPassBestValue, 0, 'f', 3);
1105 retryNumber = 0;
1106 return false;
1107}
1108
1109void LinearFocusAlgorithm::debugLog()
1110{
1111 QString str("Linear: points=[");
1112 for (int i = 0; i < positions.size(); ++i)
1113 {
1114 str.append(QString("(%1, %2, %3)").arg(positions[i]).arg(values[i]).arg(weights[i]));
1115 if (i < positions.size() - 1)
1116 str.append(", ");
1117 }
1118 str.append(QString("];iterations=%1").arg(numSteps));
1119 str.append(QString(";duration=%1").arg(stopWatch.elapsed() / 1000));
1120 str.append(QString(";solution=%1").arg(focusSolution));
1121 str.append(QString(";value=%1").arg(focusValue));
1122 str.append(QString(";filter='%1'").arg(params.filterName));
1123 str.append(QString(";temperature=%1").arg(params.temperature));
1124 str.append(QString(";focusalgorithm=%1").arg(params.focusAlgorithm));
1125 str.append(QString(";backlash=%1").arg(params.backlash));
1126 str.append(QString(";curvefit=%1").arg(params.curveFit));
1127 str.append(QString(";useweights=%1").arg(params.useWeights));
1128
1129 qCDebug(KSTARS_EKOS_FOCUS) << str;
1130}
1131
1132// Called by Linear to set state for the second pass
1133int LinearFocusAlgorithm::setupSecondPass(int position, double value, double margin)
1134{
1135 firstPassBestPosition = position;
1136 firstPassBestValue = value;
1137 inFirstPass = false;
1138 solutionPending = false;
1139 secondPassStartIndex = values.size();
1140
1141 // Arbitrarily go back "margin" steps above the best position.
1142 // Could be a problem if backlash were worse than that many steps.
1143 requestedPosition = std::min(static_cast<int>(firstPassBestPosition + stepSize * margin), maxPositionLimit);
1144 stepSize = params.initialStepSize / 2;
1145 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: 2ndPass starting at %1 step %2").arg(requestedPosition).arg(stepSize);
1146 return requestedPosition;
1147}
1148
1149// Called by L1P to finish off the first pass by setting state variables
1150int LinearFocusAlgorithm::finishFirstPass(int position, double value)
1151{
1152 firstPassBestPosition = position;
1153 firstPassBestValue = value;
1154 inFirstPass = false;
1155 requestedPosition = position;
1156
1157 if (params.refineCurveFit)
1158 {
1159 // We've completed pass1 so, if required, reprocess all the datapoints excluding outliers
1160 // First, we need the distance of each datapoint from the curve
1161 std::vector<std::pair<int, double>> curveDeltas;
1162
1163 params.curveFitting->calculateCurveDeltas(params.curveFit, curveDeltas);
1164
1165 // Check for outliers if there are enough datapoints
1166 if (curveDeltas.size() > 6)
1167 {
1168 // Build a vector of the square of the curve deltas
1169 std::vector<double> curveDeltasX2Vec;
1170 for (int i = 0; i < curveDeltas.size(); i++)
1171 curveDeltasX2Vec.push_back(pow(curveDeltas[i].second, 2.0));
1172
1173 auto curveDeltasX2Mean = Mathematics::RobustStatistics::ComputeLocation(Mathematics::RobustStatistics::LOCATION_MEAN,
1175
1176 // Order the curveDeltas, highest first, then check against the limit
1177 // Remove points over the limit, but don't remove too many points to compromise the curve.
1178 double outlierRejection = params.donutBuster ? params.outlierRejection : 0.2;
1179 int maxOutliers = curveDeltas.size() * outlierRejection;
1180
1181 double modelUnknowns = params.curveFit == CurveFitting::FOCUS_PARABOLA ? 3.0 : 4.0;
1182
1183 // Use Peirce's Criterion to get the outlier threshold
1184 // Note this operates on the square of the curve deltas
1185 double pc = peirce_criterion(static_cast<double>(curveDeltas.size()), static_cast<double>(maxOutliers), modelUnknowns);
1187
1188 // Sort the curve deltas, largest first
1189 std::sort(curveDeltas.begin(), curveDeltas.end(),
1190 [](const std::pair<int, double> &a, const std::pair<int, double> &b)
1191 {
1192 return ( a.second > b.second );
1193 });
1194
1195 // Save off pass1 variables in case they need to be reset later
1196 auto bestPass1Outliers = pass1Outliers;
1197
1198 // Work out how many outliers to exclude
1199 int outliers = 0;
1200 for (int i = 0; i < curveDeltas.size(); i++)
1201 {
1202 if(curveDeltas[i].second <= pc_threshold)
1203 break;
1204 else
1205 {
1206 pass1Outliers[curveDeltas[i].first] = true;
1207 outliers++;
1208 }
1209 if (outliers >= maxOutliers)
1210 break;
1211 }
1212
1213 // If there are any outliers then try to improve the curve fit by removing these
1214 // datapoints and reruning the curve fitting process. Note that this may not improve
1215 // the fit so we need to be prepared to reinstate the original solution.
1217 if (outliers > 0 && params.curveFitting->getCurveParams(params.curveFit, currentCoefficients))
1218 {
1219 double currentR2 = params.curveFitting->calculateR2(static_cast<CurveFitting::CurveFit>(params.curveFit));
1220
1221 // Try another curve fit on the data without outliers
1222 params.curveFitting->fitCurve(CurveFitting::FittingGoal::BEST, pass1Positions, pass1Values, pass1Weights,
1223 pass1Outliers, params.curveFit, params.useWeights, params.optimisationDirection);
1224 double minPos, minVal;
1225 bool foundFit = params.curveFitting->findMinMax(position, 0, params.maxPositionAllowed, &minPos, &minVal,
1226 static_cast<CurveFitting::CurveFit>(params.curveFit),
1227 params.optimisationDirection);
1228 if (foundFit)
1229 {
1230 double newR2 = params.curveFitting->calculateR2(static_cast<CurveFitting::CurveFit>(params.curveFit));
1231 if (newR2 > currentR2)
1232 {
1233 // We have a better solution so we'll use it
1234 requestedPosition = minPos;
1235 qCDebug(KSTARS_EKOS_FOCUS) << QString("Refine Curve Fit improved focus from %1 to %2. R2 improved from %3 to %4")
1236 .arg(position).arg(minPos).arg(currentR2).arg(newR2);
1237 }
1238 else
1239 {
1240 // New solution is not better than previous one so go with the previous one
1241 qCDebug(KSTARS_EKOS_FOCUS) <<
1242 QString("Refine Curve Fit could not improve on the original solution");
1243 pass1Outliers = bestPass1Outliers;
1244 params.curveFitting->setCurveParams(params.curveFit, currentCoefficients);
1245 }
1246 }
1247 else
1248 {
1249 qCDebug(KSTARS_EKOS_FOCUS) <<
1250 QString("Refine Curve Fit failed to fit curve whilst refining curve fit. Running with original solution");
1251 pass1Outliers = bestPass1Outliers;
1252 params.curveFitting->setCurveParams(params.curveFit, currentCoefficients);
1253 }
1254 }
1255 }
1256 }
1257 return requestedPosition;
1258}
1259
1260// Peirce's criterion based on Gould's methodology for outlier identification
1261// See https://en.wikipedia.org/wiki/Peirce%27s_criterion for details incl Peirce's original paper
1262// and Gould's paper both referenced in the notes. The wikipedia entry also includes some code fragments
1263// that form the basis of this function.
1264//
1265// Arguments:
1266// N = total number of observations
1267// n = number of outliers to be removed
1268// m = number of model unknowns
1269// Returns: squared error threshold (x2)
1270double LinearFocusAlgorithm::peirce_criterion(double N, double n, double m)
1271{
1272 double x2 = 0.0;
1273 if (N > 1)
1274 {
1275 // Calculate Q (Nth root of Gould's equation B):
1276 double Q = (pow(n, (n / N)) * pow((N - n), ((N - n) / N))) / N;
1277
1278 // Initialize R values
1279 double r_new = 1.0;
1280 double r_old = 0.0;
1281
1282 // Start iteration to converge on R:
1283 while (abs(r_new - r_old) > (N * 2.0e-16))
1284 {
1285 // Calculate Lamda
1286 // (1 / (N - n) th root of Gould's equation B
1287 double ldiv = pow(r_new, n);
1288 if (ldiv == 0)
1289 ldiv = 1.0e-6;
1290
1291 double Lamda = pow((pow(Q, N) / (ldiv)), (1.0 / (N - n)));
1292
1293 // Calculate x -squared(Gould's equation C)
1294 x2 = 1.0 + (N - m - n) / n * (1.0 - pow(Lamda, 2.0));
1295
1296 if (x2 < 0.0)
1297 {
1298 // If x2 goes negative, return 0
1299 x2 = 0.0;
1300 r_old = r_new;
1301 }
1302 else
1303 {
1304 // Use x-squared to update R(Gould 's equation D)
1305 r_old = r_new;
1306 r_new = exp((x2 - 1) / 2.0) * gsl_sf_erfc(sqrt(x2) / sqrt(2.0));
1307 }
1308 }
1309 }
1310
1311 return x2;
1312}
1313
1314// Return true if one of the 2 recent samples is among the best 2 samples so far.
1315bool LinearFocusAlgorithm::bestSamplesHeuristic()
1316{
1317 const int length = values.size();
1318 if (length < 5) return true;
1319 QVector<double> tempValues = values;
1320 std::nth_element(tempValues.begin(), tempValues.begin() + 2, tempValues.end());
1321 double secondBest = tempValues[1];
1322 if ((values[length - 1] <= secondBest) || (values[length - 2] <= secondBest))
1323 return true;
1324 return false;
1325}
1326
1327// Return true if there are "streak" consecutive values which are successively worse.
1328bool LinearFocusAlgorithm::gettingWorse()
1329{
1330 // Must have this many consecutive values getting worse.
1331 constexpr int streak = 3;
1332 const int length = values.size();
1333 if (secondPassStartIndex < 0)
1334 return false;
1335 if (length < streak + 1)
1336 return false;
1337 // This insures that all the values we're checking are in the latest 2nd pass.
1338 if (length - secondPassStartIndex < streak + 1)
1339 return false;
1340 for (int i = length - 1; i >= length - streak; --i)
1341 if (values[i] <= values[i - 1])
1342 return false;
1343 return true;
1344}
1345
1346}
1347
QString i18n(const char *text, const TYPE &arg...)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:79
QString & append(QChar ch)
QString arg(Args &&... args) const const
void push_back(QChar ch)
qsizetype size() const const
This file is part of the KDE documentation.