diff --git a/avogadro/qtplugins/forcefield/scriptenergy.cpp b/avogadro/qtplugins/forcefield/scriptenergy.cpp index 293d1cac02..abd4a60222 100644 --- a/avogadro/qtplugins/forcefield/scriptenergy.cpp +++ b/avogadro/qtplugins/forcefield/scriptenergy.cpp @@ -27,6 +27,10 @@ #include #include +// nlohmann json for msgpack +#include +using json = nlohmann::json; + namespace Avogadro::QtPlugins { ScriptEnergy::ScriptEnergy(const QString& scriptFileName_) @@ -57,6 +61,11 @@ void ScriptEnergy::setMolecule(Core::Molecule* mol) { m_molecule = mol; + qDebug() << " ScriptEnergy::setMolecule() " << mol->formula().c_str() + << " method: " << m_identifier.c_str() + << " script: " << m_interpreter->scriptFilePath() << " msgPack " + << m_msgPack; + // should check if the molecule is valid for this script // .. this should never happen, but let's be defensive if (mol == nullptr || m_interpreter == nullptr) { @@ -116,23 +125,41 @@ Real ScriptEnergy::value(const Eigen::VectorXd& x) // write the new coordinates and read the energy QByteArray input; - for (Index i = 0; i < x.size(); i += 3) { - // write as x y z (space separated) - input += QString::number(x[i]).toUtf8() + " " + - QString::number(x[i + 1]).toUtf8() + " " + - QString::number(x[i + 2]).toUtf8() + "\n"; + if (m_msgPack) { + json j; + j["coordinates"] = json::array(); + for (Index i = 0; i < x.size(); ++i) { + j["coordinates"].push_back(x[i]); + } + auto msgPack = json::to_msgpack(j); + input = QByteArray::fromRawData( + reinterpret_cast(msgPack.data()), msgPack.size()); + } else { + for (Index i = 0; i < x.size(); i += 3) { + // write as x y z (space separated) + input += QString::number(x[i]).toUtf8() + " " + + QString::number(x[i + 1]).toUtf8() + " " + + QString::number(x[i + 2]).toUtf8() + "\n"; + } } QByteArray result = m_interpreter->asyncWriteAndResponse(input); - // go through lines in result until we see "AvogadroEnergy: " - QStringList lines = QString(result).remove('\r').split('\n'); double energy = 0.0; - for (auto line : lines) { - if (line.startsWith("AvogadroEnergy:")) { - QStringList items = line.split(" ", Qt::SkipEmptyParts); - if (items.size() > 1) { - energy = items[1].toDouble(); - break; + if (m_msgPack) { + json j = json::from_msgpack(result.toStdString()); + if (j.find("energy") != j.end()) { + return j["energy"]; + } + } else { + // go through lines in result until we see "AvogadroEnergy: " + QStringList lines = QString(result).remove('\r').split('\n'); + for (auto line : lines) { + if (line.startsWith("AvogadroEnergy:")) { + QStringList items = line.split(" ", Qt::SkipEmptyParts); + if (items.size() > 1) { + energy = items[1].toDouble(); + break; + } } } } @@ -150,36 +177,56 @@ void ScriptEnergy::gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) // Get the gradient from the script // write the new coordinates and read the energy QByteArray input; - for (Index i = 0; i < x.size(); i += 3) { - // write as x y z (space separated) - input += QString::number(x[i]).toUtf8() + " " + - QString::number(x[i + 1]).toUtf8() + " " + - QString::number(x[i + 2]).toUtf8() + "\n"; + if (m_msgPack) { + json j; + j["coordinates"] = json::array(); + for (Index i = 0; i < x.size(); ++i) { + j["coordinates"].push_back(x[i]); + } + auto msgPack = json::to_msgpack(j); + input = QByteArray::fromRawData( + reinterpret_cast(msgPack.data()), msgPack.size()); + } else { + for (Index i = 0; i < x.size(); i += 3) { + // write as x y z (space separated) + input += QString::number(x[i]).toUtf8() + " " + + QString::number(x[i + 1]).toUtf8() + " " + + QString::number(x[i + 2]).toUtf8() + "\n"; + } } QByteArray result = m_interpreter->asyncWriteAndResponse(input); - // parse the result - // first split on newlines - QStringList lines = QString(result).remove('\r').split('\n'); - unsigned int i = 0; - bool readingGrad = false; - for (auto line : lines) { - if (line.startsWith("AvogadroGradient:")) { - readingGrad = true; - continue; // next line + if (m_msgPack) { + json j = json::from_msgpack(result.toStdString()); + if (j.find("gradient") != j.end()) { + for (Index i = 0; i < x.size(); ++i) { + grad[i] = j["gradient"][i]; + } } - - if (readingGrad) { - QStringList items = line.split(" ", Qt::SkipEmptyParts); - if (items.size() == 3) { - grad[i] = items[0].toDouble(); - grad[i + 1] = items[1].toDouble(); - grad[i + 2] = items[2].toDouble(); - i += 3; + } else { + // parse the result + // first split on newlines + QStringList lines = QString(result).remove('\r').split('\n'); + unsigned int i = 0; + bool readingGrad = false; + for (auto line : lines) { + if (line.startsWith("AvogadroGradient:")) { + readingGrad = true; + continue; // next line } - if (i > x.size()) - break; + if (readingGrad) { + QStringList items = line.split(" ", Qt::SkipEmptyParts); + if (items.size() == 3) { + grad[i] = items[0].toDouble(); + grad[i + 1] = items[1].toDouble(); + grad[i + 2] = items[2].toDouble(); + i += 3; + } + + if (i > x.size()) + break; + } } } @@ -229,6 +276,7 @@ void ScriptEnergy::resetMetaData() m_valid = false; m_gradients = false; m_ions = false; + m_msgPack = false; m_radicals = false; m_unitCells = false; m_inputFormat = NotUsed; @@ -351,6 +399,14 @@ void ScriptEnergy::readMetaData() } m_radicals = metaData["radical"].toBool(); + // msgpack is optional + // defaults to not used + if (metaData["msgpack"].isBool()) { + m_msgPack = metaData["msgpack"].toBool(); + } else if (metaData["msgPack"].isBool()) { + m_msgPack = metaData["msgPack"].toBool(); + } + // get the element mask // (if it doesn't exist, the default is no elements anyway) m_valid = parseElements(metaData); diff --git a/avogadro/qtplugins/forcefield/scriptenergy.h b/avogadro/qtplugins/forcefield/scriptenergy.h index 9aab7bf0a1..6e4125d7a6 100644 --- a/avogadro/qtplugins/forcefield/scriptenergy.h +++ b/avogadro/qtplugins/forcefield/scriptenergy.h @@ -94,6 +94,7 @@ class ScriptEnergy : public Avogadro::Calc::EnergyCalculator bool m_ions; bool m_radicals; bool m_unitCells; + bool m_msgPack = false; std::string m_identifier; std::string m_name; diff --git a/avogadro/qtplugins/forcefield/scripts/ani2x.py b/avogadro/qtplugins/forcefield/scripts/ani2x.py index 06266cf08b..998a7e8f1b 100644 --- a/avogadro/qtplugins/forcefield/scripts/ani2x.py +++ b/avogadro/qtplugins/forcefield/scripts/ani2x.py @@ -5,6 +5,12 @@ import json import sys +try: + import msgpack + msgpack_available = True +except ImportError: + msgpack_available = False + try: import torch import torchani @@ -34,6 +40,8 @@ def getMetaData(): "ion": False, "radical": False, } + if (msgpack_available): + metaData["msgpack"] = True return metaData @@ -54,19 +62,35 @@ def run(filename): num_atoms = len(atoms) while True: # read new coordinates from stdin - for i in range(num_atoms): - np_coords[i] = np.fromstring(input(), sep=" ") + if (msgpack_available): + # unpack the coordinates + data = msgpack.unpackb(sys.stdin.buffer.read()) + np_coords = np.array(data["coordinates"], dtype=float).reshape(-1, 3) + else: + for i in range(num_atoms): + np_coords[i] = np.fromstring(input(), sep=" ") coordinates = torch.tensor([np_coords], requires_grad=True, device=device) # first print the energy of these coordinates energy = model((species, coordinates)).energies - print("AvogadroEnergy:", energy) # in Hartree + if (msgpack_available): + response = { "energy": energy } + else: + print("AvogadroEnergy:", energy) # in Hartree # now print the gradient on each atom - print("AvogadroGradient:") derivative = torch.autograd.grad(energy.sum(), coordinates)[0] - for i in range(num_atoms): - print(derivative[0][i][0].item(), derivative[0][i][1].item(), derivative[0][i][2].item()) + + if (msgpack_available): + gradient = [] + for i in range(num_atoms): + gradient.append([derivative[0][i][0].item(), derivative[0][i][1].item(), derivative[0][i][2].item()]) + response["gradient"] = gradient + print(msgpack.packb(response)) + else: + print("AvogadroGradient:") + for i in range(num_atoms): + print(derivative[0][i][0].item(), derivative[0][i][1].item(), derivative[0][i][2].item()) if __name__ == "__main__": diff --git a/avogadro/qtplugins/forcefield/scripts/gaff.py b/avogadro/qtplugins/forcefield/scripts/gaff.py index 491c66d1f6..9c3c4ca745 100644 --- a/avogadro/qtplugins/forcefield/scripts/gaff.py +++ b/avogadro/qtplugins/forcefield/scripts/gaff.py @@ -5,6 +5,12 @@ import json import sys +try: + import msgpack + msgpack_available = True +except ImportError: + msgpack_available = False + try: from openbabel import pybel import numpy as np @@ -30,6 +36,8 @@ def getMetaData(): "ion": False, "radical": False, } + if (msgpack_available): + metaData["msgpack"] = True return metaData @@ -48,24 +56,42 @@ def run(filename): num_atoms = len(mol.atoms) while True: # read new coordinates from stdin - for i in range(num_atoms): - coordinates = np.fromstring(input(), sep=" ") - atom = mol.atoms[i] - atom.OBAtom.SetVector(coordinates[0], coordinates[1], coordinates[2]) + if (msgpack_available): + # unpack the coordinates + data = msgpack.unpackb(sys.stdin.buffer.read()) + np_coords = np.array(data["coordinates"], dtype=float).reshape(-1, 3) + for i in range(num_atoms): + atom = mol.atoms[i] + atom.OBAtom.SetVector(np_coords[i][0], np_coords[i][1], np_coords[i][2]) + else: + for i in range(num_atoms): + coordinates = np.fromstring(input(), sep=" ") + atom = mol.atoms[i] + atom.OBAtom.SetVector(coordinates[0], coordinates[1], coordinates[2]) # update the molecule geometry for the next energy ff.SetCoordinates(mol.OBMol) # first print the energy of these coordinates energy = ff.Energy(True) # in kJ/mol - print("AvogadroEnergy:", energy) # in kJ/mol + if (msgpack_available): + response = {"energy": energy} + else: + print("AvogadroEnergy:", energy) # in kJ/mol # now print the gradient on each atom - print("AvogadroGradient:") - for atom in mol.atoms: - grad = ff.GetGradient(atom.OBAtom) - print(-1.0*grad.GetX(), -1.0*grad.GetY(), -1.0*grad.GetZ()) - + if (msgpack_available): + gradient = [] + for atom in mol.atoms: + grad = ff.GetGradient(atom.OBAtom) + gradient.append([-1.0*grad.GetX(), -1.0*grad.GetY(), -1.0*grad.GetZ()]) + response["gradient"] = gradient + print(msgpack.packb(response)) + else: + print("AvogadroGradient:") + for atom in mol.atoms: + grad = ff.GetGradient(atom.OBAtom) + print(-1.0*grad.GetX(), -1.0*grad.GetY(), -1.0*grad.GetZ()) if __name__ == "__main__": diff --git a/avogadro/qtplugins/forcefield/scripts/gfn1.py b/avogadro/qtplugins/forcefield/scripts/gfn1.py index fdb5a5fb1f..8b6b1211eb 100644 --- a/avogadro/qtplugins/forcefield/scripts/gfn1.py +++ b/avogadro/qtplugins/forcefield/scripts/gfn1.py @@ -5,6 +5,12 @@ import json import sys +try: + import msgpack + msgpack_available = True +except ImportError: + msgpack_available = False + try: import numpy as np from xtb.libxtb import VERBOSITY_MUTED @@ -29,6 +35,8 @@ def getMetaData(): "ion": True, "radical": True, } + if (msgpack_available): + metaData["msgpack"] = True return metaData @@ -63,25 +71,41 @@ def run(filename): # we loop forever - Avogadro will kill the process when done while(True): # read new coordinates from stdin - for i in range(len(atoms)): - coordinates[i] = np.fromstring(input()) - # .. convert from Angstrom to Bohr - coordinates /= 0.52917721067 + if (msgpack_available): + # unpack the coordinates + data = msgpack.unpackb(sys.stdin.buffer.read()) + np_coords = np.array(data["coordinates"], dtype=float).reshape(-1, 3) + # .. we need to convert from Angstrom to Bohr + np_coords /= 0.52917721067 + coordinates = np_coords + else: + for i in range(len(atoms)): + coordinates[i] = np.fromstring(input(), sep=' ') + # .. convert from Angstrom to Bohr + coordinates /= 0.52917721067 # update the calculator and run a new calculation calc.update(coordinates) calc.singlepoint(res) # first print the energy of these coordinates - print("AvogadroEnergy:", res.get_energy()) # in Hartree + if (msgpack_available): + response = {"energy": res.get_energy()} + else: + print("AvogadroEnergy:", res.get_energy()) # in Hartree # now print the gradient # .. we don't want the "[]" in the output - print("AvogadroGradient:") - grad = res.get_gradient() * 4961.475 # convert units - output = np.array2string(grad) - output = output.replace("[", "").replace("]", "") - print(output) + if (msgpack_available): + grad = res.get_gradient() * 4961.475 + response["gradient"] = grad.tolist() + print(msgpack.packb(response)) + else: + print("AvogadroGradient:") + grad = res.get_gradient() * 4961.475 # convert units + output = np.array2string(grad) + output = output.replace("[", "").replace("]", "") + print(output) if __name__ == "__main__": diff --git a/avogadro/qtplugins/forcefield/scripts/gfn2.py b/avogadro/qtplugins/forcefield/scripts/gfn2.py index 7b67c67c7e..e21624d244 100644 --- a/avogadro/qtplugins/forcefield/scripts/gfn2.py +++ b/avogadro/qtplugins/forcefield/scripts/gfn2.py @@ -5,6 +5,12 @@ import json import sys +try: + import msgpack + msgpack_available = True +except ImportError: + msgpack_available = False + try: import numpy as np from xtb.libxtb import VERBOSITY_MUTED @@ -29,6 +35,8 @@ def getMetaData(): "ion": True, "radical": True, } + if (msgpack_available): + metaData["msgpack"] = True return metaData @@ -63,25 +71,40 @@ def run(filename): # we loop forever - Avogadro will kill the process when done while(True): # read new coordinates from stdin - for i in range(len(atoms)): - coordinates[i] = np.fromstring(input()) - # .. convert from Angstrom to Bohr - coordinates /= 0.52917721067 + if (msgpack_available): + # unpack the coordinates + data = msgpack.unpackb(sys.stdin.buffer.read()) + np_coords = np.array(data["coordinates"], dtype=float).reshape(-1, 3) + # .. we need to convert from Angstrom to Bohr + np_coords /= 0.52917721067 + coordinates = np_coords + else: + for i in range(len(atoms)): + coordinates[i] = np.fromstring(input(), sep=' ') + # .. convert from Angstrom to Bohr + coordinates /= 0.52917721067 # update the calculator and run a new calculation calc.update(coordinates) calc.singlepoint(res) # first print the energy of these coordinates - print("AvogadroEnergy:", res.get_energy()) # in Hartree + if (msgpack_available): + response = {"energy": res.get_energy()} + else: + print("AvogadroEnergy:", res.get_energy()) # in Hartree # now print the gradient - # .. we don't want the "[]" in the output - print("AvogadroGradient:") - grad = res.get_gradient() * 4961.475 # convert units - output = np.array2string(grad) - output = output.replace("[", "").replace("]", "") - print(output) + if (msgpack_available): + grad = res.get_gradient() * 4961.475 + response["gradient"] = grad.tolist() + print(msgpack.packb(response)) + else: + print("AvogadroGradient:") + grad = res.get_gradient() * 4961.475 # convert units + output = np.array2string(grad) + output = output.replace("[", "").replace("]", "") + print(output) if __name__ == "__main__": diff --git a/avogadro/qtplugins/forcefield/scripts/gfnff.py b/avogadro/qtplugins/forcefield/scripts/gfnff.py index ddb0d44596..2e39614a5a 100644 --- a/avogadro/qtplugins/forcefield/scripts/gfnff.py +++ b/avogadro/qtplugins/forcefield/scripts/gfnff.py @@ -6,6 +6,12 @@ import sys import os +try: + import msgpack + msgpack_available = True +except ImportError: + msgpack_available = False + try: import numpy as np from xtb.libxtb import VERBOSITY_MUTED @@ -74,6 +80,8 @@ def getMetaData(): "ion": True, "radical": False, } + if (msgpack_available): + metaData["msgpack"] = True return metaData @@ -105,7 +113,7 @@ def run(filename): # and then just ignore it f = io.BytesIO() with stdout_redirector(f): - calc = Calculator(Param.GFNFF, atoms, coordinates, + calc = Calculator(Param.GFNFF, atoms, coordinates, charge=charge, uhf=spin) calc.set_verbosity(VERBOSITY_MUTED) res = calc.singlepoint() @@ -113,25 +121,41 @@ def run(filename): # we loop forever - Avogadro will kill our process when done while True: # read new coordinates from stdin - for i in range(len(atoms)): - coordinates[i] = np.fromstring(input(), sep=' ') - # .. convert from Angstrom to Bohr - coordinates /= 0.52917721067 + if (msgpack_available): + # unpack the coordinates + data = msgpack.unpackb(sys.stdin.buffer.read()) + np_coords = np.array(data["coordinates"], dtype=float).reshape(-1, 3) + # .. we need to convert from Angstrom to Bohr + np_coords /= 0.52917721067 + coordinates = np_coords + else: + for i in range(len(atoms)): + coordinates[i] = np.fromstring(input(), sep=' ') + # .. convert from Angstrom to Bohr + coordinates /= 0.52917721067 # update the calculator and run a new calculation calc.update(coordinates) calc.singlepoint(res) - print("AvogadroEnergy:", res.get_energy()) # in Hartree + if (msgpack_available): + response = {"energy": res.get_energy()} + else: + print("AvogadroEnergy:", res.get_energy()) # in Hartree # times 2625.5 kJ/mol # now print the gradient # .. we don't want the "[]" in the output - print("AvogadroGradient:") - grad = res.get_gradient() * 4961.475 # convert units - output = np.array2string(grad) - output = output.replace("[", "").replace("]", "") - print(output) + if (msgpack_available): + grad = res.get_gradient() * 4961.475 + response["gradient"] = grad.tolist() + print(msgpack.packb(response)) + else: + print("AvogadroGradient:") + grad = res.get_gradient() * 4961.475 # convert units + output = np.array2string(grad) + output = output.replace("[", "").replace("]", "") + print(output) if __name__ == "__main__": diff --git a/avogadro/qtplugins/forcefield/scripts/mmff94.py b/avogadro/qtplugins/forcefield/scripts/mmff94.py index 3206123c40..4167dc0fd2 100644 --- a/avogadro/qtplugins/forcefield/scripts/mmff94.py +++ b/avogadro/qtplugins/forcefield/scripts/mmff94.py @@ -5,6 +5,12 @@ import json import sys +try: + import msgpack + msgpack_available = True +except ImportError: + msgpack_available = False + try: from openbabel import pybel import numpy as np @@ -30,6 +36,8 @@ def getMetaData(): "ion": False, "radical": False, } + if (msgpack_available): + metaData["msgpack"] = True return metaData @@ -48,23 +56,42 @@ def run(filename): num_atoms = len(mol.atoms) while True: # read new coordinates from stdin - for i in range(num_atoms): - coordinates = np.fromstring(input(), sep=" ") - atom = mol.atoms[i] - atom.OBAtom.SetVector(coordinates[0], coordinates[1], coordinates[2]) + if (msgpack_available): + # unpack the coordinates + data = msgpack.unpackb(sys.stdin.buffer.read()) + np_coords = np.array(data["coordinates"], dtype=float).reshape(-1, 3) + for i in range(num_atoms): + atom = mol.atoms[i] + atom.OBAtom.SetVector(np_coords[i][0], np_coords[i][1], np_coords[i][2]) + else: + for i in range(num_atoms): + coordinates = np.fromstring(input(), sep=" ") + atom = mol.atoms[i] + atom.OBAtom.SetVector(coordinates[0], coordinates[1], coordinates[2]) # update the molecule geometry for the next energy ff.SetCoordinates(mol.OBMol) # first print the energy of these coordinates energy = ff.Energy(True) # in kJ/mol - print("AvogadroEnergy:", energy) # in kJ/mol + if (msgpack_available): + response = {"energy": energy} + else: + print("AvogadroEnergy:", energy) # in kJ/mol # now print the gradient on each atom - print("AvogadroGradient:") - for atom in mol.atoms: - grad = ff.GetGradient(atom.OBAtom) - print(-1.0*grad.GetX(), -1.0*grad.GetY(), -1.0*grad.GetZ()) + if (msgpack_available): + gradient = [] + for atom in mol.atoms: + grad = ff.GetGradient(atom.OBAtom) + gradient.append([-1.0*grad.GetX(), -1.0*grad.GetY(), -1.0*grad.GetZ()]) + response["gradient"] = gradient + print(msgpack.packb(response)) + else: + print("AvogadroGradient:") + for atom in mol.atoms: + grad = ff.GetGradient(atom.OBAtom) + print(-1.0*grad.GetX(), -1.0*grad.GetY(), -1.0*grad.GetZ()) diff --git a/avogadro/qtplugins/forcefield/scripts/uff.py b/avogadro/qtplugins/forcefield/scripts/uff.py index 6c5c926be5..d2efc1209b 100644 --- a/avogadro/qtplugins/forcefield/scripts/uff.py +++ b/avogadro/qtplugins/forcefield/scripts/uff.py @@ -5,6 +5,12 @@ import json import sys +try: + import msgpack + msgpack_available = True +except ImportError: + msgpack_available = False + try: from openbabel import pybel import numpy as np @@ -30,6 +36,8 @@ def getMetaData(): "ion": False, "radical": False, } + if (msgpack_available): + metaData["msgpack"] = True return metaData @@ -48,23 +56,42 @@ def run(filename): num_atoms = len(mol.atoms) while True: # read new coordinates from stdin - for i in range(num_atoms): - coordinates = np.fromstring(input(), sep=" ") - atom = mol.atoms[i] - atom.OBAtom.SetVector(coordinates[0], coordinates[1], coordinates[2]) + if (msgpack_available): + # unpack the coordinates + data = msgpack.unpackb(sys.stdin.buffer.read()) + np_coords = np.array(data["coordinates"], dtype=float).reshape(-1, 3) + for i in range(num_atoms): + atom = mol.atoms[i] + atom.OBAtom.SetVector(np_coords[i][0], np_coords[i][1], np_coords[i][2]) + else: + for i in range(num_atoms): + coordinates = np.fromstring(input(), sep=" ") + atom = mol.atoms[i] + atom.OBAtom.SetVector(coordinates[0], coordinates[1], coordinates[2]) # update the molecule geometry for the next energy ff.SetCoordinates(mol.OBMol) # first print the energy of these coordinates energy = ff.Energy(True) # in kJ/mol - print("AvogadroEnergy:", energy) # in kJ/mol + if (msgpack_available): + response = {"energy": energy} + else: + print("AvogadroEnergy:", energy) # in kJ/mol # now print the gradient on each atom - print("AvogadroGradient:") - for atom in mol.atoms: - grad = ff.GetGradient(atom.OBAtom) - print(-1.0*grad.GetX(), -1.0*grad.GetY(), -1.0*grad.GetZ()) + if (msgpack_available): + gradient = [] + for atom in mol.atoms: + grad = ff.GetGradient(atom.OBAtom) + gradient.append([-1.0*grad.GetX(), -1.0*grad.GetY(), -1.0*grad.GetZ()]) + response["gradient"] = gradient + print(msgpack.packb(response)) + else: + print("AvogadroGradient:") + for atom in mol.atoms: + grad = ff.GetGradient(atom.OBAtom) + print(-1.0*grad.GetX(), -1.0*grad.GetY(), -1.0*grad.GetZ()) if __name__ == "__main__":