From 3931456f764e0754cc164cf016afee168225f995 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Wed, 6 Dec 2023 11:57:40 -0500 Subject: [PATCH] Add conformer search box (#1507) * Add conformer search dialog from Avogadro v1 Connect dialog to obprocess with appropriate options Signed-off-by: Geoff Hutchison --- avogadro/qtplugins/openbabel/CMakeLists.txt | 2 + .../openbabel/conformersearchdialog.cpp | 172 ++++++++++++ .../openbabel/conformersearchdialog.h | 50 ++++ .../openbabel/conformersearchdialog.ui | 253 ++++++++++++++++++ avogadro/qtplugins/openbabel/obprocess.cpp | 96 ++++++- avogadro/qtplugins/openbabel/obprocess.h | 24 +- avogadro/qtplugins/openbabel/openbabel.cpp | 213 ++++++++++++++- avogadro/qtplugins/openbabel/openbabel.h | 15 +- 8 files changed, 794 insertions(+), 31 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..af7e0769b9 --- /dev/null +++ b/avogadro/qtplugins/openbabel/conformersearchdialog.cpp @@ -0,0 +1,172 @@ +/****************************************************************************** + 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 +#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))); + + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton*)), this, + SLOT(buttonClicked(QAbstractButton*))); + + 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() {} + +void ConformerSearchDialog::buttonClicked(QAbstractButton* button) +{ + if (button == ui.buttonBox->button(QDialogButtonBox::Ok)) { + emit accepted(); + } + close(); +} + +QStringList ConformerSearchDialog::options() const +{ + QStringList options; + + // in OB v3.2 + options << "--steps" << QString::number(ui.optimizationStepsSpinBox->value()); + + if (ui.systematicRadio->isChecked()) + options << "--systematic"; + else if (ui.randomRadio->isChecked()) { + options << "--random"; + options << "--nconf" << QString::number(ui.numSpin->value()); + } else if (ui.weightedRadio->isChecked()) { + options << "--weighted"; + options << "--nconf" << QString::number(ui.numSpin->value()); + } else if (ui.geneticRadio->isChecked()) { + // genetic is the default, no need to specify + 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(); + } + + 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..82742db83d --- /dev/null +++ b/avogadro/qtplugins/openbabel/conformersearchdialog.h @@ -0,0 +1,50 @@ +/****************************************************************************** + 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(); + + 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); + + void buttonClicked(QAbstractButton* button); + +signals: + void accepted(); + +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..78a1de0793 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,40 @@ 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" + << "--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 +471,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 +513,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 +574,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 c461ccc73d..6abb19b2cc 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" @@ -38,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("cjson"), m_progress(nullptr) + m_defaultFormat("cml"), m_progress(nullptr), + m_conformerSearchDialog(nullptr) { auto* action = new QAction(this); action->setEnabled(true); @@ -53,6 +55,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 +91,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() {} @@ -129,7 +135,6 @@ QList OpenBabel::fileFormats() const 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())); @@ -259,9 +264,11 @@ void OpenBabel::handleReadFormatUpdate(const QMultiMap& fmts) emit fileFormatsReady(); // Update the default format if cjson is available - if (!m_readFormats.contains("Chemical JSON") && - !m_writeFormats.contains("Chemical JSON")) { - m_defaultFormat = "cml"; + if (m_readFormats.contains("Chemical JSON") && + m_writeFormats.contains("Chemical JSON")) { + m_defaultFormat = "cjson"; + + qDebug() << "Setting default format to cjson."; } } } @@ -294,9 +301,10 @@ void OpenBabel::handleWriteFormatUpdate(const QMultiMap& fmts) emit fileFormatsReady(); // Update the default format if cjson is available - if (!m_readFormats.contains("Chemical JSON") && - !m_writeFormats.contains("Chemical JSON")) { - m_defaultFormat = "cml"; + if (m_readFormats.contains("Chemical JSON") && + m_writeFormats.contains("Chemical JSON")) { + m_defaultFormat = "cjson"; + qDebug() << "Setting default format to cjson."; } } } @@ -382,6 +390,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(); + + 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() { if (!m_molecule || m_molecule->atomCount() == 0) { @@ -430,7 +465,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); @@ -527,6 +562,158 @@ 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; + } + + if (m_conformerSearchDialog == nullptr) { + return; // should't happen + } + + QSettings settings; + QStringList options = m_conformerSearchDialog->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; + } + } + + 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 diff --git a/avogadro/qtplugins/openbabel/openbabel.h b/avogadro/qtplugins/openbabel/openbabel.h index fd647ffd19..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 @@ -67,12 +69,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); @@ -100,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