Repository: rdkit/rdkit — pull request #3975 (cpp)
Title: Fixes #3781

- fixes energies and gradients for dihedral and angle constraints in both MMFF and UFF
- adds butane scan tests for MMFF and UFF

Now the butane scan works as expected for both UFF...
![image](https://user-images.githubusercontent.com/5244385/112561582-d1a90e80-8dd5-11eb-832a-bc85cfa66098.png)

...and MMFF:
![image](https://user-images.githubusercontent.com/5244385/112561383-64957900-8dd5-11eb-9306-b14bef26d81e.png)


Resolved issues:
title: Regression in dihedral constraints
body: In this code snippet I just use an added torsion constraint with a very high force constant to get certain dihedral angle.  It works well in version "2019.03.3" but gives very bad results in "2020.09.1".

```
m = Chem.rdmolfiles.MolFromMolFile('/Users/jasonb/butane.mol',removeHs = False)
mp = AllChem.MMFFGetMoleculeProperties(m)
ff = AllChem.MMFFGetMoleculeForceField(m, mp)
ff.MMFFAddTorsionConstraint(0,1,2,3, False, -180, -180, 10000.0)
ff.Minimize()
Chem.rdmolfiles.MolToMolFile(m,'/Users/jasonb/butane_180.mol')
```
Starting with the attached MOL file, rdkit version "2019.03.3" gives the top result, while version "2020.09.1" gives the bottom version:

<img width="286" alt="image" src="https://user-images.githubusercontent.com/16337306/106640656-ec84a280-654b-11eb-9994-c477e9f11d67.png">

(can't seem to attach a .mol file, renamed to 'butane.txt')

[butane.txt](https://github.com/rdkit/rdkit/files/5913233/butane.txt)

edit:

To add another example, using the same MOL file and this code
```
m = Chem.rdmolfiles.MolFromMolFile('/Users/jasonb/butane.txt',removeHs = False)
mp = AllChem.MMFFGetMoleculeProperties(m)
globalFF = AllChem.MMFFGetMoleculeForceField(m, mp)
m2 = m;
res = []
for x in range(-180,180,5):
	ff2 = AllChem.MMFFGetMoleculeForceField(m2, mp)
	ff2.MMFFAddTorsionConstraint(0,1,2,3, False, x - .1, x + .1, 100000.0)
	ff2.Minimize()
	res.append(globalFF.CalcEnergy(ff2.Positions()))
res
```
I was previously able to get a nice plot like this,
<img width="307" alt="image" src="https://user-images.githubusercontent.com/16337306/106652423-0ed1ec80-655b-11eb-979c-260176ac21ad.png">
but it now returns a **very different** list

<img width="379" alt="image" src="https://user-images.githubusercontent.com/16337306/106652585-480a5c80-655b-11eb-8024-04a1eae90ade.png">

_Originally posted by @jasondbiggs in https://github.com/rdkit/rdkit/discussions/3780_

Hints:

Fix patch:
diff --git a/Code/ForceField/ForceField.cpp b/Code/ForceField/ForceField.cpp
index b4028891d2f..0133c5f854e 100644
--- a/Code/ForceField/ForceField.cpp
+++ b/Code/ForceField/ForceField.cpp
@@ -17,45 +17,40 @@
 namespace RDKit {
 namespace ForceFieldsHelper {
 void normalizeAngleDeg(double &angleDeg) {
-  double normFactor = 360.0;
-  if (angleDeg < 0.0) {
-    normFactor = -normFactor;
-  }
-  angleDeg = fmod(angleDeg, normFactor);
-  if (fabs(angleDeg) > 180.0) {
-    angleDeg -= normFactor;
+  angleDeg = fmod(angleDeg, 360.0);
+  if (angleDeg < -180.0) {
+    angleDeg += 360.0;
+  } else if (angleDeg > 180.0) {
+    angleDeg -= 360.0;
   }
 }
 
 void computeDihedral(const RDGeom::PointPtrVect &pos, unsigned int idx1,
-    unsigned int idx2, unsigned int idx3, unsigned int idx4,
-    double *dihedral, double *cosPhi, RDGeom::Point3D r[4],
-    RDGeom::Point3D t[2], double d[2]) {
+                     unsigned int idx2, unsigned int idx3, unsigned int idx4,
+                     double *dihedral, double *cosPhi, RDGeom::Point3D r[4],
+                     RDGeom::Point3D t[2], double d[2]) {
   computeDihedral(static_cast<RDGeom::Point3D *>(pos[idx1]),
-    static_cast<RDGeom::Point3D *>(pos[idx2]),
-    static_cast<RDGeom::Point3D *>(pos[idx3]),
-    static_cast<RDGeom::Point3D *>(pos[idx4]), dihedral, cosPhi, r, t, d);
+                  static_cast<RDGeom::Point3D *>(pos[idx2]),
+                  static_cast<RDGeom::Point3D *>(pos[idx3]),
+                  static_cast<RDGeom::Point3D *>(pos[idx4]), dihedral, cosPhi,
+                  r, t, d);
 }
 
-void computeDihedral(const double *pos, unsigned int idx1,
-    unsigned int idx2, unsigned int idx3, unsigned int idx4,
-    double *dihedral, double *cosPhi, RDGeom::Point3D r[4],
-    RDGeom::Point3D t[2], double d[2]) {
-  RDGeom::Point3D p1(pos[3 * idx1], pos[3 * idx1 + 1],
-                     pos[3 * idx1 + 2]);
-  RDGeom::Point3D p2(pos[3 * idx2], pos[3 * idx2 + 1],
-                     pos[3 * idx2 + 2]);
-  RDGeom::Point3D p3(pos[3 * idx3], pos[3 * idx3 + 1],
-                     pos[3 * idx3 + 2]);
-  RDGeom::Point3D p4(pos[3 * idx4], pos[3 * idx4 + 1],
-                     pos[3 * idx4 + 2]);
+void computeDihedral(const double *pos, unsigned int idx1, unsigned int idx2,
+                     unsigned int idx3, unsigned int idx4, double *dihedral,
+                     double *cosPhi, RDGeom::Point3D r[4], RDGeom::Point3D t[2],
+                     double d[2]) {
+  RDGeom::Point3D p1(pos[3 * idx1], pos[3 * idx1 + 1], pos[3 * idx1 + 2]);
+  RDGeom::Point3D p2(pos[3 * idx2], pos[3 * idx2 + 1], pos[3 * idx2 + 2]);
+  RDGeom::Point3D p3(pos[3 * idx3], pos[3 * idx3 + 1], pos[3 * idx3 + 2]);
+  RDGeom::Point3D p4(pos[3 * idx4], pos[3 * idx4 + 1], pos[3 * idx4 + 2]);
   computeDihedral(&p1, &p2, &p3, &p4, dihedral, cosPhi, r, t, d);
 }
 
 void computeDihedral(const RDGeom::Point3D *p1, const RDGeom::Point3D *p2,
-    const RDGeom::Point3D *p3, const RDGeom::Point3D *p4,
-    double *dihedral, double *cosPhi, RDGeom::Point3D r[4],
-    RDGeom::Point3D t[2], double d[2]) {
+                     const RDGeom::Point3D *p3, const RDGeom::Point3D *p4,
+                     double *dihedral, double *cosPhi, RDGeom::Point3D r[4],
+                     RDGeom::Point3D t[2], double d[2]) {
   PRECONDITION(p1, "p1 must not be null");
   PRECONDITION(p2, "p2 must not be null");
   PRECONDITION(p3, "p3 must not be null");
@@ -95,8 +90,8 @@ void computeDihedral(const RDGeom::Point3D *p1, const RDGeom::Point3D *p2,
     *dihedral = -atan2(m.dotProduct(t[1]) / mLength, *cosPhi);
   }
 }
-}
-}
+}  // namespace ForceFieldsHelper
+}  // namespace RDKit
 
 namespace ForceFieldsHelper {
 class calcEnergy {
@@ -152,7 +147,7 @@ class calcGradient {
  private:
   ForceFields::ForceField *mp_ffHolder;
 };
-}
+}  // namespace ForceFieldsHelper
 
 namespace ForceFields {
 ForceField::~ForceField() {
@@ -429,4 +424,4 @@ void ForceField::initDistanceMatrix() {
     dp_distMat[i] = -1.0;
   }
 }
-}
+}  // namespace ForceFields
diff --git a/Code/ForceField/MMFF/AngleConstraint.cpp b/Code/ForceField/MMFF/AngleConstraint.cpp
index 1bd4db0fc02..3670ba1c556 100644
--- a/Code/ForceField/MMFF/AngleConstraint.cpp
+++ b/Code/ForceField/MMFF/AngleConstraint.cpp
@@ -10,7 +10,6 @@
 //  which is included in the file license.txt, found at the root
 //  of the RDKit source tree.
 //
-#include "AngleBend.h"
 #include "AngleConstraint.h"
 #include "Params.h"
 #include <cmath>
@@ -19,28 +18,6 @@
 
 namespace ForceFields {
 namespace MMFF {
-void _pretreatAngles(double &minAngleDeg, double &maxAngleDeg) {
-  minAngleDeg = fmod(minAngleDeg, 360.0);
-  maxAngleDeg = fmod(maxAngleDeg, 360.0);
-  if (minAngleDeg > 180.0) {
-    minAngleDeg -= 360.0;
-  }
-  if (maxAngleDeg > 180.0) {
-    maxAngleDeg -= 360.0;
-  }
-  if ((minAngleDeg < 0.0) && (!(maxAngleDeg < 0.0))) {
-    maxAngleDeg = std::max(fabs(maxAngleDeg), fabs(minAngleDeg));
-    minAngleDeg = 0.0;
-  }
-  minAngleDeg = fabs(minAngleDeg);
-  maxAngleDeg = fabs(maxAngleDeg);
-  if (minAngleDeg > maxAngleDeg) {
-    double t = minAngleDeg;
-    minAngleDeg = maxAngleDeg;
-    maxAngleDeg = t;
-  }
-}
-
 AngleConstraintContrib::AngleConstraintContrib(
     ForceField *owner, unsigned int idx1, unsigned int idx2, unsigned int idx3,
     double minAngleDeg, double maxAngleDeg, double forceConst) {
@@ -48,8 +25,10 @@ AngleConstraintContrib::AngleConstraintContrib(
   URANGE_CHECK(idx1, owner->positions().size());
   URANGE_CHECK(idx2, owner->positions().size());
   URANGE_CHECK(idx3, owner->positions().size());
-  PRECONDITION(maxAngleDeg >= minAngleDeg, "allowedDeltaDeg must be >= 0.0");
-  _pretreatAngles(minAngleDeg, maxAngleDeg);
+  PRECONDITION(!(minAngleDeg > maxAngleDeg),
+               "minAngleDeg must be <= maxAngleDeg");
+  RDKit::ForceFieldsHelper::normalizeAngleDeg(minAngleDeg);
+  RDKit::ForceFieldsHelper::normalizeAngleDeg(maxAngleDeg);
 
   dp_forceField = owner;
   d_at1Idx = idx1;
@@ -68,7 +47,8 @@ AngleConstraintContrib::AngleConstraintContrib(
   URANGE_CHECK(idx1, pos.size());
   URANGE_CHECK(idx2, pos.size());
   URANGE_CHECK(idx3, pos.size());
-  PRECONDITION(maxAngleDeg >= minAngleDeg, "allowedDeltaDeg must be >= 0.0");
+  PRECONDITION(!(minAngleDeg > maxAngleDeg),
+               "minAngleDeg must be <= maxAngleDeg");
 
   double angle = 0.0;
   if (relative) {
@@ -89,19 +69,27 @@ AngleConstraintContrib::AngleConstraintContrib(
   d_at3Idx = idx3;
   minAngleDeg += angle;
   maxAngleDeg += angle;
-  _pretreatAngles(minAngleDeg, maxAngleDeg);
+  RDKit::ForceFieldsHelper::normalizeAngleDeg(minAngleDeg);
+  RDKit::ForceFieldsHelper::normalizeAngleDeg(maxAngleDeg);
   d_minAngleDeg = minAngleDeg;
   d_maxAngleDeg = maxAngleDeg;
   d_forceConstant = forceConst;
 }
 
+double AngleConstraintContrib::computeAngleTerm(double angle) const {
+  double angleTerm = 0.0;
+  if (angle < d_minAngleDeg) {
+    angleTerm = angle - d_minAngleDeg;
+  } else if (angle > d_maxAngleDeg) {
+    angleTerm = angle - d_maxAngleDeg;
+  }
+  return angleTerm;
+}
+
 double AngleConstraintContrib::getEnergy(double *pos) const {
   PRECONDITION(dp_forceField, "no owner");
   PRECONDITION(pos, "bad vector");
 
-  double dist1 = dp_forceField->distance(d_at1Idx, d_at2Idx, pos);
-  double dist2 = dp_forceField->distance(d_at2Idx, d_at3Idx, pos);
-
   RDGeom::Point3D p1(pos[3 * d_at1Idx], pos[3 * d_at1Idx + 1],
                      pos[3 * d_at1Idx + 2]);
   RDGeom::Point3D p2(pos[3 * d_at2Idx], pos[3 * d_at2Idx + 1],
@@ -109,19 +97,14 @@ double AngleConstraintContrib::getEnergy(double *pos) const {
   RDGeom::Point3D p3(pos[3 * d_at3Idx], pos[3 * d_at3Idx + 1],
                      pos[3 * d_at3Idx + 2]);
 
-  RDGeom::Point3D p12 = (p1 - p2) / dist1;
-  RDGeom::Point3D p32 = (p3 - p2) / dist2;
-  double cosTheta = p12.dotProduct(p32);
+  RDGeom::Point3D r[2] = {p1 - p2, p3 - p2};
+  double rLengthSq[2] = {(std::max)(1.0e-5, r[0].lengthSq()),
+                         (std::max)(1.0e-5, r[1].lengthSq())};
+  double cosTheta = r[0].dotProduct(r[1]) / sqrt(rLengthSq[0] * rLengthSq[1]);
   clipToOne(cosTheta);
   double angle = RAD2DEG * acos(cosTheta);
-  double angleTerm = 0.0;
-  if (angle < d_minAngleDeg) {
-    angleTerm = angle - d_minAngleDeg;
-  } else if (angle > d_maxAngleDeg) {
-    angleTerm = angle - d_maxAngleDeg;
-  }
-  double const c = 0.5 * DEG2RAD * DEG2RAD;
-  double res = c * d_forceConstant * angleTerm * angleTerm;
+  double angleTerm = computeAngleTerm(angle);
+  double res = d_forceConstant * angleTerm * angleTerm;
 
   return res;
 }
@@ -131,39 +114,38 @@ void AngleConstraintContrib::getGrad(double *pos, double *grad) const {
   PRECONDITION(pos, "bad vector");
   PRECONDITION(grad, "bad vector");
 
-  double dist[2] = {dp_forceField->distance(d_at1Idx, d_at2Idx, pos),
-                    dp_forceField->distance(d_at2Idx, d_at3Idx, pos)};
-
   RDGeom::Point3D p1(pos[3 * d_at1Idx], pos[3 * d_at1Idx + 1],
                      pos[3 * d_at1Idx + 2]);
   RDGeom::Point3D p2(pos[3 * d_at2Idx], pos[3 * d_at2Idx + 1],
                      pos[3 * d_at2Idx + 2]);
   RDGeom::Point3D p3(pos[3 * d_at3Idx], pos[3 * d_at3Idx + 1],
                      pos[3 * d_at3Idx + 2]);
+
   double *g[3] = {&(grad[3 * d_at1Idx]), &(grad[3 * d_at2Idx]),
                   &(grad[3 * d_at3Idx])};
-  RDGeom::Point3D r[2] = {(p1 - p2) / dist[0], (p3 - p2) / dist[1]};
-  double cosTheta = r[0].dotProduct(r[1]);
+  RDGeom::Point3D r[2] = {p1 - p2, p3 - p2};
+  double rLengthSq[2] = {(std::max)(1.0e-5, r[0].lengthSq()),
+                         (std::max)(1.0e-5, r[1].lengthSq())};
+  double cosTheta = r[0].dotProduct(r[1]) / sqrt(rLengthSq[0] * rLengthSq[1]);
   clipToOne(cosTheta);
-  double sinThetaSq = 1.0 - cosTheta * cosTheta;
-  double sinTheta =
-      std::max(((sinThetaSq > 0.0) ? sqrt(sinThetaSq) : 0.0), 1.0e-8);
 
-  // use the chain rule:
-  // dE/dx = dE/dTheta * dTheta/dx
-
-  // dE/dTheta is independent of cartesians:
   double angle = RAD2DEG * acos(cosTheta);
-  double angleTerm = 0.0;
-  if (angle < d_minAngleDeg) {
-    angleTerm = angle - d_minAngleDeg;
-  } else if (angle > d_maxAngleDeg) {
-    angleTerm = angle - d_maxAngleDeg;
+  double angleTerm = computeAngleTerm(angle);
+
+  double dE_dTheta = 2.0 * RAD2DEG * d_forceConstant * angleTerm;
+
+  RDGeom::Point3D rp = r[1].crossProduct(r[0]);
+  double prefactor = dE_dTheta / (std::max)(1.0e-5, rp.length());
+  double t[2] = {-prefactor / rLengthSq[0], prefactor / rLengthSq[1]};
+  RDGeom::Point3D dedp[3];
+  dedp[0] = r[0].crossProduct(rp) * t[0];
+  dedp[2] = r[1].crossProduct(rp) * t[1];
+  dedp[1] = -dedp[0] - dedp[2];
+  for (unsigned int i = 0; i < 3; ++i) {
+    g[i][0] += dedp[i].x;
+    g[i][1] += dedp[i].y;
+    g[i][2] += dedp[i].z;
   }
-
-  double dE_dTheta = DEG2RAD * d_forceConstant * angleTerm;
-
-  Utils::calcAngleBendGrad(r, dist, g, dE_dTheta, cosTheta, sinTheta);
-}
-}
 }
+}  // namespace MMFF
+}  // namespace ForceFields
diff --git a/Code/ForceField/MMFF/AngleConstraint.h b/Code/ForceField/MMFF/AngleConstraint.h
index e0e6ebd1bdb..48ff353f0a3 100644
--- a/Code/ForceField/MMFF/AngleConstraint.h
+++ b/Code/ForceField/MMFF/AngleConstraint.h
@@ -53,6 +53,7 @@ class RDKIT_FORCEFIELD_EXPORT AngleConstraintContrib
   };
 
  private:
+  double computeAngleTerm(double angle) const;
   int d_at1Idx{-1}, d_at2Idx{-1},
       d_at3Idx{-1};                     //!< indices of atoms forming the angle
   double d_minAngleDeg, d_maxAngleDeg;  //!< rest amplitudes of the angle
diff --git a/Code/ForceField/MMFF/TorsionConstraint.cpp b/Code/ForceField/MMFF/TorsionConstraint.cpp
index 3e87a22059a..d3a391cac76 100644
--- a/Code/ForceField/MMFF/TorsionConstraint.cpp
+++ b/Code/ForceField/MMFF/TorsionConstraint.cpp
@@ -10,7 +10,6 @@
 //  which is included in the file license.txt, found at the root
 //  of the RDKit source tree.
 //
-#include "TorsionAngle.h"
 #include "TorsionConstraint.h"
 #include "Params.h"
 #include <cmath>
@@ -25,8 +24,12 @@
 namespace ForceFields {
 namespace MMFF {
 inline void checkPrecondition(const ForceField *owner, unsigned int idx1,
-    unsigned int idx2, unsigned int idx3, unsigned int idx4) {
+                              unsigned int idx2, unsigned int idx3,
+                              unsigned int idx4, double minDihedralDeg,
+                              double maxDihedralDeg) {
   PRECONDITION(owner, "bad owner");
+  PRECONDITION(!(minDihedralDeg > maxDihedralDeg),
+               "minDihedralDeg must be <= maxDihedralDeg");
   URANGE_CHECK(idx1, owner->positions().size());
   URANGE_CHECK(idx2, owner->positions().size());
   URANGE_CHECK(idx3, owner->positions().size());
@@ -35,29 +38,35 @@ inline void checkPrecondition(const ForceField *owner, unsigned int idx1,
 
 double TorsionConstraintContrib::computeDihedralTerm(double dihedral) const {
   double dihedralTarget = dihedral;
-  if (!(dihedral > d_minDihedralDeg && dihedral < d_maxDihedralDeg)
-      && !(dihedral > d_minDihedralDeg && d_minDihedralDeg > d_maxDihedralDeg)
-      && !(dihedral < d_maxDihedralDeg && d_minDihedralDeg > d_maxDihedralDeg)) {
-    double minDihedralTarget = dihedral - d_minDihedralDeg;
-    double maxDihedralTarget = dihedral - d_maxDihedralDeg;
-    RDKit::ForceFieldsHelper::normalizeAngleDeg(minDihedralTarget);
-    RDKit::ForceFieldsHelper::normalizeAngleDeg(maxDihedralTarget);
-    dihedralTarget = (fabs(minDihedralTarget) < fabs(maxDihedralTarget)
-      ? d_minDihedralDeg : d_maxDihedralDeg);
+  if (!(dihedral > d_minDihedralDeg && dihedral < d_maxDihedralDeg) &&
+      !(dihedral > d_minDihedralDeg && d_minDihedralDeg > d_maxDihedralDeg) &&
+      !(dihedral < d_maxDihedralDeg && d_minDihedralDeg > d_maxDihedralDeg)) {
+    double dihedralMinTarget = dihedral - d_minDihedralDeg;
+    RDKit::ForceFieldsHelper::normalizeAngleDeg(dihedralMinTarget);
+    double dihedralMaxTarget = dihedral - d_maxDihedralDeg;
+    RDKit::ForceFieldsHelper::normalizeAngleDeg(dihedralMaxTarget);
+    if (fabs(dihedralMinTarget) < fabs(dihedralMaxTarget)) {
+      dihedralTarget = d_minDihedralDeg;
+    } else {
+      dihedralTarget = d_maxDihedralDeg;
+    }
   }
   double dihedralTerm = dihedral - dihedralTarget;
   RDKit::ForceFieldsHelper::normalizeAngleDeg(dihedralTerm);
   return dihedralTerm;
 }
 
-void TorsionConstraintContrib::setParameters(ForceField *owner, unsigned int idx1,
-    unsigned int idx2, unsigned int idx3, unsigned int idx4, double minDihedralDeg,
-    double maxDihedralDeg, double forceConst) {
+void TorsionConstraintContrib::setParameters(
+    ForceField *owner, unsigned int idx1, unsigned int idx2, unsigned int idx3,
+    unsigned int idx4, double minDihedralDeg, double maxDihedralDeg,
+    double forceConst) {
   dp_forceField = owner;
   d_at1Idx = idx1;
   d_at2Idx = idx2;
   d_at3Idx = idx3;
   d_at4Idx = idx4;
+  RDKit::ForceFieldsHelper::normalizeAngleDeg(minDihedralDeg);
+  RDKit::ForceFieldsHelper::normalizeAngleDeg(maxDihedralDeg);
   d_minDihedralDeg = minDihedralDeg;
   d_maxDihedralDeg = maxDihedralDeg;
   d_forceConstant = forceConst;
@@ -67,44 +76,39 @@ TorsionConstraintContrib::TorsionConstraintContrib(
     ForceField *owner, unsigned int idx1, unsigned int idx2, unsigned int idx3,
     unsigned int idx4, double minDihedralDeg, double maxDihedralDeg,
     double forceConst) {
-  checkPrecondition(owner, idx1, idx2, idx3, idx4);
-  RDKit::ForceFieldsHelper::normalizeAngleDeg(minDihedralDeg);
-  RDKit::ForceFieldsHelper::normalizeAngleDeg(maxDihedralDeg);
-  setParameters(owner, idx1, idx2, idx3, idx4,
-    minDihedralDeg, maxDihedralDeg, forceConst);
+  checkPrecondition(owner, idx1, idx2, idx3, idx4, minDihedralDeg,
+                    maxDihedralDeg);
+  setParameters(owner, idx1, idx2, idx3, idx4, minDihedralDeg, maxDihedralDeg,
+                forceConst);
 }
 
 TorsionConstraintContrib::TorsionConstraintContrib(
     ForceField *owner, unsigned int idx1, unsigned int idx2, unsigned int idx3,
     unsigned int idx4, bool relative, double minDihedralDeg,
     double maxDihedralDeg, double forceConst) {
-  checkPrecondition(owner, idx1, idx2, idx3, idx4);
-  RDKit::ForceFieldsHelper::normalizeAngleDeg(minDihedralDeg);
-  RDKit::ForceFieldsHelper::normalizeAngleDeg(maxDihedralDeg);
+  checkPrecondition(owner, idx1, idx2, idx3, idx4, minDihedralDeg,
+                    maxDihedralDeg);
   if (relative) {
     double dihedral;
-    RDKit::ForceFieldsHelper::computeDihedral(
-        owner->positions(), idx1, idx2, idx3, idx4, &dihedral);
+    RDKit::ForceFieldsHelper::computeDihedral(owner->positions(), idx1, idx2,
+                                              idx3, idx4, &dihedral);
     dihedral *= RAD2DEG;
     minDihedralDeg += dihedral;
     maxDihedralDeg += dihedral;
-    RDKit::ForceFieldsHelper::normalizeAngleDeg(minDihedralDeg);
-    RDKit::ForceFieldsHelper::normalizeAngleDeg(maxDihedralDeg);
   }
-  setParameters(owner, idx1, idx2, idx3, idx4,
-    minDihedralDeg, maxDihedralDeg, forceConst);
+  setParameters(owner, idx1, idx2, idx3, idx4, minDihedralDeg, maxDihedralDeg,
+                forceConst);
 }
 
 double TorsionConstraintContrib::getEnergy(double *pos) const {
   PRECONDITION(dp_forceField, "no owner");
   PRECONDITION(pos, "bad vector");
   double dihedral;
-  RDKit::ForceFieldsHelper::computeDihedral(
-    pos, d_at1Idx, d_at2Idx, d_at3Idx, d_at4Idx, &dihedral);
+  RDKit::ForceFieldsHelper::computeDihedral(pos, d_at1Idx, d_at2Idx, d_at3Idx,
+                                            d_at4Idx, &dihedral);
   dihedral *= RAD2DEG;
   double dihedralTerm = computeDihedralTerm(dihedral);
-  static double const c = 0.5 * DEG2RAD * DEG2RAD;
-  double res = c * d_forceConstant * dihedralTerm * dihedralTerm;
+  double res = d_forceConstant * dihedralTerm * dihedralTerm;
 
   return res;
 }
@@ -120,23 +124,35 @@ void TorsionConstraintContrib::getGrad(double *pos, double *grad) const {
   RDGeom::Point3D r[4];
   RDGeom::Point3D t[2];
   double d[2];
-  double cosPhi;
   double dihedral;
   RDKit::ForceFieldsHelper::computeDihedral(
-    pos, d_at1Idx, d_at2Idx, d_at3Idx, d_at4Idx, &dihedral, &cosPhi, r, t, d);
+      pos, d_at1Idx, d_at2Idx, d_at3Idx, d_at4Idx, &dihedral, nullptr, r, t, d);
   dihedral *= RAD2DEG;
-  double sinPhiSq = 1.0 - cosPhi * cosPhi;
-  double sinPhi = ((sinPhiSq > 0.0) ? sqrt(sinPhiSq) : 0.0);
-  // dE/dPhi is independent of cartesians:
   double dihedralTerm = computeDihedralTerm(dihedral);
-  double dE_dPhi = DEG2RAD * d_forceConstant * dihedralTerm;
+  double dE_dPhi = 2.0 * RAD2DEG * d_forceConstant * dihedralTerm;
 
-  // FIX: use a tolerance here
-  // this is hacky, but it's per the
-  // recommendation from Niketic and Rasmussen:
-  double sinTerm =
-      -dE_dPhi * (isDoubleZero(sinPhi) ? (1.0 / cosPhi) : (1.0 / sinPhi));
-  Utils::calcTorsionGrad(r, t, d, g, sinTerm, cosPhi);
-}
-}
+  double d23 = dp_forceField->distance(d_at2Idx, d_at3Idx, pos);
+  RDGeom::Point3D r31(pos[3 * d_at3Idx] - pos[3 * d_at1Idx],
+                      pos[3 * d_at3Idx + 1] - pos[3 * d_at1Idx + 1],
+                      pos[3 * d_at3Idx + 2] - pos[3 * d_at1Idx + 2]);
+  RDGeom::Point3D r42(pos[3 * d_at4Idx] - pos[3 * d_at2Idx],
+                      pos[3 * d_at4Idx + 1] - pos[3 * d_at2Idx + 1],
+                      pos[3 * d_at4Idx + 2] - pos[3 * d_at2Idx + 2]);
+  double prefactor = dE_dPhi / d23;
+  RDGeom::Point3D tt[2] = {r[0].crossProduct(r[1]), r[2].crossProduct(r[3])};
+  RDGeom::Point3D dedt[2] = {
+      tt[0].crossProduct(r[2]) / tt[0].lengthSq() * prefactor,
+      tt[1].crossProduct(r[1]) / tt[1].lengthSq() * prefactor};
+  RDGeom::Point3D dedp[4] = {
+      r[2].crossProduct(dedt[0]),
+      r31.crossProduct(dedt[0]) - r[3].crossProduct(dedt[1]),
+      r[0].crossProduct(dedt[0]) + r42.crossProduct(dedt[1]),
+      r[2].crossProduct(dedt[1])};
+  for (unsigned int i = 0; i < 4; ++i) {
+    g[i][0] += dedp[i].x;
+    g[i][1] += dedp[i].y;
+    g[i][2] += dedp[i].z;
+  }
 }
+}  // namespace MMFF
+}  // namespace ForceFields
diff --git a/Code/ForceField/UFF/AngleConstraint.cpp b/Code/ForceField/UFF/AngleConstraint.cpp
index 37111b8aa5b..7c69d20b92c 100644
--- a/Code/ForceField/UFF/AngleConstraint.cpp
+++ b/Code/ForceField/UFF/AngleConstraint.cpp
@@ -10,7 +10,6 @@
 //  which is included in the file license.txt, found at the root
 //  of the RDKit source tree.
 //
-#include "AngleBend.h"
 #include "AngleConstraint.h"
 #include "Params.h"
 #include <cmath>
@@ -19,28 +18,6 @@
 
 namespace ForceFields {
 namespace UFF {
-void _pretreatAngles(double &minAngleDeg, double &maxAngleDeg) {
-  minAngleDeg = fmod(minAngleDeg, 360.0);
-  maxAngleDeg = fmod(maxAngleDeg, 360.0);
-  if (minAngleDeg > 180.0) {
-    minAngleDeg -= 360.0;
-  }
-  if (maxAngleDeg > 180.0) {
-    maxAngleDeg -= 360.0;
-  }
-  if ((minAngleDeg < 0.0) && (!(maxAngleDeg < 0.0))) {
-    maxAngleDeg = std::max(fabs(maxAngleDeg), fabs(minAngleDeg));
-    minAngleDeg = 0.0;
-  }
-  minAngleDeg = fabs(minAngleDeg);
-  maxAngleDeg = fabs(maxAngleDeg);
-  if (minAngleDeg > maxAngleDeg) {
-    double t = minAngleDeg;
-    minAngleDeg = maxAngleDeg;
-    maxAngleDeg = t;
-  }
-}
-
 AngleConstraintContrib::AngleConstraintContrib(
     ForceField *owner, unsigned int idx1, unsigned int idx2, unsigned int idx3,
     double minAngleDeg, double maxAngleDeg, double forceConst) {
@@ -48,8 +25,10 @@ AngleConstraintContrib::AngleConstraintContrib(
   URANGE_CHECK(idx1, owner->positions().size());
   URANGE_CHECK(idx2, owner->positions().size());
   URANGE_CHECK(idx3, owner->positions().size());
-  PRECONDITION(maxAngleDeg >= minAngleDeg, "allowedDeltaDeg must be >= 0.0");
-  _pretreatAngles(minAngleDeg, maxAngleDeg);
+  PRECONDITION(!(minAngleDeg > maxAngleDeg),
+               "minAngleDeg must be <= maxAngleDeg");
+  RDKit::ForceFieldsHelper::normalizeAngleDeg(minAngleDeg);
+  RDKit::ForceFieldsHelper::normalizeAngleDeg(maxAngleDeg);
 
   dp_forceField = owner;
   d_at1Idx = idx1;
@@ -68,7 +47,8 @@ AngleConstraintContrib::AngleConstraintContrib(
   URANGE_CHECK(idx1, pos.size());
   URANGE_CHECK(idx2, pos.size());
   URANGE_CHECK(idx3, pos.size());
-  PRECONDITION(maxAngleDeg >= minAngleDeg, "allowedDeltaDeg must be >= 0.0");
+  PRECONDITION(!(minAngleDeg > maxAngleDeg),
+               "minAngleDeg must be <= maxAngleDeg");
 
   double angle = 0.0;
   if (relative) {
@@ -89,19 +69,27 @@ AngleConstraintContrib::AngleConstraintContrib(
   d_at3Idx = idx3;
   minAngleDeg += angle;
   maxAngleDeg += angle;
-  _pretreatAngles(minAngleDeg, maxAngleDeg);
+  RDKit::ForceFieldsHelper::normalizeAngleDeg(minAngleDeg);
+  RDKit::ForceFieldsHelper::normalizeAngleDeg(maxAngleDeg);
   d_minAngleDeg = minAngleDeg;
   d_maxAngleDeg = maxAngleDeg;
   d_forceConstant = forceConst;
 }
 
+double AngleConstraintContrib::computeAngleTerm(double angle) const {
+  double angleTerm = 0.0;
+  if (angle < d_minAngleDeg) {
+    angleTerm = angle - d_minAngleDeg;
+  } else if (angle > d_maxAngleDeg) {
+    angleTerm = angle - d_maxAngleDeg;
+  }
+  return angleTerm;
+}
+
 double AngleConstraintContrib::getEnergy(double *pos) const {
   PRECONDITION(dp_forceField, "no owner");
   PRECONDITION(pos, "bad vector");
 
-  double dist1 = dp_forceField->distance(d_at1Idx, d_at2Idx, pos);
-  double dist2 = dp_forceField->distance(d_at2Idx, d_at3Idx, pos);
-
   RDGeom::Point3D p1(pos[3 * d_at1Idx], pos[3 * d_at1Idx + 1],
                      pos[3 * d_at1Idx + 2]);
   RDGeom::Point3D p2(pos[3 * d_at2Idx], pos[3 * d_at2Idx + 1],
@@ -109,19 +97,14 @@ double AngleConstraintContrib::getEnergy(double *pos) const {
   RDGeom::Point3D p3(pos[3 * d_at3Idx], pos[3 * d_at3Idx + 1],
                      pos[3 * d_at3Idx + 2]);
 
-  RDGeom::Point3D p12 = (p1 - p2) / dist1;
-  RDGeom::Point3D p32 = (p3 - p2) / dist2;
-  double cosTheta = p12.dotProduct(p32);
+  RDGeom::Point3D r[2] = {p1 - p2, p3 - p2};
+  double rLengthSq[2] = {(std::max)(1.0e-5, r[0].lengthSq()),
+                         (std::max)(1.0e-5, r[1].lengthSq())};
+  double cosTheta = r[0].dotProduct(r[1]) / sqrt(rLengthSq[0] * rLengthSq[1]);
   clipToOne(cosTheta);
   double angle = RAD2DEG * acos(cosTheta);
-  double angleTerm = 0.0;
-  if (angle < d_minAngleDeg) {
-    angleTerm = angle - d_minAngleDeg;
-  } else if (angle > d_maxAngleDeg) {
-    angleTerm = angle - d_maxAngleDeg;
-  }
-  double const c = 0.5 * DEG2RAD * DEG2RAD;
-  double res = c * d_forceConstant * angleTerm * angleTerm;
+  double angleTerm = computeAngleTerm(angle);
+  double res = d_forceConstant * angleTerm * angleTerm;
 
   return res;
 }
@@ -131,39 +114,38 @@ void AngleConstraintContrib::getGrad(double *pos, double *grad) const {
   PRECONDITION(pos, "bad vector");
   PRECONDITION(grad, "bad vector");
 
-  double dist[2] = {dp_forceField->distance(d_at1Idx, d_at2Idx, pos),
-                    dp_forceField->distance(d_at2Idx, d_at3Idx, pos)};
-
   RDGeom::Point3D p1(pos[3 * d_at1Idx], pos[3 * d_at1Idx + 1],
                      pos[3 * d_at1Idx + 2]);
   RDGeom::Point3D p2(pos[3 * d_at2Idx], pos[3 * d_at2Idx + 1],
                      pos[3 * d_at2Idx + 2]);
   RDGeom::Point3D p3(pos[3 * d_at3Idx], pos[3 * d_at3Idx + 1],
                      pos[3 * d_at3Idx + 2]);
+
   double *g[3] = {&(grad[3 * d_at1Idx]), &(grad[3 * d_at2Idx]),
                   &(grad[3 * d_at3Idx])};
-  RDGeom::Point3D r[2] = {(p1 - p2) / dist[0], (p3 - p2) / dist[1]};
-  double cosTheta = r[0].dotProduct(r[1]);
+  RDGeom::Point3D r[2] = {p1 - p2, p3 - p2};
+  double rLengthSq[2] = {(std::max)(1.0e-5, r[0].lengthSq()),
+                         (std::max)(1.0e-5, r[1].lengthSq())};
+  double cosTheta = r[0].dotProduct(r[1]) / sqrt(rLengthSq[0] * rLengthSq[1]);
   clipToOne(cosTheta);
-  double sinThetaSq = 1.0 - cosTheta * cosTheta;
-  double sinTheta =
-      std::max(((sinThetaSq > 0.0) ? sqrt(sinThetaSq) : 0.0), 1.0e-8);
 
-  // use the chain rule:
-  // dE/dx = dE/dTheta * dTheta/dx
-
-  // dE/dTheta is independent of cartesians:
   double angle = RAD2DEG * acos(cosTheta);
-  double angleTerm = 0.0;
-  if (angle < d_minAngleDeg) {
-    angleTerm = angle - d_minAngleDeg;
-  } else if (angle > d_maxAngleDeg) {
-    angleTerm = angle - d_maxAngleDeg;
+  double angleTerm = computeAngleTerm(angle);
+
+  double dE_dTheta = 2.0 * RAD2DEG * d_forceConstant * angleTerm;
+
+  RDGeom::Point3D rp = r[1].crossProduct(r[0]);
+  double prefactor = dE_dTheta / (std::max)(1.0e-5, rp.length());
+  double t[2] = {-prefactor / rLengthSq[0], prefactor / rLengthSq[1]};
+  RDGeom::Point3D dedp[3];
+  dedp[0] = r[0].crossProduct(rp) * t[0];
+  dedp[2] = r[1].crossProduct(rp) * t[1];
+  dedp[1] = -dedp[0] - dedp[2];
+  for (unsigned int i = 0; i < 3; ++i) {
+    g[i][0] += dedp[i].x;
+    g[i][1] += dedp[i].y;
+    g[i][2] += dedp[i].z;
   }
-
-  double dE_dTheta = DEG2RAD * d_forceConstant * angleTerm;
-
-  Utils::calcAngleBendGrad(r, dist, g, dE_dTheta, cosTheta, sinTheta);
-}
-}
 }
+}  // namespace UFF
+}  // namespace ForceFields
diff --git a/Code/ForceField/UFF/AngleConstraint.h b/Code/ForceField/UFF/AngleConstraint.h
index 66bc58f1cec..e9930a8b991 100644
--- a/Code/ForceField/UFF/AngleConstraint.h
+++ b/Code/ForceField/UFF/AngleConstraint.h
@@ -22,7 +22,7 @@ namespace UFF {
 class RDKIT_FORCEFIELD_EXPORT AngleConstraintContrib
     : public ForceFieldContrib {
  public:
-  AngleConstraintContrib()  {};
+  AngleConstraintContrib(){};
   //! Constructor
   /*!
   \param owner       pointer to the owning ForceField
@@ -47,12 +47,15 @@ class RDKIT_FORCEFIELD_EXPORT AngleConstraintContrib
   double getEnergy(double *pos) const;
 
   void getGrad(double *pos, double *grad) const;
+
   virtual AngleConstraintContrib *copy() const {
     return new AngleConstraintContrib(*this);
   };
 
  private:
-  int d_at1Idx{-1}, d_at2Idx{-1}, d_at3Idx{-1};     //!< indices of atoms forming the angle
+  double computeAngleTerm(double angle) const;
+  int d_at1Idx{-1}, d_at2Idx{-1},
+      d_at3Idx{-1};                     //!< indices of atoms forming the angle
   double d_minAngleDeg, d_maxAngleDeg;  //!< rest amplitudes of the angle
   double d_forceConstant;  //!< force constant of the angle constraint
 };
diff --git a/Code/ForceField/UFF/TorsionConstraint.cpp b/Code/ForceField/UFF/TorsionConstraint.cpp
index 5215612b8b7..cef3c0fabdd 100644
--- a/Code/ForceField/UFF/TorsionConstraint.cpp
+++ b/Code/ForceField/UFF/TorsionConstraint.cpp
@@ -10,7 +10,6 @@
 //  which is included in the file license.txt, found at the root
 //  of the RDKit source tree.
 //
-#include "TorsionAngle.h"
 #include "TorsionConstraint.h"
 #include "Params.h"
 #include <cmath>
@@ -25,8 +24,12 @@
 namespace ForceFields {
 namespace UFF {
 inline void checkPrecondition(const ForceField *owner, unsigned int idx1,
-    unsigned int idx2, unsigned int idx3, unsigned int idx4) {
+                              unsigned int idx2, unsigned int idx3,
+                              unsigned int idx4, double minDihedralDeg,
+                              double maxDihedralDeg) {
   PRECONDITION(owner, "bad owner");
+  PRECONDITION(!(minDihedralDeg > maxDihedralDeg),
+               "minDihedralDeg must be <= maxDihedralDeg");
   URANGE_CHECK(idx1, owner->positions().size());
   URANGE_CHECK(idx2, owner->positions().size());
   URANGE_CHECK(idx3, owner->positions().size());
@@ -35,29 +38,35 @@ inline void checkPrecondition(const ForceField *owner, unsigned int idx1,
 
 double TorsionConstraintContrib::computeDihedralTerm(double dihedral) const {
   double dihedralTarget = dihedral;
-  if (!(dihedral > d_minDihedralDeg && dihedral < d_maxDihedralDeg)
-      && !(dihedral > d_minDihedralDeg && d_minDihedralDeg > d_maxDihedralDeg)
-      && !(dihedral < d_maxDihedralDeg && d_minDihedralDeg > d_maxDihedralDeg)) {
-    double minDihedralTarget = dihedral - d_minDihedralDeg;
-    double maxDihedralTarget = dihedral - d_maxDihedralDeg;
-    RDKit::ForceFieldsHelper::normalizeAngleDeg(minDihedralTarget);
-    RDKit::ForceFieldsHelper::normalizeAngleDeg(maxDihedralTarget);
-    dihedralTarget = (fabs(minDihedralTarget) < fabs(maxDihedralTarget)
-      ? d_minDihedralDeg : d_maxDihedralDeg);
+  if (!(dihedral > d_minDihedralDeg && dihedral < d_maxDihedralDeg) &&
+      !(dihedral > d_minDihedralDeg && d_minDihedralDeg > d_maxDihedralDeg) &&
+      !(dihedral < d_maxDihedralDeg && d_minDihedralDeg > d_maxDihedralDeg)) {
+    double dihedralMinTarget = dihedral - d_minDihedralDeg;
+    RDKit::ForceFieldsHelper::normalizeAngleDeg(dihedralMinTarget);
+    double dihedralMaxTarget = dihedral - d_maxDihedralDeg;
+    RDKit::ForceFieldsHelper::normalizeAngleDeg(dihedralMaxTarget);
+    if (fabs(dihedralMinTarget) < fabs(dihedralMaxTarget)) {
+      dihedralTarget = d_minDihedralDeg;
+    } else {
+      dihedralTarget = d_maxDihedralDeg;
+    }
   }
   double dihedralTerm = dihedral - dihedralTarget;
   RDKit::ForceFieldsHelper::normalizeAngleDeg(dihedralTerm);
   return dihedralTerm;
 }
 
-void TorsionConstraintContrib::setParameters(ForceField *owner, unsigned int idx1,
-    unsigned int idx2, unsigned int idx3, unsigned int idx4, double minDihedralDeg,
-    double maxDihedralDeg, double forceConst) {
+void TorsionConstraintContrib::setParameters(
+    ForceField *owner, unsigned int idx1, unsigned int idx2, unsigned int idx3,
+    unsigned int idx4, double minDihedralDeg, double maxDihedralDeg,
+    double forceConst) {
   dp_forceField = owner;
   d_at1Idx = idx1;
   d_at2Idx = idx2;
   d_at3Idx = idx3;
   d_at4Idx = idx4;
+  RDKit::ForceFieldsHelper::normalizeAngleDeg(minDihedralDeg);
+  RDKit::ForceFieldsHelper::normalizeAngleDeg(maxDihedralDeg);
   d_minDihedralDeg = minDihedralDeg;
   d_maxDihedralDeg = maxDihedralDeg;
   d_forceConstant = forceConst;
@@ -67,44 +76,39 @@ TorsionConstraintContrib::TorsionConstraintContrib(
     ForceField *owner, unsigned int idx1, unsigned int idx2, unsigned int idx3,
     unsigned int idx4, double minDihedralDeg, double maxDihedralDeg,
     double forceConst) {
-  checkPrecondition(owner, idx1, idx2, idx3, idx4);
-  RDKit::ForceFieldsHelper::normalizeAngleDeg(minDihedralDeg);
-  RDKit::ForceFieldsHelper::normalizeAngleDeg(maxDihedralDeg);
-  setParameters(owner, idx1, idx2, idx3, idx4,
-    minDihedralDeg, maxDihedralDeg, forceConst);
+  checkPrecondition(owner, idx1, idx2, idx3, idx4, minDihedralDeg,
+                    maxDihedralDeg);
+  setParameters(owner, idx1, idx2, idx3, idx4, minDihedralDeg, maxDihedralDeg,
+                forceConst);
 }
 
 TorsionConstraintContrib::TorsionConstraintContrib(
     ForceField *owner, unsigned int idx1, unsigned int idx2, unsigned int idx3,
     unsigned int idx4, bool relative, double minDihedralDeg,
     double maxDihedralDeg, double forceConst) {
-  checkPrecondition(owner, idx1, idx2, idx3, idx4);
-  RDKit::ForceFieldsHelper::normalizeAngleDeg(minDihedralDeg);
-  RDKit::ForceFieldsHelper::normalizeAngleDeg(maxDihedralDeg);
+  checkPrecondition(owner, idx1, idx2, idx3, idx4, minDihedralDeg,
+                    maxDihedralDeg);
   if (relative) {
     double dihedral;
-    RDKit::ForceFieldsHelper::computeDihedral(
-        owner->positions(), idx1, idx2, idx3, idx4, &dihedral);
+    RDKit::ForceFieldsHelper::computeDihedral(owner->positions(), idx1, idx2,
+                                              idx3, idx4, &dihedral);
     dihedral *= RAD2DEG;
     minDihedralDeg += dihedral;
     maxDihedralDeg += dihedral;
-    RDKit::ForceFieldsHelper::normalizeAngleDeg(minDihedralDeg);
-    RDKit::ForceFieldsHelper::normalizeAngleDeg(maxDihedralDeg);
   }
-  setParameters(owner, idx1, idx2, idx3, idx4,
-    minDihedralDeg, maxDihedralDeg, forceConst);
+  setParameters(owner, idx1, idx2, idx3, idx4, minDihedralDeg, maxDihedralDeg,
+                forceConst);
 }
 
 double TorsionConstraintContrib::getEnergy(double *pos) const {
   PRECONDITION(dp_forceField, "no owner");
   PRECONDITION(pos, "bad vector");
   double dihedral;
-  RDKit::ForceFieldsHelper::computeDihedral(
-    pos, d_at1Idx, d_at2Idx, d_at3Idx, d_at4Idx, &dihedral);
+  RDKit::ForceFieldsHelper::computeDihedral(pos, d_at1Idx, d_at2Idx, d_at3Idx,
+                                            d_at4Idx, &dihedral);
   dihedral *= RAD2DEG;
   double dihedralTerm = computeDihedralTerm(dihedral);
-  static double const c = 0.5 * DEG2RAD * DEG2RAD;
-  double res = c * d_forceConstant * dihedralTerm * dihedralTerm;
+  double res = d_forceConstant * dihedralTerm * dihedralTerm;
 
   return res;
 }
@@ -120,23 +124,35 @@ void TorsionConstraintContrib::getGrad(double *pos, double *grad) const {
   RDGeom::Point3D r[4];
   RDGeom::Point3D t[2];
   double d[2];
-  double cosPhi;
   double dihedral;
   RDKit::ForceFieldsHelper::computeDihedral(
-    pos, d_at1Idx, d_at2Idx, d_at3Idx, d_at4Idx, &dihedral, &cosPhi, r, t, d);
+      pos, d_at1Idx, d_at2Idx, d_at3Idx, d_at4Idx, &dihedral, nullptr, r, t, d);
   dihedral *= RAD2DEG;
-  double sinPhiSq = 1.0 - cosPhi * cosPhi;
-  double sinPhi = ((sinPhiSq > 0.0) ? sqrt(sinPhiSq) : 0.0);
-  // dE/dPhi is independent of cartesians:
   double dihedralTerm = computeDihedralTerm(dihedral);
-  double dE_dPhi = DEG2RAD * d_forceConstant * dihedralTerm;
+  double dE_dPhi = 2.0 * RAD2DEG * d_forceConstant * dihedralTerm;
 
-  // FIX: use a tolerance here
-  // this is hacky, but it's per the
-  // recommendation from Niketic and Rasmussen:
-  double sinTerm =
-      -dE_dPhi * (isDoubleZero(sinPhi) ? (1.0 / cosPhi) : (1.0 / sinPhi));
-  Utils::calcTorsionGrad(r, t, d, g, sinTerm, cosPhi);
-}
-}
+  double d23 = dp_forceField->distance(d_at2Idx, d_at3Idx, pos);
+  RDGeom::Point3D r31(pos[3 * d_at3Idx] - pos[3 * d_at1Idx],
+                      pos[3 * d_at3Idx + 1] - pos[3 * d_at1Idx + 1],
+                      pos[3 * d_at3Idx + 2] - pos[3 * d_at1Idx + 2]);
+  RDGeom::Point3D r42(pos[3 * d_at4Idx] - pos[3 * d_at2Idx],
+                      pos[3 * d_at4Idx + 1] - pos[3 * d_at2Idx + 1],
+                      pos[3 * d_at4Idx + 2] - pos[3 * d_at2Idx + 2]);
+  double prefactor = dE_dPhi / d23;
+  RDGeom::Point3D tt[2] = {r[0].crossProduct(r[1]), r[2].crossProduct(r[3])};
+  RDGeom::Point3D dedt[2] = {
+      tt[0].crossProduct(r[2]) / tt[0].lengthSq() * prefactor,
+      tt[1].crossProduct(r[1]) / tt[1].lengthSq() * prefactor};
+  RDGeom::Point3D dedp[4] = {
+      r[2].crossProduct(dedt[0]),
+      r31.crossProduct(dedt[0]) - r[3].crossProduct(dedt[1]),
+      r[0].crossProduct(dedt[0]) + r42.crossProduct(dedt[1]),
+      r[2].crossProduct(dedt[1])};
+  for (unsigned int i = 0; i < 4; ++i) {
+    g[i][0] += dedp[i].x;
+    g[i][1] += dedp[i].y;
+    g[i][2] += dedp[i].z;
+  }
 }
+}  // namespace UFF
+}  // namespace ForceFields
diff --git a/Code/ForceField/UFF/TorsionConstraint.h b/Code/ForceField/UFF/TorsionConstraint.h
index 362c5324835..84108b61087 100644
--- a/Code/ForceField/UFF/TorsionConstraint.h
+++ b/Code/ForceField/UFF/TorsionConstraint.h
@@ -22,8 +22,7 @@ namespace UFF {
 class RDKIT_FORCEFIELD_EXPORT TorsionConstraintContrib
     : public ForceFieldContrib {
  public:
-  TorsionConstraintContrib()
-       {};
+  TorsionConstraintContrib(){};
   //! Constructor
   /*!
   \param owner          pointer to the owning ForceField
@@ -55,9 +54,10 @@ class RDKIT_FORCEFIELD_EXPORT TorsionConstraintContrib
   };
 
  private:
-  void setParameters(ForceField *owner, unsigned int idx1,
-    unsigned int idx2, unsigned int idx3, unsigned int idx4,
-    double minDihedralDeg, double maxDihedralDeg, double forceConst);
+  void setParameters(ForceField *owner, unsigned int idx1, unsigned int idx2,
+                     unsigned int idx3, unsigned int idx4,
+                     double minDihedralDeg, double maxDihedralDeg,
+                     double forceConst);
   double computeDihedralTerm(double dihedral) const;
   int d_at1Idx{-1}, d_at2Idx{-1}, d_at3Idx{-1},
       d_at4Idx{-1};  //!< indices of atoms forming the dihedral angle
diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index 4e66dd72e87..ba2e94eccd6 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -34,6 +34,11 @@
   is not canonical, using this argument currently causes the results to depend
   on the input atom numbering. Note that this can lead to an exception being
   thrown. (#3942)
+- Gradients for angle and torsional restraints in both UFF and MMFF were computed
+  incorrectly, which could give rise to potential instability during minimization.
+  As part of fixing this problem, force constants have been switched to using
+  kcal/degree^2 units instead of kcal/rad^2 units, consistently with the fact that
+  angle and dihedral restraints are specified in degrees. (#3975)
 
 ## Highlights
 - MolDraw2D now does a much better job of handling query features like common


Test patch:
diff --git a/Code/ForceField/MMFF/testMMFFForceField.cpp b/Code/ForceField/MMFF/testMMFFForceField.cpp
index 632f20e8716..cd0f0053a9c 100644
--- a/Code/ForceField/MMFF/testMMFFForceField.cpp
+++ b/Code/ForceField/MMFF/testMMFFForceField.cpp
@@ -402,7 +402,9 @@ int mmffValidationSuite(int argc, char *argv[]) {
           }
           delete mmffMolProperties;
         }
-        for (auto m : molVec) { delete m; }
+        for (auto m : molVec) {
+          delete m;
+        }
         sdfWriter->close();
         delete sdfWriter;
         rdkFStream.close();
@@ -1292,8 +1294,12 @@ int mmffValidationSuite(int argc, char *argv[]) {
                 errorMsg += diff.str();
               }
             }
-            for (auto &ti : rdkTorsionInstanceVec) { delete ti; }
-            for (auto &ti : refTorsionInstanceVec) { delete ti; }
+            for (auto &ti : rdkTorsionInstanceVec) {
+              delete ti;
+            }
+            for (auto &ti : refTorsionInstanceVec) {
+              delete ti;
+            }
             error = (fabs(rdkEnergy - refEnergy) > ENERGY_TOLERANCE);
             if (error && checkEnergy) {
               failed = true;
@@ -1504,7 +1510,7 @@ void testMMFFAllConstraints() {
   TEST_ASSERT(field);
   field->initialize();
   ac = new ForceFields::MMFF::AngleConstraintContrib(field, 1, 3, 6, 90.0, 90.0,
-                                                     1.0e5);
+                                                     100.0);
   field->contribs().push_back(ForceFields::ContribPtr(ac));
   field->minimize();
   TEST_ASSERT(RDKit::feq(
@@ -1517,7 +1523,7 @@ void testMMFFAllConstraints() {
   field = RDKit::MMFF::constructForceField(*mol, mmffMolProperties);
   field->initialize();
   ac = new ForceFields::MMFF::AngleConstraintContrib(field, 1, 3, 6, true,
-                                                     -10.0, 10.0, 1.0e5);
+                                                     -10.0, 10.0, 100.0);
   field->contribs().push_back(ForceFields::ContribPtr(ac));
   field->minimize();
   TEST_ASSERT(RDKit::feq(
@@ -1531,7 +1537,7 @@ void testMMFFAllConstraints() {
   field = RDKit::MMFF::constructForceField(*mol, mmffMolProperties);
   field->initialize();
   ac = new ForceFields::MMFF::AngleConstraintContrib(field, 1, 3, 6, false,
-                                                     -10.0, 10.0, 1.0e5);
+                                                     -10.0, 10.0, 100.0);
   field->contribs().push_back(ForceFields::ContribPtr(ac));
   field->minimize();
   TEST_ASSERT(RDKit::feq(
@@ -1552,12 +1558,14 @@ void testMMFFAllConstraints() {
   TEST_ASSERT(field);
   field->initialize();
   tc = new ForceFields::MMFF::TorsionConstraintContrib(field, 1, 3, 6, 8, 10.0,
-                                                       20.0, 1.0e5);
+                                                       10.0, 100.0);
   field->contribs().push_back(ForceFields::ContribPtr(tc));
   field->minimize();
+  std::cerr << MolTransforms::getDihedralDeg(mol->getConformer(), 1, 3, 6, 8)
+            << std::endl;
   TEST_ASSERT(
       RDKit::feq(MolTransforms::getDihedralDeg(mol->getConformer(), 1, 3, 6, 8),
-                 20.0, 0.5));
+                 10.0, 0.5));
   delete field;
   delete mmffMolProperties;
   MolTransforms::setDihedralDeg(mol->getConformer(), 1, 3, 6, 8, -30.0);
@@ -1567,12 +1575,12 @@ void testMMFFAllConstraints() {
   field = RDKit::MMFF::constructForceField(*mol, mmffMolProperties);
   field->initialize();
   tc = new ForceFields::MMFF::TorsionConstraintContrib(field, 1, 3, 6, 8, true,
-                                                       -10.0, 8.0, 1.0e5);
+                                                       -1.0, 1.0, 100.0);
   field->contribs().push_back(ForceFields::ContribPtr(tc));
   field->minimize();
   TEST_ASSERT(
       RDKit::feq(MolTransforms::getDihedralDeg(mol->getConformer(), 1, 3, 6, 8),
-                 -40.0, 0.5));
+                 -30.0, 1.5));
   delete field;
   delete mmffMolProperties;
   MolTransforms::setDihedralDeg(mol->getConformer(), 1, 3, 6, 8, -10.0);
@@ -1582,12 +1590,12 @@ void testMMFFAllConstraints() {
   field = RDKit::MMFF::constructForceField(*mol, mmffMolProperties);
   field->initialize();
   tc = new ForceFields::MMFF::TorsionConstraintContrib(field, 1, 3, 6, 8, false,
-                                                       -10.0, 8.0, 1.0e5);
+                                                       -10.0, -8.0, 100.0);
   field->contribs().push_back(ForceFields::ContribPtr(tc));
   field->minimize(500);
   TEST_ASSERT(
       RDKit::feq(MolTransforms::getDihedralDeg(mol->getConformer(), 1, 3, 6, 8),
-                 -10.0, 0.5));
+                 -9.0, 2.0));
   delete field;
   delete mmffMolProperties;
   delete mol;
@@ -1631,7 +1639,8 @@ void testMMFFAllConstraints() {
 
 void testMMFFTorsionConstraints() {
   std::cerr << "-------------------------------------" << std::endl;
-  std::cerr << "Unit test for MMFF TorsionConstraint close to 0 degrees." << std::endl;
+  std::cerr << "Unit test for MMFF TorsionConstraint close to 0 degrees."
+            << std::endl;
 
   std::string molBlock = R"(
      RDKit          3D
@@ -1666,11 +1675,12 @@ M  END
   mol = RDKit::MolBlockToMol(molBlock, true, false);
   TEST_ASSERT(mol);
   std::vector<double> e;
-  for (double d: {0.0001, 0.0}) {
+  for (double d : {0.0001, 0.0}) {
     MolTransforms::setDihedralDeg(mol->getConformer(), 0, 3, 6, 9, d);
     MMFF::MMFFMolProperties mmffMolProperties(*mol);
     TEST_ASSERT(mmffMolProperties.isValid());
-    ForceFields::ForceField *field = RDKit::MMFF::constructForceField(*mol, &mmffMolProperties);
+    ForceFields::ForceField *field =
+        RDKit::MMFF::constructForceField(*mol, &mmffMolProperties);
     TEST_ASSERT(field);
     field->initialize();
     auto *tc = new ForceFields::MMFF::TorsionConstraintContrib(field, 0, 3, 6,
@@ -1678,9 +1688,9 @@ M  END
     field->contribs().push_back(ForceFields::ContribPtr(tc));
     field->minimize();
     e.push_back(field->calcEnergy());
-    TEST_ASSERT(
-        RDKit::feq(MolTransforms::getDihedralDeg(mol->getConformer(), 0, 3, 6, 9),
-                   d, 0.5));
+    TEST_ASSERT(RDKit::feq(
+        MolTransforms::getDihedralDeg(mol->getConformer(), 0, 3, 6, 9), d,
+        0.5));
     delete field;
   }
   TEST_ASSERT(RDKit::feq(e[0], e[1], 0.5));
@@ -1784,11 +1794,123 @@ void testMMFFCopy() {
   std::cerr << "  done" << std::endl;
 }
 
+void testMMFFButaneScan() {
+  std::cerr << "-------------------------------------" << std::endl;
+  std::cerr << "Unit test for MMFF butane scan." << std::endl;
+
+  auto molblock = R"(
+     RDKit          3D
+
+ 14 13  0  0  0  0  0  0  0  0999 V2000
+   -1.5575    0.5684   -0.1320 C   0  0  0  0  0  0  0  0  0  0  0  0
+   -0.6987   -0.6064    0.3102 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.6987   -0.6064   -0.3102 C   0  0  0  0  0  0  0  0  0  0  0  0
+    1.5575    0.5684    0.1320 C   0  0  0  0  0  0  0  0  0  0  0  0
+   -1.6308    0.6129   -1.2232 H   0  0  0  0  0  0  0  0  0  0  0  0
+   -2.5700    0.4662    0.2715 H   0  0  0  0  0  0  0  0  0  0  0  0
+   -1.1524    1.5190    0.2272 H   0  0  0  0  0  0  0  0  0  0  0  0
+   -1.2059   -1.5359    0.0253 H   0  0  0  0  0  0  0  0  0  0  0  0
+   -0.6215   -0.6099    1.4037 H   0  0  0  0  0  0  0  0  0  0  0  0
+    0.6215   -0.6099   -1.4037 H   0  0  0  0  0  0  0  0  0  0  0  0
+    1.2059   -1.5359   -0.0253 H   0  0  0  0  0  0  0  0  0  0  0  0
+    1.6308    0.6129    1.2232 H   0  0  0  0  0  0  0  0  0  0  0  0
+    1.1524    1.5190   -0.2271 H   0  0  0  0  0  0  0  0  0  0  0  0
+    2.5700    0.4662   -0.2715 H   0  0  0  0  0  0  0  0  0  0  0  0
+  1  2  1  0
+  2  3  1  0
+  3  4  1  0
+  1  5  1  0
+  1  6  1  0
+  1  7  1  0
+  2  8  1  0
+  2  9  1  0
+  3 10  1  0
+  3 11  1  0
+  4 12  1  0
+  4 13  1  0
+  4 14  1  0
+M  END
+)";
+
+  // torsion constraints
+  std::unique_ptr<ROMol> mol(MolBlockToMol(molblock, true, false));
+  TEST_ASSERT(mol);
+  std::vector<std::pair<double, double>> diheEnergyVect;
+  std::vector<std::pair<double, double>> expectedDiheEnergyVect{
+      {-179.999, -5.07597},  {-175.1, -5.01299},    {-170.1, -4.82267},
+      {-165.1, -4.51646},    {-160.1, -4.11308},    {-155.101, -3.63773},
+      {-150.101, -3.12109},  {-145.101, -2.59801},  {-140.1, -2.10575},
+      {-135.1, -1.68158},    {-130.1, -1.35952},    {-125.1, -1.16635},
+      {-119.9, -1.11879},    {-114.9, -1.22194},    {-109.9, -1.45802},
+      {-104.9, -1.80199},    {-99.8996, -2.22002},  {-94.8995, -2.67302},
+      {-89.8996, -3.12055},  {-84.8996, -3.5255},   {-79.8997, -3.85916},
+      {-74.8998, -4.10358},  {-69.8999, -4.24974},  {-65.1, -4.29368},
+      {-60.1001, -4.23737},  {-55.1002, -4.0775},   {-50.1003, -3.81811},
+      {-45.1004, -3.46765},  {-40.1005, -3.0395},   {-35.1005, -2.55211},
+      {-30.1005, -2.02873},  {-25.1005, -1.49709},  {-20.1005, -0.988743},
+      {-15.1004, -0.538157}, {-10.1003, -0.180672}, {-5.10016, 0.051218},
+      {-0.100003, 0.13365},  {5.10016, 0.051218},   {10.1003, -0.180672},
+      {15.1004, -0.538157},  {20.1005, -0.988743},  {25.1005, -1.49709},
+      {30.1005, -2.02873},   {35.1005, -2.55211},   {40.1005, -3.0395},
+      {45.1004, -3.46765},   {50.1003, -3.81811},   {55.1002, -4.0775},
+      {60.1001, -4.23737},   {65.1, -4.29368},      {69.8999, -4.24974},
+      {74.8998, -4.10358},   {79.8997, -3.85916},   {84.8996, -3.5255},
+      {89.8996, -3.12055},   {94.8995, -2.67302},   {99.8996, -2.22002},
+      {104.9, -1.80199},     {109.9, -1.45802},     {114.9, -1.22194},
+      {119.9, -1.11879},     {125.1, -1.16635},     {130.1, -1.35952},
+      {135.1, -1.68158},     {140.1, -2.10575},     {145.101, -2.59801},
+      {150.101, -3.12109},   {155.101, -3.63773},   {160.1, -4.11308},
+      {165.1, -4.51646},     {170.1, -4.82267},     {175.1, -5.01299}};
+  MMFF::MMFFMolProperties mmffMolProperties(*mol);
+  TEST_ASSERT(mmffMolProperties.isValid());
+  std::unique_ptr<ForceFields::ForceField> globalFF(
+      RDKit::MMFF::constructForceField(*mol, &mmffMolProperties));
+  TEST_ASSERT(globalFF);
+  globalFF->initialize();
+  std::unique_ptr<ROMol> mol2(new ROMol(*mol));
+  const int start = -180;
+  const int end = 180;
+  const int incr = 5;
+  MolTransforms::setDihedralDeg(mol2->getConformer(), 0, 1, 2, 3, start);
+  for (int diheIn = start; diheIn < end; diheIn += incr) {
+    std::unique_ptr<ForceFields::ForceField> localFF(
+        RDKit::MMFF::constructForceField(*mol2, &mmffMolProperties));
+    TEST_ASSERT(localFF);
+    localFF->initialize();
+    auto *tc = new ForceFields::MMFF::TorsionConstraintContrib(
+        localFF.get(), 0, 1, 2, 3, static_cast<double>(diheIn) - 0.1,
+        static_cast<double>(diheIn) + 0.1, 100.0);
+    localFF->contribs().push_back(ForceFields::ContribPtr(tc));
+    TEST_ASSERT(localFF->minimize(10000) == 0);
+    std::vector<double> pos(3 * localFF->numPoints());
+    TEST_ASSERT(localFF->numPoints() == globalFF->numPoints());
+    auto posns = localFF->positions();
+    for (unsigned int i = 0; i < localFF->numPoints(); ++i) {
+      for (unsigned int j = 0; j < 3; ++j) {
+        pos[i * 3 + j] = (*static_cast<RDGeom::Point3D *>(posns.at(i)))[j];
+      }
+    }
+    auto diheOut =
+        MolTransforms::getDihedralDeg(mol2->getConformer(), 0, 1, 2, 3);
+    auto energy = globalFF->calcEnergy(pos.data());
+    diheEnergyVect.emplace_back(diheOut, energy);
+  }
+  for (size_t i = 0; i < diheEnergyVect.size(); ++i) {
+    TEST_ASSERT(RDKit::feq(diheEnergyVect.at(i).first,
+                           expectedDiheEnergyVect.at(i).first, 1.0));
+    TEST_ASSERT(RDKit::feq(diheEnergyVect.at(i).second,
+                           expectedDiheEnergyVect.at(i).second, 0.2));
+  }
+
+  std::cerr << "  done" << std::endl;
+}
+
 int main(int argc, char *argv[]) {
   if (!mmffValidationSuite(argc, argv)) {
     testMMFFAllConstraints();
     testMMFFTorsionConstraints();
     testMMFFCopy();
+    testMMFFButaneScan();
   }
 
   return 0;
diff --git a/Code/ForceField/UFF/testUFFForceField.cpp b/Code/ForceField/UFF/testUFFForceField.cpp
index d2c5d2aaf81..1692589dbfc 100644
--- a/Code/ForceField/UFF/testUFFForceField.cpp
+++ b/Code/ForceField/UFF/testUFFForceField.cpp
@@ -27,12 +27,12 @@
 #include <ForceField/UFF/TorsionConstraint.h>
 #include <ForceField/UFF/PositionConstraint.h>
 
-#include <GraphMol/Atom.h>
+#include <GraphMol/RDKitBase.h>
 #include <GraphMol/FileParsers/FileParsers.h>
 #include <GraphMol/ForceFieldHelpers/UFF/Builder.h>
 #include <GraphMol/MolTransforms/MolTransforms.h>
 
-using namespace RDGeom;
+using namespace RDKit;
 
 void test1() {
   std::cerr << "-------------------------------------" << std::endl;
@@ -41,7 +41,7 @@ void test1() {
   ForceFields::ForceField ff;
   TEST_ASSERT(ff.dimension() == 3);
 
-  Point3D p1(0, 0, 0), p2(1, 0, 0), p3(2, 0, 0), p4(0, 1, 0);
+  RDGeom::Point3D p1(0, 0, 0), p2(1, 0, 0), p3(2, 0, 0), p4(0, 1, 0);
   RDGeom::PointPtrVect &ps = ff.positions();
   ps.push_back(&p1);
   ps.push_back(&p2);
@@ -49,7 +49,7 @@ void test1() {
   ps.push_back(&p4);
 
 #if 0
-  Point3D f1,f2,f3,f4;
+  RDGeom::Point3D f1,f2,f3,f4;
   RDGeom::PointPtrVect &fs=ff.forces();
   fs.push_back(&f1);
   fs.push_back(&f2);
@@ -146,7 +146,7 @@ void testUFF2() {
   std::cerr << "Unit tests for UFF bond-stretch terms." << std::endl;
 
   ForceFields::ForceField ff;
-  Point3D p1(0, 0, 0), p2(1.514, 0, 0);
+  RDGeom::Point3D p1(0, 0, 0), p2(1.514, 0, 0);
   RDGeom::PointPtrVect &ps = ff.positions();
   ps.push_back(&p1);
   ps.push_back(&p2);
@@ -301,7 +301,7 @@ void testUFF4() {
   std::cerr << "Unit tests for UFF angle-bend terms." << std::endl;
 
   ForceFields::ForceField ff;
-  Point3D p1(1.514, 0, 0), p2(0, 0, 0), p3(0.1, 1.5, 0);
+  RDGeom::Point3D p1(1.514, 0, 0), p2(0, 0, 0), p3(0.1, 1.5, 0);
   RDGeom::PointPtrVect &ps = ff.positions();
   ps.push_back(&p1);
   ps.push_back(&p2);
@@ -528,7 +528,7 @@ void testUFF5() {
   std::cerr << " Test Simple UFF molecule optimizations." << std::endl;
 
   ForceFields::ForceField ff;
-  Point3D p1, p2, p3, p4, p5, p6;
+  RDGeom::Point3D p1, p2, p3, p4, p5, p6;
   RDGeom::PointPtrVect &ps = ff.positions();
   ps.push_back(&p1);
   ps.push_back(&p2);
@@ -653,7 +653,7 @@ void testUFF6() {
   std::cerr << "Unit tests for UFF nonbonded terms." << std::endl;
 
   ForceFields::ForceField ff;
-  Point3D p1(0, 0, 0), p2(0.0, 0, 0);
+  RDGeom::Point3D p1(0, 0, 0), p2(0.0, 0, 0);
   RDGeom::PointPtrVect &ps = ff.positions();
   ps.push_back(&p1);
   ps.push_back(&p2);
@@ -708,7 +708,7 @@ void testUFF7() {
   std::cerr << " Test UFF torsional terms." << std::endl;
 
   ForceFields::ForceField ff;
-  Point3D p1, p2, p3, p4;
+  RDGeom::Point3D p1, p2, p3, p4;
   RDGeom::PointPtrVect &ps = ff.positions();
   ps.push_back(&p1);
   ps.push_back(&p2);
@@ -966,7 +966,7 @@ void testUFF8() {
   std::cerr << " Test Simple UFF molecule optimization, part 2." << std::endl;
 
   ForceFields::ForceField ff;
-  Point3D p1, p2, p3, p4, p5, p6;
+  RDGeom::Point3D p1, p2, p3, p4, p5, p6;
   RDGeom::PointPtrVect &ps = ff.positions();
   ps.push_back(&p1);
   ps.push_back(&p2);
@@ -1106,7 +1106,7 @@ void testUFFTorsionConflict() {
   std::cerr << " Test UFF Torsion Conflicts." << std::endl;
 
   ForceFields::ForceField ff;
-  Point3D p1, p2, p3, p4, p5, p6, p7;
+  RDGeom::Point3D p1, p2, p3, p4, p5, p6, p7;
   RDGeom::PointPtrVect &ps = ff.positions();
   ps.push_back(&p1);
   ps.push_back(&p2);
@@ -1255,7 +1255,7 @@ void testUFFDistanceConstraints() {
   std::cerr << "Unit tests for UFF distance constraint terms." << std::endl;
 
   ForceFields::ForceField ff;
-  Point3D p1(0, 0, 0), p2(1.514, 0, 0);
+  RDGeom::Point3D p1(0, 0, 0), p2(1.514, 0, 0);
   RDGeom::PointPtrVect &ps = ff.positions();
   ps.push_back(&p1);
   ps.push_back(&p2);
@@ -1403,7 +1403,7 @@ void testUFFAllConstraints() {
   TEST_ASSERT(field);
   field->initialize();
   ac = new ForceFields::UFF::AngleConstraintContrib(field, 1, 3, 6, 90.0, 90.0,
-                                                    1.0e5);
+                                                    100.0);
   field->contribs().push_back(ForceFields::ContribPtr(ac));
   field->minimize();
   TEST_ASSERT(RDKit::feq(
@@ -1412,7 +1412,7 @@ void testUFFAllConstraints() {
   field = RDKit::UFF::constructForceField(*mol);
   field->initialize();
   ac = new ForceFields::UFF::AngleConstraintContrib(field, 1, 3, 6, true, -10.0,
-                                                    10.0, 1.0e5);
+                                                    10.0, 100.0);
   field->contribs().push_back(ForceFields::ContribPtr(ac));
   field->minimize();
   TEST_ASSERT(RDKit::feq(
@@ -1422,7 +1422,7 @@ void testUFFAllConstraints() {
   field = RDKit::UFF::constructForceField(*mol);
   field->initialize();
   ac = new ForceFields::UFF::AngleConstraintContrib(field, 1, 3, 6, false,
-                                                    -10.0, 10.0, 1.0e5);
+                                                    -10.0, 10.0, 100.0);
   field->contribs().push_back(ForceFields::ContribPtr(ac));
   field->minimize();
   TEST_ASSERT(RDKit::feq(
@@ -1439,34 +1439,34 @@ void testUFFAllConstraints() {
   TEST_ASSERT(field);
   field->initialize();
   tc = new ForceFields::UFF::TorsionConstraintContrib(field, 1, 3, 6, 8, 10.0,
-                                                      20.0, 1.0e5);
+                                                      10.0, 100.0);
   field->contribs().push_back(ForceFields::ContribPtr(tc));
   field->minimize();
   TEST_ASSERT(
       RDKit::feq(MolTransforms::getDihedralDeg(mol->getConformer(), 1, 3, 6, 8),
-                 20.0, 0.5));
+                 10.0, 0.5));
   delete field;
   MolTransforms::setDihedralDeg(mol->getConformer(), 1, 3, 6, 8, -30.0);
   field = RDKit::UFF::constructForceField(*mol);
   field->initialize();
   tc = new ForceFields::UFF::TorsionConstraintContrib(field, 1, 3, 6, 8, true,
-                                                      -10.0, 8.0, 1.0e5);
+                                                      -1.0, 1.0, 100.0);
   field->contribs().push_back(ForceFields::ContribPtr(tc));
   field->minimize();
   TEST_ASSERT(
       RDKit::feq(MolTransforms::getDihedralDeg(mol->getConformer(), 1, 3, 6, 8),
-                 -40.0, 0.5));
+                 -30.0, 1.5));
   delete field;
   MolTransforms::setDihedralDeg(mol->getConformer(), 1, 3, 6, 8, -10.0);
   field = RDKit::UFF::constructForceField(*mol);
   field->initialize();
   tc = new ForceFields::UFF::TorsionConstraintContrib(field, 1, 3, 6, 8, false,
-                                                      -10.0, 8.0, 1.0e6);
+                                                      -10.0, 8.0, 100.0);
   field->contribs().push_back(ForceFields::ContribPtr(tc));
   field->minimize(500);
   TEST_ASSERT(
       RDKit::feq(MolTransforms::getDihedralDeg(mol->getConformer(), 1, 3, 6, 8),
-                 -10.0, 0.5));
+                 -9.0, 2.0));
   delete field;
   delete mol;
 
@@ -1591,6 +1591,115 @@ void testUFFCopy() {
   std::cerr << "  done" << std::endl;
 }
 
+void testUFFButaneScan() {
+  std::cerr << "-------------------------------------" << std::endl;
+  std::cerr << "Unit test for UFF butane scan." << std::endl;
+
+  auto molblock = R"(
+     RDKit          3D
+
+ 14 13  0  0  0  0  0  0  0  0999 V2000
+   -1.5575    0.5684   -0.1320 C   0  0  0  0  0  0  0  0  0  0  0  0
+   -0.6987   -0.6064    0.3102 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.6987   -0.6064   -0.3102 C   0  0  0  0  0  0  0  0  0  0  0  0
+    1.5575    0.5684    0.1320 C   0  0  0  0  0  0  0  0  0  0  0  0
+   -1.6308    0.6129   -1.2232 H   0  0  0  0  0  0  0  0  0  0  0  0
+   -2.5700    0.4662    0.2715 H   0  0  0  0  0  0  0  0  0  0  0  0
+   -1.1524    1.5190    0.2272 H   0  0  0  0  0  0  0  0  0  0  0  0
+   -1.2059   -1.5359    0.0253 H   0  0  0  0  0  0  0  0  0  0  0  0
+   -0.6215   -0.6099    1.4037 H   0  0  0  0  0  0  0  0  0  0  0  0
+    0.6215   -0.6099   -1.4037 H   0  0  0  0  0  0  0  0  0  0  0  0
+    1.2059   -1.5359   -0.0253 H   0  0  0  0  0  0  0  0  0  0  0  0
+    1.6308    0.6129    1.2232 H   0  0  0  0  0  0  0  0  0  0  0  0
+    1.1524    1.5190   -0.2271 H   0  0  0  0  0  0  0  0  0  0  0  0
+    2.5700    0.4662   -0.2715 H   0  0  0  0  0  0  0  0  0  0  0  0
+  1  2  1  0
+  2  3  1  0
+  3  4  1  0
+  1  5  1  0
+  1  6  1  0
+  1  7  1  0
+  2  8  1  0
+  2  9  1  0
+  3 10  1  0
+  3 11  1  0
+  4 12  1  0
+  4 13  1  0
+  4 14  1  0
+M  END
+)";
+
+  // torsion constraints
+  std::unique_ptr<ROMol> mol(MolBlockToMol(molblock, true, false));
+  TEST_ASSERT(mol);
+  std::vector<std::pair<double, double>> diheEnergyVect;
+  std::vector<std::pair<double, double>> expectedDiheEnergyVect{
+      {179.9997, 1.7649},  {-175.1001, 1.8314}, {-170.1003, 2.0318},
+      {-165.1004, 2.3527}, {-160.1005, 2.7726}, {-155.1005, 3.2633},
+      {-150.1005, 3.7919}, {-145.1005, 4.3220}, {-140.1005, 4.8159},
+      {-135.1004, 5.2372}, {-130.1003, 5.5532}, {-125.1001, 5.7385},
+      {-119.9000, 5.7768}, {-114.8998, 5.6630}, {-109.8997, 5.4141},
+      {-104.8996, 5.0555}, {-99.8995, 4.6243},  {-94.8995, 4.1667},
+      {-89.8996, 3.7338},  {-84.8997, 3.3722},  {-79.8998, 3.1102},
+      {-74.8999, 2.9561},  {-70.1000, 2.9112},  {-65.1001, 2.9777},
+      {-60.1003, 3.1680},  {-55.1004, 3.4865},  {-50.1005, 3.9304},
+      {-45.1006, 4.4873},  {-40.1007, 5.1363},  {-35.1007, 5.8490},
+      {-30.1007, 6.5913},  {-25.1007, 7.3249},  {-20.1006, 8.0089},
+      {-15.1005, 8.6016},  {-10.1004, 9.0626},  {-5.1002, 9.3573},
+      {-0.1000, 9.4612},   {5.1002, 9.3573},    {10.1004, 9.0626},
+      {15.1005, 8.6016},   {20.1006, 8.0089},   {25.1007, 7.3249},
+      {30.1007, 6.5913},   {35.1007, 5.8490},   {40.1007, 5.1363},
+      {45.1006, 4.4873},   {50.1005, 3.9304},   {55.1004, 3.4865},
+      {60.1003, 3.1680},   {65.1001, 2.9777},   {70.1000, 2.9112},
+      {74.8999, 2.9561},   {79.8998, 3.1102},   {84.8997, 3.3722},
+      {89.8996, 3.7338},   {94.8995, 4.1667},   {99.8995, 4.6243},
+      {104.8996, 5.0555},  {109.8997, 5.4141},  {114.8998, 5.6630},
+      {119.9000, 5.7768},  {125.1001, 5.7385},  {130.1003, 5.5532},
+      {135.1004, 5.2372},  {140.1005, 4.8159},  {145.1005, 4.3220},
+      {150.1005, 3.7919},  {155.1005, 3.2633},  {160.1005, 2.7726},
+      {165.1004, 2.3527},  {170.1003, 2.0318},  {175.1001, 1.8314}};
+  std::unique_ptr<ForceFields::ForceField> globalFF(
+      RDKit::UFF::constructForceField(*mol));
+  TEST_ASSERT(globalFF);
+  globalFF->initialize();
+  std::unique_ptr<ROMol> mol2(new ROMol(*mol));
+  const int start = -180;
+  const int end = 180;
+  const int incr = 5;
+  MolTransforms::setDihedralDeg(mol2->getConformer(), 0, 1, 2, 3, start);
+  for (int diheIn = start; diheIn < end; diheIn += incr) {
+    std::unique_ptr<ForceFields::ForceField> localFF(
+        RDKit::UFF::constructForceField(*mol2));
+    TEST_ASSERT(localFF);
+    localFF->initialize();
+    auto *tc = new ForceFields::UFF::TorsionConstraintContrib(
+        localFF.get(), 0, 1, 2, 3, static_cast<double>(diheIn) - 0.1,
+        static_cast<double>(diheIn) + 0.1, 100.0);
+    localFF->contribs().push_back(ForceFields::ContribPtr(tc));
+    TEST_ASSERT(localFF->minimize(10000) == 0);
+    std::vector<double> pos(3 * localFF->numPoints());
+    TEST_ASSERT(localFF->numPoints() == globalFF->numPoints());
+    auto posns = localFF->positions();
+    for (unsigned int i = 0; i < localFF->numPoints(); ++i) {
+      for (unsigned int j = 0; j < 3; ++j) {
+        pos[i * 3 + j] = (*static_cast<RDGeom::Point3D *>(posns.at(i)))[j];
+      }
+    }
+    auto diheOut =
+        MolTransforms::getDihedralDeg(mol2->getConformer(), 0, 1, 2, 3);
+    auto energy = globalFF->calcEnergy(pos.data());
+    diheEnergyVect.emplace_back(diheOut, energy);
+  }
+  for (size_t i = 0; i < diheEnergyVect.size(); ++i) {
+    TEST_ASSERT(RDKit::feq(diheEnergyVect.at(i).first,
+                           expectedDiheEnergyVect.at(i).first, 1.0));
+    TEST_ASSERT(RDKit::feq(diheEnergyVect.at(i).second,
+                           expectedDiheEnergyVect.at(i).second, 0.2));
+  }
+
+  std::cerr << "  done" << std::endl;
+}
+
 int main() {
 #if 1
   test1();
@@ -1609,4 +1718,5 @@ int main() {
   testUFFDistanceConstraints();
   testUFFAllConstraints();
   testUFFCopy();
+  testUFFButaneScan();
 }
diff --git a/Code/ForceField/Wrap/testConstraints.py b/Code/ForceField/Wrap/testConstraints.py
index bffa02ba996..cefd7210c7b 100755
--- a/Code/ForceField/Wrap/testConstraints.py
+++ b/Code/ForceField/Wrap/testConstraints.py
@@ -143,7 +143,7 @@ def testUFFAngleConstraints(self):
     m = Chem.MolFromMolBlock(self.molB, True, False)
     ff = ChemicalForceFields.UFFGetMoleculeForceField(m)
     self.assertTrue(ff)
-    ff.UFFAddAngleConstraint(1, 3, 6, False, 90.0, 90.0, 1.0e5)
+    ff.UFFAddAngleConstraint(1, 3, 6, False, 90.0, 90.0, 100.0)
     r = ff.Minimize()
     self.assertTrue(r == 0)
     conf = m.GetConformer()
@@ -151,7 +151,7 @@ def testUFFAngleConstraints(self):
     self.assertTrue(int(angle) == 90)
     ff = ChemicalForceFields.UFFGetMoleculeForceField(m)
     self.assertTrue(ff)
-    ff.UFFAddAngleConstraint(1, 3, 6, True, -10.0, 10.0, 1.0e5)
+    ff.UFFAddAngleConstraint(1, 3, 6, True, -10.0, 10.0, 100.0)
     r = ff.Minimize()
     self.assertTrue(r == 0)
     conf = m.GetConformer()
@@ -159,7 +159,7 @@ def testUFFAngleConstraints(self):
     self.assertTrue(int(angle) == 100)
     ff = ChemicalForceFields.UFFGetMoleculeForceField(m)
     self.assertTrue(ff)
-    ff.UFFAddAngleConstraint(1, 3, 6, False, -10.0, 10.0, 1.0e5)
+    ff.UFFAddAngleConstraint(1, 3, 6, False, -10.0, 10.0, 100.0)
     r = ff.Minimize()
     self.assertTrue(r == 0)
     conf = m.GetConformer()
@@ -172,7 +172,7 @@ def testUFFTorsionConstraints(self):
     rdMolTransforms.SetDihedralDeg(conf, 1, 3, 6, 8, 15.0)
     ff = ChemicalForceFields.UFFGetMoleculeForceField(m)
     self.assertTrue(ff)
-    ff.UFFAddTorsionConstraint(1, 3, 6, 8, False, 10.0, 20.0, 1.0e5)
+    ff.UFFAddTorsionConstraint(1, 3, 6, 8, False, 10.0, 20.0, 100.0)
     r = ff.Minimize()
     self.assertTrue(r == 0)
     dihedral = rdMolTransforms.GetDihedralDeg(conf, 1, 3, 6, 8)
@@ -180,7 +180,7 @@ def testUFFTorsionConstraints(self):
     rdMolTransforms.SetDihedralDeg(conf, 1, 3, 6, 8, -30.0)
     ff = ChemicalForceFields.UFFGetMoleculeForceField(m)
     self.assertTrue(ff)
-    ff.UFFAddTorsionConstraint(1, 3, 6, 8, True, -10.0, 8.0, 1.0e5)
+    ff.UFFAddTorsionConstraint(1, 3, 6, 8, True, -10.0, 8.0, 100.0)
     r = ff.Minimize()
     self.assertTrue(r == 0)
     conf = m.GetConformer()
@@ -257,7 +257,7 @@ def testMMFFAngleConstraints(self):
     mp = ChemicalForceFields.MMFFGetMoleculeProperties(m)
     ff = ChemicalForceFields.MMFFGetMoleculeForceField(m, mp)
     self.assertTrue(ff)
-    ff.MMFFAddAngleConstraint(1, 3, 6, False, 90.0, 90.0, 1.0e5)
+    ff.MMFFAddAngleConstraint(1, 3, 6, False, 90.0, 90.0, 100.0)
     r = ff.Minimize()
     self.assertTrue(r == 0)
     conf = m.GetConformer()
@@ -265,7 +265,7 @@ def testMMFFAngleConstraints(self):
     self.assertTrue(int(angle) == 90)
     ff = ChemicalForceFields.MMFFGetMoleculeForceField(m, mp)
     self.assertTrue(ff)
-    ff.MMFFAddAngleConstraint(1, 3, 6, True, -10.0, 10.0, 1.0e5)
+    ff.MMFFAddAngleConstraint(1, 3, 6, True, -10.0, 10.0, 100.0)
     r = ff.Minimize()
     self.assertTrue(r == 0)
     conf = m.GetConformer()
@@ -273,7 +273,7 @@ def testMMFFAngleConstraints(self):
     self.assertTrue(int(angle) == 100)
     ff = ChemicalForceFields.MMFFGetMoleculeForceField(m, mp)
     self.assertTrue(ff)
-    ff.MMFFAddAngleConstraint(1, 3, 6, False, -10.0, 10.0, 1.0e5)
+    ff.MMFFAddAngleConstraint(1, 3, 6, False, -10.0, 10.0, 100.0)
     r = ff.Minimize()
     self.assertTrue(r == 0)
     conf = m.GetConformer()
@@ -287,7 +287,7 @@ def testMMFFTorsionConstraints(self):
     mp = ChemicalForceFields.MMFFGetMoleculeProperties(m)
     ff = ChemicalForceFields.MMFFGetMoleculeForceField(m, mp)
     self.assertTrue(ff)
-    ff.MMFFAddTorsionConstraint(1, 3, 6, 8, False, 10.0, 20.0, 1.0e5)
+    ff.MMFFAddTorsionConstraint(1, 3, 6, 8, False, 10.0, 20.0, 100.0)
     r = ff.Minimize()
     self.assertTrue(r == 0)
     dihedral = rdMolTransforms.GetDihedralDeg(conf, 1, 3, 6, 8)
@@ -295,7 +295,7 @@ def testMMFFTorsionConstraints(self):
     rdMolTransforms.SetDihedralDeg(conf, 1, 3, 6, 8, -30.0)
     ff = ChemicalForceFields.MMFFGetMoleculeForceField(m, mp)
     self.assertTrue(ff)
-    ff.MMFFAddTorsionConstraint(1, 3, 6, 8, True, -10.0, 8.0, 1.0e5)
+    ff.MMFFAddTorsionConstraint(1, 3, 6, 8, True, -10.0, 8.0, 100.0)
     r = ff.Minimize()
     self.assertTrue(r == 0)
     conf = m.GetConformer()
@@ -304,7 +304,7 @@ def testMMFFTorsionConstraints(self):
     rdMolTransforms.SetDihedralDeg(conf, 1, 3, 6, 8, -10.0)
     ff = ChemicalForceFields.MMFFGetMoleculeForceField(m, mp)
     self.assertTrue(ff)
-    ff.MMFFAddTorsionConstraint(1, 3, 6, 8, False, -10.0, 8.0, 1.0e5)
+    ff.MMFFAddTorsionConstraint(1, 3, 6, 8, False, -10.0, 8.0, 100.0)
     r = ff.Minimize(1000)
     self.assertTrue(r == 0)
     conf = m.GetConformer()