Lorentz-invariant K-matrix#
Physics#
The Lorentz-invariant description \(\boldsymbol{\hat{T}}\) of the \(\boldsymbol{T}\)-matrix is:
with the phase space factor matrix \(\boldsymbol{\rho}\) defined as:
and
This results in a similar transformation for the \(\boldsymbol{K}\)-matrix
with (compare Eq. (5) in TR-005):
It’s common to integrate these phase space factors into the parametrization of \(K_{ij}\) as well:
Compare this with Eq. (6) in TR-005.
In addition, one often uses an “energy dependent” coupled_width()
\(\Gamma_R(m)\) instead of a fixed width \(\Gamma_R\) as done in TR-005.
Show code cell content
from __future__ import annotations
import os
import re
import warnings
from typing import TYPE_CHECKING
import matplotlib.pyplot as plt
import mpl_interactions.ipyplot as iplt
import numpy as np
import symplot
import sympy as sp
from ampform.dynamics import coupled_width, phase_space_factor_complex
from ampform.dynamics.decorator import (
UnevaluatedExpression,
create_expression,
implement_doit_method,
)
from IPython.display import Math, display
from ipywidgets import widgets as ipywidgets
from mpl_interactions.controller import Controls
if TYPE_CHECKING:
from sympy.printing.latex import LatexPrinter
warnings.filterwarnings("ignore")
STATIC_WEB_PAGE = {"EXECUTE_NB", "READTHEDOCS"}.intersection(os.environ)
Implementation#
Wrapping expressions#
To keep a nice rendering, we wrap the expressions for phase_space_factor()
and coupled_width()
into a class that derives from Expr
(see e.g. the implementation of BlattWeisskopfSquared
). Note that we need to use partial_doit()
to keep these expression symbols after evaluating the Sum
.
@implement_doit_method()
class PhaseSpaceFactor(UnevaluatedExpression):
is_commutative = True
def __new__(
cls,
s: sp.Symbol,
m_a: sp.Symbol,
m_b: sp.Symbol,
i: int,
**hints,
) -> PhaseSpaceFactor:
return create_expression(cls, s, m_a, m_b, i, **hints)
def evaluate(self) -> sp.Expr:
s, m_a, m_b, *_ = self.args
return phase_space_factor_complex(s, m_a, m_b)
def _latex(self, printer: LatexPrinter, *args) -> str:
s = printer._print(self.args[0])
i = self.args[-1]
return Rf"\rho_{{{i}}}({s})"
@implement_doit_method()
class CoupledWidth(UnevaluatedExpression):
is_commutative = True
def __new__(
cls,
s: sp.Symbol,
mass0: sp.IndexedBase,
gamma0: sp.IndexedBase,
m_a: sp.IndexedBase,
m_b: sp.IndexedBase,
angular_momentum: int,
R: int | sp.Symbol,
i: int,
**hints,
) -> CoupledWidth:
return create_expression(
cls, s, mass0, gamma0, m_a, m_b, angular_momentum, R, i, **hints
)
def evaluate(self) -> sp.Expr:
s, mass0, gamma0, m_a, m_b, angular_momentum, R, i = self.args
def phsp_factor(s, m_a, m_b):
return PhaseSpaceFactor(s, m_a, m_b, i)
return coupled_width(
s,
mass0[R],
gamma0[R, i],
m_a[i],
m_b[i],
angular_momentum=angular_momentum,
meson_radius=1,
phsp_factor=phsp_factor,
)
def _latex(self, printer: LatexPrinter, *args) -> str:
s = printer._print(self.args[0])
R = self.args[-2]
i = self.args[-1]
return Rf"{{\Gamma_{{{R},{i}}}}}({s})"
And here is what the equations look like:
n_channels = 2
n_resonances, i, R, L = sp.symbols("n_R, i, R, L", integer=True, negative=False)
m = sp.Symbol("m", real=True)
M = sp.IndexedBase("m", shape=(n_resonances,))
Gamma = sp.IndexedBase("Gamma", shape=(n_resonances, n_channels))
gamma = sp.IndexedBase("gamma", shape=(n_resonances, n_channels))
m_a = sp.IndexedBase("m_a", shape=(n_channels,))
m_b = sp.IndexedBase("m_b", shape=(n_channels,))
width_expr = CoupledWidth(m**2, M, Gamma, m_a, m_b, 0, R, i)
phsp_expr = PhaseSpaceFactor(m**2, m_a[i], m_b[i], i)
Show code cell source
Math(
sp.multiline_latex(
lhs=width_expr,
rhs=width_expr.evaluate(),
)
)
Show code cell source
Math(
sp.multiline_latex(
lhs=phsp_expr,
rhs=phsp_expr.doit().simplify().subs(sp.Abs(m), m),
)
)
Note
In PhaseSpaceFactor
, we used PhaseSpaceFactorComplex
instead of PhaseSpaceFactor
, meaning that we choose the positive square root when values under the square root are negative. The only reason for doing this is, so that there is output in the figure under Visualization. The choice for which square root to choose has to do with analyticity (see TR-004) and choosing which Riemann sheet to connect to. This issue is ignored in this report.
Generalization#
The implementation is quite similar to that of TR-005, with the only difference being additional \(\boldsymbol{\rho}\)-matrix and the insertion of coupled width. Don’t forget to convert back to \(\boldsymbol{T}\) from \(\boldsymbol{\hat{T}}\) with Eq. (1).
def Kij_relativistic(
m: sp.Symbol,
M: sp.IndexedBase,
Gamma: sp.IndexedBase,
gamma: sp.IndexedBase,
i: int,
j: int,
n_resonances: int | sp.Symbol,
angular_momentum: int | sp.Symbol = 0,
) -> sp.Expr:
def residue_function(i):
return gamma[R, i] * sp.sqrt(
M[R] * CoupledWidth(m**2, M, Gamma, m_a, m_b, angular_momentum, R, i)
)
g_i = residue_function(i)
g_j = residue_function(j)
parametrization = (g_i * g_j) / (M[R] ** 2 - m**2)
return sp.Sum(parametrization, (R, 0, n_resonances - 1))
def relativistic_k_matrix(
n_resonances: int,
n_channels: int,
angular_momentum: int | sp.Symbol = 0,
) -> sp.Matrix:
# Define symbols
m = sp.Symbol("m", real=True)
M = sp.IndexedBase("m", shape=(n_resonances,))
Gamma = sp.IndexedBase("Gamma", shape=(n_resonances, n_channels))
gamma = sp.IndexedBase("gamma", shape=(n_resonances, n_channels))
m_a = sp.IndexedBase("m_a", shape=(n_channels,))
m_b = sp.IndexedBase("m_b", shape=(n_channels,))
# Define phase space matrix
sqrt_rho = sp.zeros(n_channels, n_channels)
sqrt_rho_dagger = sp.zeros(n_channels, n_channels)
for i in range(n_channels):
rho = PhaseSpaceFactor(m**2, m_a[i], m_b[i], i)
sqrt_rho[i, i] = sp.sqrt(rho)
sqrt_rho_dagger[i, i] = 1 / sp.conjugate(sqrt_rho[i, i])
# Define K-matrix and T-matrix
K = create_symbol_matrix("K", n_channels)
T_hat = K * (sp.eye(n_channels) - sp.I * rho * K).inv()
T = sqrt_rho_dagger * T_hat * sqrt_rho
# Substitute elements
return T.subs({
K[i, j]: Kij_relativistic(
m=m,
M=M,
Gamma=Gamma,
gamma=gamma,
i=i,
j=j,
n_resonances=n_resonances,
angular_momentum=angular_momentum,
)
for i in range(n_channels)
for j in range(n_channels)
})
def create_symbol_matrix(name: str, n: int) -> sp.Matrix:
symbol = sp.IndexedBase(name, shape=(n, n))
return sp.Matrix([[symbol[i, j] for j in range(n)] for i in range(n)])
Single channel, one resonance (compare relativistic_breit_wigner_with_ff()
):
expr = relativistic_k_matrix(n_resonances=1, n_channels=1)[0, 0]
Math(
sp.multiline_latex(
lhs=expr,
rhs=symplot.partial_doit(expr, sp.Sum).simplify(doit=False),
)
)
Two channels, one resonance (‘Flatté’):
expr = relativistic_k_matrix(n_resonances=1, n_channels=2)[0, 0]
symplot.partial_doit(expr, sp.Sum).simplify(doit=False)
Single channel, \(n_R\) resonances:
relativistic_k_matrix(n_resonances, n_channels=1)[0, 0]
Two channels, \(n_R\) resonances:
expr = relativistic_k_matrix(n_resonances, n_channels=2)[0, 0]
Math(sp.multiline_latex("", expr))
Visualization#
Show code cell source
def plot_relativistic_k_matrix(
n_channels: int,
n_resonances: int,
angular_momentum: int | sp.Symbol = 0,
title: str = "",
) -> None:
# Convert to Symbol: symplot cannot handle IndexedBase
epsilon = sp.Symbol("epsilon")
i, j = sp.symbols("i, j", integer=True, negative=False)
j = i
expr = relativistic_k_matrix(
n_resonances, n_channels, angular_momentum=angular_momentum
).doit()[i, j]
expr = symplot.substitute_indexed_symbols(expr)
expr = expr.subs(m, m + epsilon * sp.I)
np_expr, sliders = symplot.prepare_sliders(expr, m)
symbol_to_arg = {symbol: arg for arg, symbol in sliders._arg_to_symbol.items()}
# Set plot domain
x_min, x_max = 1e-3, 3
y_min, y_max = -0.5, +0.5
plot_domain = np.linspace(x_min, x_max, num=500)
x_values = np.linspace(x_min, x_max, num=160)
y_values = np.linspace(y_min, y_max, num=80)
X, Y = np.meshgrid(x_values, y_values)
plot_domain_complex = X + Y * 1j
# Set slider values and ranges
m0_values = np.linspace(x_min, x_max, num=n_resonances + 2)
m0_values = m0_values[1:-1]
if "L" in sliders:
sliders.set_ranges(L=(0, 8))
for R in range(n_resonances):
for i in range(n_channels):
sliders.set_ranges({
"i": (0, n_channels - 1),
"epsilon": (y_min * 0.2, y_max * 0.2, 0.01),
f"m{R}": (0, 3, 100),
Rf"\Gamma_{{{R},{i}}}": (-2, +2, 100),
Rf"\gamma_{{{R},{i}}}": (0, 10, 100),
f"m_a{i}": (0, 1, 0.01),
f"m_b{i}": (0, 1, 0.01),
})
sliders.set_values({
f"m{R}": m0_values[R],
Rf"\Gamma_{{{R},{i}}}": 2.0 * (0.4 + R * 0.2 - i * 0.3),
Rf"\gamma_{{{R},{i}}}": 0.25 * (10 - R + i),
f"m_a{i}": (i + 1) * 0.25,
f"m_b{i}": (i + 1) * 0.25,
})
# Create interactive plots
controls = Controls(**sliders)
fig, axes = plt.subplots(
nrows=2,
figsize=(8, 6),
sharex=True,
tight_layout=True,
)
fig.canvas.toolbar_visible = False
fig.canvas.header_visible = False
fig.canvas.footer_visible = False
if not title:
title = (
Rf"${n_channels} \times {n_channels}$ $K$-matrix"
f" with {n_resonances} resonances"
)
fig.suptitle(title)
for ax in axes:
ax.set_xlim(x_min, x_max)
ax_2d, ax_3d = axes
ax_2d.set_ylabel("$|T|^{2}$")
ax_2d.set_yticks([])
ax_3d.set_xlabel("Re $m$")
ax_3d.set_ylabel("Im $m$")
ax_3d.set_xticks([])
ax_3d.set_yticks([])
ax_3d.set_facecolor("white")
ax_3d.axhline(0, linewidth=0.5, c="black", linestyle="dotted")
# 2D plot
def plot(channel: int):
def wrapped(*args, **kwargs) -> sp.Expr:
kwargs["i"] = channel
return np.abs(np_expr(*args, **kwargs)) ** 2
return wrapped
for i in range(n_channels):
iplt.plot(
plot_domain,
plot(i),
ax=axes[0],
controls=controls,
ylim="auto",
label=f"channel {i}",
)
if n_channels > 1:
axes[0].legend(loc="upper right")
mass_line_style = {
"c": "red",
"alpha": 0.3,
}
for name in controls.params:
if not re.match(r"^m[0-9]+$", name):
continue
iplt.axvline(controls[name], ax=axes[0], **mass_line_style)
# 3D plot
color_mesh = None
epsilon_indicator = None
resonances_indicators = []
threshold_indicators = []
def plot3(*, z_cutoff, complex_rendering, **kwargs):
nonlocal color_mesh, epsilon_indicator
epsilon = kwargs["epsilon"]
kwargs["epsilon"] = 0
Z = np_expr(plot_domain_complex, **kwargs)
if complex_rendering == "imag":
Z_values = Z.imag
ax_title = "Re $T$"
elif complex_rendering == "real":
Z_values = Z.real
ax_title = "Im $T$"
elif complex_rendering == "abs":
Z_values = np.abs(Z)
ax_title = "$|T|$"
else:
raise NotImplementedError
if n_channels == 1:
axes[-1].set_title(ax_title)
else:
i = kwargs["i"]
axes[-1].set_title(f"{ax_title}, channel {i}")
if color_mesh is None:
color_mesh = ax_3d.pcolormesh(X, Y, Z_values, cmap=plt.cm.coolwarm)
else:
color_mesh.set_array(Z_values)
color_mesh.set_clim(vmin=-z_cutoff, vmax=+z_cutoff)
if resonances_indicators:
for R, (line, text) in enumerate(resonances_indicators):
mass = kwargs[f"m{R}"]
line.set_xdata(mass)
text.set_x(mass + (x_max - x_min) * 0.008)
else:
for R in range(n_resonances):
mass = kwargs[f"m{R}"]
line = ax_3d.axvline(mass, **mass_line_style)
text = ax_3d.text(
x=mass + (x_max - x_min) * 0.008,
y=0.95 * y_min,
s=f"$m_{R}$",
c="red",
)
resonances_indicators.append((line, text))
if epsilon_indicator is None:
line = ax.axhline(
epsilon,
linewidth=0.5,
c="blue",
linestyle="dotted",
label=R"$\epsilon$",
)
text = axes[-1].text(
x=x_min + 0.008,
y=epsilon + 0.01,
s=R"$\epsilon$",
c="blue",
)
epsilon_indicator = line, text
else:
line, text = epsilon_indicator
line.set_xdata(epsilon)
text.set_y(epsilon + 0.01)
x_offset = (x_max - x_min) * 0.015
if threshold_indicators:
for i, (line_thr, line_diff, text_thr, text_diff) in enumerate(
threshold_indicators
):
m_a = kwargs[f"m_a{i}"]
m_b = kwargs[f"m_b{i}"]
s_thr = m_a + m_b
m_diff = m_a - m_b
line_thr.set_xdata(s_thr)
line_diff.set_xdata(m_diff)
text_thr.set_x(s_thr)
text_diff.set_x(m_diff - x_offset)
else:
colors = plt.cm.plasma(np.linspace(0, 1, n_channels))
for i, color in enumerate(colors):
m_a = kwargs[f"m_a{i}"]
m_b = kwargs[f"m_b{i}"]
s_thr = m_a + m_b
m_diff = m_a - m_b
line_thr = ax.axvline(s_thr, c=color, linestyle="dotted")
line_diff = ax.axvline(m_diff, c=color, linestyle="dashed")
text_thr = ax.text(
x=s_thr,
y=0.95 * y_min,
s=f"$m_{{a{i}}}+m_{{b{i}}}$",
c=color,
rotation=-90,
)
text_diff = ax.text(
x=m_diff - x_offset,
y=0.95 * y_min,
s=f"$m_{{a{i}}}-m_{{b{i}}}$",
c=color,
rotation=+90,
)
threshold_indicators.append((line_thr, line_diff, text_thr, text_diff))
for i, (_, line_diff, _, text_diff) in enumerate(threshold_indicators):
m_a = kwargs[f"m_a{i}"]
m_b = kwargs[f"m_b{i}"]
s_thr = m_a + m_b
m_diff = m_a - m_b
if m_diff > x_offset + 0.01 and s_thr - abs(m_diff) > x_offset:
line_diff.set_alpha(0.5)
text_diff.set_alpha(0.5)
else:
line_diff.set_alpha(0)
text_diff.set_alpha(0)
# Create switch for imag/real/abs
name = "complex_rendering"
sliders._sliders[name] = ipywidgets.RadioButtons(
options=["imag", "real", "abs"],
description=R"\(s\)-plane plot",
)
sliders._arg_to_symbol[name] = name
# Create cut-off slider for z-direction
name = "z_cutoff"
sliders._sliders[name] = ipywidgets.IntSlider(
value=30,
min=+1,
max=+100,
description=R"\(z\)-cutoff",
)
sliders._arg_to_symbol[name] = name
# Create GUI
sliders_copy = dict(sliders)
h_boxes = []
for R in range(n_resonances):
buttons = [sliders_copy.pop(f"m{R}")]
if n_channels == 1:
buttons.append(sliders_copy.pop(symbol_to_arg[Rf"\Gamma_{{{R},0}}"]))
buttons.append(sliders_copy.pop(symbol_to_arg[Rf"\gamma_{{{R},0}}"]))
h_box = ipywidgets.HBox(buttons)
h_boxes.append(h_box)
remaining_sliders = sorted(
sliders_copy.values(), key=lambda s: (str(type(s)), s.description)
)
if n_channels == 1:
remaining_sliders.remove(sliders["i"])
ui = ipywidgets.VBox(h_boxes + remaining_sliders)
output = ipywidgets.interactive_output(plot3, controls=sliders)
display(ui, output)
plot_relativistic_k_matrix(
n_resonances=2,
n_channels=1,
angular_momentum=L,
title="Relativistic $K$-matrix, single channel",
)