Tupling and DecayTreeFitter with Neutrino

This example demonstrates how to use FunTuple and DecayTreeFitter with a neutrino.

The decay tree involving a neutrino is simplified as ReferenceSystem -> (MotherParticle -> VisibleSystem neutrino) IndependentSystem. Four ntuples are created, each corresponding to a different neutrino strategy: Strategy 0 assumes neutrino is parallel to visible system, forcing the mass constraint of the mother particle. Strategy 1 estimates the flight direction of reference particle using endvertex and primary vertex position, applying the constraint p_perp_visible + p_perp_neutrino + p_perp_independent = 0. Strategy 2 estimates the flight direction of reference particle using endvertex and primary vertex position, calculating the transversal momentum of mother particle from independent system (pt(independent) = - pt(mother)), assuming neutrino has zero contribution in transversal momentum. Strategy 3 determines the direction of flight of the Mother by the end vertex position of the visible system and by the end vertex position of the reference system, then like strategy 2 calculates the transversal momentum of mother particle from independent system.


from PyConf.reading import get_particles
import Functors as F
from FunTuple import FunctorCollection, FunTuple_Particles as Funtuple
from DaVinci.algorithms import create_lines_filter
from DaVinci import Options, make_config
import FunTuple.functorcollections as FC
from DaVinciMCTools import MCTruthAndBkgCat
from PyConf.Algorithms import InsertNeutrinoToDecayAlg
from DecayTreeFitter import DecayTreeFitter
from functools import partial


def chain_functors(fc, chained_functor, prefix=""):
    return FunctorCollection(
        {f"{prefix}{k}": v @ chained_functor for k, v in fc.functor_dict.items()}
    )


def truth_match_functors(truth_match):
    output = FC.MCKinematics(mctruth_alg=truth_match)
    output += FunctorCollection(
        {
            "BKGCAT": truth_match.BkgCat,
            "TRUE": truth_match(F.HAS_VALUE),
        }
    )
    return output


def neutrino_truth_matching(truth_match):

    fetch_nu = F.FIND_MCDECAY("[ tau+ -x> pi+ pi- pi+ ^nu_tau~ ]CC")

    return FunctorCollection(
        {
            "Nu_TRUEP": truth_match(F.FOURMOMENTUM @ fetch_nu),
        }
    )


def main(options: Options, isMC=True):

    # Basics
    line_name = "Hlt2RD_BdToKstTauMu_KstToKPi_TauTo3Pi_OS"
    input_particles = get_particles(f"/Event/HLT2/{line_name}/Particles")
    hlt2_filter = create_lines_filter(name="HLT2Filter", lines=[line_name])
    truth_match = MCTruthAndBkgCat(input_particles, name="truth_match")

    # Define nTuple fields
    fields = {
        "B": "[B0 -> ([J/psi(1S) -> (tau+ -> pi+ pi- pi+ nu_tau~) mu-]CC) (K*(892)0 -> K+ pi-)]CC",
        "B_Jpsi": "[B0 -> ^([J/psi(1S) -> (tau+ -> pi+ pi- pi+ nu_tau~) mu-]CC) (K*(892)0 -> K+ pi-)]CC",
        "B_taup": "[B0 -> ([J/psi(1S) -> ^(tau+ -> pi+ pi- pi+ nu_tau~) mu-]CC) (K*(892)0 -> K+ pi-)]CC",
        "B_mum": "[B0 -> ([J/psi(1S) -> (tau+ -> pi+ pi- pi+ nu_tau~) ^mu-]CC) (K*(892)0 -> K+ pi-)]CC",
        "B_Kst": "[B0 -> ([J/psi(1S) -> (tau+ -> pi+ pi- pi+ nu_tau~) mu-]CC) ^(K*(892)0 -> K+ pi-)]CC",
        "B_Kp": "[B0 -> ([J/psi(1S) -> (tau+ -> pi+ pi- pi+ nu_tau~) mu-]CC) (K*(892)0 -> ^K+ pi-)]CC",
        "B_Pim": "[B0 -> ([J/psi(1S) -> (tau+ -> pi+ pi- pi+ nu_tau~) mu-]CC) (K*(892)0 -> K+ ^pi-)]CC",
        "B_Pi1": "[B0 -> ([J/psi(1S) -> (tau+ -> ^pi+ pi- pi+ nu_tau~) mu-]CC) (K*(892)0 -> K+ pi-)]CC",
        "B_Pi2": "[B0 -> ([J/psi(1S) -> (tau+ -> pi+ ^pi- pi+ nu_tau~) mu-]CC) (K*(892)0 -> K+ pi-)]CC",
        "B_Pi3": "[B0 -> ([J/psi(1S) -> (tau+ -> pi+ pi- ^pi+ nu_tau~) mu-]CC) (K*(892)0 -> K+ pi-)]CC",
        "B_Nu": "[B0 -> ([J/psi(1S) -> (tau+ -> pi+ pi- pi+ ^nu_tau~) mu-]CC) (K*(892)0 -> K+ pi-)]CC",
    }

    # Define neutrino algorithm
    insert_neutrinos = partial(
        InsertNeutrinoToDecayAlg,
        InputParticles=input_particles,
        SelectedDecays=[
            "[B0 -> ([J/psi(1S) -> (tau+ -> pi+ pi- pi+) mu-]CC) (K*(892)0 -> K+ pi-)]CC"
        ],
        ParticleInsertionRules={
            "B0 -> (J/psi(1S) -> ^(tau+ -> pi+ pi- pi+) mu-) (K*(892)0 -> K+ pi-)": "nu_tau~",
            "B0 -> (J/psi(1S) -> ^(tau- -> pi- pi+ pi-) mu+) (K*(892)0 -> K+ pi-)": "nu_tau",
            "B~0 -> (J/psi(1S) -> ^(tau- -> pi- pi+ pi-) mu+) (K*(892)~0 -> K- pi+)": "nu_tau",
            "B~0 -> (J/psi(1S) -> ^(tau+ -> pi+ pi- pi+) mu-) (K*(892)~0 -> K- pi+)": "nu_tau~",
        },
        VisibleSystem=[
            "[B0 -> ([J/psi(1S) -> (tau+ -> ^pi+ pi- pi+ nu_tau~) mu-]CC) (K*(892)0 -> K+ pi-)]CC",
            "[B0 -> ([J/psi(1S) -> (tau+ -> pi+ ^pi- pi+ nu_tau~) mu-]CC) (K*(892)0 -> K+ pi-)]CC",
            "[B0 -> ([J/psi(1S) -> (tau+ -> pi+ pi- ^pi+ nu_tau~) mu-]CC) (K*(892)0 -> K+ pi-)]CC",
        ],
        IndependentSystem=[
            "[B0 -> ([J/psi(1S) -> (tau+ -> pi+ pi- pi+ nu_tau~) ^mu-]CC) (K*(892)0 -> K+ pi-)]CC",
            "[B0 -> ([J/psi(1S) -> (tau+ -> pi+ pi- pi+ nu_tau~) mu-]CC) (K*(892)0 -> ^K+ pi-)]CC",
            "[B0 -> ([J/psi(1S) -> (tau+ -> pi+ pi- pi+ nu_tau~) mu-]CC) (K*(892)0 -> K+ ^pi-)]CC",
        ],
        ReferenceParticle="[B0 -> ([J/psi(1S) -> (tau+ -> pi+ pi- pi+ nu_tau~) mu-]CC) (K*(892)0 -> K+ pi-)]CC",
        MotherParticle="[B0 -> ([J/psi(1S) -> ^(tau+ -> pi+ pi- pi+ nu_tau~) mu-]CC) (K*(892)0 -> K+ pi-)]CC",
        RelativeError=0.1,
        UpdateMomentum=True,
    )

    # Define FunTuple nodes
    davinci_node = {}
    for neutrino_strategy in [0, 1, 2, 3]:

        print(f"Adding FunTuple with neutrino strategy {neutrino_strategy}.")

        # Create neutrino
        neutrino = insert_neutrinos(Strategy=neutrino_strategy)
        relate_to_original_particle = F.MAP_TO_RELATED(neutrino.Relation)

        # Fit neutrino with DTF
        DTF = DecayTreeFitter(
            name=f"DTF_{neutrino_strategy}",
            constrain_to_ownpv=True,
            input_particles=neutrino.OutputDecays,
            mass_constraints=["tau+"],
        )

        # Define functors
        variables_all = FunctorCollection()
        variables_B = FunctorCollection()
        variables_tau = FunctorCollection()

        # Kinematics functors
        variables_all += FC.Kinematics()
        variables_all += chain_functors(
            FC.Kinematics(), relate_to_original_particle, prefix="ORIGINAL_"
        )

        # Truth matching functors
        variables_all += chain_functors(
            truth_match_functors(truth_match), relate_to_original_particle
        )
        variables_tau += chain_functors(
            neutrino_truth_matching(truth_match), relate_to_original_particle
        )

        # DTF functors
        variables_all += FC.DecayTreeFitterResults(
            DTF=DTF,
            decay_origin=False,
            with_lifetime=False,
            with_kinematics=True,
        )
        variables_B += FC.DecayTreeFitterResults(
            DTF=DTF,
            decay_origin=True,
            with_lifetime=True,
            with_kinematics=False,
        )

        # Assign functors
        variables = {
            "ALL": variables_all,
            "B": variables_B,
            "B_taup": variables_tau,
        }

        # Create funtuple
        funtuple = Funtuple(
            name=f"Strategy{neutrino_strategy}",
            tuple_name="DecayTree",
            fields=fields,
            variables=variables,
            inputs=neutrino.OutputDecays,
        )

        # Create FunTuple node
        davinci_node.update({f"Tupling_{neutrino_strategy}": [hlt2_filter, funtuple]})

    return make_config(options, davinci_node)

To run the example:

lbexec DaVinciExamples.tupling.option_davinci_tupling_neutrino:main $DAVINCIEXAMPLESROOT/example_data/test_neutrino.yaml

For reference, these are the options of this example

testfiledb_key: DaVinciTestNeutrino_2024_B2JpsiKst_Jpsi2TauMu_Tau2pipipinu
input_raw_format: 0.5
simulation: true
input_process: Hlt2
input_stream: full
lumi: false
evt_max: 1000
print_freq: 1000
ntuple_file: test_neutrino_ntuple.root
histo_file: test_neutrino_histo.root