[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);
}