diff --git a/DaVinciExamples/python/DaVinciExamples/tupling/AllFunctors.py b/DaVinciExamples/python/DaVinciExamples/tupling/AllFunctors.py
index 52ae9e13f40e676788959e189f93b5be057ad32a..738f0546bf475d9cb786e15319896d5efe791438 100644
--- a/DaVinciExamples/python/DaVinciExamples/tupling/AllFunctors.py
+++ b/DaVinciExamples/python/DaVinciExamples/tupling/AllFunctors.py
@@ -20,7 +20,7 @@ from FunTuple import FunTuple_Particles as Funtuple
 from PyConf.reading import get_particles, get_pvs
 from DaVinci.algorithms import add_filter
 from PyConf.reading import get_decreports, get_odin
-from DecayTreeFitter import DTFAlg
+from DecayTreeFitter import DecayTreeFitter
 from DaVinci.truth_matching import configured_MCTruthAndBkgCatAlg
 from PyConf.Algorithms import PrintDecayTree
 
@@ -36,7 +36,7 @@ _composite = 'composite'
 _toplevel = 'toplevel'
 
 
-def all_variables(pvs, DTFR, mctruth, ptype):
+def all_variables(pvs, DTF, mctruth, ptype):
     """
     function that returns dictonary of functors that work.
 
@@ -129,9 +129,8 @@ def all_variables(pvs, DTFR, mctruth, ptype):
     if basic:
         all_vars['IS_PHOTON'] = F.IS_PHOTON
 
-    all_vars['DTF_PT'] = F.MAP_INPUT(Functor=F.PT, Relations=DTFR)
-    all_vars['DTF_BPVIPCHI2'] = F.MAP_INPUT(
-        Functor=F.BPVIPCHI2(pvs), Relations=DTFR)
+    all_vars['DTF_PT'] = DTF.get_info(F.PT)
+    all_vars['DTF_BPVIPCHI2'] = DTF.get_info(F.BPVIPCHI2(pvs))
     # MAP_INPUT_ARRAY
 
     all_vars['MASS'] = F.MASS
@@ -266,9 +265,7 @@ def alg_config(options: Options):
     #
     # DecayTreeFitter Algorithm
     #
-    DTF = DTFAlg(Input=bd2dsk_data)
-    # DTFParts = DTF.Output  # Particles (not needed)
-    DTFRelations = DTF.OutputRelations  # Relations
+    DTF = DecayTreeFitter(name='DTF_Bd2DsK', input=bd2dsk_data)
     #
     # MC truth
     #
@@ -283,18 +280,12 @@ def alg_config(options: Options):
         'pip': "[B0 -> (D_s- -> ^pi+ pi- pi-) K+]CC",
     }
     variables_dsk = {
-        'B0':
-        FunctorCollection(
-            all_variables(v2_pvs, DTFRelations, mctruth, _toplevel)),
-        'Kaon':
-        FunctorCollection(
-            all_variables(v2_pvs, DTFRelations, mctruth, _basic)),
-        'Ds':
-        FunctorCollection(
-            all_variables(v2_pvs, DTFRelations, mctruth, _composite)),
-        'pip':
-        FunctorCollection(
-            all_variables(v2_pvs, DTFRelations, mctruth, _basic)),
+        'B0': FunctorCollection(
+            all_variables(v2_pvs, DTF, mctruth, _toplevel)),
+        'Kaon': FunctorCollection(all_variables(v2_pvs, DTF, mctruth, _basic)),
+        'Ds': FunctorCollection(
+            all_variables(v2_pvs, DTF, mctruth, _composite)),
+        'pip': FunctorCollection(all_variables(v2_pvs, DTF, mctruth, _basic)),
     }
 
     #
diff --git a/DaVinciExamples/python/DaVinciExamples/tupling/DTF_filtered.py b/DaVinciExamples/python/DaVinciExamples/tupling/DTF_filtered.py
index 3ca2d53045295a016eace6d797681ca582015267..8d2b5b73cba82e4c757dea25fa4e7d0edab9f917 100644
--- a/DaVinciExamples/python/DaVinciExamples/tupling/DTF_filtered.py
+++ b/DaVinciExamples/python/DaVinciExamples/tupling/DTF_filtered.py
@@ -19,7 +19,7 @@ import Functors as F
 from Gaudi.Configuration import INFO
 from DaVinci import Options, make_config
 from DaVinci.algorithms import add_filter  #, filter_on
-from DecayTreeFitter import DTFAlg
+from DecayTreeFitter import DecayTreeFitter
 from FunTuple import FunctorCollection as FC
 from FunTuple import FunTuple_Particles as Funtuple
 from PyConf.reading import get_particles
@@ -42,12 +42,11 @@ def main(options: Options):
     data_filtered = get_particles(f"/Event/Spruce/{spruce_line}/Particles")
 
     # DecayTreeFitter Algorithm.
-    DTF = DTFAlg(
-        Input=data_filtered, MassConstraints=["D_s-"], OutputLevel=INFO)
-    DTFRelations = DTF.OutputRelations  # Relations
-
-    #make a helper lambda function
-    DTF_func = lambda func: F.MAP_INPUT(Functor=func, Relations=DTFRelations)
+    DTF = DecayTreeFitter(
+        name='DTF_filtered',
+        input=data_filtered,
+        mass_constraints=["D_s-"],
+        output_level=INFO)
 
     #make collection of functors for all particles
     variables_all = FC({
@@ -59,15 +58,15 @@ def main(options: Options):
     #make collection of functors for Ds meson
     variables_ds = FC({
         'DTF_PT':
-        DTF_func(F.PT),
+        DTF.get_info(F.PT),
         'DTF_MASS':
-        DTF_func(F.MASS),
+        DTF.get_info(F.MASS),
         # Important note: specify an invalid value for integer functors if there exists no truth info.
         #                 The invalid value for floating point functors is set to nan.
         'DTF_CHILD1_ID':
-        F.VALUE_OR(0) @ DTF_func(F.CHILD(1, F.PARTICLE_ID)),
+        F.VALUE_OR(0) @ DTF.get_info(F.CHILD(1, F.PARTICLE_ID)),
         'DTF_CHILD1_MASS':
-        DTF_func(F.CHILD(1, F.MASS)),
+        DTF.get_info(F.CHILD(1, F.MASS)),
     })
 
     #associate FunctorCollection to field (branch) name
diff --git a/DaVinciExamples/python/DaVinciExamples/tupling/DTF_run_mc.py b/DaVinciExamples/python/DaVinciExamples/tupling/DTF_run_mc.py
index e0466923d296a99add552c534a730fe8b2c720ae..bb023fddc348d3487fa1ad7ee2f59eedbb4a8216 100644
--- a/DaVinciExamples/python/DaVinciExamples/tupling/DTF_run_mc.py
+++ b/DaVinciExamples/python/DaVinciExamples/tupling/DTF_run_mc.py
@@ -20,7 +20,7 @@ from RecoConf.reconstruction_objects import upfront_reconstruction
 from RecoConf.reconstruction_objects import make_pvs_v1
 from FunTuple import FunctorCollection
 from FunTuple import FunTuple_Particles as Funtuple
-from DecayTreeFitter import DTFAlg, DTF_functors
+from DecayTreeFitter import DecayTreeFitter
 from DaVinci import Options, make_config
 
 
@@ -32,9 +32,11 @@ def main(options: Options):
     # DecayTreeFitter Algorithm.
     # One with PV constraint and one without
 
-    DTF = DTFAlg(Input=dimuons, MassConstraints=["J/psi(1S)"], OutputLevel=3)
-    # DTFParts = DTF.Output  # Particles
-    DTFRelations = DTF.OutputRelations  # Relations
+    DTF = DecayTreeFitter(
+        name='DTF_dimuons',
+        input=dimuons,
+        mass_constraints=["J/psi(1S)"],
+        output_level=3)
 
     #FunTuple: Jpsi info
     fields = {}
@@ -62,26 +64,27 @@ def main(options: Options):
         'THOR_MASS':
         F.MASS,
         'DTF_PT':
-        F.MAP_INPUT(Functor=F.PT, Relations=DTFRelations),
+        DTF.get_info(F.PT),
         'DTF_MASS':
-        F.MAP_INPUT(Functor=F.MASS, Relations=DTFRelations),
+        DTF.get_info(F.MASS),
     })
 
     #
     # Another way of adding variables.
     #
-    DTF_pv = DTFAlg(Input=dimuons, InputPVs=pvs, MassConstraints=["J/psi(1S)"])
+    DTF_pv = DecayTreeFitter(
+        name='DTF_PVConstraints',
+        input=dimuons,
+        input_pvs=pvs,
+        mass_constraints=["J/psi(1S)"])
     variables_jpsi.update(
-        DTF_functors(DTF_pv, functors=[F.PT, F.MASS], head='DTF_PV_'))
+        DTF_pv.apply_functors(functors=[F.PT, F.MASS], head='DTF_PV_'))
 
     #make collection of functors for Muplus
     variables_muplus = FunctorCollection({
-        'LOKI_P':
-        'P',
-        'THOR_P':
-        F.P,
-        'DTF_PT':
-        F.MAP_INPUT(Functor=F.PT, Relations=DTFRelations)
+        'LOKI_P': 'P',
+        'THOR_P': F.P,
+        'DTF_PT': DTF.get_info(F.PT),
     })
 
     #associate FunctorCollection to field (branch) name
diff --git a/DaVinciExamples/python/DaVinciExamples/tupling/option_davinci_tupling_DTF_substitutePID.py b/DaVinciExamples/python/DaVinciExamples/tupling/option_davinci_tupling_DTF_substitutePID.py
new file mode 100644
index 0000000000000000000000000000000000000000..2a8e188a28823d75db136caa94a00ff193c118b6
--- /dev/null
+++ b/DaVinciExamples/python/DaVinciExamples/tupling/option_davinci_tupling_DTF_substitutePID.py
@@ -0,0 +1,112 @@
+###############################################################################
+# (c) Copyright 2022 CERN for the benefit of the LHCb Collaboration           #
+#                                                                             #
+# This software is distributed under the terms of the GNU General Public      #
+# Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING".   #
+#                                                                             #
+# In applying this licence, CERN does not waive the privileges and immunities #
+# granted to it by virtue of its status as an Intergovernmental Organization  #
+# or submit itself to any jurisdiction.                                       #
+###############################################################################
+"""option_davinci_tupling_DTF_substitutePID.py
+Example options to show the usage of the new DaVinciTools: DecayTreeFitter.
+"""
+
+from Gaudi.Configuration import INFO
+from FunTuple import FunTuple_Particles as Funtuple
+from FunTuple import FunctorCollection as FC
+from DaVinci.algorithms import add_filter
+from PyConf.reading import get_particles, get_pvs_v1
+from DecayTreeFitter import DecayTreeFitter
+
+import Functors as F
+from DaVinci import Options, make_config
+
+
+def main(options: Options):
+
+    B_Line = "Hlt2BsToJpsiPhi_JPsi2MuMu_PhiToKK_Line"
+    B_Data = get_particles(f'/Event/HLT2/{B_Line}/Particles')
+
+    my_filter = add_filter("HDRFilter_Bs2JpsiPhi", f"HLT_PASS('{B_Line}')")
+
+    pvs_v1 = get_pvs_v1()
+    DTF_JpsiPhi = DecayTreeFitter(
+        name='DTF_JpsiPhi',
+        input=B_Data,
+        mass_constraints=['B_s0', 'J/psi(1S)'],
+        input_pvs=pvs_v1,
+        output_level=INFO)
+
+    DTF_JpsiKst = DecayTreeFitter(
+        name='DTF_JpsiKst',
+        input=B_Data,
+        substitutions=[
+            'B_s0{{B0}}   -> (J/psi(1S) -> mu+ mu-) (phi(1020){{K*(892)0}}  -> K+  K-{{pi-}})',
+            'B_s~0{{B0}}  -> (J/psi(1S) -> mu+ mu-) (phi(1020){{K*(892)~0}} -> K-  K+{{pi+}})',
+        ],
+        mass_constraints=['B0', 'J/psi(1S)'],
+        input_pvs=pvs_v1,
+        output_level=INFO)
+
+    fields = {
+        'Bs': "[ B_s0 -> (J/psi(1S) -> mu+ mu-) (phi(1020) -> K+ K-) ]CC",
+        'Jpsi': "[ B_s0 -> ^(J/psi(1S) -> mu+ mu-) (phi(1020) -> K+ K-) ]CC",
+        'Phi': "[ B_s0 -> (J/psi(1S) -> mu+ mu-) ^(phi(1020) -> K+ K-) ]CC",
+        'MuP': "[ B_s0 -> (J/psi(1S) -> ^mu+ mu-) (phi(1020) -> K+ K-) ]CC",
+        'MuM': "[ B_s0 -> (J/psi(1S) -> mu+ ^mu-) (phi(1020) -> K+ K-) ]CC",
+        'KP': "[ B_s0 -> (J/psi(1S) -> mu+ mu-) (phi(1020) -> ^K+ K-) ]CC",
+        'KM': "[ B_s0 -> (J/psi(1S) -> mu+ mu-) (phi(1020) -> K+ ^K-) ]CC",
+    }
+
+    variables_all = FC({
+        # Original particle
+        'ORIGINAL_ID':
+        F.PARTICLE_ID,
+        'ORIGINAL_M':
+        F.MASS,
+        'ORIGINAL_P':
+        F.P,
+        'ORIGINAL_ENERGY':
+        F.ENERGY,
+        'ORIGINAL_CHI2DOF':
+        F.CHI2DOF,
+        # DTF Bs2JpsiPhi
+        'DTF_JpsiPhi_ID':
+        F.VALUE_OR(-1) @ DTF_JpsiPhi.get_info(F.PARTICLE_ID),
+        'DTF_JpsiPhi_M':
+        DTF_JpsiPhi.get_info(F.MASS),
+        'DTF_JpsiPhi_P':
+        DTF_JpsiPhi.get_info(F.P),
+        'DTF_JpsiPhi_ENERGY':
+        DTF_JpsiPhi.get_info(F.ENERGY),
+        'DTF_JpsiPhi_CHI2DOF':
+        DTF_JpsiPhi.get_info(F.CHI2DOF),
+        # DTF Bd2JpsiKst
+        'DTF_JpsiKst_ID':
+        F.VALUE_OR(-1) @ DTF_JpsiKst.get_info(F.PARTICLE_ID),
+        'DTF_JpsiKst_M':
+        DTF_JpsiKst.get_info(F.MASS),
+        'DTF_JpsiKst_P':
+        DTF_JpsiKst.get_info(F.P),
+        'DTF_JpsiKst_ENERGY':
+        DTF_JpsiKst.get_info(F.ENERGY),
+        'DTF_JpsiKst_CHI2DOF':
+        DTF_JpsiKst.get_info(F.CHI2DOF),
+    })
+
+    variables = {'ALL': variables_all}
+
+    #Configure Funtuple algorithm
+    tuple_data = Funtuple(
+        name="Bs2JpsiPhi_Tuple",
+        tuple_name="DecayTree",
+        fields=fields,
+        variables=variables,
+        inputs=B_Data)
+
+    # Run
+    algs = {
+        "Bs2JpsiPhi": [my_filter, tuple_data],
+    }
+    return make_config(options, algs)
diff --git a/DaVinciExamples/python/DaVinciExamples/tupling/option_davinci_tupling_substitutePID.py b/DaVinciExamples/python/DaVinciExamples/tupling/option_davinci_tupling_substitutePID.py
new file mode 100644
index 0000000000000000000000000000000000000000..e24757acf7c78a22190da5eb612e0fca27307263
--- /dev/null
+++ b/DaVinciExamples/python/DaVinciExamples/tupling/option_davinci_tupling_substitutePID.py
@@ -0,0 +1,113 @@
+###############################################################################
+# (c) Copyright 2022 CERN for the benefit of the LHCb Collaboration           #
+#                                                                             #
+# This software is distributed under the terms of the GNU General Public      #
+# Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING".   #
+#                                                                             #
+# In applying this licence, CERN does not waive the privileges and immunities #
+# granted to it by virtue of its status as an Intergovernmental Organization  #
+# or submit itself to any jurisdiction.                                       #
+###############################################################################
+"""
+There are many situations where you may want to change the hypothesis on PID or
+swap the PID of two particles. For the decay Ds- -> K- pi+ pi-, the K- and the
+pi- may be misidentified. Swapping the PID of K- and pi- allows us to recover
+the misidentified case. In this example, we show how to swap the PID of two
+particles, store the result in different trees, and merge two particle
+containers to tuple the swapped decay and the original decay in the same tree.
+"""
+
+from Gaudi.Configuration import INFO
+from FunTuple import FunTuple_Particles as Funtuple
+from FunTuple import FunctorCollection as FC
+from DaVinci.algorithms import add_filter
+from PyConf.reading import get_particles, get_odin
+from DaVinciTools import SubstitutePID
+from PyConf.Algorithms import ParticleContainerMerger
+from FunTuple.functorcollections import EventInfo
+from DaVinci import Options, make_config
+import Functors as F
+
+
+def main(options: Options):
+
+    # Define the input
+    B_Line = "SpruceB2OC_BdToDsmK_DsmToHHH_FEST_Line"
+    B_Data = get_particles(f'/Event/Spruce/{B_Line}/Particles')
+
+    # Filter
+    my_filter = add_filter("HDRFilter_BdToDsmK", f"HLT_PASS('{B_Line}')")
+
+    # Define the swap
+    Swapped_Data = SubstitutePID(
+        name='Subs_SwapKpi',
+        input=B_Data,
+        substitutions=[
+            'B0  -> ( D_s- -> K-{{pi-}} pi+ pi-{{K-}} ) K+',
+            'B~0 -> ( D_s+ -> K+{{pi+}} pi- pi+{{K+}} ) K-'
+        ]).Particles
+
+    # Merge the swapped decays and the original decays
+    MergedContainer = ParticleContainerMerger(
+        name='HypothesisMerger',
+        InputContainers=[B_Data, Swapped_Data],
+        OutputLevel=INFO).OutputContainer
+
+    # Prepare output for funtuple
+    fields = {
+        'B': "[B0 ->  ( D_s- ->  K-  pi+  pi- )  K+]CC",
+        'Ds': "[B0 -> ^( D_s- ->  K-  pi+  pi- )  K+]CC",
+        'Kminus': "[B0 ->  ( D_s- -> ^K-  pi+  pi- )  K+]CC",
+        'piplus': "[B0 ->  ( D_s- ->  K- ^pi+  pi- )  K+]CC",
+        'piminus': "[B0 ->  ( D_s- ->  K-  pi+ ^pi- )  K+]CC",
+        'Kplus': "[B0 ->  ( D_s- ->  K-  pi+  pi- ) ^K+]CC",
+    }
+
+    variables_all = FC({
+        'ID': F.PARTICLE_ID,
+        'M': F.MASS,
+        'P': F.P,
+        'PT': F.PT,
+        'ENERGY': F.ENERGY,
+    })
+
+    variables = {'ALL': variables_all}
+
+    # Get event information
+    odin = get_odin(options)
+    evt_vars = EventInfo(odin)
+
+    #
+    # Configure Funtuple algorithms
+    #
+
+    # 1. Original decays
+    tuple_original = Funtuple(
+        name='OriginalTuple',
+        tuple_name='DecayTree',
+        fields=fields,
+        variables=variables,
+        event_variables=evt_vars,
+        inputs=B_Data)
+    # 2. Swapped decays
+    tuple_swapped = Funtuple(
+        name='SwappedTuple',
+        tuple_name='DecayTree',
+        fields=fields,
+        variables=variables,
+        event_variables=evt_vars,
+        inputs=Swapped_Data)
+    # 3. Original decays + Swapped decays (Merged)
+    tuple_merged = Funtuple(
+        name='MergedTuple',
+        tuple_name='DecayTree',
+        fields=fields,
+        variables=variables,
+        event_variables=evt_vars,
+        inputs=MergedContainer)
+
+    # Run
+    algs = {
+        "Bd2DsmK": [my_filter, tuple_original, tuple_swapped, tuple_merged],
+    }
+    return make_config(options, algs)
diff --git a/DaVinciExamples/tests/qmtest/tupling.qms/test_davinci_tupling_DTF_SubsPID.qmt b/DaVinciExamples/tests/qmtest/tupling.qms/test_davinci_tupling_DTF_SubsPID.qmt
new file mode 100644
index 0000000000000000000000000000000000000000..b90910348603a280a3c4c8acb15dcfe70544dbc8
--- /dev/null
+++ b/DaVinciExamples/tests/qmtest/tupling.qms/test_davinci_tupling_DTF_SubsPID.qmt
@@ -0,0 +1,57 @@
+<?xml version="1.0" ?>
+<!--
+###############################################################################
+# (c) Copyright 2022 CERN for the benefit of the LHCb Collaboration           #
+#                                                                             #
+# This software is distributed under the terms of the GNU General Public      #
+# Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING".   #
+#                                                                             #
+# In applying this licence, CERN does not waive the privileges and immunities #
+# granted to it by virtue of its status as an Intergovernmental Organization  #
+# or submit itself to any jurisdiction.                                       #
+###############################################################################
+-->
+<!DOCTYPE extension  PUBLIC '-//QM/2.3/Extension//EN'  'http://www.codesourcery.com/qm/dtds/2.3/-//qm/2.3/extension//en.dtd'>
+<extension class="GaudiTest.GaudiExeTest" kind="test">
+  <argument name="program"><text>lbexec</text></argument>
+  <argument name="args"><set>
+    <text>DaVinciExamples.tupling.option_davinci_tupling_DTF_substitutePID:main</text>
+  </set></argument>
+  <argument name="options_yaml_fn"><text>$DAVINCIEXAMPLESROOT/example_data/test_passthrough_thor_lines.yaml</text></argument>
+  <argument name="extra_options_yaml"><text>
+    print_freq: 100
+  </text></argument>
+  <argument name="validator"><text>
+from DaVinciTests.QMTest.DaVinciExclusions import remove_known_warnings
+countErrorLines({"FATAL": 0, "ERROR": 0},
+                stdout=remove_known_warnings(stdout))
+import sys, os
+from ROOT import TFile
+
+
+B_vars_stored = ['Bs_DTF_JpsiKst_CHI2DOF', 'Bs_DTF_JpsiKst_ENERGY', 'Bs_DTF_JpsiKst_ID', 'Bs_DTF_JpsiKst_M', 'Bs_DTF_JpsiKst_P', 'Bs_DTF_JpsiPhi_CHI2DOF', 'Bs_DTF_JpsiPhi_ENERGY', 'Bs_DTF_JpsiPhi_ID', 'Bs_DTF_JpsiPhi_M', 'Bs_DTF_JpsiPhi_P', 'Bs_ORIGINAL_CHI2DOF', 'Bs_ORIGINAL_ENERGY', 'Bs_ORIGINAL_ID', 'Bs_ORIGINAL_M', 'Bs_ORIGINAL_P', 'Jpsi_DTF_JpsiKst_CHI2DOF', 'Jpsi_DTF_JpsiKst_ENERGY', 'Jpsi_DTF_JpsiKst_ID', 'Jpsi_DTF_JpsiKst_M', 'Jpsi_DTF_JpsiKst_P', 'Jpsi_DTF_JpsiPhi_CHI2DOF', 'Jpsi_DTF_JpsiPhi_ENERGY', 'Jpsi_DTF_JpsiPhi_ID', 'Jpsi_DTF_JpsiPhi_M', 'Jpsi_DTF_JpsiPhi_P', 'Jpsi_ORIGINAL_CHI2DOF', 'Jpsi_ORIGINAL_ENERGY', 'Jpsi_ORIGINAL_ID', 'Jpsi_ORIGINAL_M', 'Jpsi_ORIGINAL_P', 'KM_DTF_JpsiKst_CHI2DOF', 'KM_DTF_JpsiKst_ENERGY', 'KM_DTF_JpsiKst_ID', 'KM_DTF_JpsiKst_M', 'KM_DTF_JpsiKst_P', 'KM_DTF_JpsiPhi_CHI2DOF', 'KM_DTF_JpsiPhi_ENERGY', 'KM_DTF_JpsiPhi_ID', 'KM_DTF_JpsiPhi_M', 'KM_DTF_JpsiPhi_P', 'KM_ORIGINAL_CHI2DOF', 'KM_ORIGINAL_ENERGY', 'KM_ORIGINAL_ID', 'KM_ORIGINAL_M', 'KM_ORIGINAL_P', 'KP_DTF_JpsiKst_CHI2DOF', 'KP_DTF_JpsiKst_ENERGY', 'KP_DTF_JpsiKst_ID', 'KP_DTF_JpsiKst_M', 'KP_DTF_JpsiKst_P', 'KP_DTF_JpsiPhi_CHI2DOF', 'KP_DTF_JpsiPhi_ENERGY', 'KP_DTF_JpsiPhi_ID', 'KP_DTF_JpsiPhi_M', 'KP_DTF_JpsiPhi_P', 'KP_ORIGINAL_CHI2DOF', 'KP_ORIGINAL_ENERGY', 'KP_ORIGINAL_ID', 'KP_ORIGINAL_M', 'KP_ORIGINAL_P', 'MuM_DTF_JpsiKst_CHI2DOF', 'MuM_DTF_JpsiKst_ENERGY', 'MuM_DTF_JpsiKst_ID', 'MuM_DTF_JpsiKst_M', 'MuM_DTF_JpsiKst_P', 'MuM_DTF_JpsiPhi_CHI2DOF', 'MuM_DTF_JpsiPhi_ENERGY', 'MuM_DTF_JpsiPhi_ID', 'MuM_DTF_JpsiPhi_M', 'MuM_DTF_JpsiPhi_P', 'MuM_ORIGINAL_CHI2DOF', 'MuM_ORIGINAL_ENERGY', 'MuM_ORIGINAL_ID', 'MuM_ORIGINAL_M', 'MuM_ORIGINAL_P', 'MuP_DTF_JpsiKst_CHI2DOF', 'MuP_DTF_JpsiKst_ENERGY', 'MuP_DTF_JpsiKst_ID', 'MuP_DTF_JpsiKst_M', 'MuP_DTF_JpsiKst_P', 'MuP_DTF_JpsiPhi_CHI2DOF', 'MuP_DTF_JpsiPhi_ENERGY', 'MuP_DTF_JpsiPhi_ID', 'MuP_DTF_JpsiPhi_M', 'MuP_DTF_JpsiPhi_P', 'MuP_ORIGINAL_CHI2DOF', 'MuP_ORIGINAL_ENERGY', 'MuP_ORIGINAL_ID', 'MuP_ORIGINAL_M', 'MuP_ORIGINAL_P', 'Phi_DTF_JpsiKst_CHI2DOF', 'Phi_DTF_JpsiKst_ENERGY', 'Phi_DTF_JpsiKst_ID', 'Phi_DTF_JpsiKst_M', 'Phi_DTF_JpsiKst_P', 'Phi_DTF_JpsiPhi_CHI2DOF', 'Phi_DTF_JpsiPhi_ENERGY', 'Phi_DTF_JpsiPhi_ID', 'Phi_DTF_JpsiPhi_M', 'Phi_DTF_JpsiPhi_P', 'Phi_ORIGINAL_CHI2DOF', 'Phi_ORIGINAL_ENERGY', 'Phi_ORIGINAL_ID', 'Phi_ORIGINAL_M', 'Phi_ORIGINAL_P']
+
+
+#sort the expected vars
+B_vars_stored = sorted(B_vars_stored)
+
+#open the TFile and TTree
+ntuple = './passthrough_tuple.root'
+if not os.path.isfile(ntuple): raise Exception(f"File: {ntuple} does not exist!")
+f      = TFile.Open(ntuple)
+t_B    = f.Get('Bs2JpsiPhi_Tuple/DecayTree')
+
+#sort the stores vars
+b_names = sorted([b.GetName() for b in t_B.GetListOfLeaves()])
+
+B_excluded_1 = set(B_vars_stored) - set(b_names)
+B_excluded_2 = set(b_names) - set(B_vars_stored)
+if len(B_excluded_1) != 0: raise Exception('Number of stored variables is less than what is expected. The extra variables expected are: ' , B_excluded_1)
+if len(B_excluded_2) != 0: raise Exception('Number of stored variables is greater than what is expected. The extra variables stored are: ', B_excluded_2)
+
+f.Close()
+print('Test successfully completed!')
+os.system(f"rm {ntuple}")
+
+  </text></argument>
+</extension>
diff --git a/DaVinciExamples/tests/qmtest/tupling.qms/test_davinci_tupling_SubsPID.qmt b/DaVinciExamples/tests/qmtest/tupling.qms/test_davinci_tupling_SubsPID.qmt
new file mode 100644
index 0000000000000000000000000000000000000000..ea409a2c31fa0d6b95d661d539e376160693f77e
--- /dev/null
+++ b/DaVinciExamples/tests/qmtest/tupling.qms/test_davinci_tupling_SubsPID.qmt
@@ -0,0 +1,65 @@
+<?xml version="1.0" ?>
+<!--
+###############################################################################
+# (c) Copyright 2022 CERN for the benefit of the LHCb Collaboration           #
+#                                                                             #
+# This software is distributed under the terms of the GNU General Public      #
+# Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING".   #
+#                                                                             #
+# In applying this licence, CERN does not waive the privileges and immunities #
+# granted to it by virtue of its status as an Intergovernmental Organization  #
+# or submit itself to any jurisdiction.                                       #
+###############################################################################
+-->
+<!DOCTYPE extension  PUBLIC '-//QM/2.3/Extension//EN'  'http://www.codesourcery.com/qm/dtds/2.3/-//qm/2.3/extension//en.dtd'>
+<extension class="GaudiTest.GaudiExeTest" kind="test">
+  <argument name="program"><text>lbexec</text></argument>
+  <argument name="args"><set>
+    <text>DaVinciExamples.tupling.option_davinci_tupling_substitutePID:main</text>
+  </set></argument>
+  <argument name="options_yaml_fn"><text>$DAVINCIEXAMPLESROOT/example_data/Spruce_all_lines_dst.yaml</text></argument>
+  <argument name="extra_options_yaml"><text>
+    print_freq: 100
+  </text></argument>
+  <argument name="validator"><text>
+from DaVinciTests.QMTest.DaVinciExclusions import remove_known_warnings
+countErrorLines({"FATAL": 0, "ERROR": 0},
+                stdout=remove_known_warnings(stdout))
+import sys, os
+from ROOT import TFile
+
+
+B_vars_stored = ['BUNCHCROSSING_ID', 'BUNCHCROSSING_TYPE', 'B_ENERGY', 'B_ID', 'B_M', 'B_P', 'B_PT', 'Ds_ENERGY', 'Ds_ID', 'Ds_M', 'Ds_P', 'Ds_PT', 'EVENTNUMBER', 'GPSTIME', 'Kminus_ENERGY', 'Kminus_ID', 'Kminus_M', 'Kminus_P', 'Kminus_PT', 'Kplus_ENERGY', 'Kplus_ID', 'Kplus_M', 'Kplus_P', 'Kplus_PT', 'ODINTCK', 'RUNNUMBER', 'piminus_ENERGY', 'piminus_ID', 'piminus_M', 'piminus_P', 'piminus_PT', 'piplus_ENERGY', 'piplus_ID', 'piplus_M', 'piplus_P', 'piplus_PT']
+
+
+#sort the expected vars
+B_vars_stored = sorted(B_vars_stored)
+
+#open the TFile and TTree
+ntuple = './sprucing_tuple.root'
+if not os.path.isfile(ntuple): raise Exception(f"File: {ntuple} does not exist!")
+f      = TFile.Open(ntuple)
+t_Original    = f.Get('OriginalTuple/DecayTree')
+t_Swapped     = f.Get('SwappedTuple/DecayTree')
+t_Merged      = f.Get('MergedTuple/DecayTree')
+
+def check_tree(t_B, B_vars_stored):
+  #sort the stores vars
+  b_names = sorted([b.GetName() for b in t_B.GetListOfLeaves()])
+
+  B_excluded_1 = set(B_vars_stored) - set(b_names)
+  B_excluded_2 = set(b_names) - set(B_vars_stored)
+  if len(B_excluded_1) != 0: raise Exception('Number of stored variables is less than what is expected. The extra variables expected are: ' , B_excluded_1)
+  if len(B_excluded_2) != 0: raise Exception('Number of stored variables is greater than what is expected. The extra variables stored are: ', B_excluded_2)
+
+check_tree(t_Original, B_vars_stored)
+check_tree(t_Swapped , B_vars_stored)
+check_tree(t_Merged  , B_vars_stored)
+
+f.Close()
+
+print('Test successfully completed!')
+os.system(f"rm {ntuple}")
+
+  </text></argument>
+</extension>
diff --git a/DaVinciTests/python/DaVinciTests/DTF_test.py b/DaVinciTests/python/DaVinciTests/DTF_test.py
index 0aabcd3914005e6843e638c28f3810406c7c646e..f1295ed49b4928dab802c7d9d8153f1bd88ce5c2 100644
--- a/DaVinciTests/python/DaVinciTests/DTF_test.py
+++ b/DaVinciTests/python/DaVinciTests/DTF_test.py
@@ -13,13 +13,20 @@ Example of a typical DaVinci job:
  - selection of two detached opposite-charge muons
  - tuple of the selected candidates
  - runs DecayTreeFitterAlg and stores some output
+ 
+Note:
+Below mass constraints are applied to J/psi(1S) and psi(2S) going to dimuon pairs
+whereas the tupling is only performed for the former.
+The latter constraint raises an expected warning to ensure that the DTF
+does not silently ignore such spurious constraints, see https://gitlab.cern.ch/lhcb/Rec/-/issues/408.
 """
 import Functors as F
+from Gaudi.Configuration import INFO
 from Hlt2Conf.standard_particles import make_detached_mumu
 from RecoConf.reconstruction_objects import upfront_reconstruction
 from FunTuple import FunctorCollection
 from FunTuple import FunTuple_Particles as Funtuple
-from DecayTreeFitter import DTFAlg
+from DecayTreeFitter import DecayTreeFitter
 from DaVinci import Options, make_config
 
 
@@ -30,10 +37,11 @@ def main(options: Options):
     # DecayTreeFitter Algorithm.
     # One with PV constraint and one without
 
-    DTF = DTFAlg(
-        Input=dimuons, MassConstraints=["J/psi(1S)", "psi(2S)"], OutputLevel=3)
-    # DTFParts = DTF.Output  # Particles
-    DTFRelations = DTF.OutputRelations  # Relations
+    DTF = DecayTreeFitter(
+        name='DTF_dimuons',
+        input=dimuons,
+        mass_constraints=["J/psi(1S)", "psi(2S)"],
+        output_level=INFO)
 
     #FunTuple: Jpsi info
     fields = {}
@@ -44,9 +52,9 @@ def main(options: Options):
         'THOR_MASS':
         F.MASS,
         'DTF_PT':
-        F.MAP_INPUT(Functor=F.PT, Relations=DTFRelations),
+        DTF.get_info(Functor=F.PT),
         'DTF_MASS':
-        F.MAP_INPUT(Functor=F.MASS, Relations=DTFRelations),
+        DTF.get_info(Functor=F.MASS),
     })
 
     #associate FunctorCollection to field (branch) name
diff --git a/DaVinciTests/tests/qmtest/davinci.qms/test_DTF.qmt b/DaVinciTests/tests/qmtest/davinci.qms/test_DTF.qmt
index 65a931090b297ca4681051882909c7de1c47d4a1..f5c08dd5c74da3da4ebb3a8f2756f8d0244fc74d 100755
--- a/DaVinciTests/tests/qmtest/davinci.qms/test_DTF.qmt
+++ b/DaVinciTests/tests/qmtest/davinci.qms/test_DTF.qmt
@@ -11,6 +11,12 @@
 # or submit itself to any jurisdiction.                                       #
 ###############################################################################
 -->
+
+<!--
+This is a test that checks that DTF does not silently ignore spurious constraints.
+See https://gitlab.cern.ch/lhcb/Rec/-/issues/408
+-->
+
 <!DOCTYPE extension  PUBLIC '-//QM/2.3/Extension//EN'  'http://www.codesourcery.com/qm/dtds/2.3/-//qm/2.3/extension//en.dtd'>
 <extension class="GaudiTest.GaudiExeTest" kind="test">
   <argument name="program"><text>lbexec</text></argument>
@@ -28,7 +34,7 @@
 <argument name="exit_code"><integer>3</integer></argument>
 <argument name="exit_value"><text>Failed</text></argument>
 <argument name="validator"><text>
-findReferenceBlock("""DecayTreeFitterAlg                    ERROR DecayTreeFitter::DecayChain : To-be constrained particle is not in chain.""")
+findReferenceBlock("""DTF_dimuons                           ERROR DecayTreeFitter::DecayChain : To-be constrained particle is not in chain.""")
 countErrorLines({"FATAL":14, "ERROR":9})
 </text></argument>
 </extension>
diff --git a/DaVinciTutorials/python/DaVinciTutorials/tutorial6_DecayTreeFit.py b/DaVinciTutorials/python/DaVinciTutorials/tutorial6_DecayTreeFit.py
index 8148896c8dcdf86f35b89eead75df3f3421eae7f..bd547fbd9f63280dc886554be545f3be7bcac841 100644
--- a/DaVinciTutorials/python/DaVinciTutorials/tutorial6_DecayTreeFit.py
+++ b/DaVinciTutorials/python/DaVinciTutorials/tutorial6_DecayTreeFit.py
@@ -25,7 +25,7 @@ def main(options: Options):
     - https://twiki.cern.ch/twiki/bin/view/LHCb/DecayTreeFitter
     - https://www.nikhef.nl/~wouterh/topicallectures/TrackingAndVertexing/part6.pdf
     """
-    from DecayTreeFitter import DTFAlg
+    from DecayTreeFitter import DecayTreeFitter
 
     #Define a dictionary of "field name" -> "decay descriptor component".
     fields = {
@@ -42,19 +42,19 @@ def main(options: Options):
     kin = Kinematics()
 
     ####### Mass constraint
-    #For DTFAlg, as with MC Truth algorithm (previous example), this algorithm builds a relation
+    #For DecayTreeFitter, as with MC Truth algorithm (previous example), this algorithm builds a relation
     # table i.e. one-to-one map b/w B candidate -> Refitted B candidate.
-    # The relation table is output to the TES location "DTF.OutputRelations"
+    # The relation table is output to the TES location "DTF.Algorithm.OutputRelations"
+    # You can apply functors to refitted candidates using get_info member function.
     # Note: the Jpsi constraint is applied but the phi constraint seems not to be applied (see issue: https://gitlab.cern.ch/lhcb/Rec/-/issues/309)
-    DTF = DTFAlg(MassConstraints=["J/psi(1S)"], Input=input_data)
-
-    #Define a helper lambda function that takes variable name (k) prepends it with "DTF_" and functor (v) which is functor
-    DTFMAP = lambda func: F.MAP_INPUT(func, DTF.OutputRelations)
+    DTF = DecayTreeFitter(
+        name='DTF', mass_constraints=["J/psi(1S)"], input=input_data)
 
     #Loop over the functors in kinematics function and create a new functor collection
-    dtf_kin = FC(
-        {'DTF_' + k: DTFMAP(v)
-         for k, v in kin.get_thor_functors().items()})
+    dtf_kin = FC({
+        'DTF_' + k: DTF.get_info(v)
+        for k, v in kin.get_thor_functors().items()
+    })
     #print(dtf_kin)
     #########
 
@@ -67,16 +67,11 @@ def main(options: Options):
     pvs_v2 = get_pvs()
 
     #Add not only mass but also constrain Bs to be coming from primary vertex
-    DTFpv = DTFAlg(
-        InputPVs=pvs,
-        MassConstraints=["J/psi(1S)", "phi(1020)"],
-        Input=input_data)
-
-    #Helper function for decay tree fitting with PV constaint.
-    # We make here a lambda function that takes as input a functor
-    # the lambda function loads this functor into MAP_INPUT functor
-    # which we encountered previously and returns it.
-    DTFPV_MAP = lambda func: F.MAP_INPUT(func, DTFpv.OutputRelations)
+    DTFpv = DecayTreeFitter(
+        name='DTFpv',
+        input_pvs=pvs,
+        mass_constraints=["J/psi(1S)", "phi(1020)"],
+        input=input_data)
 
     #define the functors
     pv_fun = {}
@@ -90,7 +85,7 @@ def main(options: Options):
     # should have improved compared to lifetime variable pre-DTF ("BPVLTIME").
     # Below we make use of the helper function ("DTFPV_MAP") defined previously.
     pv_coll += FC({
-        'DTFPV_' + k: DTFPV_MAP(v)
+        'DTFPV_' + k: DTFpv.get_info(v)
         for k, v in pv_coll.get_thor_functors().items()
     })
 
diff --git a/DaVinciTutorials/tests/refs/test_tutorial6_DecayTreeFit.ref b/DaVinciTutorials/tests/refs/test_tutorial6_DecayTreeFit.ref
index 6ddb7507cd58d71dd9d050503c82d37815fc936b..473fee726f5166506e655482ae3beffaa996a88a 100644
--- a/DaVinciTutorials/tests/refs/test_tutorial6_DecayTreeFit.ref
+++ b/DaVinciTutorials/tests/refs/test_tutorial6_DecayTreeFit.ref
@@ -132,13 +132,13 @@ TFile: name=tutorial6_DecayTreeFit.root, title=Gaudi Trees, option=CREATE
 NTupleSvc                              INFO NTuples saved successfully
 ApplicationMgr                         INFO Application Manager Finalized successfully
 ApplicationMgr                         INFO Application Manager Terminated successfully
-DecayTreeFitterAlg                     INFO Number of counters : 4
+DTF                                    INFO Number of counters : 4
  |    Counter                                      |     #     |    sum     | mean/eff^* | rms/err^*  |     min     |     max     |
  | "Events"                                        |        12 |
  | "Fitted Particles"                              |        30 |          0 |      0.0000 |      0.0000 |  4.2950e+09 |       0.0000 |
  | "Input Particles"                               |        12 |         30 |     2.5000 |     1.6073 |      1.0000 |      6.0000 |
  | "saved Particles"                               |        12 |        210 |     17.500 |     11.251 |      7.0000 |      42.000 |
-DecayTreeFitterAlgWithPV               INFO Number of counters : 4
+DTFpv                                  INFO Number of counters : 4
  |    Counter                                      |     #     |    sum     | mean/eff^* | rms/err^*  |     min     |     max     |
  | "Events"                                        |        12 |
  | "Fitted Particles"                              |        30 |          0 |      0.0000 |      0.0000 |  4.2950e+09 |       0.0000 |