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 ¶ms);
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 ¶ms)
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 ¤tStars = 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