Fixes #3781 (rdkit/rdkit#3975)

- 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)

Regression in dihedral constraints

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:

(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,

but it now returns a **very different** list

_Originally posted by @jasondbiggs in https://github.com/rdkit/rdkit/discussions/3780_

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

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()


fixes #3967 (rdkit/rdkit#4190)

Reference Issue
fixes #3967

What does this implement/fix? Explain your changes.

Seems like the two reported smiles issues are caused by two different bugs.

For "C=c1s/c2n(c1=O)CCCCCCC\\N=2" the bug seems like it's cause by the ring closure happening on the double bond. This condition is checked on line 384 in Canon.cpp

I struggled quite a bit figuring out exactly why "C1=C\\C/C=C2C3=C/C/C=C\\C=C/C\\3C\\2\\C=C/1" isn't working. It seems like it's a similar issue to the first one, but only because "secondFromAtom1" is not aromatic and is closing the ring. The condition for checking this is on lines 385-387 in Canon.cpp

I wanted to refactor this line into an inline function since it is use many times, but I didn't want to make this PR too big.
atom2Dir = (atom2Dir == Bond::ENDUPRIGHT) ? Bond::ENDDOWNRIGHT
                                                : Bond::ENDUPRIGHT;

Double bond stereo gets flipped by SMILES reader/writer

**Describe the bug**
Double bond stereo is flipped if you roundtrip a SMILES and double bond is located at the ring closure in canonical SMILES

**To Reproduce**
```python
print(Chem.CanonSmiles('C=c1s/c2n(c1=O)CCCCCCC\\N=2'))
```
Output: `C=c1s/c2n(c1=O)CCCCCCC/N=2`

**Expected behavior**
Double bond stereo should be retained

**Screenshots**
![image](https://user-images.githubusercontent.com/2325645/112114703-a695a380-8bb8-11eb-9b5c-85ec99315ad5.png)

**Configuration (please complete the following information):**
 - RDKit version: 2020.09.5
 - OS: MacOS
 - Python version (if relevant): 3.8
 - Are you using conda? Yes
 - If you are using conda, which channel did you install the rdkit from? conda-forge
 - If you are not using conda: how did you install the RDKit?

diff --git a/.gitignore b/.gitignore
index c4bcb9015dc..3c1ac56259b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,8 @@
 #- Eclipse files
 /.project
 /.pydevproject
+/.cproject
+/.settings/

 #- CLion files
 /cmake-build-*
diff --git a/Code/GraphMol/Canon.cpp b/Code/GraphMol/Canon.cpp
index 542a563e16e..6d83b20547b 100644
--- a/Code/GraphMol/Canon.cpp
+++ b/Code/GraphMol/Canon.cpp
@@ -350,10 +350,25 @@ void canonicalizeDoubleBond(Bond *dblBond, UINT_VECT &bondVisitOrders,
     // If we're not looking at the bonds used to determine the
     // stereochemistry, we need to flip the setting on the other bond:
     const INT_VECT &stereoAtoms = dblBond->getStereoAtoms();
+
+    auto isClosingRingBond = [](Bond *bond) {
+      if (bond == nullptr) {
+        return false;
+      }
+      auto beginIdx = bond->getBeginAtomIdx();
+      auto endIdx = bond->getEndAtomIdx();
+      return beginIdx > endIdx &&
+             beginIdx - endIdx > 1 &&
+             bond->hasProp(common_properties::_TraversalRingClosureBond);
+    };
+
+    auto isFlipped = false;
+
     if (atom1->getDegree() == 3 &&
         std::find(stereoAtoms.begin(), stereoAtoms.end(),
                   static_cast<int>(atom1ControllingBond->getOtherAtomIdx(
                       atom1->getIdx()))) == stereoAtoms.end()) {
+      isFlipped = true;
       atom2Dir = (atom2Dir == Bond::ENDUPRIGHT) ? Bond::ENDDOWNRIGHT
                                                 : Bond::ENDUPRIGHT;
     }
@@ -363,6 +378,17 @@ void canonicalizeDoubleBond(Bond *dblBond, UINT_VECT &bondVisitOrders,
         std::find(stereoAtoms.begin(), stereoAtoms.end(),
                   static_cast<int>(firstFromAtom2->getOtherAtomIdx(
                       atom2->getIdx()))) == stereoAtoms.end()) {
+      isFlipped = true;
+      atom2Dir = (atom2Dir == Bond::ENDUPRIGHT) ? Bond::ENDDOWNRIGHT
+                                                : Bond::ENDUPRIGHT;
+    }
+
+    if (!isFlipped &&
+          (isClosingRingBond(dblBond) ||
+          (isClosingRingBond(secondFromAtom1) &&
+            !secondFromAtom1->getIsAromatic() &&
+            secondFromAtom1->getBondDir() != Bond::NONE))
+        ) {
       atom2Dir = (atom2Dir == Bond::ENDUPRIGHT) ? Bond::ENDDOWNRIGHT
                                                 : Bond::ENDUPRIGHT;
     }

diff --git a/Code/GraphMol/SmilesParse/test.cpp b/Code/GraphMol/SmilesParse/test.cpp
index 4a69cda451c..4e272cb66b0 100644
--- a/Code/GraphMol/SmilesParse/test.cpp
+++ b/Code/GraphMol/SmilesParse/test.cpp
@@ -4339,6 +4339,27 @@ void testOSSFuzzFailures() {
   BOOST_LOG(rdInfoLog) << "done" << std::endl;
 }

+void testGithub3967() {
+  BOOST_LOG(rdInfoLog) << "-------------------------------------" << std::endl;
+  BOOST_LOG(rdInfoLog) << "Testing Github Issue 3967: Double bond stereo gets "
+                          "flipped by SMILES reader/writer"
+                       << std::endl;
+
+  {
+    auto mol = "C=c1s/c2n(c1=O)CCCCCCC\\N=2"_smiles;
+    TEST_ASSERT(mol);
+    auto smi = MolToSmiles(*mol);
+    TEST_ASSERT(smi == "C=c1s/c2n(c1=O)CCCCCCC\\N=2");
+  }
+  {
+    auto mol = "C1=C\\C/C=C2C3=C/C/C=C\\C=C/C\\3C\\2\\C=C/1"_smiles;
+    TEST_ASSERT(mol);
+    auto smi = MolToSmiles(*mol);
+    TEST_ASSERT(smi == "C1=C\\C/C=C2C3=C/C/C=C\\C=C/C\\3C\\2\\C=C/1");
+  }
+  BOOST_LOG(rdInfoLog) << "\tdone" << std::endl;
+}
+
 int main(int argc, char *argv[]) {
   (void)argc;
   (void)argv;
@@ -4417,6 +4438,7 @@ int main(int argc, char *argv[]) {
   testdoRandomSmileGeneration();
   testGithub1028();
   testGithub3139();
+  testGithub3967();
 #endif
   testOSSFuzzFailures();
 }


Fixes #4114 (rdkit/rdkit#4207)

This closes #4114

Besides the bond properties, also EITHERDOUBLE Bond Dir was not being copied over to reaction products.

This enables copying bond properties and EITHERDOUBLE for reactant bonds that are not specified in the product template, or are specified in the product template as "Null Bond".

I'm using a custom build of RDKit 2021.03 on Mac.

It looks like RDKit reactions don't preserve bond properties from the reactants. Is this intentional?

```
from rdkit import Chem
from rdkit.Chem import rdChemReactions

rxn = rdChemReactions.ReactionFromSmarts('[O:1]>>[N:1]')

mol = Chem.MolFromSmiles('CCO')

mol.GetAtomWithIdx(0).SetIntProp('demo', 1)
reactant_bond = mol.GetBondBetweenAtoms(0, 1)
reactant_bond.SetIntProp('demo', 1)

product_groups = rxn.RunReactants([mol])
product = product_groups[0][0]

mapping = {int(a.GetProp('react_atom_idx')): a.GetIdx() for a in product.GetAtoms()}
product_bond = product.GetBondBetweenAtoms(mapping[0], mapping[1])
print(product_bond.GetPropsAsDict())
```

This shows no properties on the bond, and only the mapping properties on the atoms.

diff --git a/Code/GraphMol/ChemReactions/ReactionRunner.cpp b/Code/GraphMol/ChemReactions/ReactionRunner.cpp
index 72abd1f2a30..c7a09a8006e 100644
--- a/Code/GraphMol/ChemReactions/ReactionRunner.cpp
+++ b/Code/GraphMol/ChemReactions/ReactionRunner.cpp
@@ -718,8 +718,14 @@ void setReactantBondPropertiesToProduct(RWMOL_SPTR product,
     }

     pBond->setBondType(rBond->getBondType());
+    if (rBond->getBondType() == Bond::DOUBLE &&
+        rBond->getBondDir() == Bond::EITHERDOUBLE) {
+      pBond->setBondDir(Bond::EITHERDOUBLE);
+    }
+
     pBond->setIsAromatic(rBond->getIsAromatic());

+    pBond->updateProps(*rBond);
     if (pBond->hasProp(common_properties::NullBond)) {
       pBond->clearProp(common_properties::NullBond);
     }
@@ -858,6 +864,14 @@ void addMissingProductAtom(const Atom &reactAtom, unsigned reactNeighborIdx,
   } else {
     product->addBond(productIdx, prodNeighborIdx, origB->getBondType());
   }
+
+  auto prodB = product->getBondBetweenAtoms(prodNeighborIdx, productIdx);
+  if (origB->getBondType() == Bond::DOUBLE &&
+      origB->getBondDir() == Bond::EITHERDOUBLE) {
+    prodB->setBondDir(Bond::EITHERDOUBLE);
+  }
+  bool preserveExisting = true;
+  prodB->updateProps(*origB, preserveExisting);
 }

 void addReactantNeighborsToProduct(

diff --git a/Code/GraphMol/ChemReactions/testReaction.cpp b/Code/GraphMol/ChemReactions/testReaction.cpp
index 4b8af214ed9..05529b92325 100644
--- a/Code/GraphMol/ChemReactions/testReaction.cpp
+++ b/Code/GraphMol/ChemReactions/testReaction.cpp
@@ -7539,6 +7539,73 @@ void testGithub4162() {
   RxnOps::sanitizeRxn(*rxnFromPickle);
 }

+void testGithub4114() {
+  BOOST_LOG(rdInfoLog) << "--------------------------------------" << std::endl;
+  BOOST_LOG(rdInfoLog) << "Reactions don't propagate bond properties"
+                       << std::endl;
+
+  const std::string DUMMY_PROP{"dummy_prop"};
+  std::vector<ROMOL_SPTR> mol{"ClC(N=O)=C=C=C=C(Br)N=S"_smiles};
+
+  // React the left side of the mol
+  const std::string reaction(
+      R"([Cl:1][C:2]([N:3]=[O:4])=[C:5]=[C:6]>>[F:1][Si:2]([P:3]~[S:4])(~[Si:5]=[Si:6]))");
+  std::unique_ptr<ChemicalReaction> rxn(RxnSmartsToChemicalReaction(reaction));
+  TEST_ASSERT(rxn);
+  rxn->initReactantMatchers();
+
+  // Set dummy properties on all bonds; mark the central double bonds
+  // as EITHERDOUBLE
+  for (unsigned int i = 0; i < mol[0]->getNumBonds(); ++i) {
+    auto bnd = mol[0]->getBondWithIdx(i);
+    bnd->setProp(DUMMY_PROP, i);
+
+    if (i >= 3 && i <= 6) {
+      TEST_ASSERT(bnd->getBondType() == Bond::DOUBLE);
+      bnd->setBondDir(Bond::EITHERDOUBLE);
+    }
+  }
+
+  auto prods = rxn->runReactants(mol);
+  TEST_ASSERT(prods.size() == 1);
+  TEST_ASSERT(prods[0].size() == 1);
+
+  // Make sure the reaction did not alter the graph
+  for (const auto &at : prods[0][0]->atoms()) {
+    auto mapno = at->getProp<unsigned int>(common_properties::reactantAtomIdx);
+    TEST_ASSERT(mapno == at->getIdx());
+  }
+
+  for (const auto &rBnd : mol[0]->bonds()) {
+    auto pBnd = prods[0][0]->getBondBetweenAtoms(rBnd->getBeginAtomIdx(),
+                                                 rBnd->getEndAtomIdx());
+
+    // Bonds 0, 1 and 4 are overridden in the product template, and should
+    // therefore override the properties.
+    // Bonds 2 and 3 are "null bonds", and should get properties copied over.
+    // Bonds > 4 are reactant bonds, and should keep their properties.
+    auto rBndIdx = rBnd->getIdx();
+    if (rBndIdx == 0 || rBndIdx == 1 || rBndIdx == 4) {
+      TEST_ASSERT(!pBnd->hasProp(DUMMY_PROP));
+    } else {
+      unsigned int dummy_prop;
+      TEST_ASSERT(pBnd->getPropIfPresent(DUMMY_PROP, dummy_prop));
+      TEST_ASSERT(dummy_prop == rBndIdx);
+    }
+
+    // Bond 3 is a "null bond", and should preserve EITHERDOUBLE.
+    // Bond 4 is specified in the reaction, and should not preserve
+    // EITHERDOUBLE. Bonds 5 & 6 are not included in the reaction, so they
+    // should keep EITHERDOUBLE dir.
+    if (rBndIdx == 3 || rBndIdx == 5 || rBndIdx == 6) {
+      TEST_ASSERT(pBnd->getBondType() == Bond::DOUBLE);
+      TEST_ASSERT(pBnd->getBondDir() == Bond::EITHERDOUBLE);
+    } else {
+      TEST_ASSERT(pBnd->getBondDir() == Bond::NONE);
+    }
+  }
+}
+
 int main() {
   RDLog::InitLogs();

@@ -7633,6 +7700,7 @@ int main() {
   testRxnBlockRemoveHs();
   testGithub3078();
   testGithub4162();
+  testGithub4114();

   BOOST_LOG(rdInfoLog)
       << "*******************************************************\n";


Fixes #3998 (rdkit/rdkit#4243)

#### Reference Issue

Fixes #3998

#### What does this implement/fix? Explain your changes.

`MolFragmentToSmiles` removes ring info but it makes no sense and also causes troubles. This PR removes the code.

#### Any other comments?

MolFragmentToSmiles with kekuleSmiles=True raises AtomKekulizeException

**Describe the bug**
Release 2021_03_1 raises `AtomKekulizeException` by the following code.

**To Reproduce**
```python
from rdkit import Chem
mol = Chem.MolFromSmiles("CC(c1ccccc1)")
Chem.MolFragmentToSmiles(mol, [0], kekuleSmiles=False)
Chem.MolFragmentToSmiles(mol, [2,3,4,5,6,7], kekuleSmiles=False)
Chem.MolFragmentToSmiles(mol, [0], kekuleSmiles=True)  # Error is raised here
Chem.MolFragmentToSmiles(mol, [2,3,4,5,6,7], kekuleSmiles=True)
```

**Expected behavior**
```
'C'
'c1ccccc1'
'C'
'C1=CC=CC=C1'
```

**Screenshots**
```python
>>> import rdkit
>>> rdkit.__version__
'2021.03.1'
>>> from rdkit import Chem
>>> mol = Chem.MolFromSmiles("CC(c1ccccc1)")
>>> Chem.MolFragmentToSmiles(mol, [0], kekuleSmiles=False)
'C'
>>> Chem.MolFragmentToSmiles(mol, [2,3,4,5,6,7], kekuleSmiles=False)
'c1ccccc1'
>>> Chem.MolFragmentToSmiles(mol, [0], kekuleSmiles=True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
rdkit.Chem.rdchem.AtomKekulizeException: non-ring atom 2 marked aromatic
>>> Chem.MolFragmentToSmiles(mol, [2,3,4,5,6,7], kekuleSmiles=True)
'C1=CC=CC=C1'
```

**Configuration (please complete the following information):**
 - RDKit version: 2021.03.1
 - OS: Windows 10
 - Python version (if relevant): 3.8.5
 - Are you using conda? : Yes
 - If you are using conda, which channel did you install the rdkit from?: conda-forge
 - If you are not using conda: how did you install the RDKit? : conda install rdkit==2021.03.1

**Additional context**
The result with Release_2020_09_5 is below.
```python
>>> import rdkit
>>> rdkit.__version__
'2020.09.5'
>>> from rdkit import Chem
>>> mol = Chem.MolFromSmiles("CC(c1ccccc1)")
>>> Chem.MolFragmentToSmiles(mol, [0], kekuleSmiles=False)
'C'
>>> Chem.MolFragmentToSmiles(mol, [2,3,4,5,6,7], kekuleSmiles=False)
'c1ccccc1'
>>> Chem.MolFragmentToSmiles(mol, [0], kekuleSmiles=True)
'C'
>>> Chem.MolFragmentToSmiles(mol, [2,3,4,5,6,7], kekuleSmiles=True)
'C1:C:C:C:C:C:1'
```

diff --git a/Code/GraphMol/SmilesParse/SmilesWrite.cpp b/Code/GraphMol/SmilesParse/SmilesWrite.cpp
index fe8f331a51c..8f5998cdd53 100644
--- a/Code/GraphMol/SmilesParse/SmilesWrite.cpp
+++ b/Code/GraphMol/SmilesParse/SmilesWrite.cpp
@@ -646,33 +646,6 @@ std::string MolFragmentToSmiles(const ROMol &mol,
     }
   }

-  // copy over the rings that only involve atoms/bonds in this fragment:
-  if (mol.getRingInfo()->isInitialized()) {
-    tmol.getRingInfo()->reset();
-    tmol.getRingInfo()->initialize();
-    for (unsigned int ridx = 0; ridx < mol.getRingInfo()->numRings(); ++ridx) {
-      const INT_VECT &aring = mol.getRingInfo()->atomRings()[ridx];
-      bool keepIt = true;
-      for (auto aidx : aring) {
-        if (!atomsInPlay[aidx]) {
-          keepIt = false;
-          break;
-        }
-      }
-      if (keepIt) {
-        const INT_VECT &bring = mol.getRingInfo()->bondRings()[ridx];
-        for (auto bidx : bring) {
-          if (!bondsInPlay[bidx]) {
-            keepIt = false;
-            break;
-          }
-        }
-        if (keepIt) {
-          tmol.getRingInfo()->addRing(aring, bring);
-        }
-      }
-    }
-  }
   if (tmol.needsUpdatePropertyCache()) {
     for (auto atom : tmol.atoms()) {
       atom->updatePropertyCache(false);

diff --git a/Code/GraphMol/SmilesParse/test.cpp b/Code/GraphMol/SmilesParse/test.cpp
index 4e272cb66b0..f62a6674529 100644
--- a/Code/GraphMol/SmilesParse/test.cpp
+++ b/Code/GraphMol/SmilesParse/test.cpp
@@ -4360,6 +4360,27 @@ void testGithub3967() {
   BOOST_LOG(rdInfoLog) << "\tdone" << std::endl;
 }

+void testGithub3998() {
+  BOOST_LOG(rdInfoLog) << "-------------------------------------" << std::endl;
+  BOOST_LOG(rdInfoLog) << "Testing Github 3998: MolFragmentToSmiles with "
+                          "kekuleSmiles=True raises AtomKekulizeException"
+                       << std::endl;
+  {
+    RWMol *m;
+    std::string smiles = "CC(c1ccccc1)";
+    m = SmilesToMol(smiles);
+    TEST_ASSERT(m);
+    int as[] = {0};
+    std::vector<int> atomsToUse(as, as + sizeof(as) / sizeof(int));
+    std::string smi = MolFragmentToSmiles(*m, atomsToUse, nullptr, nullptr,
+                                               nullptr, true, true);
+    TEST_ASSERT(smi == "C");
+    delete m;
+  }
+
+  BOOST_LOG(rdInfoLog) << "done" << std::endl;
+}
+
 int main(int argc, char *argv[]) {
   (void)argc;
   (void)argv;
@@ -4439,6 +4460,7 @@ int main(int argc, char *argv[]) {
   testGithub1028();
   testGithub3139();
   testGithub3967();
+  testGithub3998();
 #endif
   testOSSFuzzFailures();
 }