File indexing completed on 2024-05-05 15:55:06

0001 /*
0002     SPDX-FileCopyrightText: 2023 John Evans <john.e.evans.email@googlemail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "adaptivefocus.h"
0008 #include <kstars_debug.h>
0009 #include "kstars.h"
0010 #include "Options.h"
0011 
0012 namespace Ekos
0013 {
0014 
0015 AdaptiveFocus::AdaptiveFocus(Focus *_focus) : m_focus(_focus)
0016 {
0017     if (m_focus == nullptr)
0018         qCDebug(KSTARS_EKOS_FOCUS) << "AdaptiveFocus constructed with null focus ptr";
0019 }
0020 
0021 AdaptiveFocus::~AdaptiveFocus()
0022 {
0023 }
0024 
0025 // Start an Adaptive Focus run
0026 void AdaptiveFocus::runAdaptiveFocus(const int currentPosition, const QString &filter)
0027 {
0028     if (!m_focus)
0029     {
0030         qCDebug(KSTARS_EKOS_FOCUS) << "runAdaptiveFocus called but focus ptr is null. Ignoring.";
0031         adaptiveFocusAdmin(currentPosition, false, false);
0032         return;
0033     }
0034 
0035     if (!m_focus->m_FilterManager)
0036     {
0037         qCDebug(KSTARS_EKOS_FOCUS) << "runAdaptiveFocus called but focus filterManager is null. Ignoring.";
0038         adaptiveFocusAdmin(currentPosition, false, false);
0039         return;
0040     }
0041 
0042     if (!m_focus->m_OpsFocusSettings->focusAdaptive->isChecked())
0043     {
0044         qCDebug(KSTARS_EKOS_FOCUS) << "runAdaptiveFocus called but focusAdaptive->isChecked is false. Ignoring.";
0045         adaptiveFocusAdmin(currentPosition, false, false);
0046         return;
0047     }
0048 
0049     if (inAdaptiveFocus())
0050     {
0051         qCDebug(KSTARS_EKOS_FOCUS) << "runAdaptiveFocus called whilst already inAdaptiveFocus. Ignoring.";
0052         adaptiveFocusAdmin(currentPosition, false, false);
0053         return;
0054     }
0055 
0056     if (m_focus->inAutoFocus || m_focus->inFocusLoop || m_focus->inAdjustFocus || m_focus->inBuildOffsets)
0057     {
0058         qCDebug(KSTARS_EKOS_FOCUS) << "adaptiveFocus called whilst other focus activity in progress. Ignoring.";
0059         adaptiveFocusAdmin(currentPosition, false, false);
0060         return;
0061     }
0062 
0063     setInAdaptiveFocus(true);
0064     m_ThisAdaptiveFocusStartPos = currentPosition;
0065 
0066     // Get the reference data to be used by Adaptive Focus
0067     int refPosition;
0068     QString adaptiveFilter = getAdaptiveFilter(filter);
0069 
0070     // If the filter has been changed since the last Adaptive Focus iteration, reset
0071     if (filter != m_LastAdaptiveFilter)
0072     {
0073         resetAdaptiveFocusCounters();
0074         m_LastAdaptiveFilter = filter;
0075     }
0076     if (!m_focus->m_FilterManager->getFilterAbsoluteFocusDetails(adaptiveFilter, refPosition,
0077             m_focus->m_LastSourceAutofocusTemperature,
0078             m_focus->m_LastSourceAutofocusAlt))
0079     {
0080         qCDebug(KSTARS_EKOS_FOCUS) << "runAdaptiveFocus unable to get last Autofocus details. Ignoring.";
0081         adaptiveFocusAdmin(currentPosition, false, false);
0082         return;
0083     }
0084 
0085     // If we are using a lock filter then adjust the reference position by the offset
0086     refPosition += getAdaptiveFilterOffset(filter, adaptiveFilter);
0087 
0088     // Find out if there is anything to do for temperature, firstly do we have a valid temperature source
0089     double currentTemp = INVALID_VALUE;
0090     double tempTicksDelta = 0.0, tempTicksDeltaLast = 0.0;
0091     if (m_focus->currentTemperatureSourceElement && m_focus->m_LastSourceAutofocusTemperature != INVALID_VALUE)
0092     {
0093         if (m_LastAdaptiveFocusTemperature == INVALID_VALUE)
0094             // 1st Adaptive Focus run so no previous results
0095             m_LastAdaptiveFocusTemperature = m_focus->m_LastSourceAutofocusTemperature;
0096 
0097         currentTemp = m_focus->currentTemperatureSourceElement->value;
0098 
0099         // Calculate the deltas since the last Autofocus and Last Adaptive Focus
0100         const double tempDelta = currentTemp - m_focus->m_LastSourceAutofocusTemperature;
0101         const double tempDeltaLast = m_LastAdaptiveFocusTemperature - m_focus->m_LastSourceAutofocusTemperature;
0102 
0103         // Scale the temperature delta to number of ticks
0104         const double ticksPerTemp = m_focus->m_FilterManager->getFilterTicksPerTemp(adaptiveFilter);
0105         tempTicksDelta = ticksPerTemp * tempDelta;
0106         tempTicksDeltaLast = ticksPerTemp * tempDeltaLast;
0107     }
0108 
0109     // Now check for altitude
0110     double currentAlt = INVALID_VALUE;
0111     double altTicksDelta = 0.0, altTicksDeltaLast = 0.0;
0112     bool altDimension = false;
0113     if (m_focus->m_LastSourceAutofocusAlt != INVALID_VALUE)
0114     {
0115         if (m_LastAdaptiveFocusAlt == INVALID_VALUE)
0116             // 1st Adaptive Focus run so no previous results
0117             m_LastAdaptiveFocusAlt = m_focus->m_LastSourceAutofocusAlt;
0118 
0119         currentAlt = m_focus->mountAlt;
0120 
0121         const double altDelta = currentAlt - m_focus->m_LastSourceAutofocusAlt;
0122         const double altDeltaLast = m_LastAdaptiveFocusAlt - m_focus->m_LastSourceAutofocusAlt;
0123 
0124         // Scale the altitude delta to number of ticks
0125         const double ticksPerAlt = m_focus->m_FilterManager->getFilterTicksPerAlt(adaptiveFilter);
0126         altDimension = (abs(ticksPerAlt) > 0.001);
0127         altTicksDelta = ticksPerAlt * altDelta;
0128         altTicksDeltaLast = ticksPerAlt * altDeltaLast;
0129     }
0130 
0131     // proposedPosition is where focuser should move; proposedPositionLast is where focuser should have moved in last Adaptive iterative
0132     int proposedPosition = refPosition + static_cast<int>(round(tempTicksDelta + altTicksDelta));
0133     int proposedPositionLast = refPosition + static_cast<int>(round(tempTicksDeltaLast + altTicksDeltaLast));
0134     int proposedMove = proposedPosition - currentPosition;
0135 
0136     // We have the total movement, now work out the split by Temp, Alt and Pos Error
0137     m_ThisAdaptiveFocusTempTicks = tempTicksDelta - tempTicksDeltaLast;
0138     m_ThisAdaptiveFocusAltTicks = altTicksDelta - altTicksDeltaLast;
0139 
0140     // If everything is going to plan the currentPosition will equal proposedPositionLast. If the focuser hasn't moved exactly to the
0141     // requested position, e.g. its 1 or 2 ticks away then we need to account for this Positioning Error. It will have been reported
0142     // on the last Adaptive run but we now need to reverse that positioning error out in the accounting for this run
0143     m_LastAdaptiveFocusPosErrorReversal = proposedPositionLast - currentPosition;
0144 
0145     // There could be a rounding error, where, for example, a small change in temp/alt (e.g. 0.1 ticks) results in a 1 tick move
0146     m_ThisAdaptiveFocusRoundingError = proposedMove - m_LastAdaptiveFocusPosErrorReversal -
0147                                        static_cast<int>(round(m_ThisAdaptiveFocusTempTicks + m_ThisAdaptiveFocusAltTicks));
0148 
0149     // Check movement is above user defined minimum
0150     if (abs(proposedMove) < m_focus->m_OpsFocusSettings->focusAdaptiveMinMove->value())
0151     {
0152         m_focus->appendLogText(i18n("Adaptive Focus: No movement (below threshold)"));
0153         adaptiveFocusAdmin(currentPosition, true, false);
0154     }
0155     else
0156     {
0157         // Now do some checks that the movement is permitted
0158         if (abs(m_focus->initialFocuserAbsPosition - proposedPosition) > m_focus->m_OpsFocusMechanics->focusMaxTravel->value())
0159         {
0160             // We are about to move the focuser beyond focus max travel so don't
0161             // Suspend adaptive focusing, user can always re-enable, if required
0162             m_focus->m_OpsFocusSettings->focusAdaptive->setChecked(false);
0163             m_focus->appendLogText(i18n("Adaptive Focus suspended. Total movement would exceed Max Travel limit"));
0164             adaptiveFocusAdmin(currentPosition, false, false);
0165         }
0166         else if (abs(m_AdaptiveTotalMove + proposedMove) > m_focus->m_OpsFocusSettings->focusAdaptiveMaxMove->value())
0167         {
0168             // We are about to move the focuser beyond adaptive focus max move so don't
0169             // Suspend adaptive focusing. User can always re-enable, if required
0170             m_focus->m_OpsFocusSettings->focusAdaptive->setChecked(false);
0171             m_focus->appendLogText(i18n("Adaptive Focus suspended. Total movement would exceed adaptive limit"));
0172             adaptiveFocusAdmin(currentPosition, false, false);
0173         }
0174         else
0175         {
0176             // For most folks, most of the time there won't be alt data or positioning errors, so don't continually report 0
0177             QString tempStr = QString("%1").arg(m_ThisAdaptiveFocusTempTicks, 0, 'f', 1);
0178             QString altStr = QString("%1").arg(m_ThisAdaptiveFocusAltTicks, 0, 'f', 1);
0179             QString text = i18n("Adaptive Focus: Moving from %1 to %2 (TempΔ %3", currentPosition, proposedPosition, tempStr);
0180             text = (!altDimension) ? text + i18n("") : text + i18n("; AltΔ %1", altStr);
0181             text = (m_LastAdaptiveFocusPosErrorReversal == 0) ? text + i18n(")") : text + i18n("; Pos Error %1)",
0182                     m_LastAdaptiveFocusPosErrorReversal);
0183             m_focus->appendLogText(text);
0184 
0185             // Go ahead and try to move the focuser. Admin tasks will be completed when the focuser move completes
0186             if (m_focus->changeFocus(proposedMove))
0187             {
0188                 // All good so update variables used after focuser move completes (see adaptiveFocusAdmin())
0189                 m_ThisAdaptiveFocusTemperature = currentTemp;
0190                 m_ThisAdaptiveFocusAlt = currentAlt;
0191                 m_AdaptiveFocusPositionReq = proposedPosition;
0192                 m_AdaptiveTotalMove += proposedMove;
0193             }
0194             else
0195             {
0196                 // Problem moving the focuser
0197                 m_focus->appendLogText(i18n("Adaptive Focus unable to move focuser"));
0198                 adaptiveFocusAdmin(currentPosition, false, false);
0199             }
0200         }
0201     }
0202 }
0203 
0204 // When adaptiveFocus succeeds the focuser is moved and admin tasks are performed to inform other modules that adaptiveFocus is complete
0205 void AdaptiveFocus::adaptiveFocusAdmin(const int currentPosition, const bool success, const bool focuserMoved)
0206 {
0207     // Reset member variable
0208     setInAdaptiveFocus(false);
0209 
0210     int thisPosError = 0;
0211     if (focuserMoved)
0212     {
0213         // Signal Capture that we are done - honour the focuser settle time after movement.
0214         QTimer::singleShot(m_focus->m_OpsFocusMechanics->focusSettleTime->value() * 1000, m_focus, [ &, success]()
0215         {
0216             emit m_focus->focusAdaptiveComplete(success);
0217         });
0218 
0219         // Check whether the focuser moved to the requested position or whether we have a positioning error (1 or 2 ticks for example)
0220         // If there is a positioning error update the total ticks to include the error.
0221         if (m_AdaptiveFocusPositionReq != INVALID_VALUE)
0222         {
0223             thisPosError = currentPosition - m_AdaptiveFocusPositionReq;
0224             m_AdaptiveTotalMove += thisPosError;
0225         }
0226     }
0227     else
0228         emit m_focus->focusAdaptiveComplete(success);
0229 
0230     // Signal Analyze if success both for focuser moves and zero moves
0231     bool check = true;
0232     if (success)
0233     {
0234         // Note we send Analyze the active filter, not the Adaptive Filter (i.e. not the lock filter)
0235         int totalTicks = static_cast<int>(round(m_ThisAdaptiveFocusTempTicks + m_ThisAdaptiveFocusAltTicks)) +
0236                          m_LastAdaptiveFocusPosErrorReversal + m_ThisAdaptiveFocusRoundingError + thisPosError;
0237 
0238         emit m_focus->adaptiveFocusComplete(m_focus->filter(), m_ThisAdaptiveFocusTemperature, m_ThisAdaptiveFocusTempTicks,
0239                                             m_ThisAdaptiveFocusAlt, m_ThisAdaptiveFocusAltTicks, m_LastAdaptiveFocusPosErrorReversal,
0240                                             thisPosError, totalTicks, currentPosition, focuserMoved);
0241 
0242         // Check that totalTicks movement is above minimum
0243         if (totalTicks < m_focus->m_OpsFocusSettings->focusAdaptiveMinMove->value())
0244             totalTicks = 0;
0245         // Perform an accounting check that the numbers add up
0246         check = (m_ThisAdaptiveFocusStartPos + totalTicks == currentPosition);
0247     }
0248 
0249     if (success && focuserMoved)
0250     {
0251         // Reset member variables in prep for the next Adaptive Focus run
0252         m_LastAdaptiveFocusTemperature = m_ThisAdaptiveFocusTemperature;
0253         m_LastAdaptiveFocusAlt = m_ThisAdaptiveFocusAlt;
0254     }
0255 
0256     // Log a debug for each Adaptive Focus.
0257     qCDebug(KSTARS_EKOS_FOCUS) << "Adaptive Focus Stats: Filter:" << m_LastAdaptiveFilter
0258                                << ", Adaptive Filter:" << getAdaptiveFilter(m_LastAdaptiveFilter)
0259                                << ", Min Move:" << m_focus->m_OpsFocusSettings->focusAdaptiveMinMove->value()
0260                                << ", success:" << (success ? "Yes" : "No")
0261                                << ", focuserMoved:" << (focuserMoved ? "Yes" : "No")
0262                                << ", Temp:" << m_ThisAdaptiveFocusTemperature
0263                                << ", Temp ticks:" << m_ThisAdaptiveFocusTempTicks
0264                                << ", Alt:" << m_ThisAdaptiveFocusAlt
0265                                << ", Alt ticks:" << m_ThisAdaptiveFocusAltTicks
0266                                << ", Last Pos Error reversal:" << m_LastAdaptiveFocusPosErrorReversal
0267                                << ", Rounding Error:" << m_ThisAdaptiveFocusRoundingError
0268                                << ", New Pos Error:" << thisPosError
0269                                << ", Starting Pos:" << m_ThisAdaptiveFocusStartPos
0270                                << ", Current Position:" << currentPosition
0271                                << ", Accounting Check:" << (check ? "Passed" : "Failed");
0272 }
0273 
0274 // Get the filter to use for Adaptive Focus
0275 QString AdaptiveFocus::getAdaptiveFilter(const QString filter)
0276 {
0277     // If the active filter has a lock filter use that
0278     QString adaptiveFilter = filter;
0279     QString lockFilter = m_focus->m_FilterManager->getFilterLock(filter);
0280     if (lockFilter != NULL_FILTER)
0281         adaptiveFilter = lockFilter;
0282 
0283     return adaptiveFilter;
0284 }
0285 
0286 // Calc the filter offset between the active filter and the adaptive filter (either active filter or lock filter)
0287 int AdaptiveFocus::getAdaptiveFilterOffset(const QString &activeFilter, const QString &adaptiveFilter)
0288 {
0289     int offset = 0;
0290     if (activeFilter != adaptiveFilter)
0291     {
0292         int activeOffset = m_focus->m_FilterManager->getFilterOffset(activeFilter);
0293         int adaptiveOffset = m_focus->m_FilterManager->getFilterOffset(adaptiveFilter);
0294         if (activeOffset != INVALID_VALUE && adaptiveOffset != INVALID_VALUE)
0295             offset = activeOffset - adaptiveOffset;
0296         else
0297             qCDebug(KSTARS_EKOS_FOCUS) << "getAdaptiveFilterOffset unable to calculate filter offsets";
0298     }
0299     return offset;
0300 }
0301 
0302 // Reset the variables used by Adaptive Focus
0303 void AdaptiveFocus::resetAdaptiveFocusCounters()
0304 {
0305     m_LastAdaptiveFilter = NULL_FILTER;
0306     m_LastAdaptiveFocusTemperature = INVALID_VALUE;
0307     m_LastAdaptiveFocusAlt = INVALID_VALUE;
0308     m_AdaptiveTotalMove = 0;
0309 }
0310 
0311 // Function to set inAdaptiveFocus
0312 void AdaptiveFocus::setInAdaptiveFocus(bool value)
0313 {
0314     m_inAdaptiveFocus = value;
0315 }
0316 
0317 // Change the start position of an autofocus run based Adaptive Focus settings
0318 // The start position uses the last successful AF run for the active filter and adapts that position
0319 // based on the temperature and altitude delta between now and when the last successful AF run happened
0320 int AdaptiveFocus::adaptStartPosition(int currentPosition, QString &AFfilter)
0321 {
0322     // If the active filter has no lock then the AF run will happen on this filter so get the start point
0323     // Otherwise get the lock filter on which the AF run will happen and get the start point of this filter
0324     // An exception is when the BuildOffsets utility is being used as this ignores the lock filter
0325     if (!m_focus->m_FilterManager)
0326         return currentPosition;
0327 
0328     QString filterText;
0329     QString lockFilter = m_focus->m_FilterManager->getFilterLock(AFfilter);
0330     if (m_focus->inBuildOffsets || lockFilter == NULL_FILTER || lockFilter == AFfilter)
0331         filterText = AFfilter;
0332     else
0333     {
0334         filterText = AFfilter + " locked to " + lockFilter;
0335         AFfilter = lockFilter;
0336     }
0337 
0338     if (!m_focus->m_OpsFocusSettings->focusAdaptStart->isChecked())
0339         // Adapt start option disabled
0340         return currentPosition;
0341 
0342     if (m_focus->m_FocusAlgorithm != Focus::FOCUS_LINEAR1PASS)
0343         // Only enabled for LINEAR 1 PASS
0344         return currentPosition;
0345 
0346     // Start with the last AF run result for the active filter
0347     int lastPos;
0348     double lastTemp, lastAlt;
0349     if(!m_focus->m_FilterManager->getFilterAbsoluteFocusDetails(AFfilter, lastPos, lastTemp, lastAlt))
0350         // Unable to get the last AF run information for the filter so just use the currentPosition
0351         return currentPosition;
0352 
0353     // Only proceed if we have a sensible lastPos
0354     if (lastPos <= 0)
0355         return currentPosition;
0356 
0357     // Do some checks on the lastPos
0358     int minTravelLimit = qMax(0.0, currentPosition - m_focus->m_OpsFocusMechanics->focusMaxTravel->value());
0359     int maxTravelLimit = qMin(m_focus->absMotionMax, currentPosition + m_focus->m_OpsFocusMechanics->focusMaxTravel->value());
0360     if (lastPos < minTravelLimit || lastPos > maxTravelLimit)
0361     {
0362         // Looks like there is a potentially dodgy lastPos so just use currentPosition
0363         m_focus->appendLogText(i18n("Adaptive start point, last AF solution outside Max Travel, ignoring"));
0364         return currentPosition;
0365     }
0366 
0367     // Adjust temperature
0368     double ticksTemp = 0.0;
0369     double tempDelta = 0.0;
0370     if (!m_focus->currentTemperatureSourceElement)
0371         m_focus->appendLogText(i18n("Adaptive start point, no temperature source available"));
0372     else if (lastTemp == INVALID_VALUE)
0373         m_focus->appendLogText(i18n("Adaptive start point, no temperature for last AF solution"));
0374     else
0375     {
0376         double currentTemp = m_focus->currentTemperatureSourceElement->value;
0377         tempDelta = currentTemp - lastTemp;
0378         if (abs(tempDelta) > 30)
0379             // Sanity check on the temperature delta
0380             m_focus->appendLogText(i18n("Adaptive start point, very large temperature delta, ignoring"));
0381         else
0382             ticksTemp = tempDelta * m_focus->m_FilterManager->getFilterTicksPerTemp(AFfilter);
0383     }
0384 
0385     // Adjust altitude
0386     double ticksAlt = 0.0;
0387     double currentAlt = m_focus->mountAlt;
0388     double altDelta = currentAlt - lastAlt;
0389 
0390     // Sanity check on the altitude delta
0391     if (lastAlt == INVALID_VALUE)
0392         m_focus->appendLogText(i18n("Adaptive start point, no alt recorded for last AF solution"));
0393     else if (abs(altDelta) > 90.0)
0394         m_focus->appendLogText(i18n("Adaptive start point, very large altitude delta, ignoring"));
0395     else
0396         ticksAlt = altDelta * m_focus->m_FilterManager->getFilterTicksPerAlt(AFfilter);
0397 
0398     // We have all the elements to adjust the AF start position so final checks before the move
0399     const int ticksTotal = static_cast<int> (round(ticksTemp + ticksAlt));
0400     int targetPos = lastPos + ticksTotal;
0401     if (targetPos < minTravelLimit || targetPos > maxTravelLimit)
0402     {
0403         // targetPos is outside Max Travel
0404         m_focus->appendLogText(i18n("Adaptive start point, target position is outside Max Travel, ignoring"));
0405         return currentPosition;
0406     }
0407 
0408     if (abs(targetPos - currentPosition) > m_focus->m_OpsFocusSettings->focusAdaptiveMaxMove->value())
0409     {
0410         // Disallow excessive movement.
0411         // No need to check minimum movement
0412         m_focus->appendLogText(i18n("Adaptive start point [%1] excessive move disallowed", filterText));
0413         qCDebug(KSTARS_EKOS_FOCUS) << "Adaptive start point: " << filterText
0414                                    << " startPosition: " << currentPosition
0415                                    << " Last filter position: " << lastPos
0416                                    << " Temp delta: " << tempDelta << " Temp ticks: " << ticksTemp
0417                                    << " Alt delta: " << altDelta << " Alt ticks: " << ticksAlt
0418                                    << " Target position: " << targetPos
0419                                    << " Exceeds max allowed move: " << m_focus->m_OpsFocusSettings->focusAdaptiveMaxMove->value()
0420                                    << " Using startPosition.";
0421         return currentPosition;
0422     }
0423     else
0424     {
0425         // All good so report the move
0426         m_focus->appendLogText(i18n("Adapting start point [%1] from %2 to %3", filterText, currentPosition, targetPos));
0427         qCDebug(KSTARS_EKOS_FOCUS) << "Adaptive start point: " << filterText
0428                                    << " startPosition: " << currentPosition
0429                                    << " Last filter position: " << lastPos
0430                                    << " Temp delta: " << tempDelta << " Temp ticks: " << ticksTemp
0431                                    << " Alt delta: " << altDelta << " Alt ticks: " << ticksAlt
0432                                    << " Target position: " << targetPos;
0433         return targetPos;
0434     }
0435 }
0436 
0437 }