diff --git a/modules/index.rst b/modules/index.rst
index 8670525746c11f543886ed9108306aa756b2c6fa..147274735b6617024bf345b5d88c6ce4b031c8fa 100644
--- a/modules/index.rst
+++ b/modules/index.rst
@@ -11,6 +11,7 @@ OpenStructure documentation
   base/base
   geom/geom
   mol/base/mol
+  mol/alg/molalg
   conop/conop
   img/base/img
   img/alg/alg
diff --git a/modules/mol/alg/molalg.rst b/modules/mol/alg/molalg.rst
new file mode 100644
index 0000000000000000000000000000000000000000..00735f6def328ed899de4005fb2f803cd23f0206
--- /dev/null
+++ b/modules/mol/alg/molalg.rst
@@ -0,0 +1,25 @@
+:mod:`mol.alg <ost.mol.alg>` -- Algorithms for Structures
+================================================================================
+
+.. module:: ost.mol.alg
+   :synopsis: Algorithms operating on molecular structures
+
+.. function:: LocalDistTest(model, reference, tolerance, radius)
+  
+  This function calculates the agreement of local contacts between the model and 
+  the reference structure. The overlap is a number between zero and one, where 
+  one indicates complete agreement, zero indicates no agreement at all. This 
+  score is similar to the GDT, but does not require any superposition of the 
+  model and the reference.
+  
+  The distance of atom pairs in the reference structure that are closer than a 
+  certain distance (radius) to each other is compared to the distance found in 
+  the model. If the difference between these two distances is smaller than a 
+  threshold value (tolerance), the model and the reference agree. Missing atoms 
+  in the model are treated disagreement and thus lower the score.
+  
+  For residue with symmetric sidechains (GLU, ASP, ARG, VAL, PHE, TYR), the 
+  naming of the atoms is ambigous. For these residues, the overlap of both 
+  possible solutions to the fixed atoms, that is, everything that is not 
+  ambigous is calculated. The solution that gives higher overlap is then used to 
+  calculate the actual overlap score.
\ No newline at end of file
diff --git a/modules/mol/alg/pymod/wrap_mol_alg.cc b/modules/mol/alg/pymod/wrap_mol_alg.cc
index ce0be429c6afb0afd5b6b55b2252cc79d446dbbd..240a029a42dd523e8a4dfab72be40fe77bd74fa7 100644
--- a/modules/mol/alg/pymod/wrap_mol_alg.cc
+++ b/modules/mol/alg/pymod/wrap_mol_alg.cc
@@ -22,6 +22,8 @@
  */
 #include <boost/python.hpp>
 #include <ost/config.hh>
+#include <ost/mol/alg/local_dist_test.hh>
+
 using namespace boost::python;
 
 void export_svdSuperPose();
@@ -36,4 +38,6 @@ BOOST_PYTHON_MODULE(_mol_alg)
   #if OST_IMG_ENABLED
   export_entity_to_density();
   #endif
+  
+  def("LocalDistTest", &ost::mol::alg::LocalDistTest);
 }
diff --git a/modules/mol/alg/src/CMakeLists.txt b/modules/mol/alg/src/CMakeLists.txt
index 242eea3d8f3a4c80f29909b3ecad313df10753f7..17dc9acbf2a5d32ac17e62b0e50af18f35122b2f 100644
--- a/modules/mol/alg/src/CMakeLists.txt
+++ b/modules/mol/alg/src/CMakeLists.txt
@@ -2,11 +2,13 @@ set(OST_MOL_ALG_HEADERS
   svd_superpose.hh
   module_config.hh
   sec_structure_segments.hh
+  local_dist_test.hh
 )
 
 set(OST_MOL_ALG_SOURCES
   svd_superpose.cc
   sec_structure_segments.cc
+  local_dist_test.cc
 )
 
 set(MOL_ALG_DEPS mol)
diff --git a/modules/mol/alg/src/local_dist_test.cc b/modules/mol/alg/src/local_dist_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..2a0c07bf58190170938e2a33340da4834837d042
--- /dev/null
+++ b/modules/mol/alg/src/local_dist_test.cc
@@ -0,0 +1,162 @@
+#include <ost/mol/mol.hh>
+#include "local_dist_test.hh"
+
+namespace ost { namespace mol { namespace alg {
+
+namespace {
+
+String swapped_name(const String& name)
+{
+  if (name=="OE1") return "OE2";
+  if (name=="OE2") return "OE1";
+
+  if (name=="OD1") return "OD2";
+  if (name=="OD2") return "OD1";
+
+  if (name=="CG1") return "CG2";
+  if (name=="CG2") return "CG1";
+
+  if (name=="CE1") return "CE2";
+  if (name=="CE2") return "CE1";  
+
+  if (name=="CD1") return "CD2";
+  if (name=="CD2") return "CD1";
+
+  if (name=="NH1") return "NH2";
+  if (name=="NH2") return "NH1";
+
+  return name;
+}
+
+bool swappable(const String& rname, const String& aname)
+{
+  if (rname=="GLU") {
+    return (aname=="OE1" || aname=="OE2");
+  }
+  if (rname=="ASP") {
+    return (aname=="OD1" || aname=="OD2");
+  }
+  if (rname=="VAL") {
+    return (aname=="CG1" || aname=="CG2");
+  }
+  if (rname=="TYR" || rname=="PHE") {
+    return (aname=="CD1" || aname=="CD2" || aname=="CE1" || aname=="CE2");
+  }
+  if (rname=="ARG") {
+    return (aname=="NH1" || aname=="NH2");
+  }  
+  return false;
+}
+
+std::pair<Real, Real> calc_overlap(ResidueView ref_res, 
+                                   EntityView ref,
+                                   ChainView mdl_chain, 
+                                   Real tol, Real max_dist, bool only_fixed, 
+                                   bool swap)
+{
+  std::pair<Real, Real> overlap(0.0, 0.0);
+  AtomViewList ref_atoms=ref_res.GetAtomList();
+
+  ResidueView mdl_res=mdl_chain.FindResidue(ref_res.GetNumber());
+  for (AtomViewList::iterator ai=ref_atoms.begin(),
+       ae=ref_atoms.end(); ai!=ae; ++ai) {
+    if (ai->GetElement()=="H") { continue; }
+    String name=swap ? swapped_name(ai->GetName()) : ai->GetName();
+    AtomView av1=mdl_res ? mdl_res.FindAtom(name) : AtomView();
+    AtomViewList within=ref.FindWithin(ai->GetPos(), max_dist);
+    for (AtomViewList::iterator aj=within.begin(),
+         ae2=within.end(); aj!=ae2; ++aj) {
+      if (aj->GetElement()=="H") { continue; }
+      if (only_fixed) {
+        if (aj->GetResidue().GetNumber()==ref_res.GetNumber()) {
+          continue;
+        }
+        if (swappable(aj->GetResidue().GetName(), aj->GetName())) {
+          continue;
+        }
+        AtomView av2=mdl_chain.FindAtom(aj->GetResidue().GetNumber(), 
+                                             aj->GetName());
+        overlap.second+=1.0;
+        if (!(av1 && av2)) {
+          continue;
+        }
+        Real mdl_dist=geom::Length(av1.GetPos()-av2.GetPos());
+        Real ref_dist=geom::Length(ai->GetPos()-aj->GetPos());
+        if (std::abs(mdl_dist-ref_dist)<tol) {
+          overlap.first+=1;
+        }
+        continue;
+      }
+      if (aj->GetResidue().GetNumber()>ref_res.GetNumber()) {
+        if (aj->GetResidue().GetNumber()<=ref_res.GetNumber()) {
+          continue;
+        }
+        AtomView av2=mdl_chain.FindAtom(aj->GetResidue().GetNumber(), 
+                                             aj->GetName());
+        overlap.second+=1.0;
+        if (!(av1 && av2)) {
+          continue;
+        }
+        Real mdl_dist=geom::Length(av1.GetPos()-av2.GetPos());
+        Real ref_dist=geom::Length(ai->GetPos()-aj->GetPos());
+        if (std::abs(mdl_dist-ref_dist)<tol) {
+          overlap.first+=1;
+        }
+      }
+    }      
+  }
+  return overlap;
+}
+
+}
+
+Real LocalDistTest(const EntityView& mdl, const EntityView& ref,
+                   Real cutoff, Real max_dist)
+{
+  ResidueViewList ref_residues=ref.GetResidueList();
+  ChainView mdl_chain=mdl.GetChainList()[0];  
+  // Residues with symmetric side-chains require special treatment as there are 
+  // two possible ways to name the atoms. We calculate the overlap with the 
+  // fixed atoms and take the solution that gives bigger scores.
+  XCSEditor edi=ref.GetHandle().RequestXCSEditor(BUFFERED_EDIT);
+  for (ResidueViewList::iterator
+       i=ref_residues.begin(), e=ref_residues.end(); i!=e; ++i) {
+    const String rname=i->GetName();
+    ResidueView mdl_res=mdl_chain.FindResidue(i->GetNumber());
+    if (!mdl_res) {
+     continue;
+    }     
+    if (!(rname=="GLU" || rname=="ASP" || rname=="VAL" || rname=="TYR" || 
+         rname=="PHE" || rname=="LYS" || rname=="ARG")) {
+      continue;
+    }
+
+    std::pair<Real, Real> ov1=calc_overlap(*i, ref, mdl_chain, 
+                                          cutoff, max_dist, true, 
+                                          false);
+    std::pair<Real, Real> ov2=calc_overlap(*i, ref, mdl_chain, 
+                                          cutoff, max_dist, true, 
+                                          true); 
+    if (ov1.first/ov1.second<ov2.first/ov2.second) {
+     AtomViewList atoms=mdl_res.GetAtomList();
+     for (AtomViewList::iterator j=atoms.begin(), 
+          e2=atoms.end(); j!=e2; ++j) {
+       if (swappable(rname, j->GetName())) {
+         edi.RenameAtom(j->GetHandle(), swapped_name(j->GetName()));
+       }
+     }
+
+    }
+  }
+  std::pair<Real, Real> total_ov(0.0, 0.0);
+  for (ResidueViewList::iterator
+       i=ref_residues.begin(), e=ref_residues.end(); i!=e; ++i) {
+     std::pair<Real, Real> ov1=calc_overlap(*i, ref, mdl_chain, cutoff, 
+                                            max_dist, false, false);
+     total_ov.first+=ov1.first;
+     total_ov.second+=ov1.second;
+  }
+  return total_ov.first/total_ov.second;
+}
+
+}}}
diff --git a/modules/mol/alg/src/local_dist_test.hh b/modules/mol/alg/src/local_dist_test.hh
new file mode 100644
index 0000000000000000000000000000000000000000..f0ba3ebea20a6138a0cf14f2a34d24839abe3ef4
--- /dev/null
+++ b/modules/mol/alg/src/local_dist_test.hh
@@ -0,0 +1,15 @@
+#ifndef OST_MOL_ALG_LOCAL_DIST_TEST_HH
+#define OST_MOL_ALG_LOCAL_DIST_TEST_HH
+
+#include <ost/mol/entity_view.hh>
+#include <ost/mol/alg/module_config.hh>
+
+namespace ost { namespace mol { namespace alg {
+  
+Real DLLEXPORT_OST_MOL_ALG LocalDistTest(const EntityView& mdl,
+                                         const EntityView& ref,
+                                         Real cutoff, Real max_dist);
+}}}
+
+#endif
+