diff --git a/avogadro/calc/chargemodel.cpp b/avogadro/calc/chargemodel.cpp index f3bfee3ea9..f3fc749a4c 100644 --- a/avogadro/calc/chargemodel.cpp +++ b/avogadro/calc/chargemodel.cpp @@ -57,7 +57,7 @@ Array ChargeModel::potentials(Core::Molecule& mol, return potentials; } -void ChargeModel::appendError(const std::string& errorString, bool newLine) +void ChargeModel::appendError(const std::string& errorString, bool newLine) const { m_error += errorString; if (newLine) diff --git a/avogadro/calc/chargemodel.h b/avogadro/calc/chargemodel.h index bfcc71240a..642394de94 100644 --- a/avogadro/calc/chargemodel.h +++ b/avogadro/calc/chargemodel.h @@ -109,10 +109,10 @@ class AVOGADROCALC_EXPORT ChargeModel * @param errorString The error to be added. * @param newLine Add a new line after the error string? */ - void appendError(const std::string& errorString, bool newLine = true); + void appendError(const std::string& errorString, bool newLine = true) const; private: - std::string m_error; + mutable std::string m_error; float m_dielectric; }; diff --git a/avogadro/qtplugins/CMakeLists.txt b/avogadro/qtplugins/CMakeLists.txt index 810f90e489..f36c852048 100644 --- a/avogadro/qtplugins/CMakeLists.txt +++ b/avogadro/qtplugins/CMakeLists.txt @@ -96,7 +96,6 @@ add_subdirectory(applycolors) add_subdirectory(bondcentrictool) add_subdirectory(bonding) add_subdirectory(cartoons) -add_subdirectory(commandscripts) add_subdirectory(coordinateeditor) add_subdirectory(copypaste) add_subdirectory(crystal) @@ -147,8 +146,12 @@ endif() add_subdirectory(apbs) add_subdirectory(cp2kinput) add_subdirectory(gamessinput) + +# script plugins (input generators, etc.) +add_subdirectory(commandscripts) add_subdirectory(quantuminput) add_subdirectory(scriptfileformats) +add_subdirectory(scriptcharges) if(USE_LIBARCHIVE) add_subdirectory(plugindownloader) @@ -160,18 +163,20 @@ endif() # The scene plugins add_subdirectory(ballandstick) -add_subdirectory(licorice) -add_subdirectory(vanderwaals) -add_subdirectory(wireframe) +add_subdirectory(closecontacts) add_subdirectory(force) +add_subdirectory(licorice) add_subdirectory(meshes) -add_subdirectory(closecontacts) add_subdirectory(noncovalent) +add_subdirectory(vanderwaals) +add_subdirectory(vanderwaalsao) +add_subdirectory(wireframe) if (USE_OPENGL) # needs some raw OpenGL code add_subdirectory(overlayaxes) endif() -add_subdirectory(vanderwaalsao) + +# other optional plugins if (USE_PROTOCALL) add_subdirectory(clientserver) endif() diff --git a/avogadro/qtplugins/scriptcharges/CMakeLists.txt b/avogadro/qtplugins/scriptcharges/CMakeLists.txt new file mode 100644 index 0000000000..33d939210d --- /dev/null +++ b/avogadro/qtplugins/scriptcharges/CMakeLists.txt @@ -0,0 +1,23 @@ +set(scriptcharges_srcs + scriptchargemodel.cpp + scriptcharges.cpp +) + +avogadro_plugin(ScriptCharges + "Scriptable electrostatics models" + ExtensionPlugin + scriptcharges.h + ScriptCharges + "${scriptcharges_srcs}" + "" +) + +target_link_libraries(ScriptCharges PRIVATE AvogadroCalc ) + +# Bundled format scripts: +set(charge_scripts + chargeScripts/xtb.py +) + +install(PROGRAMS ${charge_scripts} + DESTINATION "${INSTALL_LIBRARY_DIR}/avogadro2/scripts/charges/") diff --git a/avogadro/qtplugins/scriptcharges/chargeScripts/xtb.py b/avogadro/qtplugins/scriptcharges/chargeScripts/xtb.py new file mode 100644 index 0000000000..8813e3a309 --- /dev/null +++ b/avogadro/qtplugins/scriptcharges/chargeScripts/xtb.py @@ -0,0 +1,100 @@ +# This source file is part of the Avogadro project. +# This source code is released under the 3-Clause BSD License, (see "LICENSE"). + +import argparse +import json +import sys +import os +from shutil import which +import tempfile +import subprocess + + +def getMetaData(): + # before we return metadata, make sure xtb is in the path + if which("xtb") is None: + return {} # Avogadro will ignore us now + + metaData = {} + metaData["inputFormat"] = "xyz" # could be other formats, but this is fine + metaData["identifier"] = "GFN2" + metaData["name"] = "GFN2" + metaData["description"] = "Calculate atomic partial charges using GFN2 and xtb" + metaData["charges"] = True + metaData["potential"] = False + metaData["elements"] = "1-86" # up to Radon + return metaData + + +def charges(): + # Avogadro will send us the sdf file as stdin + # we need to write it to a temporary file + + # get the whole sdf file + xyz = sys.stdin.read() + + fd, name = tempfile.mkstemp(".xyz") + os.write(fd, xyz.encode()) + os.close(fd) + + # run xtb + xtb = which("xtb") + if xtb is None: # we check again + return "" + + # for now, ignore the output itself + tempdir = tempfile.mkdtemp() + output = subprocess.run( + [xtb, name], stdout=subprocess.PIPE, cwd=tempdir, check=True + ) + # instead we read the "charges" file + result = "" + with open(tempdir + "/" + "charges", "r", encoding="utf-8") as f: + result = f.read() + + # try to cleanup the temporary files + os.remove(name) + for filename in os.listdir(tempdir): + try: + os.remove(tempdir + "/" + filename) + except: + continue + # and try to cleanup the directory + try: + os.rmdir(tempdir) + except: + pass + + # write the charges to stdout + return result + + +def potential(): + # at the moment, xtb doesn't have a good way to do this + # and the method shouldn't be called anyway + + # if your plugin has a potential, you can return it here + # .. you'll get JSON with the file and the set of points + # e.g. { "xyz" : "xyz file contents", "points" : [ x,y,z, x,y,z, ... ] } + # or { "sdf" : "sdf file contents", "points" : [ x,y,z, x,y,z, ... ] } + # .. and you print the list of potentials to stdout + return "" + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("GFN2 partial charges") + parser.add_argument("--display-name", action="store_true") + parser.add_argument("--metadata", action="store_true") + parser.add_argument("--charges", action="store_true") + parser.add_argument("--potential", action="store_true") + parser.add_argument("--lang", nargs="?", default="en") + args = vars(parser.parse_args()) + + if args["metadata"]: + print(json.dumps(getMetaData())) + elif args["display_name"]: + print(getMetaData()["name"]) + elif args["charges"]: + print(charges()) + elif args["potential"]: + print(potential()) diff --git a/avogadro/qtplugins/scriptcharges/scriptchargemodel.cpp b/avogadro/qtplugins/scriptcharges/scriptchargemodel.cpp new file mode 100644 index 0000000000..c40364c0e9 --- /dev/null +++ b/avogadro/qtplugins/scriptcharges/scriptchargemodel.cpp @@ -0,0 +1,408 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + This source code is released under the 3-Clause BSD License, (see "LICENSE"). +******************************************************************************/ + +#include "scriptchargemodel.h" + +#include +#include + +// formats supported in scripts +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace Avogadro { +namespace QtPlugins { + +ScriptChargeModel::ScriptChargeModel(const QString& scriptFileName_) + : m_interpreter(new QtGui::PythonScript(scriptFileName_)), m_valid(false), + m_partialCharges(false), m_electrostatics(false), m_inputFormat(NotUsed) +{ + m_elements.reset(); + readMetaData(); +} + +ScriptChargeModel::~ScriptChargeModel() +{ + delete m_interpreter; +} + +QString ScriptChargeModel::scriptFilePath() const +{ + return m_interpreter->scriptFilePath(); +} + +Calc::ChargeModel* ScriptChargeModel::newInstance() const +{ + return new ScriptChargeModel(m_interpreter->scriptFilePath()); +} + +const MatrixX ScriptChargeModel::partialCharges(Core::Molecule& mol) const +{ + MatrixX charges(mol.atomCount(), 1); + + // Create the intermediate format writer + std::string intermediate; + QScopedPointer format(createFileFormat(m_inputFormat)); + + if (format.isNull()) { + appendError("Cannot create file format."); + return charges; + } + + if (!format->writeString(intermediate, mol)) { + appendError(format->error(), false); + return charges; + } + + // Call the script to convert the file + QByteArray result = + m_interpreter->execute(QStringList() << "--charges", intermediate.c_str()); + + if (m_interpreter->hasErrors()) { + foreach (const QString& err, m_interpreter->errorList()) { + appendError(err.toStdString()); + } + + return charges; + } + + // parse the result - each charge should be on a line + QString resultString = QString(result); + QStringList lines = resultString.split('\n'); + // keep a separate atom counter in case there is other text + // (e.g., "normal termination, etc.") + unsigned int atom = 0; + for (unsigned int i = 0; i < lines.size(); ++i) { + const QString line = lines.at(i); + if (line.isEmpty()) + continue; + + bool ok; + double charge = line.toDouble(&ok); + if (!ok) { + appendError("Invalid charge: " + line.toStdString()); + continue; + } + + charges(atom, 0) = charge; + ++atom; + } + + // cache the charges + mol.setPartialCharges(m_identifier, charges); + + return charges; +} + +double ScriptChargeModel::potential(Core::Molecule& mol, + const Vector3& point) const +{ + // just create an array of size one and run that + Core::Array points; + points.push_back(point); + + Core::Array results = potentials(mol, points); + if (results.size() == 1) + return results[0]; + else + return 0.0; +} + +Core::Array ScriptChargeModel::potentials( + Core::Molecule& mol, const Core::Array& points) const +{ + // first off, if the script doesn't handle potentials + // call the parent class (default method from partial charges) + if (!m_electrostatics) + return Calc::ChargeModel::potentials(mol, points); + + // Create the intermediate format writer + std::string intermediate; + QScopedPointer format(createFileFormat(m_inputFormat)); + Core::Array potentials(points.size(), 0.0); + + if (format.isNull()) { + appendError("Cannot create file format."); + return potentials; + } + + if (!format->writeString(intermediate, mol)) { + appendError(format->error(), false); + return potentials; + } + + // now we stuff the file and the points into JSON + QJsonObject json; + json[m_formatString] = QString::fromStdString(intermediate); + QJsonArray pointsArray; + for (int i = 0; i < points.size(); ++i) { + QJsonArray point; + point << points[i].x() << points[i].y() << points[i].z(); + pointsArray.append(point); + } + json["points"] = pointsArray; + QJsonDocument doc(json); + + // Call the script to convert the file + QByteArray result = + m_interpreter->execute(QStringList() << "--potentials", doc.toJson()); + + if (m_interpreter->hasErrors()) { + foreach (const QString& err, m_interpreter->errorList()) + appendError(err.toStdString()); + return potentials; + } + + // parse the result - each potential should be on a line + QString resultString = QString(result); + QStringList lines = resultString.split('\n'); + for (const QString line : lines) { + if (line.isEmpty()) + continue; + + bool ok; + double potential = line.toDouble(&ok); + if (!ok) { + appendError("Invalid potential: " + line.toStdString()); + continue; + } + potentials.push_back(potential); + } + + return potentials; +} + +ScriptChargeModel::Format ScriptChargeModel::stringToFormat( + const std::string& str) +{ + if (str == "cjson") + return Cjson; + else if (str == "cml") + return Cml; + else if (str == "mdl" || str == "mol" || str == "sdf" || str == "sd") + return Mdl; + else if (str == "pdb") + return Pdb; + else if (str == "xyz") + return Xyz; + return NotUsed; +} + +Io::FileFormat* ScriptChargeModel::createFileFormat( + ScriptChargeModel::Format fmt) +{ + switch (fmt) { + case Cjson: + return new Io::CjsonFormat; + case Cml: + return new Io::CmlFormat; + case Mdl: + return new Io::MdlFormat; + case Pdb: + return new Io::PdbFormat; + case Xyz: + return new Io::XyzFormat; + default: + case NotUsed: + return nullptr; + } +} + +void ScriptChargeModel::resetMetaData() +{ + m_valid = false; + m_partialCharges = false; + m_electrostatics = false; + m_inputFormat = NotUsed; + m_identifier.clear(); + m_name.clear(); + m_description.clear(); + m_formatString.clear(); +} + +void ScriptChargeModel::readMetaData() +{ + resetMetaData(); + + QByteArray output(m_interpreter->execute(QStringList() << "--metadata")); + + if (m_interpreter->hasErrors()) { + qWarning() << "Error retrieving metadata for charge script:" + << scriptFilePath() << "\n" + << m_interpreter->errorList(); + return; + } + + QJsonParseError parseError; + QJsonDocument doc(QJsonDocument::fromJson(output, &parseError)); + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "Error parsing metadata for charge script:" + << scriptFilePath() << "\n" + << parseError.errorString() << "(at offset" << parseError.offset + << ")"; + return; + } + + if (!doc.isObject()) { + qWarning() << "Error parsing metadata for charge script:" + << scriptFilePath() << "\nResult is not a JSON object:\n" + << output; + return; + } + + const QJsonObject metaData(doc.object()); + + // Read required inputs first. + std::string identifierTmp; + if (!parseString(metaData, "identifier", identifierTmp)) { + qWarning() << "Error parsing metadata for charge script:" + << scriptFilePath() << "\n" + << "Error parsing required member 'operations'" + << "\n" + << output; + return; + } + m_identifier = identifierTmp; + + std::string nameTmp; + if (!parseString(metaData, "name", nameTmp)) { + qWarning() << "Error parsing metadata for charge script:" + << scriptFilePath() << "\n" + << "Error parsing required member 'name'" + << "\n" + << output; + return; + } + m_name = nameTmp; + + std::string descriptionTmp; + parseString(metaData, "description", descriptionTmp); + m_description = descriptionTmp; // optional + + Format inputFormatTmp = NotUsed; + std::string inputFormatStrTmp; + if (!parseString(metaData, "inputFormat", inputFormatStrTmp)) { + qWarning() << "Error parsing metadata for charge script:" + << scriptFilePath() << "\n" + << "Member 'inputFormat' required for writable formats." + << "\n" + << output; + return; + } + m_formatString = inputFormatStrTmp.c_str(); // for the json key + + // Validate the input format + inputFormatTmp = stringToFormat(inputFormatStrTmp); + if (inputFormatTmp == NotUsed) { + qWarning() << "Error parsing metadata for charge script:" + << scriptFilePath() << "\n" + << "Member 'inputFormat' not recognized:" + << inputFormatStrTmp.c_str() + << "\nValid values are cjson, cml, mdl/sdf, pdb, or xyz.\n" + << output; + return; + } + m_inputFormat = inputFormatTmp; + + // check if we handle charges and/or potentials + if (!metaData["charges"].isBool()) { + return; // not valid + } + m_partialCharges = metaData["charges"].toBool(); + if (!metaData["potential"].isBool()) { + return; // not valid + } + m_electrostatics = metaData["potential"].toBool(); + + // get the element mask + // (if it doesn't exist, the default is no elements anyway) + m_valid = parseElements(metaData); +} + +bool ScriptChargeModel::parseString(const QJsonObject& ob, const QString& key, + std::string& str) +{ + if (!ob[key].isString()) + return false; + + str = ob[key].toString().toStdString(); + + return !str.empty(); +} + +void ScriptChargeModel::processElementString(const QString& str) +{ + // parse the QString + // first turn any commas into whitespace + QString str2(str); + str2.replace(',', ' '); + // then split on whitespace + QStringList strList = str2.split(QRegExp("\\s+"), QString::SkipEmptyParts); + foreach (QString str, strList) { + // these should be numbers or ranges (e.g., 1-84) + if (str.contains('-')) { + // range, so split on the dash + QStringList strList2 = str.split('-'); + if (strList2.size() != 2) + return; + + // get the two numbers + bool ok; + int start = strList2[0].toInt(&ok); + if (!ok || start < 1 || start > 119) + return; + int end = strList2[1].toInt(&ok); + if (!ok || end < 1 || end > 119) + return; + for (int i = start; i <= end; ++i) + m_elements.set(i); + } + + bool ok; + int i = str.toInt(&ok); + if (!ok || i < 1 || i > 119) + return; + + m_elements.set(i); + } +} + +bool ScriptChargeModel::parseElements(const QJsonObject& ob) +{ + m_elements.reset(); + + // we could either get a string or an array (of numbers) + if (ob["elements"].isString()) { + auto str = ob["elements"].toString(); + processElementString(str); + + } else if (ob["elements"].isArray()) { + QJsonArray arr = ob["elements"].toArray(); + for (int i = 0; i < arr.size(); ++i) { + if (arr[i].isString()) { + processElementString(arr[i].toString()); + } else if (arr[i].isDouble()) { + int element = arr[i].toInt(); + if (element >= 1 && element <= 119) // check the range + m_elements.set(element); + } + } + } + return true; +} + +} // namespace QtPlugins +} // namespace Avogadro diff --git a/avogadro/qtplugins/scriptcharges/scriptchargemodel.h b/avogadro/qtplugins/scriptcharges/scriptchargemodel.h new file mode 100644 index 0000000000..a9bacd68f4 --- /dev/null +++ b/avogadro/qtplugins/scriptcharges/scriptchargemodel.h @@ -0,0 +1,97 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + This source code is released under the 3-Clause BSD License, (see "LICENSE"). +******************************************************************************/ + +#ifndef AVOGADRO_QTPLUGINS_SCRIPTCHARGEMODEL_H +#define AVOGADRO_QTPLUGINS_SCRIPTCHARGEMODEL_H + +#include + +#include + +#include + +class QJsonObject; + +namespace Avogadro { + +namespace Io { +class FileFormat; +} + +namespace QtGui { +class PythonScript; +} + +namespace QtPlugins { + +class ScriptChargeModel : public Avogadro::Calc::ChargeModel +{ +public: + /** Formats that may be written to the script's input. */ + enum Format + { + NotUsed, + Cjson, + Cml, + Mdl, // sdf + Pdb, + Xyz + }; + + ScriptChargeModel(const QString& scriptFileName = ""); + ~ScriptChargeModel() override; + + QString scriptFilePath() const; + + Format inputFormat() const { return m_inputFormat; } + + bool isValid() const { return m_valid; } + + ChargeModel* newInstance() const override; + + std::string identifier() const override { return m_identifier; } + + std::string name() const override { return m_name; } + + Core::Molecule::ElementMask elements() const override { return m_elements; } + + const MatrixX partialCharges(Core::Molecule& mol) const override; + + double potential(Core::Molecule& mol, const Vector3& point) const override; + + bool supportsCharges() const { return m_partialCharges; } + + bool supportsElectrostatics() const { return m_electrostatics; } + + Core::Array potentials( + Core::Molecule& mol, const Core::Array& points) const override; + +private: + static Format stringToFormat(const std::string& str); + static Io::FileFormat* createFileFormat(Format fmt); + void resetMetaData(); + void readMetaData(); + bool parseString(const QJsonObject& ob, const QString& key, std::string& str); + void processElementString(const QString& str); + bool parseElements(const QJsonObject& ob); + +private: + QtGui::PythonScript* m_interpreter; + Format m_inputFormat; + Core::Molecule::ElementMask m_elements; + bool m_valid; + bool m_partialCharges; + bool m_electrostatics; + + std::string m_identifier; + std::string m_name; + std::string m_description; + QString m_formatString; +}; + +} // namespace QtPlugins +} // namespace Avogadro + +#endif // AVOGADRO_QTPLUGINS_SCRIPTCHARGEMODEL_H diff --git a/avogadro/qtplugins/scriptcharges/scriptcharges.cpp b/avogadro/qtplugins/scriptcharges/scriptcharges.cpp new file mode 100644 index 0000000000..3dba2529e2 --- /dev/null +++ b/avogadro/qtplugins/scriptcharges/scriptcharges.cpp @@ -0,0 +1,81 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + This source code is released under the 3-Clause BSD License, (see "LICENSE"). +******************************************************************************/ + +#include "scriptcharges.h" + +#include "scriptchargemodel.h" + +#include +#include + +#include +#include + +#include + +namespace Avogadro { +namespace QtPlugins { + +ScriptCharges::ScriptCharges(QObject* p) : ExtensionPlugin(p) +{ + refreshModels(); +} + +ScriptCharges::~ScriptCharges() {} + +QList ScriptCharges::actions() const +{ + return QList(); +} + +QStringList ScriptCharges::menuPath(QAction*) const +{ + return QStringList(); +} + +void ScriptCharges::setMolecule(QtGui::Molecule*) {} + +void ScriptCharges::refreshModels() +{ + unregisterModels(); + qDeleteAll(m_models); + m_models.clear(); + + QMap scriptPaths = + QtGui::ScriptLoader::scriptList("charges"); + foreach (const QString& filePath, scriptPaths) { + ScriptChargeModel* model = new ScriptChargeModel(filePath); + if (model->isValid()) + m_models.push_back(model); + else + delete model; + } + + registerModels(); +} + +void ScriptCharges::unregisterModels() +{ + for (QList::const_iterator it = m_models.constBegin(), + itEnd = m_models.constEnd(); + it != itEnd; ++it) { + Calc::ChargeManager::unregisterModel((*it)->identifier()); + } +} + +void ScriptCharges::registerModels() +{ + for (QList::const_iterator it = m_models.constBegin(), + itEnd = m_models.constEnd(); + it != itEnd; ++it) { + if (!Calc::ChargeManager::registerModel((*it)->newInstance()) ) { + qDebug() << "Could not register model" << (*it)->identifier().c_str() + << "due to name conflict."; + } + } +} + +} // end namespace QtPlugins +} // end namespace Avogadro diff --git a/avogadro/qtplugins/scriptcharges/scriptcharges.h b/avogadro/qtplugins/scriptcharges/scriptcharges.h new file mode 100644 index 0000000000..dc97199ba6 --- /dev/null +++ b/avogadro/qtplugins/scriptcharges/scriptcharges.h @@ -0,0 +1,56 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + This source code is released under the 3-Clause BSD License, (see "LICENSE"). +******************************************************************************/ + +#ifndef AVOGADRO_QTPLUGINS_SCRIPTCHARGES_H +#define AVOGADRO_QTPLUGINS_SCRIPTCHARGES_H + +#include +#include + +namespace Avogadro { + +namespace Calc{ +class ChargeModel; +} + +namespace QtPlugins { + +/** + * @brief This extension registers ChargeModel electrostatics + * implemented as external scripts. + */ +class ScriptCharges : public QtGui::ExtensionPlugin +{ + Q_OBJECT + +public: + explicit ScriptCharges(QObject* parent = nullptr); + ~ScriptCharges() override; + + QString name() const override { return tr("Script Charge Models"); } + + QString description() const override + { + return tr("Load electrostatic models from external scripts."); + } + + QList actions() const override; + + QStringList menuPath(QAction*) const override; + + void setMolecule(QtGui::Molecule* mol) override; + +private: + QList m_models; + + void refreshModels(); + void unregisterModels(); + void registerModels(); +}; + +} // namespace QtPlugins +} // namespace Avogadro + +#endif // AVOGADRO_QTPLUGINS_SCRIPTCHARGES_H