From 5a4105de3abf6b00d8f50f5a8d02f6fa48c84e5d Mon Sep 17 00:00:00 2001
From: Nils Krumnack <krumnack@iastate.edu>
Date: Thu, 4 Aug 2022 12:55:52 -0500
Subject: [PATCH] CP Algorithm Blocks: replace "references" with two-pass
 configuration

Given that the first iteration of the configuration block design
invoked criticism of the "reference" design for tracking containers I
now changed it, so that instead of first allocating references to
determine what the intermediate containers are, I just run through the
entire configuration twice.  The second run-through then can rely on
what it learned in the first run-through to determine what will happen
further down in the sequence.

This requires routing a number of calls through the ConfigAccumulator
object, but it definitely makes writing the actual blocks more
straightforward, and it should also allow for more flexibility if we
need to redesign the ConfigAccumulator in the future (which also
became simpler with this change).
---
 .../python/ConfigAccumulator.py               | 268 ++++++------------
 .../python/ConfigSequence.py                  |  14 +-
 .../python/MuonAnalysisConfig.py              |  61 ++--
 .../python/DiTauAnalysisConfig.py             | 139 +++++++++
 .../python/TauAnalysisConfig.py               | 153 ++++++++++
 5 files changed, 411 insertions(+), 224 deletions(-)
 create mode 100644 PhysicsAnalysis/Algorithms/TauAnalysisAlgorithms/python/DiTauAnalysisConfig.py
 create mode 100644 PhysicsAnalysis/Algorithms/TauAnalysisAlgorithms/python/TauAnalysisConfig.py

diff --git a/PhysicsAnalysis/Algorithms/AnalysisAlgorithmsConfig/python/ConfigAccumulator.py b/PhysicsAnalysis/Algorithms/AnalysisAlgorithmsConfig/python/ConfigAccumulator.py
index 8af643ce4716..2fe87717a5a8 100644
--- a/PhysicsAnalysis/Algorithms/AnalysisAlgorithmsConfig/python/ConfigAccumulator.py
+++ b/PhysicsAnalysis/Algorithms/AnalysisAlgorithmsConfig/python/ConfigAccumulator.py
@@ -1,5 +1,8 @@
 # Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
 
+import AnaAlgorithm.DualUseConfig as DualUseConfig
+
+
 def mapUserName (name) :
     """map an internal name to a name for systematics data handles
 
@@ -19,127 +22,29 @@ class ContainerConfig :
         self.name = name
         self.sourceName = sourceName
         self.index = 0
+        self.maxIndex = None
         self.viewIndex = 1
         self.selection = {}
         self.selection[''] = []
         self.selectionBits = {}
         self.selectionBits[''] = []
 
-
-    def indexName (self, index) :
-        """the name of the (temporary) container at the processing
-        step `index`"""
-
-        if index == 0 :
-            if not self.sourceName :
-                raise Exception ("no source name defined for " + self.name)
+    def currentName (self) :
+        if self.index == 0 :
             return self.sourceName
-        if index == self.index :
-            return mapUserName (self.name)
-        return mapUserName(self.name + "_STEP" + str(index))
-
-
-
-
-class ContainerReadRef :
-    """a read-only reference to a container"""
-
-    def __init__ (self, config, readIndex) :
-        self._config = config
-        self._readIndex = readIndex
-
-    def sourceContainer (self):
-        """get the original container for the container"""
-        if not self._config.sourceName:
-            raise Exception ('no source container for ' + self.config.name)
-        return self._config.sourceName
-
-    def input (self) :
-        """get the name of the input container in the event store"""
-        return self._config.indexName (self._readIndex)
-
-    def wantCopy (self) :
-        """whether a shallow copy should be configured with this reference"""
-        return False
-
-
-
-
-class ContainerUpdateRef :
-    """a copy/update reference to a container (i.e. making a copy)"""
-
-    def __init__ (self, config, readIndex, writeIndex) :
-        self._config = config
-        self._readIndex = readIndex
-        self._writeIndex = writeIndex
-
-    def sourceContainer (self):
-        """get the original container for the container"""
-        if not self._config.sourceName:
-            raise Exception ('no source container for ' + self.config.name)
-        return self._config.sourceName
-
-    def input (self) :
-        """get the name of the input container in the event store"""
-        return self._config.indexName (self._readIndex)
-
-    def output (self) :
-        """get the name of the output container in the event store"""
-        return self._config.indexName (self._writeIndex)
-
-    def wantCopy (self) :
-        """whether a shallow copy should be configured with this reference"""
-        return True
-
-
-
-
-class ContainerWriteRef :
-    """a write reference to a container (creating a new container)"""
-
-    def __init__ (self, config) :
-        self._config = config
-
-    def output (self) :
-        """get the name of the output container in the event store"""
-        return self._config.indexName (1)
-
-
+        if self.maxIndex and self.index == self.maxIndex :
+            return mapUserName(self.name)
+        return mapUserName(self.name + "_STEP" + str(self.index))
 
 
-class ContainerViewRef :
-    """a view reference to a container (providing a view container based
-    on a selection"""
-
-    def __init__ (self, config, baseConfig, baseRef, selection) :
-        self._config = config
-        self._baseConfig = baseConfig
-        self._baseRef = baseRef
-        self._selection = selection
-        self._scheduledAlg = False
-
-    def sourceContainer (self):
-        """get the original container for the container"""
-        return self._baseRef.sourceContainer()
-
-    def input (self) :
-        """get the name of the input container in the event store"""
-
-        if not self._scheduledAlg :
-            viewIndex = self._config.viewIndex
-            self._config.viewIndex += 1
-            inputContainer = self._baseRef.input()
-            outputContainer = mapUserName(self._config.name + "_VIEW" + str(viewIndex))
-            self.outputContainer = outputContainer
-            from AnaAlgorithm.DualUseConfig import createAlgorithm
-            viewalg = createAlgorithm( 'CP::AsgViewFromSelectionAlg', self._config.name + 'View' + str (viewIndex) + 'Alg' )
-            viewalg.selection = [ self._selection ]
-            viewalg.input = inputContainer
-            viewalg.output = outputContainer
-            self._baseConfig.addAlg (viewalg)
-            self._scheduledAlg = True
-
-        return self.outputContainer
+    def nextPass (self) :
+        self.maxIndex = self.index
+        self.index = 0
+        self.viewIndex = 1
+        self.selection = {}
+        self.selection[''] = []
+        self.selectionBits = {}
+        self.selectionBits[''] = []
 
 
 
@@ -168,95 +73,110 @@ class ConfigAccumulator :
         self._dataType = dataType
         self._algSeq = algSeq
         self._containerConfig = {}
+        self._pass = 0
+        self._algorithms = {}
+        self._currentAlg = None
+
 
     def dataType (self) :
         """the data type we run on (data, mc, afii)"""
         return self._dataType
 
-    def addAlg (self, alg) :
-        """add an algorithm to the sequence being created"""
-        self._algSeq += alg
+    def createAlgorithm (self, type, name) :
+        """create a new algorithm and register it as the current algorithm"""
+        if self._pass == 0 :
+            if name in self._algorithms :
+                raise Exception ('duplicate algorithms: ' + name)
+            alg = DualUseConfig.createAlgorithm (type, name)
+            self._algSeq += alg
+            self._algorithms[name] = alg
+            self._currentAlg = alg
+            return alg
+        else :
+            if name not in self._algorithms :
+                raise Exception ('unknown algorithm requested: ' + name)
+            self._currentAlg = self._algorithms[name]
+            return self._algorithms[name]
+
+
+    def createPublicTool (self, type, name) :
+        '''create a new public tool and register it as the "current algorithm"'''
+        if self._pass == 0 :
+            if name in self._algorithms :
+                raise Exception ('duplicate public tool: ' + name)
+            tool = DualUseConfig.createPublicTool (type, name)
+            try:
+                # Try to access the ToolSvc, to see whethet we're in Athena mode:
+                from AthenaCommon.AppMgr import ToolSvc  # noqa: F401
+            except ImportError:
+                # We're not, so let's remember this as a "normal" algorithm:
+                self._algSeq += tool
+            self._algorithms[name] = tool
+            self._currentAlg = tool
+            return tool
+        else :
+            if name not in self._algorithms :
+                raise Exception ('unknown public tool requested: ' + name)
+            self._currentAlg = self._algorithms[name]
+            return self._algorithms[name]
+
 
+    def addPrivateTool (self, type, name) :
+        """add a private tool to the current algorithm"""
+        if self._pass == 0 :
+            DualUseConfig.addPrivateTool (self._currentAlg, type, name)
 
-    def readOrUpdateRef (self, containerName, sourceName) :
-        """get a reference object to decorate the given container with
-        an option to update it
 
-        The idea is that for the first algorithm in the sequence we
-        usually want to make a shallow copy, and usually do that by
-        taking advantage of copy-handles that can optionally copy.  So
-        for every algorithm that could be the first in a sequence we
-        use this kind of reference which will either return a
-        read-reference or an update reference based on the context in
-        which it is used.
+    def readName (self, containerName, sourceName=None) :
+        """get the name of the "current copy" of the given container
 
-        Callers should check the wantCopy() member function on the
-        reference to see whether a copy should be configured.
+        As extra copies get created during processing this will track
+        the correct name of the current copy.  Optionally one can pass
+        in the name of the container before the first copy.
         """
         if containerName not in self._containerConfig :
             if not sourceName :
                 raise Exception ("no source container for: " + containerName)
             self._containerConfig[containerName] = ContainerConfig (containerName, sourceName)
-            config = self._containerConfig[containerName]
-            config.index += 1
-            return ContainerUpdateRef (config, config.index-1, config.index)
-        else :
-            config = self._containerConfig[containerName]
-            return ContainerReadRef (config, config.index)
+        return self._containerConfig[containerName].currentName()
 
 
-    def updateRef (self, containerName, sourceName) :
-        """get a reference object to update/copy the given container"""
+    def copyName (self, containerName) :
+        """register that a copy of the container will be made and return
+        its name"""
         if containerName not in self._containerConfig :
-            if not sourceName :
-                raise Exception ("no source container for: " + containerName)
-            self._containerConfig[containerName] = ContainerConfig (containerName, sourceName)
-        config = self._containerConfig[containerName]
-        config.index += 1
-        return ContainerUpdateRef (config, config.index-1, config.index)
-
+            raise Exception ("unknown container: " + containerName)
+        self._containerConfig[containerName].index += 1
+        return self._containerConfig[containerName].currentName()
 
-    def writeRef (self, containerName) :
-        """get a reference object to write/create the given container"""
-        if containerName in self._containerConfig :
-            raise Exception ("trying to create already existing container: " + containerName)
-        self._containerConfig[containerName] = ContainerConfig (containerName, None)
-        config = self._containerConfig[containerName]
-        config.index = 1
-        return ContainerWriteRef (config)
 
+    def wantCopy (self, containerName) :
+        """ask whether we want/need a copy of the container
 
-    def readRef (self, containerName) :
-        """get a reference object to read the given container"""
+        This usually only happens if no copy of the container has been
+        made yet and the copy is needed to allow modifications, etc.
+        """
         if containerName not in self._containerConfig :
-            self._containerConfig[containerName] = ContainerConfig (containerName, containerName)
-        config = self._containerConfig[containerName]
-        return ContainerReadRef (config, config.index)
+            raise Exception ("unknown container: " + containerName)
+        return self._containerConfig[containerName].index == 0
 
 
-    def viewRef (self, containerName) :
-        """get a reference object to read the given container with the given selection
+    def nextPass (self) :
+        """switch to the next configuration pass
 
-        The idea here is that this allows the user to specify a given view
-        as e.g. MyElectrons.tight and the reference object will then return
-        the name of a view container in the event store that has the
-        selection applied.  This brings the handling of algorithms with a
-        view-based preselection in line with other algorithms.
-
-        WARNING: Unlike the other references this may/will schedule an
-        algorithm when queried (to create the view), so care must be
-        taken to query the container name at the right time."""
-        split = containerName.split ('.')
-        if len(split) == 1:
-            return self.readRef (containerName)
-        if len(split) != 2:
-            raise Exception ('invalid name for a view input: ' + containerName)
-        baseRef = self.readRef(split[0])
-        config = self._containerConfig[split[0]]
-        return ContainerViewRef (config, self, baseRef, split[1])
+        Configuration happens in two steps, with all the blocks processed
+        twice.  This switches from the first to the second pass.
+        """
+        if self._pass != 0 :
+            raise Exception ("already performed final pass")
+        for name in self._containerConfig :
+            self._containerConfig[name].nextPass ()
+        self._pass = 1
+        self._currentAlg = None
 
 
     def getSelection (self, containerName, selectionName) :
+
         """get the selection string for the given selection on the given
         container"""
         if containerName not in self._containerConfig :
diff --git a/PhysicsAnalysis/Algorithms/AnalysisAlgorithmsConfig/python/ConfigSequence.py b/PhysicsAnalysis/Algorithms/AnalysisAlgorithmsConfig/python/ConfigSequence.py
index 595760202c08..d0c789a598f3 100644
--- a/PhysicsAnalysis/Algorithms/AnalysisAlgorithmsConfig/python/ConfigSequence.py
+++ b/PhysicsAnalysis/Algorithms/AnalysisAlgorithmsConfig/python/ConfigSequence.py
@@ -24,17 +24,6 @@ class ConfigSequence:
         self._blocks.append (block)
 
 
-    def collectReferences (self, config) :
-        """call collectReferences() on all blocks
-
-        This registers references to the containers being processed in
-        the individual blocks, allowing them to match up along the
-        sequence when creating the algorithms.
-        """
-        for block in self._blocks:
-            block.collectReferences (config)
-
-
     def makeAlgs (self, config) :
         """call makeAlgs() on all blocks
 
@@ -52,5 +41,6 @@ class ConfigSequence:
         contain all the blocks that will be configured, as it will
         perform all configuration steps at once.
         """
-        self.collectReferences (config)
+        self.makeAlgs (config)
+        config.nextPass ()
         self.makeAlgs (config)
diff --git a/PhysicsAnalysis/Algorithms/MuonAnalysisAlgorithms/python/MuonAnalysisConfig.py b/PhysicsAnalysis/Algorithms/MuonAnalysisAlgorithms/python/MuonAnalysisConfig.py
index 3d76142dcfb8..502ac0acc0b4 100644
--- a/PhysicsAnalysis/Algorithms/MuonAnalysisAlgorithms/python/MuonAnalysisConfig.py
+++ b/PhysicsAnalysis/Algorithms/MuonAnalysisAlgorithms/python/MuonAnalysisConfig.py
@@ -1,7 +1,6 @@
 # Copyright (C) 2002-2021 CERN for the benefit of the ATLAS collaboration
 
 # AnaAlgorithm import(s):
-from AnaAlgorithm.DualUseConfig import createAlgorithm, addPrivateTool
 from AnalysisAlgorithmsConfig.ConfigBlock import ConfigBlock
 import ROOT
 
@@ -15,59 +14,51 @@ class MuonCalibrationConfig (ConfigBlock):
         self.postfix = postfix
         self.ptSelectionOutput = False
 
-    def collectReferences (self, config) :
-        self._muonRef1 = config.readOrUpdateRef (self.containerName, "Muons")
-        self._muonRef2 = config.updateRef (self.containerName, "Muons")
-
     def makeAlgs (self, config) :
         # Set up the eta-cut on all muons prior to everything else
-        alg = createAlgorithm( 'CP::AsgSelectionAlg',
+        alg = config.createAlgorithm( 'CP::AsgSelectionAlg',
                                'MuonEtaCutAlg' + self.postfix )
-        addPrivateTool( alg, 'selectionTool', 'CP::AsgPtEtaSelectionTool' )
+        config.addPrivateTool( 'selectionTool', 'CP::AsgPtEtaSelectionTool' )
         alg.selectionTool.maxEta = 2.5
         alg.selectionDecoration = 'selectEta' + self.postfix + ',as_bits'
-        alg.particles = self._muonRef1.input()
-        if self._muonRef1.wantCopy() :
-            alg.particlesOut = self._muonRef1.output()
+        alg.particles = config.readName (self.containerName, "Muons")
+        if config.wantCopy (self.containerName) :
+            alg.particlesOut = config.copyName (self.containerName)
         alg.preselection = config.getSelection (self.containerName, '')
         config.addSelection (self.containerName, '', alg.selectionDecoration, 2)
         config.addSelection (self.containerName, 'output', alg.selectionDecoration, 2)
-        config.addAlg (alg)
 
         # Set up the track selection algorithm:
-        alg = createAlgorithm( 'CP::AsgLeptonTrackSelectionAlg',
+        alg = config.createAlgorithm( 'CP::AsgLeptonTrackSelectionAlg',
                                'MuonTrackSelectionAlg' + self.postfix )
         alg.selectionDecoration = 'trackSelection' + self.postfix + ',as_bits'
         alg.maxD0Significance = 3
         alg.maxDeltaZ0SinTheta = 0.5
-        alg.particles = self._muonRef2.input()
+        alg.particles = config.readName (self.containerName)
         alg.preselection = config.getSelection (self.containerName, '')
         config.addSelection (self.containerName, '', alg.selectionDecoration, 3)
         config.addSelection (self.containerName, 'output', alg.selectionDecoration, 3)
-        config.addAlg (alg)
 
         # Set up the muon calibration and smearing algorithm:
-        alg = createAlgorithm( 'CP::MuonCalibrationAndSmearingAlg',
+        alg = config.createAlgorithm( 'CP::MuonCalibrationAndSmearingAlg',
                                'MuonCalibrationAndSmearingAlg' + self.postfix )
-        addPrivateTool( alg, 'calibrationAndSmearingTool',
+        config.addPrivateTool( 'calibrationAndSmearingTool',
                         'CP::MuonCalibrationPeriodTool' )
         alg.calibrationAndSmearingTool.calibrationMode = 2 # choose ID+MS with no sagitta bias
-        alg.muons = self._muonRef2.input()
-        alg.muonsOut = self._muonRef2.output()
+        alg.muons = config.readName (self.containerName)
+        alg.muonsOut = config.copyName (self.containerName)
         alg.preselection = config.getSelection (self.containerName, '')
-        config.addAlg (alg)
 
         # Set up the the pt selection
-        alg = createAlgorithm( 'CP::AsgSelectionAlg', 'MuonPtCutAlg' + self.postfix )
+        alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'MuonPtCutAlg' + self.postfix )
         alg.selectionDecoration = 'selectPt' + self.postfix + ',as_bits'
-        addPrivateTool( alg, 'selectionTool', 'CP::AsgPtEtaSelectionTool' )
-        alg.particles = self._muonRef2.output()
+        config.addPrivateTool( 'selectionTool', 'CP::AsgPtEtaSelectionTool' )
+        alg.particles = config.readName (self.containerName)
         alg.selectionTool.minPt = 3e3
         alg.preselection = config.getSelection (self.containerName, '')
         config.addSelection (self.containerName, '', alg.selectionDecoration, 2)
         if self.ptSelectionOutput :
             config.addSelection (self.containerName, 'output', alg.selectionDecoration, 2)
-        config.addAlg (alg)
 
 
 
@@ -85,9 +76,6 @@ class MuonWorkingPointConfig (ConfigBlock) :
         self.isolation = isolation
         self.qualitySelectionOutput = False
 
-    def collectReferences (self, config) :
-        self._muonRef1 = config.readRef (self.containerName)
-
     def makeAlgs (self, config) :
 
         if self.quality == 'Tight' :
@@ -116,45 +104,42 @@ class MuonWorkingPointConfig (ConfigBlock) :
                               '\", allowed values are Iso, NonIso')
 
         # Setup the muon quality selection
-        alg = createAlgorithm( 'CP::MuonSelectionAlgV2',
+        alg = config.createAlgorithm( 'CP::MuonSelectionAlgV2',
                                'MuonSelectionAlg' + postfix )
-        addPrivateTool( alg, 'selectionTool', 'CP::MuonSelectionTool' )
+        config.addPrivateTool( 'selectionTool', 'CP::MuonSelectionTool' )
         alg.selectionTool.MuQuality = quality
         alg.selectionDecoration = 'good_muon' + postfix + ',as_bits'
         alg.badMuonVetoDecoration = 'is_bad' + postfix + ',as_char'
-        alg.muons = self._muonRef1.input()
+        alg.muons = config.readName (self.containerName)
         alg.preselection = config.getSelection (self.containerName, self.selectionName)
         config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration, 4)
         if self.qualitySelectionOutput:
             config.addSelection (self.containerName, 'output', alg.selectionDecoration, 4)
-        config.addAlg (alg)
 
         # Set up the isolation calculation algorithm:
         if self.isolation != 'NonIso' :
-            alg = createAlgorithm( 'CP::MuonIsolationAlg',
+            alg = config.createAlgorithm( 'CP::MuonIsolationAlg',
                                    'MuonIsolationAlg' + postfix )
-            addPrivateTool( alg, 'isolationTool', 'CP::IsolationSelectionTool' )
+            config.addPrivateTool( 'isolationTool', 'CP::IsolationSelectionTool' )
             alg.isolationDecoration = 'isolated_muon' + postfix + ',as_bits'
-            alg.muons = self._muonRef1.input()
+            alg.muons = config.readName (self.containerName)
             alg.preselection = config.getSelection (self.containerName, self.selectionName)
             config.addSelection (self.containerName, self.selectionName, alg.isolationDecoration, 1)
             if self.qualitySelectionOutput:
                 config.addSelection (self.containerName, 'output', alg.isolationDecoration, 1)
-            config.addAlg (alg)
 
         # Set up the efficiency scale factor calculation algorithm:
         if config.dataType() != 'data':
-            alg = createAlgorithm( 'CP::MuonEfficiencyScaleFactorAlg',
+            alg = config.createAlgorithm( 'CP::MuonEfficiencyScaleFactorAlg',
                                    'MuonEfficiencyScaleFactorAlg' + postfix )
-            addPrivateTool( alg, 'efficiencyScaleFactorTool',
+            config.addPrivateTool( 'efficiencyScaleFactorTool',
                             'CP::MuonEfficiencyScaleFactors' )
             alg.scaleFactorDecoration = 'muon_effSF' + postfix + "_%SYS%"
             alg.outOfValidity = 2 #silent
             alg.outOfValidityDeco = 'bad_eff' + postfix
             alg.efficiencyScaleFactorTool.WorkingPoint = self.quality
-            alg.muons = self._muonRef1.input()
+            alg.muons = config.readName (self.containerName)
             alg.preselection = config.getSelection (self.containerName, self.selectionName)
-            config.addAlg (alg)
 
 
 
diff --git a/PhysicsAnalysis/Algorithms/TauAnalysisAlgorithms/python/DiTauAnalysisConfig.py b/PhysicsAnalysis/Algorithms/TauAnalysisAlgorithms/python/DiTauAnalysisConfig.py
new file mode 100644
index 000000000000..93a5b282ce59
--- /dev/null
+++ b/PhysicsAnalysis/Algorithms/TauAnalysisAlgorithms/python/DiTauAnalysisConfig.py
@@ -0,0 +1,139 @@
+# Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+
+# AnaAlgorithm import(s):
+from AnalysisAlgorithmsConfig.ConfigBlock import ConfigBlock
+
+
+class DiTauCalibrationConfig (ConfigBlock):
+    """the ConfigBlock for the tau four-momentum correction"""
+
+    def __init__ (self, containerName, postfix) :
+        super (DiTauCalibrationConfig, self).__init__ ()
+        self.containerName = containerName
+        self.postfix = postfix
+        self.rerunTruthMatching = True
+
+
+    def makeAlgs (self, config) :
+
+        postfix = self.postfix
+        if postfix != '' and postfix[0] != '_' :
+            postfix = '_' + postfix
+
+        # Set up the tau 4-momentum smearing algorithm:
+        alg = config.createAlgorithm( 'CP::DiTauSmearingAlg', 'DiTauSmearingAlg' + postfix )
+        config.addPrivateTool( 'smearingTool', 'TauAnalysisTools::DiTauSmearingTool' )
+        alg.taus = config.readName (self.containerName, "DiTauJets")
+        alg.tausOut = config.copyName (self.containerName)
+        alg.preselection = config.getSelection (self.containerName, '')
+
+        # Set up the tau truth matching algorithm:
+        if self.rerunTruthMatching and config.dataType() != 'data':
+            alg = config.createAlgorithm( 'CP::DiTauTruthMatchingAlg',
+                                   'DiTauTruthMatchingAlg' + postfix )
+            config.addPrivateTool( 'matchingTool',
+                            'TauAnalysisTools::DiTauTruthMatchingTool' )
+            alg.matchingTool.WriteTruthTaus = 1
+            alg.taus = self.readName (self.containerName)
+            alg.preselection = config.getSelection (self.containerName, '')
+
+
+
+
+
+class DiTauWorkingPointConfig (ConfigBlock) :
+    """the ConfigBlock for the tau working point
+
+    This may at some point be split into multiple blocks (16 Mar 22)."""
+
+    def __init__ (self, containerName, postfix, quality) :
+        super (DiTauWorkingPointConfig, self).__init__ ()
+        self.containerName = containerName
+        self.selectionName = postfix
+        self.postfix = postfix
+        self.quality = quality
+        self.legacyRecommendations = False
+
+
+    def makeAlgs (self, config) :
+
+        postfix = self.postfix
+        if postfix != '' and postfix[0] != '_' :
+            postfix = '_' + postfix
+
+        # using enum value from: https://gitlab.cern.ch/atlas/athena/blob/21.2/PhysicsAnalysis/TauID/TauAnalysisTools/TauAnalysisTools/Enums.h
+        # the dictionary is missing in Athena, so hard-coding values here
+        if self.quality == 'Tight' :
+            IDLevel = 4 # ROOT.TauAnalysisTools.JETIDBDTTIGHT
+        elif self.quality == 'Medium' :
+            IDLevel = 3 # ROOT.TauAnalysisTools.JETIDBDTMEDIUM
+        elif self.quality == 'Loose' :
+            IDLevel = 2 # ROOT.TauAnalysisTools.JETIDBDTLOOSE
+        else :
+            raise ValueError ("invalid tau quality: \"" + self.quality +
+                              "\", allowed values are Tight, Medium, Loose")
+
+
+        # Set up the algorithm calculating the efficiency scale factors for the
+        # taus:
+        if config.dataType() != 'data':
+            alg = config.createAlgorithm( 'CP::DiTauEfficiencyCorrectionsAlg',
+                                   'DiTauEfficiencyCorrectionsAlg' + postfix )
+            config.addPrivateTool( 'efficiencyCorrectionsTool',
+                            'TauAnalysisTools::DiTauEfficiencyCorrectionsTool' )
+            alg.efficiencyCorrectionsTool.IDLevel = IDLevel
+            alg.scaleFactorDecoration = 'tau_effSF' + postfix
+            # alg.outOfValidity = 2 #silent
+            # alg.outOfValidityDeco = "bad_eff"
+            alg.taus = config.readName (self.containerName)
+            alg.preselection = config.getSelection (self.containerName, self.selectionName)
+
+
+
+
+
+def makeDiTauCalibrationConfig( seq, containerName, postfix = '',
+                              rerunTruthMatching = True):
+    """Create tau calibration analysis algorithms
+
+    This makes all the algorithms that need to be run first befor
+    all working point specific algorithms and that can be shared
+    between the working points.
+
+    Keyword arguments:
+      dataType -- The data type to run on ("data", "mc" or "afii")
+      postfix -- a postfix to apply to decorations and algorithm
+                 names.  this is mostly used/needed when using this
+                 sequence with multiple working points to ensure all
+                 names are unique.
+      rerunTruthMatching -- Whether or not to rerun truth matching
+    """
+
+    config = DiTauCalibrationConfig (containerName, postfix)
+    config.rerunTruthMatching = rerunTruthMatching
+    seq.append (config)
+
+
+
+
+
+def makeDiTauWorkingPointConfig( seq, containerName, workingPoint, postfix,
+                               legacyRecommendations = False):
+    """Create tau analysis algorithms for a single working point
+
+    Keyword arguments:
+      dataType -- The data type to run on ("data", "mc" or "afii")
+      legacyRecommendations -- use legacy tau BDT and electron veto recommendations
+      postfix -- a postfix to apply to decorations and algorithm
+                 names.  this is mostly used/needed when using this
+                 sequence with multiple working points to ensure all
+                 names are unique.
+    """
+
+    splitWP = workingPoint.split ('.')
+    if len (splitWP) != 1 :
+        raise ValueError ('working point should be of format "quality", not ' + workingPoint)
+
+    config = DiTauWorkingPointConfig (containerName, postfix, splitWP[0])
+    config.legacyRecommendations = legacyRecommendations
+    seq.append (config)
diff --git a/PhysicsAnalysis/Algorithms/TauAnalysisAlgorithms/python/TauAnalysisConfig.py b/PhysicsAnalysis/Algorithms/TauAnalysisAlgorithms/python/TauAnalysisConfig.py
new file mode 100644
index 000000000000..00a515e601d9
--- /dev/null
+++ b/PhysicsAnalysis/Algorithms/TauAnalysisAlgorithms/python/TauAnalysisConfig.py
@@ -0,0 +1,153 @@
+# Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+
+# AnaAlgorithm import(s):
+from AnalysisAlgorithmsConfig.ConfigBlock import ConfigBlock
+
+
+class TauCalibrationConfig (ConfigBlock):
+    """the ConfigBlock for the tau four-momentum correction"""
+
+    def __init__ (self, containerName, postfix) :
+        super (TauCalibrationConfig, self).__init__ ()
+        self.containerName = containerName
+        self.postfix = postfix
+        self.rerunTruthMatching = True
+
+
+    def makeAlgs (self, config) :
+
+        postfix = self.postfix
+        if postfix != '' and postfix[0] != '_' :
+            postfix = '_' + postfix
+
+        # Set up the tau truth matching algorithm:
+        if self.rerunTruthMatching and config.dataType() != 'data':
+            alg = config.createAlgorithm( 'CP::TauTruthMatchingAlg',
+                                          'TauTruthMatchingAlg' + postfix )
+            config.addPrivateTool( 'matchingTool',
+                                   'TauAnalysisTools::TauTruthMatchingTool' )
+            alg.matchingTool.WriteTruthTaus = 1
+            alg.taus = config.readName (self.containerName, 'TauJets')
+            alg.preselection = config.getSelection (self.containerName, '')
+
+        # Set up the tau 4-momentum smearing algorithm:
+        alg = config.createAlgorithm( 'CP::TauSmearingAlg', 'TauSmearingAlg' + postfix )
+        config.addPrivateTool( 'smearingTool', 'TauAnalysisTools::TauSmearingTool' )
+        alg.taus = config.readName (self.containerName, 'TauJets')
+        alg.tausOut = config.copyName (self.containerName)
+        alg.preselection = config.getSelection (self.containerName, '')
+
+
+
+
+
+class TauWorkingPointConfig (ConfigBlock) :
+    """the ConfigBlock for the tau working point
+
+    This may at some point be split into multiple blocks (16 Mar 22)."""
+
+    def __init__ (self, containerName, postfix, quality) :
+        super (TauWorkingPointConfig, self).__init__ ()
+        self.containerName = containerName
+        self.selectionName = postfix
+        self.postfix = postfix
+        self.quality = quality
+        self.legacyRecommendations = False
+
+
+    def makeAlgs (self, config) :
+
+        postfix = self.postfix
+        if postfix != '' and postfix[0] != '_' :
+            postfix = '_' + postfix
+
+        nameFormat = 'TauAnalysisAlgorithms/tau_selection_{}.conf'
+        if self.legacyRecommendations:
+            nameFormat = 'TauAnalysisAlgorithms/tau_selection_{}_legacy.conf'
+
+        if self.quality not in ['Tight', 'Medium', 'Loose', 'VeryLoose', 'NoID', 'Baseline'] :
+            raise ValueError ("invalid tau quality: \"" + self.quality +
+                              "\", allowed values are Tight, Medium, Loose, " +
+                              "VeryLoose, NoID, Baseline")
+        inputfile = nameFormat.format(self.quality.lower())
+
+        # Setup the tau selection tool
+        selectionTool = config.createPublicTool( 'TauAnalysisTools::TauSelectionTool',
+                                                 'TauSelectionTool' + postfix)
+        selectionTool.ConfigPath = inputfile
+
+        # Set up the algorithm selecting taus:
+        alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'TauSelectionAlg' + postfix )
+        config.addPrivateTool( 'selectionTool', 'TauAnalysisTools::TauSelectionTool' )
+        alg.selectionTool.ConfigPath = inputfile
+        alg.selectionDecoration = 'selected_tau' + postfix + ',as_bits'
+        alg.particles = config.readName (self.containerName)
+        alg.preselection = config.getSelection (self.containerName, self.selectionName)
+        config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration, 6)
+        config.addSelection (self.containerName, 'output', alg.selectionDecoration, 6)
+
+
+        # Set up the algorithm calculating the efficiency scale factors for the
+        # taus:
+        if config.dataType() != 'data':
+            alg = config.createAlgorithm( 'CP::TauEfficiencyCorrectionsAlg',
+                                   'TauEfficiencyCorrectionsAlg' + postfix )
+            config.addPrivateTool( 'efficiencyCorrectionsTool',
+                            'TauAnalysisTools::TauEfficiencyCorrectionsTool' )
+            alg.efficiencyCorrectionsTool.TauSelectionTool = '%s/%s' % \
+                ( selectionTool.getType(), selectionTool.getName() )
+            alg.scaleFactorDecoration = 'tau_effSF' + postfix + '_%SYS%'
+            alg.outOfValidity = 2 #silent
+            alg.outOfValidityDeco = 'bad_eff' + postfix
+            alg.taus = config.readName (self.containerName)
+            alg.preselection = config.getSelection (self.containerName, self.selectionName)
+
+
+
+
+
+def makeTauCalibrationConfig( seq, containerName, postfix = '',
+                              rerunTruthMatching = True):
+    """Create tau calibration analysis algorithms
+
+    This makes all the algorithms that need to be run first befor
+    all working point specific algorithms and that can be shared
+    between the working points.
+
+    Keyword arguments:
+      dataType -- The data type to run on ("data", "mc" or "afii")
+      postfix -- a postfix to apply to decorations and algorithm
+                 names.  this is mostly used/needed when using this
+                 sequence with multiple working points to ensure all
+                 names are unique.
+      rerunTruthMatching -- Whether or not to rerun truth matching
+    """
+
+    config = TauCalibrationConfig (containerName, postfix)
+    config.rerunTruthMatching = rerunTruthMatching
+    seq.append (config)
+
+
+
+
+
+def makeTauWorkingPointConfig( seq, containerName, workingPoint, postfix,
+                               legacyRecommendations = False):
+    """Create tau analysis algorithms for a single working point
+
+    Keyword arguments:
+      dataType -- The data type to run on ("data", "mc" or "afii")
+      legacyRecommendations -- use legacy tau BDT and electron veto recommendations
+      postfix -- a postfix to apply to decorations and algorithm
+                 names.  this is mostly used/needed when using this
+                 sequence with multiple working points to ensure all
+                 names are unique.
+    """
+
+    splitWP = workingPoint.split ('.')
+    if len (splitWP) != 1 :
+        raise ValueError ('working point should be of format "quality", not ' + workingPoint)
+
+    config = TauWorkingPointConfig (containerName, postfix, splitWP[0])
+    config.legacyRecommendations = legacyRecommendations
+    seq.append (config)
-- 
GitLab