Warning, file /education/kstars/kstars/ekos/focus/aberrationinspector.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002     SPDX-FileCopyrightText: 2023 John Evans <john.e.evans.email@googlemail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "aberrationinspector.h"
0008 #include "aberrationinspectorplot.h"
0009 #include "sensorgraphic.h"
0010 #include <kstars_debug.h>
0011 #include "kstars.h"
0012 #include "Options.h"
0013 #include <QSplitter>
0014 
0015 const float RADIANS2DEGREES = 360.0f / (2.0f * M_PI);
0016 
0017 namespace Ekos
0018 {
0019 
0020 AberrationInspector::AberrationInspector(const abInsData &data, const QVector<int> &positions,
0021         const QVector<QVector<double>> &measures, const QVector<QVector<double>> &weights,
0022         const QVector<QVector<int>> &numStars, const QVector<QPoint> &tileCenterOffsets) :
0023     m_data(data), m_positions(positions), m_measures(measures), m_weights(weights),
0024     m_numStars(numStars), m_tileOffsets(tileCenterOffsets)
0025 {
0026 #ifdef Q_OS_OSX
0027     setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
0028 #endif
0029 
0030     // 1. Setup the GUI
0031     setupUi(this);
0032     setupGUI();
0033 
0034     // 2. Initialise the widget
0035     initAberrationInspector();
0036 
0037     // 3. Curve fit the data for each tile and update Aberration Inspector with results
0038     fitCurves();
0039 
0040     // 4. Initialise the 3D graphic
0041     initGraphic();
0042 
0043     // 5. Restore persisted settings
0044     loadSettings();
0045 
0046     // 6. connect signals to persist changes to user settings
0047     connectSettings();
0048 
0049     // Display data appropriate to the tile selection
0050     setTileSelection(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
0051 }
0052 
0053 AberrationInspector::~AberrationInspector()
0054 {
0055 }
0056 
0057 void AberrationInspector::setupGUI()
0058 {
0059     // Set the title. Use run number to differentiate runs
0060     this->setWindowTitle(i18n("Aberration Inspector - Run %1", m_data.run));
0061 
0062     // Connect up button callbacks
0063     connect(aberrationInspectorButtonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, [this]()
0064     {
0065         this->done(QDialog::Accepted);
0066     });
0067 
0068     connect(abInsTileSelection, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [&](int index)
0069     {
0070         setTileSelection(static_cast<TileSelection>(index));
0071     });
0072 
0073     connect(abInsShowLabels, &QCheckBox::toggled, this, [&](bool setting)
0074     {
0075         setShowLabels(setting);
0076     });
0077 
0078     connect(abInsShowCFZ, &QCheckBox::toggled, this, [&](bool setting)
0079     {
0080         setShowCFZ(setting);
0081     });
0082 
0083     connect(abInsOptCentres, &QCheckBox::toggled, this, [&](bool setting)
0084     {
0085         setOptCentres(setting);
0086     });
0087 
0088     // Create a plot widget and add to the top of the dialog
0089     m_plot = new AberrationInspectorPlot(this);
0090     abInsPlotLayout->addWidget(m_plot);
0091 
0092     // Setup the results table
0093     QStringList Headers { i18n("Tile"), i18n("Description"), i18n("Solution"), i18n("Delta (ticks)"), i18n("Delta (μm)"), i18n("Num Stars"), i18n("R²"), i18n("Exclude")};
0094     abInsTable->setColumnCount(Headers.count());
0095     abInsTable->setHorizontalHeaderLabels(Headers);
0096 
0097     // Setup tooltips on column headers
0098     abInsTable->horizontalHeaderItem(0)->setToolTip(i18n("Tile"));
0099     abInsTable->horizontalHeaderItem(1)->setToolTip(i18n("Description"));
0100     abInsTable->horizontalHeaderItem(2)->setToolTip(i18n("Focuser Solution"));
0101     abInsTable->horizontalHeaderItem(3)->setToolTip(i18n("Delta from central tile in ticks"));
0102     abInsTable->horizontalHeaderItem(4)->setToolTip(i18n("Delta from central tile in micrometers"));
0103     abInsTable->horizontalHeaderItem(5)->setToolTip(i18n("Min / max number of stars detected in the focus run"));
0104     abInsTable->horizontalHeaderItem(6)->setToolTip(i18n("R²"));
0105     abInsTable->horizontalHeaderItem(7)->setToolTip(i18n("Check to exclude row from calculations"));
0106 
0107     // Prevent editing of table widget (except the exclude checkboxes) unless in production support mode
0108     QAbstractItemView::EditTrigger editTrigger = ABINS_DEBUG ? QAbstractItemView::DoubleClicked :
0109             QAbstractItemView::NoEditTriggers;
0110     abInsTable->setEditTriggers(editTrigger);
0111 
0112     // Connect up table widget events: cellEntered when mouse moves over a cell
0113     connect(abInsTable, &AbInsTableWidget::cellEntered, this, &AberrationInspector::newMousePos);
0114     // leaveTableEvent is signalled when the mouse is moved away from "table".
0115     connect(abInsTable, &AbInsTableWidget::leaveTableEvent, this, &AberrationInspector::leaveTableEvent);
0116 
0117     // Setup a SensorGraphic minimal window that acts like a visual tooltip
0118     sensorGraphic = new SensorGraphic(abInsTable, m_data.sensorWidth, m_data.sensorHeight, m_data.tileWidth);
0119 }
0120 
0121 // Mouse over table row, col. Show the sensor graphic
0122 void AberrationInspector::newMousePos(int row, int column)
0123 {
0124     if (column <= 1)
0125     {
0126         QTableWidgetItem *tile = abInsTable->item(row, 0);
0127         for (int i = 0; i < NUM_TILES; i++)
0128         {
0129             if (TILE_NAME[i] == tile->text())
0130             {
0131                 // Set the highlight row
0132                 sensorGraphic->setHighlight(i);
0133                 // Move the graphic to the mouse position
0134                 sensorGraphic->move(QCursor::pos());
0135                 // If the graphic is hidden then show it; if details have changed then repaint it
0136                 if (m_HighlightedRow != -1 && m_HighlightedRow != i)
0137                     sensorGraphic->repaint();
0138                 else
0139                     sensorGraphic->show();
0140                 m_HighlightedRow = row;
0141                 return;
0142             }
0143         }
0144     }
0145     m_HighlightedRow = -1;
0146     sensorGraphic->hide();
0147 }
0148 
0149 // Called when table widget gets a mouse leave event; hide the sensor graphic
0150 void AberrationInspector::leaveTableEvent()
0151 {
0152     m_HighlightedRow = -1;
0153     if (sensorGraphic)
0154         sensorGraphic->hide();
0155 }
0156 
0157 // Called when the "Exclude" checkbox state is changed by the user
0158 void AberrationInspector::onStateChanged(int state)
0159 {
0160     Q_UNUSED(state);
0161 
0162     // Get the row and state of the checkbox
0163     QCheckBox* cb = qobject_cast<QCheckBox *>(QObject::sender());
0164     int row = cb->property("row").toInt();
0165     bool checked = cb->isChecked();
0166 
0167     // Set the exclude flag for the excluded tile
0168     setExcludeTile(row, checked, static_cast<TileSelection>(abInsTileSelection->currentIndex()));
0169     // Rerun the calcs so the newly changed exclude flag is taken into account
0170     setTileSelection(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
0171 }
0172 
0173 // Called when table cell has been changed
0174 // This is a production support debug feature that is contolled by ABINS_DEBUG flag
0175 // For normal use table editing is disabled and this function not called.
0176 void AberrationInspector::onCellChanged(int row, int column)
0177 {
0178     if (column != 3)
0179         return;
0180 
0181     // Get the new value
0182     QTableWidgetItem *item = abInsTable->item(row, column);
0183     int value = item->text().toInt();
0184 
0185     int tile = getTileFromRow(static_cast<TileSelection>(abInsTileSelection->currentIndex()), row);
0186 
0187     if (tile >= 0 && tile < NUM_TILES)
0188     {
0189         m_minimum[tile] = value + m_minimum[TILE_CM];
0190         // Rerun the calcs so the newly changed exclude flag is taken into account
0191         setTileSelection(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
0192     }
0193 }
0194 
0195 int AberrationInspector::getTileFromRow(TileSelection tileSelection, int row)
0196 {
0197     int tile = -1;
0198 
0199     if (row < 0 || row >= NUM_TILES)
0200     {
0201         qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called with invalid row %2").arg(__FUNCTION__).arg(row);
0202         return tile;
0203     }
0204 
0205     switch(tileSelection)
0206     {
0207         case TileSelection::TILES_ALL:
0208             // Use all tiles
0209             tile = row;
0210             break;
0211 
0212         case TileSelection::TILES_OUTER_CORNERS:
0213             // Use tiles 0, 2, 4, 6, 8
0214             tile = row * 2;
0215             break;
0216 
0217         case TileSelection::TILES_INNER_DIAMOND:
0218             // Use tiles 1, 3, 4, 5, 7
0219             if (row < 2)
0220                 tile = (row * 2) + 1;
0221             else if (row == 2)
0222                 tile = 4;
0223             else
0224                 tile = (row * 2) - 1;
0225             break;
0226 
0227         default:
0228             qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called but TileSelection invalid").arg(__FUNCTION__);
0229             break;
0230     }
0231     return tile;
0232 }
0233 
0234 void AberrationInspector::setExcludeTile(int row, bool checked, TileSelection tileSelection)
0235 {
0236     if (row < 0 || row >= NUM_TILES)
0237     {
0238         qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called with invalid row %2").arg(__FUNCTION__).arg(row);
0239         return;
0240     }
0241 
0242     int tile = getTileFromRow(tileSelection, row);
0243     if (tile >= 0 && tile < NUM_TILES)
0244         m_excludeTile[tile] = checked;
0245 }
0246 
0247 void AberrationInspector::connectSettings()
0248 {
0249     // All Combo Boxes
0250     for (auto &oneWidget : findChildren<QComboBox*>())
0251         connect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::AberrationInspector::syncSettings);
0252 
0253     // All Checkboxes, except Sim mode checkbox - this should be defaulted to off when Aberratio Inspector starts
0254     for (auto &oneWidget : findChildren<QCheckBox*>())
0255         if (oneWidget != abInsSimMode)
0256             connect(oneWidget, &QCheckBox::toggled, this, &Ekos::AberrationInspector::syncSettings);
0257 
0258     // All Splitters
0259     for (auto &oneWidget : findChildren<QSplitter*>())
0260         connect(oneWidget, &QSplitter::splitterMoved, this, &Ekos::AberrationInspector::syncSettings);
0261 }
0262 
0263 void AberrationInspector::loadSettings()
0264 {
0265     QString key;
0266     QVariant value;
0267 
0268     // All Combo Boxes
0269     for (auto &oneWidget : findChildren<QComboBox*>())
0270     {
0271         key = oneWidget->objectName();
0272         value = Options::self()->property(key.toLatin1());
0273         if (value.isValid())
0274             oneWidget->setCurrentText(value.toString());
0275         else
0276             qCDebug(KSTARS_EKOS_FOCUS) << "ComboBox Option" << key << "not found!";
0277     }
0278 
0279     // All Checkboxes
0280     for (auto &oneWidget : findChildren<QCheckBox*>())
0281     {
0282         key = oneWidget->objectName();
0283         if (key == abInsSimMode->objectName() || key == "")
0284             // Sim mode setting isn't persisted as it is always off on Aberration Inspector start.
0285             // Also the table widget has a column of checkboxes whose value is data dependent so these aren't persisted
0286             continue;
0287 
0288         value = Options::self()->property(key.toLatin1());
0289         if (value.isValid())
0290             oneWidget->setChecked(value.toBool());
0291         else
0292             qCDebug(KSTARS_EKOS_FOCUS) << "Checkbox Option" << key << "not found!";
0293     }
0294 
0295     // All Splitters
0296     for (auto &oneWidget : findChildren<QSplitter*>())
0297     {
0298         key = oneWidget->objectName();
0299         value = Options::self()->property(key.toLatin1());
0300         if (value.isValid())
0301         {
0302             // Convert the saved QString to a QByteArray using Base64
0303             auto valueBA = QByteArray::fromBase64(value.toString().toUtf8());
0304             oneWidget->restoreState(valueBA);
0305         }
0306         else
0307             qCDebug(KSTARS_EKOS_FOCUS) << "Splitter Option" << key << "not found!";
0308     }
0309 }
0310 
0311 void AberrationInspector::syncSettings()
0312 {
0313     QComboBox *cbox = nullptr;
0314     QCheckBox *cb = nullptr;
0315     QSplitter *s = nullptr;
0316 
0317     QString key;
0318     QVariant value;
0319 
0320     if ( (cbox = qobject_cast<QComboBox*>(sender())))
0321     {
0322         key = cbox->objectName();
0323         value = cbox->currentText();
0324     }
0325     else if ( (cb = qobject_cast<QCheckBox*>(sender())))
0326     {
0327         key = cb->objectName();
0328         value = cb->isChecked();
0329     }
0330     else if ( (s = qobject_cast<QSplitter*>(sender())))
0331     {
0332         key = s->objectName();
0333         // Convert from the QByteArray to QString using Base64
0334         value = QString::fromUtf8(s->saveState().toBase64());
0335     }
0336 
0337     // Save changed setting
0338     Options::self()->setProperty(key.toLatin1(), value);
0339     Options::self()->save();
0340 }
0341 
0342 // Setup display widgets to match tileSelection
0343 void AberrationInspector::setTileSelection(TileSelection tileSelection)
0344 {
0345     // Setup array of tiles to use, based on user selection
0346     setupTiles(tileSelection);
0347     // Redraw the v-curves based on user selection
0348     m_plot->redrawCurve(m_useTile);
0349     // Update the table widget based on user selection
0350     updateTable();
0351     // Update the results based on user selection
0352     analyseResults();
0353     // Update the 3D graphic based on user selection
0354     updateGraphic(tileSelection);
0355     // resize table widget based on contents
0356     tableResize();
0357     // Updates changes to the v-curves
0358     m_plot->replot();
0359 }
0360 
0361 void AberrationInspector::setupTiles(TileSelection tileSelection)
0362 {
0363     switch(tileSelection)
0364     {
0365         case TileSelection::TILES_ALL:
0366             // Use all tiles
0367             for (int i = 0; i < NUM_TILES; i++)
0368                 m_useTile[i] = true;
0369             break;
0370 
0371         case TileSelection::TILES_OUTER_CORNERS:
0372             // Use tiles TL, TR, CM, BL, BR
0373             m_useTile[TILE_TL] = m_useTile[TILE_TR] = m_useTile[TILE_CM] = m_useTile[TILE_BL] = m_useTile[TILE_BR] = true;
0374             m_useTile[TILE_TM] = m_useTile[TILE_CL] = m_useTile[TILE_CR] = m_useTile[TILE_BM] = false;
0375             break;
0376 
0377         case TileSelection::TILES_INNER_DIAMOND:
0378             // Use tiles TM, CL, CM, CR, BM
0379             m_useTile[TILE_TL] = m_useTile[TILE_TR] = m_useTile[TILE_BL] = m_useTile[TILE_BR] = false;
0380             m_useTile[TILE_TM] = m_useTile[TILE_CL] = m_useTile[TILE_CM] = m_useTile[TILE_CR] = m_useTile[TILE_BM] = true;
0381             break;
0382 
0383         default:
0384             qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called with invalid tile selection %2").arg(__FUNCTION__).arg(tileSelection);
0385             break;
0386     }
0387 }
0388 
0389 void AberrationInspector::setShowLabels(bool setting)
0390 {
0391     m_plot->setShowLabels(setting);
0392     m_plot->replot();
0393 }
0394 
0395 void AberrationInspector::setShowCFZ(bool setting)
0396 {
0397     m_plot->setShowCFZ(setting);
0398     m_plot->replot();
0399 }
0400 
0401 // Rerun calculations to take the Optimise Tile Centres setting into account
0402 void AberrationInspector::setOptCentres(bool setting)
0403 {
0404     Q_UNUSED(setting);
0405     setTileSelection(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
0406 }
0407 
0408 void AberrationInspector::initAberrationInspector()
0409 {
0410     // Initialise the plot widget
0411     m_plot->init(m_data.yAxisLabel, m_data.starUnits, m_data.useWeights, abInsShowLabels->isChecked(),
0412                  abInsShowCFZ->isChecked());
0413 }
0414 
0415 // Resize the dialog to the data
0416 void AberrationInspector::tableResize()
0417 {
0418     // Resize the table columns to fit the data on display in it.
0419     abInsTable->horizontalHeader()->resizeSections(QHeaderView::ResizeToContents);
0420     abInsTable->verticalHeader()->resizeSections(QHeaderView::ResizeToContents);
0421 }
0422 
0423 // Run curve fitting on the collected data for each tile, updating other widgets as we go
0424 void AberrationInspector::fitCurves()
0425 {
0426     curveFitting.reset(new CurveFitting());
0427 
0428     const double expected = 0.0;
0429     int minPos, maxPos;
0430 
0431     QVector<bool> outliers;
0432     for (int i = 0; i < m_positions.count(); i++)
0433     {
0434         outliers.append(false);
0435         if (i == 0)
0436             minPos = maxPos = m_positions[i];
0437         else
0438         {
0439             minPos = std::min(minPos, m_positions[i]);
0440             maxPos = std::max(maxPos, m_positions[i]);
0441         }
0442     }
0443 
0444     for (int tile = 0; tile < m_measures.count(); tile++)
0445     {
0446         curveFitting->fitCurve(CurveFitting::FittingGoal::BEST, m_positions, m_measures[tile], m_weights[tile], outliers,
0447                                m_data.curveFit, m_data.useWeights, m_data.optDir);
0448 
0449         double position = 0.0;
0450         double measure = 0.0;
0451         double R2 = 0.0;
0452         bool foundFit = curveFitting->findMinMax(expected, static_cast<double>(minPos), static_cast<double>(maxPos), &position,
0453                         &measure, m_data.curveFit, m_data.optDir);
0454         if (foundFit)
0455             R2 = curveFitting->calculateR2(m_data.curveFit);
0456 
0457         position = round(position);
0458         m_minimum.append(position);
0459         m_minMeasure.append(measure);
0460         m_fit.append(foundFit);
0461         m_R2.append(R2);
0462 
0463         // Add the datapoints to the plot for the current tile
0464         // JEE Need to sort out what to do with outliers... for now ignore them
0465         QVector<bool> outliers;
0466         for (int i = 0; i < m_measures[tile].count(); i++)
0467         {
0468             outliers.append(false);
0469         }
0470 
0471         m_plot->addData(m_positions, m_measures[tile], m_weights[tile], outliers);
0472         // Fit the curve - note this needs curveFitting with the parameters for the current solution
0473         m_plot->drawCurve(tile, curveFitting.get(), position, measure, foundFit, R2);
0474         // Draw solutions on the plot
0475         m_plot->drawMaxMin(tile, position, measure);
0476         // Draw the CFZ for the central tile
0477         if (tile == TILE_CM)
0478             m_plot->drawCFZ(position, measure, m_data.cfzSteps);
0479     }
0480 }
0481 
0482 // Update the results table
0483 void AberrationInspector::updateTable()
0484 {
0485     // Disconnect the cell changed callback to stop it firing whilst the table is updated
0486     disconnect(abInsTable, &AbInsTableWidget::cellChanged, this, &AberrationInspector::onCellChanged);
0487 
0488     if (abInsTileSelection->currentIndex() == TILES_ALL)
0489         abInsTable->setRowCount(NUM_TILES);
0490     else
0491         abInsTable->setRowCount(5);
0492 
0493     int rowCounter = -1;
0494     for (int i = 0; i < NUM_TILES; i++)
0495     {
0496         if (!m_useTile[i])
0497             continue;
0498 
0499         ++rowCounter;
0500 
0501         QTableWidgetItem *tile = new QTableWidgetItem(TILE_NAME[i]);
0502         tile->setForeground(QColor(TILE_COLOUR[i]));
0503         abInsTable->setItem(rowCounter, 0, tile);
0504 
0505         QTableWidgetItem *description = new QTableWidgetItem(TILE_LONGNAME[i]);
0506         abInsTable->setItem(rowCounter, 1, description);
0507 
0508         QTableWidgetItem *solution = new QTableWidgetItem(QString::number(m_minimum[i]));
0509         solution->setTextAlignment(Qt::AlignRight);
0510         abInsTable->setItem(rowCounter, 2, solution);
0511 
0512         int ticks = m_minimum[i] - m_minimum[TILE_CM];
0513         QTableWidgetItem *deltaTicks = new QTableWidgetItem(QString::number(ticks));
0514         deltaTicks->setTextAlignment(Qt::AlignRight);
0515         abInsTable->setItem(rowCounter, 3, deltaTicks);
0516 
0517         int microns = ticks * m_data.focuserStepMicrons;
0518         QTableWidgetItem *deltaMicrons = new QTableWidgetItem(QString::number(microns));
0519         deltaMicrons->setTextAlignment(Qt::AlignRight);
0520         abInsTable->setItem(rowCounter, 4, deltaMicrons);
0521 
0522         int minNumStars = *std::min_element(m_numStars[i].constBegin(), m_numStars[i].constEnd());
0523         int maxNumStars = *std::max_element(m_numStars[i].constBegin(), m_numStars[i].constEnd());
0524         QTableWidgetItem *numStars = new QTableWidgetItem(QString("%1 / %2").arg(minNumStars).arg(maxNumStars));
0525         numStars->setTextAlignment(Qt::AlignRight);
0526         abInsTable->setItem(rowCounter, 5, numStars);
0527 
0528         QTableWidgetItem *R2 = new QTableWidgetItem(QString("%1").arg(m_R2[i], 0, 'f', 2));
0529         R2->setTextAlignment(Qt::AlignRight);
0530         abInsTable->setItem(rowCounter, 6, R2);
0531 
0532         QWidget *checkBoxWidget = new QWidget(abInsTable);
0533         QCheckBox *checkBox = new QCheckBox();
0534         // Set the checkbox based on whether curve fitting worked
0535         checkBox->setChecked(!m_fit[i] || m_excludeTile[i]);
0536         checkBox->setEnabled(m_fit[i]);
0537         // Add a property to identify the row when the user changes the check state.
0538         checkBox->setProperty("row", rowCounter);
0539         // In order to centre the widget, we need to insert it into a layout and align that
0540         QHBoxLayout *layoutCheckBox = new QHBoxLayout(checkBoxWidget);
0541         layoutCheckBox->addWidget(checkBox);
0542         layoutCheckBox->setAlignment(Qt::AlignCenter);
0543 
0544         abInsTable->setCellWidget(rowCounter, 7, checkBoxWidget);
0545         // The tableWidget cellChanged event doesn't fire when the checkbox state is changed.
0546         // Seems like the only way to get the event is to connect up directly to the checkbox
0547         connect(checkBox, &QCheckBox::stateChanged, this, &AberrationInspector::onStateChanged);
0548     }
0549     connect(abInsTable, &AbInsTableWidget::cellChanged, this, &AberrationInspector::onCellChanged);
0550 
0551     // Update the sensor graphic with the current tile selection
0552     sensorGraphic->setTiles(m_useTile);
0553 }
0554 
0555 // Analyse the results for backfocus delta and tilt based on tile selection.
0556 void AberrationInspector::analyseResults()
0557 {
0558     // Backfocus
0559     // +result = move sensor nearer field flattener
0560     // -result = move sensor further from field flattener
0561     bool backfocusOK = calcBackfocusDelta(static_cast<TileSelection>(abInsTileSelection->currentIndex()), m_backfocus);
0562 
0563     // Update backfocus screen widgets
0564     if (!backfocusOK)
0565         backfocus->setText(i18n("N/A"));
0566     else
0567     {
0568         QString side = "";
0569         if (m_backfocus < -0.9)
0570             side = i18n("Move sensor nearer flattener");
0571         else if (m_backfocus > 0.9)
0572             side = i18n("Move sensor away from flattener");
0573         backfocus->setText(QString("%1μm. %2").arg(m_backfocus, 0, 'f', 0).arg(side));
0574     }
0575 
0576     // Tilt
0577     bool tiltOK = calcTilt();
0578 
0579     // Update tilt screen widgets
0580     if (!tiltOK)
0581     {
0582         lrTilt->setText(i18n("N/A"));
0583         tbTilt->setText(i18n("N/A"));
0584         totalTilt->setText(i18n("N/A"));
0585     }
0586     else
0587     {
0588         lrTilt->setText(QString("%1μm / %2%").arg(m_LRMicrons, 0, 'f', 0).arg(m_LRTilt, 0, 'f', 2));
0589         tbTilt->setText(QString("%1μm / %2%").arg(m_TBMicrons, 0, 'f', 0).arg(m_TBTilt, 0, 'f', 2));
0590         totalTilt->setText(QString("%1μm / %2%").arg(m_diagonalMicrons, 0, 'f', 0)
0591                            .arg(m_diagonalTilt, 0, 'f', 2));
0592     }
0593 
0594     // Set m_resultsOK dependent on whether calcs were successful or not
0595     m_resultsOK = backfocusOK && tiltOK;
0596 }
0597 
0598 // Calculate the backfocus in microns
0599 // Note that the tile positions are at different distances from the sensor centre so we need to weight
0600 // values by the distance to tile centre. Also, we only include tiles for which curve fitting worked
0601 // and for which the user hasn't elected to exclude
0602 bool AberrationInspector::calcBackfocusDelta(TileSelection tileSelection, double &backfocusDelta)
0603 {
0604     backfocusDelta = 0.0;
0605 
0606     // Firstly check that we have a valid central tile - we can't do anything without that
0607     if (!m_fit[TILE_CM] || m_excludeTile[TILE_CM])
0608         return false;
0609 
0610     double dist = 0.0, sum = 0.0, counter = 0.0;
0611 
0612     switch(tileSelection)
0613     {
0614         case TileSelection::TILES_ALL:
0615             // Use all useable tiles weighted by their distance from the centre
0616             for (int i = 0; i < NUM_TILES; i++)
0617             {
0618                 if (i == TILE_CM)
0619                     continue;
0620 
0621                 if (m_fit[i] && !m_excludeTile[i])
0622                 {
0623                     dist = getXYTileCentre(static_cast<tileID>(i)).length();
0624                     sum += m_minimum[i] * dist;
0625                     counter += dist;
0626                 }
0627             }
0628             if (counter == 0)
0629                 // No valid tiles so can't complete the calc
0630                 return false;
0631             break;
0632 
0633         case TileSelection::TILES_OUTER_CORNERS:
0634             // All tiles are diagonal from centre... so no need to weight the calc
0635             // Use tiles 0, 2, 6, 8
0636             if (m_fit[0] && !m_excludeTile[0])
0637             {
0638                 sum += m_minimum[0];
0639                 counter++;
0640             }
0641             if (m_fit[2] && !m_excludeTile[2])
0642             {
0643                 sum += m_minimum[2];
0644                 counter++;
0645             }
0646             if (m_fit[6] && !m_excludeTile[6])
0647             {
0648                 sum += m_minimum[6];
0649                 counter++;
0650             }
0651             if (m_fit[8] && !m_excludeTile[8])
0652             {
0653                 sum += m_minimum[8];
0654                 counter++;
0655             }
0656 
0657             if (counter == 0)
0658                 // No valid tiles so can't complete the calc
0659                 return false;
0660             break;
0661 
0662         case TileSelection::TILES_INNER_DIAMOND:
0663             // Tiles are different distances from centre... so need to weight the calc
0664             // Use tiles 1, 3, 5, 7
0665             if (m_fit[TILE_TM] && !m_excludeTile[TILE_TM])
0666             {
0667                 dist = getXYTileCentre(TILE_TM).length();
0668                 sum += m_minimum[TILE_TM] * dist;
0669                 counter += dist;
0670             }
0671             if (m_fit[TILE_CL] && !m_excludeTile[TILE_CL])
0672             {
0673                 dist = getXYTileCentre(TILE_CL).length();
0674                 sum += m_minimum[TILE_CL] * dist;
0675                 counter += dist;
0676             }
0677             if (m_fit[TILE_CR] && !m_excludeTile[TILE_CR])
0678             {
0679                 dist = getXYTileCentre(TILE_CR).length();
0680                 sum += m_minimum[TILE_CR] * dist;
0681                 counter += dist;
0682             }
0683             if (m_fit[TILE_BM] && !m_excludeTile[TILE_BM])
0684             {
0685                 dist = getXYTileCentre(TILE_BM).length();
0686                 sum += m_minimum[TILE_BM] * dist;
0687                 counter += dist;
0688             }
0689 
0690             if (counter == 0)
0691                 // No valid tiles so can't complete the calc
0692                 return false;
0693             break;
0694 
0695         default:
0696             qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called with invalid tile selection %2").arg(__FUNCTION__).arg(tileSelection);
0697             return false;
0698             break;
0699     }
0700     backfocusDelta = (m_minimum[TILE_CM] - (sum / counter)) * m_data.focuserStepMicrons;
0701     return true;
0702 }
0703 
0704 bool AberrationInspector::calcTilt()
0705 {
0706     // Firstly check that we have a valid central tile - we can't do anything without that
0707     if (!m_fit[TILE_CM] || m_excludeTile[TILE_CM])
0708         return false;
0709 
0710     // Calculate the deltas relative to the centre tile in microns
0711     m_deltas.clear();
0712     for (int tile = 0; tile < NUM_TILES; tile++)
0713         m_deltas.append((m_minimum[TILE_CM] - m_minimum[tile]) * m_data.focuserStepMicrons);
0714 
0715     // Calculate the average tile position for left, right, top and bottom. If any of these cannot be
0716     // calculated then fail the whole calculation.
0717     double avLeft, avRight, avTop, avBottom;
0718     int tilesL[3] = { 0, 3, 6 };
0719     if (!avTiles(tilesL, avLeft))
0720         return false;
0721     int tilesR[3] = { 2, 5, 8 };
0722     if (!avTiles(tilesR, avRight))
0723         return false;
0724     int tilesT[3] = { 0, 1, 2 };
0725     if (!avTiles(tilesT, avTop))
0726         return false;
0727     int tilesB[3] = { 6, 7, 8 };
0728     if(!avTiles(tilesB, avBottom))
0729         return false;
0730 
0731     m_LRMicrons = avLeft - avRight;
0732     m_TBMicrons = avTop - avBottom;
0733     m_diagonalMicrons = std::hypot(m_LRMicrons, m_TBMicrons);
0734 
0735     // Calculate the sensor spans in microns
0736     const double LRSpan = (m_data.sensorWidth - m_data.tileWidth) * m_data.pixelSize;
0737     const double TBSpan = (m_data.sensorHeight - m_data.tileWidth) * m_data.pixelSize;
0738 
0739     // Calculate the tilt as a % slope
0740     m_LRTilt = m_LRMicrons / LRSpan * 100.0;
0741     m_TBTilt = m_TBMicrons / TBSpan * 100.0;
0742     m_diagonalTilt = std::hypot(m_LRTilt, m_TBTilt);
0743     return true;
0744 }
0745 
0746 // Averages upto 3 passed in tile values.
0747 bool AberrationInspector::avTiles(int tiles[3], double &average)
0748 {
0749     double sum = 0.0;
0750     int counter = 0;
0751     for (int i = 0; i < 3; i++)
0752     {
0753         if (m_useTile[tiles[i]] && m_fit[tiles[i]] && !m_excludeTile[tiles[i]])
0754         {
0755             sum += m_deltas[tiles[i]];
0756             counter++;
0757         }
0758     }
0759     if (counter > 0)
0760         average = sum / counter;
0761     return (counter > 0);
0762 }
0763 
0764 // Initialise the 3D graphic
0765 void AberrationInspector::initGraphic()
0766 {
0767     // Create a 3D Surface widget and add to the dialog
0768     m_graphic = new Q3DSurface();
0769     QWidget *container = QWidget::createWindowContainer(m_graphic);
0770 
0771     // abInsHSplitter is created in the .ui file but, by default, doesn't work - don't know why
0772     // Workaround is to create a new QSplitter object and use that.
0773     abInsHSplitter = new QSplitter(abInsVSplitter);
0774     abInsHSplitter->setObjectName(QString::fromUtf8("abInsHSplitter"));
0775     abInsHSplitter->addWidget(tableAndResultsWidget);
0776     abInsHSplitter->addWidget(widget);
0777     hLayout->insertWidget(0, container, 1);
0778     auto value = Options::abInsHSplitter();
0779     // Convert the saved QString to a QByteArray using Base64
0780     auto valueBA = QByteArray::fromBase64(value.toUtf8());
0781     abInsHSplitter->restoreState(valueBA);
0782     connect(abInsHSplitter, &QSplitter::splitterMoved, this, &Ekos::AberrationInspector::syncSettings);
0783 
0784     connect(abInsSelection, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [&](int index)
0785     {
0786         if (index == 0)
0787             m_graphic->setSelectionMode(QAbstract3DGraph::SelectionNone);
0788         else if (index == 1)
0789             m_graphic->setSelectionMode(QAbstract3DGraph::SelectionItem);
0790         else if (index == 2)
0791             m_graphic->setSelectionMode(QAbstract3DGraph::SelectionItemAndColumn | QAbstract3DGraph::SelectionSlice |
0792                                         QAbstract3DGraph::SelectionMultiSeries);
0793     });
0794 
0795     connect(abInsTheme, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [&](int index)
0796     {
0797         m_graphic->activeTheme()->setType(Q3DTheme::Theme(index));
0798     });
0799 
0800     connect(abInsLabels, &QCheckBox::toggled, this, [&](bool setting)
0801     {
0802         m_graphicLabels = setting;
0803         updateGraphic(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
0804     });
0805 
0806     connect(abInsSensor, &QCheckBox::toggled, this, [&](bool setting)
0807     {
0808         m_graphicSensor = setting;
0809         updateGraphic(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
0810     });
0811 
0812     connect(abInsPetzvalWire, &QCheckBox::toggled, this, [&](bool setting)
0813     {
0814         m_graphicPetzvalWire = setting;
0815         updateGraphic(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
0816     });
0817     connect(abInsPetzvalSurface, &QCheckBox::toggled, this, [&](bool setting)
0818     {
0819         m_graphicPetzvalSurface = setting;
0820         updateGraphic(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
0821     });
0822 
0823     // Simulation on/off
0824     connect(abInsSimMode, &QCheckBox::toggled, this, &AberrationInspector::simModeToggled);
0825 
0826     m_sensor = new QSurface3DSeries;
0827     m_sensorProxy = new QSurfaceDataProxy();
0828     m_sensor->setDataProxy(m_sensorProxy);
0829     m_graphic->addSeries(m_sensor);
0830 
0831     m_petzval = new QSurface3DSeries;
0832     m_petzvalProxy = new QSurfaceDataProxy();
0833     m_petzval->setDataProxy(m_petzvalProxy);
0834     m_graphic->addSeries(m_petzval);
0835 
0836     m_graphic->axisX()->setTitle("X Axis (μm) - Sensor Left to Right");
0837     m_graphic->axisY()->setTitle("Y Axis (μm) - Sensor Top to Bottom");
0838     m_graphic->axisZ()->setTitle("Z Axis (μm) - Axis of Telescope");
0839 
0840     m_graphic->axisX()->setLabelFormat("%.0f");
0841     m_graphic->axisY()->setLabelFormat("%.0f");
0842     m_graphic->axisZ()->setLabelFormat("%.0f");
0843 
0844     m_graphic->axisX()->setTitleVisible(true);
0845     m_graphic->axisY()->setTitleVisible(true);
0846     m_graphic->axisZ()->setTitleVisible(true);
0847 
0848     m_graphic->activeTheme()->setType(Q3DTheme::ThemePrimaryColors);
0849 
0850     // Set projection and shadows
0851     m_graphic->setOrthoProjection(false);
0852     m_graphic->setShadowQuality(QAbstract3DGraph::ShadowQualityNone);
0853     // Balance the z-axis with the x-y plane. Without this the z-axis is crushed to a very small scale
0854     m_graphic->setHorizontalAspectRatio(2.0);
0855 }
0856 
0857 // Simulation on/off switch toggled
0858 void AberrationInspector::simModeToggled(bool setting)
0859 {
0860     m_simMode = setting;
0861     abInsBackfocusSlider->setEnabled(setting);
0862     abInsTiltLRSlider->setEnabled(setting);
0863     abInsTiltTBSlider->setEnabled(setting);
0864     if (!m_simMode)
0865     {
0866         // Reset the sliders
0867         abInsBackfocusSlider->setRange(0, 10);
0868         abInsBackfocusSlider->setValue(0);
0869         abInsTiltLRSlider->setRange(0, 10);
0870         abInsTiltLRSlider->setValue(0);
0871         abInsTiltTBSlider->setRange(0, 10);
0872         abInsTiltTBSlider->setValue(0);
0873 
0874         // Reset the curve fitting object in case Sim mode has caused any problems. This will ensure the graphic
0875         // can always return to its original state after using sim mode.
0876         curveFitting.reset(new CurveFitting());
0877     }
0878     else
0879     {
0880         // Disconnect slider signals which initialising sliders
0881         disconnect(abInsBackfocusSlider, &QSlider::valueChanged, this, &AberrationInspector::simBackfocusChanged);
0882         disconnect(abInsTiltLRSlider, &QSlider::valueChanged, this, &AberrationInspector::simLRTiltChanged);
0883         disconnect(abInsTiltTBSlider, &QSlider::valueChanged, this, &AberrationInspector::simTBTiltChanged);
0884 
0885         // Setup backfocus slider.
0886         int range = 10;
0887         abInsBackfocusSlider->setRange(-range, range);
0888         int sign = m_backfocus < 0.0 ? -1 : 1;
0889         abInsBackfocusSlider->setValue(sign * 5);
0890         abInsBackfocusSlider->setTickInterval(1);
0891         m_simBackfocus = m_backfocus;
0892 
0893         // Setup Left-to-Right tilt slider.
0894         abInsTiltLRSlider->setRange(-range, range);
0895         sign = m_LRTilt < 0.0 ? -1 : 1;
0896         abInsTiltLRSlider->setValue(sign * 5);
0897         abInsTiltLRSlider->setTickInterval(1);
0898         m_simLRTilt = m_LRTilt;
0899 
0900         // Setup Top-to-Bottom tilt slider
0901         abInsTiltTBSlider->setRange(-range, range);
0902         sign = m_TBTilt < 0.0 ? -1 : 1;
0903         abInsTiltTBSlider->setValue(sign * 5);
0904         abInsTiltTBSlider->setTickInterval(1);
0905         m_simTBTilt = m_TBTilt;
0906 
0907         // Now that the sliders have been initialised, connect up signals
0908         connect(abInsBackfocusSlider, &QSlider::valueChanged, this, &AberrationInspector::simBackfocusChanged);
0909         connect(abInsTiltLRSlider, &QSlider::valueChanged, this, &AberrationInspector::simLRTiltChanged);
0910         connect(abInsTiltTBSlider, &QSlider::valueChanged, this, &AberrationInspector::simTBTiltChanged);
0911     }
0912     updateGraphic(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
0913 }
0914 
0915 // Update the 3D graphic based on user selections
0916 void AberrationInspector::updateGraphic(TileSelection tileSelection)
0917 {
0918     // If we don't have good results then don't display the 3D graphic
0919     bool ok = m_resultsOK;
0920 
0921     if (ok)
0922         // Display thw sensor
0923         ok = processSensor();
0924 
0925     if (ok)
0926     {
0927         // Add labels to the sensor
0928         processSensorLabels();
0929 
0930         // Draw the Petzval surface (from the field flattener
0931         ok = processPetzval(tileSelection);
0932     }
0933 
0934     if (ok)
0935     {
0936         // Setup axes
0937         m_graphic->axisX()->setRange(-m_maxX, m_maxX);
0938         m_graphic->axisY()->setRange(-m_maxY, m_maxY);
0939         m_graphic->axisZ()->setRange(m_minZ * 1.1, m_maxZ * 1.1);
0940 
0941         // Display / don't display the sensor
0942         m_graphicSensor ? m_graphic->addSeries(m_sensor) : m_graphic->removeSeries(m_sensor);
0943 
0944         // Display Petzval curve
0945         QSurface3DSeries::DrawFlags petzvalDrawMode { 0 };
0946         if (m_graphicPetzvalWire)
0947             petzvalDrawMode = petzvalDrawMode | QSurface3DSeries::DrawWireframe;
0948         if (m_graphicPetzvalSurface)
0949             petzvalDrawMode = petzvalDrawMode | QSurface3DSeries::DrawSurface;
0950 
0951         if (petzvalDrawMode)
0952         {
0953             m_petzval->setDrawMode(petzvalDrawMode);
0954             m_graphic->addSeries(m_petzval);
0955         }
0956         else
0957             m_graphic->removeSeries(m_petzval);
0958     }
0959 
0960     // Display / don't display the graphic
0961     ok ? m_graphic->show() : m_graphic->hide();
0962 }
0963 
0964 // Draw the sensor on the graphic
0965 bool AberrationInspector::processSensor()
0966 {
0967     // If we are in Sim mode we have previously solved the equation for the plane. All movements in
0968     // Sim mode are rotations.
0969     // If we are not in Sim mode then resolve, e.g. when tile selection changes
0970     if (!m_simMode)
0971     {
0972         // Fit a plane to the datapoints for the selected tiles. Fit is unweighted
0973         CurveFitting::DataPoint3DT plane;
0974         plane.useWeights = false;
0975         for (int tile = 0; tile < NUM_TILES; tile++)
0976         {
0977             if (m_useTile[tile])
0978             {
0979                 QVector3D tileCentre = QVector3D(getXYTileCentre(static_cast<Ekos::tileID>(tile)),
0980                                                  getBSDelta(static_cast<Ekos::tileID>(tile)));
0981                 plane.push_back(tileCentre.x(), tileCentre.y(), tileCentre.z());
0982                 // Update the graph range to accomodate the data
0983                 m_maxX = std::max(m_maxX, tileCentre.x());
0984                 m_maxY = std::max(m_maxY, tileCentre.y());
0985                 m_maxZ = std::max(m_maxZ, tileCentre.z());
0986                 m_minZ = std::min(m_minZ, tileCentre.z());
0987             }
0988         }
0989         curveFitting->fitCurve3D(plane, CurveFitting::FOCUS_PLANE);
0990         double R2 = curveFitting->calculateR2(CurveFitting::FOCUS_PLANE);
0991         // JEE need to think about how to handle failure to solve versus boundary condition of R2=0
0992         qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 sensor plane solved R2=%2").arg(__FUNCTION__).arg(R2);
0993     }
0994 
0995     // We've successfully solved the plane of the sensor so load the sensor vertices in the 3D Surface
0996     // getSensorVertex will perform the necessary rotations
0997     QSurfaceDataArray *data = new QSurfaceDataArray;
0998     QSurfaceDataRow *dataRow1 = new QSurfaceDataRow;
0999     QSurfaceDataRow *dataRow2 = new QSurfaceDataRow;
1000     *dataRow1 << getSensorVertex(TILE_TL) << getSensorVertex(TILE_TR);
1001     *dataRow2 << getSensorVertex(TILE_BL) << getSensorVertex(TILE_BR);
1002     *data << dataRow1 << dataRow2;
1003     m_sensorProxy->resetArray(data);
1004     m_sensor->setDrawMode(QSurface3DSeries::DrawSurface);
1005     return true;
1006 }
1007 
1008 void AberrationInspector::processSensorLabels()
1009 {
1010     // Now sort out the labels on the sensor
1011     for (int tile = 0; tile < NUM_TILES; tile++)
1012     {
1013         if (m_label[tile] == nullptr)
1014             m_label[tile] = new QCustom3DLabel();
1015         else
1016         {
1017             m_graphic->removeCustomItem(m_label[tile]);
1018             m_label[tile] = new QCustom3DLabel();
1019         }
1020         m_label[tile]->setText(TILE_NAME[tile]);
1021         m_label[tile]->setTextColor(TILE_COLOUR[tile]);
1022         QVector3D pos = getLabelCentre(static_cast<tileID>(tile));
1023         m_label[tile]->setPosition(pos);
1024 
1025         m_maxX = std::max(m_maxX, pos.x());
1026         m_maxY = std::max(m_maxY, pos.y());
1027         m_maxZ = std::max(m_maxZ, pos.z());
1028         m_minZ = std::min(m_minZ, pos.z());
1029 
1030         QFont font = m_label[tile]->font();
1031         font.setPointSize(500);
1032         m_label[tile]->setFont(font);
1033         m_label[tile]->setFacingCamera(true);
1034     }
1035 
1036     for (int tile = 0; tile < NUM_TILES; tile++)
1037     {
1038         if (m_useTile[tile] && m_graphicLabels)
1039             m_graphic->addCustomItem(m_label[tile]);
1040     }
1041 }
1042 
1043 // Draw the Petzval surface on the graphic. Assume a parabolid surface
1044 // z = x^2/a^2 + y^2/b^2. Assume symmetry where a = b
1045 // a^2 = (x^2 + y^2) / z
1046 //
1047 // We know that at the measured datapoints (tile centres) the z value = backfocus
1048 // This is complicated by sensor tilt, but the previously calculated backfocus is an average value
1049 // So we can use this to calculate "a" in the above equation
1050 // Note that there are 2 solutions: one giving positive z, the other negative
1051 bool AberrationInspector::processPetzval(TileSelection tileSelection)
1052 {
1053     float a = 1.0;
1054     double sum = 0.0;
1055     double backfocus = m_simMode ? m_simBackfocus : m_backfocus;
1056     double sign = (backfocus < 0.0) ? -1.0 : 1.0;
1057     backfocus = std::abs(backfocus);
1058     switch (tileSelection)
1059     {
1060         case TileSelection::TILES_ALL:
1061             // Use all tiles
1062             for (int i = 0; i < NUM_TILES; i++)
1063             {
1064                 if (i == TILE_CM)
1065                     continue;
1066                 sum += getXYTileCentre(static_cast<tileID>(i)).lengthSquared();
1067             }
1068             a = sqrt(sum / (8 * backfocus));
1069             break;
1070 
1071         case TileSelection::TILES_OUTER_CORNERS:
1072             // Use tiles 0, 2, 6, 8
1073             sum += getXYTileCentre(TILE_TL).lengthSquared() +
1074                    getXYTileCentre(TILE_TR).lengthSquared() +
1075                    getXYTileCentre(TILE_BL).lengthSquared() +
1076                    getXYTileCentre(TILE_BR).lengthSquared();
1077             a = sqrt(sum / (4 * backfocus));
1078             break;
1079 
1080         case TileSelection::TILES_INNER_DIAMOND:
1081             // Use tiles 1, 3, 5, 7
1082             sum += getXYTileCentre(TILE_TM).lengthSquared() +
1083                    getXYTileCentre(TILE_CL).lengthSquared() +
1084                    getXYTileCentre(TILE_CR).lengthSquared() +
1085                    getXYTileCentre(TILE_BM).lengthSquared();
1086             a = sqrt(sum / (4 * backfocus));
1087             break;
1088 
1089         default:
1090             qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called with invalid tile selection %2").arg(__FUNCTION__).arg(tileSelection);
1091             return false;
1092     }
1093 
1094     // Now we have the Petzval equation load a data array of 21 x 21 points
1095     auto *dataArray = new QSurfaceDataArray;
1096     float x_step = m_data.sensorWidth * m_data.pixelSize * 2.0 / 21.0;
1097     float y_step = m_data.sensorHeight * m_data.pixelSize * 2.0 / 21.0;
1098 
1099     m_maxX = std::max(m_maxX, static_cast<float>(m_data.sensorWidth));
1100     m_maxY = std::max(m_maxY, static_cast<float>(m_data.sensorHeight));
1101 
1102     // Seems like the x values in the data row need to be increasing / descreasing for the dataProxy to work
1103     for (int j = -10; j < 11; j++)
1104     {
1105         auto *newRow = new QSurfaceDataRow;
1106         float y = y_step * j;
1107         for (int i = -10; i < 11; i++)
1108         {
1109             float x = x_step * i;
1110             float z = sign * (pow(x / a, 2.0) + pow(y / a, 2.0));
1111             newRow->append(QSurfaceDataItem({x, y, z}));
1112             if (i == 10 && j == 10)
1113             {
1114                 m_maxZ = std::max(m_maxZ, z);
1115                 m_minZ = std::min(m_minZ, z);
1116             }
1117         }
1118         dataArray->append(newRow);
1119     }
1120     m_petzvalProxy->resetArray(dataArray);
1121     return true;
1122 }
1123 
1124 // Returns the X, Y centre of the tile in microns
1125 QVector2D AberrationInspector::getXYTileCentre(tileID tile)
1126 {
1127     const double halfSW = m_data.sensorWidth * m_data.pixelSize / 2.0;
1128     const double halfSH = m_data.sensorHeight * m_data.pixelSize / 2.0;
1129     const double halfTS = m_data.tileWidth * m_data.pixelSize / 2.0;
1130 
1131     // Focus calculates the average star position in each tile and passes this to Aberration Inspector as
1132     // an x, y offset from the center of the tile. If stars are homogenously distributed then the offset would
1133     // be 0, 0. If they aren't, then offset represents how much to add to the tile centre.
1134     // A user option (abInsOptCentres) specifies whether to use the offsets.
1135     double xOffset = 0.0;
1136     double yOffset = 0.0;
1137     if (abInsOptCentres->isChecked())
1138     {
1139         xOffset = m_tileOffsets[tile].x() * m_data.pixelSize;
1140         yOffset = m_tileOffsets[tile].y() * m_data.pixelSize;
1141     }
1142 
1143     switch (tile)
1144     {
1145         case TILE_TL:
1146             return QVector2D(-(halfSW - halfTS) + xOffset, halfSH - halfTS + yOffset);
1147             break;
1148 
1149         case TILE_TM:
1150             return QVector2D(xOffset, halfSH - halfTS + yOffset);
1151             break;
1152 
1153         case TILE_TR:
1154             return QVector2D(halfSW - halfTS + xOffset, halfSH - halfTS + yOffset);
1155             break;
1156 
1157         case TILE_CL:
1158             return QVector2D(-(halfSW - halfTS) + xOffset, yOffset);
1159             break;
1160 
1161         case TILE_CM:
1162             return QVector2D(xOffset, yOffset);
1163             break;
1164 
1165         case TILE_CR:
1166             return QVector2D(halfSW - halfTS + xOffset, yOffset);
1167             break;
1168 
1169         case TILE_BL:
1170             return QVector2D(-(halfSW - halfTS) + xOffset, -(halfSH - halfTS) + yOffset);
1171             break;
1172 
1173         case TILE_BM:
1174             return QVector2D(xOffset, -(halfSH - halfTS) + yOffset);
1175             break;
1176 
1177         case TILE_BR:
1178             return QVector2D(halfSW - halfTS + xOffset, -(halfSH - halfTS) + yOffset);
1179             break;
1180 
1181         default:
1182             qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called with invalid tile %2").arg(__FUNCTION__).arg(tile);
1183             return QVector2D(0.0, 0.0);
1184             break;
1185     }
1186 }
1187 
1188 // Position the labels just outside the sensor so they are always visible
1189 QVector3D AberrationInspector::getLabelCentre(tileID tile)
1190 {
1191     const double halfSW = m_data.sensorWidth * m_data.pixelSize / 2.0;
1192     const double halfSH = m_data.sensorHeight * m_data.pixelSize / 2.0;
1193     const double halfTS = m_data.tileWidth * m_data.pixelSize / 2.0;
1194 
1195     QVector3D point;
1196     point.setZ(0.0);
1197 
1198     switch (tile)
1199     {
1200         case TILE_TL:
1201             point.setX(-halfSW - halfTS);
1202             point.setY(halfSH + halfTS);
1203             break;
1204 
1205         case TILE_TM:
1206             point.setX(0.0);
1207             point.setY(halfSH + halfTS);
1208             break;
1209 
1210         case TILE_TR:
1211             point.setX(halfSW + halfTS);
1212             point.setY(halfSH + halfTS);
1213             break;
1214 
1215         case TILE_CL:
1216             point.setX(-halfSW - halfTS);
1217             point.setY(0.0);
1218             break;
1219 
1220         case TILE_CM:
1221             point.setX(0.0);
1222             point.setY(0.0);
1223             break;
1224 
1225         case TILE_CR:
1226             point.setX(halfSW + halfTS);
1227             point.setY(0.0);
1228             break;
1229 
1230         case TILE_BL:
1231             point.setX(-halfSW - halfTS);
1232             point.setY(-halfSH - halfTS);
1233             break;
1234 
1235         case TILE_BM:
1236             point.setX(0.0);
1237             point.setY(-halfSH - halfTS);
1238             break;
1239 
1240         case TILE_BR:
1241             point.setX(halfSW + halfTS);
1242             point.setY(-halfSH - halfTS);
1243             break;
1244 
1245         default:
1246             qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called with invalid tile %2").arg(__FUNCTION__).arg(tile);
1247             point.setX(0.0);
1248             point.setY(0.0);
1249             break;
1250     }
1251     // We have the coordinates of the point, so now rotate it...
1252     return rotatePoint(point);
1253 }
1254 
1255 // Returns the vertex of the sensor
1256 // x, y are the sensor corner
1257 // z is calculated from the curve fit of the sensor to a plane
1258 QVector3D AberrationInspector::getSensorVertex(tileID tile)
1259 {
1260     const double halfSW = m_data.sensorWidth * m_data.pixelSize / 2.0;
1261     const double halfSH = m_data.sensorHeight * m_data.pixelSize / 2.0;
1262 
1263     QVector3D point;
1264     point.setZ(0.0);
1265 
1266     switch (tile)
1267     {
1268         case TILE_TL:
1269             point.setX(-halfSW);
1270             point.setY(halfSH);
1271             break;
1272 
1273         case TILE_TR:
1274             point.setX(halfSW);
1275             point.setY(halfSH);
1276             break;
1277 
1278         case TILE_BL:
1279             point.setX(-halfSW);
1280             point.setY(-halfSH);
1281             break;
1282 
1283         case TILE_BR:
1284             point.setX(halfSW);
1285             point.setY(-halfSH);
1286             break;
1287 
1288         default:
1289             qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called with invalid tile %2").arg(__FUNCTION__).arg(tile);
1290             point.setX(0.0);
1291             point.setY(0.0);
1292             break;
1293     }
1294     // We have the coordinates of the point, so now rotate it...
1295     return rotatePoint(point);
1296 }
1297 
1298 // Calculate the backfocus subtracted delta of the passed in tile versus the central tile
1299 // This is really just a translation of the datapoints by the backfocus delta
1300 double AberrationInspector::getBSDelta(tileID tile)
1301 {
1302     if (tile == TILE_CM)
1303         return 0.0;
1304     else
1305         return m_deltas[tile] - m_backfocus;
1306 }
1307 
1308 // Rotate the passed in 3D point based on:
1309 // Sim mode: use the sim slider values for Left-to-Right and Top-to-Bottom tilt
1310 // !Sim mode: use the Left-to-Right and Top-to-Bottom tilt calculated from the focus position deltas
1311 //
1312 // Qt provides the QQuaternion class, although the documentation is very basic.
1313 // More info: https://en.wikipedia.org/wiki/Quaternion
1314 // The QQuaternion class provides a way to do 3D rotations. This is simpler than doing all the 3D
1315 // multiplication manually, although the results would be the same.
1316 // Be careful that multiplication is NOT commutative!
1317 //
1318 // Left-to-Right tilt is a rotation about the y-axis
1319 // Top-to-bottom tilt is a rotation about the x-axis
1320 //
1321 QVector3D AberrationInspector::rotatePoint(QVector3D point)
1322 {
1323     float LtoRAngle, TtoBAngle;
1324 
1325     if (m_simMode)
1326     {
1327         LtoRAngle = std::asin(m_simLRTilt / 100.0);
1328         TtoBAngle = std::asin(m_simTBTilt / 100.0);
1329     }
1330     else
1331     {
1332         LtoRAngle = std::asin(m_LRTilt / 100.0);
1333         TtoBAngle = std::asin(m_TBTilt / 100.0);
1334     }
1335     QQuaternion xRotation = QQuaternion::fromAxisAndAngle(1.0f, 0.0f, 0.0f, TtoBAngle * RADIANS2DEGREES);
1336     QQuaternion yRotation = QQuaternion::fromAxisAndAngle(0.0f, 1.0f, 0.0f, LtoRAngle * RADIANS2DEGREES);
1337     QQuaternion totalRotation = yRotation * xRotation;
1338     return totalRotation.rotatedVector(point);
1339 }
1340 
1341 // Backfocus simulation slider has changed
1342 void AberrationInspector::simBackfocusChanged(int value)
1343 {
1344     m_simBackfocus = abs(m_backfocus) * static_cast<double>(value) / 5.0;
1345     updateGraphic(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
1346 }
1347 
1348 // Left-to-Right tilt simulation slider has changed
1349 void AberrationInspector::simLRTiltChanged(int value)
1350 {
1351     m_simLRTilt = abs(m_LRTilt) * static_cast<double>(value) / 5.0;
1352     updateGraphic(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
1353 }
1354 
1355 // Top-to-Bottom tilt simulation slider has changed
1356 void AberrationInspector::simTBTiltChanged(int value)
1357 {
1358     m_simTBTilt = abs(m_TBTilt) * static_cast<double>(value) / 5.0;
1359     updateGraphic(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
1360 }
1361 
1362 }