Skip to content
Snippets Groups Projects
EventSelectionConfig.py 40 KiB
Newer Older
# Copyright (C) 2002-2024 CERN for the benefit of the ATLAS collaboration

from AnalysisAlgorithmsConfig.ConfigBlock import ConfigBlock
from AsgAnalysisAlgorithms.AsgAnalysisConfig import makeEventCutFlowConfig

class EventSelectionMergerConfig(ConfigBlock):
    """ConfigBlock for merging the output of various selection streams"""

    def __init__(self):
        super(EventSelectionMergerConfig, self).__init__()
        self.addOption('selections', [], type=list,
            info="the selection decisions (list of strings) to unify into a "
            "final decision (internally: selection_1 || selection_2 || ...). "
            "The default is [] (empty list).")
        self.addOption('noFilter', False, type=bool,
            info="do not apply an event filter. The default is False, i.e. "
            "remove events not passing the full list of selection cuts.")

    def makeAlgs(self, config):
        alg = config.createAlgorithm('CP::SaveFilterAlg', 'EventSelectionMerger')
        alg.FilterDescription = 'events passing at least one EventSelection algorithm'
        alg.eventDecisionOutputDecoration = 'ignore_anySelection_%SYS%'
        alg.selection = '||'.join([sel+',as_char' for sel in self.selections if sel])
        alg.noFilter = self.noFilter
        alg.selectionName = 'pass_anySelection_%SYS%'
        alg.decorationName = 'ntuplepass_anySelection_%SYS%'

class EventSelectionConfig(ConfigBlock):
    """ConfigBlock for interpreting text-based event selections"""

        super(EventSelectionConfig, self).__init__()
        self.addOption('name', name, type=str,
            noneAction='error',
            info="the name of the event selection, used to uniquely identify "
            "the EventSelectionConfig block.")
        self.addOption('electrons', "", type=str,
            info="the input electron container, with a possible selection, in "
            "the format container or container.selection. The default is '' "
            "(empty string).")
        self.addOption('muons', "", type=str,
            info="the input muon container, with a possible selection, in the "
            "format container or container.selection. The default is '' "
            "(empty string).")
        self.addOption('jets', "", type=str,
            info="the input jet container, with a possible selection, in the "
            "format container or container.selection. The default is '' "
            "(empty string).")
        self.addOption('largeRjets', "", type=str,
            info="the large-R jet container, with a possible selection, in "
            "the format container or container.selection. The default is '' "
            "(empty string).")
        self.addOption('photons', "", type=str,
            info="the input photon container, with a possible selection, in "
            "the format container or container.selection. The default is '' "
            "(empty string).")
        self.addOption('taus', "", type=str,
            info="the input tau-jet container, with a possible selection, in "
            "the format container or container.selection. The default is '' "
            "(empty string).")
        self.addOption('met', "", type=str,
            info="he input MET container. The default is '' (empty string).")
        #TODO: add info string
        self.addOption('metTerm', "Final", type=str,
            info="")
        self.addOption('btagDecoration', "", type=str,
            info="the b-tagging decoration to use when defining b-jets. "
            "The default is '' (empty string).")
        self.addOption('preselection', "", type=str,
            info="the event-wise selection flag to start this event selection "
            "from. The default is '' (empty string).")
        self.addOption('selectionCuts', "", type=str,
            noneAction='error',
            info="a single string listing one selection cut per line.")
        self.addOption('noFilter', False, type=bool,
            info="do not apply an event filter. The default is False, i.e. "
            "remove events not passing the full list of selection cuts.")
        self.addOption('debugMode', False, type=bool,
            info="whether to create an output branch for every single line "
            "of the selection cuts. The default is False (only saves the"
            " final decision).")
        self.step = 0
        self.currentDecoration = ''
        self.cutflow = []

    def makeAlgs(self, config):
        # need to re-initialize here to deal with multiple passes
        self.step = 0
        # initialize the pre-selection
        self.currentDecoration = self.preselection
        # re-initialize the cutflow
        self.cutflow = []
        # read the selection cuts
        if self.selectionCuts is None:
            raise ValueError ("[EventSelectionConfig] You must provide the 'selectionCuts' option to 'EventSelectionConfig': "
                              "a single string where each line represents a different selection cut to apply in order.")
        for line in self.selectionCuts.split("\n"):
            self.interpret(line, config)
        config.addEventCutFlow(self.name, self.getCutflow())

    def interpret(self, text, cfg):
        text = text.strip()
        if not text:
            return
        if text.startswith("#"):
            return
        self.step += 1
        if "EL_N" in text.split():
            self.add_NEL_selector(text, cfg)
        elif "MU_N" in text.split():
            self.add_NMU_selector(text, cfg)
        elif "SUM_EL_N_MU_N" in text.split():
            self.add_SUMNELNMU_selector(text, cfg)
        elif "JET_N" in text.split():
            self.add_NJET_selector(text, cfg)
        elif "JET_N_BTAG" in text.split():
            self.add_NBJET_selector(text, cfg)
        elif "PH_N" in text.split():
            self.add_NPH_selector(text, cfg)
        elif "TAU_N" in text.split():
            self.add_NTAU_selector(text, cfg)
        elif "LJET_N" in text.split():
            self.add_NLJET_selector(text, cfg)
        elif "MET" in text.split():
            self.add_MET_selector(text, cfg)
        elif "MWT" in text.split():
            self.add_MWT_selector(text, cfg)
        elif "MET+MWT" in text.split():
            self.add_METMWT_selector(text, cfg)
        elif "MLL" in text.split():
            self.add_MLL_selector(text, cfg)
        elif "MLLWINDOW" in text.split():
            self.add_MLLWINDOW_selector(text, cfg)
        elif "OS" in text.split():
            self.add_OS_selector(text, cfg)
        elif "SS" in text.split():
            self.add_SS_selector(text, cfg)
        elif "MLL_OSSF" in text.split():
            self.add_MLL_OSSF_selector(text, cfg)
        elif "LJETMASS_N" in text.split():
            self.add_NLJETMASS_selector(text, cfg)
        elif "LJETMASSWINDOW_N" in text.split():
            self.add_NLJETMASSWINDOW_selector(text, cfg)
        elif "SAVE" in text.split():
            self.add_SAVE(text, cfg)
        elif "IMPORT" in text.split():
            self.add_IMPORT(text, cfg)
        elif "EVENTFLAG" in text.split():
            self.add_EVENTFLAG(text, cfg)
        elif "GLOBALTRIGMATCH" in text.split():
            self.add_GLOBALTRIGMATCH(text, cfg)
        else:
            raise ValueError (f"[EventSelectionConfig] The following selection cut is not recognised! --> {text}")

    def raise_misconfig(self, text, keyword):
        raise ValueError (f"[EventSelectionConfig] Misconfiguration! Check {keyword} in: {text}")

    def raise_missinginput(self, collection):
        raise ValueError (f"[EventSelectionConfig] Misconfiguration! Missing input collection for {collection}")

    def check_float(self, test, requirePositive=True):
        try:
            value = float(test)
            if not requirePositive or value >= 0:
                return value
            else:
                raise ValueError (f"[EventSelectionConfig] Misconfiguration! Float {test} is not positive!")
        except ValueError:
            raise ValueError (f"[EventSelectionConfig] Misconfiguration! {test} should be a float, not {type(test)}!")

    def check_int(self, test, requirePositive=True):
        try:
            value = int(test)
            if value == float(test):
                if not requirePositive or value >= 0:
                    return value
                else:
                    raise ValueError (f"[EventSelectionConfig] Misconfiguration! Int {test} us not positive!")
            else:
                raise ValueError (f"[EventSelectionConfig] Misconfiguration! {test} should be an int, not a float!")
        except ValueError:
            raise ValueError (f"[EventSelectionConfig] Misconfiguration! {test} should be an int, not {type(test)}")

    def check_string(self, test):
        if not isinstance(test, str):
            raise ValueError (f"[EventSelectionConfig] Misconfiguration! {test} should be a string, not a number!")
        else:
            return test

    def check_sign(self, test):
        mapping = {
            "<" : "LT",
            ">" : "GT",
            "==": "EQ",
            ">=": "GE",
            "<=": "LE"
        }
        try:
            return mapping[test]
        except KeyError:
            raise KeyError (f"[EventSelectionConfig] Misconfiguration! {test} should be one of {list(mapping.keys())}")

    def check_btagging(self, test):
        test = test.split(":")
        if len(test) != 2:
            raise ValueError (f"[EventSelectionConfig] Misconfiguration! {test} should be provided as 'btagger:btagWP'")
        else:
            return test

    def getCutflow(self):
        return self.cutflow

    def setDecorationName(self, algorithm, config, decoration):
        self.cutflow.append( decoration )
            algorithm.decorationName = f'{decoration},as_char'
            self.currentDecoration = decoration
            if self.debugMode:
                config.addOutputVar('EventInfo', decoration, decoration.split("_%SYS%")[0])
        else:
            if self.currentDecoration:
                self.currentDecoration += '&&' + decoration
            else:
                self.currentDecoration = decoration
        config.addSelection('EventInfo', '', decoration)
        return

    def checkDecorationName(self, decoration):
        if decoration == '':
            return decoration
        decoration = decoration.split("&&")
        decoration = [sub + ',as_char' if ',as_char' not in sub else sub for sub in decoration]
        return '&&'.join(decoration)

    def add_IMPORT(self, text, config):
        # this is used to import a previous selection
        items = text.split()
        if items[0] != "IMPORT":
            self.raise_misconfig(text, "IMPORT")
        if len(items) != 2:
            self.raise_misconfig(text, "number of arguments")
        region = self.check_string(items[1])
        if not self.currentDecoration:
            self.currentDecoration = f'pass_{region}_%SYS%,as_char'
            self.currentDecoration = f'{self.currentDecoration},as_char&&pass_{region}_%SYS%'
        # for the cutflow, we need to retrieve all the cuts corresponding to this IMPORT
        imported_cuts = [cut for cut in config.getSelectionCutFlow('EventInfo', '') if cut.startswith(region)]
        self.cutflow += imported_cuts
        return

    def add_NEL_selector(self, text, config):
        items = text.split()
        if items[0] != "EL_N":
            self.raise_misconfig(text, "EL_N")
        if len(items) != 4 and len(items) != 5:
            self.raise_misconfig(text, "number of arguments")
        if not self.electrons:
            self.raise_missinginput("electrons")
        thisalg = f'{self.name}_NEL_{self.step}'
        alg = config.createAlgorithm('CP::NObjectPtSelectorAlg', thisalg)
        alg.particles, alg.objectSelection = config.readNameAndSelection(self.electrons)
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        if len(items) == 4:
            alg.minPt = self.check_float(items[1])
            alg.sign  = self.check_sign(items[2])
            alg.count = self.check_int(items[3])
        elif len(items) == 5:
            extraSel  = self.check_string(items[1])
            if alg.objectSelection:
                alg.objectSelection += "&&" + config.getFullSelection(self.electrons.split(".")[0], extraSel)
            else:
                alg.objectSelection = config.getFullSelection(self.electrons.split(".")[0], extraSel)
            alg.minPt = self.check_float(items[2])
            alg.sign  = self.check_sign(items[3])
            alg.count = self.check_int(items[4])
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return

    def add_NMU_selector(self, text, config):
        items = text.split()
        if items[0] != "MU_N":
            self.raise_misconfig(text, "MU_N")
        if len(items) != 4 and len(items) != 5:
            self.raise_misconfig(text, "number of arguments")
        thisalg = f'{self.name}_NMU_{self.step}'
        alg = config.createAlgorithm('CP::NObjectPtSelectorAlg', thisalg)
        alg.particles, alg.objectSelection = config.readNameAndSelection(self.muons)
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        if len(items) == 4:
            alg.minPt = self.check_float(items[1])
            alg.sign  = self.check_sign(items[2])
            alg.count = self.check_int(items[3])
        elif len(items) == 5:
            extraSel  = self.check_string(items[1])
            if alg.objectSelection:
                alg.objectSelection += "&&" + config.getFullSelection(self.muons.split(".")[0], extraSel)
            else:
                alg.objectSelection = config.getFullSelection(self.muons.split(".")[0], extraSel)
            alg.minPt = self.check_float(items[2])
            alg.sign  = self.check_sign(items[3])
            alg.count = self.check_int(items[4])
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return

    def add_SUMNELNMU_selector(self, text, config):
        items = text.split()
        if items[0] != "SUM_EL_N_MU_N":
            self.raise_misconfig(text, "SUM_EL_N_MU_N")
        if len(items) != 4 and len(items) != 5:
            self.raise_misconfig(text, "number of arguments")
        if not self.electrons and not self.muons:
            self.raise_missinginput("electrons or muons")
        thisalg = f'{self.name}_SUMNELNMU_{self.step}'
        alg = config.createAlgorithm('CP::SumNElNMuPtSelectorAlg', thisalg)
        alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
        alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        if len(items) == 4:
            alg.minPtEl = self.check_float(items[1])
            alg.minPtMu = self.check_float(items[1])
            alg.sign  = self.check_sign(items[2])
            alg.count = self.check_int(items[3])
        elif len(items) == 5:
            alg.minPtEl = self.check_float(items[1])
            alg.minPtMu = self.check_float(items[2])
            alg.sign  = self.check_sign(items[3])
            alg.count = self.check_int(items[4])
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return

    def add_NJET_selector(self, text, config):
        items = text.split()
        if items[0] != "JET_N":
            self.raise_misconfig(text, "JET_N")
        if len(items) != 4 and len(items) != 5:
            self.raise_misconfig(text, "number of arguments")
        thisalg = f'{self.name}_NJET_{self.step}'
        alg = config.createAlgorithm('CP::NObjectPtSelectorAlg', thisalg)
        alg.particles, alg.objectSelection = config.readNameAndSelection(self.jets)
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        if len(items) == 4:
            alg.minPt = self.check_float(items[1])
            alg.sign  = self.check_sign(items[2])
            alg.count = self.check_int(items[3])
        elif len(items) == 5:
            extraSel  = self.check_string(items[1])
            if alg.objectSelection:
                alg.objectSelection += "&&" + config.getFullSelection(self.jets.split(".")[0], extraSel)
            else:
                alg.objectSelection = config.getFullSelection(self.jets.split(".")[0], extraSel)
            alg.minPt = self.check_float(items[2])
            alg.sign  = self.check_sign(items[3])
            alg.count = self.check_int(items[4])
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return

    def add_NBJET_selector(self, text, config):
        items = text.split()
        if items[0] != "JET_N_BTAG":
            self.raise_misconfig(text, "JET_N_BTAG")
        if len(items) != 3 and len(items) != 4:
            self.raise_misconfig(text, "number of arguments")
        thisalg = f'{self.name}_NBJET_{self.step}'
        alg = config.createAlgorithm('CP::NObjectPtSelectorAlg', thisalg)
        particles, selection = config.readNameAndSelection(self.jets)
        alg.particles = particles
        alg.objectSelection = f'{selection}&&{self.btagDecoration},as_char'
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        alg.minPt = 25000.
        if len(items) == 3:
            alg.sign  = self.check_sign(items[1])
            alg.count = self.check_int(items[2])
        elif len(items) == 4:
            btagger, btagWP = self.check_btagging(items[1])
            customBtag = f'ftag_select_{btagger}_{btagWP}'
            alg.objectSelection = f'{selection}&&{customBtag},as_char'
            alg.sign  = self.check_sign(items[2])
            alg.count = self.check_int(items[3])
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return

    def add_NPH_selector(self, text, config):
        items = text.split()
        if items[0] != "PH_N":
            self.raise_misconfig(text, "PH_N")
        if len(items) != 4 and len(items) != 5:
            self.raise_misconfig(text, "number of arguments")
        if not self.photons:
            self.raise_missinginput("photons")
        thisalg = f'{self.name}_NPH_{self.step}'
        alg = config.createAlgorithm('CP::NObjectPtSelectorAlg', thisalg)
        alg.particles, alg.objectSelection = config.readNameAndSelection(self.photons)
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        if len(items) == 4:
            alg.minPt = self.check_float(items[1])
            alg.sign  = self.check_sign(items[2])
            alg.count = self.check_int(items[3])
        elif len(items) == 5:
            extraSel  = self.check_string(items[1])
            if alg.objectSelection:
                alg.objectSelection += "&&" + config.getFullSelection(self.photons.split(".")[0], extraSel)
            else:
                alg.objectSelection = config.getFullSelection(self.photons.split(".")[0], extraSel)
            alg.minPt = self.check_float(items[2])
            alg.sign  = self.check_sign(items[3])
            alg.count = self.check_int(items[4])
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return

    def add_NTAU_selector(self, text, config):
        items = text.split()
        if items[0] != "TAU_N":
            self.raise_misconfig(text, "TAU_N")
        if len(items) != 4 and len(items) != 5:
            self.raise_misconfig(text, "number of arguments")
        thisalg = f'{self.name}_NTAU_{self.step}'
        alg = config.createAlgorithm('CP::NObjectPtSelectorAlg', thisalg)
        alg.particles, alg.objectSelection = config.readNameAndSelection(self.taus)
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        if len(items) == 4:
            alg.minPt = self.check_float(items[1])
            alg.sign  = self.check_sign(items[2])
            alg.count = self.check_int(items[3])
        elif len(items) == 5:
            extraSel  = self.check_string(items[1])
            if alg.objectSelection:
                alg.objectSelection += "&&" + config.getFullSelection(self.taus.split(".")[0], extraSel)
            else:
                alg.objectSelection = config.getFullSelection(self.taus.split(".")[0], extraSel)
            alg.minPt = self.check_float(items[2])
            alg.sign  = self.check_sign(items[3])
            alg.count = self.check_int(items[4])
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return
    def add_NLJET_selector(self, text, config):
        items = text.split()
        if items[0] != "LJET_N":
            self.raise_misconfig(text, "LJET_N")
        if len(items) != 4 and len(items) != 5:
            self.raise_misconfig(text, "number of arguments")
        thisalg = f'{self.name}_NLJET_{self.step}'
        alg = config.createAlgorithm('CP::NObjectPtSelectorAlg', thisalg)
        alg.particles, alg.objectSelection = config.readNameAndSelection(self.largeRjets)
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        if len(items) == 4:
            alg.minPt = self.check_float(items[1])
            alg.sign  = self.check_sign(items[2])
            alg.count = self.check_int(items[3])
        elif len(items) == 5:
            extraSel  = self.check_string(items[1])
            if alg.objectSelection:
                alg.objectSelection += "&&" + config.getFullSelection(self.largeRjets.split(".")[0], extraSel)
            else:
                alg.objectSelection = config.getFullSelection(self.largeRjets.split(".")[0], extraSel)
            alg.minPt = self.check_float(items[2])
            alg.sign  = self.check_sign(items[3])
            alg.count = self.check_int(items[4])
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return

    def add_NLJETMASS_selector(self, text, config):
        items = text.split()
        if items[0] != "LJETMASS_N":
            self.raise_misconfig(text, "LJETMASS_N")
        if len(items) != 4 and len(items) != 5:
            self.raise_misconfig(text, "number of arguments")
        thisalg = f'{self.name}_NLJETMASS_{self.step}'
        alg = config.createAlgorithm('CP::NObjectMassSelectorAlg', thisalg)
        alg.particles, alg.objectSelection = config.readNameAndSelection(self.largeRjets)
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        if len(items) == 4:
            alg.minMass = self.check_float(items[1])
            alg.sign    = self.check_sign(items[2])
            alg.count   = self.check_int(items[3])
        elif len(items) == 5:
            extraSel  = self.check_string(items[1])
            if alg.objectSelection:
                alg.objectSelection += "&&" + config.getFullSelection(self.largeRjets.split(".")[0], extraSel)
            else:
                alg.objectSelection = config.getFullSelection(self.largeRjets.split(".")[0], extraSel)
            alg.minMass = self.check_float(items[2])
            alg.sign    = self.check_sign(items[3])
            alg.count   = self.check_int(items[4])
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return

    def add_NLJETMASSWINDOW_selector(self, text, config):
        items = text.split()
        if items[0] != "LJETMASSWINDOW_N":
            self.raise_misconfig(text, "LJETMASSWINDOW_N")
        if len(items) != 5 and len(items) != 6 and len(items) != 7:
            self.raise_misconfig(text, "number of arguments")
        thisalg = f'{self.name}_NLJETMASSWINDOW_{self.step}'
        alg = config.createAlgorithm('CP::NLargeRJetMassWindowSelectorAlg', thisalg)
        alg.ljets, alg.ljetSelection = config.readNameAndSelection(self.largeRjets)
        if len(items) == 5 or (len(items) == 6 and "veto" in items):
            alg.lowMass  = self.check_float(items[1])
            alg.highMass = self.check_float(items[2])
            alg.sign     = self.check_sign(items[3])
            alg.count    = self.check_int(items[4])
            alg.vetoMode = (len(items) == 6 and self.check_string(items[5]) == "veto")
        elif (len(items) == 6 and "veto" not in items) or len(items) == 7:
            extraSel = self.check_string(items[1])
            if alg.ljetSelection:
                alg.ljetSelection += "&&" + config.getFullSelection(self.largeRjets.split(".")[0], extraSel)
            else:
                alg.ljetSelection = config.getFullSelection(self.largeRjets.split(".")[0], extraSel)
            alg.lowMass  = self.check_float(items[2])
            alg.highMass = self.check_float(items[3])
            alg.sign     = self.check_sign(items[4])
            alg.count    = self.check_int(items[5])
            alg.vetoMode = (len(items) ==7 and self.check_string(items[6]) == "veto")
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return

    def add_MET_selector(self, text, config):
        items = text.split()
        if items[0] != "MET":
            self.raise_misconfig(text, "MET")
        if len(items) != 3:
            self.raise_misconfig(text, "number of arguments")
        thisalg = f'{self.name}_MET_{self.step}'
        alg = config.createAlgorithm('CP::MissingETSelectorAlg', thisalg)
        alg.met = config.readName(self.met)
        alg.metTerm = self.metTerm
        alg.sign = self.check_sign(items[1])
        alg.refMET = self.check_float(items[2])
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return
    
    def add_MWT_selector(self, text, config):
        items = text.split()
        if items[0] != "MWT":
            self.raise_misconfig(text, "MWT")
        if len(items) != 3:
            self.raise_misconfig(text, "number of arguments")
        if not self.electrons and not self.muons:
            self.raise_missinginput("electrons or muons")
        thisalg = f'{self.name}_MWT_{self.step}'
        alg = config.createAlgorithm('CP::TransverseMassSelectorAlg', thisalg)
        alg.met = config.readName(self.met)
        alg.metTerm = self.metTerm
        alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
        alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
        alg.sign = self.check_sign(items[1])
        alg.refMWT = self.check_float(items[2])
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return

    def add_METMWT_selector(self, text, config):
        items = text.split()
        if items[0] != "MET+MWT":
            self.raise_misconfig(text, "MET+MWT")
        if len(items) != 3:
            self.raise_misconfig(text, "number of arguments")
        if not self.met:
            self.raise_missinginput("MET")
        if not self.electrons and not self.muons:
            self.raise_missinginput("electrons or muons")
        thisalg = f'{self.name}_METMWT_{self.step}'
        alg = config.createAlgorithm('CP::MissingETPlusTransverseMassSelectorAlg', thisalg)
        alg.met = config.readName(self.met)
        alg.metTerm = self.metTerm
        alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
        alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
        alg.sign = self.check_sign(items[1])
        alg.refMETMWT = self.check_float(items[2])
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return

    def add_MLL_selector(self, text, config):
        items = text.split()
        if items[0] != "MLL":
            self.raise_misconfig(text, "MLL")
        if len(items) != 3:
            self.raise_misconfig(text, "number of arguments")
        if not self.electrons and not self.muons:
            self.raise_missinginput("electrons or muons")
        thisalg = f'{self.name}_MLL_{self.step}'
        alg = config.createAlgorithm('CP::DileptonInvariantMassSelectorAlg', thisalg)
        if self.electrons:
            alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
        if self.muons:
            alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
        alg.sign = self.check_sign(items[1])
        alg.refMLL = self.check_float(items[2])
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return

    def add_MLLWINDOW_selector(self, text, config):
        items = text.split()
        if items[0] != "MLLWINDOW":
            self.raise_misconfig(text, "MLLWINDOW")
        if len(items) != 3 and len(items) != 4:
            self.raise_misconfig(text, "number of arguments")
        if not self.electrons and not self.muons:
            self.raise_missinginput("electrons or muons")
        thisalg = f'{self.name}_MLLWINDOW_{self.step}'
        alg = config.createAlgorithm('CP::DileptonInvariantMassWindowSelectorAlg', thisalg)
        if self.electrons:
            alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
        if self.muons:
            alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
        alg.lowMLL = self.check_float(items[1])
        alg.highMLL = self.check_float(items[2])
        alg.vetoMode = (len(items) == 4 and self.check_string(items[3]) == "veto")
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return

    def add_OS_selector(self, text, config):
        items = text.split()
        if len(items) != 1:
            self.raise_misconfig(text, "number of arguments")
        if not self.electrons and not self.muons:
            self.raise_missinginput("electrons or muons")
        thisalg = f'{self.name}_OS_{self.step}'
        alg = config.createAlgorithm('CP::ChargeSelectorAlg', thisalg)
            if "Particle" in self.electrons or "Truth" in self.electrons:
                alg.truthElectrons, alg.truthElectronSelection = config.readNameAndSelection(self.electrons)
            else:
                alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
            if "Particle" in self.muons or "Truth" in self.muons:
                alg.truthMuons, alg.truthMuonSelection = config.readNameAndSelection(self.muons)
            else:
                alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return

    def add_SS_selector(self, text, config):
        items = text.split()
        if len(items) != 1:
            self.raise_misconfig(text, "number of arguments")
        if not self.electrons and not self.muons:
            self.raise_missinginput("electrons or muons")
        thisalg = f'{self.name}_SS_{self.step}'
        alg = config.createAlgorithm('CP::ChargeSelectorAlg', thisalg)
            if "Particle" in self.electrons or "Truth" in self.electrons:
                alg.truthElectrons, alg.truthElectronSelection = config.readNameAndSelection(self.electrons)
            else:
                alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
            if "Particle" in self.muons or "Truth" in self.muons:
                alg.truthMuons, alg.truthMuonSelection = config.readNameAndSelection(self.muons)
            else:
                alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return

    def add_MLL_OSSF_selector(self, text, config):
        items = text.split()
        if items[0] != "MLL_OSSF":
            self.raise_misconfig(text, "MLL_OSSF")
        if len(items) != 3 and len(items) != 4:
            self.raise_misconfig(text, "number of arguments")
        if not self.electrons and not self.muons:
            self.raise_missinginput("electrons or muons")
        thisalg = f'{self.name}_MLL_OSSF_{self.step}'
        alg = config.createAlgorithm('CP::DileptonOSSFInvariantMassWindowSelectorAlg', thisalg)
        if self.electrons:
            if "Particle" in self.electrons or "Truth" in self.electrons:
                alg.truthElectrons, alg.truthElectronSelection = config.readNameAndSelection(self.electrons)
            else:
                alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
        if self.muons:
            if "Particle" in self.muons or "Truth" in self.muons:
                alg.truthMuons, alg.truthMuonSelection = config.readNameAndSelection(self.muons)
            else:
                alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
        alg.lowMll = self.check_float(items[1])
        alg.highMll = self.check_float(items[2])
        alg.vetoMode = (len(items) == 4 and self.check_string(items[3]) == "veto")
        alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
        self.setDecorationName(alg, config, f'{thisalg}_%SYS%')
        return

    def add_EVENTFLAG(self, text, config):
        items = text.split()
        if items[0] != "EVENTFLAG":
            self.raise_misconfig(text, "EVENTFLAG")
        if len(items) != 2:
            self.raise_misconfig(text, "number of arguments")
        existingDecoration = self.check_string(items[1])
        self.setDecorationName(None, config, existingDecoration)
        return

    def add_GLOBALTRIGMATCH(self, text, config):
        items = text.split()
        if items[0] != "GLOBALTRIGMATCH":
            self.raise_misconfig(text, "GLOBALTRIGMATCH")
        if len(items) != 1:
            self.raise_misconfig(text, "number of arguments")
        self.setDecorationName(None, config, "globalTriggerMatch_dontsave_%SYS%,as_char")
    def add_SAVE(self, text, config):
        items = text.split()
        if items[0] != "SAVE":
            self.raise_misconfig(text, "SAVE")
        if len(items) != 1:
            self.raise_misconfig(text, "number of arguments")
        thisalg = f'{self.name}_SAVE'
        alg = config.createAlgorithm('CP::SaveFilterAlg', thisalg)
        alg.FilterDescription = f'events passing < {self.name} >'
        alg.eventDecisionOutputDecoration = f'ignore_{self.name}_%SYS%'
        alg.selection = self.checkDecorationName(self.currentDecoration)
        alg.noFilter = self.noFilter
        alg.selectionName = f'pass_{self.name}_%SYS%,as_char' # this one is used as a selection
        alg.decorationName = f'ntuplepass_{self.name}_%SYS%' # this one is saved to file
        config.addOutputVar('EventInfo', f'ntuplepass_{self.name}_%SYS%', f'pass_{self.name}')
        return

def makeEventSelectionConfig(seq,
                             name,
                             electrons=None, muons=None, jets=None,
                             photons=None, taus=None, met=None, metTerm=None,
                             btagDecoration=None, preselection=None,
                             selectionCuts=None, noFilter=None,
                             debugMode=None, cutFlowHistograms=None):
    """Create an event selection config block

    Keyword arguments:
        name -- the name defining this selection
        electrons -- the electron container and selection
        muons -- the muon container and selection
        jets -- the jet container and selection
        largeRjets -- the large-R jet container and selection
        photons -- the photon container and selection
        taus -- the tau-jet container and selection
        met -- the MET container
        metTerm -- the MET term to use (e.g. 'Final', 'NonInt')
        btagDecoration -- the b-tagging decoration to use when defining b-jets
        preselection -- optional event-wise selection flag to start from
        selectionCuts -- a string listing one selection cut per line
        noFilter -- whether to disable the event filter
        debugMode -- enables saving all intermediate decorations
        cutFlowHistograms -- whether to toggle event cutflow histograms per systematic
    """

    config = EventSelectionConfig(name)
    config.setOptionValue ('electrons', electrons)
    config.setOptionValue ('muons', muons)
    config.setOptionValue ('jets', jets)
    config.setOptionValue ('largeRjets', largeRjets)
    config.setOptionValue ('photons', photons)
    config.setOptionValue ('taus', taus)
    config.setOptionValue ('met', met)
    config.setOptionValue ('metTerm', metTerm)
    config.setOptionValue ('btagDecoration', btagDecoration)
    config.setOptionValue ('preselection', preselection)
    config.setOptionValue ('selectionCuts', selectionCuts)
    config.setOptionValue ('noFilter', noFilter)
    config.setOptionValue ('debugMode', debugMode)
    seq.append(config)

    # add event cutflow algorithm
    if cutFlowHistograms:
        makeEventCutFlowConfig(seq, 'EventInfo', selectionName='', postfix=name,
                               customSelections=name)

def makeMultipleEventSelectionConfigs(seq,
                                      electrons=None, muons=None, jets=None,
                                      photons=None, taus=None, met=None, metTerm=None,
                                      btagDecoration=None, preselection=None,
                                      selectionCutsDict=None, noFilter=None,
                                      debugMode=None, cutFlowHistograms=None):
    """Create multiple event selection config blocks

       Keyword arguments:
        electrons -- the electron container and selection
        muons -- the muon container and selection
        jets -- the jet container and selection
        largeRjets -- the large-R jet container and selection
        photons -- the photon container and selection
        taus -- the tau-jet container and selection
        met -- the MET container
        metTerm -- the MET term to use (e.g. 'Final', 'NonInt')
        btagDecoration -- the b-tagging decoration to use when defining b-jets
        preselection -- optional event-wise selection flag to start from
        selectionCutsDict -- a dictionary with key the name of the selection and value a string listing one selection cut per line
        noFilter -- whether to disable the event filter
        debugMode -- enables saving all intermediate decorations
        cutFlowHistograms -- whether to toggle event cutflow histograms per region and per systematic
    """

    # handle the case where a user is only providing one selection
    if len(list(selectionCutsDict.keys())) == 1:
        name, selectionCuts = list(selectionCutsDict.items())[0]
        makeEventSelectionConfig(seq, name, electrons, muons, jets, largeRjets, photons, taus, met, metTerm, btagDecoration, preselection, selectionCuts, noFilter=noFilter, debugMode=debugMode, cutFlowHistograms=cutFlowHistograms)
        return

    # first, we generate all the individual event selections
    # !!! it's important to pass noFilter=True, to avoid applying the individual filters in series
    for name, selectionCuts in selectionCutsDict.items():
        makeEventSelectionConfig(seq, name, electrons, muons, jets, largeRjets, photons, taus, met, metTerm, btagDecoration, preselection, selectionCuts, noFilter=True, debugMode=debugMode, cutFlowHistograms=cutFlowHistograms)

    # now we are ready to collect all the filters and apply their logical OR
    # !!! subregions (name starts with "SUB") are not used in the final filtering
    config = EventSelectionMergerConfig()
    config.setOptionValue ('selections', [f'pass_{name}_%SYS%' for name in selectionCutsDict.keys() if not name.startswith("SUB")])
    config.setOptionValue ('noFilter', noFilter)
    seq.append(config)