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(¶meters); 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(¶meters); 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 }