diff --git a/modules/io/pymod/export_omf_io.cc b/modules/io/pymod/export_omf_io.cc
index fb98237b0ffbb2e5c6e5cd6d00ceceeefde080dd..43355a4a908f97ae3f567daba57f5f0e739c34c5 100644
--- a/modules/io/pymod/export_omf_io.cc
+++ b/modules/io/pymod/export_omf_io.cc
@@ -77,6 +77,7 @@ void export_omf_io() {
     .def("GetChainNames", &wrap_get_chain_names)
     .def("GetPositions", &OMF::GetPositions, return_value_policy<reference_existing_object>(),(arg("cname")))
     .def("GetBFactors", &OMF::GetBFactors, return_value_policy<reference_existing_object>(),(arg("cname")))
+    .def("GetAvgBFactors", &OMF::GetAvgBFactors,(arg("cname")))
     .def("GetSequence", &OMF::GetSequence, (arg("cname")))
   ;
 }
diff --git a/modules/io/src/mol/omf.cc b/modules/io/src/mol/omf.cc
index 973595161ed6295631bf6e26593d9984b81b7d1e..91fdca413267c42bf41582e4422a37149bdbb59c 100644
--- a/modules/io/src/mol/omf.cc
+++ b/modules/io/src/mol/omf.cc
@@ -3833,6 +3833,31 @@ const std::vector<Real>& OMF::GetBFactors(const String& cname) const {
   return it->second->bfactors;
 }
 
+std::vector<Real> OMF::GetAvgBFactors(const String& cname) const {
+  auto it = chain_data_.find(cname);
+  if(it == chain_data_.end()) {
+    throw ost::Error("Provided chain name not in OMF structure");
+  }
+  const std::vector<Real>& bfactors = it->second->bfactors;
+  const std::vector<int>& res_def_indices = it->second->res_def_indices;
+  std::vector<Real> avg_bfactors;
+  avg_bfactors.reserve(it->second->res_def_indices.size());
+  int current_atom_idx = 0;
+  for(auto i = res_def_indices.begin(); i != res_def_indices.end(); ++i) {
+    int size = residue_definitions_[*i].anames.size();
+    Real summed_bfac = 0.0;
+    for(int j = 0; j < size; ++j) {
+      summed_bfac += bfactors[current_atom_idx];
+      ++current_atom_idx;
+    }
+    if(size > 0) {
+      summed_bfac /= size;
+    }
+    avg_bfactors.push_back(summed_bfac);
+  }
+  return avg_bfactors;
+}
+
 String OMF::GetSequence(const String& cname) const {
   auto it = chain_data_.find(cname);
   if(it == chain_data_.end()) {
diff --git a/modules/io/src/mol/omf.hh b/modules/io/src/mol/omf.hh
index f5bb8b9966b0b5c74d9d114f745f212c16b52799..9503b1f541a928b1a3f3730a39fffc60bbab36c4 100644
--- a/modules/io/src/mol/omf.hh
+++ b/modules/io/src/mol/omf.hh
@@ -199,6 +199,8 @@ public:
 
   const std::vector<Real>& GetBFactors(const String& cname) const;
 
+  std::vector<Real> GetAvgBFactors(const String& cname) const;
+
   String GetSequence(const String& cname) const;
 
 private:
diff --git a/modules/mol/alg/pymod/chain_mapping.py b/modules/mol/alg/pymod/chain_mapping.py
index c8bfa7c7789ca7440cdb84c55ce5ea6bf03709cb..01b0554a022328fb2b825192935a902b0af92acc 100644
--- a/modules/mol/alg/pymod/chain_mapping.py
+++ b/modules/mol/alg/pymod/chain_mapping.py
@@ -26,12 +26,14 @@ class MappingResult:
     Constructor is directly called within the functions, no need to construct
     such objects yourself.
     """
-    def __init__(self, target, model, chem_groups, mapping, alns):
+    def __init__(self, target, model, chem_groups, mapping, alns,
+                 opt_score=None):
         self._target = target
         self._model = model
         self._chem_groups = chem_groups
         self._mapping = mapping
         self._alns = alns
+        self._opt_score = opt_score
 
     @property
     def target(self):
@@ -89,6 +91,18 @@ class MappingResult:
         """
         return self._alns
 
+    @property
+    def opt_score(self):
+        """ Placeholder property without any guarantee of being set
+
+        Different scores get optimized in the various chain mapping algorithms.
+        Some of them may set their final optimal score in that property.
+        Consult the documentation of the respective chain mapping algorithm
+        for more information. Won't be in the return dict of
+        :func:`JSONSummary`.
+        """
+        return self._opt_score
+
     def GetFlatMapping(self, mdl_as_key=False):
         """ Returns flat mapping as :class:`dict` for all mapable chains
 
@@ -760,6 +774,9 @@ class ChainMapper:
           scoring ones are extend by *block_seed_size* chains and the best
           scoring one is exhaustively extended.
 
+        Sets :attr:`MappingResult.opt_score` in case of no trivial one-to-one
+        mapping. 
+
         :param model: Model to map
         :type model: :class:`ost.mol.EntityView`/:class:`ost.mol.EntityHandle`
         :param inclusion_radius: Inclusion radius for lDDT
@@ -827,11 +844,13 @@ class ChainMapper:
                                  alns)
 
         mapping = None
+        opt_lddt = None
 
         if strategy == "naive":
-            mapping = _lDDTNaive(self.target, mdl, inclusion_radius, thresholds,
-                                 self.chem_groups, chem_mapping, ref_mdl_alns,
-                                 self.n_max_naive)
+            mapping, opt_lddt = _lDDTNaive(self.target, mdl, inclusion_radius,
+                                           thresholds, self.chem_groups,
+                                           chem_mapping, ref_mdl_alns,
+                                           self.n_max_naive)
         else:
             # its one of the greedy strategies - setup greedy searcher
             the_greed = _lDDTGreedySearcher(self.target, mdl, self.chem_groups,
@@ -846,6 +865,8 @@ class ChainMapper:
             elif strategy == "greedy_block":
                 mapping = _lDDTGreedyBlock(the_greed, block_seed_size,
                                            block_blocks_per_chem_group)
+            # cached => lDDT computation is fast here
+            opt_lddt = the_greed.lDDT(self.chem_groups, mapping)
 
         alns = dict()
         for ref_group, mdl_group in zip(self.chem_groups, mapping):
@@ -857,7 +878,7 @@ class ChainMapper:
                     alns[(ref_ch, mdl_ch)] = aln
 
         return MappingResult(self.target, mdl, self.chem_groups, mapping,
-                             alns)
+                             alns, opt_score = opt_lddt)
 
 
     def GetQSScoreMapping(self, model, contact_d = 12.0, strategy = "naive",
@@ -894,6 +915,9 @@ class ChainMapper:
           scoring ones are extend by *block_seed_size* chains and the block with
           with best QS score is exhaustively extended.
 
+        Sets :attr:`MappingResult.opt_score` in case of no trivial one-to-one
+        mapping.
+
         :param model: Model to map
         :type model: :class:`ost.mol.EntityView`/:class:`ost.mol.EntityHandle`
         :param contact_d: Max distance between two residues to be considered as 
@@ -935,18 +959,21 @@ class ChainMapper:
                         alns[(ref_ch, mdl_ch)] = aln
             return MappingResult(self.target, mdl, self.chem_groups, one_to_one,
                                  alns)
+        mapping = None
+        opt_qsscore = None
 
         if strategy == "naive":
-            mapping = _QSScoreNaive(self.target, mdl, self.chem_groups,
-                                    chem_mapping, ref_mdl_alns, contact_d,
-                                    self.n_max_naive)
+            mapping, opt_qsscore = _QSScoreNaive(self.target, mdl,
+                                                 self.chem_groups,
+                                                 chem_mapping, ref_mdl_alns,
+                                                 contact_d, self.n_max_naive)
         else:
             # its one of the greedy strategies - setup greedy searcher
-
-            the_greed = _QSScoreGreedySearcher(self.target, mdl, self.chem_groups,
-                                            chem_mapping, ref_mdl_alns,
-                                            contact_d = contact_d,
-                                            steep_opt_rate=steep_opt_rate)
+            the_greed = _QSScoreGreedySearcher(self.target, mdl,
+                                               self.chem_groups,
+                                               chem_mapping, ref_mdl_alns,
+                                               contact_d = contact_d,
+                                               steep_opt_rate=steep_opt_rate)
             if strategy == "greedy_fast":
                 mapping = _QSScoreGreedyFast(the_greed)
             elif strategy == "greedy_full":
@@ -954,6 +981,9 @@ class ChainMapper:
             elif strategy == "greedy_block":
                 mapping = _QSScoreGreedyBlock(the_greed, block_seed_size,
                                               block_blocks_per_chem_group)
+            # cached => QSScore computation is fast here
+            opt_qsscore = the_greed.Score(mapping, check=False)
+              
 
         alns = dict()
         for ref_group, mdl_group in zip(self.chem_groups, mapping):
@@ -965,7 +995,7 @@ class ChainMapper:
                     alns[(ref_ch, mdl_ch)] = aln
 
         return MappingResult(self.target, mdl, self.chem_groups, mapping,
-                             alns)
+                             alns, opt_score = opt_qsscore)
 
     def GetRigidMapping(self, model, strategy = "greedy_single_gdtts",
                         single_chain_gdtts_thresh=0.4, subsampling=None,
@@ -2225,7 +2255,7 @@ def _lDDTNaive(trg, mdl, inclusion_radius, thresholds, chem_groups,
                 best_mapping = mapping
                 best_lddt = lDDT
 
-    return best_mapping
+    return (best_mapping, best_lddt)
 
 
 def _lDDTGreedyFast(the_greed):
@@ -2648,7 +2678,7 @@ def _QSScoreNaive(trg, mdl, chem_groups, chem_mapping, ref_mdl_alns, contact_d,
         if score_result.QS_global > best_score:
             best_mapping = mapping
             best_score = score_result.QS_global
-    return best_mapping
+    return (best_mapping, best_score)
 
 
 def _QSScoreGreedyFast(the_greed):