From de67512bad66c18c58785093eff93e56f2029584 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Mon, 11 Jul 2022 14:47:39 -0400 Subject: [PATCH 01/22] Import stable version of CppNumericalSolvers https://github.com/PatWie/CppNumericalSolvers/tree/master/include/cppoptlib Signed-off-by: Geoff Hutchison --- thirdparty/cppoptlib/BUILD | 9 + thirdparty/cppoptlib/LICENSE | 22 ++ thirdparty/cppoptlib/README.md | 236 +++++++++++++ thirdparty/cppoptlib/boundedproblem.h | 63 ++++ thirdparty/cppoptlib/linesearch/armijo.h | 83 +++++ thirdparty/cppoptlib/linesearch/morethuente.h | 314 ++++++++++++++++++ .../cppoptlib/linesearch/wolfeheuristic.h | 75 +++++ thirdparty/cppoptlib/meta.h | 152 +++++++++ thirdparty/cppoptlib/problem.h | 217 ++++++++++++ thirdparty/cppoptlib/solver/bfgssolver.h | 67 ++++ thirdparty/cppoptlib/solver/cmaesbsolver.h | 231 +++++++++++++ thirdparty/cppoptlib/solver/cmaessolver.h | 205 ++++++++++++ .../solver/conjugatedgradientdescentsolver.h | 61 ++++ .../cppoptlib/solver/gradientdescentsolver.h | 50 +++ thirdparty/cppoptlib/solver/isolver.h | 57 ++++ thirdparty/cppoptlib/solver/lbfgsbsolver.h | 306 +++++++++++++++++ thirdparty/cppoptlib/solver/lbfgssolver.h | 121 +++++++ .../cppoptlib/solver/neldermeadsolver.h | 225 +++++++++++++ .../cppoptlib/solver/newtondescentsolver.h | 43 +++ thirdparty/cppoptlib/timer.h | 90 +++++ 20 files changed, 2627 insertions(+) create mode 100644 thirdparty/cppoptlib/BUILD create mode 100644 thirdparty/cppoptlib/LICENSE create mode 100644 thirdparty/cppoptlib/README.md create mode 100644 thirdparty/cppoptlib/boundedproblem.h create mode 100644 thirdparty/cppoptlib/linesearch/armijo.h create mode 100644 thirdparty/cppoptlib/linesearch/morethuente.h create mode 100644 thirdparty/cppoptlib/linesearch/wolfeheuristic.h create mode 100644 thirdparty/cppoptlib/meta.h create mode 100644 thirdparty/cppoptlib/problem.h create mode 100644 thirdparty/cppoptlib/solver/bfgssolver.h create mode 100644 thirdparty/cppoptlib/solver/cmaesbsolver.h create mode 100644 thirdparty/cppoptlib/solver/cmaessolver.h create mode 100644 thirdparty/cppoptlib/solver/conjugatedgradientdescentsolver.h create mode 100644 thirdparty/cppoptlib/solver/gradientdescentsolver.h create mode 100644 thirdparty/cppoptlib/solver/isolver.h create mode 100644 thirdparty/cppoptlib/solver/lbfgsbsolver.h create mode 100644 thirdparty/cppoptlib/solver/lbfgssolver.h create mode 100644 thirdparty/cppoptlib/solver/neldermeadsolver.h create mode 100644 thirdparty/cppoptlib/solver/newtondescentsolver.h create mode 100644 thirdparty/cppoptlib/timer.h diff --git a/thirdparty/cppoptlib/BUILD b/thirdparty/cppoptlib/BUILD new file mode 100644 index 0000000000..f44960a115 --- /dev/null +++ b/thirdparty/cppoptlib/BUILD @@ -0,0 +1,9 @@ +cc_library( + name = "cppoptlib", + hdrs = glob([ + "*.h", + "solver/*.h", + "linesearch/*.h", + ]), + visibility = ["//visibility:public"], +) diff --git a/thirdparty/cppoptlib/LICENSE b/thirdparty/cppoptlib/LICENSE new file mode 100644 index 0000000000..5955b8ffaa --- /dev/null +++ b/thirdparty/cppoptlib/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014-2015 Patrick Wieschollek +Copyright (c) 2015- the respective contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/thirdparty/cppoptlib/README.md b/thirdparty/cppoptlib/README.md new file mode 100644 index 0000000000..862dc9ee83 --- /dev/null +++ b/thirdparty/cppoptlib/README.md @@ -0,0 +1,236 @@ +CppOptimizationLibrary (Support for TensorFlow) +================================================================= + +[![Build Status](http://ci.patwie.com/api/badges/PatWie/CppNumericalSolvers/status.svg)](http://ci.patwie.com/PatWie/CppNumericalSolvers) + +A *header-only* library with bindings to **C++**, **[TensorFlow](https://www.tensorflow.org/)** and **Matlab**. + +Have you ever looked for a C++ function *fminsearch*, which is easy to use without adding tons of dependencies and without editing many setting-structs and without dependencies? Solve even your large-scale problems by using **TensorFlow+Python** to accelerate the minimization. See the TensorFlow-Example. + +All solvers are written scratch using Eigen, which means they are very easy to use. Want a full example? + +```cpp + class Rosenbrock : public Problem { + public: + double value(const Vector &x) { + const double t1 = (1 - x[0]); + const double t2 = (x[1] - x[0] * x[0]); + return t1 * t1 + 100 * t2 * t2; + } + }; + int main(int argc, char const *argv[]) { + Rosenbrock f; + Vector x(2); x << -1, 2; + BfgsSolver solver; + solver.minimize(f, x); + std::cout << "argmin " << x.transpose() << std::endl; + std::cout << "f in argmin " << f(x) << std::endl; + return 0; + } +``` +To use another solver, simply replace `BfgsSolver` by another name. +Supported solvers are: + +- gradient descent solver (GradientDescentSolver) +- conjugate gradient descent solver (ConjugatedGradientDescentSolver) +- Newton descent solver (NewtonDescentSolver) +- BFGS solver (BfgsSolver) +- L-BFGS solver (LbfgsSolver) +- L-BFGS-B solver (LbfgsbSolver) +- CMAes solver (CMAesSolver) +- Nelder-Mead solver (NelderMeadSolver) + +These solvers are tested on the Rosenbrock function from multiple difficult starting points by unit tests using the Google Testing Framework. And yes, you can use them directly in MATLAB. +Additional benchmark functions are *Beale, GoldsteinPrice, Booth, Matyas, Levi*. Note, not all solvers are equivalently good at all problems. + +For checking your gradient this library uses high-order central difference. Study the examples for more information about including box-constraints and gradient-information. + + +Extensive Introduction +----------- + +There are currently two ways to use this library: directly in your C++ code or in MATLAB by calling the provided mex-File. + +## TensorFlow + +You can use the expressive power of TensorFlow to accelerate the problem evaluation and compute reliable the gradients. You just write the problem in Python: + +```python +# y = x'Ax + b'x + c +y = tf.matmul(x, tf.matmul(A, x, transpose_b=True)) + tf.matmul(x, b, transpose_b=True) + c +dx = tf.gradients(y, x)[0] + +y = tf.identity(y, name='problem_objective') +dx = tf.identity(dx, name='problem_gradient') +``` + +and let TensorFlow figure out how to evaluate this expression and the gradients. + +## C++ + +There are several examples within the `src/examples` directory. These are built into `build/bin/examples` during `make all`. +Checkout `rosenbrock.cpp`. Your objective and gradient computations should be stored into a tiny class. The most simple usage is + +```cpp +class YourProblem : public Problem { + double value(const Vector &x) {} +} +``` + +In contrast to previous versions of this library, I switched to classes instead of lambda function. If you poke the examples, you will notice that this is much easier to write and understand. The only method a problem has to provide is the `value` member, which returns the value of the objective function. +For most solvers it should be useful to implement the gradient computation, too. Otherwise the library internally will use finite difference for gradient computations (which is definitely unstable and slow!). + +```cpp +class YourProblem : public Problem { + double value(const Vector &x) {} + void gradient(const Vector &x, Vector &grad) {} +} +``` + +Notice, the gradient is passed by reference! +After defining the problem it can be initialized in your code by: + +```cpp +// init problem +YourProblem f; +// test your gradient ONCE +bool probably_correct = f.checkGradient(x); +``` + +By convention, a solver minimizes a given objective function starting in `x` + +```cpp +// choose a solver +BfgsSolver solver; +// and minimize the function +solver.minimize(f, x); +double minValue = f(x); +``` + +For convenience there are some typedefs: + +```cpp +cppoptlib::Vector is a column vector Eigen::Matrix; +cppoptlib::Matrix is a matrix Eigen::Matrix; +``` +### full example + +```cpp +#include +#include "../../include/cppoptlib/meta.h" +#include "../../include/cppoptlib/problem.h" +#include "../../include/cppoptlib/solver/bfgssolver.h" + +using namespace cppoptlib; +using Eigen::VectorXd; + +class Rosenbrock : public Problem { + public: + using typename cppoptlib::Problem::Scalar; + using typename cppoptlib::Problem::TVector; + + double value(const TVector &x) { + const double t1 = (1 - x[0]); + const double t2 = (x[1] - x[0] * x[0]); + return t1 * t1 + 100 * t2 * t2; + } + void gradient(const TVector &x, TVector &grad) { + grad[0] = -2 * (1 - x[0]) + 200 * (x[1] - x[0] * x[0]) * (-2 * x[0]); + grad[1] = 200 * (x[1] - x[0] * x[0]); + } +}; + +int main(int argc, char const *argv[]) { + Rosenbrock f; + BfgsSolver solver; + VectorXd x(2); x << -1, 2; + solver.minimize(f, x); + std::cout << "argmin " << x.transpose() << std::endl; + std::cout << "f in argmin " << f(x) << std::endl; + return 0; +} +``` +### using box-constraints + +The `L-BFGS-B` algorithm allows to optimize functions with box-constraints, i.e., `min_x f(x) s.t. a <= x <= b` for some `a, b`. Given a `BoundedProblem`-class you can enter these constraints by + +```cpp +// init problem +YourBoundedProblem f; +f.setLowerBound(Vector::Zero(DIM)); +f.setUpperBound(Vector::Ones(DIM)*5); +// init solver +cppoptlib::LbfgsbSolver solver; +solver.minimize(f, x); +``` + +If you do not specify a bound, the algorithm will assume the unbounded case, eg. + +```cpp +// init problem +YourBoundedProblem f; +f.setLowerBound(Vector::Zero(DIM)); +// init solver +cppoptlib::LbfgsbSolver solver; +solver.minimize(f, x); +``` + +will optimize in x s.t. `0 <= x`. +See [src/examples/nonnegls.cpp](src/examples/nonnegls.cpp) for an example using L-BFGS-B to solve a bounded problem. + +## within MATLAB + +Simply create a function file for the objective and replace `fminsearch` or `fminunc` with `cppoptlib`. If you want to use symbolic gradient or hessian information see file `example.m` for details. A basic example would be: + +```matlab +x0 = [-1,2]'; +[fx,x] = cppoptlib(x0, @rosenbrock,'gradient',@rosenbrock_grad,'solver','bfgs'); +fx = cppoptlib(x0, @rosenbrock); +fx = fminsearch(x0, @rosenbrock); +``` +Even optimizing without any gradient information this library outperforms optimization routines from MATLAB on some problems. + +Install +----------- + +Note, this library is header-only, so you just need to add `include/cppoptlib` to your project without compiling anything and without adding further dependencies. We ship some examples for demonstration purposes and use [bazel](https://bazel.build/) to compile these examples and unittests. The latest commit using CMake is da314c6581d076e0dbadacdd263aefe4d06a2397. + +When using the MATLAB-binding, you need to compile the mex-file. Hereby, open Matlab and run `make.m` inside the MATLAB folder once. For TensorFlow-Support you need to build TensorFlow-Library from source + +```console +user@host $ bazel build -c opt --copt=-mfpmath=both --copt=-msse4.2 --config=cuda //tensorflow:libtensorflow.so +user@host $ bazel build -c opt --copt=-mfpmath=both --copt=-msse4.2 --config=cuda //tensorflow:libtensorflow_cc.so +``` +# Benchmarks + +Currently, not all solvers are equally good at all objective functions. The file `src/test/benchmark.cpp` contains some challenging objective functions which are tested by each provided solver. Note, MATLAB will also fail at some objective functions. + +# Contribute + +Make sure that `python lint.py` does not display any errors and check if travis is happy. It would be great, if some of the Forks-Owner are willing to make pull-request. + +[eigen3]: http://eigen.tuxfamily.org/ +[bazel]: https://bazel.build/ +[matlab]: http://www.mathworks.de/products/matlab/ +[tensorflow]: https://www.tensorflow.org/ + +# References + +**L-BFGS-B**: A LIMITED MEMORY ALGORITHM FOR BOUND CONSTRAINED OPTIMIZATION +*Richard H. Byrd, Peihuang Lu, Jorge Nocedal and Ciyou Zhu* + +**L-BFGS**: Numerical Optimization, 2nd ed. New York: Springer +*J. Nocedal and S. J. Wright* + +# Citing this implementation + +I see some interests in citing this implementation. Please use the following bibtex entry, if you consider to cite this implementation: + +```bibtex +@misc{wieschollek2016cppoptimizationlibrary, + title={CppOptimizationLibrary}, + author={Wieschollek, Patrick}, + year={2016}, + howpublished={\url{https://github.com/PatWie/CppNumericalSolvers}}, +} +``` diff --git a/thirdparty/cppoptlib/boundedproblem.h b/thirdparty/cppoptlib/boundedproblem.h new file mode 100644 index 0000000000..7f41099ee3 --- /dev/null +++ b/thirdparty/cppoptlib/boundedproblem.h @@ -0,0 +1,63 @@ +/* + * This file is part of CppNumericalSolvers + * + * Copyright (C) Tobias Wood @spinicist 2016 + * + * This Source Code form is subject to the terms of the MIT license. + * Please see the LICENSE file. + * + */ + +#ifndef BOUNDEDPROBLEM_H +#define BOUNDEDPROBLEM_H + +#include +#include + +#include "problem.h" + +namespace cppoptlib { + +template +class BoundedProblem : public Problem { +public: + using Superclass = Problem; + using typename Superclass::Scalar; + using typename Superclass::TVector; + +protected: + TVector m_lowerBound; + TVector m_upperBound; + +public: + BoundedProblem(int RunDim = CompileDim_) : Superclass() { + TVector infBound(RunDim); + infBound.setConstant(std::numeric_limits::infinity()); + m_lowerBound = -infBound; + m_upperBound = infBound; + } + + BoundedProblem(const TVector &l, const TVector &u) : + Superclass(), + m_lowerBound(l), + m_upperBound(u) + {} + + const TVector &lowerBound() const { return m_lowerBound; } + void setLowerBound(const TVector &lb) { m_lowerBound = lb; } + const TVector &upperBound() const { return m_upperBound; } + void setUpperBound(const TVector &ub) { m_upperBound = ub; } + + void setBoxConstraint(TVector lb, TVector ub) { + setLowerBound(lb); + setUpperBound(ub); + } + + bool isValid(const TVector &x){ + return ((x - m_lowerBound).array() >= 0.0).all() && ((x - m_upperBound).array() <= 0.0).all(); + } +}; + +} // end namespace cppoptlib + +#endif // BOUNDEDPROBLEM_H diff --git a/thirdparty/cppoptlib/linesearch/armijo.h b/thirdparty/cppoptlib/linesearch/armijo.h new file mode 100644 index 0000000000..2e25cf353d --- /dev/null +++ b/thirdparty/cppoptlib/linesearch/armijo.h @@ -0,0 +1,83 @@ +// CppNumericalSolver +#ifndef ARMIJO_H_ +#define ARMIJO_H_ + +#include "../meta.h" + +namespace cppoptlib { + +template +class Armijo { +public: + using Scalar = typename ProblemType::Scalar; + using TVector = typename ProblemType::TVector; + /** + * @brief use Armijo Rule for (weak) Wolfe conditiions + * @details [long description] + * + * @param searchDir search direction for next update step + * @param objFunc handle to problem + * + * @return step-width + */ + static Scalar linesearch(const TVector &x, const TVector &searchDir, ProblemType &objFunc, const Scalar alpha_init = 1.0) { + const Scalar c = 0.2; + const Scalar rho = 0.9; + Scalar alpha = alpha_init; + Scalar f = objFunc.value(x + alpha * searchDir); + const Scalar f_in = objFunc.value(x); + TVector grad(x.rows()); + objFunc.gradient(x, grad); + const Scalar Cache = c * grad.dot(searchDir); + + while(f > f_in + alpha * Cache) { + alpha *= rho; + f = objFunc.value(x + alpha * searchDir); + } + + return alpha; + } + +}; + +template +class Armijo { + + public: + using typename ProblemType::Scalar; + using typename ProblemType::TVector; + using typename ProblemType::THessian; + /** + * @brief use Armijo Rule for (weak) Wolfe conditiions + * @details [long description] + * + * @param searchDir search direction for next update step + * @param objFunc handle to problem + * + * @return step-width + */ + static Scalar linesearch(const TVector &x, const TVector &searchDir, ProblemType &objFunc) { + const Scalar c = 0.2; + const Scalar rho = 0.9; + Scalar alpha = 1.0; + + Scalar f = objFunc.value(x + alpha * searchDir); + const Scalar f_in = objFunc.value(x); + const THessian hessian(x.rows(), x.rows()); + objFunc.hessian(x, hessian); + TVector grad(x.rows()); + objFunc.gradient(x, grad); + const Scalar Cache = c * grad.dot(searchDir) + 0.5 * c*c * searchDir.transpose() * (hessian * searchDir); + + while(f > f_in + alpha * Cache) { + alpha *= rho; + f = objFunc.value(x + alpha * searchDir); + } + return alpha; + } + +}; + +} + +#endif /* ARMIJO_H_ */ diff --git a/thirdparty/cppoptlib/linesearch/morethuente.h b/thirdparty/cppoptlib/linesearch/morethuente.h new file mode 100644 index 0000000000..7c6c6298ff --- /dev/null +++ b/thirdparty/cppoptlib/linesearch/morethuente.h @@ -0,0 +1,314 @@ +// CppNumericalSolver +#ifndef MORETHUENTE_H_ +#define MORETHUENTE_H_ + +#include "../meta.h" +#include + +namespace cppoptlib { + +template +class MoreThuente { + + public: + using Scalar = typename ProblemType::Scalar; + using TVector = typename ProblemType::TVector; + + /** + * @brief use MoreThuente Rule for (strong) Wolfe conditiions + * @details [long description] + * + * @param searchDir search direction for next update step + * @param objFunc handle to problem + * + * @return step-width + */ + + static Scalar linesearch(const TVector &x, const TVector &searchDir, ProblemType &objFunc, const Scalar alpha_init = 1.0) { + // assume step width + Scalar ak = alpha_init; + + Scalar fval = objFunc.value(x); + TVector g = x.eval(); + objFunc.gradient(x, g); + + TVector s = searchDir.eval(); + TVector xx = x.eval(); + + cvsrch(objFunc, xx, fval, g, ak, s); + + return ak; + } + + static int cvsrch(ProblemType &objFunc, TVector &x, Scalar f, TVector &g, Scalar &stp, TVector &s) { + // we rewrite this from MIN-LAPACK and some MATLAB code + int info = 0; + int infoc = 1; + const Scalar xtol = 1e-15; + const Scalar ftol = 1e-4; + const Scalar gtol = 1e-2; + const Scalar stpmin = 1e-15; + const Scalar stpmax = 1e15; + const Scalar xtrapf = 4; + const int maxfev = 20; + int nfev = 0; + + Scalar dginit = g.dot(s); + if (dginit >= 0.0) { + // no descent direction + // TODO: handle this case + return -1; + } + + bool brackt = false; + bool stage1 = true; + + Scalar finit = f; + Scalar dgtest = ftol * dginit; + Scalar width = stpmax - stpmin; + Scalar width1 = 2 * width; + TVector wa = x.eval(); + + Scalar stx = 0.0; + Scalar fx = finit; + Scalar dgx = dginit; + Scalar sty = 0.0; + Scalar fy = finit; + Scalar dgy = dginit; + + Scalar stmin; + Scalar stmax; + + while (true) { + + // make sure we stay in the interval when setting min/max-step-width + if (brackt) { + stmin = std::min(stx, sty); + stmax = std::max(stx, sty); + } else { + stmin = stx; + stmax = stp + xtrapf * (stp - stx); + } + + // Force the step to be within the bounds stpmax and stpmin. + stp = std::max(stp, stpmin); + stp = std::min(stp, stpmax); + + // Oops, let us return the last reliable values + if ( + (brackt && ((stp <= stmin) || (stp >= stmax))) + || (nfev >= maxfev - 1 ) || (infoc == 0) + || (brackt && ((stmax - stmin) <= (xtol * stmax)))) { + stp = stx; + } + + // test new point + x = wa + stp * s; + f = objFunc.value(x); + objFunc.gradient(x, g); + nfev++; + Scalar dg = g.dot(s); + Scalar ftest1 = finit + stp * dgtest; + + // all possible convergence tests + if ((brackt & ((stp <= stmin) | (stp >= stmax))) | (infoc == 0)) + info = 6; + + if ((stp == stpmax) & (f <= ftest1) & (dg <= dgtest)) + info = 5; + + if ((stp == stpmin) & ((f > ftest1) | (dg >= dgtest))) + info = 4; + + if (nfev >= maxfev) + info = 3; + + if (brackt & (stmax - stmin <= xtol * stmax)) + info = 2; + + if ((f <= ftest1) & (fabs(dg) <= gtol * (-dginit))) + info = 1; + + // terminate when convergence reached + if (info != 0) + return -1; + + if (stage1 & (f <= ftest1) & (dg >= std::min(ftol, gtol)*dginit)) + stage1 = false; + + if (stage1 & (f <= fx) & (f > ftest1)) { + Scalar fm = f - stp * dgtest; + Scalar fxm = fx - stx * dgtest; + Scalar fym = fy - sty * dgtest; + Scalar dgm = dg - dgtest; + Scalar dgxm = dgx - dgtest; + Scalar dgym = dgy - dgtest; + + cstep( stx, fxm, dgxm, sty, fym, dgym, stp, fm, dgm, brackt, stmin, stmax, infoc); + + fx = fxm + stx * dgtest; + fy = fym + sty * dgtest; + dgx = dgxm + dgtest; + dgy = dgym + dgtest; + } else { + // this is ugly and some variables should be moved to the class scope + cstep( stx, fx, dgx, sty, fy, dgy, stp, f, dg, brackt, stmin, stmax, infoc); + } + + if (brackt) { + if (fabs(sty - stx) >= 0.66 * width1) + stp = stx + 0.5 * (sty - stx); + width1 = width; + width = fabs(sty - stx); + } + } + + return 0; + } + + static int cstep(Scalar& stx, Scalar& fx, Scalar& dx, Scalar& sty, Scalar& fy, Scalar& dy, Scalar& stp, + Scalar& fp, Scalar& dp, bool& brackt, Scalar& stpmin, Scalar& stpmax, int& info) { + info = 0; + bool bound = false; + + // Check the input parameters for errors. + if ((brackt & ((stp <= std::min(stx, sty) ) | (stp >= std::max(stx, sty)))) | (dx * (stp - stx) >= 0.0) + | (stpmax < stpmin)) { + return -1; + } + + Scalar sgnd = dp * (dx / fabs(dx)); + + Scalar stpf = 0; + Scalar stpc = 0; + Scalar stpq = 0; + + if (fp > fx) { + info = 1; + bound = true; + Scalar theta = 3. * (fx - fp) / (stp - stx) + dx + dp; + Scalar s = std::max(theta, std::max(dx, dp)); + Scalar gamma = s * sqrt((theta / s) * (theta / s) - (dx / s) * (dp / s)); + if (stp < stx) + gamma = -gamma; + Scalar p = (gamma - dx) + theta; + Scalar q = ((gamma - dx) + gamma) + dp; + Scalar r = p / q; + stpc = stx + r * (stp - stx); + stpq = stx + ((dx / ((fx - fp) / (stp - stx) + dx)) / 2.) * (stp - stx); + if (fabs(stpc - stx) < fabs(stpq - stx)) + stpf = stpc; + else + stpf = stpc + (stpq - stpc) / 2; + brackt = true; + } else if (sgnd < 0.0) { + info = 2; + bound = false; + Scalar theta = 3 * (fx - fp) / (stp - stx) + dx + dp; + Scalar s = std::max(theta, std::max(dx, dp)); + Scalar gamma = s * sqrt((theta / s) * (theta / s) - (dx / s) * (dp / s)); + if (stp > stx) + gamma = -gamma; + + Scalar p = (gamma - dp) + theta; + Scalar q = ((gamma - dp) + gamma) + dx; + Scalar r = p / q; + stpc = stp + r * (stx - stp); + stpq = stp + (dp / (dp - dx)) * (stx - stp); + if (fabs(stpc - stp) > fabs(stpq - stp)) + stpf = stpc; + else + stpf = stpq; + brackt = true; + } else if (fabs(dp) < fabs(dx)) { + info = 3; + bound = 1; + Scalar theta = 3 * (fx - fp) / (stp - stx) + dx + dp; + Scalar s = std::max(theta, std::max( dx, dp)); + Scalar gamma = s * sqrt(std::max(static_cast(0.), (theta / s) * (theta / s) - (dx / s) * (dp / s))); + if (stp > stx) + gamma = -gamma; + Scalar p = (gamma - dp) + theta; + Scalar q = (gamma + (dx - dp)) + gamma; + Scalar r = p / q; + if ((r < 0.0) & (gamma != 0.0)) { + stpc = stp + r * (stx - stp); + } else if (stp > stx) { + stpc = stpmax; + } else { + stpc = stpmin; + } + stpq = stp + (dp / (dp - dx)) * (stx - stp); + if (brackt) { + if (fabs(stp - stpc) < fabs(stp - stpq)) { + stpf = stpc; + } else { + stpf = stpq; + } + } else { + if (fabs(stp - stpc) > fabs(stp - stpq)) { + stpf = stpc; + } else { + stpf = stpq; + } + + } + } else { + info = 4; + bound = false; + if (brackt) { + Scalar theta = 3 * (fp - fy) / (sty - stp) + dy + dp; + Scalar s = std::max(theta, std::max(dy, dp)); + Scalar gamma = s * sqrt((theta / s) * (theta / s) - (dy / s) * (dp / s)); + if (stp > sty) + gamma = -gamma; + + Scalar p = (gamma - dp) + theta; + Scalar q = ((gamma - dp) + gamma) + dy; + Scalar r = p / q; + stpc = stp + r * (sty - stp); + stpf = stpc; + } else if (stp > stx) + stpf = stpmax; + else { + stpf = stpmin; + } + } + + if (fp > fx) { + sty = stp; + fy = fp; + dy = dp; + } else { + if (sgnd < 0.0) { + sty = stx; + fy = fx; + dy = dx; + } + + stx = stp; + fx = fp; + dx = dp; + } + + stpf = std::min(stpmax, stpf); + stpf = std::max(stpmin, stpf); + stp = stpf; + + if (brackt & bound) { + if (sty > stx) { + stp = std::min(stx + static_cast(0.66) * (sty - stx), stp); + } else { + stp = std::max(stx + static_cast(0.66) * (sty - stx), stp); + } + } + + return 0; + + } + +}; + +} + +#endif /* MORETHUENTE_H_ */ diff --git a/thirdparty/cppoptlib/linesearch/wolfeheuristic.h b/thirdparty/cppoptlib/linesearch/wolfeheuristic.h new file mode 100644 index 0000000000..47e78be293 --- /dev/null +++ b/thirdparty/cppoptlib/linesearch/wolfeheuristic.h @@ -0,0 +1,75 @@ +// CppNumericalSolver +#ifndef WOLFERULE_H_ +#define WOLFERULE_H_ + +#include "../meta.h" + +namespace cppoptlib { + +/** + * @brief this tries to guess the correct stepwith in a bisection-style + * @details WARNING: THIS IS QUITE HACKY. TEST ARMIJO before. + * + * @tparam T scalar type + * @tparam P problem type + * @tparam Ord order of solver + */ +template +class WolfeHeuristic { + + public: + + static T linesearch(const Vector & x0, const Vector & searchDir, P &objFunc, const T alpha_init = 1) { + + Vector x = x0; + + // evaluate phi(0) + T phi0 = objFunc.value(x0); + // evaluate phi'(0) + Vector grad(x.rows()); + objFunc.gradient(x, grad); + + T phi0_dash = searchDir.dot(grad); + + T alpha = alpha_init; + bool decrease_direction = true; + + // 200 guesses + for(size_t iter = 0; iter < 200; ++iter) { + + // new guess for phi(alpha) + Vector x_candidate = x + alpha * searchDir; + const T phi = objFunc.value(x_candidate); + + // decrease condition invalid --> shrink interval + if (phi > phi0 + 0.0001 * alpha * phi0_dash) { + alpha *= 0.5; + decrease_direction = false; + } else { + + // valid decrease --> test strong wolfe condition + Vector grad2(x.rows()); + objFunc.gradient(x_candidate, grad2); + const T phi_dash = searchDir.dot(grad2); + + // curvature condition invalid ? + if ((phi_dash < 0.9 * phi0_dash) || !decrease_direction) { + // increase interval + alpha *= 4.0; + } else { + // both condition are valid --> we are happy + x = x_candidate; + grad = grad2; + phi0 = phi; + return alpha; + } + } + } + + return alpha; + } + +}; +} + +#endif /* WOLFERULE_H_ */ diff --git a/thirdparty/cppoptlib/meta.h b/thirdparty/cppoptlib/meta.h new file mode 100644 index 0000000000..1e52171f39 --- /dev/null +++ b/thirdparty/cppoptlib/meta.h @@ -0,0 +1,152 @@ +// CppNumericalSolver +#ifndef META_H +#define META_H + +#include +#include +#include + +namespace cppoptlib { + +/*template +using Vector = Eigen::Matrix; + +template +using Matrix = Eigen::Matrix; +*/ + +enum class DebugLevel { None = 0, Low, High }; +enum class Status { + NotStarted = -1, + Continue = 0, + IterationLimit, + XDeltaTolerance, + FDeltaTolerance, + GradNormTolerance, + Condition, + UserDefined +}; + +enum class SimplexOp { + Place, + Reflect, + Expand, + ContractIn, + ContractOut, + Shrink +}; + +inline std::ostream &operator<<(std::ostream &os, const SimplexOp &op) { + switch (op) { + case SimplexOp::Place: os << "place"; break; + case SimplexOp::Reflect: os << "reflect"; break; + case SimplexOp::Expand: os << "expand"; break; + case SimplexOp::ContractIn: os << "contract-in"; break; + case SimplexOp::ContractOut: os << "contract-out"; break; + case SimplexOp::Shrink: os << "shrink"; break; + } + return os; +} + +inline std::string op_to_string(SimplexOp op) { + switch (op) { + case SimplexOp::Place: + return "place"; + case SimplexOp::Expand: + return "expand"; + case SimplexOp::Reflect: + return "reflect"; + case SimplexOp::ContractIn: + return "contract-in"; + case SimplexOp::ContractOut: + return "contract-out"; + case SimplexOp::Shrink: + return "shrink"; + } + return "unknown"; +} + +template +class Criteria { +public: + size_t iterations; //!< Maximum number of iterations + T xDelta; //!< Minimum change in parameter vector + T fDelta; //!< Minimum change in cost function + T gradNorm; //!< Minimum norm of gradient vector + T condition; //!< Maximum condition number of Hessian + + Criteria() { + reset(); + } + + static Criteria defaults() { + Criteria d; + d.iterations = 10000; + d.xDelta = 0; + d.fDelta = 0; + d.gradNorm = 1e-4; + d.condition = 0; + return d; + } + + void reset() { + iterations = 0; + xDelta = 0; + fDelta = 0; + gradNorm = 0; + condition = 0; + } + + void print(std::ostream &os) const { + os << "Iterations: " << iterations << std::endl; + os << "xDelta: " << xDelta << std::endl; + os << "fDelta: " << fDelta << std::endl; + os << "GradNorm: " << gradNorm << std::endl; + os << "Condition: " << condition << std::endl; + } +}; + +template +Status checkConvergence(const Criteria &stop, const Criteria ¤t) { + + if ((stop.iterations > 0) && (current.iterations > stop.iterations)) { + return Status::IterationLimit; + } + if ((stop.xDelta > 0) && (current.xDelta < stop.xDelta)) { + return Status::XDeltaTolerance; + } + if ((stop.fDelta > 0) && (current.fDelta < stop.fDelta)) { + return Status::FDeltaTolerance; + } + if ((stop.gradNorm > 0) && (current.gradNorm < stop.gradNorm)) { + return Status::GradNormTolerance; + } + if ((stop.condition > 0) && (current.condition > stop.condition)) { + return Status::Condition; + } + return Status::Continue; +} + +inline std::ostream &operator<<(std::ostream &os, const Status &s) { + switch (s) { + case Status::NotStarted: os << "Solver not started."; break; + case Status::Continue: os << "Convergence criteria not reached."; break; + case Status::IterationLimit: os << "Iteration limit reached."; break; + case Status::XDeltaTolerance: os << "Change in parameter vector too small."; break; + case Status::FDeltaTolerance: os << "Change in cost function value too small."; break; + case Status::GradNormTolerance: os << "Gradient vector norm too small."; break; + case Status::Condition: os << "Condition of Hessian/Covariance matrix too large."; break; + case Status::UserDefined: os << "Stop condition defined in the callback."; break; + } + return os; +} + +template +std::ostream &operator<<(std::ostream &os, const Criteria &c) { + c.print(os); + return os; +} + +} // End namespace cppoptlib + +#endif /* META_H */ diff --git a/thirdparty/cppoptlib/problem.h b/thirdparty/cppoptlib/problem.h new file mode 100644 index 0000000000..e7fd4b5b5f --- /dev/null +++ b/thirdparty/cppoptlib/problem.h @@ -0,0 +1,217 @@ +#ifndef PROBLEM_H +#define PROBLEM_H + +#include +#include +#include + +#include "meta.h" + +namespace cppoptlib { + +template +class Problem { + public: + static const int Dim = Dim_; + typedef Scalar_ Scalar; + using TVector = Eigen::Matrix; + using THessian = Eigen::Matrix; + using TCriteria = Criteria; + using TIndex = typename TVector::Index; + using MatrixType = Eigen::Matrix; + + public: + Problem() {} + virtual ~Problem()= default; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" + virtual bool callback(const Criteria &state, const TVector &x) { + return true; + } + + virtual bool detailed_callback(const Criteria &state, SimplexOp op, int index, const MatrixType &x, std::vector f) { + return true; + } +#pragma GCC diagnostic pop + + /** + * @brief returns objective value in x + * @details [long description] + * + * @param x [description] + * @return [description] + */ + virtual Scalar value(const TVector &x) = 0; + /** + * @brief overload value for nice syntax + * @details [long description] + * + * @param x [description] + * @return [description] + */ + Scalar operator()(const TVector &x) { + return value(x); + } + /** + * @brief returns gradient in x as reference parameter + * @details should be overwritten by symbolic gradient + * + * @param grad [description] + */ + virtual void gradient(const TVector &x, TVector &grad) { + finiteGradient(x, grad); + } + + /** + * @brief This computes the hessian + * @details should be overwritten by symbolic hessian, if solver relies on hessian + */ + virtual void hessian(const TVector &x, THessian &hessian) { + finiteHessian(x, hessian); + } + + virtual bool checkGradient(const TVector &x, int accuracy = 3) { + // TODO: check if derived class exists: + // int(typeid(&Rosenbrock::gradient) == typeid(&Problem::gradient)) == 1 --> overwritten + const TIndex D = x.rows(); + TVector actual_grad(D); + TVector expected_grad(D); + gradient(x, actual_grad); + finiteGradient(x, expected_grad, accuracy); + for (TIndex d = 0; d < D; ++d) { + Scalar scale = std::max(static_cast(std::max(fabs(actual_grad[d]), fabs(expected_grad[d]))), Scalar(1.)); + if(fabs(actual_grad[d]-expected_grad[d])>1e-2 * scale) + return false; + } + return true; + + } + + virtual bool checkHessian(const TVector &x, int accuracy = 3) { + // TODO: check if derived class exists: + // int(typeid(&Rosenbrock::gradient) == typeid(&Problem::gradient)) == 1 --> overwritten + const TIndex D = x.rows(); + + THessian actual_hessian = THessian::Zero(D, D); + THessian expected_hessian = THessian::Zero(D, D); + hessian(x, actual_hessian); + finiteHessian(x, expected_hessian, accuracy); + for (TIndex d = 0; d < D; ++d) { + for (TIndex e = 0; e < D; ++e) { + Scalar scale = std::max(static_cast(std::max(fabs(actual_hessian(d, e)), fabs(expected_hessian(d, e)))), Scalar(1.)); + if(fabs(actual_hessian(d, e)- expected_hessian(d, e))>1e-1 * scale) + return false; + } + } + return true; + } + + void finiteGradient(const TVector &x, TVector &grad, int accuracy = 0) { + // accuracy can be 0, 1, 2, 3 + const Scalar eps = 2.2204e-6; + static const std::array, 4> coeff = + { { {1, -1}, {1, -8, 8, -1}, {-1, 9, -45, 45, -9, 1}, {3, -32, 168, -672, 672, -168, 32, -3} } }; + static const std::array, 4> coeff2 = + { { {1, -1}, {-2, -1, 1, 2}, {-3, -2, -1, 1, 2, 3}, {-4, -3, -2, -1, 1, 2, 3, 4} } }; + static const std::array dd = {2, 12, 60, 840}; + + grad.resize(x.rows()); + TVector& xx = const_cast(x); + + const int innerSteps = 2*(accuracy+1); + const Scalar ddVal = dd[accuracy]*eps; + + for (TIndex d = 0; d < x.rows(); d++) { + grad[d] = 0; + for (int s = 0; s < innerSteps; ++s) + { + Scalar tmp = xx[d]; + xx[d] += coeff2[accuracy][s]*eps; + grad[d] += coeff[accuracy][s]*value(xx); + xx[d] = tmp; + } + grad[d] /= ddVal; + } + } + + void finiteHessian(const TVector &x, THessian &hessian, int accuracy = 0) { + const Scalar eps = std::numeric_limits::epsilon()*10e7; + + hessian.resize(x.rows(), x.rows()); + TVector& xx = const_cast(x); + + if(accuracy == 0) { + for (TIndex i = 0; i < x.rows(); i++) { + for (TIndex j = 0; j < x.rows(); j++) { + Scalar tmpi = xx[i]; + Scalar tmpj = xx[j]; + + Scalar f4 = value(xx); + xx[i] += eps; + xx[j] += eps; + Scalar f1 = value(xx); + xx[j] -= eps; + Scalar f2 = value(xx); + xx[j] += eps; + xx[i] -= eps; + Scalar f3 = value(xx); + hessian(i, j) = (f1 - f2 - f3 + f4) / (eps * eps); + + xx[i] = tmpi; + xx[j] = tmpj; + } + } + } else { + /* + \displaystyle{{\frac{\partial^2{f}}{\partial{x}\partial{y}}}\approx + \frac{1}{600\,h^2} \left[\begin{matrix} + -63(f_{1,-2}+f_{2,-1}+f_{-2,1}+f_{-1,2})+\\ + 63(f_{-1,-2}+f_{-2,-1}+f_{1,2}+f_{2,1})+\\ + 44(f_{2,-2}+f_{-2,2}-f_{-2,-2}-f_{2,2})+\\ + 74(f_{-1,-1}+f_{1,1}-f_{1,-1}-f_{-1,1}) + \end{matrix}\right] } + */ + for (TIndex i = 0; i < x.rows(); i++) { + for (TIndex j = 0; j < x.rows(); j++) { + Scalar tmpi = xx[i]; + Scalar tmpj = xx[j]; + + Scalar term_1 = 0; + xx[i] = tmpi; xx[j] = tmpj; xx[i] += 1*eps; xx[j] += -2*eps; term_1 += value(xx); + xx[i] = tmpi; xx[j] = tmpj; xx[i] += 2*eps; xx[j] += -1*eps; term_1 += value(xx); + xx[i] = tmpi; xx[j] = tmpj; xx[i] += -2*eps; xx[j] += 1*eps; term_1 += value(xx); + xx[i] = tmpi; xx[j] = tmpj; xx[i] += -1*eps; xx[j] += 2*eps; term_1 += value(xx); + + Scalar term_2 = 0; + xx[i] = tmpi; xx[j] = tmpj; xx[i] += -1*eps; xx[j] += -2*eps; term_2 += value(xx); + xx[i] = tmpi; xx[j] = tmpj; xx[i] += -2*eps; xx[j] += -1*eps; term_2 += value(xx); + xx[i] = tmpi; xx[j] = tmpj; xx[i] += 1*eps; xx[j] += 2*eps; term_2 += value(xx); + xx[i] = tmpi; xx[j] = tmpj; xx[i] += 2*eps; xx[j] += 1*eps; term_2 += value(xx); + + Scalar term_3 = 0; + xx[i] = tmpi; xx[j] = tmpj; xx[i] += 2*eps; xx[j] += -2*eps; term_3 += value(xx); + xx[i] = tmpi; xx[j] = tmpj; xx[i] += -2*eps; xx[j] += 2*eps; term_3 += value(xx); + xx[i] = tmpi; xx[j] = tmpj; xx[i] += -2*eps; xx[j] += -2*eps; term_3 -= value(xx); + xx[i] = tmpi; xx[j] = tmpj; xx[i] += 2*eps; xx[j] += 2*eps; term_3 -= value(xx); + + Scalar term_4 = 0; + xx[i] = tmpi; xx[j] = tmpj; xx[i] += -1*eps; xx[j] += -1*eps; term_4 += value(xx); + xx[i] = tmpi; xx[j] = tmpj; xx[i] += 1*eps; xx[j] += 1*eps; term_4 += value(xx); + xx[i] = tmpi; xx[j] = tmpj; xx[i] += 1*eps; xx[j] += -1*eps; term_4 -= value(xx); + xx[i] = tmpi; xx[j] = tmpj; xx[i] += -1*eps; xx[j] += 1*eps; term_4 -= value(xx); + + xx[i] = tmpi; + xx[j] = tmpj; + + hessian(i, j) = (-63 * term_1+63 * term_2+44 * term_3+74 * term_4)/(600.0 * eps * eps); + } + } + } + + } + +}; +} + +#endif /* PROBLEM_H */ diff --git a/thirdparty/cppoptlib/solver/bfgssolver.h b/thirdparty/cppoptlib/solver/bfgssolver.h new file mode 100644 index 0000000000..60cb052d90 --- /dev/null +++ b/thirdparty/cppoptlib/solver/bfgssolver.h @@ -0,0 +1,67 @@ +// CppNumericalSolver +#include +#include +#include "isolver.h" +#include "../linesearch/morethuente.h" + +#ifndef BFGSSOLVER_H_ +#define BFGSSOLVER_H_ + +namespace cppoptlib { + +template +class BfgsSolver : public ISolver { + public: + using Superclass = ISolver; + using typename Superclass::Scalar; + using typename Superclass::TVector; + using typename Superclass::THessian; + + void minimize(ProblemType &objFunc, TVector & x0) { + const size_t DIM = x0.rows(); + THessian H = THessian::Identity(DIM, DIM); + TVector grad(DIM); + TVector x_old = x0; + this->m_current.reset(); + objFunc.gradient(x0, grad); + do { + TVector searchDir = -1 * H * grad; + // check "positive definite" + Scalar phi = grad.dot(searchDir); + + // positive definit ? + if ((phi > 0) || (phi != phi)) { + // no, we reset the hessian approximation + H = THessian::Identity(DIM, DIM); + searchDir = -1 * grad; + } + + const Scalar rate = MoreThuente::linesearch(x0, searchDir, objFunc) ; + x0 = x0 + rate * searchDir; + + TVector grad_old = grad; + objFunc.gradient(x0, grad); + TVector s = rate * searchDir; + TVector y = grad - grad_old; + + const Scalar rho = 1.0 / y.dot(s); + H = H - rho * (s * (y.transpose() * H) + (H * y) * s.transpose()) + rho * (rho * y.dot(H * y) + 1.0) + * (s * s.transpose()); + // std::cout << "iter: "<() < 1e-7 ) + break; + x_old = x0; + ++this->m_current.iterations; + this->m_current.gradNorm = grad.template lpNorm(); + this->m_status = checkConvergence(this->m_stop, this->m_current); + } while (objFunc.callback(this->m_current, x0) && (this->m_status == Status::Continue)); + + } + +}; + +} +/* namespace cppoptlib */ + +#endif /* BFGSSOLVER_H_ */ diff --git a/thirdparty/cppoptlib/solver/cmaesbsolver.h b/thirdparty/cppoptlib/solver/cmaesbsolver.h new file mode 100644 index 0000000000..7cf334477e --- /dev/null +++ b/thirdparty/cppoptlib/solver/cmaesbsolver.h @@ -0,0 +1,231 @@ +// CppNumericalSolver +#ifndef CMAESB_H +#define CMAESB_H + +#include +#include +#include "isolver.h" + +namespace cppoptlib { + +/** + * @brief Covariance Matrix Adaptation + */ +template +class CMAesBSolver : public ISolver { +public: + using Super = ISolver; + using typename Super::Scalar; + using typename Super::TVector; + using typename Super::THessian; + using typename Super::TCriteria; + using TMatrix = Eigen::Matrix; + using TVarVector = Eigen::Matrix; + +protected: + std::mt19937 gen; + Scalar m_stepSize; + + /* + * @brief Create a vector sampled from a normal distribution + * + */ + TVector normDist(const int n) { + TVector sample = TVector::Zero(n); + std::normal_distribution d(0, 1); + for (int i = 0; i < n; ++i) { + sample[i] = d(gen); + } + return sample; + } + + std::vector index_partial_sort(const TVarVector &x, Eigen::ArrayXd::Index N) + { + eigen_assert(x.size() >= N); + std::vector allIndices(x.size()), indices(N); + for(size_t i = 0; i < allIndices.size(); i++) { + allIndices[i] = i; + } + partial_sort(allIndices.begin(), allIndices.begin() + N, allIndices.end(), [&x](size_t i1, size_t i2) { return x[i1] < x[i2]; }); + //std::cout << "SORTED: "; + for (Eigen::ArrayXd::Index i = 0; i < N; i++) { + indices[i] = allIndices[i]; + //std::cout << indices[i] << ","; + } + //std::cout << std::endl; + return indices; + } + +public: + CMAesBSolver() : gen((std::random_device())()) { + m_stepSize = 0.5; + + // Set some sensible defaults for the stop criteria + Super::m_stop.iterations = 1e5; + Super::m_stop.gradNorm = 0; // Switch this off + Super::m_stop.condition = 1e14; + Super::m_stop.xDelta = 1e-7; + Super::m_stop.fDelta = 1e-9; + } + + /** + * @brief minimize + * @details [long description] + * + * @param objFunc [description] + */ + void minimize(TProblem &objFunc, TVector &x0) { + const int n = x0.rows(); + int la = ceil(4 + round(3 * log(n))); + const int mu = floor(la / 2); + TVarVector w = TVarVector::Zero(mu); + for (int i = 0; i < mu; ++i) { + w[i] = log(mu+1/2)-log(i+1); + } + w /= w.sum(); + const Scalar mu_eff = (w.sum()*w.sum()) / w.dot(w); + la = std::max(16, la); // Increase to 16 samples for very small populations, but AFTER calcaulting mu_eff + + const Scalar cc = (4. + mu_eff / n) / (n + 4. + 2.*mu_eff/n); + const Scalar cs = (mu_eff + 2.) / (n + mu_eff + 5.); + const Scalar c1 = 2. / (pow(n + 1.3, 2.) + mu_eff); + const Scalar cmu = std::min(1. - c1, 2.*(mu_eff - 2. + 1./mu_eff) / (pow(n+2, 2.) + mu_eff)); + const Scalar ds = 1. + cs + 2.*std::max(0., sqrt((mu_eff - 1.) / (n + 1.)) - 1.); + const Scalar chi = sqrt(n) * (1. - 1./(4.*n) + 1./(21.*n*n)); + const Scalar hsig_thr = (1.4 + 2 / (n + 1.)) * chi; + + TVector pc = TVector::Zero(n); + TVector ps = TVector::Zero(n); + THessian B = THessian::Identity(n, n); + THessian D = THessian::Identity(n, n); + THessian C = THessian::Zero(n; n); + C.diagonal() = (objFunc.upperBound() - objFunc.lowerBound()) / 2; + Eigen::SelfAdjointEigenSolver eigenSolver(C); + B = eigenSolver.eigenvectors(); + D.diagonal() = eigenSolver.eigenvalues().array().sqrt(); + Scalar sigma = m_stepSize; + + TVector xmean = x0; + TVector zmean = TVector::Zero(n); + TMatrix arz(n, la); + TMatrix arx(n, la); + TVarVector costs(la); + Scalar prevCost = objFunc.value(x0); + // Constraint handling + TVector gamma = TVector::Ones(n); + + // CMA-ES Main Loop + int eigen_last_eval = 0; + int eigen_next_eval = std::max(1, 1/(10*n*(c1+cmu))); + this->m_current.reset(); + if (Super::m_debug >= DebugLevel::Low) { + std::cout << "CMA-ES Initial Config" << std::endl; + std::cout << "n " << n << " la " << la << " mu " << mu << " mu_eff " << mu_eff << " sigma " << sigma << std::endl; + std::cout << "cs " << cs << " ds " << ds << " chi " << chi << " cc " << cc + << " c1 " << c1 << " cmu " << cmu << " hsig_thr " << hsig_thr << std::endl; + std::cout << "C" << std::endl << C << std::endl; + std::cout << "Hessian will be updated every " << eigen_next_eval << " iterations." << std::endl; + std::cout << "Iteration: " << this->m_current.iterations << " best cost " << prevCost << " sigma " << sigma + << " cond " << this->m_current.condition << " xmean " << x0.transpose() << std::endl; + } + do { + for (int k = 0; k < la; ++k) { + arz.col(k) = normDist(n); + TVector xk = xmean + sigma * B*D*arz.col(k); + arx.col(k) = xk; + Scalar penalty = 0; + const Scalar eta_sum = C.diagonal().array().log().sum()/n; + for (int d = 0; d < n; d++) { + Scalar dist = 0; + const Scalar eta = exp(0.9 * (log(C.coeffRef(d, d) - eta_sum))); + if (xk.coeffRef(d) < objFunc.lowerBound().coeffRef(d)) { + dist = xk.coeffRef(d) - objFunc.lowerBound().coeffRef(d); + xk.coeffRef(d) = objFunc.lowerBound().coeffRef(d); + } else if (xk.coeffRef(d) > objFunc.upperBound().coeffRef(d)) { + dist = objFunc.upperBound().coeffRef(d) - xk.coeffRef(d); + xk.coeffRef(d) = objFunc.upperBound().coeffRef(d); + } + penalty += (dist*dist) / eta; + } + costs[k] = objFunc(xk) + penalty/n; + } + + if (Super::m_debug >= DebugLevel::High) { + std::cout << "arz" << std::endl << arz << std::endl; + std::cout << "arx" << std::endl << arx << std::endl; + std::cout << "costs " << costs.transpose() << std::endl; + } + + std::vector indices = index_partial_sort(costs, mu); + xmean = TVector::Zero(n); + zmean = TVector::Zero(n); + for (int k = 0; k < mu; k++) { + zmean += w[k]*arz.col(indices[k]); + xmean += w[k]*arx.col(indices[k]); + } + // Update constraint weights + for (int d = 0; d < n; d++) { + if (xmean.coeffRef(d) < objFunc.lowerBound().coeffRef(d) || xmean.coeffRef(d) > objFunc.upperBound().coeffRef(d)) { + gamma.coeffRef(d) *= pow(1.1, std::max(1, mu_eff / (10*n))); + } + } + // Update evolution paths + ps = (1. - cs)*ps + sqrt(cs*(2. - cs)*mu_eff) * B*zmean; + Scalar hsig = (ps.norm()/sqrt(pow(1 - (1. - cs), (2 * (this->m_current.iterations + 1)))) < hsig_thr) ? 1.0 : 0.0; + pc = (1. - cc)*pc + hsig*sqrt(cc*(2. - cc)*mu_eff)*(B*D*zmean); + // Adapt covariance matrix + C = (1 - c1 - cmu)*C + + c1*(pc*pc.transpose() + (1 - hsig)*cc*(2 - cc)*C); + for (int k = 0; k < mu; ++k) { + TVector temp = B*D*arz.col(k); + C += cmu*w(k)*temp*temp.transpose(); + } + sigma = sigma * exp((cs/ds) * ((ps).norm()/chi - 1.)); + + ++Super::m_current.iterations; + if ((Super::m_current.iterations - eigen_last_eval) == eigen_next_eval) { + // Update B and D + eigen_last_eval = Super::m_current.iterations; + Eigen::SelfAdjointEigenSolver eigenSolver(C); + B = eigenSolver.eigenvectors(); + D.diagonal() = eigenSolver.eigenvalues().array().sqrt(); + if (Super::m_debug >= DebugLevel::High) { + std::cout << "Updated hessian." << std::endl; + std::cout << "C" << std::endl << C << std::endl; + std::cout << "B" << std::endl << B << std::endl; + std::cout << "D" << std::endl << D << std::endl; + } + } + Super::m_current.condition = D.diagonal().maxCoeff() / D.diagonal().minCoeff(); + Super::m_current.xDelta = (xmean - x0).norm(); + x0 = xmean; + Super::m_current.fDelta = fabs(costs[indices[0]] - prevCost); + prevCost = costs[indices[0]]; + if (Super::m_debug >= DebugLevel::Low) { + std::cout << "Iteration: " << this->m_current.iterations << " best cost " << costs[indices[0]] + << " sigma " << sigma << " cond " << this->m_current.condition << " xmean " << xmean.transpose() << std::endl; + } + if (fabs(costs[indices[0]] - costs[indices[mu-1]]) < 1e-6) { + sigma = sigma * exp(0.2+cs/ds); + if (Super::m_debug >= DebugLevel::Low) { + std::cout << "Flat fitness " << costs[indices[0]] << " " << costs[indices[mu-1]] << std::endl; + } + } + Super::m_status = checkConvergence(this->m_stop, this->m_current); + } while (objFunc.callback(this->m_current, x0) && (this->m_status == Status::Continue)); + // Return the best evaluated solution + x0 = xmean; + m_stepSize = sigma; + if (Super::m_debug >= DebugLevel::Low) { + std::cout << "Stop" << std::endl; + this->m_stop.print(std::cout); + std::cout << "Current" << std::endl; + this->m_current.print(std::cout); + std::cout << "Reason: " << Super::m_status << std::endl; + } + } +}; + +} /* namespace cppoptlib */ + +#endif /* CMAES_H_ */ diff --git a/thirdparty/cppoptlib/solver/cmaessolver.h b/thirdparty/cppoptlib/solver/cmaessolver.h new file mode 100644 index 0000000000..083eb48c08 --- /dev/null +++ b/thirdparty/cppoptlib/solver/cmaessolver.h @@ -0,0 +1,205 @@ +// CppNumericalSolver +#ifndef CMAES_H_ +#define CMAES_H_ + +#include +#include +#include "isolver.h" + +namespace cppoptlib { + +/** + * @brief Covariance Matrix Adaptation + */ +template +class CMAesSolver : public ISolver { +public: + using Super = ISolver; + using typename Super::Scalar; + using typename Super::TVector; + using typename Super::THessian; + using typename Super::TCriteria; + using TMatrix = Eigen::Matrix; + using TVarVector = Eigen::Matrix; + +protected: + std::mt19937 gen; + Scalar m_stepSize; + /* + * @brief Create a vector sampled from a normal distribution + * + */ + TVector normDist(const int n) { + TVector sample = TVector::Zero(n); + std::normal_distribution d(0, 1); + for (int i = 0; i < n; ++i) { + sample[i] = d(gen); + } + return sample; + } + + std::vector index_partial_sort(const TVarVector &x, Eigen::ArrayXd::Index N) + { + eigen_assert(x.size() >= N); + std::vector allIndices(x.size()), indices(N); + for(size_t i = 0; i < allIndices.size(); i++) { + allIndices[i] = i; + } + partial_sort(allIndices.begin(), allIndices.begin() + N, allIndices.end(), [&x](size_t i1, size_t i2) { return x[i1] < x[i2]; }); + //std::cout << "SORTED: "; + for (Eigen::ArrayXd::Index i = 0; i < N; i++) { + indices[i] = allIndices[i]; + //std::cout << indices[i] << ","; + } + //std::cout << std::endl; + return indices; + } + +public: + CMAesSolver() : gen((std::random_device())()) { + m_stepSize = 0.5; + // Set some sensible defaults for the stop criteria + Super::m_stop.iterations = 1e5; + Super::m_stop.gradNorm = 0; // Switch this off + Super::m_stop.condition = 1e14; + Super::m_stop.xDelta = 1e-7; + Super::m_stop.fDelta = 1e-9; + } + + void minimize(TProblem &objFunc, TVector &x0) { + TVector var0 = TVector::Ones(x0.rows()); + this->minimize(objFunc, x0, var0); + } + + /** + * @brief minimize + * @details [long description] + * + * @param objFunc [description] + */ + void minimize(TProblem &objFunc, TVector &x0, const TVector &var0) { + const int n = x0.rows(); + eigen_assert(x0.rows() == var0.rows()); + int la = ceil(4 + round(3 * log(n))); + const int mu = floor(la / 2); + TVarVector w = TVarVector::Zero(mu); + for (int i = 0; i < mu; ++i) { + w[i] = log(mu+1/2)-log(i+1); + } + w /= w.sum(); + const Scalar mu_eff = (w.sum()*w.sum()) / w.dot(w); + la = std::max(16, la); // Increase to 16 samples for very small populations, but AFTER calcaulting mu_eff + const Scalar cc = (4. + mu_eff / n) / (n + 4. + 2.*mu_eff/n); + const Scalar cs = (mu_eff + 2.) / (n + mu_eff + 5.); + const Scalar c1 = 2. / (pow(n + 1.3, 2.) + mu_eff); + const Scalar cmu = std::min(1. - c1, 2.*(mu_eff - 2. + 1./mu_eff) / (pow(n+2, 2.) + mu_eff)); + const Scalar ds = 1. + cs + 2.*std::max(0., sqrt((mu_eff - 1.) / (n + 1.)) - 1.); + const Scalar chi = sqrt(n) * (1. - 1./(4.*n) + 1./(21.*n*n)); + const Scalar hsig_thr = (1.4 + 2 / (n + 1.)) * chi; + + TVector pc = TVector::Zero(n); + TVector ps = TVector::Zero(n); + THessian B = THessian::Identity(n, n); + THessian D = THessian::Identity(n, n); + THessian C = B*D*(B*D).transpose(); + + Scalar sigma = m_stepSize; + + TVector xmean = x0; + TVector zmean = TVector::Zero(n); + TMatrix arz(n, la); + TMatrix arx(n, la); + TVarVector costs(la); + Scalar prevCost = objFunc.value(x0); + // CMA-ES Main Loop + size_t eigen_last_eval = 0; + size_t eigen_next_eval = std::max(1, 1/(10*n*(c1+cmu))); + this->m_current.reset(); + if (Super::m_debug >= DebugLevel::Low) { + std::cout << "CMA-ES Initial Config" << std::endl; + std::cout << "n " << n << " la " << la << " mu " << mu << " mu_eff " << mu_eff << " sigma " << sigma << std::endl; + std::cout << "cs " << cs << " ds " << ds << " chi " << chi << " cc " << cc << " c1 " << c1 << " cmu " << cmu + << " hsig_thr " << hsig_thr << std::endl; + std::cout << "C" << std::endl << C << std::endl; + std::cout << "Hessian will be updated every " << eigen_next_eval << " iterations." << std::endl; + std::cout << "Iteration: " << this->m_current.iterations << " best cost " << prevCost << " sigma " << sigma + << " cond " << this->m_current.condition << " xmean " << x0.transpose() << std::endl; + } + do { + for (int k = 0; k < la; ++k) { + arz.col(k) = normDist(n); + arx.col(k) = xmean + sigma * B*D*arz.col(k); + costs[k] = objFunc(arx.col(k)); + } + if (Super::m_debug >= DebugLevel::High) { + std::cout << "arz" << std::endl << arz << std::endl; + std::cout << "arx" << std::endl << arx << std::endl; + std::cout << "costs " << costs.transpose() << std::endl; + } + std::vector indices = index_partial_sort(costs, mu); + xmean = TVector::Zero(n); + zmean = TVector::Zero(n); + for (int k = 0; k < mu; k++) { + zmean += w[k]*arz.col(indices[k]); + xmean += w[k]*arx.col(indices[k]); + } + // Update evolution paths + ps = (1. - cs)*ps + sqrt(cs*(2. - cs)*mu_eff) * B*zmean; + Scalar hsig = (ps.norm()/sqrt(pow(1 - (1. - cs), (2 * (this->m_current.iterations + 1)))) < hsig_thr) ? 1.0 : 0.0; + pc = (1. - cc)*pc + hsig*sqrt(cc*(2. - cc)*mu_eff)*(B*D*zmean); + // Adapt covariance matrix + C = (1 - c1 - cmu)*C + + c1*(pc*pc.transpose() + (1 - hsig)*cc*(2 - cc)*C); + for (int k = 0; k < mu; ++k) { + TVector temp = B*D*arz.col(k); + C += cmu*w(k)*temp*temp.transpose(); + } + sigma = sigma * exp((cs/ds) * ((ps).norm()/chi - 1.)); + + ++Super::m_current.iterations; + if ((Super::m_current.iterations - eigen_last_eval) == eigen_next_eval) { + // Update B and D + eigen_last_eval = Super::m_current.iterations; + Eigen::SelfAdjointEigenSolver eigenSolver(C); + B = eigenSolver.eigenvectors(); + D.diagonal() = eigenSolver.eigenvalues().array().sqrt(); + if (Super::m_debug >= DebugLevel::High) { + std::cout << "Updated hessian." << std::endl; + std::cout << "C" << std::endl << C << std::endl; + std::cout << "B" << std::endl << B << std::endl; + std::cout << "D" << std::endl << D << std::endl; + } + } + Super::m_current.condition = D.diagonal().maxCoeff() / D.diagonal().minCoeff(); + Super::m_current.xDelta = (xmean - x0).norm(); + x0 = xmean; + Super::m_current.fDelta = fabs(costs[indices[0]] - prevCost); + prevCost = costs[indices[0]]; + if (Super::m_debug >= DebugLevel::Low) { + std::cout << "Iteration: " << this->m_current.iterations << " best cost " << costs[indices[0]] << " sigma " << sigma + << " cond " << this->m_current.condition << " xmean " << xmean.transpose() << std::endl; + } + if (fabs(costs[indices[0]] - costs[indices[mu-1]]) < 1e-6) { + sigma = sigma * exp(0.2+cs/ds); + if (Super::m_debug >= DebugLevel::Low) { + std::cout << "Flat fitness " << costs[indices[0]] << " " << costs[indices[mu-1]] << std::endl; + } + } + Super::m_status = checkConvergence(this->m_stop, this->m_current); + } while (objFunc.callback(this->m_current, x0) && (this->m_status == Status::Continue)); + // Return the best evaluated solution + x0 = xmean; + m_stepSize = sigma; + if (Super::m_debug >= DebugLevel::Low) { + std::cout << "Stop" << std::endl; + this->m_stop.print(std::cout); + std::cout << "Current" << std::endl; + this->m_current.print(std::cout); + std::cout << "Reason: " << Super::m_status << std::endl; + } + } +}; + +} /* namespace cppoptlib */ + +#endif /* CMAES_H_ */ diff --git a/thirdparty/cppoptlib/solver/conjugatedgradientdescentsolver.h b/thirdparty/cppoptlib/solver/conjugatedgradientdescentsolver.h new file mode 100644 index 0000000000..1d6a1e0dec --- /dev/null +++ b/thirdparty/cppoptlib/solver/conjugatedgradientdescentsolver.h @@ -0,0 +1,61 @@ +// CppNumericalSolver +#ifndef CONJUGATEDGRADIENTDESCENTSOLVER_H_ +#define CONJUGATEDGRADIENTDESCENTSOLVER_H_ + +#include +#include "isolver.h" +#include "../linesearch/armijo.h" + +namespace cppoptlib { + +template +class ConjugatedGradientDescentSolver : public ISolver { + + public: + using Superclass = ISolver; + using typename Superclass::Scalar; + using typename Superclass::TVector; + + /** + * @brief minimize + * @details [long description] + * + * @param objFunc [description] + */ + void minimize(ProblemType &objFunc, TVector &x0) { + TVector grad(x0.rows()); + TVector grad_old(x0.rows()); + TVector Si(x0.rows()); + TVector Si_old(x0.rows()); + + this->m_current.reset(); + do { + objFunc.gradient(x0, grad); + + if (this->m_current.iterations == 0) { + Si = -grad; + } else { + const double beta = grad.dot(grad) / (grad_old.dot(grad_old)); + Si = -grad + beta * Si_old; + } + + const double rate = Armijo::linesearch(x0, Si, objFunc) ; + + x0 = x0 + rate * Si; + + grad_old = grad; + Si_old = Si; + + this->m_current.gradNorm = grad.template lpNorm(); + // std::cout << "iter: "<m_current.iterations; + this->m_status = checkConvergence(this->m_stop, this->m_current); + } while (objFunc.callback(this->m_current, x0) && (this->m_status == Status::Continue) ); + + } + +}; + +} /* namespace cppoptlib */ + +#endif /* CONJUGATEDGRADIENTDESCENTSOLVER_H_ */ diff --git a/thirdparty/cppoptlib/solver/gradientdescentsolver.h b/thirdparty/cppoptlib/solver/gradientdescentsolver.h new file mode 100644 index 0000000000..0f3d2c4054 --- /dev/null +++ b/thirdparty/cppoptlib/solver/gradientdescentsolver.h @@ -0,0 +1,50 @@ +// CppNumericalSolver +#ifndef GRADIENTDESCENTSOLVER_H_ +#define GRADIENTDESCENTSOLVER_H_ + +#include +#include "isolver.h" +#include "../linesearch/morethuente.h" + +namespace cppoptlib { + +template +class GradientDescentSolver : public ISolver { + +public: + using Superclass = ISolver; + using typename Superclass::Scalar; + using typename Superclass::TVector; + + /** + * @brief minimize + * @details [long description] + * + * @param objFunc [description] + */ + void minimize(ProblemType &objFunc, TVector &x0) { + + TVector direction(x0.rows()); + this->m_current.reset(); + do { + ; + objFunc.gradient(x0, direction); + const Scalar rate = MoreThuente::linesearch(x0, -direction, objFunc) ; + x0 = x0 - rate * direction; + this->m_current.gradNorm = direction.template lpNorm(); + // std::cout << "iter: "<m_current.iterations; + this->m_status = checkConvergence(this->m_stop, this->m_current); + } while (objFunc.callback(this->m_current, x0) && (this->m_status == Status::Continue)); + if (this->m_debug > DebugLevel::None) { + std::cout << "Stop status was: " << this->m_status << std::endl; + std::cout << "Stop criteria were: " << std::endl << this->m_stop << std::endl; + std::cout << "Current values are: " << std::endl << this->m_current << std::endl; + } + } + +}; + +} /* namespace cppoptlib */ + +#endif /* GRADIENTDESCENTSOLVER_H_ */ diff --git a/thirdparty/cppoptlib/solver/isolver.h b/thirdparty/cppoptlib/solver/isolver.h new file mode 100644 index 0000000000..bafaa2bc5c --- /dev/null +++ b/thirdparty/cppoptlib/solver/isolver.h @@ -0,0 +1,57 @@ +// CppNumericalSolver +#ifndef ISOLVER_H_ +#define ISOLVER_H_ + +#include +#include "isolver.h" +#include "../meta.h" +#include "../problem.h" + +namespace cppoptlib { + +template +class ISolver { +public: + using Scalar = typename ProblemType::Scalar; + using TVector = typename ProblemType::TVector; + using THessian = typename ProblemType::THessian; + using TCriteria = typename ProblemType::TCriteria; +protected: + const int order_ = Ord; + TCriteria m_stop, m_current; + Status m_status = Status::NotStarted; + DebugLevel m_debug = DebugLevel::None; + +public: + virtual ~ISolver() = default; + ISolver() { + m_stop = TCriteria::defaults(); + m_current.reset(); + } + + ISolver(const TCriteria &s) { + m_stop = s; + m_current.reset(); + } + + void setStopCriteria(const TCriteria &s) { m_stop = s; } + const TCriteria &criteria() { return m_current; } + const Status &status() { return m_status; } + void setDebug(const DebugLevel &d) { m_debug = d; } + + /** + * @brief minimize an objective function given a gradient (and optinal a hessian) + * @details this is just the abstract interface + * + * @param x0 starting point + * @param funObjective objective function + * @param funGradient gradient function + * @param funcHession hessian function + */ + virtual void minimize(ProblemType &objFunc, TVector &x0) = 0; + +}; + +} /* namespace cppoptlib */ + +#endif /* ISOLVER_H_ */ diff --git a/thirdparty/cppoptlib/solver/lbfgsbsolver.h b/thirdparty/cppoptlib/solver/lbfgsbsolver.h new file mode 100644 index 0000000000..230ec61a00 --- /dev/null +++ b/thirdparty/cppoptlib/solver/lbfgsbsolver.h @@ -0,0 +1,306 @@ +// CppNumericalSolver +// based on: +// L-BFGS-B: A LIMITED MEMORY ALGORITHM FOR BOUND CONSTRAINED OPTIMIZATION +// Richard H. Byrd, Peihuang Lu, Jorge Nocedal and Ciyou Zhu +#include +#include +#include +#include "isolver.h" +#include "../boundedproblem.h" +#include "../linesearch/morethuente.h" +#ifndef LBFGSBSOLVER_H +#define LBFGSBSOLVER_H +namespace cppoptlib { +template +class LbfgsbSolver : public ISolver { + public: + using Superclass = ISolver; + using typename Superclass::Scalar; + using typename Superclass::TVector; + using MatrixType = Eigen::Matrix; + using VariableTVector = Eigen::Matrix; + protected: + // workspace matrices + MatrixType W, M; + Scalar theta; + int DIM; + int m_historySize = 5; + + /** + * @brief sort pairs (k,v) according v ascending + * @details [long description] + * + * @param v [description] + * @return [description] + */ + std::vector sort_indexes(const std::vector< std::pair > &v) { + std::vector idx(v.size()); + for (size_t i = 0; i != idx.size(); ++i) + idx[i] = v[i].first; + sort(idx.begin(), idx.end(), [&v](size_t i1, size_t i2) { + return v[i1].second < v[i2].second; + }); + return idx; + } + + void clampToBound(const TProblem &problem, TVector &x) { + for (int r = 0; r < x.rows(); ++r) + { + if(x(r) < problem.lowerBound()(r)) + x(r) = problem.lowerBound()(r); + else if (x(r) > problem.upperBound()(r)) + x(r) = problem.upperBound()(r); + } + } + + /** + * @brief Algorithm CP: Computation of the generalized Cauchy point + * @details PAGE 8 + * + * @param c [description] + */ + void getGeneralizedCauchyPoint(const TProblem &problem, const TVector &x, const TVector &g, TVector &x_cauchy, VariableTVector &c) { + const int DIM = x.rows(); + // Given x,l,u,g, and B = \theta I-WMW + // {all t_i} = { (idx,value), ... } + // TODO: use "std::set" ? + std::vector > SetOfT; + // the feasible set is implicitly given by "SetOfT - {t_i==0}" + TVector d = -g; + // n operations + for (int j = 0; j < DIM; j++) { + if (g(j) == 0) { + SetOfT.push_back(std::make_pair(j, std::numeric_limits::max())); + } else { + Scalar tmp = 0; + if (g(j) < 0) { + tmp = (x(j) - problem.upperBound()(j)) / g(j); + } else { + tmp = (x(j) - problem.lowerBound()(j)) / g(j); + } + SetOfT.push_back(std::make_pair(j, tmp)); + if (tmp == 0) d(j) = 0; + } + } + // sortedindices [1,0,2] means the minimal element is on the 1-st entry + std::vector sortedIndices = sort_indexes(SetOfT); + x_cauchy = x; + // Initialize + // p := W^Scalar*p + VariableTVector p = (W.transpose() * d); // (2mn operations) + // c := 0 + c = VariableTVector::Zero(W.cols()); + // f' := g^Scalar*d = -d^Td + Scalar f_prime = -d.dot(d); // (n operations) + // f'' := \theta*d^Scalar*d-d^Scalar*W*M*W^Scalar*d = -\theta*f' - p^Scalar*M*p + Scalar f_doubleprime = (Scalar)(-1.0 * theta) * f_prime - p.dot(M * p); // (O(m^2) operations) + f_doubleprime = std::max(std::numeric_limits::epsilon(), f_doubleprime); + Scalar f_dp_orig = f_doubleprime; + // \delta t_min := -f'/f'' + Scalar dt_min = -f_prime / f_doubleprime; + // t_old := 0 + Scalar t_old = 0; + // b := argmin {t_i , t_i >0} + int i = 0; + for (int j = 0; j < DIM; j++) { + i = j; + if (SetOfT[sortedIndices[j]].second > 0) + break; + } + int b = sortedIndices[i]; + // see below + // t := min{t_i : i in F} + Scalar t = SetOfT[b].second; + // \delta Scalar := t - 0 + Scalar dt = t ; + // examination of subsequent segments + while ((dt_min >= dt) && (i < DIM)) { + if (d(b) > 0) + x_cauchy(b) = problem.upperBound()(b); + else if (d(b) < 0) + x_cauchy(b) = problem.lowerBound()(b); + // z_b = x_p^{cp} - x_b + Scalar zb = x_cauchy(b) - x(b); + // c := c +\delta t*p + c += dt * p; + // cache + VariableTVector wbt = W.row(b); + f_prime += dt * f_doubleprime + (Scalar) g(b) * g(b) + (Scalar) theta * g(b) * zb - (Scalar) g(b) * + wbt.transpose() * (M * c); + f_doubleprime += (Scalar) - 1.0 * theta * g(b) * g(b) + - (Scalar) 2.0 * (g(b) * (wbt.dot(M * p))) + - (Scalar) g(b) * g(b) * wbt.transpose() * (M * wbt); + f_doubleprime = std::max(std::numeric_limits::epsilon() * f_dp_orig, f_doubleprime); + p += g(b) * wbt.transpose(); + d(b) = 0; + dt_min = -f_prime / f_doubleprime; + t_old = t; + ++i; + if (i < DIM) { + b = sortedIndices[i]; + t = SetOfT[b].second; + dt = t - t_old; + } + } + dt_min = std::max(dt_min, (Scalar)0.0); + t_old += dt_min; + #pragma omp parallel for + for (int ii = i; ii < x_cauchy.rows(); ii++) { + x_cauchy(sortedIndices[ii]) = x(sortedIndices[ii]) + t_old * d(sortedIndices[ii]); + } + c += dt_min * p; + } + /** + * @brief find alpha* = max {a : a <= 1 and l_i-xc_i <= a*d_i <= u_i-xc_i} + * @details [long description] + * + * @param FreeVariables [description] + * @return [description] + */ + Scalar findAlpha(const TProblem &problem, TVector &x_cp, VariableTVector &du, std::vector &FreeVariables) { + Scalar alphastar = 1; + const unsigned int n = FreeVariables.size(); + assert(du.rows() == n); + for (unsigned int i = 0; i < n; i++) { + if (du(i) > 0) { + alphastar = std::min(alphastar, (problem.upperBound()(FreeVariables[i]) - x_cp(FreeVariables[i])) / du(i)); + } else { + alphastar = std::min(alphastar, (problem.lowerBound()(FreeVariables[i]) - x_cp(FreeVariables[i])) / du(i)); + } + } + return alphastar; + } + /** + * @brief solving unbounded probelm + * @details [long description] + * + * @param SubspaceMin [description] + */ + void SubspaceMinimization(const TProblem &problem, TVector &x_cauchy, TVector &x, VariableTVector &c, TVector &g, + TVector &SubspaceMin) { + Scalar theta_inverse = 1 / theta; + std::vector FreeVariablesIndex; + for (int i = 0; i < x_cauchy.rows(); i++) { + if ((x_cauchy(i) != problem.upperBound()(i)) && (x_cauchy(i) != problem.lowerBound()(i))) { + FreeVariablesIndex.push_back(i); + } + } + const int FreeVarCount = FreeVariablesIndex.size(); + MatrixType WZ = MatrixType::Zero(W.cols(), FreeVarCount); + for (int i = 0; i < FreeVarCount; i++) + WZ.col(i) = W.row(FreeVariablesIndex[i]); + TVector rr = (g + theta * (x_cauchy - x) - W * (M * c)); + // r=r(FreeVariables); + MatrixType r = MatrixType::Zero(FreeVarCount, 1); + for (int i = 0; i < FreeVarCount; i++) + r.row(i) = rr.row(FreeVariablesIndex[i]); + // STEP 2: "v = w^T*Z*r" and STEP 3: "v = M*v" + VariableTVector v = M * (WZ * r); + // STEP 4: N = 1/theta*W^T*Z*(W^T*Z)^T + MatrixType N = theta_inverse * WZ * WZ.transpose(); + // N = I - MN + N = MatrixType::Identity(N.rows(), N.rows()) - M * N; + // STEP: 5 + // v = N^{-1}*v + if (v.size() > 0) + v = N.lu().solve(v); + // STEP: 6 + // HERE IS A MISTAKE IN THE ORIGINAL PAPER! + VariableTVector du = -theta_inverse * r - theta_inverse * theta_inverse * WZ.transpose() * v; + // STEP: 7 + Scalar alpha_star = findAlpha(problem, x_cauchy, du, FreeVariablesIndex); + // STEP: 8 + VariableTVector dStar = alpha_star * du; + SubspaceMin = x_cauchy; + for (int i = 0; i < FreeVarCount; i++) { + SubspaceMin(FreeVariablesIndex[i]) = SubspaceMin(FreeVariablesIndex[i]) + dStar(i); + } + } + public: + void setHistorySize(const int hs) { m_historySize = hs; } + + void minimize(TProblem &problem, TVector &x0) { + if(!problem.isValid(x0)) + std::cerr << "start with invalid x0" << std::endl; + DIM = x0.rows(); + theta = 1.0; + W = MatrixType::Zero(DIM, 0); + M = MatrixType::Zero(0, 0); + MatrixType yHistory = MatrixType::Zero(DIM, 0); + MatrixType sHistory = MatrixType::Zero(DIM, 0); + TVector x = x0, g = x0; + Scalar f = problem.value(x); + problem.gradient(x, g); + // conv. crit. + auto noConvergence = + [&](TVector &x, TVector &g)->bool { + return (((x - g).cwiseMax(problem.lowerBound()).cwiseMin(problem.upperBound()) - x).template lpNorm() >= 1e-4); + }; + this->m_current.reset(); + this->m_status = Status::Continue; + while (problem.callback(this->m_current, x) && noConvergence(x, g) && (this->m_status == Status::Continue)) { + Scalar f_old = f; + TVector x_old = x; + TVector g_old = g; + // STEP 2: compute the cauchy point + TVector CauchyPoint = TVector::Zero(DIM); + VariableTVector c = VariableTVector::Zero(W.cols()); + getGeneralizedCauchyPoint(problem, x, g, CauchyPoint, c); + // STEP 3: compute a search direction d_k by the primal method for the sub-problem + TVector SubspaceMin; + SubspaceMinimization(problem, CauchyPoint, x, c, g, SubspaceMin); + // STEP 4: perform linesearch and STEP 5: compute gradient + Scalar alpha_init = 1.0; + const Scalar rate = MoreThuente::linesearch(x, SubspaceMin-x , problem, alpha_init); + // update current guess and function information + x = x - rate*(x-SubspaceMin); + // if current solution is out of bound, we clip it + clampToBound(problem, x); + f = problem.value(x); + problem.gradient(x, g); + // prepare for next iteration + TVector newY = g - g_old; + TVector newS = x - x_old; + // STEP 6: + Scalar test = newS.dot(newY); + test = (test < 0) ? -1.0 * test : test; + if (test > 1e-7 * newY.squaredNorm()) { + if (yHistory.cols() < m_historySize) { + yHistory.conservativeResize(DIM, yHistory.cols() + 1); + sHistory.conservativeResize(DIM, sHistory.cols() + 1); + } else { + yHistory.leftCols(m_historySize - 1) = yHistory.rightCols(m_historySize - 1).eval(); + sHistory.leftCols(m_historySize - 1) = sHistory.rightCols(m_historySize - 1).eval(); + } + yHistory.rightCols(1) = newY; + sHistory.rightCols(1) = newS; + // STEP 7: + theta = (Scalar)(newY.transpose() * newY) / (newY.transpose() * newS); + W = MatrixType::Zero(yHistory.rows(), yHistory.cols() + sHistory.cols()); + W << yHistory, (theta * sHistory); + MatrixType A = sHistory.transpose() * yHistory; + MatrixType L = A.template triangularView(); + MatrixType MM(A.rows() + L.rows(), A.rows() + L.cols()); + MatrixType D = -1 * A.diagonal().asDiagonal(); + MM << D, L.transpose(), L, ((sHistory.transpose() * sHistory) * theta); + M = MM.inverse(); + } + if (fabs(f_old - f) < 1e-8) { + // successive function values too similar + break; + } + ++this->m_current.iterations; + this->m_current.gradNorm = g.norm(); + this->m_status = checkConvergence(this->m_stop, this->m_current); + } + x0 = x; + if (this->m_debug > DebugLevel::None) { + std::cout << "Stop status was: " << this->m_status << std::endl; + std::cout << "Stop criteria were: " << std::endl << this->m_stop << std::endl; + std::cout << "Current values are: " << std::endl << this->m_current << std::endl; + } + } +}; +} +/* namespace cppoptlib */ +#endif /* LBFGSBSOLVER_H_ */ diff --git a/thirdparty/cppoptlib/solver/lbfgssolver.h b/thirdparty/cppoptlib/solver/lbfgssolver.h new file mode 100644 index 0000000000..eb45927d27 --- /dev/null +++ b/thirdparty/cppoptlib/solver/lbfgssolver.h @@ -0,0 +1,121 @@ +// CppNumericalSolver +// based on: +// Numerical Optimization, 2nd ed. New York: Springer +// J. Nocedal and S. J. Wright +#include +#include +#include "isolver.h" +#include "../linesearch/morethuente.h" + +#ifndef LBFGSSOLVER_H_ +#define LBFGSSOLVER_H_ + +namespace cppoptlib { + +template +class LbfgsSolver : public ISolver { + public: + using Superclass = ISolver; + using typename Superclass::Scalar; + using typename Superclass::TVector; + using typename Superclass::THessian; + using MatrixType = Eigen::Matrix; + + void minimize(ProblemType &objFunc, TVector &x0) { + const size_t m = 10; + const size_t DIM = x0.rows(); + MatrixType sVector = MatrixType::Zero(DIM, m); + MatrixType yVector = MatrixType::Zero(DIM, m); + Eigen::Matrix alpha = Eigen::Matrix::Zero(m); + TVector grad(DIM), q(DIM), grad_old(DIM), s(DIM), y(DIM); + objFunc.gradient(x0, grad); + TVector x_old = x0; + + size_t iter = 0, globIter = 0; + Scalar H0k = 1; + this->m_current.reset(); + do { + const Scalar relativeEpsilon = static_cast(0.0001) * std::max(static_cast(1.0), x0.norm()); + + if (grad.norm() < relativeEpsilon) + break; + + //Algorithm 7.4 (L-BFGS two-loop recursion) + q = grad; + const int k = std::min(m, iter); + + // for i = k āˆ’ 1, k āˆ’ 2, . . . , k āˆ’ mĀ§ + for (int i = k - 1; i >= 0; i--) { + // alpha_i <- rho_i*s_i^T*q + const double rho = 1.0 / static_cast(sVector.col(i)) + .dot(static_cast(yVector.col(i))); + alpha(i) = rho * static_cast(sVector.col(i)).dot(q); + // q <- q - alpha_i*y_i + q = q - alpha(i) * yVector.col(i); + } + // r <- H_k^0*q + q = H0k * q; + //for i k āˆ’ m, k āˆ’ m + 1, . . . , k āˆ’ 1 + for (int i = 0; i < k; i++) { + // beta <- rho_i * y_i^T * r + const Scalar rho = 1.0 / static_cast(sVector.col(i)) + .dot(static_cast(yVector.col(i))); + const Scalar beta = rho * static_cast(yVector.col(i)).dot(q); + // r <- r + s_i * ( alpha_i - beta) + q = q + sVector.col(i) * (alpha(i) - beta); + } + // stop with result "H_k*f_f'=q" + + // any issues with the descent direction ? + Scalar descent = -grad.dot(q); + Scalar alpha_init = 1.0 / grad.norm(); + if (descent > -0.0001 * relativeEpsilon) { + q = -1 * grad; + iter = 0; + alpha_init = 1.0; + } + + // find steplength + const Scalar rate = MoreThuente::linesearch(x0, -q, objFunc, alpha_init) ; + // update guess + x0 = x0 - rate * q; + + grad_old = grad; + objFunc.gradient(x0, grad); + + s = x0 - x_old; + y = grad - grad_old; + + // update the history + if (iter < m) { + sVector.col(iter) = s; + yVector.col(iter) = y; + } else { + + sVector.leftCols(m - 1) = sVector.rightCols(m - 1).eval(); + sVector.rightCols(1) = s; + yVector.leftCols(m - 1) = yVector.rightCols(m - 1).eval(); + yVector.rightCols(1) = y; + } + // update the scaling factor + H0k = y.dot(s) / static_cast(y.dot(y)); + + x_old = x0; + // std::cout << "iter: "<m_current.iterations; + this->m_current.gradNorm = grad.template lpNorm(); + this->m_status = checkConvergence(this->m_stop, this->m_current); + } while ((objFunc.callback(this->m_current, x0)) && (this->m_status == Status::Continue)); + + } + +}; + +} +/* namespace cppoptlib */ + +#endif /* LBFGSSOLVER_H_ */ diff --git a/thirdparty/cppoptlib/solver/neldermeadsolver.h b/thirdparty/cppoptlib/solver/neldermeadsolver.h new file mode 100644 index 0000000000..d780f87a3e --- /dev/null +++ b/thirdparty/cppoptlib/solver/neldermeadsolver.h @@ -0,0 +1,225 @@ +// CppNumericalSolver +#ifndef NELDERMEADSOLVER_H_ +#define NELDERMEADSOLVER_H_ +#include +#include +#include "isolver.h" +#include "../meta.h" + +namespace cppoptlib { + +template +class NelderMeadSolver : public ISolver { + public: + using Superclass = ISolver; + using typename Superclass::Scalar; + using typename Superclass::TVector; + using MatrixType = Eigen::Matrix; + MatrixType x0; + SimplexOp lastOp = SimplexOp::Place; + Status stop_condition; + bool initialSimplexCreated = false; + + MatrixType makeInitialSimplex(TVector &x) { + size_t DIM = x.rows(); + + MatrixType s = MatrixType::Zero(DIM, DIM + 1); + for (int c = 0; c < int(DIM) + 1; ++c) { + for (int r = 0; r < int(DIM); ++r) { + s(r, c) = x(r); + if (r == c - 1) { + if (x(r) == 0) { + s(r, c) = 0.00025; + } else { + s(r, c) = (1 + 0.05) * x(r); + } + } + } + } + + return s; + } + + /** + * @brief minimize + * @details [long description] + * + * @param objFunc [description] + */ + void minimize(ProblemType &objFunc, TVector &x) { + + const Scalar rho = 1.; // rho > 0 + const Scalar xi = 2.; // xi > max(rho, 1) + const Scalar gam = 0.5; // 0 < gam < 1 + + const size_t DIM = x.rows(); + + // create initial simplex + if (! initialSimplexCreated) { + x0 = makeInitialSimplex(x); + } + + // compute function values + std::vector f; f.resize(DIM + 1); + std::vector index; index.resize(DIM + 1); + for (int i = 0; i < int(DIM) + 1; ++i) { + f[i] = objFunc(static_cast(x0.col(i))); + index[i] = i; + } + + sort(index.begin(), index.end(), [&](int a, int b)-> bool { return f[a] < f[b]; }); + + int iter = 0; + const int maxIter = this->m_stop.iterations * DIM; + while ( + objFunc.callback(this->m_current, x0.col(index[0])) && + (iter < maxIter) + ) { + // conv-check + Scalar max1 = fabs(f[index[1]] - f[index[0]]); + Scalar max2 = (x0.col(index[1]) - x0.col(index[0]) ).array().abs().maxCoeff(); + for (int i = 2; i < int(DIM) + 1; ++i) { + Scalar tmp1 = fabs(f[index[i]] - f[index[0]]); + if (tmp1 > max1) + max1 = tmp1; + + Scalar tmp2 = (x0.col(index[i]) - x0.col(index[0]) ).array().abs().maxCoeff(); + if (tmp2 > max2) + max2 = tmp2; + } + const Scalar tt1 = std::max(Scalar(1.e-04), 10 * std::nextafter(f[index[0]], std::numeric_limits::epsilon()) - f[index[0]]); + const Scalar tt2 = std::max(Scalar(1.e-04), 10 * (std::nextafter(x0.col(index[0]).maxCoeff(), std::numeric_limits::epsilon()) + - x0.col(index[0]).maxCoeff())); + + // User-defined stopping criteria + this->m_current.iterations = iter; + this->m_current.fDelta = max1; + this->m_current.xDelta = max2; + stop_condition = checkConvergence(this->m_stop, this->m_current); + if (this->m_stop.iterations != 0 && stop_condition != Status::Continue) { + break; + } + + // Allow stopping in the callback. This callback gets the correct current + // state unlike the simple one in while(), which get previous state. + if (objFunc.detailed_callback(this->m_current, lastOp, index[0], x0, f) == false) { + stop_condition = Status::UserDefined; + break; + } + + // max(||x - shift(x) ||_inf ) <= tol, + if (max1 <= tt1) { + // values to similar + if (max2 <= tt2) { + stop_condition = Status::FDeltaTolerance; + break; + } + } + + ////////////////////////// + + // midpoint of the simplex opposite the worst point + TVector x_bar = TVector::Zero(DIM); + for (int i = 0; i < int(DIM); ++i) { + x_bar += x0.col(index[i]); + } + x_bar /= Scalar(DIM); + + // Compute the reflection point + const TVector x_r = ( 1. + rho ) * x_bar - rho * x0.col(index[DIM]); + const Scalar f_r = objFunc(x_r); + lastOp = SimplexOp::Reflect; + + if (f_r < f[index[0]]) { + // the expansion point + const TVector x_e = ( 1. + rho * xi ) * x_bar - rho * xi * x0.col(index[DIM]); + const Scalar f_e = objFunc(x_e); + if ( f_e < f_r ) { + // expand + lastOp = SimplexOp::Expand; + x0.col(index[DIM]) = x_e; + f[index[DIM]] = f_e; + } else { + // reflect + lastOp = SimplexOp::Reflect; + x0.col(index[DIM]) = x_r; + f[index[DIM]] = f_r; + } + } else { + if ( f_r < f[index[DIM - 1]] ) { + x0.col(index[DIM]) = x_r; + f[index[DIM]] = f_r; + } else { + // contraction + if (f_r < f[index[DIM]]) { + const TVector x_c = (1 + rho * gam) * x_bar - rho * gam * x0.col(index[DIM]); + const Scalar f_c = objFunc(x_c); + if ( f_c <= f_r ) { + // outside + x0.col(index[DIM]) = x_c; + f[index[DIM]] = f_c; + lastOp = SimplexOp::ContractOut; + } else { + shrink(x0, index, f, objFunc); + lastOp = SimplexOp::Shrink; + } + } else { + // inside + const TVector x_c = ( 1 - gam ) * x_bar + gam * x0.col(index[DIM]); + const Scalar f_c = objFunc(x_c); + if (f_c < f[index[DIM]]) { + x0.col(index[DIM]) = x_c; + f[index[DIM]] = f_c; + lastOp = SimplexOp::ContractIn; + } else { + shrink(x0, index, f, objFunc); + lastOp = SimplexOp::Shrink; + } + } + } + } + sort(index.begin(), index.end(), [&](int a, int b)-> bool { return f[a] < f[b]; }); + iter++; + if (iter >= maxIter) { + stop_condition = Status::IterationLimit; + } + else { + stop_condition = Status::UserDefined; // if stopped in the callback in while() + } + } // while loop + + // report the last result + objFunc.detailed_callback(this->m_current, lastOp, index[0], x0, f); + x = x0.col(index[0]); + } + + void shrink(MatrixType &x, std::vector &index, std::vector &f, ProblemType &objFunc) { + const Scalar sig = 0.5; // 0 < sig < 1 + const int DIM = x.rows(); + f[index[0]] = objFunc(x.col(index[0])); + for (int i = 1; i < DIM + 1; ++i) { + x.col(index[i]) = sig * x.col(index[i]) + (1. - sig) * x.col(index[0]); + f[index[i]] = objFunc(x.col(index[i])); + } + } + + // Need our own checker here to get rid of the gradient test used in other solvers + template + Status checkConvergence(const Criteria &stop, const Criteria ¤t) { + if ((stop.iterations > 0) && (current.iterations > stop.iterations)) { + return Status::IterationLimit; + } + if ((stop.xDelta > 0) && (current.xDelta < stop.xDelta)) { + return Status::XDeltaTolerance; + } + if ((stop.fDelta > 0) && (current.fDelta < stop.fDelta)) { + return Status::FDeltaTolerance; + } + return Status::Continue; + } + +}; /* class NelderMeadSolver */ + +} /* namespace cppoptlib */ + +#endif /* NELDERMEADSOLVER_H_ */ diff --git a/thirdparty/cppoptlib/solver/newtondescentsolver.h b/thirdparty/cppoptlib/solver/newtondescentsolver.h new file mode 100644 index 0000000000..689997b363 --- /dev/null +++ b/thirdparty/cppoptlib/solver/newtondescentsolver.h @@ -0,0 +1,43 @@ +// CppNumericalSolver +#include +#include +#include "isolver.h" +#include "../linesearch/armijo.h" + +#ifndef NEWTONDESCENTSOLVER_H_ +#define NEWTONDESCENTSOLVER_H_ + +namespace cppoptlib { + +template +class NewtonDescentSolver : public ISolver { + public: + using Superclass = ISolver; + using typename Superclass::Scalar; + using typename Superclass::TVector; + using typename Superclass::THessian; + + void minimize(ProblemType &objFunc, TVector &x0) { + const int DIM = x0.rows(); + TVector grad = TVector::Zero(DIM); + THessian hessian = THessian::Zero(DIM, DIM); + this->m_current.reset(); + do { + objFunc.gradient(x0, grad); + objFunc.hessian(x0, hessian); + hessian += (1e-5) * THessian::Identity(DIM, DIM); + TVector delta_x = hessian.lu().solve(-grad); + const double rate = Armijo::linesearch(x0, delta_x, objFunc) ; + x0 = x0 + rate * delta_x; + // std::cout << "iter: "<m_current.iterations; + this->m_current.gradNorm = grad.template lpNorm(); + this->m_status = checkConvergence(this->m_stop, this->m_current); + } while (objFunc.callback(this->m_current, x0) && (this->m_status == Status::Continue)); + } +}; + +} +/* namespace cppoptlib */ + +#endif /* NEWTONDESCENTSOLVER_H_ */ diff --git a/thirdparty/cppoptlib/timer.h b/thirdparty/cppoptlib/timer.h new file mode 100644 index 0000000000..b25da8a7f0 --- /dev/null +++ b/thirdparty/cppoptlib/timer.h @@ -0,0 +1,90 @@ +#ifndef TIMER_H +#define TIMER_H + +#include +#include + +namespace cppoptlib { + +template +class timer +{ + std::chrono::time_point start_v; + std::chrono::time_point pause_v; + std::chrono::time_point end_v; + + bool stopped; + bool paused; + +public: + explicit timer() : stopped(false), paused(false) { + start_v = C::now(); + } + ~timer() {} + + /** + * @brief start stopwatch + * @details [long description] + */ + void start() { + start_v = C::now(); + paused = false; + stopped = false; + } + + /** + * @brief pause stopwatch, but allows resuming + * @details [long description] + */ + void pause() { + pause_v = C::now(); + paused = true; + } + + void resume() { + if(stopped) + throw std::runtime_error("cannot resume a stopped timer"); + start_v += C::now() - pause_v; + paused = false; + stopped = false; + } + + void stop() { + end_v = C::now(); + stopped = true; + } + + template + typename U::rep elapsed() const + { + /* + example: + cns::timer t; + t.sart(); + // do something + t.stop(); + std::cout << t.elapsed() << std::endl; + + where UNIT can be: + std::chrono::nanoseconds + std::chrono::microseconds + std::chrono::milliseconds + std::chrono::seconds + std::chrono::minutes + std::chrono::hours + */ + + return + (stopped) ? + std::chrono::duration_cast(end_v - start_v).count() : + (paused) ? + std::chrono::duration_cast(pause_v - start_v).count() : + std::chrono::duration_cast(C::now() - start_v).count();; + } + +}; + +} +/* namespace cns */ + +#endif /* TIMER_H */ From 2639c2ac34df364b47ad9fbdf598a202f398c3d7 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Mon, 11 Jul 2022 15:00:28 -0400 Subject: [PATCH 02/22] Initial import of previous code Signed-off-by: Geoff Hutchison --- .../calc/LennardJones612_UniversalShifted.txt | 198 ++++++++++ avogadro/calc/energycalculator.cpp | 51 +++ avogadro/calc/energycalculator.h | 71 ++++ avogadro/calc/lennardjones.cpp | 144 ++++++++ avogadro/calc/lennardjones.h | 51 +++ avogadro/qtplugins/forcefield/CMakeLists.txt | 33 ++ avogadro/qtplugins/forcefield/forcefield.cpp | 204 +++++++++++ avogadro/qtplugins/forcefield/forcefield.h | 83 +++++ .../qtplugins/forcefield/forcefielddialog.cpp | 339 ++++++++++++++++++ .../qtplugins/forcefield/forcefielddialog.h | 114 ++++++ .../qtplugins/forcefield/forcefielddialog.ui | 196 ++++++++++ avogadro/qtplugins/forcefield/obmmprocess.cpp | 164 +++++++++ avogadro/qtplugins/forcefield/obmmprocess.h | 163 +++++++++ 13 files changed, 1811 insertions(+) create mode 100644 avogadro/calc/LennardJones612_UniversalShifted.txt create mode 100644 avogadro/calc/energycalculator.cpp create mode 100644 avogadro/calc/energycalculator.h create mode 100644 avogadro/calc/lennardjones.cpp create mode 100644 avogadro/calc/lennardjones.h create mode 100644 avogadro/qtplugins/forcefield/CMakeLists.txt create mode 100644 avogadro/qtplugins/forcefield/forcefield.cpp create mode 100644 avogadro/qtplugins/forcefield/forcefield.h create mode 100644 avogadro/qtplugins/forcefield/forcefielddialog.cpp create mode 100644 avogadro/qtplugins/forcefield/forcefielddialog.h create mode 100644 avogadro/qtplugins/forcefield/forcefielddialog.ui create mode 100644 avogadro/qtplugins/forcefield/obmmprocess.cpp create mode 100644 avogadro/qtplugins/forcefield/obmmprocess.h diff --git a/avogadro/calc/LennardJones612_UniversalShifted.txt b/avogadro/calc/LennardJones612_UniversalShifted.txt new file mode 100644 index 0000000000..2727fec412 --- /dev/null +++ b/avogadro/calc/LennardJones612_UniversalShifted.txt @@ -0,0 +1,198 @@ +# cutoff and sigmas are in units of Angstroms +# epsilon are in units of Electron Volts +#Species_i # Species_j # cutoff # epsilon # sigma +H H 2.2094300 4.4778900 0.5523570 +He He 1.9956100 0.0009421 0.4989030 +Li Li 9.1228000 1.0496900 2.2807000 +Be Be 6.8421000 0.5729420 1.7105300 +B B 6.0581100 2.9670300 1.5145300 +C C 5.4166600 6.3695300 1.3541700 +N N 5.0603000 9.7537900 1.2650800 +O O 4.7039500 5.1264700 1.1759900 +F F 4.0625000 1.6059200 1.0156200 +Ne Ne 4.1337700 0.0036471 1.0334400 +Na Na 11.8311000 0.7367450 2.9577800 +Mg Mg 10.0493000 0.0785788 2.5123300 +Al Al 8.6239000 2.7006700 2.1559700 +Si Si 7.9111800 3.1743100 1.9778000 +P P 7.6260900 5.0305000 1.9065200 +S S 7.4835500 4.3692700 1.8708900 +Cl Cl 7.2697300 4.4832800 1.8174300 +Ar Ar 7.5548200 0.0123529 1.8887100 +K K 14.4682000 0.5517990 3.6170500 +Ca Ca 12.5439000 0.1326790 3.1359600 +Sc Sc 12.1162000 1.6508000 3.0290600 +Ti Ti 11.4035000 1.1802700 2.8508800 +V V 10.9046000 2.7524900 2.7261500 +Cr Cr 9.9067900 1.5367900 2.4767000 +Mn Mn 9.9067900 0.5998880 2.4767000 +Fe Fe 9.4078900 1.1844200 2.3519700 +Co Co 8.9802600 1.2776900 2.2450600 +Ni Ni 8.8377200 2.0757200 2.2094300 +Cu Cu 9.4078900 2.0446300 2.3519700 +Zn Zn 8.6951700 0.1915460 2.1737900 +Ga Ga 8.6951700 1.0642000 2.1737900 +Ge Ge 8.5526300 2.7017100 2.1381600 +As As 8.4813600 3.9599000 2.1203400 +Se Se 8.5526300 3.3867700 2.1381600 +Br Br 8.5526300 1.9706300 2.1381600 +Kr Kr 8.2675400 0.0173276 2.0668900 +Rb Rb 15.6798000 0.4682650 3.9199500 +Sr Sr 13.8980000 0.1339230 3.4745100 +Y Y 13.5417000 2.7597500 3.3854200 +Zr Zr 12.4726000 3.0520100 3.1181500 +Nb Nb 11.6886000 5.2782000 2.9221500 +Mo Mo 10.9759000 4.4749900 2.7439700 +Tc Tc 10.4770000 3.3815900 2.6192400 +Ru Ru 10.4057000 1.9617200 2.6014200 +Rh Rh 10.1206000 2.4058200 2.5301500 +Pd Pd 9.9067900 1.3709700 2.4767000 +Ag Ag 10.3344000 1.6497600 2.5836100 +Cd Cd 10.2632000 0.0377447 2.5657900 +In In 10.1206000 0.8113140 2.5301500 +Sn Sn 9.9067900 1.9005700 2.4767000 +Sb Sb 9.9067900 3.0882800 2.4767000 +Te Te 9.8355200 2.6312300 2.4588800 +I I 9.9067900 1.5393800 2.4767000 +Xe Xe 9.9780700 0.0238880 2.4945200 +Cs Cs 17.3903000 0.4166420 4.3475900 +Ba Ba 15.3235000 1.9000000 3.8308600 +La La 14.7533000 2.4996100 3.6883200 +Ce Ce 14.5395000 2.5700800 3.6348700 +Pr Pr 14.4682000 1.2994600 3.6170500 +Nd Nd 14.3257000 0.8196050 3.5814100 +Pm Pm 14.1831000 3.2413400 3.5457800 +Sm Sm 14.1118000 0.5211220 3.5279600 +Eu Eu 14.1118000 0.4299180 3.5279600 +Gd Gd 13.9693000 2.0995600 3.4923200 +Tb Tb 13.8267000 1.3999900 3.4566900 +Dy Dy 13.6842000 0.6900550 3.4210500 +Ho Ho 13.6842000 0.6900550 3.4210500 +Er Er 13.4704000 0.7387660 3.3676000 +Tm Tm 13.5417000 0.5211220 3.3854200 +Yb Yb 13.3278000 0.1303990 3.3319600 +Lu Lu 13.3278000 1.4331500 3.3319600 +Hf Hf 12.4726000 3.3608600 3.1181500 +Ta Ta 12.1162000 4.0034300 3.0290600 +W W 11.5460000 6.8638900 2.8865100 +Re Re 10.7621000 4.4387100 2.6905100 +Os Os 10.2632000 4.2625300 2.5657900 +Ir Ir 10.0493000 3.7028700 2.5123300 +Pt Pt 9.6929800 3.1401000 2.4232400 +Au Au 9.6929800 2.3058000 2.4232400 +Hg Hg 9.4078900 0.0454140 2.3519700 +Tl Tl 10.3344000 0.5770870 2.5836100 +Pb Pb 10.4057000 0.8589880 2.6014200 +Bi Bi 10.5482000 2.0798700 2.6370600 +Po Po 9.9780700 1.8995300 2.4945200 +At At 10.6908000 1.3854420 2.6727000 +Rn Rn 10.6908000 0.0214992 2.6727000 +Fr Fr 18.5307000 0.3749778 4.6326700 +Ra Ra 15.7511000 1.7100000 3.9377700 +Ac Ac 15.3235000 2.2496490 3.8308600 +Th Th 14.6820000 2.3130720 3.6705000 +Pa Pa 14.2544000 1.1695140 3.5635900 +U U 13.9693000 0.7376445 3.4923200 +Np Np 13.5417000 2.9172060 3.3854200 +Pu Pu 13.3278000 0.4690098 3.3319600 +Am Am 12.8289000 0.3869262 3.2072400 +Cm Cm 12.0450000 1.8896040 3.0112400 +Bk Bk 11.9737000 1.2599910 2.9934200 +Cf Cf 11.9737000 0.6210495 2.9934200 +Es Es 11.7599000 0.6210495 2.9399700 +Fm Fm 11.9024000 0.6648894 2.9756000 +Md Md 12.3300000 0.4690098 3.0825100 +No No 12.5439000 0.1173591 3.1359600 +Lr Lr 11.4748000 1.2898350 2.8686900 +Rf Rf 11.1897000 3.0247740 2.7974200 +Db Db 10.6195000 3.6030870 2.6548800 +Sg Sg 10.1919000 6.1775010 2.5479700 +Bh Bh 10.0493000 3.9948390 2.5123300 +Hs Hs 9.5504300 3.8362770 2.3876100 +Mt Mt 9.1940700 3.3325830 2.2985200 +Ds Ds 9.1228000 2.8260900 2.2807000 +Rg Rg 8.6239000 2.0752200 2.1559700 +Cn Cn 8.6951700 0.0408726 2.1737900 +Nh Nh 9.6929800 0.5193783 2.4232400 +Fl Fl 10.1919000 0.7730892 2.5479700 +Mc Mc 11.5460000 1.8718830 2.8865100 +Lv Lv 12.4726000 1.7095770 3.1181500 +Ts Ts 11.7599000 1.2468978 2.9399700 +Og Og 11.1897000 0.0193493 2.7974200 +electron electron 4.0 1.0 1.0 +user01 user01 4.0 1.0 1.0 +user02 user02 4.0 1.0 1.0 +user03 user03 4.0 1.0 1.0 +user04 user04 4.0 1.0 1.0 +user05 user05 4.0 1.0 1.0 +user06 user06 4.0 1.0 1.0 +user07 user07 4.0 1.0 1.0 +user08 user08 4.0 1.0 1.0 +user09 user09 4.0 1.0 1.0 +user10 user10 4.0 1.0 1.0 +user11 user11 4.0 1.0 1.0 +user12 user12 4.0 1.0 1.0 +user13 user13 4.0 1.0 1.0 +user14 user14 4.0 1.0 1.0 +user15 user15 4.0 1.0 1.0 +user16 user16 4.0 1.0 1.0 +user17 user17 4.0 1.0 1.0 +user18 user18 4.0 1.0 1.0 +user19 user19 4.0 1.0 1.0 +user20 user20 4.0 1.0 1.0 + +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the Common Development +# and Distribution License Version 1.0 (the "License"). +# +# You can obtain a copy of the license at +# http://www.opensource.org/licenses/CDDL-1.0. See the License for the +# specific language governing permissions and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each file and +# include the License file in a prominent location with the name LICENSE.CDDL. +# If applicable, add the following below this CDDL HEADER, with the fields +# enclosed by brackets "[]" replaced with your own identifying information: +# +# Portions Copyright (c) [yyyy] [name of copyright owner]. All rights reserved. +# +# CDDL HEADER END +# + +# +# Copyright (c) 2015, Regents of the University of Minnesota. +# All rights reserved. +# +# Contributors: +# Ryan S. Elliott +# Andrew Akerson +# + + +# * Sigma parameters are set to (2^{-1/6})*r_0, where r_0 is the atomic +# covalent radius. Covalent radii for elements 1--96 were taken from Wolfram +# Mathematica's `ElementData["CovalentRadius"]' command. Covalent radii for +# elements 97--118 were taken from Fig. 3 of the article Pyykko, M. Atsumi, +# J. Chem. Eur. J. 15 (2009) 12770. +# +# * Epsilon parameters are set to the bond dissociation energy. Bond +# dissociation energies for elements 1--55, 57--60, and 61--84 were taken +# from the CRC Handbook of Chemistry and Physics, 91st Edition, +# Ed. W.H. Haynes, 2010. (as posted here: +# http://staff.ustc.edu.cn/~luo971/2010-91-CRC-BDEs-Tables.pdf) +# +# The value (cohesive energy, in this case) for element 56 was obtained from +# p. 50 in Charles Kittel. Introduction to Solid State Physics, 8th +# edition. Hoboken, NJ: John Wiley & Sons, Inc, 2005. +# +# The bond dissociation energy value for element 61 was obtained from +# "Interpolation scheme for the cohesive energies for the lanthanides and +# actinides" Borje Johansson and Anders Rosengren, Phys. Rev. B 11, 1367 +# (1975). +# +# The bond dissociation energies for elements 85--118 were not found in the +# literature. Thus, the values used here are approximated by subtracting 10% +# from the value for the element in the same Group (column) and previous +# Period (row) of the periodic table. diff --git a/avogadro/calc/energycalculator.cpp b/avogadro/calc/energycalculator.cpp new file mode 100644 index 0000000000..3721b6a0a3 --- /dev/null +++ b/avogadro/calc/energycalculator.cpp @@ -0,0 +1,51 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + + This source code is released under the New BSD License, (the "License"). +******************************************************************************/ + +#include "energycalculator.h" + +#include + +namespace Avogadro { + +void EnergyCalculator::gradient(const TVector& x, TVector& grad) +{ + finiteGradient(x, grad); + cleanGradients(grad); +} + +void EnergyCalculator::cleanGradients(TVector& grad) +{ + unsigned int size = grad.rows(); + // check for overflows -- in case of divide by zero, etc. + for (unsigned int i = 0; i < size; ++i) { + if (!std::isfinite(grad[i])) { + grad[i] = 0.0; + } + } + + // freeze any masked atoms or coordinates + grad = grad.cwiseProduct(m_mask); +} + +void EnergyCalculator::freezeAtom(Index atomId) +{ + if (atomId * 3 <= m_mask.rows() - 3) { + m_mask[atomId*3] = 0.0; + m_mask[atomId*3+1] = 0.0; + m_mask[atomId*3+2] = 0.0; + } +} + +void EnergyCalculator::unfreezeAtom(Index atomId) +{ + if (atomId * 3 <= m_mask.rows() - 3) { + m_mask[atomId*3] = 1.0; + m_mask[atomId*3+1] = 1.0; + m_mask[atomId*3+2] = 1.0; + } +} + +} // namespace Avogadro diff --git a/avogadro/calc/energycalculator.h b/avogadro/calc/energycalculator.h new file mode 100644 index 0000000000..096a7134fe --- /dev/null +++ b/avogadro/calc/energycalculator.h @@ -0,0 +1,71 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + + This source code is released under the New BSD License, (the "License"). +******************************************************************************/ + +#ifndef AVOGADRO_ENERGYCALCULATOR_H +#define AVOGADRO_ENERGYCALCULATOR_H + +#include +#include + +#include + +namespace Avogadro { +namespace QtGui { +class Molecule; +} + +class EnergyCalculator + : public QObject + , public cppoptlib::Problem +{ + Q_OBJECT +public: + EnergyCalculator(QObject* parent_ = 0) + : QObject(parent_){}; + ~EnergyCalculator() { } + + /** + * @return A short translatable name for this method (e.g., MMFF94, UFF, etc.) + */ + virtual QString name() const = 0; + + /** + * @return a description of the method + */ + virtual QString description() const = 0; + + /** + * Called to set the configuration (e.g., from a GUI options dialog) + */ + virtual bool setConfiguration(Core::VariantMap& config) { return true; } + + /** + * Calculate the gradients for this method, defaulting to numerical + * finite-difference methods + */ + virtual void gradient(const TVector& x, TVector& grad) override; + + /** + * Called to 'clean' gradients @param grad (e.g., for constraints) + */ + void cleanGradients(TVector& grad); + + void freezeAtom(Index atomId); + void unfreezeAtom(Index atomId); + +public slots: + /** + * Called when the current molecule changes. + */ + virtual void setMolecule(QtGui::Molecule* mol) = 0; + +protected: + TVector m_mask; // optimize or frozen atom mask +}; + +} // namespace Avogadro + +#endif // AVOGADRO_ENERGYCALCULATOR_H diff --git a/avogadro/calc/lennardjones.cpp b/avogadro/calc/lennardjones.cpp new file mode 100644 index 0000000000..13f2f16ac4 --- /dev/null +++ b/avogadro/calc/lennardjones.cpp @@ -0,0 +1,144 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + + This source code is released under the New BSD License, (the "License"). +******************************************************************************/ + +#include "lennardjones.h" + +#include + +#include + +namespace Avogadro { + +LennardJones::LennardJones(QObject* parent_) + : EnergyCalculator(parent_) + , m_vdw(true) + , m_depth(100.0) + , m_exponent(6) +{} + +LennardJones::~LennardJones() {} + +void LennardJones::setMolecule(QtGui::Molecule* mol) +{ + m_molecule = mol; + + if (mol == nullptr) { + return; // nothing to do + } + int numAtoms = mol->atomCount(); + + // track atomic radii for this molecule + m_radii.setZero(); + Eigen::MatrixXd radii(numAtoms, numAtoms); + + // handle the frozen atoms + // set a clean mask (everything can move) + m_mask = Eigen::MatrixXd::Constant(numAtoms * 3, 1, 1.0); + + // now freeze the specified atoms + for (Index i = 0; i < numAtoms; ++i) { + if (mol->atomFrozen(i)) { + // zero out the gradients for these atoms + m_mask[i*3] = 0.0; + m_mask[i*3+1] = 0.0; + m_mask[i*3+2] = 0.0; + } + } + + for (Index i = 0; i < numAtoms; ++i) { + Core::Atom atom1 = mol->atom(i); + unsigned char number1 = atom1.atomicNumber(); + double r1; + if (m_vdw) + r1 = Core::Elements::radiusVDW(number1); + else + r1 = Core::Elements::radiusCovalent(number1); + + for (Index j = i + 1; j < numAtoms; ++j) { + Core::Atom atom2 = mol->atom(j); + unsigned char number2 = atom2.atomicNumber(); + double r2; + if (m_vdw) + r2 = Core::Elements::radiusVDW(number2); + else + r2 = Core::Elements::radiusCovalent(number2); + + radii(i, j) = r1 + r2; // expected distance + } + } + + m_radii = radii; + //@todo store unit cell if available +} + +Real LennardJones::value(const Eigen::VectorXd& x) +{ + if (!m_molecule) + return 0.0; + + // FYI https://en.wikipedia.org/wiki/Lennard-Jones_potential + //@todo handle unit cells and minimum distances + int numAtoms = m_molecule->atomCount(); + + Real energy = 0.0; + for (Index i = 0; i < numAtoms; ++i) { + Vector3 ipos(x[3 * i], x[3 * i + 1], x[3 * i + 2]); + for (Index j = i + 1; j < numAtoms; ++j) { + Vector3 jpos(x[3 * j], x[3 * j + 1], x[3 * j + 2]); + Real r = (ipos - jpos).norm(); + if (r < 0.1) + r = 0.1; // ensure we don't divide by zero + + Real ratio = pow((m_radii(i, j) / r), m_exponent); + energy += m_depth * (ratio * ratio - 2.0 * (ratio)); + } + } + + // qDebug() << " lj: " << energy; + return energy; +} + +void LennardJones::gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) +{ + if (!m_molecule) + return; + + // clear the gradients + grad.setZero(); + + //@todo handle unit cells and minimum distances + int numAtoms = m_molecule->atomCount(); + + for (Index i = 0; i < numAtoms; ++i) { + Vector3 ipos(x[3 * i], x[3 * i + 1], x[3 * i + 2]); + for (Index j = i + 1; j < numAtoms; ++j) { + Vector3 jpos(x[3 * j], x[3 * j + 1], x[3 * j + 2]); + Vector3 force = ipos - jpos; + + Real r = force.norm(); + if (r < 0.1) + r = 0.1; // ensure we don't divide by zero + + Real rad = pow(m_radii(i, j), m_exponent); + Real term1 = -2 * (m_exponent)*rad * rad * pow(r, -2 * m_exponent - 1); + Real term2 = 2 * (m_exponent)*rad * pow(r, -1 * m_exponent - 1); + Real dE = m_depth * (term1 + term2); + + force = (dE / r) * force; + + // update gradients + for (unsigned int c = 0; c < 3; ++c) { + grad[3 * i + c] += force[c]; + grad[3 * j + c] -= force[c]; + } + } + } + + // handle any constraints + cleanGradients(grad); +} + +} // namespace Avogadro diff --git a/avogadro/calc/lennardjones.h b/avogadro/calc/lennardjones.h new file mode 100644 index 0000000000..d8acac61e3 --- /dev/null +++ b/avogadro/calc/lennardjones.h @@ -0,0 +1,51 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + + This source code is released under the New BSD License, (the "License"). +******************************************************************************/ + +#ifndef AVOGADRO_QTGUI_LENNARDJONES_H +#define AVOGADRO_QTGUI_LENNARDJONES_H + +#include "energycalculator.h" + +namespace Avogadro { +namespace QtGui { +class Molecule; +} + +class LennardJones : public EnergyCalculator +{ + Q_OBJECT + +public: + explicit LennardJones(QObject* parent_ = 0); + ~LennardJones(); + + virtual QString name() const override + { return tr("Lennard-Jones"); } + + virtual QString description() const override + { return tr("Universal Lennard-Jones potential"); } + + virtual Real value(const Eigen::VectorXd& x) override; + virtual void gradient(const Eigen::VectorXd& x, + Eigen::VectorXd& grad) override; + +public slots: + /** + * Called when the current molecule changes. + */ + virtual void setMolecule(QtGui::Molecule* mol) override; + +protected: + QtGui::Molecule* m_molecule; + Eigen::MatrixXd m_radii; + bool m_vdw; + Real m_depth; + int m_exponent; +}; + +} // namespace Avogadro + +#endif // AVOGADRO_QTGUI_LENNARDJONES_H diff --git a/avogadro/qtplugins/forcefield/CMakeLists.txt b/avogadro/qtplugins/forcefield/CMakeLists.txt new file mode 100644 index 0000000000..218d7a0825 --- /dev/null +++ b/avogadro/qtplugins/forcefield/CMakeLists.txt @@ -0,0 +1,33 @@ + +# Add as "system headers" to avoid warnings generated by them with +# compilers that support that notion. +include_directories(SYSTEM "${EIGEN3_INCLUDE_DIR}" + "${AvogadroLibs_BINARY_DIR}/avogadro/io/" + "${AvogadroLibs_SOURCE_DIR}/thirdparty") + +# Extension +set(forcefield_srcs + energycalculator.cpp + forcefield.cpp + lennardjones.cpp +) + +avogadro_plugin(Forcefield + "Force field optimization and dynamics" + ExtensionPlugin + forcefield.h + Forcefield + "${forcefield_srcs}" +) + +target_link_libraries(Forcefield LINK_PRIVATE AvogadroIO) + +# Bundled forcefield scripts (open babel) +set(forcefields + scripts/uff.py + scripts/gaff.py + scripts/mmff.py +) + +#install(PROGRAMS ${forcefields} +#DESTINATION "${INSTALL_LIBRARY_DIR}/avogadro2/scripts/forcefields/") diff --git a/avogadro/qtplugins/forcefield/forcefield.cpp b/avogadro/qtplugins/forcefield/forcefield.cpp new file mode 100644 index 0000000000..4f471658af --- /dev/null +++ b/avogadro/qtplugins/forcefield/forcefield.cpp @@ -0,0 +1,204 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + + This source code is released under the New BSD License, (the "License"). +******************************************************************************/ + +#include "forcefield.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "lennardjones.h" + +#include +#include + +#include +// not currently used +#include +#include + +namespace Avogadro { +namespace QtPlugins { + +using Avogadro::QtGui::Molecule; + +const int energyAction = 0; +const int optimizeAction = 1; +const int configureAction = 2; +const int freezeAction = 3; + +Forcefield::Forcefield(QObject* parent_) + : ExtensionPlugin(parent_) + , m_molecule(nullptr) + , m_minimizer(LBFGS) + , m_method(0) // just Lennard-Jones for now + , m_maxSteps(100) + , m_outputFormat(nullptr) +{ + refreshScripts(); + + QAction* action = new QAction(this); + action->setEnabled(true); + action->setText(tr("Optimize")); + action->setData(optimizeAction); + connect(action, SIGNAL(triggered()), SLOT(optimize())); + m_actions.push_back(action); + + action = new QAction(this); + action->setEnabled(true); + action->setText(tr("Energy")); // calculate energy + action->setData(energyAction); + connect(action, SIGNAL(triggered()), SLOT(energy())); + m_actions.push_back(action); + + action = new QAction(this); + action->setEnabled(true); + action->setText(tr("Freeze Atoms")); // calculate energy + action->setData(freezeAction); + connect(action, SIGNAL(triggered()), SLOT(freezeSelected())); + m_actions.push_back(action); +} + +Forcefield::~Forcefield() {} + +QList Forcefield::actions() const +{ + return m_actions; +} + +QStringList Forcefield::menuPath(QAction* action) const +{ + QStringList path; + if (action->data() == optimizeAction) { + // optimize geometry + path << tr("&Extensions"); + return path; + } + path << tr("&Extensions") << tr("Calculate"); + return path; +} + +void Forcefield::setMolecule(QtGui::Molecule* mol) +{ + if (m_molecule == mol) + return; + + m_molecule = mol; + + // @todo set molecule for a calculator +} + +void Forcefield::refreshScripts() +{ + // call the script loader +} + +void Forcefield::optimize() +{ + if (!m_molecule) + return; + + // merge all coordinate updates into one step for undo + bool isInteractive = m_molecule->undoMolecule()->isInteractive(); + m_molecule->undoMolecule()->setInteractive(true); + + //@todo check m_minimizer for method to use + cppoptlib::LbfgsSolver solver; + + int n = m_molecule->atomCount(); + Core::Array pos = m_molecule->atomPositions3d(); + double* p = pos[0].data(); + Eigen::Map map(p, 3 * n); + Eigen::VectorXd positions = map; + + // Create a Criteria class to adjust stopping criteria + cppoptlib::Criteria crit = cppoptlib::Criteria::defaults(); + // @todo allow criteria to be set + crit.iterations = 5; + crit.xDelta = 1.0e-4; // positions converged to 1.0e-4 + crit.fDelta = 1.0e-4; // energy converged to 1.0e-4 + solver.setStopCriteria(crit); // every 5 steps, update coordinates + cppoptlib::Status status = cppoptlib::Status::NotStarted; + + // set the method + //@todo check m_method for a particular calculator + LennardJones lj(this->parent()); + lj.setMolecule(m_molecule); + + for (unsigned int i = 0; i < m_maxSteps / crit.iterations; ++i) { + solver.minimize(lj, positions); + + cppoptlib::Status currentStatus = solver.status(); + if (currentStatus != status || currentStatus == cppoptlib::Status::Continue) { + // status has changed or minimizer hasn't converged + + // update coordinates + const double* d = positions.data(); + // casting would be lovely... + for (size_t i = 0; i < n; ++i) { + pos[i] = Vector3(*(d), *(d + 1), *(d + 2)); + d += 3; + } + // todo - merge these into one undo step + m_molecule->undoMolecule()->setAtomPositions3d(pos, tr("Optimize Geometry")); + Molecule::MoleculeChanges changes = Molecule::Atoms | Molecule::Modified; + m_molecule->emitChanged(changes); + } + } + + qDebug() << " energy: " << lj.value(positions); + + m_molecule->undoMolecule()->setInteractive(isInteractive); +} + +void Forcefield::energy() +{ + if (!m_molecule) + return; + + //@todo check m_method for a particular calculator + LennardJones lj(this->parent()); + lj.setMolecule(m_molecule); + + int n = m_molecule->atomCount(); + Core::Array pos = m_molecule->atomPositions3d(); + double* p = pos[0].data(); + Eigen::Map map(p, 3 * n); + Eigen::VectorXd positions = map; + + QString msg(tr("Energy = %L1 kJ/mol").arg(lj.value(positions))); + QMessageBox::information(nullptr, tr("Avogadro"), msg); +} + +void Forcefield::freezeSelected() +{ + if (!m_molecule) + return; + + int numAtoms = m_molecule->atomCount(); + // now freeze the specified atoms + for (Index i = 0; i < numAtoms; ++i) { + if (m_molecule->atomSelected(i)) { + m_molecule->setAtomFrozen(i, true); + } + } +} + +} // end QtPlugins +} diff --git a/avogadro/qtplugins/forcefield/forcefield.h b/avogadro/qtplugins/forcefield/forcefield.h new file mode 100644 index 0000000000..a96fffaa99 --- /dev/null +++ b/avogadro/qtplugins/forcefield/forcefield.h @@ -0,0 +1,83 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + + This source code is released under the New BSD License, (the "License"). +******************************************************************************/ + +#ifndef AVOGADRO_QTPLUGINS_FORCEFIELD_H +#define AVOGADRO_QTPLUGINS_FORCEFIELD_H + +#include + +#include +#include + +class QAction; +class QDialog; + +namespace Avogadro { +namespace QtPlugins { + +/** + * @brief The Forcefield class implements the extension interface for + * forcefield (and other) optimization + * @author Geoffrey R. Hutchison + */ +class Forcefield : public QtGui::ExtensionPlugin +{ + Q_OBJECT + +public: + enum Minimizer + { + SteepestDescent = 0, + ConjugateGradients, + LBFGS, + FIRE, + }; + + explicit Forcefield(QObject* parent = 0); + ~Forcefield() override; + + QString name() const override { return tr("Forcefield optimization"); } + + QString description() const override + { + return tr("Forcefield minimization, including scripts"); + } + + QList actions() const override; + + QStringList menuPath(QAction*) const override; + + void setMolecule(QtGui::Molecule* mol) override; + +public slots: + /** + * Scan for new scripts in the Forcefield directories. + */ + void refreshScripts(); + +private slots: + void energy(); + void optimize(); + void freezeSelected(); + +private: + QList m_actions; + QtGui::Molecule* m_molecule; + + Minimizer m_minimizer; + unsigned int m_method; + unsigned int m_maxSteps; + + // maps program name --> script file path + QMap m_forcefieldScripts; + + const Io::FileFormat* m_outputFormat; + QString m_tempFileName; +}; +} +} + +#endif // AVOGADRO_QTPLUGINS_FORCEFIELD_H diff --git a/avogadro/qtplugins/forcefield/forcefielddialog.cpp b/avogadro/qtplugins/forcefield/forcefielddialog.cpp new file mode 100644 index 0000000000..1434ecfeb2 --- /dev/null +++ b/avogadro/qtplugins/forcefield/forcefielddialog.cpp @@ -0,0 +1,339 @@ +/****************************************************************************** + + This source file is part of the Avogadro project. + + Copyright 2013 Kitware, Inc. + + This source code is released under the New BSD License, (the "License"). + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +******************************************************************************/ + +#include "obforcefielddialog.h" +#include "ui_obforcefielddialog.h" + +#include +#include +#include +#include + +#include // for log10 + +namespace Avogadro { +namespace QtPlugins { + +enum OptimizationAlgorithm +{ + SteepestDescent = 0, + ConjugateGradient +}; + +enum LineSearchMethod +{ + Simple = 0, + Newton +}; + +OBForceFieldDialog::OBForceFieldDialog(const QStringList& forceFields, + QWidget* parent_) + : QDialog(parent_), ui(new Ui::OBForceFieldDialog) +{ + ui->setupUi(this); + ui->forceField->addItems(forceFields); + updateRecommendedForceField(); + + connect(ui->useRecommended, SIGNAL(toggled(bool)), + SLOT(useRecommendedForceFieldToggled(bool))); + + QSettings settings; + bool autoDetect = + settings.value("openbabel/optimizeGeometry/autoDetect", true).toBool(); + ui->useRecommended->setChecked(autoDetect); +} + +OBForceFieldDialog::~OBForceFieldDialog() +{ + delete ui; +} + +QStringList OBForceFieldDialog::prompt(QWidget* parent_, + const QStringList& forceFields, + const QStringList& startingOptions, + const QString& recommendedForceField_) +{ + OBForceFieldDialog dlg(forceFields, parent_); + dlg.setOptions(startingOptions); + dlg.setRecommendedForceField(recommendedForceField_); + + QStringList options; + if (static_cast(dlg.exec()) == Accepted) + options = dlg.options(); + + return options; +} + +QStringList OBForceFieldDialog::options() const +{ + QStringList opts; + + opts << "--crit" + << QString::number(std::pow(10.0f, ui->energyConv->value()), 'e', 0) + << "--ff" << ui->forceField->currentText() << "--steps" + << QString::number(ui->stepLimit->value()) << "--rvdw" + << QString::number(ui->vdwCutoff->value()) << "--rele" + << QString::number(ui->eleCutoff->value()) << "--freq" + << QString::number(ui->pairFreq->value()); + + switch (static_cast(ui->algorithm->currentIndex())) { + case SteepestDescent: + opts << "--sd"; + break; + default: + case ConjugateGradient: + break; + } + + switch (static_cast(ui->lineSearch->currentIndex())) { + case Newton: + opts << "--newton"; + break; + default: + case Simple: + break; + } + + if (ui->enableCutoffs->isChecked()) + opts << "--cut"; + + return opts; +} + +void OBForceFieldDialog::setOptions(const QStringList& opts) +{ + // Set some defaults. These match the defaults in obabel -L minimize + ui->energyConv->setValue(-6); + ui->algorithm->setCurrentIndex(static_cast(ConjugateGradient)); + ui->lineSearch->setCurrentIndex(static_cast(Simple)); + ui->stepLimit->setValue(2500); + ui->enableCutoffs->setChecked(false); + ui->vdwCutoff->setValue(10.0); + ui->eleCutoff->setValue(10.0); + ui->pairFreq->setValue(10); + + for (QStringList::const_iterator it = opts.constBegin(), + itEnd = opts.constEnd(); + it < itEnd; ++it) { + + // We'll always use log: + if (*it == "--log") { + continue; + } + + // Energy convergence: + else if (*it == "--crit") { + ++it; + if (it == itEnd) { + qWarning() << "OBForceFieldDialog::setOptions: " + "--crit missing argument."; + continue; + } + + bool ok; + float econv = it->toFloat(&ok); + if (!ok) { + qWarning() << "OBForceFieldDialog::setOptions: " + "--crit is not numeric: " + << *it; + continue; + } + + // We just show the econv as 10^(x), so calculate the nearest x + int exponent = static_cast(std::floor(std::log10(econv) + 0.5)); + ui->energyConv->setValue(exponent); + continue; + } + + // Use steepest descent? + else if (*it == "--sd") { + ui->algorithm->setCurrentIndex(SteepestDescent); + continue; + } + + // Use newton linesearch? + else if (*it == "--newton") { + ui->lineSearch->setCurrentIndex(Newton); + continue; + } + + // Force field? + else if (*it == "--ff") { + ++it; + if (it == itEnd) { + qWarning() << "OBForceFieldDialog::setOptions: " + "--ff missing argument."; + continue; + } + + int index = ui->forceField->findText(*it); + if (index < 0) { + qWarning() << "OBForceFieldDialog::setOptions: " + "--ff unknown: " + << *it; + continue; + } + + ui->forceField->setCurrentIndex(index); + continue; + } + + // Step limit? + else if (*it == "--steps") { + ++it; + if (it == itEnd) { + qWarning() << "OBForceFieldDialog::setOptions: " + "--steps missing argument."; + continue; + } + + bool ok; + int numSteps = it->toInt(&ok); + if (!ok) { + qWarning() << "OBForceFieldDialog::setOptions: " + "--steps is not numeric: " + << *it; + continue; + } + + ui->stepLimit->setValue(numSteps); + continue; + } + + // Use cutoff? + else if (*it == "--cut") { + ui->enableCutoffs->setChecked(true); + continue; + } + + // Van der Waals cutoff + else if (*it == "--rvdw") { + ++it; + if (it == itEnd) { + qWarning() << "OBForceFieldDialog::setOptions: " + "--rvdw missing argument."; + continue; + } + + bool ok; + double cutoff = it->toDouble(&ok); + if (!ok) { + qWarning() << "OBForceFieldDialog::setOptions: " + "--rvdw is not numeric: " + << *it; + continue; + } + + ui->vdwCutoff->setValue(cutoff); + continue; + } + + // electrostatic cutoff + else if (*it == "--rele") { + ++it; + if (it == itEnd) { + qWarning() << "OBForceFieldDialog::setOptions: " + "--rele missing argument."; + continue; + } + + bool ok; + double cutoff = it->toDouble(&ok); + if (!ok) { + qWarning() << "OBForceFieldDialog::setOptions: " + "--rele is not numeric: " + << *it; + continue; + } + + ui->eleCutoff->setValue(cutoff); + continue; + } + + // Pair update frequency: + else if (*it == "--freq") { + ++it; + if (it == itEnd) { + qWarning() << "OBForceFieldDialog::setOptions: " + "--freq missing argument."; + continue; + } + + bool ok; + int numSteps = it->toInt(&ok); + if (!ok) { + qWarning() << "OBForceFieldDialog::setOptions: " + "--freq is not numeric: " + << *it; + continue; + } + + ui->pairFreq->setValue(numSteps); + continue; + } + + // ????? + else { + qWarning() << "OBForceFieldDialog::setOptions: " + "Unrecognized option: " + << *it; + } + } +} + +void OBForceFieldDialog::setRecommendedForceField(const QString& rff) +{ + if (rff == m_recommendedForceField) + return; + + if (ui->forceField->findText(rff) == -1) + return; + + m_recommendedForceField = rff; + updateRecommendedForceField(); +} + +void OBForceFieldDialog::useRecommendedForceFieldToggled(bool state) +{ + if (!m_recommendedForceField.isEmpty()) { + if (state) { + int index = ui->forceField->findText(m_recommendedForceField); + if (index >= 0) { + ui->forceField->setCurrentIndex(index); + } + } + } + ui->forceField->setEnabled(!state); + + QSettings().setValue("openbabel/optimizeGeometry/autoDetect", state); +} + +void OBForceFieldDialog::updateRecommendedForceField() +{ + if (m_recommendedForceField.isEmpty()) { + ui->useRecommended->hide(); + ui->forceField->setEnabled(true); + } else { + ui->useRecommended->setText( + tr("Autodetect (%1)").arg(m_recommendedForceField)); + // Force the combo box to update if needed: + useRecommendedForceFieldToggled(ui->useRecommended->isChecked()); + ui->useRecommended->show(); + } +} + +} // namespace QtPlugins +} // namespace Avogadro diff --git a/avogadro/qtplugins/forcefield/forcefielddialog.h b/avogadro/qtplugins/forcefield/forcefielddialog.h new file mode 100644 index 0000000000..546d34e2f1 --- /dev/null +++ b/avogadro/qtplugins/forcefield/forcefielddialog.h @@ -0,0 +1,114 @@ +/****************************************************************************** + + This source file is part of the Avogadro project. + + Copyright 2013 Kitware, Inc. + + This source code is released under the New BSD License, (the "License"). + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +******************************************************************************/ + +#ifndef AVOGADRO_QTPLUGINS_OBFORCEFIELDDIALOG_H +#define AVOGADRO_QTPLUGINS_OBFORCEFIELDDIALOG_H + +#include + +namespace Avogadro { +namespace QtPlugins { + +namespace Ui { +class OBForceFieldDialog; +} + +/** + * @brief The OBForceFieldDialog class is used to prompt the user for parameters + * to be used in an OpenBabel force field optimization. + */ +class OBForceFieldDialog : public QDialog +{ + Q_OBJECT + +public: + /** + * Construct a new dialog using the forcefields in @a forceFields. + */ + explicit OBForceFieldDialog(const QStringList& forceFields, + QWidget* parent_ = 0); + ~OBForceFieldDialog() override; + + /** + * Construct a new dialog using the forcefields in @a forceFields and + * initialize the options to those in @a startingOptions (see setOptions). + * If the user chooses the recommended force field, @a recommendedForceField_ + * will be set. This is useful for preferring a specific force field for a + * particular molecule. + * When the user closes the dialog, the options they selected are returned. If + * the user cancels the dialog, an empty list is returned. + */ + static QStringList prompt(QWidget* parent_, const QStringList& forceFields, + const QStringList& startingOptions, + const QString& recommendedForceField_ = QString()); + + /** + * Get/set the options displayed in the dialog. The option format is a list of + * strings that may be used directly as arguments in a call to + * QProcess::start, with the exception of the `-i`, + * `-o` and `--minimize` options, which are not used by this + * class. See `obabel -L minimize` for a complete listing of available + * options. + * + * Each option (and argument, if applicable) must be a separate string in the + * list. For instance, to refer to the options in the call: +@code +obabel -icml -ocml --minimize --log --crit 1e-05 --ff Ghemical --sd" +@endcode + * + * The option list should contain, in order: + * - `--crit` + * - `1e-05` + * - `--ff` + * - `Ghemical` + * - `--sd` + * + * @note The `--log` option is always added in the list returned by + * options, and is ignored by the setOptions method. + * + * @{ + */ + QStringList options() const; + void setOptions(const QStringList& opts); + /**@}*/ + + /** + * Get/set the recommended forcefield for the current molecule. If an empty + * string, the user will not be shown an option to use the recommended + * forcefield. + * If the string is non-empty (and in the forceFields list passed in the + * constructor), the user will have the option of setting the forcefield to + * this value. + * + * @{ + */ + QString recommendedForceField() const { return m_recommendedForceField; } + void setRecommendedForceField(const QString& rff); + /**@}*/ + +private slots: + void useRecommendedForceFieldToggled(bool state); + +private: + void updateRecommendedForceField(); + + Ui::OBForceFieldDialog* ui; + QString m_recommendedForceField; +}; + +} // namespace QtPlugins +} // namespace Avogadro +#endif // AVOGADRO_QTPLUGINS_OBFORCEFIELDDIALOG_H diff --git a/avogadro/qtplugins/forcefield/forcefielddialog.ui b/avogadro/qtplugins/forcefield/forcefielddialog.ui new file mode 100644 index 0000000000..b4622e8153 --- /dev/null +++ b/avogadro/qtplugins/forcefield/forcefielddialog.ui @@ -0,0 +1,196 @@ + + + Avogadro::QtPlugins::OBForceFieldDialog + + + + 0 + 0 + 357 + 295 + + + + + 0 + 0 + + + + Geometry Optimization Parameters + + + + QLayout::SetFixedSize + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Convergence Criteria + + + + + + "Energy" convergence: + + + + + + + Step limit: + + + + + + + units + + + 10^ + + + -10 + + + 9 + + + -6 + + + + + + + steps + + + 0 + + + 100000 + + + 250 + + + 2500 + + + + + + + + + + Optimization Method + + + + + + Force field: + + + + + + + + + + Autodetect + + + + + + + Qt::Horizontal + + + + + + + Optimization algorithm: + + + + + + + + Steepest Descent + + + + + Conjugate Gradient + + + + + + + + + + + forceField + useRecommended + algorithm + energyConv + stepLimit + buttonBox + + + + + buttonBox + accepted() + Avogadro::QtPlugins::OBForceFieldDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Avogadro::QtPlugins::OBForceFieldDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/avogadro/qtplugins/forcefield/obmmprocess.cpp b/avogadro/qtplugins/forcefield/obmmprocess.cpp new file mode 100644 index 0000000000..5fae2b990f --- /dev/null +++ b/avogadro/qtplugins/forcefield/obmmprocess.cpp @@ -0,0 +1,164 @@ +/****************************************************************************** + + This source file is part of the Avogadro project. + + Copyright 2019 Geoffrey R. Hutchison + + This source code is released under the New BSD License, (the "License"). + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +******************************************************************************/ + +#include "OBMMProcess.h" + +#include +#include +#include +#include +#include +#include + +namespace Avogadro { +namespace QtPlugins { + +OBMMProcess::OBMMProcess(QObject* parent_) + : QObject(parent_), m_processLocked(false), m_aborted(false), + m_process(new QProcess(this)), +#if defined(_WIN32) + m_obmmExecutable("obmm.exe") +#else + m_obmmExecutable("obmm") +#endif +{ + // Read the AVO_OBABEL_EXECUTABLE env var to optionally override the + // executable used. + QByteArray obmmExec = qgetenv("AVO_OBMM_EXECUTABLE"); + if (!obabelExec.isEmpty()) { + m_obmmExecutable = obmmExec; + } else { + // If not overridden, look for an obabel next to the executable. + QDir baseDir(QCoreApplication::applicationDirPath()); + if (!baseDir.absolutePath().startsWith("/usr/") && + QFileInfo(baseDir.absolutePath() + '/' + m_obabelExecutable).exists()) { + m_obabelExecutable = baseDir.absolutePath() + '/' + m_obmmExecutable; + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); +#if defined(_WIN32) + env.insert("BABEL_DATADIR", + QCoreApplication::applicationDirPath() + "/data"); +#else + QDir dir(QCoreApplication::applicationDirPath() + "/../share/openbabel"); + QStringList filters; + filters << "2.*"; + QStringList dirs = dir.entryList(filters); + if (dirs.size() == 1) { + env.insert("BABEL_DATADIR", QCoreApplication::applicationDirPath() + + "/../share/openbabel/" + dirs[0]); + } else { + qDebug() << "Error, Open Babel data directory not found."; + } + dir.setPath(QCoreApplication::applicationDirPath() + "/../lib/openbabel"); + dirs = dir.entryList(filters); + if (dirs.size() == 1) { + env.insert("BABEL_LIBDIR", QCoreApplication::applicationDirPath() + + "/../lib/openbabel/" + dirs[0]); + } else { + qDebug() << "Error, Open Babel plugins directory not found."; + } +#endif + m_process->setProcessEnvironment(env); + } + } +} + +void OBMMProcess::abort() +{ + m_aborted = true; + emit aborted(); +} + +void OBMMProcess::obError() +{ + qDebug() << "Process encountered an error, and did not execute correctly."; + if (m_process) { + qDebug() << "\tExit code:" << m_process->exitCode(); + qDebug() << "\tExit status:" << m_process->exitStatus(); + qDebug() << "\tExit output:" << m_process->readAll(); + } +} + +bool OBMMProcess::queryForceFields() +{ + if (!tryLockProcess()) { + qWarning() << "OBMMProcess::queryForceFields(): process already in use."; + return false; + } + + QStringList options; + options << "-L" + << "forcefields"; + + executeObabel(options, this, SLOT(queryForceFieldsPrepare())); + return true; +} + +void OBMMProcess::queryForceFieldsPrepare() +{ + if (m_aborted) { + releaseProcess(); + return; + } + + QMap result; + + QString output = QString::fromLatin1(m_process->readAllStandardOutput()); + + QRegExp parser("([^\\s]+)\\s+(\\S[^\\n]*[^\\n\\.]+)\\.?\\n"); + int pos = 0; + while ((pos = parser.indexIn(output, pos)) != -1) { + QString key = parser.cap(1); + QString desc = parser.cap(2); + result.insertMulti(key, desc); + pos += parser.matchedLength(); + } + + releaseProcess(); + emit queryForceFieldsFinished(result); +} + +void OBMMProcess::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(error(QProcess::ProcessError)), receiver, slot); + connect(m_process, SIGNAL(error(QProcess::ProcessError)), this, + SLOT(obError())); + } + + // Start process + qDebug() << "OBMMProcess::executeObabel: " + "Running" + << m_obabelExecutable << options.join(" "); + m_process->start(m_obabelExecutable, options); + if (!obabelStdin.isNull()) { + m_process->write(obabelStdin); + m_process->closeWriteChannel(); + } +} + +void OBMMProcess::resetState() +{ + m_aborted = false; + m_process->disconnect(this); + disconnect(m_process); + connect(this, SIGNAL(aborted()), m_process, SLOT(kill())); +} + +} // namespace QtPlugins +} // namespace Avogadro diff --git a/avogadro/qtplugins/forcefield/obmmprocess.h b/avogadro/qtplugins/forcefield/obmmprocess.h new file mode 100644 index 0000000000..87e026d5f3 --- /dev/null +++ b/avogadro/qtplugins/forcefield/obmmprocess.h @@ -0,0 +1,163 @@ +/****************************************************************************** + + This source file is part of the Avogadro project. + + Copyright 2019 Geoffrey R. Hutchison + + This source code is released under the New BSD License, (the "License"). + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +******************************************************************************/ + +#ifndef AVOGADRO_QTPLUGINS_OBMMPROCESS_H +#define AVOGADRO_QTPLUGINS_OBMMPROCESS_H + +#include +#include +#include + +class QProcess; + +namespace Avogadro { +namespace QtPlugins { + +/** + * @brief The OBMMProcess class provides an interface to the `obmm` executable, + * which is run in a separate process. + * + * The `obmm` executable used by this class can be overridden by setting the + * AVO_OBABEL_EXECUTABLE environment variable. + */ +class OBMMProcess : public QObject +{ + Q_OBJECT +public: + explicit OBMMProcess(QObject* parent_ = 0); + + /** + * @name Process Management + * Methods, slots, and signals used to interact with the OpenBabel process. + * @{ + */ +public: + /** + * The `obmm` executable used by the process. + */ + QString obmmExecutable() const { return m_obmmExecutable; } + + /** + * @return True if the process is in use, false otherwise. + */ + bool inUse() const { return m_processLocked; } + +public slots: + /** + * Abort any currently running processes. + * + * This will cause aborted() to be emitted, but not any of the + * operation-specific "finished" signals. + */ + void abort(); + + /** + * Called when an error in the process occurs. + */ + void obError(); + +signals: + /** + * Emitted when the abort() method has been called. + */ + void aborted(); + + // end Process Management doxygen group + /**@}*/ + +public slots: + /** + * Request a list of all supported force fields from obabel. + * + * After calling this method, the queryForceFieldsFinished signal will be + * emitted. This method executes + * + * `obabel -L forcefields` + * + * and parses the output. + * + * If an error occurs, queryReadFormatsFinished will be emitted with an empty + * argument. + * + * @return True if the process started successfully, false otherwise. + */ + bool queryForceFields(); + +signals: + /** + * Triggered when the process started by queryForceFields() completes. + * @param forceFields The force fields supported by OpenBabel. Keys + * are unique identifiers for the force fields, and the values are + * non-translated (english), human-readable descriptions. + * + * If an error occurs, forceFields will be empty. + */ + void queryForceFieldsFinished(const QMap& forceFields); + +private slots: + void queryForceFieldsPrepare(); + + +private: + /** + * Internal method for launching the obmm executable. + * @param options List of options to pass to QProcess::start + * @param receiver A QObject subclass instance that has @a slot as a member. + * @param slot The slot to call when completed. Must have no arguments. + * @param obmmStdin Standard input for the obmm process (optional). + * + * Call this method like so: +@code +QStringList options; + +executeobmm(options, this, SLOT(mySlot())); +@endcode + * + * @a slot will be connected to QProcess::finished(int) and + * QProcess::error(QProcess::ProcessError) with @a receiver as receiver and + * @a m_process as sender. @a m_process is then started using + * m_obmmExecutable and options as arguments. If provided, the obmmStdin + * data will be written to the obmm stdin channel. + */ + void executeobmm(const QStringList& options, QObject* receiver = nullptr, + const char* slot = nullptr, + const QByteArray& obmmStdin = QByteArray()); + + void resetState(); + + // Not thread safe -- just uses a bool. + bool tryLockProcess() + { + if (m_processLocked) + return false; + m_processLocked = true; + resetState(); + return true; + } + + void releaseProcess() { m_processLocked = false; } + + bool m_processLocked; + bool m_aborted; + QProcess* m_process; + QString m_obmmExecutable; + +}; + +} // namespace QtPlugins +} // namespace Avogadro + +#endif // AVOGADRO_QTPLUGINS_OBMMProcess_H From 1fc49797b5bef5a6d12ce77b25c9776c3218b32b Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Mon, 11 Jul 2022 16:53:08 -0400 Subject: [PATCH 03/22] More work Signed-off-by: Geoff Hutchison --- avogadro/calc/CMakeLists.txt | 4 +++ avogadro/calc/energycalculator.cpp | 7 ++--- avogadro/calc/energycalculator.h | 47 ++++++++++++++++++------------ avogadro/calc/lennardjones.cpp | 11 +++---- avogadro/calc/lennardjones.h | 30 ++++++++++--------- 5 files changed, 56 insertions(+), 43 deletions(-) diff --git a/avogadro/calc/CMakeLists.txt b/avogadro/calc/CMakeLists.txt index 0e565e8e34..d009f8f051 100644 --- a/avogadro/calc/CMakeLists.txt +++ b/avogadro/calc/CMakeLists.txt @@ -8,12 +8,16 @@ set(HEADERS chargemodel.h chargemanager.h defaultmodel.h + energycalculator.h + lennardjones.h ) set(SOURCES chargemodel.cpp chargemanager.cpp defaultmodel.cpp + energycalculator.cpp + lennardjones.cpp ) avogadro_add_library(AvogadroCalc ${HEADERS} ${SOURCES}) diff --git a/avogadro/calc/energycalculator.cpp b/avogadro/calc/energycalculator.cpp index 3721b6a0a3..9eac17dd5e 100644 --- a/avogadro/calc/energycalculator.cpp +++ b/avogadro/calc/energycalculator.cpp @@ -1,14 +1,11 @@ /****************************************************************************** This source file is part of the Avogadro project. - - This source code is released under the New BSD License, (the "License"). + This source code is released under the 3-Clause BSD License, (see "LICENSE"). ******************************************************************************/ #include "energycalculator.h" -#include - -namespace Avogadro { +namespace Avogadro::Calc { void EnergyCalculator::gradient(const TVector& x, TVector& grad) { diff --git a/avogadro/calc/energycalculator.h b/avogadro/calc/energycalculator.h index 096a7134fe..9a69f56fad 100644 --- a/avogadro/calc/energycalculator.h +++ b/avogadro/calc/energycalculator.h @@ -1,44 +1,46 @@ /****************************************************************************** This source file is part of the Avogadro project. - - This source code is released under the New BSD License, (the "License"). + This source code is released under the 3-Clause BSD License, (see "LICENSE"). ******************************************************************************/ -#ifndef AVOGADRO_ENERGYCALCULATOR_H -#define AVOGADRO_ENERGYCALCULATOR_H +#ifndef AVOGADRO_CALC_ENERGYCALCULATOR_H +#define AVOGADRO_CALC_ENERGYCALCULATOR_H #include -#include #include namespace Avogadro { -namespace QtGui { +namespace Core { class Molecule; } +namespace Calc { + class EnergyCalculator - : public QObject - , public cppoptlib::Problem + : public cppoptlib::Problem { - Q_OBJECT public: - EnergyCalculator(QObject* parent_ = 0) - : QObject(parent_){}; + EnergyCalculator() {} ~EnergyCalculator() { } + /** + * @return a unique identifier for this calculator. + */ + virtual std::string identifier() const = 0; + /** * @return A short translatable name for this method (e.g., MMFF94, UFF, etc.) */ - virtual QString name() const = 0; + virtual std::string name() const = 0; /** * @return a description of the method */ - virtual QString description() const = 0; + virtual std::string description() const = 0; /** - * Called to set the configuration (e.g., from a GUI options dialog) + * Called to set the configuration (e.g., for a GUI options dialog) */ virtual bool setConfiguration(Core::VariantMap& config) { return true; } @@ -53,19 +55,28 @@ class EnergyCalculator */ void cleanGradients(TVector& grad); + /** + * Freeze a particular atom (e.g., during editing / manipulation) + * @param atomId the atom to freeze + */ void freezeAtom(Index atomId); + + /** + * Unfreeze a particular atom (e.g., during editing / manipulation) + * @param atomId the atom to unfreeze + */ void unfreezeAtom(Index atomId); -public slots: /** * Called when the current molecule changes. */ - virtual void setMolecule(QtGui::Molecule* mol) = 0; + virtual void setMolecule(Core::Molecule* mol) = 0; protected: TVector m_mask; // optimize or frozen atom mask }; -} // namespace Avogadro +} // end namespace Calc +} // end namespace Avogadro -#endif // AVOGADRO_ENERGYCALCULATOR_H +#endif // AVOGADRO_CALC_ENERGYCALCULATOR_H diff --git a/avogadro/calc/lennardjones.cpp b/avogadro/calc/lennardjones.cpp index 13f2f16ac4..2f53e62646 100644 --- a/avogadro/calc/lennardjones.cpp +++ b/avogadro/calc/lennardjones.cpp @@ -1,19 +1,16 @@ /****************************************************************************** This source file is part of the Avogadro project. - - This source code is released under the New BSD License, (the "License"). + This source code is released under the 3-Clause BSD License, (see "LICENSE"). ******************************************************************************/ #include "lennardjones.h" #include -#include - namespace Avogadro { -LennardJones::LennardJones(QObject* parent_) - : EnergyCalculator(parent_) +LennardJones::LennardJones() + : , m_vdw(true) , m_depth(100.0) , m_exponent(6) @@ -21,7 +18,7 @@ LennardJones::LennardJones(QObject* parent_) LennardJones::~LennardJones() {} -void LennardJones::setMolecule(QtGui::Molecule* mol) +void LennardJones::setMolecule(Core::Molecule* mol) { m_molecule = mol; diff --git a/avogadro/calc/lennardjones.h b/avogadro/calc/lennardjones.h index d8acac61e3..fcf2dc0ef2 100644 --- a/avogadro/calc/lennardjones.h +++ b/avogadro/calc/lennardjones.h @@ -1,19 +1,20 @@ /****************************************************************************** This source file is part of the Avogadro project. - - This source code is released under the New BSD License, (the "License"). + This source code is released under the 3-Clause BSD License, (see "LICENSE"). ******************************************************************************/ -#ifndef AVOGADRO_QTGUI_LENNARDJONES_H -#define AVOGADRO_QTGUI_LENNARDJONES_H +#ifndef AVOGADRO_CALC_LENNARDJONES_H +#define AVOGADRO_CALC_LENNARDJONES_H #include "energycalculator.h" namespace Avogadro { -namespace QtGui { +namespace Core { class Molecule; } +namespace Calc { + class LennardJones : public EnergyCalculator { Q_OBJECT @@ -22,30 +23,33 @@ class LennardJones : public EnergyCalculator explicit LennardJones(QObject* parent_ = 0); ~LennardJones(); - virtual QString name() const override - { return tr("Lennard-Jones"); } + virtual std::string identifier() const overrride + { return "LJ"; } + + virtual std::string name() const override + { return "Lennard-Jones"; } - virtual QString description() const override - { return tr("Universal Lennard-Jones potential"); } + virtual std::string description() const override + { return "Universal Lennard-Jones potential"; } virtual Real value(const Eigen::VectorXd& x) override; virtual void gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) override; -public slots: /** * Called when the current molecule changes. */ - virtual void setMolecule(QtGui::Molecule* mol) override; + virtual void setMolecule(Core::Molecule* mol) override; protected: - QtGui::Molecule* m_molecule; + Core::Molecule* m_molecule; Eigen::MatrixXd m_radii; bool m_vdw; Real m_depth; int m_exponent; }; +} // namespace Calc } // namespace Avogadro -#endif // AVOGADRO_QTGUI_LENNARDJONES_H +#endif // AVOGADRO_CALC_LENNARDJONES_H From d193b1f372f3e1303322147cdedc9832292d3115 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Mon, 11 Jul 2022 17:01:05 -0400 Subject: [PATCH 04/22] Add template for FLAME minimizer Minimize molecules using an MD-inspired minimizer: FLAME = "Structural Relaxation Made Simple" Phys Rev Lett 97, 170201 (2006). DOI: 10.1103/PhysRevLett.97.170201 Not sure about masses - paper implies that it sets all to 1.0 amu? Signed-off-by: Geoff Hutchison Signed-off-by: Geoff Hutchison --- avogadro/calc/CMakeLists.txt | 2 + avogadro/calc/flameminimize.cpp | 113 ++++++++++++++++++++++++++++++++ avogadro/calc/flameminimize.h | 49 ++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 avogadro/calc/flameminimize.cpp create mode 100644 avogadro/calc/flameminimize.h diff --git a/avogadro/calc/CMakeLists.txt b/avogadro/calc/CMakeLists.txt index d009f8f051..0cc4931e6d 100644 --- a/avogadro/calc/CMakeLists.txt +++ b/avogadro/calc/CMakeLists.txt @@ -9,6 +9,7 @@ set(HEADERS chargemanager.h defaultmodel.h energycalculator.h + flameminimize.h lennardjones.h ) @@ -17,6 +18,7 @@ set(SOURCES chargemanager.cpp defaultmodel.cpp energycalculator.cpp + flameminimize.cpp lennardjones.cpp ) diff --git a/avogadro/calc/flameminimize.cpp b/avogadro/calc/flameminimize.cpp new file mode 100644 index 0000000000..4cc7e8dd3b --- /dev/null +++ b/avogadro/calc/flameminimize.cpp @@ -0,0 +1,113 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + This source code is released under the 3-Clause BSD License, (see "LICENSE"). +******************************************************************************/ + +#include "flameminimize.h" + +#include + +#include + +namespace Avogadro::Calc { + +FlameMinimize::FlameMinimize() + : m_molecule(nullptr), m_calc(nullptr) +{} + +void FlameMinimize::setMolecule(Core::Molecule* mol) +{ + m_molecule = mol; + + m_forces.resize(3 * mol->atomCount()); + m_forces.setZero(); + m_velocities.resize(3 * mol->atomCount()); + m_velocities.setZero(); + m_accel.resize(3 * mol->atomCount()); + m_accel.setZero(); + + m_invMasses.resize(3 * mol->atomCount()); + m_invMasses.setZero(); + for (unsigned int i = 0; i < mol->atomCount(); ++i) { + //@todo should this be set to 1.0 amu? + double scaledMass = log(Core::Elements::mass(mol->atom(i).atomicNumber())); + + m_invMasses[3 * i] = 1.0 / scaledMass; + m_invMasses[3 * i + 1] = 1.0 / scaledMass; + m_invMasses[3 * i + 2] = 1.0 / scaledMass; + } +} + +bool FlameMinimize::minimize(EnergyCalculator& calc, + Eigen::VectorXd& positions) +{ + if (m_molecule == nullptr) + return false; + + m_calc = &calc; + + //@todo - set convergence criteria (e.g., max steps, min gradients, energy, + // etc.) + + double alpha = 0.1; // start + double deltaT = 0.1 * 1.0e-15; // fs + unsigned int positiveSteps = 0; + + m_forces.setZero(); + m_velocities.setZero(); + m_accel.setZero(); + + for (unsigned int i = 0; i < 20; ++i) { + verletIntegrate(positions, deltaT); + qDebug() << "vvi forces " << m_forces.norm() << " vel " << m_velocities.norm(); + + // Step 1 + double power = m_forces.dot(m_velocities); + + // Step 2 + m_velocities = (1.0 - alpha) * m_velocities + alpha* + m_forces.cwiseProduct(m_velocities.cwiseAbs()); + + if (power > 0.0) { + // Step 3 + positiveSteps++; + if (positiveSteps > 5) { + deltaT = std::min(1.1 * deltaT, 1.0); + alpha = 0.99 * alpha; + } + } else { + // Step 4 + positiveSteps = 0; + deltaT = 0.5 * deltaT; + m_velocities.setZero(); + alpha = 0.1; + } + + double Frms = m_forces.norm() / sqrt(positions.rows()); + if (Frms < 1.0e-5) + break; + } + + return true; +} + +void FlameMinimize::verletIntegrate(Eigen::VectorXd& positions, double deltaT) +{ + // See https://en.wikipedia.org/wiki/Verlet_integration#Velocity_Verlet + // (as one of many examples) + if (m_molecule == nullptr || m_calc == nullptr) + return; + + positions += deltaT * m_velocities + (deltaT * deltaT / 2.0) * m_accel; + m_calc->gradient(positions, m_forces); + m_forces = -1*m_forces; + // F = m * a ==> a = F/m + // use coefficient-wise product from Eigen + // see http://eigen.tuxfamily.org/dox/group__TutorialArrayClass.html + Eigen::VectorXd newAccel(3 * m_molecule->atomCount()); + newAccel = m_forces.cwiseProduct(m_invMasses); + m_velocities += 0.5 * deltaT * (m_accel + newAccel); + m_accel = newAccel; +} + +} // namespace Avogadro diff --git a/avogadro/calc/flameminimize.h b/avogadro/calc/flameminimize.h new file mode 100644 index 0000000000..ddd65daa08 --- /dev/null +++ b/avogadro/calc/flameminimize.h @@ -0,0 +1,49 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + This source code is released under the 3-Clause BSD License, (see "LICENSE"). +******************************************************************************/ + +#ifndef AVOGADRO_CALC_FLAMEMINIMIZE_H +#define AVOGADRO_CALC_FLAMEMINIMIZE_H + +#include + +#include "energycalculator.h" + +namespace Avogadro { +namespace Core { +class Molecule; +} + +class FlameMinimize +{ +public: + FlameMinimize(QObject* parent_ = 0); + ~FlameMinimize() {} + + bool minimize(EnergyCalculator &cal, Eigen::VectorXd &positions); + + // @todo probably want a "take N steps" and a way to set convergence + // (e.g., if the forces/gradients are very small) + // ( might also check if energy changes are v. small) + + /** + * Called when the current molecule changes. + */ + void setMolecule(QtGui::Molecule* mol); + +protected: + + void verletIntegrate(Eigen::VectorXd &positions, double deltaT); + + QtGui::Molecule* m_molecule; + EnergyCalculator* m_calc; + Eigen::VectorXd m_invMasses; + Eigen::VectorXd m_forces; + Eigen::VectorXd m_velocities; + Eigen::VectorXd m_accel; +}; + +} // namespace Avogadro + +#endif // AVOGADRO_FLAMEMINIMIZE_H From 4ffafc46f1f15130cd0c5a410afad73c834ea040 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Mon, 11 Jul 2022 17:37:43 -0400 Subject: [PATCH 05/22] Remove qDebug Signed-off-by: Geoff Hutchison Signed-off-by: Geoff Hutchison --- avogadro/calc/flameminimize.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avogadro/calc/flameminimize.cpp b/avogadro/calc/flameminimize.cpp index 4cc7e8dd3b..7946855088 100644 --- a/avogadro/calc/flameminimize.cpp +++ b/avogadro/calc/flameminimize.cpp @@ -59,7 +59,7 @@ bool FlameMinimize::minimize(EnergyCalculator& calc, for (unsigned int i = 0; i < 20; ++i) { verletIntegrate(positions, deltaT); - qDebug() << "vvi forces " << m_forces.norm() << " vel " << m_velocities.norm(); + //qDebug() << "vvi forces " << m_forces.norm() << " vel " << m_velocities.norm(); // Step 1 double power = m_forces.dot(m_velocities); From ca85bd722e88ff273cde60a6b42db1139fc009f8 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Mon, 11 Jul 2022 20:19:33 -0400 Subject: [PATCH 06/22] Merge Lennard-Jones parameters into a header Signed-off-by: Geoff Hutchison --- .../calc/LennardJones612_UniversalShifted.txt | 198 ------------------ avogadro/calc/lennardjonesdata.h | 198 ++++++++++++++++++ 2 files changed, 198 insertions(+), 198 deletions(-) delete mode 100644 avogadro/calc/LennardJones612_UniversalShifted.txt create mode 100644 avogadro/calc/lennardjonesdata.h diff --git a/avogadro/calc/LennardJones612_UniversalShifted.txt b/avogadro/calc/LennardJones612_UniversalShifted.txt deleted file mode 100644 index 2727fec412..0000000000 --- a/avogadro/calc/LennardJones612_UniversalShifted.txt +++ /dev/null @@ -1,198 +0,0 @@ -# cutoff and sigmas are in units of Angstroms -# epsilon are in units of Electron Volts -#Species_i # Species_j # cutoff # epsilon # sigma -H H 2.2094300 4.4778900 0.5523570 -He He 1.9956100 0.0009421 0.4989030 -Li Li 9.1228000 1.0496900 2.2807000 -Be Be 6.8421000 0.5729420 1.7105300 -B B 6.0581100 2.9670300 1.5145300 -C C 5.4166600 6.3695300 1.3541700 -N N 5.0603000 9.7537900 1.2650800 -O O 4.7039500 5.1264700 1.1759900 -F F 4.0625000 1.6059200 1.0156200 -Ne Ne 4.1337700 0.0036471 1.0334400 -Na Na 11.8311000 0.7367450 2.9577800 -Mg Mg 10.0493000 0.0785788 2.5123300 -Al Al 8.6239000 2.7006700 2.1559700 -Si Si 7.9111800 3.1743100 1.9778000 -P P 7.6260900 5.0305000 1.9065200 -S S 7.4835500 4.3692700 1.8708900 -Cl Cl 7.2697300 4.4832800 1.8174300 -Ar Ar 7.5548200 0.0123529 1.8887100 -K K 14.4682000 0.5517990 3.6170500 -Ca Ca 12.5439000 0.1326790 3.1359600 -Sc Sc 12.1162000 1.6508000 3.0290600 -Ti Ti 11.4035000 1.1802700 2.8508800 -V V 10.9046000 2.7524900 2.7261500 -Cr Cr 9.9067900 1.5367900 2.4767000 -Mn Mn 9.9067900 0.5998880 2.4767000 -Fe Fe 9.4078900 1.1844200 2.3519700 -Co Co 8.9802600 1.2776900 2.2450600 -Ni Ni 8.8377200 2.0757200 2.2094300 -Cu Cu 9.4078900 2.0446300 2.3519700 -Zn Zn 8.6951700 0.1915460 2.1737900 -Ga Ga 8.6951700 1.0642000 2.1737900 -Ge Ge 8.5526300 2.7017100 2.1381600 -As As 8.4813600 3.9599000 2.1203400 -Se Se 8.5526300 3.3867700 2.1381600 -Br Br 8.5526300 1.9706300 2.1381600 -Kr Kr 8.2675400 0.0173276 2.0668900 -Rb Rb 15.6798000 0.4682650 3.9199500 -Sr Sr 13.8980000 0.1339230 3.4745100 -Y Y 13.5417000 2.7597500 3.3854200 -Zr Zr 12.4726000 3.0520100 3.1181500 -Nb Nb 11.6886000 5.2782000 2.9221500 -Mo Mo 10.9759000 4.4749900 2.7439700 -Tc Tc 10.4770000 3.3815900 2.6192400 -Ru Ru 10.4057000 1.9617200 2.6014200 -Rh Rh 10.1206000 2.4058200 2.5301500 -Pd Pd 9.9067900 1.3709700 2.4767000 -Ag Ag 10.3344000 1.6497600 2.5836100 -Cd Cd 10.2632000 0.0377447 2.5657900 -In In 10.1206000 0.8113140 2.5301500 -Sn Sn 9.9067900 1.9005700 2.4767000 -Sb Sb 9.9067900 3.0882800 2.4767000 -Te Te 9.8355200 2.6312300 2.4588800 -I I 9.9067900 1.5393800 2.4767000 -Xe Xe 9.9780700 0.0238880 2.4945200 -Cs Cs 17.3903000 0.4166420 4.3475900 -Ba Ba 15.3235000 1.9000000 3.8308600 -La La 14.7533000 2.4996100 3.6883200 -Ce Ce 14.5395000 2.5700800 3.6348700 -Pr Pr 14.4682000 1.2994600 3.6170500 -Nd Nd 14.3257000 0.8196050 3.5814100 -Pm Pm 14.1831000 3.2413400 3.5457800 -Sm Sm 14.1118000 0.5211220 3.5279600 -Eu Eu 14.1118000 0.4299180 3.5279600 -Gd Gd 13.9693000 2.0995600 3.4923200 -Tb Tb 13.8267000 1.3999900 3.4566900 -Dy Dy 13.6842000 0.6900550 3.4210500 -Ho Ho 13.6842000 0.6900550 3.4210500 -Er Er 13.4704000 0.7387660 3.3676000 -Tm Tm 13.5417000 0.5211220 3.3854200 -Yb Yb 13.3278000 0.1303990 3.3319600 -Lu Lu 13.3278000 1.4331500 3.3319600 -Hf Hf 12.4726000 3.3608600 3.1181500 -Ta Ta 12.1162000 4.0034300 3.0290600 -W W 11.5460000 6.8638900 2.8865100 -Re Re 10.7621000 4.4387100 2.6905100 -Os Os 10.2632000 4.2625300 2.5657900 -Ir Ir 10.0493000 3.7028700 2.5123300 -Pt Pt 9.6929800 3.1401000 2.4232400 -Au Au 9.6929800 2.3058000 2.4232400 -Hg Hg 9.4078900 0.0454140 2.3519700 -Tl Tl 10.3344000 0.5770870 2.5836100 -Pb Pb 10.4057000 0.8589880 2.6014200 -Bi Bi 10.5482000 2.0798700 2.6370600 -Po Po 9.9780700 1.8995300 2.4945200 -At At 10.6908000 1.3854420 2.6727000 -Rn Rn 10.6908000 0.0214992 2.6727000 -Fr Fr 18.5307000 0.3749778 4.6326700 -Ra Ra 15.7511000 1.7100000 3.9377700 -Ac Ac 15.3235000 2.2496490 3.8308600 -Th Th 14.6820000 2.3130720 3.6705000 -Pa Pa 14.2544000 1.1695140 3.5635900 -U U 13.9693000 0.7376445 3.4923200 -Np Np 13.5417000 2.9172060 3.3854200 -Pu Pu 13.3278000 0.4690098 3.3319600 -Am Am 12.8289000 0.3869262 3.2072400 -Cm Cm 12.0450000 1.8896040 3.0112400 -Bk Bk 11.9737000 1.2599910 2.9934200 -Cf Cf 11.9737000 0.6210495 2.9934200 -Es Es 11.7599000 0.6210495 2.9399700 -Fm Fm 11.9024000 0.6648894 2.9756000 -Md Md 12.3300000 0.4690098 3.0825100 -No No 12.5439000 0.1173591 3.1359600 -Lr Lr 11.4748000 1.2898350 2.8686900 -Rf Rf 11.1897000 3.0247740 2.7974200 -Db Db 10.6195000 3.6030870 2.6548800 -Sg Sg 10.1919000 6.1775010 2.5479700 -Bh Bh 10.0493000 3.9948390 2.5123300 -Hs Hs 9.5504300 3.8362770 2.3876100 -Mt Mt 9.1940700 3.3325830 2.2985200 -Ds Ds 9.1228000 2.8260900 2.2807000 -Rg Rg 8.6239000 2.0752200 2.1559700 -Cn Cn 8.6951700 0.0408726 2.1737900 -Nh Nh 9.6929800 0.5193783 2.4232400 -Fl Fl 10.1919000 0.7730892 2.5479700 -Mc Mc 11.5460000 1.8718830 2.8865100 -Lv Lv 12.4726000 1.7095770 3.1181500 -Ts Ts 11.7599000 1.2468978 2.9399700 -Og Og 11.1897000 0.0193493 2.7974200 -electron electron 4.0 1.0 1.0 -user01 user01 4.0 1.0 1.0 -user02 user02 4.0 1.0 1.0 -user03 user03 4.0 1.0 1.0 -user04 user04 4.0 1.0 1.0 -user05 user05 4.0 1.0 1.0 -user06 user06 4.0 1.0 1.0 -user07 user07 4.0 1.0 1.0 -user08 user08 4.0 1.0 1.0 -user09 user09 4.0 1.0 1.0 -user10 user10 4.0 1.0 1.0 -user11 user11 4.0 1.0 1.0 -user12 user12 4.0 1.0 1.0 -user13 user13 4.0 1.0 1.0 -user14 user14 4.0 1.0 1.0 -user15 user15 4.0 1.0 1.0 -user16 user16 4.0 1.0 1.0 -user17 user17 4.0 1.0 1.0 -user18 user18 4.0 1.0 1.0 -user19 user19 4.0 1.0 1.0 -user20 user20 4.0 1.0 1.0 - -# -# CDDL HEADER START -# -# The contents of this file are subject to the terms of the Common Development -# and Distribution License Version 1.0 (the "License"). -# -# You can obtain a copy of the license at -# http://www.opensource.org/licenses/CDDL-1.0. See the License for the -# specific language governing permissions and limitations under the License. -# -# When distributing Covered Code, include this CDDL HEADER in each file and -# include the License file in a prominent location with the name LICENSE.CDDL. -# If applicable, add the following below this CDDL HEADER, with the fields -# enclosed by brackets "[]" replaced with your own identifying information: -# -# Portions Copyright (c) [yyyy] [name of copyright owner]. All rights reserved. -# -# CDDL HEADER END -# - -# -# Copyright (c) 2015, Regents of the University of Minnesota. -# All rights reserved. -# -# Contributors: -# Ryan S. Elliott -# Andrew Akerson -# - - -# * Sigma parameters are set to (2^{-1/6})*r_0, where r_0 is the atomic -# covalent radius. Covalent radii for elements 1--96 were taken from Wolfram -# Mathematica's `ElementData["CovalentRadius"]' command. Covalent radii for -# elements 97--118 were taken from Fig. 3 of the article Pyykko, M. Atsumi, -# J. Chem. Eur. J. 15 (2009) 12770. -# -# * Epsilon parameters are set to the bond dissociation energy. Bond -# dissociation energies for elements 1--55, 57--60, and 61--84 were taken -# from the CRC Handbook of Chemistry and Physics, 91st Edition, -# Ed. W.H. Haynes, 2010. (as posted here: -# http://staff.ustc.edu.cn/~luo971/2010-91-CRC-BDEs-Tables.pdf) -# -# The value (cohesive energy, in this case) for element 56 was obtained from -# p. 50 in Charles Kittel. Introduction to Solid State Physics, 8th -# edition. Hoboken, NJ: John Wiley & Sons, Inc, 2005. -# -# The bond dissociation energy value for element 61 was obtained from -# "Interpolation scheme for the cohesive energies for the lanthanides and -# actinides" Borje Johansson and Anders Rosengren, Phys. Rev. B 11, 1367 -# (1975). -# -# The bond dissociation energies for elements 85--118 were not found in the -# literature. Thus, the values used here are approximated by subtracting 10% -# from the value for the element in the same Group (column) and previous -# Period (row) of the periodic table. diff --git a/avogadro/calc/lennardjonesdata.h b/avogadro/calc/lennardjonesdata.h new file mode 100644 index 0000000000..14aa971f6e --- /dev/null +++ b/avogadro/calc/lennardjonesdata.h @@ -0,0 +1,198 @@ +/***************************************************************************** +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the Common Development +# and Distribution License Version 1.0 (the "License"). +# +# You can obtain a copy of the license at +# http://www.opensource.org/licenses/CDDL-1.0. See the License for the +# specific language governing permissions and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each file and +# include the License file in a prominent location with the name LICENSE.CDDL. +# If applicable, add the following below this CDDL HEADER, with the fields +# enclosed by brackets "[]" replaced with your own identifying information: +# +# Portions Copyright (c) [yyyy] [name of copyright owner]. All rights reserved. +# +# CDDL HEADER END +# + +# +# Copyright (c) 2015, Regents of the University of Minnesota. +# All rights reserved. +# +# Contributors: +# Ryan S. Elliott +# Andrew Akerson +# + +# * Sigma parameters are set to (2^{-1/6})*r_0, where r_0 is the atomic +# covalent radius. Covalent radii for elements 1--96 were taken from Wolfram +# Mathematica's `ElementData["CovalentRadius"]' command. Covalent radii for +# elements 97--118 were taken from Fig. 3 of the article Pyykko, M. Atsumi, +# J. Chem. Eur. J. 15 (2009) 12770. +# +# * Epsilon parameters are set to the bond dissociation energy. Bond +# dissociation energies for elements 1--55, 57--60, and 61--84 were taken +# from the CRC Handbook of Chemistry and Physics, 91st Edition, +# Ed. W.H. Haynes, 2010. (as posted here: +# http://staff.ustc.edu.cn/~luo971/2010-91-CRC-BDEs-Tables.pdf) +# +# The value (cohesive energy, in this case) for element 56 was obtained from +# p. 50 in Charles Kittel. Introduction to Solid State Physics, 8th +# edition. Hoboken, NJ: John Wiley & Sons, Inc, 2005. +# +# The bond dissociation energy value for element 61 was obtained from +# "Interpolation scheme for the cohesive energies for the lanthanides and +# actinides" Borje Johansson and Anders Rosengren, Phys. Rev. B 11, 1367 +# (1975). +# +# The bond dissociation energies for elements 85--118 were not found in the +# literature. Thus, the values used here are approximated by subtracting 10% +# from the value for the element in the same Group (column) and previous +# Period (row) of the periodic table. +*****************************************************************************/ + +// Formatted as a C / C++ header for Avogadro +// from OpenKIM LennardJones612_UniversalShifted.txt +// https://doi.org/10.25950/962b4967 + +// This is a 'universal' parameterization for the LennardJones612 model driver +// for all species types supported by the KIM API. The parameterization uses a +// shifted cutoff so that all interactions have a continuous energy function at +// the cutoff radius. + +// This model was automatically fit using Lorentz-Berthelot mixing rules. It +// reproduces the dimer equilibrium separation (covalent radii) and the bond +// dissociation energies. It has not been fitted to other physical properties +// and its ability to model structures other than dimers is unknown. + +#ifndef AVOGADRO_CALC_LENNARDJONES_DATA +#define AVOGADRO_CALC_LENNARDJONES_DATA + +// values are # cutoff # epsilon # sigma +// cutoff and sigmas are in units of Angstroms +// epsilon are in units of Electron Volts + +float ljparams[][3] = { { 4.0, 1.0, 1.0 }, // dummy + { 2.2094300, 4.4778900, 0.5523570 }, // hydrogen + { 1.9956100, 0.0009421, 0.4989030 }, + { 9.1228000, 1.0496900, 2.2807000 }, + { 6.8421000, 0.5729420, 1.7105300 }, + { 6.0581100, 2.9670300, 1.5145300 }, + { 5.4166600, 6.3695300, 1.3541700 }, + { 5.0603000, 9.7537900, 1.2650800 }, + { 4.7039500, 5.1264700, 1.1759900 }, + { 4.0625000, 1.6059200, 1.0156200 }, + { 4.1337700, 0.0036471, 1.0334400 }, + { 11.8311000, 0.7367450, 2.9577800 }, + { 10.0493000, 0.0785788, 2.5123300 }, + { 8.6239000, 2.7006700, 2.1559700 }, + { 7.9111800, 3.1743100, 1.9778000 }, + { 7.6260900, 5.0305000, 1.9065200 }, + { 7.4835500, 4.3692700, 1.8708900 }, + { 7.2697300, 4.4832800, 1.8174300 }, + { 7.5548200, 0.0123529, 1.8887100 }, + { 14.4682000, 0.5517990, 3.6170500 }, + { 12.5439000, 0.1326790, 3.1359600 }, + { 12.1162000, 1.6508000, 3.0290600 }, + { 11.4035000, 1.1802700, 2.8508800 }, + { 10.9046000, 2.7524900, 2.7261500 }, + { 9.9067900, 1.5367900, 2.4767000 }, + { 9.9067900, 0.5998880, 2.4767000 }, + { 9.4078900, 1.1844200, 2.3519700 }, + { 8.9802600, 1.2776900, 2.2450600 }, + { 8.8377200, 2.0757200, 2.2094300 }, + { 9.4078900, 2.0446300, 2.3519700 }, + { 8.6951700, 0.1915460, 2.1737900 }, + { 8.6951700, 1.0642000, 2.1737900 }, + { 8.5526300, 2.7017100, 2.1381600 }, + { 8.4813600, 3.9599000, 2.1203400 }, + { 8.5526300, 3.3867700, 2.1381600 }, + { 8.5526300, 1.9706300, 2.1381600 }, + { 8.2675400, 0.0173276, 2.0668900 }, + { 15.6798000, 0.4682650, 3.9199500 }, + { 13.8980000, 0.1339230, 3.4745100 }, + { 13.5417000, 2.7597500, 3.3854200 }, + { 12.4726000, 3.0520100, 3.1181500 }, + { 11.6886000, 5.2782000, 2.9221500 }, + { 10.9759000, 4.4749900, 2.7439700 }, + { 10.4770000, 3.3815900, 2.6192400 }, + { 10.4057000, 1.9617200, 2.6014200 }, + { 10.1206000, 2.4058200, 2.5301500 }, + { 9.9067900, 1.3709700, 2.4767000 }, + { 10.3344000, 1.6497600, 2.5836100 }, + { 10.2632000, 0.0377447, 2.5657900 }, + { 10.1206000, 0.8113140, 2.5301500 }, + { 9.9067900, 1.9005700, 2.4767000 }, + { 9.9067900, 3.0882800, 2.4767000 }, + { 9.8355200, 2.6312300, 2.4588800 }, + { 9.9067900, 1.5393800, 2.4767000 }, + { 9.9780700, 0.0238880, 2.4945200 }, + { 17.3903000, 0.4166420, 4.3475900 }, + { 15.3235000, 1.9000000, 3.8308600 }, + { 14.7533000, 2.4996100, 3.6883200 }, + { 14.5395000, 2.5700800, 3.6348700 }, + { 14.4682000, 1.2994600, 3.6170500 }, + { 14.3257000, 0.8196050, 3.5814100 }, + { 14.1831000, 3.2413400, 3.5457800 }, + { 14.1118000, 0.5211220, 3.5279600 }, + { 14.1118000, 0.4299180, 3.5279600 }, + { 13.9693000, 2.0995600, 3.4923200 }, + { 13.8267000, 1.3999900, 3.4566900 }, + { 13.6842000, 0.6900550, 3.4210500 }, + { 13.6842000, 0.6900550, 3.4210500 }, + { 13.4704000, 0.7387660, 3.3676000 }, + { 13.5417000, 0.5211220, 3.3854200 }, + { 13.3278000, 0.1303990, 3.3319600 }, + { 13.3278000, 1.4331500, 3.3319600 }, + { 12.4726000, 3.3608600, 3.1181500 }, + { 12.1162000, 4.0034300, 3.0290600 }, + { 11.5460000, 6.8638900, 2.8865100 }, + { 10.7621000, 4.4387100, 2.6905100 }, + { 10.2632000, 4.2625300, 2.5657900 }, + { 10.0493000, 3.7028700, 2.5123300 }, + { 9.6929800, 3.1401000, 2.4232400 }, + { 9.6929800, 2.3058000, 2.4232400 }, + { 9.4078900, 0.0454140, 2.3519700 }, + { 10.3344000, 0.5770870, 2.5836100 }, + { 10.4057000, 0.8589880, 2.6014200 }, + { 10.5482000, 2.0798700, 2.6370600 }, + { 9.9780700, 1.8995300, 2.4945200 }, + { 10.6908000, 1.3854420, 2.6727000 }, + { 10.6908000, 0.0214992, 2.6727000 }, + { 18.5307000, 0.3749778, 4.6326700 }, + { 15.7511000, 1.7100000, 3.9377700 }, + { 15.3235000, 2.2496490, 3.8308600 }, + { 14.6820000, 2.3130720, 3.6705000 }, + { 14.2544000, 1.1695140, 3.5635900 }, + { 13.9693000, 0.7376445, 3.4923200 }, + { 13.5417000, 2.9172060, 3.3854200 }, + { 13.3278000, 0.4690098, 3.3319600 }, + { 12.8289000, 0.3869262, 3.2072400 }, + { 12.0450000, 1.8896040, 3.0112400 }, + { 11.9737000, 1.2599910, 2.9934200 }, + { 11.9737000, 0.6210495, 2.9934200 }, + { 11.7599000, 0.6210495, 2.9399700 }, + { 11.9024000, 0.6648894, 2.9756000 }, + { 12.3300000, 0.4690098, 3.0825100 }, + { 12.5439000, 0.1173591, 3.1359600 }, + { 11.4748000, 1.2898350, 2.8686900 }, + { 11.1897000, 3.0247740, 2.7974200 }, + { 10.6195000, 3.6030870, 2.6548800 }, + { 10.1919000, 6.1775010, 2.5479700 }, + { 10.0493000, 3.9948390, 2.5123300 }, + { 9.5504300, 3.8362770, 2.3876100 }, + { 9.1940700, 3.3325830, 2.2985200 }, + { 9.1228000, 2.8260900, 2.2807000 }, + { 8.6239000, 2.0752200, 2.1559700 }, + { 8.6951700, 0.0408726, 2.1737900 }, + { 9.6929800, 0.5193783, 2.4232400 }, + { 10.1919000, 0.7730892, 2.5479700 }, + { 11.5460000, 1.8718830, 2.8865100 }, + { 12.4726000, 1.7095770, 3.1181500 }, + { 11.7599000, 1.2468978, 2.9399700 }, + { 11.1897000, 0.0193493, 2.7974200 } }; + +#endif // AVOGADRO_CALC_LENNARDJONES_DATA_H \ No newline at end of file From a374e03b8abf88f660a0878be3d9e49f045808c5 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Fri, 2 Sep 2022 16:16:07 -0400 Subject: [PATCH 07/22] Compiling Signed-off-by: Geoff Hutchison --- avogadro/calc/CMakeLists.txt | 4 +--- avogadro/calc/energycalculator.h | 1 + avogadro/calc/lennardjones.cpp | 26 ++++---------------------- avogadro/calc/lennardjones.h | 6 ++---- 4 files changed, 8 insertions(+), 29 deletions(-) diff --git a/avogadro/calc/CMakeLists.txt b/avogadro/calc/CMakeLists.txt index 0cc4931e6d..1cb61766cb 100644 --- a/avogadro/calc/CMakeLists.txt +++ b/avogadro/calc/CMakeLists.txt @@ -2,14 +2,13 @@ find_package(Eigen3 REQUIRED) # Add as "system headers" to avoid warnings generated by them with # compilers that support that notion. -include_directories(SYSTEM "${EIGEN3_INCLUDE_DIR}") +include_directories(SYSTEM "${EIGEN3_INCLUDE_DIR}" "${AvogadroLibs_SOURCE_DIR}/thirdparty") set(HEADERS chargemodel.h chargemanager.h defaultmodel.h energycalculator.h - flameminimize.h lennardjones.h ) @@ -18,7 +17,6 @@ set(SOURCES chargemanager.cpp defaultmodel.cpp energycalculator.cpp - flameminimize.cpp lennardjones.cpp ) diff --git a/avogadro/calc/energycalculator.h b/avogadro/calc/energycalculator.h index 9a69f56fad..7041f6e381 100644 --- a/avogadro/calc/energycalculator.h +++ b/avogadro/calc/energycalculator.h @@ -7,6 +7,7 @@ #define AVOGADRO_CALC_ENERGYCALCULATOR_H #include +#include #include diff --git a/avogadro/calc/lennardjones.cpp b/avogadro/calc/lennardjones.cpp index 2f53e62646..b4d0c1f8ab 100644 --- a/avogadro/calc/lennardjones.cpp +++ b/avogadro/calc/lennardjones.cpp @@ -6,15 +6,11 @@ #include "lennardjones.h" #include +#include -namespace Avogadro { +namespace Avogadro::Calc { -LennardJones::LennardJones() - : - , m_vdw(true) - , m_depth(100.0) - , m_exponent(6) -{} +LennardJones::LennardJones() : m_vdw(true), m_depth(100.0), m_exponent(6) {} LennardJones::~LennardJones() {} @@ -31,20 +27,6 @@ void LennardJones::setMolecule(Core::Molecule* mol) m_radii.setZero(); Eigen::MatrixXd radii(numAtoms, numAtoms); - // handle the frozen atoms - // set a clean mask (everything can move) - m_mask = Eigen::MatrixXd::Constant(numAtoms * 3, 1, 1.0); - - // now freeze the specified atoms - for (Index i = 0; i < numAtoms; ++i) { - if (mol->atomFrozen(i)) { - // zero out the gradients for these atoms - m_mask[i*3] = 0.0; - m_mask[i*3+1] = 0.0; - m_mask[i*3+2] = 0.0; - } - } - for (Index i = 0; i < numAtoms; ++i) { Core::Atom atom1 = mol->atom(i); unsigned char number1 = atom1.atomicNumber(); @@ -138,4 +120,4 @@ void LennardJones::gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) cleanGradients(grad); } -} // namespace Avogadro +} // namespace Avogadro::Calc diff --git a/avogadro/calc/lennardjones.h b/avogadro/calc/lennardjones.h index fcf2dc0ef2..e65c19ed92 100644 --- a/avogadro/calc/lennardjones.h +++ b/avogadro/calc/lennardjones.h @@ -17,13 +17,11 @@ namespace Calc { class LennardJones : public EnergyCalculator { - Q_OBJECT - public: - explicit LennardJones(QObject* parent_ = 0); + LennardJones(); ~LennardJones(); - virtual std::string identifier() const overrride + virtual std::string identifier() const override { return "LJ"; } virtual std::string name() const override From 6897bb9de226188c338d059db7499a98ea5679a5 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Sat, 17 Sep 2022 15:52:51 -0400 Subject: [PATCH 08/22] Changeup freeze atoms (now in constraint class) for mask Signed-off-by: Geoff Hutchison --- avogadro/calc/energycalculator.h | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/avogadro/calc/energycalculator.h b/avogadro/calc/energycalculator.h index 7041f6e381..5c6cffa9d2 100644 --- a/avogadro/calc/energycalculator.h +++ b/avogadro/calc/energycalculator.h @@ -57,16 +57,14 @@ class EnergyCalculator void cleanGradients(TVector& grad); /** - * Freeze a particular atom (e.g., during editing / manipulation) - * @param atomId the atom to freeze + * Called to update the "frozen" mask (e.g., during editing) */ - void freezeAtom(Index atomId); + void setMask(TVector mask) { m_mask = mask; } /** - * Unfreeze a particular atom (e.g., during editing / manipulation) - * @param atomId the atom to unfreeze + * @return the frozen atoms mask */ - void unfreezeAtom(Index atomId); + TVector mask() const { return m_mask; } /** * Called when the current molecule changes. From cab91f371372ecb3d0c6ca6cbbc17e92839a02fd Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Mon, 4 Sep 2023 16:29:15 -0400 Subject: [PATCH 09/22] Move thirdparty headers to sensible paths Signed-off-by: Geoff Hutchison --- avogadro/calc/CMakeLists.txt | 2 +- thirdparty/CMakeLists.txt | 8 +++++++- thirdparty/nlohmann/{nlohmann => }/adl_serializer.hpp | 0 .../{nlohmann => }/detail/conversions/from_json.hpp | 0 .../{nlohmann => }/detail/conversions/to_chars.hpp | 0 .../{nlohmann => }/detail/conversions/to_json.hpp | 0 thirdparty/nlohmann/{nlohmann => }/detail/exceptions.hpp | 0 .../{nlohmann => }/detail/input/binary_reader.hpp | 0 .../{nlohmann => }/detail/input/input_adapters.hpp | 0 .../nlohmann/{nlohmann => }/detail/input/json_sax.hpp | 0 thirdparty/nlohmann/{nlohmann => }/detail/input/lexer.hpp | 0 .../nlohmann/{nlohmann => }/detail/input/parser.hpp | 0 .../{nlohmann => }/detail/iterators/internal_iterator.hpp | 0 .../{nlohmann => }/detail/iterators/iter_impl.hpp | 0 .../{nlohmann => }/detail/iterators/iteration_proxy.hpp | 0 .../detail/iterators/json_reverse_iterator.hpp | 0 .../detail/iterators/primitive_iterator.hpp | 0 .../nlohmann/{nlohmann => }/detail/json_pointer.hpp | 0 thirdparty/nlohmann/{nlohmann => }/detail/json_ref.hpp | 0 thirdparty/nlohmann/{nlohmann => }/detail/macro_scope.hpp | 0 .../nlohmann/{nlohmann => }/detail/macro_unscope.hpp | 0 .../nlohmann/{nlohmann => }/detail/meta/cpp_future.hpp | 0 .../nlohmann/{nlohmann => }/detail/meta/detected.hpp | 0 thirdparty/nlohmann/{nlohmann => }/detail/meta/is_sax.hpp | 0 .../nlohmann/{nlohmann => }/detail/meta/type_traits.hpp | 0 thirdparty/nlohmann/{nlohmann => }/detail/meta/void_t.hpp | 0 .../{nlohmann => }/detail/output/binary_writer.hpp | 0 .../{nlohmann => }/detail/output/output_adapters.hpp | 0 .../nlohmann/{nlohmann => }/detail/output/serializer.hpp | 0 thirdparty/nlohmann/{nlohmann => }/detail/value_t.hpp | 0 thirdparty/nlohmann/{nlohmann => }/json.hpp | 0 thirdparty/nlohmann/{nlohmann => }/json_fwd.hpp | 0 32 files changed, 8 insertions(+), 2 deletions(-) rename thirdparty/nlohmann/{nlohmann => }/adl_serializer.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/conversions/from_json.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/conversions/to_chars.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/conversions/to_json.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/exceptions.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/input/binary_reader.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/input/input_adapters.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/input/json_sax.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/input/lexer.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/input/parser.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/iterators/internal_iterator.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/iterators/iter_impl.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/iterators/iteration_proxy.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/iterators/json_reverse_iterator.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/iterators/primitive_iterator.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/json_pointer.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/json_ref.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/macro_scope.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/macro_unscope.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/meta/cpp_future.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/meta/detected.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/meta/is_sax.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/meta/type_traits.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/meta/void_t.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/output/binary_writer.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/output/output_adapters.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/output/serializer.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/detail/value_t.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/json.hpp (100%) rename thirdparty/nlohmann/{nlohmann => }/json_fwd.hpp (100%) diff --git a/avogadro/calc/CMakeLists.txt b/avogadro/calc/CMakeLists.txt index 8594b5f174..12b5d06a0e 100644 --- a/avogadro/calc/CMakeLists.txt +++ b/avogadro/calc/CMakeLists.txt @@ -19,4 +19,4 @@ target_sources(Calc PRIVATE avogadro_add_library(Calc) target_link_libraries(Calc - PUBLIC Avogadro::Core) + PUBLIC Avogadro::Core cppoptlib) diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index 14b5bf495d..1513781763 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -14,7 +14,7 @@ if(USE_EXTERNAL_NLOHMANN) else() add_library(nlohmann_json INTERFACE IMPORTED GLOBAL) target_include_directories(nlohmann_json - INTERFACE $) + INTERFACE $) add_library(nlohmann_json::nlohmann_json ALIAS nlohmann_json) endif(USE_EXTERNAL_NLOHMANN) @@ -42,6 +42,12 @@ else() unset(_STRUCT_ROOT) endif(USE_EXTERNAL_STRUCT) +if(NOT TARGET cppoptlib) + add_library(cppoptlib INTERFACE IMPORTED GLOBAL) + target_include_directories(cppoptlib INTERFACE + $) +endif(NOT TARGET cppoptlib) + if(NOT TARGET tinycolormap) add_library(tinycolormap INTERFACE IMPORTED GLOBAL) target_include_directories(tinycolormap INTERFACE diff --git a/thirdparty/nlohmann/nlohmann/adl_serializer.hpp b/thirdparty/nlohmann/adl_serializer.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/adl_serializer.hpp rename to thirdparty/nlohmann/adl_serializer.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/conversions/from_json.hpp b/thirdparty/nlohmann/detail/conversions/from_json.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/conversions/from_json.hpp rename to thirdparty/nlohmann/detail/conversions/from_json.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/conversions/to_chars.hpp b/thirdparty/nlohmann/detail/conversions/to_chars.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/conversions/to_chars.hpp rename to thirdparty/nlohmann/detail/conversions/to_chars.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/conversions/to_json.hpp b/thirdparty/nlohmann/detail/conversions/to_json.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/conversions/to_json.hpp rename to thirdparty/nlohmann/detail/conversions/to_json.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/exceptions.hpp b/thirdparty/nlohmann/detail/exceptions.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/exceptions.hpp rename to thirdparty/nlohmann/detail/exceptions.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/input/binary_reader.hpp b/thirdparty/nlohmann/detail/input/binary_reader.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/input/binary_reader.hpp rename to thirdparty/nlohmann/detail/input/binary_reader.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/input/input_adapters.hpp b/thirdparty/nlohmann/detail/input/input_adapters.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/input/input_adapters.hpp rename to thirdparty/nlohmann/detail/input/input_adapters.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/input/json_sax.hpp b/thirdparty/nlohmann/detail/input/json_sax.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/input/json_sax.hpp rename to thirdparty/nlohmann/detail/input/json_sax.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/input/lexer.hpp b/thirdparty/nlohmann/detail/input/lexer.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/input/lexer.hpp rename to thirdparty/nlohmann/detail/input/lexer.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/input/parser.hpp b/thirdparty/nlohmann/detail/input/parser.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/input/parser.hpp rename to thirdparty/nlohmann/detail/input/parser.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/iterators/internal_iterator.hpp b/thirdparty/nlohmann/detail/iterators/internal_iterator.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/iterators/internal_iterator.hpp rename to thirdparty/nlohmann/detail/iterators/internal_iterator.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/iterators/iter_impl.hpp b/thirdparty/nlohmann/detail/iterators/iter_impl.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/iterators/iter_impl.hpp rename to thirdparty/nlohmann/detail/iterators/iter_impl.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/iterators/iteration_proxy.hpp b/thirdparty/nlohmann/detail/iterators/iteration_proxy.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/iterators/iteration_proxy.hpp rename to thirdparty/nlohmann/detail/iterators/iteration_proxy.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/iterators/json_reverse_iterator.hpp b/thirdparty/nlohmann/detail/iterators/json_reverse_iterator.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/iterators/json_reverse_iterator.hpp rename to thirdparty/nlohmann/detail/iterators/json_reverse_iterator.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/iterators/primitive_iterator.hpp b/thirdparty/nlohmann/detail/iterators/primitive_iterator.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/iterators/primitive_iterator.hpp rename to thirdparty/nlohmann/detail/iterators/primitive_iterator.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/json_pointer.hpp b/thirdparty/nlohmann/detail/json_pointer.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/json_pointer.hpp rename to thirdparty/nlohmann/detail/json_pointer.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/json_ref.hpp b/thirdparty/nlohmann/detail/json_ref.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/json_ref.hpp rename to thirdparty/nlohmann/detail/json_ref.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/macro_scope.hpp b/thirdparty/nlohmann/detail/macro_scope.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/macro_scope.hpp rename to thirdparty/nlohmann/detail/macro_scope.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/macro_unscope.hpp b/thirdparty/nlohmann/detail/macro_unscope.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/macro_unscope.hpp rename to thirdparty/nlohmann/detail/macro_unscope.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/meta/cpp_future.hpp b/thirdparty/nlohmann/detail/meta/cpp_future.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/meta/cpp_future.hpp rename to thirdparty/nlohmann/detail/meta/cpp_future.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/meta/detected.hpp b/thirdparty/nlohmann/detail/meta/detected.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/meta/detected.hpp rename to thirdparty/nlohmann/detail/meta/detected.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/meta/is_sax.hpp b/thirdparty/nlohmann/detail/meta/is_sax.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/meta/is_sax.hpp rename to thirdparty/nlohmann/detail/meta/is_sax.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/meta/type_traits.hpp b/thirdparty/nlohmann/detail/meta/type_traits.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/meta/type_traits.hpp rename to thirdparty/nlohmann/detail/meta/type_traits.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/meta/void_t.hpp b/thirdparty/nlohmann/detail/meta/void_t.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/meta/void_t.hpp rename to thirdparty/nlohmann/detail/meta/void_t.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/output/binary_writer.hpp b/thirdparty/nlohmann/detail/output/binary_writer.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/output/binary_writer.hpp rename to thirdparty/nlohmann/detail/output/binary_writer.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/output/output_adapters.hpp b/thirdparty/nlohmann/detail/output/output_adapters.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/output/output_adapters.hpp rename to thirdparty/nlohmann/detail/output/output_adapters.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/output/serializer.hpp b/thirdparty/nlohmann/detail/output/serializer.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/output/serializer.hpp rename to thirdparty/nlohmann/detail/output/serializer.hpp diff --git a/thirdparty/nlohmann/nlohmann/detail/value_t.hpp b/thirdparty/nlohmann/detail/value_t.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/detail/value_t.hpp rename to thirdparty/nlohmann/detail/value_t.hpp diff --git a/thirdparty/nlohmann/nlohmann/json.hpp b/thirdparty/nlohmann/json.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/json.hpp rename to thirdparty/nlohmann/json.hpp diff --git a/thirdparty/nlohmann/nlohmann/json_fwd.hpp b/thirdparty/nlohmann/json_fwd.hpp similarity index 100% rename from thirdparty/nlohmann/nlohmann/json_fwd.hpp rename to thirdparty/nlohmann/json_fwd.hpp From de2c387e2e79d4b6d911fe6a637f6a1cba5aae7a Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Mon, 4 Sep 2023 16:29:33 -0400 Subject: [PATCH 10/22] Fix freeze / unfreeze compilation Signed-off-by: Geoff Hutchison --- avogadro/calc/energycalculator.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/avogadro/calc/energycalculator.h b/avogadro/calc/energycalculator.h index 5c6cffa9d2..71ee281632 100644 --- a/avogadro/calc/energycalculator.h +++ b/avogadro/calc/energycalculator.h @@ -6,8 +6,8 @@ #ifndef AVOGADRO_CALC_ENERGYCALCULATOR_H #define AVOGADRO_CALC_ENERGYCALCULATOR_H -#include #include +#include #include @@ -18,12 +18,11 @@ class Molecule; namespace Calc { -class EnergyCalculator - : public cppoptlib::Problem +class EnergyCalculator : public cppoptlib::Problem { public: EnergyCalculator() {} - ~EnergyCalculator() { } + ~EnergyCalculator() {} /** * @return a unique identifier for this calculator. @@ -66,13 +65,16 @@ class EnergyCalculator */ TVector mask() const { return m_mask; } + void freezeAtom(Index atomId); + void unfreezeAtom(Index atomId); + /** * Called when the current molecule changes. */ virtual void setMolecule(Core::Molecule* mol) = 0; protected: - TVector m_mask; // optimize or frozen atom mask + TVector m_mask; // optimize or frozen atom mask }; } // end namespace Calc From 0dc412a36b61d3e5761d046a4fd982a39910e429 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Tue, 5 Sep 2023 14:54:15 -0400 Subject: [PATCH 11/22] More fixes Signed-off-by: Geoff Hutchison --- avogadro/qtplugins/CMakeLists.txt | 1 + avogadro/qtplugins/forcefield/CMakeLists.txt | 4 +--- avogadro/qtplugins/forcefield/forcefield.cpp | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/avogadro/qtplugins/CMakeLists.txt b/avogadro/qtplugins/CMakeLists.txt index 49d7c5223b..b1054dd1b3 100644 --- a/avogadro/qtplugins/CMakeLists.txt +++ b/avogadro/qtplugins/CMakeLists.txt @@ -107,6 +107,7 @@ add_subdirectory(customelements) add_subdirectory(editor) add_subdirectory(fetchpdb) add_subdirectory(focus) +add_subdirectory(forcefield) add_subdirectory(hydrogens) add_subdirectory(importpqr) add_subdirectory(insertdna) diff --git a/avogadro/qtplugins/forcefield/CMakeLists.txt b/avogadro/qtplugins/forcefield/CMakeLists.txt index 218d7a0825..e4d29784d2 100644 --- a/avogadro/qtplugins/forcefield/CMakeLists.txt +++ b/avogadro/qtplugins/forcefield/CMakeLists.txt @@ -7,9 +7,7 @@ include_directories(SYSTEM "${EIGEN3_INCLUDE_DIR}" # Extension set(forcefield_srcs - energycalculator.cpp forcefield.cpp - lennardjones.cpp ) avogadro_plugin(Forcefield @@ -20,7 +18,7 @@ avogadro_plugin(Forcefield "${forcefield_srcs}" ) -target_link_libraries(Forcefield LINK_PRIVATE AvogadroIO) +target_link_libraries(Forcefield LINK_PRIVATE AvogadroIO AvogadroCalc) # Bundled forcefield scripts (open babel) set(forcefields diff --git a/avogadro/qtplugins/forcefield/forcefield.cpp b/avogadro/qtplugins/forcefield/forcefield.cpp index 4f471658af..873a7609b0 100644 --- a/avogadro/qtplugins/forcefield/forcefield.cpp +++ b/avogadro/qtplugins/forcefield/forcefield.cpp @@ -23,7 +23,7 @@ #include #include -#include "lennardjones.h" +#include #include #include From f33a553a123342d599db36055c91d7aa3775cf66 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Tue, 10 Oct 2023 20:21:28 -0400 Subject: [PATCH 12/22] Compiles Signed-off-by: Geoff Hutchison --- avogadro/calc/energycalculator.h | 4 +++- avogadro/calc/lennardjones.h | 6 ++++-- avogadro/qtplugins/CMakeLists.txt | 2 +- avogadro/qtplugins/forcefield/CMakeLists.txt | 2 +- avogadro/qtplugins/forcefield/forcefield.cpp | 9 ++++++--- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/avogadro/calc/energycalculator.h b/avogadro/calc/energycalculator.h index 71ee281632..8b65d53345 100644 --- a/avogadro/calc/energycalculator.h +++ b/avogadro/calc/energycalculator.h @@ -6,6 +6,8 @@ #ifndef AVOGADRO_CALC_ENERGYCALCULATOR_H #define AVOGADRO_CALC_ENERGYCALCULATOR_H +#include "avogadrocalcexport.h" + #include #include @@ -18,7 +20,7 @@ class Molecule; namespace Calc { -class EnergyCalculator : public cppoptlib::Problem +class AVOGADROCALC_EXPORT EnergyCalculator : public cppoptlib::Problem { public: EnergyCalculator() {} diff --git a/avogadro/calc/lennardjones.h b/avogadro/calc/lennardjones.h index e65c19ed92..d54f9151f6 100644 --- a/avogadro/calc/lennardjones.h +++ b/avogadro/calc/lennardjones.h @@ -6,7 +6,9 @@ #ifndef AVOGADRO_CALC_LENNARDJONES_H #define AVOGADRO_CALC_LENNARDJONES_H -#include "energycalculator.h" +#include "avogadrocalcexport.h" + +#include namespace Avogadro { namespace Core { @@ -15,7 +17,7 @@ class Molecule; namespace Calc { -class LennardJones : public EnergyCalculator +class AVOGADROCALC_EXPORT LennardJones : public EnergyCalculator { public: LennardJones(); diff --git a/avogadro/qtplugins/CMakeLists.txt b/avogadro/qtplugins/CMakeLists.txt index b1054dd1b3..e1b4f28d4e 100644 --- a/avogadro/qtplugins/CMakeLists.txt +++ b/avogadro/qtplugins/CMakeLists.txt @@ -250,7 +250,7 @@ target_sources(QtPlugins PRIVATE avogadro_add_library(QtPlugins ${HEADERS} ${SOURCES}) target_link_libraries(QtPlugins PUBLIC Qt::Core - PRIVATE ${AvogadroLibs_STATIC_PLUGINS} Avogadro::QtGui) + PRIVATE ${AvogadroLibs_STATIC_PLUGINS} Avogadro::QtGui Avogadro::Calc) if(QT_VERSION EQUAL 6) target_link_libraries(QtPlugins PRIVATE Qt6::OpenGLWidgets Qt6::Network) diff --git a/avogadro/qtplugins/forcefield/CMakeLists.txt b/avogadro/qtplugins/forcefield/CMakeLists.txt index e4d29784d2..88546724ea 100644 --- a/avogadro/qtplugins/forcefield/CMakeLists.txt +++ b/avogadro/qtplugins/forcefield/CMakeLists.txt @@ -18,7 +18,7 @@ avogadro_plugin(Forcefield "${forcefield_srcs}" ) -target_link_libraries(Forcefield LINK_PRIVATE AvogadroIO AvogadroCalc) +target_link_libraries(Forcefield PRIVATE Avogadro::Calc) # Bundled forcefield scripts (open babel) set(forcefields diff --git a/avogadro/qtplugins/forcefield/forcefield.cpp b/avogadro/qtplugins/forcefield/forcefield.cpp index 873a7609b0..a6ab813d91 100644 --- a/avogadro/qtplugins/forcefield/forcefield.cpp +++ b/avogadro/qtplugins/forcefield/forcefield.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -37,6 +38,8 @@ namespace Avogadro { namespace QtPlugins { using Avogadro::QtGui::Molecule; +using Avogadro::QtGui::RWMolecule; +using Avogadro::Calc::EnergyCalculator; const int energyAction = 0; const int optimizeAction = 1; @@ -138,7 +141,7 @@ void Forcefield::optimize() // set the method //@todo check m_method for a particular calculator - LennardJones lj(this->parent()); + Calc::LennardJones lj; lj.setMolecule(m_molecule); for (unsigned int i = 0; i < m_maxSteps / crit.iterations; ++i) { @@ -173,7 +176,7 @@ void Forcefield::energy() return; //@todo check m_method for a particular calculator - LennardJones lj(this->parent()); + Calc::LennardJones lj; lj.setMolecule(m_molecule); int n = m_molecule->atomCount(); @@ -195,7 +198,7 @@ void Forcefield::freezeSelected() // now freeze the specified atoms for (Index i = 0; i < numAtoms; ++i) { if (m_molecule->atomSelected(i)) { - m_molecule->setAtomFrozen(i, true); + // m_molecule->setAtomFrozen(i, true); } } } From 05bd338ddc5751fa73ccb2957e69a183105d96a5 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Wed, 11 Oct 2023 20:50:26 -0400 Subject: [PATCH 13/22] Add support for unit cell optimizing through minimum distances Signed-off-by: Geoff Hutchison --- avogadro/calc/lennardjones.cpp | 124 +++++++++++++------ avogadro/calc/lennardjones.h | 2 + avogadro/core/unitcell.h | 2 +- avogadro/qtplugins/forcefield/forcefield.cpp | 27 ++-- 4 files changed, 110 insertions(+), 45 deletions(-) diff --git a/avogadro/calc/lennardjones.cpp b/avogadro/calc/lennardjones.cpp index b4d0c1f8ab..63e90e0c58 100644 --- a/avogadro/calc/lennardjones.cpp +++ b/avogadro/calc/lennardjones.cpp @@ -7,10 +7,14 @@ #include #include +#include namespace Avogadro::Calc { -LennardJones::LennardJones() : m_vdw(true), m_depth(100.0), m_exponent(6) {} +LennardJones::LennardJones() + : m_vdw(true), m_depth(100.0), m_exponent(6), m_cell(nullptr) +{ +} LennardJones::~LennardJones() {} @@ -21,11 +25,16 @@ void LennardJones::setMolecule(Core::Molecule* mol) if (mol == nullptr) { return; // nothing to do } + + m_cell = mol->unitCell(); // could be nullptr int numAtoms = mol->atomCount(); // track atomic radii for this molecule m_radii.setZero(); Eigen::MatrixXd radii(numAtoms, numAtoms); + Eigen::MatrixXd mask(numAtoms*3, 1); + mask.setOnes(); + m_mask = mask; for (Index i = 0; i < numAtoms; ++i) { Core::Atom atom1 = mol->atom(i); @@ -50,7 +59,6 @@ void LennardJones::setMolecule(Core::Molecule* mol) } m_radii = radii; - //@todo store unit cell if available } Real LennardJones::value(const Eigen::VectorXd& x) @@ -63,16 +71,34 @@ Real LennardJones::value(const Eigen::VectorXd& x) int numAtoms = m_molecule->atomCount(); Real energy = 0.0; - for (Index i = 0; i < numAtoms; ++i) { - Vector3 ipos(x[3 * i], x[3 * i + 1], x[3 * i + 2]); - for (Index j = i + 1; j < numAtoms; ++j) { - Vector3 jpos(x[3 * j], x[3 * j + 1], x[3 * j + 2]); - Real r = (ipos - jpos).norm(); - if (r < 0.1) - r = 0.1; // ensure we don't divide by zero - - Real ratio = pow((m_radii(i, j) / r), m_exponent); - energy += m_depth * (ratio * ratio - 2.0 * (ratio)); + // we put the conditional here outside the double loop + if (m_cell == nullptr) { + // regular molecule + for (Index i = 0; i < numAtoms; ++i) { + Vector3 ipos(x[3 * i], x[3 * i + 1], x[3 * i + 2]); + for (Index j = i + 1; j < numAtoms; ++j) { + Vector3 jpos(x[3 * j], x[3 * j + 1], x[3 * j + 2]); + Real r = (ipos - jpos).norm(); + if (r < 0.1) + r = 0.1; // ensure we don't divide by zero + + Real ratio = pow((m_radii(i, j) / r), m_exponent); + energy += m_depth * (ratio * ratio - 2.0 * (ratio)); + } + } + } else { + // use the unit cell to get minimum distances + for (Index i = 0; i < numAtoms; ++i) { + Vector3 ipos(x[3 * i], x[3 * i + 1], x[3 * i + 2]); + for (Index j = i + 1; j < numAtoms; ++j) { + Vector3 jpos(x[3 * j], x[3 * j + 1], x[3 * j + 2]); + Real r = m_cell->distance(ipos, jpos); + if (r < 0.1) + r = 0.1; // ensure we don't divide by zero + + Real ratio = pow((m_radii(i, j) / r), m_exponent); + energy += m_depth * (ratio * ratio - 2.0 * (ratio)); + } } } @@ -88,30 +114,58 @@ void LennardJones::gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) // clear the gradients grad.setZero(); - //@todo handle unit cells and minimum distances int numAtoms = m_molecule->atomCount(); - - for (Index i = 0; i < numAtoms; ++i) { - Vector3 ipos(x[3 * i], x[3 * i + 1], x[3 * i + 2]); - for (Index j = i + 1; j < numAtoms; ++j) { - Vector3 jpos(x[3 * j], x[3 * j + 1], x[3 * j + 2]); - Vector3 force = ipos - jpos; - - Real r = force.norm(); - if (r < 0.1) - r = 0.1; // ensure we don't divide by zero - - Real rad = pow(m_radii(i, j), m_exponent); - Real term1 = -2 * (m_exponent)*rad * rad * pow(r, -2 * m_exponent - 1); - Real term2 = 2 * (m_exponent)*rad * pow(r, -1 * m_exponent - 1); - Real dE = m_depth * (term1 + term2); - - force = (dE / r) * force; - - // update gradients - for (unsigned int c = 0; c < 3; ++c) { - grad[3 * i + c] += force[c]; - grad[3 * j + c] -= force[c]; + // we put the conditional here outside the double loop + if (m_cell == nullptr) { + // regular molecule + for (Index i = 0; i < numAtoms; ++i) { + Vector3 ipos(x[3 * i], x[3 * i + 1], x[3 * i + 2]); + for (Index j = i + 1; j < numAtoms; ++j) { + Vector3 jpos(x[3 * j], x[3 * j + 1], x[3 * j + 2]); + Vector3 force = ipos - jpos; + + Real r = force.norm(); + if (r < 0.1) + r = 0.1; // ensure we don't divide by zero + + Real rad = pow(m_radii(i, j), m_exponent); + Real term1 = -2 * (m_exponent)*rad * rad * pow(r, -2 * m_exponent - 1); + Real term2 = 2 * (m_exponent)*rad * pow(r, -1 * m_exponent - 1); + Real dE = m_depth * (term1 + term2); + + force = (dE / r) * force; + + // update gradients + for (unsigned int c = 0; c < 3; ++c) { + grad[3 * i + c] += force[c]; + grad[3 * j + c] -= force[c]; + } + } + } + } else { + // unit cell + for (Index i = 0; i < numAtoms; ++i) { + Vector3 ipos(x[3 * i], x[3 * i + 1], x[3 * i + 2]); + for (Index j = i + 1; j < numAtoms; ++j) { + Vector3 jpos(x[3 * j], x[3 * j + 1], x[3 * j + 2]); + Vector3 force = m_cell->minimumImage(ipos - jpos); + + Real r = force.norm(); + if (r < 0.1) + r = 0.1; // ensure we don't divide by zero + + Real rad = pow(m_radii(i, j), m_exponent); + Real term1 = -2 * (m_exponent)*rad * rad * pow(r, -2 * m_exponent - 1); + Real term2 = 2 * (m_exponent)*rad * pow(r, -1 * m_exponent - 1); + Real dE = m_depth * (term1 + term2); + + force = (dE / r) * force; + + // update gradients + for (unsigned int c = 0; c < 3; ++c) { + grad[3 * i + c] += force[c]; + grad[3 * j + c] -= force[c]; + } } } } diff --git a/avogadro/calc/lennardjones.h b/avogadro/calc/lennardjones.h index d54f9151f6..3364ff3f3e 100644 --- a/avogadro/calc/lennardjones.h +++ b/avogadro/calc/lennardjones.h @@ -13,6 +13,7 @@ namespace Avogadro { namespace Core { class Molecule; +class UnitCell; } namespace Calc { @@ -43,6 +44,7 @@ class AVOGADROCALC_EXPORT LennardJones : public EnergyCalculator protected: Core::Molecule* m_molecule; + Core::UnitCell* m_cell; Eigen::MatrixXd m_radii; bool m_vdw; Real m_depth; diff --git a/avogadro/core/unitcell.h b/avogadro/core/unitcell.h index 99ef4de1db..8565f95a2a 100644 --- a/avogadro/core/unitcell.h +++ b/avogadro/core/unitcell.h @@ -342,7 +342,7 @@ inline Vector3 UnitCell::minimumImage(const Vector3& v) const inline Real UnitCell::distance(const Vector3& v1, const Vector3& v2) const { - return std::fabs(minimumImage(v1 - v2).norm()); + return minimumImage(v1 - v2).norm(); } } // namespace Core diff --git a/avogadro/qtplugins/forcefield/forcefield.cpp b/avogadro/qtplugins/forcefield/forcefield.cpp index a6ab813d91..ca20d539cf 100644 --- a/avogadro/qtplugins/forcefield/forcefield.cpp +++ b/avogadro/qtplugins/forcefield/forcefield.cpp @@ -60,6 +60,7 @@ Forcefield::Forcefield(QObject* parent_) action->setEnabled(true); action->setText(tr("Optimize")); action->setData(optimizeAction); + action->setProperty("menu priority", 920); connect(action, SIGNAL(triggered()), SLOT(optimize())); m_actions.push_back(action); @@ -67,6 +68,7 @@ Forcefield::Forcefield(QObject* parent_) action->setEnabled(true); action->setText(tr("Energy")); // calculate energy action->setData(energyAction); + action->setProperty("menu priority", 910); connect(action, SIGNAL(triggered()), SLOT(energy())); m_actions.push_back(action); @@ -93,7 +95,7 @@ QStringList Forcefield::menuPath(QAction* action) const path << tr("&Extensions"); return path; } - path << tr("&Extensions") << tr("Calculate"); + path << tr("&Extensions") << tr("&Calculate"); return path; } @@ -122,20 +124,23 @@ void Forcefield::optimize() m_molecule->undoMolecule()->setInteractive(true); //@todo check m_minimizer for method to use - cppoptlib::LbfgsSolver solver; + //cppoptlib::LbfgsSolver solver; + cppoptlib::ConjugatedGradientDescentSolver solver; int n = m_molecule->atomCount(); Core::Array pos = m_molecule->atomPositions3d(); + // just to get the right size / shape + Core::Array forces = m_molecule->atomPositions3d(); double* p = pos[0].data(); Eigen::Map map(p, 3 * n); Eigen::VectorXd positions = map; + Eigen::VectorXd gradient = Eigen::VectorXd::Zero(3 * n); // Create a Criteria class to adjust stopping criteria cppoptlib::Criteria crit = cppoptlib::Criteria::defaults(); // @todo allow criteria to be set crit.iterations = 5; - crit.xDelta = 1.0e-4; // positions converged to 1.0e-4 - crit.fDelta = 1.0e-4; // energy converged to 1.0e-4 + crit.fDelta = 1.0e-6; solver.setStopCriteria(crit); // every 5 steps, update coordinates cppoptlib::Status status = cppoptlib::Status::NotStarted; @@ -148,8 +153,11 @@ void Forcefield::optimize() solver.minimize(lj, positions); cppoptlib::Status currentStatus = solver.status(); - if (currentStatus != status || currentStatus == cppoptlib::Status::Continue) { - // status has changed or minimizer hasn't converged + // qDebug() << " status: " << (int)currentStatus; + // qDebug() << " energy: " << lj.value(positions); + // check the gradient norm + lj.gradient(positions, gradient); + // qDebug() << " gradient: " << gradient.norm(); // update coordinates const double* d = positions.data(); @@ -157,16 +165,17 @@ void Forcefield::optimize() for (size_t i = 0; i < n; ++i) { pos[i] = Vector3(*(d), *(d + 1), *(d + 2)); d += 3; + + forces[i] = Vector3(gradient[3 * i], gradient[3 * i + 1], + gradient[3 * i + 2]); } // todo - merge these into one undo step m_molecule->undoMolecule()->setAtomPositions3d(pos, tr("Optimize Geometry")); + m_molecule->setForceVectors(forces); Molecule::MoleculeChanges changes = Molecule::Atoms | Molecule::Modified; m_molecule->emitChanged(changes); - } } - qDebug() << " energy: " << lj.value(positions); - m_molecule->undoMolecule()->setInteractive(isInteractive); } From d73b1860eb93ab1f178c07cb668622dcfe94ea8b Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Wed, 11 Oct 2023 22:32:24 -0400 Subject: [PATCH 14/22] Remove include paths Signed-off-by: Geoff Hutchison --- avogadro/qtplugins/forcefield/CMakeLists.txt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/avogadro/qtplugins/forcefield/CMakeLists.txt b/avogadro/qtplugins/forcefield/CMakeLists.txt index 88546724ea..b3d670d4bf 100644 --- a/avogadro/qtplugins/forcefield/CMakeLists.txt +++ b/avogadro/qtplugins/forcefield/CMakeLists.txt @@ -1,11 +1,3 @@ - -# Add as "system headers" to avoid warnings generated by them with -# compilers that support that notion. -include_directories(SYSTEM "${EIGEN3_INCLUDE_DIR}" - "${AvogadroLibs_BINARY_DIR}/avogadro/io/" - "${AvogadroLibs_SOURCE_DIR}/thirdparty") - -# Extension set(forcefield_srcs forcefield.cpp ) From dfad83196265387f5b1db831b82bd78199be4241 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Sat, 14 Oct 2023 13:55:36 -0400 Subject: [PATCH 15/22] More cleanups, including a manager class to filter methods Signed-off-by: Geoff Hutchison --- avogadro/calc/CMakeLists.txt | 2 + avogadro/calc/chargemanager.h | 2 +- avogadro/calc/energycalculator.h | 29 ++++ avogadro/calc/energymanager.cpp | 124 +++++++++++++++ avogadro/calc/energymanager.h | 143 ++++++++++++++++++ avogadro/calc/flameminimize.cpp | 113 -------------- avogadro/calc/flameminimize.h | 49 ------ avogadro/calc/lennardjones.cpp | 4 + avogadro/calc/lennardjones.h | 21 ++- avogadro/qtplugins/forcefield/CMakeLists.txt | 12 +- .../qtplugins/forcefield/forcefielddialog.ui | 51 +++++-- 11 files changed, 365 insertions(+), 185 deletions(-) create mode 100644 avogadro/calc/energymanager.cpp create mode 100644 avogadro/calc/energymanager.h delete mode 100644 avogadro/calc/flameminimize.cpp delete mode 100644 avogadro/calc/flameminimize.h diff --git a/avogadro/calc/CMakeLists.txt b/avogadro/calc/CMakeLists.txt index 12b5d06a0e..df6deef248 100644 --- a/avogadro/calc/CMakeLists.txt +++ b/avogadro/calc/CMakeLists.txt @@ -5,6 +5,7 @@ avogadro_headers(Calc chargemanager.h defaultmodel.h energycalculator.h + energymanager.h lennardjones.h ) @@ -13,6 +14,7 @@ target_sources(Calc PRIVATE chargemanager.cpp defaultmodel.cpp energycalculator.cpp + energymanager.cpp lennardjones.cpp ) diff --git a/avogadro/calc/chargemanager.h b/avogadro/calc/chargemanager.h index 7d611c5b8e..13137d8598 100644 --- a/avogadro/calc/chargemanager.h +++ b/avogadro/calc/chargemanager.h @@ -53,7 +53,7 @@ class AVOGADROCALC_EXPORT ChargeManager /** * @brief Register a new charge model with the manager. - * @param format An instance of the format to manage, the manager assumes + * @param model An instance of the model to manage, the manager assumes * ownership of the object passed in. * @return True on success, false on failure. */ diff --git a/avogadro/calc/energycalculator.h b/avogadro/calc/energycalculator.h index 8b65d53345..f59c73546b 100644 --- a/avogadro/calc/energycalculator.h +++ b/avogadro/calc/energycalculator.h @@ -8,6 +8,7 @@ #include "avogadrocalcexport.h" +#include #include #include @@ -46,6 +47,34 @@ class AVOGADROCALC_EXPORT EnergyCalculator : public cppoptlib::Problem */ virtual bool setConfiguration(Core::VariantMap& config) { return true; } + /** + * @brief Indicate if your method only treats a subset of elements + * @return an element mask corresponding to the defined subset + */ + virtual Core::Molecule::ElementMask elements() const = 0; + + /** + * @brief Indicate if your method can handle unit cells + * @return true if unit cells are supported + */ + virtual bool acceptsUnitCell() const { return false; } + + /** + * @brief Indicate if your method can handle ions + * Many methods only treat neutral systems, either + * a neutral molecule or a neutral unit cell. + * + * @return true if ions are supported + */ + virtual bool acceptsIons() const { return false; } + + /** + * @brief Indicate if your method can handle radicals + * Most methods only treat closed-shell molecules. + * @return true if radicals are supported + */ + virtual bool acceptsRadicals() const { return false; } + /** * Calculate the gradients for this method, defaulting to numerical * finite-difference methods diff --git a/avogadro/calc/energymanager.cpp b/avogadro/calc/energymanager.cpp new file mode 100644 index 0000000000..fdd23cd6dd --- /dev/null +++ b/avogadro/calc/energymanager.cpp @@ -0,0 +1,124 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + This source code is released under the 3-Clause BSD License, (see "LICENSE"). +******************************************************************************/ + +#include "energymanager.h" +#include "energycalculator.h" +#include "lennardjones.h" + +#include +#include + +namespace Avogadro::Calc { + +EnergyManager& EnergyManager::instance() +{ + static EnergyManager instance; + return instance; +} + +void EnergyManager::appendError(const std::string& errorMessage) +{ + m_error += errorMessage + "\n"; +} + +bool EnergyManager::registerModel(EnergyCalculator* model) +{ + return instance().addModel(model); +} + +bool EnergyManager::unregisterModel(const std::string& identifier) +{ + return instance().removeModel(identifier); +} + +bool EnergyManager::addModel(EnergyCalculator* model) +{ + if (model == nullptr) { + appendError("Supplied model was null."); + return false; + } + + if (m_identifiers.find(model->identifier()) != m_identifiers.end()) { + appendError("Model " + model->identifier() + " already loaded."); + return false; + } + + // If we got here then the format is unique enough to be added. + size_t index = m_models.size(); + m_models.push_back(model); + m_identifiers[model->identifier()] = index; + m_identifierToName[model->identifier()] = model->name(); + + return true; +} + +bool EnergyManager::removeModel(const std::string& identifier) +{ + auto ids = m_identifiers[identifier]; + m_identifiers.erase(identifier); + m_identifierToName.erase(identifier); + + auto* model = m_models[ids]; + + if (model != nullptr) { + m_models[ids] = nullptr; + delete model; + } + + return true; +} + +std::string EnergyManager::nameForModel(const std::string& identifier) const +{ + auto it = m_identifierToName.find(identifier); + if (it == m_identifierToName.end()) { + return identifier; + } + return it->second; +} + +EnergyManager::EnergyManager() +{ + // add any default models here (EEM maybe?) +} + +EnergyManager::~EnergyManager() +{ + // Delete the models that were loaded. + for (auto& m_model : m_models) { + delete m_model; + } + m_models.clear(); +} + +std::set EnergyManager::identifiersForMolecule( + const Core::Molecule& molecule) const +{ + std::set identifiers; + + // check our models for compatibility + for (auto m_model : m_models) { + // we can check easy things first + // - is the molecule an ion based on total charge + // - is the molecule a radical based on spin multiplicity + // - does the molecule have a unit cell + if (molecule.totalCharge() != 0 && !m_model->acceptsIons()) + continue; + if (molecule.totalSpinMultiplicity() != 1 && !m_model->acceptsRadicals()) + continue; + if (molecule.unitCell() != nullptr && !m_model->acceptsUnitCell()) + continue; + + // Finally, we check that every element in the molecule + // is handled by the model + auto mask = m_model->elements() & molecule.elements(); + if (mask.count() == molecule.elements().count()) + identifiers.insert(m_model->identifier()); // this one will work + } + + return identifiers; +} + +} // namespace Avogadro::Calc diff --git a/avogadro/calc/energymanager.h b/avogadro/calc/energymanager.h new file mode 100644 index 0000000000..308bd501f4 --- /dev/null +++ b/avogadro/calc/energymanager.h @@ -0,0 +1,143 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + This source code is released under the 3-Clause BSD License, (see "LICENSE"). +******************************************************************************/ + +#ifndef AVOGADRO_CALC_ENERGYMANAGER_H +#define AVOGADRO_CALC_ENERGYMANAGER_H + +#include "avogadrocalcexport.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace Avogadro { +namespace Core { +class Molecule; +} +namespace Calc { + +class EnergyCalculator; + +/** + * @class EnergyManager chargemanager.h + * + * @brief Class to manage registration, searching and creation of force field + * (energy) calculators. + * @author Geoffrey R. Hutchison + * + * The energy manager is a singleton class that handles the runtime + * registration, search, creation and eventual destruction of calculators + * for geometry optimization and molecular dynamics. + * It can be used to gain a listing of available models, register new + * models, etc. + * + * All energy calculation can take place independent of this code, but for + * automated registration and look up, this is the preferred API. + */ +class AVOGADROCALC_EXPORT EnergyManager +{ +public: + /** + * Get the singleton instance of the energy manager. This instance should + * not be deleted. + */ + static EnergyManager& instance(); + + /** + * @brief Register a new model with the manager. + * @param model An instance of the calculator to manage, the manager assumes + * ownership of the object passed in. + * @return True on success, false on failure. + */ + static bool registerModel(EnergyCalculator* model); + + /** + * @brief Unregister a charge model from the manager. + * @param identifier The identifier for the model to remove. + * @return True on success, false on failure. + */ + static bool unregisterModel(const std::string& identifier); + + /** + * Add the supplied @p model to the manager, registering its ID and other + * relevant data for later lookup. The manager assumes ownership of the + * supplied object. + * @return True on success, false on failure. + */ + bool addModel(EnergyCalculator* model); + + /** + * Remove the model with the identifier @a identifier from the manager. + * @return True on success, false on failure. + */ + bool removeModel(const std::string& identifier); + + /** + * New instance of the model for the specified @p identifier. Ownership + * is passed to the caller. + * @param identifier The unique identifier of the model. + * @return Instance of the model, nullptr if not found. Ownership passes to + * the caller. + */ + EnergyCalculator* newModelFromIdentifier(const std::string& identifier) const; + + /** + * Get a list of all loaded identifiers + */ + std::set identifiers() const; + + /** + * @brief Get a list of models that work for this molecule. + * + * This is probably the method you want to get a list for a user + */ + std::set identifiersForMolecule( + const Core::Molecule& molecule) const; + + /** + * @brief Get the name of the model for the specified identifier. + * + * The name is a user-visible string, and may be translated. + * @param identifier The unique identifier of the model. + * @return The name of the model, or an empty string if not found. + */ + std::string nameForModel(const std::string& identifier) const; + + /** + * Get any errors that have been logged when loading models. + */ + std::string error() const; + +private: + typedef std::map ModelIdMap; + + EnergyManager(); + ~EnergyManager(); + + EnergyManager(const EnergyManager&); // Not implemented. + EnergyManager& operator=(const EnergyManager&); // Not implemented. + + /** + * @brief Append warnings/errors to the error message string. + * @param errorMessage The error message to append. + */ + void appendError(const std::string& errorMessage); + + std::vector m_models; + mutable ModelIdMap m_identifiers; + mutable std::map m_identifierToName; + + std::string m_error; +}; + +} // namespace Calc +} // namespace Avogadro + +#endif // AVOGADRO_CALC_ENERGYMANAGER_H diff --git a/avogadro/calc/flameminimize.cpp b/avogadro/calc/flameminimize.cpp deleted file mode 100644 index 7946855088..0000000000 --- a/avogadro/calc/flameminimize.cpp +++ /dev/null @@ -1,113 +0,0 @@ -/****************************************************************************** - This source file is part of the Avogadro project. - This source code is released under the 3-Clause BSD License, (see "LICENSE"). -******************************************************************************/ - -#include "flameminimize.h" - -#include - -#include - -namespace Avogadro::Calc { - -FlameMinimize::FlameMinimize() - : m_molecule(nullptr), m_calc(nullptr) -{} - -void FlameMinimize::setMolecule(Core::Molecule* mol) -{ - m_molecule = mol; - - m_forces.resize(3 * mol->atomCount()); - m_forces.setZero(); - m_velocities.resize(3 * mol->atomCount()); - m_velocities.setZero(); - m_accel.resize(3 * mol->atomCount()); - m_accel.setZero(); - - m_invMasses.resize(3 * mol->atomCount()); - m_invMasses.setZero(); - for (unsigned int i = 0; i < mol->atomCount(); ++i) { - //@todo should this be set to 1.0 amu? - double scaledMass = log(Core::Elements::mass(mol->atom(i).atomicNumber())); - - m_invMasses[3 * i] = 1.0 / scaledMass; - m_invMasses[3 * i + 1] = 1.0 / scaledMass; - m_invMasses[3 * i + 2] = 1.0 / scaledMass; - } -} - -bool FlameMinimize::minimize(EnergyCalculator& calc, - Eigen::VectorXd& positions) -{ - if (m_molecule == nullptr) - return false; - - m_calc = &calc; - - //@todo - set convergence criteria (e.g., max steps, min gradients, energy, - // etc.) - - double alpha = 0.1; // start - double deltaT = 0.1 * 1.0e-15; // fs - unsigned int positiveSteps = 0; - - m_forces.setZero(); - m_velocities.setZero(); - m_accel.setZero(); - - for (unsigned int i = 0; i < 20; ++i) { - verletIntegrate(positions, deltaT); - //qDebug() << "vvi forces " << m_forces.norm() << " vel " << m_velocities.norm(); - - // Step 1 - double power = m_forces.dot(m_velocities); - - // Step 2 - m_velocities = (1.0 - alpha) * m_velocities + alpha* - m_forces.cwiseProduct(m_velocities.cwiseAbs()); - - if (power > 0.0) { - // Step 3 - positiveSteps++; - if (positiveSteps > 5) { - deltaT = std::min(1.1 * deltaT, 1.0); - alpha = 0.99 * alpha; - } - } else { - // Step 4 - positiveSteps = 0; - deltaT = 0.5 * deltaT; - m_velocities.setZero(); - alpha = 0.1; - } - - double Frms = m_forces.norm() / sqrt(positions.rows()); - if (Frms < 1.0e-5) - break; - } - - return true; -} - -void FlameMinimize::verletIntegrate(Eigen::VectorXd& positions, double deltaT) -{ - // See https://en.wikipedia.org/wiki/Verlet_integration#Velocity_Verlet - // (as one of many examples) - if (m_molecule == nullptr || m_calc == nullptr) - return; - - positions += deltaT * m_velocities + (deltaT * deltaT / 2.0) * m_accel; - m_calc->gradient(positions, m_forces); - m_forces = -1*m_forces; - // F = m * a ==> a = F/m - // use coefficient-wise product from Eigen - // see http://eigen.tuxfamily.org/dox/group__TutorialArrayClass.html - Eigen::VectorXd newAccel(3 * m_molecule->atomCount()); - newAccel = m_forces.cwiseProduct(m_invMasses); - m_velocities += 0.5 * deltaT * (m_accel + newAccel); - m_accel = newAccel; -} - -} // namespace Avogadro diff --git a/avogadro/calc/flameminimize.h b/avogadro/calc/flameminimize.h deleted file mode 100644 index ddd65daa08..0000000000 --- a/avogadro/calc/flameminimize.h +++ /dev/null @@ -1,49 +0,0 @@ -/****************************************************************************** - This source file is part of the Avogadro project. - This source code is released under the 3-Clause BSD License, (see "LICENSE"). -******************************************************************************/ - -#ifndef AVOGADRO_CALC_FLAMEMINIMIZE_H -#define AVOGADRO_CALC_FLAMEMINIMIZE_H - -#include - -#include "energycalculator.h" - -namespace Avogadro { -namespace Core { -class Molecule; -} - -class FlameMinimize -{ -public: - FlameMinimize(QObject* parent_ = 0); - ~FlameMinimize() {} - - bool minimize(EnergyCalculator &cal, Eigen::VectorXd &positions); - - // @todo probably want a "take N steps" and a way to set convergence - // (e.g., if the forces/gradients are very small) - // ( might also check if energy changes are v. small) - - /** - * Called when the current molecule changes. - */ - void setMolecule(QtGui::Molecule* mol); - -protected: - - void verletIntegrate(Eigen::VectorXd &positions, double deltaT); - - QtGui::Molecule* m_molecule; - EnergyCalculator* m_calc; - Eigen::VectorXd m_invMasses; - Eigen::VectorXd m_forces; - Eigen::VectorXd m_velocities; - Eigen::VectorXd m_accel; -}; - -} // namespace Avogadro - -#endif // AVOGADRO_FLAMEMINIMIZE_H diff --git a/avogadro/calc/lennardjones.cpp b/avogadro/calc/lennardjones.cpp index 63e90e0c58..b2999cba2b 100644 --- a/avogadro/calc/lennardjones.cpp +++ b/avogadro/calc/lennardjones.cpp @@ -14,6 +14,10 @@ namespace Avogadro::Calc { LennardJones::LennardJones() : m_vdw(true), m_depth(100.0), m_exponent(6), m_cell(nullptr) { + // defined for 1-118 + for (unsigned int i = 1; i <= 118; ++i) { + m_elements.set(i); + } } LennardJones::~LennardJones() {} diff --git a/avogadro/calc/lennardjones.h b/avogadro/calc/lennardjones.h index 3364ff3f3e..5fb7466644 100644 --- a/avogadro/calc/lennardjones.h +++ b/avogadro/calc/lennardjones.h @@ -24,23 +24,30 @@ class AVOGADROCALC_EXPORT LennardJones : public EnergyCalculator LennardJones(); ~LennardJones(); - virtual std::string identifier() const override + std::string identifier() const override { return "LJ"; } - virtual std::string name() const override + std::string name() const override { return "Lennard-Jones"; } - virtual std::string description() const override + std::string description() const override { return "Universal Lennard-Jones potential"; } - virtual Real value(const Eigen::VectorXd& x) override; - virtual void gradient(const Eigen::VectorXd& x, + bool acceptsUnitCell() const override { return true; } + + Core::Molecule::ElementMask elements() const override + { + return (m_elements); + } + + Real value(const Eigen::VectorXd& x) override; + void gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) override; /** * Called when the current molecule changes. */ - virtual void setMolecule(Core::Molecule* mol) override; + void setMolecule(Core::Molecule* mol) override; protected: Core::Molecule* m_molecule; @@ -49,6 +56,8 @@ class AVOGADROCALC_EXPORT LennardJones : public EnergyCalculator bool m_vdw; Real m_depth; int m_exponent; + + Core::Molecule::ElementMask m_elements; }; } // namespace Calc diff --git a/avogadro/qtplugins/forcefield/CMakeLists.txt b/avogadro/qtplugins/forcefield/CMakeLists.txt index b3d670d4bf..74fd65392d 100644 --- a/avogadro/qtplugins/forcefield/CMakeLists.txt +++ b/avogadro/qtplugins/forcefield/CMakeLists.txt @@ -12,12 +12,14 @@ avogadro_plugin(Forcefield target_link_libraries(Forcefield PRIVATE Avogadro::Calc) -# Bundled forcefield scripts (open babel) +# Bundled forcefield scripts set(forcefields + scripts/gfn1.py + scripts/gfn2.py + scripts/gfnff.py + scripts/mmff94.py scripts/uff.py - scripts/gaff.py - scripts/mmff.py ) -#install(PROGRAMS ${forcefields} -#DESTINATION "${INSTALL_LIBRARY_DIR}/avogadro2/scripts/forcefields/") +install(PROGRAMS ${forcefields} +DESTINATION "${INSTALL_LIBRARY_DIR}/avogadro2/scripts/forcefields/") diff --git a/avogadro/qtplugins/forcefield/forcefielddialog.ui b/avogadro/qtplugins/forcefield/forcefielddialog.ui index b4622e8153..8c72edc764 100644 --- a/avogadro/qtplugins/forcefield/forcefielddialog.ui +++ b/avogadro/qtplugins/forcefield/forcefielddialog.ui @@ -1,12 +1,12 @@ - Avogadro::QtPlugins::OBForceFieldDialog - + Avogadro::QtPlugins::ForceFieldDialog + 0 0 - 357 + 365 295 @@ -42,11 +42,11 @@ - "Energy" convergence: + Energy convergence: - + Step limit: @@ -72,7 +72,7 @@ - + steps @@ -84,10 +84,36 @@ 100000 + 50 + + 250 + + + + + + Gradient convergence: + + + + + + + units + + + 10^ + + + -10 + + + -1 + - 2500 + -4 @@ -133,14 +159,17 @@ + + 1 + - Steepest Descent + Conjugate Gradient - Conjugate Gradient + BFGS @@ -163,7 +192,7 @@ buttonBox accepted() - Avogadro::QtPlugins::OBForceFieldDialog + Avogadro::QtPlugins::ForceFieldDialog accept() @@ -179,7 +208,7 @@ buttonBox rejected() - Avogadro::QtPlugins::OBForceFieldDialog + Avogadro::QtPlugins::ForceFieldDialog reject() From 29b73db111a908fdaf5e26ff4cc7e994d7bc25c2 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Sat, 14 Oct 2023 16:19:13 -0400 Subject: [PATCH 16/22] Implement a script interface - currently just energies Also implement frozen atoms / coordinates in the molecule class Signed-off-by: Geoff Hutchison --- avogadro/calc/energycalculator.cpp | 18 - avogadro/calc/energycalculator.h | 19 +- avogadro/calc/energymanager.cpp | 6 +- avogadro/calc/lennardjones.h | 22 +- avogadro/core/molecule.cpp | 57 +++ avogadro/core/molecule.h | 22 ++ avogadro/qtgui/pythonscript.cpp | 28 ++ avogadro/qtgui/pythonscript.h | 12 + avogadro/qtplugins/forcefield/CMakeLists.txt | 3 +- avogadro/qtplugins/forcefield/forcefield.cpp | 138 ++++--- avogadro/qtplugins/forcefield/forcefield.h | 26 +- .../qtplugins/forcefield/scriptenergy.cpp | 367 ++++++++++++++++++ avogadro/qtplugins/forcefield/scriptenergy.h | 107 +++++ avogadro/qtplugins/forcefield/scripts/gfn1.py | 102 +++++ avogadro/qtplugins/forcefield/scripts/gfn2.py | 102 +++++ .../qtplugins/forcefield/scripts/gfnff.py | 104 +++++ .../qtplugins/forcefield/scripts/mmff94.py | 85 ++++ avogadro/qtplugins/forcefield/scripts/uff.py | 85 ++++ .../scriptcharges/scriptchargemodel.cpp | 2 +- 19 files changed, 1209 insertions(+), 96 deletions(-) create mode 100644 avogadro/qtplugins/forcefield/scriptenergy.cpp create mode 100644 avogadro/qtplugins/forcefield/scriptenergy.h create mode 100644 avogadro/qtplugins/forcefield/scripts/gfn1.py create mode 100644 avogadro/qtplugins/forcefield/scripts/gfn2.py create mode 100644 avogadro/qtplugins/forcefield/scripts/gfnff.py create mode 100644 avogadro/qtplugins/forcefield/scripts/mmff94.py create mode 100644 avogadro/qtplugins/forcefield/scripts/uff.py diff --git a/avogadro/calc/energycalculator.cpp b/avogadro/calc/energycalculator.cpp index 9eac17dd5e..25fbcda18a 100644 --- a/avogadro/calc/energycalculator.cpp +++ b/avogadro/calc/energycalculator.cpp @@ -27,22 +27,4 @@ void EnergyCalculator::cleanGradients(TVector& grad) grad = grad.cwiseProduct(m_mask); } -void EnergyCalculator::freezeAtom(Index atomId) -{ - if (atomId * 3 <= m_mask.rows() - 3) { - m_mask[atomId*3] = 0.0; - m_mask[atomId*3+1] = 0.0; - m_mask[atomId*3+2] = 0.0; - } -} - -void EnergyCalculator::unfreezeAtom(Index atomId) -{ - if (atomId * 3 <= m_mask.rows() - 3) { - m_mask[atomId*3] = 1.0; - m_mask[atomId*3+1] = 1.0; - m_mask[atomId*3+2] = 1.0; - } -} - } // namespace Avogadro diff --git a/avogadro/calc/energycalculator.h b/avogadro/calc/energycalculator.h index f59c73546b..b9784e4055 100644 --- a/avogadro/calc/energycalculator.h +++ b/avogadro/calc/energycalculator.h @@ -27,6 +27,12 @@ class AVOGADROCALC_EXPORT EnergyCalculator : public cppoptlib::Problem EnergyCalculator() {} ~EnergyCalculator() {} + /** + * Create a new instance of the model. Ownership passes to the + * caller. + */ + virtual EnergyCalculator* newInstance() const = 0; + /** * @return a unique identifier for this calculator. */ @@ -96,16 +102,23 @@ class AVOGADROCALC_EXPORT EnergyCalculator : public cppoptlib::Problem */ TVector mask() const { return m_mask; } - void freezeAtom(Index atomId); - void unfreezeAtom(Index atomId); - /** * Called when the current molecule changes. */ virtual void setMolecule(Core::Molecule* mol) = 0; protected: + /** + * @brief Append an error to the error string for the model. + * @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) const; + TVector m_mask; // optimize or frozen atom mask + +private: + mutable std::string m_error; }; } // end namespace Calc diff --git a/avogadro/calc/energymanager.cpp b/avogadro/calc/energymanager.cpp index fdd23cd6dd..76e93d78f0 100644 --- a/avogadro/calc/energymanager.cpp +++ b/avogadro/calc/energymanager.cpp @@ -81,7 +81,11 @@ std::string EnergyManager::nameForModel(const std::string& identifier) const EnergyManager::EnergyManager() { - // add any default models here (EEM maybe?) + // add any default models here + + // LJ is the fallback, since it can handle anything + // (maybe not well, but it can handle it) + addModel(new LennardJones); } EnergyManager::~EnergyManager() diff --git a/avogadro/calc/lennardjones.h b/avogadro/calc/lennardjones.h index 5fb7466644..3334ffe48d 100644 --- a/avogadro/calc/lennardjones.h +++ b/avogadro/calc/lennardjones.h @@ -14,7 +14,7 @@ namespace Avogadro { namespace Core { class Molecule; class UnitCell; -} +} // namespace Core namespace Calc { @@ -24,25 +24,23 @@ class AVOGADROCALC_EXPORT LennardJones : public EnergyCalculator LennardJones(); ~LennardJones(); - std::string identifier() const override - { return "LJ"; } + LennardJones* newInstance() const override { return new LennardJones; } - std::string name() const override - { return "Lennard-Jones"; } + std::string identifier() const override { return "LJ"; } + + std::string name() const override { return "Lennard-Jones"; } std::string description() const override - { return "Universal Lennard-Jones potential"; } + { + return "Universal Lennard-Jones potential"; + } bool acceptsUnitCell() const override { return true; } - Core::Molecule::ElementMask elements() const override - { - return (m_elements); - } + Core::Molecule::ElementMask elements() const override { return (m_elements); } Real value(const Eigen::VectorXd& x) override; - void gradient(const Eigen::VectorXd& x, - Eigen::VectorXd& grad) override; + void gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) override; /** * Called when the current molecule changes. diff --git a/avogadro/core/molecule.cpp b/avogadro/core/molecule.cpp index e04046c925..db6567f0a9 100644 --- a/avogadro/core/molecule.cpp +++ b/avogadro/core/molecule.cpp @@ -49,6 +49,7 @@ Molecule::Molecule(const Molecule& other) m_residues(other.m_residues), m_hallNumber(other.m_hallNumber), m_graph(other.m_graph), m_bondOrders(other.m_bondOrders), m_atomicNumbers(other.m_atomicNumbers), + m_frozenAtomMask(other.m_frozenAtomMask), m_layers(LayerManager::getMoleculeLayer(this)) { // Copy over any meshes @@ -90,6 +91,7 @@ Molecule::Molecule(Molecule&& other) noexcept m_residues(other.m_residues), m_hallNumber(other.m_hallNumber), m_graph(other.m_graph), m_bondOrders(other.m_bondOrders), m_atomicNumbers(other.m_atomicNumbers), + m_frozenAtomMask(other.m_frozenAtomMask), m_layers(LayerManager::getMoleculeLayer(this)) { m_basisSet = other.m_basisSet; @@ -133,6 +135,7 @@ Molecule& Molecule::operator=(const Molecule& other) m_bondOrders = other.m_bondOrders; m_atomicNumbers = other.m_atomicNumbers; m_hallNumber = other.m_hallNumber; + m_frozenAtomMask = other.m_frozenAtomMask; clearMeshes(); @@ -193,6 +196,7 @@ Molecule& Molecule::operator=(Molecule&& other) noexcept m_bondOrders = other.m_bondOrders; m_atomicNumbers = other.m_atomicNumbers; m_hallNumber = other.m_hallNumber; + m_frozenAtomMask = other.m_frozenAtomMask; clearMeshes(); m_meshes = std::move(other.m_meshes); @@ -267,6 +271,59 @@ std::set Molecule::partialChargeTypes() const return types; } +void Molecule::setFrozenAtom(Index atomId, bool frozen) +{ + if (atomId >= m_atomicNumbers.size()) + return; + + // check if we need to resize + unsigned int size = m_frozenAtomMask.rows(); + if (m_frozenAtomMask.rows() != 3*m_atomicNumbers.size()) + m_frozenAtomMask.conservativeResize(3*m_atomicNumbers.size()); + + // do we need to initialize new values? + if (m_frozenAtomMask.rows() > size) + for (unsigned int i = size; i < m_frozenAtomMask.rows(); ++i) + m_frozenAtomMask[i] = 1.0; + + float value = frozen ? 0.0 : 1.0; + if (atomId * 3 <= m_frozenAtomMask.rows() - 3) { + m_frozenAtomMask[atomId*3] = value; + m_frozenAtomMask[atomId*3+1] = value; + m_frozenAtomMask[atomId*3+2] = value; + } +} + +bool Molecule::frozenAtom(Index atomId) const +{ + bool frozen = false; + if (atomId * 3 <= m_frozenAtomMask.rows() - 3) { + frozen = (m_frozenAtomMask[atomId*3] == 0.0 && + m_frozenAtomMask[atomId*3+1] == 0.0 && + m_frozenAtomMask[atomId*3+2] == 0.0); + } + return frozen; +} + +void Molecule::setFrozenAtomAxis(Index atomId, int axis, bool frozen) +{ + // check if we need to resize + unsigned int size = m_frozenAtomMask.rows(); + if (m_frozenAtomMask.rows() != 3*m_atomicNumbers.size()) + m_frozenAtomMask.conservativeResize(3*m_atomicNumbers.size()); + + // do we need to initialize new values? + if (m_frozenAtomMask.rows() > size) + for (unsigned int i = size; i < m_frozenAtomMask.rows(); ++i) + m_frozenAtomMask[i] = 1.0; + + float value = frozen ? 0.0 : 1.0; + if (atomId * 3 <= m_frozenAtomMask.rows() - 3) { + m_frozenAtomMask[atomId*3 + axis] = value; + } +} + + void Molecule::setData(const std::string& name, const Variant& value) { m_data.setValue(name, value); diff --git a/avogadro/core/molecule.h b/avogadro/core/molecule.h index f2b921ebea..b4c61297d3 100644 --- a/avogadro/core/molecule.h +++ b/avogadro/core/molecule.h @@ -693,6 +693,26 @@ class AVOGADROCORE_EXPORT Molecule */ bool setAtomicNumber(Index atomId, unsigned char atomicNumber); + /** + * Freeze or unfreeze an atom for optimization + */ + void setFrozenAtom(Index atomId, bool frozen); + + /** + * Get the frozen status of an atom + */ + bool frozenAtom(Index atomId) const; + + /** + * Freeze or unfreeze X, Y, or Z coordinate of an atom for optimization + * @param atomId The index of the atom to modify. + * @param axis The axis to freeze (0, 1, or 2 for X, Y, or Z) + * @param frozen True to freeze, false to unfreeze + */ + void setFrozenAtomAxis(Index atomId, int axis, bool frozen); + + Eigen::VectorXd frozenAtomMask() const { return m_frozenAtomMask; } + /** * @return a map of components and count. */ @@ -763,6 +783,8 @@ class AVOGADROCORE_EXPORT Molecule // This will be stored from the last space group operation unsigned short m_hallNumber = 0; + Eigen::VectorXd m_frozenAtomMask; + private: mutable Graph m_graph; // A transformation of the molecule to a graph. // edge information diff --git a/avogadro/qtgui/pythonscript.cpp b/avogadro/qtgui/pythonscript.cpp index b075b91b75..04650e1704 100644 --- a/avogadro/qtgui/pythonscript.cpp +++ b/avogadro/qtgui/pythonscript.cpp @@ -221,6 +221,34 @@ void PythonScript::processFinished(int, QProcess::ExitStatus) emit finished(); } +QByteArray PythonScript::asyncWriteAndResponse(QByteArray input, const int expectedLines) +{ + if (m_process == nullptr) { + return QByteArray(); // wait + } + + m_process->write(input); + QByteArray buffer; + + if (expectedLines == -1) { + m_process->waitForFinished(); + buffer = m_process->readAll(); + } else { + // only read a certain number of lines + // (e.g., energy and gradients) + int remainingLines = expectedLines; + bool ready = m_process->waitForReadyRead(); + if (ready) { + while (m_process->canReadLine() && remainingLines > 0) { + buffer += m_process->readLine(); + remainingLines--; + } + } + } + + return buffer; +} + QByteArray PythonScript::asyncResponse() { if (m_process == nullptr || m_process->state() == QProcess::Running) { diff --git a/avogadro/qtgui/pythonscript.h b/avogadro/qtgui/pythonscript.h index 28a259ce3e..b49491076e 100644 --- a/avogadro/qtgui/pythonscript.h +++ b/avogadro/qtgui/pythonscript.h @@ -98,6 +98,18 @@ class AVOGADROQTGUI_EXPORT PythonScript : public QObject void asyncExecute(const QStringList& args, const QByteArray& scriptStdin = QByteArray()); + /** + * Write input to the asynchronous process' standard input and return the + * standard output when ready. Does not wait for the process to terminate + * before returning (e.g. "server mode"). + * + * @param input The input to write to the process' standard input + * @param expectedLines The number of lines to expect in the output. If -1, + * the output is read until the process terminates. + * @return The standard output of the process + */ + QByteArray asyncWriteAndResponse(QByteArray input, const int expectedLines = -1); + /** * Returns the standard output of the asynchronous process when finished. */ diff --git a/avogadro/qtplugins/forcefield/CMakeLists.txt b/avogadro/qtplugins/forcefield/CMakeLists.txt index 74fd65392d..115725018d 100644 --- a/avogadro/qtplugins/forcefield/CMakeLists.txt +++ b/avogadro/qtplugins/forcefield/CMakeLists.txt @@ -1,5 +1,6 @@ set(forcefield_srcs forcefield.cpp + scriptenergy.cpp ) avogadro_plugin(Forcefield @@ -22,4 +23,4 @@ set(forcefields ) install(PROGRAMS ${forcefields} -DESTINATION "${INSTALL_LIBRARY_DIR}/avogadro2/scripts/forcefields/") +DESTINATION "${INSTALL_LIBRARY_DIR}/avogadro2/scripts/energy/") diff --git a/avogadro/qtplugins/forcefield/forcefield.cpp b/avogadro/qtplugins/forcefield/forcefield.cpp index ca20d539cf..ab63b9a831 100644 --- a/avogadro/qtplugins/forcefield/forcefield.cpp +++ b/avogadro/qtplugins/forcefield/forcefield.cpp @@ -5,25 +5,25 @@ ******************************************************************************/ #include "forcefield.h" +#include "scriptenergy.h" #include #include #include -#include -#include #include #include +#include +#include #include -#include -#include -#include -#include #include #include #include +#include + +#include #include #include @@ -37,22 +37,16 @@ namespace Avogadro { namespace QtPlugins { +using Avogadro::Calc::EnergyCalculator; using Avogadro::QtGui::Molecule; using Avogadro::QtGui::RWMolecule; -using Avogadro::Calc::EnergyCalculator; const int energyAction = 0; const int optimizeAction = 1; const int configureAction = 2; const int freezeAction = 3; -Forcefield::Forcefield(QObject* parent_) - : ExtensionPlugin(parent_) - , m_molecule(nullptr) - , m_minimizer(LBFGS) - , m_method(0) // just Lennard-Jones for now - , m_maxSteps(100) - , m_outputFormat(nullptr) +Forcefield::Forcefield(QObject* parent_) : ExtensionPlugin(parent_) { refreshScripts(); @@ -74,7 +68,7 @@ Forcefield::Forcefield(QObject* parent_) action = new QAction(this); action->setEnabled(true); - action->setText(tr("Freeze Atoms")); // calculate energy + action->setText(tr("Freeze Atoms")); action->setData(freezeAction); connect(action, SIGNAL(triggered()), SLOT(freezeSelected())); m_actions.push_back(action); @@ -106,12 +100,7 @@ void Forcefield::setMolecule(QtGui::Molecule* mol) m_molecule = mol; - // @todo set molecule for a calculator -} - -void Forcefield::refreshScripts() -{ - // call the script loader + // @todo set the available method list } void Forcefield::optimize() @@ -123,9 +112,7 @@ void Forcefield::optimize() bool isInteractive = m_molecule->undoMolecule()->isInteractive(); m_molecule->undoMolecule()->setInteractive(true); - //@todo check m_minimizer for method to use - //cppoptlib::LbfgsSolver solver; - cppoptlib::ConjugatedGradientDescentSolver solver; + cppoptlib::LbfgsSolver solver; int n = m_molecule->atomCount(); Core::Array pos = m_molecule->atomPositions3d(); @@ -138,42 +125,49 @@ void Forcefield::optimize() // Create a Criteria class to adjust stopping criteria cppoptlib::Criteria crit = cppoptlib::Criteria::defaults(); - // @todo allow criteria to be set - crit.iterations = 5; - crit.fDelta = 1.0e-6; - solver.setStopCriteria(crit); // every 5 steps, update coordinates - cppoptlib::Status status = cppoptlib::Status::NotStarted; + + // e.g., every 5 steps, update coordinates + crit.iterations = m_nSteps; + // we don't set function or gradient criteria + // .. these seem to be broken in the solver code + // .. so we handle ourselves + solver.setStopCriteria(crit); // set the method //@todo check m_method for a particular calculator Calc::LennardJones lj; lj.setMolecule(m_molecule); + double energy = lj.value(positions); for (unsigned int i = 0; i < m_maxSteps / crit.iterations; ++i) { solver.minimize(lj, positions); - cppoptlib::Status currentStatus = solver.status(); - // qDebug() << " status: " << (int)currentStatus; - // qDebug() << " energy: " << lj.value(positions); - // check the gradient norm + double currentEnergy = lj.value(positions); lj.gradient(positions, gradient); - // qDebug() << " gradient: " << gradient.norm(); - - // update coordinates - const double* d = positions.data(); - // casting would be lovely... - for (size_t i = 0; i < n; ++i) { - pos[i] = Vector3(*(d), *(d + 1), *(d + 2)); - d += 3; - - forces[i] = Vector3(gradient[3 * i], gradient[3 * i + 1], - gradient[3 * i + 2]); - } - // todo - merge these into one undo step - m_molecule->undoMolecule()->setAtomPositions3d(pos, tr("Optimize Geometry")); - m_molecule->setForceVectors(forces); - Molecule::MoleculeChanges changes = Molecule::Atoms | Molecule::Modified; - m_molecule->emitChanged(changes); + + // update coordinates + const double* d = positions.data(); + // casting would be lovely... + for (size_t i = 0; i < n; ++i) { + pos[i] = Vector3(*(d), *(d + 1), *(d + 2)); + d += 3; + + forces[i] = -1.0 * Vector3(gradient[3 * i], gradient[3 * i + 1], + gradient[3 * i + 2]); + } + // todo - merge these into one undo step + m_molecule->undoMolecule()->setAtomPositions3d(pos, + tr("Optimize Geometry")); + m_molecule->setForceVectors(forces); + Molecule::MoleculeChanges changes = Molecule::Atoms | Molecule::Modified; + m_molecule->emitChanged(changes); + + // check for convergence + if (fabs(gradient.maxCoeff()) < m_gradientTolerance) + break; + if (fabs(currentEnergy - energy) < m_tolerance) + break; + energy = currentEnergy; } m_molecule->undoMolecule()->setInteractive(isInteractive); @@ -212,5 +206,47 @@ void Forcefield::freezeSelected() } } -} // end QtPlugins +void Forcefield::refreshScripts() +{ + unregisterScripts(); + qDeleteAll(m_scripts); + m_scripts.clear(); + + QMap scriptPaths = + QtGui::ScriptLoader::scriptList("energy"); + foreach (const QString& filePath, scriptPaths) { + auto* model = new ScriptEnergy(filePath); + if (model->isValid()) + m_scripts.push_back(model); + else + delete model; + } + + registerScripts(); +} + +void Forcefield::unregisterScripts() +{ + for (QList::const_iterator + it = m_scripts.constBegin(), + itEnd = m_scripts.constEnd(); + it != itEnd; ++it) { + Calc::EnergyManager::unregisterModel((*it)->identifier()); + } } + +void Forcefield::registerScripts() +{ + for (QList::const_iterator + it = m_scripts.constBegin(), + itEnd = m_scripts.constEnd(); + it != itEnd; ++it) { + if (!Calc::EnergyManager::registerModel((*it)->newInstance())) { + qDebug() << "Could not register model" << (*it)->identifier().c_str() + << "due to name conflict."; + } + } +} + +} // namespace QtPlugins +} // namespace Avogadro diff --git a/avogadro/qtplugins/forcefield/forcefield.h b/avogadro/qtplugins/forcefield/forcefield.h index a96fffaa99..2c51afe531 100644 --- a/avogadro/qtplugins/forcefield/forcefield.h +++ b/avogadro/qtplugins/forcefield/forcefield.h @@ -16,6 +16,11 @@ class QAction; class QDialog; namespace Avogadro { + +namespace Calc { +class EnergyCalculator; +} + namespace QtPlugins { /** @@ -57,6 +62,8 @@ public slots: * Scan for new scripts in the Forcefield directories. */ void refreshScripts(); + void registerScripts(); + void unregisterScripts(); private slots: void energy(); @@ -65,18 +72,19 @@ private slots: private: QList m_actions; - QtGui::Molecule* m_molecule; + QtGui::Molecule* m_molecule = nullptr; - Minimizer m_minimizer; - unsigned int m_method; - unsigned int m_maxSteps; + // defaults + Minimizer m_minimizer = LBFGS; + unsigned int m_maxSteps = 250; + unsigned int m_nSteps = 5; + double m_tolerance = 1.0e-6; + double m_gradientTolerance = 1.0e-4; + Calc::EnergyCalculator *m_method = nullptr; - // maps program name --> script file path - QMap m_forcefieldScripts; - - const Io::FileFormat* m_outputFormat; - QString m_tempFileName; + QList m_scripts; }; + } } diff --git a/avogadro/qtplugins/forcefield/scriptenergy.cpp b/avogadro/qtplugins/forcefield/scriptenergy.cpp new file mode 100644 index 0000000000..13cc2c2117 --- /dev/null +++ b/avogadro/qtplugins/forcefield/scriptenergy.cpp @@ -0,0 +1,367 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + This source code is released under the 3-Clause BSD License, (see "LICENSE"). +******************************************************************************/ + +#include "scriptenergy.h" + +#include +#include + +// formats supported in scripts +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +namespace Avogadro::QtPlugins { + +ScriptEnergy::ScriptEnergy(const QString& scriptFileName_) + : m_interpreter(new QtGui::PythonScript(scriptFileName_)), m_valid(false), + m_inputFormat(NotUsed), m_gradients(false), m_ions(false), + m_radicals(false), m_unitCells(false) +{ + m_elements.reset(); + readMetaData(); +} + +ScriptEnergy::~ScriptEnergy() +{ + delete m_interpreter; +} + +QString ScriptEnergy::scriptFilePath() const +{ + return m_interpreter->scriptFilePath(); +} + +Calc::EnergyCalculator* ScriptEnergy::newInstance() const +{ + return new ScriptEnergy(m_interpreter->scriptFilePath()); +} + +void ScriptEnergy::setMolecule(Core::Molecule* mol) +{ + m_molecule = mol; + + // 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) { + return; // nothing to do + } + + if (!m_unitCells && mol->unitCell()) { + // appendError("Unit cell not supported for this script."); + return; + } + if (!m_ions && mol->totalCharge() != 0) { + // appendError("Ionized molecules not supported for this script."); + return; + } + if (!m_radicals && mol->totalSpinMultiplicity() != 1) { + // appendError("Radical molecules not supported for this script."); + return; + } + + // start the process + // we need a tempory file to write the molecule + QScopedPointer format(createFileFormat(m_inputFormat)); + if (format.isNull()) { + // appendError("Invalid input format."); + return; + } + // get a temporary filename + QString tempPath = QDir::tempPath(); + if (!tempPath.endsWith(QDir::separator())) + tempPath += QDir::separator(); + QString tempPattern = + tempPath + "avogadroenergyXXXXXX." + format->fileExtensions()[0].c_str(); + m_tempFile.setFileTemplate(tempPattern); + if (!m_tempFile.open()) { + // appendError("Error creating temporary file."); + return; + } + + // write the molecule + format->writeFile(m_tempFile.fileName().toStdString(), *mol); + m_tempFile.close(); + + // construct the command line options + QStringList options; + options << "-f" << m_tempFile.fileName(); + + // start the interpreter + m_interpreter->asyncExecute(options); +} + +Real ScriptEnergy::value(const Eigen::VectorXd& x) +{ + if (m_molecule == nullptr || m_interpreter == nullptr) + return 0.0; // nothing to do + + // 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]) + " " + QString::number(x[i + 1]) + " " + + QString::number(x[i + 2]) + "\n"; + } + QByteArray result = m_interpreter->asyncWriteAndResponse(input, 1); + + return result.toDouble(); // if conversion fails, returns 0.0 +} + +void ScriptEnergy::gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) +{ + EnergyCalculator::gradient(x, grad); +} + +ScriptEnergy::Format ScriptEnergy::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* ScriptEnergy::createFileFormat(ScriptEnergy::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 ScriptEnergy::resetMetaData() +{ + m_valid = false; + m_gradients = false; + m_ions = false; + m_radicals = false; + m_unitCells = false; + m_inputFormat = NotUsed; + m_identifier.clear(); + m_name.clear(); + m_description.clear(); + m_formatString.clear(); + m_elements.reset(); +} + +void ScriptEnergy::readMetaData() +{ + resetMetaData(); + + QByteArray output(m_interpreter->execute(QStringList() << "--metadata")); + + if (m_interpreter->hasErrors()) { + qWarning() << tr("Error retrieving metadata for energy script: %1") + .arg(scriptFilePath()) + << "\n" + << m_interpreter->errorList(); + return; + } + + QJsonParseError parseError; + QJsonDocument doc(QJsonDocument::fromJson(output, &parseError)); + if (parseError.error != QJsonParseError::NoError) { + qWarning() << tr("Error parsing metadata for energy script: %1") + .arg(scriptFilePath()) + << "\n" + << parseError.errorString(); + return; + } + + if (!doc.isObject()) { + qWarning() << tr("Error parsing metadata for energy script: %1\n" + "Result is not a JSON object.\n") + .arg(scriptFilePath()); + return; + } + + const QJsonObject metaData(doc.object()); + + // Read required inputs first. + std::string identifierTmp; + if (!parseString(metaData, "identifier", identifierTmp)) { + qWarning() << "Error parsing metadata for energy script:" + << scriptFilePath() << "\n" + << "Error parsing required member 'identifier'" + << "\n" + << output; + return; + } + m_identifier = identifierTmp; + + std::string nameTmp; + if (!parseString(metaData, "name", nameTmp)) { + qWarning() << "Error parsing metadata for energy 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 energy 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 energy 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 ions, radicals, unit cells + /* + "unitCell": False, + "gradients": True, + "ion": False, + "radical": False, + */ + if (!metaData["gradients"].isBool()) { + return; // not valid + } + m_gradients = metaData["gradients"].toBool(); + + if (!metaData["unitCell"].isBool()) { + return; // not valid + } + m_unitCells = metaData["unitCell"].toBool(); + + if (!metaData["ion"].isBool()) { + return; // not valid + } + m_ions = metaData["ion"].toBool(); + + if (!metaData["radical"].isBool()) { + return; // not valid + } + m_radicals = metaData["radical"].toBool(); + + // get the element mask + // (if it doesn't exist, the default is no elements anyway) + m_valid = parseElements(metaData); +} + +bool ScriptEnergy::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 ScriptEnergy::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 sstr, strList) { + // these should be numbers or ranges (e.g., 1-84) + if (sstr.contains('-')) { + // range, so split on the dash + QStringList strList2 = sstr.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 = sstr.toInt(&ok); + if (!ok || i < 1 || i > 119) + return; + + m_elements.set(i); + } +} + +bool ScriptEnergy::parseElements(const QJsonObject& object) +{ + m_elements.reset(); + + // we could either get a string or an array (of numbers) + if (object["elements"].isString()) { + auto str = object["elements"].toString(); + processElementString(str); + + } else if (object["elements"].isArray()) { + QJsonArray arr = object["elements"].toArray(); + for (auto&& i : arr) { + if (i.isString()) { + processElementString(i.toString()); + } else if (i.isDouble()) { + int element = i.toInt(); + if (element >= 1 && element <= 119) // check the range + m_elements.set(element); + } + } + } + return true; +} + +} // namespace Avogadro::QtPlugins diff --git a/avogadro/qtplugins/forcefield/scriptenergy.h b/avogadro/qtplugins/forcefield/scriptenergy.h new file mode 100644 index 0000000000..32783d2ff5 --- /dev/null +++ b/avogadro/qtplugins/forcefield/scriptenergy.h @@ -0,0 +1,107 @@ +/****************************************************************************** + 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_SCRIPTENERGY_H +#define AVOGADRO_QTPLUGINS_SCRIPTENERGY_H + +#include + +#include + +#include +#include +#include + +class QJsonObject; + +namespace Avogadro { + +namespace Io { +class FileFormat; +} + +namespace QtGui { +class PythonScript; +} + +namespace QtPlugins { + +class ScriptEnergy : public Avogadro::Calc::EnergyCalculator +{ + Q_DECLARE_TR_FUNCTIONS(ScriptEnergy) + +public: + /** Formats that may be written to the script's input. */ + enum Format + { + NotUsed, + Cjson, + Cml, + Mdl, // sdf + Pdb, + Xyz + }; + + ScriptEnergy(const QString& scriptFileName = ""); + ~ScriptEnergy() override; + + QString scriptFilePath() const; + + Format inputFormat() const { return m_inputFormat; } + bool isValid() const { return m_valid; } + + Calc::EnergyCalculator* newInstance() const override; + + std::string identifier() const override { return m_identifier; } + std::string name() const override { return m_name; } + std::string description() const override { return m_description; } + + Core::Molecule::ElementMask elements() const override { return m_elements; } + bool supportsGradients() const { return m_gradients; } + bool supportsIons() const { return m_ions; } + bool supportsRadicals() const { return m_radicals; } + bool supportsUnitCells() const { return m_unitCells; } + + // This will check if the molecule is valid for this script + // and then start the external process + void setMolecule(Core::Molecule* mol) override; + // energy + Real value(const Eigen::VectorXd& x) override; + // gradient (which may be unsupported and fall back to numeric) + void gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) 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* m_molecule; + + // what's supported by this script + Core::Molecule::ElementMask m_elements; + bool m_valid; + bool m_gradients; + bool m_ions; + bool m_radicals; + bool m_unitCells; + + std::string m_identifier; + std::string m_name; + std::string m_description; + QString m_formatString; + QTemporaryFile m_tempFile; +}; + +} // namespace QtPlugins +} // namespace Avogadro + +#endif // AVOGADRO_QTPLUGINS_SCRIPTENERGY_H diff --git a/avogadro/qtplugins/forcefield/scripts/gfn1.py b/avogadro/qtplugins/forcefield/scripts/gfn1.py new file mode 100644 index 0000000000..3673b1a8ad --- /dev/null +++ b/avogadro/qtplugins/forcefield/scripts/gfn1.py @@ -0,0 +1,102 @@ +# 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 + +try: + import numpy as np + from xtb.libxtb import VERBOSITY_MUTED + from xtb.interface import Calculator, Param + imported = True +except ImportError: + imported = False + +def getMetaData(): + # before we return metadata, make sure xtb is in the path + if not imported: + return {} # Avogadro will ignore us now + + metaData = { + "name": "GFN1", + "identifier": "GFN1", + "description": "Calculate GFN1-xtb energies and gradients", + "inputFormat": "cjson", + "elements": "1-86", + "unitCell": False, + "gradients": True, + "ion": True, + "radical": True, + } + return metaData + + +def run(filename): + # we get the molecule from the supplied filename + # in cjson format (it's a temporary file created by Avogadro) + with open(filename, "r") as f: + mol_cjson = json.load(f) + + # first setup the calculator + atoms = np.array(mol_cjson["atoms"]["elements"]["number"]) + coord_list = mol_cjson["atoms"]["coords"]["3d"] + coordinates = np.array(coord_list, dtype=float).reshape(-1, 3) + # .. we need to convert from Angstrom to Bohr + coordinates /= 0.52917721067 + + # check for total charge + # and spin multiplicity + charge = None # neutral + spin = None # singlet + if "properties" in mol_cjson: + if "totalCharge" in mol_cjson["properties"]: + charge = mol_cjson["properties"]["totalCharge"] + if "totalSpinMultiplicity" in mol_cjson["properties"]: + spin = mol_cjson["properties"]["totalSpinMultiplicity"] + + calc = Calculator(Param.GFN1xTB, atoms, coordinates, + charge=charge, uhf=spin) + calc.set_verbosity(VERBOSITY_MUTED) + res = calc.singlepoint() + + # we loop forever - Avogadro will kill the process when done + while(True): + # first print the energy of these coordinates + print(res.get_energy()) # in Hartree + + # now print the gradient + # .. we don't want the "[]" in the output + output = np.array2string(res.get_gradient()) + output = output.replace("[", "").replace("]", "") + print(output) + + # 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 + + # update the calculator and run a new calculation + calc.update(coordinates) + calc.singlepoint(res) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("GFN1 calculator") + parser.add_argument("--display-name", action="store_true") + parser.add_argument("--metadata", action="store_true") + parser.add_argument("-f", "--file", nargs=1) + parser.add_argument("--lang", nargs="?", default="en") + args = vars(parser.parse_args()) + + if args["metadata"]: + print(json.dumps(getMetaData())) + elif args["display_name"]: + name = getMetaData().get("name") + if name: + print(name) + else: + sys.exit("xtb-python is unavailable") + elif args["file"]: + run(args["file"][0]) diff --git a/avogadro/qtplugins/forcefield/scripts/gfn2.py b/avogadro/qtplugins/forcefield/scripts/gfn2.py new file mode 100644 index 0000000000..e7035d147b --- /dev/null +++ b/avogadro/qtplugins/forcefield/scripts/gfn2.py @@ -0,0 +1,102 @@ +# 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 + +try: + import numpy as np + from xtb.libxtb import VERBOSITY_MUTED + from xtb.interface import Calculator, Param + imported = True +except ImportError: + imported = False + +def getMetaData(): + # before we return metadata, make sure xtb is in the path + if not imported: + return {} # Avogadro will ignore us now + + metaData = { + "name": "GFN2", + "identifier": "GFN2", + "description": "Calculate GFN2-xtb energies and gradients", + "inputFormat": "cjson", + "elements": "1-86", + "unitCell": False, + "gradients": True, + "ion": True, + "radical": True, + } + return metaData + + +def run(filename): + # we get the molecule from the supplied filename + # in cjson format (it's a temporary file created by Avogadro) + with open(filename, "r") as f: + mol_cjson = json.load(f) + + # first setup the calculator + atoms = np.array(mol_cjson["atoms"]["elements"]["number"]) + coord_list = mol_cjson["atoms"]["coords"]["3d"] + coordinates = np.array(coord_list, dtype=float).reshape(-1, 3) + # .. we need to convert from Angstrom to Bohr + coordinates /= 0.52917721067 + + # check for total charge + # and spin multiplicity + charge = None # neutral + spin = None # singlet + if "properties" in mol_cjson: + if "totalCharge" in mol_cjson["properties"]: + charge = mol_cjson["properties"]["totalCharge"] + if "totalSpinMultiplicity" in mol_cjson["properties"]: + spin = mol_cjson["properties"]["totalSpinMultiplicity"] + + calc = Calculator(Param.GFN2xTB, atoms, coordinates, + charge=charge, uhf=spin) + calc.set_verbosity(VERBOSITY_MUTED) + res = calc.singlepoint() + + # we loop forever - Avogadro will kill the process when done + while(True): + # first print the energy of these coordinates + print(res.get_energy()) # in Hartree + + # now print the gradient + # .. we don't want the "[]" in the output + output = np.array2string(res.get_gradient()) + output = output.replace("[", "").replace("]", "") + print(output) + + # 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 + + # update the calculator and run a new calculation + calc.update(coordinates) + calc.singlepoint(res) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("GFN2 calculator") + parser.add_argument("--display-name", action="store_true") + parser.add_argument("--metadata", action="store_true") + parser.add_argument("-f", "--file", nargs=1) + parser.add_argument("--lang", nargs="?", default="en") + args = vars(parser.parse_args()) + + if args["metadata"]: + print(json.dumps(getMetaData())) + elif args["display_name"]: + name = getMetaData().get("name") + if name: + print(name) + else: + sys.exit("xtb-python is unavailable") + elif args["file"]: + run(args["file"][0]) diff --git a/avogadro/qtplugins/forcefield/scripts/gfnff.py b/avogadro/qtplugins/forcefield/scripts/gfnff.py new file mode 100644 index 0000000000..c6e6223f9c --- /dev/null +++ b/avogadro/qtplugins/forcefield/scripts/gfnff.py @@ -0,0 +1,104 @@ +# 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 + +try: + import numpy as np + from xtb.libxtb import VERBOSITY_MUTED + from xtb.interface import Calculator, Param + + imported = True +except ImportError: + imported = False + + +def getMetaData(): + # before we return metadata, make sure xtb is in the path + if not imported: + return {} # Avogadro will ignore us now + + metaData = { + "name": "GFN-FF", + "identifier": "GFN-FF", + "description": "Calculate GFNFF-xtb energies and gradients", + "inputFormat": "cjson", + "elements": "1-86", + "unitCell": False, + "gradients": True, + "ion": True, + "radical": False, + } + return metaData + + +def run(filename): + # we get the molecule from the supplied filename + # in cjson format (it's a temporary file created by Avogadro) + with open(filename, "r") as f: + mol_cjson = json.load(f) + + # first setup the calculator + atoms = np.array(mol_cjson["atoms"]["elements"]["number"]) + coord_list = mol_cjson["atoms"]["coords"]["3d"] + coordinates = np.array(coord_list, dtype=float).reshape(-1, 3) + # .. we need to convert from Angstrom to Bohr + coordinates /= 0.52917721067 + + # check for total charge + # and spin multiplicity + charge = None # neutral + spin = None # singlet + if "properties" in mol_cjson: + if "totalCharge" in mol_cjson["properties"]: + charge = mol_cjson["properties"]["totalCharge"] + if "totalSpinMultiplicity" in mol_cjson["properties"]: + spin = mol_cjson["properties"]["totalSpinMultiplicity"] + + calc = Calculator(Param.GFNFF, atoms, coordinates, + charge=charge, uhf=spin) + calc.set_verbosity(VERBOSITY_MUTED) + res = calc.singlepoint() + + # we loop forever - Avogadro will kill our process when done + while True: + # first print the energy of these coordinates + print(res.get_energy()) # in Hartree + + # now print the gradient + # .. we don't want the "[]" in the output + output = np.array2string(res.get_gradient()) + output = output.replace("[", "").replace("]", "") + print(output) + + # 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 + + # update the calculator and run a new calculation + calc.update(coordinates) + calc.singlepoint(res) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("GFN2 calculator") + parser.add_argument("--display-name", action="store_true") + parser.add_argument("--metadata", action="store_true") + parser.add_argument("-f", "--file", nargs=1) + parser.add_argument("--lang", nargs="?", default="en") + args = vars(parser.parse_args()) + + if args["metadata"]: + print(json.dumps(getMetaData())) + elif args["display_name"]: + name = getMetaData().get("name") + if name: + print(name) + else: + sys.exit("xtb-python is unavailable") + elif args["file"]: + run(args["file"][0]) diff --git a/avogadro/qtplugins/forcefield/scripts/mmff94.py b/avogadro/qtplugins/forcefield/scripts/mmff94.py new file mode 100644 index 0000000000..e0f826e3da --- /dev/null +++ b/avogadro/qtplugins/forcefield/scripts/mmff94.py @@ -0,0 +1,85 @@ +# 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 + +try: + from openbabel import pybel + import numpy as np + + imported = True +except ImportError: + imported = False + + +def getMetaData(): + # before we return metadata, make sure xtb is in the path + if not imported: + return {} # Avogadro will ignore us now + + metaData = { + "name": "MMFF94", + "identifier": "MMFF94", + "description": "Calculate MMFF94 energies and gradients", + "inputFormat": "cml", + "elements": "1,6-9,14-17,35,53", + "unitCell": False, + "gradients": True, + "ion": False, + "radical": False, + } + return metaData + + +def run(filename): + # we get the molecule from the supplied filename + # in cjson format (it's a temporary file created by Avogadro) + mol = next(pybel.readfile("cml", filename)) + + ff = pybel._forcefields["mmff94"] + success = ff.Setup(mol.OBMol) + if not success: + # should never happen, but just in case + sys.exit("MMFF94 force field setup failed") + + # we loop forever - Avogadro will kill the process when done + num_atoms = len(mol.atoms) + while True: + # first print the energy of these coordinates + print(ff.Energy(True)) # in Hartree + + # now print the gradient on each atom + for atom in mol.atoms: + grad = ff.GetGradient(atom.OBAtom) + print(grad.GetX(), grad.GetY(), grad.GetZ()) + + # 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]) + + # update the molecule geometry for the next energy + ff.SetCoordinates(mol.OBMol) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("MMFF94 calculator") + parser.add_argument("--display-name", action="store_true") + parser.add_argument("--metadata", action="store_true") + parser.add_argument("-f", "--file", nargs=1) + parser.add_argument("--lang", nargs="?", default="en") + args = vars(parser.parse_args()) + + if args["metadata"]: + print(json.dumps(getMetaData())) + elif args["display_name"]: + name = getMetaData().get("name") + if name: + print(name) + else: + sys.exit("pybel is unavailable") + elif args["file"]: + run(args["file"][0]) diff --git a/avogadro/qtplugins/forcefield/scripts/uff.py b/avogadro/qtplugins/forcefield/scripts/uff.py new file mode 100644 index 0000000000..7d35226262 --- /dev/null +++ b/avogadro/qtplugins/forcefield/scripts/uff.py @@ -0,0 +1,85 @@ +# 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 + +try: + from openbabel import pybel + import numpy as np + + imported = True +except ImportError: + imported = False + + +def getMetaData(): + # before we return metadata, make sure xtb is in the path + if not imported: + return {} # Avogadro will ignore us now + + metaData = { + "name": "UFF", + "identifier": "UFF", + "description": "Calculate UFF energies and gradients", + "inputFormat": "cml", + "elements": "1-86", + "unitCell": False, + "gradients": True, + "ion": False, + "radical": False, + } + return metaData + + +def run(filename): + # we get the molecule from the supplied filename + # in cjson format (it's a temporary file created by Avogadro) + mol = next(pybel.readfile("cml", filename)) + + ff = pybel._forcefields["uff"] + success = ff.Setup(mol.OBMol) + if not success: + # should never happen, but just in case + sys.exit("UFF force field setup failed") + + # we loop forever - Avogadro will kill the process when done + num_atoms = len(mol.atoms) + while True: + # first print the energy of these coordinates + print(ff.Energy(True)) # in Hartree + + # now print the gradient on each atom + for atom in mol.atoms: + grad = ff.GetGradient(atom.OBAtom) + print(grad.GetX(), grad.GetY(), grad.GetZ()) + + # 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]) + + # update the molecule geometry for the next energy + ff.SetCoordinates(mol.OBMol) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("UFF calculator") + parser.add_argument("--display-name", action="store_true") + parser.add_argument("--metadata", action="store_true") + parser.add_argument("-f", "--file", nargs=1) + parser.add_argument("--lang", nargs="?", default="en") + args = vars(parser.parse_args()) + + if args["metadata"]: + print(json.dumps(getMetaData())) + elif args["display_name"]: + name = getMetaData().get("name") + if name: + print(name) + else: + sys.exit("pybel is unavailable") + elif args["file"]: + run(args["file"][0]) diff --git a/avogadro/qtplugins/scriptcharges/scriptchargemodel.cpp b/avogadro/qtplugins/scriptcharges/scriptchargemodel.cpp index 2c789616d0..5fa58a1a6c 100644 --- a/avogadro/qtplugins/scriptcharges/scriptchargemodel.cpp +++ b/avogadro/qtplugins/scriptcharges/scriptchargemodel.cpp @@ -277,7 +277,7 @@ void ScriptChargeModel::readMetaData() if (!parseString(metaData, "identifier", identifierTmp)) { qWarning() << "Error parsing metadata for charge script:" << scriptFilePath() << "\n" - << "Error parsing required member 'operations'" + << "Error parsing required member 'identifier'" << "\n" << output; return; From 3381b92c969a60dd11e1f6820818bc762cd399ae Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Sat, 21 Oct 2023 00:06:39 -0400 Subject: [PATCH 17/22] Handles energies through scripts - still need gradients Signed-off-by: Geoff Hutchison --- avogadro/calc/chargemanager.h | 10 -- avogadro/calc/energycalculator.cpp | 2 + avogadro/calc/energymanager.cpp | 9 ++ avogadro/calc/energymanager.h | 2 +- avogadro/calc/lennardjones.cpp | 2 + avogadro/qtgui/pythonscript.cpp | 6 + avogadro/qtplugins/forcefield/forcefield.cpp | 112 +++++++++++++++--- avogadro/qtplugins/forcefield/forcefield.h | 4 + .../qtplugins/forcefield/scriptenergy.cpp | 7 +- .../qtplugins/forcefield/scripts/gfnff.py | 6 + 10 files changed, 130 insertions(+), 30 deletions(-) diff --git a/avogadro/calc/chargemanager.h b/avogadro/calc/chargemanager.h index 13137d8598..3a8e87a8e5 100644 --- a/avogadro/calc/chargemanager.h +++ b/avogadro/calc/chargemanager.h @@ -80,16 +80,6 @@ class AVOGADROCALC_EXPORT ChargeManager */ bool removeModel(const std::string& identifier); - /** - * New instance of the model for the specified @p identifier. Ownership - * is passed to the caller. - * @param identifier The unique identifier of the format. - * @return Instance of the format, nullptr if not found. Ownership passes to - * the - * caller. - */ - ChargeModel* newModelFromIdentifier(const std::string& identifier) const; - /** * Get a list of all loaded identifiers */ diff --git a/avogadro/calc/energycalculator.cpp b/avogadro/calc/energycalculator.cpp index 25fbcda18a..88eb83cec8 100644 --- a/avogadro/calc/energycalculator.cpp +++ b/avogadro/calc/energycalculator.cpp @@ -5,6 +5,8 @@ #include "energycalculator.h" +#include + namespace Avogadro::Calc { void EnergyCalculator::gradient(const TVector& x, TVector& grad) diff --git a/avogadro/calc/energymanager.cpp b/avogadro/calc/energymanager.cpp index 76e93d78f0..9fec41d6ae 100644 --- a/avogadro/calc/energymanager.cpp +++ b/avogadro/calc/energymanager.cpp @@ -54,6 +54,15 @@ bool EnergyManager::addModel(EnergyCalculator* model) return true; } +EnergyCalculator* EnergyManager::model(const std::string& identifier) const +{ + auto it = m_identifiers.find(identifier); + if (it == m_identifiers.end()) { + return nullptr; + } + return m_models[it->second]->newInstance(); +} + bool EnergyManager::removeModel(const std::string& identifier) { auto ids = m_identifiers[identifier]; diff --git a/avogadro/calc/energymanager.h b/avogadro/calc/energymanager.h index 308bd501f4..69df53a6c6 100644 --- a/avogadro/calc/energymanager.h +++ b/avogadro/calc/energymanager.h @@ -86,7 +86,7 @@ class AVOGADROCALC_EXPORT EnergyManager * @return Instance of the model, nullptr if not found. Ownership passes to * the caller. */ - EnergyCalculator* newModelFromIdentifier(const std::string& identifier) const; + EnergyCalculator* model(const std::string& identifier) const; /** * Get a list of all loaded identifiers diff --git a/avogadro/calc/lennardjones.cpp b/avogadro/calc/lennardjones.cpp index b2999cba2b..c169b8db72 100644 --- a/avogadro/calc/lennardjones.cpp +++ b/avogadro/calc/lennardjones.cpp @@ -30,6 +30,8 @@ void LennardJones::setMolecule(Core::Molecule* mol) return; // nothing to do } + m_mask = mol->frozenAtomMask(); + m_cell = mol->unitCell(); // could be nullptr int numAtoms = mol->atomCount(); diff --git a/avogadro/qtgui/pythonscript.cpp b/avogadro/qtgui/pythonscript.cpp index 04650e1704..e5e37ab9b4 100644 --- a/avogadro/qtgui/pythonscript.cpp +++ b/avogadro/qtgui/pythonscript.cpp @@ -227,6 +227,8 @@ QByteArray PythonScript::asyncWriteAndResponse(QByteArray input, const int expec return QByteArray(); // wait } + qDebug() << " writing to process: " << input << " " << expectedLines; + m_process->write(input); QByteArray buffer; @@ -243,9 +245,13 @@ QByteArray PythonScript::asyncWriteAndResponse(QByteArray input, const int expec buffer += m_process->readLine(); remainingLines--; } + // clear the rest of the buffer + qDebug() << " remaining " << m_process->readAll(); } } + qDebug() << " read from process: " << buffer; + return buffer; } diff --git a/avogadro/qtplugins/forcefield/forcefield.cpp b/avogadro/qtplugins/forcefield/forcefield.cpp index ab63b9a831..ba86fa6194 100644 --- a/avogadro/qtplugins/forcefield/forcefield.cpp +++ b/avogadro/qtplugins/forcefield/forcefield.cpp @@ -45,6 +45,8 @@ const int energyAction = 0; const int optimizeAction = 1; const int configureAction = 2; const int freezeAction = 3; +const int unfreezeAction = 4; +const int constraintAction = 5; Forcefield::Forcefield(QObject* parent_) : ExtensionPlugin(parent_) { @@ -68,10 +70,17 @@ Forcefield::Forcefield(QObject* parent_) : ExtensionPlugin(parent_) action = new QAction(this); action->setEnabled(true); - action->setText(tr("Freeze Atoms")); + action->setText(tr("Freeze Selected Atoms")); action->setData(freezeAction); connect(action, SIGNAL(triggered()), SLOT(freezeSelected())); m_actions.push_back(action); + + action = new QAction(this); + action->setEnabled(true); + action->setText(tr("Unfreeze Selected Atoms")); + action->setData(unfreezeAction); + connect(action, SIGNAL(triggered()), SLOT(unfreezeSelected())); + m_actions.push_back(action); } Forcefield::~Forcefield() {} @@ -99,8 +108,6 @@ void Forcefield::setMolecule(QtGui::Molecule* mol) return; m_molecule = mol; - - // @todo set the available method list } void Forcefield::optimize() @@ -115,15 +122,18 @@ void Forcefield::optimize() cppoptlib::LbfgsSolver solver; int n = m_molecule->atomCount(); + // we have to cast the current 3d positions into a VectorXd Core::Array pos = m_molecule->atomPositions3d(); - // just to get the right size / shape - Core::Array forces = m_molecule->atomPositions3d(); double* p = pos[0].data(); Eigen::Map map(p, 3 * n); Eigen::VectorXd positions = map; + Eigen::VectorXd gradient = Eigen::VectorXd::Zero(3 * n); + // just to get the right size / shape + // we'll use this to draw the force arrows later + Core::Array forces = m_molecule->atomPositions3d(); - // Create a Criteria class to adjust stopping criteria + // Create a Criteria class so we can update coords every N steps cppoptlib::Criteria crit = cppoptlib::Criteria::defaults(); // e.g., every 5 steps, update coordinates @@ -134,20 +144,27 @@ void Forcefield::optimize() solver.setStopCriteria(crit); // set the method - //@todo check m_method for a particular calculator - Calc::LennardJones lj; - lj.setMolecule(m_molecule); + std::string recommended = recommendedForceField(); + qDebug() << "Energy method: " << recommended.c_str(); + + if (m_method == nullptr) { + // we have to create the calculator + m_method = Calc::EnergyManager::instance().model(recommended); + } + m_method->setMolecule(m_molecule); + m_method->setMask(m_molecule->frozenAtomMask()); - double energy = lj.value(positions); + Real energy = m_method->value(positions); for (unsigned int i = 0; i < m_maxSteps / crit.iterations; ++i) { - solver.minimize(lj, positions); + solver.minimize(*m_method, positions); - double currentEnergy = lj.value(positions); - lj.gradient(positions, gradient); + Real currentEnergy = m_method->value(positions); + // get the current gradient for force visualization + m_method->gradient(positions, gradient); // update coordinates const double* d = positions.data(); - // casting would be lovely... + // casting back would be lovely... for (size_t i = 0; i < n; ++i) { pos[i] = Vector3(*(d), *(d + 1), *(d + 2)); d += 3; @@ -179,19 +196,61 @@ void Forcefield::energy() return; //@todo check m_method for a particular calculator - Calc::LennardJones lj; - lj.setMolecule(m_molecule); + std::string recommended = recommendedForceField(); + qDebug() << "Energy method: " << recommended.c_str(); + + if (m_method == nullptr) { + // we have to create the calculator + m_method = Calc::EnergyManager::instance().model(recommended); + } + m_method->setMolecule(m_molecule); int n = m_molecule->atomCount(); + // we have to cast the current 3d positions into a VectorXd Core::Array pos = m_molecule->atomPositions3d(); double* p = pos[0].data(); Eigen::Map map(p, 3 * n); Eigen::VectorXd positions = map; - QString msg(tr("Energy = %L1 kJ/mol").arg(lj.value(positions))); + // now get the energy + Real energy = m_method->value(positions); + + QString msg(tr("Energy = %L1").arg(energy)); QMessageBox::information(nullptr, tr("Avogadro"), msg); } +std::string Forcefield::recommendedForceField() const +{ + // if we have a unit cell, we need to use the LJ calculator + // (implementing something better would be nice) + if (m_molecule == nullptr || m_molecule->unitCell() != nullptr) + return "LJ"; + + // otherwise, let's see what identifers are returned + auto list = + Calc::EnergyManager::instance().identifiersForMolecule(*m_molecule); + if (list.empty()) + return "LJ"; // this will always work + + // iterate to see what we have + std::string bestOption; + for (auto options : list) { + // ideally, we'd use GFN-FF but it needs tweaking + // everything else is a ranking + // GAFF is better than MMFF94 which is better than UFF + if (options == "UFF" && bestOption != "GAFF" || bestOption != "MMFF94") + bestOption = options; + if (options == "MMFF94" && bestOption != "GAFF") + bestOption = options; + if (options == "GAFF") + bestOption = options; + } + if (!bestOption.empty()) + return bestOption; + else + return "LJ"; // this will always work +} + void Forcefield::freezeSelected() { if (!m_molecule) @@ -201,7 +260,21 @@ void Forcefield::freezeSelected() // now freeze the specified atoms for (Index i = 0; i < numAtoms; ++i) { if (m_molecule->atomSelected(i)) { - // m_molecule->setAtomFrozen(i, true); + m_molecule->setFrozenAtom(i, true); + } + } +} + +void Forcefield::unfreezeSelected() +{ + if (!m_molecule) + return; + + int numAtoms = m_molecule->atomCount(); + // now freeze the specified atoms + for (Index i = 0; i < numAtoms; ++i) { + if (m_molecule->atomSelected(i)) { + m_molecule->setFrozenAtom(i, false); } } } @@ -241,6 +314,9 @@ void Forcefield::registerScripts() it = m_scripts.constBegin(), itEnd = m_scripts.constEnd(); it != itEnd; ++it) { + + qDebug() << " register " << (*it)->identifier().c_str(); + if (!Calc::EnergyManager::registerModel((*it)->newInstance())) { qDebug() << "Could not register model" << (*it)->identifier().c_str() << "due to name conflict."; diff --git a/avogadro/qtplugins/forcefield/forcefield.h b/avogadro/qtplugins/forcefield/forcefield.h index 2c51afe531..2388d24f95 100644 --- a/avogadro/qtplugins/forcefield/forcefield.h +++ b/avogadro/qtplugins/forcefield/forcefield.h @@ -57,6 +57,8 @@ class Forcefield : public QtGui::ExtensionPlugin void setMolecule(QtGui::Molecule* mol) override; + std::string recommendedForceField() const; + public slots: /** * Scan for new scripts in the Forcefield directories. @@ -69,8 +71,10 @@ private slots: void energy(); void optimize(); void freezeSelected(); + void unfreezeSelected(); private: + QList m_actions; QtGui::Molecule* m_molecule = nullptr; diff --git a/avogadro/qtplugins/forcefield/scriptenergy.cpp b/avogadro/qtplugins/forcefield/scriptenergy.cpp index 13cc2c2117..d5e09dbfda 100644 --- a/avogadro/qtplugins/forcefield/scriptenergy.cpp +++ b/avogadro/qtplugins/forcefield/scriptenergy.cpp @@ -27,7 +27,7 @@ namespace Avogadro::QtPlugins { ScriptEnergy::ScriptEnergy(const QString& scriptFileName_) - : m_interpreter(new QtGui::PythonScript(scriptFileName_)), m_valid(false), + : m_interpreter(new QtGui::PythonScript(scriptFileName_)), m_valid(true), m_inputFormat(NotUsed), m_gradients(false), m_ions(false), m_radicals(false), m_unitCells(false) { @@ -101,6 +101,8 @@ void ScriptEnergy::setMolecule(Core::Molecule* mol) options << "-f" << m_tempFile.fileName(); // start the interpreter + //@ todo - check if there was a previous process running + // .. if so, kill it m_interpreter->asyncExecute(options); } @@ -123,6 +125,7 @@ Real ScriptEnergy::value(const Eigen::VectorXd& x) void ScriptEnergy::gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) { + //@ todo read the gradient if available from the process EnergyCalculator::gradient(x, grad); } @@ -290,6 +293,8 @@ void ScriptEnergy::readMetaData() // get the element mask // (if it doesn't exist, the default is no elements anyway) m_valid = parseElements(metaData); + + qDebug() << " finished parsing metadata "; } bool ScriptEnergy::parseString(const QJsonObject& ob, const QString& key, diff --git a/avogadro/qtplugins/forcefield/scripts/gfnff.py b/avogadro/qtplugins/forcefield/scripts/gfnff.py index c6e6223f9c..c415ec5eb3 100644 --- a/avogadro/qtplugins/forcefield/scripts/gfnff.py +++ b/avogadro/qtplugins/forcefield/scripts/gfnff.py @@ -40,6 +40,9 @@ def run(filename): with open(filename, "r") as f: mol_cjson = json.load(f) + with open("/Users/ghutchis/gfnff.log", "a") as f: + f.write(filename + "\n") + # first setup the calculator atoms = np.array(mol_cjson["atoms"]["elements"]["number"]) coord_list = mol_cjson["atoms"]["coords"]["3d"] @@ -67,6 +70,9 @@ def run(filename): # first print the energy of these coordinates print(res.get_energy()) # in Hartree + with open("/Users/ghutchis/gfnff.log", "a") as f: + f.write(str(res.get_energy()) + "\n") + # now print the gradient # .. we don't want the "[]" in the output output = np.array2string(res.get_gradient()) From 78696ba7563419a159daa0abfe163e663fe338bd Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Sun, 22 Oct 2023 15:58:04 -0400 Subject: [PATCH 18/22] Add initial obmm support Signed-off-by: Geoff Hutchison --- avogadro/calc/energycalculator.cpp | 9 +- avogadro/qtgui/pythonscript.cpp | 35 +- avogadro/qtgui/pythonscript.h | 23 +- avogadro/qtplugins/forcefield/CMakeLists.txt | 3 +- avogadro/qtplugins/forcefield/forcefield.cpp | 97 +++-- avogadro/qtplugins/forcefield/forcefield.h | 2 +- avogadro/qtplugins/forcefield/obmmenergy.cpp | 366 ++++++++++++++++++ avogadro/qtplugins/forcefield/obmmenergy.h | 92 +++++ avogadro/qtplugins/forcefield/obmmprocess.cpp | 164 -------- avogadro/qtplugins/forcefield/obmmprocess.h | 163 -------- .../qtplugins/forcefield/scriptenergy.cpp | 65 +++- .../qtplugins/forcefield/scripts/gfnff.py | 26 +- avogadro/qtplugins/forcefield/scripts/uff.py | 25 +- 13 files changed, 654 insertions(+), 416 deletions(-) create mode 100644 avogadro/qtplugins/forcefield/obmmenergy.cpp create mode 100644 avogadro/qtplugins/forcefield/obmmenergy.h delete mode 100644 avogadro/qtplugins/forcefield/obmmprocess.cpp delete mode 100644 avogadro/qtplugins/forcefield/obmmprocess.h diff --git a/avogadro/calc/energycalculator.cpp b/avogadro/calc/energycalculator.cpp index 88eb83cec8..89222b569d 100644 --- a/avogadro/calc/energycalculator.cpp +++ b/avogadro/calc/energycalculator.cpp @@ -20,13 +20,18 @@ void EnergyCalculator::cleanGradients(TVector& grad) unsigned int size = grad.rows(); // check for overflows -- in case of divide by zero, etc. for (unsigned int i = 0; i < size; ++i) { - if (!std::isfinite(grad[i])) { + if (!std::isfinite(grad[i]) || std::isnan(grad[i])) { grad[i] = 0.0; } } // freeze any masked atoms or coordinates - grad = grad.cwiseProduct(m_mask); + /* + if (m_mask.rows() == size) + grad = grad.cwiseProduct(m_mask); + else + std::cerr << "Error: mask size " << m_mask.rows() << " " << grad.rows() << std::endl; + */ } } // namespace Avogadro diff --git a/avogadro/qtgui/pythonscript.cpp b/avogadro/qtgui/pythonscript.cpp index e5e37ab9b4..8b567ceb43 100644 --- a/avogadro/qtgui/pythonscript.cpp +++ b/avogadro/qtgui/pythonscript.cpp @@ -221,37 +221,32 @@ void PythonScript::processFinished(int, QProcess::ExitStatus) emit finished(); } -QByteArray PythonScript::asyncWriteAndResponse(QByteArray input, const int expectedLines) +void PythonScript::asyncTerminate() +{ + if (m_process != nullptr) { + m_process->terminate(); + disconnect(m_process, SIGNAL(finished()), this, SLOT(processFinished())); + m_process->deleteLater(); + m_process = nullptr; + } +} + +QByteArray PythonScript::asyncWriteAndResponse(QByteArray input) { if (m_process == nullptr) { return QByteArray(); // wait } - qDebug() << " writing to process: " << input << " " << expectedLines; - m_process->write(input); QByteArray buffer; - if (expectedLines == -1) { - m_process->waitForFinished(); - buffer = m_process->readAll(); - } else { - // only read a certain number of lines - // (e.g., energy and gradients) - int remainingLines = expectedLines; - bool ready = m_process->waitForReadyRead(); - if (ready) { - while (m_process->canReadLine() && remainingLines > 0) { - buffer += m_process->readLine(); - remainingLines--; - } - // clear the rest of the buffer - qDebug() << " remaining " << m_process->readAll(); + bool ready = m_process->waitForReadyRead(); + if (ready) { + while (m_process->canReadLine()) { + buffer += m_process->readLine(); } } - qDebug() << " read from process: " << buffer; - return buffer; } diff --git a/avogadro/qtgui/pythonscript.h b/avogadro/qtgui/pythonscript.h index b49491076e..4b19d5cdc8 100644 --- a/avogadro/qtgui/pythonscript.h +++ b/avogadro/qtgui/pythonscript.h @@ -12,9 +12,9 @@ #include #include +#include #include #include -#include namespace Avogadro { namespace QtGui { @@ -92,23 +92,26 @@ class AVOGADROQTGUI_EXPORT PythonScript : public QObject * Start a new process to execute asynchronously * " [args ...]", * optionally passing scriptStdin to the processes standard input. - * + * * Will send asyncFinished() signal when finished */ void asyncExecute(const QStringList& args, - const QByteArray& scriptStdin = QByteArray()); + const QByteArray& scriptStdin = QByteArray()); /** * Write input to the asynchronous process' standard input and return the * standard output when ready. Does not wait for the process to terminate * before returning (e.g. "server mode"). - * + * * @param input The input to write to the process' standard input - * @param expectedLines The number of lines to expect in the output. If -1, - * the output is read until the process terminates. * @return The standard output of the process + */ + QByteArray asyncWriteAndResponse(QByteArray input); + + /** + * Terminate the asynchronous process. */ - QByteArray asyncWriteAndResponse(QByteArray input, const int expectedLines = -1); + void asyncTerminate(); /** * Returns the standard output of the asynchronous process when finished. @@ -116,9 +119,9 @@ class AVOGADROQTGUI_EXPORT PythonScript : public QObject QByteArray asyncResponse(); signals: -/** - * The asynchronous execution is finished or timed out - */ + /** + * The asynchronous execution is finished or timed out + */ void finished(); public slots: diff --git a/avogadro/qtplugins/forcefield/CMakeLists.txt b/avogadro/qtplugins/forcefield/CMakeLists.txt index 115725018d..238d46ba9f 100644 --- a/avogadro/qtplugins/forcefield/CMakeLists.txt +++ b/avogadro/qtplugins/forcefield/CMakeLists.txt @@ -1,5 +1,6 @@ set(forcefield_srcs forcefield.cpp + obmmenergy.cpp scriptenergy.cpp ) @@ -18,8 +19,6 @@ set(forcefields scripts/gfn1.py scripts/gfn2.py scripts/gfnff.py - scripts/mmff94.py - scripts/uff.py ) install(PROGRAMS ${forcefields} diff --git a/avogadro/qtplugins/forcefield/forcefield.cpp b/avogadro/qtplugins/forcefield/forcefield.cpp index ba86fa6194..22ed9658c4 100644 --- a/avogadro/qtplugins/forcefield/forcefield.cpp +++ b/avogadro/qtplugins/forcefield/forcefield.cpp @@ -5,6 +5,7 @@ ******************************************************************************/ #include "forcefield.h" +#include "obmmenergy.h" #include "scriptenergy.h" #include @@ -50,7 +51,10 @@ const int constraintAction = 5; Forcefield::Forcefield(QObject* parent_) : ExtensionPlugin(parent_) { - refreshScripts(); + //refreshScripts(); + Calc::EnergyManager::registerModel(new OBMMEnergy("MMFF94")); + Calc::EnergyManager::registerModel(new OBMMEnergy("UFF")); + Calc::EnergyManager::registerModel(new OBMMEnergy("GAFF")); QAction* action = new QAction(this); action->setEnabled(true); @@ -119,7 +123,8 @@ void Forcefield::optimize() bool isInteractive = m_molecule->undoMolecule()->isInteractive(); m_molecule->undoMolecule()->setInteractive(true); - cppoptlib::LbfgsSolver solver; + //cppoptlib::LbfgsSolver solver; + cppoptlib::ConjugatedGradientDescentSolver solver; int n = m_molecule->atomCount(); // we have to cast the current 3d positions into a VectorXd @@ -127,6 +132,7 @@ void Forcefield::optimize() double* p = pos[0].data(); Eigen::Map map(p, 3 * n); Eigen::VectorXd positions = map; + Eigen::VectorXd lastPositions = positions; Eigen::VectorXd gradient = Eigen::VectorXd::Zero(3 * n); // just to get the right size / shape @@ -137,14 +143,14 @@ void Forcefield::optimize() cppoptlib::Criteria crit = cppoptlib::Criteria::defaults(); // e.g., every 5 steps, update coordinates - crit.iterations = m_nSteps; + crit.iterations = 5; // we don't set function or gradient criteria // .. these seem to be broken in the solver code // .. so we handle ourselves solver.setStopCriteria(crit); // set the method - std::string recommended = recommendedForceField(); + std::string recommended = "UFF"; qDebug() << "Energy method: " << recommended.c_str(); if (m_method == nullptr) { @@ -152,39 +158,78 @@ void Forcefield::optimize() m_method = Calc::EnergyManager::instance().model(recommended); } m_method->setMolecule(m_molecule); - m_method->setMask(m_molecule->frozenAtomMask()); + auto mask = m_molecule->frozenAtomMask(); + if (mask.rows() != m_molecule->atomCount()) { + mask = Eigen::VectorXd::Zero(3 * m_molecule->atomCount()); + // set to 1.0 + for (Index i = 0; i < 3 * m_molecule->atomCount(); ++i) { + mask[i] = 1.0; + } + } + m_method->setMask(mask); Real energy = m_method->value(positions); + m_method->gradient(positions, gradient); + qDebug() << " initial " << energy + << " gradNorm: " << gradient.norm() + << " posNorm: " << positions.norm(); + + Real currentEnergy = 0.0; for (unsigned int i = 0; i < m_maxSteps / crit.iterations; ++i) { solver.minimize(*m_method, positions); - Real currentEnergy = m_method->value(positions); + currentEnergy = m_method->value(positions); // get the current gradient for force visualization m_method->gradient(positions, gradient); + qDebug() << " optimize " << i << currentEnergy + << " gradNorm: " << gradient.norm() + << " posNorm: " << positions.norm(); // update coordinates - const double* d = positions.data(); - // casting back would be lovely... - for (size_t i = 0; i < n; ++i) { - pos[i] = Vector3(*(d), *(d + 1), *(d + 2)); - d += 3; - - forces[i] = -1.0 * Vector3(gradient[3 * i], gradient[3 * i + 1], - gradient[3 * i + 2]); + bool isFinite = std::isfinite(currentEnergy); + if (isFinite) { + const double* d = positions.data(); + bool isFinite = true; + // casting back would be lovely... + for (size_t i = 0; i < n; ++i) { + if (!std::isfinite(*d) || !std::isfinite(*(d + 1)) || + !std::isfinite(*(d + 2))) { + isFinite = false; + break; + } + + pos[i] = Vector3(*(d), *(d + 1), *(d + 2)); + d += 3; + + forces[i] = -1.0 * Vector3(gradient[3 * i], gradient[3 * i + 1], + gradient[3 * i + 2]); + } } + // todo - merge these into one undo step - m_molecule->undoMolecule()->setAtomPositions3d(pos, - tr("Optimize Geometry")); - m_molecule->setForceVectors(forces); - Molecule::MoleculeChanges changes = Molecule::Atoms | Molecule::Modified; - m_molecule->emitChanged(changes); - - // check for convergence - if (fabs(gradient.maxCoeff()) < m_gradientTolerance) - break; - if (fabs(currentEnergy - energy) < m_tolerance) - break; - energy = currentEnergy; + if (isFinite) { + qDebug() << " finite! "; + m_molecule->undoMolecule()->setAtomPositions3d(pos, + tr("Optimize Geometry")); + m_molecule->setForceVectors(forces); + Molecule::MoleculeChanges changes = Molecule::Atoms | Molecule::Modified; + m_molecule->emitChanged(changes); + lastPositions = positions; + + // check for convergence + /* + if (fabs(gradient.maxCoeff()) < m_gradientTolerance) + break; + if (fabs(currentEnergy - energy) < m_tolerance) + break; + */ + + energy = currentEnergy; + } else { + // reset to last positions + positions = lastPositions; + gradient = Eigen::VectorXd::Zero(3 * n); + } } m_molecule->undoMolecule()->setInteractive(isInteractive); diff --git a/avogadro/qtplugins/forcefield/forcefield.h b/avogadro/qtplugins/forcefield/forcefield.h index 2388d24f95..3cf7f8fd14 100644 --- a/avogadro/qtplugins/forcefield/forcefield.h +++ b/avogadro/qtplugins/forcefield/forcefield.h @@ -80,7 +80,7 @@ private slots: // defaults Minimizer m_minimizer = LBFGS; - unsigned int m_maxSteps = 250; + unsigned int m_maxSteps = 100; unsigned int m_nSteps = 5; double m_tolerance = 1.0e-6; double m_gradientTolerance = 1.0e-4; diff --git a/avogadro/qtplugins/forcefield/obmmenergy.cpp b/avogadro/qtplugins/forcefield/obmmenergy.cpp new file mode 100644 index 0000000000..42af19a97e --- /dev/null +++ b/avogadro/qtplugins/forcefield/obmmenergy.cpp @@ -0,0 +1,366 @@ +/****************************************************************************** + This source file is part of the Avogadro project. + This source code is released under the 3-Clause BSD License, (see "LICENSE"). +******************************************************************************/ + +#include "obmmenergy.h" + +#include + +#include + +#include +#include +#include +#include + +namespace Avogadro::QtPlugins { + +class OBMMEnergy::ProcessListener : public QObject +{ + Q_OBJECT +public: + ProcessListener(QProcess *proc) : QObject(), m_finished(false), m_process(proc) + { + connect(m_process, SIGNAL(readyRead()), SLOT(readyRead())); + } + + bool waitForOutput(QByteArray output, int msTimeout = 2000) + { + if (!wait(msTimeout)) + return false; + + // success! + output = m_output; + return true; + } + +public slots: + void readyRead() + { + m_finished = true; + m_output = m_process->readAllStandardOutput(); + } + +private: + bool wait(int msTimeout) + { + QTimer timer; + timer.start(msTimeout); + + while (timer.isActive() && !m_finished) + qApp->processEvents(QEventLoop::AllEvents, 500); + + return m_finished; + } + + QProcess* m_process; + bool m_finished; + QByteArray m_output; +}; + +OBMMEnergy::OBMMEnergy(const std::string& method) + : m_identifier(method), m_name(method), m_process(nullptr), +#if defined(_WIN32) + m_executable("obmm.exe") +#else + m_executable("obmm") +#endif +{ + // eventually CJSON might be nice + m_inputFormat = new Io::CmlFormat; + + if (method == "UFF") { + m_description = tr("Universal Force Field"); + m_elements.reset(); + for (unsigned int i = 1; i < 102; ++i) + m_elements.set(i); + } else if (method == "GAFF") { + m_description = tr("Generalized Amber Force Field"); + + // H, C, N, O, F, P, S, Cl, Br, and I + m_elements.set(1); + m_elements.set(6); + m_elements.set(7); + m_elements.set(8); + m_elements.set(9); + m_elements.set(15); + m_elements.set(16); + m_elements.set(17); + m_elements.set(35); + m_elements.set(53); + } else if (method == "MMFF94") { + m_description = tr("Merck Molecular Force Field 94"); + m_elements.reset(); + + // H, C, N, O, F, Si, P, S, Cl, Br, and I + m_elements.set(1); + m_elements.set(6); + m_elements.set(7); + m_elements.set(8); + m_elements.set(9); + m_elements.set(14); + m_elements.set(15); + m_elements.set(16); + m_elements.set(17); + m_elements.set(35); + m_elements.set(53); + } +} + +OBMMEnergy::~OBMMEnergy() +{ + delete m_inputFormat; + delete m_process; +} + +void OBMMEnergy::setupProcess() +{ + if (m_process != nullptr) { + m_process->kill(); + delete m_process; + } + + m_process = new QProcess(); + + // Read the AVO_OBMM_EXECUTABLE env var to optionally override the + // executable used. + QByteArray obmmExec = qgetenv("AVO_OBMM_EXECUTABLE"); + if (!obmmExec.isEmpty()) { + m_executable = obmmExec; + } else { + // If not overridden, look for an obmm next to the executable. + QDir baseDir(QCoreApplication::applicationDirPath()); + if (!baseDir.absolutePath().startsWith("/usr/") && + QFileInfo(baseDir.absolutePath() + '/' + m_executable).exists()) { + m_executable = baseDir.absolutePath() + '/' + m_executable; + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); +#if defined(_WIN32) + env.insert("BABEL_DATADIR", + QCoreApplication::applicationDirPath() + "/data"); +#else + QDir dir(QCoreApplication::applicationDirPath() + "/../share/openbabel"); + QStringList filters; + filters << "3.*"; + QStringList dirs = dir.entryList(filters); + if (dirs.size() == 1) { + env.insert("BABEL_DATADIR", QCoreApplication::applicationDirPath() + + "/../share/openbabel/" + dirs[0]); + } else { + qDebug() << "Error, Open Babel data directory not found."; + } + dir.setPath(QCoreApplication::applicationDirPath() + "/../lib/openbabel"); + dirs = dir.entryList(filters); + if (dirs.size() == 1) { + env.insert("BABEL_LIBDIR", QCoreApplication::applicationDirPath() + + "/../lib/openbabel/" + dirs[0]); + } else { + qDebug() << "Error, Open Babel plugins directory not found."; + } +#endif + m_process->setProcessEnvironment(env); + } + } +} + +Calc::EnergyCalculator* OBMMEnergy::newInstance() const +{ + return new OBMMEnergy(m_name); +} + +void OBMMEnergy::setMolecule(Core::Molecule* mol) +{ + m_molecule = mol; + + // should check if the molecule is valid for this script + // .. this should never happen, but let's be defensive + if (mol == nullptr) { + return; // nothing to do + } + + setupProcess(); + + // start the process + // we need a tempory file to write the molecule + // get a temporary filename + QString tempPath = QDir::tempPath(); + if (!tempPath.endsWith(QDir::separator())) + tempPath += QDir::separator(); + QString tempPattern = tempPath + "avogadroOBMMXXXXXX.cml"; + m_tempFile.setFileTemplate(tempPattern); + if (!m_tempFile.open()) { + // appendError("Error creating temporary file."); + return; + } + + // write the molecule + m_inputFormat->writeFile(m_tempFile.fileName().toStdString(), *mol); + m_tempFile.close(); + + // start the process + m_process->start(m_executable, QStringList() << m_tempFile.fileName()); + if (!m_process->waitForStarted()) { + // appendError("Error starting process."); + return; + } + ProcessListener listener(m_process); + QByteArray result; + if (!listener.waitForOutput(result)) { + // appendError("Error running process."); + return; + } + + // okay, we need to write "load " to the interpreter + // and then read the response + QByteArray input = "load " + m_tempFile.fileName().toLocal8Bit() + "\n"; + m_process->write(input); + if (!listener.waitForOutput(result)) { + // appendError("Error running process."); + return; + } + qDebug() << "OBMM: " << result; + + // set the method m_identifier.c_str() + + input = QByteArray("ff MMFF94\n"); + m_process->write(input); + if (!listener.waitForOutput(result)) { + // appendError("Error running process."); + return; + } + qDebug() << "OBMM ff: " << result; + + // check for an energy + input = QByteArray("energy\n"); + result.clear(); + m_process->write(input); + if (!listener.waitForOutput(result)) { + // appendError("Error running process."); + return; + } + qDebug() << "OBMM energy: " << result; +} + +Real OBMMEnergy::value(const Eigen::VectorXd& x) +{ + if (m_molecule == nullptr || m_process == nullptr) + return 0.0; // nothing to do + + m_process->waitForReadyRead(); + QByteArray result; + while (!result.contains("command >")) { + result += m_process->readLine(); + } + qDebug() << " starting " << result; + + // write the new coordinates and read the energy + QByteArray input = "coord\n"; + for (Index i = 0; i < x.size(); i += 3) { + // write as x y z (space separated) + input += QString::number(x[i]) + " " + QString::number(x[i + 1]) + " " + + QString::number(x[i + 2]) + "\n"; + } + input += "\n"; + + m_process->write(input); + m_process->waitForBytesWritten(); + m_process->waitForReadyRead(); + result.clear(); + while (!result.contains("command >")) { + result += m_process->readLine(); + } + + qDebug() << " asking energy " << result << m_process->state(); + + // now ask for the energy + input = "energy\n\n"; + result.clear(); + m_process->write(input); + m_process->waitForBytesWritten(); + m_process->waitForReadyRead(); + while (!result.contains("command >")) { + result += m_process->readLine(); + } + + qDebug() << "OBMM: " << result; + + // go through lines in result until we see "total energy" + QStringList lines = QString(result).remove('\r').split('\n'); + double energy = 0.0; + for (auto line : lines) { + if (line.contains("total energy =")) { + qDebug() << " OBMM: " << line; + QStringList items = line.split(" ", Qt::SkipEmptyParts); + if (items.size() > 4) + energy = items[3].toDouble(); + } + } + + qDebug() << " OBMM: " << energy << " done"; + + return energy; // if conversion fails, returns 0.0 +} + +void OBMMEnergy::gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) +{ + if (m_molecule == nullptr || m_process == nullptr) + return; + + EnergyCalculator::gradient(x, grad); + return; + + qDebug() << "OBMM: gradient"; + + // write the new coordinates and read the energy + QByteArray input = "coord\n"; + for (Index i = 0; i < x.size(); i += 3) { + // write as x y z (space separated) + input += QString::number(x[i]) + " " + QString::number(x[i + 1]) + " " + + QString::number(x[i + 2]) + "\n"; + } + + m_process->write(input); + qDebug() << "OBMM Grad wrote coords"; + m_process->waitForReadyRead(); + QByteArray result; + while (m_process->canReadLine()) { + result += m_process->readLine(); + } + + qDebug() << "OBMM: " << result; + + // now ask for the energy + input = "grad\n"; + m_process->write(input); + m_process->waitForReadyRead(); + while (m_process->canReadLine()) { + result += m_process->readLine(); + } + + qDebug() << "OBMM: " << result; + + // go through lines in result until we see "gradient " + QStringList lines = QString(result).remove('\r').split('\n'); + bool readingGradient = false; + unsigned int i = 0; + for (auto line : lines) { + if (line.contains("gradient")) { + readingGradient = true; + continue; + } + if (readingGradient) { + QStringList items = line.split(" ", Qt::SkipEmptyParts); + if (items.size() == 3) { + grad[3 * i] = items[0].toDouble(); + grad[3 * i + 1] = items[1].toDouble(); + grad[3 * i + 2] = items[2].toDouble(); + ++i; + } + } + } + + cleanGradients(grad); +} + +} // namespace Avogadro::QtPlugins + +#include "obmmenergy.moc" diff --git a/avogadro/qtplugins/forcefield/obmmenergy.h b/avogadro/qtplugins/forcefield/obmmenergy.h new file mode 100644 index 0000000000..1c6bf2e902 --- /dev/null +++ b/avogadro/qtplugins/forcefield/obmmenergy.h @@ -0,0 +1,92 @@ +/****************************************************************************** + 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_OBMMENERGY_H +#define AVOGADRO_QTPLUGINS_OBMMENERGY_H + +#include + +#include + +#include +#include +#include +#include + +class QJsonObject; + +namespace Avogadro { + +namespace Io { +class FileFormat; +} + +namespace QtGui { +class PythonScript; +} + +namespace QtPlugins { + +class OBMMEnergy : public Avogadro::Calc::EnergyCalculator +{ + Q_DECLARE_TR_FUNCTIONS(OBMMEnergy) + +public: + /** Formats that may be written to the script's input. */ + enum Format + { + NotUsed, + Cjson, + Cml, + Mdl, // sdf + Pdb, + Xyz + }; + + OBMMEnergy(const std::string& method = ""); + ~OBMMEnergy() override; + + std::string method() const { return m_identifier; } + void setupProcess(); + + Calc::EnergyCalculator* newInstance() const override; + + std::string identifier() const override { return m_identifier; } + std::string name() const override { return m_name; } + std::string description() const override { return m_description.toStdString(); } + + Core::Molecule::ElementMask elements() const override { return (m_elements); } + + // This will check if the molecule is valid for this script + // and then start the external process + void setMolecule(Core::Molecule* mol) override; + // energy + Real value(const Eigen::VectorXd& x) override; + // gradient (which may be unsupported and fall back to numeric) + void gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) override; + + /** + * @brief Synchronous use of the QProcess. + */ + class ProcessListener; + +private: + Core::Molecule* m_molecule; + Io::FileFormat* m_inputFormat; + QProcess* m_process; + QString m_executable; + + Core::Molecule::ElementMask m_elements; + std::string m_identifier; + std::string m_name; + QString m_description; + + QTemporaryFile m_tempFile; +}; + +} // namespace QtPlugins +} // namespace Avogadro + +#endif // AVOGADRO_QTPLUGINS_OBMMENERGY_H diff --git a/avogadro/qtplugins/forcefield/obmmprocess.cpp b/avogadro/qtplugins/forcefield/obmmprocess.cpp deleted file mode 100644 index 5fae2b990f..0000000000 --- a/avogadro/qtplugins/forcefield/obmmprocess.cpp +++ /dev/null @@ -1,164 +0,0 @@ -/****************************************************************************** - - This source file is part of the Avogadro project. - - Copyright 2019 Geoffrey R. Hutchison - - This source code is released under the New BSD License, (the "License"). - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -******************************************************************************/ - -#include "OBMMProcess.h" - -#include -#include -#include -#include -#include -#include - -namespace Avogadro { -namespace QtPlugins { - -OBMMProcess::OBMMProcess(QObject* parent_) - : QObject(parent_), m_processLocked(false), m_aborted(false), - m_process(new QProcess(this)), -#if defined(_WIN32) - m_obmmExecutable("obmm.exe") -#else - m_obmmExecutable("obmm") -#endif -{ - // Read the AVO_OBABEL_EXECUTABLE env var to optionally override the - // executable used. - QByteArray obmmExec = qgetenv("AVO_OBMM_EXECUTABLE"); - if (!obabelExec.isEmpty()) { - m_obmmExecutable = obmmExec; - } else { - // If not overridden, look for an obabel next to the executable. - QDir baseDir(QCoreApplication::applicationDirPath()); - if (!baseDir.absolutePath().startsWith("/usr/") && - QFileInfo(baseDir.absolutePath() + '/' + m_obabelExecutable).exists()) { - m_obabelExecutable = baseDir.absolutePath() + '/' + m_obmmExecutable; - QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); -#if defined(_WIN32) - env.insert("BABEL_DATADIR", - QCoreApplication::applicationDirPath() + "/data"); -#else - QDir dir(QCoreApplication::applicationDirPath() + "/../share/openbabel"); - QStringList filters; - filters << "2.*"; - QStringList dirs = dir.entryList(filters); - if (dirs.size() == 1) { - env.insert("BABEL_DATADIR", QCoreApplication::applicationDirPath() + - "/../share/openbabel/" + dirs[0]); - } else { - qDebug() << "Error, Open Babel data directory not found."; - } - dir.setPath(QCoreApplication::applicationDirPath() + "/../lib/openbabel"); - dirs = dir.entryList(filters); - if (dirs.size() == 1) { - env.insert("BABEL_LIBDIR", QCoreApplication::applicationDirPath() + - "/../lib/openbabel/" + dirs[0]); - } else { - qDebug() << "Error, Open Babel plugins directory not found."; - } -#endif - m_process->setProcessEnvironment(env); - } - } -} - -void OBMMProcess::abort() -{ - m_aborted = true; - emit aborted(); -} - -void OBMMProcess::obError() -{ - qDebug() << "Process encountered an error, and did not execute correctly."; - if (m_process) { - qDebug() << "\tExit code:" << m_process->exitCode(); - qDebug() << "\tExit status:" << m_process->exitStatus(); - qDebug() << "\tExit output:" << m_process->readAll(); - } -} - -bool OBMMProcess::queryForceFields() -{ - if (!tryLockProcess()) { - qWarning() << "OBMMProcess::queryForceFields(): process already in use."; - return false; - } - - QStringList options; - options << "-L" - << "forcefields"; - - executeObabel(options, this, SLOT(queryForceFieldsPrepare())); - return true; -} - -void OBMMProcess::queryForceFieldsPrepare() -{ - if (m_aborted) { - releaseProcess(); - return; - } - - QMap result; - - QString output = QString::fromLatin1(m_process->readAllStandardOutput()); - - QRegExp parser("([^\\s]+)\\s+(\\S[^\\n]*[^\\n\\.]+)\\.?\\n"); - int pos = 0; - while ((pos = parser.indexIn(output, pos)) != -1) { - QString key = parser.cap(1); - QString desc = parser.cap(2); - result.insertMulti(key, desc); - pos += parser.matchedLength(); - } - - releaseProcess(); - emit queryForceFieldsFinished(result); -} - -void OBMMProcess::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(error(QProcess::ProcessError)), receiver, slot); - connect(m_process, SIGNAL(error(QProcess::ProcessError)), this, - SLOT(obError())); - } - - // Start process - qDebug() << "OBMMProcess::executeObabel: " - "Running" - << m_obabelExecutable << options.join(" "); - m_process->start(m_obabelExecutable, options); - if (!obabelStdin.isNull()) { - m_process->write(obabelStdin); - m_process->closeWriteChannel(); - } -} - -void OBMMProcess::resetState() -{ - m_aborted = false; - m_process->disconnect(this); - disconnect(m_process); - connect(this, SIGNAL(aborted()), m_process, SLOT(kill())); -} - -} // namespace QtPlugins -} // namespace Avogadro diff --git a/avogadro/qtplugins/forcefield/obmmprocess.h b/avogadro/qtplugins/forcefield/obmmprocess.h deleted file mode 100644 index 87e026d5f3..0000000000 --- a/avogadro/qtplugins/forcefield/obmmprocess.h +++ /dev/null @@ -1,163 +0,0 @@ -/****************************************************************************** - - This source file is part of the Avogadro project. - - Copyright 2019 Geoffrey R. Hutchison - - This source code is released under the New BSD License, (the "License"). - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -******************************************************************************/ - -#ifndef AVOGADRO_QTPLUGINS_OBMMPROCESS_H -#define AVOGADRO_QTPLUGINS_OBMMPROCESS_H - -#include -#include -#include - -class QProcess; - -namespace Avogadro { -namespace QtPlugins { - -/** - * @brief The OBMMProcess class provides an interface to the `obmm` executable, - * which is run in a separate process. - * - * The `obmm` executable used by this class can be overridden by setting the - * AVO_OBABEL_EXECUTABLE environment variable. - */ -class OBMMProcess : public QObject -{ - Q_OBJECT -public: - explicit OBMMProcess(QObject* parent_ = 0); - - /** - * @name Process Management - * Methods, slots, and signals used to interact with the OpenBabel process. - * @{ - */ -public: - /** - * The `obmm` executable used by the process. - */ - QString obmmExecutable() const { return m_obmmExecutable; } - - /** - * @return True if the process is in use, false otherwise. - */ - bool inUse() const { return m_processLocked; } - -public slots: - /** - * Abort any currently running processes. - * - * This will cause aborted() to be emitted, but not any of the - * operation-specific "finished" signals. - */ - void abort(); - - /** - * Called when an error in the process occurs. - */ - void obError(); - -signals: - /** - * Emitted when the abort() method has been called. - */ - void aborted(); - - // end Process Management doxygen group - /**@}*/ - -public slots: - /** - * Request a list of all supported force fields from obabel. - * - * After calling this method, the queryForceFieldsFinished signal will be - * emitted. This method executes - * - * `obabel -L forcefields` - * - * and parses the output. - * - * If an error occurs, queryReadFormatsFinished will be emitted with an empty - * argument. - * - * @return True if the process started successfully, false otherwise. - */ - bool queryForceFields(); - -signals: - /** - * Triggered when the process started by queryForceFields() completes. - * @param forceFields The force fields supported by OpenBabel. Keys - * are unique identifiers for the force fields, and the values are - * non-translated (english), human-readable descriptions. - * - * If an error occurs, forceFields will be empty. - */ - void queryForceFieldsFinished(const QMap& forceFields); - -private slots: - void queryForceFieldsPrepare(); - - -private: - /** - * Internal method for launching the obmm executable. - * @param options List of options to pass to QProcess::start - * @param receiver A QObject subclass instance that has @a slot as a member. - * @param slot The slot to call when completed. Must have no arguments. - * @param obmmStdin Standard input for the obmm process (optional). - * - * Call this method like so: -@code -QStringList options; - -executeobmm(options, this, SLOT(mySlot())); -@endcode - * - * @a slot will be connected to QProcess::finished(int) and - * QProcess::error(QProcess::ProcessError) with @a receiver as receiver and - * @a m_process as sender. @a m_process is then started using - * m_obmmExecutable and options as arguments. If provided, the obmmStdin - * data will be written to the obmm stdin channel. - */ - void executeobmm(const QStringList& options, QObject* receiver = nullptr, - const char* slot = nullptr, - const QByteArray& obmmStdin = QByteArray()); - - void resetState(); - - // Not thread safe -- just uses a bool. - bool tryLockProcess() - { - if (m_processLocked) - return false; - m_processLocked = true; - resetState(); - return true; - } - - void releaseProcess() { m_processLocked = false; } - - bool m_processLocked; - bool m_aborted; - QProcess* m_process; - QString m_obmmExecutable; - -}; - -} // namespace QtPlugins -} // namespace Avogadro - -#endif // AVOGADRO_QTPLUGINS_OBMMProcess_H diff --git a/avogadro/qtplugins/forcefield/scriptenergy.cpp b/avogadro/qtplugins/forcefield/scriptenergy.cpp index d5e09dbfda..939d6ca1b8 100644 --- a/avogadro/qtplugins/forcefield/scriptenergy.cpp +++ b/avogadro/qtplugins/forcefield/scriptenergy.cpp @@ -100,9 +100,9 @@ void ScriptEnergy::setMolecule(Core::Molecule* mol) QStringList options; options << "-f" << m_tempFile.fileName(); + // if there was a previous process, kill it + m_interpreter->asyncTerminate(); // start the interpreter - //@ todo - check if there was a previous process running - // .. if so, kill it m_interpreter->asyncExecute(options); } @@ -118,15 +118,66 @@ Real ScriptEnergy::value(const Eigen::VectorXd& x) input += QString::number(x[i]) + " " + QString::number(x[i + 1]) + " " + QString::number(x[i + 2]) + "\n"; } - QByteArray result = m_interpreter->asyncWriteAndResponse(input, 1); + 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(" "); + if (items.size() > 1) + energy = items[1].toDouble(); + } + } - return result.toDouble(); // if conversion fails, returns 0.0 + return energy; // if conversion fails, returns 0.0 } void ScriptEnergy::gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) { - //@ todo read the gradient if available from the process - EnergyCalculator::gradient(x, grad); + if (!m_gradients) { + EnergyCalculator::gradient(x, grad); + return; + } + + // 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]) + " " + QString::number(x[i + 1]) + " " + + QString::number(x[i + 2]) + "\n"; + } + QByteArray result = m_interpreter->asyncWriteAndResponse(input); + + // parse the result + // first split on newlines + QStringList lines = QString(result).remove('\r').split('\n'); + double energy = 0.0; + unsigned int i = 0; + bool readingGrad = false; + for (auto line : lines) { + if (line.startsWith("AvogadroGradient:")) { + readingGrad = true; + continue; // next line + } + + if (readingGrad) { + QStringList items = line.split(" "); + 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; + } + } + + cleanGradients(grad); } ScriptEnergy::Format ScriptEnergy::stringToFormat(const std::string& str) @@ -293,8 +344,6 @@ void ScriptEnergy::readMetaData() // get the element mask // (if it doesn't exist, the default is no elements anyway) m_valid = parseElements(metaData); - - qDebug() << " finished parsing metadata "; } bool ScriptEnergy::parseString(const QJsonObject& ob, const QString& key, diff --git a/avogadro/qtplugins/forcefield/scripts/gfnff.py b/avogadro/qtplugins/forcefield/scripts/gfnff.py index c415ec5eb3..24b2b3876f 100644 --- a/avogadro/qtplugins/forcefield/scripts/gfnff.py +++ b/avogadro/qtplugins/forcefield/scripts/gfnff.py @@ -67,18 +67,6 @@ def run(filename): # we loop forever - Avogadro will kill our process when done while True: - # first print the energy of these coordinates - print(res.get_energy()) # in Hartree - - with open("/Users/ghutchis/gfnff.log", "a") as f: - f.write(str(res.get_energy()) + "\n") - - # now print the gradient - # .. we don't want the "[]" in the output - output = np.array2string(res.get_gradient()) - output = output.replace("[", "").replace("]", "") - print(output) - # read new coordinates from stdin for i in range(len(atoms)): coordinates[i] = np.fromstring(input(), sep=" ") @@ -89,6 +77,20 @@ def run(filename): calc.update(coordinates) calc.singlepoint(res) + # first print the energy of these coordinates + print("AvogadroEnergy:", res.get_energy()) # in Hartree + + with open("/Users/ghutchis/gfnff.log", "a") as f: + f.write(str(res.get_energy()) + "\n") + + # now print the gradient + # .. we don't want the "[]" in the output + print("AvogadroGradient:") + grad = res.get_gradient() * 4961.475 + output = np.array2string(grad) + output = output.replace("[", "").replace("]", "") + print(output) + if __name__ == "__main__": parser = argparse.ArgumentParser("GFN2 calculator") diff --git a/avogadro/qtplugins/forcefield/scripts/uff.py b/avogadro/qtplugins/forcefield/scripts/uff.py index 7d35226262..a2d7945902 100644 --- a/avogadro/qtplugins/forcefield/scripts/uff.py +++ b/avogadro/qtplugins/forcefield/scripts/uff.py @@ -47,23 +47,32 @@ def run(filename): # we loop forever - Avogadro will kill the process when done num_atoms = len(mol.atoms) while True: - # first print the energy of these coordinates - print(ff.Energy(True)) # in Hartree - - # now print the gradient on each atom - for atom in mol.atoms: - grad = ff.GetGradient(atom.OBAtom) - print(grad.GetX(), grad.GetY(), grad.GetZ()) - # 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]) + with open("/Users/ghutchis/uff.log", "a") as f: + f.write("Coordinates:\n") + for atom in mol.atoms: + f.write(str(atom.OBAtom.GetX()) + " " + str(atom.OBAtom.GetY()) + " " + str(atom.OBAtom.GetZ()) + "\n") + # update the molecule geometry for the next energy ff.SetCoordinates(mol.OBMol) + # first print the energy of these coordinates + energy = ff.Energy(True) # in Hartree + print("AvogadroEnergy:", energy) # in Hartree + with open("/Users/ghutchis/uff.log", "a") as f: + f.write(str(energy) + "\n") + + # now print the gradient on each atom + print("AvogadroGradient:") + for atom in mol.atoms: + grad = ff.GetGradient(atom.OBAtom) + print(grad.GetX(), grad.GetY(), grad.GetZ()) + if __name__ == "__main__": parser = argparse.ArgumentParser("UFF calculator") From 10d7d4025b0d4f63924341396a898dd3d5c48657 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Sun, 22 Oct 2023 20:12:35 -0400 Subject: [PATCH 19/22] Continue working on scripts Signed-off-by: Geoff Hutchison --- avogadro/qtplugins/forcefield/forcefield.cpp | 14 +-- avogadro/qtplugins/forcefield/obmmenergy.cpp | 13 +-- avogadro/qtplugins/forcefield/scripts/gaff.py | 88 +++++++++++++++++++ avogadro/qtplugins/forcefield/scripts/gfn1.py | 22 ++--- avogadro/qtplugins/forcefield/scripts/gfn2.py | 22 ++--- .../qtplugins/forcefield/scripts/gfnff.py | 5 +- .../qtplugins/forcefield/scripts/mmff94.py | 19 ++-- avogadro/qtplugins/forcefield/scripts/uff.py | 11 +-- 8 files changed, 141 insertions(+), 53 deletions(-) create mode 100644 avogadro/qtplugins/forcefield/scripts/gaff.py diff --git a/avogadro/qtplugins/forcefield/forcefield.cpp b/avogadro/qtplugins/forcefield/forcefield.cpp index 22ed9658c4..217c343c23 100644 --- a/avogadro/qtplugins/forcefield/forcefield.cpp +++ b/avogadro/qtplugins/forcefield/forcefield.cpp @@ -51,10 +51,12 @@ const int constraintAction = 5; Forcefield::Forcefield(QObject* parent_) : ExtensionPlugin(parent_) { - //refreshScripts(); + refreshScripts(); + /*/ Calc::EnergyManager::registerModel(new OBMMEnergy("MMFF94")); Calc::EnergyManager::registerModel(new OBMMEnergy("UFF")); Calc::EnergyManager::registerModel(new OBMMEnergy("GAFF")); + */ QAction* action = new QAction(this); action->setEnabled(true); @@ -124,7 +126,8 @@ void Forcefield::optimize() m_molecule->undoMolecule()->setInteractive(true); //cppoptlib::LbfgsSolver solver; - cppoptlib::ConjugatedGradientDescentSolver solver; + //cppoptlib::ConjugatedGradientDescentSolver solver; + cppoptlib::GradientDescentSolver solver; int n = m_molecule->atomCount(); // we have to cast the current 3d positions into a VectorXd @@ -142,15 +145,15 @@ void Forcefield::optimize() // Create a Criteria class so we can update coords every N steps cppoptlib::Criteria crit = cppoptlib::Criteria::defaults(); - // e.g., every 5 steps, update coordinates - crit.iterations = 5; + // e.g., every N steps, update coordinates + crit.iterations = 10; // we don't set function or gradient criteria // .. these seem to be broken in the solver code // .. so we handle ourselves solver.setStopCriteria(crit); // set the method - std::string recommended = "UFF"; + std::string recommended = "MMFF94"; qDebug() << "Energy method: " << recommended.c_str(); if (m_method == nullptr) { @@ -208,7 +211,6 @@ void Forcefield::optimize() // todo - merge these into one undo step if (isFinite) { - qDebug() << " finite! "; m_molecule->undoMolecule()->setAtomPositions3d(pos, tr("Optimize Geometry")); m_molecule->setForceVectors(forces); diff --git a/avogadro/qtplugins/forcefield/obmmenergy.cpp b/avogadro/qtplugins/forcefield/obmmenergy.cpp index 42af19a97e..fc633876ad 100644 --- a/avogadro/qtplugins/forcefield/obmmenergy.cpp +++ b/avogadro/qtplugins/forcefield/obmmenergy.cpp @@ -22,16 +22,18 @@ class OBMMEnergy::ProcessListener : public QObject public: ProcessListener(QProcess *proc) : QObject(), m_finished(false), m_process(proc) { - connect(m_process, SIGNAL(readyRead()), SLOT(readyRead())); } bool waitForOutput(QByteArray output, int msTimeout = 2000) { + connect(m_process, SIGNAL(readyRead()), SLOT(readyRead())); if (!wait(msTimeout)) return false; // success! output = m_output; + disconnect(m_process, nullptr, nullptr, nullptr); + m_finished = false; return true; } @@ -209,6 +211,7 @@ void OBMMEnergy::setMolecule(Core::Molecule* mol) // appendError("Error running process."); return; } + qDebug() << "OBMM start: " << result; // okay, we need to write "load " to the interpreter // and then read the response @@ -233,9 +236,9 @@ void OBMMEnergy::setMolecule(Core::Molecule* mol) input = QByteArray("energy\n"); result.clear(); m_process->write(input); - if (!listener.waitForOutput(result)) { - // appendError("Error running process."); - return; + result.clear(); + while (!result.contains("command >")) { + result += m_process->readLine(); } qDebug() << "OBMM energy: " << result; } @@ -262,7 +265,6 @@ Real OBMMEnergy::value(const Eigen::VectorXd& x) input += "\n"; m_process->write(input); - m_process->waitForBytesWritten(); m_process->waitForReadyRead(); result.clear(); while (!result.contains("command >")) { @@ -275,7 +277,6 @@ Real OBMMEnergy::value(const Eigen::VectorXd& x) input = "energy\n\n"; result.clear(); m_process->write(input); - m_process->waitForBytesWritten(); m_process->waitForReadyRead(); while (!result.contains("command >")) { result += m_process->readLine(); diff --git a/avogadro/qtplugins/forcefield/scripts/gaff.py b/avogadro/qtplugins/forcefield/scripts/gaff.py new file mode 100644 index 0000000000..9c34087c2e --- /dev/null +++ b/avogadro/qtplugins/forcefield/scripts/gaff.py @@ -0,0 +1,88 @@ +# 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 + +try: + from openbabel import pybel + import numpy as np + + imported = True +except ImportError: + imported = False + + +def getMetaData(): + # before we return metadata, make sure xtb is in the path + if not imported: + return {} # Avogadro will ignore us now + + metaData = { + "name": "GAFF", + "identifier": "GAFF", + "description": "Calculate GAFF energies and gradients", + "inputFormat": "cml", + "elements": "1,6-9,14-17,35,53", + "unitCell": False, + "gradients": True, + "ion": False, + "radical": False, + } + return metaData + + +def run(filename): + # we get the molecule from the supplied filename + # in cjson format (it's a temporary file created by Avogadro) + mol = next(pybel.readfile("cml", filename)) + + ff = pybel._forcefields["gaff"] + success = ff.Setup(mol.OBMol) + if not success: + # should never happen, but just in case + sys.exit("GAFF setup failed") + + # we loop forever - Avogadro will kill the process when done + 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]) + + # 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 + + # now print the gradient on each atom + print("AvogadroGradient:") + for atom in mol.atoms: + grad = ff.GetGradient(atom.OBAtom) + print(grad.GetX(), grad.GetY(), grad.GetZ()) + + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("GAFF calculator") + parser.add_argument("--display-name", action="store_true") + parser.add_argument("--metadata", action="store_true") + parser.add_argument("-f", "--file", nargs=1) + parser.add_argument("--lang", nargs="?", default="en") + args = vars(parser.parse_args()) + + if args["metadata"]: + print(json.dumps(getMetaData())) + elif args["display_name"]: + name = getMetaData().get("name") + if name: + print(name) + else: + sys.exit("pybel is unavailable") + elif args["file"]: + run(args["file"][0]) diff --git a/avogadro/qtplugins/forcefield/scripts/gfn1.py b/avogadro/qtplugins/forcefield/scripts/gfn1.py index 3673b1a8ad..14e2e83a07 100644 --- a/avogadro/qtplugins/forcefield/scripts/gfn1.py +++ b/avogadro/qtplugins/forcefield/scripts/gfn1.py @@ -62,25 +62,27 @@ def run(filename): # we loop forever - Avogadro will kill the process when done while(True): - # first print the energy of these coordinates - print(res.get_energy()) # in Hartree - - # now print the gradient - # .. we don't want the "[]" in the output - output = np.array2string(res.get_gradient()) - output = output.replace("[", "").replace("]", "") - print(output) - # 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 - + # 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 + + # 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 __name__ == "__main__": parser = argparse.ArgumentParser("GFN1 calculator") diff --git a/avogadro/qtplugins/forcefield/scripts/gfn2.py b/avogadro/qtplugins/forcefield/scripts/gfn2.py index e7035d147b..46cd1402c9 100644 --- a/avogadro/qtplugins/forcefield/scripts/gfn2.py +++ b/avogadro/qtplugins/forcefield/scripts/gfn2.py @@ -62,25 +62,27 @@ def run(filename): # we loop forever - Avogadro will kill the process when done while(True): - # first print the energy of these coordinates - print(res.get_energy()) # in Hartree - - # now print the gradient - # .. we don't want the "[]" in the output - output = np.array2string(res.get_gradient()) - output = output.replace("[", "").replace("]", "") - print(output) - # 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 - + # 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 + + # 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 __name__ == "__main__": parser = argparse.ArgumentParser("GFN2 calculator") diff --git a/avogadro/qtplugins/forcefield/scripts/gfnff.py b/avogadro/qtplugins/forcefield/scripts/gfnff.py index 24b2b3876f..912e9ce911 100644 --- a/avogadro/qtplugins/forcefield/scripts/gfnff.py +++ b/avogadro/qtplugins/forcefield/scripts/gfnff.py @@ -80,13 +80,10 @@ def run(filename): # first print the energy of these coordinates print("AvogadroEnergy:", res.get_energy()) # in Hartree - with open("/Users/ghutchis/gfnff.log", "a") as f: - f.write(str(res.get_energy()) + "\n") - # now print the gradient # .. we don't want the "[]" in the output print("AvogadroGradient:") - grad = res.get_gradient() * 4961.475 + grad = res.get_gradient() * 4961.475 # convert units output = np.array2string(grad) output = output.replace("[", "").replace("]", "") print(output) diff --git a/avogadro/qtplugins/forcefield/scripts/mmff94.py b/avogadro/qtplugins/forcefield/scripts/mmff94.py index e0f826e3da..271b29fe4b 100644 --- a/avogadro/qtplugins/forcefield/scripts/mmff94.py +++ b/avogadro/qtplugins/forcefield/scripts/mmff94.py @@ -47,14 +47,6 @@ def run(filename): # we loop forever - Avogadro will kill the process when done num_atoms = len(mol.atoms) while True: - # first print the energy of these coordinates - print(ff.Energy(True)) # in Hartree - - # now print the gradient on each atom - for atom in mol.atoms: - grad = ff.GetGradient(atom.OBAtom) - print(grad.GetX(), grad.GetY(), grad.GetZ()) - # read new coordinates from stdin for i in range(num_atoms): coordinates = np.fromstring(input(), sep=" ") @@ -64,6 +56,17 @@ def run(filename): # 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 + + # now print the gradient on each atom + print("AvogadroGradient:") + for atom in mol.atoms: + grad = ff.GetGradient(atom.OBAtom) + print(grad.GetX(), grad.GetY(), grad.GetZ()) + + if __name__ == "__main__": parser = argparse.ArgumentParser("MMFF94 calculator") diff --git a/avogadro/qtplugins/forcefield/scripts/uff.py b/avogadro/qtplugins/forcefield/scripts/uff.py index a2d7945902..bd83771aeb 100644 --- a/avogadro/qtplugins/forcefield/scripts/uff.py +++ b/avogadro/qtplugins/forcefield/scripts/uff.py @@ -53,19 +53,12 @@ def run(filename): atom = mol.atoms[i] atom.OBAtom.SetVector(coordinates[0], coordinates[1], coordinates[2]) - with open("/Users/ghutchis/uff.log", "a") as f: - f.write("Coordinates:\n") - for atom in mol.atoms: - f.write(str(atom.OBAtom.GetX()) + " " + str(atom.OBAtom.GetY()) + " " + str(atom.OBAtom.GetZ()) + "\n") - # update the molecule geometry for the next energy ff.SetCoordinates(mol.OBMol) # first print the energy of these coordinates - energy = ff.Energy(True) # in Hartree - print("AvogadroEnergy:", energy) # in Hartree - with open("/Users/ghutchis/uff.log", "a") as f: - f.write(str(energy) + "\n") + energy = ff.Energy(True) # in kJ/mol + print("AvogadroEnergy:", energy) # in kJ/mol # now print the gradient on each atom print("AvogadroGradient:") From f2e416f5f8a52580b5cedada3f2879f14dde370a Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Sun, 22 Oct 2023 20:47:10 -0400 Subject: [PATCH 20/22] Fixed problem - gradients went the wrong way Signed-off-by: Geoff Hutchison --- avogadro/calc/energycalculator.cpp | 2 -- avogadro/qtgui/pythonscript.cpp | 2 +- avogadro/qtplugins/forcefield/CMakeLists.txt | 3 +++ avogadro/qtplugins/forcefield/forcefield.cpp | 20 +++++++++---------- .../qtplugins/forcefield/scriptenergy.cpp | 7 +++---- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/avogadro/calc/energycalculator.cpp b/avogadro/calc/energycalculator.cpp index 89222b569d..9a264d3ad6 100644 --- a/avogadro/calc/energycalculator.cpp +++ b/avogadro/calc/energycalculator.cpp @@ -26,12 +26,10 @@ void EnergyCalculator::cleanGradients(TVector& grad) } // freeze any masked atoms or coordinates - /* if (m_mask.rows() == size) grad = grad.cwiseProduct(m_mask); else std::cerr << "Error: mask size " << m_mask.rows() << " " << grad.rows() << std::endl; - */ } } // namespace Avogadro diff --git a/avogadro/qtgui/pythonscript.cpp b/avogadro/qtgui/pythonscript.cpp index 8b567ceb43..0825912dba 100644 --- a/avogadro/qtgui/pythonscript.cpp +++ b/avogadro/qtgui/pythonscript.cpp @@ -224,8 +224,8 @@ void PythonScript::processFinished(int, QProcess::ExitStatus) void PythonScript::asyncTerminate() { if (m_process != nullptr) { - m_process->terminate(); disconnect(m_process, SIGNAL(finished()), this, SLOT(processFinished())); + m_process->kill(); m_process->deleteLater(); m_process = nullptr; } diff --git a/avogadro/qtplugins/forcefield/CMakeLists.txt b/avogadro/qtplugins/forcefield/CMakeLists.txt index 238d46ba9f..737130ce3e 100644 --- a/avogadro/qtplugins/forcefield/CMakeLists.txt +++ b/avogadro/qtplugins/forcefield/CMakeLists.txt @@ -16,9 +16,12 @@ target_link_libraries(Forcefield PRIVATE Avogadro::Calc) # Bundled forcefield scripts set(forcefields + scripts/gaff.py scripts/gfn1.py scripts/gfn2.py scripts/gfnff.py + scripts/mmff94.py + scripts/uff.py ) install(PROGRAMS ${forcefields} diff --git a/avogadro/qtplugins/forcefield/forcefield.cpp b/avogadro/qtplugins/forcefield/forcefield.cpp index 217c343c23..68c12b016f 100644 --- a/avogadro/qtplugins/forcefield/forcefield.cpp +++ b/avogadro/qtplugins/forcefield/forcefield.cpp @@ -29,9 +29,8 @@ #include #include - +#include #include -// not currently used #include #include @@ -125,9 +124,8 @@ void Forcefield::optimize() bool isInteractive = m_molecule->undoMolecule()->isInteractive(); m_molecule->undoMolecule()->setInteractive(true); - //cppoptlib::LbfgsSolver solver; - //cppoptlib::ConjugatedGradientDescentSolver solver; - cppoptlib::GradientDescentSolver solver; + cppoptlib::ConjugatedGradientDescentSolver solver; + //cppoptlib::GradientDescentSolver solver; int n = m_molecule->atomCount(); // we have to cast the current 3d positions into a VectorXd @@ -146,17 +144,17 @@ void Forcefield::optimize() cppoptlib::Criteria crit = cppoptlib::Criteria::defaults(); // e.g., every N steps, update coordinates - crit.iterations = 10; + crit.iterations = m_nSteps; // we don't set function or gradient criteria // .. these seem to be broken in the solver code // .. so we handle ourselves solver.setStopCriteria(crit); // set the method - std::string recommended = "MMFF94"; + std::string recommended = recommendedForceField(); qDebug() << "Energy method: " << recommended.c_str(); - if (m_method == nullptr) { + if (m_method == nullptr || m_method->identifier() != recommended) { // we have to create the calculator m_method = Calc::EnergyManager::instance().model(recommended); } @@ -181,6 +179,8 @@ void Forcefield::optimize() for (unsigned int i = 0; i < m_maxSteps / crit.iterations; ++i) { solver.minimize(*m_method, positions); + qApp->processEvents(QEventLoop::AllEvents, 500); + currentEnergy = m_method->value(positions); // get the current gradient for force visualization m_method->gradient(positions, gradient); @@ -204,7 +204,7 @@ void Forcefield::optimize() pos[i] = Vector3(*(d), *(d + 1), *(d + 2)); d += 3; - forces[i] = -1.0 * Vector3(gradient[3 * i], gradient[3 * i + 1], + forces[i] = -0.1 * Vector3(gradient[3 * i], gradient[3 * i + 1], gradient[3 * i + 2]); } } @@ -246,7 +246,7 @@ void Forcefield::energy() std::string recommended = recommendedForceField(); qDebug() << "Energy method: " << recommended.c_str(); - if (m_method == nullptr) { + if (m_method == nullptr || m_method->identifier() != recommended) { // we have to create the calculator m_method = Calc::EnergyManager::instance().model(recommended); } diff --git a/avogadro/qtplugins/forcefield/scriptenergy.cpp b/avogadro/qtplugins/forcefield/scriptenergy.cpp index 939d6ca1b8..60671b2cb4 100644 --- a/avogadro/qtplugins/forcefield/scriptenergy.cpp +++ b/avogadro/qtplugins/forcefield/scriptenergy.cpp @@ -154,7 +154,6 @@ void ScriptEnergy::gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) // parse the result // first split on newlines QStringList lines = QString(result).remove('\r').split('\n'); - double energy = 0.0; unsigned int i = 0; bool readingGrad = false; for (auto line : lines) { @@ -166,9 +165,9 @@ void ScriptEnergy::gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) if (readingGrad) { QStringList items = line.split(" "); if (items.size() == 3) { - grad[i] = items[0].toDouble(); - grad[i + 1] = items[1].toDouble(); - grad[i + 2] = items[2].toDouble(); + grad[i] = -1.0*items[0].toDouble(); + grad[i + 1] = -1.0*items[1].toDouble(); + grad[i + 2] = -1.0*items[2].toDouble(); i += 3; } From 2f947b5f23393b8e5f292a5adb7fa19e0613a37c Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Mon, 23 Oct 2023 14:27:00 -0400 Subject: [PATCH 21/22] Fix formatting, tweak UI including configuration dialog Signed-off-by: Geoff Hutchison --- avogadro/calc/chargemanager.cpp | 10 +- avogadro/calc/chargemanager.h | 2 +- avogadro/calc/chargemodel.cpp | 7 +- avogadro/calc/energycalculator.cpp | 5 +- avogadro/calc/energymanager.cpp | 2 +- avogadro/calc/energymanager.h | 4 +- avogadro/calc/lennardjones.cpp | 2 +- avogadro/qtgui/pythonscript.cpp | 2 +- avogadro/qtgui/pythonscript.h | 2 +- avogadro/qtplugins/forcefield/CMakeLists.txt | 3 + avogadro/qtplugins/forcefield/forcefield.cpp | 173 +++++++---- avogadro/qtplugins/forcefield/forcefield.h | 15 +- .../qtplugins/forcefield/forcefielddialog.cpp | 284 +++--------------- .../qtplugins/forcefield/forcefielddialog.h | 69 ++--- .../qtplugins/forcefield/forcefielddialog.ui | 34 +-- avogadro/qtplugins/forcefield/obmmenergy.cpp | 7 +- avogadro/qtplugins/forcefield/obmmenergy.h | 5 +- .../qtplugins/forcefield/scriptenergy.cpp | 6 +- .../qtplugins/forcefield/scripts/ani2x.py | 89 ++++++ avogadro/qtplugins/forcefield/scripts/gaff.py | 2 +- .../qtplugins/forcefield/scripts/gfnff.py | 5 +- .../qtplugins/forcefield/scripts/mmff94.py | 2 +- avogadro/qtplugins/forcefield/scripts/uff.py | 2 +- 23 files changed, 305 insertions(+), 427 deletions(-) create mode 100644 avogadro/qtplugins/forcefield/scripts/ani2x.py diff --git a/avogadro/calc/chargemanager.cpp b/avogadro/calc/chargemanager.cpp index 53f4421e77..5521b1d2f5 100644 --- a/avogadro/calc/chargemanager.cpp +++ b/avogadro/calc/chargemanager.cpp @@ -10,7 +10,6 @@ #include #include - namespace Avogadro::Calc { ChargeManager& ChargeManager::instance() @@ -88,7 +87,7 @@ ChargeManager::ChargeManager() ChargeManager::~ChargeManager() { // Delete the models that were loaded. - for (auto & m_model : m_models) { + for (auto& m_model : m_models) { delete m_model; } m_models.clear(); @@ -102,7 +101,6 @@ std::set ChargeManager::identifiersForMolecule( // check our models for compatibility for (auto m_model : m_models) { - // We check that every element in the molecule // is handled by the model auto mask = m_model->elements() & molecule.elements(); @@ -113,8 +111,8 @@ std::set ChargeManager::identifiersForMolecule( return identifiers; } -MatrixX ChargeManager::partialCharges( - const std::string& identifier, Core::Molecule& molecule) const +MatrixX ChargeManager::partialCharges(const std::string& identifier, + Core::Molecule& molecule) const { // first check if the type is found in the molecule // (i.e., read from a file not computed dynamically) @@ -181,4 +179,4 @@ Core::Array ChargeManager::potentials( return model->potentials(molecule, points); } -} // namespace Avogadro +} // namespace Avogadro::Calc diff --git a/avogadro/calc/chargemanager.h b/avogadro/calc/chargemanager.h index 3a8e87a8e5..3a0475c6be 100644 --- a/avogadro/calc/chargemanager.h +++ b/avogadro/calc/chargemanager.h @@ -101,7 +101,7 @@ class AVOGADROCALC_EXPORT ChargeManager * @return atomic partial charges for the molecule, or 0.0 if undefined */ MatrixX partialCharges(const std::string& identifier, - Core::Molecule& mol) const; + Core::Molecule& mol) const; /** * @return the potential at the point for the molecule, or 0.0 if the model is diff --git a/avogadro/calc/chargemodel.cpp b/avogadro/calc/chargemodel.cpp index f3fc749a4c..4cf6e1a1c4 100644 --- a/avogadro/calc/chargemodel.cpp +++ b/avogadro/calc/chargemodel.cpp @@ -39,7 +39,7 @@ double ChargeModel::potential(Molecule& mol, const Vector3& point) const double distance = (positions[i] - point).norm(); if (distance > 0.01) { // drop small distances to avoid overflow - potential += charges(i,0) / distance; + potential += charges(i, 0) / distance; } } @@ -52,12 +52,13 @@ Array ChargeModel::potentials(Core::Molecule& mol, // This is naive and slow, but can be re-implemented by methods // for batching Array potentials(points.size(), 0.0); - for(unsigned int i = 0; i < points.size(); ++i) + for (unsigned int i = 0; i < points.size(); ++i) potentials[i] = potential(mol, points[i]); return potentials; } -void ChargeModel::appendError(const std::string& errorString, bool newLine) const +void ChargeModel::appendError(const std::string& errorString, + bool newLine) const { m_error += errorString; if (newLine) diff --git a/avogadro/calc/energycalculator.cpp b/avogadro/calc/energycalculator.cpp index 9a264d3ad6..a75dd6d429 100644 --- a/avogadro/calc/energycalculator.cpp +++ b/avogadro/calc/energycalculator.cpp @@ -29,7 +29,8 @@ void EnergyCalculator::cleanGradients(TVector& grad) if (m_mask.rows() == size) grad = grad.cwiseProduct(m_mask); else - std::cerr << "Error: mask size " << m_mask.rows() << " " << grad.rows() << std::endl; + std::cerr << "Error: mask size " << m_mask.rows() << " " << grad.rows() + << std::endl; } -} // namespace Avogadro +} // namespace Avogadro::Calc diff --git a/avogadro/calc/energymanager.cpp b/avogadro/calc/energymanager.cpp index 9fec41d6ae..53751e45dc 100644 --- a/avogadro/calc/energymanager.cpp +++ b/avogadro/calc/energymanager.cpp @@ -45,7 +45,7 @@ bool EnergyManager::addModel(EnergyCalculator* model) return false; } - // If we got here then the format is unique enough to be added. + // If we got here then the model is unique enough to be added. size_t index = m_models.size(); m_models.push_back(model); m_identifiers[model->identifier()] = index; diff --git a/avogadro/calc/energymanager.h b/avogadro/calc/energymanager.h index 69df53a6c6..bea488c5d9 100644 --- a/avogadro/calc/energymanager.h +++ b/avogadro/calc/energymanager.h @@ -103,11 +103,11 @@ class AVOGADROCALC_EXPORT EnergyManager /** * @brief Get the name of the model for the specified identifier. - * + * * The name is a user-visible string, and may be translated. * @param identifier The unique identifier of the model. * @return The name of the model, or an empty string if not found. - */ + */ std::string nameForModel(const std::string& identifier) const; /** diff --git a/avogadro/calc/lennardjones.cpp b/avogadro/calc/lennardjones.cpp index c169b8db72..1dede2af31 100644 --- a/avogadro/calc/lennardjones.cpp +++ b/avogadro/calc/lennardjones.cpp @@ -38,7 +38,7 @@ void LennardJones::setMolecule(Core::Molecule* mol) // track atomic radii for this molecule m_radii.setZero(); Eigen::MatrixXd radii(numAtoms, numAtoms); - Eigen::MatrixXd mask(numAtoms*3, 1); + Eigen::MatrixXd mask(numAtoms * 3, 1); mask.setOnes(); m_mask = mask; diff --git a/avogadro/qtgui/pythonscript.cpp b/avogadro/qtgui/pythonscript.cpp index 0825912dba..56896fbdc2 100644 --- a/avogadro/qtgui/pythonscript.cpp +++ b/avogadro/qtgui/pythonscript.cpp @@ -224,7 +224,7 @@ void PythonScript::processFinished(int, QProcess::ExitStatus) void PythonScript::asyncTerminate() { if (m_process != nullptr) { - disconnect(m_process, SIGNAL(finished()), this, SLOT(processFinished())); + disconnect(m_process, nullptr, nullptr, nullptr); m_process->kill(); m_process->deleteLater(); m_process = nullptr; diff --git a/avogadro/qtgui/pythonscript.h b/avogadro/qtgui/pythonscript.h index 4b19d5cdc8..5e50ed990c 100644 --- a/avogadro/qtgui/pythonscript.h +++ b/avogadro/qtgui/pythonscript.h @@ -110,7 +110,7 @@ class AVOGADROQTGUI_EXPORT PythonScript : public QObject /** * Terminate the asynchronous process. - */ + */ void asyncTerminate(); /** diff --git a/avogadro/qtplugins/forcefield/CMakeLists.txt b/avogadro/qtplugins/forcefield/CMakeLists.txt index 737130ce3e..62079d56a2 100644 --- a/avogadro/qtplugins/forcefield/CMakeLists.txt +++ b/avogadro/qtplugins/forcefield/CMakeLists.txt @@ -1,5 +1,6 @@ set(forcefield_srcs forcefield.cpp + forcefielddialog.cpp obmmenergy.cpp scriptenergy.cpp ) @@ -10,12 +11,14 @@ avogadro_plugin(Forcefield forcefield.h Forcefield "${forcefield_srcs}" + forcefielddialog.ui ) target_link_libraries(Forcefield PRIVATE Avogadro::Calc) # Bundled forcefield scripts set(forcefields + scripts/ani2x.py scripts/gaff.py scripts/gfn1.py scripts/gfn2.py diff --git a/avogadro/qtplugins/forcefield/forcefield.cpp b/avogadro/qtplugins/forcefield/forcefield.cpp index 68c12b016f..d97402e837 100644 --- a/avogadro/qtplugins/forcefield/forcefield.cpp +++ b/avogadro/qtplugins/forcefield/forcefield.cpp @@ -1,14 +1,16 @@ /****************************************************************************** This source file is part of the Avogadro project. - - This source code is released under the New BSD License, (the "License"). + This source code is released under the 3-Clause BSD License, (see "LICENSE"). ******************************************************************************/ #include "forcefield.h" +#include "forcefielddialog.h" #include "obmmenergy.h" #include "scriptenergy.h" #include +#include + #include #include @@ -30,9 +32,9 @@ #include #include #include -#include #include #include +#include namespace Avogadro { namespace QtPlugins { @@ -48,10 +50,21 @@ const int freezeAction = 3; const int unfreezeAction = 4; const int constraintAction = 5; -Forcefield::Forcefield(QObject* parent_) : ExtensionPlugin(parent_) +Forcefield::Forcefield(QObject* parent_) + : ExtensionPlugin(parent_), m_method(nullptr) { + QSettings settings; + settings.beginGroup("forcefield"); + m_autodetect = settings.value("autodetect", true).toBool(); + m_methodName = settings.value("forcefield", "LJ").toString().toStdString(); + m_nSteps = settings.value("steps", 10).toInt(); + m_maxSteps = settings.value("maxSteps", 250).toInt(); + m_tolerance = settings.value("tolerance", 1.0e-4).toDouble(); + m_gradientTolerance = settings.value("gradientTolerance", 1.0e-4).toDouble(); + settings.endGroup(); + refreshScripts(); - /*/ + /* @todo - finish OBMM interface Calc::EnergyManager::registerModel(new OBMMEnergy("MMFF94")); Calc::EnergyManager::registerModel(new OBMMEnergy("UFF")); Calc::EnergyManager::registerModel(new OBMMEnergy("GAFF")); @@ -73,6 +86,18 @@ Forcefield::Forcefield(QObject* parent_) : ExtensionPlugin(parent_) connect(action, SIGNAL(triggered()), SLOT(energy())); m_actions.push_back(action); + action = new QAction(this); + action->setEnabled(true); + action->setText(tr("Configureā€¦")); + action->setData(configureAction); + action->setProperty("menu priority", 900); + connect(action, SIGNAL(triggered()), SLOT(showDialog())); + m_actions.push_back(action); + + action = new QAction(this); + action->setSeparator(true); + m_actions.push_back(action); + action = new QAction(this); action->setEnabled(true); action->setText(tr("Freeze Selected Atoms")); @@ -98,26 +123,82 @@ QList Forcefield::actions() const QStringList Forcefield::menuPath(QAction* action) const { QStringList path; - if (action->data() == optimizeAction) { - // optimize geometry - path << tr("&Extensions"); - return path; - } path << tr("&Extensions") << tr("&Calculate"); return path; } +void Forcefield::showDialog() +{ + QStringList forceFields; + auto list = + Calc::EnergyManager::instance().identifiersForMolecule(*m_molecule); + for (auto option : list) { + forceFields << option.c_str(); + } + + QSettings settings; + QVariantMap options; + options["forcefield"] = m_methodName.c_str(); + options["nSteps"] = m_nSteps; + options["maxSteps"] = m_maxSteps; + options["tolerance"] = m_tolerance; + options["gradientTolerance"] = m_gradientTolerance; + options["autodetect"] = m_autodetect; + + QVariantMap results = ForceFieldDialog::prompt( + nullptr, forceFields, options, recommendedForceField().c_str()); + + if (!results.isEmpty()) { + // update settings + settings.beginGroup("forcefield"); + m_methodName = results["forcefield"].toString().toStdString(); + settings.setValue("forcefield", m_methodName.c_str()); + + m_maxSteps = results["maxSteps"].toInt(); + settings.setValue("maxSteps", m_maxSteps); + m_tolerance = results["tolerance"].toDouble(); + settings.setValue("tolerance", m_tolerance); + m_gradientTolerance = results["gradientTolerance"].toDouble(); + settings.setValue("gradientTolerance", m_gradientTolerance); + m_autodetect = results["autodetect"].toBool(); + settings.setValue("autodetect", m_autodetect); + settings.endGroup(); + } + setupMethod(); +} + void Forcefield::setMolecule(QtGui::Molecule* mol) { if (m_molecule == mol) return; m_molecule = mol; + + setupMethod(); +} + +void Forcefield::setupMethod() +{ + if (m_autodetect) + m_methodName = recommendedForceField(); + + qDebug() << " setup method " << m_methodName.c_str() << " autodetect: " + << m_autodetect << " recommended " << recommendedForceField().c_str(); + + if (m_method == nullptr) { + // we have to create the calculator + m_method = Calc::EnergyManager::instance().model(m_methodName); + } else if (m_method->identifier() != m_methodName) { + delete m_method; // delete the previous one + m_method = Calc::EnergyManager::instance().model(m_methodName); + } + + m_method->setMolecule(m_molecule); } void Forcefield::optimize() { - if (!m_molecule) + if (m_molecule == nullptr || m_method == nullptr) return; // merge all coordinate updates into one step for undo @@ -125,9 +206,21 @@ void Forcefield::optimize() m_molecule->undoMolecule()->setInteractive(true); cppoptlib::ConjugatedGradientDescentSolver solver; - //cppoptlib::GradientDescentSolver solver; int n = m_molecule->atomCount(); + + // double-check the mask + auto mask = m_molecule->frozenAtomMask(); + if (mask.rows() != 3*n) { + mask = Eigen::VectorXd::Zero(3 * n); + // set to 1.0 + for (Index i = 0; i < 3 * n; ++i) { + mask[i] = 1.0; + } + } + m_method->setMolecule(m_molecule); + m_method->setMask(mask); + // we have to cast the current 3d positions into a VectorXd Core::Array pos = m_molecule->atomPositions3d(); double* p = pos[0].data(); @@ -144,36 +237,16 @@ void Forcefield::optimize() cppoptlib::Criteria crit = cppoptlib::Criteria::defaults(); // e.g., every N steps, update coordinates - crit.iterations = m_nSteps; + crit.iterations = 5; // we don't set function or gradient criteria // .. these seem to be broken in the solver code // .. so we handle ourselves solver.setStopCriteria(crit); - // set the method - std::string recommended = recommendedForceField(); - qDebug() << "Energy method: " << recommended.c_str(); - - if (m_method == nullptr || m_method->identifier() != recommended) { - // we have to create the calculator - m_method = Calc::EnergyManager::instance().model(recommended); - } - m_method->setMolecule(m_molecule); - auto mask = m_molecule->frozenAtomMask(); - if (mask.rows() != m_molecule->atomCount()) { - mask = Eigen::VectorXd::Zero(3 * m_molecule->atomCount()); - // set to 1.0 - for (Index i = 0; i < 3 * m_molecule->atomCount(); ++i) { - mask[i] = 1.0; - } - } - m_method->setMask(mask); - Real energy = m_method->value(positions); m_method->gradient(positions, gradient); - qDebug() << " initial " << energy - << " gradNorm: " << gradient.norm() - << " posNorm: " << positions.norm(); + qDebug() << " initial " << energy << " gradNorm: " << gradient.norm(); + qDebug() << " maxSteps" << m_maxSteps << " steps " << m_maxSteps / crit.iterations; Real currentEnergy = 0.0; for (unsigned int i = 0; i < m_maxSteps / crit.iterations; ++i) { @@ -185,8 +258,7 @@ void Forcefield::optimize() // get the current gradient for force visualization m_method->gradient(positions, gradient); qDebug() << " optimize " << i << currentEnergy - << " gradNorm: " << gradient.norm() - << " posNorm: " << positions.norm(); + << " gradNorm: " << gradient.norm(); // update coordinates bool isFinite = std::isfinite(currentEnergy); @@ -239,19 +311,9 @@ void Forcefield::optimize() void Forcefield::energy() { - if (!m_molecule) + if (m_molecule == nullptr || m_method == nullptr) return; - //@todo check m_method for a particular calculator - std::string recommended = recommendedForceField(); - qDebug() << "Energy method: " << recommended.c_str(); - - if (m_method == nullptr || m_method->identifier() != recommended) { - // we have to create the calculator - m_method = Calc::EnergyManager::instance().model(recommended); - } - m_method->setMolecule(m_molecule); - int n = m_molecule->atomCount(); // we have to cast the current 3d positions into a VectorXd Core::Array pos = m_molecule->atomPositions3d(); @@ -260,9 +322,10 @@ void Forcefield::energy() Eigen::VectorXd positions = map; // now get the energy + m_method->setMolecule(m_molecule); Real energy = m_method->value(positions); - QString msg(tr("Energy = %L1").arg(energy)); + QString msg(tr("%1 Energy = %L2").arg(m_methodName.c_str()).arg(energy)); QMessageBox::information(nullptr, tr("Avogadro"), msg); } @@ -281,16 +344,14 @@ std::string Forcefield::recommendedForceField() const // iterate to see what we have std::string bestOption; - for (auto options : list) { + for (auto option : list) { // ideally, we'd use GFN-FF but it needs tweaking // everything else is a ranking // GAFF is better than MMFF94 which is better than UFF - if (options == "UFF" && bestOption != "GAFF" || bestOption != "MMFF94") - bestOption = options; - if (options == "MMFF94" && bestOption != "GAFF") - bestOption = options; - if (options == "GAFF") - bestOption = options; + if (option == "UFF" && bestOption != "GAFF" && bestOption != "MMFF94") + bestOption = option; + if (option == "MMFF94" && bestOption != "GAFF") + bestOption = option; } if (!bestOption.empty()) return bestOption; diff --git a/avogadro/qtplugins/forcefield/forcefield.h b/avogadro/qtplugins/forcefield/forcefield.h index 3cf7f8fd14..05170b66cf 100644 --- a/avogadro/qtplugins/forcefield/forcefield.h +++ b/avogadro/qtplugins/forcefield/forcefield.h @@ -1,7 +1,6 @@ /****************************************************************************** This source file is part of the Avogadro project. - - This source code is released under the New BSD License, (the "License"). + This source code is released under the 3-Clause BSD License, (see "LICENSE"). ******************************************************************************/ #ifndef AVOGADRO_QTPLUGINS_FORCEFIELD_H @@ -56,6 +55,7 @@ class Forcefield : public QtGui::ExtensionPlugin QStringList menuPath(QAction*) const override; void setMolecule(QtGui::Molecule* mol) override; + void setupMethod(); std::string recommendedForceField() const; @@ -67,6 +67,8 @@ public slots: void registerScripts(); void unregisterScripts(); + void showDialog(); + private slots: void energy(); void optimize(); @@ -74,9 +76,11 @@ private slots: void unfreezeSelected(); private: - QList m_actions; QtGui::Molecule* m_molecule = nullptr; + Calc::EnergyCalculator* m_method = nullptr; + std::string m_methodName; + bool m_autodetect; // defaults Minimizer m_minimizer = LBFGS; @@ -84,12 +88,11 @@ private slots: unsigned int m_nSteps = 5; double m_tolerance = 1.0e-6; double m_gradientTolerance = 1.0e-4; - Calc::EnergyCalculator *m_method = nullptr; QList m_scripts; }; -} -} +} // namespace QtPlugins +} // namespace Avogadro #endif // AVOGADRO_QTPLUGINS_FORCEFIELD_H diff --git a/avogadro/qtplugins/forcefield/forcefielddialog.cpp b/avogadro/qtplugins/forcefield/forcefielddialog.cpp index 1434ecfeb2..6997467780 100644 --- a/avogadro/qtplugins/forcefield/forcefielddialog.cpp +++ b/avogadro/qtplugins/forcefield/forcefielddialog.cpp @@ -1,21 +1,10 @@ /****************************************************************************** - This source file is part of the Avogadro project. - - Copyright 2013 Kitware, Inc. - - This source code is released under the New BSD License, (the "License"). - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - + This source code is released under the 3-Clause BSD License, (see "LICENSE"). ******************************************************************************/ -#include "obforcefielddialog.h" -#include "ui_obforcefielddialog.h" +#include "forcefielddialog.h" +#include "ui_forcefielddialog.h" #include #include @@ -27,21 +16,9 @@ namespace Avogadro { namespace QtPlugins { -enum OptimizationAlgorithm -{ - SteepestDescent = 0, - ConjugateGradient -}; - -enum LineSearchMethod -{ - Simple = 0, - Newton -}; - -OBForceFieldDialog::OBForceFieldDialog(const QStringList& forceFields, - QWidget* parent_) - : QDialog(parent_), ui(new Ui::OBForceFieldDialog) +ForceFieldDialog::ForceFieldDialog(const QStringList& forceFields, + QWidget* parent_) + : QDialog(parent_), ui(new Ui::ForceFieldDialog) { ui->setupUi(this); ui->forceField->addItems(forceFields); @@ -56,245 +33,56 @@ OBForceFieldDialog::OBForceFieldDialog(const QStringList& forceFields, ui->useRecommended->setChecked(autoDetect); } -OBForceFieldDialog::~OBForceFieldDialog() +ForceFieldDialog::~ForceFieldDialog() { delete ui; } -QStringList OBForceFieldDialog::prompt(QWidget* parent_, - const QStringList& forceFields, - const QStringList& startingOptions, - const QString& recommendedForceField_) +QVariantMap ForceFieldDialog::prompt(QWidget* parent_, + const QStringList& forceFields, + const QVariantMap& startingOptions, + const QString& recommendedForceField_) { - OBForceFieldDialog dlg(forceFields, parent_); + ForceFieldDialog dlg(forceFields, parent_); dlg.setOptions(startingOptions); dlg.setRecommendedForceField(recommendedForceField_); - QStringList options; + QVariantMap options; if (static_cast(dlg.exec()) == Accepted) options = dlg.options(); return options; } -QStringList OBForceFieldDialog::options() const +QVariantMap ForceFieldDialog::options() const { - QStringList opts; - - opts << "--crit" - << QString::number(std::pow(10.0f, ui->energyConv->value()), 'e', 0) - << "--ff" << ui->forceField->currentText() << "--steps" - << QString::number(ui->stepLimit->value()) << "--rvdw" - << QString::number(ui->vdwCutoff->value()) << "--rele" - << QString::number(ui->eleCutoff->value()) << "--freq" - << QString::number(ui->pairFreq->value()); - - switch (static_cast(ui->algorithm->currentIndex())) { - case SteepestDescent: - opts << "--sd"; - break; - default: - case ConjugateGradient: - break; - } + QVariantMap opts; - switch (static_cast(ui->lineSearch->currentIndex())) { - case Newton: - opts << "--newton"; - break; - default: - case Simple: - break; - } - - if (ui->enableCutoffs->isChecked()) - opts << "--cut"; + opts["forcefield"] = ui->forceField->currentText(); + opts["maxSteps"] = ui->stepLimit->value(); + opts["tolerance"] = pow(10, ui->energyConv->value()); + opts["gradientTolerance"] = pow(10, ui->gradConv->value()); + opts["autodetect"] = ui->useRecommended->isChecked(); return opts; } -void OBForceFieldDialog::setOptions(const QStringList& opts) +void ForceFieldDialog::setOptions(const QVariantMap& opts) { - // Set some defaults. These match the defaults in obabel -L minimize - ui->energyConv->setValue(-6); - ui->algorithm->setCurrentIndex(static_cast(ConjugateGradient)); - ui->lineSearch->setCurrentIndex(static_cast(Simple)); - ui->stepLimit->setValue(2500); - ui->enableCutoffs->setChecked(false); - ui->vdwCutoff->setValue(10.0); - ui->eleCutoff->setValue(10.0); - ui->pairFreq->setValue(10); - - for (QStringList::const_iterator it = opts.constBegin(), - itEnd = opts.constEnd(); - it < itEnd; ++it) { - - // We'll always use log: - if (*it == "--log") { - continue; - } - - // Energy convergence: - else if (*it == "--crit") { - ++it; - if (it == itEnd) { - qWarning() << "OBForceFieldDialog::setOptions: " - "--crit missing argument."; - continue; - } - - bool ok; - float econv = it->toFloat(&ok); - if (!ok) { - qWarning() << "OBForceFieldDialog::setOptions: " - "--crit is not numeric: " - << *it; - continue; - } - - // We just show the econv as 10^(x), so calculate the nearest x - int exponent = static_cast(std::floor(std::log10(econv) + 0.5)); - ui->energyConv->setValue(exponent); - continue; - } - - // Use steepest descent? - else if (*it == "--sd") { - ui->algorithm->setCurrentIndex(SteepestDescent); - continue; - } - - // Use newton linesearch? - else if (*it == "--newton") { - ui->lineSearch->setCurrentIndex(Newton); - continue; - } - - // Force field? - else if (*it == "--ff") { - ++it; - if (it == itEnd) { - qWarning() << "OBForceFieldDialog::setOptions: " - "--ff missing argument."; - continue; - } - - int index = ui->forceField->findText(*it); - if (index < 0) { - qWarning() << "OBForceFieldDialog::setOptions: " - "--ff unknown: " - << *it; - continue; - } - - ui->forceField->setCurrentIndex(index); - continue; - } - - // Step limit? - else if (*it == "--steps") { - ++it; - if (it == itEnd) { - qWarning() << "OBForceFieldDialog::setOptions: " - "--steps missing argument."; - continue; - } - - bool ok; - int numSteps = it->toInt(&ok); - if (!ok) { - qWarning() << "OBForceFieldDialog::setOptions: " - "--steps is not numeric: " - << *it; - continue; - } - - ui->stepLimit->setValue(numSteps); - continue; - } - - // Use cutoff? - else if (*it == "--cut") { - ui->enableCutoffs->setChecked(true); - continue; - } - - // Van der Waals cutoff - else if (*it == "--rvdw") { - ++it; - if (it == itEnd) { - qWarning() << "OBForceFieldDialog::setOptions: " - "--rvdw missing argument."; - continue; - } - - bool ok; - double cutoff = it->toDouble(&ok); - if (!ok) { - qWarning() << "OBForceFieldDialog::setOptions: " - "--rvdw is not numeric: " - << *it; - continue; - } - - ui->vdwCutoff->setValue(cutoff); - continue; - } - - // electrostatic cutoff - else if (*it == "--rele") { - ++it; - if (it == itEnd) { - qWarning() << "OBForceFieldDialog::setOptions: " - "--rele missing argument."; - continue; - } - - bool ok; - double cutoff = it->toDouble(&ok); - if (!ok) { - qWarning() << "OBForceFieldDialog::setOptions: " - "--rele is not numeric: " - << *it; - continue; - } - - ui->eleCutoff->setValue(cutoff); - continue; - } - - // Pair update frequency: - else if (*it == "--freq") { - ++it; - if (it == itEnd) { - qWarning() << "OBForceFieldDialog::setOptions: " - "--freq missing argument."; - continue; - } - - bool ok; - int numSteps = it->toInt(&ok); - if (!ok) { - qWarning() << "OBForceFieldDialog::setOptions: " - "--freq is not numeric: " - << *it; - continue; - } - - ui->pairFreq->setValue(numSteps); - continue; - } - - // ????? - else { - qWarning() << "OBForceFieldDialog::setOptions: " - "Unrecognized option: " - << *it; - } - } + if (opts.contains("forcefield") && opts["forcefield"].canConvert()) + ui->forceField->setCurrentText(opts["forcefield"].toString()); + if (opts.contains("maxSteps") && opts["maxSteps"].canConvert()) + ui->stepLimit->setValue(opts["maxSteps"].toInt()); + if (opts.contains("tolerance") && opts["tolerance"].canConvert()) + ui->energyConv->setValue(log10(opts["tolerance"].toDouble())); + if (opts.contains("gradientTolerance") && + opts["gradientTolerance"].canConvert()) + ui->gradConv->setValue(log10(opts["gradientTolerance"].toDouble())); + if (opts.contains("autodetect") && opts["autodetect"].canConvert()) + ui->useRecommended->setChecked(opts["autodetect"].toBool()); } -void OBForceFieldDialog::setRecommendedForceField(const QString& rff) +void ForceFieldDialog::setRecommendedForceField(const QString& rff) { if (rff == m_recommendedForceField) return; @@ -306,7 +94,7 @@ void OBForceFieldDialog::setRecommendedForceField(const QString& rff) updateRecommendedForceField(); } -void OBForceFieldDialog::useRecommendedForceFieldToggled(bool state) +void ForceFieldDialog::useRecommendedForceFieldToggled(bool state) { if (!m_recommendedForceField.isEmpty()) { if (state) { @@ -318,10 +106,10 @@ void OBForceFieldDialog::useRecommendedForceFieldToggled(bool state) } ui->forceField->setEnabled(!state); - QSettings().setValue("openbabel/optimizeGeometry/autoDetect", state); + QSettings().setValue("forcefield/autoDetect", state); } -void OBForceFieldDialog::updateRecommendedForceField() +void ForceFieldDialog::updateRecommendedForceField() { if (m_recommendedForceField.isEmpty()) { ui->useRecommended->hide(); diff --git a/avogadro/qtplugins/forcefield/forcefielddialog.h b/avogadro/qtplugins/forcefield/forcefielddialog.h index 546d34e2f1..8a6fb71c2a 100644 --- a/avogadro/qtplugins/forcefield/forcefielddialog.h +++ b/avogadro/qtplugins/forcefield/forcefielddialog.h @@ -1,21 +1,10 @@ /****************************************************************************** - This source file is part of the Avogadro project. - - Copyright 2013 Kitware, Inc. - - This source code is released under the New BSD License, (the "License"). - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - + This source code is released under the 3-Clause BSD License, (see "LICENSE"). ******************************************************************************/ -#ifndef AVOGADRO_QTPLUGINS_OBFORCEFIELDDIALOG_H -#define AVOGADRO_QTPLUGINS_OBFORCEFIELDDIALOG_H +#ifndef AVOGADRO_QTPLUGINS_FORCEFIELDDIALOG_H +#define AVOGADRO_QTPLUGINS_FORCEFIELDDIALOG_H #include @@ -23,14 +12,14 @@ namespace Avogadro { namespace QtPlugins { namespace Ui { -class OBForceFieldDialog; +class ForceFieldDialog; } /** - * @brief The OBForceFieldDialog class is used to prompt the user for parameters - * to be used in an OpenBabel force field optimization. + * @brief The ForceFieldDialog class is used to prompt the user for parameters + * to be used in a force field optimization. */ -class OBForceFieldDialog : public QDialog +class ForceFieldDialog : public QDialog { Q_OBJECT @@ -38,9 +27,9 @@ class OBForceFieldDialog : public QDialog /** * Construct a new dialog using the forcefields in @a forceFields. */ - explicit OBForceFieldDialog(const QStringList& forceFields, - QWidget* parent_ = 0); - ~OBForceFieldDialog() override; + explicit ForceFieldDialog(const QStringList& forceFields, + QWidget* parent_ = 0); + ~ForceFieldDialog() override; /** * Construct a new dialog using the forcefields in @a forceFields and @@ -51,39 +40,15 @@ class OBForceFieldDialog : public QDialog * When the user closes the dialog, the options they selected are returned. If * the user cancels the dialog, an empty list is returned. */ - static QStringList prompt(QWidget* parent_, const QStringList& forceFields, - const QStringList& startingOptions, + static QVariantMap prompt(QWidget* parent_, const QStringList& forceFields, + const QVariantMap& startingOptions, const QString& recommendedForceField_ = QString()); /** - * Get/set the options displayed in the dialog. The option format is a list of - * strings that may be used directly as arguments in a call to - * QProcess::start, with the exception of the `-i`, - * `-o` and `--minimize` options, which are not used by this - * class. See `obabel -L minimize` for a complete listing of available - * options. - * - * Each option (and argument, if applicable) must be a separate string in the - * list. For instance, to refer to the options in the call: -@code -obabel -icml -ocml --minimize --log --crit 1e-05 --ff Ghemical --sd" -@endcode - * - * The option list should contain, in order: - * - `--crit` - * - `1e-05` - * - `--ff` - * - `Ghemical` - * - `--sd` - * - * @note The `--log` option is always added in the list returned by - * options, and is ignored by the setOptions method. - * - * @{ + * Get/set the options displayed in the dialog. */ - QStringList options() const; - void setOptions(const QStringList& opts); - /**@}*/ + QVariantMap options() const; + void setOptions(const QVariantMap& opts); /** * Get/set the recommended forcefield for the current molecule. If an empty @@ -105,10 +70,10 @@ private slots: private: void updateRecommendedForceField(); - Ui::OBForceFieldDialog* ui; + Ui::ForceFieldDialog* ui; QString m_recommendedForceField; }; } // namespace QtPlugins } // namespace Avogadro -#endif // AVOGADRO_QTPLUGINS_OBFORCEFIELDDIALOG_H +#endif // AVOGADRO_QTPLUGINS_FORCEFIELDDIALOG_H diff --git a/avogadro/qtplugins/forcefield/forcefielddialog.ui b/avogadro/qtplugins/forcefield/forcefielddialog.ui index 8c72edc764..5f84754016 100644 --- a/avogadro/qtplugins/forcefield/forcefielddialog.ui +++ b/avogadro/qtplugins/forcefield/forcefielddialog.ui @@ -7,7 +7,7 @@ 0 0 365 - 295 + 275 @@ -143,37 +143,6 @@ - - - - Qt::Horizontal - - - - - - - Optimization algorithm: - - - - - - - 1 - - - - Conjugate Gradient - - - - - BFGS - - - - @@ -182,7 +151,6 @@ forceField useRecommended - algorithm energyConv stepLimit buttonBox diff --git a/avogadro/qtplugins/forcefield/obmmenergy.cpp b/avogadro/qtplugins/forcefield/obmmenergy.cpp index fc633876ad..e58d798243 100644 --- a/avogadro/qtplugins/forcefield/obmmenergy.cpp +++ b/avogadro/qtplugins/forcefield/obmmenergy.cpp @@ -20,7 +20,8 @@ class OBMMEnergy::ProcessListener : public QObject { Q_OBJECT public: - ProcessListener(QProcess *proc) : QObject(), m_finished(false), m_process(proc) + ProcessListener(QProcess* proc) + : QObject(), m_finished(false), m_process(proc) { } @@ -71,7 +72,7 @@ OBMMEnergy::OBMMEnergy(const std::string& method) { // eventually CJSON might be nice m_inputFormat = new Io::CmlFormat; - + if (method == "UFF") { m_description = tr("Universal Force Field"); m_elements.reset(); @@ -351,7 +352,7 @@ void OBMMEnergy::gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) if (readingGradient) { QStringList items = line.split(" ", Qt::SkipEmptyParts); if (items.size() == 3) { - grad[3 * i] = items[0].toDouble(); + grad[3 * i] = -1.0 * items[0].toDouble(); grad[3 * i + 1] = items[1].toDouble(); grad[3 * i + 2] = items[2].toDouble(); ++i; diff --git a/avogadro/qtplugins/forcefield/obmmenergy.h b/avogadro/qtplugins/forcefield/obmmenergy.h index 1c6bf2e902..f12e7ba1fd 100644 --- a/avogadro/qtplugins/forcefield/obmmenergy.h +++ b/avogadro/qtplugins/forcefield/obmmenergy.h @@ -55,7 +55,10 @@ class OBMMEnergy : public Avogadro::Calc::EnergyCalculator std::string identifier() const override { return m_identifier; } std::string name() const override { return m_name; } - std::string description() const override { return m_description.toStdString(); } + std::string description() const override + { + return m_description.toStdString(); + } Core::Molecule::ElementMask elements() const override { return (m_elements); } diff --git a/avogadro/qtplugins/forcefield/scriptenergy.cpp b/avogadro/qtplugins/forcefield/scriptenergy.cpp index 60671b2cb4..a637f680bc 100644 --- a/avogadro/qtplugins/forcefield/scriptenergy.cpp +++ b/avogadro/qtplugins/forcefield/scriptenergy.cpp @@ -165,9 +165,9 @@ void ScriptEnergy::gradient(const Eigen::VectorXd& x, Eigen::VectorXd& grad) if (readingGrad) { QStringList items = line.split(" "); if (items.size() == 3) { - grad[i] = -1.0*items[0].toDouble(); - grad[i + 1] = -1.0*items[1].toDouble(); - grad[i + 2] = -1.0*items[2].toDouble(); + grad[i] = items[0].toDouble(); + grad[i + 1] = items[1].toDouble(); + grad[i + 2] = items[2].toDouble(); i += 3; } diff --git a/avogadro/qtplugins/forcefield/scripts/ani2x.py b/avogadro/qtplugins/forcefield/scripts/ani2x.py new file mode 100644 index 0000000000..06266cf08b --- /dev/null +++ b/avogadro/qtplugins/forcefield/scripts/ani2x.py @@ -0,0 +1,89 @@ +# 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 + +try: + import torch + import torchani + import numpy as np + + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + model = torchani.models.ANI2x(periodic_table_index=True).to(device) + + imported = True +except ImportError: + imported = False + + +def getMetaData(): + # before we return metadata, make sure xtb is in the path + if not imported: + return {} # Avogadro will ignore us now + + metaData = { + "name": "ANI2x", + "identifier": "ANI2x", + "description": "Calculate ANI-2x energies and gradients", + "inputFormat": "cjson", + "elements": "1,6-9,16-17", + "unitCell": False, + "gradients": True, + "ion": False, + "radical": False, + } + return metaData + + +def run(filename): + # we get the molecule from the supplied filename + # in cjson format (it's a temporary file created by Avogadro) + with open(filename, "r") as f: + mol_cjson = json.load(f) + + # first setup the calculator + atoms = np.array(mol_cjson["atoms"]["elements"]["number"]) + species = torch.tensor([atoms], device=device) + coord_list = mol_cjson["atoms"]["coords"]["3d"] + np_coords = np.array(coord_list, dtype=float).reshape(-1, 3) + coordinates = torch.tensor([np_coords], requires_grad=True, device=device) + + # we loop forever - Avogadro will kill the process when done + num_atoms = len(atoms) + while True: + # read new coordinates from stdin + 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 + + # 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 __name__ == "__main__": + parser = argparse.ArgumentParser("ANI-2x calculator") + parser.add_argument("--display-name", action="store_true") + parser.add_argument("--metadata", action="store_true") + parser.add_argument("-f", "--file", nargs=1) + parser.add_argument("--lang", nargs="?", default="en") + args = vars(parser.parse_args()) + + if args["metadata"]: + print(json.dumps(getMetaData())) + elif args["display_name"]: + name = getMetaData().get("name") + if name: + print(name) + else: + sys.exit("ANI-2x is unavailable") + elif args["file"]: + run(args["file"][0]) diff --git a/avogadro/qtplugins/forcefield/scripts/gaff.py b/avogadro/qtplugins/forcefield/scripts/gaff.py index 9c34087c2e..491c66d1f6 100644 --- a/avogadro/qtplugins/forcefield/scripts/gaff.py +++ b/avogadro/qtplugins/forcefield/scripts/gaff.py @@ -64,7 +64,7 @@ def run(filename): print("AvogadroGradient:") for atom in mol.atoms: grad = ff.GetGradient(atom.OBAtom) - print(grad.GetX(), grad.GetY(), grad.GetZ()) + print(-1.0*grad.GetX(), -1.0*grad.GetY(), -1.0*grad.GetZ()) diff --git a/avogadro/qtplugins/forcefield/scripts/gfnff.py b/avogadro/qtplugins/forcefield/scripts/gfnff.py index 912e9ce911..c487949193 100644 --- a/avogadro/qtplugins/forcefield/scripts/gfnff.py +++ b/avogadro/qtplugins/forcefield/scripts/gfnff.py @@ -40,9 +40,6 @@ def run(filename): with open(filename, "r") as f: mol_cjson = json.load(f) - with open("/Users/ghutchis/gfnff.log", "a") as f: - f.write(filename + "\n") - # first setup the calculator atoms = np.array(mol_cjson["atoms"]["elements"]["number"]) coord_list = mol_cjson["atoms"]["coords"]["3d"] @@ -83,7 +80,7 @@ def run(filename): # now print the gradient # .. we don't want the "[]" in the output print("AvogadroGradient:") - grad = res.get_gradient() * 4961.475 # convert units + grad = res.get_gradient() * 496100.475 # convert units output = np.array2string(grad) output = output.replace("[", "").replace("]", "") print(output) diff --git a/avogadro/qtplugins/forcefield/scripts/mmff94.py b/avogadro/qtplugins/forcefield/scripts/mmff94.py index 271b29fe4b..3206123c40 100644 --- a/avogadro/qtplugins/forcefield/scripts/mmff94.py +++ b/avogadro/qtplugins/forcefield/scripts/mmff94.py @@ -64,7 +64,7 @@ def run(filename): print("AvogadroGradient:") for atom in mol.atoms: grad = ff.GetGradient(atom.OBAtom) - print(grad.GetX(), grad.GetY(), grad.GetZ()) + 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 bd83771aeb..6c5c926be5 100644 --- a/avogadro/qtplugins/forcefield/scripts/uff.py +++ b/avogadro/qtplugins/forcefield/scripts/uff.py @@ -64,7 +64,7 @@ def run(filename): print("AvogadroGradient:") for atom in mol.atoms: grad = ff.GetGradient(atom.OBAtom) - print(grad.GetX(), grad.GetY(), grad.GetZ()) + print(-1.0*grad.GetX(), -1.0*grad.GetY(), -1.0*grad.GetZ()) if __name__ == "__main__": From 5298261affa7c49a494ceadcda9ba54cf0c1fd5f Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Mon, 23 Oct 2023 15:09:38 -0400 Subject: [PATCH 22/22] Ensure m_molecule is always initialized Signed-off-by: Geoff Hutchison --- avogadro/calc/lennardjones.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/avogadro/calc/lennardjones.cpp b/avogadro/calc/lennardjones.cpp index 1dede2af31..be9ffc2927 100644 --- a/avogadro/calc/lennardjones.cpp +++ b/avogadro/calc/lennardjones.cpp @@ -12,7 +12,8 @@ namespace Avogadro::Calc { LennardJones::LennardJones() - : m_vdw(true), m_depth(100.0), m_exponent(6), m_cell(nullptr) + : m_vdw(true), m_depth(100.0), m_exponent(6), m_cell(nullptr), + m_molecule(nullptr) { // defined for 1-118 for (unsigned int i = 1; i <= 118; ++i) {