[education/kstars] kstars/ekos/guide: Guide: Let dark guiding run in streaming without starving the frame loop

Jasem Mutlaq <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 927fb8079ae2ed3ac0b6a7a4fcd37a582ac4ea29 by Jasem Mutlaq, on behalf of Andreas R..
Committed on 31/07/2026 at 15:51.
Pushed by mutlaqja into branch 'master'.

Guide: Let dark guiding run in streaming without starving the frame loop

# MR: Guide — AI guider (and GPG) support for streaming guide mode

**Source:** `cfuture/k-stars-ext-guide-looping:feat/ai-guider-streaming` **Target:** `education/kstars:master` **Title:** `Guide: Support dark guiding + AI feed-forward in streaming guide mode`

---

## Summary

Makes the AI feed-forward guider (and GPG dark guiding) work correctly with **guide streaming**, not just single-capture. Builds directly on the three fixes already merged (`3384173c3`: OBJCTDEC parse, mount-state sourcing, debug-log leak).

The mount-state fix already lets the AI read altitude/declination/pier from the mount object instead of FITS headers — so stream frames (which carry no headers) already feed the physics model correctly. The one remaining blocker was **dark-guide pulses starving the stream frame loop**.

## Background

Dark guiding (GPG and AI) issues _predicted_ correction pulses **between** camera exposures. In streaming those pulses were emitted as `DontCaptureAfterPulses`, so `Guide` re-armed the streaming frame gate (`m_streamingPulseGuard`) for each one and dropped the next real frame. At the 0.5 s dark interval this starved the measurement loop — `cgmath`'s `update()` stopped being called and the feed-forward filter ran open-loop.

## The change (3 files, +17/-5)

Distinguish the two pulse kinds rather than disabling the feature:

- New `CaptureAfterPulses::DarkGuidePulse` for between-frame prediction pulses.
- `InternalGuider::darkGuide()` emits `DarkGuidePulse` (was `DontCaptureAfterPulses`).
- `Guide::sendSinglePulse()` arms the streaming frame gate **only** for `DontCaptureAfterPulses` (real correction pulses that need the mount to settle before the next measurement). `DarkGuidePulse` sends the prediction and returns **without** gating, so the next real frame is still processed.

Correction pulses still gate the stream exactly as before; only the between-frame predictions stop dropping measurements.

## What this does

- **Dark guiding stays available in every mode** — single-capture or streaming, GPG or AI. Nothing is disabled; this fix makes it _work_ in streaming (where it previously starved the loop) instead of silently degrading. It's most useful at longer streaming exposures (e.g. \>1 s for dim guide stars), where the predictions usefully fill the inter-frame gap.

## Testing

- Compiles clean (no warnings), astyle-clean against `.astylerc`.
- Simulator procedure (validates the code path: no starvation, mount state flows, `ai_state` progresses) — see `ai-guiding/streaming-ai-simulator-test.md`. The CCD Simulator's synthetic drift/PE can't validate guiding _quality_; that needs a real mount.
- **On-sky validation pending.**

### Reviewer caveat to watch

Dark-guide predictions can now be in flight when a real frame triggers a correction pulse (a brief pulse overlap). Dark pulses are small and this mirrors established PHD2/GPG behaviour, but worth confirming no oscillation from overlapping pulses during testing.

## Note on training

For best results the model should be trained at the **same guide exposure and mode** used for guiding — the fingerprint already enforces exposure. Streaming-mode training is valid: the protocol timestamps everything against real wall-clock time (free-drift/standard accumulate real per-frame `dt`; pulse-response records true seconds since the pulse via `m_PulseSentAtMs`), so the frame:left_right_arrow:pulse:left_right_arrow:time association is exact in both capture modes.

M  +6    -3    kstars/ekos/guide/guide.cpp
M  +8    -1    kstars/ekos/guide/guideinterface.h
M  +3    -1    kstars/ekos/guide/internalguide/internalguider.cpp

https://invent.kde.org/education/kstars/-/commit/927fb8079ae2ed3ac0b6a7a4fcd37a582ac4ea29

diff --git a/kstars/ekos/guide/guide.cpp b/kstars/ekos/guide/guide.cpp
index 25253c967b..4b150af72a 100644
--- a/kstars/ekos/guide/guide.cpp
+++ b/kstars/ekos/guide/guide.cpp
@@ -1575,12 +1575,13 @@ bool Guide::sendSinglePulse(GuideDirection dir, int msecs, CaptureAfterPulses fo
             auto delay = std::max(static_cast<int>(guideDelay->value() * 1000), ms);
             m_PulseTimer.start(delay);
         }
-        else if (m_StreamingGuide)
+        else if (m_StreamingGuide && followWithCapture == DontCaptureAfterPulses)
         {
             auto ms = msecs + 100;
             auto delay = std::max(static_cast<int>(guideDelay->value() * 1000), ms);
             m_streamingPulseGuard.start(delay);
         }
+        // DarkGuidePulse: no capture, no stream gate — just a between-frame prediction.
         return true;
     }
 
@@ -1593,14 +1594,16 @@ bool Guide::sendSinglePulse(GuideDirection dir, int msecs, CaptureAfterPulses fo
 
         m_PulseTimer.start(delay);
     }
-    else if (m_StreamingGuide)
+    else if (m_StreamingGuide && followWithCapture == DontCaptureAfterPulses)
     {
-        // Same pulse-in-flight gate as sendMultiPulse() above.
+        // Same pulse-in-flight gate as sendMultiPulse() above — correction pulses only.
         auto ms = msecs + 100;
         auto delay = std::max(static_cast<int>(guideDelay->value() * 1000), ms);
         qCDebug(KSTARS_EKOS_GUIDE) << "Streaming pulse guard started for" << delay << "ms (single pulse)";
         m_streamingPulseGuard.start(delay);
     }
+    // DarkGuidePulse falls through: send the prediction without gating the stream, so the next
+    // real frame is still processed (dark guiding stays usable in streaming, e.g. long exposures).
 
     return m_Guider->doPulse(dir, msecs);
 }
diff --git a/kstars/ekos/guide/guideinterface.h b/kstars/ekos/guide/guideinterface.h
index 2c70ef4a10..51f0de1f66 100644
--- a/kstars/ekos/guide/guideinterface.h
+++ b/kstars/ekos/guide/guideinterface.h
@@ -109,6 +109,13 @@ class GuideInterface : public QObject
         ISD::Mount::PierSide pierSide { ISD::Mount::PIER_UNKNOWN };
 };
 
-enum CaptureAfterPulses {StartCaptureAfterPulses, DontCaptureAfterPulses};
+// StartCaptureAfterPulses  : single-capture mode — request the next exposure once the pulse completes.
+// DontCaptureAfterPulses   : streaming correction pulse — frames arrive continuously; gate the stream
+//                            while the mount settles so the next measured frame isn't taken mid-move.
+// DarkGuidePulse           : a between-frame prediction pulse (GPG/AI dark guiding). Like
+//                            DontCaptureAfterPulses it requests no capture, but it must NOT gate the
+//                            stream — its whole purpose is to run between real frames, so gating it
+//                            would drop the very measurements the filter needs (frame starvation).
+enum CaptureAfterPulses {StartCaptureAfterPulses, DontCaptureAfterPulses, DarkGuidePulse};
 
 }
diff --git a/kstars/ekos/guide/internalguide/internalguider.cpp b/kstars/ekos/guide/internalguide/internalguider.cpp
index cf6054fe5f..96cd43e8b3 100644
--- a/kstars/ekos/guide/internalguide/internalguider.cpp
+++ b/kstars/ekos/guide/internalguide/internalguider.cpp
@@ -1170,7 +1170,9 @@ void InternalGuider::darkGuide()
         pmath->performDarkGuiding(state, timeStep);
 
         out = pmath->getOutputParameters();
-        Q_EMIT newSinglePulse(out->pulse_dir[GUIDE_RA], out->pulse_length[GUIDE_RA], DontCaptureAfterPulses);
+        // DarkGuidePulse (not DontCaptureAfterPulses): in streaming this must not arm the frame gate,
+        // or each between-frame prediction would drop the next real frame and starve the measurement loop.
+        Q_EMIT newSinglePulse(out->pulse_dir[GUIDE_RA], out->pulse_length[GUIDE_RA], DarkGuidePulse);
 
         emitAxisPulse(out);
     }
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.