[education/kstars] kstars/ekos/guide/internalguide: AI Guide: remove unphysical DEC PE oscillator states from Harmonic Kalman filter

Jasem Mutlaq <[email protected]> Wed, 5 Aug 2026 11:02:41 +0000 (UTC)
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit f1f5ee41f8cd98a844f21ea392fb3da44f9314f2 by Jasem Mutlaq.
Committed on 05/08/2026 at 11:02.
Pushed by mutlaqja into branch 'master'.

AI Guide: remove unphysical DEC PE oscillator states from Harmonic Kalman filter

Strain-wave periodic error comes from the continuously rotating RA drive;
DEC has no equivalent mechanism. The DEC_PE_SIN/COS and DEC_PE2_SIN/COS
states had nothing real to lock onto, and with only DEC_VEL competing for
credit against the same single DEC position observation, the two became
degenerate: they drifted apart as a large, growing, mutually-cancelling
pair, leaving a small but steadily growing DEC bias as the residual.

Disconnect those states from both the process noise (computeQ) and the
observation matrix (kalmanUpdate) so no Kalman gain flows into them; they
now stay at their zero-initialized value for the session, matching the
physical reality that DEC PE doesn't exist. RA is unaffected.

M  +11   -9    kstars/ekos/guide/internalguide/harmonic_guider.cpp
M  +4    -0    kstars/ekos/guide/internalguide/harmonic_guider.h

https://invent.kde.org/education/kstars/-/commit/f1f5ee41f8cd98a844f21ea392fb3da44f9314f2

diff --git a/kstars/ekos/guide/internalguide/harmonic_guider.cpp b/kstars/ekos/guide/internalguide/harmonic_guider.cpp
index 3e328699c3..e11549dc31 100644
--- a/kstars/ekos/guide/internalguide/harmonic_guider.cpp
+++ b/kstars/ekos/guide/internalguide/harmonic_guider.cpp
@@ -313,20 +313,22 @@ HarmonicGuider::computeQ(double snr, double snr_delta,
     Q(RA_SPRING, RA_SPRING) = 0.001 * dt;
     Q(DEC_SPRING, DEC_SPRING) = 0.001 * dt;
 
-    // PE process noise (small — PE is nearly deterministic)
+    // PE process noise (small — PE is nearly deterministic). RA only: strain-wave
+    // periodic error comes from the continuously rotating RA drive, so a DEC PE
+    // oscillator has nothing real to lock onto. With no measurement on those states
+    // either (see kalmanUpdate()), a slow sinusoid and DEC_VEL's linear ramp become
+    // degenerate over a session shorter than one PE period, and the filter settles
+    // into a large, growing, mutually-cancelling pair whose small residual is a
+    // steadily growing DEC bias.
     if (m_pe_period > 0.0)
     {
         Q(RA_PE_SIN, RA_PE_SIN) = 0.001 * dt;
         Q(RA_PE_COS, RA_PE_COS) = 0.001 * dt;
-        Q(DEC_PE_SIN, DEC_PE_SIN) = 0.001 * dt;
-        Q(DEC_PE_COS, DEC_PE_COS) = 0.001 * dt;
     }
     if (m_pe2_period > 0.0)
     {
         Q(RA_PE2_SIN, RA_PE2_SIN) = 0.001 * dt;
         Q(RA_PE2_COS, RA_PE2_COS) = 0.001 * dt;
-        Q(DEC_PE2_SIN, DEC_PE2_SIN) = 0.001 * dt;
-        Q(DEC_PE2_COS, DEC_PE2_COS) = 0.001 * dt;
     }
 
     return Q;
@@ -385,20 +387,20 @@ void HarmonicGuider::kalmanPredict(double dt, double alt_deg, double parallactic
 // ── Kalman update step ───────────────────────────────────────────────────────
 void HarmonicGuider::kalmanUpdate(double ra_meas_px, double dec_meas_px, double snr)
 {
-    // Observation matrix: observe position + PE_sin
-    // H extracts: ra_obs = ra_err + pe_sin_ra, dec_obs = dec_err + pe_sin_dec
+    // Observation matrix: observe position + PE_sin, RA only (see computeQ() for why
+    // DEC has no PE oscillator to observe). DEC_PE_SIN/DEC_PE2_SIN never receive a
+    // Kalman gain and stay at their zero-initialized value for the whole session.
+    // H extracts: ra_obs = ra_err + pe_sin_ra, dec_obs = dec_err
     Eigen::Matrix<double, N_OBS, N_STATES> H = Eigen::Matrix<double, N_OBS, N_STATES>::Zero();
     H(0, RA_POS) = 1.0;
     H(1, DEC_POS) = 1.0;
     if (m_pe_period > 0.0)
     {
         H(0, RA_PE_SIN) = 1.0;
-        H(1, DEC_PE_SIN) = 1.0;
     }
     if (m_pe2_period > 0.0)
     {
         H(0, RA_PE2_SIN) = 1.0;
-        H(1, DEC_PE2_SIN) = 1.0;
     }
 
     // Measurement noise from the current frame's SNR (~0.5 px at SNR 30)
diff --git a/kstars/ekos/guide/internalguide/harmonic_guider.h b/kstars/ekos/guide/internalguide/harmonic_guider.h
index ddf04144c3..4bee0ab68a 100644
--- a/kstars/ekos/guide/internalguide/harmonic_guider.h
+++ b/kstars/ekos/guide/internalguide/harmonic_guider.h
@@ -15,6 +15,10 @@
  *
  * The PE states evolve as a 2D rotation at the detected PE frequency, allowing
  * the Kalman filter to automatically estimate PE amplitude and phase online.
+ * PE is an RA-only phenomenon (strain-wave error from the continuously rotating
+ * RA drive); pe_sin_dec/pe_cos_dec/pe2_sin_dec/pe2_cos_dec occupy state-vector
+ * slots but are never driven by process noise or observed (see computeQ() and
+ * kalmanUpdate()), so they stay at zero for the whole session.
  *
  * SPDX-License-Identifier: GPL-2.0-or-later
  */