Repository: openforcefield/openff-toolkit

Issue:
OpenEye charging for trans-carboxylic acids will fail; incorporate workaround
Currently, OpenEye charging for (neutral) trans carboxylic acids will fail for reasons I'll explain below. OpenEye has come up with a fix/workaround which we should likely propagate into our charging code, see https://github.com/oess/openmm_orion/blob/gcalabro_data_record/MDOrion/LigPrep/ff_utils.py#L28-L103

(Tangentially, Antechamber charging for trans carboxylic acids will succeed but likely lead to incorrect/artifactual charges, which is a somewhat separate issue but worth noting.)

*Chemical explanation follows*:
Chris Bayly's ELF10 AM1-BCC implementation in the OpenEye toolkits tries to avoid strong internal electrostatic interactions when assigning charges, as these can lead to artifactual charges. So, if provided with a multi-conformer carboxylic acid with both cis and trans conformers, it will throw away the trans conformers when charging so as to avoid such interactions, and use only cis. This often works fine, but if for some reason no cis conformers are generated by Omega, charging will fail in such cases (I ran into one such molecule in a separate project). To fix this, they've implemented a workaround where such carboxylic acid proton torsions are converted to cis before charging so that some cis conformers will always be available. The above points to the code for this. 

The Antechamber AM1-BCC implementation will cheerfully assign charges in such cases, which, in Chris's view, will lead to artifactual charges.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/openforcefield/openff-toolkit/pull/1171?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openforcefield) Report
> Merging [#1171](https://codecov.io/gh/openforcefield/openff-toolkit/pull/1171?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openforcefield) (14420e2) into [master](https://codecov.io/gh/openforcefield/openff-toolkit/commit/3f99baa46f4c6bb5376dc3f967256279def4d9b1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openforcefield) (3f99baa) will **decrease** coverage by `8.71%`.
> The diff coverage is `86.44%`.

> :exclamation: Current head 14420e2 differs from pull request most recent head 94d272b. Consider uploading reports for the commit 94d272b to get more accurate results

davidlmobley: Replying to @SimonBoothroyd :
> @davidlmobley may want to have a quick scan of this, [in particular these lines](https://github.com/openforcefield/openff-toolkit/blob/b52e4f930b1ac95a83657a7affdbaf9b6e92c51d/openff/toolkit/topology/molecule.py#L3288-L3317) to make sure this aligns with his understanding of the problem as well as he's had more conversations with Chris about this.

This is consistent with my recollection. However, I have this quirky problem where I frequently get this backwards, so I am doublechecking with Christopher. However, for now let's assume that you have it right AND my memory is right. :) 

Will skim the rest of the issue and see if there's anything else I should chime in on, so if I don't respond again it means I agree with everything here. :) cc @j-wags 
j-wags: Thanks for the feedback @davidlmobley, you've resolved the blocking question that we had! 

Interestingly, it looks like we largely disagree on the specific questions originally raised by @Yoshanuikabundi. This can easily get bogged down into bikeshedding, so @Yoshanuikabundi  please resolve in the following order:

1) If you have a strong preference to take @davidlmobley's answer instead of mine, do so.
2) If you don't have a strong preference, take my answer.

I'll see if I can check in with @SimonBoothroyd  before he heads out about whether the his merge block should persist after we delete the extended test that he requested removing, and will report back if so.
Yoshanuikabundi: OK. I have a strong preference for making `make_carboxylic_acids_cis` `True` by default in `generate_conformers()`, but otherwise don't have strong feelings, so I've made that change.

> Actually, maybe I'll reconsider -- "hard but possible" may be the right place to be, as I can see wanting to check how much assigned force field parameters would vary depending on COOH orientation when using WBO interpolation, and it would be substantially harder if the toolkit didn't allow it. So maybe there should be a toolkit-level override that comes with disclaimers/warnings.

The current code in `assign_partial_charges()` only makes everything cis if it generates conformers. If conformers are provided it'll use them directly. For instance, this code produces trans COOH:

```python3
from openff.toolkit.topology import Molecule
from openff.toolkit.utils import OpenEyeToolkitWrapper

mol = Molecule.from_smiles("C(=O)OH", toolkit_registry=OpenEyeToolkitWrapper())
mol.generate_conformers(make_carboxylic_acids_cis=False, toolkit_registry=OpenEyeToolkitWrapper())
mol.assign_partial_charges("am1bccelf10", use_conformers=mol.conformers, toolkit_registry=OpenEyeToolkitWrapper())

mol.visualize("nglview")
```

Patch:
diff --git a/docs/releasehistory.md b/docs/releasehistory.md
index f363d422..1e16afec 100644
--- a/docs/releasehistory.md
+++ b/docs/releasehistory.md
@@ -8,6 +8,21 @@ Releases follow the `major.minor.micro` scheme recommended by [PEP440](https://w
 
 ## Current development
 
+### Behaviors changed and bugfixes
+
+- [PR #1171](https://github.com/openforcefield/openforcefield/pull/1171): Failure of 
+  [`Molecule.apply_elf_conformer_selection()`] due to excluding all available conformations ([Issue #428] (https://github.com/openforcefield/openff-toolkit/issues/428) )
+  now provides a better error. The `make_carboxylic_acids_cis` argument (`False` by default) has been added to 
+  [`Molecule.generate_conformers()`] to mitigate a common cause of this error. By setting this argument to `True` in internal use of this method, trans carboxylic 
+  acids are no longer generated in [`Molecule.assign_partial_charges()`] and 
+  [`Molecule.assign_fractional_bond_orders()`] methods (though users may still pass trans conformers in, they'll just be pruned by ELF methods). This should work around most instances
+  of the OpenEye Omega bug where trans carboxylic acids are more common than they should be.
+
+[`Molecule.apply_elf_conformer_selection()`]: openff.toolkit.topology.Molecule.apply_elf_conformer_selection
+[`Molecule.generate_conformers()`]: openff.toolkit.topology.Molecule.generate_conformers
+[`Molecule.assign_partial_charges()`]: openff.toolkit.topology.Molecule.assign_partial_charges
+[`Molecule.assign_fractional_bond_orders()`]: openff.toolkit.topology.Molecule.assign_fractional_bond_orders
+
 ### Improved documentation and warnings
 - [PR #1172](https://github.com/openforcefield/openff-toolkit/pull/1172): Adding
   discussion about constraints to the FAQ
diff --git a/openff/toolkit/topology/molecule.py b/openff/toolkit/topology/molecule.py
index 1f3135d3..6b4ef39a 100644
--- a/openff/toolkit/topology/molecule.py
+++ b/openff/toolkit/topology/molecule.py
@@ -3222,6 +3222,7 @@ class FrozenMolecule(Serializable):
         n_conformers=10,
         rms_cutoff=None,
         clear_existing=True,
+        make_carboxylic_acids_cis=True,
     ):
         """
         Generate conformers for this molecule using an underlying toolkit.
@@ -3241,6 +3242,11 @@ class FrozenMolecule(Serializable):
             for each ``ToolkitWrapper`` (generally 1 Angstrom).
         clear_existing : bool, default=True
             Whether to overwrite existing conformers for the molecule
+        make_carboxylic_acids_cis: bool, default=True
+            Guarantee all conformers have exclusively cis carboxylic acid groups (COOH)
+            by rotating the proton in any trans carboxylic acids 180 degrees around the
+            C-O bond. Works around a bug in conformer generation by the OpenEye toolkit
+            where trans COOH is much more common than it should be.
 
         Examples
         --------
@@ -3268,6 +3274,7 @@ class FrozenMolecule(Serializable):
                 rms_cutoff=rms_cutoff,
                 clear_existing=clear_existing,
                 raise_exception_types=[],
+                make_carboxylic_acids_cis=make_carboxylic_acids_cis,
             )
         elif isinstance(toolkit_registry, ToolkitWrapper):
             toolkit = toolkit_registry
@@ -3276,6 +3283,7 @@ class FrozenMolecule(Serializable):
                 n_conformers=n_conformers,
                 rms_cutoff=rms_cutoff,
                 clear_existing=clear_existing,
+                make_carboxylic_acids_cis=make_carboxylic_acids_cis,
             )
         else:
             raise InvalidToolkitRegistryError(
@@ -3284,6 +3292,151 @@ class FrozenMolecule(Serializable):
                 )
             )
 
+    def _make_carboxylic_acids_cis(self, toolkit_registry=GLOBAL_TOOLKIT_REGISTRY):
+        """
+        Rotate dihedral angle of any conformers with trans COOH groups so they are cis
+
+        Carboxylic acid groups almost always exist in nature in the cis conformation,
+        with the hydrogen atom in between the two oxygen atoms::
+
+                  O----H
+                 /
+                /
+               /
+            --C
+               \\
+                \\
+                  O
+
+        However, the OpenEye toolkit frequently produces carboxylic acid geometries
+        in the unrealistic trans conformation::
+
+             H----O
+                 /
+                /
+               /
+            --C
+               \\
+                \\
+                  O
+
+        This method converts all conformers in the Molecule with the trans conformation
+        into the corresponding cis conformer by rotating the OH bond around the CO bond
+        by 180 degrees. Carboxylic acids that are already cis are unchanged. Carboxylic
+        acid groups are considered cis if their O-C-O-H dihedral angle is acute.
+
+        Parameters
+        ----------
+        toolkit_registry : openff.toolkit.utils.toolkits.ToolkitRegistry or openff.toolkit.utils.toolkits.ToolkitWrapper, optional, default=None
+            :class:`ToolkitRegistry` or :class:`ToolkitWrapper` to use for SMILES-to-molecule conversion
+        """
+
+        # Return early if there are no conformers
+        if not self._conformers:
+            return
+
+        # Convert all conformers into one big array
+        conformers = np.asarray(
+            [q.value_in_unit(unit.angstrom) for q in self._conformers]
+        )
+
+        # Scan the molecule for carboxylic acids
+        cooh_indices = self.chemical_environment_matches(
+            "[C:2]([O:3][H:4])=[O:1]", toolkit_registry=toolkit_registry
+        )
+        n_conformers, n_cooh_groups = len(conformers), len(cooh_indices)
+        # Exit early if there are no carboxylic acids
+        if not n_cooh_groups:
+            return
+
+        # Pull out the coordinates of all carboxylic acid groups into cooh_xyz
+        cooh_xyz = conformers[:, cooh_indices, :]
+        assert cooh_xyz.shape == (n_conformers, n_cooh_groups, 4, 3)
+
+        def dot(a, b):
+            """Compute dot product along last axis of arrays"""
+            return np.sum(a * b, axis=-1)[..., np.newaxis]
+
+        def norm(a):
+            """Compute norm along last axis of array"""
+            return np.linalg.norm(a, axis=-1)[..., np.newaxis]
+
+        def dihedral(a):
+            """Compute dihedrals of array with shape (..., 4, 3)"""
+            # Praxeolitic formula
+            # 1 sqrt, 1 cross product
+            # from https://stackoverflow.com/questions/20305272/dihedral-torsion-angle-from-four-points-in-cartesian-coordinates-in-python
+            p0 = a[..., 0, :]
+            p1 = a[..., 1, :]
+            p2 = a[..., 2, :]
+            p3 = a[..., 3, :]
+
+            b0 = -1.0 * (p1 - p0)
+            b1 = p2 - p1
+            b2 = p3 - p2
+
+            # normalize b1 so that it does not influence magnitude of vector
+            # rejections that come next
+            b1 /= norm(b1)
+
+            # vector rejections
+            # v = projection of b0 onto plane perpendicular to b1
+            #   = b0 minus component that aligns with b1
+            # w = projection of b2 onto plane perpendicular to b1
+            #   = b2 minus component that aligns with b1
+            v = b0 - dot(b0, b1) * b1
+            w = b2 - dot(b2, b1) * b1
+
+            # angle between v and w in a plane is the torsion angle
+            # v and w may not be normalized but that's fine since tan is y/x
+            x = dot(v, w)
+            y = dot(np.cross(b1, v), w)
+            return np.arctan2(y, x)
+
+        dihedrals = dihedral(cooh_xyz)
+        assert dihedrals.shape == (n_conformers, n_cooh_groups, 1)
+        dihedrals.shape = (n_conformers, n_cooh_groups, 1, 1)
+
+        # Get indices of trans COOH groups
+        trans_indices = np.logical_not(
+            np.logical_and((-np.pi / 2) < dihedrals, dihedrals < (np.pi / 2))
+        )
+        # Expand array so it can be used to index cooh_xyz
+        trans_indices = np.repeat(trans_indices, repeats=4, axis=2)
+        trans_indices = np.repeat(trans_indices, repeats=3, axis=3)
+        # Get indices of individual atoms in trans COOH groups (except terminal O)
+        trans_indices_h = trans_indices.copy()
+        trans_indices_h[:, :, (0, 1, 2), :] = False
+        trans_indices_c = trans_indices.copy()
+        trans_indices_c[:, :, (0, 2, 3), :] = False
+        trans_indices_o = trans_indices.copy()
+        trans_indices_o[:, :, (0, 1, 3), :] = False
+
+        # Rotate OH around CO bond
+        # We want to rotate H 180 degrees around the CO bond (b1)
+        c = cooh_xyz[trans_indices_c].reshape(-1, 3)
+        o = cooh_xyz[trans_indices_o].reshape(-1, 3)
+        h = cooh_xyz[trans_indices_h].reshape(-1, 3)
+        # Axis is defined as the line from the origin along a unit vector, so
+        # move C to the origin and normalize
+        point = h - c
+        axis = o - c
+        axis /= norm(axis)
+        # Do the rotation
+        # https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle
+        rotated = axis * (dot(axis, point)) - np.cross(np.cross(axis, point), axis)
+        # Move rotated point back to original coordinates
+        rotated = rotated + c
+
+        # Update the coordinates
+        cooh_xyz[trans_indices_h] = rotated.reshape((-1))
+
+        # Update conformers with rotated coordinates
+        conformers[:, cooh_indices, :] = cooh_xyz
+
+        # Return conformers to original type
+        self._conformers = [conf * unit.angstrom for conf in conformers]
+
     def compute_virtual_site_positions_from_conformer(self, conformer_idx):
         """
         Compute the position of all virtual sites given an existing
@@ -3385,7 +3538,7 @@ class FrozenMolecule(Serializable):
         elif isinstance(toolkit_registry, ToolkitWrapper):
             toolkit = toolkit_registry
             toolkit.apply_elf_conformer_selection(  # type: ignore[attr-defined]
-                self, molecule=self, percentage=percentage, limit=limit, **kwargs
+                molecule=self, percentage=percentage, limit=limit, **kwargs
             )
         else:
             raise InvalidToolkitRegistryError(
diff --git a/openff/toolkit/utils/openeye_wrapper.py b/openff/toolkit/utils/openeye_wrapper.py
index af5c3e1d..d3038e6b 100644
--- a/openff/toolkit/utils/openeye_wrapper.py
+++ b/openff/toolkit/utils/openeye_wrapper.py
@@ -1723,7 +1723,12 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
         return molecule
 
     def generate_conformers(
-        self, molecule, n_conformers=1, rms_cutoff=None, clear_existing=True
+        self,
+        molecule,
+        n_conformers=1,
+        rms_cutoff=None,
+        clear_existing=True,
+        make_carboxylic_acids_cis=False,
     ):
         r"""
         Generate molecule conformers using OpenEye Omega.
@@ -1747,6 +1752,9 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
             If None, the cutoff is set to 1 Angstrom
         clear_existing : bool, default=True
             Whether to overwrite existing conformers for the molecule
+        make_carboxylic_acids_cis: bool, default=False
+            Guarantee all conformers have exclusively cis carboxylic acid groups (COOH)
+            by rotating the proton in any trans carboxylic acids 180 degrees around the C-O bond.
         """
         import copy
 
@@ -1789,6 +1797,9 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
         for conformer in molecule2._conformers:
             molecule._add_conformer(conformer)
 
+        if make_carboxylic_acids_cis:
+            molecule._make_carboxylic_acids_cis(toolkit_registry=self)
+
     def apply_elf_conformer_selection(
         self,
         molecule: "Molecule",
@@ -1805,8 +1816,13 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
         -----
         * The input molecule should have a large set of conformers already
           generated to select the ELF conformers from.
-        * The selected conformers will be retained in the `molecule.conformers` list
+        * The selected conformers will be retained in the ``molecule.conformers`` list
           while unselected conformers will be discarded.
+        * Conformers generated with the OpenEye toolkit often include trans
+          carboxylic acids (COOH). These are unphysical and will be rejected by
+          ``apply_elf_conformer_selection``. If no conformers are selected, try
+          re-running ``generate_conformers`` with the ``make_carboxylic_acids_cis``
+          argument set to ``True``
 
         See Also
         --------
@@ -1853,9 +1869,17 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
 
         # Check to make sure the call to OE was succesful, and re-route any
         # non-fatal warnings to the correct logger.
-        if not status:
+        if output_string and not status:
             raise RuntimeError("\n" + output_string)
-        elif len(output_string) > 0:
+        elif not status:
+            raise RuntimeError(
+                "OpenEye failed to select conformers, but did not return any output. "
+                "This most commonly occurs when the Molecule does not have enough conformers to "
+                "select from. Try calling Molecule.apply_elf_conformer_selection() again after "
+                "running Molecule.generate_conformers() with a much larger value of n_conformers "
+                "or with make_carboxylic_acids_cis=True."
+            )
+        elif output_string:
             logger.warning(output_string)
 
         # Extract and store the ELF conformers on the input molecule.
@@ -2002,6 +2026,7 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
                     mol_copy,
                     n_conformers=charge_method["rec_confs"],
                     rms_cutoff=0.25 * unit.angstrom,
+                    make_carboxylic_acids_cis=True,
                 )
                 # TODO: What's a "best practice" RMS cutoff to use here?
         else:
@@ -2171,12 +2196,14 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
         is_elf_method = bond_order_model in ["am1-wiberg-elf10", "pm3-wiberg-elf10"]
 
         if use_conformers is None:
-            temp_mol.generate_conformers(
+            self.generate_conformers(
+                molecule=temp_mol,
                 n_conformers=1 if not is_elf_method else 500,
                 # 0.05 is the recommended RMS when generating a 'Dense' amount of
                 # conformers using Omega: https://docs.eyesopen.com/toolkits/python/
                 # omegatk/OEConfGenConstants/OEFragBuilderMode.html.
                 rms_cutoff=None if not is_elf_method else 0.05 * unit.angstrom,
+                make_carboxylic_acids_cis=True,
             )
         else:
             temp_mol._conformers = None
diff --git a/openff/toolkit/utils/rdkit_wrapper.py b/openff/toolkit/utils/rdkit_wrapper.py
index b84e2e6e..f42feba4 100644
--- a/openff/toolkit/utils/rdkit_wrapper.py
+++ b/openff/toolkit/utils/rdkit_wrapper.py
@@ -851,7 +851,13 @@ class RDKitToolkitWrapper(base_wrapper.ToolkitWrapper):
         return molecule
 
     def generate_conformers(
-        self, molecule, n_conformers=1, rms_cutoff=None, clear_existing=True, _cls=None
+        self,
+        molecule,
+        n_conformers=1,
+        rms_cutoff=None,
+        clear_existing=True,
+        _cls=None,
+        make_carboxylic_acids_cis=False,
     ):
         r"""
         Generate molecule conformers using RDKit.
@@ -878,6 +884,9 @@ class RDKitToolkitWrapper(base_wrapper.ToolkitWrapper):
             Whether to overwrite existing conformers for the molecule.
         _cls : class
             Molecule constructor
+        make_carboxylic_acids_cis: bool, default=False
+            Guarantee all conformers have exclusively cis carboxylic acid groups (COOH)
+            by rotating the proton in any trans carboxylic acids 180 degrees around the C-O bond.
 
         """
         from rdkit.Chem import AllChem
@@ -908,6 +917,9 @@ class RDKitToolkitWrapper(base_wrapper.ToolkitWrapper):
         for conformer in molecule2._conformers:
             molecule._add_conformer(conformer)
 
+        if make_carboxylic_acids_cis:
+            molecule._make_carboxylic_acids_cis(toolkit_registry=self)
+
     def assign_partial_charges(
         self,
         molecule,


Test patch:
diff --git a/openff/toolkit/tests/test_molecule.py b/openff/toolkit/tests/test_molecule.py
index 226795c8..006c9156 100644
--- a/openff/toolkit/tests/test_molecule.py
+++ b/openff/toolkit/tests/test_molecule.py
@@ -3551,18 +3551,22 @@ class TestMolecule:
         recomputed_charges = molecule._partial_charges
         assert np.allclose(initial_charges, recomputed_charges, atol=0.002)
 
-    @pytest.mark.parametrize("toolkit", ["openeye", "rdkit"])
-    def test_apply_elf_conformer_selection(self, toolkit):
+    @pytest.mark.parametrize(
+        "toolkit_wrapper", [OpenEyeToolkitWrapper, RDKitToolkitWrapper]
+    )
+    @pytest.mark.parametrize("use_registry", [True, False])
+    def test_apply_elf_conformer_selection(self, toolkit_wrapper, use_registry):
         """Test applying the ELF10 method."""
 
-        if toolkit == "openeye":
+        if toolkit_wrapper == OpenEyeToolkitWrapper:
             pytest.importorskip("openeye")
-            toolkit_registry = ToolkitRegistry(
-                toolkit_precedence=[OpenEyeToolkitWrapper]
-            )
-        elif toolkit == "rdkit":
+        elif toolkit_wrapper == RDKitToolkitWrapper:
             pytest.importorskip("rdkit")
-            toolkit_registry = ToolkitRegistry(toolkit_precedence=[RDKitToolkitWrapper])
+
+        if use_registry:
+            toolkit = toolkit_wrapper()
+        else:
+            toolkit = ToolkitRegistry(toolkit_precedence=[toolkit_wrapper])
 
         molecule = Molecule.from_file(
             get_data_file_path(os.path.join("molecules", "z_3_hydroxy_propenal.sdf")),
@@ -3605,7 +3609,7 @@ class TestMolecule:
         molecule._conformers = [*initial_conformers]
 
         # Apply ELF10
-        molecule.apply_elf_conformer_selection(toolkit_registry=toolkit_registry)
+        molecule.apply_elf_conformer_selection(toolkit_registry=toolkit)
         elf10_conformers = molecule.conformers
 
         assert len(elf10_conformers) == 1
@@ -3615,6 +3619,45 @@ class TestMolecule:
             initial_conformers[1].value_in_unit(unit.angstrom),
         )
 
+    def test_make_carboxylic_acids_cis(self):
+        # First, check that we get exactly the right coords for cis and trans methanoic acid
+        offmol = Molecule.from_mapped_smiles("[H:1][C:2](=[O:3])[O:4][H:5]")
+        # cis methanoic (formic) acid
+        cis_xyz = (
+            np.array(
+                [
+                    [-1.0, -1.0, 0.0],  # HC
+                    [+0.0, +0.0, 0.0],  # C
+                    [-1.0, +1.0, 0.0],  # =O
+                    [+1.0, +0.0, 0.0],  # -O
+                    [+2.0, +1.0, 0.0],  # HO
+                ]
+            )
+            * unit.angstrom
+        )
+        trans_xyz = (
+            np.array(
+                [
+                    [-1.0, -1.0, 0.0],  # HC
+                    [+0.0, +0.0, 0.0],  # C
+                    [-1.0, +1.0, 0.0],  # =O
+                    [+1.0, +0.0, 0.0],  # -O
+                    [+2.0, -1.0, 0.0],  # HO
+                ]
+            )
+            * unit.angstrom
+        )
+
+        offmol._conformers = [trans_xyz, cis_xyz]
+        expected_conformers = [cis_xyz, cis_xyz]
+
+        offmol._make_carboxylic_acids_cis()
+
+        assert np.all(
+            np.abs(np.asarray(offmol.conformers) - np.asarray(expected_conformers))
+            < 1e-5
+        )
+
     @requires_openeye
     def test_assign_fractional_bond_orders(self):
         """Test assignment of fractional bond orders"""