diff --git a/modules/mol/alg/pymod/ligand_scoring.py b/modules/mol/alg/pymod/ligand_scoring.py
index 84d1291ac67f5e6e3b91d770f0db7ddbf0d325d9..db2e1d307c6cffc8be4de9be3a29310659a1c4d3 100644
--- a/modules/mol/alg/pymod/ligand_scoring.py
+++ b/modules/mol/alg/pymod/ligand_scoring.py
@@ -4,6 +4,7 @@ import numpy as np
 import networkx
 
 from ost import mol
+from ost import geom
 from ost import LogError, LogWarning, LogScript, LogInfo, LogVerbose, LogDebug
 from ost.mol.alg import chain_mapping
 
@@ -303,4 +304,142 @@ def ResidueToGraph(residue):
     return nxg
 
 
-__all__ = ["LigandScorer", "ResidueToGraph"]
+def LigandRMSD(model_ligand, target_ligand, transformation=geom.Mat4(),
+               substructure_match=False):
+    """Calculate symmetry-corrected RMSD.
+
+    Binding site superposition must be computed separately and passed as
+    `transformation`.
+
+    :param model_ligand: The model ligand
+    :type model_ligand: :class:`ost.mol.ResidueHandle` or
+                        :class:`ost.mol.ResidueView`
+    :param target_ligand: The target ligand
+    :type target_ligand: :class:`ost.mol.ResidueHandle` or
+                         :class:`ost.mol.ResidueView`
+    :param transformation: Optional transformation to apply on each atom
+                           position of model_ligand.
+    :type transformation: :class:`ost.geom.Mat4`
+    :param substructure_match: Set this to True to allow partial target
+                               ligand.
+    :type substructure_match: :class:`bool`
+    :rtype: :class:`float`
+    :raises: :class:`NoSymmetryError` when no symmetry can be found.
+    """
+
+    symmetries = _ComputeSymmetries(model_ligand, target_ligand,
+                                    substructure_match=substructure_match,
+                                    by_atom_index=True)
+
+    best_rmsd = float('inf')
+    for i, (trg_sym, mdl_sym) in enumerate(symmetries):
+        # Prepare Entities for RMSD
+        trg_lig_rmsd_ent = mol.CreateEntity()
+        trg_lig_rmsd_editor = trg_lig_rmsd_ent.EditXCS()
+        trg_lig_rmsd_chain = trg_lig_rmsd_editor.InsertChain("_")
+        trg_lig_rmsd_res = trg_lig_rmsd_editor.AppendResidue(trg_lig_rmsd_chain, "LIG")
+
+        mdl_lig_rmsd_ent = mol.CreateEntity()
+        mdl_lig_rmsd_editor = mdl_lig_rmsd_ent.EditXCS()
+        mdl_lig_rmsd_chain = mdl_lig_rmsd_editor.InsertChain("_")
+        mdl_lig_rmsd_res = mdl_lig_rmsd_editor.AppendResidue(mdl_lig_rmsd_chain, "LIG")
+
+        for mdl_anum, trg_anum in zip(mdl_sym, trg_sym):
+            # Rename model atoms according to symmetry
+            trg_atom = target_ligand.atoms[trg_anum]
+            mdl_atom = model_ligand.atoms[mdl_anum]
+            # Add atoms in the correct order to the RMSD entities
+            trg_lig_rmsd_editor.InsertAtom(trg_lig_rmsd_res, trg_atom.name, trg_atom.pos)
+            mdl_lig_rmsd_editor.InsertAtom(mdl_lig_rmsd_res, mdl_atom.name, mdl_atom.pos)
+
+        trg_lig_rmsd_editor.UpdateICS()
+        mdl_lig_rmsd_editor.UpdateICS()
+
+        rmsd = mol.alg.CalculateRMSD(mdl_lig_rmsd_ent.CreateFullView(),
+                                     trg_lig_rmsd_ent.CreateFullView(),
+                                     transformation)
+        if rmsd < best_rmsd:
+            best_rmsd = rmsd
+
+    return best_rmsd
+
+
+def _ComputeSymmetries(model_ligand, target_ligand, substructure_match=False,
+                       by_atom_index=False):
+    """Return a list of symmetries (isomorphisms) of the model onto the target
+    residues.
+
+    :param model_ligand: The model ligand
+    :type model_ligand: :class:`ost.mol.ResidueHandle` or
+                        :class:`ost.mol.ResidueView`
+    :param target_ligand: The target ligand
+    :type target_ligand: :class:`ost.mol.ResidueHandle` or
+                         :class:`ost.mol.ResidueView`
+    :param substructure_match: Set this to True to allow partial ligands
+                               in the reference.
+    :type substructure_match: :class:`bool`
+    :param by_atom_index: Set this parameter to True if you need the symmetries
+                          to refer to atom index (within the residue).
+                          Otherwise, if False, the symmetries refer to atom
+                          names.
+    :raises: :class:`NoSymmetryError` when no symmetry can be found.
+
+    """
+
+    # Get the Graphs of the ligands
+    model_graph = ResidueToGraph(model_ligand)
+    target_graph = ResidueToGraph(target_ligand)
+
+    if by_atom_index:
+        networkx.relabel_nodes(model_graph,
+                               {a: b for a, b in zip(
+                                   [a.name for a in model_ligand.atoms],
+                                   range(len(model_ligand.atoms)))},
+                               False)
+        networkx.relabel_nodes(target_graph,
+                               {a: b for a, b in zip(
+                                   [a.name for a in target_ligand.atoms],
+                                   range(len(target_ligand.atoms)))},
+                               False)
+
+    # Note the argument order (model, target) which differs from spyrmsd.
+    # This is because a subgraph of model is isomorphic to target - but not the opposite
+    # as we only consider partial ligands in the reference.
+    # Make sure to generate the symmetries correctly in the end
+    GM = networkx.algorithms.isomorphism.GraphMatcher(
+        model_graph, target_graph, node_match=lambda x, y:
+        x["element"] == y["element"])
+    if GM.is_isomorphic():
+        symmetries = [
+            (list(isomorphism.values()), list(isomorphism.keys()))
+                for isomorphism in GM.isomorphisms_iter()]
+        assert len(symmetries) > 0
+        LogDebug("Found %s isomorphic mappings (symmetries)" % len(symmetries))
+    elif GM.subgraph_is_isomorphic() and substructure_match:
+        symmetries = [(list(isomorphism.values()), list(isomorphism.keys())) for isomorphism in
+                      GM.subgraph_isomorphisms_iter()]
+        assert len(symmetries) > 0
+        # Assert that all the atoms in the target are part of the substructure
+        assert len(symmetries[0][0]) == len(target_ligand.atoms)
+        LogDebug("Found %s subgraph isomorphisms (symmetries)" % len(symmetries))
+    elif GM.subgraph_is_isomorphic():
+        LogDebug("Found subgraph isomorphisms (symmetries), but"
+                 " ignoring because substructure_match=False")
+        raise NoSymmetryError("No symmetry between %s and %s" % (
+            str(model_ligand), str(target_ligand)))
+    else:
+        LogDebug("Found no isomorphic mappings (symmetries)")
+        raise NoSymmetryError("No symmetry between %s and %s" % (
+            str(model_ligand), str(target_ligand)))
+
+    return symmetries
+
+
+class NoSymmetryError(Exception):
+    """Exception to be raised when no symmetry can be found
+
+    Those are cases we might want to capture for default behavior.
+    """
+    pass
+
+__all__ = ["LigandScorer", "ResidueToGraph", "LigandRMSD", "NoSymmetryError"]
diff --git a/modules/mol/alg/tests/test_ligand_scoring.py b/modules/mol/alg/tests/test_ligand_scoring.py
index baf8b2bf6244dc23ea038f9aebc2093aeaad7736..98eda28319a212684dc94efe4e787a919e50f157 100644
--- a/modules/mol/alg/tests/test_ligand_scoring.py
+++ b/modules/mol/alg/tests/test_ligand_scoring.py
@@ -1,9 +1,10 @@
 import unittest, os, sys
 
-from ost import io, mol
+from ost import io, mol, geom
 # check if we can import: fails if numpy or scipy not available
 try:
     from ost.mol.alg.ligand_scoring import *
+    import ost.mol.alg.ligand_scoring
 except ImportError:
     print("Failed to import ligand_scoring.py. Happens when numpy, scipy or "
           "networkx is missing. Ignoring test_ligand_scoring.py tests.")
@@ -122,6 +123,86 @@ class TestLigandScoring(unittest.TestCase):
         graph = ResidueToGraph(mdl_lig.residues[0])
         assert len(graph.edges) == 34
         assert len(graph.nodes) == 32
+        # Check an arbitrary node
+        assert [a for a in graph.adj["14"].keys()] == ["13", "29"]
+
+    def test__ComputeSymmetries(self):
+        """Test that _ComputeSymmetries works.
+        """
+        trg, trg_seqres = io.LoadMMCIF(os.path.join('testfiles', "1r8q.cif.gz"), seqres=True)
+        mdl, mdl_seqres = io.LoadMMCIF(os.path.join('testfiles', "P84080_model_02.cif.gz"), seqres=True)
+
+        trg_mg1 = trg.FindResidue("E", 1)
+        trg_g3d1 = trg.FindResidue("F", 1)
+        trg_afb1 = trg.FindResidue("G", 1)
+        trg_g3d2 = trg.FindResidue("J", 1)
+        mdl_g3d = mdl.FindResidue("L_2", 1)
+
+        sym = ost.mol.alg.ligand_scoring._ComputeSymmetries(mdl_g3d, trg_g3d1)
+        assert len(sym) == 72
+
+        sym = ost.mol.alg.ligand_scoring._ComputeSymmetries(mdl_g3d, trg_g3d1, by_atom_index=True)
+        assert len(sym) == 72
+
+        # Test that it works with views and only consider atoms in the view
+        # Skip PA, PB and O[1-3]A and O[1-3]B in target and model
+        # We assume atom index are fixed and won't change
+        trg_g3d1_sub = trg_g3d1.Select("aindex>6019").residues[0]
+        mdl_g3d_sub = mdl_g3d.Select("aindex>1447").residues[0]
+
+        sym = ost.mol.alg.ligand_scoring._ComputeSymmetries(mdl_g3d_sub, trg_g3d1_sub)
+        assert len(sym) == 6
+
+        sym = ost.mol.alg.ligand_scoring._ComputeSymmetries(mdl_g3d_sub, trg_g3d1_sub, by_atom_index=True)
+        assert len(sym) == 6
+
+        # Substructure matches
+        self.skipTest("Substructure matches don't work yet")
+        sym = ost.mol.alg.ligand_scoring._ComputeSymmetries(mdl_g3d, trg_g3d1_sub, substructure_match=True)
+        assert len(sym) == 6
+
+        # Missing atoms only allowed in target, not in model
+        with self.assertRaises(NoSymmetryError):
+            ost.mol.alg.ligand_scoring._ComputeSymmetries(mdl_g3d_sub, trg_g3d1, substructure_match=True)
+
+    def test_LigandRMSD(self):
+        """Test that LigandRMSD works.
+        """
+        trg, trg_seqres = io.LoadMMCIF(os.path.join('testfiles', "1r8q.cif.gz"), seqres=True)
+        mdl, mdl_seqres = io.LoadMMCIF(os.path.join('testfiles', "P84080_model_02.cif.gz"), seqres=True)
+
+        trg_mg1 = trg.FindResidue("E", 1)
+        trg_g3d1 = trg.FindResidue("F", 1)
+        trg_afb1 = trg.FindResidue("G", 1)
+        trg_g3d2 = trg.FindResidue("J", 1)
+        mdl_g3d = mdl.FindResidue("L_2", 1)
+
+        rmsd = LigandRMSD(mdl_g3d, trg_g3d1)
+        self.assertAlmostEqual(rmsd, 2.21341e-06, 10)
+        rmsd = LigandRMSD(mdl_g3d, trg_g3d2)
+        self.assertAlmostEqual(rmsd, 61.21325, 4)
+
+        # Ensure we raise a NoSymmetryError if the ligand is wrong
+        with self.assertRaises(NoSymmetryError):
+            LigandRMSD(mdl_g3d, trg_mg1)
+        with self.assertRaises(NoSymmetryError):
+            LigandRMSD(mdl_g3d, trg_afb1)
+
+        # Assert that transform works
+        trans = geom.Mat4(-0.999256, 0.00788487, -0.0377333, -15.4397,
+                          0.0380652, 0.0473315, -0.998154, 29.9477,
+                          -0.00608426, -0.998848, -0.0475963, 28.8251,
+                          0, 0, 0, 1)
+        rmsd = LigandRMSD(mdl_g3d, trg_g3d2, transformation=trans)
+        self.assertAlmostEqual(rmsd, 0.293972, 5)
+
+        # Assert that substructure matches work
+        self.skipTest("Substructure matches don't work yet")
+        trg_g3d1_sub = trg_g3d1.Select("aindex>6019").residues[0] # Skip PA, PB and O[1-3]A and O[1-3]B.
+        mdl_g3d_sub = mdl_g3d.Select("aindex>1447").residues[0] # Skip PA, PB and O[1-3]A and O[1-3]B.
+        with self.assertRaises(NoSymmetryError):
+            LigandRMSD(mdl_g3d, trg_g3d1_sub)  # no full match
+        rmsd = LigandRMSD(mdl_g3d, trg_g3d1_sub, substructure_match=True)
 
 
 if __name__ == "__main__":