@@ -196,15 +196,15 @@ class AlignmentPluginTest : public ::testing::Test
196196
197197 static void buildEntry (::Alignment &gen,
198198 TelescopeDirectionVectorSupportFunctions *pSupport,
199- double ra, double dec, double lst_hrs,
199+ double ra, double dec, double lst_hrs, double jd,
200200 ::Alignment::MOUNT_TYPE mountType,
201201 AlignmentDatabaseEntry &entry)
202202 {
203203 Angle ha (get_local_hour_angle (lst_hrs, ra), Angle::HOURS ), adec (dec);
204204
205205 entry.RightAscension = ra;
206206 entry.Declination = dec;
207- entry.ObservationJulianDate = fixedJD ;
207+ entry.ObservationJulianDate = jd ;
208208
209209 if (mountType == ::Alignment::ALTAZ )
210210 {
@@ -260,8 +260,8 @@ class AlignmentPluginTest : public ::testing::Test
260260 getSkyPoint (raw.first , raw.second , minDec, maxDec, ra, dec);
261261
262262 AlignmentDatabaseEntry entry;
263- buildEntry (generator, pSupport, ra, dec, lst, mountType, entry);
264- db.GetAlignmentDatabase ().push_back (entry);
263+ buildEntry (generator, pSupport, ra, dec, lst, fixedJD , mountType, entry);
264+ db.GetAlignmentDatabase ().push_back (entry);
265265 }
266266
267267 ASSERT_TRUE (plugin.Initialise (&db));
@@ -322,7 +322,7 @@ class AlignmentPluginTest : public ::testing::Test
322322 getSkyPoint (raw.first , raw.second , minDec, maxDec, ra, dec);
323323
324324 AlignmentDatabaseEntry entry;
325- buildEntry (generator, pSupport, ra, dec, lst, mountType, entry);
325+ buildEntry (generator, pSupport, ra, dec, lst, fixedJD, mountType, entry);
326326 alignDb.GetAlignmentDatabase ().push_back (entry);
327327 }
328328
@@ -423,7 +423,7 @@ class AlignmentPluginTest : public ::testing::Test
423423 getSkyPoint (raw.first , raw.second , minDec, maxDec, ra, dec);
424424
425425 AlignmentDatabaseEntry entry;
426- buildEntry (generator, pSupport, ra, dec, lst, mountType, entry);
426+ buildEntry (generator, pSupport, ra, dec, lst, fixedJD, mountType, entry);
427427 cleanDb.GetAlignmentDatabase ().push_back (entry);
428428
429429 addNoiseToDirection (entry.TelescopeDirection , noiseSigmaDeg, rng);
@@ -477,6 +477,80 @@ class AlignmentPluginTest : public ::testing::Test
477477 << " Plugin failed noise-stress P95 target " << targetRMSArcsec * 1.5 << " \" " ;
478478 }
479479
480+ void RunTimeShiftValidate (MathPlugin &plugin, const MountErrors &errors,
481+ int numAlign, int numValidate,
482+ double shiftHours, double targetRMSArcsec,
483+ INDI ::IGeographicCoordinates site = kLosAngeles ,
484+ ::Alignment::MOUNT_TYPE mountType = ::Alignment::EQ_GEM )
485+ {
486+ double minDec = (mountType == ::Alignment::ALTAZ ) ? kAZ_MinEl : kEQ_MinDec ;
487+ double maxDec = (mountType == ::Alignment::ALTAZ ) ? kAZ_MaxEl : kEQ_MaxDec ;
488+
489+ InMemoryDatabase alignDb;
490+ alignDb.SetDatabaseReferencePosition (site);
491+
492+ MountAlignment_t alignment = (mountType == ::Alignment::ALTAZ ) ? ZENITH :
493+ site.latitude >= 0 ? NORTH_CELESTIAL_POLE
494+ : SOUTH_CELESTIAL_POLE ;
495+ plugin.SetApproximateMountAlignment (alignment);
496+
497+ ::Alignment generator = makeGenerator (errors, site, mountType);
498+ auto *pSupport = dynamic_cast <TelescopeDirectionVectorSupportFunctions *>(&plugin);
499+ ASSERT_NE (pSupport, nullptr );
500+
501+ // Training Phase: JD = fixedJD
502+ double gmst = ln_get_apparent_sidereal_time (fixedJD);
503+ double lst = range24 (gmst + DEG_TO_HOURS (site.longitude ));
504+
505+ HaltonSequence alignSeq (2 , 3 );
506+ for (int i = 1 ; i <= numAlign; ++i)
507+ {
508+ double ra, dec;
509+ auto raw = alignSeq.getRaw (i);
510+ getSkyPoint (raw.first , raw.second , minDec, maxDec, ra, dec);
511+
512+ AlignmentDatabaseEntry entry;
513+ buildEntry (generator, pSupport, ra, dec, lst, fixedJD, mountType, entry);
514+ alignDb.GetAlignmentDatabase ().push_back (entry);
515+ }
516+
517+ ASSERT_TRUE (plugin.Initialise (&alignDb));
518+
519+ // Validation Phase: JD = fixedJD + shiftHours/24
520+ double valJD = fixedJD + shiftHours / 24.0 ;
521+ double joff = valJD - ln_get_julian_from_sys ();
522+
523+ HaltonSequence validateSeq (5 , 7 );
524+ std::vector<double > residuals;
525+
526+ for (int i = 1 ; i <= numValidate; ++i)
527+ {
528+ double ra, dec;
529+ auto raw = validateSeq.getRaw (i);
530+ getSkyPoint (raw.first , raw.second , minDec, maxDec, ra, dec);
531+
532+ TelescopeDirectionVector resultV;
533+ // The plugin must correctly interpret ra, dec relative to the new joff (time)
534+ ASSERT_TRUE (plugin.TransformCelestialToTelescope (ra, dec, joff, resultV));
535+
536+ double outRA, outDec;
537+ plugin.TransformTelescopeToCelestial (resultV, outRA, outDec, joff);
538+
539+ double dRA = DEG_TO_ARCSEC (rangeHA (outRA - ra) * 15.0 ) * std::cos (DEG_TO_RAD (dec));
540+ double dDec = DEG_TO_ARCSEC (outDec - dec);
541+ double residual = std::sqrt (dRA * dRA + dDec * dDec);
542+
543+ residuals.push_back (residual);
544+ }
545+
546+ ErrorStats stats = ErrorStats::compute (residuals);
547+ GTEST_LOG_ (INFO ) << " [ TIME SHIFT " << shiftHours << " h ] RMS: " << stats.rms << " \" Target: " << targetRMSArcsec
548+ << " \" P95: " << stats.p95 << " \" Peak: " << stats.max << " \" " ;
549+
550+ EXPECT_LT (stats.rms , targetRMSArcsec)
551+ << " Plugin failed time-shift RMS quality target " << targetRMSArcsec << " \" " ;
552+ }
553+
480554 // -----------------------------------------------------------------------
481555 // RunGotoConventionRegression -- AltAz C->T convention check.
482556 //
@@ -514,7 +588,7 @@ class AlignmentPluginTest : public ::testing::Test
514588 auto raw = seq.getRaw (i);
515589 getSkyPoint (raw.first , raw.second , kAZ_MinEl , kAZ_MaxEl , ra, dec);
516590 AlignmentDatabaseEntry entry;
517- buildEntry (gen, pSupport, ra, dec, lst, ::Alignment::ALTAZ , entry);
591+ buildEntry (gen, pSupport, ra, dec, lst, fixedJD, ::Alignment::ALTAZ , entry);
518592 db.GetAlignmentDatabase ().push_back (entry);
519593 }
520594 ASSERT_TRUE (plugin.Initialise (&db));
@@ -572,7 +646,7 @@ class AlignmentPluginTest : public ::testing::Test
572646 auto raw = seq.getRaw (i);
573647 getSkyPoint (raw.first , raw.second , kEQ_MinDec , kEQ_MaxDec , ra, dec);
574648 AlignmentDatabaseEntry entry;
575- buildEntry (gen, pSupport, ra, dec, lst, ::Alignment::EQ_GEM , entry);
649+ buildEntry (gen, pSupport, ra, dec, lst, fixedJD, ::Alignment::EQ_GEM , entry);
576650 db.GetAlignmentDatabase ().push_back (entry);
577651 }
578652 ASSERT_TRUE (plugin.Initialise (&db));
@@ -609,6 +683,64 @@ class AlignmentPluginTest : public ::testing::Test
609683 EXPECT_LT (totalDiff, tolArcsec) << " Encoder position differs by " << totalDiff << " \" — likely ME/MA mapping bug" ;
610684 }
611685
686+ void RunAltAzPolarRegression (SPKMathPlugin &plugin,
687+ const MountErrors &errors,
688+ int numAlign,
689+ double valRA, double valDec,
690+ double tolArcsec,
691+ INDI ::IGeographicCoordinates site = kLosAngeles )
692+ {
693+ plugin.SetApproximateMountAlignment (ZENITH );
694+
695+ InMemoryDatabase db;
696+ db.SetDatabaseReferencePosition (site);
697+
698+ ::Alignment gen = makeGenerator (errors, site, ::Alignment::ALTAZ );
699+ auto *pSupport = dynamic_cast <TelescopeDirectionVectorSupportFunctions *>(&plugin);
700+ ASSERT_NE (pSupport, nullptr );
701+
702+ double gmst = ln_get_apparent_sidereal_time (fixedJD);
703+ double lst = range24 (gmst + DEG_TO_HOURS (site.longitude ));
704+
705+ HaltonSequence seq (2 , 3 );
706+ for (int i = 1 ; i <= numAlign; ++i)
707+ {
708+ double ra, dec;
709+ auto raw = seq.getRaw (i);
710+ getSkyPoint (raw.first , raw.second , kAZ_MinEl , kAZ_MaxEl , ra, dec);
711+ AlignmentDatabaseEntry entry;
712+ buildEntry (gen, pSupport, ra, dec, lst, fixedJD, ::Alignment::ALTAZ , entry);
713+ db.GetAlignmentDatabase ().push_back (entry);
714+ }
715+ ASSERT_TRUE (plugin.Initialise (&db));
716+
717+ double joff = fixedJD - ln_get_julian_from_sys ();
718+
719+ // Ground truth encoder position (Alt/Az) from scopesim
720+ Angle ha (get_local_hour_angle (lst, valRA), Angle::HOURS );
721+ Angle adec (valDec);
722+ Angle encAz, encAlt;
723+ gen.apparentHaDecToMount (ha, adec, &encAz, &encAlt);
724+
725+ // Plugin C->T prediction, decoded to Alt/Az without T->C
726+ TelescopeDirectionVector tdv;
727+ ASSERT_TRUE (plugin.TransformCelestialToTelescope (valRA, valDec, joff, tdv));
728+ INDI ::IHorizontalCoordinates horCoords;
729+ pSupport->AltitudeAzimuthFromTelescopeDirectionVector (tdv, horCoords);
730+
731+ double azDiff = std::fmod (horCoords.azimuth - encAz.Degrees () + 540.0 , 360.0 ) - 180.0 ;
732+ double altDiff = std::abs (horCoords.altitude - encAlt.Degrees ());
733+ double totalDiff = std::sqrt (std::pow (azDiff * std::cos (DEG_TO_RAD (horCoords.altitude )), 2 ) + altDiff * altDiff);
734+ totalDiff *= 3600.0 ; // arcsec
735+
736+ GTEST_LOG_ (INFO ) << " encAz=" << encAz.Degrees () << " pluginAz=" << horCoords.azimuth
737+ << " azDiff=" << azDiff * 3600.0 << " \" " ;
738+ GTEST_LOG_ (INFO ) << " encAlt=" << encAlt.Degrees () << " pluginAlt=" << horCoords.altitude
739+ << " altDiff=" << altDiff * 3600.0 << " \" " ;
740+
741+ EXPECT_LT (totalDiff, tolArcsec) << " Encoder position differs by " << totalDiff << " \" — likely AN/AW mapping bug" ;
742+ }
743+
612744 // -----------------------------------------------------------------------
613745 // RunMeridianFlipValidate -- exercises GEM pier flips in both hemispheres
614746 // -----------------------------------------------------------------------
@@ -639,7 +771,7 @@ class AlignmentPluginTest : public ::testing::Test
639771 getSkyPoint (raw.first , raw.second , kEQ_MinDec , kEQ_MaxDec , ra, dec);
640772
641773 AlignmentDatabaseEntry entry;
642- buildEntry (generator, pSupport, ra, dec, lst, ::Alignment::EQ_GEM , entry);
774+ buildEntry (generator, pSupport, ra, dec, lst, fixedJD, ::Alignment::EQ_GEM , entry);
643775 alignDb.GetAlignmentDatabase ().push_back (entry);
644776 }
645777
@@ -723,8 +855,8 @@ class AlignmentPluginTest : public ::testing::Test
723855 auto raw = seq.getRaw (i);
724856 getSkyPoint (raw.first , raw.second , minDec, maxDec, ra, dec);
725857 AlignmentDatabaseEntry entry;
726- buildEntry (generator, pSupport, ra, dec, lst, mountType, entry);
727- db.GetAlignmentDatabase ().push_back (entry);
858+ buildEntry (generator, pSupport, ra, dec, lst, fixedJD , mountType, entry);
859+ db.GetAlignmentDatabase ().push_back (entry);
728860 }
729861 EXPECT_TRUE (plugin.Initialise (&db));
730862 return generator;
@@ -1029,7 +1161,7 @@ TEST_F(AlignmentPluginTest, Nearest_AltAz_Goto_Convention_Regression)
10291161 getSkyPoint (raw.first , raw.second , kAZ_MinEl , kAZ_MaxEl , ra, dec);
10301162 if (i == 1 ) { valRA = ra; valDec = dec; }
10311163 AlignmentDatabaseEntry entry;
1032- buildEntry (gen, pSupport, ra, dec, lst, ::Alignment::ALTAZ , entry);
1164+ buildEntry (gen, pSupport, ra, dec, lst, fixedJD, ::Alignment::ALTAZ , entry);
10331165 db.GetAlignmentDatabase ().push_back (entry);
10341166 }
10351167 ASSERT_TRUE (plugin.Initialise (&db));
@@ -1091,7 +1223,7 @@ TEST_F(AlignmentPluginTest, SVD_AlignValidate_ConvexHullFallback)
10911223 if (ra >= 6.0 && ra < 18.0 )
10921224 continue ;
10931225 AlignmentDatabaseEntry entry;
1094- buildEntry (generator, pSupport, ra, dec, lst, ::Alignment::EQ_GEM , entry);
1226+ buildEntry (generator, pSupport, ra, dec, lst, fixedJD, ::Alignment::EQ_GEM , entry);
10951227 alignDb.GetAlignmentDatabase ().push_back (entry);
10961228 ++alignCount;
10971229 }
@@ -1473,6 +1605,31 @@ TEST_F(AlignmentPluginTest, SPK_EQ_Polar_Mapping_Regression)
14731605 10 , 18.0 , 45.0 , 1.0 );
14741606}
14751607
1608+ TEST_F (AlignmentPluginTest, SPK_EQ_ConeError_Mapping_Regression)
1609+ {
1610+ SPKMathPlugin plugin;
1611+ // Inject significant CH and verify the plugin maps it correctly.
1612+ RunEqPolarRegression (plugin, {.ch = ARCMIN_TO_DEG (10 )},
1613+ 10 , 18.0 , 45.0 , 1.0 );
1614+ }
1615+
1616+ TEST_F (AlignmentPluginTest, SPK_EQ_Sydney_Mapping_Regression)
1617+ {
1618+ SPKMathPlugin plugin;
1619+ // Verify Southern Hemisphere polar mapping (Sydney, lat=-33.9)
1620+ RunEqPolarRegression (plugin, {.ma = ARCMIN_TO_DEG (10 ), .me = ARCMIN_TO_DEG (5 )},
1621+ 10 , 18.0 , -45.0 , 1.0 , kSydney );
1622+ }
1623+
1624+ TEST_F (AlignmentPluginTest, SPK_AltAz_Polar_Mapping_Regression)
1625+ {
1626+ SPKMathPlugin plugin;
1627+ // Inject significant AN and AW and verify the plugin maps them to the correct axes.
1628+ // Use 10 points to ensure a full 6-term fit is well-conditioned.
1629+ RunAltAzPolarRegression (plugin, {.ma = ARCMIN_TO_DEG (10 ), .me = ARCMIN_TO_DEG (5 )},
1630+ 10 , 18.0 , 45.0 , 1.0 );
1631+ }
1632+
14761633TEST_F (AlignmentPluginTest, SPK_AlignValidate_AltAz_NonPerp)
14771634{
14781635 SPKMathPlugin plugin;
@@ -1512,6 +1669,18 @@ TEST_F(AlignmentPluginTest, SPK_AlignValidate_AltAz_Southern)
15121669
15131670static constexpr double kNoiseSigma30arcsec = ARCSEC_TO_DEG (30.0 ); // 30" per axis
15141671
1672+ TEST_F (AlignmentPluginTest, SPK_TimeShift_Regression)
1673+ {
1674+ SPKMathPlugin plugin;
1675+ RunTimeShiftValidate (plugin, kMixed , 6 , 100 , 3.0 , 1.0 );
1676+ }
1677+
1678+ TEST_F (AlignmentPluginTest, SVD_TimeShift_Regression)
1679+ {
1680+ SVDMathPlugin plugin;
1681+ RunTimeShiftValidate (plugin, kMixed , 3 , 100 , 3.0 , 1.0 );
1682+ }
1683+
15151684TEST_F (AlignmentPluginTest, NoiseStress_SPK_3pts)
15161685{
15171686 SPKMathPlugin noisyPlugin, refPlugin;
@@ -1583,16 +1752,16 @@ static void RunPolarDegeneracyTestImpl(
15831752 return gen;
15841753 };
15851754
1586- auto buildEntry = [&](::Alignment &gen,
1755+ auto buildEntryLocal = [&](::Alignment &gen,
15871756 TelescopeDirectionVectorSupportFunctions *pSupport,
1588- double ra, double dec, double lst_hrs,
1757+ double ra, double dec, double lst_hrs, double jd,
15891758 ::Alignment::MOUNT_TYPE mt,
15901759 AlignmentDatabaseEntry &entry)
15911760 {
15921761 Angle ha (get_local_hour_angle (lst_hrs, ra), Angle::HOURS ), adec (dec);
15931762 entry.RightAscension = ra;
15941763 entry.Declination = dec;
1595- entry.ObservationJulianDate = fixedJD ;
1764+ entry.ObservationJulianDate = jd ;
15961765 if (mt == ::Alignment::ALTAZ )
15971766 {
15981767 Angle primary, secondary;
@@ -1641,7 +1810,7 @@ static void RunPolarDegeneracyTestImpl(
16411810 }
16421811
16431812 AlignmentDatabaseEntry entry;
1644- buildEntry (gen, pSupport, syncRA, syncDec, lst, mountType, entry);
1813+ buildEntryLocal (gen, pSupport, syncRA, syncDec, lst, fixedJD , mountType, entry);
16451814 db.GetAlignmentDatabase ().push_back (entry);
16461815
16471816 ASSERT_TRUE (plugin.Initialise (&db));
@@ -1770,7 +1939,7 @@ TEST_F(AlignmentPluginTest, SPK_Incremental_MatchesCold_Equatorial)
17701939 auto raw = HaltonSequence (2 , 3 ).getRaw (7 );
17711940 getSkyPoint (raw.first , raw.second , kEQ_MinDec , kEQ_MaxDec , ra, dec);
17721941 AlignmentDatabaseEntry entry;
1773- buildEntry (generator, pSupport, ra, dec, lst, ::Alignment::EQ_GEM , entry);
1942+ buildEntry (generator, pSupport, ra, dec, lst, fixedJD, ::Alignment::EQ_GEM , entry);
17741943 db.GetAlignmentDatabase ().push_back (entry);
17751944 }
17761945 ASSERT_TRUE (hotPlugin.Initialise (&db));
@@ -1796,7 +1965,7 @@ TEST_F(AlignmentPluginTest, SPK_Incremental_MatchesCold_AltAz)
17961965 auto raw = HaltonSequence (2 , 3 ).getRaw (7 );
17971966 getSkyPoint (raw.first , raw.second , kAZ_MinEl , kAZ_MaxEl , ra, dec);
17981967 AlignmentDatabaseEntry entry;
1799- buildEntry (generator, pSupport, ra, dec, lst, ::Alignment::ALTAZ , entry);
1968+ buildEntry (generator, pSupport, ra, dec, lst, fixedJD, ::Alignment::ALTAZ , entry);
18001969 db.GetAlignmentDatabase ().push_back (entry);
18011970 }
18021971 ASSERT_TRUE (hotPlugin.Initialise (&db));
@@ -1827,7 +1996,7 @@ TEST_F(AlignmentPluginTest, SPK_Incremental_InvalidStateNotUsed)
18271996 auto raw = HaltonSequence (2 , 3 ).getRaw (i);
18281997 getSkyPoint (raw.first , raw.second , kEQ_MinDec , kEQ_MaxDec , ra, dec);
18291998 AlignmentDatabaseEntry entry;
1830- buildEntry (generator, pSupport, ra, dec, lst, ::Alignment::EQ_GEM , entry);
1999+ buildEntry (generator, pSupport, ra, dec, lst, fixedJD, ::Alignment::EQ_GEM , entry);
18312000 db.GetAlignmentDatabase ().push_back (entry);
18322001 ASSERT_TRUE (plugin.Initialise (&db));
18332002 }
0 commit comments