File indexing completed on 2024-05-12 15:23:41

0001 /*
0002     SPDX-FileCopyrightText: 2020 Hy Murveit <hy@murveit.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "gpg.h"
0008 #include <vector>
0009 
0010 #include "guidestars.h"
0011 #include "Options.h"
0012 #include "ekos_guide_debug.h"
0013 
0014 #include "calibration.h"
0015 
0016 #include <QElapsedTimer>
0017 
0018 namespace
0019 {
0020 
0021 // Don't have GPG model large errors. Instantaneous large errors are probably not due to
0022 // periodic error, but rather one-offs.
0023 constexpr double MAX_ARCSEC_ERROR = 5.0;
0024 
0025 // If we skip this many samples, reset gpg.
0026 constexpr int MAX_SKIPPED_SAMPLES = 4;
0027 
0028 // Fills parameters from the KStars options.
0029 void getGPGParameters(GaussianProcessGuider::guide_parameters *parameters)
0030 {
0031     // Parameters from standard options.
0032     parameters->control_gain_                      = Options::rAProportionalGain();
0033     // We need a calibration to determine min move. This is re-set in computePulse() below.
0034     parameters->min_move_                          = 0.25;
0035 
0036     // Parameters from GPG-specific options.
0037     parameters->min_periods_for_inference_         = Options::gPGMinPeriodsForInference();
0038     parameters->SE0KLengthScale_                   = Options::gPGSE0KLengthScale();
0039     parameters->SE0KSignalVariance_                = Options::gPGSE0KSignalVariance();
0040     parameters->PKLengthScale_                     = Options::gPGPKLengthScale();
0041     parameters->PKPeriodLength_                    = Options::gPGPeriod();
0042     parameters->PKSignalVariance_                  = Options::gPGPKSignalVariance();
0043     parameters->SE1KLengthScale_                   = Options::gPGSE1KLengthScale();
0044     parameters->SE1KSignalVariance_                = Options::gPGSE1KSignalVariance();
0045     parameters->min_periods_for_period_estimation_ = Options::gPGMinPeriodsForPeriodEstimate();
0046     parameters->points_for_approximation_          = Options::gPGPointsForApproximation();
0047     parameters->prediction_gain_                   = Options::gPGpWeight();
0048     parameters->compute_period_                    = Options::gPGEstimatePeriod();
0049 }
0050 
0051 // Returns the SNR returned by guideStars, or if guideStars is null (e.g. we aren't
0052 // running SEP MultiStar, then just 50, as this isn't a critical parameter.
0053 double getSNR(const GuideStars *guideStars, const double raArcsecError)
0054 {
0055     if (fabs(raArcsecError) > MAX_ARCSEC_ERROR)
0056         return 1.0;
0057 
0058     // If no SEP MultiStar, just fudge the SNR. Won't be a big deal.
0059     if (guideStars == nullptr)
0060         return 50.0;
0061     else
0062         return guideStars->getGuideStarSNR();
0063 }
0064 
0065 const QString directionStr(GuideDirection dir)
0066 {
0067     switch (dir)
0068     {
0069         case RA_DEC_DIR:
0070             return "Decrease RA";
0071         case RA_INC_DIR:
0072             return "Increase RA";
0073         case DEC_DEC_DIR:
0074             return "Decrease DEC";
0075         case DEC_INC_DIR:
0076             return "Increase DEC";
0077         default:
0078             return "NO DIR";
0079     }
0080 }
0081 
0082 }  // namespace
0083 
0084 double GPG::predictionContribution()
0085 {
0086     if (gpg.get() == nullptr)
0087     {
0088         qCDebug(KSTARS_EKOS_GUIDE) << "Nullptr in predictionContribution()";
0089         return 0.0;
0090     }
0091     return gpg->getPredictionContribution();
0092 }
0093 
0094 void GPG::updateParameters()
0095 {
0096     // Parameters would be set when the gpg stars up.
0097     if (gpg.get() == nullptr) return;
0098 
0099     GaussianProcessGuider::guide_parameters parameters;
0100     getGPGParameters(&parameters);
0101     gpg->SetControlGain(parameters.control_gain_);
0102     gpg->SetPeriodLengthsInference(parameters.min_periods_for_inference_);
0103     gpg->SetMinMove(parameters.min_move_);
0104     gpg->SetPeriodLengthsPeriodEstimation(parameters.min_periods_for_period_estimation_);
0105     gpg->SetNumPointsForApproximation(parameters.points_for_approximation_);
0106     gpg->SetPredictionGain(parameters.prediction_gain_);
0107     gpg->SetBoolComputePeriod(parameters.compute_period_);
0108 
0109     // The GPG header really should be in a namespace so NumParameters
0110     // is not in a global namespace.
0111     std::vector<double> hyperparameters(NumParameters);
0112     hyperparameters[SE0KLengthScale]    = parameters.SE0KLengthScale_;
0113     hyperparameters[SE0KSignalVariance] = parameters.SE0KSignalVariance_;
0114     hyperparameters[PKLengthScale]      = parameters.PKLengthScale_;
0115     hyperparameters[PKSignalVariance]   = parameters.PKSignalVariance_;
0116     hyperparameters[SE1KLengthScale]    = parameters.SE1KLengthScale_;
0117     hyperparameters[SE1KSignalVariance] = parameters.SE1KSignalVariance_;
0118     hyperparameters[PKPeriodLength]     = parameters.PKPeriodLength_;
0119     gpg->SetGPHyperparameters(hyperparameters);
0120 }
0121 
0122 
0123 GPG::GPG()
0124 {
0125     GaussianProcessGuider::guide_parameters parameters;
0126     getGPGParameters(&parameters);
0127     gpg.reset(new GaussianProcessGuider(parameters));
0128     reset();
0129 }
0130 
0131 void GPG::reset()
0132 {
0133     gpgSamples = 0;
0134     gpgSkippedSamples = 0;
0135     gpg->reset();
0136     qCDebug(KSTARS_EKOS_GUIDE) << "Resetting GPG";
0137 }
0138 
0139 void GPG::startDithering(double dx, double dy, const Calibration &cal)
0140 {
0141     // convert the x and y offsets to RA and DEC offsets.
0142     double raPixels, decPixels;
0143     cal.rotateToRaDec(dx, dy, &raPixels, &decPixels);
0144 
0145     // The 1000 in the denominator below converts gear-milliseconds into gear-seconds,
0146     // which is the unit GPG wants. Since calibration uses arcseconds, we convert using
0147     // arcseconds/pixel, though that's inexact when the pixels aren't square.
0148     // amount = (pixels * ms/pixel) / 1000;
0149     const double amount = raPixels * cal.raPulseMillisecondsPerArcsecond() * cal.xArcsecondsPerPixel() / 1000.0;
0150 
0151     qCDebug(KSTARS_EKOS_GUIDE) << "GPG Dither started. Gear-seconds =" << amount << "x,y was: " << dx << dy;
0152     gpg->GuidingDithered(amount, 1.0);
0153 }
0154 
0155 void GPG::ditheringSettled(bool success)
0156 {
0157     gpg->GuidingDitherSettleDone(success);
0158     if (success)
0159         qCDebug(KSTARS_EKOS_GUIDE) << "GPG Dither done (success)";
0160     else
0161         qCDebug(KSTARS_EKOS_GUIDE) << "GPG Dither done (failed)";
0162 }
0163 
0164 void GPG::suspended(const GuiderUtils::Vector &guideStarPosition,
0165                     const GuiderUtils::Vector &reticlePosition,
0166                     GuideStars *guideStars,
0167                     const Calibration &cal)
0168 {
0169     constexpr int MaxGpgSamplesForReset = 25;
0170     // We just reset the gpg if there's not enough samples to make
0171     // it worth our while to keep it going. We're probably worse off
0172     // to start it and then feed it bad samples.
0173     if (gpgSamples < MaxGpgSamplesForReset)
0174     {
0175         reset();
0176         return;
0177     }
0178 
0179     const GuiderUtils::Vector arc_star = cal.convertToArcseconds(guideStarPosition);
0180     const GuiderUtils::Vector arc_reticle = cal.convertToArcseconds(reticlePosition);
0181     const GuiderUtils::Vector star_drift =  cal.rotateToRaDec(arc_star - arc_reticle);
0182 
0183     double gpgInput = star_drift.x;
0184     if (guideStars != nullptr)
0185     {
0186         double multiStarRADrift, multiStarDECDrift;
0187         if (guideStars->getDrift(sqrt(star_drift.x * star_drift.x + star_drift.y * star_drift.y),
0188                                  reticlePosition.x, reticlePosition.y,
0189                                  &multiStarRADrift, &multiStarDECDrift))
0190             gpgInput = multiStarRADrift;
0191     }
0192 
0193     // Temporarily set the control & prediction gains to 0,
0194     // since we won't be guiding while suspended.
0195     double predictionGain = gpg->GetPredictionGain();
0196     gpg->SetPredictionGain(0.0);
0197     double controlGain = gpg->GetControlGain();
0198     gpg->SetControlGain(0.0);
0199 
0200     QElapsedTimer gpgTimer;
0201     gpgTimer.restart();
0202     const double gpgResult = gpg->result(gpgInput, getSNR(guideStars, gpgInput), Options::guideExposure());
0203     // Store the updated period length.
0204     std::vector<double> gpgParams = gpg->GetGPHyperparameters();
0205     Options::setGPGPeriod(gpgParams[PKPeriodLength]);
0206     // Not incrementing gpgSamples here, want real samples for that.
0207     const double gpgTime = gpgTimer.elapsed();
0208     qCDebug(KSTARS_EKOS_GUIDE) << QString("GPG(suspended): elapsed %1s. RA in %2, result: %3")
0209                                .arg(gpgTime / 1000.0)
0210                                .arg(gpgInput)
0211                                .arg(gpgResult);
0212     // Reset the gains that were zero'd earlier.
0213     gpg->SetPredictionGain(predictionGain);
0214     gpg->SetControlGain(controlGain);
0215 }
0216 
0217 bool GPG::computePulse(double raArcsecError, GuideStars *guideStars,
0218                        int *pulseLength, GuideDirection *pulseDir,
0219                        const Calibration &cal, Seconds timeStep)
0220 {
0221     if (!Options::gPGEnabled())
0222         return false;
0223 
0224     if (fabs(raArcsecError) > MAX_ARCSEC_ERROR)
0225     {
0226         if (++gpgSkippedSamples > MAX_SKIPPED_SAMPLES)
0227         {
0228             qCDebug(KSTARS_EKOS_GUIDE) << "Resetting GPG because RA error = "
0229                                        << raArcsecError;
0230             reset();
0231         }
0232         else
0233             qCDebug(KSTARS_EKOS_GUIDE) << "Skipping GPG because RA error = "
0234                                        << raArcsecError;
0235         return false;
0236     }
0237     gpgSkippedSamples = 0;
0238 
0239     // I want to start off the gpg on the right foot.
0240     // If the initial guiding is messed up by an early focus,
0241     // wait until RA has settled down.
0242     if (gpgSamples == 0 && fabs(raArcsecError) > 1)
0243     {
0244         qCDebug(KSTARS_EKOS_GUIDE) << "Delaying GPG startup. RA error = "
0245                                    << raArcsecError;
0246         reset();
0247         return false;
0248     }
0249 
0250     // GPG uses proportional gain and min-move from standard controls. Make sure they're using up-to-date values.
0251     gpg->SetControlGain(Options::rAProportionalGain());
0252     gpg->SetMinMove(Options::rAMinimumPulseArcSec());
0253 
0254     // GPG input is in RA arcseconds.
0255     QElapsedTimer gpgTimer;
0256     gpgTimer.restart();
0257 
0258     // Cast back to a raw double
0259     auto const rawTime = timeStep.count();
0260 
0261     const double gpgResult = gpg->result(raArcsecError, getSNR(guideStars, raArcsecError), rawTime);
0262     const double gpgTime = gpgTimer.elapsed();
0263     gpgSamples++;
0264 
0265     // GPG output is in RA arcseconds. pulseLength and pulseDir are set by convertCorrectionToPulseMilliseconds.
0266     const double gpgPulse = convertCorrectionToPulseMilliseconds(cal, pulseLength, pulseDir, gpgResult);
0267 
0268     qCDebug(KSTARS_EKOS_GUIDE)
0269             << QString("GPG: elapsed %1s. RA in %2, result: %3 * %4 --> %5 : %6ms %7")
0270             .arg(gpgTime / 1000.0)
0271             .arg(raArcsecError, 0, 'f', 2)
0272             .arg(gpgResult, 0, 'f', 2)
0273             .arg(cal.raPulseMillisecondsPerArcsecond(), 0, 'f', 1)
0274             .arg(gpgPulse, 0, 'f', 1)
0275             .arg(*pulseLength)
0276             .arg(directionStr(*pulseDir));
0277     if (Options::gPGEstimatePeriod())
0278     {
0279         double period_length = gpg->GetGPHyperparameters()[PKPeriodLength];
0280         Options::setGPGPeriod(period_length);
0281     }
0282     return true;
0283 }
0284 
0285 double GPG::convertCorrectionToPulseMilliseconds(const Calibration &cal, int *pulseLength,
0286         GuideDirection *pulseDir, const double gpgResult)
0287 {
0288     const double gpgPulse = gpgResult * cal.raPulseMillisecondsPerArcsecond();
0289 
0290     *pulseDir = gpgPulse > 0 ? RA_DEC_DIR : RA_INC_DIR;
0291     *pulseLength = fabs(gpgPulse);
0292     return gpgPulse;
0293 }
0294 
0295 bool GPG::darkGuiding(int *pulseLength, GuideDirection *pulseDir, const Calibration &cal,
0296                       Seconds timeStep)
0297 {
0298     if (!Options::gPGEnabled() || !Options::gPGDarkGuiding())
0299     {
0300         qCDebug(KSTARS_EKOS_GUIDE) << "dark guiding isn't enabled!";
0301         return false;
0302     }
0303 
0304     // GPG uses proportional gain and min-move from standard controls. Make sure they're using up-to-date values.
0305     gpg->SetControlGain(Options::rAProportionalGain());
0306     gpg->SetMinMove(Options::rAMinimumPulseArcSec());
0307 
0308     QElapsedTimer gpgTimer;
0309     gpgTimer.restart();
0310     const double gpgResult = gpg->deduceResult(timeStep.count());
0311     const double gpgTime = gpgTimer.elapsed();
0312 
0313     // GPG output is in RA arcseconds.
0314     const double gpgPulse = convertCorrectionToPulseMilliseconds(cal, pulseLength, pulseDir, gpgResult);
0315 
0316     qCDebug(KSTARS_EKOS_GUIDE)
0317             << QString("GPG dark guiding: elapsed %1s. RA result: %2 * %3 --> %4 : %5ms %6")
0318             .arg(gpgTime / 1000.0)
0319             .arg(gpgResult)
0320             .arg(cal.raPulseMillisecondsPerArcsecond())
0321             .arg(gpgPulse)
0322             .arg(*pulseLength)
0323             .arg(directionStr(*pulseDir));
0324     return true;
0325 }