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

KDE's Doxygen guidelines are available online.