From 835722063bcba6be5dc9ef440cedbc16d8131208 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Mon, 5 Sep 2022 13:45:19 -0400 Subject: [PATCH 1/3] Add conformer search dialog from Avogadro v1 Signed-off-by: Geoff Hutchison --- avogadro/qtplugins/openbabel/CMakeLists.txt | 2 + .../openbabel/conformersearchdialog.cpp | 175 ++++++++++++ .../openbabel/conformersearchdialog.h | 48 ++++ .../openbabel/conformersearchdialog.ui | 253 ++++++++++++++++++ avogadro/qtplugins/openbabel/obprocess.cpp | 97 ++++++- avogadro/qtplugins/openbabel/obprocess.h | 24 +- avogadro/qtplugins/openbabel/openbabel.cpp | 203 +++++++++++++- avogadro/qtplugins/openbabel/openbabel.h | 6 + 8 files changed, 782 insertions(+), 26 deletions(-) create mode 100644 avogadro/qtplugins/openbabel/conformersearchdialog.cpp create mode 100644 avogadro/qtplugins/openbabel/conformersearchdialog.h create mode 100644 avogadro/qtplugins/openbabel/conformersearchdialog.ui diff --git a/avogadro/qtplugins/openbabel/CMakeLists.txt b/avogadro/qtplugins/openbabel/CMakeLists.txt index 25073c7c95..126601023f 100644 --- a/avogadro/qtplugins/openbabel/CMakeLists.txt +++ b/avogadro/qtplugins/openbabel/CMakeLists.txt @@ -3,6 +3,7 @@ if(QT_VERSION EQUAL 6) endif() set(openbabel_srcs + conformersearchdialog.cpp obcharges.cpp obfileformat.cpp obforcefielddialog.cpp @@ -11,6 +12,7 @@ set(openbabel_srcs ) set(openbabel_uis + conformersearchdialog.ui obforcefielddialog.ui ) diff --git a/avogadro/qtplugins/openbabel/conformersearchdialog.cpp b/avogadro/qtplugins/openbabel/conformersearchdialog.cpp new file mode 100644 index 0000000000..ae49e0d1e2 --- /dev/null +++ b/avogadro/qtplugins/openbabel/conformersearchdialog.cpp @@ -0,0 +1,175 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + This source code is released under the 3-Clause BSD License, (see "LICENSE"). +******************************************************************************/ + +#include "conformersearchdialog.h" + +#include +#include + +namespace Avogadro { + +ConformerSearchDialog::ConformerSearchDialog(QWidget* parent, Qt::WindowFlags f) + : QDialog(parent, f) +{ + ui.setupUi(this); + + connect(ui.systematicRadio, SIGNAL(toggled(bool)), this, + SLOT(systematicToggled(bool))); + connect(ui.randomRadio, SIGNAL(toggled(bool)), this, + SLOT(randomToggled(bool))); + connect(ui.weightedRadio, SIGNAL(toggled(bool)), this, + SLOT(weightedToggled(bool))); + connect(ui.geneticRadio, SIGNAL(toggled(bool)), this, + SLOT(geneticToggled(bool))); + + m_method = 1; // systematic + m_numConformers = 100; + + ui.numSpin->setValue(0); + ui.systematicRadio->setChecked(true); + ui.randomRadio->setChecked(false); + ui.weightedRadio->setChecked(false); + ui.geneticRadio->setChecked(false); + ui.childrenSpinBox->setEnabled(false); + ui.mutabilitySpinBox->setEnabled(false); + ui.convergenceSpinBox->setEnabled(false); + ui.scoringComboBox->setEnabled(false); +} + +ConformerSearchDialog::~ConformerSearchDialog() {} + +QStringList ConformerSearchDialog::prompt(QWidget* parent_, + const QStringList& startingOptions) +{ + ConformerSearchDialog dialog(parent_); + + // dlg.setOptions(startingOptions); + + QStringList options; + // TODO: add options for number of steps (currently ignored by OB) + if (static_cast(dialog.exec()) == Accepted) { + options = dialog.options(); + } + + return options; +} + +QStringList ConformerSearchDialog::options() const +{ + QStringList options; + options << "--conformer"; + if (ui.systematicRadio->isChecked()) + options << "--systematic"; + else if (ui.randomRadio->isChecked()) { + options << "--random"; + options << "--nconf" << QString(ui.numSpin->value()); + } else if (ui.weightedRadio->isChecked()) { + options << "--weighted"; + options << "--nconf" << QString(ui.numSpin->value()); + } else if (ui.geneticRadio->isChecked()) { + // genetic is the default, no need to specify + options << "--nconf" << QString(ui.numSpin->value()); + options << "--children" << QString(ui.childrenSpinBox->value()); + options << "--mutability" << QString(ui.mutabilitySpinBox->value()); + options << "--convergence" << QString(ui.convergenceSpinBox->value()); + options << "--scoring" << ui.scoringComboBox->currentText(); + } + + options << "--writeconformers" + << "--log"; + + return options; +} + +void ConformerSearchDialog::systematicToggled(bool checked) +{ + if (checked) { + m_method = 1; + ui.systematicRadio->setChecked(true); + ui.randomRadio->setChecked(false); + ui.weightedRadio->setChecked(false); + ui.geneticRadio->setChecked(false); + ui.childrenSpinBox->setEnabled(false); + ui.mutabilitySpinBox->setEnabled(false); + ui.convergenceSpinBox->setEnabled(false); + ui.scoringComboBox->setEnabled(false); + + ui.numSpin->setEnabled(false); + ui.numSpin->setValue(0); + } +} + +void ConformerSearchDialog::randomToggled(bool checked) +{ + if (checked) { + m_method = 2; + ui.systematicRadio->setChecked(false); + ui.randomRadio->setChecked(true); + ui.weightedRadio->setChecked(false); + ui.geneticRadio->setChecked(false); + ui.childrenSpinBox->setEnabled(false); + ui.mutabilitySpinBox->setEnabled(false); + ui.convergenceSpinBox->setEnabled(false); + ui.scoringComboBox->setEnabled(false); + ui.numSpin->setEnabled(true); + ui.numSpin->setValue(100); + } +} + +void ConformerSearchDialog::weightedToggled(bool checked) +{ + if (checked) { + m_method = 3; + ui.systematicRadio->setChecked(false); + ui.randomRadio->setChecked(false); + ui.weightedRadio->setChecked(true); + ui.geneticRadio->setChecked(false); + ui.childrenSpinBox->setEnabled(false); + ui.mutabilitySpinBox->setEnabled(false); + ui.convergenceSpinBox->setEnabled(false); + ui.scoringComboBox->setEnabled(false); + ui.numSpin->setEnabled(true); + ui.numSpin->setValue(100); + } +} + +void ConformerSearchDialog::geneticToggled(bool checked) +{ + if (checked) { + m_method = 4; + ui.systematicRadio->setChecked(false); + ui.randomRadio->setChecked(false); + ui.weightedRadio->setChecked(false); + ui.geneticRadio->setChecked(true); + ui.childrenSpinBox->setEnabled(true); + ui.mutabilitySpinBox->setEnabled(true); + ui.convergenceSpinBox->setEnabled(true); + ui.scoringComboBox->setEnabled(true); + ui.numSpin->setEnabled(true); + ui.numSpin->setValue(100); + } +} + +void ConformerSearchDialog::accept() +{ + m_numConformers = ui.numSpin->value(); + hide(); +} + +void ConformerSearchDialog::reject() +{ + hide(); +} + +int ConformerSearchDialog::numConformers() +{ + return m_numConformers; +} + +int ConformerSearchDialog::method() +{ + return m_method; +} +} // namespace Avogadro diff --git a/avogadro/qtplugins/openbabel/conformersearchdialog.h b/avogadro/qtplugins/openbabel/conformersearchdialog.h new file mode 100644 index 0000000000..de3fcf9ebe --- /dev/null +++ b/avogadro/qtplugins/openbabel/conformersearchdialog.h @@ -0,0 +1,48 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + This source code is released under the 3-Clause BSD License, (see "LICENSE"). +******************************************************************************/ + +#ifndef CONFORMERSEARCHDIALOG_H +#define CONFORMERSEARCHDIALOG_H + +#include + +#include "ui_conformersearchdialog.h" + +namespace Avogadro { +class ConformerSearchDialog : public QDialog +{ + Q_OBJECT + +public: + //! Constructor + explicit ConformerSearchDialog(QWidget* parent = 0, Qt::WindowFlags f = 0); + //! Desconstructor + ~ConformerSearchDialog(); + + int method(); + int numConformers(); + + static QStringList prompt(QWidget* parent_, + const QStringList& startingOptions); + + QStringList options() const; + +public slots: + void accept(); + void reject(); + void systematicToggled(bool checked); + void randomToggled(bool checked); + void weightedToggled(bool checked); + void geneticToggled(bool checked); + +private: + Ui::ConformerSearchDialog ui; + + int m_method; + int m_numConformers; +}; +} // namespace Avogadro + +#endif diff --git a/avogadro/qtplugins/openbabel/conformersearchdialog.ui b/avogadro/qtplugins/openbabel/conformersearchdialog.ui new file mode 100644 index 0000000000..86542e79f4 --- /dev/null +++ b/avogadro/qtplugins/openbabel/conformersearchdialog.ui @@ -0,0 +1,253 @@ + + + ConformerSearchDialog + + + + 0 + 0 + 338 + 400 + + + + Conformer Search + + + + + + Method + + + + + + Number of conformers: + + + + + + + 10000 + + + + + + + Systematic rotor search + + + + + + + Random rotor search + + + + + + + Weighted rotor search + + + + + + + Genetic algorithm search + + + + + + + Optimization per conformer: + + + + + + + steps + + + 5 + + + 250 + + + 25 + + + + + + + + + + Genetic Algorithm Options + + + + + + + + number of children for each parent geometry + + + Children: + + + + + + + number of children for each parent geometry + + + 1 + + + 9999 + + + 5 + + + + + + + mutation frequency (lower = more frequent mutations) + + + Mutability: + + + + + + + mutation frequency (lower = more frequent mutations) + + + 1 + + + 9999 + + + 5 + + + + + + + number of identical generations before convergence is reached + + + Convergence: + + + + + + + number of identical generations before convergence is reached + + + 2 + + + 999 + + + 25 + + + + + + + Scoring method: + + + + + + + scoring method for the genetic algorithm (RMSD = geometric distance, energy = lowest energies) + + + + RMSD + + + + + Energy + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + ConformerSearchDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ConformerSearchDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/avogadro/qtplugins/openbabel/obprocess.cpp b/avogadro/qtplugins/openbabel/obprocess.cpp index 30cb3f5222..1505a10cae 100644 --- a/avogadro/qtplugins/openbabel/obprocess.cpp +++ b/avogadro/qtplugins/openbabel/obprocess.cpp @@ -232,8 +232,8 @@ void OBProcess::convertPrepareOutput() // Check for errors. QString errorOutput = QString::fromLatin1(m_process->readAllStandardError()); QRegularExpression errorChecker("\\b0 molecules converted\\b" - "|" - "obabel: cannot read input format!"); + "|" + "obabel: cannot read input format!"); if (!errorOutput.contains(errorChecker)) { if (m_process->exitStatus() == QProcess::NormalExit) output = m_process->readAllStandardOutput(); @@ -342,9 +342,7 @@ bool OBProcess::calculateCharges(const QByteArray& mol, realOptions << "-icml"; } realOptions << "-onul" // ignore the output - << "--partialcharge" - << type.c_str() - << "--print"; + << "--partialcharge" << type.c_str() << "--print"; // Start the optimization executeObabel(realOptions, this, SLOT(chargesPrepareOutput()), mol); @@ -364,8 +362,8 @@ void OBProcess::chargesPrepareOutput() // Check for errors. QString errorOutput = QString::fromLatin1(m_process->readAllStandardError()); QRegularExpression errorChecker("\\b0 molecules converted\\b" - "|" - "obabel: cannot read input format!"); + "|" + "obabel: cannot read input format!"); if (!errorOutput.contains(errorChecker)) { if (m_process->exitStatus() == QProcess::NormalExit) output = m_process->readAllStandardOutput(); @@ -384,7 +382,7 @@ void OBProcess::chargesPrepareOutput() double charge = line.toDouble(&ok); if (!ok) break; - + charges.push_back(charge); } @@ -426,6 +424,41 @@ bool OBProcess::optimizeGeometry(const QByteArray& mol, return true; } +bool OBProcess::generateConformers(const QByteArray& mol, + const QStringList& options, + const std::string format) +{ + if (!tryLockProcess()) { + qWarning() << "OBProcess::generateConformers(): process already in use."; + return false; + } + + QStringList realOptions; + if (format == "cjson") { + realOptions << "-icjson" + << "-ocjson"; + } else { + realOptions << "-icml" + << "-ocml"; + } + realOptions << "--conformer" + << "--writeconformers" + << "--noh" // new in OB 3.0.1 + << "--log" << options; + + // We'll need to read the log (printed to stderr) to update progress + connect(m_process, SIGNAL(readyReadStandardError()), + SLOT(conformerReadLog())); + + // Initialize the log reader ivars + m_optimizeGeometryLog.clear(); + m_maxConformers = -1; + + // Start the optimization + executeObabel(realOptions, this, SLOT(conformerPrepare()), mol); + return true; +} + void OBProcess::optimizeGeometryPrepare() { if (m_aborted) { @@ -439,6 +472,19 @@ void OBProcess::optimizeGeometryPrepare() emit optimizeGeometryFinished(result); } +void OBProcess::conformerPrepare() +{ + if (m_aborted) { + releaseProcess(); + return; + } + + QByteArray result = m_process->readAllStandardOutput(); + + releaseProcess(); + emit generateConformersFinished(result); +} + void OBProcess::optimizeGeometryReadLog() { // Append the current stderr to the log @@ -468,13 +514,44 @@ void OBProcess::optimizeGeometryReadLog() } } +void OBProcess::conformerReadLog() +{ + // Append the current stderr to the log + // (we're grabbing the log from the geometry optimization) + m_optimizeGeometryLog += + QString::fromLatin1(m_process->readAllStandardError()); + + // Search for the maximum number of steps if we haven't found it yet + if (m_optimizeGeometryMaxSteps < 0) { + QRegExp maxStepsParser("\nSTEPS = ([0-9]+)\n\n"); + if (maxStepsParser.indexIn(m_optimizeGeometryLog) != -1) { + m_optimizeGeometryMaxSteps = maxStepsParser.cap(1).toInt(); + emit optimizeGeometryStatusUpdate(0, m_optimizeGeometryMaxSteps, 0.0, + 0.0); + } + } + + // Emit the last printed step + if (m_optimizeGeometryMaxSteps >= 0) { + QRegExp lastStepParser(R"(\n\s*([0-9]+)\s+([-0-9.]+)\s+([-0-9.]+)\n)"); + if (lastStepParser.lastIndexIn(m_optimizeGeometryLog) != -1) { + int step = lastStepParser.cap(1).toInt(); + double energy = lastStepParser.cap(2).toDouble(); + double lastEnergy = lastStepParser.cap(3).toDouble(); + emit optimizeGeometryStatusUpdate(step, m_optimizeGeometryMaxSteps, + energy, lastEnergy); + } + } +} + void OBProcess::executeObabel(const QStringList& options, QObject* receiver, const char* slot, const QByteArray& obabelStdin) { // Setup exit handler if (receiver) { connect(m_process, SIGNAL(finished(int)), receiver, slot); - connect(m_process, SIGNAL(errorOccurred(QProcess::ProcessError)), receiver, slot); + connect(m_process, SIGNAL(errorOccurred(QProcess::ProcessError)), receiver, + slot); connect(m_process, SIGNAL(errorOccurred(QProcess::ProcessError)), this, SLOT(obError())); } @@ -498,4 +575,4 @@ void OBProcess::resetState() connect(this, SIGNAL(aborted()), m_process, SLOT(kill())); } -} // namespace Avogadro +} // namespace Avogadro::QtPlugins diff --git a/avogadro/qtplugins/openbabel/obprocess.h b/avogadro/qtplugins/openbabel/obprocess.h index 94aeedd7ba..bbafcf11ee 100644 --- a/avogadro/qtplugins/openbabel/obprocess.h +++ b/avogadro/qtplugins/openbabel/obprocess.h @@ -3,7 +3,6 @@ This source code is released under the 3-Clause BSD License, (see "LICENSE"). ******************************************************************************/ - #ifndef AVOGADRO_QTPLUGINS_OBPROCESS_H #define AVOGADRO_QTPLUGINS_OBPROCESS_H @@ -271,7 +270,7 @@ public slots: * optimization finishes, optimizeGeometryFinished will be emitted with the * result of the optimization. * - * The optimization is started with, e.g. + * The optimization is started with, e.g. * `obabel -icml -ocml --minimize ` * * The standard output is recorded and returned by optimizeGeometryFinished. @@ -281,13 +280,17 @@ public slots: * * @return True if the process started successfully, false otherwise. */ - bool optimizeGeometry(const QByteArray& cml, const QStringList& options, std::string format = "cml"); + bool optimizeGeometry(const QByteArray& cml, const QStringList& options, + std::string format = "cml"); + bool generateConformers(const QByteArray& cml, const QStringList& options, + std::string format = "cml"); signals: /** * Emitted with the standard output of the process when it finishes. * If an error occurs, the argument will not be valid CML. */ void optimizeGeometryFinished(const QByteArray& cml); + void generateConformersFinished(const QByteArray& cml); /** * Emitted every 10 steps of the optimization to indicate the current * progress. @@ -300,9 +303,14 @@ public slots: */ void optimizeGeometryStatusUpdate(int step, int maxSteps, double currentEnergy, double lastEnergy); + + void conformerStatusUpdate(int step, int maxSteps, double currentEnergy, + double lastEnergy); private slots: void optimizeGeometryPrepare(); void optimizeGeometryReadLog(); + void conformerPrepare(); + void conformerReadLog(); // end Force Fields doxygen group /**@}*/ @@ -331,7 +339,7 @@ public slots: */ bool queryCharges(); - signals: +signals: /** * Triggered when the process started by queryCharges() completes. * @param charges The charge models supported by OpenBabel. Keys @@ -359,11 +367,14 @@ public slots: * indicate return status along with the charges as text. * * The process is performed as: - * `obabel -i -onul --partialcharge --print < input > output` + * `obabel -i -onul --partialcharge --print < input > + * output` * * @return True if the process started successfully, false otherwise. */ - bool calculateCharges(const QByteArray& input, const std::string& inFormat = "cml", const std::string& type = "mmff94"); + bool calculateCharges(const QByteArray& input, + const std::string& inFormat = "cml", + const std::string& type = "mmff94"); private slots: void chargesPrepareOutput(); @@ -423,6 +434,7 @@ executeObabel(options, this, SLOT(mySlot())); // Optimize geometry ivars: int m_optimizeGeometryMaxSteps; + unsigned m_maxConformers; QString m_optimizeGeometryLog; }; diff --git a/avogadro/qtplugins/openbabel/openbabel.cpp b/avogadro/qtplugins/openbabel/openbabel.cpp index fc00590de3..3d42b1c78e 100644 --- a/avogadro/qtplugins/openbabel/openbabel.cpp +++ b/avogadro/qtplugins/openbabel/openbabel.cpp @@ -5,6 +5,7 @@ #include "openbabel.h" +#include "conformersearchdialog.h" #include "obcharges.h" #include "obfileformat.h" #include "obforcefielddialog.h" @@ -53,6 +54,12 @@ OpenBabel::OpenBabel(QObject* p) connect(action, SIGNAL(triggered()), SLOT(onConfigureGeometryOptimization())); m_actions.push_back(action); + action = new QAction(this); + action->setEnabled(true); + action->setText(tr("Conformer Search…")); + connect(action, SIGNAL(triggered()), SLOT(onConfigureConformerSearch())); + m_actions.push_back(action); + action = new QAction(this); action->setEnabled(true); action->setText(tr("Perceive Bonds")); @@ -83,16 +90,14 @@ OpenBabel::OpenBabel(QObject* p) refreshCharges(); QString info = openBabelInfo(); - /* if (info.isEmpty()) { qWarning() << tr("%1 not found! Disabling Open Babel plugin actions.") .arg(OBProcess().obabelExecutable()); foreach (QAction* a, m_actions) a->setEnabled(false); } else { - */ - qDebug() << OBProcess().obabelExecutable() << " found: " << info; - // } + qDebug() << OBProcess().obabelExecutable() << " found: " << info; + } } OpenBabel::~OpenBabel() {} @@ -126,10 +131,9 @@ QList OpenBabel::fileFormats() const std::vector fmime; // Simple lambda to replace toSet in QList - auto toSet = [&] (const QList& list) { + auto toSet = [&](const QList& list) { return QSet(list.begin(), list.end()); }; - QSet formatDescriptions; formatDescriptions.unite(toSet(m_readFormats.uniqueKeys())); formatDescriptions.unite(toSet(m_writeFormats.uniqueKeys())); @@ -262,6 +266,8 @@ void OpenBabel::handleReadFormatUpdate(const QMultiMap& fmts) if (m_readFormats.contains("Chemical JSON") && m_writeFormats.contains("Chemical JSON")) { m_defaultFormat = "cjson"; + + qDebug() << "Setting default format to cjson."; } } } @@ -297,6 +303,7 @@ void OpenBabel::handleWriteFormatUpdate(const QMultiMap& fmts) if (m_readFormats.contains("Chemical JSON") && m_writeFormats.contains("Chemical JSON")) { m_defaultFormat = "cjson"; + qDebug() << "Setting default format to cjson."; } } } @@ -313,7 +320,8 @@ void OpenBabel::refreshForceFields() proc->queryForceFields(); } -void OpenBabel::handleForceFieldsUpdate(const QMultiMap& ffMap) +void OpenBabel::handleForceFieldsUpdate( + const QMultiMap& ffMap) { auto* proc = qobject_cast(sender()); if (proc) @@ -334,7 +342,8 @@ void OpenBabel::refreshCharges() proc->queryCharges(); } -void OpenBabel::handleChargesUpdate(const QMultiMap& chargeMap) +void OpenBabel::handleChargesUpdate( + const QMultiMap& chargeMap) { auto* proc = qobject_cast(sender()); if (proc) @@ -380,6 +389,33 @@ void OpenBabel::onConfigureGeometryOptimization() settings.setValue("openbabel/optimizeGeometry/lastOptions", options); } +void OpenBabel::onConfigureConformerSearch() +{ + // If the force field map is empty, there is probably a problem with the + // obabel executable. Warn the user and return. + if (m_forceFields.isEmpty()) { + QMessageBox::critical(qobject_cast(parent()), tr("Error"), + tr("An error occurred while retrieving the list of " + "supported forcefields. (using '%1').") + .arg(m_process->obabelExecutable()), + QMessageBox::Ok); + return; + } + + QSettings settings; + QStringList options = + settings.value("openbabel/conformerSearch/lastOptions").toStringList(); + + options = + ConformerSearchDialog::prompt(qobject_cast(parent()), options); + + // User cancel + if (options.isEmpty()) + return; + + settings.setValue("openbabel/conformerSearch/lastOptions", options); +} + void OpenBabel::onOptimizeGeometry() { if (!m_molecule || m_molecule->atomCount() == 0) { @@ -428,7 +464,7 @@ void OpenBabel::onOptimizeGeometry() // Setup progress dialog initializeProgressDialog(tr("Optimizing Geometry (Open Babel)"), - tr("Generating MDL…"), 0, 0, 0); + tr("Generating…"), 0, 0, 0); // Connect process disconnect(m_process); @@ -525,6 +561,153 @@ void OpenBabel::onOptimizeGeometryFinished(const QByteArray& output) m_progress->reset(); } +void OpenBabel::onGenerateConformers() +{ + if (!m_molecule || m_molecule->atomCount() == 0) { + QMessageBox::critical(qobject_cast(parent()), tr("Error"), + tr("Molecule invalid. Cannot generate conformers."), + QMessageBox::Ok); + return; + } + + // If the force field map is empty, there is probably a problem with the + // obabel executable. Warn the user and return. + if (m_forceFields.isEmpty()) { + QMessageBox::critical(qobject_cast(parent()), tr("Error"), + tr("An error occurred while retrieving the list of " + "supported forcefields. (using '%1').") + .arg(m_process->obabelExecutable()), + QMessageBox::Ok); + return; + } + + // Fail here if the process is already in use + if (m_process->inUse()) { + showProcessInUseError(tr("Cannot generate conformers with Open Babel.")); + return; + } + + QSettings settings; + QStringList options; + QStringList ffOptions = + settings.value("openbabel/optimizeGeometry/lastOptions").toStringList(); + bool autoDetect = + settings.value("openbabel/optimizeGeometry/autoDetect", true).toBool(); + + if (autoDetect) { + QString ff = autoDetectForceField(); + int ffIndex = ffOptions.indexOf("--ff"); + if (ffIndex >= 0) { + // Shouldn't happen, but just to be safe... + if (ffIndex + 1 == ffOptions.size()) + ffOptions << ff; + else + ffOptions[ffIndex + 1] = ff; + } else { + ffOptions << "--ff" << ff; + } + } + + qDebug() << "Force field options: " << ffOptions; + + // Setup progress dialog + initializeProgressDialog(tr("Generating Conformers (Open Babel)"), + tr("Generating…"), 0, 0, 0); + + // Connect process + disconnect(m_process); + m_process->disconnect(this); + connect(m_progress, SIGNAL(canceled()), m_process, SLOT(abort())); + connect(m_process, SIGNAL(conformerStatusUpdate(int, int, double, double)), + SLOT(onConformerStatusUpdate(int, int, double, double))); + connect(m_process, SIGNAL(generateConformersFinished(QByteArray)), + SLOT(onGenerateConformersFinished(QByteArray))); + + std::string mol; + if (!Io::FileFormatManager::instance().writeString(*m_molecule, mol, + m_defaultFormat)) { + m_progress->reset(); + QMessageBox::critical( + qobject_cast(parent()), tr("Error"), + tr("An internal error occurred while generating an " + "Open Babel representation of the current molecule."), + QMessageBox::Ok); + return; + } + + m_progress->setLabelText(tr("Starting %1…", "arg is an executable file.") + .arg(m_process->obabelExecutable())); + + // Run obabel + m_process->generateConformers(QByteArray(mol.c_str()), options, + m_defaultFormat); +} + +void OpenBabel::onConformerStatusUpdate(int step, int numSteps, double energy, + double lastEnergy) +{ + QString status; + + if (step == 0) { + status = tr("Step %1 of %2\nCurrent energy: %3\ndE: %4") + .arg(step) + .arg(numSteps) + .arg(fabs(energy) > 1e-10 ? QString::number(energy, 'g', 5) + : QString("(pending)")) + .arg("(pending)"); + } else { + double dE = energy - lastEnergy; + status = tr("Step %1 of %2\nCurrent energy: %3\ndE: %4") + .arg(step) + .arg(numSteps) + .arg(energy, 0, 'g', 5) + .arg(dE, 0, 'g', 5); + } + + m_progress->setRange(0, numSteps); + m_progress->setValue(step); + m_progress->setLabelText(status); +} + +void OpenBabel::onGenerateConformersFinished(const QByteArray& output) +{ + m_progress->setLabelText(tr("Updating molecule…")); + + // output --> molecule + Core::Molecule mol; + if (!Io::FileFormatManager::instance().readString(mol, output.constData(), + m_defaultFormat)) { + m_progress->reset(); + QMessageBox::critical(qobject_cast(parent()), tr("Error"), + tr("Error interpreting Open Babel output."), + QMessageBox::Ok); + qDebug() << "Open Babel:" << output; + return; + } + + /// @todo cache a pointer to the current molecule in the above slot, and + /// verify that we're still operating on the same molecule. + + // Check that the atom count hasn't changed: + if (mol.atomCount() != m_molecule->atomCount()) { + m_progress->reset(); + QMessageBox::critical(qobject_cast(parent()), tr("Error"), + tr("Number of atoms in obabel output (%1) does not " + "match the number of atoms in the original " + "molecule (%2).") + .arg(mol.atomCount()) + .arg(m_molecule->atomCount()), + QMessageBox::Ok); + return; + } + + //@todo .. multiple coordinate sets + m_molecule->undoMolecule()->setAtomPositions3d(mol.atomPositions3d(), + tr("Generate Conformers")); + m_molecule->emitChanged(QtGui::Molecule::Atoms | QtGui::Molecule::Modified); + m_progress->reset(); +} + void OpenBabel::onPerceiveBonds() { // Fail here if the process is already in use @@ -860,4 +1043,4 @@ QString OpenBabel::autoDetectForceField() const return result; } -} // namespace Avogadro +} // namespace Avogadro::QtPlugins diff --git a/avogadro/qtplugins/openbabel/openbabel.h b/avogadro/qtplugins/openbabel/openbabel.h index fd647ffd19..98a7ff0ad2 100644 --- a/avogadro/qtplugins/openbabel/openbabel.h +++ b/avogadro/qtplugins/openbabel/openbabel.h @@ -67,12 +67,18 @@ private slots: void handleChargesUpdate(const QMultiMap& chargeMap); void onConfigureGeometryOptimization(); + void onConfigureConformerSearch(); void onOptimizeGeometry(); void onOptimizeGeometryStatusUpdate(int step, int numSteps, double energy, double lastEnergy); void onOptimizeGeometryFinished(const QByteArray& output); + void onGenerateConformers(); + void onConformerStatusUpdate(int step, int numSteps, double energy, + double lastEnergy); + void onGenerateConformersFinished(const QByteArray& output); + void onPerceiveBonds(); void onPerceiveBondsFinished(const QByteArray& output); From eac0dd1bad7b9bfc5d37f855a0c797f327eca379 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Tue, 5 Dec 2023 18:04:07 -0500 Subject: [PATCH 2/3] Connect dialog to obprocess with appropriate options Signed-off-by: Geoff Hutchison --- .../openbabel/conformersearchdialog.cpp | 41 +++++++++---------- .../openbabel/conformersearchdialog.h | 8 ++-- avogadro/qtplugins/openbabel/obprocess.cpp | 1 - avogadro/qtplugins/openbabel/openbabel.cpp | 28 ++++++++----- avogadro/qtplugins/openbabel/openbabel.h | 11 +++-- 5 files changed, 49 insertions(+), 40 deletions(-) diff --git a/avogadro/qtplugins/openbabel/conformersearchdialog.cpp b/avogadro/qtplugins/openbabel/conformersearchdialog.cpp index ae49e0d1e2..5abc2aa1cf 100644 --- a/avogadro/qtplugins/openbabel/conformersearchdialog.cpp +++ b/avogadro/qtplugins/openbabel/conformersearchdialog.cpp @@ -6,6 +6,7 @@ #include "conformersearchdialog.h" #include +#include #include namespace Avogadro { @@ -24,6 +25,9 @@ ConformerSearchDialog::ConformerSearchDialog(QWidget* parent, Qt::WindowFlags f) connect(ui.geneticRadio, SIGNAL(toggled(bool)), this, SLOT(geneticToggled(bool))); + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton*)), this, + SLOT(buttonClicked(QAbstractButton*))); + m_method = 1; // systematic m_numConformers = 100; @@ -40,46 +44,39 @@ ConformerSearchDialog::ConformerSearchDialog(QWidget* parent, Qt::WindowFlags f) ConformerSearchDialog::~ConformerSearchDialog() {} -QStringList ConformerSearchDialog::prompt(QWidget* parent_, - const QStringList& startingOptions) +void ConformerSearchDialog::buttonClicked(QAbstractButton* button) { - ConformerSearchDialog dialog(parent_); - - // dlg.setOptions(startingOptions); - - QStringList options; - // TODO: add options for number of steps (currently ignored by OB) - if (static_cast(dialog.exec()) == Accepted) { - options = dialog.options(); + if (button == ui.buttonBox->button(QDialogButtonBox::Ok)) { + emit accepted(); } - - return options; + close(); } QStringList ConformerSearchDialog::options() const { QStringList options; - options << "--conformer"; + + // in OB v3.2 + options << "--steps" << QString::number(ui.optStepsSpinBox->value()); + if (ui.systematicRadio->isChecked()) options << "--systematic"; else if (ui.randomRadio->isChecked()) { options << "--random"; - options << "--nconf" << QString(ui.numSpin->value()); + options << "--nconf" << QString::number(ui.numSpin->value()); } else if (ui.weightedRadio->isChecked()) { options << "--weighted"; - options << "--nconf" << QString(ui.numSpin->value()); + options << "--nconf" << QString::number(ui.numSpin->value()); } else if (ui.geneticRadio->isChecked()) { // genetic is the default, no need to specify - options << "--nconf" << QString(ui.numSpin->value()); - options << "--children" << QString(ui.childrenSpinBox->value()); - options << "--mutability" << QString(ui.mutabilitySpinBox->value()); - options << "--convergence" << QString(ui.convergenceSpinBox->value()); + options << "--nconf" << QString::number(ui.numSpin->value()); + options << "--children" << QString::number(ui.childrenSpinBox->value()); + options << "--mutability" << QString::number(ui.mutabilitySpinBox->value()); + options << "--convergence" + << QString::number(ui.convergenceSpinBox->value()); options << "--scoring" << ui.scoringComboBox->currentText(); } - options << "--writeconformers" - << "--log"; - return options; } diff --git a/avogadro/qtplugins/openbabel/conformersearchdialog.h b/avogadro/qtplugins/openbabel/conformersearchdialog.h index de3fcf9ebe..82742db83d 100644 --- a/avogadro/qtplugins/openbabel/conformersearchdialog.h +++ b/avogadro/qtplugins/openbabel/conformersearchdialog.h @@ -24,9 +24,6 @@ class ConformerSearchDialog : public QDialog int method(); int numConformers(); - static QStringList prompt(QWidget* parent_, - const QStringList& startingOptions); - QStringList options() const; public slots: @@ -37,6 +34,11 @@ public slots: void weightedToggled(bool checked); void geneticToggled(bool checked); + void buttonClicked(QAbstractButton* button); + +signals: + void accepted(); + private: Ui::ConformerSearchDialog ui; diff --git a/avogadro/qtplugins/openbabel/obprocess.cpp b/avogadro/qtplugins/openbabel/obprocess.cpp index 1505a10cae..78a1de0793 100644 --- a/avogadro/qtplugins/openbabel/obprocess.cpp +++ b/avogadro/qtplugins/openbabel/obprocess.cpp @@ -442,7 +442,6 @@ bool OBProcess::generateConformers(const QByteArray& mol, << "-ocml"; } realOptions << "--conformer" - << "--writeconformers" << "--noh" // new in OB 3.0.1 << "--log" << options; diff --git a/avogadro/qtplugins/openbabel/openbabel.cpp b/avogadro/qtplugins/openbabel/openbabel.cpp index 3d42b1c78e..6abb19b2cc 100644 --- a/avogadro/qtplugins/openbabel/openbabel.cpp +++ b/avogadro/qtplugins/openbabel/openbabel.cpp @@ -39,7 +39,8 @@ namespace Avogadro::QtPlugins { OpenBabel::OpenBabel(QObject* p) : ExtensionPlugin(p), m_molecule(nullptr), m_process(new OBProcess(this)), m_readFormatsPending(true), m_writeFormatsPending(true), - m_defaultFormat("cml"), m_progress(nullptr) + m_defaultFormat("cml"), m_progress(nullptr), + m_conformerSearchDialog(nullptr) { auto* action = new QAction(this); action->setEnabled(true); @@ -406,14 +407,14 @@ void OpenBabel::onConfigureConformerSearch() QStringList options = settings.value("openbabel/conformerSearch/lastOptions").toStringList(); - options = - ConformerSearchDialog::prompt(qobject_cast(parent()), options); - - // User cancel - if (options.isEmpty()) - return; - - settings.setValue("openbabel/conformerSearch/lastOptions", options); + if (m_conformerSearchDialog == nullptr) { + m_conformerSearchDialog = + new ConformerSearchDialog(qobject_cast(parent())); + connect(m_conformerSearchDialog, SIGNAL(accepted()), this, + SLOT(onGenerateConformers())); + } + // todo set options from last run + m_conformerSearchDialog->show(); } void OpenBabel::onOptimizeGeometry() @@ -587,8 +588,13 @@ void OpenBabel::onGenerateConformers() return; } + if (m_conformerSearchDialog == nullptr) { + return; // should't happen + } + QSettings settings; - QStringList options; + QStringList options = m_conformerSearchDialog->options(); + QStringList ffOptions = settings.value("openbabel/optimizeGeometry/lastOptions").toStringList(); bool autoDetect = @@ -608,7 +614,7 @@ void OpenBabel::onGenerateConformers() } } - qDebug() << "Force field options: " << ffOptions; + options << ffOptions; // Setup progress dialog initializeProgressDialog(tr("Generating Conformers (Open Babel)"), diff --git a/avogadro/qtplugins/openbabel/openbabel.h b/avogadro/qtplugins/openbabel/openbabel.h index 98a7ff0ad2..d2c353983f 100644 --- a/avogadro/qtplugins/openbabel/openbabel.h +++ b/avogadro/qtplugins/openbabel/openbabel.h @@ -6,6 +6,8 @@ #ifndef AVOGADRO_QTPLUGINS_OPENBABEL_H #define AVOGADRO_QTPLUGINS_OPENBABEL_H +#include "conformersearchdialog.h" + #include #include @@ -76,7 +78,7 @@ private slots: void onGenerateConformers(); void onConformerStatusUpdate(int step, int numSteps, double energy, - double lastEnergy); + double lastEnergy); void onGenerateConformersFinished(const QByteArray& output); void onPerceiveBonds(); @@ -106,8 +108,11 @@ private slots: QMultiMap m_charges; std::string m_defaultFormat; QProgressDialog* m_progress; + + ConformerSearchDialog* m_conformerSearchDialog; }; -} -} + +} // namespace QtPlugins +} // namespace Avogadro #endif // AVOGADRO_QTPLUGINS_OPENBABEL_H From 5108fa93b9dc36039a15a66e2c8e0f0e7e89c24d Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Wed, 6 Dec 2023 10:26:37 -0500 Subject: [PATCH 3/3] Fix compile error Signed-off-by: Geoff Hutchison --- avogadro/qtplugins/openbabel/conformersearchdialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avogadro/qtplugins/openbabel/conformersearchdialog.cpp b/avogadro/qtplugins/openbabel/conformersearchdialog.cpp index 5abc2aa1cf..af7e0769b9 100644 --- a/avogadro/qtplugins/openbabel/conformersearchdialog.cpp +++ b/avogadro/qtplugins/openbabel/conformersearchdialog.cpp @@ -57,7 +57,7 @@ QStringList ConformerSearchDialog::options() const QStringList options; // in OB v3.2 - options << "--steps" << QString::number(ui.optStepsSpinBox->value()); + options << "--steps" << QString::number(ui.optimizationStepsSpinBox->value()); if (ui.systematicRadio->isChecked()) options << "--systematic";