Kstars

internalguider.cpp
1 /*
2  SPDX-FileCopyrightText: 2016 Jasem Mutlaq <[email protected]>.
3 
4  Based on lin_guider
5 
6  SPDX-License-Identifier: GPL-2.0-or-later
7 */
8 
9 #include "internalguider.h"
10 
11 #include "ekos_guide_debug.h"
12 #include "gmath.h"
13 #include "Options.h"
14 #include "auxiliary/kspaths.h"
15 #include "fitsviewer/fitsdata.h"
16 #include "fitsviewer/fitsview.h"
17 #include "guidealgorithms.h"
18 #include "ksnotification.h"
19 #include "ekos/auxiliary/stellarsolverprofileeditor.h"
20 
21 #include <KMessageBox>
22 
23 #include <random>
24 #include <chrono>
25 #include <QTimer>
26 #include <QString>
27 
28 #define MAX_GUIDE_STARS 10
29 
30 using namespace std::chrono_literals;
31 
32 namespace Ekos
33 {
34 InternalGuider::InternalGuider()
35 {
36  // Create math object
37  pmath.reset(new cgmath());
38  connect(pmath.get(), &cgmath::newStarPosition, this, &InternalGuider::newStarPosition);
39  connect(pmath.get(), &cgmath::guideStats, this, &InternalGuider::guideStats);
40 
41  // Do this so that stored calibration will be visible on the
42  // guide options menu. Calibration will get restored again when needed.
43  pmath->getMutableCalibration()->restore(
44  pierSide, Options::reverseDecOnPierSideChange(), subBinX, subBinY, nullptr);
45 
46  state = GUIDE_IDLE;
47  m_DitherOrigin = QVector3D(0, 0, 0);
48 
49  emit guideInfo("");
50 
51  m_darkGuideTimer = std::make_unique<QTimer>(this);
52  m_captureTimer = std::make_unique<QTimer>(this);
53 
54  setDarkGuideTimerInterval();
55 
56  setExposureTime();
57 
58  connect(this, &Ekos::GuideInterface::frameCaptureRequested, this, [ = ]()
59  {
60  this->m_captureTimer->start();
61  });
62 }
63 
64 void InternalGuider::setExposureTime()
65 {
66  Seconds seconds(Options::guideExposure());
67  setTimer(m_captureTimer, seconds);
68 }
69 
70 void InternalGuider::setTimer(std::unique_ptr<QTimer> &timer, Seconds seconds)
71 {
72  const std::chrono::duration<double, std::milli> inMilliseconds(seconds);
73  timer->setInterval((int)(inMilliseconds.count()));
74 }
75 
76 void InternalGuider::setDarkGuideTimerInterval()
77 {
78  constexpr double kMinInterval = 0.5; // 0.5s is the shortest allowed dark-guiding period.
79  const Seconds seconds(std::max(kMinInterval, Options::gPGDarkGuidingInterval()));
80  setTimer(m_darkGuideTimer, seconds);
81 }
82 
83 void InternalGuider::resetDarkGuiding()
84 {
85  m_darkGuideTimer->stop();
86  m_captureTimer->stop();
87 }
88 
89 bool InternalGuider::isInferencePeriodFinished()
90 {
91  auto const contribution = pmath->getGPG().predictionContribution();
92  return contribution >= 0.99;
93 }
94 bool InternalGuider::guide()
95 {
96  if (state >= GUIDE_GUIDING)
97  {
98  return processGuiding();
99  }
100 
101  if (state == GUIDE_SUSPENDED)
102  {
103  return true;
104  }
105  m_GuideFrame->disconnect(this);
106 
107  pmath->start();
108  emit guideInfo("");
109 
110  m_starLostCounter = 0;
111  m_highRMSCounter = 0;
112  m_DitherOrigin = QVector3D(0, 0, 0);
113 
114  m_isFirstFrame = true;
115 
116  if (state == GUIDE_IDLE)
117  {
118  if (Options::saveGuideLog())
119  guideLog.enable();
120  GuideLog::GuideInfo info;
121  fillGuideInfo(&info);
122  guideLog.startGuiding(info);
123  }
124  state = GUIDE_GUIDING;
125 
126  emit newStatus(state);
127 
128  emit frameCaptureRequested();
129 
130  startDarkGuiding();
131 
132  return true;
133 }
134 
135 /**
136  * @brief InternalGuider::abort Abort all internal guider operations.
137  * This includes calibration, dithering, guiding, capturing, and reaquiring.
138  * The state is set to IDLE or ABORTED depending on the current state since
139  * ABORTED can lead to different behavior by external actors than IDLE
140  * @return True if abort succeeds, false otherwise.
141  */
142 bool InternalGuider::abort()
143 {
144  // calibrationStage = CAL_IDLE; remove totally when understand trackingStarSelected
145 
146  logFile.close();
147  guideLog.endGuiding();
148  emit guideInfo("");
149 
150  if (state == GUIDE_CALIBRATING ||
151  state == GUIDE_GUIDING ||
152  state == GUIDE_DITHERING ||
153  state == GUIDE_MANUAL_DITHERING ||
154  state == GUIDE_REACQUIRE)
155  {
156  if (state == GUIDE_DITHERING || state == GUIDE_MANUAL_DITHERING)
157  emit newStatus(GUIDE_DITHERING_ERROR);
158  emit newStatus(GUIDE_ABORTED);
159 
160  qCDebug(KSTARS_EKOS_GUIDE) << "Aborting" << getGuideStatusString(state);
161  }
162  else
163  {
164  emit newStatus(GUIDE_IDLE);
165  qCDebug(KSTARS_EKOS_GUIDE) << "Stopping internal guider.";
166  }
167 
168  resetDarkGuiding();
169  disconnect(m_darkGuideTimer.get(), nullptr, nullptr, nullptr);
170 
171  pmath->abort();
172 
173 
174 
175  m_ProgressiveDither.clear();
176  m_starLostCounter = 0;
177  m_highRMSCounter = 0;
178 
179  m_DitherOrigin = QVector3D(0, 0, 0);
180 
181  pmath->suspend(false);
182  state = GUIDE_IDLE;
183  qCDebug(KSTARS_EKOS_GUIDE) << "Guiding aborted.";
184 
185  return true;
186 }
187 
188 bool InternalGuider::suspend()
189 {
190  guideLog.pauseInfo();
191  state = GUIDE_SUSPENDED;
192 
193  resetDarkGuiding();
194  emit newStatus(state);
195 
196  pmath->suspend(true);
197  emit guideInfo("");
198 
199  return true;
200 }
201 
202 void InternalGuider::startDarkGuiding()
203 {
204  if (Options::gPGDarkGuiding())
205  {
206  connect(m_darkGuideTimer.get(), &QTimer::timeout, this, &InternalGuider::darkGuide, Qt::UniqueConnection);
207 
208  // Start the two dark guide timers. The capture timer is started automatically by a signal.
209  m_darkGuideTimer->start();
210 
211  qCDebug(KSTARS_EKOS_GUIDE) << "Starting dark guiding.";
212  }
213 }
214 
215 bool InternalGuider::resume()
216 {
217  qCDebug(KSTARS_EKOS_GUIDE) << "Resuming...";
218  emit guideInfo("");
219  guideLog.resumeInfo();
220  state = GUIDE_GUIDING;
221  emit newStatus(state);
222 
223  pmath->suspend(false);
224 
225  startDarkGuiding();
226 
227  setExposureTime();
228 
229  emit frameCaptureRequested();
230 
231  return true;
232 }
233 
234 bool InternalGuider::ditherXY(double x, double y)
235 {
236  m_ProgressiveDither.clear();
237  m_DitherRetries = 0;
238  double cur_x, cur_y;
239  pmath->getTargetPosition(&cur_x, &cur_y);
240 
241  // Find out how many "jumps" we need to perform in order to get to target.
242  // The current limit is now 1/4 of the box size to make sure the star stays within detection
243  // threashold inside the window.
244  double oneJump = (guideBoxSize / 4.0);
245  double targetX = cur_x, targetY = cur_y;
246  int xSign = (x >= cur_x) ? 1 : -1;
247  int ySign = (y >= cur_y) ? 1 : -1;
248 
249  do
250  {
251  if (fabs(targetX - x) > oneJump)
252  targetX += oneJump * xSign;
253  else if (fabs(targetX - x) < oneJump)
254  targetX = x;
255 
256  if (fabs(targetY - y) > oneJump)
257  targetY += oneJump * ySign;
258  else if (fabs(targetY - y) < oneJump)
259  targetY = y;
260 
261  m_ProgressiveDither.enqueue(GuiderUtils::Vector(targetX, targetY, -1));
262 
263  }
264  while (targetX != x || targetY != y);
265 
266  m_DitherTargetPosition = m_ProgressiveDither.dequeue();
267  pmath->setTargetPosition(m_DitherTargetPosition.x, m_DitherTargetPosition.y);
268  guideLog.ditherInfo(x, y, m_DitherTargetPosition.x, m_DitherTargetPosition.y);
269 
270  state = GUIDE_MANUAL_DITHERING;
271  emit newStatus(state);
272 
273  processGuiding();
274 
275  return true;
276 }
277 
278 bool InternalGuider::dither(double pixels)
279 {
280  double ret_x, ret_y;
281  pmath->getTargetPosition(&ret_x, &ret_y);
282 
283  // Just calling getStarScreenPosition() will get the position at the last time the guide star
284  // was found, which is likely before the most recent guide pulse.
285  // Instead we call findLocalStarPosition() which does the analysis from the image.
286  // Unfortunately, processGuiding() will repeat that computation.
287  // We currently don't cache it.
288  GuiderUtils::Vector star_position = pmath->findLocalStarPosition(m_ImageData, m_GuideFrame, false);
289  if (pmath->isStarLost() || (star_position.x == -1) || (star_position.y == -1))
290  {
291  // If the star position is lost, just lose this iteration.
292  // If it happens too many time, abort.
293  if (++m_starLostCounter > MAX_LOST_STAR_THRESHOLD)
294  {
295  qCDebug(KSTARS_EKOS_GUIDE) << "Too many consecutive lost stars." << m_starLostCounter << "Aborting dither.";
296  return abortDither();
297  }
298  qCDebug(KSTARS_EKOS_GUIDE) << "Dither lost star. Trying again.";
299  emit frameCaptureRequested();
300  return true;
301  }
302  else
303  m_starLostCounter = 0;
304 
305  if (state != GUIDE_DITHERING)
306  {
307  m_DitherRetries = 0;
308 
309  auto seed = std::chrono::system_clock::now().time_since_epoch().count();
310  std::default_random_engine generator(seed);
311  std::uniform_real_distribution<double> angleMagnitude(0, 360);
312 
313  double angle = angleMagnitude(generator) * dms::DegToRad;
314  double diff_x = pixels * cos(angle);
315  double diff_y = pixels * sin(angle);
316 
317  if (pmath->getCalibration().declinationSwapEnabled())
318  diff_y *= -1;
319 
320  if (m_DitherOrigin.x() == 0 && m_DitherOrigin.y() == 0)
321  {
322  m_DitherOrigin = QVector3D(ret_x, ret_y, 0);
323  }
324  double totalXOffset = ret_x - m_DitherOrigin.x();
325  double totalYOffset = ret_y - m_DitherOrigin.y();
326 
327  // if we've dithered too far, and diff_x or diff_y is pushing us even further away, then change its direction.
328  // Note: it is possible that we've dithered too far, but diff_x/y is pointing in the right direction.
329  // Don't change it in that 2nd case.
330  if (((diff_x + totalXOffset > MAX_DITHER_TRAVEL) && (diff_x > 0)) ||
331  ((diff_x + totalXOffset < -MAX_DITHER_TRAVEL) && (diff_x < 0)))
332  {
333  qCDebug(KSTARS_EKOS_GUIDE)
334  << QString("Dithering target off by too much in X (abs(%1 + %2) > %3), adjust diff_x from %4 to %5")
335  .arg(diff_x).arg(totalXOffset).arg(MAX_DITHER_TRAVEL).arg(diff_x).arg(diff_x * -1.5);
336  diff_x *= -1.5;
337  }
338  if (((diff_y + totalYOffset > MAX_DITHER_TRAVEL) && (diff_y > 0)) ||
339  ((diff_y + totalYOffset < -MAX_DITHER_TRAVEL) && (diff_y < 0)))
340  {
341  qCDebug(KSTARS_EKOS_GUIDE)
342  << QString("Dithering target off by too much in Y (abs(%1 + %2) > %3), adjust diff_y from %4 to %5")
343  .arg(diff_y).arg(totalYOffset).arg(MAX_DITHER_TRAVEL).arg(diff_y).arg(diff_y * -1.5);
344  diff_y *= -1.5;
345  }
346 
347  m_DitherTargetPosition = GuiderUtils::Vector(ret_x, ret_y, 0) + GuiderUtils::Vector(diff_x, diff_y, 0);
348 
349  qCDebug(KSTARS_EKOS_GUIDE)
350  << QString("Dithering by %1 pixels. Target: %2,%3 Current: %4,%5 Move: %6,%7 Wander: %8,%9")
351  .arg(pixels, 3, 'f', 1)
352  .arg(m_DitherTargetPosition.x, 5, 'f', 1).arg(m_DitherTargetPosition.y, 5, 'f', 1)
353  .arg(ret_x, 5, 'f', 1).arg(ret_y, 5, 'f', 1)
354  .arg(diff_x, 4, 'f', 1).arg(diff_y, 4, 'f', 1)
355  .arg(totalXOffset + diff_x, 5, 'f', 1).arg(totalYOffset + diff_y, 5, 'f', 1);
356  guideLog.ditherInfo(diff_x, diff_y, m_DitherTargetPosition.x, m_DitherTargetPosition.y);
357 
358  pmath->setTargetPosition(m_DitherTargetPosition.x, m_DitherTargetPosition.y);
359 
360  if (Options::gPGEnabled())
361  // This is the offset in image coordinates, but needs to be converted to RA.
362  pmath->getGPG().startDithering(diff_x, diff_y, pmath->getCalibration());
363 
364  state = GUIDE_DITHERING;
365  emit newStatus(state);
366 
367  processGuiding();
368 
369  return true;
370  }
371 
372  // These will be the RA & DEC drifts of the current star position from the reticle position in pixels.
373  double driftRA, driftDEC;
374  pmath->getCalibration().computeDrift(
375  star_position,
376  GuiderUtils::Vector(m_DitherTargetPosition.x, m_DitherTargetPosition.y, 0),
377  &driftRA, &driftDEC);
378 
379  double pixelOffsetX = m_DitherTargetPosition.x - star_position.x;
380  double pixelOffsetY = m_DitherTargetPosition.y - star_position.y;
381 
382  qCDebug(KSTARS_EKOS_GUIDE)
383  << QString("Dithering in progress. Current: %1,%2 Target: %3,%4 Diff: %5,%6 Wander: %8,%9")
384  .arg(star_position.x, 5, 'f', 1).arg(star_position.y, 5, 'f', 1)
385  .arg(m_DitherTargetPosition.x, 5, 'f', 1).arg(m_DitherTargetPosition.y, 5, 'f', 1)
386  .arg(pixelOffsetX, 4, 'f', 1).arg(pixelOffsetY, 4, 'f', 1)
387  .arg(star_position.x - m_DitherOrigin.x(), 5, 'f', 1)
388  .arg(star_position.y - m_DitherOrigin.y(), 5, 'f', 1);
389 
390  if (Options::ditherWithOnePulse() || (fabs(driftRA) < 1 && fabs(driftDEC) < 1))
391  {
392  pmath->setTargetPosition(star_position.x, star_position.y);
393 
394  // In one-pulse dithering we want the target to be whereever we end up
395  // after the pulse. So, the first guide frame should not send any pulses
396  // and should reset the reticle to the position it finds.
397  if (Options::ditherWithOnePulse())
398  m_isFirstFrame = true;
399 
400  qCDebug(KSTARS_EKOS_GUIDE) << "Dither complete.";
401 
402  if (Options::ditherSettle() > 0)
403  {
404  state = GUIDE_DITHERING_SETTLE;
405  guideLog.settleStartedInfo();
406  emit newStatus(state);
407  }
408 
409  if (Options::gPGEnabled())
410  pmath->getGPG().ditheringSettled(true);
411 
412  QTimer::singleShot(Options::ditherSettle() * 1000, this, SLOT(setDitherSettled()));
413  }
414  else
415  {
416  if (++m_DitherRetries > Options::ditherMaxIterations())
417  return abortDither();
418 
419  processGuiding();
420  }
421 
422  return true;
423 }
424 
425 bool InternalGuider::abortDither()
426 {
427  if (Options::ditherFailAbortsAutoGuide())
428  {
429  emit newStatus(Ekos::GUIDE_DITHERING_ERROR);
430  abort();
431  return false;
432  }
433  else
434  {
435  emit newLog(i18n("Warning: Dithering failed. Autoguiding shall continue as set in the options in case "
436  "of dither failure."));
437 
438  if (Options::ditherSettle() > 0)
439  {
440  state = GUIDE_DITHERING_SETTLE;
441  guideLog.settleStartedInfo();
442  emit newStatus(state);
443  }
444 
445  if (Options::gPGEnabled())
446  pmath->getGPG().ditheringSettled(false);
447 
448  QTimer::singleShot(Options::ditherSettle() * 1000, this, SLOT(setDitherSettled()));
449  return true;
450  }
451 }
452 
453 bool InternalGuider::processManualDithering()
454 {
455  double cur_x, cur_y;
456  pmath->getTargetPosition(&cur_x, &cur_y);
457  pmath->getStarScreenPosition(&cur_x, &cur_y);
458 
459  // These will be the RA & DEC drifts of the current star position from the reticle position in pixels.
460  double driftRA, driftDEC;
461  pmath->getCalibration().computeDrift(
462  GuiderUtils::Vector(cur_x, cur_y, 0),
463  GuiderUtils::Vector(m_DitherTargetPosition.x, m_DitherTargetPosition.y, 0),
464  &driftRA, &driftDEC);
465 
466  qCDebug(KSTARS_EKOS_GUIDE) << "Manual Dithering in progress. Diff star X:" << driftRA << "Y:" << driftDEC;
467 
468  if (fabs(driftRA) < guideBoxSize / 5.0 && fabs(driftDEC) < guideBoxSize / 5.0)
469  {
470  if (m_ProgressiveDither.empty() == false)
471  {
472  m_DitherTargetPosition = m_ProgressiveDither.dequeue();
473  pmath->setTargetPosition(m_DitherTargetPosition.x, m_DitherTargetPosition.y);
474  qCDebug(KSTARS_EKOS_GUIDE) << "Next Dither Jump X:" << m_DitherTargetPosition.x << "Jump Y:" << m_DitherTargetPosition.y;
475  m_DitherRetries = 0;
476 
477  processGuiding();
478 
479  return true;
480  }
481 
482  if (fabs(driftRA) < 1 && fabs(driftDEC) < 1)
483  {
484  pmath->setTargetPosition(cur_x, cur_y);
485  qCDebug(KSTARS_EKOS_GUIDE) << "Manual Dither complete.";
486 
487  if (Options::ditherSettle() > 0)
488  {
489  state = GUIDE_DITHERING_SETTLE;
490  guideLog.settleStartedInfo();
491  emit newStatus(state);
492  }
493 
494  QTimer::singleShot(Options::ditherSettle() * 1000, this, SLOT(setDitherSettled()));
495  }
496  else
497  {
498  processGuiding();
499  }
500  }
501  else
502  {
503  if (++m_DitherRetries > Options::ditherMaxIterations())
504  {
505  emit newLog(i18n("Warning: Manual Dithering failed."));
506 
507  if (Options::ditherSettle() > 0)
508  {
509  state = GUIDE_DITHERING_SETTLE;
510  guideLog.settleStartedInfo();
511  emit newStatus(state);
512  }
513 
514  QTimer::singleShot(Options::ditherSettle() * 1000, this, SLOT(setDitherSettled()));
515  return true;
516  }
517 
518  processGuiding();
519  }
520 
521  return true;
522 }
523 
524 void InternalGuider::setDitherSettled()
525 {
526  guideLog.settleCompletedInfo();
527  emit newStatus(Ekos::GUIDE_DITHERING_SUCCESS);
528 
529  // Back to guiding
530  state = GUIDE_GUIDING;
531 }
532 
533 bool InternalGuider::calibrate()
534 {
535  bool ccdInfo = true, scopeInfo = true;
536  QString errMsg;
537 
538  if (subW == 0 || subH == 0)
539  {
540  errMsg = "CCD";
541  ccdInfo = false;
542  }
543 
544  if (mountAperture == 0.0 || mountFocalLength == 0.0)
545  {
546  scopeInfo = false;
547  if (ccdInfo == false)
548  errMsg += " & Telescope";
549  else
550  errMsg += "Telescope";
551  }
552 
553  if (ccdInfo == false || scopeInfo == false)
554  {
555  KSNotification::error(i18n("%1 info are missing. Please set the values in INDI Control Panel.", errMsg),
556  i18n("Missing Information"));
557  return false;
558  }
559 
560  if (state != GUIDE_CALIBRATING)
561  {
562  pmath->getTargetPosition(&calibrationStartX, &calibrationStartY);
563  calibrationProcess.reset(
564  new CalibrationProcess(calibrationStartX, calibrationStartY,
565  !Options::twoAxisEnabled()));
566  state = GUIDE_CALIBRATING;
567  emit newStatus(GUIDE_CALIBRATING);
568  }
569 
570  if (calibrationProcess->inProgress())
571  {
572  iterateCalibration();
573  return true;
574  }
575 
576  if (restoreCalibration())
577  {
578  calibrationProcess.reset();
579  emit newStatus(Ekos::GUIDE_CALIBRATION_SUCCESS);
580  KSNotification::event(QLatin1String("CalibrationRestored"),
581  i18n("Guiding calibration restored"), KSNotification::Guide);
582  reset();
583  return true;
584  }
585 
586  // Initialize the calibration parameters.
587  // CCD pixel values comes in in microns and we want mm.
588  pmath->getMutableCalibration()->setParameters(
589  ccdPixelSizeX / 1000.0, ccdPixelSizeY / 1000.0, mountFocalLength,
590  subBinX, subBinY, pierSide, mountRA, mountDEC);
591 
592  calibrationProcess->useCalibration(pmath->getMutableCalibration());
593 
594  m_GuideFrame->disconnect(this);
595 
596  // Must reset dec swap before we run any calibration procedure!
597  emit DESwapChanged(false);
598  pmath->setLostStar(false);
599 
600  if (Options::saveGuideLog())
601  guideLog.enable();
602  GuideLog::GuideInfo info;
603  fillGuideInfo(&info);
604  guideLog.startCalibration(info);
605 
606  calibrationProcess->startup();
607  calibrationProcess->setGuideLog(&guideLog);
608  iterateCalibration();
609 
610  return true;
611 }
612 
613 void InternalGuider::iterateCalibration()
614 {
615  if (calibrationProcess->inProgress())
616  {
617  auto const timeStep = calculateGPGTimeStep();
618  pmath->performProcessing(GUIDE_CALIBRATING, m_ImageData, m_GuideFrame, timeStep);
619 
620  QString info = "";
621  if (pmath->usingSEPMultiStar())
622  {
623  auto gs = pmath->getGuideStars();
624  info = QString("%1 stars, %2/%3 refs")
625  .arg(gs.getNumStarsDetected())
626  .arg(gs.getNumReferencesFound())
627  .arg(gs.getNumReferences());
628  }
629  emit guideInfo(info);
630 
631  if (pmath->isStarLost())
632  {
633  emit newLog(i18n("Lost track of the guide star. Try increasing the square size or reducing pulse duration."));
634  emit newStatus(Ekos::GUIDE_CALIBRATION_ERROR);
635  emit calibrationUpdate(GuideInterface::CALIBRATION_MESSAGE_ONLY,
636  i18n("Guide Star lost."));
637  reset();
638  return;
639  }
640  }
641  double starX, starY;
642  pmath->getStarScreenPosition(&starX, &starY);
643  calibrationProcess->iterate(starX, starY);
644 
645  auto status = calibrationProcess->getStatus();
646  if (status != GUIDE_CALIBRATING)
647  emit newStatus(status);
648 
649  QString logStatus = calibrationProcess->getLogStatus();
650  if (logStatus.length())
651  emit newLog(logStatus);
652 
653  QString updateMessage;
654  double x, y;
655  GuideInterface::CalibrationUpdateType type;
656  calibrationProcess->getCalibrationUpdate(&type, &updateMessage, &x, &y);
657  if (updateMessage.length())
658  emit calibrationUpdate(type, updateMessage, x, y);
659 
660  GuideDirection pulseDirection;
661  int pulseMsecs;
662  calibrationProcess->getPulse(&pulseDirection, &pulseMsecs);
663  if (pulseDirection != NO_DIR)
664  emit newSinglePulse(pulseDirection, pulseMsecs, StartCaptureAfterPulses);
665 
666  if (status == GUIDE_CALIBRATION_ERROR)
667  {
668  KSNotification::event(QLatin1String("CalibrationFailed"), i18n("Guiding calibration failed"),
669  KSNotification::Guide, KSNotification::Alert);
670  reset();
671  }
672  else if (status == GUIDE_CALIBRATION_SUCCESS)
673  {
674  KSNotification::event(QLatin1String("CalibrationSuccessful"),
675  i18n("Guiding calibration completed successfully"), KSNotification::Guide);
676  emit DESwapChanged(pmath->getCalibration().declinationSwapEnabled());
677  pmath->setTargetPosition(calibrationStartX, calibrationStartY);
678  reset();
679  }
680 }
681 
682 void InternalGuider::setGuideView(const QSharedPointer<GuideView> &guideView)
683 {
684  m_GuideFrame = guideView;
685 }
686 
687 void InternalGuider::setImageData(const QSharedPointer<FITSData> &data)
688 {
689  m_ImageData = data;
690  if (Options::saveGuideImages())
691  {
693  QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("guide/" +
694  now.toString("yyyy-MM-dd"));
695  QDir dir;
696  dir.mkpath(path);
697  // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-'
698  // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts
699  QString name = "guide_frame_" + now.toString("HH-mm-ss") + ".fits";
700  QString filename = path + QStringLiteral("/") + name + QStringLiteral("[compress R 100,100]");
701  m_ImageData->saveImage(filename);
702  }
703 }
704 
705 void InternalGuider::reset()
706 {
707  qCDebug(KSTARS_EKOS_GUIDE) << "Resetting internal guider...";
708  state = GUIDE_IDLE;
709 
710  resetDarkGuiding();
711 
712  connect(m_GuideFrame.get(), &FITSView::trackingStarSelected, this, &InternalGuider::trackingStarSelected,
714  calibrationProcess.reset();
715 }
716 
717 bool InternalGuider::clearCalibration()
718 {
719  Options::setSerializedCalibration("");
720  pmath->getMutableCalibration()->reset();
721  return true;
722 }
723 
724 bool InternalGuider::restoreCalibration()
725 {
726  bool success = Options::reuseGuideCalibration() &&
727  pmath->getMutableCalibration()->restore(
728  pierSide, Options::reverseDecOnPierSideChange(),
729  subBinX, subBinY, &mountDEC);
730  if (success)
731  emit DESwapChanged(pmath->getCalibration().declinationSwapEnabled());
732  return success;
733 }
734 
735 void InternalGuider::setStarPosition(QVector3D &starCenter)
736 {
737  pmath->setTargetPosition(starCenter.x(), starCenter.y());
738 }
739 
740 void InternalGuider::trackingStarSelected(int x, int y)
741 {
742  Q_UNUSED(x);
743  Q_UNUSED(y);
744  /*
745 
746  Not sure what's going on here--manual star selection for calibration?
747  Don't really see how the logic works.
748 
749  if (calibrationStage == CAL_IDLE)
750  return;
751 
752  pmath->setTargetPosition(x, y);
753 
754  calibrationStage = CAL_START;
755  */
756 }
757 
758 void InternalGuider::setDECSwap(bool enable)
759 {
760  pmath->getMutableCalibration()->setDeclinationSwapEnabled(enable);
761 }
762 
763 void InternalGuider::setSquareAlgorithm(int index)
764 {
765  if (index == SEP_MULTISTAR && !pmath->usingSEPMultiStar())
766  m_isFirstFrame = true;
767  pmath->setAlgorithmIndex(index);
768 }
769 
770 bool InternalGuider::getReticleParameters(double *x, double *y)
771 {
772  return pmath->getTargetPosition(x, y);
773 }
774 
775 bool InternalGuider::setGuiderParams(double ccdPixelSizeX, double ccdPixelSizeY, double mountAperture,
776  double mountFocalLength)
777 {
778  this->ccdPixelSizeX = ccdPixelSizeX;
779  this->ccdPixelSizeY = ccdPixelSizeY;
780  this->mountAperture = mountAperture;
781  this->mountFocalLength = mountFocalLength;
782  return pmath->setGuiderParameters(ccdPixelSizeX, ccdPixelSizeY, mountAperture, mountFocalLength);
783 }
784 
785 bool InternalGuider::setFrameParams(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t binX, uint16_t binY)
786 {
787  if (w <= 0 || h <= 0)
788  return false;
789 
790  subX = x;
791  subY = y;
792  subW = w;
793  subH = h;
794 
795  subBinX = binX;
796  subBinY = binY;
797 
798  pmath->setVideoParameters(w, h, subBinX, subBinY);
799 
800  return true;
801 }
802 
803 void InternalGuider::emitAxisPulse(const cproc_out_params *out)
804 {
805  double raPulse = out->pulse_length[GUIDE_RA];
806  double dePulse = out->pulse_length[GUIDE_DEC];
807 
808  //If the pulse was not sent to the mount, it should have 0 value
809  if(out->pulse_dir[GUIDE_RA] == NO_DIR)
810  raPulse = 0;
811  //If the pulse was not sent to the mount, it should have 0 value
812  if(out->pulse_dir[GUIDE_DEC] == NO_DIR)
813  dePulse = 0;
814  //If the pulse was in the Negative direction, it should have a negative sign.
815  if(out->pulse_dir[GUIDE_RA] == RA_INC_DIR)
816  raPulse = -raPulse;
817  //If the pulse was in the Negative direction, it should have a negative sign.
818  if(out->pulse_dir[GUIDE_DEC] == DEC_INC_DIR)
819  dePulse = -dePulse;
820 
821  emit newAxisPulse(raPulse, dePulse);
822 }
823 
824 bool InternalGuider::processGuiding()
825 {
826  const cproc_out_params *out;
827 
828  // On first frame, center the box (reticle) around the star so we do not start with an offset the results in
829  // unnecessary guiding pulses.
830  bool process = true;
831 
832  if (m_isFirstFrame)
833  {
834  m_isFirstFrame = false;
835  if (state == GUIDE_GUIDING)
836  {
837  GuiderUtils::Vector star_pos = pmath->findLocalStarPosition(m_ImageData, m_GuideFrame, true);
838  if (star_pos.x != -1 && star_pos.y != -1)
839  pmath->setTargetPosition(star_pos.x, star_pos.y);
840  else
841  {
842  // We were not able to get started.
843  process = false;
844  m_isFirstFrame = true;
845  }
846  }
847  }
848 
849  if (process)
850  {
851  auto const timeStep = calculateGPGTimeStep();
852  pmath->performProcessing(state, m_ImageData, m_GuideFrame, timeStep, &guideLog);
853  if (pmath->usingSEPMultiStar())
854  {
855  QString info = "";
856  auto gs = pmath->getGuideStars();
857  info = QString("%1 stars, %2/%3 refs")
858  .arg(gs.getNumStarsDetected())
859  .arg(gs.getNumReferencesFound())
860  .arg(gs.getNumReferences());
861 
862  emit guideInfo(info);
863  }
864 
865  // Restart the dark-guiding timer, so we get the full interval on its 1st timeout.
866  if (this->m_darkGuideTimer->isActive())
867  this->m_darkGuideTimer->start();
868  }
869 
870  if (state == GUIDE_SUSPENDED)
871  {
872  if (Options::gPGEnabled())
873  emit frameCaptureRequested();
874  return true;
875  }
876  else
877  {
878  if (pmath->isStarLost())
879  m_starLostCounter++;
880  else
881  m_starLostCounter = 0;
882  }
883 
884  // do pulse
885  out = pmath->getOutputParameters();
886 
887  if (isPoorGuiding(out))
888  return true;
889 
890  bool sendPulses = !pmath->isStarLost();
891 
892 
893  // Send pulse if we have one active direction at least.
894  if (sendPulses && (out->pulse_dir[GUIDE_RA] != NO_DIR || out->pulse_dir[GUIDE_DEC] != NO_DIR))
895  {
896  emit newMultiPulse(out->pulse_dir[GUIDE_RA], out->pulse_length[GUIDE_RA],
897  out->pulse_dir[GUIDE_DEC], out->pulse_length[GUIDE_DEC], StartCaptureAfterPulses);
898  }
899  else
900  emit frameCaptureRequested();
901 
902  if (state == GUIDE_DITHERING || state == GUIDE_MANUAL_DITHERING)
903  return true;
904 
905  // Hy 9/13/21: Check above just looks for GUIDE_DITHERING or GUIDE_MANUAL_DITHERING
906  // but not the other dithering possibilities (error, success, settle).
907  // Not sure if they should be included above, so conservatively not changing the
908  // code, but don't think they should broadcast the newAxisDelta which might
909  // interrup a capture.
910  if (state < GUIDE_DITHERING)
911  emit newAxisDelta(out->delta[GUIDE_RA], out->delta[GUIDE_DEC]);
912 
913  emitAxisPulse(out);
914  emit newAxisSigma(out->sigma[GUIDE_RA], out->sigma[GUIDE_DEC]);
915  if (SEPMultiStarEnabled())
916  emit newSNR(pmath->getGuideStarSNR());
917 
918  return true;
919 }
920 
921 
922 // Here we calculate the time until the next time we will be emitting guiding corrections.
923 std::pair<Seconds, Seconds> InternalGuider::calculateGPGTimeStep()
924 {
925  Seconds timeStep;
926 
927  const Seconds guideDelay{(Options::guideDelay())};
928 
929  auto const captureInterval = Seconds(m_captureTimer->intervalAsDuration()) + guideDelay;
930  auto const darkGuideInterval = Seconds(m_darkGuideTimer->intervalAsDuration());
931 
932  if (!Options::gPGDarkGuiding() || !isInferencePeriodFinished())
933  {
934  return std::pair<Seconds, Seconds>(captureInterval, captureInterval);
935  }
936  auto const captureTimeRemaining = Seconds(m_captureTimer->remainingTimeAsDuration()) + guideDelay;
937  auto const darkGuideTimeRemaining = Seconds(m_darkGuideTimer->remainingTimeAsDuration());
938  // Are both firing at the same time (or at least, both due)?
939  if (captureTimeRemaining <= Seconds::zero()
940  && darkGuideTimeRemaining <= Seconds::zero())
941  {
942  timeStep = std::min(captureInterval, darkGuideInterval);
943  }
944  else if (captureTimeRemaining <= Seconds::zero())
945  {
946  timeStep = std::min(captureInterval, darkGuideTimeRemaining);
947  }
948  else if (darkGuideTimeRemaining <= Seconds::zero())
949  {
950  timeStep = std::min(captureTimeRemaining, darkGuideInterval);
951  }
952  else
953  {
954  timeStep = std::min(captureTimeRemaining, darkGuideTimeRemaining);
955  }
956  return std::pair<Seconds, Seconds>(timeStep, captureInterval);
957 }
958 
959 
960 
961 void InternalGuider::darkGuide()
962 {
963  // Only dark guide when guiding--e.g. don't dark guide if dithering.
964  if (state != GUIDE_GUIDING)
965  return;
966 
967  if(Options::gPGDarkGuiding() && isInferencePeriodFinished())
968  {
969  const cproc_out_params *out;
970  auto const timeStep = calculateGPGTimeStep();
971  pmath->performDarkGuiding(state, timeStep);
972 
973  out = pmath->getOutputParameters();
974  emit newSinglePulse(out->pulse_dir[GUIDE_RA], out->pulse_length[GUIDE_RA], DontCaptureAfterPulses);
975 
976  emitAxisPulse(out);
977  }
978 }
979 
980 bool InternalGuider::isPoorGuiding(const cproc_out_params* out)
981 {
982  double delta_rms = std::hypot(out->delta[GUIDE_RA], out->delta[GUIDE_DEC]);
983  if (delta_rms > Options::guideMaxDeltaRMS())
984  m_highRMSCounter++;
985  else
986  m_highRMSCounter = 0;
987 
988  uint8_t abortStarLostThreshold = (state == GUIDE_DITHERING
989  || state == GUIDE_MANUAL_DITHERING) ? MAX_LOST_STAR_THRESHOLD * 3 : MAX_LOST_STAR_THRESHOLD;
990  uint8_t abortRMSThreshold = (state == GUIDE_DITHERING
991  || state == GUIDE_MANUAL_DITHERING) ? MAX_RMS_THRESHOLD * 3 : MAX_RMS_THRESHOLD;
992  if (m_starLostCounter > abortStarLostThreshold || m_highRMSCounter > abortRMSThreshold)
993  {
994  qCDebug(KSTARS_EKOS_GUIDE) << "m_starLostCounter" << m_starLostCounter
995  << "m_highRMSCounter" << m_highRMSCounter
996  << "delta_rms" << delta_rms;
997 
998  if (m_starLostCounter > abortStarLostThreshold)
999  emit newLog(i18n("Lost track of the guide star. Searching for guide stars..."));
1000  else
1001  emit newLog(i18n("Delta RMS threshold value exceeded. Searching for guide stars..."));
1002 
1003  reacquireTimer.start();
1004  rememberState = state;
1005  state = GUIDE_REACQUIRE;
1006  emit newStatus(state);
1007  return true;
1008  }
1009  return false;
1010 }
1011 bool InternalGuider::selectAutoStarSEPMultistar()
1012 {
1013  m_GuideFrame->updateFrame();
1014  m_DitherOrigin = QVector3D(0, 0, 0);
1015  QVector3D newStarCenter = pmath->selectGuideStar(m_ImageData);
1016  if (newStarCenter.x() >= 0)
1017  {
1018  emit newStarPosition(newStarCenter, true);
1019  return true;
1020  }
1021  return false;
1022 }
1023 
1024 bool InternalGuider::SEPMultiStarEnabled()
1025 {
1026  return Options::guideAlgorithm() == SEP_MULTISTAR;
1027 }
1028 
1029 bool InternalGuider::selectAutoStar()
1030 {
1031  m_DitherOrigin = QVector3D(0, 0, 0);
1032  if (Options::guideAlgorithm() == SEP_MULTISTAR)
1033  return selectAutoStarSEPMultistar();
1034 
1035  bool useNativeDetection = false;
1036 
1037  QList<Edge *> starCenters;
1038 
1039  if (Options::guideAlgorithm() != SEP_THRESHOLD)
1040  starCenters = GuideAlgorithms::detectStars(m_ImageData, m_GuideFrame->getTrackingBox());
1041 
1042  if (starCenters.empty())
1043  {
1044  QVariantMap settings;
1045  settings["maxStarsCount"] = 50;
1046  settings["optionsProfileIndex"] = Options::guideOptionsProfile();
1047  settings["optionsProfileGroup"] = static_cast<int>(Ekos::GuideProfiles);
1048  m_ImageData->setSourceExtractorSettings(settings);
1049 
1050  if (Options::guideAlgorithm() == SEP_THRESHOLD)
1051  m_ImageData->findStars(ALGORITHM_SEP).waitForFinished();
1052  else
1053  m_ImageData->findStars().waitForFinished();
1054 
1055  starCenters = m_ImageData->getStarCenters();
1056  if (starCenters.empty())
1057  return false;
1058 
1059  useNativeDetection = true;
1060  // For SEP, prefer flux total
1061  if (Options::guideAlgorithm() == SEP_THRESHOLD)
1062  std::sort(starCenters.begin(), starCenters.end(), [](const Edge * a, const Edge * b)
1063  {
1064  return a->val > b->val;
1065  });
1066  else
1067  std::sort(starCenters.begin(), starCenters.end(), [](const Edge * a, const Edge * b)
1068  {
1069  return a->width > b->width;
1070  });
1071 
1072  m_GuideFrame->setStarsEnabled(true);
1073  m_GuideFrame->updateFrame();
1074  }
1075 
1076  int maxX = m_ImageData->width();
1077  int maxY = m_ImageData->height();
1078 
1079  int scores[MAX_GUIDE_STARS];
1080 
1081  int maxIndex = MAX_GUIDE_STARS < starCenters.count() ? MAX_GUIDE_STARS : starCenters.count();
1082 
1083  for (int i = 0; i < maxIndex; i++)
1084  {
1085  int score = 100;
1086 
1087  Edge *center = starCenters.at(i);
1088 
1089  if (useNativeDetection)
1090  {
1091  // Severely reject stars close to edges
1092  if (center->x < (center->width * 5) || center->y < (center->width * 5) ||
1093  center->x > (maxX - center->width * 5) || center->y > (maxY - center->width * 5))
1094  score -= 1000;
1095 
1096  // Reject stars bigger than square
1097  if (center->width > float(guideBoxSize) / subBinX)
1098  score -= 1000;
1099  else
1100  {
1101  if (Options::guideAlgorithm() == SEP_THRESHOLD)
1102  score += sqrt(center->val);
1103  else
1104  // Moderately favor brighter stars
1105  score += center->width * center->width;
1106  }
1107 
1108  // Moderately reject stars close to other stars
1109  foreach (Edge *edge, starCenters)
1110  {
1111  if (edge == center)
1112  continue;
1113 
1114  if (fabs(center->x - edge->x) < center->width * 2 && fabs(center->y - edge->y) < center->width * 2)
1115  {
1116  score -= 15;
1117  break;
1118  }
1119  }
1120  }
1121  else
1122  {
1123  score = center->val;
1124  }
1125 
1126  scores[i] = score;
1127  }
1128 
1129  int maxScore = -1;
1130  int maxScoreIndex = -1;
1131  for (int i = 0; i < maxIndex; i++)
1132  {
1133  if (scores[i] > maxScore)
1134  {
1135  maxScore = scores[i];
1136  maxScoreIndex = i;
1137  }
1138  }
1139 
1140  if (maxScoreIndex < 0)
1141  {
1142  qCDebug(KSTARS_EKOS_GUIDE) << "No suitable star detected.";
1143  return false;
1144  }
1145 
1146  QVector3D newStarCenter(starCenters[maxScoreIndex]->x, starCenters[maxScoreIndex]->y, 0);
1147 
1148  if (useNativeDetection == false)
1149  qDeleteAll(starCenters);
1150 
1151  emit newStarPosition(newStarCenter, true);
1152 
1153  return true;
1154 }
1155 
1156 bool InternalGuider::reacquire()
1157 {
1158  bool rc = selectAutoStar();
1159  if (rc)
1160  {
1161  m_highRMSCounter = m_starLostCounter = 0;
1162  m_isFirstFrame = true;
1163  pmath->reset();
1164  // If we were in the process of dithering, wait until settle and resume
1165  if (rememberState == GUIDE_DITHERING || state == GUIDE_MANUAL_DITHERING)
1166  {
1167  if (Options::ditherSettle() > 0)
1168  {
1169  state = GUIDE_DITHERING_SETTLE;
1170  guideLog.settleStartedInfo();
1171  emit newStatus(state);
1172  }
1173 
1174  QTimer::singleShot(Options::ditherSettle() * 1000, this, SLOT(setDitherSettled()));
1175  }
1176  else
1177  {
1178  state = GUIDE_GUIDING;
1179  emit newStatus(state);
1180  }
1181 
1182  }
1183  else if (reacquireTimer.elapsed() > static_cast<int>(Options::guideLostStarTimeout() * 1000))
1184  {
1185  emit newLog(i18n("Failed to find any suitable guide stars. Aborting..."));
1186  abort();
1187  return false;
1188  }
1189 
1190  emit frameCaptureRequested();
1191  return rc;
1192 }
1193 
1194 void InternalGuider::fillGuideInfo(GuideLog::GuideInfo *info)
1195 {
1196  // NOTE: just using the X values, phd2logview assumes x & y the same.
1197  // pixel scale in arc-sec / pixel. The 2nd and 3rd values seem redundent, but are
1198  // in the phd2 logs.
1199  info->pixelScale = (206.26481 * this->ccdPixelSizeX * this->subBinX) / this->mountFocalLength;
1200  info->binning = this->subBinX;
1201  info->focalLength = this->mountFocalLength;
1202  info->ra = this->mountRA.Degrees();
1203  info->dec = this->mountDEC.Degrees();
1204  info->azimuth = this->mountAzimuth.Degrees();
1205  info->altitude = this->mountAltitude.Degrees();
1206  info->pierSide = this->pierSide;
1207  info->xangle = pmath->getCalibration().getRAAngle();
1208  info->yangle = pmath->getCalibration().getDECAngle();
1209  // Calibration values in ms/pixel, xrate is in pixels/second.
1210  info->xrate = 1000.0 / pmath->getCalibration().raPulseMillisecondsPerArcsecond();
1211  info->yrate = 1000.0 / pmath->getCalibration().decPulseMillisecondsPerArcsecond();
1212 }
1213 
1214 void InternalGuider::updateGPGParameters()
1215 {
1216  setDarkGuideTimerInterval();
1217  pmath->getGPG().updateParameters();
1218 }
1219 
1220 void InternalGuider::resetGPG()
1221 {
1222  pmath->getGPG().reset();
1223  resetDarkGuiding();
1224 }
1225 
1226 const Calibration &InternalGuider::getCalibration() const
1227 {
1228  return pmath->getCalibration();
1229 }
1230 }
Type type(const QSqlDatabase &db)
Ekos is an advanced Astrophotography tool for Linux. It is based on a modular extensible framework to...
Definition: align.cpp:69
QDateTime currentDateTime()
int count(const T &value) const const
static constexpr double DegToRad
DegToRad is a const static member equal to the number of radians in one degree (dms::PI/180....
Definition: dms.h:385
bool empty() const const
QString i18n(const char *text, const TYPE &arg...)
void timeout()
int length() const const
const T & at(int i) const const
UniqueConnection
KIOFILEWIDGETS_EXPORT QString dir(const QString &fileClass)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString path(const QString &relativePath)
QString name(StandardShortcut id)
QString filePath(const QString &fileName) const const
KGuiItem reset()
QList::iterator begin()
float x() const const
float y() const const
QTextStream & center(QTextStream &stream)
QList::iterator end()
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Mon May 8 2023 03:57:31 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.