diff --git a/kstars/ekos/focus/focus.h b/kstars/ekos/focus/focus.h --- a/kstars/ekos/focus/focus.h +++ b/kstars/ekos/focus/focus.h @@ -408,7 +408,9 @@ /// Positions //////////////////////////////////////////////////////////////////// void getAbsFocusPosition(); + bool autoFocusChecks(); void autoFocusAbs(); + void autoFocusLinear(); void autoFocusRel(); void resetButtons(); void stop(bool aborted = false); diff --git a/kstars/ekos/focus/focus.cpp b/kstars/ekos/focus/focus.cpp --- a/kstars/ekos/focus/focus.cpp +++ b/kstars/ekos/focus/focus.cpp @@ -477,8 +477,8 @@ canRelMove = currentFocuser->canRelMove(); // In case we have a purely relative focuser, we pretend - // it is an absolute focuser with initial point set at 50,000 - // This is done we can use the same algorithm used for absolute focuser + // it is an absolute focuser with initial point set at 50,000. + // This is done we can use the same algorithm used for absolute focuser. if (canAbsMove == false && canRelMove == true) { currentPosition = 50000; @@ -488,6 +488,17 @@ canTimerMove = currentFocuser->canTimerMove(); + // In case we have a timer-based focuser and using the linear focus algorithm, + // we pretend it is an absolute focuser with initial point set at 50,000. + // These variables don't have in impact on timer-based focusers if the algorithm + // is not the linear focus algorithm. + if (!canAbsMove && !canRelMove && canTimerMove) + { + currentPosition = 50000; + absMotionMax = 100000; + absMotionMin = 0; + } + focusType = (canRelMove || canAbsMove || canTimerMove) ? FOCUS_AUTO : FOCUS_MANUAL; bool hasBacklash = currentFocuser->hasBacklash(); @@ -597,6 +608,10 @@ { pulseDuration = stepIN->value(); + absIterations = 0; + absMotionMax = 100000; + absMotionMin = 0; + if (pulseDuration <= MINIMUM_PULSE_TIMER) { appendLogText(i18n("Starting pulse step is too low. Increase the step size to %1 or higher...", @@ -668,12 +683,13 @@ KSNotification::event(QLatin1String("FocusStarted"), i18n("Autofocus operation started")); - if (focusAlgorithm == FOCUS_LINEAR && (canAbsMove || canRelMove)) + // Used for all the focuser types. + if (focusAlgorithm == FOCUS_LINEAR) { const int position = static_cast(currentPosition); FocusAlgorithmInterface::FocusParams params( maxTravelIN->value(), stepIN->value(), position, absMotionMin, absMotionMax, - MAXIMUM_ABS_ITERATIONS, toleranceIN->value() / 100.0); + MAXIMUM_ABS_ITERATIONS, toleranceIN->value() / 100.0, filter()); linearFocuser.reset(MakeLinearFocuser(params)); const int newPosition = adjustLinearPosition(position, linearFocuser->initialPosition()); if (newPosition != position) @@ -733,7 +749,7 @@ void Focus::stop(bool aborted) { - qCDebug(KSTARS_EKOS_FOCUS) << "Stopppig Focus"; + qCDebug(KSTARS_EKOS_FOCUS) << "Stopping Focus"; captureTimeout.stop(); @@ -937,9 +953,11 @@ if (currentFocuser == nullptr) return false; + // This needs to be re-thought. Just returning does not set the timer + // and the algorithm ends in limbo. // Ignore zero - if (amount == 0) - return true; + // if (amount == 0) + // return true; if (currentFocuser->isConnected() == false) { @@ -1254,7 +1272,12 @@ // Append point to the #Iterations vs #HFR chart in case of looping or in case in autofocus with a focus // that does not support position feedback. - if (inFocusLoop || (inAutoFocus && canAbsMove == false && canRelMove == false)) + + // If inAutoFocus is true without canAbsMove and without canRelMove, canTimerMove must be true. + // We'd only want to execute this if the focus linear algorithm is not being used, as that + // algorithm simulates a position-based system even for timer-based focusers. + if (inFocusLoop || (inAutoFocus && canAbsMove == false && canRelMove == false && + focusAlgorithm != FOCUS_LINEAR)) { if (hfr_position.empty()) hfr_position.append(1); @@ -1533,8 +1556,10 @@ // Now let's kick in the algorithms - // Position-based algorithms - if (canAbsMove || canRelMove) + if (focusAlgorithm == FOCUS_LINEAR) + autoFocusLinear(); + else if (canAbsMove || canRelMove) + // Position-based algorithms autoFocusAbs(); else // Time open-looped algorithms @@ -1585,10 +1610,14 @@ if (hfr_value.size() > 0) minHFRVal = std::max(0, static_cast(0.9 * *std::min_element(hfr_value.begin(), hfr_value.end()))); - if (inFocusLoop == false && (canAbsMove || canRelMove)) + // True for the position-based algorithms and those that simulate position. + if (inFocusLoop == false && (canAbsMove || canRelMove || (focusAlgorithm == FOCUS_LINEAR))) { - //HFRPlot->xAxis->setLabel(i18n("Position")); - HFRPlot->xAxis->setRange(minPos - pulseDuration, maxPos + pulseDuration); + const double minPosition = hfr_position.empty() ? + 0 : *std::min_element(hfr_position.constBegin(), hfr_position.constEnd()); + const double maxPosition = hfr_position.empty() ? + 1e6 : *std::max_element(hfr_position.constBegin(), hfr_position.constEnd()); + HFRPlot->xAxis->setRange(minPosition - pulseDuration, maxPosition + pulseDuration); HFRPlot->yAxis->setRange(minHFRVal, maxHFR); } else @@ -1649,32 +1678,14 @@ emit newProfilePixmap(profilePixmap); } -void Focus::autoFocusAbs() +bool Focus::autoFocusChecks() { - static int minHFRPos = 0, focusOutLimit = 0, focusInLimit = 0; - static double minHFR = 0; - double targetPosition = 0, delta = 0; - - QString deltaTxt = QString("%1").arg(fabs(currentHFR - minHFR) * 100.0, 0, 'g', 3); - QString HFRText = QString("%1").arg(currentHFR, 0, 'g', 3); - - qCDebug(KSTARS_EKOS_FOCUS) << "========================================"; - qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR: " << currentHFR << " Current Position: " << currentPosition; - qCDebug(KSTARS_EKOS_FOCUS) << "Last minHFR: " << minHFR << " Last MinHFR Pos: " << minHFRPos; - qCDebug(KSTARS_EKOS_FOCUS) << "Delta: " << deltaTxt << "%"; - qCDebug(KSTARS_EKOS_FOCUS) << "========================================"; - - if (minHFR) - appendLogText(i18n("FITS received. HFR %1 @ %2. Delta (%3%)", HFRText, currentPosition, deltaTxt)); - else - appendLogText(i18n("FITS received. HFR %1 @ %2.", HFRText, currentPosition)); - if (++absIterations > MAXIMUM_ABS_ITERATIONS) { appendLogText(i18n("Autofocus failed to reach proper focus. Try increasing tolerance value.")); abort(); setAutoFocusResult(false); - return; + return false; } // No stars detected, try to capture again @@ -1685,7 +1696,7 @@ appendLogText(i18n("No stars detected, capturing again...")); capture(); noStarCount++; - return; + return false; } else if (noStarCount == MAX_RECAPTURE_RETRIES) { @@ -1697,78 +1708,118 @@ appendLogText(i18n("Failed to detect any stars. Reset frame and try again.")); abort(); setAutoFocusResult(false); - return; + return false; } } else noStarCount = 0; + return true; +} - if (hfr_position.empty()) - { - maxPos = 1; - minPos = 1e6; - } - - if (currentPosition > maxPos) - maxPos = currentPosition; - if (currentPosition < minPos) - minPos = currentPosition; +void Focus::autoFocusLinear() +{ + if (!autoFocusChecks()) + return; hfr_position.append(currentPosition); hfr_value.append(currentHFR); drawHFRPlot(); - if (focusAlgorithm == FOCUS_LINEAR) + if (hfr_position.size() > 3) { - if (hfr_position.size() > 3) + polynomialFit.reset(new PolynomialFit(2, hfr_position, hfr_value)); + double min_position, min_value; + const FocusAlgorithmInterface::FocusParams ¶ms = linearFocuser->getParams(); + double searchMin = std::max(params.minPositionAllowed, params.startPosition - params.maxTravel); + double searchMax = std::min(params.maxPositionAllowed, params.startPosition + params.maxTravel); + if (polynomialFit->findMinimum(linearFocuser->getParams().startPosition, + searchMin, searchMax, &min_position, &min_value)) { - // For now, just plots, doesn't use the polynomial algorithmically. - polynomialFit.reset(new PolynomialFit(2, hfr_position, hfr_value)); - double min_position, min_value; - const FocusAlgorithmInterface::FocusParams ¶ms = linearFocuser->getParams(); - double searchMin = std::max(params.minPositionAllowed, params.startPosition - params.maxTravel); - double searchMax = std::min(params.maxPositionAllowed, params.startPosition + params.maxTravel); - if (polynomialFit->findMinimum(linearFocuser->getParams().startPosition, - searchMin, searchMax, &min_position, &min_value)) - { - polynomialFit->drawPolynomial(HFRPlot, polynomialGraph); - polynomialFit->drawMinimum(HFRPlot, focusPoint, min_position, min_value, font()); - } + QPen pen; + pen.setWidth(1); + pen.setColor(QColor(180,180,180)); + polynomialGraph->setPen(pen); + + polynomialFit->drawPolynomial(HFRPlot, polynomialGraph); + polynomialFit->drawMinimum(HFRPlot, focusPoint, min_position, min_value, font()); } + else + { + // During development of this algorithm, we show the polynomial graph in red if + // no minimum was found. That happens when the order-2 polynomial is an inverted U + // instead of a U shape (i.e. it has a maximum, but no minimum). + QPen pen; + pen.setWidth(1); + pen.setColor(QColor(254,0,0)); + polynomialGraph->setPen(pen); + polynomialFit->drawPolynomial(HFRPlot, polynomialGraph); + + polynomialGraph->data()->clear(); + focusPoint->data()->clear(); + } + } - const int nextPosition = adjustLinearPosition( - static_cast(currentPosition), - linearFocuser->newMeasurement(currentPosition, currentHFR)); - if (nextPosition == -1) + const int nextPosition = adjustLinearPosition( + static_cast(currentPosition), + linearFocuser->newMeasurement(currentPosition, currentHFR)); + if (nextPosition == -1) + { + if (linearFocuser->isDone() && linearFocuser->solution() != -1) { - if (linearFocuser->isDone() && linearFocuser->solution() != -1) - { - appendLogText(i18np("Autofocus complete after %1 iteration.", - "Autofocus complete after %1 iterations.", hfr_position.count())); - stop(); - setAutoFocusResult(true); - } - else - { - qCDebug(KSTARS_EKOS_FOCUS) << linearFocuser->doneReason(); - appendLogText("Linear autofocus algorithm aborted."); - abort(); - setAutoFocusResult(false); - } - return; + appendLogText(i18np("Autofocus complete after %1 iteration.", + "Autofocus complete after %1 iterations.", hfr_position.count())); + stop(); + setAutoFocusResult(true); } else { - delta = nextPosition - currentPosition; - if (!changeFocus(static_cast(delta))) - { - abort(); - setAutoFocusResult(false); - } - return; + qCDebug(KSTARS_EKOS_FOCUS) << linearFocuser->doneReason(); + appendLogText("Linear autofocus algorithm aborted."); + abort(); + setAutoFocusResult(false); } + return; } + else + { + const int delta = nextPosition - currentPosition; + if (!changeFocus(delta)) + { + abort(); + setAutoFocusResult(false); + } + return; + } +} + +void Focus::autoFocusAbs() +{ + static int minHFRPos = 0, focusOutLimit = 0, focusInLimit = 0; + static double minHFR = 0; + double targetPosition = 0, delta = 0; + + QString deltaTxt = QString("%1").arg(fabs(currentHFR - minHFR) * 100.0, 0, 'g', 3); + QString HFRText = QString("%1").arg(currentHFR, 0, 'g', 3); + + qCDebug(KSTARS_EKOS_FOCUS) << "========================================"; + qCDebug(KSTARS_EKOS_FOCUS) << "Current HFR: " << currentHFR << " Current Position: " << currentPosition; + qCDebug(KSTARS_EKOS_FOCUS) << "Last minHFR: " << minHFR << " Last MinHFR Pos: " << minHFRPos; + qCDebug(KSTARS_EKOS_FOCUS) << "Delta: " << deltaTxt << "%"; + qCDebug(KSTARS_EKOS_FOCUS) << "========================================"; + + if (minHFR) + appendLogText(i18n("FITS received. HFR %1 @ %2. Delta (%3%)", HFRText, currentPosition, deltaTxt)); + else + appendLogText(i18n("FITS received. HFR %1 @ %2.", HFRText, currentPosition)); + + if (!autoFocusChecks()) + return; + + hfr_position.append(currentPosition); + hfr_value.append(currentHFR); + + drawHFRPlot(); switch (lastFocusDirection) { @@ -2355,6 +2406,10 @@ if (canAbsMove == false && canRelMove == false && inAutoFocus) { + // Used by the linear focus algorithm. Ignored if that's not in use for the timer-focuser. + INumber *pos = IUFindNumber(nvp, "FOCUS_TIMER_VALUE"); + if (pos) + currentPosition += pos->value * (lastFocusDirection == FOCUS_IN ? -1 : 1); autoFocusProcessPositionChange(nvp->s); } else if (nvp->s == IPS_ALERT) diff --git a/kstars/ekos/focus/focus.ui b/kstars/ekos/focus/focus.ui --- a/kstars/ekos/focus/focus.ui +++ b/kstars/ekos/focus/focus.ui @@ -1201,7 +1201,7 @@ - Linear (Experimental) + Linear (Experimental v2) diff --git a/kstars/ekos/focus/focusalgorithms.h b/kstars/ekos/focus/focusalgorithms.h --- a/kstars/ekos/focus/focusalgorithms.h +++ b/kstars/ekos/focus/focusalgorithms.h @@ -39,14 +39,16 @@ int maxIterations; // The focus algorithm may terminate if it gets within this fraction of the best focus, e.g. 0.10. double focusTolerance; + // The name of the filter used, if any. + QString filterName; FocusParams(int _maxTravel, int _initialStepSize, int _startPosition, int _minPositionAllowed, int _maxPositionAllowed, - int _maxIterations, double _focusTolerance) : + int _maxIterations, double _focusTolerance, const QString &filterName_) : maxTravel(_maxTravel), initialStepSize(_initialStepSize), startPosition(_startPosition), minPositionAllowed(_minPositionAllowed), maxPositionAllowed(_maxPositionAllowed), maxIterations(_maxIterations), - focusTolerance(_focusTolerance) {} + focusTolerance(_focusTolerance), filterName(filterName_) {} }; // Constructor initializes an autofocus algorithm from the input params. @@ -79,6 +81,7 @@ FocusParams params; bool done = false; int focusSolution = -1; + double focusHFR = -1; QString doneString; }; diff --git a/kstars/ekos/focus/focusalgorithms.cpp b/kstars/ekos/focus/focusalgorithms.cpp --- a/kstars/ekos/focus/focusalgorithms.cpp +++ b/kstars/ekos/focus/focusalgorithms.cpp @@ -9,6 +9,7 @@ #include "focusalgorithms.h" +#include "polynomialfit.h" #include #include "kstars.h" @@ -46,44 +47,52 @@ // Determines the desired focus position for the first sample. void computeInitialPosition(); - // Returns a score evaluating whether index is the location of a minumum in the - // HFR vector. -1 means no. A positive integer indicates yes, but can be overruled - // by another index with a better score. - int evaluateMinimum(int index, bool lastSample); - // Sets the internal state for re-finding the minimum, and returns the requested next // position to sample. - int setupSecondPass(int minIndex, double margin = 2.0); + int setupSecondPass(int position, double value, double margin = 2.0); // Used in the 2nd pass. Focus is getting worse. Requires several consecutive samples getting worse. bool gettingWorse(); + // Adds to the debug log a line summarizing the result of running this algorithm. + void debugLog(); + + // Used to time the focus algorithm. + QTime stopWatch; + // A vector containing the HFR values sampled by this algorithm so far. QVector values; // A vector containing the focus positions corresponding to the HFR values stored above. QVector positions; // Focus position requested by this algorithm the previous step. int requestedPosition; - // Number of iterations processed so far. + // The position where the first pass is begun. Usually requestedPosition unless there's a restart. + int passStartPosition; + // Number of iterations processed so far. Used to see it doesn't exceed params.maxIterations. int numSteps; // The best value in the first pass. The 2nd pass attempts to get within // tolerance of this value. - double minValue; - // The sampling interval--the number of focuser steps reduced each iteration. + double firstPassBestValue; + // The position of the minimum found in the first pass. + int firstPassBestPosition; + // The sampling interval--the recommended number of focuser steps moved inward each iteration + // of the first pass. int stepSize; // The minimum focus position to use. Computed from the focuser limits and maxTravel. int minPositionLimit; // The maximum focus position to use. Computed from the focuser limits and maxTravel. int maxPositionLimit; - // The index of the minimum found in the first pass. - int firstPassMinIndex; - // True if the first iteration has already found a minimum. - bool minimumFound; - // True if the 2nd pass re-found the minimum, and thus the algorithm is done. - bool minimumReFound; - // True if the algorithm failed, and thus is done. - bool searchFailed; + // Counter for the number of times a v-curve minimum above the current position was found, + // which implies the initial focus sweep has passed the minimum and should be terminated. + int numPolySolutionsFound; + // Counter for the number of times a v-curve minimum above the passStartPosition was found, + // which implies the sweep should restart at a higher position. + int numRestartSolutionsFound; + // The index (into values and positions) when the most recent 2nd pass started. + int secondPassStartIndex; + // True if performing the first focus sweep. + bool inFirstPass; }; FocusAlgorithmInterface *MakeLinearFocuser(const FocusAlgorithmInterface::FocusParams& params) @@ -94,24 +103,29 @@ LinearFocusAlgorithm::LinearFocusAlgorithm(const FocusParams &focusParams) : FocusAlgorithmInterface(focusParams) { - requestedPosition = params.startPosition; - stepSize = params.initialStepSize; - minimumFound = false; + stopWatch.start(); + // These variables don't get reset if we restart the algorithm. numSteps = 0; - minimumReFound = false; - searchFailed = false; - minValue = 0; - maxPositionLimit = std::min(params.maxPositionAllowed, params.startPosition + params.maxTravel); minPositionLimit = std::max(params.minPositionAllowed, params.startPosition - params.maxTravel); - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: Travel %1 initStep %2 pos %3 min %4 max %5 maxIters %6 tolerance %7 minlimit %8 maxlimit %9") - .arg(params.maxTravel).arg(params.initialStepSize).arg(params.startPosition).arg(params.minPositionAllowed) - .arg(params.maxPositionAllowed).arg(params.maxIterations).arg(params.focusTolerance).arg(minPositionLimit).arg(maxPositionLimit); computeInitialPosition(); } void LinearFocusAlgorithm::computeInitialPosition() { + // These variables get reset if the algorithm is restarted. + stepSize = params.initialStepSize; + inFirstPass = true; + firstPassBestValue = -1; + numPolySolutionsFound = 0; + numRestartSolutionsFound = 0; + secondPassStartIndex = -1; + + qCDebug(KSTARS_EKOS_FOCUS) + << QString("Linear: 1st pass. Travel %1 initStep %2 pos %3 min %4 max %5 maxIters %6 tolerance %7 minlimit %8 maxlimit %9") + .arg(params.maxTravel).arg(params.initialStepSize).arg(params.startPosition).arg(params.minPositionAllowed) + .arg(params.maxPositionAllowed).arg(params.maxIterations).arg(params.focusTolerance).arg(minPositionLimit).arg(maxPositionLimit); + const int position = params.startPosition; int start, end; @@ -159,180 +173,218 @@ } } requestedPosition = start; - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: initialPosition %1 end %2 steps %3 sized %4") + passStartPosition = requestedPosition; + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: initialPosition %1 end %2 steps %3 sized %4") .arg(start).arg(end).arg((start-end)/params.initialStepSize).arg(params.initialStepSize); } int LinearFocusAlgorithm::newMeasurement(int position, double value) { + int thisStepSize = stepSize; ++numSteps; - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: step %1, newMeasurement(%2, %3)").arg(numSteps).arg(position).arg(value); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: step %1, newMeasurement(%2, %3)").arg(numSteps).arg(position).arg(value); // Not sure how to get a general value for this. Skip this check? constexpr int LINEAR_POSITION_TOLERANCE = 25; if (abs(position - requestedPosition) > LINEAR_POSITION_TOLERANCE) { - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: error didn't get the requested position"); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: error didn't get the requested position"); return requestedPosition; } // Have we already found a solution? if (focusSolution != -1) { doneString = i18n("Called newMeasurement after a solution was found."); - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: error %1").arg(doneString); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: error %1").arg(doneString); + debugLog(); return -1; } // Store the sample values. values.push_back(value); positions.push_back(position); - if (!minimumFound) + if (inFirstPass) { - // The first pass. We're looking for the minimum of a v-curve. - // Check all possible indices to see if we have a V-curve minimum position, and, - // if so, which is the best. - int bestScore = -1; - int bestIndex = -1; - // Is this the last sample before we bump into a limit. - bool lastSample = requestedPosition - stepSize < minPositionLimit; - for (int i = 0; i < values.size(); ++i) { - const int score = evaluateMinimum(i, lastSample); - if (score > 0) { - minimumFound = true; - if (score > bestScore) { - bestScore = score; - bestIndex = i; + constexpr int kMinPolynomialPoints = 5; + constexpr int kNumPolySolutionsRequired = 3; + constexpr int kNumRestartSolutionsRequired = 3; + + if (values.size() >= kMinPolynomialPoints) + { + PolynomialFit fit(2, positions, values); + double minPos, minVal; + if (fit.findMinimum(position, 0, 100000, &minPos, &minVal)) + { + const int distanceToMin = static_cast(position - minPos); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: poly fit(%1): %2 = %3 @ %4 distToMin %5") + .arg(positions.size()).arg(minPos).arg(minVal).arg(position).arg(distanceToMin); + if (distanceToMin >= 0) + { + // The minimum is further inward. + numPolySolutionsFound = 0; + numRestartSolutionsFound = 0; + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: Solutions reset %1 = %2").arg(minPos).arg(minVal); + const int stepsToMin = distanceToMin / stepSize; + // Temporarily increase the step size if the minimum is very far inward. + if (stepsToMin >= 8) + thisStepSize = stepSize * 4; + else if (stepsToMin >= 4) + thisStepSize = stepSize * 2; + } + else + { + // We have potentially passed the bottom of the curve, + // but it's possible it is further back than the start of our sweep. + if (minPos > passStartPosition) + { + numRestartSolutionsFound++; + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: RESTART Solution #%1 %2 = %3 @ %4") + .arg(numRestartSolutionsFound).arg(minPos).arg(minVal).arg(position); + } + else + { + numPolySolutionsFound++; + numRestartSolutionsFound = 0; + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: Solution #%1: %2 = %3 @ %4") + .arg(numPolySolutionsFound).arg(minPos).arg(minVal).arg(position); + } + } + + if (numPolySolutionsFound >= kNumPolySolutionsRequired) + { + // We found a minimum. Setup the 2nd pass. We could use either the polynomial min or the + // min measured star as the target HFR. Will use the min of both as I've seen using just + // the polynomial minimum to be too conservative. + double minMeasurement = *std::min_element(values.begin(), values.end()); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: 1stPass solution @ %1: pos %2 val %3, min measurement %4") + .arg(position).arg(minPos).arg(minVal).arg(minMeasurement); + return setupSecondPass(static_cast(minPos), std::min(minVal, minMeasurement)); + } + else if (numRestartSolutionsFound >= kNumRestartSolutionsRequired) + { + params.startPosition = static_cast(minPos); + computeInitialPosition(); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: Restart @ %1: %1 = %2, start at %3") + .arg(position).arg(minPos).arg(minVal).arg(requestedPosition); + return requestedPosition; } } + else + { + // Minimum failed indicating the 2nd-order polynomial is an inverted U--it has a maximum, + // but no minimum. This is, of course, not a sensible solution for the focuser, but can + // happen with noisy data and perhaps small step sizes. We still might be able to take advantage, + // and notice whether the polynomial is increasing or decreasing locally. For now, do nothing. + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: ******** No poly min: Poly must be inverted"); + } } - if (minimumFound) + else { - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: Minimum found at index %1").arg(bestIndex); - return setupSecondPass(bestIndex); + // Don't have enough samples to reliably fit a polynomial. + // Simply step the focus in one more time and iterate. } } else { - // We previously found the v-curve minimum. We're now in a 2nd pass looking to recreate it. - if (value < minValue * (1.0 + params.focusTolerance)) + // In a 2nd pass looking to recreate the 1st pass' minimum. + if (value < firstPassBestValue * (1.0 + params.focusTolerance)) { focusSolution = position; - minimumReFound = true; + focusHFR = value; done = true; doneString = i18n("Solution found."); - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: solution at position %1 value %2 (best %3)").arg(position).arg(value).arg(minValue); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: 2ndPass solution @ %1 = %2 (best %3)") + .arg(position).arg(value).arg(firstPassBestValue); + debugLog(); return -1; } - else + else if (gettingWorse()) { - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: %1 %2 not a solution, not < %3").arg(position).arg(value).arg(minValue * (1.0+params.focusTolerance)); - if (gettingWorse()) - { - // Doesn't look like we'll find something close to the min. Retry the 2nd pass. - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: getting worse, re-running 2nd pass"); - return setupSecondPass(firstPassMinIndex); - } + // Doesn't look like we'll find something close to the min. Retry the 2nd pass. + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: getting worse, re-running 2nd pass"); + return setupSecondPass(firstPassBestPosition, firstPassBestValue); } } - if (numSteps == params.maxIterations - 1) + if (numSteps == params.maxIterations - 2) { // If we're close to exceeding the iteration limit, retry this pass near the old minimum position. const int minIndex = static_cast(std::min_element(values.begin(), values.end()) - values.begin()); - return setupSecondPass(minIndex, 0.5); + return setupSecondPass(positions[minIndex], values[minIndex], 0.5); } else if (numSteps > params.maxIterations) { // Fail. Exceeded our alloted number of iterations. - searchFailed = true; done = true; doneString = i18n("Too many steps."); - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: error %1").arg(doneString); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: error %1").arg(doneString); + debugLog(); return -1; } // Setup the next sample. - requestedPosition = requestedPosition - stepSize; + requestedPosition = requestedPosition - thisStepSize; // Make sure the next sample is within bounds. if (requestedPosition < minPositionLimit) { // The position is too low. Pick the min value and go to (or retry) a 2nd iteration. const int minIndex = static_cast(std::min_element(values.begin(), values.end()) - values.begin()); - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: reached end without Vmin. Restarting %1 pos %2 value %3") + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: reached end without Vmin. Restarting %1 pos %2 value %3") .arg(minIndex).arg(positions[minIndex]).arg(values[minIndex]); - return setupSecondPass(minIndex); + return setupSecondPass(positions[minIndex], values[minIndex]); } - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: requesting position %1").arg(requestedPosition); + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: requesting position %1").arg(requestedPosition); return requestedPosition; } -int LinearFocusAlgorithm::setupSecondPass(int minIndex, double margin) +void LinearFocusAlgorithm::debugLog() { - firstPassMinIndex = minIndex; - minimumFound = true; + QString str("Linear: points=["); + for (int i = 0; i < positions.size(); ++i) + { + str.append(QString("(%1, %2)").arg(positions[i]).arg(values[i])); + if (i < positions.size()-1) + str.append(", "); + } + str.append(QString("];iterations=%1").arg(numSteps)); + str.append(QString(";duration=%1").arg(stopWatch.elapsed()/1000)); + str.append(QString(";solution=%1").arg(focusSolution)); + str.append(QString(";HFR=%1").arg(focusHFR)); + str.append(QString(";filter='%1'").arg(params.filterName)); - int bestPosition = positions[minIndex]; - minValue = values[minIndex]; - // Arbitrarily go back "margin" steps above the best position. - // Could be a problem if backlash were worse than that many steps. - requestedPosition = std::min(static_cast(bestPosition + stepSize * margin), maxPositionLimit); - stepSize = params.initialStepSize / 2; - qCDebug(KSTARS_EKOS_FOCUS) << QString("LinearFocuser: 2ndPass starting at %1 step %2").arg(requestedPosition).arg(stepSize); - return requestedPosition; + qCDebug(KSTARS_EKOS_FOCUS) << str; } -// Given the values in the two vectors (values & positions), Evaluates whether index -// is the position of a "V". If a negative number is returned, the answer is no. -// A positive return value is a score. Best score wins. -// -// Implemented as simple heuristic. A V point must have: -// - at least 2 points on both sides that have higher values -// - for both sides: # higher values - # lower values > 2 -// Return value is the number of higher values minus the number of lower values -// counted on both sides. -// Note: could have just returned positive for only the minimum value, but this -// allows for others to be possible minima as well, if the min is too close to the edge. - -int LinearFocusAlgorithm::evaluateMinimum(int index, bool lastSample) +int LinearFocusAlgorithm::setupSecondPass(int position, double value, double margin) { - const double indexValue = values[index]; - const int threshold = 2; - - if (index < threshold) - return -1; - if (!lastSample && index >= values.size() - threshold) - return -1; + firstPassBestPosition = position; + firstPassBestValue = value; + inFirstPass = false; + secondPassStartIndex = values.size(); - // Left means lower focus position (focused in), right means higher position. - int right_num_higher = 0, right_num_lower = 0; - int left_num_higher = 0, left_num_lower = 0; - for (int i = 0; i < index; ++i) - { - if (values[i] >= indexValue) ++right_num_higher; - else ++right_num_lower; - } - for (int i = index+1; i < values.size(); ++i) - { - if (values[i] >= indexValue) ++left_num_higher; - else ++left_num_lower; - } - const int right_difference = right_num_higher - right_num_lower; - const int left_difference = left_num_higher - left_num_lower; - if (right_difference >= threshold && left_difference >= threshold) - return right_difference + left_difference; - return -1; + // Arbitrarily go back "margin" steps above the best position. + // Could be a problem if backlash were worse than that many steps. + requestedPosition = std::min(static_cast(firstPassBestPosition + stepSize * margin), maxPositionLimit); + stepSize = params.initialStepSize / 2; + qCDebug(KSTARS_EKOS_FOCUS) << QString("Linear: 2ndPass starting at %1 step %2").arg(requestedPosition).arg(stepSize); + return requestedPosition; } // Return true if there are "streak" consecutive values which are successively worse. bool LinearFocusAlgorithm::gettingWorse() { // Must have this many consecutive values getting worse. constexpr int streak = 3; const int length = values.size(); + if (secondPassStartIndex < 0) + return false; if (length < streak+1) return false; + // This insures that all the values we're checking are in the latest 2nd pass. + if (length - secondPassStartIndex < streak + 1) + return false; for (int i = length-1; i >= length-streak; --i) if (values[i] <= values[i-1]) return false; diff --git a/kstars/ekos/focus/polynomialfit.h b/kstars/ekos/focus/polynomialfit.h --- a/kstars/ekos/focus/polynomialfit.h +++ b/kstars/ekos/focus/polynomialfit.h @@ -21,6 +21,7 @@ // Constructor. Pass in the degree of the desired polynomial fit, and a vector with the x and y values. // The constructor solves for the polynomial coefficients. PolynomialFit(int degree, const QVector& x, const QVector& y); + PolynomialFit(int degree, const QVector& x, const QVector& y); // Returns the minimum position and value in the pointers for the solved polynomial. // Returns false if the polynomial couldn't be solved. diff --git a/kstars/ekos/focus/polynomialfit.cpp b/kstars/ekos/focus/polynomialfit.cpp --- a/kstars/ekos/focus/polynomialfit.cpp +++ b/kstars/ekos/focus/polynomialfit.cpp @@ -12,6 +12,18 @@ PolynomialFit::PolynomialFit(int degree_, const QVector& x_, const QVector& y_) : degree(degree_), x(x_), y(y_) { + Q_ASSERT(x_.size() == y_.size()); + solve(x, y); +} + +PolynomialFit::PolynomialFit(int degree_, const QVector& x_, const QVector& y_) + : degree(degree_), y(y_) +{ + Q_ASSERT(x_.size() == y_.size()); + for (int i = 0; i < x_.size(); ++i) + { + x.push_back(static_cast(x_[i])); + } solve(x, y); }