[education/kstars] kstars/ekos/guide/internalguide: The AI feed-forward block in cgmath::performProcessing() fetches the guide frame's declination with
Jasem Mutlaq <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 3384173c339d8b136b4af763ce27ca18a38f642f by Jasem Mutlaq, on behalf of Andreas R..
Committed on 30/07/2026 at 19:47.
Pushed by mutlaqja into branch 'master'.
The AI feed-forward block in cgmath::performProcessing() fetches the guide frame's declination with
QVariant::toDouble(), but OBJCTDEC is a sexagesimal string, so the conversion always fails and
the declination silently stays 0.0 on every frame.
The AI feed-forward model needs mount altitude (refraction), declination and site latitude (parallactic
angle) and pier side (to reset state across a meridian flip). cgmath::performProcessing() read all of
these out of the guide frame's FITS header, which has three problems.
1. The keywords only exist when the camera driver snoops the mount.
INDI::CCD::addFITSKeywords() writes them only for LIGHT frames with valid J2000RA/J2000DE and
valid site coordinates, and gates OBJCTAZ/OBJCTALT additionally on a valid Airmass
(indiccd.cpp:2232). When any of that is missing the code silently substituted altitude 45° and
azimuth 180°, so the RA refraction term k_ref / cos^2(alt) was evaluated at a fictitious altitude with
nothing in the log to say so.
2. A missing PIERSIDE left pier_side_east permanently false, so the meridian-flip check never
fired and MountSpecificGuider::resetSession(true) was never called. The persisted Kalman state,
including periodic-error phase, then carried across a flip with inverted axis sense.
3. Inference and training disagreed on the source. OpsAIGuide::slotOnGuideStats() records
altitude, azimuth and declination from mount()->currentCoordinates() while collecting
system-identification data, so the trained coefficients were fitted against mount-derived values and
then evaluated against header-derived ones.
cgmath owns m_AIDebugFile, allocated on demand in performProcessing() when the AI feed-forward
model runs. reset() deletes it so each guiding session gets a fresh log, but the destructor only freed
the two drift buffers, so the QFile allocated for the final session was leaked when cgmath itself
went away.
M +68 -45 kstars/ekos/guide/internalguide/gmath.cpp
M +26 -0 kstars/ekos/guide/internalguide/gmath.h
M +25 -0 kstars/ekos/guide/internalguide/internalguider.cpp
M +6 -0 kstars/ekos/guide/internalguide/internalguider.h
https://invent.kde.org/education/kstars/-/commit/3384173c339d8b136b4af763ce27ca18a38f642f
diff --git a/kstars/ekos/guide/internalguide/gmath.cpp b/kstars/ekos/guide/internalguide/gmath.cpp
index f1bad9e18b..9d2588e28c 100644
--- a/kstars/ekos/guide/internalguide/gmath.cpp
+++ b/kstars/ekos/guide/internalguide/gmath.cpp
@@ -95,6 +95,11 @@ cgmath::~cgmath()
{
delete[] drift[GUIDE_RA];
delete[] drift[GUIDE_DEC];
+
+ // reset() closes this between sessions, but it is also owned across the object's whole
+ // lifetime, so it has to be released here for the case where the last session is never reset.
+ delete m_AIDebugFile;
+ m_AIDebugFile = nullptr;
}
bool cgmath::setVideoParameters(int vid_wd, int vid_ht, int binX, int binY)
@@ -919,46 +924,76 @@ void cgmath::performProcessing(Ekos::GuideState state, QSharedPointer<FITSData>
m_sessionStartTime = current_time_sec;
frameData.t_session_sec = current_time_sec;
- // Fetch altitude from FITS header if available
- QVariant altVariant;
- if (imageData->getRecordValue("OBJCTALT", altVariant))
- {
- frameData.altitude_deg = altVariant.toDouble();
- }
- else
- {
- frameData.altitude_deg = 45.0;
- }
+ // Mount pointing state. Prefer the live mount coordinates pushed in via setMountState():
+ // they are maintained from the mount's own property updates, independently of the image
+ // pipeline, so they are present for every frame and match the values the data-collection
+ // wizard recorded while training. The FITS-header path below is kept as a fallback for the
+ // case where no mount is connected to the guide module.
+ double dec_target = 0.0;
+ double lat_target = 45.0;
- // Fetch azimuth from FITS header if available
- QVariant azVariant;
- if (imageData->getRecordValue("OBJCTAZ", azVariant))
+ if (m_MountState.valid)
{
- frameData.azimuth_deg = azVariant.toDouble();
+ frameData.altitude_deg = m_MountState.altitude_deg;
+ frameData.azimuth_deg = m_MountState.azimuth_deg;
+ frameData.pier_side_east = m_MountState.pier_side_east;
+ dec_target = m_MountState.declination_deg;
+ lat_target = m_MountState.latitude_deg;
}
else
{
- frameData.azimuth_deg = 180.0;
- }
+ // Fetch altitude from FITS header if available
+ QVariant altVariant;
+ if (imageData->getRecordValue("OBJCTALT", altVariant))
+ {
+ frameData.altitude_deg = altVariant.toDouble();
+ }
+ else
+ {
+ frameData.altitude_deg = 45.0;
+ }
- // Fetch DEC for parallactic angle computation
- double dec_target = 0.0;
- QVariant decVariant;
- if (imageData->getRecordValue("OBJCTDEC", decVariant))
- {
- bool ok;
- double d = decVariant.toDouble(&ok);
- if (ok) dec_target = d;
- }
+ // Fetch azimuth from FITS header if available
+ QVariant azVariant;
+ if (imageData->getRecordValue("OBJCTAZ", azVariant))
+ {
+ frameData.azimuth_deg = azVariant.toDouble();
+ }
+ else
+ {
+ frameData.azimuth_deg = 180.0;
+ }
- // Fetch Latitude
- double lat_target = 45.0;
- QVariant latVariant;
- if (imageData->getRecordValue("SITELAT", latVariant))
- {
- bool ok;
- double l = latVariant.toDouble(&ok);
- if (ok) lat_target = l;
+ // Fetch DEC for parallactic angle computation.
+ // OBJCTDEC is written by INDI as a sexagesimal string ("dd mm ss", see
+ // INDI::CCD::addFITSKeywords) so it must be parsed with dms::fromString();
+ // QVariant::toDouble() always fails on it and would silently leave the declination
+ // at 0. The numeric DEC card is used as a fallback, mirroring FITSData::parseSolution().
+ QVariant decVariant;
+ if (imageData->getRecordValue("OBJCTDEC", decVariant))
+ dec_target = dms::fromString(decVariant.toString(), true).Degrees();
+ else if (imageData->getRecordValue("DEC", decVariant))
+ {
+ bool ok;
+ const double d = decVariant.toDouble(&ok);
+ if (ok) dec_target = d;
+ }
+
+ // Fetch Latitude
+ QVariant latVariant;
+ if (imageData->getRecordValue("SITELAT", latVariant))
+ {
+ bool ok;
+ const double l = latVariant.toDouble(&ok);
+ if (ok) lat_target = l;
+ }
+
+ // Fetch PierSide from FITS header if available
+ QVariant pierVariant;
+ if (imageData->getRecordValue("PIERSIDE", pierVariant))
+ frameData.pier_side_east = (pierVariant.toString().toUpper() == "EAST");
+ else
+ frameData.pier_side_east = false; // default
}
// Calculate Parallactic Angle (q)
@@ -977,18 +1012,6 @@ void cgmath::performProcessing(Ekos::GuideState state, QSharedPointer<FITSData>
frameData.parallactic_angle_deg = 0.0;
}
- // Fetch PierSide from FITS header if available
- QVariant pierVariant;
- if (imageData->getRecordValue("PIERSIDE", pierVariant))
- {
- QString pierStr = pierVariant.toString().toUpper();
- frameData.pier_side_east = (pierStr == "EAST");
- }
- else
- {
- frameData.pier_side_east = false; // default
- }
-
static bool last_pier_side_east = frameData.pier_side_east;
// If this is the first time we're setting it up, just initialize it without resetting
static bool pier_initialized = false;
diff --git a/kstars/ekos/guide/internalguide/gmath.h b/kstars/ekos/guide/internalguide/gmath.h
index dce953d646..df6c7a9c2c 100644
--- a/kstars/ekos/guide/internalguide/gmath.h
+++ b/kstars/ekos/guide/internalguide/gmath.h
@@ -156,6 +156,29 @@ class cgmath : public QObject
{
return m_aiRequiredButUnavailable;
}
+
+ /**
+ * @brief Pointing state of the mount, pushed in by the guider whenever the mount reports new
+ * coordinates. This is the preferred source for the AI feed-forward physics model: it is
+ * maintained independently of the image pipeline, so it is available for every frame
+ * regardless of whether the camera driver snoops the mount and populates FITS headers.
+ * Declination is apparent (JNow), which is what the parallactic angle needs.
+ */
+ struct MountState
+ {
+ double altitude_deg { 45.0 };
+ double azimuth_deg { 180.0 };
+ double declination_deg { 0.0 };
+ double latitude_deg { 45.0 };
+ bool pier_side_east { false };
+ /// False until the mount has reported at least once; the FITS-header path is used instead.
+ bool valid { false };
+ };
+
+ void setMountState(const MountState &state)
+ {
+ m_MountState = state;
+ }
const cproc_out_params *getOutputParameters() const
{
return &out_params;
@@ -286,6 +309,9 @@ class cgmath : public QObject
GuideOutput m_lastAIPrediction;
double m_sessionStartTime { 0.0 };
+ /// Latest mount pointing state; see setMountState(). Stays invalid if no mount is connected.
+ MountState m_MountState;
+
// Accumulate pulses sent between camera frames
double m_accumulated_pulse_ra { 0.0 };
double m_accumulated_pulse_dec { 0.0 };
diff --git a/kstars/ekos/guide/internalguide/internalguider.cpp b/kstars/ekos/guide/internalguide/internalguider.cpp
index a3e061fc9c..cf6054fe5f 100644
--- a/kstars/ekos/guide/internalguide/internalguider.cpp
+++ b/kstars/ekos/guide/internalguide/internalguider.cpp
@@ -15,6 +15,7 @@
#include "fitsviewer/fitsdata.h"
#include "fitsviewer/fitsview.h"
#include "guidealgorithms.h"
+#include "kstarsdata.h"
#include "ksnotification.h"
#include "ekos/auxiliary/stellarsolverprofileeditor.h"
#include "fitsviewer/fitsdata.h"
@@ -911,6 +912,30 @@ void InternalGuider::setDECSwap(bool enable)
pmath->getMutableCalibration()->setDeclinationSwapEnabled(enable);
}
+void InternalGuider::setMountCoords(const SkyPoint &position, ISD::Mount::PierSide side)
+{
+ GuideInterface::setMountCoords(position, side);
+
+ if (!pmath)
+ return;
+
+ // The AI feed-forward physics model needs altitude (refraction), declination and site latitude
+ // (parallactic angle) and the pier side (to reset its state across a meridian flip). Sourcing them
+ // here rather than from FITS header keywords means they are available for every guide frame -- the
+ // header values only exist when the camera driver snoops the mount -- and that they match the
+ // values OpsAIGuide recorded during data collection.
+ cgmath::MountState state;
+ state.altitude_deg = mountAltitude.Degrees();
+ state.azimuth_deg = mountAzimuth.Degrees();
+ state.declination_deg = mountDEC.Degrees();
+ state.pier_side_east = (side == ISD::Mount::PIER_EAST);
+ if (KStarsData::Instance() && KStarsData::Instance()->geo() && KStarsData::Instance()->geo()->lat())
+ state.latitude_deg = KStarsData::Instance()->geo()->lat()->Degrees();
+ state.valid = true;
+
+ pmath->setMountState(state);
+}
+
void InternalGuider::setStarDetectionAlgorithm(int index)
{
if (index == SEP_MULTISTAR && !pmath->usingSEPMultiStar())
diff --git a/kstars/ekos/guide/internalguide/internalguider.h b/kstars/ekos/guide/internalguide/internalguider.h
index 56788cc892..c6c0fd233c 100644
--- a/kstars/ekos/guide/internalguide/internalguider.h
+++ b/kstars/ekos/guide/internalguide/internalguider.h
@@ -74,6 +74,12 @@ class InternalGuider : public GuideInterface
// Set Star Position
void setStarPosition(QVector3D &starCenter) override;
+ /**
+ * @brief Caches the mount pointing state (base class) and forwards it to the guide math,
+ * where the AI feed-forward model uses it in preference to FITS header keywords.
+ */
+ void setMountCoords(const SkyPoint &position, ISD::Mount::PierSide side) override;
+
// Select algorithm
void setStarDetectionAlgorithm(int index);