Polarimeter vector field#

Hide code cell content

import itertools
import logging
import os

import jax.numpy as jnp
import matplotlib.pyplot as plt
import numpy as np
import qrules
import sympy as sp
from ampform.sympy import (
    PoolSum,
    UnevaluatedExpression,
    create_expression,
    implement_doit_method,
    make_commutative,
    perform_cached_doit,
)
from attrs import frozen
from IPython.display import HTML, Image, Math, display
from ipywidgets import Button, Combobox, HBox, HTMLMath, Tab, VBox, interactive_output
from matplotlib.colors import LogNorm
from matplotlib_inline.backend_inline import set_matplotlib_formats
from qrules.particle import Particle
from symplot import create_slider
from sympy.core.symbol import Str
from sympy.physics.matrices import msigma
from sympy.physics.quantum.spin import Rotation as Wigner
from tensorwaves.data.transform import SympyDataTransformer
from tensorwaves.function.sympy import create_function, create_parametrized_function
from tensorwaves.interface import DataSample, ParametrizedFunction

LOGGER = logging.getLogger()
LOGGER.setLevel(logging.ERROR)
STATIC_WEB_PAGE = {"EXECUTE_NB", "READTHEDOCS"}.intersection(os.environ)

PDG = qrules.load_pdg()


def display_definitions(definitions: dict[sp.Symbol, sp.Expr]) -> None:
    latex = R"\begin{array}{rcl}" + "\n"
    for symbol, expr in definitions.items():
        symbol = sp.sympify(symbol)
        expr = sp.sympify(expr)
        lhs = sp.latex(symbol)
        rhs = sp.latex(expr)
        latex += Rf"  {lhs} & = & {rhs} \\" + "\n"
    latex += R"\end{array}"
    display(Math(latex))


def display_doit(
    expr: UnevaluatedExpression, deep=False, terms_per_line: int = 10
) -> None:
    latex = sp.multiline_latex(
        lhs=expr,
        rhs=expr.doit(deep=deep),
        terms_per_line=terms_per_line,
        environment="eqnarray",
    )
    display(Math(latex))


# hack for moving Indexed indices below superscript of the base
def _print_Indexed_latex(self, printer, *args):
    base = printer._print(self.base)
    indices = ", ".join(map(printer._print, self.indices))
    return f"{base}_{{{indices}}}"


sp.Indexed._latex = _print_Indexed_latex
set_matplotlib_formats("svg")

Amplitude model#

The helicity amplitude for the \(\Lambda_c \to p K \pi\) decay reads as a sum of three partial wave series, incorporating \(\Lambda^{**}\) resonances, \(\Delta^{**}\) resonances, and \(K^{**}\) resonances. The particles are ordered as \(\Lambda_c(\mathbf{0}) \to p(\mathbf{1}) \pi(\mathbf{2}) K(\mathbf{3})\).

(1)#\[\begin{split} \begin{align} \mathcal{A}_{\nu,\lambda}(m_{K\pi},m_{pK}) &= \sum_{\nu',\lambda'} \big[\\ &\qquad d_{\nu,\nu'}^{1/2}(\zeta_{1(1)}^0) \mathcal{A}_{\nu',\lambda'}^{K} d_{\lambda',\lambda}^{1/2}(\zeta_{1(1)}^1) &\mathbf{subsystem\,1\;}(\to 23)\\ &\qquad + d_{\nu,\nu'}^{1/2}(\zeta_{2(1)}^0) \mathcal{A}_{\nu',\lambda'}^{\Lambda} d_{\lambda',\lambda}^{1/2}(\zeta_{2(1)}^1) &\mathbf{subsystem\,2\;}(\to 31)\\ &\qquad + d_{\nu,\nu'}^{1/2}(\zeta_{3(1)}^0) \mathcal{A}_{\nu',\lambda'}^{\Delta} d_{\lambda',\lambda}^{1/2}(\zeta_{3(1)}^1)\big]\,. &\mathbf{subsystem\,3\;}(\to 12)\\ \end{align} \end{split}\]

where \(\zeta^{i}_{j(k)}\) is the Wigner rotation for particle \(i,k\) and chain \(j\). The number in brackets indicates the overall definition of the helicity states \(|1/2,\nu\rangle\) and \(|1/2,\lambda\rangle\) for \(\Lambda_c\) and proton, respectively.

We use the particle-2 convention for the helicity couplings, which leads to the phase factor in the transitions \(\Lambda_c\to K^{**}p\), and \(\Lambda\to K p\):

\[\begin{split} \begin{align} \mathcal{A}^{K}_{\nu,\lambda} &= \sum_{j,\tau} \delta_{\nu,\tau - \lambda}\mathcal{H}^{\Lambda_c \to K^{**} p}_{\tau,\lambda} (-)^{1/2 - \lambda} \,d^{j}_{\lambda,0} (\theta_{23}) \, \mathcal{H}^{K^{**} \to \pi K}_{0,0}\\ % \mathcal{A}^{\Lambda}_{\nu,\lambda} &= \sum_{j,\tau} \delta_{\nu,\tau} \mathcal{H}^{\Lambda_c \to \Lambda^{**} \pi}_{\tau,0} d^{j}_{\tau,-\lambda} (\theta_{31}) \mathcal{H}^{\Lambda^{**} \to K p}_{0,\lambda} (-)^{1/2-\lambda} \\ % \mathcal{A}^{\Delta}_{\nu,\lambda} &= \sum_{j,\tau} \delta_{\nu,\tau} \mathcal{H}^{\Lambda_c \to \Delta^{**} K}_{\tau,0} d^{j}_{\tau,\lambda}(\theta_{12}) \mathcal{H}^{\Delta^{**} \to p\pi}_{\lambda,0} \,. \end{align} \end{split}\]

The helicity couplings in the particle-2 convention obey simple properties with respect to the parity transformation:

\[ \begin{align} \mathcal{H}^{A\to BC}_{-\lambda,-\lambda'} = P_A P_B P_C (-)^{j_A-j_B-j_C} \mathcal{H}^{A\to BC}_{\lambda,\lambda'} \end{align} \]

It reduced amount of the couplings in the strong decay of isobars. Moreover the magnitude of the couplings cannot be determined separately, therefore, it is set to 1:

\[\begin{split} \begin{align} \mathcal{H}^{\Lambda^{**} \to K p}_{0,1/2} &= 1\,, & \mathcal{H}^{\Delta^{**} \to p\pi}_{1/2,0} &= 1\,,& \mathcal{H}^{K^{**} \to \pi K}_{0,0} &= 1, \\ \mathcal{H}^{\Lambda^{**} \to K p}_{0,-1/2} &= -P_\Lambda (-)^{j-1/2}\,, & \mathcal{H}^{\Delta^{**} \to p\pi}_{-1/2,0} &= -P_\Delta (-)^{j-1/2}\,, && \end{align} \end{split}\]

The helicity couplings for the \(\Lambda_c^+\) decay are fit parameters. There are four of them for the \(K^{**}\) chain, and two for both the \(\Delta^{**}\) and \(\Lambda^{**}\) chains.

Resonances and LS-scheme#

Hide code cell source

Λc = PDG["Lambda(c)+"]
p = PDG["p"]
K = PDG["K-"]
π = PDG["pi+"]
decay_products = {
    1: (π, K),
    2: (p, K),
    3: (p, π),
}
siblings = {
    1: p,
    2: π,
    3: K,
}
chain_ids = {
    1: "K",
    2: "L",
    3: "D",
}
chain_labels = {
    1: "K^{**}",
    2: R"\Lambda^{**}",
    3: R"\Delta^{**}",
}

Resonance choices and their \(LS\)-couplings are defined as follows:

resonance_names = {
    1: ["K*(892)0"],
    2: ["Lambda(1520)", "Lambda(1670)"],
    3: ["Delta(1232)++"],
}

Hide code cell source

@frozen
class Resonance:
    particle: Particle
    l_R: int
    l_Λc: int

    @staticmethod
    def generate_ls(particle: Particle, chain_id: int) -> "Resonance":
        LS_prod = generate_ls(Λc, particle, siblings[chain_id], strong=False)
        LS_prod = [L for L, S in LS_prod]
        LS_dec = generate_ls(particle, *decay_products[chain_id])
        LS_dec = [L for L, S in LS_dec]
        return Resonance(particle, l_R=min(LS_dec), l_Λc=min(LS_prod))


def generate_ls(
    parent: Particle,
    child1: Particle,
    child2: Particle,
    strong: bool = True,
    max_L: int = 3,
):
    s1 = child1.spin
    s2 = child2.spin
    s_values = arange(abs(s1 - s2), s1 + s2)
    LS_values = set()
    for S in s_values:
        for L in arange(0, max_L):
            if not abs(L - S) <= parent.spin <= L + S:
                continue
            η0, η1, η2 = [
                int(parent.parity),
                int(child1.parity),
                int(child2.parity),
            ]
            if strong and η0 != η1 * η2 * (-1) ** L:
                continue
            LS_values.add((L, S))
    return sorted(LS_values)


def arange(x1, x2):
    spin_range = np.arange(float(x1), +float(x2) + 0.5)
    return list(map(sp.Rational, spin_range))


resonance_particles = {
    chain_id: [PDG[name] for name in names]
    for chain_id, names in resonance_names.items()
}
ls_resonances = {
    chain_id: [Resonance.generate_ls(particle, chain_id) for particle in particles]
    for chain_id, particles in resonance_particles.items()
}


def jp(particle: Particle):
    p = "+" if particle.parity > 0 else "-"
    j = sp.Rational(particle.spin)
    return Rf"\({j}^{p}\)"


def create_html_table_row(*items, typ="td"):
    items = (f"<{typ}>{i}</{typ}>" for i in items)
    return "<tr>" + "".join(items) + "</tr>\n"


column_names = [
    "resonance",
    R"\(j^P\)",
    R"\(m\) (MeV)",
    R"\(\Gamma_0\) (MeV)",
    R"\(l_R\)",
    R"\(l_{\Lambda_c}^\mathrm{min}\)",
]
src = "<table>\n"
src += create_html_table_row(*column_names, typ="th")
for chain_id, resonance_list in ls_resonances.items():
    child1, child2 = decay_products[chain_id]
    for resonance in resonance_list:
        src += create_html_table_row(
            Rf"\({resonance.particle.latex} \to {child1.latex} {child2.latex}\)",
            jp(resonance.particle),
            int(1e3 * resonance.particle.mass),
            int(1e3 * resonance.particle.width),
            resonance.l_R,
            resonance.l_Λc,
        )
src += "</table>\n"
HTML(src)
resonance\(j^P\)\(m\) (MeV)\(\Gamma_0\) (MeV)\(l_R\)\(l_{\Lambda_c}^\mathrm{min}\)
\(K^{*}(892)^{0} \to \pi^{+} K^{-}\)\(1^-\)8954710
\(\Lambda(1520) \to p K^{-}\)\(3/2^-\)15191621
\(\Lambda(1670) \to p K^{-}\)\(1/2^-\)16743000
\(\Delta(1232)^{++} \to p \pi^{+}\)\(3/2^+\)123211711

Aligned amplitude#

Hide code cell source

A_K = sp.IndexedBase(R"A^K")
A_Λ = sp.IndexedBase(R"A^{\Lambda}")
A_Δ = sp.IndexedBase(R"A^{\Delta}")

half = sp.S.Half

ζ_0_11 = sp.Symbol(R"\zeta^0_{1(1)}", real=True)
ζ_0_21 = sp.Symbol(R"\zeta^0_{2(1)}", real=True)
ζ_0_31 = sp.Symbol(R"\zeta^0_{3(1)}", real=True)
ζ_1_11 = sp.Symbol(R"\zeta^1_{1(1)}", real=True)
ζ_1_21 = sp.Symbol(R"\zeta^1_{2(1)}", real=True)
ζ_1_31 = sp.Symbol(R"\zeta^1_{3(1)}", real=True)


def formulate_aligned_amplitude(λ_Λc, λ_p):
     = sp.Symbol(R"\nu^{\prime}", rational=True)
     = sp.Symbol(R"\lambda^{\prime}", rational=True)
    return PoolSum(
        A_K[, ] * Wigner.d(half, λ_Λc, , ζ_0_11) * Wigner.d(half, , λ_p, ζ_1_11)
        + A_Λ[, ]
        * Wigner.d(half, λ_Λc, , ζ_0_21)
        * Wigner.d(half, , λ_p, ζ_1_21)
        + A_Δ[, ]
        * Wigner.d(half, λ_Λc, , ζ_0_31)
        * Wigner.d(half, , λ_p, ζ_1_31),
        (, [-half, +half]),
        (, [-half, +half]),
    )


ν = sp.Symbol("nu")
λ = sp.Symbol("lambda")
formulate_aligned_amplitude(λ_Λc=ν, λ_p=λ)
\[\displaystyle \sum_{\lambda^{\prime}=-1/2}^{1/2} \sum_{\nu^{\prime}=-1/2}^{1/2}{A^{K}_{\nu^{\prime}, \lambda^{\prime}} d^{\frac{1}{2}}_{\lambda^{\prime},\lambda}\left(\zeta^1_{1(1)}\right) d^{\frac{1}{2}}_{\nu,\nu^{\prime}}\left(\zeta^0_{1(1)}\right) + A^{\Delta}_{\nu^{\prime}, \lambda^{\prime}} d^{\frac{1}{2}}_{\lambda^{\prime},\lambda}\left(\zeta^1_{3(1)}\right) d^{\frac{1}{2}}_{\nu,\nu^{\prime}}\left(\zeta^0_{3(1)}\right) + A^{\Lambda}_{\nu^{\prime}, \lambda^{\prime}} d^{\frac{1}{2}}_{\lambda^{\prime},\lambda}\left(\zeta^1_{2(1)}\right) d^{\frac{1}{2}}_{\nu,\nu^{\prime}}\left(\zeta^0_{2(1)}\right)}\]

Dynamics#

The lineshape function is factored out of the \(\Lambda_c\) helicity coupling:

\[ \begin{align} \mathcal{H}^{\Lambda_c \to R x}_{\lambda,\lambda'} = \hat{\mathcal{H}}^{\Lambda_c \to R x}_{\lambda,\lambda'}\,\mathcal{R}(s)\,. \end{align} \]

The relativistic Breit-Wigner parametrization reads:

\[ \begin{align} \mathcal{R}(s) = \left(\frac{q}{q_0}\right)^{l_{\Lambda_c}^\text{min}} \frac{F_{l_{\Lambda_c}^\text{min}}(q R)}{F_{l_{\Lambda_c}^\text{min}}(q_0 R)}\, \frac{1}{m^2-s-im\Gamma(s)} \left(\frac{p}{p_0}\right)^{l_R} \frac{F_{l_R}(pR)}{F_{l_R}(p_0R)}, \end{align} \]

with energy-dependent width given by

\[ \begin{align} \Gamma(s) = \Gamma_0 \left(\frac{p}{p_0}\right)^{2l_R+1} \frac{m}{\sqrt{s}} \, \frac{F_{l_R}^2(pR)}{F_{l_R}^2(p_0R)}\,, \end{align} \]

The form-factor \(F\) is the Blatt-Weisskopf factor with the length factor \(R=5\,\)GeV\(^{-1}\):

\[ \begin{align} F_0(pR) &= 1\,,& F_1(pR) &= \sqrt{\frac{1}{1+(pR)^2}}\,,& F_2(pR) &= \sqrt{\frac{1}{9+3(pR)^2+(pR)^4}}\,. \end{align} \]

The break-up momenta is calculated for every decay chain separately. Using the notations \(0->R(\to ij) k\), one writes:

\[ \begin{align} p &= \lambda^{1/2}(s,m_i^2,m_j^2)/(2\sqrt{s})\,, & q &= \lambda^{1/2}(s,m_0^2,m_k^2)/(2m_0)\,. \end{align} \]

The momenta with subindex zero are computed for nominal mass of the resonance, \(s=m^2\). The three-argument Källén function reads:

(2)#\[ \begin{align} \lambda(x,y,z) = x^2+y^2+z^2 - 2xy-2yz-2zx\,. \end{align} \]

Formulation with SymPy

Hide code cell source

@make_commutative
@implement_doit_method
class BlattWeisskopf(UnevaluatedExpression):
    def __new__(cls, z, L, **hints):
        return create_expression(cls, z, L, **hints)

    def evaluate(self):
        z, L = self.args
        cases = {
            0: 1,
            1: 1 / (1 + z**2),
            2: 1 / (9 + 3 * z**2 + z**4),
        }
        return sp.Piecewise(*[
            (sp.sqrt(expr), sp.Eq(L, l_val)) for l_val, expr in cases.items()
        ])

    def _latex(self, printer, *args):
        z, L = map(printer._print, self.args)
        return Rf"F_{{{L}}}\left({z}\right)"


z = sp.Symbol("z", positive=True)
L = sp.Symbol("L", integer=True, nonnegative=True)
latex = sp.multiline_latex(BlattWeisskopf(z, L), BlattWeisskopf(z, L).doit())
Math(latex)
\[\begin{split}\displaystyle \begin{align*} F_{L}\left(z\right) = & \begin{cases} 1 & \text{for}\: L = 0 \\\frac{1}{\sqrt{z^{2} + 1}} & \text{for}\: L = 1 \\\frac{1}{\sqrt{z^{4} + 3 z^{2} + 9}} & \text{for}\: L = 2 \end{cases} \end{align*}\end{split}\]

Hide code cell source

@make_commutative
@implement_doit_method
class Källén(UnevaluatedExpression):
    def __new__(cls, x, y, z, **hints):
        return create_expression(cls, x, y, z, **hints)

    def evaluate(self) -> sp.Expr:
        x, y, z = self.args
        return x**2 + y**2 + z**2 - 2 * x * y - 2 * y * z - 2 * z * x

    def _latex(self, printer, *args):
        x, y, z = map(printer._print, self.args)
        return Rf"\lambda\left({x}, {y}, {z}\right)"


x, y, z = sp.symbols("x:z")
display_doit(Källén(x, y, z))
\[\displaystyle \begin{eqnarray} \lambda\left(x, y, z\right) & = & x^{2} - 2 x y - 2 x z + y^{2} - 2 y z + z^{2} \end{eqnarray}\]

Hide code cell source

@make_commutative
@implement_doit_method
class P(UnevaluatedExpression):
    def __new__(cls, s, mi, mj, **hints):
        return create_expression(cls, s, mi, mj, **hints)

    def evaluate(self):
        s, mi, mj = self.args
        return sp.sqrt(Källén(s, mi**2, mj**2)) / (2 * sp.sqrt(s))

    def _latex(self, printer, *args):
        s = printer._print(self.args[0])
        return Rf"p_{{{s}}}"


@make_commutative
@implement_doit_method
class Q(UnevaluatedExpression):
    def __new__(cls, s, m0, mk, **hints):
        return create_expression(cls, s, m0, mk, **hints)

    def evaluate(self):
        s, m0, mk = self.args
        return sp.sqrt(Källén(s, m0**2, mk**2)) / (2 * m0)  # <-- not s!

    def _latex(self, printer, *args):
        s = printer._print(self.args[0])
        return Rf"q_{{{s}}}"


s, m0, mi, mj, mk = sp.symbols("s m0 m_i:k", nonnegative=True)
display_doit(P(s, mi, mj))
display_doit(Q(s, m0, mk))
\[\displaystyle \begin{eqnarray} p_{s} & = & \frac{\sqrt{\lambda\left(s, m_{i}^{2}, m_{j}^{2}\right)}}{2 \sqrt{s}} \end{eqnarray}\]
\[\displaystyle \begin{eqnarray} q_{s} & = & \frac{\sqrt{\lambda\left(s, m_{0}^{2}, m_{k}^{2}\right)}}{2 m_{0}} \end{eqnarray}\]

Hide code cell source

R = sp.Symbol("R")
parameter_defaults = {
    R: 5,  # GeV^{-1} (length factor)
}


@make_commutative
@implement_doit_method
class EnergyDependentWidth(UnevaluatedExpression):
    def __new__(cls, s, m0, Γ0, m1, m2, L, R):
        return create_expression(cls, s, m0, Γ0, m1, m2, L, R)

    def evaluate(self):
        s, m0, Γ0, m1, m2, L, R = self.args
        p = P(s, m1, m2)
        p0 = P(m0**2, m1, m2)
        ff = BlattWeisskopf(p * R, L) ** 2
        ff0 = BlattWeisskopf(p0 * R, L) ** 2
        return sp.Mul(
            Γ0,
            (p / p0) ** (2 * L + 1),
            m0 / sp.sqrt(s),
            ff / ff0,
            evaluate=False,
        )

    def _latex(self, printer, *args) -> str:
        s = printer._print(self.args[0])
        return Rf"\Gamma\left({s}\right)"


l_R = sp.Symbol("l_R", integer=True, positive=True)
m, Γ0, m1, m2 = sp.symbols("m Γ0 m1 m2", nonnegative=True)
display_doit(EnergyDependentWidth(s, m, Γ0, m1, m2, l_R, R))
\[\displaystyle \begin{eqnarray} \Gamma\left(s\right) & = & Γ_{0} \frac{m}{\sqrt{s}} \frac{F_{l_{R}}\left(R p_{s}\right)^{2}}{F_{l_{R}}\left(R p_{m^{2}}\right)^{2}} \left(\frac{p_{s}}{p_{m^{2}}}\right)^{2 l_{R} + 1} \end{eqnarray}\]

Hide code cell source

@make_commutative
@implement_doit_method
class RelativisticBreitWigner(UnevaluatedExpression):
    def __new__(cls, s, m0, Γ0, m1, m2, l_R, l_Λc, R):
        return create_expression(cls, s, m0, Γ0, m1, m2, l_R, l_Λc, R)

    def evaluate(self):
        s, m0, Γ0, m1, m2, l_R, l_Λc, R = self.args
        q = Q(s, m1, m2)
        q0 = Q(m0**2, m1, m2)
        p = P(s, m1, m2)
        p0 = P(m0**2, m1, m2)
        width = EnergyDependentWidth(s, m0, Γ0, m1, m2, l_R, R)
        return sp.Mul(
            (q / q0) ** l_Λc,
            BlattWeisskopf(q * R, l_Λc) / BlattWeisskopf(q0 * R, l_Λc),
            1 / (m0**2 - s - sp.I * m0 * width),
            (p / p0) ** l_R,
            BlattWeisskopf(p * R, l_R) / BlattWeisskopf(p0 * R, l_R),
            evaluate=False,
        )

    def _latex(self, printer, *args) -> str:
        s = printer._print(self.args[0])
        return Rf"\mathcal{{R}}\left({s}\right)"


l_Λc = sp.Symbol(R"l_{\Lambda_c}", integer=True, positive=True)
display_doit(RelativisticBreitWigner(s, m, Γ0, m1, m2, l_R, l_Λc, R))
\[\displaystyle \begin{eqnarray} \mathcal{R}\left(s\right) & = & \frac{\frac{F_{l_{R}}\left(R p_{s}\right)}{F_{l_{R}}\left(R p_{m^{2}}\right)} \frac{F_{l_{\Lambda_c}}\left(R q_{s}\right)}{F_{l_{\Lambda_c}}\left(R q_{m^{2}}\right)} \left(\frac{p_{s}}{p_{m^{2}}}\right)^{l_{R}} \left(\frac{q_{s}}{q_{m^{2}}}\right)^{l_{\Lambda_c}}}{m^{2} - i m \Gamma\left(s\right) - s} \end{eqnarray}\]

Decay chain amplitudes#

Hide code cell source

def formulate_chain_amplitude(chain_id: int, λ_Λc, λ_p):
    resonances = ls_resonances[chain_id]
    if chain_id == 1:
        return formulate_K_amplitude(λ_Λc, λ_p, resonances)
    if chain_id == 2:
        return formulate_Λ_amplitude(λ_Λc, λ_p, resonances)
    if chain_id == 3:
        return formulate_Δ_amplitude(λ_Λc, λ_p, resonances)
    raise NotImplementedError


H_prod = sp.IndexedBase(R"\mathcal{H}^\mathrm{production}")
H_dec = sp.IndexedBase(R"\mathcal{H}^\mathrm{decay}")

θ23 = sp.Symbol("theta23", real=True)
θ31 = sp.Symbol("theta31", real=True)
θ12 = sp.Symbol("theta12", real=True)

σ1, σ2, σ3 = sp.symbols("sigma1:4", nonnegative=True)
m1, m2, m3 = sp.symbols(R"m_p m_pi m_K", nonnegative=True)


def formulate_K_amplitude(λ_Λc, λ_p, resonances: list[Resonance]):
    τ = sp.Symbol("tau", rational=True)
    return sp.Add(*[
        PoolSum(
            sp.KroneckerDelta(λ_Λc, τ - λ_p)
            * H_prod[stringify(res), τ, λ_p]
            * formulate_dynamics(res, σ1, m2, m3)
            * (-1) ** (half - λ_p)
            * Wigner.d(sp.Rational(res.particle.spin), τ, 0, θ23)
            * H_dec[stringify(res), 0, 0],
            (τ, create_spin_range(res.particle.spin)),
        )
        for res in resonances
    ])


def formulate_Λ_amplitude(λ_Λc, λ_p, resonances: list[Resonance]):
    τ = sp.Symbol("tau", rational=True)
    return sp.Add(*[
        PoolSum(
            sp.KroneckerDelta(λ_Λc, τ)
            * H_prod[stringify(res), τ, 0]
            * formulate_dynamics(res, σ2, m1, m3)
            * Wigner.d(sp.Rational(res.particle.spin), τ, -λ_p, θ31)
            * H_dec[stringify(res), 0, λ_p]
            * (-1) ** (half - λ_p),
            (τ, create_spin_range(res.particle.spin)),
        )
        for res in resonances
    ])


def formulate_Δ_amplitude(λ_Λc, λ_p, resonances: list[Resonance]):
    τ = sp.Symbol("tau", rational=True)
    return sp.Add(*[
        PoolSum(
            sp.KroneckerDelta(λ_Λc, τ)
            * H_prod[stringify(res), τ, 0]
            * formulate_dynamics(res, σ3, m1, m2)
            * Wigner.d(sp.Rational(res.particle.spin), τ, λ_p, θ12)
            * H_dec[stringify(res), λ_p, 0],
            (τ, create_spin_range(res.particle.spin)),
        )
        for res in resonances
    ])


def formulate_dynamics(decay: Resonance, s, m1, m2):
    l_R = sp.Rational(decay.l_R)
    l_Λc = sp.Rational(decay.l_Λc)
    mass = sp.Symbol(f"m_{{{decay.particle.latex}}}")
    width = sp.Symbol(Rf"\Gamma_{{{decay.particle.latex}}}")
    parameter_defaults[mass] = decay.particle.mass
    parameter_defaults[width] = decay.particle.width
    return RelativisticBreitWigner(s, mass, width, m1, m2, l_R, l_Λc, R)


def stringify(particle: Particle | Resonance) -> Str:
    if isinstance(particle, Resonance):
        particle = particle.particle
    return Str(particle.latex)


def create_spin_range(j):
    return arange(-j, +j)


display(
    formulate_chain_amplitude(1, ν, λ),
    formulate_chain_amplitude(2, ν, λ),
    formulate_chain_amplitude(3, ν, λ),
)
\[\displaystyle \sum_{\tau=-1}^{1}{\left(-1\right)^{\frac{1}{2} - \lambda} \delta_{\nu, - \lambda + \tau} \mathcal{H}^\mathrm{decay}_{K^{*}(892)^{0}, 0, 0} \mathcal{H}^\mathrm{production}_{K^{*}(892)^{0}, \tau, \lambda} \mathcal{R}\left(\sigma_{1}\right) d^{1}_{\tau,0}\left(\theta_{23}\right)}\]
\[\displaystyle \sum_{\tau=-3/2}^{3/2}{\left(-1\right)^{\frac{1}{2} - \lambda} \delta_{\nu \tau} \mathcal{H}^\mathrm{decay}_{\Lambda(1520), 0, \lambda} \mathcal{H}^\mathrm{production}_{\Lambda(1520), \tau, 0} \mathcal{R}\left(\sigma_{2}\right) d^{\frac{3}{2}}_{\tau,- \lambda}\left(\theta_{31}\right)} + \sum_{\tau=-1/2}^{1/2}{\left(-1\right)^{\frac{1}{2} - \lambda} \delta_{\nu \tau} \mathcal{H}^\mathrm{decay}_{\Lambda(1670), 0, \lambda} \mathcal{H}^\mathrm{production}_{\Lambda(1670), \tau, 0} \mathcal{R}\left(\sigma_{2}\right) d^{\frac{1}{2}}_{\tau,- \lambda}\left(\theta_{31}\right)}\]
\[\displaystyle \sum_{\tau=-3/2}^{3/2}{\delta_{\nu \tau} \mathcal{H}^\mathrm{decay}_{\Delta(1232)^{++}, \lambda, 0} \mathcal{H}^\mathrm{production}_{\Delta(1232)^{++}, \tau, 0} \mathcal{R}\left(\sigma_{3}\right) d^{\frac{3}{2}}_{\tau,\lambda}\left(\theta_{12}\right)}\]

Angle definitions#

Angles with repeated lower indices are trivial. The other angles are computed from the invariants and these are the angles with sign

\[\begin{split} \begin{align} \zeta_{1(1)}^{0} &= \hat{\theta}_{1(1)}^{0} = 0\,, & \zeta_{1(1)}^{1} &= 0\,,\\ \zeta_{2(1)}^0 &=\hat{\theta}_{2(1)} =-\hat{\theta}_{1(2)}\,,\\ \zeta_{3(1)}^0 &= \hat{\theta}_{3(1)}\,, &\zeta_{3(1)}^1 &= -\zeta_{1(3)}^1\,, \end{align} \end{split}\]

The expressions for the cosine of the positive (anticlockwise) angles, \(\theta_{12}, \theta_{23}, \theta_{13}\) and \(\hat\theta_{1(2)}, \hat\theta_{3(1)}, \zeta^1_{1(3)}\) can be expressed in terms of Mandelstam variables \(\sigma_1, \sigma_2, \sigma_3\) using [Mikhasenko et al., 2020], Appendix A:

Hide code cell source

m0 = sp.Symbol(R"m_{\Lambda_c}", nonnegative=True)
angles = {
    θ12: sp.acos(
        (2 * σ3 * (σ2 - m3**2 - m1**2) - (σ3 + m1**2 - m2**2) * (m0**2 - σ3 - m3**2))
        / (sp.sqrt(Källén(m0**2, m3**2, σ3)) * sp.sqrt(Källén(σ3, m1**2, m2**2)))
    ),
    θ23: sp.acos(
        (2 * σ1 * (σ3 - m1**2 - m2**2) - (σ1 + m2**2 - m3**2) * (m0**2 - σ1 - m1**2))
        / (sp.sqrt(Källén(m0**2, m1**2, σ1)) * sp.sqrt(Källén(σ1, m2**2, m3**2)))
    ),
    θ31: sp.acos(
        (2 * σ2 * (σ1 - m2**2 - m3**2) - (σ2 + m3**2 - m1**2) * (m0**2 - σ2 - m2**2))
        / (sp.sqrt(Källén(m0**2, m2**2, σ2)) * sp.sqrt(Källén(σ2, m3**2, m1**2)))
    ),
    ζ_0_11: sp.S.Zero,  # = \hat\theta^0_{1(1)}
    ζ_0_21: -sp.acos(  # = -\hat\theta^{1(2)}
        ((m0**2 + m1**2 - σ1) * (m0**2 + m2**2 - σ2) - 2 * m0**2 * (σ3 - m1**2 - m2**2))
        / (sp.sqrt(Källén(m0**2, m2**2, σ2)) * sp.sqrt(Källén(m0**2, σ1, m1**2)))
    ),
    ζ_0_31: sp.acos(  # = \hat\theta^{3(1)}
        ((m0**2 + m3**2 - σ3) * (m0**2 + m1**2 - σ1) - 2 * m0**2 * (σ2 - m3**2 - m1**2))
        / (sp.sqrt(Källén(m0**2, m1**2, σ1)) * sp.sqrt(Källén(m0**2, σ3, m3**2)))
    ),
    ζ_1_11: sp.S.Zero,
    ζ_1_21: sp.acos(
        (2 * m1**2 * (σ3 - m0**2 - m3**2) + (m0**2 + m1**2 - σ1) * (σ2 - m1**2 - m3**2))
        / (sp.sqrt(Källén(m0**2, m1**2, σ1)) * sp.sqrt(Källén(σ2, m1**2, m3**2)))
    ),
    ζ_1_31: -sp.acos(  # = -\zeta^1_{1(3)}
        (2 * m1**2 * (σ2 - m0**2 - m2**2) + (m0**2 + m1**2 - σ1) * (σ3 - m1**2 - m2**2))
        / (sp.sqrt(Källén(m0**2, m1**2, σ1)) * sp.sqrt(Källén(σ3, m1**2, m2**2)))
    ),
}

display_definitions(angles)
\[\begin{split}\displaystyle \begin{array}{rcl} \theta_{12} & = & \operatorname{acos}{\left(\frac{2 \sigma_{3} \left(- m_{K}^{2} - m_{p}^{2} + \sigma_{2}\right) - \left(- m_{K}^{2} + m_{\Lambda_c}^{2} - \sigma_{3}\right) \left(m_{p}^{2} - m_{\pi}^{2} + \sigma_{3}\right)}{\sqrt{\lambda\left(m_{\Lambda_c}^{2}, m_{K}^{2}, \sigma_{3}\right)} \sqrt{\lambda\left(\sigma_{3}, m_{p}^{2}, m_{\pi}^{2}\right)}} \right)} \\ \theta_{23} & = & \operatorname{acos}{\left(\frac{2 \sigma_{1} \left(- m_{p}^{2} - m_{\pi}^{2} + \sigma_{3}\right) - \left(- m_{K}^{2} + m_{\pi}^{2} + \sigma_{1}\right) \left(- m_{p}^{2} + m_{\Lambda_c}^{2} - \sigma_{1}\right)}{\sqrt{\lambda\left(m_{\Lambda_c}^{2}, m_{p}^{2}, \sigma_{1}\right)} \sqrt{\lambda\left(\sigma_{1}, m_{\pi}^{2}, m_{K}^{2}\right)}} \right)} \\ \theta_{31} & = & \operatorname{acos}{\left(\frac{2 \sigma_{2} \left(- m_{K}^{2} - m_{\pi}^{2} + \sigma_{1}\right) - \left(m_{K}^{2} - m_{p}^{2} + \sigma_{2}\right) \left(- m_{\pi}^{2} + m_{\Lambda_c}^{2} - \sigma_{2}\right)}{\sqrt{\lambda\left(m_{\Lambda_c}^{2}, m_{\pi}^{2}, \sigma_{2}\right)} \sqrt{\lambda\left(\sigma_{2}, m_{K}^{2}, m_{p}^{2}\right)}} \right)} \\ \zeta^0_{1(1)} & = & 0 \\ \zeta^0_{2(1)} & = & - \operatorname{acos}{\left(\frac{- 2 m_{\Lambda_c}^{2} \left(- m_{p}^{2} - m_{\pi}^{2} + \sigma_{3}\right) + \left(m_{p}^{2} + m_{\Lambda_c}^{2} - \sigma_{1}\right) \left(m_{\pi}^{2} + m_{\Lambda_c}^{2} - \sigma_{2}\right)}{\sqrt{\lambda\left(m_{\Lambda_c}^{2}, m_{\pi}^{2}, \sigma_{2}\right)} \sqrt{\lambda\left(m_{\Lambda_c}^{2}, \sigma_{1}, m_{p}^{2}\right)}} \right)} \\ \zeta^0_{3(1)} & = & \operatorname{acos}{\left(\frac{- 2 m_{\Lambda_c}^{2} \left(- m_{K}^{2} - m_{p}^{2} + \sigma_{2}\right) + \left(m_{K}^{2} + m_{\Lambda_c}^{2} - \sigma_{3}\right) \left(m_{p}^{2} + m_{\Lambda_c}^{2} - \sigma_{1}\right)}{\sqrt{\lambda\left(m_{\Lambda_c}^{2}, m_{p}^{2}, \sigma_{1}\right)} \sqrt{\lambda\left(m_{\Lambda_c}^{2}, \sigma_{3}, m_{K}^{2}\right)}} \right)} \\ \zeta^1_{1(1)} & = & 0 \\ \zeta^1_{2(1)} & = & \operatorname{acos}{\left(\frac{2 m_{p}^{2} \left(- m_{K}^{2} - m_{\Lambda_c}^{2} + \sigma_{3}\right) + \left(- m_{K}^{2} - m_{p}^{2} + \sigma_{2}\right) \left(m_{p}^{2} + m_{\Lambda_c}^{2} - \sigma_{1}\right)}{\sqrt{\lambda\left(m_{\Lambda_c}^{2}, m_{p}^{2}, \sigma_{1}\right)} \sqrt{\lambda\left(\sigma_{2}, m_{p}^{2}, m_{K}^{2}\right)}} \right)} \\ \zeta^1_{3(1)} & = & - \operatorname{acos}{\left(\frac{2 m_{p}^{2} \left(- m_{\pi}^{2} - m_{\Lambda_c}^{2} + \sigma_{2}\right) + \left(- m_{p}^{2} - m_{\pi}^{2} + \sigma_{3}\right) \left(m_{p}^{2} + m_{\Lambda_c}^{2} - \sigma_{1}\right)}{\sqrt{\lambda\left(m_{\Lambda_c}^{2}, m_{p}^{2}, \sigma_{1}\right)} \sqrt{\lambda\left(\sigma_{3}, m_{p}^{2}, m_{\pi}^{2}\right)}} \right)} \\ \end{array}\end{split}\]

where \(m_0\) is the mass of the initial state \(\Lambda_c\) and \(m_1, m_2, m_3\) are the masses of \(p, \pi, K\), respectively:

Hide code cell source

masses = {
    m0: Λc.mass,
    m1: p.mass,
    m2: π.mass,
    m3: K.mass,
}
parameter_defaults.update(masses)
display_definitions(masses)
\[\begin{split}\displaystyle \begin{array}{rcl} m_{\Lambda_c} & = & 2.28646 \\ m_{p} & = & 0.938272081 \\ m_{\pi} & = & 0.13957039 \\ m_{K} & = & 0.493677 \\ \end{array}\end{split}\]

Helicity coupling values#

Hide code cell source

dec_couplings = {}
for res in ls_resonances[1]:
    i = stringify(res)
    dec_couplings[H_dec[i, 0, 0]] = 1
for res in ls_resonances[2]:
    i = stringify(res.particle)
    dec_couplings[H_dec[i, 0, half]] = 1
    dec_couplings[H_dec[i, 0, -half]] = int(
        int(res.particle.parity)
        * int(K.parity)
        * int(p.parity)
        * (-1) ** (res.particle.spin - K.spin - p.spin)
    )
for res in ls_resonances[3]:
    i = stringify(res.particle)
    dec_couplings[H_dec[i, half, 0]] = 1
    dec_couplings[H_dec[i, -half, 0]] = int(
        int(res.particle.parity)
        * int(p.parity)
        * int(π.parity)
        * (-1) ** (res.particle.spin - p.spin - π.spin)
    )
parameter_defaults.update(dec_couplings)
display_definitions(dec_couplings)
\[\begin{split}\displaystyle \begin{array}{rcl} \mathcal{H}^\mathrm{decay}_{K^{*}(892)^{0}, 0, 0} & = & 1 \\ \mathcal{H}^\mathrm{decay}_{\Lambda(1520), 0, \frac{1}{2}} & = & 1 \\ \mathcal{H}^\mathrm{decay}_{\Lambda(1520), 0, - \frac{1}{2}} & = & -1 \\ \mathcal{H}^\mathrm{decay}_{\Lambda(1670), 0, \frac{1}{2}} & = & 1 \\ \mathcal{H}^\mathrm{decay}_{\Lambda(1670), 0, - \frac{1}{2}} & = & 1 \\ \mathcal{H}^\mathrm{decay}_{\Delta(1232)^{++}, \frac{1}{2}, 0} & = & 1 \\ \mathcal{H}^\mathrm{decay}_{\Delta(1232)^{++}, - \frac{1}{2}, 0} & = & 1 \\ \end{array}\end{split}\]

Hide code cell source

prod_couplings = {
    # chain 23:
    H_prod[Str("K^{*}(892)^{0}"), 0, -half]: 1,
    H_prod[Str("K^{*}(892)^{0}"), -1, -half]: 1 - 1j,
    H_prod[Str("K^{*}(892)^{0}"), +1, +half]: -3 - 3j,
    H_prod[Str("K^{*}(892)^{0}"), 0, +half]: -1 - 4j,
    H_prod[Str("K_{0}^{*}(1430)^{0}"), 0, -half]: 1,
    H_prod[Str("K_{0}^{*}(1430)^{0}"), -1, -half]: 1 - 1j,
    H_prod[Str("K_{0}^{*}(1430)^{0}"), +1, +half]: -3 - 3j,
    H_prod[Str("K_{0}^{*}(1430)^{0}"), 0, +half]: -1 - 4j,
    H_prod[Str("K_{2}^{*}(1430)^{0}"), 0, -half]: 1,
    H_prod[Str("K_{2}^{*}(1430)^{0}"), -1, -half]: 1 - 1j,
    H_prod[Str("K_{2}^{*}(1430)^{0}"), +1, +half]: -3 - 3j,
    H_prod[Str("K_{2}^{*}(1430)^{0}"), 0, +half]: -1 - 4j,
    #
    # chain 31:
    H_prod[Str(R"\Lambda(1520)"), +half, 0]: 1.5,
    H_prod[Str(R"\Lambda(1520)"), -half, 0]: 0.3,
    H_prod[Str(R"\Lambda(1670)"), +half, 0]: -0.5 + 1j,
    H_prod[Str(R"\Lambda(1670)"), -half, 0]: -0.3 - 0.1j,
    # chain 12:
    H_prod[Str(R"\Delta(1232)^{++}"), +half, 0]: -13 + 5j,
    H_prod[Str(R"\Delta(1232)^{++}"), -half, 0]: -7 + 3j,
}
display_definitions(prod_couplings)
couplings = dict(dec_couplings)
couplings.update(prod_couplings)
parameter_defaults.update(prod_couplings)
\[\begin{split}\displaystyle \begin{array}{rcl} \mathcal{H}^\mathrm{production}_{K^{*}(892)^{0}, 0, - \frac{1}{2}} & = & 1 \\ \mathcal{H}^\mathrm{production}_{K^{*}(892)^{0}, -1, - \frac{1}{2}} & = & 1.0 - 1.0 i \\ \mathcal{H}^\mathrm{production}_{K^{*}(892)^{0}, 1, \frac{1}{2}} & = & -3.0 - 3.0 i \\ \mathcal{H}^\mathrm{production}_{K^{*}(892)^{0}, 0, \frac{1}{2}} & = & -1.0 - 4.0 i \\ \mathcal{H}^\mathrm{production}_{K_{0}^{*}(1430)^{0}, 0, - \frac{1}{2}} & = & 1 \\ \mathcal{H}^\mathrm{production}_{K_{0}^{*}(1430)^{0}, -1, - \frac{1}{2}} & = & 1.0 - 1.0 i \\ \mathcal{H}^\mathrm{production}_{K_{0}^{*}(1430)^{0}, 1, \frac{1}{2}} & = & -3.0 - 3.0 i \\ \mathcal{H}^\mathrm{production}_{K_{0}^{*}(1430)^{0}, 0, \frac{1}{2}} & = & -1.0 - 4.0 i \\ \mathcal{H}^\mathrm{production}_{K_{2}^{*}(1430)^{0}, 0, - \frac{1}{2}} & = & 1 \\ \mathcal{H}^\mathrm{production}_{K_{2}^{*}(1430)^{0}, -1, - \frac{1}{2}} & = & 1.0 - 1.0 i \\ \mathcal{H}^\mathrm{production}_{K_{2}^{*}(1430)^{0}, 1, \frac{1}{2}} & = & -3.0 - 3.0 i \\ \mathcal{H}^\mathrm{production}_{K_{2}^{*}(1430)^{0}, 0, \frac{1}{2}} & = & -1.0 - 4.0 i \\ \mathcal{H}^\mathrm{production}_{\Lambda(1520), \frac{1}{2}, 0} & = & 1.5 \\ \mathcal{H}^\mathrm{production}_{\Lambda(1520), - \frac{1}{2}, 0} & = & 0.3 \\ \mathcal{H}^\mathrm{production}_{\Lambda(1670), \frac{1}{2}, 0} & = & -0.5 + 1.0 i \\ \mathcal{H}^\mathrm{production}_{\Lambda(1670), - \frac{1}{2}, 0} & = & -0.3 - 0.1 i \\ \mathcal{H}^\mathrm{production}_{\Delta(1232)^{++}, \frac{1}{2}, 0} & = & -13.0 + 5.0 i \\ \mathcal{H}^\mathrm{production}_{\Delta(1232)^{++}, - \frac{1}{2}, 0} & = & -7.0 + 3.0 i \\ \end{array}\end{split}\]

Intensity expression#

Incoherent sum of the amplitudes defined by Aligned amplitude:

Hide code cell source

intensity_expr = PoolSum(
    sp.Abs(formulate_aligned_amplitude(ν, λ)) ** 2,
    (λ, [-half, +half]),
    (ν, [-half, +half]),
)
intensity_expr
\[\displaystyle \sum_{\lambda=-1/2}^{1/2} \sum_{\nu=-1/2}^{1/2}{\left|{\sum_{\lambda^{\prime}=-1/2}^{1/2} \sum_{\nu^{\prime}=-1/2}^{1/2}{A^{K}_{\nu^{\prime}, \lambda^{\prime}} d^{\frac{1}{2}}_{\lambda^{\prime},\lambda}\left(\zeta^1_{1(1)}\right) d^{\frac{1}{2}}_{\nu,\nu^{\prime}}\left(\zeta^0_{1(1)}\right) + A^{\Delta}_{\nu^{\prime}, \lambda^{\prime}} d^{\frac{1}{2}}_{\lambda^{\prime},\lambda}\left(\zeta^1_{3(1)}\right) d^{\frac{1}{2}}_{\nu,\nu^{\prime}}\left(\zeta^0_{3(1)}\right) + A^{\Lambda}_{\nu^{\prime}, \lambda^{\prime}} d^{\frac{1}{2}}_{\lambda^{\prime},\lambda}\left(\zeta^1_{2(1)}\right) d^{\frac{1}{2}}_{\nu,\nu^{\prime}}\left(\zeta^0_{2(1)}\right)}}\right|^{2}}\]

Remaining free_symbols are indeed the specific amplitudes as defined by Decay chain amplitudes:

The specific amplitudes from Decay chain amplitudes need to be formulated for each value of \(\nu, \lambda\), so that they can be substituted in the top expression:

Hide code cell source

A = {1: A_K, 2: A_Λ, 3: A_Δ}
amp_definitions = {}
for chain_id in chain_ids:
    for Λc_heli, p_heli in itertools.product([-half, +half], [-half, +half]):
        symbol = A[chain_id][Λc_heli, p_heli]
        expr = formulate_chain_amplitude(chain_id, ν, λ)
        amp_definitions[symbol] = expr.subs({ν: Λc_heli, λ: p_heli})
display_definitions(amp_definitions)
\[\begin{split}\displaystyle \begin{array}{rcl} A^{K}_{- \frac{1}{2}, - \frac{1}{2}} & = & \sum_{\tau=-1}^{1}{- \delta_{- \frac{1}{2}, \tau + \frac{1}{2}} \mathcal{H}^\mathrm{decay}_{K^{*}(892)^{0}, 0, 0} \mathcal{H}^\mathrm{production}_{K^{*}(892)^{0}, \tau, - \frac{1}{2}} \mathcal{R}\left(\sigma_{1}\right) d^{1}_{\tau,0}\left(\theta_{23}\right)} \\ A^{K}_{- \frac{1}{2}, \frac{1}{2}} & = & \sum_{\tau=-1}^{1}{\delta_{- \frac{1}{2}, \tau - \frac{1}{2}} \mathcal{H}^\mathrm{decay}_{K^{*}(892)^{0}, 0, 0} \mathcal{H}^\mathrm{production}_{K^{*}(892)^{0}, \tau, \frac{1}{2}} \mathcal{R}\left(\sigma_{1}\right) d^{1}_{\tau,0}\left(\theta_{23}\right)} \\ A^{K}_{\frac{1}{2}, - \frac{1}{2}} & = & \sum_{\tau=-1}^{1}{- \delta_{\frac{1}{2}, \tau + \frac{1}{2}} \mathcal{H}^\mathrm{decay}_{K^{*}(892)^{0}, 0, 0} \mathcal{H}^\mathrm{production}_{K^{*}(892)^{0}, \tau, - \frac{1}{2}} \mathcal{R}\left(\sigma_{1}\right) d^{1}_{\tau,0}\left(\theta_{23}\right)} \\ A^{K}_{\frac{1}{2}, \frac{1}{2}} & = & \sum_{\tau=-1}^{1}{\delta_{\frac{1}{2}, \tau - \frac{1}{2}} \mathcal{H}^\mathrm{decay}_{K^{*}(892)^{0}, 0, 0} \mathcal{H}^\mathrm{production}_{K^{*}(892)^{0}, \tau, \frac{1}{2}} \mathcal{R}\left(\sigma_{1}\right) d^{1}_{\tau,0}\left(\theta_{23}\right)} \\ A^{\Lambda}_{- \frac{1}{2}, - \frac{1}{2}} & = & \sum_{\tau=-3/2}^{3/2}{- \delta_{- \frac{1}{2} \tau} \mathcal{H}^\mathrm{decay}_{\Lambda(1520), 0, - \frac{1}{2}} \mathcal{H}^\mathrm{production}_{\Lambda(1520), \tau, 0} \mathcal{R}\left(\sigma_{2}\right) d^{\frac{3}{2}}_{\tau,\frac{1}{2}}\left(\theta_{31}\right)} + \sum_{\tau=-1/2}^{1/2}{- \delta_{- \frac{1}{2} \tau} \mathcal{H}^\mathrm{decay}_{\Lambda(1670), 0, - \frac{1}{2}} \mathcal{H}^\mathrm{production}_{\Lambda(1670), \tau, 0} \mathcal{R}\left(\sigma_{2}\right) d^{\frac{1}{2}}_{\tau,\frac{1}{2}}\left(\theta_{31}\right)} \\ A^{\Lambda}_{- \frac{1}{2}, \frac{1}{2}} & = & \sum_{\tau=-3/2}^{3/2}{\delta_{- \frac{1}{2} \tau} \mathcal{H}^\mathrm{decay}_{\Lambda(1520), 0, \frac{1}{2}} \mathcal{H}^\mathrm{production}_{\Lambda(1520), \tau, 0} \mathcal{R}\left(\sigma_{2}\right) d^{\frac{3}{2}}_{\tau,- \frac{1}{2}}\left(\theta_{31}\right)} + \sum_{\tau=-1/2}^{1/2}{\delta_{- \frac{1}{2} \tau} \mathcal{H}^\mathrm{decay}_{\Lambda(1670), 0, \frac{1}{2}} \mathcal{H}^\mathrm{production}_{\Lambda(1670), \tau, 0} \mathcal{R}\left(\sigma_{2}\right) d^{\frac{1}{2}}_{\tau,- \frac{1}{2}}\left(\theta_{31}\right)} \\ A^{\Lambda}_{\frac{1}{2}, - \frac{1}{2}} & = & \sum_{\tau=-3/2}^{3/2}{- \delta_{\frac{1}{2} \tau} \mathcal{H}^\mathrm{decay}_{\Lambda(1520), 0, - \frac{1}{2}} \mathcal{H}^\mathrm{production}_{\Lambda(1520), \tau, 0} \mathcal{R}\left(\sigma_{2}\right) d^{\frac{3}{2}}_{\tau,\frac{1}{2}}\left(\theta_{31}\right)} + \sum_{\tau=-1/2}^{1/2}{- \delta_{\frac{1}{2} \tau} \mathcal{H}^\mathrm{decay}_{\Lambda(1670), 0, - \frac{1}{2}} \mathcal{H}^\mathrm{production}_{\Lambda(1670), \tau, 0} \mathcal{R}\left(\sigma_{2}\right) d^{\frac{1}{2}}_{\tau,\frac{1}{2}}\left(\theta_{31}\right)} \\ A^{\Lambda}_{\frac{1}{2}, \frac{1}{2}} & = & \sum_{\tau=-3/2}^{3/2}{\delta_{\frac{1}{2} \tau} \mathcal{H}^\mathrm{decay}_{\Lambda(1520), 0, \frac{1}{2}} \mathcal{H}^\mathrm{production}_{\Lambda(1520), \tau, 0} \mathcal{R}\left(\sigma_{2}\right) d^{\frac{3}{2}}_{\tau,- \frac{1}{2}}\left(\theta_{31}\right)} + \sum_{\tau=-1/2}^{1/2}{\delta_{\frac{1}{2} \tau} \mathcal{H}^\mathrm{decay}_{\Lambda(1670), 0, \frac{1}{2}} \mathcal{H}^\mathrm{production}_{\Lambda(1670), \tau, 0} \mathcal{R}\left(\sigma_{2}\right) d^{\frac{1}{2}}_{\tau,- \frac{1}{2}}\left(\theta_{31}\right)} \\ A^{\Delta}_{- \frac{1}{2}, - \frac{1}{2}} & = & \sum_{\tau=-3/2}^{3/2}{\delta_{- \frac{1}{2} \tau} \mathcal{H}^\mathrm{decay}_{\Delta(1232)^{++}, - \frac{1}{2}, 0} \mathcal{H}^\mathrm{production}_{\Delta(1232)^{++}, \tau, 0} \mathcal{R}\left(\sigma_{3}\right) d^{\frac{3}{2}}_{\tau,- \frac{1}{2}}\left(\theta_{12}\right)} \\ A^{\Delta}_{- \frac{1}{2}, \frac{1}{2}} & = & \sum_{\tau=-3/2}^{3/2}{\delta_{- \frac{1}{2} \tau} \mathcal{H}^\mathrm{decay}_{\Delta(1232)^{++}, \frac{1}{2}, 0} \mathcal{H}^\mathrm{production}_{\Delta(1232)^{++}, \tau, 0} \mathcal{R}\left(\sigma_{3}\right) d^{\frac{3}{2}}_{\tau,\frac{1}{2}}\left(\theta_{12}\right)} \\ A^{\Delta}_{\frac{1}{2}, - \frac{1}{2}} & = & \sum_{\tau=-3/2}^{3/2}{\delta_{\frac{1}{2} \tau} \mathcal{H}^\mathrm{decay}_{\Delta(1232)^{++}, - \frac{1}{2}, 0} \mathcal{H}^\mathrm{production}_{\Delta(1232)^{++}, \tau, 0} \mathcal{R}\left(\sigma_{3}\right) d^{\frac{3}{2}}_{\tau,- \frac{1}{2}}\left(\theta_{12}\right)} \\ A^{\Delta}_{\frac{1}{2}, \frac{1}{2}} & = & \sum_{\tau=-3/2}^{3/2}{\delta_{\frac{1}{2} \tau} \mathcal{H}^\mathrm{decay}_{\Delta(1232)^{++}, \frac{1}{2}, 0} \mathcal{H}^\mathrm{production}_{\Delta(1232)^{++}, \tau, 0} \mathcal{R}\left(\sigma_{3}\right) d^{\frac{3}{2}}_{\tau,\frac{1}{2}}\left(\theta_{12}\right)} \\ \end{array}\end{split}\]

Hide code cell content

unfolded_intensity_expr = perform_cached_doit(
    perform_cached_doit(intensity_expr).xreplace(amp_definitions)
)
expr = unfolded_intensity_expr.xreplace(angles).doit()
expr = expr.xreplace(parameter_defaults)
assert expr.free_symbols == {σ1, σ2, σ3}
del expr

Polarization sensitivity#

We introduce the polarimeter vector field (polarization sensitivity) of the \(\Lambda_c\) decay. It is defined by three quantities \((\alpha_x,\alpha_y,\alpha_z)\) forming a three-dimensional vector \(\vec\alpha\) dependent on just two decay variables, \(\sigma_1=m_{K\pi}^2\), and \(\sigma_2=m_{pK}^2\).

The polarimeter vector field is computed by averaging the Pauli matrices \(\vec\sigma\) contracted with the \(\Lambda_c^+\) helicity indices given the transition amplitude.

(3)#\[ \begin{align} \vec\alpha(m_{K\pi},m_{pK}) = \sum_{\lambda,\nu,\nu'} A^{*}_{\nu,\lambda}\vec\sigma_{\nu,\nu'} A_{\nu',\lambda} \,\big / \sum_{\lambda,\nu} \left|A_{\nu,\lambda}\right|^2 \end{align} \]

The quantities \(\vec\alpha(m_{K\pi},m_{pK})\) give the model-independent representation of the \(\Lambda_c^+\) decay process. It can be used to study \(\Lambda_c^+\) production polarization using

(4)#\[ \begin{align} I(\alpha,\beta,\gamma,m_{K\pi},m_{pK}) = I_0(m_{K\pi},m_{pK})\, \left(1 + \sum_{i,j} P_i R_{ij}(\alpha,\beta,\gamma) \alpha_j(m_{K\pi},m_{pK}) \right)\,, \end{align} \]

where \(R_{ij}(\alpha,\beta,\gamma)\) is a three-dimensional rotation matrix:

\[ \begin{align} R(\alpha,\beta,\gamma) = R_z(\alpha)R_y(\beta)R_z(\gamma)\,, \end{align} \]

and \(I_0\) is the averaged decay rate

\[ \begin{align} I_0(m_{K\pi},m_{pK}) = \sum_{\lambda,\nu}\left|A_{\nu,\lambda}\right|^2\,. \end{align} \]
def to_index(helicity):
    """Symbolic conversion of half-value helicities to Pauli matrix indices."""
    # https://github.com/ComPWA/compwa.github.io/pull/129#issuecomment-1096599896
    return sp.Piecewise(
        (1, sp.LessThan(helicity, 0)),
        (0, True),
    )


ν_prime = sp.Symbol(R"\nu^{\prime}")
polarimetry_exprs = tuple(
    PoolSum(
        formulate_aligned_amplitude(ν, λ).conjugate()
        * msigma(i)[to_index(ν), to_index(ν_prime)]
        * formulate_aligned_amplitude(ν_prime, λ),
        (λ, [-half, +half]),
        (ν, [-half, +half]),
        (ν_prime, [-half, +half]),
    )
    / intensity_expr
    for i in (1, 2, 3)
)

Hide code cell source

unfolded_polarimetry_exprs = tuple(
    perform_cached_doit(perform_cached_doit(x).xreplace(amp_definitions))
    for x in polarimetry_exprs
)

Properties of the vector \(\vec\alpha\)#

The vector \(\vec \alpha\) introduced in Eq. (3) obeys the following properties:

  1. It is a three-dimensional vector defined in the rest frame of the decaying particle. Particularly, it is transformed as a regular vector in case initial (alignment) configuration change.

  2. The length of the vector is limited by 1: \(|\vec{\alpha}| < 1\)

  3. \(\alpha_y=0\) for the decays of a fermion to a fermions and (pseudo)scalar

Here is the prove of the second statement:

\[\begin{split} \begin{align} I_{\nu',\nu} = \sum_{\lambda} A_{\nu',\lambda}^* A_{\nu,\lambda} = \begin{pmatrix} a & c^*\\ c & b \end{pmatrix} = \frac{a+b}{2}\left( \mathbb{I} + (\vec{\sigma} \cdot \vec{\alpha}) \right)\,, \end{align} \end{split}\]

where

\[\begin{split} \begin{align} a &= \left|A_{+,+}\right|^2+\left|A_{+,-}\right|^2\,,\\ \nonumber b &= \left|A_{-,+}\right|^2+\left|A_{-,-}\right|^2\,,\\ \nonumber c &= A_{+,+}^*A_{-,+} + A_{+,-}^*A_{-,-}\,, \end{align} \end{split}\]

and

\[ \begin{align} \alpha_x &= \frac{\text{Re}\,c}{a+b}\,,& \alpha_x &= \frac{\text{Im}\,c}{a+b}\,,& \alpha_z &= \frac{a-b}{a+b}\,, \end{align} \]

To constraint the length of the \(\vec\alpha\), one notices \(ab - c \ge 0\). Therefore,

\[ \begin{align} |\vec\alpha|^2 &= \frac{(a-b)^2+c^2}{(a+b)^2} = \frac{(a+b)^2-4ab+c^2}{(a+b)^2} \leq \frac{(a+b)^2-3ab}{(a+b)^2} \leq 1\, \end{align} \]

since \(a,b \geq 0\).

Computations with TensorWaves#

Conversion to computational backend#

The full expression tree can be converted to a computational, parametrized function as follows. Note that identify all coupling symbols are interpreted as parameters. The remaining symbols (the angles) become arguments to the function.

free_parameters = {
    symbol: value
    for symbol, value in parameter_defaults.items()
    if (symbol.name.startswith("m_") and symbol not in masses)
    or symbol.name.startswith(R"\Gamma_")
    or symbol in prod_couplings
}
fixed_parameters = {
    symbol: value
    for symbol, value in parameter_defaults.items()
    if symbol not in free_parameters
}
intensity_func = create_parametrized_function(
    unfolded_intensity_expr.xreplace(fixed_parameters),
    parameters=free_parameters,
    backend="jax",
)
polarimetry_funcs = tuple(
    create_parametrized_function(
        expr.xreplace(fixed_parameters),
        parameters=free_parameters,
        backend="jax",
    )
    for expr in unfolded_polarimetry_exprs
)

Phase space#

The \(\Lambda_c^+ \to p K \pi\) kinematics is fully described by two dynamic variables, \(m_{K\pi}\) and \(m_{pK}\) (see Phase space for a three-body decay). The third Mandelstam variable can be computed from the other two and the masses of the initial and final state:

Hide code cell source

computed_σ3 = m0**2 + m1**2 + m2**2 + m3**2 - σ1 - σ2
compute_third_mandelstam = create_function(computed_σ3.subs(masses), backend="jax")
display_definitions({σ3: computed_σ3})
\[\begin{split}\displaystyle \begin{array}{rcl} \sigma_{3} & = & m_{K}^{2} + m_{p}^{2} + m_{\pi}^{2} + m_{\Lambda_c}^{2} - \sigma_{1} - \sigma_{2} \\ \end{array}\end{split}\]

Values for the angles will be computed form the Mandelstam values with a data transformer for the symbolic angle definitions:

kinematic_variables = {
    symbol: expression.doit().xreplace(masses).xreplace(fixed_parameters)
    for symbol, expression in angles.items()
}
kinematic_variables.update({s: s for s in [σ1, σ2, σ3]})  # include identity
transformer = SympyDataTransformer.from_sympy(kinematic_variables, backend="jax")

We now define phase space over a grid that contains the space in the Dalitz plane that is kinematically ‘available’ to the decay:

m0_val, m1_val, m2_val, m3_val = masses.values()
σ1_min = (m2_val + m3_val) ** 2
σ1_max = (m0_val - m1_val) ** 2
σ2_min = (m1_val + m3_val) ** 2
σ2_max = (m0_val - m2_val) ** 2


def generate_phsp_grid(resolution: int):
    x = np.linspace(σ1_min, σ1_max, num=resolution)
    y = np.linspace(σ2_min, σ2_max, num=resolution)
    X, Y = np.meshgrid(x, y)
    Z = compute_third_mandelstam.function(X, Y)
    phsp = {"sigma1": X, "sigma2": Y, "sigma3": Z}
    return X, Y, transformer(phsp)


X, Y, phsp = generate_phsp_grid(resolution=500)

Intensity distribution#

Finally, all intensities can be computed as follows:

Hide code cell source

s1_label = R"$\sigma_1=m^2\left(K\pi\right)$"
s2_label = R"$\sigma_2=m^2\left(pK\right)$"
s3_label = R"$\sigma_3=m^2\left(p\pi\right)$"

plt.rc("font", size=15)
fig, ax = plt.subplots(
    figsize=(9, 8),
    tight_layout=True,
)
ax.set_box_aspect(1)
ax.set_title("Intensity distribution")
ax.set_xlabel(s1_label)
ax.set_ylabel(s2_label)

total_intensities = intensity_func(phsp)
mesh = ax.pcolormesh(X, Y, total_intensities, norm=LogNorm(), rasterized=True)
fig.colorbar(mesh, ax=ax, fraction=0.05, pad=0.02)
fig.savefig("intensity-distribution.png", dpi=200)
plt.show()

Hide code cell source

def compute_sub_function(
    func: ParametrizedFunction, phsp: DataSample, non_zero_couplings: str
) -> jnp.ndarray:
    zero_couplings = {
        par: 0
        for par in func.parameters
        if par.startswith(R"\mathcal{H}")
        if "production" in par
        if not any(s in par for s in non_zero_couplings)
    }
    original_parameters = dict(func.parameters)
    func.update_parameters(zero_couplings)
    computed_values = func(phsp)
    func.update_parameters(original_parameters)
    return computed_values


def set_ylim_to_zero(ax):
    _, y_max = ax.get_ylim()
    ax.set_ylim(0, y_max)


fig, (ax1, ax2) = plt.subplots(
    ncols=2,
    figsize=(12, 5),
    sharey=True,
    tight_layout=True,
)
ax1.set_xlabel(s1_label)
ax2.set_xlabel(s2_label)
ax1.set_yticks([])

x = X[0]
y = Y[:, 0]
ax1.fill(x, np.nansum(total_intensities, axis=0), alpha=0.3)
ax2.fill(y, np.nansum(total_intensities, axis=1), alpha=0.3)
for chain_id, chain_name in chain_ids.items():
    label = f"${chain_labels[chain_id]}$"
    sub_intensities = compute_sub_function(
        intensity_func, phsp, non_zero_couplings=[chain_name]
    )
    ax1.plot(x, np.nansum(sub_intensities, axis=0), label=label)
    ax2.plot(y, np.nansum(sub_intensities, axis=1), label=label)
set_ylim_to_zero(ax1)
set_ylim_to_zero(ax2)
ax2.legend()
fig.savefig("intensity-projections.svg")
plt.show()

Fit fractions#

The total decay rate for \(\Lambda_c^+ \to pK\pi\) can be broken into fractions that correspond to the different decay chains and interference terms. The total rate is computed as an integral of the intensity over decay kinematics:

\[ \begin{align} I_\text{tot}(\{\mathcal{H}\}) = \int d m_{pK}^2 d m_{K\pi}^2\, I_0(m_{pK}, m_{K\pi} | \{\mathcal{H}\}) \approx \frac{\Phi_0}{N_\text{MC}} \sum_{e=1}^{N_\text{MC}}\,\,I_0(m_{pK,e}, m_{K\pi,e} | \{\mathcal{H}\})\,, \end{align} \]

where \(\Phi_0\) is an (irrelevant) constant equal to the flat phase-space integral, \((m_{pK,e}, m_{K\pi,e})\) is a vector of the kinematic variables for the \(e\)-th point in the MC sample.

The conditional argument \(\{\mathcal{H}\}\) indicates dependence of the rate on the value of the couplings. The individual fractions are found by computing the total rate for a subset of couplings set to zero,

\[\begin{split} \begin{align} I_\text{tot}^{K} &= I_\text{tot}\left(\{\mathcal{H}^{\Lambda_c^+\to\Delta^{**} K}, \mathcal{H}^{\Lambda_c^+\to\Lambda^{**} \pi} = 0\}\right)\,,\\ I_\text{tot}^{\Delta} &= I_\text{tot}\left(\{\mathcal{H}^{\Lambda_c^+\to K^{**} p}, \mathcal{H}^{\Lambda_c^+\to\Lambda^{**} \pi} = 0\}\right)\,,\\ I_\text{tot}^{\Lambda} &= I_\text{tot}\left(\{\mathcal{H}^{\Lambda_c^+\to\Delta^{**} K}, \mathcal{H}^{\Lambda_c^+\to K^{**} p} = 0\}\right)\,,\\ I_\text{tot}^{K/\Lambda} &= I_\text{tot}\left(\{\mathcal{H}^{\Lambda_c^+\to\Delta^{**} K} = 0\}\right) - I_\text{tot}^{K} - I_\text{tot}^{\Lambda}\,,\\ & \dots\,, \end{align} \end{split}\]

where the terms with a single chain index are the rate of the decay chain. The sum of all fractions should give the total rate:

\[ \begin{align} I_\text{tot}\left(\{\mathcal{H}\}\right) = \sum_{R} I_\text{tot}^{R} + \sum_{R < R'} I_\text{tot}^{R/R'} \end{align} \]

Code for computing decay rates

def integrate_intensity(
    intensity_func: ParametrizedFunction,
    phsp: DataSample,
    non_zero_couplings: list[str] | None = None,
) -> float:
    if non_zero_couplings is None:
        intensities = intensity_func(phsp)
    else:
        intensities = compute_sub_function(intensity_func, phsp, non_zero_couplings)
    return np.nansum(intensities) / len(intensities)


def compute_interference(
    intensity_func: ParametrizedFunction,
    phsp: DataSample,
    chain1: list[str],
    chain2: list[str],
) -> float:
    I_interference = integrate_intensity(intensity_func, phsp, chain1 + chain2)
    I_chain1 = integrate_intensity(intensity_func, phsp, chain1)
    I_chain2 = integrate_intensity(intensity_func, phsp, chain2)
    return I_interference - I_chain1 - I_chain2


I_tot = integrate_intensity(intensity_func, phsp)
np.testing.assert_allclose(
    I_tot,
    integrate_intensity(intensity_func, phsp, ["K", R"\Lambda", R"\Delta"]),
)
I_K = integrate_intensity(intensity_func, phsp, non_zero_couplings=["K"])
I_Λ = integrate_intensity(intensity_func, phsp, non_zero_couplings=["Lambda"])
I_Δ = integrate_intensity(intensity_func, phsp, non_zero_couplings=["Delta"])
I_ΛΔ = compute_interference(intensity_func, phsp, ["Lambda"], ["Delta"])
I_KΔ = compute_interference(intensity_func, phsp, ["K"], ["Delta"])
I_KΛ = compute_interference(intensity_func, phsp, ["K"], ["Lambda"])
np.testing.assert_allclose(I_tot, I_K + I_Λ + I_Δ + I_ΛΔ + I_KΔ + I_KΛ)

Hide code cell source

def render_resonance_row(chain_id):
    rows = [
        (
            Rf"\color{{gray}}{{{p.latex}}}",
            (
                Rf"\color{{gray}}{{{integrate_intensity(intensity_func, phsp, [p.name]) / I_tot:.3f}}}"
            ),
        )
        for p in resonance_particles[chain_id]
    ]
    if len(rows) > 1:
        return rows
    return []


rows = [
    R"\hline",
    ("K^{**}", f"{I_K / I_tot:.3f}"),
    *render_resonance_row(chain_id=1),
    (R"\Lambda^{**}", f"{I_Λ / I_tot:.3f}"),
    *render_resonance_row(chain_id=2),
    (R"\Delta^{**}", f"{I_Δ / I_tot:.3f}"),
    *render_resonance_row(chain_id=3),
    (R"\Delta/\Lambda", f"{I_ΛΔ / I_tot:.3f}"),
    (R"K/\Delta", f"{I_KΔ / I_tot:.3f}"),
    (R"K/\Lambda", f"{I_KΛ / I_tot:.3f}"),
    R"\hline",
    (
        R"\mathrm{total}",
        f"{(I_K + I_Λ + I_Δ + I_ΛΔ + I_KΔ + I_KΛ) / I_tot:.3f}",
    ),
]

latex = R"\begin{array}{crr}" + "\n"
latex += R"& I_\mathrm{sub}\,/\,I \\" + "\n"
for row in rows:
    if row == R"\hline":
        latex += R"\hline"
    else:
        latex += "  " + " & ".join(row) + R" \\" + "\n"
latex += R"\end{array}"
Math(latex)
\[\begin{split}\displaystyle \begin{array}{crr} & I_\mathrm{sub}\,/\,I \\ \hline K^{**} & 0.371 \\ \Lambda^{**} & 0.051 \\ \color{gray}{\Lambda(1520)} & \color{gray}{0.031} \\ \color{gray}{\Lambda(1670)} & \color{gray}{0.020} \\ \Delta^{**} & 0.582 \\ \Delta/\Lambda & 0.013 \\ K/\Delta & -0.018 \\ K/\Lambda & 0.001 \\ \hline \mathrm{total} & 1.000 \\ \end{array}\end{split}\]

Polarimetry distributions#

Hide code cell source

def render_mean(array, plus=True):
    array = array.real
    mean = f"{np.nanmean(array):.3f}"
    std = f"{np.nanstd(array):.3f}"
    if plus and float(mean) > 0:
        mean = f"+{mean}"
    return Rf"{mean} \pm {std}"


latex = R"\begin{array}{cccc}" + "\n"
latex += R"& \bar{|\alpha|} & \bar\alpha_x & \bar\alpha_y & \bar\alpha_z \\" + "\n"
for chain_id, chain_name in chain_ids.items():
    latex += f"  {chain_labels[chain_id]} & "
    x, y, z = tuple(
        compute_sub_function(func, phsp, non_zero_couplings=[chain_name])
        for func in polarimetry_funcs
    )
    latex += render_mean(np.sqrt(x**2 + y**2 + z**2), plus=False) + " & "
    latex += " & ".join(map(render_mean, [x, y, z]))
    latex += R" \\" + "\n"
latex += R"\end{array}"
Math(latex)
\[\begin{split}\displaystyle \begin{array}{cccc} & \bar{|\alpha|} & \bar\alpha_x & \bar\alpha_y & \bar\alpha_z \\ K^{**} & 0.873 \pm 0.040 & 0.000 \pm 0.554 & 0.000 \pm 0.396 & +0.100 \pm 0.538 \\ \Lambda^{**} & 0.906 \pm 0.101 & -0.529 \pm 0.214 & 0.000 \pm 0.190 & -0.338 \pm 0.596 \\ \Delta^{**} & 0.540 \pm 0.000 & +0.320 \pm 0.153 & -0.000 \pm 0.000 & -0.324 \pm 0.246 \\ \end{array}\end{split}\]

Hide code cell content

# Slider construction
sliders = {}
for symbol in free_parameters:
    if symbol.name.startswith(R"\mathcal{H}"):
        real_slider = create_slider(symbol)
        imag_slider = create_slider(symbol)
        sliders[f"{symbol.name}_real"] = real_slider
        sliders[f"{symbol.name}_imag"] = imag_slider
        real_slider.description = R"\(\mathrm{Re}\)"
        imag_slider.description = R"\(\mathrm{Im}\)"
    else:
        slider = create_slider(symbol)
        sliders[symbol.name] = slider

# Slider ranges
σ3_max = (m0_val - m3_val) ** 2
σ3_min = (m1_val + m2_val) ** 2

for name, slider in sliders.items():
    slider.continuous_update = True
    slider.step = 0.01
    if name.startswith("m_"):
        if "K" in name:
            slider.min = np.sqrt(σ1_min)
            slider.max = np.sqrt(σ1_max)
        elif R"\Lambda" in name:
            slider.min = np.sqrt(σ2_min)
            slider.max = np.sqrt(σ2_max)
        elif R"\Delta" in name:
            slider.min = np.sqrt(σ3_min)
            slider.max = np.sqrt(σ3_max)
    elif name.startswith(R"\Gamma_"):
        slider.min = 0
        slider.max = max(0.5, 2 * slider.value)
    elif name.startswith(R"\mathcal{H}"):
        slider.min = -15
        slider.max = +15


# Slider values
def reset_sliders(click_event):
    for symbol, value in free_parameters.items():
        if symbol.name.startswith(R"\mathcal{H}"):
            set_slider(sliders[symbol.name + "_real"], value)
            set_slider(sliders[symbol.name + "_imag"], value)
        else:
            set_slider(sliders[symbol.name], value)


def set_coupling_to_zero(filter_pattern):
    if isinstance(filter_pattern, Combobox):
        filter_pattern = filter_pattern.value
    for name, slider in sliders.items():
        if not name.startswith(R"\mathcal{H}"):
            continue
        if filter_pattern not in name:
            continue
        set_slider(slider, 0)


def set_slider(slider, value):
    if slider.description == R"\(\mathrm{Im}\)":
        value = complex(value).imag
    else:
        value = complex(value).real
    n_decimals = -round(np.log10(slider.step))
    if slider.value != round(value, n_decimals):  # widget performance
        slider.value = value


reset_sliders(click_event=None)
reset_button = Button(description="Reset slider values")
reset_button.on_click(reset_sliders)

all_resonances = [r.latex for r_list in resonance_particles.values() for r in r_list]
filter_button = Combobox(
    placeholder="Enter coupling filter pattern",
    options=all_resonances,
    description=R"$\mathcal{H}=0$",
)
filter_button.on_submit(set_coupling_to_zero)

# UI design
latex = {symbol.name: sp.latex(symbol) for symbol in free_parameters}
mass_sliders = [sliders[n] for n in sliders if n.startswith("m_")]
width_sliders = [sliders[n] for n in sliders if n.startswith(R"\Gamma_")]
coupling_sliders = {}
for res_list in resonance_particles.values():
    for res in res_list:
        coupling_sliders[res.name] = (
            [s for n, s in sliders.items() if n.endswith("_real") and res.latex in n],
            [s for n, s in sliders.items() if n.endswith("_imag") and res.latex in n],
            [
                HTMLMath(f"${latex[n[:-5]]}$")
                for n in sliders
                if n.endswith("_real") and res.latex in n
            ],
        )
slider_tabs = Tab(
    children=[
        Tab(
            children=[
                VBox([HBox(s) for s in zip(*pair, strict=True)])
                for pair in coupling_sliders.values()
            ],
            titles=tuple(coupling_sliders),
        ),
        VBox([HBox([r, i]) for r, i in zip(mass_sliders, width_sliders, strict=True)]),
    ],
    titles=("Couplings", "Masses and widths"),
)
ui = VBox([slider_tabs, HBox([reset_button, filter_button])])

Hide code cell source

fig, axes = plt.subplots(
    figsize=(12, 6.2),
    ncols=2,
    sharey=True,
)
ax1, ax2 = axes
ax1.set_title("Intensity distribution")
ax2.set_title("Polarimeter vector field")
ax1.set_xlabel(Rf"${s1_label[1:-1]}, \alpha_x$")
ax2.set_xlabel(Rf"${s1_label[1:-1]}, \alpha_x$")
ax1.set_ylabel(Rf"${s2_label[1:-1]}, \alpha_z$")
for ax in axes:
    ax.set_box_aspect(1)
fig.canvas.toolbar_visible = False
fig.canvas.header_visible = False
fig.canvas.footer_visible = False

mesh = None
quiver = None
, , phsp_α = generate_phsp_grid(resolution=35)
XI, YI, phsp_I = generate_phsp_grid(resolution=200)


def plot3(**kwargs):
    global quiver, mesh
    kwargs = to_complex_kwargs(**kwargs)
    for func in [*list(polarimetry_funcs), intensity_func]:
        func.update_parameters(kwargs)
    intensity = intensity_func(phsp_I)
    αx, αy, αz = tuple(func(phsp_α).real for func in polarimetry_funcs)
    abs_α = jnp.sqrt(αx**2 + αy**2 + αz**2)
    if mesh is None:
        mesh = ax1.pcolormesh(XI, YI, intensity, cmap=plt.cm.Reds, rasterized=True)
        c_bar = fig.colorbar(mesh, ax=ax1, pad=0.01, fraction=0.0473)
        c_bar.ax.set_yticks([])
    else:
        mesh.set_array(intensity)
    if quiver is None:
        quiver = ax2.quiver(, , αx, αz, abs_α, cmap=plt.cm.viridis_r, clim=(0, 1))
        c_bar = fig.colorbar(quiver, ax=ax2, pad=0.01, fraction=0.0473)
        c_bar.ax.set_ylabel(R"$\left|\vec\alpha\right|$")
    else:
        quiver.set_UVC(αx, αz, abs_α)
    fig.canvas.draw_idle()


def to_complex_kwargs(**kwargs):
    complex_valued_kwargs = {}
    for key, value in dict(kwargs).items():
        if key.endswith("real"):
            symbol_name = key[:-5]
            imag = kwargs[f"{symbol_name}_imag"]
            complex_valued_kwargs[symbol_name] = complex(value, imag)
        elif key.endswith("imag"):
            continue
        else:
            complex_valued_kwargs[key] = value
    return complex_valued_kwargs


output = interactive_output(plot3, controls=sliders)
fig.tight_layout()
display(ui, output)