Amplitude behavior investigation#
Intensity function for two-pseudoscalar system:
where \(Z_{l}^{m}(\Omega,\Phi)=Y_{l}^{m}(\Omega)e^{-i\Phi}\) is a phase-rotated spherical harmonic, \(\Omega\) is the solid angle, \(\Phi\) is the angle between the production and polarization planes, \(P_{\gamma}\) is the polarization magnitude, \([l]\) are the partial wave amplitudes, \(m\) is the associated m-projection, \(k\) refers to a spin flip (\(k=1\)) or non-flip (\(k=0\)) at the nucleon vertex, and \(\kappa\) is an overall phase space factor.
Main intensity definition#
We build the amplitude symbolically and step by step using SymPy. Of course, we can automate things here later, but defining everything step-by-step gives us most control. Let’s start by defining the main symbols that appear in amplitude expression in Eq. (1).
k, l, m = sp.symbols("k l m")
phi, theta, Phi = sp.symbols("phi theta Phi", real=True)
kappa = sp.Symbol("kappa")
Py = sp.Symbol("P_gamma")
Next, we define a special handler class for nicely rendering the \(Z_m^l(\theta,\phi,\Phi)\) function:
We’re now ready to define the main intensity. Note the use of IndexedBase
for generating the \([l]^{(\pm)}_{m;k}\) symbols and the use of PoolSum
, which is more suited for nested sums than SymPy’s Sum
class.
SymPy can ‘unfold’ all definitons like \(Z^m_l\) and \(\sum_{m;k}\) into basic mathematical operations as follows. This is not only a nice mathematical exercise, but is crucial for when we use the expression to create a numerical function with lambdification.
sp.expand_func(intensity_expr.doit())
Analytic substitution#
Before we go into numerical computations, let’s try what happens when we substitute certain parameters analytically with SymPy. First, we define a mapping of symbols in our expressions to values with which they should be substituted.
This mapping can then be used to substitute the symbols in our ‘unfolded’ expression analytically. Note the use of xreplace()
, which is faster than subs()
:
substituted_expr = intensity_expr.doit().xreplace(parameter_defaults)
substituted_expr
The expression can be further unfolded and simplified as follows:
sp.expand_func(substituted_expr).simplify()
Widget for investigating amplitude behavior#
SymPy is not suitable for evaluating expressions over large data samples. For this, we need to lambdify our main expression to a numerical function that uses faster backends, such as NumPy or JAX.
In this section, we use this mechanism to evaluate the intensity function over a uniform \((\theta,\phi)\) phase space grid sample. To get an idea of performance, we make the visualisation of the intensity distribution interactive with ipywidgets
sliders that allow us to modify the parameters and amplitudes in our model, as well as chose specific values for \(\Phi\).
Our phase space grid sample is defined as follows. Note that the grid is defined in terms of \(\cos\theta\), but that our intensity expects \(\theta\) values.
cos_theta_array, phi_array = np.meshgrid(
np.linspace(-1, +1, num=200),
np.linspace(0, 2 * np.pi, num=400),
)
theta_array = np.arccos(cos_theta_array)
To lambdify to a numerical function, we need to completely unfold our main intensity expression and identify which symbols are to become arguments to our numerical function. These function arguments are then placeholds to data columns (here: \(\theta\) and \(\phi\)) and parameters (all remaining symbols).
free_symbols = sorted(intensity_expr.doit().free_symbols, key=str)
amp_symbols = [s for s in free_symbols if isinstance(s, sp.Indexed)]
arguments = (theta, phi, Phi, Py, *amp_symbols)
plotted_expr = intensity_expr.doit().xreplace({kappa: sp.pi}).expand(func=True)
intensity_func = sp.lambdify(arguments, plotted_expr, "numpy", cse=True)
intensity_func
<function _lambdifygenerated(theta, phi, Phi, P_gamma, _Dummy_43, _Dummy_42, _Dummy_41, _Dummy_40, _Dummy_39, _Dummy_38, _Dummy_37, _Dummy_36, _Dummy_35, _Dummy_34)>
The following code is the most complicated part, but is just there to create a nice user interface for the sliders as well as the interactive visualization…