Note
Go to the end to download the full example code.
Examples for Helicity Angle
Here we use the same config in ex3_particle_config.py
9 config_str = """
10 decay:
11 A:
12 - [R1, B]
13 - [R2, C]
14 - [R3, D]
15 R1: [C, D]
16 R2: [B, D]
17 R3: [B, C]
18
19 particle:
20 $top:
21 A: { mass: 1.86, J: 0, P: -1}
22 $finals:
23 B: { mass: 0.494, J: 0, P: -1}
24 C: { mass: 0.139, J: 0, P: -1}
25 D: { mass: 0.139, J: 0, P: -1}
26 R1: [ R1_a, R1_b ]
27 R1_a: { mass: 0.7, width: 0.05, J: 1, P: -1}
28 R1_b: { mass: 0.5, width: 0.05, J: 0, P: +1}
29 R2: { mass: 0.824, width: 0.05, J: 0, P: +1}
30 R3: { mass: 0.824, width: 0.05, J: 0, P: +1}
31
32 """
33
34 import matplotlib.pyplot as plt
35 import numpy as np
36 import tensorflow as tf
37 import yaml
38
39 from tf_pwa.config_loader import ConfigLoader
40 from tf_pwa.particle import BaseParticle
41
42 config = ConfigLoader(yaml.full_load(config_str))
TFPWA provide useful class to use helicity angle. we can import it from data_trans module
48 from tf_pwa.data_trans.helicity_angle import HelicityAngle
The class use DecayChain as input. We can get the DecayChain for ConfigLoader
54 decay_chain = config.get_decay(False).get_decay_chain("R1")
55
56 hel = HelicityAngle(decay_chain)
HelcicityAngle provide two main method, find_variable and build_data.
find_variable can find the variables in data dict
{A: <tf.Tensor: shape=(1,), dtype=float64, numpy=array([1.86])>, R1: <tf.Tensor: shape=(1,), dtype=float64, numpy=array([1.27674445])>, C: <tf.Tensor: shape=(1,), dtype=float64, numpy=array([0.139])>, B: <tf.Tensor: shape=(1,), dtype=float64, numpy=array([0.494])>, D: <tf.Tensor: shape=(1,), dtype=float64, numpy=array([0.139])>}
[<tf.Tensor: shape=(1,), dtype=float64, numpy=array([0.01868061])>, <tf.Tensor: shape=(1,), dtype=float64, numpy=array([-0.50535739])>]
[<tf.Tensor: shape=(1,), dtype=float64, numpy=array([1.11946138])>, <tf.Tensor: shape=(1,), dtype=float64, numpy=array([1.04215005])>]
build_data is the inverse function of cal_angle, it can build 4-momentum from the helicity angles
{C: <tf.Tensor: shape=(1, 4), dtype=float64, numpy=array([[ 0.58761901, -0.49934137, 0.03417889, -0.27470912]])>, D: <tf.Tensor: shape=(1, 4), dtype=float64, numpy=array([[0.71497249, 0.61194093, 0.19812761, 0.27953249]])>, B: <tf.Tensor: shape=(1, 4), dtype=float64, numpy=array([[ 0.5574085 , -0.11259956, -0.2323065 , -0.00482337]])>}
We can check that the 4-momentums have the same angle as what we input, within the precision range.
80 data2 = hel.cal_angle(p4)
81 mass2, costheta2, phi2 = hel.find_variable(data2)
82
83 print(mass2)
84 print(costheta2)
85 print(phi2)
86
87 assert all(abs(mass[i] - mass2[i]) < 1e-6 for i in mass.keys())
88 assert all(abs(a - b) < 1e-6 for a, b in zip(costheta, costheta2))
89 assert all(abs(a - b) < 1e-6 for a, b in zip(phi, phi2))
{A: <tf.Tensor: shape=(1,), dtype=float64, numpy=array([1.86])>, R1: <tf.Tensor: shape=(1,), dtype=float64, numpy=array([1.27674445])>, C: <tf.Tensor: shape=(1,), dtype=float64, numpy=array([0.139])>, B: <tf.Tensor: shape=(1,), dtype=float64, numpy=array([0.494])>, D: <tf.Tensor: shape=(1,), dtype=float64, numpy=array([0.139])>}
[<tf.Tensor: shape=(1,), dtype=float64, numpy=array([0.01868061])>, <tf.Tensor: shape=(1,), dtype=float64, numpy=array([-0.50535739])>]
[<tf.Tensor: shape=(1,), dtype=float64, numpy=array([1.11946138])>, <tf.Tensor: shape=(1,), dtype=float64, numpy=array([1.04215005])>]
The helicity angle and related masses have the same number of degree of freedom as 4-momentums, it can be used in many cases.
Phase space generator
The most common usage is the Phase Space generator, helicity angle is uniform in phase space. After generate related masses, we can generate uniform random number for helicity angle to build the full 4-momentums. See Phase Space for more information.
99 from tf_pwa.phasespace import PhaseSpaceGenerator
100
101 gen = PhaseSpaceGenerator(1.86, [0.139, 0.139, 0.494])
102 N = 1
103 mass_gen = gen.generate_mass(N)
104 costheta_gen = [
105 np.random.uniform(-1, 1, size=(N,)),
106 np.random.uniform(-1, 1, size=(N,)),
107 ]
108 phi_gen = [
109 np.random.uniform(-np.pi, np.pi, size=(N,)),
110 np.random.uniform(-np.pi, np.pi, size=(N,)),
111 ]
112
113 p4 = hel.build_data({"R1": mass_gen[0]}, costheta_gen, phi_gen)
Particle Function
We can fix the other variables to see the dependences of one variable.
120 m = np.linspace(0.3, 0.8)
121 mass = {"R1": m}
122 costheta = [np.zeros(m.shape), np.zeros(m.shape)]
123 phi = [np.zeros(m.shape), np.zeros(m.shape)]
124
125 amp = config.get_amplitude()
126 data = config.data.cal_angle(hel.build_data(mass, costheta, phi))
127
128 with amp.temp_used_res(["R1_b"]):
129 prob = amp(data)
There is a useful function get_particle_function provided by ConfigLoader.
134 f = config.get_particle_function("R1_b")
135 prob2 = tf.abs(f(m)[:, 0]) ** 2
136
137 plt.clf()
138 plt.plot(m, prob2.numpy(), label="from particle function")
139 plt.plot(m, prob.numpy(), ls="--", label="from amplitude")
140 plt.legend()
141 plt.xlabel("mass")
142 plt.ylabel("$|A|^2$")
143 plt.show()

Resolution
Another common usage in TFPWA is used in resolution, when we ony consider resolution in one mass, we can fix the helicity angles, replace the mass into different values, rebuild the 4-monmentums and sum over them to do the integration. See Resolution in Partial Wave Analysis for more information.
156 probs = []
157 delta = np.linspace(-0.2, 0.2, 20)
158 for delta_i in delta:
159 masses_new = mass.copy()
160 new_mass = m + delta_i
161 # be careful for the boundary, the outer value would be NaN
162 new_mass = tf.clip_by_value(
163 new_mass, 0.139 + 0.139 + 1e-6, 1.86 - 0.494 - 1e-6
164 )
165 masses_new[BaseParticle("R1")] = new_mass
166 p4 = hel.build_data(masses_new, costheta, phi)
167 data = config.data.cal_angle(p4)
168 with amp.temp_used_res(["R1_b"]):
169 prob_i = amp(data)
170 probs.append(prob_i)
171
172 gauss = tf.exp(-(delta**2) / 0.05**2 / 2)
173 gauss = gauss / tf.reduce_sum(gauss)
174
175 smear_prob = tf.reduce_sum(tf.stack(probs, axis=-1) * gauss, axis=-1)
176
177 plt.clf()
178 plt.plot(m, smear_prob.numpy(), label="smear $|A|^2$")
179 plt.plot(m, prob.numpy(), ls="--", label="origin $|A|^2$")
180 plt.legend()
181 plt.xlabel("mass")
182 plt.ylabel("$|A|^2$")
183 plt.show()

Distribution transform
Finally, we can use the helicity angle to study the distribution of other variables.
The jacobian can be calculated by automatic differentiation.
For example, we can verify that the Dalitz plot variable is uniform distribution. First we generate some variable in phase space.
and than calculate the jacobian
210 x = tf.Variable(tf.stack([mass[BaseParticle("R1")], costheta[1]], axis=-1))
211 with tf.GradientTape() as tape:
212 mass[BaseParticle("R1")] = x[:, 0]
213 costheta[1] = x[:, 1]
214 p4 = hel.build_data(mass, costheta, phi)
215 s12 = mass2(p4[b] + p4[c])
216 s23 = mass2(p4[c] + p4[d])
217 y = tf.stack([s12, s23], axis=-1)
218
219 jac = tape.batch_jacobian(y, x)
These are Dalitz plot variables
224 print("s12, s23 = ", s12, s23)
s12, s23 = tf.Tensor([1.07086893 0.45713135 1.5645898 ], shape=(3,), dtype=float64) tf.Tensor([1.50300046 0.69188737 0.40849295], shape=(3,), dtype=float64)
the jacobian of the first variable is
229 print(jac[0])
tf.Tensor(
[[-1.04858802 0.58956795]
[ 2.45193839 0. ]], shape=(2, 2), dtype=float64)
We can see that the probability density at such points have the same values
234 prob = 1 / tf.abs(tf.linalg.det(jac)) * hel.eval_phsp_factor(mass)
235
236 print("prob = ", prob)
prob = tf.Tensor([0.1344086 0.1344086 0.1344086], shape=(3,), dtype=float64)
Total running time of the script: (0 minutes 6.630 seconds)