[education/kstars] /: Dark guiding for the GPG guider

Jasem Mutlaq null at kde.org
Fri Sep 16 08:52:39 BST 2022


Git commit b5b447baa4b2d2b53e999fcf6dac07a03e3a66ed by Jasem Mutlaq, on behalf of Sophie Taylor.
Committed on 16/09/2022 at 07:52.
Pushed by mutlaqja into branch 'master'.

Dark guiding for the GPG guider

One of the primary benefits of the GPG guiding system is the ability to predict the behaviour of the mount at any point in time; not just when guide camera images are received. This PR is intended to introduce a rapid control loop that occurs several times per guide exposure, to correct for the predicted error. This way, not only can the benefits of long duration guide exposures be taken advantage of (e.g. capturing fainter guide stars), but some of the benefit of short-term exposures too; namely, quicker corrections to the mount.

TODO:

- [x] Synchronise dark guiding options with the code as it changes, rather than just at Ekos loading
- [x] Remove `GuideStatus::DARKGUIDING` maybe?
- [x] Log predictive corrections
- [x] Correct for the guide delay patch
- [x] Document

FUTURE IDEAS:
- Adaptive exposure times. Set a minimum and maximum guide exposure, and adapt it in two ways:
      1. Start off with the minimum exposure rate, to get fine-grained samples of the periodic error; after number of periods for inference have passed, slowly increase it to the maximum exposure time. This way, we get a good initial model for the periodic error, while automatically transitioning to the longer duration exposure.
      2. When unexpected errors arise past some threshold (e.g. a long gust of wind), shorten the exposure time, to more quickly deal with the unmodeled errors. Once the guide error falls below the threshold again (with some hysteresis), start increasing the duration again. Note that the corrections with the shorter exposures will be associated with a lower SNR, and thus will not contribute too much to the adaptive model (besides, the short-range kernel will probably account for them anyway.)
- Model DEC error with GPG; output corrections for it when dark guiding as well. Pass the GPG predictions through a hysteresis-aware control algorithm to deal with backlash.
- Add further GPG kernels. E.g., a kernel at sidereal (or King) rate to estimate and neutralise polar alignment errors.

M  +11   -4    doc/ekos-guide.docbook
M  +24   -15   kstars/ekos/guide/guide.cpp
M  +4    -3    kstars/ekos/guide/guide.h
M  +3    -0    kstars/ekos/guide/guideinterface.h
M  +28   -1    kstars/ekos/guide/internalguide/MPI_IS_gaussian_process/src/gaussian_process_guider.h
M  +112  -69   kstars/ekos/guide/internalguide/gmath.cpp
M  +9    -1    kstars/ekos/guide/internalguide/gmath.h
M  +64   -13   kstars/ekos/guide/internalguide/gpg.cpp
M  +12   -2    kstars/ekos/guide/internalguide/gpg.h
M  +207  -55   kstars/ekos/guide/internalguide/internalguider.cpp
M  +23   -6    kstars/ekos/guide/internalguide/internalguider.h
M  +119  -56   kstars/ekos/guide/opsgpg.ui
M  +2    -1    kstars/indi/indicommon.h
M  +2    -1    kstars/indi/indifocuser.cpp
M  +6    -0    kstars/kstars.kcfg

https://invent.kde.org/education/kstars/commit/b5b447baa4b2d2b53e999fcf6dac07a03e3a66ed

diff --git a/doc/ekos-guide.docbook b/doc/ekos-guide.docbook
index 882a4bfc4..667368c4b 100644
--- a/doc/ekos-guide.docbook
+++ b/doc/ekos-guide.docbook
@@ -238,6 +238,10 @@
                 </para>
             </listitem>
         </itemizedlist>
+    </sect3>
+
+    <sect3 id="guide-direction-control">
+        <title>Guiding Direction Control</title>
         <screenshot>
             <screeninfo>
                 Guiding Direction Control
@@ -251,10 +255,6 @@
                 </textobject>
             </mediaobject>
         </screenshot>
-    </sect3>
-
-    <sect3 id="guide-direction-control">
-        <title>Guiding Direction Control</title>
         <para>
             You can fine-tune the guiding performance in the Control Section. The autoguide process works like a <ulink url="https://en.wikipedia.org/wiki/PID_controller">PID controller</ulink> when sending correction commands to the mount. You can alter the Proportional and Integral gains to improve the guiding performance if necessary. By default, guiding corrective pulses are sent to both mount axis in all directions: positive and negative. You can fine-tune control by selecting which axis shall receive corrective guiding pulses and within each axis, you can indicate which direction <guilabel>(Positive) +</guilabel> or <guilabel>Negative (-)</guilabel> receives the guiding pulses. For example, for the Declination axis, the <guilabel>+</guilabel> direction is North and <guilabel>-</guilabel> is South.
         </para>
@@ -303,6 +303,13 @@
         </para>
     </sect3>
 
+    <sect3 id="guide-gpg">
+        <title>Guiding with GPG</title>
+        <para>
+            The internal guider can use predictive and adaptive guiding by enabling GPG guiding. This adaptively models the periodic error of the mount, and adds its predicted contribution to each guide pulse. Optionally, by enabling Dark Guiding, it can output the predicted corrections much faster than the guide camera exposure rate, effectively performing periodic error correction and allowing longer guide camera exposures.
+        </para>
+    </sect3>
+
     <sect3 id="guide-phd2-support">
         <title>PHD2 Support</title>
         <para>
diff --git a/kstars/ekos/guide/guide.cpp b/kstars/ekos/guide/guide.cpp
index 0c40830da..6de91e4c0 100644
--- a/kstars/ekos/guide/guide.cpp
+++ b/kstars/ekos/guide/guide.cpp
@@ -1097,32 +1097,37 @@ void Guide::setDECSwap(bool enable)
     }
 }
 
-bool Guide::sendMultiPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection dec_dir, int dec_msecs)
+bool Guide::sendMultiPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection dec_dir, int dec_msecs, CaptureAfterPulses followWithCapture)
 {
     if (m_Guider == nullptr || (ra_dir == NO_DIR && dec_dir == NO_DIR))
         return false;
 
-    // Delay next capture by user-configurable delay.
-    // If user delay is zero, delay by the pulse length plus 100 milliseconds before next capture.
-    auto ms = std::max(ra_msecs, dec_msecs) + 100;
-    auto delay = std::max(static_cast<int>(Options::guideDelay() * 1000), ms);
-
-    m_PulseTimer.start(delay);
+    if (followWithCapture == StartCaptureAfterPulses)
+    {
+        // Delay next capture by user-configurable delay.
+        // If user delay is zero, delay by the pulse length plus 100 milliseconds before next capture.
+        auto ms = std::max(ra_msecs, dec_msecs) + 100;
+        auto delay = std::max(static_cast<int>(Options::guideDelay() * 1000), ms);
 
+        m_PulseTimer.start(delay);
+    }
     return m_Guider->doPulse(ra_dir, ra_msecs, dec_dir, dec_msecs);
 }
 
-bool Guide::sendSinglePulse(GuideDirection dir, int msecs)
+bool Guide::sendSinglePulse(GuideDirection dir, int msecs, CaptureAfterPulses followWithCapture)
 {
     if (m_Guider == nullptr || dir == NO_DIR)
         return false;
 
-    // Delay next capture by user-configurable delay.
-    // If user delay is zero, delay by the pulse length plus 100 milliseconds before next capture.
-    auto ms = msecs + 100;
-    auto delay = std::max(static_cast<int>(Options::guideDelay() * 1000), ms);
+    if (followWithCapture == StartCaptureAfterPulses)
+    {
+        // Delay next capture by user-configurable delay.
+        // If user delay is zero, delay by the pulse length plus 100 milliseconds before next capture.
+        auto ms = msecs + 100;
+        auto delay = std::max(static_cast<int>(Options::guideDelay() * 1000), ms);
 
-    m_PulseTimer.start(delay);
+        m_PulseTimer.start(delay);
+    }
 
     return m_Guider->doPulse(dir, msecs);
 }
@@ -1645,7 +1650,12 @@ void Guide::setDarkFrameEnabled(bool enable)
 void Guide::saveDefaultGuideExposure()
 {
     if(guiderType == GUIDE_PHD2)
+
         phd2Guider->requestSetExposureTime(guideExposure->value() * 1000);
+    else if (guiderType == GUIDE_INTERNAL)
+    {
+        internalGuider->setExposureTime();
+    }
 }
 
 void Guide::setStarPosition(const QVector3D &newCenter, bool updateNow)
@@ -2473,7 +2483,7 @@ void Guide::nonGuidedDither()
                                "de_polarity:" << decPolarity;
 
     bool rc = sendMultiPulse(raPolarity > 0 ? RA_INC_DIR : RA_DEC_DIR, raMsec, decPolarity > 0 ? DEC_INC_DIR : DEC_DEC_DIR,
-                             decMsec);
+                             decMsec, StartCaptureAfterPulses);
 
     if (rc)
     {
@@ -2840,7 +2850,6 @@ void Guide::removeDevice(const QSharedPointer<ISD::GenericDevice> &device)
         m_AO->disconnect(this);
         m_AO = nullptr;
     }
-
 }
 
 void Guide::loop()
diff --git a/kstars/ekos/guide/guide.h b/kstars/ekos/guide/guide.h
index d819c3d98..0b11a126c 100644
--- a/kstars/ekos/guide/guide.h
+++ b/kstars/ekos/guide/guide.h
@@ -340,9 +340,10 @@ class Guide : public QWidget, public Ui::Guide
         // Capture
         void setCaptureComplete();
 
-        // Send pulse to ST4 driver
-        bool sendMultiPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection dec_dir, int dec_msecs);
-        bool sendSinglePulse(GuideDirection dir, int msecs);
+        // Pulse both RA and DEC axes
+        bool sendMultiPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection dec_dir, int dec_msecs, CaptureAfterPulses followWithCapture);
+        // Pulse for one of the mount axes
+        bool sendSinglePulse(GuideDirection dir, int msecs, CaptureAfterPulses followWithCapture);
 
         /**
              * @brief setDECSwap Change ST4 declination pulse direction. +DEC pulses increase DEC if swap is OFF. When on +DEC pulses result in decreasing DEC.
diff --git a/kstars/ekos/guide/guideinterface.h b/kstars/ekos/guide/guideinterface.h
index 1c5416d09..8de648cf8 100644
--- a/kstars/ekos/guide/guideinterface.h
+++ b/kstars/ekos/guide/guideinterface.h
@@ -105,4 +105,7 @@ class GuideInterface : public QObject
         dms mountRA, mountDEC, mountAzimuth, mountAltitude;
         ISD::Mount::PierSide pierSide { ISD::Mount::PIER_UNKNOWN };
 };
+
+enum CaptureAfterPulses {StartCaptureAfterPulses, DontCaptureAfterPulses};
+
 }
diff --git a/kstars/ekos/guide/internalguide/MPI_IS_gaussian_process/src/gaussian_process_guider.h b/kstars/ekos/guide/internalguide/MPI_IS_gaussian_process/src/gaussian_process_guider.h
index 100b617df..8f740ecfb 100644
--- a/kstars/ekos/guide/internalguide/MPI_IS_gaussian_process/src/gaussian_process_guider.h
+++ b/kstars/ekos/guide/internalguide/MPI_IS_gaussian_process/src/gaussian_process_guider.h
@@ -22,7 +22,7 @@
 #include "gaussian_process.h"
 #include "covariance_functions.h"
 #include "math_tools.h"
-
+#include "ekos_guide_debug.h"
 #include <chrono>
 
 enum Hyperparameters
@@ -247,6 +247,33 @@ class GaussianProcessGuider
         double GetPredictionGain() const;
         bool SetPredictionGain(double);
 
+        /**
+         * Returns the weight of the prediction on the output control value
+         */
+        double getPredictionContribution()
+        {
+            auto const period_length = GetGPHyperparameters()[PKPeriodLength];
+            if (get_number_of_measurements() <= 10)
+            {
+                qCDebug(KSTARS_EKOS_GUIDE) << "Less than 10 measurements!";
+                return 0.0;
+            }
+
+            auto current_time = std::chrono::system_clock::now();
+            double delta_measurement_time = std::chrono::duration<double>(current_time - last_time_).count();
+
+            if (parameters.min_periods_for_inference_ * period_length == 0 || delta_measurement_time == 0)
+            {
+                return 0;
+            }
+
+            auto time = std::chrono::duration<double>(current_time - start_time_).count()
+                    - (delta_measurement_time / 2.0) // use the midpoint as time stamp
+                    + dither_offset_; // correct for the gear time offset from dithering
+            return time / (parameters.min_periods_for_inference_ * period_length);
+        }
+
+
         GaussianProcessGuider(guide_parameters parameters);
         ~GaussianProcessGuider();
 
diff --git a/kstars/ekos/guide/internalguide/gmath.cpp b/kstars/ekos/guide/internalguide/gmath.cpp
index 5669f225a..54ea813d8 100644
--- a/kstars/ekos/guide/internalguide/gmath.cpp
+++ b/kstars/ekos/guide/internalguide/gmath.cpp
@@ -265,10 +265,8 @@ const QString directionStr(GuideDirection dir)
 }
 }  // namespace
 
-void cgmath::calculatePulses(Ekos::GuideState state)
+bool cgmath::configureInParams(Ekos::GuideState state)
 {
-    qCDebug(KSTARS_EKOS_GUIDE) << "Processing Axes";
-
     const bool dithering = state == Ekos::GuideState::GUIDE_DITHERING;
 
     if (!dithering)
@@ -279,7 +277,7 @@ void cgmath::calculatePulses(Ekos::GuideState state)
         in_params.integral_gain[0] = Options::rAIntegralGain();
         in_params.integral_gain[1] = Options::dECIntegralGain();
 
-        // Always pulse if were dithering.
+        // Always pulse if we're dithering.
         in_params.enabled[0] = Options::rAGuideEnabled();
         in_params.enabled[1] = Options::dECGuideEnabled();
 
@@ -320,23 +318,63 @@ void cgmath::calculatePulses(Ekos::GuideState state)
         in_params.enabled_axis2[1] = true;
     }
 
+    return dithering;
+}
 
-    // process axes...
-    for (int k = GUIDE_RA; k <= GUIDE_DEC; k++)
+void cgmath::updateOutParams(int k, const double arcsecDrift, int pulseLength, GuideDirection pulseDirection)
+{
+    out_params.pulse_dir[k]  = pulseDirection;
+    out_params.pulse_length[k] = pulseLength;
+    out_params.delta[k] = arcsecDrift;
+}
+
+void cgmath::outputGuideLog()
+{
+    if (Options::guideLogging())
     {
-        // zero all out commands
-        GuideDirection pulseDirection = NO_DIR;
-        int pulseLength = 0;  // milliseconds
-        GuideDirection dir;
+        QTextStream out(&logFile);
+        out << iterationCounter << "," << logTime.elapsed() << "," << out_params.delta[0] << "," << out_params.pulse_length[0] <<
+            ","
+            << directionStr(out_params.pulse_dir[0]) << "," << out_params.delta[1] << ","
+            << out_params.pulse_length[1] << "," << directionStr(out_params.pulse_dir[1]) << Qt::endl;
+    }
+}
 
-        // Get the drift for this axis
-        const int idx = driftUpto[k];
-        const double arcsecDrift = drift[k][idx];
+void cgmath::processAxis(const int k, const bool dithering, const bool darkGuide, const Seconds &timeStep)
+{
+    // zero all out commands
+    GuideDirection pulseDirection = NO_DIR;
+    int pulseLength = 0;  // milliseconds
+    GuideDirection dir;
 
-        const double pulseConverter = (k == GUIDE_RA) ?
-                                      calibration.raPulseMillisecondsPerArcsecond() :
-                                      calibration.decPulseMillisecondsPerArcsecond();
-        const double maxPulseMilliseconds = in_params.max_pulse_arcsec[k] * pulseConverter;
+    // Get the drift for this axis
+    const int idx = driftUpto[k];
+    const double arcsecDrift = drift[k][idx];
+
+    const double pulseConverter = (k == GUIDE_RA) ?
+                                  calibration.raPulseMillisecondsPerArcsecond() :
+                                  calibration.decPulseMillisecondsPerArcsecond();
+    const double maxPulseMilliseconds = in_params.max_pulse_arcsec[k] * pulseConverter;
+
+    // GPG pulse computation
+    bool useGPG = !dithering && Options::gPGEnabled() && (k == GUIDE_RA) && in_params.enabled[k];
+    if (useGPG && darkGuide)
+    {
+        qCDebug(KSTARS_EKOS_GUIDE) << "dark guiding";
+        gpg->darkGuiding(&pulseLength, &dir, calibration, timeStep);
+        pulseDirection = dir;
+    }
+    else if (useGPG && gpg->computePulse(arcsecDrift,
+                                    usingSEPMultiStar() ? &guideStars : nullptr, &pulseLength, &dir, calibration, timeStep))
+    {
+        pulseDirection = dir;
+        pulseLength = std::min(pulseLength, static_cast<int>(maxPulseMilliseconds + 0.5));
+    }
+    else
+    {
+        // This is the main non-GPG guide-pulse computation.
+        // Traditionally it was hardwired so that proportional_gain=133 was about a control gain of 1.0
+        // This is now in the 0.0 - 1.0 range, and multiplies the calibrated mount performance.
 
         // Compute the average drift in the recent past for the integral control term.
         drift_integral[k] = 0;
@@ -347,73 +385,64 @@ void cgmath::calculatePulses(Ekos::GuideState state)
         qCDebug(KSTARS_EKOS_GUIDE) << "drift[" << axisStr(k) << "] = " << arcsecDrift
                                    << " integral[" << axisStr(k) << "] = " << drift_integral[k];
 
-        // GPG pulse computation
-        bool useGPG = !dithering && Options::gPGEnabled() && (k == GUIDE_RA) && in_params.enabled[k];
-        if (useGPG && gpg->computePulse(arcsecDrift,
-                                        usingSEPMultiStar() ? &guideStars : nullptr, &pulseLength, &dir, calibration))
+        const double arcsecPerMsPulse = k == GUIDE_RA ? calibration.raPulseMillisecondsPerArcsecond() :
+                                        calibration.decPulseMillisecondsPerArcsecond();
+        const double proportionalResponse = arcsecDrift * in_params.proportional_gain[k] * arcsecPerMsPulse;
+        const double integralResponse = drift_integral[k] * in_params.integral_gain[k] * arcsecPerMsPulse;
+        pulseLength = std::min(fabs(proportionalResponse + integralResponse), maxPulseMilliseconds);
+
+        // calc direction
+        // We do not send pulse if direction is disabled completely, or if direction in a specific axis (e.g. N or S) is disabled
+        if (!in_params.enabled[k] || // This axis not enabled
+                // Positive direction of this axis not enabled.
+                (arcsecDrift > 0 && !in_params.enabled_axis1[k]) ||
+                // Negative direction of this axis not enabled.
+                (arcsecDrift < 0 && !in_params.enabled_axis2[k]))
         {
-            pulseDirection = dir;
-            pulseLength = std::min(pulseLength, static_cast<int>(maxPulseMilliseconds + 0.5));
+            pulseDirection = NO_DIR;
+            pulseLength = 0;
         }
         else
         {
-            // This is the main non-GPG guide-pulse computation.
-            // Traditionally it was hardwired so that proportional_gain=133 was about a control gain of 1.0
-            // This is now in the 0.0 - 1.0 range, and multiplies the calibrated mount performance.
-
-            const double arcsecPerMsPulse = k == GUIDE_RA ? calibration.raPulseMillisecondsPerArcsecond() :
-                                            calibration.decPulseMillisecondsPerArcsecond();
-            const double proportionalResponse = arcsecDrift * in_params.proportional_gain[k] * arcsecPerMsPulse;
-            const double integralResponse = drift_integral[k] * in_params.integral_gain[k] * arcsecPerMsPulse;
-            pulseLength = std::min(fabs(proportionalResponse + integralResponse), maxPulseMilliseconds);
-
-            // calc direction
-            // We do not send pulse if direction is disabled completely, or if direction in a specific axis (e.g. N or S) is disabled
-            if (!in_params.enabled[k] || // This axis not enabled
-                    // Positive direction of this axis not enabled.
-                    (arcsecDrift > 0 && !in_params.enabled_axis1[k]) ||
-                    // Negative direction of this axis not enabled.
-                    (arcsecDrift < 0 && !in_params.enabled_axis2[k]))
-            {
-                pulseDirection = NO_DIR;
-                pulseLength = 0;
-            }
-            else
+            // Check the min pulse value, and assign the direction.
+            const double pulseArcSec = pulseConverter > 0 ? pulseLength / pulseConverter : 0;
+            if (pulseArcSec >= in_params.min_pulse_arcsec[k])
             {
-                // Check the min pulse value, and assign the direction.
-                const double pulseArcSec = pulseConverter > 0 ? pulseLength / pulseConverter : 0;
-                if (pulseArcSec >= in_params.min_pulse_arcsec[k])
-                {
-                    if (k == GUIDE_RA)
-                        pulseDirection = arcsecDrift > 0 ? RA_DEC_DIR : RA_INC_DIR;
-                    else
-                        pulseDirection = arcsecDrift > 0 ? DEC_INC_DIR : DEC_DEC_DIR; // GUIDE_DEC.
-                }
+                if (k == GUIDE_RA)
+                    pulseDirection = arcsecDrift > 0 ? RA_DEC_DIR : RA_INC_DIR;
                 else
-                    pulseDirection = NO_DIR;
+                    pulseDirection = arcsecDrift > 0 ? DEC_INC_DIR : DEC_DEC_DIR; // GUIDE_DEC.
             }
-
+            else
+                pulseDirection = NO_DIR;
         }
-        qCDebug(KSTARS_EKOS_GUIDE) << "pulse_length[" << axisStr(k) << "] = " << pulseLength
-                                   << "ms, Direction = " << directionStr(pulseDirection);
 
-        out_params.pulse_dir[k]  = pulseDirection;
-        out_params.pulse_length[k] = pulseLength;
-        out_params.delta[k] = arcsecDrift;
     }
+    qCDebug(KSTARS_EKOS_GUIDE) << "pulse_length[" << axisStr(k) << "] = " << pulseLength
+                               << "ms, Direction = " << directionStr(pulseDirection);
 
-    if (Options::guideLogging())
+    updateOutParams(k, arcsecDrift, pulseLength, pulseDirection);
+}
+
+void cgmath::calculatePulses(Ekos::GuideState state, const std::array<Seconds, 2> &timeStep)
+{
+    qCDebug(KSTARS_EKOS_GUIDE) << "Processing Axes";
+
+    const bool dithering = configureInParams(state);
+
+
+    // process axes...
+    for (int k = GUIDE_RA; k <= GUIDE_DEC; k++)
     {
-        QTextStream out(&logFile);
-        out << iterationCounter << "," << logTime.elapsed() << "," << out_params.delta[0] << "," << out_params.pulse_length[0] <<
-            ","
-            << directionStr(out_params.pulse_dir[0]) << "," << out_params.delta[1] << ","
-            << out_params.pulse_length[1] << "," << directionStr(out_params.pulse_dir[1]) << Qt::endl;
+        processAxis(k, dithering, false, timeStep[k]);
     }
+
+    outputGuideLog();
 }
 
 void cgmath::performProcessing(Ekos::GuideState state, QSharedPointer<FITSData> &imageData,
-                               QSharedPointer<GuideView> &guideView, GuideLog *logger)
+                               QSharedPointer<GuideView> &guideView,
+                               const std::array<Seconds,2> &timeStep, GuideLog *logger)
 {
     if (suspended)
     {
@@ -521,7 +550,7 @@ void cgmath::performProcessing(Ekos::GuideState state, QSharedPointer<FITSData>
     const double decDrift = drift[GUIDE_DEC][driftUpto[GUIDE_DEC]];
 
     // make decision by axes
-    calculatePulses(state);
+    calculatePulses(state, timeStep);
 
     if (state == Ekos::GUIDE_GUIDING)
     {
@@ -566,6 +595,20 @@ void cgmath::performProcessing(Ekos::GuideState state, QSharedPointer<FITSData>
     qCDebug(KSTARS_EKOS_GUIDE) << "################## FINISH PROCESSING ##################";
 }
 
+void cgmath::performDarkGuiding(Ekos::GuideState state, const std::array<Seconds,2> &timeStep, GuideLog *logger)
+{
+
+    const bool dithering = configureInParams(state);
+    //out_params.sigma[GUIDE_RA] = 0;
+
+    processAxis(GUIDE_RA, dithering, true, timeStep[GUIDE_RA]);
+
+    // Don't guide in DEC when dark guiding
+    updateOutParams(GUIDE_DEC, 0, 0, NO_DIR);
+
+    outputGuideLog();
+}
+
 void cgmath::emitStats()
 {
     double pulseRA = 0;
diff --git a/kstars/ekos/guide/internalguide/gmath.h b/kstars/ekos/guide/internalguide/gmath.h
index c5ab29779..2092eea2a 100644
--- a/kstars/ekos/guide/internalguide/gmath.h
+++ b/kstars/ekos/guide/internalguide/gmath.h
@@ -127,8 +127,12 @@ class cgmath : public QObject
         void performProcessing(Ekos::GuideState state,
                                QSharedPointer<FITSData> &imageData,
                                QSharedPointer<GuideView> &guideView,
+                               const std::array<Seconds,2> &timeStep,
                                GuideLog *logger = nullptr);
 
+        void performDarkGuiding(Ekos::GuideState state, const std::array<Seconds,2> &timeStep,
+                                GuideLog *logger = nullptr);
+
         bool calibrate1D(double start_x, double start_y, double end_x, double end_y, int RATotalPulse);
         bool calibrate2D(double start_ra_x, double start_ra_y, double end_ra_x, double end_ra_y,
                          double start_dec_x, double start_dec_y, double end_dec_x, double end_dec_y,
@@ -165,7 +169,7 @@ class cgmath : public QObject
 
         void updateCircularBuffers(void);
         GuiderUtils::Vector point2arcsec(const GuiderUtils::Vector &p) const;
-        void calculatePulses(Ekos::GuideState state);
+        void calculatePulses(Ekos::GuideState state, const std::array<Seconds,2> &timeStep);
         void calculateRmsError(void);
 
         // Old-stye Logging--deprecate.
@@ -216,4 +220,8 @@ class cgmath : public QObject
 
         std::unique_ptr<GPG> gpg;
         Calibration calibration;
+        bool configureInParams(Ekos::GuideState state);
+        void updateOutParams(int k, const double arcsecDrift, int pulseLength, GuideDirection pulseDirection);
+        void outputGuideLog();
+        void processAxis(const int k, const bool dithering, const bool darkGuiding, const Seconds &timeStep);
 };
diff --git a/kstars/ekos/guide/internalguide/gpg.cpp b/kstars/ekos/guide/internalguide/gpg.cpp
index 58b7242cc..dcb46e378 100644
--- a/kstars/ekos/guide/internalguide/gpg.cpp
+++ b/kstars/ekos/guide/internalguide/gpg.cpp
@@ -61,7 +61,14 @@ double getSNR(const GuideStars *guideStars, const double raArcsecError)
 }
 
 }  // namespace
-
+double GPG::predictionContribution() {
+    if (gpg.get() == nullptr)
+    {
+        qCDebug(KSTARS_EKOS_GUIDE) << "Nullptr in predictionContribution()";
+        return 0.0;
+    }
+    return gpg->getPredictionContribution();
+}
 void GPG::updateParameters()
 {
     // Parameters would be set when the gpg stars up.
@@ -187,7 +194,7 @@ void GPG::suspended(const GuiderUtils::Vector &guideStarPosition,
 
 bool GPG::computePulse(double raArcsecError, GuideStars *guideStars,
                        int *pulseLength, GuideDirection *pulseDir,
-                       const Calibration &cal)
+                       const Calibration &cal, Seconds timeStep)
 {
     if (!Options::gPGEnabled())
         return false;
@@ -225,20 +232,16 @@ bool GPG::computePulse(double raArcsecError, GuideStars *guideStars,
     // GPG input is in RA arcseconds.
     QElapsedTimer gpgTimer;
     gpgTimer.restart();
-    const double gpgResult = gpg->result(raArcsecError, getSNR(guideStars, raArcsecError), Options::guideExposure());
-    const double gpgTime = gpgTimer.elapsed();
-    gpgSamples++;
 
-    // GPG output is in RA arcseconds.
-    const double gpgPulse = gpgResult * cal.raPulseMillisecondsPerArcsecond();
+    // Cast back to a raw double
+    auto const rawTime = timeStep.count();
 
-    *pulseDir = gpgPulse > 0 ? RA_DEC_DIR : RA_INC_DIR;
-    *pulseLength = fabs(gpgPulse);
+    const double gpgResult = gpg->result(raArcsecError, getSNR(guideStars, raArcsecError), rawTime);
+    const double gpgTime = gpgTimer.elapsed();
+    gpgSamples++;
 
-    if (*pulseDir == RA_DEC_DIR)
-        qCDebug(KSTARS_EKOS_GUIDE) << "pulse_length  [RA] ="  << *pulseLength << "Direction     : Decrease";
-    else
-        qCDebug(KSTARS_EKOS_GUIDE) << "pulse_length  [RA] ="  << *pulseLength << "Direction     : Increase";
+    // GPG output is in RA arcseconds. pulseLength and pulseDir are set by convertCorrectionToPulseMilliseconds.
+    const double gpgPulse = convertCorrectionToPulseMilliseconds(cal, pulseLength, pulseDir, gpgResult);
 
     qCDebug(KSTARS_EKOS_GUIDE)
             << QString("GPG: elapsed %1s. RA in %2, result: %3 * %4 --> %5 : len %6 dir %7")
@@ -256,3 +259,51 @@ bool GPG::computePulse(double raArcsecError, GuideStars *guideStars,
     }
     return true;
 }
+
+double GPG::convertCorrectionToPulseMilliseconds(const Calibration &cal, int *pulseLength,
+                                                 GuideDirection *pulseDir, const double gpgResult)
+{
+    const double gpgPulse = gpgResult * cal.raPulseMillisecondsPerArcsecond();
+
+    *pulseDir = gpgPulse > 0 ? RA_DEC_DIR : RA_INC_DIR;
+    *pulseLength = fabs(gpgPulse);
+
+    if (*pulseDir == RA_DEC_DIR)
+        qCDebug(KSTARS_EKOS_GUIDE) << "pulse_length  [RA] ="  << *pulseLength << "Direction     : Decrease";
+    else
+        qCDebug(KSTARS_EKOS_GUIDE) << "pulse_length  [RA] ="  << *pulseLength << "Direction     : Increase";
+
+    return gpgPulse;
+}
+
+bool GPG::darkGuiding(int *pulseLength, GuideDirection *pulseDir, const Calibration &cal,
+                      Seconds timeStep)
+{
+  if (!Options::gPGEnabled() || !Options::gPGDarkGuiding())
+  {
+      qCDebug(KSTARS_EKOS_GUIDE) << "dark guiding isn't enabled!";
+      return false;
+  }
+
+  // GPG uses proportional gain and min-move from standard controls. Make sure they're using up-to-date values.
+  gpg->SetControlGain(Options::rAProportionalGain());
+  gpg->SetMinMove(Options::rAMinimumPulseArcSec());
+
+  QElapsedTimer gpgTimer;
+  gpgTimer.restart();
+  const double gpgResult = gpg->deduceResult(timeStep.count());
+  const double gpgTime = gpgTimer.elapsed();
+
+  // GPG output is in RA arcseconds.
+  const double gpgPulse = convertCorrectionToPulseMilliseconds(cal, pulseLength, pulseDir, gpgResult);
+
+  qCDebug(KSTARS_EKOS_GUIDE)
+          << QString("GPG dark guiding: elapsed %1s. RA result: %2 * %3 --> %4 : len %5 dir %6")
+          .arg(gpgTime / 1000.0)
+          .arg(gpgResult)
+          .arg(cal.raPulseMillisecondsPerArcsecond())
+          .arg(gpgPulse)
+          .arg(*pulseLength)
+          .arg(*pulseDir);
+  return true;
+}
diff --git a/kstars/ekos/guide/internalguide/gpg.h b/kstars/ekos/guide/internalguide/gpg.h
index 9d9b92889..c2b727ab8 100644
--- a/kstars/ekos/guide/internalguide/gpg.h
+++ b/kstars/ekos/guide/internalguide/gpg.h
@@ -9,7 +9,7 @@
 #include "vect.h"
 #include "indi/indicommon.h"
 #include "MPI_IS_gaussian_process/src/gaussian_process_guider.h"
-
+#include "ekos_guide_debug.h"
 class GuideStars;
 class GaussianProcessGuider;
 class Calibration;
@@ -48,10 +48,20 @@ class GPG
         // Returns false if it chooses not to compute a pulse.
         bool computePulse(double raArcsecError, GuideStars *guideStars,
                           int *pulseLength, GuideDirection *pulseDir,
-                          const Calibration &cal);
+                          const Calibration &cal, Seconds timeStep);
+
+        double predictionContribution();
+
+
+        // Compute dark guiding RA pulse.
+        // Returns false if it chooses not to compute a pulse.
+        bool darkGuiding(int *pulseLength, GuideDirection *pulseDir,
+                         const Calibration &cal, Seconds timeStep);
 
     private:
         std::unique_ptr<GaussianProcessGuider> gpg;
         int gpgSamples = 0;
         int gpgSkippedSamples = 0;
+        // Converts the gpg output to pulse milliseconds
+        double convertCorrectionToPulseMilliseconds(const Calibration &cal, int *pulseLength, GuideDirection *pulseDir, const double gpgResult);
 };
diff --git a/kstars/ekos/guide/internalguide/internalguider.cpp b/kstars/ekos/guide/internalguide/internalguider.cpp
index 312a7fb19..32ac82fab 100644
--- a/kstars/ekos/guide/internalguide/internalguider.cpp
+++ b/kstars/ekos/guide/internalguide/internalguider.cpp
@@ -27,6 +27,8 @@
 
 #define MAX_GUIDE_STARS           10
 
+using namespace std::chrono_literals;
+
 namespace Ekos
 {
 InternalGuider::InternalGuider()
@@ -43,9 +45,49 @@ InternalGuider::InternalGuider()
 
     state = GUIDE_IDLE;
     m_DitherOrigin = QVector3D(0, 0, 0);
+
     emit guideInfo("");
+
+    m_darkGuideTimer = std::make_unique<QTimer>(this);
+    m_captureTimer = std::make_unique<QTimer>(this);
+
+    setDarkGuideTimerInterval();
+
+    setExposureTime();
+
+    connect(this, &Ekos::GuideInterface::frameCaptureRequested, this, [=](){this->m_captureTimer->start();});
+}
+
+void InternalGuider::setExposureTime()
+{
+    Seconds seconds(Options::guideExposure());
+    setTimer(m_captureTimer, seconds);
+}
+
+void InternalGuider::setTimer(std::unique_ptr<QTimer> &timer, Seconds seconds)
+{
+    const std::chrono::duration<double,std::milli> inMilliseconds(seconds);
+    timer->setInterval((int)(inMilliseconds.count()));
+}
+
+void InternalGuider::setDarkGuideTimerInterval()
+{
+    const Seconds seconds(Options::gPGDarkGuidingInterval());
+    setTimer(m_darkGuideTimer, seconds);
 }
 
+void InternalGuider::resetDarkGuiding()
+{
+    m_darkGuideTimer->stop();
+    m_captureTimer->stop();
+}
+
+bool InternalGuider::isInferencePeriodFinished()
+{
+    auto const contribution = pmath->getGPG().predictionContribution();
+    qCDebug(KSTARS_EKOS_GUIDE) << "GPG contribution proportion:" << contribution;
+    return contribution >= 0.99;
+}
 bool InternalGuider::guide()
 {
     if (state >= GUIDE_GUIDING)
@@ -76,12 +118,14 @@ bool InternalGuider::guide()
         fillGuideInfo(&info);
         guideLog.startGuiding(info);
     }
-
     state = GUIDE_GUIDING;
+
     emit newStatus(state);
 
     emit frameCaptureRequested();
 
+    startDarkGuiding();
+
     return true;
 }
 
@@ -118,8 +162,13 @@ bool InternalGuider::abort()
         qCDebug(KSTARS_EKOS_GUIDE) << "Stopping internal guider.";
     }
 
+    resetDarkGuiding();
+    disconnect(m_darkGuideTimer.get(), nullptr, nullptr, nullptr);
+
     pmath->abort();
 
+
+
     m_ProgressiveDither.clear();
     m_starLostCounter = 0;
     m_highRMSCounter = 0;
@@ -136,6 +185,8 @@ bool InternalGuider::suspend()
 {
     guideLog.pauseInfo();
     state = GUIDE_SUSPENDED;
+
+    resetDarkGuiding();
     emit newStatus(state);
 
     pmath->suspend(true);
@@ -144,6 +195,19 @@ bool InternalGuider::suspend()
     return true;
 }
 
+void InternalGuider::startDarkGuiding()
+{
+    if (Options::gPGDarkGuiding())
+    {
+        connect(m_darkGuideTimer.get(), &QTimer::timeout, this, &InternalGuider::darkGuide, Qt::UniqueConnection);
+
+        // Start the two dark guide timers. The capture timer is started automatically by a signal.
+        m_darkGuideTimer->start();
+
+        qCDebug(KSTARS_EKOS_GUIDE) << "Starting dark guiding.";
+    }
+}
+
 bool InternalGuider::resume()
 {
     emit guideInfo("");
@@ -153,6 +217,10 @@ bool InternalGuider::resume()
 
     pmath->suspend(false);
 
+    startDarkGuiding();
+
+    setExposureTime();
+
     emit frameCaptureRequested();
 
     return true;
@@ -527,7 +595,9 @@ void InternalGuider::iterateCalibration()
 {
     if (calibrationProcess->inProgress())
     {
-        pmath->performProcessing(GUIDE_CALIBRATING, m_ImageData, m_GuideFrame);
+        auto const timeStep = calculateGPGTimeStep();
+        pmath->performProcessing(GUIDE_CALIBRATING, m_ImageData, m_GuideFrame, timeStep);
+
         QString info = "";
         if (pmath->usingSEPMultiStar())
         {
@@ -538,6 +608,7 @@ void InternalGuider::iterateCalibration()
                    .arg(gs.getNumReferences());
         }
         emit guideInfo(info);
+
         if (pmath->isStarLost())
         {
             emit newLog(i18n("Lost track of the guide star. Try increasing the square size or reducing pulse duration."));
@@ -571,7 +642,7 @@ void InternalGuider::iterateCalibration()
     int pulseMsecs;
     calibrationProcess->getPulse(&pulseDirection, &pulseMsecs);
     if (pulseDirection != NO_DIR)
-        emit newSinglePulse(pulseDirection, pulseMsecs);
+        emit newSinglePulse(pulseDirection, pulseMsecs, StartCaptureAfterPulses);
 
     if (status == GUIDE_CALIBRATION_ERROR)
     {
@@ -616,6 +687,8 @@ void InternalGuider::reset()
 {
     state = GUIDE_IDLE;
 
+    resetDarkGuiding();
+
     connect(m_GuideFrame.get(), &FITSView::trackingStarSelected, this, &InternalGuider::trackingStarSelected,
             Qt::UniqueConnection);
     calibrationProcess.reset();
@@ -707,6 +780,27 @@ bool InternalGuider::setFrameParams(uint16_t x, uint16_t y, uint16_t w, uint16_t
     return true;
 }
 
+void InternalGuider::emitAxisPulse(const cproc_out_params *out)
+{
+    double raPulse = out->pulse_length[GUIDE_RA];
+    double dePulse = out->pulse_length[GUIDE_DEC];
+
+    //If the pulse was not sent to the mount, it should have 0 value
+    if(out->pulse_dir[GUIDE_RA] == NO_DIR)
+        raPulse = 0;
+    //If the pulse was not sent to the mount, it should have 0 value
+    if(out->pulse_dir[GUIDE_DEC] == NO_DIR)
+        dePulse = 0;
+    //If the pulse was in the Negative direction, it should have a negative sign.
+    if(out->pulse_dir[GUIDE_RA] == RA_INC_DIR)
+        raPulse = -raPulse;
+    //If the pulse was in the Negative direction, it should have a negative sign.
+    if(out->pulse_dir[GUIDE_DEC] == DEC_INC_DIR)
+        dePulse = -dePulse;
+
+    emit newAxisPulse(raPulse, dePulse);
+}
+
 bool InternalGuider::processGuiding()
 {
     const cproc_out_params *out;
@@ -714,6 +808,7 @@ bool InternalGuider::processGuiding()
     // On first frame, center the box (reticle) around the star so we do not start with an offset the results in
     // unnecessary guiding pulses.
     bool process = true;
+
     if (m_isFirstFrame)
     {
         m_isFirstFrame = false;
@@ -731,20 +826,23 @@ bool InternalGuider::processGuiding()
         }
     }
 
-    QString info = "";
     if (process)
     {
-        pmath->performProcessing(state, m_ImageData, m_GuideFrame, &guideLog);
+        auto const timeStep = calculateGPGTimeStep();
+        pmath->performProcessing(state, m_ImageData, m_GuideFrame, timeStep, &guideLog);
         if (pmath->usingSEPMultiStar())
         {
+        QString info = "";
             auto gs = pmath->getGuideStars();
             info = QString("%1 stars, %2/%3 refs")
                    .arg(gs.getNumStarsDetected())
                    .arg(gs.getNumReferencesFound())
                    .arg(gs.getNumReferences());
+
+            emit guideInfo(info);
         }
+
     }
-    emit guideInfo(info);
 
     if (state == GUIDE_SUSPENDED)
     {
@@ -752,17 +850,113 @@ bool InternalGuider::processGuiding()
             emit frameCaptureRequested();
         return true;
     }
-
-    if (pmath->isStarLost())
-        m_starLostCounter++;
     else
-        m_starLostCounter = 0;
+    {
+        if (pmath->isStarLost())
+            m_starLostCounter++;
+        else
+            m_starLostCounter = 0;
+    }
 
     // do pulse
     out = pmath->getOutputParameters();
 
+    if (isPoorGuiding(out))
+        return true;
+
     bool sendPulses = !pmath->isStarLost();
 
+
+    // Send pulse if we have one active direction at least.
+    if (sendPulses && (out->pulse_dir[GUIDE_RA] != NO_DIR || out->pulse_dir[GUIDE_DEC] != NO_DIR))
+    {
+        emit newMultiPulse(out->pulse_dir[GUIDE_RA], out->pulse_length[GUIDE_RA],
+                           out->pulse_dir[GUIDE_DEC], out->pulse_length[GUIDE_DEC], StartCaptureAfterPulses);
+    }
+    else
+        emit frameCaptureRequested();
+
+    if (state == GUIDE_DITHERING || state == GUIDE_MANUAL_DITHERING)
+        return true;
+
+    // Hy 9/13/21: Check above just looks for GUIDE_DITHERING or GUIDE_MANUAL_DITHERING
+    // but not the other dithering possibilities (error, success, settle).
+    // Not sure if they should be included above, so conservatively not changing the
+    // code, but don't think they should broadcast the newAxisDelta which might
+    // interrup a capture.
+    if (state < GUIDE_DITHERING)
+        emit newAxisDelta(out->delta[GUIDE_RA], out->delta[GUIDE_DEC]);
+
+    emitAxisPulse(out);
+    emit newAxisSigma(out->sigma[GUIDE_RA], out->sigma[GUIDE_DEC]);
+    if (SEPMultiStarEnabled())
+        emit newSNR(pmath->getGuideStarSNR());
+
+    return true;
+}
+
+
+// Here we calculate the time until the next time we will be emitting guiding corrections.
+std::array<Seconds,2> InternalGuider::calculateGPGTimeStep()
+{
+    Seconds timeStep;
+
+    const Seconds guideDelay{(Options::guideDelay())};
+
+    auto const captureInterval = Seconds(m_captureTimer->intervalAsDuration()) + guideDelay;
+    auto const darkGuideInterval = Seconds(m_darkGuideTimer->intervalAsDuration());
+
+    if (!Options::gPGDarkGuiding() || !isInferencePeriodFinished())
+    {
+        return std::array<Seconds,2> {captureInterval, captureInterval};
+    }
+    auto const captureTimeRemaining = Seconds(m_captureTimer->remainingTimeAsDuration()) + guideDelay;
+    auto const darkGuideTimeRemaining = Seconds(m_darkGuideTimer->remainingTimeAsDuration());
+    // Are both firing at the same time (or at least, both due)?
+    if (captureTimeRemaining <= Seconds::zero()
+            && darkGuideTimeRemaining <= Seconds::zero())
+    {
+        timeStep = std::min(captureInterval, darkGuideInterval);
+    }
+    else if (captureTimeRemaining <= Seconds::zero())
+    {
+        timeStep = std::min(captureInterval, darkGuideTimeRemaining);
+    }
+    else if (darkGuideTimeRemaining <= Seconds::zero())
+    {
+        timeStep = std::min(captureTimeRemaining, darkGuideInterval);
+    }
+    else
+    {
+        timeStep = std::min(captureTimeRemaining, darkGuideTimeRemaining);
+    }
+    qCDebug(KSTARS_EKOS_GUIDE) << "calculated timestep" << timeStep.count() << "seconds";
+    return std::array<Seconds,2> {timeStep, captureInterval};
+}
+
+
+
+void InternalGuider::darkGuide()
+{
+     if(Options::gPGDarkGuiding() && isInferencePeriodFinished())
+     {
+         qCDebug(KSTARS_EKOS_GUIDE) << "##########BEGIN DARK GUIDING############";
+         const cproc_out_params *out;
+         auto const timeStep = calculateGPGTimeStep();
+         pmath->performDarkGuiding(state, timeStep, &guideLog);
+
+         out = pmath->getOutputParameters();
+
+         emit newMultiPulse(out->pulse_dir[GUIDE_RA], out->pulse_length[GUIDE_RA],
+                       out->pulse_dir[GUIDE_DEC], out->pulse_length[GUIDE_DEC], DontCaptureAfterPulses);
+
+         emitAxisPulse(out);
+         qCDebug(KSTARS_EKOS_GUIDE) << "##########END DARK GUIDING############";
+    }
+}
+
+bool InternalGuider::isPoorGuiding(const cproc_out_params* out)
+{
     double delta_rms = std::hypot(out->delta[GUIDE_RA], out->delta[GUIDE_DEC]);
     if (delta_rms > Options::guideMaxDeltaRMS())
         m_highRMSCounter++;
@@ -790,52 +984,8 @@ bool InternalGuider::processGuiding()
         emit newStatus(state);
         return true;
     }
-
-    // Send pulse if we have one active direction at least.
-    if (sendPulses && (out->pulse_dir[GUIDE_RA] != NO_DIR || out->pulse_dir[GUIDE_DEC] != NO_DIR))
-    {
-        emit newMultiPulse(out->pulse_dir[GUIDE_RA], out->pulse_length[GUIDE_RA],
-                           out->pulse_dir[GUIDE_DEC], out->pulse_length[GUIDE_DEC]);
-    }
-    else
-        emit frameCaptureRequested();
-
-    if (state == GUIDE_DITHERING || state == GUIDE_MANUAL_DITHERING)
-        return true;
-
-    // Hy 9/13/21: Check above just looks for GUIDE_DITHERING or GUIDE_MANUAL_DITHERING
-    // but not the other dithering possibilities (error, success, settle).
-    // Not sure if they should be included above, so conservatively not changing the
-    // code, but don't think they should broadcast the newAxisDelta which might
-    // interrup a capture.
-    if (state < GUIDE_DITHERING)
-        emit newAxisDelta(out->delta[GUIDE_RA], out->delta[GUIDE_DEC]);
-
-    double raPulse = out->pulse_length[GUIDE_RA];
-    double dePulse = out->pulse_length[GUIDE_DEC];
-
-    //If the pulse was not sent to the mount, it should have 0 value
-    if(out->pulse_dir[GUIDE_RA] == NO_DIR)
-        raPulse = 0;
-    //If the pulse was not sent to the mount, it should have 0 value
-    if(out->pulse_dir[GUIDE_DEC] == NO_DIR)
-        dePulse = 0;
-    //If the pulse was in the Negative direction, it should have a negative sign.
-    if(out->pulse_dir[GUIDE_RA] == RA_INC_DIR)
-        raPulse = -raPulse;
-    //If the pulse was in the Negative direction, it should have a negative sign.
-    if(out->pulse_dir[GUIDE_DEC] == DEC_INC_DIR)
-        dePulse = -dePulse;
-
-    emit newAxisPulse(raPulse, dePulse);
-
-    emit newAxisSigma(out->sigma[GUIDE_RA], out->sigma[GUIDE_DEC]);
-    if (SEPMultiStarEnabled())
-        emit newSNR(pmath->getGuideStarSNR());
-
-    return true;
+    return false;
 }
-
 bool InternalGuider::selectAutoStarSEPMultistar()
 {
     m_GuideFrame->updateFrame();
@@ -1041,12 +1191,14 @@ void InternalGuider::fillGuideInfo(GuideLog::GuideInfo *info)
 
 void InternalGuider::updateGPGParameters()
 {
+    setDarkGuideTimerInterval();
     pmath->getGPG().updateParameters();
 }
 
 void InternalGuider::resetGPG()
 {
     pmath->getGPG().reset();
+    resetDarkGuiding();
 }
 
 const Calibration &InternalGuider::getCalibration() const
diff --git a/kstars/ekos/guide/internalguide/internalguider.h b/kstars/ekos/guide/internalguide/internalguider.h
index 982706d13..88e70357a 100644
--- a/kstars/ekos/guide/internalguide/internalguider.h
+++ b/kstars/ekos/guide/internalguide/internalguider.h
@@ -15,7 +15,8 @@
 #include "guidelog.h"
 #include "calibration.h"
 #include "calibrationprocess.h"
-
+#include "gmath.h"
+#include "ekos_guide_debug.h"
 #include <QFile>
 #include <QPointer>
 #include <QQueue>
@@ -114,24 +115,29 @@ class InternalGuider : public GuideInterface
 
         void updateGPGParameters();
         void resetGPG() override;
+        void resetDarkGuiding();
+        void setExposureTime();
+        void setDarkGuideTimerInterval();
+        void setTimer(std::unique_ptr<QTimer> &timer, Seconds seconds);
 
     public slots:
         void setDECSwap(bool enable);
 
+
     protected slots:
         void trackingStarSelected(int x, int y);
         void setDitherSettled();
+        void darkGuide();
 
     signals:
-        void newMultiPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection dec_dir, int dec_msecs);
-        void newSinglePulse(GuideDirection dir, int msecs);
+        void newMultiPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection dec_dir, int dec_msecs, CaptureAfterPulses followWithCapture);
+        void newSinglePulse(GuideDirection dir, int msecs, CaptureAfterPulses followWithCapture);
         //void newStarPosition(QVector3D, bool);
         void DESwapChanged(bool enable);
-
-    private:
+private:
         // Guiding
         bool processGuiding();
-
+        void startDarkGuiding();
         bool abortDither();
 
         void reset();
@@ -162,6 +168,13 @@ class InternalGuider : public GuideInterface
         // Progressive Manual Dither
         QQueue<GuiderUtils::Vector> m_ProgressiveDither;
 
+        // Dark guiding timer
+        std::unique_ptr<QTimer> m_darkGuideTimer;
+        std::unique_ptr<QTimer> m_captureTimer;
+        std::array<Seconds,2> calculateGPGTimeStep();
+        bool isInferencePeriodFinished();
+
+
         // How many high RMS pulses before we stop
         static const uint8_t MAX_RMS_THRESHOLD = 10;
         // How many lost stars before we stop
@@ -186,5 +199,9 @@ class InternalGuider : public GuideInterface
         std::unique_ptr<CalibrationProcess> calibrationProcess;
         double calibrationStartX = 0;
         double calibrationStartY = 0;
+
+
+        bool isPoorGuiding(const cproc_out_params *out);
+        void emitAxisPulse(const cproc_out_params *out);
 };
 }
diff --git a/kstars/ekos/guide/opsgpg.ui b/kstars/ekos/guide/opsgpg.ui
index b24113974..aa046a717 100644
--- a/kstars/ekos/guide/opsgpg.ui
+++ b/kstars/ekos/guide/opsgpg.ui
@@ -7,7 +7,7 @@
     <x>0</x>
     <y>0</y>
     <width>471</width>
-    <height>570</height>
+    <height>582</height>
    </rect>
   </property>
   <layout class="QVBoxLayout" name="verticalLayout_2">
@@ -70,6 +70,13 @@
         <property name="spacing">
          <number>3</number>
         </property>
+        <item row="0" column="1">
+         <widget class="QCheckBox" name="kcfg_GPGEnabled">
+          <property name="toolTip">
+           <string>Enable the GPG guider for RA guiding.</string>
+          </property>
+         </widget>
+        </item>
         <item row="0" column="0">
          <widget class="QLabel" name="label_gpg0">
           <property name="font">
@@ -85,13 +92,6 @@
           </property>
          </widget>
         </item>
-        <item row="0" column="1">
-         <widget class="QCheckBox" name="kcfg_GPGEnabled">
-          <property name="toolTip">
-           <string>Enable the GPG guider for RA guiding.</string>
-          </property>
-         </widget>
-        </item>
        </layout>
       </item>
      </layout>
@@ -123,6 +123,23 @@
         <property name="spacing">
          <number>3</number>
         </property>
+        <item row="4" column="0">
+         <widget class="QLabel" name="label_4">
+          <property name="toolTip">
+           <string>Maximum time between emitting predictive corrections while capturing guide images. This might be on the order of 1 second or less, with the guide exposure much longer. The exact values and the ratio between them will depend greatly on your mount and environmental conditions; but generally, the worse periodic error of your mount, the greater the ratio between guide exposure and dark guiding interval.</string>
+          </property>
+          <property name="text">
+           <string>Dark guiding interval</string>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="2">
+         <widget class="QLabel" name="label_gpgs0c">
+          <property name="text">
+           <string>seconds</string>
+          </property>
+         </widget>
+        </item>
         <item row="0" column="0">
          <widget class="QLabel" name="label_gpgs0a">
           <property name="toolTip">
@@ -133,23 +150,43 @@
           </property>
          </widget>
         </item>
+        <item row="2" column="0">
+         <widget class="QLabel" name="label_gpgs2a">
+          <property name="toolTip">
+           <string>The fraction of its prediction the GPG uses to move the mount.</string>
+          </property>
+          <property name="text">
+           <string>Prediction Gain</string>
+          </property>
+         </widget>
+        </item>
+        <item row="3" column="0">
+         <widget class="QLabel" name="label_gpgsDarkGuiding">
+          <property name="toolTip">
+           <string>Enable predictive corrections during acquisition of guide camera images</string>
+          </property>
+          <property name="text">
+           <string>Intra-frame dark guiding</string>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="2">
+         <widget class="QLabel" name="label_gpgs2b">
+          <property name="text">
+           <string/>
+          </property>
+         </widget>
+        </item>
         <item row="0" column="1">
          <widget class="QDoubleSpinBox" name="kcfg_GPGPeriod">
           <property name="toolTip">
            <string>The length in seconds of the mount's major period (that's being corrected).</string>
           </property>
           <property name="minimum">
-           <number>0</number>
+           <double>0.000000000000000</double>
           </property>
           <property name="maximum">
-           <number>3600</number>
-          </property>
-         </widget>
-        </item>
-        <item row="0" column="2">
-         <widget class="QLabel" name="label_gpgs0c">
-          <property name="text">
-           <string>seconds</string>
+           <double>3600.000000000000000</double>
           </property>
          </widget>
         </item>
@@ -163,13 +200,6 @@
           </property>
          </widget>
         </item>
-        <item row="1" column="1">
-         <widget class="QCheckBox" name="kcfg_GPGEstimatePeriod">
-          <property name="toolTip">
-           <string>If checked, the GPG estimates the mount's major period. Otherwise, it uses the entry above.</string>
-          </property>
-         </widget>
-        </item>
         <item row="1" column="2">
          <widget class="QLabel" name="label_gpgs1b">
           <property name="text">
@@ -177,40 +207,58 @@
           </property>
          </widget>
         </item>
-        <item row="2" column="0">
-         <widget class="QLabel" name="label_gpgs2a">
-          <property name="toolTip">
-           <string>The fraction of its prediction the GPG uses to move the mount.</string>
+        <item row="5" column="1" colspan="2">
+         <widget class="QLabel" name="label_2">
+          <property name="font">
+           <font>
+            <pointsize>9</pointsize>
+           </font>
           </property>
           <property name="text">
-           <string>Prediction Gain</string>
+           <string>Uses RA "Aggressiveness" from Guide controls</string>
           </property>
          </widget>
         </item>
-        <item row="2" column="1">
-         <widget class="QDoubleSpinBox" name="kcfg_GPGpWeight">
+        <item row="1" column="1">
+         <widget class="QCheckBox" name="kcfg_GPGEstimatePeriod">
           <property name="toolTip">
-           <string>The fraction of its prediction the GPG uses to move the mount.</string>
+           <string>If checked, the GPG estimates the mount's major period. Otherwise, it uses the entry above.</string>
           </property>
-          <property name="minimum">
-           <double>0.000000000000000</double>
+         </widget>
+        </item>
+        <item row="6" column="0">
+         <widget class="QLabel" name="label_gpgs4a">
+          <property name="toolTip">
+           <string>The min-move parameter the GPG uses to move the mount when it uses its backoff proportional guider.</string>
           </property>
-          <property name="maximum">
-           <double>1.000000000000000</double>
+          <property name="text">
+           <string>Minimum Move</string>
           </property>
-          <property name="singleStep">
-           <double>0.100000000000000</double>
+         </widget>
+        </item>
+        <item row="6" column="1" colspan="2">
+         <widget class="QLabel" name="label_3">
+          <property name="font">
+           <font>
+            <pointsize>9</pointsize>
+           </font>
+          </property>
+          <property name="text">
+           <string>Uses RA "Min error" from Guide controls</string>
           </property>
          </widget>
         </item>
-        <item row="2" column="2">
-         <widget class="QLabel" name="label_gpgs2b">
+        <item row="3" column="1">
+         <widget class="QCheckBox" name="kcfg_GPGDarkGuiding">
+          <property name="toolTip">
+           <string>Enable predictive corrections during acquisition of guide camera images</string>
+          </property>
           <property name="text">
            <string/>
           </property>
          </widget>
         </item>
-        <item row="3" column="0">
+        <item row="5" column="0">
          <widget class="QLabel" name="label_gpgs3a">
           <property name="toolTip">
            <string>The fraction of the guide-star drift that the GPG uses to move the mount.</string>
@@ -220,27 +268,42 @@
           </property>
          </widget>
         </item>
-        <item row="4" column="0">
-         <widget class="QLabel" name="label_gpgs4a">
+        <item row="2" column="1">
+         <widget class="QDoubleSpinBox" name="kcfg_GPGpWeight">
           <property name="toolTip">
-           <string>The min-move parameter the GPG uses to move the mount when it uses its backoff proportional guider.</string>
+           <string>The fraction of its prediction the GPG uses to move the mount.</string>
           </property>
-          <property name="text">
-           <string>Minimum Move</string>
+          <property name="minimum">
+           <double>0.000000000000000</double>
+          </property>
+          <property name="maximum">
+           <double>1.000000000000000</double>
+          </property>
+          <property name="singleStep">
+           <double>0.100000000000000</double>
           </property>
          </widget>
         </item>
-        <item row="3" column="1" colspan="2">
-         <widget class="QLabel" name="label_2">
-          <property name="text">
-           <string>Uses RA "Aggressiveness" from Guide controls</string>
+        <item row="4" column="1">
+         <widget class="QDoubleSpinBox" name="kcfg_GPGDarkGuidingInterval">
+          <property name="toolTip">
+           <string>Maximum time between emitting predictive corrections while capturing guide images. This might be on the order of 1 second or less, with the guide exposure much longer. The exact values and the ratio between them will depend greatly on your mount and environmental conditions; but generally, the worse periodic error of your mount, the greater the ratio between guide exposure and dark guiding interval.</string>
+          </property>
+          <property name="minimum">
+           <double>0.000000000000000</double>
+          </property>
+          <property name="maximum">
+           <double>3600.000000000000000</double>
+          </property>
+          <property name="value">
+           <double>1.000000000000000</double>
           </property>
          </widget>
         </item>
-        <item row="4" column="1" colspan="2">
-         <widget class="QLabel" name="label_3">
+        <item row="4" column="2">
+         <widget class="QLabel" name="label_5">
           <property name="text">
-           <string>Uses RA "Min error" from Guide controls</string>
+           <string>seconds</string>
           </property>
          </widget>
         </item>
@@ -317,7 +380,7 @@
         <item row="0" column="2">
          <widget class="QLabel" name="label_gpgas0c">
           <property name="text">
-           <string/>
+           <string>seconds</string>
           </property>
          </widget>
         </item>
@@ -390,7 +453,7 @@
         <item row="2" column="2">
          <widget class="QLabel" name="label_gpgas2c">
           <property name="text">
-           <string/>
+           <string>seconds</string>
           </property>
          </widget>
         </item>
@@ -473,7 +536,7 @@
         <item row="4" column="2">
          <widget class="QLabel" name="label_gpgas4c">
           <property name="text">
-           <string/>
+           <string>seconds</string>
           </property>
          </widget>
         </item>
diff --git a/kstars/indi/indicommon.h b/kstars/indi/indicommon.h
index 4a9dc80fd..42f228cec 100644
--- a/kstars/indi/indicommon.h
+++ b/kstars/indi/indicommon.h
@@ -8,7 +8,7 @@
 
 #include <QString>
 #include <QMap>
-
+#include <chrono>
 /*!
 \page INDI "INDI Overview"
 \tableofcontents
@@ -162,3 +162,4 @@ typedef enum { SOURCE_MANUAL, SOURCE_FLATCAP, SOURCE_WALL, SOURCE_DAWN_DUSK, SOU
 
 typedef enum { DURATION_MANUAL, DURATION_ADU } FlatFieldDuration;
 
+using Seconds = std::chrono::duration<double>;
diff --git a/kstars/indi/indifocuser.cpp b/kstars/indi/indifocuser.cpp
index b07eadcdb..a62ad906f 100644
--- a/kstars/indi/indifocuser.cpp
+++ b/kstars/indi/indifocuser.cpp
@@ -159,7 +159,8 @@ bool Focuser::moveRel(int steps)
         focusProp = getNumber("manualfocusdrive");
 
         FocusDirection dir;
-        getFocusDirection(&dir);
+        if (!getFocusDirection(&dir))
+                return false;
         if (dir == FOCUS_INWARD)
             steps = -abs(steps);
         else if (dir == FOCUS_OUTWARD)
diff --git a/kstars/kstars.kcfg b/kstars/kstars.kcfg
index ecd30206f..db91a394a 100644
--- a/kstars/kstars.kcfg
+++ b/kstars/kstars.kcfg
@@ -2398,6 +2398,12 @@
       <entry name="GPGEnabled" type="Bool">
          <default>false</default>
       </entry>
+      <entry name="GPGDarkGuiding" type="Bool">
+         <default>false</default>
+      </entry>
+      <entry name="GPGDarkGuidingInterval" type="Double">
+         <default>1</default>
+      </entry>
       <entry name="GPGcWeight" type="Double">
          <default>0.6</default>
       </entry>


More information about the kde-doc-english mailing list