1 /*
2     SPDX-FileCopyrightText: 2019 Hy Murveit <hy-1@murveit.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "focusalgorithms.h"
8 
9 #include "polynomialfit.h"
10 #include <QVector>
11 #include "kstars.h"
12 #include "fitsviewer/fitsstardetector.h"
13 #include "focusstars.h"
14 #include <ekos_focus_debug.h>
15 
16 namespace Ekos
17 {
18 /**
19  * @class LinearFocusAlgorithm
20  * @short Autofocus algorithm that linearly samples HFR values.
21  *
22  * @author Hy Murveit
23  * @version 1.0
24  */
25 class LinearFocusAlgorithm : public FocusAlgorithmInterface
26 {
27     public:
28 
29         // Constructor initializes a linear autofocus algorithm, starting at some initial position,
30         // sampling HFR values, decreasing the position by some step size, until the algorithm believe's
31         // it's seen a minimum. It then tries to find that minimum in a 2nd pass.
32         LinearFocusAlgorithm(const FocusParams &params);
33 
34         // After construction, this should be called to get the initial position desired by the
35         // focus algorithm. It returns the initial position passed to the constructor if
36         // it has no movement request.
initialPosition()37         int initialPosition() override
38         {
39             return requestedPosition;
40         }
41 
42         // Pass in the measurement for the last requested position. Returns the position for the next
43         // requested measurement, or -1 if the algorithm's done or if there's an error.
44         // If stars is not nullptr, then the relativeHFR scheme is used to modify the HFR value.
45         int newMeasurement(int position, double value, const QList<Edge*> *stars) override;
46 
47         FocusAlgorithmInterface *Copy() override;
48 
getMeasurements(QVector<int> * pos,QVector<double> * hfrs) const49         void getMeasurements(QVector<int> *pos, QVector<double> *hfrs) const override
50         {
51             *pos = positions;
52             *hfrs = values;
53         }
54 
getPass1Measurements(QVector<int> * pos,QVector<double> * hfrs) const55         void getPass1Measurements(QVector<int> *pos, QVector<double> *hfrs) const override
56         {
57             *pos = pass1Positions;
58             *hfrs = pass1Values;
59         }
60 
latestHFR() const61         double latestHFR() const override
62         {
63             if (values.size() > 0)
64                 return values.last();
65             return -1;
66         }
67 
getTextStatus()68         QString getTextStatus() override
69         {
70             if (done)
71             {
72                 if (focusSolution > 0)
73                     return QString("Solution: %1").arg(focusSolution);
74                 else
75                     return "Failed";
76             }
77             if (solutionPending)
78                 return QString("Pending: %1,  %2").arg(solutionPendingPosition).arg(solutionPendingValue, 0, 'f', 2);
79             if (inFirstPass)
80                 return "1st Pass";
81             else
82                 return QString("2nd Pass. 1st: %1,  %2")
83                        .arg(firstPassBestPosition).arg(firstPassBestValue, 0, 'f', 2);
84         }
85 
86     private:
87 
88         // Called in newMeasurement. Sets up the next iteration.
89         int completeIteration(int step);
90 
91         // Does the bookkeeping for the final focus solution.
92         int setupSolution(int position, double value);
93 
94         // Called when we've found a solution, e.g. the HFR value is within tolerance of the desired value.
95         // It it returns true, then it's decided tht we should try one more sample for a possible improvement.
96         // If it returns false, then "play it safe" and use the current position as the solution.
97         bool setupPendingSolution(int position);
98 
99         // Determines the desired focus position for the first sample.
100         void computeInitialPosition();
101 
102         // Sets the internal state for re-finding the minimum, and returns the requested next
103         // position to sample.
104         int setupSecondPass(int position, double value, double margin = 2.0);
105 
106         // Used in the 2nd pass. Focus is getting worse. Requires several consecutive samples getting worse.
107         bool gettingWorse();
108 
109         // If one of the last 2 samples are as good or better than the 2nd best so far, return true.
110         bool bestSamplesHeuristic();
111 
112         // Adds to the debug log a line summarizing the result of running this algorithm.
113         void debugLog();
114 
115         // Use the detailed star measurements to adjust the HFR value.
116         // See comment above method definition.
117         double relativeHFR(double origHFR, const QList<Edge*> *stars);
118 
119         // Used to time the focus algorithm.
120         QTime stopWatch;
121 
122         // A vector containing the HFR values sampled by this algorithm so far.
123         QVector<double> values;
124         QVector<double> pass1Values;
125         // A vector containing the focus positions corresponding to the HFR values stored above.
126         QVector<int> positions;
127         QVector<int> pass1Positions;
128 
129         // Focus position requested by this algorithm the previous step.
130         int requestedPosition;
131         // The position where the first pass is begun. Usually requestedPosition unless there's a restart.
132         int passStartPosition;
133         // Number of iterations processed so far. Used to see it doesn't exceed params.maxIterations.
134         int numSteps;
135         // The best value in the first pass. The 2nd pass attempts to get within
136         // tolerance of this value.
137         double firstPassBestValue;
138         // The position of the minimum found in the first pass.
139         int firstPassBestPosition;
140         // The sampling interval--the recommended number of focuser steps moved inward each iteration
141         // of the first pass.
142         int stepSize;
143         // The minimum focus position to use. Computed from the focuser limits and maxTravel.
144         int minPositionLimit;
145         // The maximum focus position to use. Computed from the focuser limits and maxTravel.
146         int maxPositionLimit;
147         // Counter for the number of times a v-curve minimum above the current position was found,
148         // which implies the initial focus sweep has passed the minimum and should be terminated.
149         int numPolySolutionsFound;
150         // Counter for the number of times a v-curve minimum above the passStartPosition was found,
151         // which implies the sweep should restart at a higher position.
152         int numRestartSolutionsFound;
153         // The index (into values and positions) when the most recent 2nd pass started.
154         int secondPassStartIndex;
155         // True if performing the first focus sweep.
156         bool inFirstPass;
157         // True if the 2nd pass has found a solution, and it's now optimizing the solution.
158         bool solutionPending;
159         // When we're near done, but the HFR just got worse, we may retry the current position
160         // in case the HFR value was noisy.
161         int retryNumber = 0;
162 
163         // Keep the solution-pending position/value for the status messages.
164         double solutionPendingValue = 0;
165         int solutionPendingPosition = 0;
166 
167         // RelativeHFR variables
168         bool relativeHFREnabled = false;
169         QVector<FocusStars> starLists;
170         double bestRelativeHFR = 0;
171         int bestRelativeHFRIndex = 0;
172 };
173 
174 // Copies the object. Used in testing to examine alternate possible inputs given
175 // the current state.
Copy()176 FocusAlgorithmInterface *LinearFocusAlgorithm::Copy()
177 {
178     LinearFocusAlgorithm *alg = new LinearFocusAlgorithm(params);
179     *alg = *this;
180     return dynamic_cast<FocusAlgorithmInterface*>(alg);
181 }
182 
MakeLinearFocuser(const FocusAlgorithmInterface::FocusParams & params)183 FocusAlgorithmInterface *MakeLinearFocuser(const FocusAlgorithmInterface::FocusParams &params)
184 {
185     return new LinearFocusAlgorithm(params);
186 }
187 
LinearFocusAlgorithm(const FocusParams & focusParams)188 LinearFocusAlgorithm::LinearFocusAlgorithm(const FocusParams &focusParams)
189     : FocusAlgorithmInterface(focusParams)
190 {
191     stopWatch.start();
192     // These variables don't get reset if we restart the algorithm.
193     numSteps = 0;
194     maxPositionLimit = std::min(params.maxPositionAllowed, params.startPosition + params.maxTravel);
195     minPositionLimit = std::max(params.minPositionAllowed, params.startPosition - params.maxTravel);
196     computeInitialPosition();
197 }
198 
computeInitialPosition()199 void LinearFocusAlgorithm::computeInitialPosition()
200 {
201     // These variables get reset if the algorithm is restarted.
202     stepSize = params.initialStepSize;
203     inFirstPass = true;
204     solutionPending = false;
205     retryNumber = 0;
206     firstPassBestValue = -1;
207     firstPassBestPosition = 0;
208     numPolySolutionsFound = 0;
209     numRestartSolutionsFound = 0;
210     secondPassStartIndex = -1;
211 
212     qCDebug(KSTARS_EKOS_FOCUS)
213             << QString("Linear: v3.3. 1st pass. Travel %1 initStep %2 pos %3 min %4 max %5 maxIters %6 tolerance %7 minlimit %8 maxlimit %9 init#steps %10")
214             .arg(params.maxTravel).arg(params.initialStepSize).arg(params.startPosition).arg(params.minPositionAllowed)
215             .arg(params.maxPositionAllowed).arg(params.maxIterations).arg(params.focusTolerance).arg(minPositionLimit).arg(
216                 maxPositionLimit)
217             .arg(params.initialOutwardSteps);
218 
219     requestedPosition = std::min(maxPositionLimit,
220                                  static_cast<int>(params.startPosition + params.initialOutwardSteps * params.initialStepSize));
221     passStartPosition = requestedPosition;
222     qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: initialPosition %1 sized %2")
223                                .arg(requestedPosition).arg(params.initialStepSize);
224 }
225 
226 // The Problem:
227 // The HFR values passed in to these focus algorithms are simply the mean or median HFR values
228 // for all stars detected (with some other constraints). However, due to randomness in the
229 // star detection scheme, two sets of star measurements may use different sets of actual stars to
230 // compute their mean HFRs. This adds noise to determining best focus (e.g. including a
231 // wider star in one set but not the other will tend to increase the HFR for that star set).
232 // relativeHFR tries to correct for this by comparing two sets of stars only using their common stars.
233 //
234 // The Solution:
235 // We maintain a reference set of stars, along with the HFR computed for those reference stars (bestRelativeHFR).
236 // We find the common set of stars between an input star set and those reference stars. We compute
237 // HFRs for the input stars in that common set, and for the reference common set as well.
238 // The ratio of those common-set HFRs multiply bestRelativeHFR to generate the relative HFR for this
239 // input star set--that is, it's the HFR "relative to the reference set". E.g. if the common-set HFR
240 // for the input stars is twice worse than that from the reference common stars, then the relative HFR
241 // would be 2 * bestRelativeHFR.
242 // The reference set is the best HFR set of stars seen so far, but only from the 1st pass.
243 // Thus, in the 2nd pass, the reference stars would be the best HFR set from the 1st pass.
244 //
245 // In unusual cases, e.g. when the common set can't be computed, we just return the input origHFR.
relativeHFR(double origHFR,const QList<Edge * > * stars)246 double LinearFocusAlgorithm::relativeHFR(double origHFR, const QList<Edge*> *stars)
247 {
248     constexpr double minHFR = 0.3;
249     if (origHFR < minHFR) return origHFR;
250 
251     const int currentIndex = values.size();
252     const bool isFirstSample = (currentIndex == 0);
253     double relativeHFR = origHFR;
254 
255     if (isFirstSample && stars != nullptr)
256         relativeHFREnabled = true;
257 
258     if (relativeHFREnabled && stars == nullptr)
259     {
260         // Error--we have been getting relativeHFR stars, and now we're not.
261         qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: Error: Inconsistent relativeHFR, disabling.");
262         relativeHFREnabled = false;
263     }
264 
265     if (!relativeHFREnabled)
266         return origHFR;
267 
268     if (starLists.size() != positions.size())
269     {
270         // Error--inconsistent data structures!
271         qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: Error: Inconsistent relativeHFR data structures, disabling.");
272         relativeHFREnabled = false;
273         return origHFR;
274     }
275 
276     // Copy the input stars.
277     // FocusStars enables us to measure HFR relatively consistently,
278     // by using the same stars when comparing two measurements.
279     constexpr int starDistanceThreshold = 5;  // Max distance (pixels) for two positions to be considered the same star.
280     FocusStars fs(*stars, starDistanceThreshold);
281     starLists.push_back(fs);
282     auto &currentStars = starLists.back();
283 
284     if (isFirstSample)
285     {
286         relativeHFR = currentStars.getHFR();
287         if (relativeHFR <= 0)
288         {
289             // Fall back to the simpler scheme.
290             relativeHFREnabled = false;
291             return origHFR;
292         }
293         // 1st measurement. By definition this is the best HFR.
294         bestRelativeHFR = relativeHFR;
295         bestRelativeHFRIndex = currentIndex;
296     }
297     else
298     {
299         // HFR computed relative to the best measured so far.
300         auto &bestStars = starLists[bestRelativeHFRIndex];
301         double hfr = currentStars.relativeHFR(bestStars, bestRelativeHFR);
302         if (hfr > 0)
303         {
304             relativeHFR = hfr;
305         }
306         else
307         {
308             relativeHFR = currentStars.getHFR();
309             if (relativeHFR <= 0)
310                 return origHFR;
311         }
312 
313         // In the 1st pass we compute the current HFR relative to the best HFR measured yet.
314         // In the 2nd pass we compute the current HFR relative to the best HFR in the 1st pass.
315         if (inFirstPass && relativeHFR <= bestRelativeHFR)
316         {
317             bestRelativeHFR = relativeHFR;
318             bestRelativeHFRIndex = currentIndex;
319         }
320     }
321 
322     qCDebug(KSTARS_EKOS_FOCUS) << QString("RelativeHFR: orig %1 computed %2 relative %3")
323                                .arg(origHFR).arg(currentStars.getHFR()).arg(relativeHFR);
324 
325     return relativeHFR;
326 }
327 
newMeasurement(int position,double value,const QList<Edge * > * stars)328 int LinearFocusAlgorithm::newMeasurement(int position, double value, const QList<Edge*> *stars)
329 {
330     double origValue = value;
331     value = relativeHFR(value, stars);
332     int thisStepSize = stepSize;
333     ++numSteps;
334     if (inFirstPass)
335         qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: step %1, newMeasurement(%2, %3 -> %4, %5)")
336                                    .arg(numSteps).arg(position).arg(origValue).arg(value)
337                                    .arg(stars == nullptr ? 0 : stars->size());
338     else
339         qCDebug(KSTARS_EKOS_FOCUS)
340                 << QString("Linear: step %1, newMeasurement(%2, %3 -> %4, %5) 1stBest %6 %7").arg(numSteps)
341                 .arg(position).arg(origValue).arg(value).arg(stars == nullptr ? 0 : stars->size())
342                 .arg(firstPassBestPosition).arg(firstPassBestValue, 0, 'f', 3);
343 
344     const int LINEAR_POSITION_TOLERANCE = params.initialStepSize;
345     if (abs(position - requestedPosition) > LINEAR_POSITION_TOLERANCE)
346     {
347         qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: error didn't get the requested position");
348         return requestedPosition;
349     }
350     // Have we already found a solution?
351     if (focusSolution != -1)
352     {
353         doneString = i18n("Called newMeasurement after a solution was found.");
354         qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: error %1").arg(doneString);
355         debugLog();
356         return -1;
357     }
358 
359     // Store the sample values.
360     values.push_back(value);
361     positions.push_back(position);
362     if (inFirstPass)
363     {
364         pass1Values.push_back(value);
365         pass1Positions.push_back(position);
366     }
367 
368     // If we're in the 2nd pass and either
369     // - the current value is within tolerance, or
370     // - we're optimizing because we've previously found a within-tolerance solution,
371     // then setupPendingSolution decides whether to continue optimizing or to complete.
372     if (solutionPending ||
373             (!inFirstPass && (value < firstPassBestValue * (1.0 + params.focusTolerance))))
374     {
375         if (setupPendingSolution(position))
376             // We can continue to look a little further.
377             return completeIteration(retryNumber > 0 ? 0 : stepSize);
378         else
379             // Finish now
380             return setupSolution(position, value);
381     }
382 
383     if (inFirstPass)
384     {
385         constexpr int kMinPolynomialPoints = 5;
386         constexpr int kNumPolySolutionsRequired = 2;
387         constexpr int kNumRestartSolutionsRequired = 3;
388         constexpr double kDecentValue = 2.5;
389 
390         if (values.size() >= kMinPolynomialPoints)
391         {
392             PolynomialFit fit(2, positions, values);
393             double minPos, minVal;
394             bool foundFit = fit.findMinimum(position, 0, params.maxPositionAllowed, &minPos, &minVal);
395             if (!foundFit)
396             {
397                 // I've found that the first sample can be odd--perhaps due to backlash.
398                 // Try again skipping the first sample, if we have sufficient points.
399                 if (values.size() > kMinPolynomialPoints)
400                 {
401                     PolynomialFit fit2(2, positions.mid(1), values.mid(1));
402                     foundFit = fit2.findMinimum(position, 0, params.maxPositionAllowed, &minPos, &minVal);
403                     minPos = minPos + 1;
404                 }
405             }
406             if (foundFit)
407             {
408                 const int distanceToMin = static_cast<int>(position - minPos);
409                 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: poly fit(%1): %2 = %3 @ %4 distToMin %5")
410                                            .arg(positions.size()).arg(minPos).arg(minVal).arg(position).arg(distanceToMin);
411                 if (distanceToMin >= 0)
412                 {
413                     // The minimum is further inward.
414                     numPolySolutionsFound = 0;
415                     numRestartSolutionsFound = 0;
416                     qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: Solutions reset %1 = %2").arg(minPos).arg(minVal);
417                     if (value > kDecentValue)
418                     {
419                         // Only skip samples if the HFV values aren't very good.
420                         const int stepsToMin = distanceToMin / stepSize;
421                         // Temporarily increase the step size if the minimum is very far inward.
422                         if (stepsToMin >= 8)
423                             thisStepSize = stepSize * 4;
424                         else if (stepsToMin >= 4)
425                             thisStepSize = stepSize * 2;
426                     }
427                 }
428                 else if (!bestSamplesHeuristic())
429                 {
430                     // We have potentially passed the bottom of the curve,
431                     // but it's possible it is further back than the start of our sweep.
432                     if (minPos > passStartPosition)
433                     {
434                         numRestartSolutionsFound++;
435                         qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: RESTART Solution #%1 %2 = %3 @ %4")
436                                                    .arg(numRestartSolutionsFound).arg(minPos).arg(minVal).arg(position);
437                     }
438                     else
439                     {
440                         numPolySolutionsFound++;
441                         numRestartSolutionsFound = 0;
442                         qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: Solution #%1: %2 = %3 @ %4")
443                                                    .arg(numPolySolutionsFound).arg(minPos).arg(minVal).arg(position);
444                     }
445                 }
446 
447                 if (numPolySolutionsFound >= kNumPolySolutionsRequired)
448                 {
449                     // We found a minimum. Setup the 2nd pass. We could use either the polynomial min or the
450                     // min measured star as the target HFR. I've seen using the polynomial minimum to be
451                     // sometimes too conservative, sometimes too low. For now using the min sample.
452                     double minMeasurement = *std::min_element(values.begin(), values.end());
453                     qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: 1stPass solution @ %1: pos %2 val %3, min measurement %4")
454                                                .arg(position).arg(minPos).arg(minVal).arg(minMeasurement);
455                     return setupSecondPass(static_cast<int>(minPos), minMeasurement);
456                 }
457                 else if (numRestartSolutionsFound >= kNumRestartSolutionsRequired)
458                 {
459                     // We need to restart--that is the error is rising and thus our initial position
460                     // was already past the minimum. Restart the algorithm with a greater initial position.
461                     // If the min position from the polynomial solution is not too far from the current start position
462                     // use that, but don't let it go too far away.
463                     const double highestStartPosition = params.startPosition + params.initialOutwardSteps * params.initialStepSize;
464                     params.startPosition = std::min(minPos, highestStartPosition);
465                     computeInitialPosition();
466                     qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: Restart. Current pos %1, min pos %2, min val %3, re-starting at %4")
467                                                .arg(position).arg(minPos).arg(minVal).arg(requestedPosition);
468                     return requestedPosition;
469                 }
470 
471             }
472             else
473             {
474                 // Minimum failed indicating the 2nd-order polynomial is an inverted U--it has a maximum,
475                 // but no minimum.  This is, of course, not a sensible solution for the focuser, but can
476                 // happen with noisy data and perhaps small step sizes. We still might be able to take advantage,
477                 // and notice whether the polynomial is increasing or decreasing locally. For now, do nothing.
478                 qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: ******** No poly min: Poly must be inverted");
479             }
480         }
481         else
482         {
483             // Don't have enough samples to reliably fit a polynomial.
484             // Simply step the focus in one more time and iterate.
485         }
486     }
487     else if (gettingWorse())
488     {
489         // Doesn't look like we'll find something close to the min. Retry the 2nd pass.
490         qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: getting worse, re-running 2nd pass");
491         return setupSecondPass(firstPassBestPosition, firstPassBestValue);
492     }
493 
494     return completeIteration(thisStepSize);
495 }
496 
setupSolution(int position,double value)497 int LinearFocusAlgorithm::setupSolution(int position, double value)
498 {
499     focusSolution = position;
500     focusHFR = value;
501     done = true;
502     doneString = i18n("Solution found.");
503     qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: 2ndPass solution @ %1 = %2 (best %3)")
504                                .arg(position).arg(value).arg(firstPassBestValue);
505     debugLog();
506     return -1;
507 }
508 
completeIteration(int step)509 int LinearFocusAlgorithm::completeIteration(int step)
510 {
511     if (numSteps == params.maxIterations - 2)
512     {
513         // If we're close to exceeding the iteration limit, retry this pass near the old minimum position.
514         const int minIndex = static_cast<int>(std::min_element(values.begin(), values.end()) - values.begin());
515         return setupSecondPass(positions[minIndex], values[minIndex], 0.5);
516     }
517     else if (numSteps > params.maxIterations)
518     {
519         // Fail. Exceeded our alloted number of iterations.
520         done = true;
521         doneString = i18n("Too many steps.");
522         qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: error %1").arg(doneString);
523         debugLog();
524         return -1;
525     }
526 
527     // Setup the next sample.
528     requestedPosition = requestedPosition - step;
529 
530     // Make sure the next sample is within bounds.
531     if (requestedPosition < minPositionLimit)
532     {
533         // The position is too low. Pick the min value and go to (or retry) a 2nd iteration.
534         const int minIndex = static_cast<int>(std::min_element(values.begin(), values.end()) - values.begin());
535         qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: reached end without Vmin. Restarting %1 pos %2 value %3")
536                                    .arg(minIndex).arg(positions[minIndex]).arg(values[minIndex]);
537         return setupSecondPass(positions[minIndex], values[minIndex]);
538     }
539     qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: requesting position %1").arg(requestedPosition);
540     return requestedPosition;
541 }
542 
543 // This is called in the 2nd pass, when we've found a sample point that's within tolerance
544 // of the 1st pass' best value. Since it's within tolerance, the 2nd pass might finish, using
545 // the current value as the final solution. However, this method checks to see if we might go
546 // a bit further. Once solutionPending is true, it's committed to finishing soon.
547 // It goes further if:
548 // - the current HFR value is an improvement on the previous 2nd-pass value,
549 // - the number of steps taken so far is not close the max number of allowable steps.
550 // If instead the HFR got worse, we retry the current position a few times to see if it might
551 // get better before giving up.
setupPendingSolution(int position)552 bool LinearFocusAlgorithm::setupPendingSolution(int position)
553 {
554     const int length = values.size();
555     const int secondPassIndex = length - secondPassStartIndex;
556     const double thisValue = values[length - 1];
557 
558     // ReferenceValue is the value used in the notGettingWorse computation.
559     // Basically, it's the previous value, or the value before retries.
560     double referenceValue = (secondPassIndex <= 1) ? 1e6 : values[length - 2];
561     if (retryNumber > 0 && length - (2 + retryNumber) >= 0)
562         referenceValue = values[length - (2 + retryNumber)];
563 
564     // NotGettingWorse: not worse than the previous (non-repeat) 2nd pass sample, or the 1st 2nd pass sample.
565     const bool notGettingWorse = (secondPassIndex <= 1) || (thisValue <= referenceValue);
566 
567     // CouldGoFurther: Not near a boundary in position or number of steps.
568     const bool couldGoFather = (numSteps < params.maxIterations - 2) && (position - stepSize > minPositionLimit);
569 
570     // Allow passing the 1st pass' minimum HFR position, but take smaller steps and don't retry as much.
571     int maxNumRetries = 3;
572     if (position - stepSize / 2 < firstPassBestPosition)
573     {
574         stepSize = std::max(2, params.initialStepSize / 4);
575         maxNumRetries = 1;
576     }
577 
578     if (notGettingWorse && couldGoFather)
579     {
580         qCDebug(KSTARS_EKOS_FOCUS)
581                 << QString("Linear: %1: Position(%2) & HFR(%3) -- Pass1: %4 %5, solution pending, searching further")
582                 .arg(length).arg(position).arg(thisValue, 0, 'f', 3).arg(firstPassBestPosition).arg(firstPassBestValue, 0, 'f', 3);
583         solutionPending = true;
584         solutionPendingPosition = position;
585         solutionPendingValue = thisValue;
586         retryNumber = 0;
587         return true;
588     }
589     else if (solutionPending && couldGoFather && retryNumber < maxNumRetries &&
590              (secondPassIndex > 1) && (thisValue >= referenceValue))
591     {
592         qCDebug(KSTARS_EKOS_FOCUS)
593                 << QString("Linear: %1: Position(%2) & HFR(%3) -- Pass1: %4 %5, solution pending, got worse, retrying")
594                 .arg(length).arg(position).arg(thisValue, 0, 'f', 3).arg(firstPassBestPosition).arg(firstPassBestValue, 0, 'f', 3);
595         // Try this poisition again.
596         retryNumber++;
597         return true;
598     }
599     qCDebug(KSTARS_EKOS_FOCUS)
600             << QString("Linear: %1: Position(%2) & HFR(%3) -- Pass1: %4 %5, finishing, can't go further")
601             .arg(length).arg(position).arg(thisValue, 0, 'f', 3).arg(firstPassBestPosition).arg(firstPassBestValue, 0, 'f', 3);
602     retryNumber = 0;
603     return false;
604 }
605 
debugLog()606 void LinearFocusAlgorithm::debugLog()
607 {
608     QString str("Linear: points=[");
609     for (int i = 0; i < positions.size(); ++i)
610     {
611         str.append(QString("(%1, %2)").arg(positions[i]).arg(values[i]));
612         if (i < positions.size() - 1)
613             str.append(", ");
614     }
615     str.append(QString("];iterations=%1").arg(numSteps));
616     str.append(QString(";duration=%1").arg(stopWatch.elapsed() / 1000));
617     str.append(QString(";solution=%1").arg(focusSolution));
618     str.append(QString(";HFR=%1").arg(focusHFR));
619     str.append(QString(";filter='%1'").arg(params.filterName));
620     str.append(QString(";temperature=%1").arg(params.temperature));
621 
622     qCDebug(KSTARS_EKOS_FOCUS) << str;
623 }
624 
setupSecondPass(int position,double value,double margin)625 int LinearFocusAlgorithm::setupSecondPass(int position, double value, double margin)
626 {
627     firstPassBestPosition = position;
628     firstPassBestValue = value;
629     inFirstPass = false;
630     solutionPending = false;
631     secondPassStartIndex = values.size();
632 
633     // Arbitrarily go back "margin" steps above the best position.
634     // Could be a problem if backlash were worse than that many steps.
635     requestedPosition = std::min(static_cast<int>(firstPassBestPosition + stepSize * margin), maxPositionLimit);
636     stepSize = params.initialStepSize / 2;
637     qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: 2ndPass starting at %1 step %2").arg(requestedPosition).arg(stepSize);
638     return requestedPosition;
639 }
640 
641 // Return true if one of the 2 recent samples is among the best 2 samples so far.
bestSamplesHeuristic()642 bool LinearFocusAlgorithm::bestSamplesHeuristic()
643 {
644     const int length = values.size();
645     if (length < 5) return true;
646     QVector<double> tempValues = values;
647     std::nth_element(tempValues.begin(), tempValues.begin() + 2, tempValues.end());
648     double secondBest = tempValues[1];
649     if ((values[length - 1] <= secondBest) || (values[length - 2] <= secondBest))
650         return true;
651     return false;
652 }
653 
654 // Return true if there are "streak" consecutive values which are successively worse.
gettingWorse()655 bool LinearFocusAlgorithm::gettingWorse()
656 {
657     // Must have this many consecutive values getting worse.
658     constexpr int streak = 3;
659     const int length = values.size();
660     if (secondPassStartIndex < 0)
661         return false;
662     if (length < streak + 1)
663         return false;
664     // This insures that all the values we're checking are in the latest 2nd pass.
665     if (length - secondPassStartIndex < streak + 1)
666         return false;
667     for (int i = length - 1; i >= length - streak; --i)
668         if (values[i] <= values[i - 1])
669             return false;
670     return true;
671 }
672 
673 }
674 
675