/*************************************************************************** PMusrStep.cpp Author: Andreas Suter e-mail: andreas.suter@psi.ch ***************************************************************************/ /*************************************************************************** * Copyright (C) 2007-2025 by Andreas Suter * * andreas.suter@psi.ch * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ /** * @file PMusrStep.cpp * @brief Implementation of the PMusrStep and PModSelect dialog classes. * @details This file contains the implementation of the GUI dialogs used * by the musrStep application to modify fit parameter step sizes in * muSR msr-files. * * @author Andreas Suter * @date 2007-2025 * @copyright GNU General Public License v2 or later */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "PMusrStep.h" //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ /** * @brief Constructor for the PModSelect dialog. * @details Creates and initializes the dialog UI with the following controls: * - "Scale by Factor" button with associated factor input field * - "Absolute Value" checkbox to toggle between multiplicative and absolute scaling * - "Scale Automatically" button for automatic scaling based on parameter names * - "Cancel" button to close the dialog without applying changes * * @param parent Pointer to the parent widget (default: Q_NULLPTR) */ PModSelect::PModSelect(QWidget *parent) : QDialog(parent) { setWindowTitle("Modify Selected"); fScaleByFactor = std::make_unique("Scale by &Factor"); fScaleByFactor->setWhatsThis("if pressed it will use the Factor value, and the absolut value selection to change the selected parameter steps."); fFactorLabel = std::make_unique("Factor"); fFactorLineEdit = std::make_unique("0.01"); fFactorLineEdit->setValidator(new QDoubleValidator); fAbsVal = std::make_unique("Absolute Value"); fAbsVal->setWhatsThis("if checked, the factor is used as an absolut value rather than a multiplication factor for the selected steps."); QHBoxLayout *top = new QHBoxLayout; top->addWidget(fScaleByFactor.get()); top->addWidget(fFactorLabel.get()); top->addWidget(fFactorLineEdit.get()); top->addWidget(fAbsVal.get()); fScaleAutomatic = std::make_unique("Scale &Automatically"); fScaleAutomatic->setWhatsThis("Will try to reset the step size of the selected items based on some crude rules"); fCancel = std::make_unique("&Cancel"); QHBoxLayout *bottom = new QHBoxLayout; bottom->addWidget(fScaleAutomatic.get()); bottom->addWidget(fCancel.get()); QVBoxLayout *main = new QVBoxLayout; main->addLayout(top); main->addLayout(bottom); setLayout(main); connect(fAbsVal.get(), SIGNAL(stateChanged(int)), this, SLOT(absoluteValueStateChanged(int))); connect(fScaleAutomatic.get(), SIGNAL(pressed()), this, SLOT(scaleAuto())); connect(fScaleByFactor.get(), SIGNAL(pressed()), this, SLOT(getFactor())); connect(fCancel.get(), SIGNAL(pressed()), this, SLOT(reject())); } //------------------------------------------------------------------------- /** * @brief Handles state changes of the absolute value checkbox. * @details Updates the UI labels to reflect the current mode: * - When unchecked: Shows "Factor" label and "Scale by Factor" button text * - When checked: Shows "Value" label and "Set Abs. Value" button text * * @param ival The new checkbox state (Qt::Checked or Qt::Unchecked) */ void PModSelect::absoluteValueStateChanged(int ival) { if (ival == Qt::Unchecked) { fFactorLabel->setText("Factor"); fScaleByFactor->setText("Scale by &Factor"); } else if (ival == Qt::Checked) { fFactorLabel->setText("Value"); fScaleByFactor->setText("Set Abs. Value"); } } //------------------------------------------------------------------------- /** * @brief Triggers automatic scaling of selected parameters. * @details Emits the scale() signal with automatic=true and a default * factor of 0.01. The parent dialog will then apply scaling factors * based on parameter naming conventions using its lookupTable() method. */ void PModSelect::scaleAuto() { emit scale(true, 0.01, false); done(1); } //------------------------------------------------------------------------- /** * @brief Reads the user-specified factor and emits the scale signal. * @details Retrieves the factor value from the input field and the * absolute value checkbox state, then emits the scale() signal with * automatic=false to indicate manual scaling mode. */ void PModSelect::getFactor() { double factor = fFactorLineEdit->text().toDouble(); bool state = fAbsVal->isChecked(); emit scale(false, factor, state); done(1); } //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ /** * @brief Constructor for the PMusrStep main dialog. * @details Initializes the dialog with the following operations: * 1. Sets up the window title and icon based on the current theme * 2. Reads and parses the msr-file to extract fit parameters * 3. Creates the parameter table with columns for name, value, and step * 4. Sets up selection buttons (Check Specific, Check All, Uncheck All) * 5. Sets up modification buttons (Modify Automatic, Modify Selected) * 6. Sets up action buttons (Save&Quit, Cancel) * 7. Creates the PModSelect sub-dialog for detailed modification options * * The dialog height is automatically adjusted based on the number of * parameters, with a maximum limit based on screen resolution. * * @param fln Path to the msr-file to open and edit * @param parent Pointer to the parent widget (default: Q_NULLPTR) */ PMusrStep::PMusrStep(const char *fln, QWidget *parent) : QDialog(parent), fMsrFileName(fln) { setWindowTitle("musrStep"); QString str = QIcon::themeName(); bool isDarkTheme = false; if (str.contains("dark", Qt::CaseInsensitive)) isDarkTheme = true; if (isDarkTheme) str = QString(":/icons/musrStep-22x22-dark.svg"); else str = QString(":/icons/musrStep-22x22.svg"); setWindowIcon(QIcon(QPixmap(str))); fValid = false; QString title = QString("%1").arg(fMsrFileName); fTitleLabel = std::make_unique(title); QLabel *icon = new QLabel(); if (isDarkTheme) str = QString(":/icons/musrStep-32x32-dark.svg"); else str = QString(":/icons/musrStep-32x32.svg"); icon->setPixmap(QPixmap(str)); QHBoxLayout *titleLayout = new QHBoxLayout; titleLayout->addWidget(fTitleLabel.get()); titleLayout->addWidget(icon); titleLayout->insertStretch(1); int status = 0; if ((status=readMsrFile()) == 1) { fValid = true; } else { QString msg = QString("Failed to read msr-file: %1 (status=%2)").arg(fMsrFileName).arg(status); QMessageBox::critical(nullptr, "ERROR", msg); } int height; if (fParamVec.size() < 70) height = 20*fParamVec.size(); else height = 900; // make sure that the minimal height is not larger than the screen resolution height QScreen *screen = QGuiApplication::primaryScreen(); int hh = screen->geometry().height(); if (height > hh) height = hh - 70; setMinimumSize(400, height); // populate dialog fParamTable = std::make_unique(fParamVec.size(), 3); QStringList strL; strL << "name" << "value" << "step"; fParamTable->setHorizontalHeaderLabels(strL); QTableWidgetItem *item; for (int i=0; isetFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Unchecked); fParamTable->setItem(i, 0, item); item = new QTableWidgetItem(fParamVec[i].value); item->setFlags(Qt::ItemIsEnabled); fParamTable->setItem(i, 1, item); item = new QTableWidgetItem(fParamVec[i].step); fParamTable->setItem(i, 2, item); } fCheckSpecific = std::make_unique("Check S&pecific"); fCheckSpecific->setWhatsThis("Allows to specify a template name which is used\nto select parameter names fitting to it."); fCheckAll = std::make_unique("Check A&ll"); fCheckAll->setWhatsThis("Select all parameter names,\nexcept the ones with step == 0"); fUnCheckAll = std::make_unique("&Uncheck All"); fUnCheckAll->setWhatsThis("Unselect all parameter names"); QHBoxLayout *checkLayout = new QHBoxLayout; checkLayout->addWidget(fCheckSpecific.get()); checkLayout->addWidget(fCheckAll.get()); checkLayout->addWidget(fUnCheckAll.get()); fModifyAuto = std::make_unique("Modify &Automatic"); fModifyAuto->setWhatsThis("Will try to reset the step size,\nbased on some crude rules"); fModifyAuto->setDefault(true); fModifySelected = std::make_unique("&Modify Selected"); fModifySelected->setWhatsThis("Will call a dialog which all to specify how\nto proceed with the selected parameter steps."); QHBoxLayout *modifyLayout = new QHBoxLayout; modifyLayout->addWidget(fModifyAuto.get()); modifyLayout->addWidget(fModifySelected.get()); fSave = std::make_unique("&Save&&Quit"); fCancel = std::make_unique("&Cancel"); QHBoxLayout *buttomLayout = new QHBoxLayout; buttomLayout->addWidget(fSave.get()); buttomLayout->addWidget(fCancel.get()); QVBoxLayout *mainLayout = new QVBoxLayout; mainLayout->addLayout(titleLayout); mainLayout->addWidget(fParamTable.get()); mainLayout->addLayout(checkLayout); mainLayout->addLayout(modifyLayout); mainLayout->addLayout(buttomLayout); setLayout(mainLayout); connect(fParamTable.get(), SIGNAL(cellChanged(int, int)), this, SLOT(handleCellChanged(int, int))); connect(fCheckSpecific.get(), SIGNAL(pressed()), this, SLOT(checkSpecific())); connect(fCheckAll.get(), SIGNAL(pressed()), this, SLOT(checkAll())); connect(fUnCheckAll.get(), SIGNAL(pressed()), this, SLOT(unCheckAll())); connect(fModifyAuto.get(), SIGNAL(pressed()), this, SLOT(modifyAuto())); connect(fModifySelected.get(), SIGNAL(pressed()), this, SLOT(modifyChecked())); connect(fSave.get(), SIGNAL(pressed()), this, SLOT(saveAndQuit())); connect(fCancel.get(), SIGNAL(pressed()), this, SLOT(reject())); fModSelect = new PModSelect(this); connect(fModSelect, SIGNAL(scale(bool,double,bool)), this, SLOT(handleModSelect(bool,double,bool))); } //------------------------------------------------------------------------- /** * @brief Handles changes to cells in the parameter table. * @details Performs validation when table cells are modified: * - Column 0 (name): Prevents selection of fixed parameters (step == 0) * - Column 2 (step): Validates that entered values are valid numbers * * If an invalid step value is entered, the cell is restored to its * previous value from fParamVec. * * @param row The row index of the changed cell * @param column The column index of the changed cell */ void PMusrStep::handleCellChanged(int row, int column) { QString str; bool ok; if (column == 0) { str = fParamTable->item(row, 2)->text(); if ((fParamTable->item(row, column)->checkState() == Qt::Checked) && ((str == "0") || (str == "0.0"))) { fParamTable->item(row, column)->setCheckState(Qt::Unchecked); QMessageBox::warning(nullptr, "WARNING", "You cannot select a fixed value (step == 0)."); } } else if (column == 2) { str = fParamTable->item(row, column)->text(); str.toDouble(&ok); // result is of no interest if (ok) { fParamVec[row].step = str; } else { fParamTable->item(row, column)->setText(fParamVec[row].step); } } } //------------------------------------------------------------------------- /** * @brief Allows selection of parameters matching a template string. * @details Opens an input dialog where the user can enter a template string. * All parameters whose names contain this template (case-sensitive) are * then checked, except for fixed parameters (step == 0 or "0.0"). */ void PMusrStep::checkSpecific() { bool ok; QString str = QInputDialog::getText(this, "Enter Param Name Template", "Template:", QLineEdit::Normal, "", &ok); if (!ok) return; QString step(""); for (int i=0; irowCount(); i++) { if (fParamTable->item(i,0)->text().contains(str)) { step = fParamTable->item(i,2)->text(); if ((step != "0") && (step != "0.0")) fParamTable->item(i,0)->setCheckState(Qt::Checked); } } } //------------------------------------------------------------------------- /** * @brief Selects all non-fixed parameters in the table. * @details Iterates through all rows and checks parameters whose step * value is not "0" or "0.0" (fixed parameters remain unchecked). */ void PMusrStep::checkAll() { QString str(""); for (int i=0; irowCount(); i++) { str = fParamTable->item(i,2)->text(); if ((str != "0") && (str != "0.0")) fParamTable->item(i,0)->setCheckState(Qt::Checked); } } //------------------------------------------------------------------------- /** * @brief Deselects all parameters in the table. * @details Iterates through all rows and unchecks every parameter, * regardless of whether it is fixed or not. */ void PMusrStep::unCheckAll() { for (int i=0; irowCount(); i++) { fParamTable->item(i,0)->setCheckState(Qt::Unchecked); } } //------------------------------------------------------------------------- /** * @brief Applies automatic step size modification to all non-fixed parameters. * @details Iterates through all parameters and applies automatic scaling * using lookupTable() and adoptStep(). Fixed parameters (step == 0) are * skipped. Both the table display and the internal fParamVec are updated. */ void PMusrStep::modifyAuto() { QString str; bool absVal; double factor; for (int i=0; irowCount(); i++) { str = fParamTable->item(i,2)->text(); if ((str != "0") && (str != "0.0")) { factor = lookupTable(fParamTable->item(i,0)->text(), absVal); str = adoptStep(fParamTable->item(i,1)->text(), factor, absVal); fParamTable->item(i,2)->setText(str); fParamVec[i].step = str; } } } //------------------------------------------------------------------------- /** * @brief Opens the modification dialog for checked parameters. * @details Shows the PModSelect dialog, which allows the user to choose * between automatic scaling and manual factor-based scaling for the * currently selected parameters. */ void PMusrStep::modifyChecked() { fModSelect->show(); } //------------------------------------------------------------------------- /** * @brief Processes the scaling selection from the PModSelect dialog. * @details Applies the specified scaling operation to all checked parameters. * If automatic mode is selected, the factor is determined by lookupTable() * for each parameter individually. * * @param automatic If true, use automatic scaling based on parameter names * @param factor The scaling factor (used when automatic is false) * @param absVal If true, factor is used as absolute value; if false, as multiplier */ void PMusrStep::handleModSelect(bool automatic, double factor, bool absVal) { QString str; for (int i=0; irowCount(); i++) { if (fParamTable->item(i,0)->checkState() == Qt::Checked) { if (automatic) { factor = lookupTable(fParamTable->item(i,0)->text(), absVal); } str = adoptStep(fParamTable->item(i,1)->text(), factor, absVal); fParamTable->item(i,2)->setText(str); fParamVec[i].step = str; } } } //------------------------------------------------------------------------- /** * @brief Saves the modified msr-file and closes the dialog. * @details Calls writeMsrFile() to save all modifications, then * accepts the dialog to close it with a success status. */ void PMusrStep::saveAndQuit() { writeMsrFile(); accept(); } //------------------------------------------------------------------------- /** * @brief Initializes a PParam structure with empty strings. * @details Sets all fields of the parameter structure to empty QString * objects. Used before populating a new parameter from file data. * * @param param Reference to the PParam structure to initialize */ void PMusrStep::initParam(PParam ¶m) { param.number = ""; param.name = ""; param.value = ""; param.step = ""; param.posErr = ""; param.boundLow = ""; param.boundHigh = ""; } //------------------------------------------------------------------------- /** * @brief Determines the appropriate step size factor based on parameter name. * @details Uses common muSR parameter naming conventions to identify * parameter types and return appropriate scaling factors: * - freq, frq, field: factor = 1e-3 (high precision for frequency/field) * - lambda, sigma, rlx, rate: factor = 0.1 (relaxation rates) * - phase, phs: factor = 5.0 (absolute value, for phase parameters) * - N0, Nrm, N_bkg, Bgr: factor = 0.01 (normalization/background) * - default: factor = 0.01 * * The comparison is case-insensitive and matches the start of the * parameter name. * * @param str The parameter name to look up * @param absVal Output flag set to true if factor should be used as absolute value * @return The scaling factor to apply */ double PMusrStep::lookupTable(const QString str, bool &absVal) { double factor = 0.01; absVal = false; if (str.startsWith("freq", Qt::CaseInsensitive) || str.startsWith("frq", Qt::CaseInsensitive) || str.startsWith("field", Qt::CaseInsensitive)) { factor = 1.0e-3; } else if (str.startsWith("lambda", Qt::CaseInsensitive) || str.startsWith("sigma", Qt::CaseInsensitive) || str.startsWith("rlx", Qt::CaseInsensitive) || str.startsWith("rate", Qt::CaseInsensitive)) { factor = 0.1; } else if (str.startsWith("phase", Qt::CaseInsensitive) || str.startsWith("phs", Qt::CaseInsensitive)) { factor = 5.0; absVal = true; } else if (str.startsWith("N0", Qt::CaseInsensitive) || str.startsWith("Nrm", Qt::CaseInsensitive) || str.startsWith("N_bkg", Qt::CaseInsensitive) || str.startsWith("Bgr", Qt::CaseInsensitive)) { factor = 0.01; } return factor; } //------------------------------------------------------------------------- /** * @brief Calculates the new step value based on current value and factor. * @details Computes the new step size using either: * - Absolute mode (absVal=true): Returns the factor directly as the step * - Multiplicative mode (absVal=false): Returns factor * current_value * * @param str The current parameter value as a string * @param factor The scaling factor to apply * @param absVal If true, return factor directly; if false, multiply by value * @return The new step value as a QString */ QString PMusrStep::adoptStep(const QString str, double factor, bool absVal) { bool ok; double dval = str.toDouble(&ok); QString step(""); if (absVal) step = QString("%1").arg(factor); else step = QString("%1").arg(factor*dval); return step; } //------------------------------------------------------------------------- /** * @brief Reads and parses the FITPARAMETER block from the msr-file. * @details Opens the msr-file specified in fMsrFileName and extracts * all parameters from the FITPARAMETER block (between FITPARAMETER * and THEORY keywords). Each parameter line must have either 5 or 7 * space-separated fields: * - 5 fields: number, name, value, step, posErr * - 7 fields: above plus boundLow, boundHigh * * Comment lines (starting with #) and empty lines are ignored. * * @return 1 on success * @return -1 if file cannot be opened * @return -2 if a parameter line has invalid format */ int PMusrStep::readMsrFile() { fParamVec.clear(); QFile fin(fMsrFileName); if (!fin.open(QIODevice::ReadOnly|QIODevice::Text)) return -1; bool done = false, parameter = false; QByteArray line; QString str; QStringList strL; PParam param; while(!done && !fin.atEnd()) { line = fin.readLine(); str = line.data(); str = str.trimmed(); if (str.isEmpty() || str.startsWith("#")) { continue; } else if (str.startsWith("FITPARAMETER")) { parameter = true; continue; } else if (str.startsWith("THEORY")) { done = true; continue; } if (parameter) { strL = str.split(" ", Qt::SkipEmptyParts); if ((strL.size() != 5) && (strL.size() != 7)) { fin.close(); return -2; } initParam(param); param.number = strL[0]; param.name = strL[1]; param.value = strL[2]; param.step = strL[3]; param.posErr = strL[4]; if (strL.size() == 7) { param.boundLow = strL[5]; param.boundHigh= strL[6]; } fParamVec.push_back(param); } } fin.close(); return 1; } //------------------------------------------------------------------------- /** * @brief Writes the modified parameters back to the msr-file. * @details Reads the entire original msr-file, then rewrites it with * updated step values in the FITPARAMETER block. All other content * is preserved unchanged. A "*** FIT DID NOT CONVERGE ***" marker is * appended to indicate the file has been modified. * * @return 1 on success * @return -1 if input file cannot be opened for reading * @return -2 if output file cannot be opened for writing */ int PMusrStep::writeMsrFile() { // read whole msr-file QFile fin(fMsrFileName); if (!fin.open(QIODevice::ReadOnly|QIODevice::Text)) return -1; QByteArray data = fin.readAll(); fin.close(); QFile fileOut(fMsrFileName); if (!fileOut.open(QIODevice::WriteOnly|QIODevice::Text)) return -2; QTextStream fout(&fileOut); int idx = 0; QString line, paramLine; bool done = false; bool paramBlock = false; do { line = getLine(data, idx); if ((idx == -1) || (idx == data.size())) { done = true; } else { if (line.startsWith("FITPARAMETER")) { paramBlock = true; } else if (line.startsWith("THEORY")) { paramBlock = false; } if (paramBlock) { paramLine = updateParamLine(line); if (paramLine == "") // comment line, etc. fout << line << "\n"; else fout << paramLine << "\n"; } else { fout << line << "\n"; } } } while (!done); fout << "*** FIT DID NOT CONVERGE ***\n"; fileOut.close(); return 1; } //------------------------------------------------------------------------- /** * @brief Extracts a single line from a byte array at the given position. * @details Searches for the next newline character starting at idx and * extracts the text between the current position and the newline. * The idx parameter is updated to point to the character after the newline. * * @param data Reference to the byte array containing file data * @param idx Reference to the current position index (updated to next line start) * @return The extracted line as a QString, or empty string if no newline found */ QString PMusrStep::getLine(QByteArray &data, int &idx) { int newIdx = data.indexOf('\n', idx); QString line(""); if (newIdx != -1) { line = data.mid(idx, newIdx-idx).data(); idx = newIdx+1; } return line; } //------------------------------------------------------------------------- /** * @brief Reconstructs a parameter line with updated values. * @details Searches fParamVec for a parameter matching the given line, * then formats a new parameter line with proper field widths: * - Column 1 (10 chars, right-aligned): parameter number * - Column 2 (12+ chars, left-aligned): parameter name * - Columns 3-5 (11 chars each): value, step, posErr * - Columns 6-7 (11 chars each, optional): boundLow, boundHigh * * The parameter is identified by finding its name sandwiched between * spaces in the input string to avoid partial matches. * * @param str The original parameter line to update * @return The reconstructed parameter line, or empty string if no match found */ QString PMusrStep::updateParamLine(const QString str) { // find proper parameter index int idx = -1; QString paramStr; for (int i=0; i= 12) width = fParamVec[idx].name.length()+1; ss.setFieldWidth(width); ss << fParamVec[idx].name; ss.setFieldWidth(11); ss << fParamVec[idx].value; ss << fParamVec[idx].step; ss << fParamVec[idx].posErr; if (fParamVec[idx].boundLow != "") { ss << fParamVec[idx].boundLow; ss << fParamVec[idx].boundHigh; } return result; }