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 }