Kstars

focusadvisor.cpp
1/*
2 SPDX-FileCopyrightText: 2024 John Evans <john.e.evans.email@googlemail.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "focusadvisor.h"
8#include "focus.h"
9#include "focusalgorithms.h"
10#include "Options.h"
11#include "ekos/auxiliary/opticaltrainmanager.h"
12#include "ekos/auxiliary/opticaltrainsettings.h"
13#include "ekos/auxiliary/stellarsolverprofile.h"
14
15namespace Ekos
16{
17
18const char * FOCUSER_SIMULATOR = "Focuser Simulator";
19const int MAXIMUM_FOCUS_ADVISOR_ITERATIONS = 1001;
20const int NUM_JUMPS_PER_SECTOR = 10;
21const int FIND_STARS_MIN_STARS = 2;
22const double TARGET_MAXMIN_HFR_RATIO = 3.0;
23const double INITIAL_MAXMIN_HFR_RATIO = 2.0;
24const int NUM_STEPS_PRE_AF = 11;
25
26FocusAdvisor::FocusAdvisor(QWidget *parent) : QDialog(parent)
27{
28#ifdef Q_OS_OSX
29 setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
30#endif
31
32 setupUi(this);
33 m_focus = static_cast<Focus *>(parent);
34
35 processUI();
36 setupHelpTable();
37}
38
39FocusAdvisor::~FocusAdvisor()
40{
41}
42
43void FocusAdvisor::processUI()
44{
45 // Setup the help dialog
46 m_helpDialog = new QDialog(this);
47 m_helpUI.reset(new Ui::focusAdvisorHelpDialog());
48 m_helpUI->setupUi(m_helpDialog);
49#ifdef Q_OS_OSX
50 m_helpDialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
51#endif
52
53 m_runButton = focusAdvButtonBox->addButton("Run", QDialogButtonBox::ActionRole);
54 m_stopButton = focusAdvButtonBox->addButton("Stop", QDialogButtonBox::ActionRole);
55 m_helpButton = focusAdvButtonBox->addButton("Help", QDialogButtonBox::HelpRole);
56
57 // Set tooltips for the buttons
58 m_runButton->setToolTip("Run Focus Advisor");
59 m_stopButton->setToolTip("Stop Focus Advisor");
60 m_helpButton->setToolTip("List parameter settings");
61
62 // Connect up button callbacks
63 connect(m_runButton, &QPushButton::clicked, this, &FocusAdvisor::start);
64 connect(m_stopButton, &QPushButton::clicked, this, &FocusAdvisor::stop);
65 connect(m_helpButton, &QPushButton::clicked, this, &FocusAdvisor::help);
66
67 // Initialise buttons
68 setButtons(false);
69
70 // Setup the results table
71 setupResultsTable();
72}
73
74void FocusAdvisor::setupResultsTable()
75{
76 focusAdvTable->setColumnCount(RESULTS_MAX_COLS);
77 focusAdvTable->setRowCount(0);
78
79 QTableWidgetItem *itemSection = new QTableWidgetItem(i18n ("Section"));
80 itemSection->setToolTip(i18n("Section"));
81 focusAdvTable->setHorizontalHeaderItem(RESULTS_SECTION, itemSection);
82
83 QTableWidgetItem *itemRunNumber = new QTableWidgetItem(i18n ("Run"));
84 itemRunNumber->setToolTip(i18n("Run number"));
85 focusAdvTable->setHorizontalHeaderItem(RESULTS_RUN_NUMBER, itemRunNumber);
86
87 QTableWidgetItem *itemStartPosition = new QTableWidgetItem(i18n ("Start Pos"));
88 itemStartPosition->setToolTip(i18n("Start position"));
89 focusAdvTable->setHorizontalHeaderItem(RESULTS_START_POSITION, itemStartPosition);
90
91 QTableWidgetItem *itemStepSize = new QTableWidgetItem(i18n ("Step/Jump Size"));
92 itemStepSize->setToolTip(i18n("Step Size"));
93 focusAdvTable->setHorizontalHeaderItem(RESULTS_STEP_SIZE, itemStepSize);
94
95 QTableWidgetItem *itemAFOverscan = new QTableWidgetItem(i18n ("Overscan"));
96 itemAFOverscan->setToolTip(i18n("AF Overscan"));
97 focusAdvTable->setHorizontalHeaderItem(RESULTS_AFOVERSCAN, itemAFOverscan);
98
99 QTableWidgetItem *itemText = new QTableWidgetItem(i18n ("Comments"));
100 itemText->setToolTip(i18n("Additional Text"));
101 focusAdvTable->setHorizontalHeaderItem(RESULTS_TEXT, itemText);
102
103 focusAdvTable->horizontalHeader()->resizeSections(QHeaderView::ResizeToContents);
104 focusAdvTable->hide();
105 resizeDialog();
106}
107
108
109void FocusAdvisor::setupHelpTable()
110{
111 QTableWidgetItem *itemParameter = new QTableWidgetItem(i18n ("Parameter"));
112 itemParameter->setToolTip(i18n("Parameter Name"));
113 m_helpUI->table->setHorizontalHeaderItem(HELP_PARAMETER, itemParameter);
114
115 QTableWidgetItem *itemCurrentValue = new QTableWidgetItem(i18n ("Current Value"));
116 itemCurrentValue->setToolTip(i18n("Current value of the parameter"));
117 m_helpUI->table->setHorizontalHeaderItem(HELP_CURRENT_VALUE, itemCurrentValue);
118
119 QTableWidgetItem *itemProposedValue = new QTableWidgetItem(i18n ("Proposed Value"));
120 itemProposedValue->setToolTip(i18n("Focus Advisor proposed value for the parameter"));
121 m_helpUI->table->setHorizontalHeaderItem(HELP_NEW_VALUE, itemProposedValue);
122
123 connect(m_helpUI->focusAdvHelpOnlyChanges, static_cast<void (QCheckBox::*)(int)>(&QCheckBox::stateChanged), this, [this]()
124 {
125 setupParams("");
126 });
127}
128
129void FocusAdvisor::setButtons(const bool running)
130{
131 bool canRun = m_focus->m_Focuser && m_focus->m_Focuser->isConnected() && m_focus->m_Focuser->canAbsMove();
132 m_runButton->setEnabled(!running && canRun);
133 m_stopButton->setEnabled(running);
134 m_helpButton->setEnabled(!running);
135 focusAdvButtonBox->button(QDialogButtonBox::Close)->setEnabled(!running);
136}
137
138bool FocusAdvisor::canFocusAdvisorRun()
139{
140 // Focus Advisor can only be run if the following...
141 return m_focus->m_Focuser && m_focus->m_Focuser->isConnected() && m_focus->m_Focuser->canAbsMove() &&
142 m_focus->m_FocusAlgorithm == Focus::FOCUS_LINEAR1PASS &&
143 (m_focus->m_StarMeasure == Focus::FOCUS_STAR_HFR || m_focus->m_StarMeasure == Focus::FOCUS_STAR_HFR_ADJ
144 || m_focus->m_StarMeasure == Focus::FOCUS_STAR_FWHM) &&
145 (m_focus->m_CurveFit == CurveFitting::FOCUS_HYPERBOLA || m_focus->m_CurveFit == CurveFitting::FOCUS_PARABOLA);
146}
147
148bool FocusAdvisor::start()
149{
150 // Reset the results table
151 focusAdvTable->setRowCount(0);
152 focusAdvTable->sizePolicy();
153
154 if (!m_focus)
155 return false;
156
157 if (m_focus->inFocusLoop || m_focus->inAdjustFocus || m_focus->inAutoFocus || m_focus->inBuildOffsets
158 || m_focus->inScanStartPos || inFocusAdvisor())
159 {
160 m_focus->appendLogText(i18n("Focus Advisor: another focus action in progress. Please try again."));
161 return false;
162 }
163
164 if (focusAdvUpdateParams->isChecked())
165 {
166 updateParams();
167 focusAdvUpdateParamsLabel->setText(i18n("Done"));
168 emit newStage(UpdatingParameters);
169 }
170
171 m_inFindStars = focusAdvFindStars->isChecked();
172 m_inPreAFAdj = focusAdvCoarseAdj->isChecked();
173 m_inAFAdj = focusAdvFineAdj->isChecked();
174 if (m_inFindStars || m_inPreAFAdj || m_inAFAdj)
175 {
176 if (canFocusAdvisorRun())
177 {
178 // Deselect ScanStartPos to stop interference with Focus Advisor
179 m_initialScanStartPos = m_focus->m_OpsFocusProcess->focusScanStartPos->isChecked();
180 m_focus->m_OpsFocusProcess->focusScanStartPos->setChecked(false);
181 m_focus->runAutoFocus(FOCUS_FOCUS_ADVISOR, "");
182 }
183 else
184 {
185 m_focus->appendLogText(i18n("Focus Advisor cannot run with current params."));
186 return false;
187 }
188 }
189
190 return true;
191}
192
193void FocusAdvisor::stop()
194{
195 abort(i18n("Focus Advisor stopped"));
196 emit newStage(Idle);
197}
198
199// Focus Advisor help popup
200void FocusAdvisor::help()
201{
202 setupParams("");
203 m_helpDialog->show();
204 m_helpDialog->raise();
205}
206
207void FocusAdvisor::addSectionToHelpTable(int &row, const QString &section)
208{
209 if (++row >= m_helpUI->table->rowCount())
210 m_helpUI->table->setRowCount(row + 1);
212 item->setText(section);
213 QFont font = item->font();
214 font.setUnderline(true);
215 font.setPointSize(font.pointSize() + 2);
216 item->setFont(font);
217 m_helpUI->table->setItem(row, HELP_PARAMETER, item);
218}
219
220void FocusAdvisor::addParamToHelpTable(int &row, const QString &parameter, const QString &currentValue,
221 const QString &newValue)
222{
223 if (m_helpUI->focusAdvHelpOnlyChanges->isChecked() && newValue == currentValue)
224 return;
225
226 if (++row >= m_helpUI->table->rowCount())
227 m_helpUI->table->setRowCount(row + 1);
228 QTableWidgetItem *itemParameter = new QTableWidgetItem(parameter);
229 m_helpUI->table->setItem(row, HELP_PARAMETER, itemParameter);
230 QTableWidgetItem *itemCurrentValue = new QTableWidgetItem(currentValue);
231 m_helpUI->table->setItem(row, HELP_CURRENT_VALUE, itemCurrentValue);
232 QTableWidgetItem *itemNewValue = new QTableWidgetItem(newValue);
233 if (newValue != currentValue)
234 {
235 // Highlight changes
236 QFont font = itemNewValue->font();
237 font.setBold(true);
238 itemNewValue->setFont(font);
239 }
240 m_helpUI->table->setItem(row, HELP_NEW_VALUE, itemNewValue);
241}
242
243// Resize the help dialog to the data
244void FocusAdvisor::resizeHelpDialog()
245{
246 // Resize the columns to the data
247 m_helpUI->table->horizontalHeader()->resizeSections(QHeaderView::ResizeToContents);
248
249 // Resize the dialog to the width of the table widget + decoration
250 int left, right, top, bottom;
251 m_helpUI->verticalLayout->layout()->getContentsMargins(&left, &top, &right, &bottom);
252
253 const int width = m_helpUI->table->horizontalHeader()->length() +
254 m_helpUI->table->verticalHeader()->width() +
255 m_helpUI->table->verticalScrollBar()->width() +
256 m_helpUI->table->frameWidth() * 2 +
257 left + right +
258 m_helpDialog->contentsMargins().left() + m_helpDialog->contentsMargins().right() + 8;
259
260 m_helpDialog->resize(width, m_helpDialog->height());
261}
262
263// Action the focus params recommendations
264void FocusAdvisor::updateParams()
265{
266 m_focus->setAllSettings(m_map);
267 addResultsTable(i18n("Update Parameters"), -1, -1, -1, -1, "Done");
268}
269
270// Load up the Focus Advisor recommendations
271void FocusAdvisor::setupParams(const QString &OTName)
272{
273 // See if there is another OT that can be used to default parameters
274 m_map = getOTDefaults(OTName);
275 bool noDefaults = m_map.isEmpty();
276
277 if (!m_map.isEmpty())
278 // If we have found parameters from another OT that fit then we're done
279 return;
280
281 bool longFL = m_focus->m_FocalLength > 1500;
282 double imageScale = m_focus->getStarUnits(Focus::FOCUS_STAR_HFR, Focus::FOCUS_UNITS_ARCSEC);
283 bool centralObstruction = scopeHasObstruction(m_focus->m_ScopeType);
284
285 // Set the label based on the optical train
286 m_helpUI->helpLabel->setText(QString("Recommendations: %1 FL=%2 ImageScale=%3")
287 .arg(m_focus->m_ScopeType).arg(m_focus->m_FocalLength).arg(imageScale, 0, 'f', 2));
288
289 bool ok;
290 // Reset the table
291 m_helpUI->table->setRowCount(0);
292
293 // Camera options
294 int row = -1;
295 addSectionToHelpTable(row, i18n("Camera & Filter Wheel Parameters"));
296
297 // Exposure
298 double exposure = longFL ? 4.0 : 2.0;
299 processParam(exposure, row, m_map, m_focus->focusExposure, "Exposure");
300
301 // Binning
302 QString binning;
303 if (m_focus->focusBinning->isEnabled())
304 {
305 // Only try and update binning if camera supports it (binning field enabled)
306 QString binTarget = (imageScale < 1.0) ? "2x2" : "1x1";
307
308 for (int i = 0; i < m_focus->focusBinning->count(); i++)
309 {
310 if (m_focus->focusBinning->itemText(i) == binTarget)
311 {
312 binning = binTarget;
313 break;
314 }
315 }
316 }
317 processParam(binning, row, m_map, m_focus->focusBinning, "Binning");
318
319 // Gain - don't know a generic way to default to Unity gain so use current setting
320 processParam(m_focus->focusGain->value(), row, m_map, m_focus->focusGain, "Gain");
321
322 // ISO - default to current value
323 processParam(m_focus->focusISO->currentText(), row, m_map, m_focus->focusISO, "ISO");
324
325 // Filter
326 processParam(m_focus->focusFilter->currentText(), row, m_map, m_focus->focusFilter, "Filter");
327
328 // Settings
329 addSectionToHelpTable(row, i18n("Settings Parameters"));
330
331 // Auto Select Star
332 processParam(false, row, m_map, m_focus->m_OpsFocusSettings->focusAutoStarEnabled, "Auto Select Star");
333
334 // Suspend Guiding - leave alone
335 processParam(m_focus->m_OpsFocusSettings->focusSuspendGuiding->isChecked(), row, m_map,
336 m_focus->m_OpsFocusSettings->focusSuspendGuiding, "Auto Select Star");
337
338 // Use Dark Frame
339 processParam(false, row, m_map, m_focus->m_OpsFocusSettings->useFocusDarkFrame, "Dark Frame");
340
341 // Full Field & Subframe
342 processParam(true, row, m_map, m_focus->m_OpsFocusSettings->focusUseFullField, "Full Field");
343 processParam(false, row, m_map, m_focus->m_OpsFocusSettings->focusSubFrame, "Sub Frame");
344
345 // Subframe box
346 processParam(32, row, m_map, m_focus->m_OpsFocusSettings->focusBoxSize, "Box");
347
348 // Display Units - leave alone
349 processParam(m_focus->m_OpsFocusSettings->focusUnits->currentText(), row, m_map, m_focus->m_OpsFocusSettings->focusUnits,
350 "Display Units");
351
352 // Guide Settle - leave alone
353 processParam(m_focus->m_OpsFocusSettings->focusGuideSettleTime->value(), row, m_map,
354 m_focus->m_OpsFocusSettings->focusGuideSettleTime, "Guide Settle");
355
356 // Mask
357 QString maskCurrent, maskNew;
358 if (m_focus->m_OpsFocusSettings->focusNoMaskRB->isChecked())
359 maskCurrent = "Use all stars";
360 else if (m_focus->m_OpsFocusSettings->focusRingMaskRB->isChecked())
361 {
362 double inner = m_focus->m_OpsFocusSettings->focusFullFieldInnerRadius->value();
363 double outer = m_focus->m_OpsFocusSettings->focusFullFieldOuterRadius->value();
364 maskCurrent = QString("Ring Mask %1%-%2%").arg(inner, 0, 'f', 1).arg(outer, 0, 'f', 1);
365 }
366 else
367 {
368 int width = m_focus->m_OpsFocusSettings->focusMosaicTileWidth->value();
369 int spacer = m_focus->m_OpsFocusSettings->focusMosaicSpace->value();
370 maskCurrent = QString("Mosaic Mask %1% (%2 px)").arg(width).arg(spacer);
371 }
372
373 if (noDefaults)
374 {
375 // Set a Ring Mask 0% - 80%
376 m_map.insert("focusNoMaskRB", false);
377 m_map.insert("focusRingMaskRB", true);
378 m_map.insert("focusMosaicMaskRB", false);
379 m_map.insert("focusFullFieldInnerRadius", 0.0);
380 m_map.insert("focusFullFieldOuterRadius", 80.0);
381 maskNew = QString("Ring Mask %1%-%2%").arg(0.0, 0, 'f', 1).arg(80.0, 0, 'f', 1);
382 }
383 else
384 {
385 bool noMask = m_map.value("focusNoMaskRB", false).toBool();
386 bool ringMask = m_map.value("focusRingMaskRB", false).toBool();
387 bool mosaicMask = m_map.value("focusMosaicMaskRB", false).toBool();
388 if (noMask)
389 maskNew = "Use all stars";
390 else if (ringMask)
391 {
392 double inner = m_map.value("focusFullFieldInnerRadius", 0.0).toDouble(&ok);
393 if (!ok || inner < 0.0 || inner > 100.0)
394 inner = 0.0;
395 double outer = m_map.value("focusFullFieldOuterRadius", 80.0).toDouble(&ok);
396 if (!ok || outer < 0.0 || outer > 100.0)
397 outer = 80.0;
398 maskNew = QString("Ring Mask %1%-%2%").arg(inner, 0, 'f', 1).arg(outer, 0, 'f', 1);
399 }
400 else if (mosaicMask)
401 {
402 int width = m_map.value("focusMosaicTileWidth", 0.0).toInt(&ok);
403 if (!ok || width < 0 || width > 100)
404 width = 0.0;
405 int spacer = m_map.value("focusMosaicSpace", 0.0).toInt(&ok);
406 if (!ok || spacer < 0 || spacer > 100)
407 spacer = 0.0;
408 maskNew = QString("Mosaic Mask %1% (%2 px)").arg(width).arg(spacer);
409 }
410 }
411 addParamToHelpTable(row, i18n("Mask"), maskCurrent, maskNew);
412
413 // Adaptive Focus
414 processParam(false, row, m_map, m_focus->m_OpsFocusSettings->focusAdaptive, "Adaptive Focus");
415
416 // Min Move - leave default
417 processParam(m_focus->m_OpsFocusSettings->focusAdaptiveMinMove->value(), row, m_map,
418 m_focus->m_OpsFocusSettings->focusAdaptiveMinMove, "Min Move");
419
420 // Adapt Start Pos
421 processParam(false, row, m_map, m_focus->m_OpsFocusSettings->focusAdaptStart, "Adapt Start Pos");
422
423 // Max Movement - leave default
424 processParam(m_focus->m_OpsFocusSettings->focusAdaptiveMaxMove->value(), row, m_map,
425 m_focus->m_OpsFocusSettings->focusAdaptiveMaxMove, "Max Total Move");
426
427 // Process
428 addSectionToHelpTable(row, i18n("Process Parameters"));
429
430 // Detection method
431 processParam(QString("SEP"), row, m_map, m_focus->m_OpsFocusProcess->focusDetection, "Detection");
432
433 // SEP profile
434 QString profile = centralObstruction ? FOCUS_DEFAULT_DONUT_NAME : FOCUS_DEFAULT_NAME;
435 processParam(profile, row, m_map, m_focus->m_OpsFocusProcess->focusSEPProfile, "SEP Profile");
436
437 // Algorithm
438 processParam(QString("Linear 1 Pass"), row, m_map, m_focus->m_OpsFocusProcess->focusAlgorithm, "Algorithm");
439
440 // Curve Fit
441 processParam(QString("Hyperbola"), row, m_map, m_focus->m_OpsFocusProcess->focusCurveFit, "Curve Fit");
442
443 // Measure
444 processParam(QString("HFR"), row, m_map, m_focus->m_OpsFocusProcess->focusStarMeasure, "Measure");
445
446 // Use Weights
447 processParam(true, row, m_map, m_focus->m_OpsFocusProcess->focusUseWeights, "Use Weights");
448
449 // R2 limit
450 processParam(0.8, row, m_map, m_focus->m_OpsFocusProcess->focusR2Limit, "R² Limit");
451
452 // Refine Curve Fit
453 processParam(true, row, m_map, m_focus->m_OpsFocusProcess->focusRefineCurveFit, "Refine Curve Fit");
454
455 // Frames Count
456 processParam(1, row, m_map, m_focus->m_OpsFocusProcess->focusFramesCount, "Average Over");
457
458 // HFR Frames Count
459 processParam(1, row, m_map, m_focus->m_OpsFocusProcess->focusHFRFramesCount, "Average HFR Check");
460
461 // Donut buster
462 processParam(centralObstruction, row, m_map, m_focus->m_OpsFocusProcess->focusDonut, "Donut Buster");
463 processParam(1.0, row, m_map, m_focus->m_OpsFocusProcess->focusTimeDilation, "Time Dilation x");
464 processParam(0.2, row, m_map, m_focus->m_OpsFocusProcess->focusOutlierRejection, "Outlier Rejection");
465
466 // Scan for Start Position
467 processParam(true, row, m_map, m_focus->m_OpsFocusProcess->focusScanStartPos, "Scan for Start Position");
468 processParam(false, row, m_map, m_focus->m_OpsFocusProcess->focusScanAlwaysOn, "Always On");
469 processParam(5, row, m_map, m_focus->m_OpsFocusProcess->focusScanDatapoints, "Num Datapoints");
470 processParam(1.0, row, m_map, m_focus->m_OpsFocusProcess->focusScanStepSizeFactor, "Initial Step Sixe x");
471
472 // Mechanics
473 addSectionToHelpTable(row, i18n("Mechanics Parameters"));
474
475 // Walk
476 processParam(QString("Fixed Steps"), row, m_map, m_focus->m_OpsFocusMechanics->focusWalk, "Walk");
477
478 // Settle Time
479 processParam(1.0, row, m_map, m_focus->m_OpsFocusMechanics->focusSettleTime, "Focuser Settle");
480
481 // Initial Step Size - Sim = 5000 otherwise 20
482 int stepSize = m_focus->m_Focuser && m_focus->m_Focuser->getDeviceName() == FOCUSER_SIMULATOR ? 5000 : 20;
483 processParam(stepSize, row, m_map, m_focus->m_OpsFocusMechanics->focusTicks, "Initial Step Size");
484
485 // Number of steps
486 processParam(11, row, m_map, m_focus->m_OpsFocusMechanics->focusNumSteps, "Number Steps");
487
488 // Max Travel - leave default
489 processParam(m_focus->m_OpsFocusMechanics->focusMaxTravel->maximum(), row, m_map,
490 m_focus->m_OpsFocusMechanics->focusMaxTravel, "Max Travel");
491
492 // Backlash - leave default
493 processParam(m_focus->m_OpsFocusMechanics->focusBacklash->value(), row, m_map, m_focus->m_OpsFocusMechanics->focusBacklash,
494 "Driver Backlash");
495
496 // AF Overscan - leave default
497 processParam(m_focus->m_OpsFocusMechanics->focusAFOverscan->value(), row, m_map,
498 m_focus->m_OpsFocusMechanics->focusAFOverscan, "AF Overscan");
499
500 // Overscan Delay
501 processParam(0.0, row, m_map, m_focus->m_OpsFocusMechanics->focusOverscanDelay, "AF Overscan Delay");
502
503 // Capture timeout
504 processParam(30, row, m_map, m_focus->m_OpsFocusMechanics->focusCaptureTimeout, "Capture Timeout");
505
506 // Motion timeout
507 processParam(30, row, m_map, m_focus->m_OpsFocusMechanics->focusMotionTimeout, "Motion Timeout");
508
509 resizeHelpDialog();
510 setButtons(false);
511}
512
513QString FocusAdvisor::boolText(const bool flag)
514{
515 return flag ? "On" : "Off";
516}
517
518// Function to set inFocusAdvisor
519void FocusAdvisor::setInFocusAdvisor(bool value)
520{
521 m_inFocusAdvisor = value;
522}
523
524// Return a prefix to use when Focus saves off a focus frame
525QString FocusAdvisor::getFocusFramePrefix()
526{
527 QString prefix;
528 if (inFocusAdvisor() && m_inFindStars)
529 prefix = "_fs_" + QString("%1_%2").arg(m_findStarsRunNum).arg(m_focus->absIterations + 1);
530 else if (inFocusAdvisor() && m_inPreAFAdj)
531 prefix = "_ca_" + QString("%1_%2").arg(m_preAFRunNum).arg(m_focus->absIterations + 1);
532 else if (inFocusAdvisor() && m_focus->inScanStartPos)
533 prefix = "_ssp_" + QString("%1_%2").arg(m_focus->m_AFRun).arg(m_focus->absIterations + 1);
534 return prefix;
535}
536
537// Find similar OTs to seed defaults
538QVariantMap FocusAdvisor::getOTDefaults(const QString &OTName)
539{
540 QVariantMap map;
541
542 // If a blank OTName is passed in return an empty map
543 if (OTName.isEmpty())
544 return map;
545
546 for (auto &tName : OpticalTrainManager::Instance()->getTrainNames())
547 {
548 if (tName == OTName)
549 continue;
550 auto tFocuser = OpticalTrainManager::Instance()->getFocuser(tName);
551 if (tFocuser != m_focus->m_Focuser)
552 continue;
553 auto tScope = OpticalTrainManager::Instance()->getScope(tName);
554 auto tScopeType = tScope["type"].toString();
555 if (tScopeType != m_focus->m_ScopeType)
556 continue;
557
558 // We have an OT with the same Focuser and scope type so see if we have any parameters
559 auto tID = OpticalTrainManager::Instance()->id(tName);
560 OpticalTrainSettings::Instance()->setOpticalTrainID(tID);
561 auto settings = OpticalTrainSettings::Instance()->getOneSetting(OpticalTrainSettings::Focus);
562 if (settings.isValid())
563 {
564 // We have a set of parameters
565 map = settings.toJsonObject().toVariantMap();
566 // We will adjust the step size here
567 // We will use the CFZ. The CFZ scales with f#^2, so adjust step size in the same way
568 auto tAperture = tScope["aperture"].toDouble(-1);
569 auto tFocalLength = tScope["focal_length"].toDouble(-1);
570 auto tFocalRatio = tScope["focal_ratio"].toDouble(-1);
571 auto tReducer = OpticalTrainManager::Instance()->getReducer(tName);
572 if (tFocalLength > 0.0)
573 tFocalLength *= tReducer;
574
575 // Use the adjusted focal length to calculate an adjusted focal ratio
576 if (tFocalRatio <= 0.0)
577 // For a scope, FL and aperture are specified so calc the F#
578 tFocalRatio = (tAperture > 0.001) ? tFocalLength / tAperture : 0.0f;
579 // else if (tAperture < 0.0)
580 // // DSLR Lens. FL and F# are specified so calc the aperture
581 // tAperture = tFocalLength / tFocalRatio;
582
583 int stepSize = 5000;
584 if (m_focus->m_Focuser && m_focus->m_Focuser->getDeviceName() == FOCUSER_SIMULATOR)
585 // The Simulator is a special case so use 5000 as that works well
586 stepSize = 5000;
587 else
588 stepSize = map.value("focusTicks", stepSize).toInt() * pow(m_focus->m_FocalRatio, 2.0) / pow(tFocalRatio, 2.0);
589 // Add the value to map if one doesn't exist or update it if it does
590 map.insert("focusTicks", stepSize);
591 break;
592 }
593 }
594 // Reset Optical Train Manager to the original OT
595 auto id = OpticalTrainManager::Instance()->id(OTName);
596 OpticalTrainSettings::Instance()->setOpticalTrainID(id);
597 return map;
598}
599
600// Returns whether or not the passed in scopeType has a central obstruction or not. The scopeTypes
601// are defined in the equipmentWriter code. It would be better, probably, if that code included
602// a flag for central obstruction, rather than hard coding strings for the scopeType that are compared
603// in this routine.
604bool FocusAdvisor::scopeHasObstruction(const QString &scopeType)
605{
606 return (scopeType != "Refractor" && scopeType != "Kutter (Schiefspiegler)");
607}
608
609// Focus Advisor control function initialiser
610void FocusAdvisor::initControl()
611{
612 setInFocusAdvisor(true);
613 setButtons(true);
614 m_initialStepSize = m_focus->m_OpsFocusMechanics->focusTicks->value();
615 m_initialBacklash = m_focus->m_OpsFocusMechanics->focusBacklash->value();
616 m_initialAFOverscan = m_focus->m_OpsFocusMechanics->focusAFOverscan->value();
617 m_initialUseWeights = m_focus->m_OpsFocusProcess->focusUseWeights->isChecked();
618
619 if (m_inFindStars)
620 initFindStars(m_focus->currentPosition);
621 else if (m_inPreAFAdj)
622 initPreAFAdj(m_focus->currentPosition);
623 else if (m_inAFAdj)
624 initAFAdj(m_focus->currentPosition, false);
625}
626
627// Focus Advisor control function
628void FocusAdvisor::control()
629{
630 if (!inFocusAdvisor())
631 return;
632
633 if (m_inFindStars)
634 findStars();
635 else if (m_inPreAFAdj)
636 preAFAdj();
637 else
638 abort(i18n("Focus Advisor aborted due to internal error"));
639}
640
641// Prepare the Find Stars algorithm
642void FocusAdvisor::initFindStars(const int startPos)
643{
644 // check whether Focus Advisor can run with the current parameters
645 if (!canFocusAdvisorRun())
646 {
647 abort(i18n("Focus Advisor cannot run with current params"));
648 return;
649 }
650
651 focusAdvFindStarsLabel->setText(i18n("In progress..."));
652 emit newStage(FindingStars);
653
654 // Set the initial position, which we'll fallback to in case of failure
655 m_focus->initialFocuserAbsPosition = startPos;
656 m_focus->linearRequestedPosition = startPos;
657
658 // Set useWeights off as it makes the v-graph display unnecessarily complex whilst adding nothing
659 m_focus->m_OpsFocusProcess->focusUseWeights->setChecked(false);
660
661 m_focus->absIterations = 0;
662 m_position.clear();
663 m_measure.clear();
664 m_focus->clearDataPoints();
665 m_jumpsToGo = NUM_JUMPS_PER_SECTOR;
666 m_jumpSize = m_focus->m_OpsFocusMechanics->focusTicks->value() * NUM_JUMPS_PER_SECTOR;
667 m_findStarsIn = false;
668 m_findStarsMaxBound = m_findStarsMinBound = false;
669 m_findStarsSector = 0;
670 m_findStarsRange = false;
671 m_findStarsRangeIn = false;
672 m_findStarsFindInEdge = false;
673 m_findStarsInRange = -1;
674 m_findStarsOutRange = -1;
675 m_findStarsRunNum++;
676
677 // Set backlash and Overscan off as its not needed at this stage - we'll calculate later
678 m_focus->m_OpsFocusMechanics->focusBacklash->setValue(0);
679 m_focus->m_OpsFocusMechanics->focusAFOverscan->setValue(0);
680
681 addResultsTable(i18n("Find Stars"), m_findStarsRunNum, startPos, m_jumpSize,
682 m_focus->m_OpsFocusMechanics->focusAFOverscan->value(), "");
683 emit m_focus->setTitle(QString(i18n("Find Stars: Scanning for stars...")), true);
684 if (!m_focus->changeFocus(startPos - m_focus->currentPosition))
685 abort(i18n("Find Stars: Failed"));
686}
687
688// Algorithm to scan the focuser's range of motion to find stars
689// The algorithm is seeded with a start value, jump size and number of jumps and it sweeps out by num jumps,
690// then sweeps in taking a frame and looking for stars. Once some stars have been found it searches the range
691// of motion containing stars to get the central position which is passed as start point to the next stage.
692void FocusAdvisor::findStars()
693{
694 // Plot the datapoint
695 emit m_focus->newHFRPlotPosition(static_cast<double>(m_focus->currentPosition), m_focus->currentMeasure,
696 std::pow(m_focus->currentWeight, -0.5), false, m_jumpSize, true);
697
698 int offset = 0;
699 bool starsExist = starsFound();
700 if (m_findStarsRange)
701 {
702 bool outOfRange = false;
703 if (starsExist)
704 {
705 // We have some stars but check we're not about to run out of range
706 if (m_findStarsRangeIn)
707 outOfRange = (m_focus->currentPosition - m_jumpSize) < static_cast<int>(m_focus->absMotionMin);
708 else
709 outOfRange = (m_focus->currentPosition + m_jumpSize) > static_cast<int>(m_focus->absMotionMax);
710 }
711
712 if (m_findStarsFindInEdge)
713 {
714 if (starsExist)
715 {
716 // We found the inner boundary of stars / no stars
717 m_findStarsFindInEdge = false;
718 m_findStarsInRange = m_focus->currentPosition - m_jumpSize;
719 }
720 }
721 else if (!starsExist && m_findStarsInRange < 0 && m_findStarsOutRange < 0)
722 {
723 // We have 1 side of the stars range, but not the other... so reverse motion to get the other side
724 // Calculate the offset to get back to the start position where stars were found
725 if (m_findStarsRangeIn)
726 {
727 m_findStarsInRange = m_focus->currentPosition;
728 auto max = std::max_element(std::begin(m_position), std::end(m_position));
729 offset = *max - m_focus->currentPosition;
730 }
731 else
732 {
733 m_findStarsOutRange = m_focus->currentPosition;
734 auto min = std::min_element(std::begin(m_position), std::end(m_position));
735 offset = *min - m_focus->currentPosition;
736 }
737 m_findStarsRangeIn = !m_findStarsRangeIn;
738 }
739 else if (!starsExist || outOfRange)
740 {
741 // We're reached the other side of the zone in which stars can be detected so we're done
742 // A good place to use for the next phase will be the centre of the zone
743 m_inFindStars = false;
744 if (m_findStarsRangeIn)
745 // Range for stars is inwards
746 m_findStarsInRange = m_focus->currentPosition;
747 else
748 // Range for stars is outwards
749 m_findStarsOutRange = m_focus->currentPosition;
750 const int zoneCentre = (m_findStarsInRange + m_findStarsOutRange) / 2;
751 focusAdvFindStarsLabel->setText(i18n("Done"));
752 updateResultsTable(i18n("Stars detected, range center %1", QString::number(zoneCentre)));
753 // Now move onto the next stage or stop if nothing more to do
754 if (m_inPreAFAdj)
755 initPreAFAdj(zoneCentre);
756 else if (m_inAFAdj)
757 initAFAdj(zoneCentre, true);
758 else
759 {
760 m_focus->absTicksSpin->setValue(zoneCentre);
761 emit m_focus->setTitle(QString(i18n("Stars detected, range centre %1", zoneCentre)), true);
762 complete(false, i18n("Focus Advisor Find Stars completed"));
763 }
764 return;
765 }
766 }
767
768 // Log the results
769 m_position.push_back(m_focus->currentPosition);
770 m_measure.push_back(m_focus->currentMeasure);
771
772 // Now check if we have any stars
773 if (starsExist)
774 {
775 // We have stars! Now try and find the centre of range where stars exist. We'll be conservative here
776 // and set the range to the point where stars disappear.
777 if (!m_findStarsRange)
778 {
779 m_findStarsRange = true;
780
781 // If stars found first position we don't know where we are in the zone so explore both ends
782 // Otherwise we know where 1 boundary is so we only need to expore the other
783 if (m_position.size() > 1)
784 {
785 if (m_position.last() < m_position[m_position.size() - 2])
786 {
787 // Stars were found whilst moving inwards so we know the outer boundary
788 QVector<int> positionsCopy = m_position;
789 std::sort(positionsCopy.begin(), positionsCopy.end(), std::greater<int>());
790 m_findStarsOutRange = positionsCopy[1];
791 m_findStarsOutRange = m_position[m_position.size() - 2];
792 m_findStarsRangeIn = true;
793 }
794 else
795 {
796 // Stars found whilst moving outwards. Firstly we need to find the inward edge of no stars / stars
797 // so set the position back to the previous max position before the current point.
798 m_findStarsFindInEdge = true;
799 QVector<int> positionsCopy = m_position;
800 std::sort(positionsCopy.begin(), positionsCopy.end(), std::greater<int>());
801 offset = positionsCopy[1] - m_focus->currentPosition;
802 }
803 }
804 }
805 }
806
807 // Cap the maximum number of iterations before failing
808 if (++m_focus->absIterations > MAXIMUM_FOCUS_ADVISOR_ITERATIONS)
809 {
810 abort(i18n("Find Stars: exceeded max iterations %1", MAXIMUM_FOCUS_ADVISOR_ITERATIONS));
811 return;
812 }
813
814 int deltaPos;
815 if (m_findStarsRange)
816 {
817 // Collect more data to find the range of focuser motion with stars
818 emit m_focus->setTitle(QString(i18n("Stars detected, centering range")), true);
819 updateResultsTable(i18n("Stars detected, centering range"));
820 int nextPos;
821 if (m_findStarsRangeIn)
822 nextPos = std::max(m_focus->currentPosition + offset - m_jumpSize, static_cast<int>(m_focus->absMotionMin));
823 else
824 nextPos = std::min(m_focus->currentPosition + offset + m_jumpSize, static_cast<int>(m_focus->absMotionMax));
825 deltaPos = nextPos - m_focus->currentPosition;
826 }
827 else if (m_position.size() == 1)
828 {
829 // No luck with stars at the seed position so jump outwards and start an inward sweep
830 deltaPos = m_jumpSize * m_jumpsToGo;
831 // Check the proposed move is within bounds
832 if (m_focus->currentPosition + deltaPos >= m_focus->absMotionMax)
833 {
834 deltaPos = m_focus->absMotionMax - m_focus->currentPosition;
835 m_jumpsToGo = deltaPos / m_jumpSize + (deltaPos % m_jumpSize != 0);
836 m_findStarsMaxBound = true;
837 }
838 m_findStarsJumpsInSector = m_jumpsToGo;
839 m_findStarsSector = 1;
840 emit m_focus->setTitle(QString(i18n("Find Stars Run %1: No stars at start %2, scanning...", m_findStarsRunNum,
841 m_focus->currentPosition)), true);
842 }
843 else if (m_jumpsToGo > 0)
844 {
845 // Collect more data in the current sweep
846 emit m_focus->setTitle(QString(i18n("Find Stars Run %1 Sector %2: Scanning %3/%4", m_findStarsRunNum, m_findStarsSector,
847 m_jumpsToGo, m_findStarsJumpsInSector)), true);
848 updateResultsTable(i18n("Find Stars Run %1: Scanning Sector %2", m_findStarsRunNum, m_findStarsSector));
849 int nextPos = std::max(m_focus->currentPosition - m_jumpSize, static_cast<int>(m_focus->absMotionMin));
850 deltaPos = nextPos - m_focus->currentPosition;
851 }
852 else
853 {
854 // We've completed the current sweep, but still no stars so check if we still have focuser range to search...
855 if(m_findStarsMaxBound && m_findStarsMinBound)
856 {
857 // We're out of road... covered the entire focuser range of motion but couldn't find any stars
858 // halve the step size and go again
859 updateResultsTable(i18n("No stars detected"));
860 int newStepSize = m_focus->m_OpsFocusMechanics->focusTicks->value() / 2;
861 if (newStepSize > 1)
862 {
863 m_focus->m_OpsFocusMechanics->focusTicks->setValue(newStepSize);
864 initFindStars(m_focus->initialFocuserAbsPosition);
865 }
866 else
867 abort(i18n("Find Stars Run %1: failed to find any stars", m_findStarsRunNum));
868 return;
869 }
870
871 // Setup the next sweep sector...
872 m_jumpsToGo = NUM_JUMPS_PER_SECTOR;
873 if (m_findStarsIn)
874 {
875 // We're "inside" the starting point so flip to the outside unless max bound already hit
876 if (m_findStarsMaxBound)
877 {
878 // Ensure deltaPos doesn't go below minimum
879 if (m_focus->currentPosition - m_jumpSize < static_cast<int>(m_focus->absMotionMin))
880 deltaPos = static_cast<int>(m_focus->absMotionMin) - m_focus->currentPosition;
881 else
882 deltaPos = -m_jumpSize;
883 }
884 else
885 {
886 auto max = std::max_element(std::begin(m_position), std::end(m_position));
887 int sweepMax = *max + (m_jumpSize * m_jumpsToGo);
888 if (sweepMax >= static_cast<int>(m_focus->absMotionMax))
889 {
890 sweepMax = static_cast<int>(m_focus->absMotionMax);
891 m_jumpsToGo = (sweepMax - *max) / m_jumpSize + ((sweepMax - *max) % m_jumpSize != 0);
892 m_findStarsMaxBound = true;
893 }
894 m_findStarsIn = false;
895 deltaPos = sweepMax - m_focus->currentPosition;
896 }
897 }
898 else
899 {
900 // We're "outside" the starting point so continue inwards unless min bound already hit
901 if (m_findStarsMinBound)
902 {
903 auto max = std::max_element(std::begin(m_position), std::end(m_position));
904 int sweepMax = *max + (m_jumpSize * m_jumpsToGo);
905 if (sweepMax >= static_cast<int>(m_focus->absMotionMax))
906 {
907 sweepMax = static_cast<int>(m_focus->absMotionMax);
908 m_jumpsToGo = (sweepMax - *max) / m_jumpSize + ((sweepMax - *max) % m_jumpSize != 0);
909 m_findStarsMaxBound = true;
910 }
911 deltaPos = sweepMax - m_focus->currentPosition;
912 }
913 else
914 {
915 auto min = std::min_element(std::begin(m_position), std::end(m_position));
916 int sweepMin = *min - (m_jumpSize * m_jumpsToGo);
917 if (sweepMin <= static_cast<int>(m_focus->absMotionMin))
918 {
919 // This sweep will hit the min bound
920 m_findStarsMinBound = true;
921 sweepMin = static_cast<int>(m_focus->absMotionMin);
922 m_jumpsToGo = (*min - sweepMin) / m_jumpSize + ((*min - sweepMin) % m_jumpSize != 0);
923 }
924 // We've already done the most inward point of a sweep at the start so jump to the next point.
925 m_findStarsIn = true;
926 int nextJumpPos = std::max(*min - m_jumpSize, static_cast<int>(m_focus->absMotionMin));
927 deltaPos = nextJumpPos - m_focus->currentPosition;
928 }
929 }
930 m_findStarsSector++;
931 m_findStarsJumpsInSector = m_jumpsToGo;
932 emit m_focus->setTitle(QString(i18n("Find Stars Run %1 Sector %2: scanning %3/%4", m_findStarsRunNum, m_findStarsSector,
933 m_jumpsToGo, m_findStarsJumpsInSector)), true);
934 }
935 if (!m_findStarsRange)
936 m_jumpsToGo--;
937 m_focus->linearRequestedPosition = m_focus->currentPosition + deltaPos;
938 if (!m_focus->changeFocus(deltaPos))
939 abort(i18n("Focus Advisor Find Stars run %1: failed to move focuser", m_findStarsRunNum));
940}
941
942bool FocusAdvisor::starsFound()
943{
944 return (m_focus->currentMeasure != INVALID_STAR_MEASURE && m_focus->currentNumStars > FIND_STARS_MIN_STARS);
945
946}
947
948// Initialise the pre-Autofocus adjustment algorithm
949void FocusAdvisor::initPreAFAdj(const int startPos)
950{
951 // check whether Focus Advisor can run with the current parameters
952 if (!canFocusAdvisorRun())
953 {
954 abort(i18n("Focus Advisor cannot run with current params"));
955 return;
956 }
957
958 // Check we're not looping
959 if (m_preAFRunNum > 50)
960 {
961 abort(i18n("Focus Advisor Coarse Adjustment aborted after %1 runs", m_preAFRunNum));
962 return;
963 }
964
965 focusAdvCoarseAdjLabel->setText(i18n("In progress..."));
966 emit newStage(CoarseAdjustments);
967
968 m_focus->initialFocuserAbsPosition = startPos;
969 m_focus->absIterations = 0;
970 m_position.clear();
971 m_measure.clear();
972 m_preAFInner = 0;
973 m_preAFNoStarsOut = m_preAFNoStarsIn = false;
974 m_preAFRunNum++;
975
976 // Reset the v-curve - otherwise there's too much data to see what's going on
977 m_focus->clearDataPoints();
978 emit m_focus->setTitle(QString(i18n("Coarse Adjustment Scan...")), true);
979
980 // Setup a sweep of m_jumpSize either side of startPos
981 m_jumpSize = m_focus->m_OpsFocusMechanics->focusTicks->value() * NUM_STEPS_PRE_AF;
982
983 // Check that the sweep can fit into the focuser's motion range
984 if (m_jumpSize > m_focus->absMotionMax - m_focus->absMotionMin)
985 {
986 m_jumpSize = m_focus->absMotionMax - m_focus->absMotionMin;
987 m_focus->m_OpsFocusMechanics->focusTicks->setValue(m_jumpSize / NUM_STEPS_PRE_AF);
988 }
989
990 int deltaPos = (startPos - m_focus->currentPosition) + m_jumpSize / 2;
991 if (m_focus->currentPosition + deltaPos > maxStarsLimit())
992 deltaPos = maxStarsLimit() - m_focus->currentPosition;
993
994 m_preAFInner = startPos - m_jumpSize / 2;
995 if (m_preAFInner < minStarsLimit())
996 m_preAFInner = minStarsLimit();
997
998 // Set backlash and AF Overscan off on first run, and reset useWeights
999 if (m_preAFRunNum == 1)
1000 {
1001 m_focus->m_OpsFocusMechanics->focusBacklash->setValue(0);
1002 m_focus->m_OpsFocusMechanics->focusAFOverscan->setValue(0);
1003 m_focus->m_OpsFocusProcess->focusUseWeights->setChecked(m_initialUseWeights);
1004 }
1005
1006 addResultsTable(i18n("Coarse Adjustment"), m_preAFRunNum, startPos,
1007 m_focus->m_OpsFocusMechanics->focusTicks->value(),
1008 m_focus->m_OpsFocusMechanics->focusAFOverscan->value(), "");
1009
1010 m_focus->linearRequestedPosition = m_focus->currentPosition + deltaPos;
1011 if (!m_focus->changeFocus(deltaPos))
1012 abort(i18n("Focus Advisor Coarse Adjustment failed to move focuser"));
1013}
1014
1015// Pre Autofocus coarse adjustment algorithm.
1016// Move the focuser until we get a reasonable movement in measure (x2)
1017void FocusAdvisor::preAFAdj()
1018{
1019 // Cap the maximum number of iterations before failing
1020 if (++m_focus->absIterations > MAXIMUM_FOCUS_ADVISOR_ITERATIONS)
1021 {
1022 abort(i18n("Focus Advisor Coarse Adjustment: exceeded max iterations %1", MAXIMUM_FOCUS_ADVISOR_ITERATIONS));
1023 return;
1024 }
1025
1026 int deltaPos;
1027 const int step = m_focus->m_OpsFocusMechanics->focusTicks->value();
1028
1029 bool starsExist = starsFound();
1030 if (starsExist)
1031 {
1032 // If we have stars, persist the results for later analysis
1033 m_position.push_back(m_focus->currentPosition);
1034 m_measure.push_back(m_focus->currentMeasure);
1035
1036 if (m_preAFNoStarsOut && (m_position.size() == 0))
1037 {
1038 if (m_findStarsOutRange < 0)
1039 m_findStarsOutRange = m_focus->currentPosition;
1040 else
1041 m_findStarsOutRange = std::max(m_findStarsOutRange, m_focus->currentPosition);
1042 }
1043 }
1044 else
1045 {
1046 // No stars - record whether this is at the inward or outward. If in the middle, just ignore
1047 if (m_position.size() < 2)
1048 m_preAFNoStarsOut = true;
1049 else if (m_focus->currentPosition - (2 * step) <= m_preAFInner)
1050 {
1051 m_preAFNoStarsIn = true;
1052 if (m_findStarsInRange < 0)
1053 m_findStarsInRange = m_position.last();
1054 else
1055 m_findStarsInRange = std::min(m_findStarsInRange, m_position.last());
1056 }
1057 }
1058
1059 emit m_focus->newHFRPlotPosition(static_cast<double>(m_focus->currentPosition), m_focus->currentMeasure,
1060 std::pow(m_focus->currentWeight, -0.5), false, step, true);
1061
1062 // See if we need to extend the sweep
1063 if (m_focus->currentPosition - step < m_preAFInner && starsExist)
1064 {
1065 // Next step would take us beyond our bound, so see if we have enough data or want to extend the sweep
1066 auto it = std::min_element(std::begin(m_measure), std::end(m_measure));
1067 auto min = std::distance(std::begin(m_measure), it);
1068 if (m_position.size() < 5 || (m_position.size() < 2 * NUM_STEPS_PRE_AF && m_position.size() - min < 3))
1069 {
1070 // Not much data or minimum is at the edge so continue for a bit, if we can
1071 if (m_preAFInner - step >= minStarsLimit())
1072 m_preAFInner -= step;
1073 }
1074 }
1075
1076 if (m_focus->currentPosition - step >= m_preAFInner)
1077 {
1078 // Collect more data in the current sweep
1079 emit m_focus->setTitle(QString(i18n("Coarse Adjustment Run %1 scan...", m_preAFRunNum)), true);
1080 deltaPos = -step;
1081 }
1082 else
1083 {
1084 // We've completed the current sweep, so analyse the data...
1085 if (m_position.size() < 5)
1086 {
1087 abort(i18n("Focus Advisor Coarse Adjustment Run %1: insufficient data to proceed", m_preAFRunNum));
1088 return;
1089 }
1090 else
1091 {
1092 auto it = std::min_element(std::begin(m_measure), std::end(m_measure));
1093 auto min = std::distance(std::begin(m_measure), it);
1094 const double minMeasure = *it;
1095 int minPos = m_position[min];
1096
1097 auto it2 = std::max_element(std::begin(m_measure), std::end(m_measure));
1098 const double maxMeasure = *it2;
1099 // Get a ratio of max to min scaled to NUM_STEPS_PRE_AF
1100 const double scaling = static_cast<double>(NUM_STEPS_PRE_AF) / static_cast<double>(m_measure.count());
1101 const double measureRatio = maxMeasure / minMeasure * scaling;
1102
1103 // Is the minimum near the centre of the sweep?
1104 double whereInRange = static_cast<double>(minPos - m_position.last()) / static_cast<double>
1105 (m_position.first() - m_position.last());
1106 bool nearCenter = whereInRange > 0.3 && whereInRange < 0.7;
1107
1108 QVector<double> gradients;
1109 for (int i = 0; i < m_position.size() - 1; i++)
1110 {
1111 double gradient = (m_measure[i] - m_measure[i + 1]) / (m_position[i] - m_position[i + 1]);
1112 gradients.push_back(std::abs(gradient));
1113 }
1114
1115 // Average the largest 3 gradients (to stop distortion by a single value). The largest gradients should occur
1116 // at the furthest out position. Assume smaller gradients at furthest out position are caused by backlash
1117 QVector<double> gradientsCopy = gradients;
1118 std::sort(gradientsCopy.begin(), gradientsCopy.end(), std::greater<double>());
1119 std::vector<double> gradientsMax;
1120 for (int i = 0; i < 3; i++)
1121 gradientsMax.push_back(gradientsCopy[i]);
1122
1123 double gradientsMean = Mathematics::RobustStatistics::ComputeLocation(Mathematics::RobustStatistics::LOCATION_MEAN,
1124 gradientsMax);
1125
1126 const double threshold = gradientsMean * 0.5;
1127 int overscan = m_focus->m_OpsFocusMechanics->focusAFOverscan->value();
1128 for (int i = 0; i < gradients.size(); i++)
1129 {
1130 if (gradients[i] <= threshold)
1131 overscan += m_position[i] - m_position[i + 1];
1132 else
1133 break;
1134 }
1135 overscan = std::max(overscan, step);
1136 m_focus->m_OpsFocusMechanics->focusAFOverscan->setValue(overscan);
1137
1138 const bool hitNoStarsRegion = m_preAFNoStarsIn || m_preAFNoStarsOut;
1139
1140 // Is everything good enough to proceed, or do we need to run again?
1141 if (nearCenter && maxMinRatioOK(INITIAL_MAXMIN_HFR_RATIO, measureRatio) && !hitNoStarsRegion)
1142 {
1143 // We're done with the coarse adjustment step so prepare for the next step
1144 updateResultsTable(i18n("Max/Min Ratio: %1", QString::number(measureRatio, 'f', 1)));
1145 m_inPreAFAdj = false;
1146 focusAdvCoarseAdjLabel->setText(i18n("Done"));
1147 m_focus->absIterations = 0;
1148 m_position.clear();
1149 m_measure.clear();
1150 if (m_inAFAdj)
1151 {
1152 // Reset the v-curve - otherwise there's too much data to see what's going on
1153 m_focus->clearDataPoints();
1154 // run Autofocus
1155 m_nearFocus = true;
1156 initAFAdj(minPos, true);
1157 }
1158 else
1159 {
1160 m_focus->absTicksSpin->setValue(minPos);
1161 complete(false, i18n("Focus Advisor Coarse Adjustment completed."));
1162 }
1163 return;
1164 }
1165 else
1166 {
1167 // Curve is too flat / steep - so we need to change the range, select a better start position and rerun...
1168 int startPosition;
1169 const double expansion = TARGET_MAXMIN_HFR_RATIO / measureRatio;
1170 int newStepSize = m_focus->m_OpsFocusMechanics->focusTicks->value() * expansion;
1171
1172 if (m_preAFNoStarsIn && m_preAFNoStarsOut)
1173 {
1174 // We have no stars both inwards and outwards
1175 const int range = m_position.first() - m_position.last();
1176 newStepSize = range / NUM_STEPS_PRE_AF;
1177 startPosition = (m_position.first() + m_position.last()) / 2;
1178 m_preAFMaxRange = true;
1179 if (newStepSize < 1)
1180 {
1181 // Looks like data is inconsistent so stop here
1182 abort(i18n("Focus Advisor Coarse Adjustment: data quality too poor to continue"));
1183 return;
1184 }
1185 }
1186 else if (m_preAFNoStarsIn)
1187 {
1188 // Shift start position outwards as we had no stars inwards
1189 int range = NUM_STEPS_PRE_AF * newStepSize;
1190 int jumpPosition = m_position.last() + range;
1191 if (jumpPosition > maxStarsLimit())
1192 {
1193 range = maxStarsLimit() - m_position.last();
1194 newStepSize = range / NUM_STEPS_PRE_AF;
1195 m_preAFMaxRange = true;
1196 }
1197 startPosition = m_position.last() + (range / 2);
1198 }
1199 else if (m_preAFNoStarsOut)
1200 {
1201 // Shift start position inwards as we had no stars outwards
1202 int range = NUM_STEPS_PRE_AF * newStepSize;
1203 int endPosition = m_position.first() - range;
1204 if (endPosition < minStarsLimit())
1205 {
1206 range = m_position.first() - minStarsLimit();
1207 newStepSize = range / NUM_STEPS_PRE_AF;
1208 m_preAFMaxRange = true;
1209 }
1210 startPosition = m_position.first() - (range / 2);
1211 }
1212 else
1213 // Set the start position to the previous minimum
1214 startPosition = minPos;
1215
1216 updateResultsTable(i18n("Max/Min Ratio: %1, Next Step Size: %2, Next Overscan: %3", QString::number(measureRatio, 'f', 1),
1217 QString::number(newStepSize), QString::number(overscan)));
1218 m_focus->m_OpsFocusMechanics->focusTicks->setValue(newStepSize);
1219 initPreAFAdj(startPosition);
1220 return;
1221 }
1222 }
1223 }
1224 m_focus->linearRequestedPosition = m_focus->currentPosition + deltaPos;
1225 if (!m_focus->changeFocus(deltaPos))
1226 abort(i18n("Focus Advisor Coarse Adjustment failed to move focuser"));
1227}
1228
1229// Check whether the Max / Min star measure ratio is good enough
1230bool FocusAdvisor::maxMinRatioOK(const double limit, const double maxMinRatio)
1231{
1232 if (m_preAFMaxRange)
1233 // We've hit the maximum focuser range where we have stars so go with what we've got in terms of maxMinRatio
1234 return true;
1235 return maxMinRatio >= limit;
1236}
1237
1238// Minimum focuser position where stars exist - if unsure about stars return min focuser position
1239int FocusAdvisor::minStarsLimit()
1240{
1241 return std::max(static_cast<int>(m_focus->absMotionMin), m_findStarsInRange);
1242}
1243
1244// Maximum focuser position where stars exist - if unsure about stars return max focuser position
1245int FocusAdvisor::maxStarsLimit()
1246{
1247 if (m_findStarsOutRange < 0)
1248 return m_focus->absMotionMax;
1249 else
1250 return std::min(static_cast<int>(m_focus->absMotionMax), m_findStarsOutRange);
1251}
1252
1253void FocusAdvisor::initAFAdj(const int startPos, const bool retryOverscan)
1254{
1255 // check whether Focus Advisor can run with the current parameters
1256 if (!canFocusAdvisorRun())
1257 {
1258 abort(i18n("Focus Advisor cannot run with current params"));
1259 return;
1260 }
1261
1262 focusAdvFineAdjLabel->setText(i18n("In progress..."));
1263 emit newStage(FineAdjustments);
1264
1265 // The preAF routine will have estimated AF Overscan but because its a crude measure its likely to be an overestimate.
1266 // We will try and refine the estimate by halving the current Overscan
1267 if (retryOverscan)
1268 {
1269 const int newOverscan = m_focus->m_OpsFocusMechanics->focusAFOverscan->value() / 2;
1270 m_focus->m_OpsFocusMechanics->focusBacklash->setValue(0);
1271 m_focus->m_OpsFocusMechanics->focusAFOverscan->setValue(newOverscan);
1272 }
1273
1274 // Reset useWeights setting
1275 m_focus->m_OpsFocusProcess->focusUseWeights->setChecked(m_initialUseWeights);
1276
1277 addResultsTable(i18n("Fine Adjustment"), ++m_AFRunCount, startPos,
1278 m_focus->m_OpsFocusMechanics->focusTicks->value(),
1279 m_focus->m_OpsFocusMechanics->focusAFOverscan->value(), "");
1280
1281 startAF(startPos);
1282}
1283
1284void FocusAdvisor::startAF(const int startPos)
1285{
1286 if (m_nearFocus)
1287 {
1288 setInFocusAdvisor(false);
1289 m_focus->absIterations = 0;
1290 m_focus->setupLinearFocuser(startPos);
1291 if (!m_focus->changeFocus(m_focus->linearRequestedPosition - m_focus->currentPosition))
1292 m_focus->completeFocusProcedure(Ekos::FOCUS_ABORTED, Ekos::FOCUS_FAIL_FOCUSER_NO_MOVE);
1293 }
1294 else
1295 {
1296 m_nearFocus = true;
1297 m_focus->initScanStartPos(true, startPos);
1298 }
1299}
1300
1301bool FocusAdvisor::analyseAF()
1302{
1303 if (m_focus->m_FocusAlgorithm != Focus::FOCUS_LINEAR1PASS || !m_focus->linearFocuser || !m_focus->linearFocuser->isDone()
1304 || m_focus->linearFocuser->solution() == -1)
1305 return false;
1306
1307 bool runAgainRatio = false;
1308 QVector<int> positions;
1309 QVector<double> measures, weights;
1310 QVector<bool> outliers;
1311 m_focus->linearFocuser->getPass1Measurements(&positions, &measures, &weights, &outliers);
1312
1313 int minPosition = m_focus->linearFocuser->solution();
1314 double minMeasure = m_focus->linearFocuser->solutionValue();
1315
1316 int maxPositon = positions[0];
1317 double maxMeasure = m_focus->curveFitting->f(maxPositon);
1318
1319 const int stepSize = m_focus->m_OpsFocusMechanics->focusTicks->value();
1320 int newStepSize = stepSize;
1321 // Firstly check that the step size is giving a good measureRatio
1322 double measureRatio = maxMeasure / minMeasure;
1323 if (measureRatio > 2.5 && measureRatio < 3.5)
1324 // Sweet spot
1325 runAgainRatio = false;
1326 else
1327 {
1328 runAgainRatio = true;
1329 // Adjust the step size to try and get the measureRatio around 3
1330 int pos = m_focus->curveFitting->fy(minMeasure * TARGET_MAXMIN_HFR_RATIO);
1331 newStepSize = (pos - minPosition) / ((positions.size() - 1.0) / 2.0);
1332 // Throttle newStepSize. Usually this stage should be close to good parameters so changes in step size
1333 // should be small, but if run with poor parameters changes should be throttled to prevent big swings that
1334 // can lead to Autofocus failure.
1335 double ratio = static_cast<double>(newStepSize) / static_cast<double>(stepSize);
1336 ratio = std::max(std::min(ratio, 2.0), 0.5);
1337 newStepSize = stepSize * ratio;
1338 m_focus->m_OpsFocusMechanics->focusTicks->setValue(newStepSize);
1339 }
1340
1341 // Look at the backlash
1342 // So assume flatness of curve at the outward point is all due to backlash.
1343 if (!m_overscanFound)
1344 {
1345 double backlashPoints = 0.0;
1346 for (int i = 0; i < positions.size() / 2; i++)
1347 {
1348 double deltaAct = measures[i] - measures[i + 1];
1349 double deltaExp = m_focus->curveFitting->f(positions[i]) - m_focus->curveFitting->f(positions[i + 1]);
1350 double delta = std::abs(deltaAct / deltaExp);
1351 // May have to play around with this threshold
1352 if (delta > 0.75)
1353 break;
1354 if (delta > 0.5)
1355 backlashPoints += 0.5;
1356 else
1357 backlashPoints++;
1358 }
1359
1360 const int overscan = m_focus->m_OpsFocusMechanics->focusAFOverscan->value();
1361 int newOverscan = overscan;
1362
1363 if (backlashPoints > 0.0)
1364 {
1365 // We've found some additional Overscan so we know the current value is too low and now have a reasonable estimate
1366 newOverscan = overscan + (stepSize * backlashPoints);
1367 m_overscanFound = true;
1368 }
1369 else if (overscan == 0)
1370 m_overscanFound = true;
1371 else if (!m_overscanFound)
1372 // No additional Overscan was detected so the current Overscan estimate may be too high so try reducing it
1373 newOverscan = overscan <= 2 * stepSize ? 0 : overscan / 2;
1374
1375 m_focus->m_OpsFocusMechanics->focusAFOverscan->setValue(newOverscan);
1376 }
1377
1378 // Try again for a poor R2 - but retry just once (unless something else changes so we don't get stuck in a loop
1379 if (m_runAgainR2)
1380 m_runAgainR2 = false;
1381 else
1382 m_runAgainR2 = m_focus->R2 < m_focus->m_OpsFocusProcess->focusR2Limit->value();
1383 bool runAgain = runAgainRatio || m_runAgainR2 || !m_overscanFound;
1384
1385 updateResultsTable(i18n("Max/Min Ratio: %1, R2: %2, Step Size: %3, Overscan: %4", QString::number(measureRatio, 'f', 1),
1386 QString::number(m_focus->R2, 'f', 3), QString::number(newStepSize),
1387 QString::number(m_focus->m_OpsFocusMechanics->focusAFOverscan->value())));
1388 if (!runAgain)
1389 {
1390 m_inAFAdj = false;
1391 focusAdvFineAdjLabel->setText(i18n("Done"));
1392 complete(true, i18n("Focus Advisor Fine Adjustment completed"));
1393 emit newStage(Idle);
1394 }
1395 return runAgain;
1396}
1397
1398// Reset the Focus Advisor
1399void FocusAdvisor::reset()
1400{
1401 m_inFocusAdvisor = false;
1402 m_initialStepSize = -1;
1403 m_initialBacklash = -1;
1404 m_initialAFOverscan = -1;
1405 m_initialUseWeights = false;
1406 m_initialScanStartPos = false;
1407 m_position.clear();
1408 m_measure.clear();
1409 m_inFindStars = false;
1410 m_inPreAFAdj = false;
1411 m_inAFAdj = false;
1412 m_jumpsToGo = 0;
1413 m_jumpSize = 0;
1414 m_findStarsIn = false;
1415 m_findStarsMaxBound = false;
1416 m_findStarsMinBound = false;
1417 m_findStarsSector = 0;
1418 m_findStarsRunNum = 0;
1419 m_findStarsRange = false;
1420 m_findStarsRangeIn = false;
1421 m_findStarsFindInEdge = false;
1422 m_findStarsInRange = -1;
1423 m_findStarsOutRange = -1;
1424 m_preAFInner = 0;
1425 m_preAFNoStarsOut = false;
1426 m_preAFNoStarsIn = false;
1427 m_preAFMaxRange = false;
1428 m_preAFRunNum = 0;
1429 m_overscanFound = false;
1430 m_runAgainR2 = false;
1431 m_nearFocus = false;
1432 m_AFRunCount = 0;
1433 setButtons(false);
1434 focusAdvUpdateParamsLabel->setText("");
1435 focusAdvFindStarsLabel->setText("");
1436 focusAdvCoarseAdjLabel->setText("");
1437 focusAdvFineAdjLabel->setText("");
1438}
1439
1440// Abort the Focus Advisor
1441void FocusAdvisor::abort(const QString &msg)
1442{
1443 m_focus->appendLogText(msg);
1444
1445 // Restore settings to initial value
1446 resetSavedSettings(false);
1447 m_focus->processAbort();
1448}
1449
1450// Focus Advisor completed successfully
1451// If Autofocus was run then do the usual processing / notification of success, otherwise treat it as an autofocus fail
1452void FocusAdvisor::complete(const bool autofocus, const QString &msg)
1453{
1454 m_focus->appendLogText(msg);
1455 // Restore settings to initial value
1456 resetSavedSettings(true);
1457
1458 if (!autofocus)
1459 {
1460 if (m_initialBacklash > -1) m_focus->m_OpsFocusMechanics->focusBacklash->setValue(m_initialBacklash);
1461 if (m_initialAFOverscan > -1) m_focus->m_OpsFocusMechanics->focusAFOverscan->setValue(m_initialAFOverscan);
1462 m_focus->completeFocusProcedure(Ekos::FOCUS_IDLE, Ekos::FOCUS_FAIL_ADVISOR_COMPLETE);
1463 }
1464}
1465
1466void FocusAdvisor::resetSavedSettings(const bool success)
1467{
1468 m_focus->m_OpsFocusProcess->focusUseWeights->setChecked(m_initialUseWeights);
1469 m_focus->m_OpsFocusProcess->focusScanStartPos->setChecked(m_initialScanStartPos);
1470
1471 if (!success)
1472 {
1473 // Restore settings to initial value since Focus Advisor failed
1474 if (m_initialStepSize > -1) m_focus->m_OpsFocusMechanics->focusTicks->setValue(m_initialStepSize);
1475 if (m_initialBacklash > -1) m_focus->m_OpsFocusMechanics->focusBacklash->setValue(m_initialBacklash);
1476 if (m_initialAFOverscan > -1) m_focus->m_OpsFocusMechanics->focusAFOverscan->setValue(m_initialAFOverscan);
1477 }
1478}
1479
1480// Add a new row to the results table
1481void FocusAdvisor::addResultsTable(QString section, int run, int startPos, int stepSize, int overscan, QString text)
1482{
1483 focusAdvTable->insertRow(0);
1484 QTableWidgetItem *itemSection = new QTableWidgetItem(section);
1485 focusAdvTable->setItem(0, RESULTS_SECTION, itemSection);
1486 QString runStr = (run >= 0) ? QString("%1").arg(run) : "N/A";
1487 QTableWidgetItem *itemRunNumber = new QTableWidgetItem(runStr);
1489 focusAdvTable->setItem(0, RESULTS_RUN_NUMBER, itemRunNumber);
1490 QString startPosStr = (startPos >= 0) ? QString("%1").arg(startPos) : "N/A";
1491 QTableWidgetItem *itemStartPos = new QTableWidgetItem(startPosStr);
1493 focusAdvTable->setItem(0, RESULTS_START_POSITION, itemStartPos);
1494 QString stepSizeStr = (stepSize >= 0) ? QString("%1").arg(stepSize) : "N/A";
1495 QTableWidgetItem *itemStepSize = new QTableWidgetItem(stepSizeStr);
1497 focusAdvTable->setItem(0, RESULTS_STEP_SIZE, itemStepSize);
1498 QString overscanStr = (stepSize >= 0) ? QString("%1").arg(overscan) : "N/A";
1499 QTableWidgetItem *itemAFOverscan = new QTableWidgetItem(overscanStr);
1501 focusAdvTable->setItem(0, RESULTS_AFOVERSCAN, itemAFOverscan);
1502 QTableWidgetItem *itemText = new QTableWidgetItem(text);
1503 focusAdvTable->setItem(0, RESULTS_TEXT, itemText);
1504
1505 emit newMessage(text);
1506
1507 if (focusAdvTable->rowCount() == 1)
1508 {
1509 focusAdvTable->show();
1510 resizeDialog();
1511 }
1512}
1513
1514// Update text for current row (0) in the results table with the passed in value
1515void FocusAdvisor::updateResultsTable(QString text)
1516{
1517 QTableWidgetItem *itemText = new QTableWidgetItem(text);
1518 focusAdvTable->setItem(0, RESULTS_TEXT, itemText);
1519 focusAdvTable->horizontalHeader()->resizeSections(QHeaderView::ResizeToContents);
1520}
1521
1522void FocusAdvisor::resizeDialog()
1523{
1524 focusAdvTable->horizontalHeader()->resizeSections(QHeaderView::ResizeToContents);
1525 int left, right, top, bottom;
1526 this->verticalLayout->layout()->getContentsMargins(&left, &top, &right, &bottom);
1527
1528 int width = left + right;
1529 for (int i = 0; i < focusAdvTable->horizontalHeader()->count(); i++)
1530 width += focusAdvTable->columnWidth(i);
1531 const int height = focusAdvGroupBox->height() + focusAdvTable->height() + focusAdvButtonBox->height();
1532 focusAdvTable->horizontalHeader()->height() + focusAdvTable->rowHeight(0);
1533 this->resize(width, height);
1534}
1535}
QString i18n(const char *text, const TYPE &arg...)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:79
void clicked(bool checked)
void stateChanged(int state)
int pointSize() const const
void setBold(bool enable)
void setPointSize(int pointSize)
void setUnderline(bool enable)
iterator begin()
iterator end()
void push_back(parameter_type value)
qsizetype size() const const
QString arg(Args &&... args) const const
bool isEmpty() const const
QString number(double n, char format, int precision)
AlignRight
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
QFont font() const const
void setFont(const QFont &font)
void setText(const QString &text)
void setTextAlignment(Qt::Alignment alignment)
void setToolTip(const QString &toolTip)
QFuture< void > map(Iterator begin, Iterator end, MapFunctor &&function)
QFuture< T > run(Function function,...)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Aug 30 2024 11:50:34 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.