9import scipy.interpolate
12def addHNLtoROOT(pid: int = 9900015, m: float = 1.0, g: float = 3.654203020370371e-21) ->
None:
13 pdg = ROOT.TDatabasePDG.Instance()
14 pdg.AddParticle(
"N2",
"HNL", m,
False, g, 0.0,
"N2", pid)
17def getbr_rpvsusy(h, histoname, mass, coupling):
19 normalized_br = h[histoname](mass)
20 br = normalized_br * coupling
26def getmaxsumbrrpvsusy(h, histograms, mass, couplings):
30 for histoname
in histograms:
31 item = histoname.split(
"_")
34 coupling = couplings[1]
36 sumbrs[meson] += getbr_rpvsusy(h, histoname, mass, coupling)
38 sumbrs[meson] = getbr_rpvsusy(h, histoname, mass, coupling)
39 print(list(sumbrs.values()))
40 maxsumbr = max(sumbrs.values())
44def gettotalbrrpvsusy(h, histograms, mass, couplings) -> float:
46 for histoname
in histograms:
48 coupling = couplings[1]
49 totalbr += getbr_rpvsusy(h, histoname, mass, coupling)
53def make_particles_stable(P8gen, above_lifetime) -> None:
56 Make the particles with a lifetime above the specified one stable, to allow
57 them to decay
in Geant4 instead.
59 p8 = P8gen.getPythiaInstance()
62 n = p8.particleData.nextId(n)
63 p = p8.particleData.particleDataEntryPtr(n)
64 if p.tau0() > above_lifetime:
65 command = f
"{n}:mayDecay = false"
66 p8.readString(command)
67 print(f
"Pythia8 configuration: Made {p.name()} stable for Pythia, should decay in Geant4")
72 This function parses a file containing histograms of branching ratios.
74 It places them in a dictionary indexed by the decay string (e.g.
'd_K0_e'),
75 as a pair ([masses...], [branching ratios...]), where the mass
is expressed
78 with open(filepath)
as f:
81 th1f_exp = re.compile(
r"^TH1F\|.+")
82 header_exp = re.compile(
r"^TH1F\|(.+?)\|B(?:R|F)/U2(.+?)\|.+? mass \(GeV\)\|?")
83 subheader_exp = re.compile(
r"^\s*?(\d+?),\s*(\d+?\.\d+?),\s*(\d+\.\d+)\s*$")
84 data_exp = re.compile(
r"^\s*(\d+?)\s*,\s*(\d+\.\d+)\s*$")
86 header_line_idx = [i
for i
in range(len(lines))
if th1f_exp.match(lines[i])
is not None]
89 for offset
in header_line_idx:
91 mh = header_exp.match(lines[offset])
92 if mh
is None or len(mh.groups()) != 2:
93 raise ValueError(f
"Malformed header encountered: {lines[offset]}")
94 decay_code = mh.group(1)
96 ms = subheader_exp.match(lines[offset + 1])
97 if ms
is None or len(ms.groups()) != 3:
98 raise ValueError(f
"Malformed sub-header encountered: {lines[offset + 1]}")
99 npoints = int(ms.group(1))
100 min_mass = float(ms.group(2))
101 max_mass = float(ms.group(3))
102 masses = np.linspace(min_mass, max_mass, npoints, endpoint=
False)
103 branching_ratios = np.zeros(npoints)
105 for line
in lines[offset + 2 : offset + npoints + 1]:
106 md = data_exp.match(line)
107 if md
is None or len(md.groups()) != 2:
108 raise ValueError(f
"Malformed data row encountered: {line}")
109 idx = int(md.group(1))
110 br = float(md.group(2))
111 branching_ratios[idx] = br
112 histograms[decay_code] = (masses, branching_ratios)
116def make_interpolators(filepath, kind: str =
"linear"):
118 This function reads a file containing branching ratio histograms, and
119 returns a dictionary of interpolators of the branching ratios, indexed by
124 for hist_string, (masses, br)
in histogram_data.items():
125 histograms[hist_string] = scipy.interpolate.interp1d(
126 masses, br, kind=kind, bounds_error=
False, fill_value=0, assume_sorted=
True
131def get_br(histograms, channel, mass, couplings):
133 Utility function used to reliably query the branching ratio for a given
134 channel at a given mass, taking into account the correct coupling.
136 hist = histograms[channel["decay"]]
137 coupling = couplings[channel[
"coupling"]]
138 normalized_br = hist(mass)
139 return normalized_br * coupling
142def add_particles(P8gen, particles, data) -> None:
144 Adds the corresponding particles to PYTHIA.
146 `particles` must be a list containing either the particles PDG IDs, or
147 their PYTHIA names. The commands needed to add the particles are queried
150 If the particle
is not self-conjugate, the antiparticle
is automatically
153 for particle_id
in particles:
155 particle = next((p
for p
in data[
"particles"]
if particle_id
in [p[
"id"], p[
"name"]]),
None)
157 raise ValueError(f
"Could not find particle ID {particle_id} in particle database")
159 P8gen.SetParameters(particle[
"cmd"])
162def add_channel(P8gen, ch, histograms, mass, couplings, scale_factor) -> None:
163 "Add to PYTHIA a leptonic or semileptonic decay channel to HNL."
165 br = get_br(histograms, ch, mass, couplings)
170 "{}:addChannel 1 {:.17} 22 {} 9900015 {}".format(
171 ch[
"id"], br * scale_factor, ch[
"idlepton"], ch[
"idhadron"]
176 "{}:addChannel 1 {:.17} 0 9900015 {}".format(
177 ch[
"id"], br * scale_factor, ch[
"idlepton"]
181 raise ValueError(f
"Missing key 'idlepton' in channel {ch}")
184def add_tau_channel(P8gen, ch, histograms, mass, couplings, scale_factor) -> None:
185 "Add to PYTHIA a tau decay channel to HNL."
187 br = get_br(histograms, ch, mass, couplings)
192 "{}:addChannel 1 {:.16} 1531 9900015 {} {}".format(
193 ch[
"id"], br * scale_factor, ch[
"idlepton"], ch[
"idhadron"]
198 "{}:addChannel 1 {:.16} 1521 9900015 {}".format(
199 ch[
"id"], br * scale_factor, ch[
"idhadron"]
203 raise ValueError(f
"Missing key 'idhadron' in channel {ch}")
206def fill_missing_channels(P8gen, max_total_br, decay_chains, epsilon: float = 1e-6) ->
None:
208 Add dummy channels for correct rejection sampling.
210 Even after rescaling the branching ratios, they do
not sum up to unity
211 for most particles since we are ignoring SM processes.
213 This function adds a
"filler" channel
for each particle,
in order to
214 preserve the ratios between different branching ratios.
217 for particle
in top_level_particles:
219 remainder = 1 - my_total_br / max_total_br
220 assert remainder > -epsilon
221 assert remainder < 1 + epsilon
222 if remainder > epsilon:
228 Add a dummy channel to PYTHIA, with branching ratio equal to `remainder.`
230 The purpose of this function
is to compensate
for the absence of SM
231 channels, which are ignored when investigating rare processes. A dummy
232 decay channel
is instead added to each particle
in order to maintain the
233 correct ratios between the branching ratios of each particle to rare
234 processes. This
is usually combined
with a
global reweighting of the
237 In order to keep PYTHIA
from complaining about charge conservation, a
238 suitable process
is added which conserves electric charge.
240 All dummy channels can be identified by the presence of a photon among the
243 pdg = P8gen.getPythiaInstance().particleData
244 charge = pdg.charge(particle)
246 P8gen.SetParameters(f
"{particle}:addChannel 1 {remainder:.16} 0 22 -11")
248 P8gen.SetParameters(f
"{particle}:addChannel 1 {remainder:.16} 0 22 11")
250 P8gen.SetParameters(f
"{particle}:addChannel 1 {remainder:.16} 0 22 22")
253def compute_max_total_br(decay_chains) -> int:
255 This function computes the maximum total branching ratio for all decay chains.
257 In order to make the event generation
as efficient
as possible when
258 studying very rare processes, it
is necessary to rescale the branching
259 ratios,
while enforcing the invariant that any total branching ratio must
260 remain lower that unity.
262 This
is accomplished by computing,
for each particle, the total branching
263 ratio to processes of interest,
and then dividing all branching ratios by
264 the highest of those.
266 Note: the decay chain length must be at most 2.
270 total_branching_ratios = [
compute_total_br(particle, decay_chains)
for particle
in top_level_particles]
272 return max(total_branching_ratios)
277 Returns the total branching ratio to HNLs for a given particle.
279 return sum(np.prod(branching_ratios)
for (top, branching_ratios)
in decay_chains
if top == particle)
284 Returns the set of particles which are at the top of a decay chain.
286 return {top
for (top, branching_ratios)
in decay_chains}
289def exit_if_zero_br(max_total_br, selection, mass, particle: str =
"HNL") ->
None:
290 if max_total_br <= 0:
291 print(f
"No phase space for {particle} from {selection} at this mass: {mass}. Quitting.")
295def print_scale_factor(scaling_factor) -> None:
296 "Prints the scale factor used to make event generation more efficient."
297 print(f
"One simulated event per {scaling_factor:.4g} meson decays")
def get_top_level_particles(decay_chains)
None add_dummy_channel(P8gen, particle, remainder)
def parse_histograms(filepath)
int compute_total_br(particle, decay_chains)
int open(const char *, int)
Opens a file descriptor.