Source code for skim.WGs.fei

#!/usr/bin/env python3

##########################################################################
# basf2 (Belle II Analysis Software Framework)                           #
# Author: The Belle II Collaboration                                     #
#                                                                        #
# See git log for contributors and copyright holders.                    #
# This file is licensed under LGPL-3.0, see LICENSE.md.                  #
##########################################################################

"""
(Semi-)Leptonic Working Group Skims for missing energy modes that use the `FullEventInterpretation` (FEI) algorithm.
"""

from functools import lru_cache, wraps

import basf2 as b2
import fei
import modularAnalysis as ma
from skim import BaseSkim, fancy_skim_header
from skim.utils.misc import _sphinxify_decay
from variables import variables as vm

__liaison__ = "Cameron Harris <cameron.harris@adelaide.edu.au>, Tommy Martinov <tommy.martinov@desy.de>"
_VALIDATION_SAMPLE = "mdst14.root"


def _merge_boolean_dicts(*dicts):
    """Merge dicts of boolean, with `True` values taking precedence if values
    differ.

    This is a utility function for combining FEI configs. It acts in the following
    way:

        >>> d1 = {"neutralB": True, "chargedB": False, "hadronic": True}
        >>> d2 = {"chargedB": True, "semileptonic": True}
        >>> _merge_FEI_configs(d1, d2)
        {"chargedB": True, "hadronic": True, "neutralB": True, "semileptonic": True}

    Parameters:
        dicts (dict(str -> bool)): Any number of dicts of keyword-boolean pairs.

    Returns:
        merged (dict(str -> bool)): A single dict, containing all the keys of the
            input dicts.
    """
    keys = {k for d in dicts for k in d}
    occurances = {k: [d for d in dicts if k in d] for k in keys}
    merged = {k: any(d[k] for d in occurances[k]) for k in keys}

    # Sort the merged dict before returning
    merged = dict(sorted(merged.items()))

    return merged


def _get_fei_channel_names(particleName, **kwargs):
    """Create a list containing the decay strings of all decay channels available to a
    particle. Any keyword arguments are passed to `fei.get_default_channels`.

    This is a utility function for autogenerating FEI skim documentation.

    Args:
        particleName (str): the PDG name of a particle, e.g. ``'K+'``, ``'pi-'``, ``'D*0'``.
    """
    particleList = fei.get_default_channels(**kwargs)
    particleDict = {particle.name: particle for particle in particleList}

    try:
        particle = particleDict[particleName]
    except KeyError:
        print(f"Error! Couldn't find particle with name {particleName}")
        return []

    channels = [channel.decayString for channel in particle.channels]
    return channels


def _hash_dict(func):
    """Wrapper for `functools.lru_cache` to deal with dictionaries. Dictionaries are
    mutable, so cannot be cached. This wrapper turns all dict arguments into a hashable
    dict type, so we can use caching.
    """
    class HashableDict(dict):
        def __hash__(self):
            return hash(frozenset(self.items()))

    @wraps(func)
    def wrapped(*args, **kwargs):
        args = tuple([HashableDict(arg) if isinstance(arg, dict) else arg for arg in args])
        kwargs = {k: HashableDict(v) if isinstance(v, dict) else v for k, v in kwargs.items()}
        return func(*args, **kwargs)
    return wrapped


[docs]class BaseFEISkim(BaseSkim): """Base class for FEI skims. Applies event-level pre-cuts and applies the FEI.""" __authors__ = ["Racha Cheaib", "Hannah Wakeling", "Phil Grace"] __contact__ = __liaison__ __category__ = "physics, Full Event Interpretation" FEIPrefix = "FEIv4_2022_MC15_light-2205-abys" """Prefix label for the FEI training used in the FEI skims.""" FEIChannelArgs = {} """Dict of ``str -> bool`` pairs to be passed to `fei.get_default_channels`. When inheriting from `BaseFEISkim`, override this value to apply the FEI for only *e.g.* SL charged :math:`B`'s.""" MergeDataStructures = {"FEIChannelArgs": _merge_boolean_dicts} NoisyModules = ["ParticleCombiner"] ApplyHLTHadronCut = True produce_on_tau_samples = False # retention is very close to zero on taupair
[docs] @staticmethod @lru_cache() def fei_precuts(path): """ Skim pre-cuts are applied before running the FEI, to reduce computation time. This setup function is run by all FEI skims, so they all have the save event-level pre-cuts: * :math:`n_{\\text{cleaned tracks}} \\geq 3` * :math:`n_{\\text{cleaned ECL clusters}} \\geq 3` * :math:`\\text{Visible energy of event (CMS frame)}>4~{\\rm GeV}` We define "cleaned" tracks and clusters as: * Cleaned tracks (``pi+:FEI_cleaned``): :math:`d_0 < 0.5~{\\rm cm}`, :math:`|z_0| < 2~{\\rm cm}`, and :math:`p_T > 0.1~{\\rm GeV}` * Cleaned ECL clusters (``gamma:FEI_cleaned``): :math:`0.296706 < \\theta < 2.61799`, and :math:`E>0.1~{\\rm GeV}` """ # Pre-selection cuts CleanedTrackCuts = "abs(dz) < 2.0 and abs(dr) < 0.5 and pt > 0.1" CleanedClusterCuts = "E > 0.1 and thetaInCDCAcceptance" ma.fillParticleList(decayString="pi+:FEI_cleaned", cut=CleanedTrackCuts, path=path) ma.fillParticleList(decayString="gamma:FEI_cleaned", cut=CleanedClusterCuts, path=path) ma.buildEventKinematics(inputListNames=["pi+:FEI_cleaned", "gamma:FEI_cleaned"], path=path) EventCuts = " and ".join( [ f"nCleanedTracks({CleanedTrackCuts})>=3", f"nCleanedECLClusters({CleanedClusterCuts})>=3", "visibleEnergyOfEventCMS>4", ] ) # NOTE: The FEI skims are somewhat complicated, and require some manual handling # of conditional paths to avoid adding the FEI to the path twice. In general, DO # NOT do this kind of path handling in your own skim. Instead, use: # >>> path = self.skim_event_cuts(EventLevelCuts, path=path) ConditionalPath = b2.Path() eselect = path.add_module("VariableToReturnValue", variable=f"passesEventCut({EventCuts})") eselect.if_value('=1', ConditionalPath, b2.AfterConditionPath.CONTINUE) return ConditionalPath
# This is a cached static method so that we can avoid adding FEI path twice. # In combined skims, FEIChannelArgs must be combined across skims first, so that all # the required particles are included in the FEI.
[docs] @staticmethod @_hash_dict @lru_cache() def run_fei_for_skims(FEIChannelArgs, FEIPrefix, analysisGlobaltag, *, path): """Reconstruct hadronic and semileptonic :math:`B^0` and :math:`B^+` tags using the generically trained FEI. Parameters: FEIChannelArgs (dict(str, bool)): A dict of keyword-boolean pairs to be passed to `fei.get_default_channels`. FEIPrefix (str): Prefix label for the FEI training used in the FEI skims. path (`basf2.Path`): The skim path to be processed. """ # Run FEI if analysisGlobaltag is None: b2.B2FATAL("The analysis globaltag is not set in the FEI skim.") b2.conditions.prepend_globaltag(analysisGlobaltag) particles = fei.get_default_channels(**FEIChannelArgs) configuration = fei.config.FeiConfiguration( prefix=FEIPrefix, training=False, monitor=False) feistate = fei.get_path(particles, configuration) path.add_path(feistate.path)
[docs] @staticmethod @_hash_dict @lru_cache() def setup_fei_aliases(FEIChannelArgs): # Aliases for pre-FEI event-level cuts vm.addAlias("E_ECL_pi_FEI", "totalECLEnergyOfParticlesInList(pi+:FEI_cleaned)") vm.addAlias("E_ECL_gamma_FEI", "totalECLEnergyOfParticlesInList(gamma:FEI_cleaned)") vm.addAlias("E_ECL_FEI", "formula(E_ECL_pi_FEI+E_ECL_gamma_FEI)") # Aliases for variables available after running the FEI vm.addAlias("sigProb", "extraInfo(SignalProbability)") vm.addAlias("log10_sigProb", "log10(extraInfo(SignalProbability))") vm.addAlias("dmID", "extraInfo(decayModeID)") vm.addAlias("decayModeID", "extraInfo(decayModeID)") if "semileptonic" in FEIChannelArgs and FEIChannelArgs["semileptonic"]: # Aliases specific to SL FEI vm.addAlias("cosThetaBY", "cosThetaBetweenParticleAndNominalB") vm.addAlias("d1_p_CMSframe", "useCMSFrame(daughter(1,p))") vm.addAlias("d2_p_CMSframe", "useCMSFrame(daughter(2,p))") vm.addAlias( "p_lepton_CMSframe", "conditionalVariableSelector(dmID<4, d1_p_CMSframe, d2_p_CMSframe)" )
[docs] def additional_setup(self, path): """Apply pre-FEI event-level cuts and apply the FEI. This setup function is run by all FEI skims, so they all have the save event-level pre-cuts. This function passes `FEIChannelArgs` to the cached function `run_fei_for_skims` to avoid applying the FEI twice. See also: `fei_precuts` for event-level cut definitions. """ self.setup_fei_aliases(self.FEIChannelArgs) path = self.fei_precuts(path) # The FEI skims require some manual handling of paths that is not necessary in # any other skim. self._ConditionalPath = path self.run_fei_for_skims(self.FEIChannelArgs, self.FEIPrefix, self.analysisGlobaltag, path=path)
def _FEI_skim_header(ParticleNames): """Decorator factory for applying the `fancy_skim_header` header and replacing <CHANNELS> in the class docstring with a list of FEI channels. The list is numbered with all of the corresponding decay mode IDs, and the decay modes are formatted in beautiful LaTeX. .. code-block:: python @FEI_skim_header("B0") class feiSLB0(BaseFEISkim): # docstring here including the string '<CHANNELS>' somewhere Parameters: ParticleNames (str, list(str)): One of either ``B0`` or ``B+``, or a list of both. """ def decorator(SkimClass): if isinstance(ParticleNames, str): particles = [ParticleNames] else: particles = ParticleNames ChannelsString = "List of reconstructed channels and corresponding decay mode IDs:" for particle in particles: channels = _get_fei_channel_names(particle, **SkimClass.FEIChannelArgs) FormattedChannels = [_sphinxify_decay(channel) for channel in channels] ChannelList = "\n".join( [f" {dmID}. {channel}" for (dmID, channel) in enumerate(FormattedChannels)] ) if len(particles) == 1: ChannelsString += "\n\n" + ChannelList else: ChannelsString += f"\n\n ``{particle}`` channels:\n\n" + ChannelList if SkimClass.__doc__ is None: return SkimClass else: SkimClass.__doc__ = SkimClass.__doc__.replace("<CHANNELS>", ChannelsString) return fancy_skim_header(SkimClass) return decorator
[docs]@_FEI_skim_header("B0") class feiHadronicB0(BaseFEISkim): """ Tag side :math:`B` cuts: * :math:`M_{\\text{bc}} > 5.2~{\\rm GeV}` * :math:`|\\Delta E| < 0.3~{\\rm GeV}` * :math:`\\text{signal probability} > 0.001` (omitted for decay mode 23) All available FEI :math:`B^0` hadronic tags are reconstructed. From `Thomas Keck's thesis <https://docs.belle2.org/record/275/files/BELLE2-MTHESIS-2015-001.pdf>`_, "the channel :math:`B^0 \\to \\overline{D}^0 \\pi^0` was used by the FR, but is not yet used in the FEI due to unexpected technical restrictions in the KFitter algorithm". <CHANNELS> See also: `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for event-level cuts made before applying the FEI. """ __description__ = "FEI-tagged neutral :math:`B`'s decaying hadronically." validation_sample = _VALIDATION_SAMPLE FEIChannelArgs = { "neutralB": True, "chargedB": False, "hadronic": True, "semileptonic": False, "KLong": False, "baryonic": True }
[docs] def build_lists(self, path): ma.applyCuts("B0:generic", "Mbc>5.2", path=path) ma.applyCuts("B0:generic", "abs(deltaE)<0.300", path=path) ma.applyCuts("B0:generic", "sigProb>0.001 or extraInfo(dmID)==23", path=path) return ["B0:generic"]
[docs] def validation_histograms(self, path): # NOTE: the validation package is not part of the light releases, so this import # must be made here rather than at the top of the file. from validation_tools.metadata import create_validation_histograms vm.addAlias('sigProb', 'extraInfo(SignalProbability)') vm.addAlias('log10_sigProb', 'log10(extraInfo(SignalProbability))') vm.addAlias('d0_massDiff', 'daughter(0,massDifference(0))') vm.addAlias('d0_M', 'daughter(0,M)') vm.addAlias('decayModeID', 'extraInfo(decayModeID)') vm.addAlias('nDaug', 'countDaughters(1>0)') # Dummy cut so all daughters are selected. histogramFilename = f"{self}_Validation.root" create_validation_histograms( rootfile=histogramFilename, particlelist='B0:generic', variables_1d=[ ('sigProb', 100, 0.0, 1.0, 'Signal probability', __liaison__, 'Signal probability of the reconstructed tag B candidates', 'Most around zero, with a tail at non-zero values.', 'Signal probability', 'Candidates', 'logy'), ('nDaug', 6, 0.0, 6, 'Number of daughters of tag B', __liaison__, 'Number of daughters of tag B', 'Some distribution of number of daughters', 'n_{daughters}', 'Candidates'), ('d0_massDiff', 100, 0.0, 0.5, 'Mass difference of D* and D', __liaison__, 'Mass difference of D^{*} and D', 'Peak at 0.14 GeV', 'm(D^{*})-m(D) [GeV]', 'Candidates', 'shifter'), ('d0_M', 100, 0.0, 3.0, 'Mass of zeroth daughter (D* or D)', __liaison__, 'Mass of zeroth daughter of tag B (either a $D^{*}$ or a D)', 'Peaks at 1.86 GeV and 2.00 GeV', 'm(D^{(*)}) [GeV]', 'Candidates', 'shifter'), ('deltaE', 40, -0.3, 0.3, '#Delta E', __liaison__, '$\\Delta E$ of event', 'Peak around zero', '#Delta E [GeV]', 'Candidates', 'shifter'), ('Mbc', 40, 5.2, 5.3, 'Mbc', __liaison__, 'Beam-constrained mass of event', 'Peaking around B mass (5.28 GeV)', 'M_{bc} [GeV]', 'Candidates', 'shifter')], variables_2d=[('deltaE', 100, -0.3, 0.3, 'Mbc', 100, 5.2, 5.3, 'Mbc vs deltaE', __liaison__, 'Plot of the $\\Delta E$ of the event against the beam constrained mass', 'Peak of $\\Delta E$ around zero, and $M_{bc}$ around B mass (5.28 GeV)', '#Delta E [GeV]', 'M_{bc} [GeV]', 'colz'), ('decayModeID', 26, 0, 26, 'log10_sigProb', 100, -3.0, 0.0, 'Signal probability for each decay mode ID', __liaison__, 'Signal probability for each decay mode ID', 'Some distribtuion of candidates in the first few decay mode IDs', 'Decay mode ID', '#log_10(signal probability)', 'colz')], path=path)
[docs]@_FEI_skim_header("B+") class feiHadronicBplus(BaseFEISkim): """ Tag side :math:`B` cuts: * :math:`M_{\\text{bc}} > 5.2~{\\rm GeV}` * :math:`|\\Delta E| < 0.3~{\\rm GeV}` * :math:`\\text{signal probability} > 0.001` (omitted for decay mode 25) All available FEI :math:`B^+` hadronic tags are reconstructed. <CHANNELS> See also: `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for event-level cuts made before applying the FEI. """ __description__ = "FEI-tagged charged :math:`B`'s decaying hadronically." validation_sample = _VALIDATION_SAMPLE FEIChannelArgs = { "neutralB": False, "chargedB": True, "hadronic": True, "semileptonic": False, "KLong": False, "baryonic": True }
[docs] def build_lists(self, path): ma.applyCuts("B+:generic", "Mbc>5.2", path=path) ma.applyCuts("B+:generic", "abs(deltaE)<0.300", path=path) ma.applyCuts("B+:generic", "sigProb>0.001 or extraInfo(dmID)==25", path=path) return ["B+:generic"]
[docs] def validation_histograms(self, path): # NOTE: the validation package is not part of the light releases, so this import # must be made here rather than at the top of the file. from validation_tools.metadata import create_validation_histograms vm.addAlias('sigProb', 'extraInfo(SignalProbability)') vm.addAlias('log10_sigProb', 'log10(extraInfo(SignalProbability))') vm.addAlias('d0_massDiff', 'daughter(0,massDifference(0))') vm.addAlias('d0_M', 'daughter(0,M)') vm.addAlias('decayModeID', 'extraInfo(decayModeID)') vm.addAlias('nDaug', 'countDaughters(1>0)') # Dummy cut so all daughters are selected. histogramFilename = f"{self}_Validation.root" create_validation_histograms( rootfile=histogramFilename, particlelist='B+:generic', variables_1d=[ ('sigProb', 100, 0.0, 1.0, 'Signal probability', __liaison__, 'Signal probability of the reconstructed tag B candidates', 'Most around zero, with a tail at non-zero values.', 'Signal probability', 'Candidates', 'logy'), ('nDaug', 6, 0.0, 6, 'Number of daughters of tag B', __liaison__, 'Number of daughters of tag B', 'Some distribution of number of daughters', 'n_{daughters}', 'Candidates'), ('d0_massDiff', 100, 0.0, 0.5, 'Mass difference of D* and D', __liaison__, 'Mass difference of D^{*} and D', 'Peak at 0.14 GeV', 'm(D^{*})-m(D) [GeV]', 'Candidates', 'shifter'), ('d0_M', 100, 0.0, 3.0, 'Mass of zeroth daughter (D* or D)', __liaison__, 'Mass of zeroth daughter of tag B (either a $D^{*}$ or a D)', 'Peaks at 1.86 GeV and 2.00 GeV', 'm(D^{(*)}) [GeV]', 'Candidates', 'shifter'), ('deltaE', 40, -0.3, 0.3, '#Delta E', __liaison__, '$\\Delta E$ of event', 'Peak around zero', '#Delta E [GeV]', 'Candidates', 'shifter'), ('Mbc', 40, 5.2, 5.3, 'Mbc', __liaison__, 'Beam-constrained mass of event', 'Peak around B mass (5.28 GeV)', 'M_{bc} [GeV]', 'Candidates', 'shifter')], variables_2d=[('deltaE', 100, -0.3, 0.3, 'Mbc', 100, 5.2, 5.3, 'Mbc vs deltaE', __liaison__, 'Plot of the $\\Delta E$ of the event against the beam constrained mass', 'Peak of $\\Delta E$ around zero, and $M_{bc}$ around B mass (5.28 GeV)', '#Delta E [GeV]', 'M_{bc} [GeV]', 'colz'), ('decayModeID', 29, 0, 29, 'log10_sigProb', 100, -3.0, 0.0, 'Signal probability for each decay mode ID', __liaison__, 'Signal probability for each decay mode ID', 'Some distribtuion of candidates in the first few decay mode IDs', 'Decay mode ID', '#log_10(signal probability)', 'colz')], path=path)
[docs]@_FEI_skim_header("B0") class feiSLB0(BaseFEISkim): """ Tag side :math:`B` cuts: * :math:`-4 < \\cos\\theta_{BY} < 3` * :math:`\\log_{10}(\\text{signal probability}) > -2.4` * :math:`p_{\\ell}^{*} > 1.0~{\\rm GeV}` in CMS frame SL :math:`B^0` tags are reconstructed. Hadronic :math:`B` with SL :math:`D` are not reconstructed, as these are rare and time-intensive. <CHANNELS> See also: `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for event-level cuts made before applying the FEI. """ __description__ = "FEI-tagged neutral :math:`B`'s decaying semileptonically." validation_sample = _VALIDATION_SAMPLE FEIChannelArgs = { "neutralB": True, "chargedB": False, "hadronic": False, "semileptonic": True, "KLong": False, "baryonic": True, "removeSLD": True }
[docs] def build_lists(self, path): ma.applyCuts("B0:semileptonic", "dmID<8", path=path) ma.applyCuts("B0:semileptonic", "log10(sigProb)>-2.4", path=path) ma.applyCuts("B0:semileptonic", "-4.0<cosThetaBY<3.0", path=path) ma.applyCuts("B0:semileptonic", "p_lepton_CMSframe>1.0", path=path) return ["B0:semileptonic"]
[docs] def validation_histograms(self, path): # NOTE: the validation package is not part of the light releases, so this import # must be made here rather than at the top of the file. from validation_tools.metadata import create_validation_histograms vm.addAlias('sigProb', 'extraInfo(SignalProbability)') vm.addAlias('log10_sigProb', 'log10(extraInfo(SignalProbability))') vm.addAlias('d0_massDiff', 'daughter(0,massDifference(0))') vm.addAlias('d0_M', 'daughter(0,M)') vm.addAlias('decayModeID', 'extraInfo(decayModeID)') vm.addAlias('nDaug', 'countDaughters(1>0)') # Dummy cut so all daughters are selected. histogramFilename = f"{self}_Validation.root" create_validation_histograms( rootfile=histogramFilename, particlelist='B0:semileptonic', variables_1d=[ ('sigProb', 100, 0.0, 1.0, 'Signal probability', __liaison__, 'Signal probability of the reconstructed tag B candidates', 'Most around zero, with a tail at non-zero values.', 'Signal probability', 'Candidates', 'logy'), ('nDaug', 6, 0.0, 6, 'Number of daughters of tag B', __liaison__, 'Number of daughters of tag B', 'Some distribution of number of daughters', 'n_{daughters}', 'Candidates'), ('cosThetaBetweenParticleAndNominalB', 100, -6.0, 4.0, '#cos#theta_{BY}', __liaison__, 'Cosine of angle between the reconstructed B and the nominal B', 'Distribution peaking between -1 and 1', '#cos#theta_{BY}', 'Candidates'), ('d0_massDiff', 100, 0.0, 0.5, 'Mass difference of D* and D', __liaison__, 'Mass difference of $D^{*}$ and D', 'Peak at 0.14 GeV', 'm(D^{*})-m(D) [GeV]', 'Candidates', 'shifter'), ('d0_M', 100, 0.0, 3.0, 'Mass of zeroth daughter (D* or D)', __liaison__, 'Mass of zeroth daughter of tag B (either a $D^{*}$ or a D)', 'Peaks at 1.86 GeV and 2.00 GeV', 'm(D^{(*)}) [GeV]', 'Candidates', 'shifter')], variables_2d=[('decayModeID', 8, 0, 8, 'log10_sigProb', 100, -3.0, 0.0, 'Signal probability for each decay mode ID', __liaison__, 'Signal probability for each decay mode ID', 'Some distribtuion of candidates in the first few decay mode IDs', 'Decay mode ID', '#log_10(signal probability)', 'colz')], path=path)
[docs]@_FEI_skim_header("B0") class feiSLB0_RDstar(BaseFEISkim): """ Tag side :math:`B` cuts: * :math:`\\text{FoxWolframR2} < 0.4` * :math:`-1.75 < \\cos\\theta_{BY} < 1.1` * :math:`\\log_{10}(\\text{signal probability}) > -2.0` * :math:`p_{\\ell}^{*} > 1.0~{\\rm GeV}` in CMS frame * :math:`\\text{BCS:signal probability}` SL :math:`B^0` tags are reconstructed. Hadronic :math:`B` with SL :math:`D` are not reconstructed, as these are rare and time-intensive. <CHANNELS> See also: `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for event-level cuts made before applying the FEI. """ __description__ = ("FEI-tagged neutral :math:`B`'s decaying semileptonically", "Analysis cuts included, best sigProb candidate kept" ) validation_sample = _VALIDATION_SAMPLE FEIChannelArgs = { "neutralB": True, "chargedB": False, "hadronic": False, "semileptonic": True, "KLong": False, "baryonic": True, "removeSLD": True }
[docs] def build_lists(self, path): CleanedTrackCuts = "abs(z0) < 2.0 and abs(d0) < 0.5 and pt > 0.1" CleanedClusterCuts = "E > 0.1 and thetaInCDCAcceptance" ma.fillParticleList(decayString="pi+:FEI_cleaned", cut=CleanedTrackCuts, path=path) ma.fillParticleList(decayString="gamma:FEI_cleaned", cut=CleanedClusterCuts, path=path) ma.buildEventKinematics(inputListNames=["pi+:FEI_cleaned", "gamma:FEI_cleaned"], path=path) ma.buildEventShape(inputListNames=['pi+:FEI_cleaned', 'gamma:FEI_cleaned'], allMoments=True, foxWolfram=True, harmonicMoments=True, cleoCones=True, thrust=True, collisionAxis=True, jets=True, sphericity=True, checkForDuplicates=True, path=path) # additional cut on Fox WR R2 vm.addAlias('foxWolframR2_maskedNaN', 'ifNANgiveX(foxWolframR2,1)') EventCuts = " and ".join( [ f"nCleanedTracks({CleanedTrackCuts})>=3", f"nCleanedECLClusters({CleanedClusterCuts})>=3", "visibleEnergyOfEventCMS>4", "foxWolframR2_maskedNaN<0.40", ] ) eselect = b2.register_module('VariableToReturnValue') eselect.param('variable', 'passesEventCut(' + EventCuts + ')') path.add_module(eselect) empty_path = b2.create_path() eselect.if_value('<1', empty_path) ma.copyList("B0:SLRDstar", "B0:semileptonic", path=path) ma.applyCuts("B0:SLRDstar", "dmID<8", path=path) # tightened cut on sigprob ma.applyCuts("B0:SLRDstar", "log10(sigProb)>-2.0", path=path) # tightened cut on cosThetaBY ma.applyCuts("B0:SLRDstar", "-1.75<cosThetaBY<1.1", path=path) ma.applyCuts("B0:SLRDstar", "p_lepton_CMSframe>1.0", path=path) # best candidate selection on signal probability ma.rankByHighest("B0:SLRDstar", "sigProb", numBest=1, allowMultiRank=True, outputVariable='sigProb_rank_tag', path=path) return ["B0:SLRDstar"]
[docs] def validation_histograms(self, path): # NOTE: the validation package is not part of the light releases, so this import # must be made here rather than at the top of the file. from validation_tools.metadata import create_validation_histograms vm.addAlias('sigProb', 'extraInfo(SignalProbability)') vm.addAlias('log10_sigProb', 'log10(extraInfo(SignalProbability))') vm.addAlias('d0_massDiff', 'daughter(0,massDifference(0))') vm.addAlias('d0_M', 'daughter(0,M)') vm.addAlias('decayModeID', 'extraInfo(decayModeID)') vm.addAlias('nDaug', 'countDaughters(1>0)') # Dummy cut so all daughters are selected. histogramFilename = f"{self}_Validation.root" create_validation_histograms( rootfile=histogramFilename, particlelist='B0:SLRDstar', variables_1d=[ ('sigProb', 100, 0.0, 1.0, 'Signal probability', __liaison__, 'Signal probability of the reconstructed tag B candidates', 'Most around zero, with a tail at non-zero values.', 'Signal probability', 'Candidates', 'logy'), ('nDaug', 6, 0.0, 6, 'Number of daughters of tag B', __liaison__, 'Number of daughters of tag B', 'Some distribution of number of daughters', 'n_{daughters}', 'Candidates'), ('cosThetaBetweenParticleAndNominalB', 100, -6.0, 4.0, '#cos#theta_{BY}', __liaison__, 'Cosine of angle between the reconstructed B and the nominal B', 'Distribution peaking between -1 and 1', '#cos#theta_{BY}', 'Candidates'), ('d0_massDiff', 100, 0.0, 0.5, 'Mass difference of D* and D', __liaison__, 'Mass difference of $D^{*}$ and D', 'Peak at 0.14 GeV', 'm(D^{*})-m(D) [GeV]', 'Candidates', 'shifter'), ('d0_M', 100, 0.0, 3.0, 'Mass of zeroth daughter (D* or D)', __liaison__, 'Mass of zeroth daughter of tag B (either a $D^{*}$ or a D)', 'Peaks at 1.86 GeV and 2.00 GeV', 'm(D^{(*)}) [GeV]', 'Candidates', 'shifter')], variables_2d=[('decayModeID', 8, 0, 8, 'log10_sigProb', 100, -3.0, 0.0, 'Signal probability for each decay mode ID', __liaison__, 'Signal probability for each decay mode ID', 'Some distribtuion of candidates in the first few decay mode IDs', 'Decay mode ID', '#log_10(signal probability)', 'colz')], path=path)
[docs]@_FEI_skim_header("B+") class feiSLBplus(BaseFEISkim): """ Tag side :math:`B` cuts: * :math:`-4 < \\cos\\theta_{BY} < 3` * :math:`\\log_{10}(\\text{signal probability}) > -2.4` * :math:`p_{\\ell}^{*} > 1.0~{\\rm GeV}` in CMS frame SL :math:`B^+` tags are reconstructed. Hadronic :math:`B^+` with SL :math:`D` are not reconstructed, as these are rare and time-intensive. <CHANNELS> See also: `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for event-level cuts made before applying the FEI. """ __description__ = "FEI-tagged charged :math:`B`'s decaying semileptonically." validation_sample = _VALIDATION_SAMPLE FEIChannelArgs = { "neutralB": False, "chargedB": True, "hadronic": False, "semileptonic": True, "KLong": False, "baryonic": True, "removeSLD": True }
[docs] def build_lists(self, path): ma.applyCuts("B+:semileptonic", "dmID<8", path=path) ma.applyCuts("B+:semileptonic", "log10_sigProb>-2.4", path=path) ma.applyCuts("B+:semileptonic", "-4.0<cosThetaBY<3.0", path=path) ma.applyCuts("B+:semileptonic", "p_lepton_CMSframe>1.0", path=path) return ["B+:semileptonic"]
[docs] def validation_histograms(self, path): # NOTE: the validation package is not part of the light releases, so this import # must be made here rather than at the top of the file. from validation_tools.metadata import create_validation_histograms vm.addAlias('sigProb', 'extraInfo(SignalProbability)') vm.addAlias('log10_sigProb', 'log10(extraInfo(SignalProbability))') vm.addAlias('d0_massDiff', 'daughter(0,massDifference(0))') vm.addAlias('d0_M', 'daughter(0,M)') vm.addAlias('decayModeID', 'extraInfo(decayModeID)') vm.addAlias('nDaug', 'countDaughters(1>0)') # Dummy cut so all daughters are selected. histogramFilename = f"{self}_Validation.root" create_validation_histograms( rootfile=histogramFilename, particlelist='B+:semileptonic', variables_1d=[ ('sigProb', 100, 0.0, 1.0, 'Signal probability', __liaison__, 'Signal probability of the reconstructed tag B candidates', 'Most around zero, with a tail at non-zero values.', 'Signal probability', 'Candidates', 'logy'), ('nDaug', 6, 0.0, 6, 'Number of daughters of tag B', __liaison__, 'Number of daughters of tag B', 'Some distribution of number of daughters', 'n_{daughters}', 'Candidates'), ('cosThetaBetweenParticleAndNominalB', 100, -6.0, 4.0, '#cos#theta_{BY}', __liaison__, 'Cosine of angle between the reconstructed B and the nominal B', 'Distribution peaking between -1 and 1', '#cos#theta_{BY}', 'Candidates'), ('d0_massDiff', 100, 0.0, 0.5, 'Mass difference of D* and D', __liaison__, 'Mass difference of $D^{*}$ and D', 'Peak at 0.14 GeV', 'm(D^{*})-m(D) [GeV]', 'Candidates', 'shifter'), ('d0_M', 100, 0.0, 3.0, 'Mass of zeroth daughter (D* or D)', __liaison__, 'Mass of zeroth daughter of tag B (either a $D^{*}$ or a D)', 'Peaks at 1.86 GeV and 2.00 GeV', 'm(D^{(*)}) [GeV]', 'Candidates', 'shifter')], variables_2d=[('decayModeID', 8, 0, 8, 'log10_sigProb', 100, -3.0, 0.0, 'Signal probability for each decay mode ID', __liaison__, 'Signal probability for each decay mode ID', 'Some distribtuion of candidates in the first few decay mode IDs', 'Decay mode ID', '#log_10(signal probability)', 'colz')], path=path)
[docs]@_FEI_skim_header(["B0", "B+"]) class feiHadronic(BaseFEISkim): """ Tag side :math:`B` cuts: * :math:`M_{\\text{bc}} > 5.2~{\\rm GeV}` * :math:`|\\Delta E| < 0.3~{\\rm GeV}` * :math:`\\text{signal probability} > 0.001` (omitted for decay mode 23 for :math:`B^+`, and decay mode 25 for :math:`B^0`) All available FEI :math:`B^0` and :math:`B^+` hadronic tags are reconstructed. From `Thomas Keck's thesis <https://docs.belle2.org/record/275/files/BELLE2-MTHESIS-2015-001.pdf>`_, "the channel :math:`B^0 \\to \\overline{D}^0 \\pi^0` was used by the FR, but is not yet used in the FEI due to unexpected technical restrictions in the KFitter algorithm". <CHANNELS> See also: `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for event-level cuts made before applying the FEI. """ __description__ = "FEI-tagged neutral and charged :math:`B`'s decaying hadronically." FEIChannelArgs = { "neutralB": True, "chargedB": True, "hadronic": True, "semileptonic": False, "KLong": False, "baryonic": True }
[docs] def build_lists(self, path): ma.copyList("B0:feiHadronic", "B0:generic", path=path) ma.copyList("B+:feiHadronic", "B+:generic", path=path) HadronicBLists = ["B0:feiHadronic", "B+:feiHadronic"] for BList in HadronicBLists: ma.applyCuts(BList, "Mbc>5.2", path=path) ma.applyCuts(BList, "abs(deltaE)<0.300", path=path) ma.applyCuts("B+:feiHadronic", "sigProb>0.001 or extraInfo(dmID)==25", path=path) ma.applyCuts("B0:feiHadronic", "sigProb>0.001 or extraInfo(dmID)==23", path=path) return HadronicBLists
[docs]@_FEI_skim_header(["B0", "B+"]) class feiHadronic_DstEllNu(BaseFEISkim): """ Tag side :math:`B` cuts: * :math:`M_{\\text{bc}} > 5.27~{\\rm GeV}` * :math:`-0.150 < \\Delta E < 0.100~{\\rm GeV}` * :math:`\\text{signal probability} > 0.001` (omitted for decay mode 23 for :math:`B^+`, and decay mode 25 for :math:`B^0`) * :math:`\\cos{TBTO} < 0.9` * Selects only the two best candidates that survive based on the signalProbability All available FEI :math:`B^0` and :math:`B^+` hadronic tags are reconstructed. From `Thomas Keck's thesis <https://docs.belle2.org/record/275/files/BELLE2-MTHESIS-2015-001.pdf>`_, "the channel :math:`B^0 \\to \\overline{D}^0 \\pi^0` was used by the FR, but is not yet used in the FEI due to unexpected technical restrictions in the KFitter algorithm". <CHANNELS> See also: `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for event-level cuts made before applying the FEI. """ __description__ = ("FEI-tagged neutral and charged :math:`B`'s decaying hadronically. " "Analysis specific cuts applied during skimming. Best 2 candidates ranked by sigProb kept" ) FEIChannelArgs = { "neutralB": True, "chargedB": True, "hadronic": True, "semileptonic": False, "KLong": False, "baryonic": True, }
[docs] def build_lists(self, path): ma.copyList("B0:feiHadronicDstEllNu", "B0:generic", path=path) ma.copyList("B+:feiHadronicDstEllNu", "B+:generic", path=path) HadronicBLists = ["B0:feiHadronicDstEllNu", "B+:feiHadronicDstEllNu"] ma.applyCuts( "B+:feiHadronicDstEllNu", "sigProb>0.001 or extraInfo(dmID)==25", path=path ) ma.applyCuts( "B0:feiHadronicDstEllNu", "sigProb>0.001 or extraInfo(dmID)==23", path=path ) for BList in HadronicBLists: ma.applyCuts(BList, "Mbc>5.27", path=path) ma.applyCuts(BList, "-0.150 <= deltaE <= 0.100", path=path) # Need to build Btag_ROE to build continuum suppression variables i.e. cosTBTO self._build_continuum_suppression(particle_list=BList, path=path) ma.applyCuts(BList, "cosTBTO < 0.9", path=path) # Keep only the best 2 candidates that survive based on the Signal probability. # The second candidate is kept just in case an analyst wishes to # perform some tag sipyde studies ma.rankByHighest( particleList=BList, variable="extraInfo(SignalProbability)", numBest=2, path=path, ) return HadronicBLists
def _build_continuum_suppression(self, particle_list: str, path: b2.Path): """Builds continuum suppression for a given b-meson list. This module is required to save the CS variables. Args:2 list_name (str) : name of the b meson list. Can be list of signal or tag b meson lists path (b2.Path): the basf2 path to append modules """ mask_name = "btag_cs" # First build the rest of the event for the b meson ma.buildRestOfEvent(target_list_name=particle_list, path=path) # Append the ROE mask ma.appendROEMasks( particle_list, [self._build_btag_roe_mask(mask_name=mask_name)], path=path, ) # Build the continuum object for the given b meson ma.buildContinuumSuppression( list_name=particle_list, roe_mask=mask_name, path=path ) @staticmethod def _build_btag_roe_mask(mask_name: str): """Prepares a tuple with the ROE mask name and the ROE cuts for tracks and clusters The actual cuts are read from a selections dictionary The cuts are aligning with the continuum rejection that is applied for the centrally produced FEI calibration Args: mask_name (str): The name of the mask. This is the name that will be used to append the mask Return: tuple of mask_name, tracking cuts and cluster_cuts """ selections = { "tracks": [ "abs(dr) < 2", "abs(dz) < 4", "pt > 0.2", "thetaInCDCAcceptance == 1", ], "clusters": [ "clusterNHits > 1.5", "[[clusterReg==1 and E > 0.080] or [clusterReg==2 and E > 0.030] or [clusterReg==3 and E > 0.060]]", "[[clusterTheta > 0.2967] and [clusterTheta < 2.6180]]", "abs(clusterTiming) < 200", ], } track_cuts = "".join(["[", " and ".join(selections["tracks"]), "]"]) cluster_cuts = "".join(["[", " and ".join(selections["clusters"]), "]"]) return (mask_name, track_cuts, cluster_cuts)
[docs]@_FEI_skim_header(["B0", "B+"]) class feiSL(BaseFEISkim): """ Tag side :math:`B` cuts: * :math:`-4 < \\cos\\theta_{BY} < 3` * :math:`\\log_{10}(\\text{signal probability}) > -2.4` * :math:`p_{\\ell}^{*} > 1.0~{\\rm GeV}` in CMS frame SL :math:`B^0` and :math:`B^+` tags are reconstructed. Hadronic :math:`B` with SL :math:`D` are not reconstructed, as these are rare and time-intensive. <CHANNELS> See also: `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for event-level cuts made before applying the FEI. """ __description__ = "FEI-tagged neutral and charged :math:`B`'s decaying semileptonically." FEIChannelArgs = { "neutralB": True, "chargedB": True, "hadronic": False, "semileptonic": True, "KLong": False, "baryonic": True, "removeSLD": True }
[docs] def build_lists(self, path): ma.copyList("B0:feiSL", "B0:semileptonic", path=path) ma.copyList("B+:feiSL", "B+:semileptonic", path=path) SLBLists = ["B0:feiSL", "B+:feiSL"] Bcuts = ["log10_sigProb>-2.4", "-4.0<cosThetaBY<3.0", "p_lepton_CMSframe>1.0"] for BList in SLBLists: for cut in Bcuts: ma.applyCuts(BList, cut, path=path) return SLBLists