Blatt–Weisskopf from Hankel function#
Show code cell source
from __future__ import annotations
from functools import cache
import sympy as sp
from ampform.dynamics.phasespace import BreakupMomentumSquared
from ampform.io import aslatex
from ampform.sympy import unevaluated
from IPython.display import Math, display
Introduction#
As of AmpForm v0.15, the implementation of BlattWeisskopfSquared
contains hard-coded polynomials, see implementation here.
The motivation for this can be found in the citations mentioned in its API documentation.
However, as noted by @mmikhasenko in ComPWA/ampform#417, the polynomials can be derived from the spherical[1] Hankel functions of the first kind.
Von Hippel and Quigg[2] derived a generalization of the centrifugal barrier factor \(F_L\), also called form factor, that was introduced by [Blatt and Weisskopf, 1979], showing that
where \(h_\ell^{(1)}\) is a Hankel function of the first kind. They also noted that, if \(z\in\mathbb{R}\),
In the following, we call \(F_\ell(z)\) the unnormalized Blatt–Weisskopf form factor. Following Chung and other resources (see e.g. [Chung et al., 1995], p. 415), AmpForm implements a unitless, normalized Blatt–Weisskopf factor \(B_L\), meaning that \(B_L(1)=1\).[3] It can be defined in terms of \(F_L\) as
Note
As of writing, AmpForm uses \(z\) as argument in BlattWeisskopfSquared
.
This means we have to work with a square root and assume that \(z \geq 0\), meaning
Hankel function of the first kind#
Built-in SymPy function#
SymPy offers a Hankel function of the first kind, scipy.special.hankel1
.
z = sp.Symbol("z", nonnegative=True, real=True)
ell = sp.Symbol(R"\ell", integer=True, nonnegative=True)
sp.hankel1(ell, z)
This function is the general[1] Hankel function \(H_\ell\) and the class does not offer algebraic simplifications for specific values or assumptions of \(\ell\) and \(z\).
display(
sp.hankel1(ell, z).doit(),
sp.hankel1(ell, 0).doit(),
sp.hankel1(0, z).doit(),
sp.hankel1(0, 0).doit(),
)
Custom class definition#
To implement Equation (1) for the spherical Hankel function, we have to define a custom @unevaluated
expression class.
The following class evaluates to the sum given in Equation (1).
We introduce a special sympy.Sum
class that does not ‘unfold’ on symbolic input for \(\ell\) if doit()
is called (see Nested doit).
@unevaluated
class SphericalHankel1(sp.Expr):
l: sp.Symbol | int
z: sp.Symbol | float
_latex_repr_ = R"h_{{{l}}}^{{(1)}}\left({z}\right)"
def evaluate(self) -> sp.Expr:
l, z = self.args
k = sp.Dummy("k", integer=True, nonnegative=True)
return (
(-sp.I) ** (1 + l)
* (sp.exp(z * sp.I) / z)
* SymbolicSum(
sp.factorial(l + k)
/ (sp.factorial(l - k) * sp.factorial(k))
* (sp.I / (2 * z)) ** k,
(k, 0, l),
)
)
class SymbolicSum(sp.Sum):
def doit(self, deep: bool = True, **kwargs) -> sp.Expr:
if _get_indices(self):
expression = self.args[0]
indices = self.args[1:]
return SymbolicSum(expression.doit(deep=deep, **kwargs), *indices)
return super().doit(deep=deep, **kwargs)
@cache
def _get_indices(expr: sp.Sum) -> set[sp.Symbol]:
free_symbols = set()
for index in expr.args[1:]:
free_symbols.update(index.free_symbols)
return {s for s in free_symbols if not isinstance(s, sp.Dummy)}
Indeed, the absolute squared value \(\left|h_\ell^{(1)}\right|^2\) results in a clean fraction of polynomials (after some algebraic simplifications).
Show code cell source
Show code cell source
Normalized Blatt–Weisskopf form factor#
We now have the required expression classes for re-implementing BlattWeisskopfSquared
using Equation (2) (with \(z\) as input, instead of \(z^2\)).
@unevaluated
class BlattWeisskopfSquared(sp.Expr):
L: sp.Symbol | int
z: sp.Symbol | float
_latex_repr_ = R"B^2_{{{L}}}\left({z}\right)"
def evaluate(self) -> sp.Expr:
L = self.L
z = sp.Dummy("z", nonnegative=True, real=True)
expr = (
sp.Abs(SphericalHankel1(L, 1)) ** 2
/ sp.Abs(SphericalHankel1(L, sp.sqrt(z))) ** 2
/ z
)
if not L.free_symbols:
expr = expr.doit().simplify()
return expr.xreplace({z: self.z})
Note
An explicit simplify()
is required in order to reproduce the polynomial form upon evaluation.
To make the simplification as fast as possible, it is done internally within evaluate()
with \(z\) as a dummy variable.
This is to avoid performing nested simplifications if \(z\) is in itself an expression (see Nested doit).
Show code cell source
Indeed the polynomials are exactly the same as the original BlattWeisskopfSquared
!
Show code cell source
Nested doit#
Eventually, the barrier factors take \(z=q/q_R\), with \(q\) the break-up momentum and \(q_R\) an impact factor. Here it becomes crucial that only \(\left|h_\ell^{(1)}(z)\right|^2\) is simplified to a polynomial fraction, not \(q\) itself. The break-up momentum does need to unfold though.
Show code cell source
s, m1, m2, qR = sp.symbols("s m1 m2 q_R", nonnegative=True)
q2 = BreakupMomentumSquared(s, m1, m2)
Math(aslatex({q2: q2.doit()}))
Symbolic angular momentum#
BlattWeisskopfSquared(L, z=q2 / qR**2).doit(deep=False)
BlattWeisskopfSquared(L, z=q2 / qR**2).doit(deep=True)
Numeric angular momentum#
BlattWeisskopfSquared(L=2, z=q2 / qR**2).doit(deep=False)
BlattWeisskopfSquared(L=2, z=q2 / qR**2).doit(deep=True)