diff --git a/modelling/pymod/CMakeLists.txt b/modelling/pymod/CMakeLists.txt
index 5eb74fde751666a7242a6efb10546e003fff76dc..f1ce0c85ccee3e04448f36fde5893896c1a374eb 100644
--- a/modelling/pymod/CMakeLists.txt
+++ b/modelling/pymod/CMakeLists.txt
@@ -18,6 +18,7 @@ set(MODELLING_PYMOD
   _pipeline.py
   _ring_punches.py
   _mtm.py
+  _denovo.py
 )
 
 pymod(NAME modelling
diff --git a/modelling/pymod/_closegaps.py b/modelling/pymod/_closegaps.py
index 651c2bacd8e8537e3263367a3c685bb86d2eaa04..cf48cd9d18290317549c9ae2aea31c5dcb3fbdf9 100644
--- a/modelling/pymod/_closegaps.py
+++ b/modelling/pymod/_closegaps.py
@@ -7,9 +7,15 @@ as argument.
 from promod3 import loop, sidechain, core
 from _modelling import *
 from _ring_punches import *
+from _denovo import *
 # external
 import ost
 import sys
+import os
+try:
+  import hashlib
+except ImportError:
+  import md5 as hashlib
 
 ###############################################################################
 # helper functions
@@ -918,135 +924,28 @@ def FillLoopsByMonteCarlo(mhandle, torsion_sampler, max_loops_to_search=6,
             gap_idx = new_idx
 
 
-def CloseGaps(mhandle, merge_distance=4, fragment_db=None, structure_db=None, 
-              torsion_sampler=None, chain_idx = None, resnum_range = None):
-    '''
-    Tries to close all gaps in a model, except termini. It will go through
-    following steps:
-
-    - Try to close small deletions by relaxing them 
-      (see :func:`CloseSmallDeletions`)
-    - Iteratively merge gaps up to a distance **merge_distance**
-      (see :func:`MergeGapsByDistance`) and try to fill them with a database 
-      approach (see :func:`FillLoopsByDatabase`)
-    - Try to fill remaining gaps using a Monte Carlo approach
-      (see :func:`FillLoopsByMonteCarlo`)
-    - Large deletions get closed using a last resort approach
-      (see :func:`CloseLargeDeletions`)
-
-    :param mhandle: Modelling handle on which to apply change.
-    :type mhandle:  :class:`ModellingHandle`
-
-    :param merge_distance:  Max. merge distance when performing the database 
-                            approach
-    :type merge_distance:   :class:`int`
-    :param fragment_db:     Database for searching fragments in database 
-                            approach, must be consistent with provided
-                            **structure_db**. A default is loaded if None.
-    :type fragment_db:      :class:`~promod3.loop.FragDB`
-    :param structure_db:    Structure db from which the **fragment_db** gets
-                            it's structural information. A default is loaded 
-                            if None.
-    :type structure_db:     :class:`~promod3.loop.StructureDB`
-    :param torsion_sampler: Used as parameter for :func:`FillLoopsByDatabase`
-                            and :func:`FillLoopsByMonteCarlo` A default one is 
-                            loaded if None.
-    :param chain_idx: If not None, only gaps from chain with given index get
-                      processed
-    :type chain_idx:  :class:`int`
-
-    :param resnum_range: If not None, only gaps within this resnum range get
-                         processed.
-    :type resnum_range: :class:`tuple` containing two :class:`int`  
-
-    '''
-
-
-    # load stuff if needed
-    if not IsBackboneScorerSet(mhandle) or not IsBackboneScorerEnvSet(mhandle):
-        SetupDefaultBackboneScorer(mhandle)
-    if fragment_db is None:
-        fragment_db = loop.LoadFragDB()
-    if structure_db is None:
-        structure_db = loop.LoadStructureDB()
-    if torsion_sampler is None:
-        torsion_sampler = loop.LoadTorsionSamplerCoil()
-
-    #try to close small deletions by relaxing them
-    CloseSmallDeletions(mhandle, chain_idx = chain_idx, 
-                        resnum_range = resnum_range)
-
-    # iteratively merge gaps of distance i and fill loops by database
-    for distance in range(merge_distance):
-        MergeGapsByDistance(mhandle, distance, chain_idx = chain_idx,
-                            resnum_range = resnum_range)
-        FillLoopsByDatabase(mhandle, fragment_db, structure_db,
-                            torsion_sampler, min_loops_required=-1,
-                            max_res_extension=6, chain_idx = chain_idx,
-                            resnum_range = resnum_range)
-        
-    # if above fails, try DB-fill with less restrictions
-    FillLoopsByDatabase(mhandle, fragment_db, structure_db,
-                        torsion_sampler, min_loops_required=-1,
-                        chain_idx = chain_idx, resnum_range = resnum_range)
-    FillLoopsByDatabase(mhandle, fragment_db, structure_db,
-                        torsion_sampler, chain_idx = chain_idx,
-                        resnum_range = resnum_range)
-
-    # close remaining gaps by Monte Carlo
-    FillLoopsByMonteCarlo(mhandle, torsion_sampler, chain_idx = chain_idx,
-                          resnum_range = resnum_range)
-
-    # last resort approach to close large deletions
-    CloseLargeDeletions(mhandle, structure_db, chain_idx = chain_idx, 
-                        resnum_range = resnum_range)
-
-    #In the function above, we call :func:`FillLoopsByDatabase` multiple
-    #times. First, we try to close "easy" gaps which require few extensions 
-    #(we wish to limit the damage we do on the template) and for which we have 
-    #plenty of loop candidates. If some gaps cannot be closed like this, we try 
-    #less restrictive options. This approach is helpful if neighboring gaps are 
-    #close together and the one closer to the C-terminus is easier to close. 
-    #Several variants were evaluated on 1752 target-template-pairs and this one 
-    #worked best.
-
-def ModelTermini(mhandle, torsion_sampler, fragger_handles=None,
-                 mc_num_loops=20, mc_steps=5000):
+def ModelTermini(mhandle, structure_db = None, fragment_cache_dir = None,
+                 mc_num_loops = 20, avg_sampling_per_position = 600,
+                 scratch_dir = os.getcwd()):
     '''Try to model termini with Monte Carlo sampling.
 
     Use with care! This is an experimental feature which will increase coverage
     but we do not assume that the resulting termini are of high quality!
-
-    The termini are modelled by either sampling the dihedral angles or (if
-    *fragger_handles* is given) :class:`~promod3.loop.Fragger` lists. The latter
-    is only used if the gap length is >= the length of fragments stored.
-
-    Terminal gaps of length 1 are ignored by this function!
-
-    .. literalinclude:: ../../../tests/doc/scripts/modelling_model_termini.py
-
-    :param mhandle: Modelling handle on which to apply change.
-    :type mhandle:  :class:`ModellingHandle`
-
-    :param torsion_sampler: A sampler for torsion angles.
-    :type torsion_sampler: :class:`~promod3.loop.TorsionSampler`
-
-    :param fragger_handles: Either None (no fragger sampling used) or one
-                            fragger handle for each chain in *mhandle*.
-    :type fragger_handles:  :class:`list` of :class:`~promod3.loop.FraggerHandle`
-
-    :param mc_num_loops: Number of loop candidates to consider for each terminal gap
-                         (see :meth:`~LoopCandidates.FillFromMonteCarloSampler`)
-    :type mc_num_loops:  :class:`int`
-
-    :param mc_steps: Number of MC steps to perform for each loop candidate
-                     (see :meth:`~LoopCandidates.FillFromMonteCarloSampler`)
-    :type mc_steps:  :class:`int`
     '''
 
     prof_name = 'closegaps::ModelTermini'
     prof = core.StaticRuntimeProfiler.StartScoped(prof_name)
 
+    if structure_db == None:
+        structure_db = loop.LoadStructureDB()
+
+    fragment_cache = None
+    if fragment_cache_dir != None:
+        if not os.path.exists(fragment_cache_dir):
+            err = "You provided a fragment cache path, that doesn't exist!"
+            raise RuntimeError(err)
+        fragment_cache = core.FileCache(fragment_cache_dir)
+
     # get terminal gaps (copies as we'll clear them as we go)
     terminal_gaps = [g.Copy() for g in mhandle.gaps if g.IsTerminal() and g.length > 1]
     if len(terminal_gaps) > 0:
@@ -1055,57 +954,58 @@ def ModelTermini(mhandle, torsion_sampler, fragger_handles=None,
     # model them
     for actual_gap in terminal_gaps:
         
-        # extract info
+        start_resnum = None
         if actual_gap.IsNTerminal():
             start_resnum = 1
-        else:
+        elif actual_gap.IsCTerminal():
             start_resnum = actual_gap.before.GetNumber().GetNum()
-        actual_chain = actual_gap.GetChain()
-        actual_chain_idx = actual_gap.GetChainIndex()
-        if fragger_handles is not None:
-            fragger_handle = fragger_handles[actual_chain_idx]
-
-        # choose sampler
-        if fragger_handles is None or \
-           actual_gap.length < fragger_handle.fragment_length:
-            mc_sampler = PhiPsiSampler(actual_gap.full_seq,
-                                       torsion_sampler)
         else:
-            end_pos = start_resnum-1 + actual_gap.length
-            fragger_list = fragger_handle.GetList(start_resnum-1, end_pos)
-            mc_sampler = FragmentSampler(actual_gap.full_seq, fragger_list,
-                                         init_fragments=5)
-
-        # choose closer
-        if actual_gap.IsNTerminal():
-            mc_closer = NTerminalCloser(actual_gap.after)
-        else:
-            mc_closer = CTerminalCloser(actual_gap.before)
-
-        # setup scorer
-        scorer_weights = dict()
-        scorer_weights["reduced"] = 3.0
-        scorer_weights["cb_packing"] = 2.0
-        scorer_weights["hbond"] = 1.0
-        scorer_weights["clash"] = 0.05
-        mc_scorer = LinearScorer(mhandle.backbone_scorer, start_resnum,
-                                 actual_chain_idx, scorer_weights)
-
-        # setup cooler
-        start_temperature = 100
-        cooling_factor = 0.9
-        # the number of 109 is roughly the number of times we have to apply
-        # a factor of 0.9 to 100 until it reaches a value of 0.001
-        cooling_interval = int(round(mc_steps/109.0))
-        mc_cooler = ExponentialCooler(cooling_interval, start_temperature,
-                                      cooling_factor)
-
-        # try to get loop candidates
-        ost.LogVerbose("Firing MC for " + str(actual_gap))
-        candidates = LoopCandidates.FillFromMonteCarloSampler(
-                actual_gap.full_seq, mc_num_loops, mc_steps, mc_sampler,
-                mc_closer, mc_scorer, mc_cooler)
+            err = "Can only model N-terminal or C-terminal gaps in "
+            err += "ModelTermini function!"
+            raise RuntimeError(err)
 
+        # generate a FraggerHandle for that particular chain
+        actual_chain_idx = actual_gap.GetChainIndex()
+        actual_chain = mhandle.model.chains[actual_chain_idx]
+        actual_sequence = mhandle.seqres[actual_chain_idx].GetString()
+
+        actual_profile = None
+        actual_psipred_pred = None
+        if len(mhandle.profiles) > 0:
+            actual_profile = mhandle.profiles[actual_chain_idx]
+        if len(mhandle.psipred_predictions):
+            actual_psipred_pred = mhandle.psipred_predictions[actual_chain_idx]
+
+        fragger_handle = loop.FraggerHandle(actual_sequence,
+                                            profile = actual_profile,
+                                            psipred_pred = actual_psipred_pred,
+                                            structure_db = structure_db)
+
+        # if there is a fragment_cache, we try to copy the file to our scratch_dir
+        # and load the fragments from there
+        if fragment_cache:
+            # the caching is based on md5 hashes
+            md5_hash = hashlib.md5(actual_sequence).hexdigest()
+            if(fragment_cache.CopyFromCache(md5_hash, scratch_dir)):
+                # yeah, we can load cached stuff
+                fragger_handle.LoadCached(os.path.join(scratch_dir, md5_hash))
+
+        # let's get the loop candidates
+        candidates = GenerateTerminiTrajectories(mhandle, actual_gap, 20,
+                                                      fragger_handle = fragger_handle)
+
+        # Once it's done, we copy the fragments back to cache
+        # in case of multiple jobs accessing the same data in parallel, this leads
+        # to data loss. We just don't care at this point (it does not lead to 
+        # crashes, but it might be that we loose fragments that already have
+        # been calculated => we have to recalculate them in the next run)
+        if fragment_cache:
+            # the caching is based on md5 hashes
+            md5_hash = hashlib.md5(actual_sequence).hexdigest()
+            fragger_handle.SaveCached(os.path.join(scratch_dir, md5_hash))
+            fragment_cache.CopyToCache(md5_hash, scratch_dir)
+
+        # select the best candidate
         if len(candidates) > 0:
             # score candidates
             scores = candidates.CalculateLinearScores(\
@@ -1332,6 +1232,106 @@ def CloseLargeDeletions(mhandle, structure_db, linker_length=8,
                     (str(gap_orig), str(actual_gap)))
         
 
+def CloseGaps(mhandle, merge_distance=4, fragment_db=None, structure_db=None, 
+              torsion_sampler=None, chain_idx = None, resnum_range = None,
+              fragment_cache_dir = None, model_termini = False,
+              scratch_dir = os.getcwd()):
+    '''
+    Tries to close all gaps in a model, except termini. It will go through
+    following steps:
+
+    - Try to close small deletions by relaxing them 
+      (see :func:`CloseSmallDeletions`)
+    - Iteratively merge gaps up to a distance **merge_distance**
+      (see :func:`MergeGapsByDistance`) and try to fill them with a database 
+      approach (see :func:`FillLoopsByDatabase`)
+    - Try to fill remaining gaps using a Monte Carlo approach
+      (see :func:`FillLoopsByMonteCarlo`)
+    - Large deletions get closed using a last resort approach
+      (see :func:`CloseLargeDeletions`)
+
+    :param mhandle: Modelling handle on which to apply change.
+    :type mhandle:  :class:`ModellingHandle`
+
+    :param merge_distance:  Max. merge distance when performing the database 
+                            approach
+    :type merge_distance:   :class:`int`
+    :param fragment_db:     Database for searching fragments in database 
+                            approach, must be consistent with provided
+                            **structure_db**. A default is loaded if None.
+    :type fragment_db:      :class:`~promod3.loop.FragDB`
+    :param structure_db:    Structure db from which the **fragment_db** gets
+                            it's structural information. A default is loaded 
+                            if None.
+    :type structure_db:     :class:`~promod3.loop.StructureDB`
+    :param torsion_sampler: Used as parameter for :func:`FillLoopsByDatabase`
+                            and :func:`FillLoopsByMonteCarlo` A default one is 
+                            loaded if None.
+    :param chain_idx: If not None, only gaps from chain with given index get
+                      processed
+    :type chain_idx:  :class:`int`
+
+    :param resnum_range: If not None, only gaps within this resnum range get
+                         processed.
+    :type resnum_range: :class:`tuple` containing two :class:`int`  
+
+    '''
+
+
+    # load stuff if needed
+    if not IsBackboneScorerSet(mhandle) or not IsBackboneScorerEnvSet(mhandle):
+        SetupDefaultBackboneScorer(mhandle)
+    if fragment_db is None:
+        fragment_db = loop.LoadFragDB()
+    if structure_db is None:
+        structure_db = loop.LoadStructureDB()
+    if torsion_sampler is None:
+        torsion_sampler = loop.LoadTorsionSamplerCoil()
+
+    #try to close small deletions by relaxing them
+    CloseSmallDeletions(mhandle, chain_idx = chain_idx, 
+                        resnum_range = resnum_range)
+
+    # iteratively merge gaps of distance i and fill loops by database
+    for distance in range(merge_distance):
+        MergeGapsByDistance(mhandle, distance, chain_idx = chain_idx,
+                            resnum_range = resnum_range)
+        FillLoopsByDatabase(mhandle, fragment_db, structure_db,
+                            torsion_sampler, min_loops_required=-1,
+                            max_res_extension=6, chain_idx = chain_idx,
+                            resnum_range = resnum_range)
+        
+    # if above fails, try DB-fill with less restrictions
+    FillLoopsByDatabase(mhandle, fragment_db, structure_db,
+                        torsion_sampler, min_loops_required=-1,
+                        chain_idx = chain_idx, resnum_range = resnum_range)
+    FillLoopsByDatabase(mhandle, fragment_db, structure_db,
+                        torsion_sampler, chain_idx = chain_idx,
+                        resnum_range = resnum_range)
+
+    # close remaining gaps by Monte Carlo
+    FillLoopsByMonteCarlo(mhandle, torsion_sampler, chain_idx = chain_idx,
+                          resnum_range = resnum_range)
+
+    # last resort approach to close large deletions
+    CloseLargeDeletions(mhandle, structure_db, chain_idx = chain_idx, 
+                        resnum_range = resnum_range)
+
+    # model termini if required
+    if model_termini:
+        ModelTermini(mhandle, structure_db = structure_db, 
+                     fragment_cache_dir = fragment_cache_dir, 
+                     scratch_dir = scratch_dir)
+
+    #In the function above, we call :func:`FillLoopsByDatabase` multiple
+    #times. First, we try to close "easy" gaps which require few extensions 
+    #(we wish to limit the damage we do on the template) and for which we have 
+    #plenty of loop candidates. If some gaps cannot be closed like this, we try 
+    #less restrictive options. This approach is helpful if neighboring gaps are 
+    #close together and the one closer to the C-terminus is easier to close. 
+    #Several variants were evaluated on 1752 target-template-pairs and this one 
+    #worked best.
+
 # these methods will be exported into module
 __all__ = ('CloseSmallDeletions', 'MergeGapsByDistance', 'FillLoopsByDatabase',
            'FillLoopsByMonteCarlo', 'CloseGaps', 'ModelTermini', 
diff --git a/modelling/pymod/_pipeline.py b/modelling/pymod/_pipeline.py
index efc8c81c73291d7422869b2e30f8696ffc0cc264..f3b63987a080d4b80ae1a877f97fc308d75e27dc 100644
--- a/modelling/pymod/_pipeline.py
+++ b/modelling/pymod/_pipeline.py
@@ -407,7 +407,9 @@ def CheckFinalModel(mhandle):
                         "of residue " + str(res))
 
 def BuildFromRawModel(mhandle, alternative_mhandles = None, 
-                      use_amber_ff=False, extra_force_fields=list()):
+                      use_amber_ff=False, extra_force_fields=list(),
+                      fragment_cache_dir = None, model_termini = False,
+                      scratch_dir = os.getcwd()):
     '''Build a model starting with a raw model (see :func:`BuildRawModel`).
 
     This function implements a recommended pipeline to generate complete models
@@ -475,20 +477,24 @@ def BuildFromRawModel(mhandle, alternative_mhandles = None,
         current_num_residues = mhandle.model.GetResidueCount()
         new_num_residues = None
         while current_num_residues != new_num_residues:
-          current_num_residues = mhandle.model.GetResidueCount()
-          ModelExtensions(mhandle, alternative_mhandles, 
-                          extension_strategy = "overlapping")
-          ModelExtensions(mhandle, alternative_mhandles,
-                          extension_strategy = "non-overlapping")
-          new_num_residues = mhandle.model.GetResidueCount()        
+            current_num_residues = mhandle.model.GetResidueCount()
+            ModelExtensions(mhandle, alternative_mhandles, 
+                            extension_strategy = "overlapping")
+            ModelExtensions(mhandle, alternative_mhandles,
+                            extension_strategy = "non-overlapping")
+            new_num_residues = mhandle.model.GetResidueCount()        
 
     # remove terminal gaps
-    RemoveTerminalGaps(mhandle)
+    if not model_termini:
+        RemoveTerminalGaps(mhandle)
 
     # close gaps
     CloseGaps(mhandle, merge_distance = merge_distance, 
               fragment_db = fragment_db, structure_db = structure_db, 
-              torsion_sampler = torsion_sampler)
+              torsion_sampler = torsion_sampler, 
+              fragment_cache_dir = fragment_cache_dir,
+              model_termini = model_termini,
+              scratch_dir = scratch_dir)
 
     # build sidechains
     BuildSidechains(mhandle, merge_distance, fragment_db,