Lorentz-invariant K-matrix#

Physics#

The Lorentz-invariant description \(\boldsymbol{\hat{T}}\) of the \(\boldsymbol{T}\)-matrix is:

(1)#\[ \boldsymbol{T} = \sqrt{\boldsymbol{\rho^\dagger}} \, \boldsymbol{\hat{T}} \sqrt{\boldsymbol{\rho}} \]

with the phase space factor matrix \(\boldsymbol{\rho}\) defined as:

(2)#\[\begin{split} \sqrt{\boldsymbol{\rho}} = \begin{pmatrix} \rho_0 & \cdots & 0 \\ \vdots & \ddots & \vdots \\ 0 & \cdots & \rho_{n-1} \end{pmatrix} \end{split}\]

and

(3)#\[ \rho_i = \frac{2q_i}{m} = \sqrt{ \left[1-\left(\frac{m_{i,a}+m_{i,b}}{m}\right)^2\right] \left[1-\left(\frac{m_{i,a}-m_{i,b}}{m}\right)^2\right] } \]

This results in a similar transformation for the \(\boldsymbol{K}\)-matrix

(4)#\[ \boldsymbol{K} = \sqrt{\boldsymbol{\rho^\dagger}} \; \boldsymbol{\hat{K}} \sqrt{\boldsymbol{\rho}} \]

with (compare Eq. (5) in TR-005):

(5)#\[ \boldsymbol{\hat{T}} = \boldsymbol{\hat{K}}(\boldsymbol{I} - i\boldsymbol{\rho}\boldsymbol{\hat{K}})^{-1} \]

It’s common to integrate these phase space factors into the parametrization of \(K_{ij}\) as well:

(6)#\[ K_{ij} = \sum_R \frac{g_{R,i}(m)g_{R,j}(m)}{\left(m_R^2-m^2\right)\sqrt{\rho_i\rho_j}} \]

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.

Hide 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 matplotlib import cm
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)
Hide code cell source
Math(
    sp.multiline_latex(
        lhs=width_expr,
        rhs=width_expr.evaluate(),
    )
)
\[\displaystyle \begin{align*} {\Gamma_{R,i}}(m^{2}) = & \frac{B_{0}^2\left(\frac{\left(m^{2} - \left({m_{a}}_{i} - {m_{b}}_{i}\right)^{2}\right) \left(m^{2} - \left({m_{a}}_{i} + {m_{b}}_{i}\right)^{2}\right)}{4 m^{2}}\right) {\Gamma}_{R,i} \rho_{i}(m^{2})}{B_{0}^2\left(\frac{\left(- \left({m_{a}}_{i} - {m_{b}}_{i}\right)^{2} + {m}_{R}^{2}\right) \left(- \left({m_{a}}_{i} + {m_{b}}_{i}\right)^{2} + {m}_{R}^{2}\right)}{4 {m}_{R}^{2}}\right) \rho_{i}({m}_{R}^{2})} \end{align*}\]
Hide code cell source
Math(
    sp.multiline_latex(
        lhs=phsp_expr,
        rhs=phsp_expr.doit().simplify().subs(sp.Abs(m), m),
    )
)
\[\displaystyle \begin{align*} \rho_{i}(m^{2}) = & \frac{\sqrt[\mathrm{c}]{\frac{\left(m^{2} - \left({m_{a}}_{i} - {m_{b}}_{i}\right)^{2}\right) \left(m^{2} - \left({m_{a}}_{i} + {m_{b}}_{i}\right)^{2}\right)}{4 m^{2}}}}{8 \pi m} \end{align*}\]

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),
    )
)
\[\displaystyle \begin{align*} \frac{\sqrt{\rho_{0}(m^{2})} \sum_{R=0}^{0} \frac{{\Gamma_{R,0}}(m^{2}) {\gamma}_{R,0}^{2} {m}_{R}}{- m^{2} + {m}_{R}^{2}}}{\left(- i \rho_{0}(m^{2}) \sum_{R=0}^{0} \frac{{\Gamma_{R,0}}(m^{2}) {\gamma}_{R,0}^{2} {m}_{R}}{- m^{2} + {m}_{R}^{2}} + 1\right) \overline{\sqrt{\rho_{0}(m^{2})}}} = &- \frac{{\Gamma_{0,0}}(m^{2}) {\gamma}_{0,0}^{2} {m}_{0} \sqrt{\rho_{0}(m^{2})}}{\left(m^{2} + i {\Gamma_{0,0}}(m^{2}) {\gamma}_{0,0}^{2} {m}_{0} \rho_{0}(m^{2}) - {m}_{0}^{2}\right) \overline{\sqrt{\rho_{0}(m^{2})}}} \end{align*}\]

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)
\[\displaystyle \frac{{\Gamma_{0,0}}(m^{2}) {\gamma}_{0,0}^{2} {m}_{0} \sqrt{\rho_{0}(m^{2})}}{\left(- m^{2} - i {\Gamma_{0,0}}(m^{2}) {\gamma}_{0,0}^{2} {m}_{0} \rho_{1}(m^{2}) - i {\Gamma_{0,1}}(m^{2}) {\gamma}_{0,1}^{2} {m}_{0} \rho_{1}(m^{2}) + {m}_{0}^{2}\right) \overline{\sqrt{\rho_{0}(m^{2})}}}\]

Single channel, \(n_R\) resonances:

relativistic_k_matrix(n_resonances, n_channels=1)[0, 0]
\[\displaystyle \frac{\sqrt{\rho_{0}(m^{2})} \sum_{R=0}^{n_{R} - 1} \frac{{\Gamma_{R,0}}(m^{2}) {\gamma}_{R,0}^{2} {m}_{R}}{- m^{2} + {m}_{R}^{2}}}{\left(- i \rho_{0}(m^{2}) \sum_{R=0}^{n_{R} - 1} \frac{{\Gamma_{R,0}}(m^{2}) {\gamma}_{R,0}^{2} {m}_{R}}{- m^{2} + {m}_{R}^{2}} + 1\right) \overline{\sqrt{\rho_{0}(m^{2})}}}\]

Two channels, \(n_R\) resonances:

expr = relativistic_k_matrix(n_resonances, n_channels=2)[0, 0]
Math(sp.multiline_latex("", expr))
\[\displaystyle \begin{align*} \mathtt{\text{}} = & \frac{\left(\frac{\left(- i \rho_{1}(m^{2}) \sum_{R=0}^{n_{R} - 1} \frac{{\Gamma_{R,1}}(m^{2}) {\gamma}_{R,1}^{2} {m}_{R}}{- m^{2} + {m}_{R}^{2}} + 1\right) \sum_{R=0}^{n_{R} - 1} \frac{{\Gamma_{R,0}}(m^{2}) {\gamma}_{R,0}^{2} {m}_{R}}{- m^{2} + {m}_{R}^{2}}}{- \rho_{1}(m^{2})^{2} \left(\sum_{R=0}^{n_{R} - 1} \frac{{\Gamma_{R,0}}(m^{2}) {\gamma}_{R,0}^{2} {m}_{R}}{- m^{2} + {m}_{R}^{2}}\right) \sum_{R=0}^{n_{R} - 1} \frac{{\Gamma_{R,1}}(m^{2}) {\gamma}_{R,1}^{2} {m}_{R}}{- m^{2} + {m}_{R}^{2}} + \rho_{1}(m^{2})^{2} \left(\sum_{R=0}^{n_{R} - 1} \frac{\sqrt{{\Gamma_{R,0}}(m^{2}) {m}_{R}} \sqrt{{\Gamma_{R,1}}(m^{2}) {m}_{R}} {\gamma}_{R,0} {\gamma}_{R,1}}{- m^{2} + {m}_{R}^{2}}\right)^{2} - i \rho_{1}(m^{2}) \sum_{R=0}^{n_{R} - 1} \frac{{\Gamma_{R,0}}(m^{2}) {\gamma}_{R,0}^{2} {m}_{R}}{- m^{2} + {m}_{R}^{2}} - i \rho_{1}(m^{2}) \sum_{R=0}^{n_{R} - 1} \frac{{\Gamma_{R,1}}(m^{2}) {\gamma}_{R,1}^{2} {m}_{R}}{- m^{2} + {m}_{R}^{2}} + 1} + \frac{i \rho_{1}(m^{2}) \left(\sum_{R=0}^{n_{R} - 1} \frac{\sqrt{{\Gamma_{R,0}}(m^{2}) {m}_{R}} \sqrt{{\Gamma_{R,1}}(m^{2}) {m}_{R}} {\gamma}_{R,0} {\gamma}_{R,1}}{- m^{2} + {m}_{R}^{2}}\right)^{2}}{- \rho_{1}(m^{2})^{2} \left(\sum_{R=0}^{n_{R} - 1} \frac{{\Gamma_{R,0}}(m^{2}) {\gamma}_{R,0}^{2} {m}_{R}}{- m^{2} + {m}_{R}^{2}}\right) \sum_{R=0}^{n_{R} - 1} \frac{{\Gamma_{R,1}}(m^{2}) {\gamma}_{R,1}^{2} {m}_{R}}{- m^{2} + {m}_{R}^{2}} + \rho_{1}(m^{2})^{2} \left(\sum_{R=0}^{n_{R} - 1} \frac{\sqrt{{\Gamma_{R,0}}(m^{2}) {m}_{R}} \sqrt{{\Gamma_{R,1}}(m^{2}) {m}_{R}} {\gamma}_{R,0} {\gamma}_{R,1}}{- m^{2} + {m}_{R}^{2}}\right)^{2} - i \rho_{1}(m^{2}) \sum_{R=0}^{n_{R} - 1} \frac{{\Gamma_{R,0}}(m^{2}) {\gamma}_{R,0}^{2} {m}_{R}}{- m^{2} + {m}_{R}^{2}} - i \rho_{1}(m^{2}) \sum_{R=0}^{n_{R} - 1} \frac{{\Gamma_{R,1}}(m^{2}) {\gamma}_{R,1}^{2} {m}_{R}}{- m^{2} + {m}_{R}^{2}} + 1}\right) \sqrt{\rho_{0}(m^{2})}}{\overline{\sqrt{\rho_{0}(m^{2})}}} \end{align*}\]

Visualization#

Hide 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=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 = 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",
)