Source code for variables.utils

##########################################################################
# 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.                  #
##########################################################################

import functools
import collections
import re
import variables
from typing import Iterable, Union, List, Tuple, Optional


[docs]def create_aliases(list_of_variables: Iterable[str], wrapper: str, prefix="") -> List[str]: """ The function creates aliases for variables from the variables list with given wrapper and returns list of the aliases. If the variables in the list have arguments (like ``useLabFrame(p)``) all non-alphanumeric characters in the variable will be replaced by underscores (for example ``useLabFrame_x``) for the alias name. >>> list_of_variables = ['M','p','matchedMC(useLabFrame(px))'] >>> wrapper = 'daughter(1,{variable})' >>> prefix = 'pref' >>> print(create_aliases(list_of_variables, wrapper, prefix)) ['pref_M', 'pref_p', 'pref_matchedMC_useLabFrame_px'] >>> from variables import variables >>> variables.printAliases() [INFO] ===================================== [INFO] The following aliases are registered: [INFO] pref_M --> daughter(1,M) [INFO] pref_matchedMC_useLabFrame_px --> daughter(1,matchedMC(useLabFrame(px))) [INFO] pref_p --> daughter(1,p) [INFO] ===================================== Parameters: list_of_variables (list(str)): list of variable names wrapper (str): metafunction taking variables from list_of_variables as a parameter \ (``<metafunction>(<some configs>, {variable} ,<some other configs>)`` prefix (str): alias prefix used for wrapped variables. Returns: list(str): new variables list """ replacement = re.compile('[^a-zA-Z0-9]+') aliases = [] for var in list_of_variables: # replace all non-safe characters for alias name with _ (but remove from the end) safe = replacement.sub("_", var).strip("_") aliases.append(f"{prefix}_{safe}" if prefix else f"{safe}") variables.variables.addAlias(aliases[-1], wrapper.format(variable=var)) return aliases
def get_hierarchy_of_decay(decay_string: str) -> List[List[Tuple[int, str]]]: """ This function returns paths of the particles selected in decay string. For each selected particle return a list of (index, name) tuples which indicate which daughter index to choose to arrive at the selected particle. For example for the decay string ``B+ -> [ D+ -> pi0 ^K+ ] pi0`` the resulting path for the K+ would be ``[(0, 'D'), (1, 'K')]``: The K is the second daughter of the first daughter of the B+ >>> get_hierarchy_of_decay('B+ -> [ D+ -> ^K+ pi0 ] pi0') [[(0, 'D'), (0, 'K')]] Every selected particle has its own path so if multiple particles are collected a list of paths is returned >>> get_hierarchy_of_decay('B+ -> [ D+ -> ^K+ pi0 ] ^pi0') [[(0, 'D'), (0, 'K')], [(1, 'pi0')]] If the mother particle is selected an empty list will be returned as its path >>> get_hierarchy_of_decay('^B+ -> ^pi+ pi-') [[], [(0, 'pi')] Parameters: decay_string (str): Decay string with selected particles Returns: list(list(tuple(int, str))): list of hierarchies of selected particles. """ from ROOT import Belle2 d = Belle2.DecayDescriptor() if not d.init(decay_string): raise ValueError("Invalid decay string") selected_particles = [] for path in d.getHierarchyOfSelected(): selected_particles.append([tuple(e) for e in path[1:]]) return selected_particles
[docs]def create_daughter_aliases( list_of_variables: Iterable[str], indices: Union[int, Iterable[int]], prefix="", include_indices=True ) -> List[str]: """Create Aliases for all variables for a given daughter hierarchy Arguments: list_of_variables (list(str)): list of variables to create aliases for indices (int or list(int)): index of the daughter, grand-daughter, grand-grand-daughter, and so forth prefix (str): optional prefix to prepend to the aliases include_indices(bool): if set to True (default) the aliases will contain the daughter indices as dX_dY_dZ... Returns: list(str): new variables list * create aliases for the second daughter as "d1_E", "d1_M" (daughters start at 0) >>> create_daughter_aliases(["E", "m"], 1) ['d1_E', 'd1_m'] >>> from variables import variables >>> variables.printAliases() [INFO] ========================= [INFO] Following aliases exists: [INFO] 'd1_E' --> 'daughter(1,E)' [INFO] 'd1_m' --> 'daughter(1,m)' [INFO] ========================= * create aliases for the first grand daughter of the second daughter, starting with "my" and without including the indices, resulting in "my_E", "my_m" >>> create_daughter_aliases(["E", "m"], [1, 0], prefix="my", include_indices=False) ['my_E', 'my_m'] >>> from variables import variables >>> variables.printAliases() [INFO] ========================= [INFO] Following aliases exists: [INFO] 'my_E' --> 'daughter(1,daughter(0,E))' [INFO] 'my_m' --> 'daughter(1,daughter(0,m))' [INFO] ========================= * create aliases for the second grand grand daughter of the third grand daughter of the fifth daughter, starting with my and including the indices, resulting in "my_d4_d2_d1_E", "my_d4_d2_d1_m" >>> create_daughter_aliases(["E", "m"], [4, 2, 1], prefix="my") ['my_d4_d2_d1_E', 'my_d4_d2_d1_m'] >>> from variables import variables >>> variables.printAliases() [INFO] ========================= [INFO] Following aliases exists: [INFO] 'my_d4_d2_d1_E' --> 'daughter(4,daughter(2,daughter(1,E))' [INFO] 'my_d4_d2_d1_m' --> 'daughter(4,daughter(2,daughter(1,m))' [INFO] ========================= """ if not isinstance(indices, collections.abc.Iterable): indices = [indices] if include_indices: prefix = functools.reduce(lambda x, y: f"{x}_d{y}", indices, prefix).lstrip("_") template = functools.reduce(lambda x, y: f"daughter({y},{x})", reversed(indices), "{variable}") return create_aliases(list_of_variables, template, prefix)
class DecayParticleNode: """ Class to present selected particles from a DecayString as tree structure. For each node of the tree we safe the name of the particle, whether it is selected and a dictionary of all children (as mapping decayIndex -> Node) """ def __init__(self, name): """Just set default values""" #: name of the particle self.name = name #: whether or not this particle is selected self.selected = False #: mapping of children decayIndex->Node self.children = {} def get_prefixes(self, always_include_indices=False, use_relative_indices=False): """ Recursively walk through the tree of selected particles and return a list of prefixes for aliases and a tuple of decay indexes for that prefix. For example for ``B0 -> [D0 -> ^pi+] ^pi0`` it might return >>> DecayParticleNode.build('^B0 -> [D0 -> ^pi+] ^pi0').get_prefixes() [ ("", None), ("D0_pi", (0, 0)), ("pi0", (1,)) ] and to create aliases from these one would use the indices as arguments for the b2:var:`daughter` meta variable. This function will make sure that prefix names are unique: If there are multiple siblings of one node with the same particle name they will be distinguished by either suffixing them with the decay index (if ``use_relative_indices=False``) or they will just be enumerated starting at 0 otherwise. Arguments: always_include_indices (bool): If True always add the index of the particle to the prefix, otherwise the index is only added if more than one sibling of the same particle exist. use_relative_indices (bool): If True the indices used will **not** be the daughter indices in the full decay string but just the relative indices: If multiple sibling particles with the same name they will be just numbered starting at zero as they appear in the aliases. """ return self.__walk(always_include_indices, use_relative_indices, "", tuple()) def __walk(self, always_include_indices, use_relative_indices, current_prefix, current_path): """Recursively walk the tree and collect all prefixes See: `get_prefixes` Arguments: always_include_indices (bool): see `get_prefixes()` use_relative_indices (bool): see `get_prefixes()` current_prefix: the current prefix so far collected from any parent particle. current_path: the current path of indices so far collected from any parent particle. """ result = [] # are we the mother particle and selected selected? if so, add a "no-prefix" to the output if not current_path and self.selected: result.append(("", None)) # count the particle names of all daughters so that we know which ones we # have to index names = collections.Counter(e.name for e in self.children.values()) # if we use relative indices start counting them at zero relative_indices = collections.defaultdict(int) # now loop over all children for index, c in sorted(self.children.items()): # prepare the full index path full_path = current_path + (index,) # and prepare the prefix prefix = current_prefix + c.name # is this particle name ambiguous or are all indices requested? add index if always_include_indices or names[c.name] > 1: prefix += f"_{relative_indices[c.name] if use_relative_indices else index}" # always increase the relative indices relative_indices[c.name] += 1 # if the particle is selected add the prefix and the path if c.selected: result.append((prefix, full_path)) # but in any case also process all children recursively result += c.__walk(always_include_indices, use_relative_indices, prefix + "_", full_path) # done, return all prefixes and their paths return result @classmethod def build(cls, decay_string): """Build a tree of selected particles from a `DecayString` This will return a `DecayParticleNode` instance which is the top of a tree of all the selected particles from the decat string. Arguments: decay_string (str): `DecayString` containing at least one selected particle """ selected = get_hierarchy_of_decay(decay_string) if not selected: raise ValueError("No particle selected in decay string") # create the top of the tree top = cls("") # now loop over all selected particles for path in selected: current = top # and walk through the path for index, name in path: # creating tree children as needed if index not in current.children: current.children[index] = cls(name) # and update the pointer current = current.children[index] # after walking the tree the pointer is at the selected particle so # just set the selected to True current.selected = True # done, return the tree return top
[docs]def create_aliases_for_selected( list_of_variables: List[str], decay_string: str, prefix: Optional[Union[str, List[str]]] = None, *, use_names=True, always_include_indices=False, use_relative_indices=False ) -> List[str]: """ The function creates list of aliases for given variables so that they are calculated for particles selected in decay string. That is for each particle selected in the decay string an alias is created to calculate each variable in the ``list_of_variables``. If ``use_names=True`` (the default) then the names of the aliases are assigned as follows: * If names are unambiguous, it's semi-laconic :doc:`DecayString` style: The aliases will be prefixed with the names of all parent particle names separated by underscore. For example given the decay string ``B0 -> [D0 -> ^pi+ K-] pi0`` the aliases for the ``pi+` will start with ``D0_pi_`` followed by the variable name. >>> list_of_variables = ['M','p'] >>> decay_string = 'B0 -> [D0 -> ^pi+ K-] pi0' >>> create_aliases_for_selected(list_of_variables, decay_string) ['D0_pi_M', 'D0_pi_p'] >>> from variables import variables >>> variables.printAliases() [INFO] ========================= [INFO] Following aliases exists: [INFO] 'D0_pi_M' --> 'daughter(0,daughter(0,M))' [INFO] 'D0_pi_p' --> 'daughter(0,daughter(0,p))' [INFO] ========================= * If names are ambiguous because there are multiple daughters with the same name these particles will be followed by their daughter index. For example given the decay string ``B0 -> [D0 -> ^pi+:1 ^pi-:2 ^pi0:3 ] ^pi0:4`` will create aliases with the following prefixes for particle with the corresponding number as list name: 1. ``D0_pi_0_`` 2. ``D0_pi_1_`` 3. ``D0_pi0_`` 4. ``pi0_`` >>> list_of_variables = ['M','p'] >>> decay_string = 'B0 -> [D0 -> ^pi+ ^pi- ^pi0] ^pi0' >>> create_aliases_for_selected(list_of_variables, decay_string) ['D0_pi_0_M', 'D0_pi_0_p', 'D0_pi_1_M', 'D0_pi_1_p', 'D0_pi0_M', 'D0_pi0_p', 'pi0_M', 'pi0_p'] >>> from variables import variables >>> variables.printAliases() [INFO] ========================= [INFO] Following aliases exists: [INFO] 'D0_pi0_M' --> 'daughter(0,daughter(2,M))' [INFO] 'D0_pi0_p' --> 'daughter(0,daughter(2,p))' [INFO] 'D0_pi_0_M' --> 'daughter(0,daughter(0,M))' [INFO] 'D0_pi_0_p' --> 'daughter(0,daughter(0,p))' [INFO] 'D0_pi_1_M' --> 'daughter(0,daughter(1,M))' [INFO] 'D0_pi_1_p' --> 'daughter(0,daughter(1,p))' [INFO] 'D0_pi_M' --> 'daughter(0,daughter(0,M))' [INFO] 'D0_pi_p' --> 'daughter(0,daughter(0,p))' [INFO] 'pi0_M' --> 'daughter(1,M)' [INFO] 'pi0_p' --> 'daughter(1,p)' [INFO] ========================= * The user can select to always include the index even for unambiguous particles by passing ``always_include_indices=True`` * The user can choose two different numbering schemes: If ``use_relative_indices=False`` the original decay string indices will be used if a index is added to a particle name. But if ``use_relative_indices=True`` the indices will just start at zero for each particle which is part of the prefixes. For example for ``B0-> e+ ^e-`` >>> create_aliases_for_selected(['M'], 'B0-> mu+ e- ^e+ ^e-', use_relative_indices=False) ['e_2_M', 'e_3_M'] >>> create_aliases_for_selected(['M'], 'B0-> mu+ e- ^e+ ^e-', use_relative_indices=True) ['e_0_M', 'e_1_M'] If ``use_names=False`` the aliases will just start with the daughter indices of all parent particles prefixed with a ``d`` and separated by underscore. So for the previous example ``B0 -> [D0 -> ^pi+:1 ^pi-:2 ^pi0:3 ] ^pi0:4`` this would result in aliases starting with 1. ``d0_d0_`` 2. ``d0_d1_`` 3. ``d0_d2_`` 4. ``d1_`` In this case the ``always_include_indices`` and ``use_relative_indices`` arguments are ignored. The naming can be modified by providing a custom prefix for each selected particle. In this case the parameter ``prefix`` needs to be either a simple string if only one particle is selected or a list of strings with one prefix for each selected particle. >>> list_of_variables = ['M','p'] >>> decay_string = 'B0 -> [D0 -> ^pi+ ^pi- pi0] pi0' >>> create_aliases_for_selected(list_of_variables, decay_string, prefix=['pip', 'pim']) ['pip_M', 'pip_p', 'pim_M', 'pim_p'] >>> from variables import variables >>> variables.printAliases() [INFO] ========================= [INFO] Following aliases exists: [INFO] 'pim_M' --> 'daughter(0,daughter(1,M))' [INFO] 'pim_p' --> 'daughter(0,daughter(1,p))' [INFO] 'pip_M' --> 'daughter(0,daughter(0,M))' [INFO] 'pip_p' --> 'daughter(0,daughter(0,p))' [INFO] ========================= If the mother particle itself is selected the input list of variables will also be added to the returned list of created aliases. If custom prefixes are supplied then aliases will be created for the mother particle as well: >>> create_aliases_for_selected(['M', 'p'], '^B0 -> pi+ ^pi-') ['M', 'p', 'pi_M', 'pi_p'] >>> create_aliases_for_selected(['M', 'p'], '^B0 -> pi+ ^pi-', prefix=['MyB', 'MyPi']) ['MyB_M', 'MyB_p', 'MyPi_M', 'MyPi_p'] Parameters: list_of_variables (list(str)): list of variable names decay_string (str): Decay string with selected particles prefix (str, list(str)): Custom prefix for all selected particles use_names (bool): Include the names of the particles in the aliases always_include_indices (bool): If ``use_names=True`` include the decay index of the particles in the alias name even if the particle could be uniquely identified without them. use_relative_indices (bool): If ``use_names=True`` use a relative indicing which always starts at 0 for each particle appearing in the alias names independent of the absolute position in the decay string Returns: list(str): new variables list """ selected_particles = DecayParticleNode.build(decay_string) prefixes = selected_particles.get_prefixes(always_include_indices, use_relative_indices) # transpose -> convert [(prefix, path), (prefix, path) ...] into # (prefix, prefix...), (path, path, ...) prefixes, paths = zip(*prefixes) alias_list = [] # in most cases we don't want to add daughter indices `dM_dN` to the # aliases include_indices = False # but we might have custom prefixes if prefix is not None: # check custom prefix to be a list if isinstance(prefix, str): prefix = [prefix] # and make sure we have the correct amount if len(prefix) != len(prefixes): raise ValueError("Number of selected particles does not match number of supplied custom prefixes") # final check: make sure we don't have duplicate prefixes in here prefix_counts = collections.Counter(prefix) if max(prefix_counts.values()) > 1: raise ValueError("Prefixes need to be unique") # ok, just override calculated prefixes prefixes = prefix elif not use_names: # otherwise, if we don't use names we just override the prefixes # containing the names to be empty and make sure we include the # daughter indices prefixes = [""] * len(prefixes) include_indices = True for prefix, path in zip(prefixes, paths): if path is None: # mother particle selected, just create custom aliases which # don't do anything special if the prefix is not empty if prefix: alias_list += create_aliases(list_of_variables, "{variable}", prefix) else: # but if prefix is empty just return the variables alias_list += list_of_variables else: # mother particle selected, just create custom aliases alias_list += create_daughter_aliases(list_of_variables, path, prefix, include_indices) return alias_list
[docs]def create_mctruth_aliases( list_of_variables: Iterable[str], prefix="mc" ) -> List[str]: """ The function wraps variables from the list with 'matchedMC()'. >>> list_of_variables = ['M','p'] >>> create_mctruth_aliases(list_of_variables) ['mc_M', 'mc_p'] >>> from variables import variables >>> variables.printAliases() [INFO] ========================= [INFO] Following aliases exists: [INFO] 'mc_M' --> 'matchedMC(M)' [INFO] 'mc_p' --> 'matchedMC(p)' [INFO] ========================= Parameters: list_of_variables (list(str)): list of variable names Returns: list(str): list of created aliases """ return create_aliases(list_of_variables, 'matchedMC({variable})', prefix)
[docs]def add_collection(list_of_variables: Iterable[str], collection_name: str) -> str: """ The function creates variable collection from the given list of variables It wraps the `VariableManager.addCollection` method which is not particularly user-friendly. Example: Defining the collection >>> variables.utils.add_collection(['p','E'], "my_collection") Passing it as an argument to variablesToNtuple >>> modularAnalysis.variablesToNtuple(variables=['my_collection'], ...) Parameters: list_of_variables (list(str)): list of variable names collection_name (str): name of the collection Returns: str: name of the variable collection """ variables.variables.addCollection(collection_name, variables.std_vector(*tuple(list_of_variables))) return collection_name
[docs]def create_isSignal_alias(aliasName, flags): """ Make a `VariableManager` alias for a customized :b2:var:`isSignal`, which accepts specified mc match errors. .. seealso:: see :doc:`MCMatching` for a definition of the mc match error flags. The following code defines a new variable ``isSignalAcceptMissingGammaAndMissingNeutrino``, which is same as :b2:var:`isSignal`, but also accepts missing gamma and missing neutrino >>> create_isSignal_alias("isSignalAcceptMissingGammaAndMissingNeutrino", [16, 8]) Logically, this ``isSignalAcceptMissingGammaAndMissingNeutrino`` = :b2:var:`isSignalAcceptMissingGamma` || :b2:var:`isSignalAcceptMissingNeutrino`. In the example above, create_isSignal_alias() creates ``isSignalAcceptMissingGammaAndMissingNeutrino`` by unmasking (setting bits to zero) the ``c_MissGamma`` bit (16 or 0b00010000) and ``c_MissNeutrino`` bit (8 or 0b00001000) in mcErrors. For more information, please check this `example script <https://gitlab.desy.de/belle2/software/basf2/-/tree/main/analysis/examples/VariableManager/isSignalAcceptFlags.py>`_. Parameters: aliasName (str): the name of the alias to be set flags (list(int)): a list of the bits to unmask """ mask = 0 for flag in flags: if isinstance(flag, int): mask |= flag else: informationString = "The type of input flags of create_isSignal_alias() should be integer." informationString += "Now one of the input flags is " + str(int) + " ." raise ValueError(informationString) variables.variables.addAlias(aliasName, f"passesCut(unmask(mcErrors, {int(mask)}) == {int(0)})")